Skip to content

Commit cba88ba

Browse files
committed
GDScript: Implement @if_features annotation
1 parent 1b7b009 commit cba88ba

11 files changed

+375
-10
lines changed

modules/gdscript/doc_classes/@GDScript.xml

+11
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,17 @@
730730
[b]Note:[/b] Unlike most other annotations, the argument of the [annotation @icon] annotation must be a string literal (constant expressions are not supported).
731731
</description>
732732
</annotation>
733+
<annotation name="@if_features" qualifiers="vararg">
734+
<return type="void" />
735+
<param index="0" name="feature" type="String" default="null" />
736+
<description>
737+
Marks the following function to be taken into account only if all the features passed as arguments are declared by the platform or export preset.
738+
This is meant to be applied to a set of functions with the same name so at runtime you have a single implementation of some feature according to the features supported.
739+
If the annotation has no features specified, it's considered a default one as so it's only used if none of the other functions with the same name are proper fits.
740+
At runtime from an editor build, the features are checked at the moment the script is parsed, against what the OS advertises.
741+
At export time, the features are checked against the target platform's OS as well as the list of custom features specified in an export preset. Moreover, [b]the functions not matching the features, are removed from the source code[/b] so the exported project won't contain them at all.
742+
</description>
743+
</annotation>
733744
<annotation name="@onready">
734745
<return type="void" />
735746
<description>

modules/gdscript/gdscript_cache.cpp

+11-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ String GDScriptParserRef::get_path() const {
4646
return path;
4747
}
4848

49+
void GDScriptParserRef::set_path(const String &p_path) {
50+
path = p_path;
51+
}
52+
4953
uint32_t GDScriptParserRef::get_source_hash() const {
5054
return source_hash;
5155
}
@@ -134,9 +138,15 @@ void GDScriptParserRef::clear() {
134138
}
135139

136140
GDScriptParserRef::~GDScriptParserRef() {
141+
#ifdef TOOLS_ENABLED
142+
bool remove_from_map = !parser || !parser->is_for_export();
143+
#else
144+
bool remove_from_map = true;
145+
#endif
146+
137147
clear();
138148

139-
if (!abandoned) {
149+
if (remove_from_map && !abandoned) {
140150
MutexLock lock(GDScriptCache::singleton->mutex);
141151
GDScriptCache::singleton->parser_map.erase(path);
142152
}

modules/gdscript/gdscript_cache.h

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class GDScriptParserRef : public RefCounted {
6767
public:
6868
Status get_status() const;
6969
String get_path() const;
70+
void set_path(const String &p_path);
7071
uint32_t get_source_hash() const;
7172
GDScriptParser *get_parser();
7273
GDScriptAnalyzer *get_analyzer();

modules/gdscript/gdscript_compiler.cpp

+8
Original file line numberDiff line numberDiff line change
@@ -2947,6 +2947,14 @@ Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser:
29472947
const GDScriptParser::ClassNode::Member &member = p_class->members[i];
29482948
if (member.type == member.FUNCTION) {
29492949
const GDScriptParser::FunctionNode *function = member.function;
2950+
#ifdef TOOLS_ENABLED
2951+
if (!Engine::get_singleton()->is_editor_hint()) {
2952+
// Runtime in editor build. Ignore @if_features-decorated function if unfitting.
2953+
if (function->if_features.potential_candidate_index != -1) {
2954+
continue;
2955+
}
2956+
}
2957+
#endif
29502958
Error err = OK;
29512959
_parse_function(err, p_script, p_class, function);
29522960
if (err) {

modules/gdscript/gdscript_editor.cpp

+21
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "gdscript_utility_functions.h"
3737

3838
#ifdef TOOLS_ENABLED
39+
#include "editor/export/editor_export.h"
3940
#include "editor/gdscript_docgen.h"
4041
#include "editor/script_templates/templates.gen.h"
4142
#endif
@@ -982,6 +983,26 @@ static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_a
982983
r_result.insert(option.display, option);
983984
}
984985
}
986+
} else if (p_annotation->name == SNAME("@if_features")) {
987+
#ifdef TOOLS_ENABLED
988+
HashSet<String> features;
989+
for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {
990+
const Ref<EditorExportPreset> &preset = EditorExport::get_singleton()->get_export_preset(i);
991+
for (const String &feature : preset->get_custom_features().split(",", false)) {
992+
features.insert(feature.strip_edges());
993+
}
994+
List<String> platform_features;
995+
preset->get_platform()->get_platform_features(&platform_features);
996+
for (const String &feature : platform_features) {
997+
features.insert(feature.strip_edges());
998+
}
999+
}
1000+
for (const String &feature : features) {
1001+
ScriptLanguage::CodeCompletionOption option(feature, ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT);
1002+
option.insert_text = option.display.quote(p_quote_style);
1003+
r_result.insert(option.display, option);
1004+
}
1005+
#endif
9851006
}
9861007
}
9871008

modules/gdscript/gdscript_parser.cpp

+115
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ GDScriptParser::GDScriptParser() {
132132
register_annotation(MethodInfo("@warning_ignore_restore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::STANDALONE, &GDScriptParser::warning_ignore_region_annotations, varray(), true);
133133
// Networking.
134134
register_annotation(MethodInfo("@rpc", PropertyInfo(Variant::STRING, "mode"), PropertyInfo(Variant::STRING, "sync"), PropertyInfo(Variant::STRING, "transfer_mode"), PropertyInfo(Variant::INT, "transfer_channel")), AnnotationInfo::FUNCTION, &GDScriptParser::rpc_annotation, varray("authority", "call_remote", "unreliable", 0));
135+
// Preprocessing.
136+
register_annotation(MethodInfo("@if_features", PropertyInfo(Variant::STRING, "feature")), AnnotationInfo::FUNCTION, &GDScriptParser::if_features_annotation, varray(Variant()), true);
135137
}
136138

137139
#ifdef DEBUG_ENABLED
@@ -944,11 +946,22 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b
944946

945947
// Consume annotations.
946948
List<AnnotationNode *> annotations;
949+
#ifdef TOOLS_ENABLED
950+
constexpr bool parsing_function = std::is_same_v<T, FunctionNode>;
951+
AnnotationNode *if_features = nullptr;
952+
#endif
947953
while (!annotation_stack.is_empty()) {
948954
AnnotationNode *last_annotation = annotation_stack.back()->get();
949955
if (last_annotation->applies_to(p_target)) {
950956
annotations.push_front(last_annotation);
951957
annotation_stack.pop_back();
958+
#ifdef TOOLS_ENABLED
959+
if constexpr (parsing_function) {
960+
if (last_annotation->name == StringName("@if_features")) {
961+
if_features = last_annotation;
962+
}
963+
}
964+
#endif
952965
} else {
953966
push_error(vformat(R"(Annotation "%s" cannot be applied to a %s.)", last_annotation->name, p_member_kind));
954967
clear_unused_annotations();
@@ -994,6 +1007,19 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b
9941007
}
9951008

9961009
min_member_doc_line = member->end_line + 1; // Prevent multiple members from using the same doc comment.
1010+
1011+
if constexpr (parsing_function) {
1012+
if (if_features) {
1013+
// Mark this one as a default implementation if the annotation provides no features.
1014+
member->if_features.is_default_impl = if_features->arguments.is_empty();
1015+
// Let the first default one with of same name fully in.
1016+
// The others are added so they are parsed, but not indexed so name clash error is avoided.
1017+
if (!member->if_features.is_default_impl || current_class->members_indices.has(member->identifier->name)) {
1018+
current_class->add_if_features_potential_candidate(member);
1019+
return;
1020+
}
1021+
}
1022+
}
9971023
#endif // TOOLS_ENABLED
9981024

9991025
if (member->identifier != nullptr) {
@@ -4975,6 +5001,95 @@ bool GDScriptParser::warning_ignore_region_annotations(AnnotationNode *p_annotat
49755001
#endif // DEBUG_ENABLED
49765002
}
49775003

5004+
bool GDScriptParser::if_features_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
5005+
#if defined(TOOLS_ENABLED)
5006+
ERR_FAIL_COND_V_MSG(p_target->type != Node::FUNCTION, false, vformat(R"("%s" annotation can only be applied to functions.)", p_annotation->name));
5007+
5008+
FunctionNode *function = static_cast<FunctionNode *>(p_target);
5009+
if (function->if_features.used) {
5010+
push_error("The @if_features annotation can only be used once per function.");
5011+
return false;
5012+
}
5013+
5014+
function->if_features.used = true;
5015+
5016+
thread_local LocalVector<String> features;
5017+
features.clear();
5018+
for (const ExpressionNode *arg : p_annotation->arguments) {
5019+
DEV_ASSERT(arg->reduced);
5020+
if (arg->reduced_value.get_type() == Variant::STRING) {
5021+
features.push_back(arg->reduced_value);
5022+
} else {
5023+
push_error("The arguments to @if_features must be strings.");
5024+
return false;
5025+
}
5026+
}
5027+
5028+
if (Engine::get_singleton()->is_editor_hint() && !for_export) {
5029+
// At edit time there's nothing else to process.
5030+
return true;
5031+
}
5032+
5033+
// The idea is to keep the first one fitting, with only the default possibly overridden by another one coming later.
5034+
5035+
FunctionNode *target_function = static_cast<FunctionNode *>(p_target);
5036+
const StringName &function_name = target_function->identifier->name;
5037+
5038+
bool current_match_overridable = false;
5039+
if (target_function->if_features.potential_candidate_index != -1) { // Otherwise, it's already the chosen one when parsing the function.
5040+
if (p_class->has_function(function_name)) {
5041+
bool current_match_is_default = p_class->get_member(function_name).function->if_features.is_default_impl;
5042+
bool incoming_candidate_is_default = target_function->if_features.is_default_impl;
5043+
current_match_overridable = current_match_is_default && !incoming_candidate_is_default;
5044+
}
5045+
}
5046+
5047+
if (current_match_overridable) {
5048+
bool fitting = false;
5049+
if (features.size()) {
5050+
fitting = true;
5051+
for (const String &feature : features) {
5052+
if (for_export) {
5053+
// Export time in editor build.
5054+
if (!export_features.has(feature)) {
5055+
fitting = false;
5056+
break;
5057+
}
5058+
} else {
5059+
// Runtime in editor build.
5060+
if (!OS::get_singleton()->has_feature(feature)) {
5061+
fitting = false;
5062+
break;
5063+
}
5064+
}
5065+
}
5066+
}
5067+
if (fitting) {
5068+
// If fits, replace the current match.
5069+
int64_t current_match_index = p_class->members_indices[function_name];
5070+
p_class->members[current_match_index].function->if_features.potential_candidate_index = current_match_index;
5071+
p_class->members_indices[function_name] = target_function->if_features.potential_candidate_index;
5072+
target_function->if_features.potential_candidate_index = -1;
5073+
}
5074+
}
5075+
#endif
5076+
5077+
return true;
5078+
}
5079+
5080+
#ifdef TOOLS_ENABLED
5081+
void GDScriptParser::collect_unfitting_functions(ClassNode *p_class, LocalVector<Pair<ClassNode *, FunctionNode *>> &r_functions) {
5082+
for (const ClassNode::Member &member : p_class->members) {
5083+
if (member.type == ClassNode::Member::CLASS) {
5084+
collect_unfitting_functions(member.m_class, r_functions);
5085+
}
5086+
if (member.type == ClassNode::Member::FUNCTION && member.function->if_features.potential_candidate_index != -1) {
5087+
r_functions.push_back(Pair(p_class, member.function));
5088+
}
5089+
}
5090+
}
5091+
#endif
5092+
49785093
bool GDScriptParser::rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
49795094
ERR_FAIL_COND_V_MSG(p_target->type != Node::FUNCTION, false, vformat(R"("%s" annotation can only be applied to functions.)", p_annotation->name));
49805095

modules/gdscript/gdscript_parser.h

+26
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,13 @@ class GDScriptParser {
785785
members_indices[p_member_node->identifier->name] = members.size();
786786
members.push_back(Member(p_member_node));
787787
}
788+
#ifdef TOOLS_ENABLED
789+
template <typename T>
790+
void add_if_features_potential_candidate(T *p_member_node) {
791+
p_member_node->if_features.potential_candidate_index = members.size();
792+
members.push_back(Member(p_member_node));
793+
}
794+
#endif
788795
void add_member(const EnumNode::Value &p_enum_value) {
789796
members_indices[p_enum_value.identifier->name] = members.size();
790797
members.push_back(Member(p_enum_value));
@@ -862,6 +869,11 @@ class GDScriptParser {
862869
#ifdef TOOLS_ENABLED
863870
MemberDocData doc_data;
864871
int min_local_doc_line = 0;
872+
struct {
873+
bool used = false;
874+
bool is_default_impl = false;
875+
int potential_candidate_index = -1;
876+
} if_features;
865877
#endif // TOOLS_ENABLED
866878

867879
bool resolved_signature = false;
@@ -1335,6 +1347,10 @@ class GDScriptParser {
13351347
bool _is_tool = false;
13361348
String script_path;
13371349
bool for_completion = false;
1350+
#ifdef TOOLS_ENABLED
1351+
bool for_export = false;
1352+
HashSet<String> export_features;
1353+
#endif
13381354
bool parse_body = true;
13391355
bool panic_mode = false;
13401356
bool can_break = false;
@@ -1519,6 +1535,7 @@ class GDScriptParser {
15191535
bool warning_ignore_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15201536
bool warning_ignore_region_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15211537
bool rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
1538+
bool if_features_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15221539
// Statements.
15231540
Node *parse_statement();
15241541
VariableNode *parse_variable(bool p_is_static);
@@ -1590,6 +1607,15 @@ class GDScriptParser {
15901607
void get_annotation_list(List<MethodInfo> *r_annotations) const;
15911608
bool annotation_exists(const String &p_annotation_name) const;
15921609

1610+
#ifdef TOOLS_ENABLED
1611+
bool is_for_export() const { return for_export; }
1612+
void set_export_features(const HashSet<String> &p_features) {
1613+
for_export = true;
1614+
export_features = p_features;
1615+
}
1616+
void collect_unfitting_functions(ClassNode *p_class, LocalVector<Pair<ClassNode *, FunctionNode *>> &r_functions);
1617+
#endif
1618+
15931619
const List<ParserError> &get_errors() const { return errors; }
15941620
const List<String> get_dependencies() const {
15951621
// TODO: Keep track of deps.

0 commit comments

Comments
 (0)