Custom Placement Rules
This guide covers how to author your own placement rules in Grid Building 5.0.8.
For the built-in rule catalog and runtime rule flow, see Placement Rules.
Version note: This guide is validated for Grid Building 5.0.8.
Godot 4.4 Compatibility
Grid Building 5.0.8 is intended to stay compatible with Godot 4.4 workflows.
That means custom rules should not rely on newer GDScript abstract-class syntax for enforcement. Instead, the plugin uses a 4.4-safe virtual-method contract:
PlacementRule.validate_placement()returns a failure by default.- Custom rules must override the methods they actually need.
- Tests enforce the contract for common authoring mistakes.
If you forget to override validate_placement(), the base rule fails with:
This is a virtual condition function and should be implemented in a class that inherits from PlacementRule
Choose the Right Base Class
| Base class | Use it when | Typical examples |
| *********- | ************- | ***************- |
| PlacementRule | The rule depends on owner state, inventory, economy, or gameplay services | Credit cost, tech unlocks, faction rules |
| TileCheckRule | The rule depends on indicator positions, tile coverage, or per-tile validity | Bounds checks, tile data checks, collision checks |
Thread Safety and Threaded Physics (5.0.8+)
Important
If your project has Godot’s threaded physics enabled (physics/2d/run_on_separate_thread = true), calling standard C++ collision methods (like is_colliding(), get_collider(), or force_shapecast_update()) directly on indicator nodes from your custom rules can trigger engine crashes or DirectSpaceState2D is locked errors.
To ensure your custom TileCheckRule is fully thread-safe:
- Do not call
indicator.is_colliding()orindicator.get_collider(...)directly. - Always query the transparently cached properties:
- Use
indicator.cached_is_collidingto check for any active collisions. - Use
indicator.cached_collidersto retrieve the array of colliding nodes.
- Use
These properties will automatically update the collision state safely within the physics tick when indicators move, or return the last known safe state when accessed outside the physics thread.
Required Behavior
1. Override validate_placement()
This is the actual pass/fail entry point.
| |
An empty issues array means success.
2. Use get_setup_issues() only for blocking setup problems
Use this when the rule is not usable at all because required data is missing.
| |
These issues are returned from setup(...) and will fail rule setup.
3. Use get_runtime_issues() for non-blocking diagnostics
Use this for notes that are useful to inspect but should not fail setup.
| |
5.0.8 fix: Always call super.get_runtime_issues() BEFORE appending your custom issues. The base class may return early (e.g., “Property [target_map] is NULL”) and if you call _get_issues() first, your issues get lost.
In 5.0.8, these messages remain available for diagnostics, but they do not get appended into setup failures. To make runtime issues affect indicator display, see the section on overriding get_failing_indicators() below.
4. For TileCheckRule, override get_failing_indicators() only when you need per-indicator precision
If you do not override it, TileCheckRule falls back to validate_placement():
- success returns no failing indicators
- failure marks all provided indicators as failing
That fallback is good enough for many custom rules. Override get_failing_indicators() only if you need some tiles red and others green.
5. Making get_runtime_issues() affect indicator display (5.0.8)
⚠️ QUICK REFERENCE - Common Bug ⚠️
If your rule overrides
get_runtime_issues()and expects indicators to show red/green based on those issues, you MUST overrideget_failing_indicators()to call it. The base implementation only callsvalidate_placement().The bug: Indicator shows green when it should be red, even though
get_runtime_issues()returns failures. The fix: Overrideget_failing_indicators()(see pattern below).See also:
test/demos/building/my_grid_bounds_rule_bug_test.gdfor a full reproduction.
If you want your rule’s get_runtime_issues() to control whether the indicator shows red or green, you MUST override get_failing_indicators():
| |
Why this is needed: The base TileCheckRule.get_failing_indicators() only calls validate_placement(). It never calls get_runtime_issues(). So without this override, your runtime diagnostics are invisible to the indicator system.
Authoring Patterns
Simple non-tile gameplay rule
| |
Simple tile-based rule with validate-only fallback
| |
Because this extends TileCheckRule, a failed result will also make all supplied indicators invalid unless you override get_failing_indicators() with more specific logic.
Tile-based rule with explicit per-indicator failures
| |
Making get_runtime_issues() affect indicator display (5.0.8)
If your rule overrides get_runtime_issues() and you want those issues to control whether the indicator shows red or green, you MUST override get_failing_indicators():
| |
Why this is needed (5.0.8): The base TileCheckRule.get_failing_indicators() only calls validate_placement(). It never calls get_runtime_issues(). So without this override, your runtime diagnostics are invisible to the indicator system.
What the Tests Enforce
The focused custom-rule suites currently lock in these behaviors:
| Situation | Expected result |
| ********* | *************** |
| validate_placement() returns issues | Placement fails with those issues |
| TileCheckRule fails and does not override get_failing_indicators() | All provided indicators become invalid |
| get_runtime_issues() returns an informational note | The note is visible in runtime diagnostics only |
| get_setup_issues() returns a message | setup(...) fails with that message |
| setup_rules(...) is called with null targeting state | Setup fails with GridTargetingState is null |
This matters because it keeps setup failures, placement failures, and indicator visuals aligned instead of mixing them together.
Common Mistakes
- Not overriding
validate_placement(). - Putting placement pass/fail logic into
get_runtime_issues(). - Returning setup-breaking messages from diagnostics that should be informational only.
- Overriding
setup(...)and not callingsuper.setup(...)unless you are deliberately reproducing the base behavior yourself. - Extending
PlacementRulewhen the rule really needs tile or indicator data fromTileCheckRule.
Practical Recommendations
- Start with
validate_placement()only. - Add
get_setup_issues()only when the rule truly cannot run. - Add
get_runtime_issues()only for diagnostics you want to inspect without blocking setup. - Stay on the
TileCheckRulefallback unless you need per-indicator coloring. - Keep side effects in
apply()so they happen only after successful validation.