Skip to content

Grid Placement v6.1.0

Game Bridge Recommendation

Recommendations for bridging Grid Placement 6.1 with game-owned systems.

Status
Current
Version
v6.1.0
Source updated
2026-09-24
Generated on
2026-09-23

A game bridge is useful when one build action must coordinate Grid Placement with several game systems, such as ownership, economy, construction progress, faction rules, AI tasks, or save IDs.

It is optional. A simple game can call Grid Placement directly.

Use a bridge when it removes repeated integration code

Good reasons:

  • several game systems need one game-specific build operation;
  • a successful placement must create or update a game entity;
  • faction or player ownership must be attached to committed placement;
  • construction, cost, or progression rules belong to the game;
  • your save system maps plugin placement_instance_id to a separate game building/entity ID;
  • you want plugin-specific types isolated from a large simulation module.

Do not add a bridge because of an arbitrary call-count or file-length threshold.

Keep placement state in one place

Grid Placement is authoritative for placement mechanics, not for the consuming game's simulation. It decides placement-session state, candidate transforms and placement validity. The consuming game remains authoritative for gameplay entities and consequences such as resources, construction, ownership, persistence and destruction.

A bridge translates between game logic and Grid Placement without creating a second placement database.

game request/policy
→ game bridge/BuildingAuthority
→ Grid Placement preview/validate/commit
→ typed placement result
→ game updates game-owned state

Grid Placement tracks reusable placement facts:

  • target and placement validity;
  • committed placement identity;
  • GRID and SMOOTH occupancy;
  • CELL/EDGE/FACE/CORNER/TOP mount occupancy and SMOOTH socket occupancy;
  • placement transform, mount, and support data;
  • placement-world restore.

Game-owned state typically includes:

  • ownership and factions;
  • construction progress;
  • costs and resources beyond the placement integration;
  • game entity/building IDs;
  • AI and pathfinding consequences;
  • game save schema and progression.

External / game-authority path

A simulation-heavy game can keep Grid Placement as the placement UX/rules layer while its own world model remains authoritative. The supported flow is:

  1. The player proposes a placement through Grid Placement.
  2. Grid Placement computes the candidate transform and placement validity.
  3. Grid Placement returns a stable PlacementLifecycleResult with the plugin-owned attempt_id and placed_instance_id, plus the opaque correlation_context that is echoed back untouched.
  4. The integration can decide and apply its own consequences and keep its own entity ID in its own save or a side table keyed by the plugin's placement ID. Plugin IDs and game entity IDs are separate namespaces.
  5. Presentation can be created or updated from game state.
  6. Later move/demolish operations pass through the same boundary: ManipulationState.pre_move and ManipulationState.pre_demolish are veto windows, and consequences such as refunds run after FINISHED through Refunder.on_demolish_confirmed.

The simple built-in path remains fully supported: the plugin instantiates scenes and manages placement bookkeeping while the game reacts to the results. PlaceableInstance.saved_game_data carries game-owned save fields through plugin round trips.

Occupancy state in a bridge

The full addon's occupancy registry answers placement queries. A bridge-side _occupied dictionary that mirrors it duplicates that fact. An integration that keeps its own placement authority can keep its own occupancy data as a separate namespace.

Game-specific operations

A bridge is most useful when it exposes game actions instead of wrapping every plugin method.

Example shape:

class_name BuildingAuthority
extends Node

signal building_committed(building_id:int, placement_id:StringName)
signal building_rejected(reason:String)

func request_build(owner_id:int, entry_id:StringName, target)-> void:
    # 1. Check game-owned permissions/cost policy.
    # 2. Submit through the supported Grid Placement API.
    # 3. On commit, create/update the game-owned building record.
    pass

The target and result types can differ between 2D GRID, 3D GRID mounts, and SMOOTH placement. Do not force every dimension through an invented dictionary wrapper only to make the bridge interface look uniform.

Concrete example

The following is a complete, minimal BuildingAuthority that coordinates game-owned building state with Grid Placement. It shows the full lifecycle: select, react to commit, correlate IDs, and save/load mapping.

class_name BuildingAuthority
extends Node

## Maps a plugin placement ID to the game-owned building record.
var _buildings:Dictionary = {}

signal building_committed(building_id:StringName, placed_node:Node)
signal building_rejected(reason:String)

@onready var _host:GridPlacementHost = $GridPlacementHost

func _ready()-> void:
    var session:= PlacementSession.new()
    session.grid_placement_bundle= preload("res://config/grid_placement_bundle.tres")
    _host.configure(session)
    # React to every committed/rejected placement.
    session.get_building_state().action_performed.connect(_on_placement_action)

## Player chooses what to place. Preview follows the cursor automatically.
func select_placeable(entry:ScenePlacementEntry)-> void:
    var session:= _host.get_session()
    if session== null or not _host.select_placeable_for_session(session, entry):
        building_rejected.emit("Placement is not ready.")

## Optional: game-owned gate before allowing a build request.
func request_build(entry:ScenePlacementEntry, player:Node)-> void:
    if not _can_afford(entry, player):
        building_rejected.emit("Not enough resources.")
        return
    select_placeable(entry)

func _on_placement_action(data:PlacementActionData)-> void:
    if data== null or data.report== null:
        return
    if not data.report.is_successful():
        var issues:Array[String]= data.report.get_issues()
        building_rejected.emit("; ".join(issues)if issues.is_empty()== false else "Placement failed.")
        return
    var placed_node:Node = data.report.placed
    var lifecycle:PlacementLifecycleResult = data.report.lifecycle_result
    var placement_id:StringName = lifecycle.placed_instance_id
    var building_id:StringName = _make_building_id(lifecycle)
    _buildings[placement_id]= _new_building_record(building_id, placed_node, lifecycle)
    building_committed.emit(building_id, placed_node)

func _make_building_id(lifecycle:PlacementLifecycleResult)-> StringName:
    # Your own ID scheme; independent of the plugin's placement ID.
    return &"building_%s" % lifecycle.attempt_id

func save_state()-> Dictionary:
    # Persist only game-owned fields keyed by placement ID.
    var out:Dictionary = {}
    for placement_id:StringName in _buildings:
        var rec:Dictionary = _buildings[placement_id]
        out[placement_id]= {"building_id": rec["building_id"],"kind": rec["kind"]}
    return out

func load_state(saved:Dictionary, placement_id_to_node:Dictionary)-> void:
    # Reconnect after Grid Placement restores placement records.
    for placement_id:StringName in saved:
        var node:Node = placement_id_to_node.get(placement_idas String)
        if node== null:
            continue
        _buildings[placement_id]= saved[placement_id].duplicate()
        _buildings[placement_id]["node"]= node

func _can_afford(entry:ScenePlacementEntry, player:Node)-> bool:
    # Game-owned economy check.
    return true

func _new_building_record(id:StringName, node:Node, lifecycle:PlacementLifecycleResult)-> Dictionary:
    return {"building_id": id,"node": node,"kind": lifecycle.definition_key}

What this example demonstrates

  • Selection goes through GridPlacementHost.select_placeable_for_session(), which updates the authoritative session and activates the correct GRID/SMOOTH placement path — no manual preview management.
  • Commit observation uses PlacementState.action_performed, the canonical signal for "a placement just happened." The typed PlacementActionData.report gives you the placed node, success/failure, and the lifecycle result.
  • ID correlation maps the plugin's placed_instance_id (stable, unique) to your own building_id. The two identities are separate namespaces and can be correlated without merging.
  • Save/load persists only your game-owned fields, then reconnects them to restored placement records through the stable ID mapping. Grid Placement restores placement transforms; you restore game meaning.

Key choices

  • The bridge reacts to placements; it does not drive the input loop. Let the host's _unhandled_input dispatch builds.
  • If you need programmatic placement (AI builders, scripted spawns), build a PlacementCommand.commit(entry, target, correlation) and call PlacementRuntime.new(host).execute(command). The target matches the session's coordinate mode: a Vector2i/Vector3i cell in GRID, a Vector2/Vector3 world position in SMOOTH. The returned HostDispatchResult carries the terminal PlacementLifecycleResult with your correlation data. Scene-free backends use SMOOTH; 2D GRID runs the session's rule pipeline and needs its positioner and target map (see placement/PLACEMENT_LIFECYCLE_CONTRACT.md).
  • For read-only game facts consumed during validation (tech unlocks, protected zones), a custom placement rule or world-facts provider can replace a full bridge and keep validation logic declarative.

Providers and rules for read-only facts

If Grid Placement only needs to read a game fact during validation, a custom placement rule or world-facts provider is often smaller than a full bridge.

Examples:

  • protected build zone;
  • technology unlocked;
  • 2D reserved cell;
  • inventory availability.

A bridge is useful when a larger game lifecycle coordinates around committed placement.

Save/load mapping

Keep plugin placement identity and game entity identity distinct:

placement_instance_id ↔ game building/entity id

Persist that mapping in your game save layer. Let Grid Placement restore placement records, then reconnect your game-owned fields through the stable mapping.

Remove / demolish flow

Demolition follows the same single-authority split: the plugin destroys the placed node, the game drops its own building record. Never free the placed node from game code — observe and react instead.

Two hooks, in order:

  1. Gate (optional). ManipulationState.pre_demolish fires before anything is destroyed. Call data.veto() to cancel game-side (a building under construction, a protected plot); the node is preserved and the attempt reports CANCELED.
  2. Cleanup. ManipulationState.action_performed also fires for demolitions. When data.action == PlacementEnums.Action.DEMOLISH and the status is FINISHED, read the canonical placement ID from the host's lifecycle result (_host.get_last_dispatch_result().get_lifecycle_result().placed_instance_id) and erase the matching game record.

Commit, move, and demolish results all populate placed_instance_id, so the lifecycle result is the stable handoff instead of a SceneTree lookup of the destroyed node. Refunder.on_pre_demolish is veto-only; apply no consequences there. Game consequences such as refunds belong in Refunder.on_demolish_confirmed, which runs only after the demolition is FINISHED.

func _ready()-> void:
    var manipulation:= _host.get_session().get_manipulation_state()
    manipulation.pre_demolish.connect(_on_pre_demolish)
    manipulation.action_performed.connect(_on_manipulation_action)

func _on_pre_demolish(data:DemolishData)-> void:
    var placement_id:= _placement_id_for_node(data.target.root)
    if placement_id.is_empty():
        return
    # Game-owned gate: construction in progress cannot be demolished.
    if _is_under_construction(placement_id):
        data.veto()

func _on_manipulation_action(data:ManipulationData)-> void:
    if data.action!= PlacementEnums.Action.DEMOLISH:
        return
    if data.status!= PlacementEnums.Status.FINISHED:
        return
    # Canonical result: commit/move/demolish populate placed_instance_id.
    var lifecycle:PlacementLifecycleResult = _host.get_last_dispatch_result().get_lifecycle_result()
    var placement_id:StringName = lifecycle.placed_instance_id
    if placement_id.is_empty():
        return
    _buildings.erase(placement_id)
    # Notify your own game systems (construction tasks, build-cap counts, save-dirty flag) here.

func _placement_id_for_node(node:Node)-> StringName:
    # Game-owned bookkeeping: the bridge stored the placed node per placement ID.
    for placement_id:StringName in _buildings:
        if _buildings[placement_id].get("node")== node:
            return placement_id
    return &""

If the attempt was vetoed or failed, the node still exists and the game record stays untouched — there is no half-demolished state to reconcile.

Anti-patterns

Avoid:

  • bypassing Grid Placement validation or occupancy from the bridge;
  • re-exporting private/internal service objects as your game's public API;
  • duplicating placement records in unsynchronized dictionaries;
  • putting faction, economy, or construction types into the reusable addon;
  • wrapping every plugin method one-for-one without adding a useful game-specific operation.