/*************************************************************************/
/*  scene_synchronizer_debugger.cpp                                      */
/*************************************************************************/
/*                         This file is part of:                         */
/*                          PANDEMONIUM ENGINE                           */
/*             https://github.com/Relintai/pandemonium_engine            */
/*************************************************************************/
/* Copyright (c) 2022-present Péter Magyar.                              */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
/*                                                                       */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the       */
/* "Software"), to deal in the Software without restriction, including   */
/* without limitation the rights to use, copy, modify, merge, publish,   */
/* distribute, sublicense, and/or sell copies of the Software, and to    */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions:                                             */
/*                                                                       */
/* The above copyright notice and this permission notice shall be        */
/* included in all copies or substantial portions of the Software.       */
/*                                                                       */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
/*************************************************************************/

#include "scene_synchronizer_debugger.h"

#ifdef DEBUG_ENABLED

#include "__generated__debugger_ui.h"
#include "core/io/json.h"
#include "core/os/dir_access.h"
#include "core/os/file_access.h"
#include "core/os/os.h"
#include "data_buffer.h"
#include "net_utilities.h"
#include "scene/main/viewport.h"
#include "scene_synchronizer.h"

#endif

SceneSynchronizerDebugger *SceneSynchronizerDebugger::the_singleton = nullptr;

SceneSynchronizerDebugger *SceneSynchronizerDebugger::singleton() {
	return the_singleton;
}

void SceneSynchronizerDebugger::_bind_methods() {
	ClassDB::bind_method(D_METHOD("on_node_added"), &SceneSynchronizerDebugger::on_node_added);
	ClassDB::bind_method(D_METHOD("on_node_removed"), &SceneSynchronizerDebugger::on_node_removed);

	ClassDB::bind_method(D_METHOD("add_node_message", "node", "message"), &SceneSynchronizerDebugger::add_node_message);
	ClassDB::bind_method(D_METHOD("add_node_message_by_path", "node_path", "message"), &SceneSynchronizerDebugger::add_node_message_by_path);

	ClassDB::bind_method(D_METHOD("debug_print", "node", "message", "silent"), &SceneSynchronizerDebugger::debug_print);
	ClassDB::bind_method(D_METHOD("debug_warning", "node", "message", "silent"), &SceneSynchronizerDebugger::debug_warning);
	ClassDB::bind_method(D_METHOD("debug_error", "node", "message", "silent"), &SceneSynchronizerDebugger::debug_error);
}

SceneSynchronizerDebugger::SceneSynchronizerDebugger() :
		Node() {
	if (the_singleton == nullptr) {
		the_singleton = this;
	}
#ifdef DEBUG_ENABLED
// Code here
#endif
}

SceneSynchronizerDebugger::~SceneSynchronizerDebugger() {
	if (the_singleton == this) {
		the_singleton = nullptr;
	}

#ifdef DEBUG_ENABLED
	tracked_nodes.reset();
	classes_property_lists.clear();
	frame_dump__begin_state.clear();
	frame_dump__end_state.clear();
	frame_dump__node_log.clear();
	frame_dump__data_buffer_writes.clear();
	frame_dump__data_buffer_reads.clear();
	frame_dump__are_inputs_different_results.clear();
#endif
}

void SceneSynchronizerDebugger::set_dump_enabled(bool p_dump_enabled) {
#ifdef DEBUG_ENABLED
	dump_enabled = p_dump_enabled;
#endif
}

bool SceneSynchronizerDebugger::get_dump_enabled() const {
#ifdef DEBUG_ENABLED
	return dump_enabled;
#else
	return false;
#endif
}

void SceneSynchronizerDebugger::register_class_for_node_to_dump(Node *p_node) {
#ifdef DEBUG_ENABLED
	register_class_to_dump(p_node->get_class_name());
	track_node(p_node, false);
#endif
}

void SceneSynchronizerDebugger::register_class_to_dump(const StringName &p_class) {
#ifdef DEBUG_ENABLED
	ERR_FAIL_COND(p_class == StringName());

	if (dump_classes.find(p_class) == -1) {
		dump_classes.push_back(p_class);
	}
#endif
}

void SceneSynchronizerDebugger::unregister_class_to_dump(const StringName &p_class) {
#ifdef DEBUG_ENABLED
	const int64_t index = dump_classes.find(p_class);
	if (index >= 0) {
		dump_classes.remove_unordered(index);
	}
#endif
}

void SceneSynchronizerDebugger::setup_debugger(const String &p_dump_name, int p_peer, SceneTree *p_scene_tree) {
#ifdef DEBUG_ENABLED
	if (setup_done == false) {
		setup_done = true;

		// Setup `dump_enabled`
		if (!dump_enabled) {
			dump_enabled = GLOBAL_GET("NetworkSynchronizer/debugger/dump_enabled");
		}

		// Setup `dump_classes`.
		{
			Array classes = GLOBAL_GET("NetworkSynchronizer/debugger/dump_classes");
			for (uint32_t i = 0; i < uint32_t(classes.size()); i += 1) {
				if (classes[i].get_type() == Variant::STRING) {
					register_class_to_dump(classes[i]);
				}
			}
		}
	}

	// Setup directories.
	main_dump_directory_path = OS::get_singleton()->get_executable_path().get_base_dir() + "/net-sync-debugs/dump";
	dump_name = p_dump_name;

	prepare_dumping(p_peer, p_scene_tree);
	setup_debugger_python_ui();
#endif
}

void SceneSynchronizerDebugger::prepare_dumping(int p_peer, SceneTree *p_scene_tree) {
#ifdef DEBUG_ENABLED
	if (!dump_enabled) {
		// Dumping is disabled, nothing to do.
		return;
	}

	// Prepare the dir.
	{
		String path = main_dump_directory_path + "/" + dump_name;

		DirAccess *dir = DirAccess::create_for_path(path);

		Error e;
		e = dir->make_dir_recursive(path);

		if (e != OK) {
			memdelete(dir);
			ERR_FAIL_COND(e != OK);
		}

		e = dir->change_dir(path);

		if (e != OK) {
			memdelete(dir);
			ERR_FAIL_COND(e != OK);
		}

		// Empty the directory making sure we are ready to write.
		dir->erase_contents_recursive();
		//if (dir->list_dir_begin() == OK) {
		//	for (String name = dir->get_next(); name != String(); name = dir->get_next()) {
		//		if (name.begins_with("fd-" + itos(p_peer) + "-")) {
		//			dir->remove(name);
		//		}
		//	}
		//	dir->list_dir_end();
		//}

		// Dispose the pointer.
		memdelete(dir);
	}

	// Store generic info about this dump.
	{
		Error e;
		FileAccess *file = FileAccess::open(main_dump_directory_path + "/" + "dump-info-" + dump_name + /*"-" + itos(p_peer) +*/ ".json", FileAccess::WRITE, &e);

		ERR_FAIL_COND(e != OK);

		OS::Date date = OS::get_singleton()->get_date();
		OS::Time time = OS::get_singleton()->get_time();

		Dictionary d;
		d["dump-name"] = dump_name;
		d["peer"] = p_peer;
		d["date"] = itos(date.day) + "/" + itos(date.month) + "/" + itos(date.year);
		d["time"] = itos(time.hour) + "::" + itos(time.min);

		file->flush();
		file->store_string(JSON::print(d));
		file->close();

		memdelete(file);
	}

	if (scene_tree) {
		scene_tree->disconnect("node_added", this, "on_node_added");
		scene_tree->disconnect("node_removed", this, "on_node_removed");
	}

	tracked_nodes.clear();
	classes_property_lists.clear();
	scene_tree = p_scene_tree;

	if (scene_tree) {
		scene_tree->connect("node_added", this, "on_node_added");
		scene_tree->connect("node_removed", this, "on_node_removed");

		// Start by tracking the existing node.
		track_node(scene_tree->get_root(), true);
	}
#endif
}

void SceneSynchronizerDebugger::setup_debugger_python_ui() {
#ifdef DEBUG_ENABLED

	DirAccess *dir = DirAccess::create_for_path(main_dump_directory_path);

	Error e;
	e = dir->make_dir_recursive(main_dump_directory_path);

	memdelete(dir);

	ERR_FAIL_COND_MSG(e != OK, "Can't create the `" + main_dump_directory_path + "` directory.");

	// Verify if file exists.
	const String path = main_dump_directory_path + "/debugger.py";

	if (FileAccess::exists(path)) {
		// Nothing to do.
		return;
	}

	// Copy the python UI into the directory.
	FileAccess *f = FileAccess::open(path, FileAccess::WRITE);
	ERR_FAIL_COND_MSG(!f, "Can't create the `" + path + "` file.");

	f->store_buffer((uint8_t *)__debugger_ui_code, __debugger_ui_code_size);

	f->close();
	memdelete(f);
#endif
}

void SceneSynchronizerDebugger::track_node(Node *p_node, bool p_recursive) {
#ifdef DEBUG_ENABLED
	if (tracked_nodes.find(p_node) == -1) {
		const bool is_tracked = dump_classes.find(p_node->get_class_name()) != -1;
		if (is_tracked) {
			// Verify if the property list already exists.
			if (!classes_property_lists.has(p_node->get_class_name())) {
				// Property list not yet cached, fetch it now.
				classes_property_lists.insert(p_node->get_class_name(), List<PropertyInfo>());
				List<PropertyInfo> *properties = classes_property_lists.lookup_ptr(p_node->get_class_name());

				p_node->get_property_list(properties);
			}

			List<PropertyInfo> *properties = classes_property_lists.lookup_ptr(p_node->get_class_name());

			// Can't happen, as it was just created.
			CRASH_COND(properties == nullptr);

			// Assign the property list pointer for fast access.
			tracked_nodes.push_back(TrackedNode(p_node, properties));
		}
	}

	if (p_recursive) {
		for (int i = 0; i < p_node->get_child_count(); i += 1) {
			Node *child = p_node->get_child(i);
			track_node(child, true);
		}
	}
#endif
}

void SceneSynchronizerDebugger::on_node_added(Node *p_node) {
#ifdef DEBUG_ENABLED
	track_node(p_node, false);
#endif
}

void SceneSynchronizerDebugger::on_node_removed(Node *p_node) {
#ifdef DEBUG_ENABLED
	const int64_t index = tracked_nodes.find(p_node);
	if (index != -1) {
		tracked_nodes.remove_unordered(index);
	}
#endif
}

void SceneSynchronizerDebugger::write_dump(int p_peer, uint32_t p_frame_index) {
#ifdef DEBUG_ENABLED
	if (!dump_enabled) {
		return;
	}

	if (p_frame_index == UINT32_MAX) {
		// Nothing to write.
		return;
	}

	FileAccess *file = nullptr;
	{
		String file_path = "";

		int iteration = 0;
		String iteration_mark = "";
		do {
			file_path = main_dump_directory_path + "/" + dump_name + "/fd-" /*+ itos(p_peer) + "-"*/ + itos(p_frame_index) + iteration_mark + ".json";
			iteration_mark += "@";
			iteration += 1;
		} while (FileAccess::exists(file_path) && iteration < 100);

		Error e;
		file = FileAccess::open(file_path, FileAccess::WRITE, &e);
		ERR_FAIL_COND(e != OK);
	}

	String frame_summary;

	if (frame_dump__has_warnings) {
		frame_summary += "* ";
	} else if (frame_dump__has_errors) {
		frame_summary += "!️ ";
	}

	if ((frame_dump__frame_events & FrameEvent::CLIENT_DESYNC_DETECTED) > 0) {
		frame_summary += "Client desync; ";

	} else if ((frame_dump__frame_events & FrameEvent::CLIENT_DESYNC_DETECTED_SOFT) > 0) {
		frame_summary += "Client desync; No controller rewind; ";
	}

	Dictionary d;
	d["frame"] = itos(p_frame_index);
	d["peer"] = itos(p_peer);
	d["frame_summary"] = frame_summary;
	d["begin_state"] = frame_dump__begin_state;
	d["end_state"] = frame_dump__end_state;
	d["node_log"] = frame_dump__node_log;
	d["data_buffer_writes"] = frame_dump__data_buffer_writes;
	d["data_buffer_reads"] = frame_dump__data_buffer_reads;
	d["are_inputs_different_results"] = frame_dump__are_inputs_different_results;

	file->store_string(JSON::print(d));
	file->close();

	memdelete(file);
#endif
}

void SceneSynchronizerDebugger::start_new_frame() {
#ifdef DEBUG_ENABLED
	frame_dump__node_log.clear();
	frame_dump__frame_events = FrameEvent::EMPTY;
	frame_dump__has_warnings = false;
	frame_dump__has_errors = false;
	frame_dump__data_buffer_writes.clear();
	frame_dump__data_buffer_reads.clear();
	frame_dump__are_inputs_different_results.clear();
	log_counter = 0;
#endif
}

#ifdef DEBUG_ENABLED
String type_to_string(Variant::Type p_type) {
	switch (p_type) {
		case Variant::NIL:
			return "NIL";

		case Variant::BOOL:
			return "BOOL";
		case Variant::INT:
			return "INT";
		case Variant::REAL:
			return "REAL";
		case Variant::STRING:
			return "STRING";

		case Variant::RECT2:
			return "RECT2";
		case Variant::RECT2I:
			return "RECT2I";

		case Variant::VECTOR2:
			return "VECTOR2";
		case Variant::VECTOR2I:
			return "VECTOR2I";
		case Variant::VECTOR3:
			return "VECTOR3";
		case Variant::VECTOR3I:
			return "VECTOR3I";
		case Variant::VECTOR4:
			return "VECTOR4";
		case Variant::VECTOR4I:
			return "VECTOR4I";

		case Variant::PLANE:
			return "PLANE";
		case Variant::QUATERNION:
			return "QUATERNION";
		case Variant::AABB:
			return "AABB";
		case Variant::BASIS:
			return "BASIS";
		case Variant::TRANSFORM:
			return "TRANSFORM";
		case Variant::TRANSFORM2D:
			return "TRANSFORM2D";
		case Variant::PROJECTION:
			return "PROJECTION";

		case Variant::COLOR:
			return "COLOR";
		case Variant::NODE_PATH:
			return "NODE_PATH";
		case Variant::RID:
			return "RID";
		case Variant::OBJECT:
			return "OBJECT";
		case Variant::STRING_NAME:
			return "STRING_NAME";
		case Variant::DICTIONARY:
			return "DICTIONARY";
		case Variant::ARRAY:
			return "ARRAY";

		case Variant::POOL_BYTE_ARRAY:
			return "POOL_BYTE_ARRAY";
		case Variant::POOL_INT_ARRAY:
			return "POOL_INT_ARRAY";
		case Variant::POOL_REAL_ARRAY:
			return "POOL_REAL_ARRAY";
		case Variant::POOL_STRING_ARRAY:
			return "POOL_STRING_ARRAY";
		case Variant::POOL_VECTOR2_ARRAY:
			return "POOL_VECTOR2_ARRAY";
		case Variant::POOL_VECTOR2I_ARRAY:
			return "POOL_VECTOR2I_ARRAY";
		case Variant::POOL_VECTOR3_ARRAY:
			return "POOL_VECTOR3_ARRAY";
		case Variant::POOL_VECTOR3I_ARRAY:
			return "POOL_VECTOR3I_ARRAY";
		case Variant::POOL_VECTOR4_ARRAY:
			return "POOL_VECTOR4_ARRAY";
		case Variant::POOL_VECTOR4I_ARRAY:
			return "POOL_VECTOR4I_ARRAY";
		case Variant::POOL_COLOR_ARRAY:
			return "POOL_COLOR_ARRAY";

		case Variant::VARIANT_MAX:
			return "VARIANT_MAX";
	}
	return "";
}

String data_type_to_string(uint32_t p_type) {
	switch (p_type) {
		case DataBuffer::DATA_TYPE_BOOL:
			return "Bool";
		case DataBuffer::DATA_TYPE_INT:
			return "Int";
		case DataBuffer::DATA_TYPE_UINT:
			return "Uint";
		case DataBuffer::DATA_TYPE_REAL:
			return "Real";
		case DataBuffer::DATA_TYPE_POSITIVE_UNIT_REAL:
			return "Positive Unit Real";
		case DataBuffer::DATA_TYPE_UNIT_REAL:
			return "Unit Real";
		case DataBuffer::DATA_TYPE_VECTOR2:
			return "Vector2";
		case DataBuffer::DATA_TYPE_NORMALIZED_VECTOR2:
			return "Normalized Vector2";
		case DataBuffer::DATA_TYPE_VECTOR3:
			return "Vector3";
		case DataBuffer::DATA_TYPE_NORMALIZED_VECTOR3:
			return "Normalized Vector3";
		case DataBuffer::DATA_TYPE_VARIANT:
			return "Variant";
	}

	return "UNDEFINED";
}

String compression_level_to_string(uint32_t p_type) {
	switch (p_type) {
		case DataBuffer::COMPRESSION_LEVEL_0:
			return "Compression Level 0";
		case DataBuffer::COMPRESSION_LEVEL_1:
			return "Compression Level 1";
		case DataBuffer::COMPRESSION_LEVEL_2:
			return "Compression Level 2";
		case DataBuffer::COMPRESSION_LEVEL_3:
			return "Compression Level 3";
	}

	return "Compression Level UNDEFINED";
}
#endif

void SceneSynchronizerDebugger::scene_sync_process_start(const SceneSynchronizer *p_scene_sync) {
#ifdef DEBUG_ENABLED
	if (!dump_enabled) {
		return;
	}

	dump_tracked_objects(p_scene_sync, frame_dump__begin_state);
#endif
}

void SceneSynchronizerDebugger::scene_sync_process_end(const SceneSynchronizer *p_scene_sync) {
#ifdef DEBUG_ENABLED
	if (!dump_enabled) {
		return;
	}

	dump_tracked_objects(p_scene_sync, frame_dump__end_state);
#endif
}

void SceneSynchronizerDebugger::databuffer_operation_begin_record(Node *p_node, DataBufferDumpMode p_mode) {
#ifdef DEBUG_ENABLED
	if (!dump_enabled) {
		return;
	}

	frame_dump__data_buffer_name = p_node->get_path();
	frame_dump_data_buffer_dump_mode = p_mode;

	if (frame_dump_data_buffer_dump_mode == DataBufferDumpMode::WRITE) {
		add_node_message_by_path(frame_dump__data_buffer_name, "[WRITE] DataBuffer start write");
	} else {
		add_node_message_by_path(frame_dump__data_buffer_name, "[READ] DataBuffer start read");
	}
#endif
}

void SceneSynchronizerDebugger::databuffer_operation_end_record() {
#ifdef DEBUG_ENABLED
	if (!dump_enabled) {
		return;
	}

	if (frame_dump_data_buffer_dump_mode == DataBufferDumpMode::WRITE) {
		add_node_message_by_path(frame_dump__data_buffer_name, "[WRITE] end");
	} else {
		add_node_message_by_path(frame_dump__data_buffer_name, "[READ] end");
	}

	frame_dump_data_buffer_dump_mode = DataBufferDumpMode::NONE;
	frame_dump__data_buffer_name = "";
#endif
}

void SceneSynchronizerDebugger::databuffer_write(uint32_t p_data_type, uint32_t p_compression_level, Variant p_variable) {
#ifdef DEBUG_ENABLED
	if (!dump_enabled) {
		return;
	}

	if (frame_dump_data_buffer_dump_mode != DataBufferDumpMode::WRITE) {
		return;
	}

	List<const void *> stack;
	String val_string = p_variable.stringify(stack);

	frame_dump__data_buffer_writes.push_back(val_string);

	const String operation = "[WRITE]      [" + compression_level_to_string(p_compression_level) + "] [" + data_type_to_string(p_data_type) + "] " + val_string;

	add_node_message_by_path(frame_dump__data_buffer_name, operation);
#endif
}

void SceneSynchronizerDebugger::databuffer_read(uint32_t p_data_type, uint32_t p_compression_level, Variant p_variable) {
#ifdef DEBUG_ENABLED
	if (!dump_enabled) {
		return;
	}

	if (frame_dump_data_buffer_dump_mode != DataBufferDumpMode::READ) {
		return;
	}

	List<const void *> stack;
	String val_string = p_variable.stringify(stack);

	frame_dump__data_buffer_reads.push_back(val_string);

	const String operation = "[READ]     [" + compression_level_to_string(p_compression_level) + "] [" + data_type_to_string(p_data_type) + "] " + val_string;

	add_node_message_by_path(frame_dump__data_buffer_name, operation);
#endif
}

void SceneSynchronizerDebugger::notify_input_sent_to_server(Node *p_node, uint32_t p_frame_index, uint32_t p_input_index) {
#ifdef DEBUG_ENABLED
	debug_print(p_node, "The client sent to server the input `" + itos(p_input_index) + "` for frame:`" + itos(p_frame_index) + "`.", true);
#endif
}

void SceneSynchronizerDebugger::notify_are_inputs_different_result(
		Node *p_node,
		uint32_t p_other_frame_index,
		bool p_is_similar) {
#ifdef DEBUG_ENABLED
	if (p_is_similar) {
		debug_print(p_node, "This frame input is SIMILAR to `" + itos(p_other_frame_index) + "`", true);
	} else {
		debug_print(p_node, "This frame input is DIFFERENT to `" + itos(p_other_frame_index) + "`", true);
	}
	frame_dump__are_inputs_different_results[p_other_frame_index] = p_is_similar;
#endif
}

void SceneSynchronizerDebugger::add_node_message(Node *p_node, const String &p_message) {
#ifdef DEBUG_ENABLED
	if (p_node) {
		add_node_message_by_path(p_node->get_path(), p_message);
	} else {
		add_node_message_by_path("GLOBAL", p_message);
	}
#endif
}

void SceneSynchronizerDebugger::add_node_message_by_path(const String &p_node_path, const String &p_message) {
#ifdef DEBUG_ENABLED
	if (!dump_enabled) {
		return;
	}

	if (!frame_dump__node_log.has(p_node_path)) {
		frame_dump__node_log[p_node_path] = Array();
	}

	Variant *v = frame_dump__node_log.getptr(p_node_path);
	Array a = *v;

	Dictionary m;
	m["i"] = log_counter;
	m["m"] = p_message;
	a.append(m);
	log_counter += 1;
#endif
}

void SceneSynchronizerDebugger::debug_print(Node *p_node, const String &p_message, bool p_silent) {
#ifdef DEBUG_ENABLED
	if (!p_silent) {
		NET_DEBUG_PRINT(p_message);
	}
	add_node_message(p_node, "[INFO]    " + p_message);
#endif
}

void SceneSynchronizerDebugger::debug_warning(Node *p_node, const String &p_message, bool p_silent) {
#ifdef DEBUG_ENABLED
	if (!p_silent) {
		NET_DEBUG_WARN(p_message);
	}
	add_node_message(p_node, "[WARNING] " + p_message);
	frame_dump__has_warnings = true;
#endif
}

void SceneSynchronizerDebugger::debug_error(Node *p_node, const String &p_message, bool p_silent) {
#ifdef DEBUG_ENABLED
	if (!p_silent) {
		NET_DEBUG_ERR(p_message);
	}
	add_node_message(p_node, "[ERROR]   " + p_message);
	frame_dump__has_errors = true;
#endif
}

void SceneSynchronizerDebugger::notify_event(FrameEvent p_event) {
#ifdef DEBUG_ENABLED
	if (!dump_enabled) {
		return;
	}

	frame_dump__frame_events = FrameEvent(frame_dump__frame_events | p_event);
#endif
}

void SceneSynchronizerDebugger::dump_tracked_objects(const SceneSynchronizer *p_scene_sync, Dictionary &p_dump) {
#ifdef DEBUG_ENABLED
	p_dump.clear();

	List<const void *> stack;

	for (uint32_t i = 0; i < tracked_nodes.size(); i += 1) {
		Dictionary node_dump;

		String node_path = tracked_nodes[i].node->get_path();
		node_dump["node_path"] = node_path;

		for (List<PropertyInfo>::Element *e = tracked_nodes[i].properties->front(); e; e = e->next()) {
			String prefix;
			if (p_scene_sync->is_variable_registered(tracked_nodes[i].node, e->get().name)) {
				prefix = "* ";
			}

			node_dump[prefix + e->get().name + "::" + type_to_string(e->get().type)] = tracked_nodes[i].node->get(e->get().name).stringify(stack);
		}

		p_dump[node_path] = node_dump;
	}
#endif
}