Skip to content

Grid Placement v6.2.0

6.1 to 6.2 Migration Guide

Upgrade a 6.1 project to 6.2: the renames, removed classes, and typed-record reads you have to fix by hand.

Status
Current
Version
v6.2.0
Source updated
2026-09-24
Generated on
2026-10-01

6.2 is a refactor: classes were split along single responsibilities, parameters were typed, and a lot of dead API was deleted. Your scenes and resources still load — the values that survived kept their numbers. What does not survive is code that calls the removed API, which fails to load at runtime.

Read this top to bottom once, then use the grouped lists as your checklist.

What the automated migrator does for you

Almost nothing, and the honest answer is "two lines". The in-editor converter (Project → Tools → Grid Placement → Migrate 5.x Project…) was written for 5.x → 6.x. It finds legacy res://addons/grid_building/ paths and GB*-prefixed 5.x class names, neither of which a 6.1.2 project contains.

It does have two rules that are not 5.x-specific, and they do fire on your project, because they are plain word-boundary text rewrites over .gd files:

Auto-converted Change
Mode.BUILD Mode.PLACE
Action.BUILD Action.PLACE

That is the complete list. Verified against migration/project_migration_map.gd: it contains no 6.2 symbol at all — no collsion_test, no GridPositioner3DSpike, no PlacementInjectable, no ALLOW_DIAGONOL, no create_with_injection. Every other 6.2 break below is invisible to it.

If your project uses the BUILD aliases, run the dry run and apply — it is free. If it does not, you do not need the converter. Do not rely on a "0 remaining findings" dry run to mean your project is 6.2-clean. It means there were no 5.x shapes, which was already true.

Everything else is yours to do: the renames in the next section are a find-and-replace, and the rest are code changes you have to make by hand.

Before you start

  1. Commit or branch. 6.2 removals are not revertible by re-installing an older addon over a rewritten project.
  2. Note which of your scripts extend classes from the addon and call into placement services. Those are the files at risk; most projects touch a small fraction of the surface.
  3. After upgrading, open the project and read the errors. A removed class makes a script fail to load, and the editor names it.

Group 1: find-and-replace

These are pure renames. No behaviour changes. The class or folder keeps its UID, so scenes and resources that reference it by class_name or uid:// keep working — you only need to change hand-written preload() paths and identifier references.

Replace With Note
placement/collsion_test/ placement/collision_test/ Typo fix. Folder only; class names and UIDs unchanged.
GridPositioner3DSpike GridCellRaycaster3D Class rename, same UID.
.SPIKE_MISS .MISS The constant on the class.
get_spike() get_raycaster() On GridPositioner3D.
MoveDirection.ALLOW_DIAGONOL MoveDirection.ALLOW_DIAGONAL Misspelling removed.
ObjectPlacementService3D.NO_GROUND ObjectPlacementGeometry3D.NO_GROUND The constant moved to the geometry class.

The last one is easy to miss because the old name still reads plausibly.

Group 2: PlacementInjectable is gone

If any of your scripts start with extends PlacementInjectable, they will fail to load. Change the base class:

# 6.1
extends PlacementInjectable

func inject_dependencies(session:PlacementSession)-> void:
    ...

# 6.2
extends RefCounted

func resolve_placement_dependencies(session:PlacementSession)-> void:
    ...

The pattern is a two-step split, and the second step is the part people miss:

# 6.1 — dependencies were injected, errors surfaced elsewhere
helper.inject_dependencies(session)

# 6.2 — resolve, then CHECK the result
helper.resolve_placement_dependencies(session)
if not helper.get_runtime_issues().is_empty():
    # do something about it
    ...

inject_dependencies() and resolve_placement_runtime() are both removed. If you were calling resolve_placement_runtime() and ignoring the result, the missing check is now a real behaviour gap rather than a dead call.

The addon's own helpers were converted for you: PlacementValidator, TargetHighlighterService, PlacementInstantiatorService, PreviewFactory, PreviewBuilder, CollisionMapper2D, CollisionProcessor2D, IndicatorService2D, TestSetupFactory, and PlaceableSelectionLogic all extend RefCounted now. If you subclassed any of them, follow the same split.

The create_with_injection() static factories are gone from CollisionMapper2D, IndicatorManager, IndicatorService2D, PlacementLogger, PlacementValidator, PreviewFactory, and TestSetupFactory. Construct the object with its dependencies and resolve them:

# 6.1
var mapper= CollisionMapper2D.create_with_injection(session)

# 6.2
var mapper= CollisionMapper2D.new()
mapper.resolve_placement_dependencies(session)

Group 3: renamed class, moved file

PlacementValidator moved into placement/placement_rules/. The class name and UID are unchanged, so bare PlacementValidator references and uid:// links keep working. Only a preload() of the old placement/placement_validator.gd path breaks — repoint it at the file in placement/placement_rules/. Write the path relative rather than as a full res:// string when you document it: the validator is excluded from the presentation-only package, so a full path here would be a reference the packaging check cannot resolve.

Group 4: removed constructors and setters

Signature changes that fail at the call site, not at load time.

  • PreviewBuilder.initialize() is removed. Pass the settings and states to the constructor instead.

  • ManipulationService3D.set_indicator_manager() is removed. 3D move verdicts already came only from MoveValidation3D, so delete the call.

  • SmoothOccupancyRegistry2D.rebuild_from_scene() and the 3D equivalent take only the parent node. The ignored footprint callback parameter is gone:

    # 6.1
    registry.rebuild_from_scene(parent, _on_footprint)
    
    # 6.2
    registry.rebuild_from_scene(parent)

Group 5: dictionaries became typed records

The largest group, and the one that produces the most confusing errors: the API still works, but reads through a typed object, and "key" lookups return nothing at runtime rather than failing to compile.

Call 6.1 6.2
SmoothPlacementService3D.last_resolve Dictionary Pose — read .position, .yaw, .socket_id, .snapped, .ground_miss, .socket_required_miss; is_ok() replaces the "ok" key. Null until the first resolve.
PlacementWorldRestoreCoordinator{2,3}D.restore() Dictionary Result — read .ok and .nodes instead of ["ok"] / ["nodes"].
SmoothSocketRegistry3D.get_socket() / nearest() Dictionary (empty when absent) Socket, or null when absent — read .origin, .id, and the other fields. Note the absence test changes from "is it empty" to "is it null".
SmoothOccupancyRegistry.snapshot_state() / restore_state() Array of dictionaries Array[Entry] of Entry2D/Entry3D copies.
ObjectPlacementService.simulate_ordered_cells() Dictionary OrderedSim with .cells, .rects, .capped, .scanned, .validated.
ObjectDragShapePreview.get_last_geometry() / geometry_for_cells() Dictionary Geometry with .kind, .points, .rect, .fill, .accepted_cells, .accepted_rects, .style.
ObjectFillSpan3D.edge_mounts() 2-element array EdgeMounts — read .cells and .edges instead of indexes 0 and 1.
SurfaceSupportSampler3D.sample_footprint() Array of dictionaries Array[Sample], built with Sample.new() rather than a dict literal.
PlacementWorldRegistry.snapshot_state() / restore_state() Dictionary Snapshot — read .records; null now fails closed.
ObjectPlacementGeometry3D.consumer_anchor_local() {ok, local} FoundTransform — read .transform, or null when the anchor is missing.
TerrainTilePainter.resolve_terrain_by_name() Dictionary (empty on a miss) TerrainId with .terrain_set / .terrain, or null on a miss.
GridPositioner3D.get_last_pick_hit() Dictionary PickHit with .has_hit, .position, .normal, .cell, .collider.
2D collision tile windows {"start", "end_exclusive"} Rect2i — .position is the first tile, .end is exclusive.
PlaceableSequence.get_variant() generic resource ScenePlacementEntry.

ObjectPlacementService3D also moved its per-socket maps into a new ObjectMountRegistry3D. get_mounts() returns it for inspection, snapshot_placement_state() returns one, and restore_placement_state() takes one. PlacementValidation3D.PlacementValidationContext3D carries the same registry as mounts in place of its twelve separate map fields.

SmoothOccupancyRegistry2D and the 3D equivalent now share a SmoothOccupancyRegistry base. SmoothOccupancyEntry2D and SmoothOccupancyEntry3D are removed — use the inner Entry2D / Entry3D classes. The 3D rebuild now clears entries when a SMOOTH record has no smooth_occupancy projection, where it previously skipped that record, so a stale entry can now disappear; that is the fix, not a regression.

Group 6: signatures you call with different arguments

  • GridCellRaycaster3D.ray_from_screen_to_cell_or_null() and the matching GridPositioner3D wrapper are removed. Use the typed ray_from_screen_to_cell() -> Vector3i and compare against the class's MISS constant:

    # 6.1
    var cell= raycaster.ray_from_screen_to_cell_or_null(ray)
    if cell!= null:
        ...
    
    # 6.2
    var cell:Vector3i = raycaster.ray_from_screen_to_cell(ray)
    if cell!= GridCellRaycaster3D.MISS:
        ...

    The same applies to the p_volume_owners parameter of cell_from_ray() and ray_from_screen_to_cell_occluded() — now Array[Vector3i], so pass MISS where you passed null.

  • GridCellRaycaster3D.surface_height_source() returns a float. Return ObjectPlacementGeometry3D.NO_GROUND (or any non-finite value) for a column with no ground, instead of null.

  • PlacementWorldRegistry.get_pick_volume_owners_3d() returns Array[Vector3i]. A volume with no GRID mount holds PlacementWorldRegistry.NO_PICK_OWNER (equal to GridCellRaycaster3D.MISS) instead of null.

  • PlacementIntentScheduler takes typed deferred records. Construct PlacementIntentScheduler.DeferredIntent(intent, session) and pass it to enqueue(); set has_cell, dimensions, and cell_2d or cell_3d when capturing a destination. flush() takes one callback receiving that record, and the callback owns session validation and activation. flush_with_context() and the older two-argument forms are removed.

  • HostDispatchResult.inner_result and its constructor require a supported RefCounted result or null — PlacementReport, ValidationResults, or ManipulationData (including TerrainDemolishData). Duck-typed diagnostic objects now fail at assignment; use the typed factories.

  • ObjectDragShapePreview.resolve_placement_runtime() requires a typed GridPlacementHost and PlacementSession. Tests and integrations must supply the real contracts.

  • IndicatorManager.resolve_placement_dependencies() takes a PlacementSession; CollisionUtilities2D.get_rect_tile_positions() takes a TileMapLayer; the p_builder_owner of TerrainPlacementService2D.try_place_terrain_by_name() and try_place_terrain_brush() is a PlacementOwner. These already failed further in, so the error just arrives earlier.

Group 7: PlacementSystemsContext is removed

The legacy registry is gone, along with PlacementContexts.systems and the unused building_system / targeting_system / manipulation_system runtime check flags. Placement services are owned by the host; targeting state owns the positioner.

  • PlacementContexts.get_runtime_issues() takes no arguments.
  • Optional Camera2D checks now belong to PlacementConfigurationValidator, and require the active camera in that positioner's viewport. They re-evaluate the current wiring on each call, so do not cache the result.

Group 8: custom manipulation services

Rotate and flip gates now live in ManipulationServiceContract. rotate(), flip_horizontal(), flip_vertical(), and is_ready() are declared once on the contract and run the same gates in both dimensions. As a result, the live-dependent gate now also runs for 2D rotation, where it always allows.

A custom service overrides two methods instead of four:

# 6.1 — four public methods, each re-running the gates
func rotate()-> bool: ...
func flip_horizontal()-> bool: ...
func flip_vertical()-> bool: ...
func is_ready()-> bool: ...

# 6.2 — supply only the transform
func _apply_transform()-> void: ...
func _manipulation_parent()-> Node2D: ...

Group 9: sequence and tool-list typing

  • PlaceableList.add_sequence() accepts PlaceableSequence and returns PlaceableListEntry; its selected-entry getter and selection signal use that row type. PlaceableListEntry.placeable and get_active_placeable() use ScenePlacementEntry, and sequence uses PlaceableSequence. Generic resource stand-ins are no longer accepted.
  • The 2D debug widget reads the total indicator count separately from collisions and refreshes it when the manager changes. The unused demo copy of the widget is removed — the demo scene already uses the addon implementation. If your scene pointed at the demo copy, point it at the addon one.
  • Action logs read rule issues directly instead of probing for an unsupported legacy reason method.
  • ScenePlacementEntry.resolve_object_tools(), TilePlacementEntry.resolve_terrain_tools(), PlacementProfile.collect_supported_tools(), PlaceableSelectionUI.available_object_tools(), BrushShapeBar.canonical_tool_order() and available_shapes() now return Array[PlacementEnums.ObjectTool] instead of a bare Array. collect_supported_tools() and PlacementRuleResolver.any_profile_ignores_base() take Array[PlacementProfile], and the host's ground-carry move methods take a ManipulationData.
  • PlacementValidator.setup() and PlacementRuleValidationLogic.setup_rules() return Dictionary[PlacementRule, Array].

Group 10: commit paths and message constants

SmoothPlacementService, SmoothPlacementService3D and ObjectPlacementService3D build, reject, and commit through the new PlacementCommitAttempt, and give committed roots their identity through PlaceableInstance.ensure_on(). Reports and player-facing text are unchanged, but the per-service message constants are replaced by the shared PlacementCommitAttempt.MSG_* constants:

The shared set is small — five constants, all on PlacementCommitAttempt: MSG_BLOCKED, MSG_OCCUPIED, MSG_TAKEN, MSG_MOUNT_TAKEN, and MSG_INSTANTIATION_FAILED. If your code referenced a per-service constant that is not one of those, grep for the replacement rather than assuming the name carried over; the per-service names did not all survive. The strings behind them are unchanged, so nothing player-facing needs rewriting.

Group 11: cost providers

SpendCostRuleBase and RefundCalculator still accept any object implementing get_count_by_id / try_remove_by_id / try_add_by_id, but probe it in one place. The SpendMaterialsRuleById._mat_container alias is removed — set the provider through SpendByIdRule or the rule's locator.

The placement rules guide documents the limits of spending through the plugin and recommends game-owned cost handling for shipped games. Read it before shipping a build where spending matters.

Group 12: removed scripts

These files are deleted. None were used by the addon, demos, or templates, but your project may have referenced them:

CollisionShapeProcessor, IndicatorCollisionTestSetup, PlacementCamera2DValidator, IndicatorFactory3D, RuleValidationLogic, TargetingShapeCast3D, TargetingArea3D, TargetingArea2D, ResourceStack (with BuildCost.from_resource_stacks()), the unused 3D collision mapper chain (CollisionMapper3D, CollisionObjectResolver3D, CollisionShapeProcessor3D, CollisionUtilities3D), the virtual/ item-container stub, and two unnamed helper scripts. 3D indicators map cells through IndicatorManager3D.

CursorSettings is also gone. Delete components/cursor_settings.gd, the [sub_resource] block that uses it, and the cursor = ... line. Code that called get_cursor() should set the mouse cursor itself — for example with Input.set_custom_mouse_cursor — when the placement mode changes.

NodeSearchLogic keeps only what NodeLocator uses: find_nodes_by_name, find_nodes_by_script, find_nodes_by_group, get_script_name. The unused find_nodes_by_class, find_nodes_by_property, find_nodes_by_method_result, combine_search_results, filter_search_results, sort_search_results and validate_search_params are removed.

Group 13: removed methods that were already unused

Nothing in the addon, demos, templates, or tests called these. They are listed so you can grep rather than discover them. tests/api_golden/golden_api.txt has the full list.

  • ValidationResults.get_failing_rule_results()
  • IndicatorManager.get_placement_validator(), setup_collision_mapper()
  • RuleCheckIndicator3D.add_rule(), get_rules(), clear()
  • IndicatorManager3D.update_for_edges()
  • GridPositioner3D.move_positioner_by_tile()
  • ManipulationParent3D.apply_rotation_counter_clockwise(), reset_and_snap_to_grid()
  • ManipulationState.is_targeted_movable()
  • PlacementWorldRegistry.register_projection()
  • BrushCoordinator.complete_terrain_brush_immediate(), complete_terrain_drag_only()
  • PreviewBuilder.update_position()
  • PlaceableSelectionUI.add_placeables(), remove_placeables() — these two were broken: they treated category tabs as ItemLists, but tabs are scroll pages of entry views, so calling either errored. Edit placeables and call rebuild() instead.
  • PlacementCatalog.find_terrain() — was an alias; use find_tile_entry()
  • PlacementLogger.log_verbose_throttled(), log_debug_throttled(), log_trace_throttled(), log_verbose_once(), log_trace_once(). The remaining log_debug_once(), log_warning_once(), log_error_once() and log_info_once() behave as before.
  • PlacementLifecycleAllocator.reserve_placed_instance_id(), reserve_world_id(), reserve_session_id(). Snapshots no longer carry next_instance, next_world or next_session; snapshots saved with them still restore, and the extra keys are ignored.
  • DemoManifest.internal_demos() — all_demos() still reads the internal_only flag.
  • PlacementDiagnostics.format_indicator(), describe_collision_layers_3d()
  • PlacementAStarPathManager.on_*_changed() and update_if_dirty() — use configure()
  • ManipulationServiceContract.set_source_tree_exiting_callback()
  • GridTargetingDebugText.set_indicator_position(), clear_indicator_position()
  • RuleCheckIndicatorLogic.format_indicator_state_from_parts(), build_visuals_diag_from_parts()
  • Static helpers on PhysicsMatchingUtils2D, PlacementGeometryUtils, PlacementGeometryMath, PlacementGridRotationUtils, PlacementGridRotationUtils3D, PlacementDiagnostics, ObjectPlacementGeometry3D, and DragService.

Verifying the upgrade

  1. Open the project. A removed class or method makes the editor print the exact script and line.
  2. Grep your own scripts for the Group 1 names — they are the cheap wins: GridPositioner3DSpike, SPIKE_MISS, get_spike, ALLOW_DIAGONOL, collsion_test, ObjectPlacementService3D.NO_GROUND.
  3. Grep for dictionary-style access on the values in Group 5. These do not error at load time; they return null and fail later at runtime.
  4. Run your project and exercise placement, move, and rotate in both 2D and 3D. Those three cover most of the removed surface.

What did not change

Your saved scenes and resources load. The remaining enum values kept their numbers, so a stored Mode value of 2 or an Action value of 0 no longer matches any member — check those two explicitly if you persist raw enum integers. Commit-path message text is unchanged, so player-facing strings do not need updating. Footprint projections were never saved, so save files need no migration for the occupancy changes.