Skip to content

Commit 1bd79bf

Browse files
committed
Allow creating GDExtension plugins from inside the Godot editor
1 parent d09d82d commit 1bd79bf

40 files changed

+1685
-5
lines changed

.pre-commit-config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ repos:
157157
exclude: |
158158
(?x)^(
159159
core/math/bvh_.*\.inc$|
160+
editor/plugins/gdextension/cpp_scons/template/*|
160161
platform/(?!android|ios|linuxbsd|macos|web|windows)\w+/.*|
161162
platform/android/java/editor/src/main/java/com/android/.*|
162163
platform/android/java/lib/src/com/.*|

editor/editor_node.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@
146146
#include "editor/plugins/editor_preview_plugins.h"
147147
#include "editor/plugins/editor_resource_conversion_plugin.h"
148148
#include "editor/plugins/game_view_plugin.h"
149-
#include "editor/plugins/gdextension_export_plugin.h"
149+
#include "editor/plugins/gdextension/gdextension_export_plugin.h"
150150
#include "editor/plugins/material_editor_plugin.h"
151151
#include "editor/plugins/mesh_library_editor_plugin.h"
152152
#include "editor/plugins/node_3d_editor_plugin.h"

editor/gui/editor_validation_panel.cpp

+5-1
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ void EditorValidationPanel::add_line(int p_id, const String &p_valid_message) {
6464
ERR_FAIL_COND(valid_messages.has(p_id));
6565

6666
Label *label = memnew(Label);
67-
message_container->add_child(label);
6867
label->set_custom_minimum_size(Size2(200 * EDSCALE, 0));
6968
label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
7069
label->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
70+
message_container->add_child(label);
7171

7272
valid_messages[p_id] = p_valid_message;
7373
labels[p_id] = label;
@@ -124,6 +124,10 @@ void EditorValidationPanel::set_message(int p_id, const String &p_text, MessageT
124124
}
125125
}
126126

127+
int EditorValidationPanel::get_message_count() const {
128+
return valid_messages.size();
129+
}
130+
127131
bool EditorValidationPanel::is_valid() const {
128132
return valid;
129133
}

editor/gui/editor_validation_panel.h

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class EditorValidationPanel : public PanelContainer {
8080

8181
void update();
8282
void set_message(int p_id, const String &p_text, MessageType p_type, bool p_auto_prefix = true);
83+
int get_message_count() const;
8384
bool is_valid() const;
8485

8586
EditorValidationPanel();

editor/plugins/SCsub

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ Import("env")
55

66
env.add_source_files(env.editor_sources, "*.cpp")
77

8+
SConscript("gdextension/SCsub")
89
SConscript("gizmos/SCsub")
910
SConscript("tiles/SCsub")

editor/plugins/gdextension/SCsub

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env python
2+
3+
Import("env")
4+
5+
env.add_source_files(env.editor_sources, "*.cpp")
6+
7+
SConscript("cpp_scons/SCsub")
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env python
2+
3+
import os
4+
5+
Import("env")
6+
7+
env.add_source_files(env.editor_sources, "*.cpp")
8+
9+
10+
def parse_template(source):
11+
with open(source) as file:
12+
lines = file.readlines()
13+
script_template = ""
14+
for line in lines:
15+
script_template += line
16+
if env["precision"] != "double":
17+
script_template = script_template.replace('ARGUMENTS["precision"] = "double"', "")
18+
name = os.path.basename(source).upper().replace(".", "_")
19+
return "\nconst String " + name + ' = R"(' + script_template.rstrip() + ')";\n'
20+
21+
22+
def make_templates(target, source, env):
23+
dst = str(target[0])
24+
with StringIO() as s:
25+
s.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n\n")
26+
s.write("#ifndef GDEXTENSION_TEMPLATE_FILES_GEN_H\n")
27+
s.write("#define GDEXTENSION_TEMPLATE_FILES_GEN_H\n\n")
28+
s.write('#include "core/string/ustring.h"\n')
29+
parsed_template_string = ""
30+
for file in source:
31+
filepath = str(file)
32+
if os.path.isfile(filepath):
33+
parsed_template_string += parse_template(filepath)
34+
s.write(parsed_template_string)
35+
s.write("\n#endif // GDEXTENSION_TEMPLATE_FILES_GEN_H\n")
36+
with open(dst, "w", encoding="utf-8", newline="\n") as f:
37+
f.write(s.getvalue())
38+
39+
40+
env["BUILDERS"]["MakeGDExtTemplateBuilder"] = Builder(
41+
action=env.Run(make_templates),
42+
suffix=".h",
43+
)
44+
45+
# Template files
46+
templates_sources = Glob("template/*") + Glob("template/*/*") + Glob("template/*/*/*")
47+
48+
dest_file = "gdextension_template_files.gen.h"
49+
env.Alias("editor_template_gdext", [env.MakeGDExtTemplateBuilder(dest_file, templates_sources)])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/**************************************************************************/
2+
/* cpp_scons_gdext_creator.cpp */
3+
/**************************************************************************/
4+
/* This file is part of: */
5+
/* GODOT ENGINE */
6+
/* https://godotengine.org */
7+
/**************************************************************************/
8+
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9+
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10+
/* */
11+
/* Permission is hereby granted, free of charge, to any person obtaining */
12+
/* a copy of this software and associated documentation files (the */
13+
/* "Software"), to deal in the Software without restriction, including */
14+
/* without limitation the rights to use, copy, modify, merge, publish, */
15+
/* distribute, sublicense, and/or sell copies of the Software, and to */
16+
/* permit persons to whom the Software is furnished to do so, subject to */
17+
/* the following conditions: */
18+
/* */
19+
/* The above copyright notice and this permission notice shall be */
20+
/* included in all copies or substantial portions of the Software. */
21+
/* */
22+
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23+
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24+
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25+
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26+
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27+
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28+
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29+
/**************************************************************************/
30+
31+
#include "cpp_scons_gdext_creator.h"
32+
33+
#include "core/core_bind.h"
34+
#include "core/io/dir_access.h"
35+
#include "core/string/string_builder.h"
36+
#include "core/version.h"
37+
#include "gdextension_template_files.gen.h"
38+
39+
#include "editor/editor_node.h"
40+
41+
void CppSconsGDExtensionCreator::_git_clone_godot_cpp(const String &p_parent_path, bool p_compile) {
42+
EditorProgress ep("Preparing GDExtension C++ plugin", "Preparing GDExtension C++ plugin", 3);
43+
List<String> args;
44+
args.push_back("clone");
45+
args.push_back("--single-branch");
46+
args.push_back("--branch");
47+
args.push_back(VERSION_BRANCH);
48+
args.push_back("https://github.com/godotengine/godot-cpp");
49+
const String godot_cpp_path = p_parent_path.trim_prefix("res://").path_join("godot-cpp");
50+
args.push_back(godot_cpp_path);
51+
ep.step(TTR("Cloning godot-cpp..."), 1);
52+
String output = "";
53+
int result = OS::get_singleton()->execute("git", args, &output);
54+
Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_RESOURCES);
55+
if (result != 0 || !dir->dir_exists(godot_cpp_path)) {
56+
args.get(3) = "master";
57+
output = "";
58+
result = OS::get_singleton()->execute("git", args, &output);
59+
}
60+
ERR_FAIL_COND_MSG(result != 0 || !dir->dir_exists(godot_cpp_path), "Failed to clone godot-cpp. Please clone godot-cpp manually in order to have a working GDExtension plugin.");
61+
if (p_compile) {
62+
ep.step(TTR("Performing initial compile... (this may take several minutes)"), 2);
63+
result = OS::get_singleton()->execute("scons", List<String>());
64+
ERR_FAIL_COND_MSG(result != 0, "Failed to compile godot-cpp. Please ensure SCons is installed, then run the `scons` command in your project.");
65+
}
66+
ep.step(TTR("Done!"), 3);
67+
}
68+
69+
String CppSconsGDExtensionCreator::_process_template(const String &p_contents) {
70+
String ret;
71+
if (strip_module_defines) {
72+
StringBuilder builder;
73+
bool keep = true;
74+
PackedStringArray lines = p_contents.split("\n");
75+
for (const String &line : lines) {
76+
if (line == "#if GDEXTENSION" || line == "#else") {
77+
continue;
78+
} else if (line == "#elif GODOT_MODULE") {
79+
keep = false;
80+
continue;
81+
} else if (line == "#endif") {
82+
keep = true;
83+
continue;
84+
}
85+
if (keep) {
86+
builder += line;
87+
builder += "\n";
88+
}
89+
}
90+
ret = builder.as_string();
91+
} else {
92+
ret = p_contents;
93+
}
94+
if (ClassDB::class_exists("ExampleNode")) {
95+
ret = ret.replace("ExampleNode", example_node_name);
96+
}
97+
ret = ret.replace("__BASE_NAME__", base_name);
98+
ret = ret.replace("__BASE_NAME_UPPER__", base_name.to_upper());
99+
ret = ret.replace("__LIBRARY_NAME__", library_name);
100+
ret = ret.replace("__LIBRARY_NAME_UPPER__", library_name.to_upper());
101+
ret = ret.replace("__GODOT_VERSION__", VERSION_BRANCH);
102+
ret = ret.replace("__BASE_PATH__", res_path.trim_prefix("res://"));
103+
ret = ret.replace("__UPDIR_DOTS__", updir_dots);
104+
return ret;
105+
}
106+
107+
void CppSconsGDExtensionCreator::_write_file(const String &p_file_path, const String &p_contents) {
108+
Error err;
109+
Ref<FileAccess> file = FileAccess::open(p_file_path, FileAccess::WRITE, &err);
110+
ERR_FAIL_COND_MSG(err != OK, "Couldn't write file at path: " + p_file_path + ".");
111+
file->store_string(_process_template(p_contents));
112+
file->close();
113+
}
114+
115+
void CppSconsGDExtensionCreator::_ensure_file_contains(const String &p_file_path, const String &p_new_contents) {
116+
Error err;
117+
Ref<FileAccess> file = FileAccess::open(p_file_path, FileAccess::READ_WRITE, &err);
118+
if (err != OK) {
119+
_write_file(p_file_path, p_new_contents);
120+
return;
121+
}
122+
String new_contents = _process_template(p_new_contents);
123+
String existing_contents = file->get_as_text();
124+
if (existing_contents.is_empty()) {
125+
file->store_string(new_contents);
126+
} else {
127+
file->seek_end();
128+
PackedStringArray lines = new_contents.split("\n", false);
129+
for (const String &line : lines) {
130+
if (!existing_contents.contains(line)) {
131+
file->store_string(line + "\n");
132+
}
133+
}
134+
}
135+
file->close();
136+
}
137+
138+
void CppSconsGDExtensionCreator::_write_common_files_and_dirs() {
139+
DirAccess::make_dir_recursive_absolute(res_path.path_join("doc_classes"));
140+
DirAccess::make_dir_recursive_absolute(res_path.path_join("icons"));
141+
DirAccess::make_dir_recursive_absolute(res_path.path_join("src"));
142+
_ensure_file_contains("res://SConstruct", SCONSTRUCT_TOP_LEVEL);
143+
_write_file(res_path.path_join("doc_classes/" + example_node_name + ".xml"), EXAMPLENODE_XML);
144+
_write_file(res_path.path_join("icons/" + example_node_name + ".svg"), EXAMPLENODE_SVG);
145+
_write_file(res_path.path_join("icons/" + example_node_name + ".svg.import"), EXAMPLENODE_SVG_IMPORT);
146+
_write_file(res_path.path_join("src/.gdignore"), "");
147+
_write_file(res_path.path_join(".gitignore"), GDEXT_GITIGNORE + "\n*.obj");
148+
_write_file(res_path.path_join(library_name + ".gdextension"), LIBRARY_NAME_GDEXTENSION);
149+
}
150+
151+
void CppSconsGDExtensionCreator::_write_gdext_only_files() {
152+
_ensure_file_contains("res://.gitignore", "*.dblite");
153+
_write_file(res_path.path_join("src/example_node.cpp"), EXAMPLE_NODE_CPP);
154+
_write_file(res_path.path_join("src/example_node.h"), EXAMPLE_NODE_H);
155+
_write_file(res_path.path_join("src/register_types.cpp"), REGISTER_TYPES_CPP);
156+
_write_file(res_path.path_join("src/register_types.h"), REGISTER_TYPES_H);
157+
_write_file(res_path.path_join("src/" + library_name + "_defines.h"), GDEXT_DEFINES_H);
158+
_write_file(res_path.path_join("src/initialize_gdextension.cpp"), INITIALIZE_GDEXTENSION_CPP.replace("#include \"__UPDIR_DOTS__/../", "#include \""));
159+
_write_file(res_path.path_join("SConstruct"), SCONSTRUCT_ADDON.replace(" + Glob(\"__UPDIR_DOTS__/*.cpp\")", "").replace(",__UPDIR_DOTS__/", "").replace("__UPDIR_DOTS__/editor", "src/editor"));
160+
}
161+
162+
void CppSconsGDExtensionCreator::_write_gdext_module_files() {
163+
_ensure_file_contains("res://.gitignore", GDEXT_GITIGNORE);
164+
DirAccess::make_dir_recursive_absolute("res://tests");
165+
_write_file("res://SCsub", SCSUB);
166+
_write_file("res://config.py", CONFIG_PY);
167+
_write_file("res://example_node.cpp", EXAMPLE_NODE_CPP);
168+
_write_file("res://example_node.h", EXAMPLE_NODE_H);
169+
_write_file("res://register_types.cpp", REGISTER_TYPES_CPP);
170+
_write_file("res://register_types.h", REGISTER_TYPES_H);
171+
_write_file("res://" + library_name + "_defines.h", SHARED_DEFINES_H);
172+
_write_file("res://tests/test_" + base_name + ".h", TEST_BASE_NAME_H);
173+
_write_file("res://tests/test_example_node.h", TEST_EXAMPLE_NODE_H);
174+
_write_file(res_path.path_join("src/initialize_gdextension.cpp"), INITIALIZE_GDEXTENSION_CPP);
175+
_write_file(res_path.path_join("SConstruct"), SCONSTRUCT_ADDON);
176+
}
177+
178+
void CppSconsGDExtensionCreator::create_gdextension(const String &p_path, const String &p_base_name, const String &p_library_name, int p_variation_index, bool p_compile) {
179+
res_path = p_path;
180+
base_name = p_base_name;
181+
library_name = p_library_name;
182+
updir_dots = String("../").repeat(p_path.count("/", 6)) + "..";
183+
strip_module_defines = p_variation_index == LANG_VAR_GDEXT_ONLY;
184+
if (ClassDB::class_exists("ExampleNode")) {
185+
int discriminator = 2;
186+
example_node_name = "ExampleNode2";
187+
while (ClassDB::class_exists(example_node_name)) {
188+
discriminator++;
189+
example_node_name = "ExampleNode" + itos(discriminator);
190+
}
191+
}
192+
_write_common_files_and_dirs();
193+
if (p_variation_index == LANG_VAR_GDEXT_ONLY) {
194+
_write_gdext_only_files();
195+
} else {
196+
_write_gdext_module_files();
197+
}
198+
if (does_git_exist) {
199+
_git_clone_godot_cpp(p_path.path_join("src"), p_compile);
200+
}
201+
}
202+
203+
void CppSconsGDExtensionCreator::setup_creator() {
204+
// Check for Git and SCons.
205+
List<String> args;
206+
args.push_back("--version");
207+
String output;
208+
OS::get_singleton()->execute("git", args, &output);
209+
if (output.is_empty()) {
210+
does_git_exist = false;
211+
} else {
212+
does_git_exist = true;
213+
output = "";
214+
OS::get_singleton()->execute("scons", args, &output);
215+
does_scons_exist = !output.is_empty();
216+
}
217+
}
218+
219+
PackedStringArray CppSconsGDExtensionCreator::get_language_variations() const {
220+
PackedStringArray variants;
221+
// Keep this in sync with enum LanguageVariation.
222+
variants.push_back("C++ with SCons, GDExtension only");
223+
variants.push_back("C++ with SCons, GDExtension and engine module");
224+
return variants;
225+
}
226+
227+
Dictionary CppSconsGDExtensionCreator::get_validation_messages(const String &p_path, const String &p_base_name, const String &p_library_name, int p_variation_index, bool p_compile) {
228+
Dictionary messages;
229+
// Check for Git and SCons.
230+
MessageType compile_consequence = p_compile ? MSG_ERROR : MSG_WARNING;
231+
if (does_git_exist) {
232+
if (does_scons_exist) {
233+
#ifdef WINDOWS_ENABLED
234+
messages[TTR("Both Git and SCons were found. You also need a C++17-compatible compiler, such as GCC, Clang/LLVM, or MSVC from Visual Studio.")] = MSG_OK;
235+
#else
236+
messages[TTR("Both Git and SCons were found. You also need a C++17-compatible compiler, such as GCC or Clang/LLVM.")] = MSG_OK;
237+
#endif
238+
} else {
239+
messages[TTR("Cannot compile now, SCons was not found.")] = compile_consequence;
240+
}
241+
} else {
242+
messages[TTR("Cannot compile now, Git was not found.")] = compile_consequence;
243+
}
244+
// Check for existing engine module.
245+
if (p_variation_index == LANG_VAR_GDEXT_MODULE) {
246+
Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_RESOURCES);
247+
if (dir->file_exists("SCsub")) {
248+
messages[TTR("This project already contains a C++ engine module.")] = MSG_ERROR;
249+
} else {
250+
messages[TTR("Able to create engine module in this Godot project.")] = MSG_OK;
251+
messages[TTR("Warning: This will turn the root of your project into an engine module!")] = MSG_WARNING;
252+
}
253+
}
254+
return messages;
255+
}

0 commit comments

Comments
 (0)