Zurück zu Skills
gamedev-skills/awesome-gamedev-agent-skillsPrüfung bestanden

SKILL DETAIL

defold-core

gamedev-skills/awesome-gamedev-agent-skills/defold-core

Build and debug the core of a Defold game in Lua: game.project settings, collections, game objects and components, script lifecycle and self state, go.property, input focus and bindings, URLs and message passing, factories, collection factories and collection proxies, plus building and smoke-running with Bob. Use when a project has game.project or .collection, .go, .script, .gui_script or .input_binding files, or the request mentions Defold, msg.post, on_message, on_input, acquire_input_focus, factory.create, collection proxy, bob.jar, or a Defold build error.

Installationen · 234Quelle ansehen

Installation

npx skills add https://github.com/gamedev-skills/awesome-gamedev-agent-skills --skill defold-core

Skill-Dateien

SKILL.md

Zuletzt synchronisiert · 25.10.2026

agents/openai.yaml›
interface:
  display_name: "Defold Core"
  short_description: "Build and debug Defold games in Lua with Bob checks"
  default_prompt: "Use $defold-core to add or fix this feature in my Defold game and verify it builds and runs."
references/build-and-run.md›
# Build and run with Bob (Defold 1.14.0)

Bob (`bob.jar`) is Defold's command-line builder. Everything here was run with Bob 1.14.0
(sha1 `d295ed4edff2e49072719835713e625f409849f1`) on Linux x86_64; commands, flags and the
error messages quoted are from those runs or from the official
[Bob manual](https://defold.com/manuals/bob/) and
[automated testing manual](https://defold.com/manuals/automated-testing/).

## Get the tools

- **Java:** Bob 1.14.0 needs OpenJDK 25. On Java 21 it fails at once with
  `UnsupportedClassVersionError … class file version 69.0`.
- **bob.jar:** use the release the project is built with. Current stable release info is at
  [d.defold.com/stable/info.json](https://d.defold.com/stable/info.json) (version and sha1).
  Download by version from the GitHub release assets
  (`https://github.com/defold/defold/releases/download/1.14.0/bob.jar`) or by sha1
  (`https://d.defold.com/archive/stable/<sha1>/bob/bob.jar`).
- **Check it:** `java -jar bob.jar --version` prints the version, sha1 and channel.

## Commands

Bob runs the listed commands in order: `resolve` (fetch library dependencies into
`.internal/lib/`), `clean`, `distclean`, `build`, `bundle`.

```sh
# Build only: compiles everything reachable from game.project into build/default
java -jar bob.jar --root . --variant headless --archive resolve build

# Desktop bundle (release)
java -jar bob.jar --root . --archive --platform x86_64-linux --variant release \
  --bundle-output ../dist resolve distclean build bundle

# Machine-readable report of what was built
java -jar bob.jar --root . --archive --build-report-json ../report.json resolve build
```

- Platforms: `x86_64-linux`, `arm64-linux`, `x86_64-win32`, `x86_64-macos`, `arm64-macos`,
  `arm64-ios`, `arm64_sim-ios`, `armv7-android`, `wasm-web`.
- Variants: `debug` (default), `release`, `headless` (null graphics device).
- `bundle` needs `--archive` and `--platform`. `--bundle-output` must be **outside** the
  project's `build/` folder, or Bob stops with `Folder '…/build' in the project folder can't
  be used for bundling as this folder is reserved for Defold build system`.
- `--settings file.settings` overlays `game.project` keys (several files apply left to
  right). Use it for test bootstraps instead of editing `game.project`.
- The bundle lands in a folder named after `project.title`, with the executable named after
  the title without spaces (`Defold Core Example/DefoldCoreExample.x86_64`).
- Bob exits 1 on a build error. The useful line starts with `ERROR`; Java stack traces follow
  as `SEVERE` lines. See `file-formats.md` for tracing errors that name a generated file.

## Smoke test a running game

A build proves the resources compile, not that scripts run. Give the project a test bootstrap
that loads the real main collection through a proxy, runs it for a while, prints a marker
and exits. These three files and the commands below were run as shown.

`test/smoke.settings`:

```ini
[bootstrap]
main_collection = /test/smoke.collectionc
```

`test/smoke.collection`:

```text
name: "smoke"
embedded_instances {
  id: "smoke"
  data: "components {\n"
  "  id: \"smoke\"\n"
  "  component: \"/test/smoke.script\"\n"
  "}\n"
  "embedded_components {\n"
  "  id: \"game\"\n"
  "  type: \"collectionproxy\"\n"
  "  data: \"collection: \\\"/main/main.collection\\\"\\n\"\n"
  "  \"\"\n"
  "}\n"
  ""
}
scale_along_z: 0
```

`test/smoke.script`:

```lua
-- Smoke-test bootstrap: loads the real main collection, lets it run, then exits.
go.property("seconds", 2)

local PROXY_LOADED = hash("proxy_loaded")

function init(self)
	msg.post(".", "acquire_input_focus")
	msg.post("#game", "load")
	self.time = 0
end

function update(self, dt)
	self.time = self.time + dt
	if self.time >= self.seconds then
		print("SMOKE done")
		sys.exit(0)
	end
end

function on_message(self, message_id, message, sender)
	if message_id == PROXY_LOADED then
		msg.post(sender, "init")
		msg.post(sender, "enable")
	end
end
```

The main collection's `name` must differ from `smoke`, since both worlds are loaded at once.

```sh
java -jar bob.jar --root . --settings test/smoke.settings --platform x86_64-linux \
  --variant headless --archive --bundle-output ../smoke-bundle resolve build bundle
timeout 30 "../smoke-bundle/Defold Core Example/DefoldCoreExample.x86_64" > run.log 2>&1
grep -q "SMOKE done" run.log && ! grep -q "^ERROR:" run.log && echo PASS
```

Judge the run by the log, not only the exit status:

- A Lua error in a callback prints `ERROR:SCRIPT: main/x.script:8: attempt to index …` with a
  traceback and the engine **keeps running**: without a timeout the test hangs, and with
  one it ends with the timeout's status (124 for GNU `timeout`).
- A main collection that cannot be created (for example a `require` that fails at load)
  exits 1 after `ERROR:GAMEOBJECT: Could not instantiate game object from prototype …`.
- Bad message targets and proxy clashes log `ERROR:GAMEOBJECT:` lines but do not stop the
  game, so check for them explicitly.
- For many assertions, print one structured line per result (the automated testing manual
  suggests a prefix such as `TEST {json}`) and fail the run on a missing final line.

Without a bundle, the headless engine binary can run a plain build from the project root
(it loads `build/default/game.dmanifest`). The official manuals do not document this binary;
it is published next to `bob.jar` as
`https://d.defold.com/archive/stable/<sha1>/engine/x86_64-linux/dmengine_headless`. Prefer
the headless bundle above when you need a documented path.

## Ports and parallel runs

A debug or headless engine starts an engine service on port 8001, a log server on a random
port and the Remotery profiler (`ws://127.0.0.1:17815`). A second engine started at the same
time logs `ERROR:PROFILER: Failed to initialize Remotery`. Run one engine per test job, always
under a timeout, and make sure it has exited before the next run.

## Beyond the core

The headless engine has no input devices and draws nothing, so check input, rendering and
layout in the editor (Project ▸ Build) or a desktop bundle. For the next subsystems, read the
official manuals:

- GUI scenes and `.gui_script`: [GUI](https://defold.com/manuals/gui/) — a GUI script uses
  `gui.*` functions instead of `go.*` and acquires input focus the same way.
- Sprites and flipbooks: [Sprite](https://defold.com/manuals/sprite/) and
  [Atlas](https://defold.com/manuals/atlas/); play an animation with
  `sprite.play_flipbook("#sprite", "run")`.
- Tilemaps: [Tilemap](https://defold.com/manuals/tilemap/) and
  [Tile source](https://defold.com/manuals/tilesource/).
- Collision objects and physics messages: [Physics](https://defold.com/manuals/physics/);
  with a fixed timestep, act on physics in `fixed_update`.
- Engine-agnostic design for these topics: the related skills listed in `SKILL.md`.
references/file-formats.md›
# Defold file formats (1.14.0)

Every file below comes from one small project that Bob 1.14.0 built (`--variant headless`) and
the 1.14.0 headless engine ran: a player that moves and fires bullets from a factory, a loader
that spawns a wave with a collection factory and loads a level through a collection proxy.
Copy these shapes when you must write editor files by hand; when the editor is available,
let it write them.

The editor files are protobuf text format. Fields not shown keep their defaults, `#` starts a
comment, and the field order the editor writes does not matter to Bob. The schemas are the
`*_ddf.proto` files in the Defold engine source.

```text
game.project
input/game.input_binding
main/main.collection      main/player.go   main/player.script
main/main.atlas           main/bullet.go   main/bullet.script
main/loader.go            main/loader.script        main/check.script
main/wave.collection      main/enemy.go
levels/level1.collection  levels/level.script
```

## game.project

INI file, required, in the project root, named exactly `game.project`. Paths name the compiled
file (trailing `c`). Without the `[bootstrap]` key Defold looks for `/logic/main.collectionc`;
without `[input]` it uses `/input/game.input_bindingc`.

```ini
[project]
title = Defold Core Example

[bootstrap]
main_collection = /main/main.collectionc

[input]
game_binding = /input/game.input_bindingc

[physics]
use_fixed_timestep = 1

[engine]
fixed_update_frequency = 60
```

Library dependencies are listed as `dependencies#0 = https://…/archive.zip`, `dependencies#1 =
…`; Bob's `resolve` command fetches them into `.internal/lib/`. Read any setting at run time
with `sys.get_config_string("project.title")`.

## .input_binding

```text
key_trigger {
  input: KEY_LEFT
  action: "left"
}
key_trigger {
  input: KEY_RIGHT
  action: "right"
}
key_trigger {
  input: KEY_SPACE
  action: "fire"
}
mouse_trigger {
  input: MOUSE_BUTTON_LEFT
  action: "touch"
}
```

`/builtins/input/all.input_binding` exists for quick tests; a game should own its bindings.

## .collection

Instances of game object files, an embedded instance, a nested collection and a per-instance
script property override. `name` becomes the socket when the collection is loaded through a
proxy, so every proxied collection needs a unique name.

```text
name: "main"
instances {
  id: "player"
  prototype: "/main/player.go"
  position {
    x: 480.0
    y: 120.0
  }
  component_properties {
    id: "player"                 # id of the script component inside player.go
    properties { id: "speed" value: "250.0" type: PROPERTY_TYPE_NUMBER }
  }
}
instances {
  id: "loader"
  prototype: "/main/loader.go"
}
embedded_instances {
  id: "check"                    # a game object defined inline; here it holds one script
  data: "components {\n  id: \"check\"\n  component: \"/main/check.script\"\n}\n"
}
collection_instances {
  id: "decor"
  collection: "/main/wave.collection"
  position {
    x: 100.0
    y: 400.0
  }
}
scale_along_z: 0
```

Ids inside a nested collection are prefixed with its id: the `leader` object in `decor` is
`/decor/leader`. A child is attached with `children: "child_id"` on the parent instance:

```text
name: "wave"
instances {
  id: "leader"
  prototype: "/main/enemy.go"
  children: "wingman"
}
instances {
  id: "wingman"
  prototype: "/main/enemy.go"
  position {
    x: 64.0
  }
}
scale_along_z: 0
```

A collection that only holds embedded instances, as loaded by a proxy:

```text
name: "level1"
embedded_instances {
  id: "level"
  data: "components {\n"
  "  id: \"level\"\n"
  "  component: \"/levels/level.script\"\n"
  "}\n"
  ""
}
scale_along_z: 0
```

## .go

`components` reference component files (a script here); `embedded_components` carry the
component's own file as an escaped string, split across adjacent quoted lines.

```text
components {
  id: "player"
  component: "/main/player.script"
}
embedded_components {
  id: "sprite"
  type: "sprite"
  data: "default_animation: \"blob\"\n"
  "material: \"/builtins/materials/sprite.material\"\n"
  "textures {\n"
  "  sampler: \"texture_sampler\"\n"
  "  texture: \"/main/main.atlas\"\n"
  "}\n"
  ""
}
embedded_components {
  id: "bulletfactory"
  type: "factory"
  data: "prototype: \"/main/bullet.go\"\n"
  ""
}
```

The loader object holds the other two spawning components:

```text
components {
  id: "loader"
  component: "/main/loader.script"
}
embedded_components {
  id: "levelproxy"
  type: "collectionproxy"
  data: "collection: \"/levels/level1.collection\"\n"
  ""
}
embedded_components {
  id: "wavefactory"
  type: "collectionfactory"
  data: "prototype: \"/main/wave.collection\"\n"
  ""
}
```

Older sprite data uses `tile_set: "/main/main.atlas"` instead of the `textures` block; keep
whichever form the project already uses.

## .atlas

```text
images {
  image: "/builtins/graphics/particle_blob.png"
}
animations {
  id: "blob"
  images {
    image: "/builtins/graphics/particle_blob.png"
  }
  playback: PLAYBACK_LOOP_FORWARD
  fps: 8
}
```

A sprite's `default_animation` names an image (file name without extension) or an animation
id in its atlas.

## Scripts

`bullet.script`, spawned by the player's factory with a `velocity` override:

```lua
go.property("velocity", vmath.vector3(0, 400, 0))
go.property("lifetime", 1.5)

function update(self, dt)
	go.set_position(go.get_position() + self.velocity * dt)
	self.lifetime = self.lifetime - dt
	if self.lifetime <= 0 then
		go.delete()                         -- deletes this game object at the end of the frame
	end
end
```

`level.script`, inside the proxy-loaded world, reporting back to the main world:

```lua
local LEVEL_DONE = hash("level_done")

function init(self)
	self.time = 0
	print("level init", msg.url())          -- url: [level1:/level#level]
end

function update(self, dt)
	self.time = self.time + dt
	if self.time >= 0.5 and not self.reported then
		self.reported = true
		msg.post("main:/loader#loader", LEVEL_DONE, { seconds = self.time })
	end
end
```

Shared code goes in `.lua` modules, loaded with a dotted path from the project root:
`local util = require "modules.util"` loads `/modules/util.lua`.

The run printed, in order: `wave leader hash: [/collection0/leader]`,
`level init url: [level1:/level#level]`, `level done after 0.5…`, `level unloaded`.

## Where build errors point

Bob names the source file and line 0 for most reference errors:

```text
ERROR: game.project:0: '…the bootstrap.main_collection '/main/main.collectio' can't be found'
ERROR: main/probe.script:0: '…the Lua module '/modules/nothere.lua' can't be found'
ERROR main/main.atlas:0 Resource 'main/images/missing.png' is not created
```

For an embedded component, Bob reports a generated file instead of the `.go` that holds it:

```text
ERROR: build/default/_generated_c1d664c47a440ab4.factory:0: '…the prototype '/main/missing.go' can't be found'
```

The generated name does not exist in the sources. Search them for the quoted path in the
message (`"/main/missing.go"`, written as `\"/main/missing.go\"` inside embedded data) or run
`scripts/check_defold_project.py`, which reports the `.go` file and line.

A missing resource property (`go.property("art", resource.atlas("/main/nope.atlas"))`) appears
only in the `SEVERE` stack trace lines: `Resource '/main/nope.atlas' referenced from script
resource property 'art' does not exist`.
scripts/check_defold_project.py›
#!/usr/bin/env python3
"""Check a Defold project for broken resource references. Read-only.

Run it after creating, moving or renaming collections, game objects, scripts,
atlases or other resources. It reads the project's text files and reports
references that Bob (the Defold build tool) will fail to resolve. It never
edits anything and needs no Defold editor, Java or bob.jar.

What it checks:

* ``game.project``: ``bootstrap.main_collection`` and ``input.game_binding``
  must name an existing file and carry the compiled suffix (``.collectionc``,
  ``.input_bindingc``). When a key is absent, Defold's default
  (``/logic/main.collectionc``, ``/input/game.input_bindingc``) is checked
  instead. Other settings whose value is a root-absolute path, and the
  ``custom_resources``, ``bundle_resources`` and ``include_dirs`` lists, are
  checked as warnings.
* Every quoted root-absolute resource path (``"/main/player.go"``, also inside
  the escaped ``data:`` strings of embedded components) in collections, game
  objects, atlases, GUI scenes, tile sources and the other Defold text resource
  files. A string is treated as a path only when its last part has a file
  extension and it contains no ``#`` or ``:``, so game object ids and URLs
  are left alone.
* In ``.script``, ``.gui_script``, ``.render_script`` and ``.lua`` files:
  literal ``require "a.b"`` modules (``/a/b.lua``) and literal
  ``resource.*("/path")`` properties; ``sys.load_resource("/path")`` is a
  warning because it reads custom resources at run time. Comments are ignored.
* Paths whose letter case differs from the file on disk. This can work on
  Windows and macOS and break on Linux and in CI.

Bob only builds files reachable from ``game.project``, so a broken reference
in a file nothing uses is reported as a warning, not an error.

Paths under ``/builtins/`` are skipped. Paths provided by library
dependencies are checked against the archives Bob fetched into
``.internal/lib/``; when a dependency has not been fetched, an unresolved path
is only a warning. Skipped while scanning: the ``build/`` folder, hidden files
and folders (``.internal/``, ``.git/``, ``.editor_settings``) and anything
listed in ``.defignore``.

Exit codes: 0 no errors, 1 errors (or warnings with --strict),
2 usage problem or no game.project found.
"""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import re
import sys
import zipfile
from dataclasses import dataclass
from pathlib import Path

TOOL = "check_defold_project"
SCHEMA = 1

# Defold resource files stored as protobuf text; any quoted "/path.ext" in them is a resource.
RESOURCE_EXTS = {
    ".animationset", ".atlas", ".buffer", ".camera", ".collection", ".collectionfactory",
    ".collectionproxy", ".collisionobject", ".compute", ".convexshape", ".display_profiles",
    ".factory", ".font", ".gamepads", ".go", ".gui", ".input_binding", ".label", ".material",
    ".mesh", ".model", ".particlefx", ".render", ".render_target", ".sound", ".sprite",
    ".texture_profiles", ".tilemap", ".tilesource",
}
SCRIPT_EXTS = {".script", ".gui_script", ".render_script", ".lua"}

# Names Bob accepts in require() without a project file (engine-provided modules).
ENGINE_MODULES = {"socket", "string", "table", "math", "debug", "os", "io", "coroutine", "bit", "package"}

# Defaults from the Defold project settings manual and Bob's behaviour when a key is absent.
DEFAULT_MAIN_COLLECTION = "/logic/main.collectionc"
DEFAULT_GAME_BINDING = "/input/game.input_bindingc"

QUOTED_PATH_RE = re.compile(r'\\*"(/[^"\\\n]+)\\*"')
# Matched against masked code (string contents blanked); each ends at a string's opening quote.
REQUIRE_RE = re.compile(r"""(?<![\w.:])require\s*\(?\s*["']""")
RESOURCE_CALL_RE = re.compile(r"""\bresource\.[a-z_]+\s*\(\s*["']""")
LOAD_RESOURCE_RE = re.compile(r"""\bsys\.load_resource\s*\(\s*["']""")
EXT_RE = re.compile(r"\.[A-Za-z0-9_]+$")


@dataclass
class Issue:
    severity: str  # "error" or "warning"
    code: str
    file: str
    line: int | None
    message: str
    fix: str

    def as_dict(self) -> dict:
        return {"code": self.code, "file": self.file, "line": self.line, "message": self.message, "fix": self.fix}


@dataclass
class Ref:
    file: str  # project-relative file that holds the reference
    line: int
    path: str  # root-absolute path as written, e.g. "/main/player.go"
    kind: str  # resource, require, resource-property, load-resource, setting
    severity: str  # severity when the target is missing and the file is reachable


# ---------------------------------------------------------------- text helpers


def read_text(path: Path) -> str:
    """Read a text file, tolerating a UTF-8 BOM, CRLF line endings and bad bytes."""
    text = path.read_bytes().decode("utf-8", errors="replace")
    if text.startswith("\ufeff"):
        text = text[1:]
    return text.replace("\r\n", "\n").replace("\r", "\n")


def line_of(text: str, offset: int) -> int:
    return text.count("\n", 0, offset) + 1


def parse_ini(text: str) -> dict[str, tuple[str, int]]:
    """Parse game.project into ``{"section.key": (value, line)}``."""
    values: dict[str, tuple[str, int]] = {}
    section = ""
    for lineno, raw in enumerate(text.split("\n"), 1):
        line = raw.strip()
        if not line or line[0] in "#;":
            continue
        if line.startswith("[") and line.endswith("]"):
            section = line[1:-1].strip()
            continue
        if "=" in line:
            key, value = line.split("=", 1)
            values[f"{section}.{key.strip()}"] = (value.strip(), lineno)
    return values


def mask_lua(code: str) -> tuple[str, dict[int, str]]:
    """Blank out Lua comments and the contents of string literals.

    Returns the masked code (same length and line breaks as the input) and a map
    from the offset of each short string's opening quote to its contents, so a
    call such as ``require "a.b"`` is found in code but never inside a string.
    """
    out: list[str] = []
    strings: dict[int, str] = {}
    i, n = 0, len(code)
    long_open = re.compile(r"\[(=*)\[")

    def blank(text: str) -> str:
        return re.sub(r"[^\n]", " ", text)

    while i < n:
        ch = code[i]
        if code.startswith("--", i):
            m = long_open.match(code, i + 2)
            if m:
                close = "]" + m.group(1) + "]"
                end = code.find(close, m.end())
                end = n if end < 0 else end + len(close)
            else:
                end = code.find("\n", i)
                end = n if end < 0 else end
            out.append(blank(code[i:end]))
            i = end
        elif ch in "\"'":
            j = i + 1
            while j < n and code[j] != ch and code[j] != "\n":
                j += 2 if code[j] == "\\" else 1
            strings[i] = code[i + 1 : j]
            out.append(ch + blank(code[i + 1 : j]) + (ch if j < n and code[j] == ch else ""))
            i = j + 1
        elif ch == "[" and long_open.match(code, i):
            m = long_open.match(code, i)
            close = "]" + m.group(1) + "]"
            end = code.find(close, m.end())
            end = n if end < 0 else end + len(close)
            out.append(blank(code[i:end]))
            i = end
        else:
            out.append(ch)
            i += 1
    return "".join(out), strings


def defignore_regex(pattern: str) -> re.Pattern[str] | None:
    """Translate one .defignore line into a regex over project paths ("/a/b")."""
    pattern = pattern.strip()
    if not pattern.startswith("/"):
        return None
    parts = []
    i = 0
    while i < len(pattern):
        if pattern.startswith("/**/", i):
            parts.append("/(?:.*/)?")
            i += 4
        elif pattern.startswith("/**", i) and i + 3 == len(pattern):
            parts.append("(?:/.*)?")
            i += 3
        elif pattern[i] == "*":
            parts.append("[^/]*")
            i += 1
        elif pattern[i] == "?":
            parts.append("[^/]")
            i += 1
        else:
            parts.append(re.escape(pattern[i]))
            i += 1
    # A pattern matches the path itself or any of its parent folders.
    return re.compile("".join(parts) + "(?:/.*)?$")


# ------------------------------------------------------------------- project


class Project:
    def __init__(self, root: Path) -> None:
        self.root = root
        self.files: list[str] = []  # project-relative posix paths ("main/player.go")
        self.file_set: set[str] = set()
        self.dir_set: set[str] = set()
        self.by_casefold: dict[str, str] = {}
        self.ignore: list[re.Pattern[str]] = []
        self.ignored_files: set[str] = set()
        self.lib_files: set[str] = set()  # files provided by fetched library archives
        self.lib_dirs: set[str] = set()  # top folders those archives provide
        self.unfetched: list[str] = []  # dependency URLs with no fetched archive
        self.settings: dict[str, tuple[str, int]] = {}
        self.refs: list[Ref] = []
        self.issues: list[Issue] = []
        self.files_checked = 0

    def add(self, severity: str, code: str, file: str, line: int | None, message: str, fix: str) -> None:
        self.issues.append(Issue(severity, code, file, line, message, fix))

    # -- scanning

    def ignored(self, rel: str) -> bool:
        return any(p.match("/" + rel) for p in self.ignore)

    def read_defignore(self) -> None:
        path = self.root / ".defignore"
        if path.is_file():
            for line in read_text(path).split("\n"):
                regex = defignore_regex(line)
                if regex:
                    self.ignore.append(regex)

    def scan(self) -> None:
        stack = [""]
        seen: set[tuple[int, int]] = set()
        while stack:
            rel_dir = stack.pop()
            full = self.root / rel_dir if rel_dir else self.root
            try:
                info = os.stat(full)
                if info.st_ino:
                    key = (info.st_dev, info.st_ino)
                    if key in seen:
                        continue  # symlink loop
                    seen.add(key)
                entries = sorted(os.scandir(full), key=lambda e: e.name)
            except OSError:
                self.add("warning", "dir-unreadable", rel_dir or ".", None,
                         "Folder could not be read, so files in it were not checked.",
                         "Check the folder's permissions.")
                continue
            for entry in entries:
                rel = f"{rel_dir}/{entry.name}" if rel_dir else entry.name
                if entry.name.startswith(".") or (not rel_dir and entry.name == "build"):
                    continue
                try:
                    is_dir = entry.is_dir()
                    is_file = entry.is_file()
                except OSError:
                    continue
                if is_dir:
                    if self.ignored(rel):
                        continue
                    self.dir_set.add(rel)
                    stack.append(rel)
                elif is_file:
                    if self.ignored(rel):
                        self.ignored_files.add(rel)
                        continue
                    self.files.append(rel)
        self.files.sort()
        self.file_set = set(self.files)
        for rel in self.files:
            self.by_casefold.setdefault(rel.casefold(), rel)

    # -- library dependencies

    def dependency_urls(self) -> list[tuple[str, int]]:
        urls: list[tuple[str, int]] = []
        for key, (value, line) in self.settings.items():
            if key == "project.dependencies" or key.startswith("project.dependencies#"):
                urls.extend((u.strip(), line) for u in value.split(",") if u.strip())
        return urls

    def load_libraries(self) -> None:
        lib_dir = self.root / ".internal" / "lib"
        try:
            archives = sorted(os.listdir(lib_dir))
        except OSError:
            archives = []
        for url, _line in self.dependency_urls():
            # Bob stores each fetched library as .internal/lib/<sha1 of the URL>-<etag>.zip.
            prefix = hashlib.sha1(url.encode("utf-8")).hexdigest() + "-"
            match = next((a for a in archives if a.startswith(prefix) and a.endswith(".zip")), None)
            if not match or not self.read_library(lib_dir / match):
                self.unfetched.append(url)

    def read_library(self, archive: Path) -> bool:
        try:
            with zipfile.ZipFile(archive) as zf:
                names = zf.namelist()
                project = next((n for n in names if n.count("/") <= 1 and n.endswith("game.project")), None)
                if project is None:
                    return False
                base = project[: -len("game.project")]
                text = zf.read(project).decode("utf-8", errors="replace")
        except (OSError, zipfile.BadZipFile, KeyError):
            return False
        include = parse_ini(text.replace("\r\n", "\n")).get("library.include_dirs", ("", 0))[0]
        dirs = [d.strip("/") for d in include.split() if d.strip("/")]
        for name in names:
            if not name.startswith(base) or name.endswith("/"):
                continue
            rel = name[len(base):]
            if rel.split("/", 1)[0] in dirs:
                self.lib_files.add(rel)
        self.lib_dirs.update(dirs)
        return True

    # -- path lookup

    def lookup(self, path: str) -> tuple[str, str | None]:
        """Return (status, actual) for a root-absolute path.

        status is ok, builtin, case, ignored, maybe-library or missing.
        """
        rel = path.lstrip("/")
        if rel.startswith("builtins/"):
            return "builtin", None
        if rel in self.file_set or rel in self.lib_files:
            return "ok", rel
        if rel in self.ignored_files or self.ignored(rel):
            return "ignored", None
        actual = self.by_casefold.get(rel.casefold())
        if actual:
            return "case", actual
        if self.unfetched and rel.split("/", 1)[0] not in self.dir_set:
            return "maybe-library", None
        return "missing", None

    def similar(self, path: str) -> list[str]:
        name = path.rsplit("/", 1)[-1].casefold()
        return ["/" + f for f in self.files if f.rsplit("/", 1)[-1].casefold() == name][:3]

    # -- reference collection

    def collect_resource_refs(self, rel: str, text: str) -> None:
        for m in QUOTED_PATH_RE.finditer(text):
            path = m.group(1)
            last = path.rsplit("/", 1)[-1]
            if "#" in path or ":" in path or not EXT_RE.search(last):
                continue
            self.refs.append(Ref(rel, line_of(text, m.start()), path, "resource", "error"))

    def collect_script_refs(self, rel: str, text: str) -> None:
        code, strings = mask_lua(text)

        def literal_args(regex: re.Pattern[str]):
            for m in regex.finditer(code):
                value = strings.get(m.end() - 1)
                if value is not None:
                    yield m, value

        for m, module in literal_args(REQUIRE_RE):
            module = module.strip()
            if module.startswith("builtins."):
                continue
            if "." not in module and "/" not in module:
                if module in ENGINE_MODULES:
                    continue
                severity = "warning"  # a single name might be provided by the engine or an extension
            else:
                severity = "error"
            path = "/" + module.replace(".", "/") + ".lua"
            self.refs.append(Ref(rel, line_of(code, m.start()), path, "require", severity))
        for m, path in literal_args(RESOURCE_CALL_RE):
            if path.startswith("/"):
                self.refs.append(Ref(rel, line_of(code, m.start()), path, "resource-property", "error"))
        for m, path in literal_args(LOAD_RESOURCE_RE):
            if path.startswith("/"):
                self.refs.append(Ref(rel, line_of(code, m.start()), path, "load-resource", "warning"))

    # -- game.project

    def check_settings(self) -> list[str]:
        """Check game.project and return the root resources Bob builds from."""
        roots: list[str] = []
        for key, default, suffix, what in (
            ("bootstrap.main_collection", DEFAULT_MAIN_COLLECTION, ".collectionc", "main collection"),
            ("input.game_binding", DEFAULT_GAME_BINDING, ".input_bindingc", "input binding"),
        ):
            value, line = self.settings.get(key, ("", 0))
            written = bool(value)
            value = value or default
            note = "" if written else f" ({key} is not set, so Defold uses {default})"
            if not value.endswith(suffix):
                if value.endswith(suffix[:-1]):
                    fix = f"Write {value}c: game.project names the compiled file."
                else:
                    fix = f"Point {key} at a {suffix[1:-1]} file, written with a trailing 'c' ({suffix})."
                self.add("error", "setting-suffix", "game.project", line or None,
                         f"{key} = {value} must use the compiled suffix '{suffix}'.", fix)
                source = value
            else:
                source = value[:-1]
            status, actual = self.lookup(source)
            if status in ("ok", "builtin"):
                if actual:
                    roots.append(actual)
            elif status == "case":
                self.add("error", "path-case", "game.project", line or None,
                         f"{key} points to {source} but the file on disk is /{actual}{note}.",
                         "Match the letter case exactly; Linux builds are case-sensitive.")
            elif status == "maybe-library":
                self.add("warning", "path-unverified", "game.project", line or None,
                         f"{key} points to {source}, which is not in the project and may come "
                         f"from a library dependency that is not fetched{note}.",
                         "Fetch libraries (Project > Fetch Libraries, or bob 'resolve') and re-run.")
            else:
                self.add("error", "setting-missing", "game.project", line or None,
                         f"The {what} {source} does not exist{note}.",
                         self.missing_fix(source, f"Point {key} at an existing file"))

        for key, (value, line) in sorted(self.settings.items()):
            if key in ("bootstrap.main_collection", "input.game_binding"):
                continue
            if key in ("project.custom_resources", "project.bundle_resources"):
                for item in (v.strip() for v in value.split(",")):
                    rel = item.strip("/")
                    if rel and rel not in self.file_set and rel not in self.dir_set:
                        self.add("warning", "setting-missing", "game.project", line,
                                 f"{key} lists '{item}', which is not a file or folder in the project.",
                                 "Fix the path (relative to the project root) or remove it from the list.")
                continue
            if key == "library.include_dirs":
                for item in value.split():
                    if item.strip("/") not in self.dir_set:
                        self.add("warning", "setting-missing", "game.project", line,
                                 f"library.include_dirs lists '{item}', which is not a folder in the project.",
                                 "Name a top-level folder that this library shares, or remove it.")
                continue
            if not value.startswith("/") or "," in value or " " in value or not EXT_RE.search(value):
                continue
            candidates = [value[:-1], value] if value.endswith("c") else [value]
            found = None
            for candidate in candidates:
                status, actual = self.lookup(candidate)
                if status in ("ok", "builtin"):
                    found = actual or ""
                    break
            if found is None:
                status, actual = self.lookup(candidates[0])
                if status == "case":
                    message = f"{key} = {value} differs in letter case from /{actual}."
                elif status == "maybe-library":
                    continue
                else:
                    message = f"{key} = {value} points to a file that does not exist."
                self.add("warning", "setting-missing", "game.project", line, message,
                         self.missing_fix(candidates[0], f"Point {key} at an existing file"))
            elif found:
                roots.append(found)
        return roots

    def missing_fix(self, path: str, action: str) -> str:
        similar = [s for s in self.similar(path) if s != path]
        if similar:
            return f"{action}; a file with that name exists at {', '.join(similar)}."
        return f"{action}, or restore the file."

    # -- run

    def run(self) -> None:
        self.settings = parse_ini(read_text(self.root / "game.project"))
        self.read_defignore()
        self.scan()
        self.load_libraries()
        self.files_checked = 1  # game.project
        for rel in self.files:
            suffix = Path(rel).suffix.lower()
            if suffix not in RESOURCE_EXTS and suffix not in SCRIPT_EXTS:
                continue
            try:
                text = read_text(self.root / rel)
            except OSError:
                self.add("warning", "file-unreadable", rel, None, "File could not be read, so it was not checked.",
                         "Check the file's permissions.")
                continue
            self.files_checked += 1
            if suffix in RESOURCE_EXTS:
                self.collect_resource_refs(rel, text)
            else:
                self.collect_script_refs(rel, text)

        roots = self.check_settings()
        reachable = self.reachable(roots)
        for ref in self.refs:
            self.report_ref(ref, ref.file in reachable)

        if self.unfetched:
            line = self.dependency_urls()[0][1]
            self.add("warning", "dependencies-unfetched", "game.project", line,
                     f"{len(self.unfetched)} library dependenc{'y is' if len(self.unfetched) == 1 else 'ies are'} "
                     "not fetched, so paths they provide could not be checked.",
                     "Fetch libraries (Project > Fetch Libraries in the editor, or "
                     "'java -jar bob.jar resolve') and re-run this check.")

    def reachable(self, roots: list[str]) -> set[str]:
        edges: dict[str, list[str]] = {}
        for ref in self.refs:
            status, actual = self.lookup(ref.path)
            if status == "ok" and actual in self.file_set:
                edges.setdefault(ref.file, []).append(actual)
        seen: set[str] = set()
        stack = [r for r in roots if r in self.file_set]
        while stack:
            rel = stack.pop()
            if rel in seen:
                continue
            seen.add(rel)
            stack.extend(edges.get(rel, []))
        return seen

    def report_ref(self, ref: Ref, reachable: bool) -> None:
        status, actual = self.lookup(ref.path)
        if status in ("ok", "builtin"):
            return
        what = {
            "resource": "Resource",
            "require": "Lua module file",
            "resource-property": "Resource property",
            "load-resource": "sys.load_resource() file",
        }[ref.kind]
        severity = ref.severity if reachable else "warning"
        unused = "" if reachable else " This file is not used by the build yet, so Bob does not report it."
        if status == "case":
            self.add(severity, "path-case", ref.file, ref.line,
                     f"{what} {ref.path} differs in letter case from /{actual}.{unused}",
                     "Match the letter case exactly; Linux builds and CI are case-sensitive.")
        elif status == "ignored":
            self.add(severity, "path-ignored", ref.file, ref.line,
                     f"{what} {ref.path} is excluded by .defignore, so Bob cannot find it.{unused}",
                     "Remove the matching line from .defignore or stop referencing the file.")
        elif status == "maybe-library":
            self.add("warning", "path-unverified", ref.file, ref.line,
                     f"{what} {ref.path} is not in the project and may come from a library "
                     "dependency that is not fetched.",
                     "Fetch libraries and re-run; if no library provides it, fix the path.")
        elif ref.kind == "load-resource":
            self.add("warning", "path-missing", ref.file, ref.line,
                     f"{what} {ref.path} does not exist.",
                     "Fix the path and list the file or its folder in project.custom_resources.")
        else:
            if ref.kind == "require" and ref.severity == "warning":
                fix = (f"Add {ref.path}, or ignore this if the module is provided by the engine "
                       "or a native extension.")
            elif ref.kind == "require":
                fix = self.missing_fix(ref.path, "Fix the dotted module name (a.b means /a/b.lua)")
            else:
                fix = self.missing_fix(ref.path, "Fix the path")
            self.add(severity, "path-missing", ref.file, ref.line,
                     f"{what} {ref.path} does not exist.{unused}", fix)


# ----------------------------------------------------------------------- CLI


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="check_defold_project.py",
        description=(
            "Read-only check of a Defold project for broken references: game.project main "
            "collection and input binding (with the compiled 'c' suffix), quoted /paths in "
            "collections, game objects, atlases, GUI and other resources, require() modules "
            "and resource properties in scripts, letter-case mismatches, .defignore'd files "
            "and paths from fetched library dependencies."
        ),
        epilog=(
            "example:\n"
            "  python scripts/check_defold_project.py path/to/my_game\n"
            "  python scripts/check_defold_project.py . --json --max-issues 20\n\n"
            "exit codes: 0 no errors, 1 errors found (or warnings with --strict), "
            "2 usage problem or no game.project"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("project_dir", nargs="?", default=".", metavar="PROJECT_DIR",
                        help="folder that contains game.project (default: current folder)")
    parser.add_argument("--json", action="store_true", help="print one JSON object instead of text")
    parser.add_argument("--strict", action="store_true", help="exit 1 when there are warnings too")
    parser.add_argument("--max-issues", type=int, default=50, metavar="N",
                        help="print at most N issues, errors first (default: 50; 0 = no limit)")
    return parser


def main(argv: list[str] | None = None) -> int:
    # A console or pipe that cannot encode a file name (Windows code pages) must not crash the report.
    for stream in (sys.stdout, sys.stderr):
        if hasattr(stream, "reconfigure"):
            stream.reconfigure(errors="backslashreplace")
    parser = build_parser()
    args = parser.parse_args(argv)
    if args.max_issues < 0:
        parser.error("--max-issues must be 0 or more")
    root = Path(args.project_dir).expanduser()
    if not root.is_dir():
        print(f"error: {args.project_dir} is not a folder", file=sys.stderr)
        return 2
    if not (root / "game.project").is_file():
        print(f"error: {args.project_dir} is not a Defold project (no game.project found there)",
              file=sys.stderr)
        return 2
    root = root.resolve()
    project = Project(root)
    project.run()

    def sort_key(issue: Issue) -> tuple:
        return (issue.file, issue.line or 0, issue.code, issue.message)

    errors = sorted((i for i in project.issues if i.severity == "error"), key=sort_key)
    warnings = sorted((i for i in project.issues if i.severity == "warning"), key=sort_key)
    ordered = errors + warnings
    shown = ordered[: args.max_issues] if args.max_issues else ordered
    truncated = len(ordered) > len(shown)

    if args.json:
        payload = {
            "tool": TOOL,
            "schema": SCHEMA,
            "root": root.as_posix(),
            "files_checked": project.files_checked,
            "error_count": len(errors),
            "warning_count": len(warnings),
            "errors": [i.as_dict() for i in shown if i.severity == "error"],
            "warnings": [i.as_dict() for i in shown if i.severity == "warning"],
            "truncated": truncated,
        }
        print(json.dumps(payload, indent=2))
    else:
        for issue in shown:
            where = f"{issue.file}:{issue.line}" if issue.line else issue.file
            print(f"{issue.severity.upper()} {where}: {issue.message}")
            print(f"  fix: {issue.fix}")
        if truncated:
            print(f"... and {len(ordered) - len(shown)} more")
        print(f"Summary: {len(errors)} errors, {len(warnings)} warnings in {project.files_checked} files checked")

    if errors or (args.strict and warnings):
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
SKILL.md›
---
name: defold-core
description: >
  Build and debug the core of a Defold game in Lua: game.project settings, collections, game
  objects and components, script lifecycle and self state, go.property, input focus and
  bindings, URLs and message passing, factories, collection factories and collection
  proxies, plus building and smoke-running with Bob. Use when a project has game.project or
  .collection, .go, .script, .gui_script or .input_binding files, or the request mentions
  Defold, msg.post, on_message, on_input, acquire_input_focus, factory.create, collection
  proxy, bob.jar, or a Defold build error.
---

# Defold Core

Structure, wire and verify a Defold game: editor resource files, the Lua script lifecycle,
addressing and messages, runtime spawning and world loading, and a Bob build-and-run check.
Examples target **Defold 1.14.0**; they were built with Bob 1.14.0 and run on the 1.14.0
headless engine. An existing project keeps its Defold version (see [Version](#version)).

## When to use

- Use when adding or fixing game objects, collections, scripts, input, messages, spawning or
  level loading in a Defold project (`game.project` in the root, `.collection`, `.go`,
  `.script`, `.gui_script` files), or when Bob reports a missing resource.

**When *not* to use:** plain Lua or LÖVE projects (`love2d-core`); GUI layout, sprites,
tilemaps and physics depth beyond the pointers in the references; engine-agnostic design
handled by the related skills below.

## Workflow

1. **Inspect first.** Read `game.project` (`[bootstrap] main_collection`, `[input]
   game_binding`, `[project] dependencies`), the main collection, and the scripts near the
   request. Follow the project's folder layout, id naming and message names; do not
   reorganize. Note which Defold/Bob version CI or the team uses.
2. **Edit the right file type.** Game objects (`.go`) hold components; collections
   (`.collection`) place game objects and other collections; logic goes in `.script`
   (game objects), `.gui_script` (GUI) or shared `.lua` modules. Editor files are
   protobuf text: keep their structure, and copy shapes from
   `references/file-formats.md` instead of guessing fields.
3. **Check references.** Run `python scripts/check_defold_project.py <project-dir>` (path
   relative to this skill folder; Python 3.10+, no Defold install needed). Fix every `ERROR`
   (missing or wrong-case `/paths`, a `game.project` path without the compiled `c` suffix,
   a `require` whose file does not exist). Warnings cover files nothing uses yet, unfetched
   library dependencies and runtime-loaded files. Re-run until it exits 0.
4. **Build with Bob.** `java -jar bob.jar --root . --variant headless --archive resolve
   build` needs JDK 25 and a `bob.jar` matching the project's Defold version. Exit 1 plus
   an `ERROR:` line means a broken reference or a Lua syntax error (with file and line);
   `references/file-formats.md` shows how to trace an error that names a generated file.
5. **Run it.** A build does not prove the game works. Run a headless bundle under a timeout
   (`references/build-and-run.md`) and read the log: `ERROR:SCRIPT:` lines are Lua runtime
   errors, `ERROR:GAMEOBJECT:` lines bad addresses or socket clashes. Require a line your
   test prints when it finishes; the exit code alone proves nothing. The headless engine has
   no keyboard or mouse, so check input changes in the editor or a desktop build, and say
   what you could not verify.

## Patterns

### Project and resource references

`game.project` is an INI file in the project root. Resource paths everywhere are
root-absolute (`/main/player.go`). In `game.project` they name the **compiled** file, so the
source path gets a trailing `c`:

```ini
[bootstrap]
main_collection = /main/main.collectionc

[input]
game_binding = /input/game.input_bindingc
```

Bob builds only what is reachable from `game.project`; `/builtins/...` ships with the engine.

### Collections, game objects, components

A collection places instances and can override script properties per instance; a game object
lists file components and embedded components (escaped `data:` strings). Full verified
files are in `references/file-formats.md`.

```text
# main.collection (excerpt): place player.go and override its script's speed
instances {
  id: "player"
  prototype: "/main/player.go"
  component_properties {
    id: "player"
    properties { id: "speed" value: "250.0" type: PROPERTY_TYPE_NUMBER }
  }
}
```

### Script lifecycle, `self` and properties

```lua
go.property("speed", 200)                   -- top level only, literal default; editable per instance

local LEFT = hash("left")                   -- hash ids once, not every frame
local RIGHT = hash("right")
local FIRE = hash("fire")

function init(self)
	msg.post(".", "acquire_input_focus")    -- no focus, no on_input calls
	self.held = {}                          -- per-instance state lives on self
end

function update(self, dt)
	local dir = (self.held[RIGHT] and 1 or 0) - (self.held[LEFT] and 1 or 0)
	if dir ~= 0 then
		local pos = go.get_position()
		pos.x = pos.x + dir * self.speed * dt    -- dt-scaled: same speed at any frame rate
		go.set_position(pos)
	end
end

function on_input(self, action_id, action)
	if action_id == LEFT or action_id == RIGHT then
		if action.pressed then self.held[action_id] = true
		elseif action.released then self.held[action_id] = nil end
		return true                         -- consumed: listeners lower on the stack skip it
	elseif action_id == FIRE and action.pressed then
		factory.create("#bulletfactory", go.get_position(), nil,
			{ velocity = vmath.vector3(0, 600, 0) })
		return true
	end
end

function final(self)
	msg.post(".", "release_input_focus")
end
```

Callbacks: `init`, `final`, `fixed_update`, `update`, `late_update` (after every `update`,
before render), `on_message`, `on_input`, `on_reload`. Update order between game
objects is unspecified. `fixed_update` runs 0..N times per frame at `[engine]
fixed_update_frequency`; set `[physics] use_fixed_timestep = 1` too so physics steps at the
same rate, and put physics-driven code there. `self` is userdata: store
fields on it, but `pairs(self)` and `pprint(self)` do not work. Property types: number,
boolean, hash, `msg.url`, `vmath.vector3`/`vector4`, `vmath.quat`, resources such as
`resource.atlas("/main/main.atlas")`, and (since 1.13.2) strings.

### Input bindings and the input stack

`.input_binding` maps devices to action names (`key_trigger`, `mouse_trigger`, plus
gamepad, touch and text triggers). Every component that acquired focus sits on an input
stack; the last to acquire is on top and gets input first, and returning `true` from
`on_input` stops it there, which is how pause menus block gameplay input. Key actions report
`pressed` and `released`; track held keys yourself as above.

### Addressing and messages

A URL is `[socket:][path][#fragment]`: `"."` is this game object, `"#"` this component,
`"#sprite"` a sibling component, `"enemy#ai"` a relative object, `"/manager#controller"` an
absolute one, `"main:/loader#loader"` an object in another world. Collections create naming
scopes, so a string id is resolved relative to the sender's collection; a pre-hashed id is
always absolute.

```lua
msg.post("/manager#controller", "enemy_spotted", { id = go.get_id() })

function on_message(self, message_id, message, sender)
	if message_id == hash("enemy_spotted") then
		print("from", sender, "id", message.id)
	end
end
```

Messages are queued and dispatched later in the frame; the table is capped at 2 KB. A bad
address logs `ERROR:GAMEOBJECT: Instance '/x' could not be found when dispatching message`.

### Factories and collection factories

```lua
-- factory component "#bulletfactory" -> one game object; properties override go.property values
local id = factory.create("#bulletfactory", pos, nil, { velocity = vmath.vector3(0, 600, 0) })

-- collectionfactory component "#wavefactory" -> table of collection ids -> new instance ids
local ids = collectionfactory.create("#wavefactory", vmath.vector3(480, 500, 0))
local leader = ids[hash("/leader")]         -- e.g. hash: [/collection0/leader]
go.delete(ids, true)                        -- later: delete them and their children
```

Position and rotation default to the spawning object's. `go.delete()` takes effect at the
end of the frame. Per-instance properties for a collection factory are keyed by id:
`{ [hash("/leader")] = { hp = 5 } }`.

### Collection proxies (load another world)

```lua
-- excerpt from the loader script; the collection's "#levelproxy" component is a collectionproxy
function init(self)
	msg.post(".", "acquire_input_focus")        -- else the loaded world gets no input
	msg.post("#levelproxy", "async_load")       -- or "load"
end

function on_message(self, message_id, message, sender)
	if message_id == hash("proxy_loaded") then
		msg.post(sender, "init")
		msg.post(sender, "enable")
	elseif message_id == hash("level_done") then
		msg.post("#levelproxy", "unload")       -- disables and finalizes first
	elseif message_id == hash("proxy_unloaded") then
		-- safe to load the next level here
	end
end
```

The loaded world's socket is the **`name:` of the loaded collection file**, so scripts in it
reply with `msg.post("main:/loader#loader", ...)` and its own URLs read
`level1:/level#level`. `set_time_step { factor = 0, mode = 0 }` pauses a proxy world.

## Version

`game.project` has no engine version key. Use the Defold editor or `bob.jar` version the
project is built with (CI often pins a release sha1), and `library.defold_min_version` when the
project is a library. `java -jar bob.jar --version` prints the version and sha1. Keep that
version; do not migrate unless asked.

## Pitfalls

- **Bob: `the bootstrap.main_collection '/main/main.collectio' can't be found`** → the
  setting lacks the compiled suffix and Bob stripped the last letter. Write
  `/main/main.collectionc` (same for `input.game_binding` → `.input_bindingc`).
- **Bob: `ERROR: build/default/_generated_….factory:0: the prototype '/main/x.go' can't be
  found`** → an embedded component in some `.go` points at a missing file. Search the
  sources for the quoted path (`/main/x.go`), not the generated name, or run the checker.
- **`on_input` never fires** → the object never sent `acquire_input_focus`, the action name
  differs from the binding, `input.game_binding` points elsewhere, or a listener higher on
  the stack returns `true`.
- **Input dead inside a proxy-loaded level** → the game object holding the collection proxy
  did not acquire input focus.
- **`already a socket with the same name` and the proxy never sends `proxy_loaded`** → the
  loaded collection's `name:` equals an already loaded world (often both are `"main"` or
  `"default"`). Give every proxied collection a unique name.
- **`fixed_update` never runs** → `[engine] fixed_update_frequency` is 0 (default 60). For
  physics also set `[physics] use_fixed_timestep = 1`, as the manual requires both.
- **`module 'modules.util' not found` at run time although Bob succeeded** → a `require` used
  slashes (`require "modules/util"`). Use the dotted path from the project root,
  `require "modules.util"` for `/modules/util.lua`.
- **`tostring(id)` prints `hash: [<unknown:…>]`** → hashes keep their source text only in
  debug builds. Compare hashes, never their strings.
- **A second engine logs `Failed to initialize Remotery`, or a test run never ends** → the
  debug engine opens an engine service (port 8001), a log server and a profiler port, and
  waits forever without `sys.exit`. Run one engine at a time and always use a timeout.

## References

- `references/file-formats.md` — verified minimal `game.project`, `.collection`, `.go`,
  `.atlas`, `.input_binding` and script files, and how to trace build errors back to a
  source file; read before writing editor files by hand.
- `references/build-and-run.md` — JDK 25 and `bob.jar`, Bob commands, platforms and
  variants, a headless smoke test with timeout and log checks, and pointers for GUI,
  sprites, tilemaps and collision objects.
- `scripts/check_defold_project.py` — read-only reference checker; `--help` lists every check.

## Related skills

- `input-systems` — rebinding, gamepad support and buffering on top of Defold bindings.
- `game-ui-ux` — HUD and menu design to build with GUI scenes and `.gui_script`.
- `physics-tuning` — fixed-timestep and collision tuning for collision objects.
- `camera-systems` — follow/deadzone logic, typically applied in `late_update`.
- `save-systems` — save formats and slots; pair with Defold's `sys.save`/`sys.load`.
- `game-feel` — juice built from `go.animate` and timers.
- `platformer`, `level-design` — genre and layout guidance that binds to this skill.
- `love2d-core` — the other Lua engine in this catalog, for LÖVE projects.