first commit

This commit is contained in:
Leonid Pershin
2026-08-10 20:39:06 +03:00
commit f16f60445c
281 changed files with 43576 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
@tool
extends RefCounted
## GridMap authoring — set, fill, clear, and read 3D cells plus mesh-library
## items directly in the editor scene with full undo/redo support.
##
## All ops target GridMap nodes in the currently edited scene by
## scene-relative path (e.g. "/Main/Terrain"). All write ops are undoable
## via EditorUndoRedoManager.
const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
const MAX_FILL_CELLS := 4096
var _undo_redo: EditorUndoRedoManager
func _init(undo_redo: EditorUndoRedoManager) -> void:
_undo_redo = undo_redo
## Set a single cell item. item = -1 erases the cell.
## params: {path, item, map_x, map_y, map_z, orientation=0}
## Returns: {map_x, map_y, map_z, item, orientation, undoable}
func set_item(params: Dictionary) -> Dictionary:
var gm := _resolve_gridmap(params)
if gm.has("error"): return gm
var node: GridMap = gm.node
var pos := Vector3i(int(params.get("map_x", 0)), int(params.get("map_y", 0)), int(params.get("map_z", 0)))
var item := int(params.get("item", 0))
var orientation := int(params.get("orientation", 0))
if orientation < 0 or orientation > 24:
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
"orientation must be in 0..24 (GridMap baked rotations), got %d" % orientation)
var prev := _capture_cell_state(node, pos)
_undo_redo.create_action("MCP: GridMap set_item")
_undo_redo.add_do_method(node, "set_cell_item", pos, item, orientation)
_undo_redo.add_undo_method(self, "_restore_cell_state", node, pos, prev)
_undo_redo.commit_action()
return {"data": {"map_x": pos.x, "map_y": pos.y, "map_z": pos.z,
"item": item, "orientation": orientation, "undoable": true}}
## Fill a box region with one item in a single undo action.
## params: {path, item, rect_x, rect_y, rect_z, rect_w, rect_h, rect_d, orientation=0}
## Returns: {cells_filled, rect: {x, y, z, w, h, d}}
func fill(params: Dictionary) -> Dictionary:
var gm := _resolve_gridmap(params)
if gm.has("error"): return gm
var node: GridMap = gm.node
var item := int(params.get("item", 0))
var orientation := int(params.get("orientation", 0))
if orientation < 0 or orientation > 24:
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
"orientation must be in 0..24 (GridMap baked rotations), got %d" % orientation)
var rx := int(params.get("rect_x", 0)); var ry := int(params.get("rect_y", 0)); var rz := int(params.get("rect_z", 0))
var rw := int(params.get("rect_w", 1)); var rh := int(params.get("rect_h", 1)); var rd := int(params.get("rect_d", 1))
if rw <= 0 or rh <= 0 or rd <= 0:
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
"rect_w, rect_h and rect_d must be > 0 (got %d x %d x %d)" % [rw, rh, rd])
var cell_count := rw * rh * rd
if cell_count > MAX_FILL_CELLS:
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
"Region too large: %d cells exceeds max %d" % [cell_count, MAX_FILL_CELLS])
var snapshot: Array[Dictionary] = []
for x in range(rx, rx + rw):
for y in range(ry, ry + rh):
for z in range(rz, rz + rd):
var pos := Vector3i(x, y, z)
snapshot.append({"pos": pos, "state": _capture_cell_state(node, pos)})
_undo_redo.create_action("MCP: GridMap fill %dx%dx%d" % [rw, rh, rd])
## First callback targets the node so the action lands in the scene undo
## history (first-target routing); _apply_fill batches the rest of the
## region into a single history entry instead of one per cell.
_undo_redo.add_do_method(node, "set_cell_item", snapshot[0].pos, item, orientation)
_undo_redo.add_do_method(self, "_apply_fill", node, snapshot, item, orientation)
_undo_redo.add_undo_method(self, "_restore_rect_snapshot", node, snapshot)
_undo_redo.commit_action()
return {"data": {"cells_filled": snapshot.size(),
"rect": {"x": rx, "y": ry, "z": rz, "w": rw, "h": rh, "d": rd}, "undoable": true}}
## Remove all cells from the GridMap.
## params: {path}
## Returns: {cleared: true}
func clear_layer(params: Dictionary) -> Dictionary:
var gm := _resolve_gridmap(params)
if gm.has("error"): return gm
var node: GridMap = gm.node
var used := node.get_used_cells()
if used.size() > MAX_FILL_CELLS:
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
"GridMap has %d cells, exceeds max %d for undoable clear"
% [used.size(), MAX_FILL_CELLS])
var snapshot := _capture_used_cells_snapshot(node)
_undo_redo.create_action("MCP: GridMap clear")
_undo_redo.add_do_method(node, "clear")
_undo_redo.add_undo_method(self, "_restore_cells_snapshot", node, snapshot)
_undo_redo.commit_action()
return {"data": {"cleared": true, "undoable": true}}
## Return all used cell coordinates.
## params: {path}
## Returns: {cells: [{x, y, z}, ...], count: int}
func get_used_cells(params: Dictionary) -> Dictionary:
var gm := _resolve_gridmap(params)
if gm.has("error"): return gm
var node: GridMap = gm.node
var result: Array = []
for c in node.get_used_cells():
result.append({"x": c.x, "y": c.y, "z": c.z})
return {"data": {"cells": result, "count": result.size()}}
## List the items available in the GridMap's MeshLibrary, so agents can
## discover item ids and names before placing cells (the 3D analogue of
## tileset atlas inspection).
## params: {path}
## Returns: {library: res:// path or "", items: [{item, name, mesh}...], count}
func list_library_items(params: Dictionary) -> Dictionary:
var gm := _resolve_gridmap(params)
if gm.has("error"): return gm
var node: GridMap = gm.node
var library: MeshLibrary = node.mesh_library
if library == null:
return {"data": {"library": "", "items": [], "count": 0}}
var items: Array = []
var ids := library.get_item_list()
ids.sort()
for item in ids:
var mesh: Mesh = library.get_item_mesh(item)
var mesh_path := mesh.resource_path if mesh != null else ""
items.append({
"item": item,
"name": library.get_item_name(item),
"mesh": mesh_path,
})
return {"data": {"library": library.resource_path, "items": items, "count": items.size()}}
## Resolve a GridMap node from params["path"] in the currently edited
## scene. Returns {"node": GridMap} on success, or an error dict.
func _resolve_gridmap(params: Dictionary) -> Dictionary:
var path: String = params.get("path", "")
var scene_file: String = params.get("scene_file", "")
var resolved := McpNodeValidator.resolve_or_error(path, "path", scene_file)
if resolved.has("error"):
return resolved
var node: Node = resolved.node
if not node is GridMap:
return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
"Node is not a GridMap: %s" % path)
return {"node": node}
func _capture_cell_state(node: GridMap, pos: Vector3i) -> Dictionary:
var item := node.get_cell_item(pos)
if item == -1:
return {"has_item": false}
return {"has_item": true, "item": item, "orientation": node.get_cell_item_orientation(pos)}
func _capture_used_cells_snapshot(node: GridMap) -> Array[Dictionary]:
var snapshot: Array[Dictionary] = []
for pos in node.get_used_cells():
snapshot.append({"pos": pos, "state": _capture_cell_state(node, pos)})
return snapshot
func _restore_cells_snapshot(node: GridMap, snapshot: Array[Dictionary]) -> void:
node.clear()
for entry in snapshot:
_restore_cell_state(node, entry.pos, entry.state)
## Batched do-method for fill: one undo-history entry applies the whole
## region instead of one entry per cell.
func _apply_fill(node: GridMap, snapshot: Array[Dictionary], item: int, orientation: int) -> void:
for entry in snapshot:
node.set_cell_item(entry.pos, item, orientation)
func _restore_rect_snapshot(node: GridMap, snapshot: Array[Dictionary]) -> void:
for entry in snapshot:
_restore_cell_state(node, entry.pos, entry.state)
func _restore_cell_state(node: GridMap, pos: Vector3i, state: Dictionary) -> void:
if not state.get("has_item", false):
node.set_cell_item(pos, -1)
return
node.set_cell_item(pos, int(state.get("item", 0)), int(state.get("orientation", 0)))