193 lines
7.7 KiB
GDScript
193 lines
7.7 KiB
GDScript
@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)))
|