From 7d0d80bbe3c84497dddea69b42ee7ef96e0a2d21 Mon Sep 17 00:00:00 2001 From: David Snopek Date: Thu, 25 Jan 2024 08:54:58 -0600 Subject: [PATCH] Allow submitting documentation to the Godot editor --- gdextension/gdextension_interface.h | 25 ++++++++++++++ include/godot_cpp/godot.hpp | 8 +++++ src/godot.cpp | 52 +++++++++++++++++++++++++++++ test/SConstruct | 3 ++ test/doc_classes/Example.xml | 25 ++++++++++++++ test/project/project.godot | 2 +- tools/godotcpp.py | 46 ++++++++++++++++++++++++- 7 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 test/doc_classes/Example.xml diff --git a/gdextension/gdextension_interface.h b/gdextension/gdextension_interface.h index 60ec8d4175..c8a8dd75f5 100644 --- a/gdextension/gdextension_interface.h +++ b/gdextension/gdextension_interface.h @@ -2835,6 +2835,31 @@ typedef void (*GDExtensionInterfaceEditorAddPlugin)(GDExtensionConstStringNamePt */ typedef void (*GDExtensionInterfaceEditorRemovePlugin)(GDExtensionConstStringNamePtr p_class_name); +/** + * @name editor_help_load_xml_from_utf8_chars + * @since 4.3 + * + * Loads new XML-formatted documentation data in the editor. + * + * The provided pointer can be immediately freed once the function returns. + * + * @param p_data A pointer to an UTF-8 encoded C string (null terminated). + */ +typedef void (*GDExtensionsInterfaceEditorHelpLoadXmlFromUtf8Chars)(const char *p_data); + +/** + * @name editor_help_load_xml_from_utf8_chars_and_len + * @since 4.3 + * + * Loads new XML-formatted documentation data in the editor. + * + * The provided pointer can be immediately freed once the function returns. + * + * @param p_data A pointer to an UTF-8 encoded C string. + * @param p_size The number of bytes (not code units). + */ +typedef void (*GDExtensionsInterfaceEditorHelpLoadXmlFromUtf8CharsAndLen)(const char *p_data, GDExtensionInt p_size); + #ifdef __cplusplus } #endif diff --git a/include/godot_cpp/godot.hpp b/include/godot_cpp/godot.hpp index 5a6293027e..e0cf7edd1d 100644 --- a/include/godot_cpp/godot.hpp +++ b/include/godot_cpp/godot.hpp @@ -190,6 +190,14 @@ extern "C" GDExtensionInterfaceClassdbUnregisterExtensionClass gdextension_inter extern "C" GDExtensionInterfaceGetLibraryPath gdextension_interface_get_library_path; extern "C" GDExtensionInterfaceEditorAddPlugin gdextension_interface_editor_add_plugin; extern "C" GDExtensionInterfaceEditorRemovePlugin gdextension_interface_editor_remove_plugin; +extern "C" GDExtensionsInterfaceEditorHelpLoadXmlFromUtf8Chars gdextension_interface_editor_help_load_xml_from_utf8_chars; +extern "C" GDExtensionsInterfaceEditorHelpLoadXmlFromUtf8CharsAndLen gdextension_interface_editor_help_load_xml_from_utf8_chars_and_len; + +class DocDataRegistration { +public: + DocDataRegistration(const char *p_hash, int p_uncompressed_size, int p_compressed_size, const unsigned char *p_data); + +}; } // namespace internal diff --git a/src/godot.cpp b/src/godot.cpp index 8a031be23b..01977de40d 100644 --- a/src/godot.cpp +++ b/src/godot.cpp @@ -196,6 +196,38 @@ GDExtensionInterfaceClassdbUnregisterExtensionClass gdextension_interface_classd GDExtensionInterfaceGetLibraryPath gdextension_interface_get_library_path = nullptr; GDExtensionInterfaceEditorAddPlugin gdextension_interface_editor_add_plugin = nullptr; GDExtensionInterfaceEditorRemovePlugin gdextension_interface_editor_remove_plugin = nullptr; +GDExtensionsInterfaceEditorHelpLoadXmlFromUtf8Chars gdextension_interface_editor_help_load_xml_from_utf8_chars = nullptr; +GDExtensionsInterfaceEditorHelpLoadXmlFromUtf8CharsAndLen gdextension_interface_editor_help_load_xml_from_utf8_chars_and_len = nullptr; + +struct DocData { + const char *hash = nullptr; + int uncompressed_size = 0; + int compressed_size = 0; + const unsigned char *data = nullptr; + + inline bool is_valid() const { + return hash != nullptr && uncompressed_size > 0 && compressed_size > 0 && data != nullptr; + } + + void load_data() const; +}; + +static DocData &get_doc_data() { + static DocData doc_data; + return doc_data; +} + +DocDataRegistration::DocDataRegistration(const char *p_hash, int p_uncompressed_size, int p_compressed_size, const unsigned char *p_data) { + DocData &doc_data = get_doc_data(); + if (doc_data.is_valid()) { + printf("ERROR: Attempting to register documentation data when we already have some - discarding.\n"); + return; + } + doc_data.hash = p_hash; + doc_data.uncompressed_size = p_uncompressed_size; + doc_data.compressed_size = p_compressed_size; + doc_data.data = p_data; +} } // namespace internal @@ -436,6 +468,8 @@ GDExtensionBool GDExtensionBinding::init(GDExtensionInterfaceGetProcAddress p_ge LOAD_PROC_ADDRESS(get_library_path, GDExtensionInterfaceGetLibraryPath); LOAD_PROC_ADDRESS(editor_add_plugin, GDExtensionInterfaceEditorAddPlugin); LOAD_PROC_ADDRESS(editor_remove_plugin, GDExtensionInterfaceEditorRemovePlugin); + LOAD_PROC_ADDRESS(editor_help_load_xml_from_utf8_chars, GDExtensionsInterfaceEditorHelpLoadXmlFromUtf8Chars); + LOAD_PROC_ADDRESS(editor_help_load_xml_from_utf8_chars_and_len, GDExtensionsInterfaceEditorHelpLoadXmlFromUtf8CharsAndLen); r_initialization->initialize = initialize_level; r_initialization->deinitialize = deinitialize_level; @@ -465,6 +499,13 @@ void GDExtensionBinding::initialize_level(void *p_userdata, GDExtensionInitializ ClassDB::initialize(p_level); } level_initialized[p_level]++; + + if ((ModuleInitializationLevel)p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) { + const internal::DocData &doc_data = internal::get_doc_data(); + if (doc_data.is_valid()) { + doc_data.load_data(); + } + } } void GDExtensionBinding::deinitialize_level(void *p_userdata, GDExtensionInitializationLevel p_level) { @@ -531,4 +572,15 @@ GDExtensionBool GDExtensionBinding::InitObject::init() const { return GDExtensionBinding::init(get_proc_address, library, init_data, initialization); } +void internal::DocData::load_data() const { + PackedByteArray compressed; + compressed.resize(compressed_size); + memcpy(compressed.ptrw(), data, compressed_size); + + // FileAccess::COMPRESSION_DEFLATE = 1 + PackedByteArray decompressed = compressed.decompress(uncompressed_size, 1); + + internal::gdextension_interface_editor_help_load_xml_from_utf8_chars_and_len(reinterpret_cast(decompressed.ptr()), uncompressed_size); +} + } // namespace godot diff --git a/test/SConstruct b/test/SConstruct index 9c25917bb4..3d76592987 100644 --- a/test/SConstruct +++ b/test/SConstruct @@ -16,6 +16,9 @@ env = SConscript("../SConstruct") env.Append(CPPPATH=["src/"]) sources = Glob("src/*.cpp") +doc_data = env.GodotCPPDocData("src/doc_data.gen.cpp", source=Glob("doc_classes/*.xml")) +sources.append(doc_data) + if env["platform"] == "macos": library = env.SharedLibrary( "project/bin/libgdexample.{}.{}.framework/libgdexample.{}.{}".format( diff --git a/test/doc_classes/Example.xml b/test/doc_classes/Example.xml new file mode 100644 index 0000000000..2a28f2c983 --- /dev/null +++ b/test/doc_classes/Example.xml @@ -0,0 +1,25 @@ + + + + A test control defined in GDExtension. + + + A control used for the automated GDExtension tests. + + + + + + + + Tests a simple function call. + + + + + + + + + + diff --git a/test/project/project.godot b/test/project/project.godot index 4f51c07f60..df3dd70f74 100644 --- a/test/project/project.godot +++ b/test/project/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="GDExtension Test Project" run/main_scene="res://main.tscn" -config/features=PackedStringArray("4.2") +config/features=PackedStringArray("4.3") config/icon="res://icon.png" [native_extensions] diff --git a/tools/godotcpp.py b/tools/godotcpp.py index 13a57e96d3..daae350f1e 100644 --- a/tools/godotcpp.py +++ b/tools/godotcpp.py @@ -325,6 +325,49 @@ def options(opts, env): tool.options(opts) +def make_doc_source(target, source, env): + import zlib + + dst = str(target[0]) + g = open(dst, "w", encoding="utf-8") + buf = "" + docbegin = "" + docend = "" + for src in source: + src_path = str(src) + if not src_path.endswith(".xml"): + continue + with open(src_path, "r", encoding="utf-8") as f: + content = f.read() + buf += content + + buf = (docbegin + buf + docend).encode("utf-8") + decomp_size = len(buf) + + # Use maximum zlib compression level to further reduce file size + # (at the cost of initial build times). + buf = zlib.compress(buf, zlib.Z_BEST_COMPRESSION) + + g.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") + g.write("\n") + g.write('#include \n') + g.write("\n") + + g.write('static const char *_doc_data_hash = "' + str(hash(buf)) + '";\n') + g.write("static const int _doc_data_uncompressed_size = " + str(decomp_size) + ";\n") + g.write("static const int _doc_data_compressed_size = " + str(len(buf)) + ";\n") + g.write("static const unsigned char _doc_data_compressed[] = {\n") + for i in range(len(buf)): + g.write("\t" + str(buf[i]) + ",\n") + g.write("};\n") + g.write("\n") + + g.write("static godot::internal::DocDataRegistration _doc_data_registration(_doc_data_hash, _doc_data_uncompressed_size, _doc_data_compressed_size, _doc_data_compressed);\n") + g.write("\n") + + g.close() + + def generate(env): # Default num_jobs to local cpu count if not user specified. # SCons has a peculiarity where user-specified options won't be overridden @@ -451,7 +494,8 @@ def generate(env): # Builders env.Append( BUILDERS={ - "GodotCPPBindings": Builder(action=Action(scons_generate_bindings, "$GENCOMSTR"), emitter=scons_emit_files) + "GodotCPPBindings": Builder(action=Action(scons_generate_bindings, "$GENCOMSTR"), emitter=scons_emit_files), + "GodotCPPDocData": Builder(action=make_doc_source), } ) env.AddMethod(_godot_cpp, "GodotCPP")