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
- Commit or branch. 6.2 removals are not revertible by re-installing an older addon over a rewritten project.
- 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.
- 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 fromMoveValidation3D, 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 matchingGridPositioner3Dwrapper are removed. Use the typedray_from_screen_to_cell() -> Vector3iand compare against the class'sMISSconstant:# 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_ownersparameter ofcell_from_ray()andray_from_screen_to_cell_occluded()— nowArray[Vector3i], so passMISSwhere you passednull.GridCellRaycaster3D.surface_height_source()returns afloat. ReturnObjectPlacementGeometry3D.NO_GROUND(or any non-finite value) for a column with no ground, instead ofnull.PlacementWorldRegistry.get_pick_volume_owners_3d()returnsArray[Vector3i]. A volume with no GRID mount holdsPlacementWorldRegistry.NO_PICK_OWNER(equal toGridCellRaycaster3D.MISS) instead ofnull.PlacementIntentSchedulertakes typed deferred records. ConstructPlacementIntentScheduler.DeferredIntent(intent, session)and pass it toenqueue(); sethas_cell,dimensions, andcell_2dorcell_3dwhen 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_resultand its constructor require a supportedRefCountedresult ornull—PlacementReport,ValidationResults, orManipulationData(includingTerrainDemolishData). Duck-typed diagnostic objects now fail at assignment; use the typed factories.ObjectDragShapePreview.resolve_placement_runtime()requires a typedGridPlacementHostandPlacementSession. Tests and integrations must supply the real contracts.IndicatorManager.resolve_placement_dependencies()takes aPlacementSession;CollisionUtilities2D.get_rect_tile_positions()takes aTileMapLayer; thep_builder_ownerofTerrainPlacementService2D.try_place_terrain_by_name()andtry_place_terrain_brush()is aPlacementOwner. 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()acceptsPlaceableSequenceand returnsPlaceableListEntry; its selected-entry getter and selection signal use that row type.PlaceableListEntry.placeableandget_active_placeable()useScenePlacementEntry, andsequenceusesPlaceableSequence. 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()andavailable_shapes()now returnArray[PlacementEnums.ObjectTool]instead of a bareArray.collect_supported_tools()andPlacementRuleResolver.any_profile_ignores_base()takeArray[PlacementProfile], and the host's ground-carry move methods take aManipulationData.PlacementValidator.setup()andPlacementRuleValidationLogic.setup_rules()returnDictionary[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 asItemLists, but tabs are scroll pages of entry views, so calling either errored. Editplaceablesand callrebuild()instead.PlacementCatalog.find_terrain()— was an alias; usefind_tile_entry()PlacementLogger.log_verbose_throttled(),log_debug_throttled(),log_trace_throttled(),log_verbose_once(),log_trace_once(). The remaininglog_debug_once(),log_warning_once(),log_error_once()andlog_info_once()behave as before.PlacementLifecycleAllocator.reserve_placed_instance_id(),reserve_world_id(),reserve_session_id(). Snapshots no longer carrynext_instance,next_worldornext_session; snapshots saved with them still restore, and the extra keys are ignored.DemoManifest.internal_demos()—all_demos()still reads theinternal_onlyflag.PlacementDiagnostics.format_indicator(),describe_collision_layers_3d()PlacementAStarPathManager.on_*_changed()andupdate_if_dirty()— useconfigure()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, andDragService.
Verifying the upgrade
- Open the project. A removed class or method makes the editor print the exact script and line.
- 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. - 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.
- 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.