From d92f5e57996bfab1dffb56ac1193c8437441807d Mon Sep 17 00:00:00 2001
From: Souchet Ferdinand <Siandfrance@gmail.com>
Date: Mon, 11 Nov 2024 13:29:38 +0100
Subject: [PATCH] Add `TileMapLayer._update_cells` virtual callback called when
 the TileMapLayer's cells are updated

Made `_update_cells` a hook into the `TileMapLayer`'s internal update
---
 doc/classes/TileMapLayer.xml | 15 +++++++++++++++
 scene/2d/tile_map_layer.cpp  | 23 +++++++++++++++++++++++
 scene/2d/tile_map_layer.h    |  2 ++
 3 files changed, 40 insertions(+)

diff --git a/doc/classes/TileMapLayer.xml b/doc/classes/TileMapLayer.xml
index 6cbec9c2aaa3..e814d76b73ec 100644
--- a/doc/classes/TileMapLayer.xml
+++ b/doc/classes/TileMapLayer.xml
@@ -22,6 +22,21 @@
 				[b]Note:[/b] If the properties of [param tile_data] object should change over time, use [method notify_runtime_tile_data_update] to notify the [TileMapLayer] it needs an update.
 			</description>
 		</method>
+		<method name="_update_cells" qualifiers="virtual">
+			<return type="void" />
+			<param index="0" name="coords" type="Vector2i[]" />
+			<param index="1" name="forced_cleanup" type="bool" />
+			<description>
+				Called when this [TileMapLayer]'s cells need an internal update. This update may be caused from individual cells being modified or by a change in the [member tile_set] (causing all cells to be queued for an update). The first call to this function is always for initializing all the [TileMapLayer]'s cells. [param coords] contains the coordinates of all modified cells, roughly in the order they were modified. [param forced_cleanup] is [code]true[/code] when the [TileMapLayer]'s internals should be fully cleaned up. This is the case when:
+				- The layer is disabled;
+				- The layer is not visible;
+				- [member tile_set] is set to [code]null[/code];
+				- The node is removed from the tree;
+				- The node is freed.
+				Note that any internal update happening while one of these conditions is verified is considered to be a "cleanup". See also [method update_internals].
+				[b]Warning:[/b] Implementing this method may degrade the [TileMapLayer]'s performance.
+			</description>
+		</method>
 		<method name="_use_tile_data_runtime_update" qualifiers="virtual">
 			<return type="bool" />
 			<param index="0" name="coords" type="Vector2i" />
diff --git a/scene/2d/tile_map_layer.cpp b/scene/2d/tile_map_layer.cpp
index 30bd59c1a26e..c4a2f35d3183 100644
--- a/scene/2d/tile_map_layer.cpp
+++ b/scene/2d/tile_map_layer.cpp
@@ -1460,6 +1460,24 @@ void TileMapLayer::_clear_runtime_update_tile_data_for_cell(CellData &r_cell_dat
 	}
 }
 
+void TileMapLayer::_update_cells_callback(bool p_force_cleanup) {
+	if (!GDVIRTUAL_IS_OVERRIDDEN(_update_cells)) {
+		return;
+	}
+
+	// Check if we should cleanup everything.
+	bool forced_cleanup = p_force_cleanup || !enabled || tile_set.is_null() || !is_visible_in_tree();
+
+	// List all the dirty cell's positions to notify script of cell updates.
+	TypedArray<Vector2i> dirty_cell_positions;
+	for (SelfList<CellData> *cell_data_list_element = dirty.cell_list.first(); cell_data_list_element; cell_data_list_element = cell_data_list_element->next()) {
+		CellData &cell_data = *cell_data_list_element->self();
+		dirty_cell_positions.push_back(cell_data.coords);
+	}
+
+	GDVIRTUAL_CALL(_update_cells, dirty_cell_positions, forced_cleanup);
+}
+
 TileSet::TerrainsPattern TileMapLayer::_get_best_terrain_pattern_for_constraints(int p_terrain_set, const Vector2i &p_position, const RBSet<TerrainConstraint> &p_constraints, TileSet::TerrainsPattern p_current_pattern) const {
 	if (tile_set.is_null()) {
 		return TileSet::TerrainsPattern();
@@ -1673,6 +1691,10 @@ void TileMapLayer::_internal_update(bool p_force_cleanup) {
 	// This may add cells to the dirty list if a runtime modification has been notified.
 	_build_runtime_update_tile_data(p_force_cleanup);
 
+	// Callback for implementing custom subsystems.
+	// This may add to the dirty list if some cells are changed inside _update_cells.
+	_update_cells_callback(p_force_cleanup);
+
 	// Update all subsystems.
 	_rendering_update(p_force_cleanup);
 	_physics_update(p_force_cleanup);
@@ -1861,6 +1883,7 @@ void TileMapLayer::_bind_methods() {
 
 	GDVIRTUAL_BIND(_use_tile_data_runtime_update, "coords");
 	GDVIRTUAL_BIND(_tile_data_runtime_update, "coords", "tile_data");
+	GDVIRTUAL_BIND(_update_cells, "coords", "forced_cleanup");
 
 	ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "tile_map_data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_tile_map_data_from_array", "get_tile_map_data_as_array");
 
diff --git a/scene/2d/tile_map_layer.h b/scene/2d/tile_map_layer.h
index 6cb481849c54..912fa2be155c 100644
--- a/scene/2d/tile_map_layer.h
+++ b/scene/2d/tile_map_layer.h
@@ -321,6 +321,7 @@ class TileMapLayer : public Node2D {
 	bool _runtime_update_needs_all_cells_cleaned_up = false;
 	void _clear_runtime_update_tile_data();
 	void _clear_runtime_update_tile_data_for_cell(CellData &r_cell_data);
+	void _update_cells_callback(bool p_force_cleanup);
 
 	// Per-system methods.
 #ifdef DEBUG_ENABLED
@@ -462,6 +463,7 @@ class TileMapLayer : public Node2D {
 	void notify_runtime_tile_data_update();
 	GDVIRTUAL1R(bool, _use_tile_data_runtime_update, Vector2i);
 	GDVIRTUAL2(_tile_data_runtime_update, Vector2i, TileData *);
+	GDVIRTUAL2(_update_cells, TypedArray<Vector2i>, bool);
 
 	// --- Shortcuts to methods defined in TileSet ---
 	Vector2i map_pattern(const Vector2i &p_position_in_tilemap, const Vector2i &p_coords_in_pattern, Ref<TileMapPattern> p_pattern);