diff options
Diffstat (limited to 'modules')
101 files changed, 2775 insertions, 705 deletions
diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index 32acad76aa..923b2fe30d 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -304,7 +304,7 @@ <return type="void" /> <param index="0" name="names" type="String" /> <description> - Export an [int] or [String] property as an enumerated list of options. If the property is an [int], then the index of the value is stored, in the same order the values are provided. You can add specific identifiers for allowed values using a colon. If the property is a [String], then the value is stored. + Export an [int] or [String] property as an enumerated list of options. If the property is an [int], then the index of the value is stored, in the same order the values are provided. You can add explicit values using a colon. If the property is a [String], then the value is stored. See also [constant PROPERTY_HINT_ENUM]. [codeblock] @export_enum("Warrior", "Magician", "Thief") var character_class: int @@ -357,6 +357,20 @@ [codeblock] @export_flags("Fire", "Water", "Earth", "Wind") var spell_elements = 0 [/codeblock] + You can add explicit values using a colon: + [codeblock] + @export_flags("Self:4", "Allies:8", "Foes:16") var spell_targets = 0 + [/codeblock] + You can also combine several flags: + [codeblock] + @export_flags("Self:4", "Allies:8", "Self and Allies:12", "Foes:16") + var spell_targets = 0 + [/codeblock] + [b]Note:[/b] A flag value must be at least [code]1[/code] and at most [code]2 ** 32 - 1[/code]. + [b]Note:[/b] Unlike [annotation @export_enum], the previous explicit value is not taken into account. In the following example, A is 16, B is 2, C is 4. + [codeblock] + @export_flags("A:16", "B", "C") var x + [/codeblock] </description> </annotation> <annotation name="@export_flags_2d_navigation"> @@ -565,14 +579,22 @@ </annotation> <annotation name="@rpc" qualifiers="vararg"> <return type="void" /> - <param index="0" name="mode" type="String" default="""" /> - <param index="1" name="sync" type="String" default="""" /> - <param index="2" name="transfer_mode" type="String" default="""" /> + <param index="0" name="mode" type="String" default=""authority"" /> + <param index="1" name="sync" type="String" default=""call_remote"" /> + <param index="2" name="transfer_mode" type="String" default=""unreliable"" /> <param index="3" name="transfer_channel" type="int" default="0" /> <description> Mark the following method for remote procedure calls. See [url=$DOCS_URL/tutorials/networking/high_level_multiplayer.html]High-level multiplayer[/url]. + The order of [code]mode[/code], [code]sync[/code] and [code]transfer_mode[/code] does not matter and all arguments can be omitted, but [code]transfer_channel[/code] always has to be the last argument. The accepted values for [code]mode[/code] are [code]"any_peer"[/code] or [code]"authority"[/code], for [code]sync[/code] are [code]"call_remote"[/code] or [code]"call_local"[/code] and for [code]transfer_mode[/code] are [code]"unreliable"[/code], [code]"unreliable_ordered"[/code] or [code]"reliable"[/code]. [codeblock] - @rpc() + @rpc + func fn(): pass + + @rpc("any_peer", "unreliable_ordered") + func fn_update_pos(): pass + + @rpc("authority", "call_remote", "unreliable", 0) # Equivalent to @rpc + func fn_default(): pass [/codeblock] </description> </annotation> diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 8324cb0fe0..fe79f37454 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -1496,7 +1496,12 @@ GDScript::~GDScript() { // Order matters since clearing the stack may already cause // the GDScriptFunctionState to be destroyed and thus removed from the list. pending_func_states.remove(E); - E->self()->_clear_stack(); + GDScriptFunctionState *state = E->self(); + ObjectID state_id = state->get_instance_id(); + state->_clear_connections(); + if (ObjectDB::get_instance(state_id)) { + state->_clear_stack(); + } } } @@ -1920,7 +1925,12 @@ GDScriptInstance::~GDScriptInstance() { // Order matters since clearing the stack may already cause // the GDSCriptFunctionState to be destroyed and thus removed from the list. pending_func_states.remove(E); - E->self()->_clear_stack(); + GDScriptFunctionState *state = E->self(); + ObjectID state_id = state->get_instance_id(); + state->_clear_connections(); + if (ObjectDB::get_instance(state_id)) { + state->_clear_stack(); + } } if (script.is_valid() && owner) { @@ -2568,7 +2578,6 @@ GDScriptLanguage::GDScriptLanguage() { #ifdef DEBUG_ENABLED GLOBAL_DEF("debug/gdscript/warnings/enable", true); - GLOBAL_DEF("debug/gdscript/warnings/treat_warnings_as_errors", false); GLOBAL_DEF("debug/gdscript/warnings/exclude_addons", true); for (int i = 0; i < (int)GDScriptWarning::WARNING_MAX; i++) { GDScriptWarning::Code code = (GDScriptWarning::Code)i; diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 1c2b743909..602d07d9a7 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -138,13 +138,25 @@ static GDScriptParser::DataType make_enum_type(const StringName &p_enum_name, co } static GDScriptParser::DataType make_native_enum_type(const StringName &p_enum_name, const StringName &p_native_class, const bool p_meta = true) { - GDScriptParser::DataType type = make_enum_type(p_enum_name, p_native_class, p_meta); + // Find out which base class declared the enum, so the name is always the same even when coming from other contexts. + StringName native_base = p_native_class; + while (true && native_base != StringName()) { + if (ClassDB::has_enum(native_base, p_enum_name, true)) { + break; + } + native_base = ClassDB::get_parent_class_nocheck(native_base); + } + + GDScriptParser::DataType type = make_enum_type(p_enum_name, native_base, p_meta); + if (p_meta) { + type.builtin_type = Variant::NIL; // Native enum types are not Dictionaries + } List<StringName> enum_values; - ClassDB::get_enum_constants(p_native_class, p_enum_name, &enum_values); + ClassDB::get_enum_constants(native_base, p_enum_name, &enum_values, true); for (const StringName &E : enum_values) { - type.enum_values[E] = ClassDB::get_integer_constant(p_native_class, E); + type.enum_values[E] = ClassDB::get_integer_constant(native_base, E); } return type; @@ -782,6 +794,22 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, resolving_datatype.kind = GDScriptParser::DataType::RESOLVING; { +#ifdef DEBUG_ENABLED + HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings; + GDScriptParser::Node *member_node = member.get_source_node(); + if (member_node && member_node->type != GDScriptParser::Node::ANNOTATION) { + // Apply @warning_ignore annotations before resolving member. + for (GDScriptParser::AnnotationNode *&E : member_node->annotations) { + if (E->name == SNAME("@warning_ignore")) { + resolve_annotation(E); + E->apply(parser, member.variable); + } + } + for (GDScriptWarning::Code ignored_warning : member_node->ignored_warnings) { + parser->ignored_warnings.insert(ignored_warning); + } + } +#endif switch (member.type) { case GDScriptParser::ClassNode::Member::VARIABLE: { check_class_member_name_conflict(p_class, member.variable->identifier->name, member.variable); @@ -790,9 +818,48 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, // Apply annotations. for (GDScriptParser::AnnotationNode *&E : member.variable->annotations) { - resolve_annotation(E); - E->apply(parser, member.variable); + if (E->name != SNAME("@warning_ignore")) { + resolve_annotation(E); + E->apply(parser, member.variable); + } + } +#ifdef DEBUG_ENABLED + if (member.variable->exported && member.variable->onready) { + parser->push_warning(member.variable, GDScriptWarning::ONREADY_WITH_EXPORT); } + if (member.variable->initializer) { + // Check if it is call to get_node() on self (using shorthand $ or not), so we can check if @onready is needed. + // This could be improved by traversing the expression fully and checking the presence of get_node at any level. + if (!member.variable->onready && member.variable->initializer && (member.variable->initializer->type == GDScriptParser::Node::GET_NODE || member.variable->initializer->type == GDScriptParser::Node::CALL || member.variable->initializer->type == GDScriptParser::Node::CAST)) { + GDScriptParser::Node *expr = member.variable->initializer; + if (expr->type == GDScriptParser::Node::CAST) { + expr = static_cast<GDScriptParser::CastNode *>(expr)->operand; + } + bool is_get_node = expr->type == GDScriptParser::Node::GET_NODE; + bool is_using_shorthand = is_get_node; + if (!is_get_node && expr->type == GDScriptParser::Node::CALL) { + is_using_shorthand = false; + GDScriptParser::CallNode *call = static_cast<GDScriptParser::CallNode *>(expr); + if (call->function_name == SNAME("get_node")) { + switch (call->get_callee_type()) { + case GDScriptParser::Node::IDENTIFIER: { + is_get_node = true; + } break; + case GDScriptParser::Node::SUBSCRIPT: { + GDScriptParser::SubscriptNode *subscript = static_cast<GDScriptParser::SubscriptNode *>(call->callee); + is_get_node = subscript->is_attribute && subscript->base->type == GDScriptParser::Node::SELF; + } break; + default: + break; + } + } + } + if (is_get_node) { + parser->push_warning(member.variable, GDScriptWarning::GET_NODE_DEFAULT_WITHOUT_ONREADY, is_using_shorthand ? "$" : "get_node()"); + } + } + } +#endif } break; case GDScriptParser::ClassNode::Member::CONSTANT: { check_class_member_name_conflict(p_class, member.constant->identifier->name, member.constant); @@ -878,6 +945,10 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, } } break; case GDScriptParser::ClassNode::Member::FUNCTION: + for (GDScriptParser::AnnotationNode *&E : member.function->annotations) { + resolve_annotation(E); + E->apply(parser, member.function); + } resolve_function_signature(member.function, p_source); break; case GDScriptParser::ClassNode::Member::ENUM_VALUE: { @@ -931,6 +1002,9 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, ERR_PRINT("Trying to resolve undefined member."); break; } +#ifdef DEBUG_ENABLED + parser->ignored_warnings = previously_ignored_warnings; +#endif } parser->current_class = previous_class; @@ -1059,19 +1133,7 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co resolve_annotation(E); E->apply(parser, member.function); } - -#ifdef DEBUG_ENABLED - HashSet<uint32_t> previously_ignored = parser->ignored_warning_codes; - for (uint32_t ignored_warning : member.function->ignored_warnings) { - parser->ignored_warning_codes.insert(ignored_warning); - } -#endif // DEBUG_ENABLED - resolve_function_body(member.function); - -#ifdef DEBUG_ENABLED - parser->ignored_warning_codes = previously_ignored; -#endif // DEBUG_ENABLED } else if (member.type == GDScriptParser::ClassNode::Member::VARIABLE && member.variable->property != GDScriptParser::VariableNode::PROP_NONE) { if (member.variable->property == GDScriptParser::VariableNode::PROP_INLINE) { if (member.variable->getter != nullptr) { @@ -1102,9 +1164,9 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co GDScriptParser::ClassNode::Member member = p_class->members[i]; if (member.type == GDScriptParser::ClassNode::Member::VARIABLE) { #ifdef DEBUG_ENABLED - HashSet<uint32_t> previously_ignored = parser->ignored_warning_codes; - for (uint32_t ignored_warning : member.function->ignored_warnings) { - parser->ignored_warning_codes.insert(ignored_warning); + HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings; + for (GDScriptWarning::Code ignored_warning : member.variable->ignored_warnings) { + parser->ignored_warnings.insert(ignored_warning); } if (member.variable->usages == 0 && String(member.variable->identifier->name).begins_with("_")) { parser->push_warning(member.variable->identifier, GDScriptWarning::UNUSED_PRIVATE_CLASS_VARIABLE, member.variable->identifier->name); @@ -1179,7 +1241,7 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co } } #ifdef DEBUG_ENABLED - parser->ignored_warning_codes = previously_ignored; + parser->ignored_warnings = previously_ignored_warnings; #endif // DEBUG_ENABLED } } @@ -1289,6 +1351,11 @@ void GDScriptAnalyzer::resolve_node(GDScriptParser::Node *p_node, bool p_is_root void GDScriptAnalyzer::resolve_annotation(GDScriptParser::AnnotationNode *p_annotation) { ERR_FAIL_COND_MSG(!parser->valid_annotations.has(p_annotation->name), vformat(R"(Annotation "%s" not found to validate.)", p_annotation->name)); + if (p_annotation->is_resolved) { + return; + } + p_annotation->is_resolved = true; + const MethodInfo &annotation_info = parser->valid_annotations[p_annotation->name].info; const List<PropertyInfo>::Element *E = annotation_info.arguments.front(); @@ -1355,6 +1422,13 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * } p_function->resolved_signature = true; +#ifdef DEBUG_ENABLED + HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings; + for (GDScriptWarning::Code ignored_warning : p_function->ignored_warnings) { + parser->ignored_warnings.insert(ignored_warning); + } +#endif + GDScriptParser::FunctionNode *previous_function = parser->current_function; parser->current_function = p_function; @@ -1421,7 +1495,8 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * int default_par_count = 0; bool is_static = false; bool is_vararg = false; - if (!p_is_lambda && get_function_signature(p_function, false, base_type, function_name, parent_return_type, parameters_types, default_par_count, is_static, is_vararg)) { + StringName native_base; + if (!p_is_lambda && get_function_signature(p_function, false, base_type, function_name, parent_return_type, parameters_types, default_par_count, is_static, is_vararg, &native_base)) { bool valid = p_function->is_static == is_static; valid = valid && parent_return_type == p_function->get_datatype(); @@ -1447,8 +1522,8 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * parameter = "Variant"; } parent_signature += parameter; - if (j == parameters_types.size() - default_par_count) { - parent_signature += " = default"; + if (j >= parameters_types.size() - default_par_count) { + parent_signature += " = <default>"; } j++; @@ -1464,6 +1539,11 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * push_error(vformat(R"(The function signature doesn't match the parent. Parent signature is "%s".)", parent_signature), p_function); } +#ifdef DEBUG_ENABLED + if (native_base != StringName()) { + parser->push_warning(p_function, GDScriptWarning::NATIVE_METHOD_OVERRIDE, function_name, native_base); + } +#endif } #endif // TOOLS_ENABLED } @@ -1472,6 +1552,9 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * p_function->set_datatype(prev_datatype); } +#ifdef DEBUG_ENABLED + parser->ignored_warnings = previously_ignored_warnings; +#endif parser->current_function = previous_function; } @@ -1481,16 +1564,20 @@ void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_fun } p_function->resolved_body = true; +#ifdef DEBUG_ENABLED + HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings; + for (GDScriptWarning::Code ignored_warning : p_function->ignored_warnings) { + parser->ignored_warnings.insert(ignored_warning); + } +#endif + GDScriptParser::FunctionNode *previous_function = parser->current_function; parser->current_function = p_function; resolve_suite(p_function->body); - GDScriptParser::DataType return_type = p_function->body->get_datatype(); - - if (!p_function->get_datatype().is_hard_type() && return_type.is_set()) { + if (!p_function->get_datatype().is_hard_type() && p_function->body->get_datatype().is_set()) { // Use the suite inferred type if return isn't explicitly set. - return_type.type_source = GDScriptParser::DataType::INFERRED; p_function->set_datatype(p_function->body->get_datatype()); } else if (p_function->get_datatype().is_hard_type() && (p_function->get_datatype().kind != GDScriptParser::DataType::BUILTIN || p_function->get_datatype().builtin_type != Variant::NIL)) { if (!p_function->body->has_return && (p_is_lambda || p_function->identifier->name != GDScriptLanguage::get_singleton()->strings._init)) { @@ -1498,6 +1585,9 @@ void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_fun } } +#ifdef DEBUG_ENABLED + parser->ignored_warnings = previously_ignored_warnings; +#endif parser->current_function = previous_function; } @@ -1538,16 +1628,16 @@ void GDScriptAnalyzer::resolve_suite(GDScriptParser::SuiteNode *p_suite) { } #ifdef DEBUG_ENABLED - HashSet<uint32_t> previously_ignored = parser->ignored_warning_codes; - for (uint32_t ignored_warning : stmt->ignored_warnings) { - parser->ignored_warning_codes.insert(ignored_warning); + HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings; + for (GDScriptWarning::Code ignored_warning : stmt->ignored_warnings) { + parser->ignored_warnings.insert(ignored_warning); } #endif // DEBUG_ENABLED resolve_node(stmt); #ifdef DEBUG_ENABLED - parser->ignored_warning_codes = previously_ignored; + parser->ignored_warnings = previously_ignored_warnings; #endif // DEBUG_ENABLED decide_suite_type(p_suite, stmt); @@ -1599,6 +1689,11 @@ void GDScriptAnalyzer::resolve_assignable(GDScriptParser::AssignableNode *p_assi } else if (initializer_type.kind == GDScriptParser::DataType::BUILTIN && initializer_type.builtin_type == Variant::NIL && !is_constant) { push_error(vformat(R"(Cannot infer the type of "%s" %s because the value is "null".)", p_assignable->identifier->name, p_kind), p_assignable->initializer); } +#ifdef DEBUG_ENABLED + if (initializer_type.is_hard_type() && initializer_type.is_variant()) { + parser->push_warning(p_assignable, GDScriptWarning::INFERENCE_ON_VARIANT, p_kind); + } +#endif } else { if (!initializer_type.is_set()) { push_error(vformat(R"(Could not resolve type for %s "%s".)", p_kind, p_assignable->identifier->name), p_assignable->initializer); @@ -2240,6 +2335,28 @@ void GDScriptAnalyzer::reduce_assignment(GDScriptParser::AssignmentNode *p_assig if (assignee_type.is_constant || (p_assignment->assignee->type == GDScriptParser::Node::SUBSCRIPT && static_cast<GDScriptParser::SubscriptNode *>(p_assignment->assignee)->base->is_constant)) { push_error("Cannot assign a new value to a constant.", p_assignment->assignee); + return; + } else if (assignee_type.is_read_only) { + push_error("Cannot assign a new value to a read-only property.", p_assignment->assignee); + return; + } else if (p_assignment->assignee->type == GDScriptParser::Node::SUBSCRIPT) { + GDScriptParser::SubscriptNode *sub = static_cast<GDScriptParser::SubscriptNode *>(p_assignment->assignee); + while (sub) { + const GDScriptParser::DataType &base_type = sub->base->datatype; + if (base_type.is_hard_type() && base_type.is_read_only) { + if (base_type.kind == GDScriptParser::DataType::BUILTIN && !Variant::is_type_shared(base_type.builtin_type)) { + push_error("Cannot assign a new value to a read-only property.", p_assignment->assignee); + return; + } + } else { + break; + } + if (sub->base->type == GDScriptParser::Node::SUBSCRIPT) { + sub = static_cast<GDScriptParser::SubscriptNode *>(sub->base); + } else { + sub = nullptr; + } + } } // Check if assigned value is an array literal, so we can make it a typed array too if appropriate. @@ -2351,30 +2468,27 @@ void GDScriptAnalyzer::reduce_await(GDScriptParser::AwaitNode *p_await) { return; } - GDScriptParser::DataType awaiting_type; - if (p_await->to_await->type == GDScriptParser::Node::CALL) { reduce_call(static_cast<GDScriptParser::CallNode *>(p_await->to_await), true); - awaiting_type = p_await->to_await->get_datatype(); } else { reduce_expression(p_await->to_await); } - if (p_await->to_await->is_constant) { + GDScriptParser::DataType await_type = p_await->to_await->get_datatype(); + // We cannot infer the type of the result of waiting for a signal. + if (await_type.is_hard_type() && await_type.kind == GDScriptParser::DataType::BUILTIN && await_type.builtin_type == Variant::SIGNAL) { + await_type.kind = GDScriptParser::DataType::VARIANT; + await_type.type_source = GDScriptParser::DataType::UNDETECTED; + } else if (p_await->to_await->is_constant) { p_await->is_constant = p_await->to_await->is_constant; p_await->reduced_value = p_await->to_await->reduced_value; - - awaiting_type = p_await->to_await->get_datatype(); - } else { - awaiting_type.kind = GDScriptParser::DataType::VARIANT; - awaiting_type.type_source = GDScriptParser::DataType::UNDETECTED; } - - p_await->set_datatype(awaiting_type); + await_type.is_coroutine = false; + p_await->set_datatype(await_type); #ifdef DEBUG_ENABLED - awaiting_type = p_await->to_await->get_datatype(); - if (!(awaiting_type.has_no_type() || awaiting_type.is_coroutine || awaiting_type.builtin_type == Variant::SIGNAL)) { + GDScriptParser::DataType to_await_type = p_await->to_await->get_datatype(); + if (!(to_await_type.has_no_type() || to_await_type.is_coroutine || to_await_type.builtin_type == Variant::SIGNAL)) { parser->push_warning(p_await, GDScriptWarning::REDUNDANT_AWAIT); } #endif @@ -2931,7 +3045,11 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a // Enums do not have functions other than the built-in dictionary ones. if (base_type.kind == GDScriptParser::DataType::ENUM && base_type.is_meta_type) { - push_error(vformat(R"*(Enums only have Dictionary built-in methods. Function "%s()" does not exist for enum "%s".)*", p_call->function_name, base_type.enum_type), p_call->callee); + if (base_type.builtin_type == Variant::DICTIONARY) { + push_error(vformat(R"*(Enums only have Dictionary built-in methods. Function "%s()" does not exist for enum "%s".)*", p_call->function_name, base_type.enum_type), p_call->callee); + } else { + push_error(vformat(R"*(The native enum "%s" does not behave like Dictionary and does not have methods of its own.)*", base_type.enum_type), p_call->callee); + } } else if (!p_call->is_super && callee_type != GDScriptParser::Node::NONE) { // Check if the name exists as something else. GDScriptParser::IdentifierNode *callee_id; if (callee_type == GDScriptParser::Node::IDENTIFIER) { @@ -3329,7 +3447,8 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod StringName getter_name = ClassDB::get_property_getter(native, name); MethodBind *getter = ClassDB::get_method(native, getter_name); if (getter != nullptr) { - p_identifier->set_datatype(type_from_property(getter->get_return_info())); + bool has_setter = ClassDB::get_property_setter(native, name) != StringName(); + p_identifier->set_datatype(type_from_property(getter->get_return_info(), false, !has_setter)); p_identifier->source = GDScriptParser::IdentifierNode::INHERITED_VARIABLE; } return; @@ -3988,7 +4107,6 @@ void GDScriptAnalyzer::reduce_ternary_op(GDScriptParser::TernaryOpNode *p_ternar if (!is_type_compatible(true_type, false_type)) { result = false_type; if (!is_type_compatible(false_type, true_type)) { - result.type_source = GDScriptParser::DataType::UNDETECTED; result.kind = GDScriptParser::DataType::VARIANT; #ifdef DEBUG_ENABLED parser->push_warning(p_ternary_op, GDScriptWarning::INCOMPATIBLE_TERNARY); @@ -3996,6 +4114,7 @@ void GDScriptAnalyzer::reduce_ternary_op(GDScriptParser::TernaryOpNode *p_ternar } } } + result.type_source = true_type.is_hard_type() && false_type.is_hard_type() ? GDScriptParser::DataType::ANNOTATED_INFERRED : GDScriptParser::DataType::INFERRED; p_ternary_op->set_datatype(result); } @@ -4037,6 +4156,10 @@ void GDScriptAnalyzer::reduce_unary_op(GDScriptParser::UnaryOpNode *p_unary_op) Variant GDScriptAnalyzer::make_expression_reduced_value(GDScriptParser::ExpressionNode *p_expression, bool &is_reduced) { Variant value; + if (p_expression == nullptr) { + return value; + } + if (p_expression->is_constant) { is_reduced = true; value = p_expression->reduced_value; @@ -4101,6 +4224,10 @@ Variant GDScriptAnalyzer::make_dictionary_reduced_value(GDScriptParser::Dictiona } Variant GDScriptAnalyzer::make_subscript_reduced_value(GDScriptParser::SubscriptNode *p_subscript, bool &is_reduced) { + if (p_subscript->base == nullptr || p_subscript->index == nullptr) { + return Variant(); + } + bool is_base_value_reduced = false; Variant base_value = make_expression_reduced_value(p_subscript->base, is_base_value_reduced); if (!is_base_value_reduced) { @@ -4260,8 +4387,9 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_metatype(const GDScriptPars return result; } -GDScriptParser::DataType GDScriptAnalyzer::type_from_property(const PropertyInfo &p_property, bool p_is_arg) const { +GDScriptParser::DataType GDScriptAnalyzer::type_from_property(const PropertyInfo &p_property, bool p_is_arg, bool p_is_readonly) const { GDScriptParser::DataType result; + result.is_read_only = p_is_readonly; result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; if (p_property.type == Variant::NIL && (p_is_arg || (p_property.usage & PROPERTY_USAGE_NIL_IS_VARIANT))) { // Variant @@ -4302,15 +4430,27 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_property(const PropertyInfo } elem_type.is_constant = false; result.set_container_element_type(elem_type); + } else if (p_property.type == Variant::INT) { + // Check if it's enum. + if (p_property.class_name != StringName()) { + Vector<String> names = String(p_property.class_name).split("."); + if (names.size() == 2) { + result = make_native_enum_type(names[1], names[0], false); + result.is_constant = false; + } + } } } return result; } -bool GDScriptAnalyzer::get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType p_base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg) { +bool GDScriptAnalyzer::get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType p_base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg, StringName *r_native_class) { r_static = false; r_vararg = false; r_default_arg_count = 0; + if (r_native_class) { + *r_native_class = StringName(); + } StringName function_name = p_function; bool was_enum = false; @@ -4445,6 +4585,12 @@ bool GDScriptAnalyzer::get_function_signature(GDScriptParser::Node *p_source, bo if (valid && Engine::get_singleton()->has_singleton(base_native)) { r_static = true; } +#ifdef DEBUG_ENABLED + MethodBind *native_method = ClassDB::get_method(base_native, function_name); + if (native_method && r_native_class) { + *r_native_class = native_method->get_instance_class(); + } +#endif return valid; } diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h index b51564fb0a..a4c84db6b9 100644 --- a/modules/gdscript/gdscript_analyzer.h +++ b/modules/gdscript/gdscript_analyzer.h @@ -115,9 +115,9 @@ class GDScriptAnalyzer { Array make_array_from_element_datatype(const GDScriptParser::DataType &p_element_datatype, const GDScriptParser::Node *p_source_node = nullptr); GDScriptParser::DataType type_from_variant(const Variant &p_value, const GDScriptParser::Node *p_source); static GDScriptParser::DataType type_from_metatype(const GDScriptParser::DataType &p_meta_type); - GDScriptParser::DataType type_from_property(const PropertyInfo &p_property, bool p_is_arg = false) const; + GDScriptParser::DataType type_from_property(const PropertyInfo &p_property, bool p_is_arg = false, bool p_is_readonly = false) const; GDScriptParser::DataType make_global_class_meta_type(const StringName &p_class_name, const GDScriptParser::Node *p_source); - bool get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg); + bool get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg, StringName *r_native_class = nullptr); bool function_signature_from_info(const MethodInfo &p_info, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg); void validate_call_arg(const List<GDScriptParser::DataType> &p_par_types, int p_default_args_count, bool p_is_vararg, const GDScriptParser::CallNode *p_call); void validate_call_arg(const MethodInfo &p_method, const GDScriptParser::CallNode *p_call); diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index f88ac581ca..3543c0a79f 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -800,6 +800,15 @@ static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_a warning.insert_text = warning.display.quote(p_quote_style); r_result.insert(warning.display, warning); } + } else if (p_annotation->name == SNAME("@rpc")) { + if (p_argument == 0 || p_argument == 1 || p_argument == 2) { + static const char *options[7] = { "call_local", "call_remote", "any_peer", "authority", "reliable", "unreliable", "unreliable_ordered" }; + for (int i = 0; i < 7; i++) { + ScriptLanguage::CodeCompletionOption option(options[i], ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); + option.insert_text = option.display.quote(p_quote_style); + r_result.insert(option.display, option); + } + } } } @@ -968,7 +977,7 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, } break; case GDScriptParser::ClassNode::Member::SIGNAL: - if (p_only_functions || outer) { + if (p_only_functions || outer || p_static) { continue; } option = ScriptLanguage::CodeCompletionOption(member.signal->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location); @@ -1024,6 +1033,14 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, location); r_result.insert(option.display, option); } + + List<MethodInfo> signals; + scr->get_script_signal_list(&signals); + for (const MethodInfo &E : signals) { + int location = p_recursion_depth + _get_signal_location(scr->get_class_name(), E.name); + ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location); + r_result.insert(option.display, option); + } } HashMap<StringName, Variant> constants; scr->get_constants(&constants); @@ -1032,14 +1049,6 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base ScriptLanguage::CodeCompletionOption option(E.key.operator String(), ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT, location); r_result.insert(option.display, option); } - - List<MethodInfo> signals; - scr->get_script_signal_list(&signals); - for (const MethodInfo &E : signals) { - int location = p_recursion_depth + _get_signal_location(scr->get_class_name(), E.name); - ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location); - r_result.insert(option.display, option); - } } List<MethodInfo> methods; @@ -1084,14 +1093,6 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base r_result.insert(option.display, option); } - List<MethodInfo> signals; - ClassDB::get_signal_list(type, &signals); - for (const MethodInfo &E : signals) { - int location = p_recursion_depth + _get_signal_location(type, StringName(E.name)); - ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location); - r_result.insert(option.display, option); - } - if (!base_type.is_meta_type || Engine::get_singleton()->has_singleton(type)) { List<PropertyInfo> pinfo; ClassDB::get_property_list(type, &pinfo); @@ -1106,6 +1107,14 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, location); r_result.insert(option.display, option); } + + List<MethodInfo> signals; + ClassDB::get_signal_list(type, &signals); + for (const MethodInfo &E : signals) { + int location = p_recursion_depth + _get_signal_location(type, StringName(E.name)); + ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location); + r_result.insert(option.display, option); + } } } @@ -1953,17 +1962,19 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, case GDScriptParser::DataType::CLASS: if (base_type.class_type->has_function(p_context.current_function->identifier->name)) { GDScriptParser::FunctionNode *parent_function = base_type.class_type->get_member(p_context.current_function->identifier->name).function; - const GDScriptParser::ParameterNode *parameter = parent_function->parameters[parent_function->parameters_indices[p_identifier]]; - if ((!id_type.is_set() || id_type.is_variant()) && parameter->get_datatype().is_hard_type()) { - id_type = parameter->get_datatype(); - } - if (parameter->initializer) { - GDScriptParser::CompletionContext c = p_context; - c.current_function = parent_function; - c.current_class = base_type.class_type; - c.base = nullptr; - if (_guess_expression_type(c, parameter->initializer, r_type)) { - return true; + if (parent_function->parameters_indices.has(p_identifier)) { + const GDScriptParser::ParameterNode *parameter = parent_function->parameters[parent_function->parameters_indices[p_identifier]]; + if ((!id_type.is_set() || id_type.is_variant()) && parameter->get_datatype().is_hard_type()) { + id_type = parameter->get_datatype(); + } + if (parameter->initializer) { + GDScriptParser::CompletionContext c = p_context; + c.current_function = parent_function; + c.current_class = base_type.class_type; + c.base = nullptr; + if (_guess_expression_type(c, parameter->initializer, r_type)) { + return true; + } } } } @@ -2020,6 +2031,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; r_type.type.script_path = script; r_type.type.class_type = parser->get_parser()->get_tree(); + r_type.type.is_meta_type = true; r_type.type.is_constant = false; r_type.type.kind = GDScriptParser::DataType::CLASS; r_type.value = Variant(); @@ -2131,6 +2143,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext & r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; r_type.type.kind = GDScriptParser::DataType::CLASS; r_type.type.class_type = member.m_class; + r_type.type.is_meta_type = true; return true; case GDScriptParser::ClassNode::Member::GROUP: return false; // No-op, but silences warnings. @@ -3478,6 +3491,14 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co break; } + if (context.current_class) { + if (context.type != GDScriptParser::COMPLETION_SUPER_METHOD) { + base.type = context.current_class->get_datatype(); + } else { + base.type = context.current_class->base_type; + } + } + if (_lookup_symbol_from_base(base.type, p_symbol, is_function, r_result) == OK) { return OK; } diff --git a/modules/gdscript/gdscript_function.cpp b/modules/gdscript/gdscript_function.cpp index 71831a3a97..a6b4dc7981 100644 --- a/modules/gdscript/gdscript_function.cpp +++ b/modules/gdscript/gdscript_function.cpp @@ -296,6 +296,15 @@ void GDScriptFunctionState::_clear_stack() { } } +void GDScriptFunctionState::_clear_connections() { + List<Object::Connection> conns; + get_signals_connected_to_this(&conns); + + for (Object::Connection &c : conns) { + c.signal.disconnect(c.callable); + } +} + void GDScriptFunctionState::_bind_methods() { ClassDB::bind_method(D_METHOD("resume", "arg"), &GDScriptFunctionState::resume, DEFVAL(Variant())); ClassDB::bind_method(D_METHOD("is_valid", "extended_check"), &GDScriptFunctionState::is_valid, DEFVAL(false)); diff --git a/modules/gdscript/gdscript_function.h b/modules/gdscript/gdscript_function.h index 37416a734d..f45c1f9577 100644 --- a/modules/gdscript/gdscript_function.h +++ b/modules/gdscript/gdscript_function.h @@ -628,6 +628,7 @@ public: Variant resume(const Variant &p_arg = Variant()); void _clear_stack(); + void _clear_connections(); GDScriptFunctionState(); ~GDScriptFunctionState(); diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 713ad3ed17..b5cb5a4680 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -109,7 +109,7 @@ GDScriptParser::GDScriptParser() { // Warning annotations. register_annotation(MethodInfo("@warning_ignore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::CLASS | AnnotationInfo::VARIABLE | AnnotationInfo::SIGNAL | AnnotationInfo::CONSTANT | AnnotationInfo::FUNCTION | AnnotationInfo::STATEMENT, &GDScriptParser::warning_annotations, varray(), true); // Networking. - 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("", "", "", 0), true); + 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), true); #ifdef DEBUG_ENABLED is_ignoring_warnings = !(bool)GLOBAL_GET("debug/gdscript/warnings/enable"); @@ -158,14 +158,10 @@ void GDScriptParser::push_warning(const Node *p_source, GDScriptWarning::Code p_ return; } - if (ignored_warning_codes.has(p_code)) { + if (ignored_warnings.has(p_code)) { return; } - String warn_name = GDScriptWarning::get_name_from_code((GDScriptWarning::Code)p_code).to_lower(); - if (ignored_warnings.has(warn_name)) { - return; - } int warn_level = (int)GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(p_code)); if (!warn_level) { return; @@ -179,8 +175,8 @@ void GDScriptParser::push_warning(const Node *p_source, GDScriptWarning::Code p_ warning.leftmost_column = p_source->leftmost_column; warning.rightmost_column = p_source->rightmost_column; - if (warn_level == GDScriptWarning::WarnLevel::ERROR || bool(GLOBAL_GET("debug/gdscript/warnings/treat_warnings_as_errors"))) { - push_error(warning.get_message(), p_source); + if (warn_level == GDScriptWarning::WarnLevel::ERROR) { + push_error(warning.get_message() + String(" (Warning treated as error.)"), p_source); return; } @@ -487,24 +483,34 @@ void GDScriptParser::parse_program() { current_class = head; bool can_have_class_or_extends = true; - while (match(GDScriptTokenizer::Token::ANNOTATION)) { - AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::STANDALONE | AnnotationInfo::CLASS_LEVEL); - if (annotation != nullptr) { - if (annotation->applies_to(AnnotationInfo::SCRIPT)) { - // `@icon` needs to be applied in the parser. See GH-72444. - if (annotation->name == SNAME("@icon")) { - annotation->apply(this, head); + while (!check(GDScriptTokenizer::Token::TK_EOF)) { + if (match(GDScriptTokenizer::Token::ANNOTATION)) { + AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::STANDALONE | AnnotationInfo::CLASS_LEVEL); + if (annotation != nullptr) { + if (annotation->applies_to(AnnotationInfo::SCRIPT)) { + // `@icon` needs to be applied in the parser. See GH-72444. + if (annotation->name == SNAME("@icon")) { + annotation->apply(this, head); + } else { + head->annotations.push_back(annotation); + } } else { - head->annotations.push_back(annotation); + annotation_stack.push_back(annotation); + // This annotation must appear after script-level annotations + // and class_name/extends (ex: could be @onready or @export), + // so we stop looking for script-level stuff. + can_have_class_or_extends = false; + break; } - } else { - annotation_stack.push_back(annotation); - // This annotation must appear after script-level annotations - // and class_name/extends (ex: could be @onready or @export), - // so we stop looking for script-level stuff. - can_have_class_or_extends = false; - break; } + } else if (check(GDScriptTokenizer::Token::LITERAL) && current.literal.get_type() == Variant::STRING) { + // Allow strings in class body as multiline comments. + advance(); + if (!match(GDScriptTokenizer::Token::NEWLINE)) { + push_error("Expected newline after comment string."); + } + } else { + break; } } @@ -528,6 +534,16 @@ void GDScriptParser::parse_program() { end_statement("superclass"); } break; + case GDScriptTokenizer::Token::LITERAL: + if (current.literal.get_type() == Variant::STRING) { + // Allow strings in class body as multiline comments. + advance(); + if (!match(GDScriptTokenizer::Token::NEWLINE)) { + push_error("Expected newline after comment string."); + } + break; + } + [[fallthrough]]; default: // No tokens are allowed between script annotations and class/extends. can_have_class_or_extends = false; @@ -833,6 +849,16 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) { case GDScriptTokenizer::Token::DEDENT: class_end = true; break; + case GDScriptTokenizer::Token::LITERAL: + if (current.literal.get_type() == Variant::STRING) { + // Allow strings in class body as multiline comments. + advance(); + if (!match(GDScriptTokenizer::Token::NEWLINE)) { + push_error("Expected newline after comment string."); + } + break; + } + [[fallthrough]]; default: // Display a completion with identifiers. make_completion_context(COMPLETION_IDENTIFIER, nullptr); @@ -1679,6 +1705,12 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { // Standalone lambdas can't be used, so make this an error. push_error("Standalone lambdas cannot be accessed. Consider assigning it to a variable.", expression); break; + case Node::LITERAL: + if (static_cast<GDScriptParser::LiteralNode *>(expression)->value.get_type() == Variant::STRING) { + // Allow strings as multiline comments. + break; + } + [[fallthrough]]; default: push_warning(expression, GDScriptWarning::STANDALONE_EXPRESSION); } @@ -1837,10 +1869,18 @@ GDScriptParser::IfNode *GDScriptParser::parse_if(const String &p_token) { if (match(GDScriptTokenizer::Token::ELIF)) { SuiteNode *else_block = alloc_node<SuiteNode>(); + else_block->parent_function = current_function; + else_block->parent_block = current_suite; + + SuiteNode *previous_suite = current_suite; + current_suite = else_block; + IfNode *elif = parse_if("elif"); else_block->statements.push_back(elif); complete_extents(else_block); n_if->false_block = else_block; + + current_suite = previous_suite; } else if (match(GDScriptTokenizer::Token::ELSE)) { consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after "else".)"); n_if->false_block = parse_suite(R"("else" block)"); @@ -2141,7 +2181,12 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_precedence(Precedence p_pr make_completion_context(COMPLETION_IDENTIFIER, nullptr); GDScriptTokenizer::Token token = current; - ParseFunction prefix_rule = get_rule(token.type)->prefix; + GDScriptTokenizer::Token::Type token_type = token.type; + if (token.is_identifier()) { + // Allow keywords that can be treated as identifiers. + token_type = GDScriptTokenizer::Token::IDENTIFIER; + } + ParseFunction prefix_rule = get_rule(token_type)->prefix; if (prefix_rule == nullptr) { // Expected expression. Let the caller give the proper error message. @@ -3006,7 +3051,14 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p path_state = PATH_STATE_NODE_NAME; } else if (current.is_node_name()) { advance(); - get_node->full_path += previous.get_identifier(); + String identifier = previous.get_identifier(); +#ifdef DEBUG_ENABLED + // Check spoofing. + if (TS->has_feature(TextServer::FEATURE_UNICODE_SECURITY) && TS->spoof_check(identifier)) { + push_warning(get_node, GDScriptWarning::CONFUSABLE_IDENTIFIER, identifier); + } +#endif + get_node->full_path += identifier; path_state = PATH_STATE_NODE_NAME; } else if (!check(GDScriptTokenizer::Token::SLASH) && !check(GDScriptTokenizer::Token::PERCENT)) { @@ -3548,7 +3600,11 @@ const GDScriptParser::SuiteNode::Local &GDScriptParser::SuiteNode::get_local(con return empty; } -bool GDScriptParser::AnnotationNode::apply(GDScriptParser *p_this, Node *p_target) const { +bool GDScriptParser::AnnotationNode::apply(GDScriptParser *p_this, Node *p_target) { + if (is_applied) { + return true; + } + is_applied = true; return (p_this->*(p_this->valid_annotations[name].apply))(this, p_target); } @@ -3602,6 +3658,7 @@ bool GDScriptParser::tool_annotation(const AnnotationNode *p_annotation, Node *p bool GDScriptParser::icon_annotation(const AnnotationNode *p_annotation, Node *p_node) { ERR_FAIL_COND_V_MSG(p_node->type != Node::CLASS, false, R"("@icon" annotation can only be applied to classes.)"); + ERR_FAIL_COND_V(p_annotation->resolved_arguments.is_empty(), false); ClassNode *p_class = static_cast<ClassNode *>(p_node); p_class->icon_path = p_annotation->resolved_arguments[0]; return true; @@ -3610,6 +3667,10 @@ bool GDScriptParser::icon_annotation(const AnnotationNode *p_annotation, Node *p bool GDScriptParser::onready_annotation(const AnnotationNode *p_annotation, Node *p_node) { ERR_FAIL_COND_V_MSG(p_node->type != Node::VARIABLE, false, R"("@onready" annotation can only be applied to class variables.)"); + if (current_class && !ClassDB::is_parent_class(current_class->get_datatype().native_type, SNAME("Node"))) { + push_error(R"("@onready" can only be used in classes that inherit "Node".)", p_annotation); + } + VariableNode *variable = static_cast<VariableNode *>(p_node); if (variable->onready) { push_error(R"("@onready" annotation can only be used once per variable.)"); @@ -3624,15 +3685,6 @@ template <PropertyHint t_hint, Variant::Type t_type> bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node *p_node) { ERR_FAIL_COND_V_MSG(p_node->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name)); - { - const int max_flags = 32; - - if (t_hint == PropertyHint::PROPERTY_HINT_FLAGS && p_annotation->resolved_arguments.size() > max_flags) { - push_error(vformat(R"(The argument count limit for "@export_flags" is exceeded (%d/%d).)", p_annotation->resolved_arguments.size(), max_flags), p_annotation); - return false; - } - } - VariableNode *variable = static_cast<VariableNode *>(p_node); if (variable->exported) { push_error(vformat(R"(Annotation "%s" cannot be used with another "@export" annotation.)", p_annotation->name), p_annotation); @@ -3646,14 +3698,50 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node String hint_string; for (int i = 0; i < p_annotation->resolved_arguments.size(); i++) { - if (p_annotation->name != SNAME("@export_placeholder") && String(p_annotation->resolved_arguments[i]).contains(",")) { - push_error(vformat(R"(Argument %d of annotation "%s" contains a comma. Use separate arguments instead.)", i + 1, p_annotation->name), p_annotation->arguments[i]); - return false; + String arg_string = String(p_annotation->resolved_arguments[i]); + + if (p_annotation->name != SNAME("@export_placeholder")) { + if (arg_string.is_empty()) { + push_error(vformat(R"(Argument %d of annotation "%s" is empty.)", i + 1, p_annotation->name), p_annotation->arguments[i]); + return false; + } + if (arg_string.contains(",")) { + push_error(vformat(R"(Argument %d of annotation "%s" contains a comma. Use separate arguments instead.)", i + 1, p_annotation->name), p_annotation->arguments[i]); + return false; + } + } + + if (p_annotation->name == SNAME("@export_flags")) { + const int64_t max_flags = 32; + Vector<String> t = arg_string.split(":", true, 1); + if (t[0].is_empty()) { + push_error(vformat(R"(Invalid argument %d of annotation "@export_flags": Expected flag name.)", i + 1), p_annotation->arguments[i]); + return false; + } + if (t.size() == 2) { + if (t[1].is_empty()) { + push_error(vformat(R"(Invalid argument %d of annotation "@export_flags": Expected flag value.)", i + 1), p_annotation->arguments[i]); + return false; + } + if (!t[1].is_valid_int()) { + push_error(vformat(R"(Invalid argument %d of annotation "@export_flags": The flag value must be a valid integer.)", i + 1), p_annotation->arguments[i]); + return false; + } + int64_t value = t[1].to_int(); + if (value < 1 || value >= (1LL << max_flags)) { + push_error(vformat(R"(Invalid argument %d of annotation "@export_flags": The flag value must be at least 1 and at most 2 ** %d - 1.)", i + 1, max_flags), p_annotation->arguments[i]); + return false; + } + } else if (i >= max_flags) { + push_error(vformat(R"(Invalid argument %d of annotation "@export_flags": Starting from argument %d, the flag value must be specified explicitly.)", i + 1, max_flags + 1), p_annotation->arguments[i]); + return false; + } } + if (i > 0) { hint_string += ","; } - hint_string += String(p_annotation->resolved_arguments[i]); + hint_string += arg_string; } variable->export_info.hint_string = hint_string; @@ -3684,6 +3772,13 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node variable->export_info.type = Variant::DICTIONARY; return true; + } else if (export_type.builtin_type == Variant::PACKED_STRING_ARRAY) { + String hint_prefix = itos(Variant::STRING) + "/" + itos(variable->export_info.hint); + variable->export_info.hint = PROPERTY_HINT_TYPE_STRING; + variable->export_info.hint_string = hint_prefix + ":" + variable->export_info.hint_string; + variable->export_info.type = Variant::PACKED_STRING_ARRAY; + + return true; } } @@ -3830,6 +3925,10 @@ template <PropertyUsageFlags t_usage> bool GDScriptParser::export_group_annotations(const AnnotationNode *p_annotation, Node *p_node) { AnnotationNode *annotation = const_cast<AnnotationNode *>(p_annotation); + if (annotation->resolved_arguments.is_empty()) { + return false; + } + annotation->export_info.name = annotation->resolved_arguments[0]; switch (t_usage) { @@ -3887,7 +3986,7 @@ bool GDScriptParser::rpc_annotation(const AnnotationNode *p_annotation, Node *p_ Dictionary rpc_config; rpc_config["rpc_mode"] = MultiplayerAPI::RPC_MODE_AUTHORITY; - if (p_annotation->resolved_arguments.size()) { + if (!p_annotation->resolved_arguments.is_empty()) { int last = p_annotation->resolved_arguments.size() - 1; if (p_annotation->resolved_arguments[last].get_type() == Variant::INT) { rpc_config["channel"] = p_annotation->resolved_arguments[last].operator int(); @@ -3897,26 +3996,46 @@ bool GDScriptParser::rpc_annotation(const AnnotationNode *p_annotation, Node *p_ push_error(R"(Invalid RPC arguments. At most 4 arguments are allowed, where only the last argument can be an integer to specify the channel.')", p_annotation); return false; } + + unsigned char locality_args = 0; + unsigned char permission_args = 0; + unsigned char transfer_mode_args = 0; + for (int i = last; i >= 0; i--) { - String mode = p_annotation->resolved_arguments[i].operator String(); - if (mode == "any_peer") { - rpc_config["rpc_mode"] = MultiplayerAPI::RPC_MODE_ANY_PEER; - } else if (mode == "authority") { - rpc_config["rpc_mode"] = MultiplayerAPI::RPC_MODE_AUTHORITY; - } else if (mode == "call_local") { + String arg = p_annotation->resolved_arguments[i].operator String(); + if (arg == "call_local") { + locality_args++; rpc_config["call_local"] = true; - } else if (mode == "call_remote") { + } else if (arg == "call_remote") { + locality_args++; rpc_config["call_local"] = false; - } else if (mode == "reliable") { + } else if (arg == "any_peer") { + permission_args++; + rpc_config["rpc_mode"] = MultiplayerAPI::RPC_MODE_ANY_PEER; + } else if (arg == "authority") { + permission_args++; + rpc_config["rpc_mode"] = MultiplayerAPI::RPC_MODE_AUTHORITY; + } else if (arg == "reliable") { + transfer_mode_args++; rpc_config["transfer_mode"] = MultiplayerPeer::TRANSFER_MODE_RELIABLE; - } else if (mode == "unreliable") { + } else if (arg == "unreliable") { + transfer_mode_args++; rpc_config["transfer_mode"] = MultiplayerPeer::TRANSFER_MODE_UNRELIABLE; - } else if (mode == "unreliable_ordered") { + } else if (arg == "unreliable_ordered") { + transfer_mode_args++; rpc_config["transfer_mode"] = MultiplayerPeer::TRANSFER_MODE_UNRELIABLE_ORDERED; } else { - push_error(R"(Invalid RPC argument. Must be one of: 'call_local'/'call_remote' (local calls), 'any_peer'/'authority' (permission), 'reliable'/'unreliable'/'unreliable_ordered' (transfer mode).)", p_annotation); + push_error(R"(Invalid RPC argument. Must be one of: "call_local"/"call_remote" (local calls), "any_peer"/"authority" (permission), "reliable"/"unreliable"/"unreliable_ordered" (transfer mode).)", p_annotation); } } + + if (locality_args > 1) { + push_error(R"(Invalid RPC config. The locality ("call_local"/"call_remote") must be specified no more than once.)", p_annotation); + } else if (permission_args > 1) { + push_error(R"(Invalid RPC config. The permission ("any_peer"/"authority") must be specified no more than once.)", p_annotation); + } else if (transfer_mode_args > 1) { + push_error(R"(Invalid RPC config. The transfer mode ("reliable"/"unreliable"/"unreliable_ordered") must be specified no more than once.)", p_annotation); + } } function->rpc_config = rpc_config; return true; diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 07dac25ec5..0ba0d5b6da 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -122,6 +122,7 @@ public: TypeSource type_source = UNDETECTED; bool is_constant = false; + bool is_read_only = false; bool is_meta_type = false; bool is_coroutine = false; // For function calls. @@ -206,6 +207,7 @@ public: void operator=(const DataType &p_other) { kind = p_other.kind; type_source = p_other.type_source; + is_read_only = p_other.is_read_only; is_constant = p_other.is_constant; is_meta_type = p_other.is_meta_type; is_coroutine = p_other.is_coroutine; @@ -297,7 +299,9 @@ public: int leftmost_column = 0, rightmost_column = 0; Node *next = nullptr; List<AnnotationNode *> annotations; - Vector<uint32_t> ignored_warnings; +#ifdef DEBUG_ENABLED + Vector<GDScriptWarning::Code> ignored_warnings; +#endif DataType datatype; @@ -329,8 +333,10 @@ public: AnnotationInfo *info = nullptr; PropertyInfo export_info; + bool is_resolved = false; + bool is_applied = false; - bool apply(GDScriptParser *p_this, Node *p_target) const; + bool apply(GDScriptParser *p_this, Node *p_target); bool applies_to(uint32_t p_target_kinds) const; AnnotationNode() { @@ -1263,8 +1269,7 @@ private: #ifdef DEBUG_ENABLED bool is_ignoring_warnings = false; List<GDScriptWarning> warnings; - HashSet<String> ignored_warnings; - HashSet<uint32_t> ignored_warning_codes; + HashSet<GDScriptWarning::Code> ignored_warnings; HashSet<int> unsafe_lines; #endif diff --git a/modules/gdscript/gdscript_tokenizer.cpp b/modules/gdscript/gdscript_tokenizer.cpp index d7f1114fd3..d586380c41 100644 --- a/modules/gdscript/gdscript_tokenizer.cpp +++ b/modules/gdscript/gdscript_tokenizer.cpp @@ -168,7 +168,11 @@ bool GDScriptTokenizer::Token::is_identifier() const { switch (type) { case IDENTIFIER: case MATCH: // Used in String.match(). - case CONST_INF: // Used in Vector{2,3,4}.INF + // Allow constants to be treated as regular identifiers. + case CONST_PI: + case CONST_INF: + case CONST_NAN: + case CONST_TAU: return true; default: return false; @@ -188,6 +192,10 @@ bool GDScriptTokenizer::Token::is_node_name() const { case CLASS_NAME: case CLASS: case CONST: + case CONST_PI: + case CONST_INF: + case CONST_NAN: + case CONST_TAU: case CONTINUE: case ELIF: case ELSE: @@ -530,9 +538,12 @@ void GDScriptTokenizer::make_keyword_list() { #endif // DEBUG_ENABLED GDScriptTokenizer::Token GDScriptTokenizer::potential_identifier() { + bool only_ascii = _peek(-1) < 128; + // Consume all identifier characters. while (is_unicode_identifier_continue(_peek())) { - _advance(); + char32_t c = _advance(); + only_ascii = only_ascii && c < 128; } int len = _current - _start; @@ -587,7 +598,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::potential_identifier() { #ifdef DEBUG_ENABLED // Additional checks for identifiers but only in debug and if it's available in TextServer. - if (TS->has_feature(TextServer::FEATURE_UNICODE_SECURITY)) { + if (!only_ascii && TS->has_feature(TextServer::FEATURE_UNICODE_SECURITY)) { int64_t confusable = TS->is_confusable(name, keyword_list); if (confusable >= 0) { push_error(vformat(R"(Identifier "%s" is visually similar to the GDScript keyword "%s" and thus not allowed.)", name, keyword_list[confusable])); diff --git a/modules/gdscript/gdscript_vm.cpp b/modules/gdscript/gdscript_vm.cpp index e18a4a6190..6c26e226a5 100644 --- a/modules/gdscript/gdscript_vm.cpp +++ b/modules/gdscript/gdscript_vm.cpp @@ -811,13 +811,22 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #ifdef DEBUG_ENABLED if (!valid) { + Object *obj = dst->get_validated_object(); String v = index->operator String(); - if (!v.is_empty()) { - v = "'" + v + "'"; + bool read_only_property = false; + if (obj) { + read_only_property = ClassDB::has_property(obj->get_class_name(), v) && (ClassDB::get_property_setter(obj->get_class_name(), v) == StringName()); + } + if (read_only_property) { + err_text = vformat(R"(Cannot set value into property "%s" (on base "%s") because it is read-only.)", v, _get_var_type(dst)); } else { - v = "of type '" + _get_var_type(index) + "'"; + if (!v.is_empty()) { + v = "'" + v + "'"; + } else { + v = "of type '" + _get_var_type(index) + "'"; + } + err_text = "Invalid set index " + v + " (on base: '" + _get_var_type(dst) + "') with value of type '" + _get_var_type(value) + "'"; } - err_text = "Invalid set index " + v + " (on base: '" + _get_var_type(dst) + "') with value of type '" + _get_var_type(value) + "'"; OPCODE_BREAK; } #endif @@ -1003,8 +1012,16 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #ifdef DEBUG_ENABLED if (!valid) { - String err_type; - err_text = "Invalid set index '" + String(*index) + "' (on base: '" + _get_var_type(dst) + "') with value of type '" + _get_var_type(value) + "'."; + Object *obj = dst->get_validated_object(); + bool read_only_property = false; + if (obj) { + read_only_property = ClassDB::has_property(obj->get_class_name(), *index) && (ClassDB::get_property_setter(obj->get_class_name(), *index) == StringName()); + } + if (read_only_property) { + err_text = vformat(R"(Cannot set value into property "%s" (on base "%s") because it is read-only.)", String(*index), _get_var_type(dst)); + } else { + err_text = "Invalid set index '" + String(*index) + "' (on base: '" + _get_var_type(dst) + "') with value of type '" + _get_var_type(value) + "'."; + } OPCODE_BREAK; } #endif @@ -3410,7 +3427,10 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a String message_str; if (_code_ptr[ip + 2] != 0) { GET_VARIANT_PTR(message, 1); - message_str = *message; + Variant message_var = *message; + if (message->get_type() != Variant::NIL) { + message_str = message_var; + } } if (message_str.is_empty()) { err_text = "Assertion failed."; diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp index 9436146bed..ef59a07f1a 100644 --- a/modules/gdscript/gdscript_warning.cpp +++ b/modules/gdscript/gdscript_warning.cpp @@ -170,6 +170,21 @@ String GDScriptWarning::get_message() const { case RENAMED_IN_GD4_HINT: { break; // Renamed identifier hint is taken care of by the GDScriptAnalyzer. No message needed here. } + case INFERENCE_ON_VARIANT: { + CHECK_SYMBOLS(1); + return vformat("The %s type is being inferred from a Variant value, so it will be typed as Variant.", symbols[0]); + } + case NATIVE_METHOD_OVERRIDE: { + CHECK_SYMBOLS(2); + return vformat(R"(The method "%s" overrides a method from native class "%s". This won't be called by the engine and may not work as expected.)", symbols[0], symbols[1]); + } + case GET_NODE_DEFAULT_WITHOUT_ONREADY: { + CHECK_SYMBOLS(1); + return vformat(R"*(The default value is using "%s" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this.)*", symbols[0]); + } + case ONREADY_WITH_EXPORT: { + return R"(The "@onready" annotation will make the default value to be set after the "@export" takes effect and will override it.)"; + } case WARNING_MAX: break; // Can't happen, but silences warning } @@ -179,14 +194,8 @@ String GDScriptWarning::get_message() const { } int GDScriptWarning::get_default_value(Code p_code) { - if (get_name_from_code(p_code).to_lower().begins_with("unsafe_")) { - return WarnLevel::IGNORE; - } - // Too spammy by default on common cases (connect, Tween, etc.). - if (p_code == RETURN_VALUE_DISCARDED) { - return WarnLevel::IGNORE; - } - return WarnLevel::WARN; + ERR_FAIL_INDEX_V_MSG(p_code, WARNING_MAX, WarnLevel::IGNORE, "Getting default value of invalid warning code."); + return default_warning_levels[p_code]; } PropertyInfo GDScriptWarning::get_property_info(Code p_code) { @@ -240,7 +249,11 @@ String GDScriptWarning::get_name_from_code(Code p_code) { "INT_AS_ENUM_WITHOUT_MATCH", "STATIC_CALLED_ON_INSTANCE", "CONFUSABLE_IDENTIFIER", - "RENAMED_IN_GODOT_4_HINT" + "RENAMED_IN_GODOT_4_HINT", + "INFERENCE_ON_VARIANT", + "NATIVE_METHOD_OVERRIDE", + "GET_NODE_DEFAULT_WITHOUT_ONREADY", + "ONREADY_WITH_EXPORT", }; static_assert((sizeof(names) / sizeof(*names)) == WARNING_MAX, "Amount of warning types don't match the amount of warning names."); diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h index fa2907cdae..f0123c518c 100644 --- a/modules/gdscript/gdscript_warning.h +++ b/modules/gdscript/gdscript_warning.h @@ -82,9 +82,58 @@ public: STATIC_CALLED_ON_INSTANCE, // A static method was called on an instance of a class instead of on the class itself. CONFUSABLE_IDENTIFIER, // The identifier contains misleading characters that can be confused. E.g. "usеr" (has Cyrillic "е" instead of Latin "e"). RENAMED_IN_GD4_HINT, // A variable or function that could not be found has been renamed in Godot 4 + INFERENCE_ON_VARIANT, // The declaration uses type inference but the value is typed as Variant. + NATIVE_METHOD_OVERRIDE, // The script method overrides a native one, this may not work as intended. + GET_NODE_DEFAULT_WITHOUT_ONREADY, // A class variable uses `get_node()` (or the `$` notation) as its default value, but does not use the @onready annotation. + ONREADY_WITH_EXPORT, // The `@onready` annotation will set the value after `@export` which is likely not intended. WARNING_MAX, }; + constexpr static WarnLevel default_warning_levels[] = { + WARN, // UNASSIGNED_VARIABLE + WARN, // UNASSIGNED_VARIABLE_OP_ASSIGN + WARN, // UNUSED_VARIABLE + WARN, // UNUSED_LOCAL_CONSTANT + WARN, // SHADOWED_VARIABLE + WARN, // SHADOWED_VARIABLE_BASE_CLASS + WARN, // UNUSED_PRIVATE_CLASS_VARIABLE + WARN, // UNUSED_PARAMETER + WARN, // UNREACHABLE_CODE + WARN, // UNREACHABLE_PATTERN + WARN, // STANDALONE_EXPRESSION + WARN, // NARROWING_CONVERSION + WARN, // INCOMPATIBLE_TERNARY + WARN, // UNUSED_SIGNAL + IGNORE, // RETURN_VALUE_DISCARDED // Too spammy by default on common cases (connect, Tween, etc.). + WARN, // PROPERTY_USED_AS_FUNCTION + WARN, // CONSTANT_USED_AS_FUNCTION + WARN, // FUNCTION_USED_AS_PROPERTY + WARN, // INTEGER_DIVISION + IGNORE, // UNSAFE_PROPERTY_ACCESS // Too common in untyped scenarios. + IGNORE, // UNSAFE_METHOD_ACCESS // Too common in untyped scenarios. + IGNORE, // UNSAFE_CAST // Too common in untyped scenarios. + IGNORE, // UNSAFE_CALL_ARGUMENT // Too common in untyped scenarios. + WARN, // UNSAFE_VOID_RETURN + WARN, // DEPRECATED_KEYWORD + WARN, // STANDALONE_TERNARY + WARN, // ASSERT_ALWAYS_TRUE + WARN, // ASSERT_ALWAYS_FALSE + WARN, // REDUNDANT_AWAIT + WARN, // EMPTY_FILE + WARN, // SHADOWED_GLOBAL_IDENTIFIER + WARN, // INT_AS_ENUM_WITHOUT_CAST + WARN, // INT_AS_ENUM_WITHOUT_MATCH + WARN, // STATIC_CALLED_ON_INSTANCE + WARN, // CONFUSABLE_IDENTIFIER + WARN, // RENAMED_IN_GD4_HINT + ERROR, // INFERENCE_ON_VARIANT // Most likely done by accident, usually inference is trying for a particular type. + ERROR, // NATIVE_METHOD_OVERRIDE // May not work as expected. + ERROR, // GET_NODE_DEFAULT_WITHOUT_ONREADY // May not work as expected. + ERROR, // ONREADY_WITH_EXPORT // May not work as expected. + }; + + static_assert((sizeof(default_warning_levels) / sizeof(default_warning_levels[0])) == WARNING_MAX, "Amount of default levels does not match the amount of warnings."); + Code code = WARNING_MAX; int start_line = -1, end_line = -1; int leftmost_column = -1, rightmost_column = -1; diff --git a/modules/gdscript/language_server/gdscript_text_document.cpp b/modules/gdscript/language_server/gdscript_text_document.cpp index b9e6921034..35fbdca949 100644 --- a/modules/gdscript/language_server/gdscript_text_document.cpp +++ b/modules/gdscript/language_server/gdscript_text_document.cpp @@ -108,6 +108,7 @@ void GDScriptTextDocument::didSave(const Variant &p_param) { scr->reload(true); } scr->update_exports(); + ScriptEditor::get_singleton()->reload_scripts(true); ScriptEditor::get_singleton()->update_docs_from_script(scr); } } diff --git a/modules/gdscript/tests/gdscript_test_runner.cpp b/modules/gdscript/tests/gdscript_test_runner.cpp index 5b8af0ff34..57405aa1ce 100644 --- a/modules/gdscript/tests/gdscript_test_runner.cpp +++ b/modules/gdscript/tests/gdscript_test_runner.cpp @@ -146,11 +146,11 @@ GDScriptTestRunner::GDScriptTestRunner(const String &p_source_dir, bool p_init_l init_language(p_source_dir); } #ifdef DEBUG_ENABLED - // Enable all warnings for GDScript, so we can test them. + // Set all warning levels to "Warn" in order to test them properly, even the ones that default to error. ProjectSettings::get_singleton()->set_setting("debug/gdscript/warnings/enable", true); for (int i = 0; i < (int)GDScriptWarning::WARNING_MAX; i++) { - String warning = GDScriptWarning::get_name_from_code((GDScriptWarning::Code)i).to_lower(); - ProjectSettings::get_singleton()->set_setting("debug/gdscript/warnings/" + warning, true); + String warning_setting = GDScriptWarning::get_settings_path_from_code((GDScriptWarning::Code)i); + ProjectSettings::get_singleton()->set_setting(warning_setting, (int)GDScriptWarning::WARN); } #endif diff --git a/modules/gdscript/tests/scripts/analyzer/errors/assign_to_read_only_property.gd b/modules/gdscript/tests/scripts/analyzer/errors/assign_to_read_only_property.gd new file mode 100644 index 0000000000..2b1c4c9594 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/assign_to_read_only_property.gd @@ -0,0 +1,4 @@ +func test(): + var tree := SceneTree.new() + tree.root = Window.new() + tree.free() diff --git a/modules/gdscript/tests/scripts/analyzer/errors/assign_to_read_only_property.out b/modules/gdscript/tests/scripts/analyzer/errors/assign_to_read_only_property.out new file mode 100644 index 0000000000..b236d70ec8 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/assign_to_read_only_property.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot assign a new value to a read-only property. diff --git a/modules/gdscript/tests/scripts/analyzer/errors/assign_to_read_only_property_indirectly.gd b/modules/gdscript/tests/scripts/analyzer/errors/assign_to_read_only_property_indirectly.gd new file mode 100644 index 0000000000..c97ee0ea69 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/assign_to_read_only_property_indirectly.gd @@ -0,0 +1,4 @@ +func test(): + var state := PhysicsDirectBodyState3DExtension.new() + state.center_of_mass.x += 1.0 + state.free() diff --git a/modules/gdscript/tests/scripts/analyzer/errors/assign_to_read_only_property_indirectly.out b/modules/gdscript/tests/scripts/analyzer/errors/assign_to_read_only_property_indirectly.out new file mode 100644 index 0000000000..b236d70ec8 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/assign_to_read_only_property_indirectly.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot assign a new value to a read-only property. diff --git a/modules/gdscript/tests/scripts/analyzer/errors/await_signal_no_infer.gd b/modules/gdscript/tests/scripts/analyzer/errors/await_signal_no_infer.gd new file mode 100644 index 0000000000..c787d9e50e --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/await_signal_no_infer.gd @@ -0,0 +1,4 @@ +signal my_signal() + +func test(): + var _a := await my_signal diff --git a/modules/gdscript/tests/scripts/analyzer/errors/await_signal_no_infer.out b/modules/gdscript/tests/scripts/analyzer/errors/await_signal_no_infer.out new file mode 100644 index 0000000000..8f8744ad7e --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/await_signal_no_infer.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot infer the type of "_a" variable because the value doesn't have a set type. diff --git a/modules/gdscript/tests/scripts/analyzer/errors/function_dont_match_parent_signature_parameter_default_values.out b/modules/gdscript/tests/scripts/analyzer/errors/function_dont_match_parent_signature_parameter_default_values.out index c70a1df10d..508e46742f 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/function_dont_match_parent_signature_parameter_default_values.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/function_dont_match_parent_signature_parameter_default_values.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -The function signature doesn't match the parent. Parent signature is "my_function(int = default) -> int". +The function signature doesn't match the parent. Parent signature is "my_function(int = <default>) -> int". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/get_node_shorthand_within_non_node.gd b/modules/gdscript/tests/scripts/analyzer/errors/get_node_shorthand_within_non_node.gd new file mode 100644 index 0000000000..91f5071fa9 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/get_node_shorthand_within_non_node.gd @@ -0,0 +1,5 @@ +extends RefCounted + +func test(): + var nope := $Node + print("Cannot use $ without a Node base") diff --git a/modules/gdscript/tests/scripts/analyzer/errors/get_node_shorthand_within_non_node.out b/modules/gdscript/tests/scripts/analyzer/errors/get_node_shorthand_within_non_node.out new file mode 100644 index 0000000000..33365908bf --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/get_node_shorthand_within_non_node.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot use shorthand "get_node()" notation ("$") on a class that isn't a node. diff --git a/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node.gd b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node.gd new file mode 100644 index 0000000000..e781315266 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node.gd @@ -0,0 +1,6 @@ +extends RefCounted + +@onready var nope := 0 + +func test(): + print("Cannot use @onready without a Node base") diff --git a/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node.out b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node.out new file mode 100644 index 0000000000..8088d28329 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +"@onready" can only be used in classes that inherit "Node". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node_inner_class.gd b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node_inner_class.gd new file mode 100644 index 0000000000..1639bbbd52 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node_inner_class.gd @@ -0,0 +1,7 @@ +extends Node + +class Inner extends RefCounted: + @onready var nope = 0 + +func test(): + print("Cannot use @onready without a Node base") diff --git a/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node_inner_class.out b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node_inner_class.out new file mode 100644 index 0000000000..8088d28329 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node_inner_class.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +"@onready" can only be used in classes that inherit "Node". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/ternary_weak_infer.gd b/modules/gdscript/tests/scripts/analyzer/errors/ternary_weak_infer.gd new file mode 100644 index 0000000000..fac0e8756c --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/ternary_weak_infer.gd @@ -0,0 +1,6 @@ +func test(): + var left_hard_int := 1 + var right_weak_int = 2 + var result_hm_int := left_hard_int if true else right_weak_int + + print('not ok') diff --git a/modules/gdscript/tests/scripts/analyzer/errors/ternary_weak_infer.out b/modules/gdscript/tests/scripts/analyzer/errors/ternary_weak_infer.out new file mode 100644 index 0000000000..71d1e2f8ae --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/ternary_weak_infer.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot infer the type of "result_hm_int" variable because the value doesn't have a set type. diff --git a/modules/gdscript/tests/scripts/analyzer/features/allow_get_node_with_onready.gd b/modules/gdscript/tests/scripts/analyzer/features/allow_get_node_with_onready.gd new file mode 100644 index 0000000000..a9004a346b --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/allow_get_node_with_onready.gd @@ -0,0 +1,18 @@ +extends Node + +@onready var shorthand = $Node +@onready var call = get_node(^"Node") +@onready var shorthand_with_cast = $Node as Node +@onready var call_with_cast = get_node(^"Node") as Node + +func _init(): + var node := Node.new() + node.name = "Node" + add_child(node) + +func test(): + # Those are expected to be `null` since `_ready()` is never called on tests. + prints("shorthand", shorthand) + prints("call", call) + prints("shorthand_with_cast", shorthand_with_cast) + prints("call_with_cast", call_with_cast) diff --git a/modules/gdscript/tests/scripts/analyzer/features/allow_get_node_with_onready.out b/modules/gdscript/tests/scripts/analyzer/features/allow_get_node_with_onready.out new file mode 100644 index 0000000000..eddc2deec0 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/allow_get_node_with_onready.out @@ -0,0 +1,5 @@ +GDTEST_OK +shorthand <null> +call <null> +shorthand_with_cast <null> +call_with_cast <null> diff --git a/modules/gdscript/tests/scripts/analyzer/features/assign_to_native_enum_property.gd b/modules/gdscript/tests/scripts/analyzer/features/assign_to_native_enum_property.gd new file mode 100644 index 0000000000..02120db868 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/assign_to_native_enum_property.gd @@ -0,0 +1,13 @@ +# https://github.com/godotengine/godot/issues/72501 +extends Node + +func test(): + prints("before", process_mode) + process_mode = PROCESS_MODE_PAUSABLE + prints("after", process_mode) + + var node := Node.new() + add_child(node) + prints("before", node.process_mode) + node.process_mode = PROCESS_MODE_PAUSABLE + prints("after", node.process_mode) diff --git a/modules/gdscript/tests/scripts/analyzer/features/assign_to_native_enum_property.out b/modules/gdscript/tests/scripts/analyzer/features/assign_to_native_enum_property.out new file mode 100644 index 0000000000..1eb045a4e4 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/assign_to_native_enum_property.out @@ -0,0 +1,5 @@ +GDTEST_OK +before 0 +after 1 +before 0 +after 1 diff --git a/modules/gdscript/tests/scripts/analyzer/features/await_type_inference.gd b/modules/gdscript/tests/scripts/analyzer/features/await_type_inference.gd new file mode 100644 index 0000000000..9d8cfc7f99 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/await_type_inference.gd @@ -0,0 +1,15 @@ +func coroutine() -> int: + @warning_ignore("redundant_await") + await 0 + return 1 + +func not_coroutine() -> int: + return 2 + +func test(): + var a := await coroutine() + @warning_ignore("redundant_await") + var b := await not_coroutine() + @warning_ignore("redundant_await") + var c := await 3 + prints(a, b, c) diff --git a/modules/gdscript/tests/scripts/analyzer/features/await_type_inference.out b/modules/gdscript/tests/scripts/analyzer/features/await_type_inference.out new file mode 100644 index 0000000000..2920e2ce9c --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/await_type_inference.out @@ -0,0 +1,2 @@ +GDTEST_OK +1 2 3 diff --git a/modules/gdscript/tests/scripts/analyzer/features/hard_variants.gd b/modules/gdscript/tests/scripts/analyzer/features/hard_variants.gd index 48a804ff54..b447180ea8 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/hard_variants.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/hard_variants.gd @@ -2,16 +2,18 @@ func variant() -> Variant: return null var member_weak = variant() var member_typed: Variant = variant() +@warning_ignore("inference_on_variant") var member_inferred := variant() func param_weak(param = variant()) -> void: print(param) func param_typed(param: Variant = variant()) -> void: print(param) +@warning_ignore("inference_on_variant") func param_inferred(param := variant()) -> void: print(param) func return_untyped(): return variant() func return_typed() -> Variant: return variant() -@warning_ignore("unused_variable") +@warning_ignore("unused_variable", "inference_on_variant") func test() -> void: var weak = variant() var typed: Variant = variant() diff --git a/modules/gdscript/tests/scripts/analyzer/features/onready_on_inner_class_with_non_node_outer.gd b/modules/gdscript/tests/scripts/analyzer/features/onready_on_inner_class_with_non_node_outer.gd new file mode 100644 index 0000000000..1ac03c2181 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/onready_on_inner_class_with_non_node_outer.gd @@ -0,0 +1,7 @@ +extends RefCounted + +func test(): + print("ok") + +class Inner extends Node: + @onready var okay = 0 diff --git a/modules/gdscript/tests/scripts/analyzer/features/onready_on_inner_class_with_non_node_outer.out b/modules/gdscript/tests/scripts/analyzer/features/onready_on_inner_class_with_non_node_outer.out new file mode 100644 index 0000000000..1b47ed10dc --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/onready_on_inner_class_with_non_node_outer.out @@ -0,0 +1,2 @@ +GDTEST_OK +ok diff --git a/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.gd b/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.gd new file mode 100644 index 0000000000..44ca5f4dd0 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.gd @@ -0,0 +1,15 @@ +func test(): + var left_hard_int := 1 + var right_hard_int := 2 + var result_hard_int := left_hard_int if true else right_hard_int + assert(result_hard_int == 1) + + @warning_ignore("inference_on_variant") + var left_hard_variant := 1 as Variant + @warning_ignore("inference_on_variant") + var right_hard_variant := 2.0 as Variant + @warning_ignore("inference_on_variant") + var result_hard_variant := left_hard_variant if true else right_hard_variant + assert(result_hard_variant == 1) + + print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.out b/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.out new file mode 100644 index 0000000000..1b47ed10dc --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.out @@ -0,0 +1,2 @@ +GDTEST_OK +ok diff --git a/modules/gdscript/tests/scripts/analyzer/features/typed_array_usage.gd b/modules/gdscript/tests/scripts/analyzer/features/typed_array_usage.gd index e1e6134fd4..092ae49d00 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/typed_array_usage.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/typed_array_usage.gd @@ -201,4 +201,10 @@ func test(): assert(typed_enums.get_typed_builtin() == TYPE_INT) + var a := A.new() + var typed_natives: Array[RefCounted] = [a] + var typed_scripts = Array(typed_natives, TYPE_OBJECT, "RefCounted", A) + assert(typed_scripts[0] == a) + + print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/get_node_without_onready.gd b/modules/gdscript/tests/scripts/analyzer/warnings/get_node_without_onready.gd new file mode 100644 index 0000000000..849df0921e --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/get_node_without_onready.gd @@ -0,0 +1,17 @@ +extends Node + +var add_node = do_add_node() # Hack to have one node on init and not fail at runtime. + +var shorthand = $Node +var with_self = self.get_node(^"Node") +var without_self = get_node(^"Node") +var with_cast = get_node(^"Node") as Node +var shorthand_with_cast = $Node as Node + +func test(): + print("warn") + +func do_add_node(): + var node = Node.new() + node.name = "Node" + add_child(node) diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/get_node_without_onready.out b/modules/gdscript/tests/scripts/analyzer/warnings/get_node_without_onready.out new file mode 100644 index 0000000000..62b3ae291f --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/get_node_without_onready.out @@ -0,0 +1,22 @@ +GDTEST_OK +>> WARNING +>> Line: 5 +>> GET_NODE_DEFAULT_WITHOUT_ONREADY +>> The default value is using "$" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this. +>> WARNING +>> Line: 6 +>> GET_NODE_DEFAULT_WITHOUT_ONREADY +>> The default value is using "get_node()" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this. +>> WARNING +>> Line: 7 +>> GET_NODE_DEFAULT_WITHOUT_ONREADY +>> The default value is using "get_node()" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this. +>> WARNING +>> Line: 8 +>> GET_NODE_DEFAULT_WITHOUT_ONREADY +>> The default value is using "get_node()" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this. +>> WARNING +>> Line: 9 +>> GET_NODE_DEFAULT_WITHOUT_ONREADY +>> The default value is using "$" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this. +warn diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/inference_with_variant.gd b/modules/gdscript/tests/scripts/analyzer/warnings/inference_with_variant.gd new file mode 100644 index 0000000000..024e91b7c6 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/inference_with_variant.gd @@ -0,0 +1,6 @@ +func test(): + var inferred_with_variant := return_variant() + print(inferred_with_variant) + +func return_variant() -> Variant: + return "warn" diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/inference_with_variant.out b/modules/gdscript/tests/scripts/analyzer/warnings/inference_with_variant.out new file mode 100644 index 0000000000..1d4078d2f7 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/inference_with_variant.out @@ -0,0 +1,6 @@ +GDTEST_OK +>> WARNING +>> Line: 2 +>> INFERENCE_ON_VARIANT +>> The variable type is being inferred from a Variant value, so it will be typed as Variant. +warn diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.gd b/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.gd new file mode 100644 index 0000000000..0b358ca5f2 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.gd @@ -0,0 +1,6 @@ +extends Node + +@onready @export var conflict = "" + +func test(): + print("warn") diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.out b/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.out new file mode 100644 index 0000000000..ff184f9f04 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.out @@ -0,0 +1,6 @@ +GDTEST_OK +>> WARNING +>> Line: 3 +>> ONREADY_WITH_EXPORT +>> The "@onready" annotation will make the default value to be set after the "@export" takes effect and will override it. +warn diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.gd b/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.gd new file mode 100644 index 0000000000..19d40f8ec8 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.gd @@ -0,0 +1,5 @@ +func test(): + print("warn") + +func get(_property: StringName) -> Variant: + return null diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.out b/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.out new file mode 100644 index 0000000000..793faa05d4 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.out @@ -0,0 +1,6 @@ +GDTEST_OK +>> WARNING +>> Line: 4 +>> NATIVE_METHOD_OVERRIDE +>> The method "get" overrides a method from native class "Object". This won't be called by the engine and may not work as expected. +warn diff --git a/modules/gdscript/tests/scripts/parser/features/allow_id_similar_to_keyword_in_ascii.gd b/modules/gdscript/tests/scripts/parser/features/allow_id_similar_to_keyword_in_ascii.gd new file mode 100644 index 0000000000..390d314b94 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/allow_id_similar_to_keyword_in_ascii.gd @@ -0,0 +1,3 @@ +func test(): + var P1 = "ok" # Technically it is visually similar to keyword "PI" but allowed since it's in ASCII range. + print(P1) diff --git a/modules/gdscript/tests/scripts/parser/features/allow_id_similar_to_keyword_in_ascii.out b/modules/gdscript/tests/scripts/parser/features/allow_id_similar_to_keyword_in_ascii.out new file mode 100644 index 0000000000..1b47ed10dc --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/allow_id_similar_to_keyword_in_ascii.out @@ -0,0 +1,2 @@ +GDTEST_OK +ok diff --git a/modules/gdscript/tests/scripts/parser/features/allow_strings_as_comments.gd b/modules/gdscript/tests/scripts/parser/features/allow_strings_as_comments.gd new file mode 100644 index 0000000000..3ecd65ad9c --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/allow_strings_as_comments.gd @@ -0,0 +1,21 @@ +""" +This is a comment. +""" + +@tool + +""" +This is also a comment. +""" + +extends RefCounted + +''' +This is a comment too. +''' + +func test(): + """ + This too is a comment. + """ + print("ok") diff --git a/modules/gdscript/tests/scripts/parser/features/allow_strings_as_comments.out b/modules/gdscript/tests/scripts/parser/features/allow_strings_as_comments.out new file mode 100644 index 0000000000..1b47ed10dc --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/allow_strings_as_comments.out @@ -0,0 +1,2 @@ +GDTEST_OK +ok diff --git a/modules/gdscript/tests/scripts/parser/features/allowed_keywords_as_identifiers.gd b/modules/gdscript/tests/scripts/parser/features/allowed_keywords_as_identifiers.gd new file mode 100644 index 0000000000..7e1982597c --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/allowed_keywords_as_identifiers.gd @@ -0,0 +1,16 @@ +func test(): + # The following keywords are allowed as identifiers: + var match = "match" + print(match) + + var PI = "PI" + print(PI) + + var INF = "INF" + print(INF) + + var NAN = "NAN" + print(NAN) + + var TAU = "TAU" + print(TAU) diff --git a/modules/gdscript/tests/scripts/parser/features/allowed_keywords_as_identifiers.out b/modules/gdscript/tests/scripts/parser/features/allowed_keywords_as_identifiers.out new file mode 100644 index 0000000000..aae2ae13d5 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/allowed_keywords_as_identifiers.out @@ -0,0 +1,6 @@ +GDTEST_OK +match +PI +INF +NAN +TAU diff --git a/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.gd b/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.gd index e2caac8ffd..41b38c4bba 100644 --- a/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.gd +++ b/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.gd @@ -1,5 +1,12 @@ +extends Node + func test(): var port = 0 # Only latin characters. var pοrt = 1 # The "ο" is Greek omicron. prints(port, pοrt) + +# Do not call this since nodes aren't in the tree. It is just a parser check. +func nodes(): + var _node1 = $port # Only latin characters. + var _node2 = $pοrt # The "ο" is Greek omicron. diff --git a/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.out b/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.out index c483396443..c189204285 100644 --- a/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.out +++ b/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.out @@ -1,6 +1,10 @@ GDTEST_OK >> WARNING ->> Line: 3 +>> Line: 5 +>> CONFUSABLE_IDENTIFIER +>> The identifier "pοrt" has misleading characters and might be confused with something else. +>> WARNING +>> Line: 12 >> CONFUSABLE_IDENTIFIER >> The identifier "pοrt" has misleading characters and might be confused with something else. 0 1 diff --git a/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.gd b/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.gd index 18ea260fa2..dc4223ec2d 100644 --- a/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.gd +++ b/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.gd @@ -1,6 +1,5 @@ func test(): # The following statements should all be reported as standalone expressions: - "This is a standalone expression" 1234 0.0 + 0.0 Color(1, 1, 1) diff --git a/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.out b/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.out index 99ec87438e..a2c67a6e51 100644 --- a/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.out +++ b/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.out @@ -8,14 +8,10 @@ GDTEST_OK >> STANDALONE_EXPRESSION >> Standalone expression (the line has no effect). >> WARNING ->> Line: 5 +>> Line: 6 >> STANDALONE_EXPRESSION >> Standalone expression (the line has no effect). >> WARNING >> Line: 7 >> STANDALONE_EXPRESSION >> Standalone expression (the line has no effect). ->> WARNING ->> Line: 8 ->> STANDALONE_EXPRESSION ->> Standalone expression (the line has no effect). diff --git a/modules/gdscript/tests/scripts/runtime/assign_to_read_only_property.gd b/modules/gdscript/tests/scripts/runtime/assign_to_read_only_property.gd new file mode 100644 index 0000000000..19c4186622 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/assign_to_read_only_property.gd @@ -0,0 +1,7 @@ +func test(): + var state = PhysicsDirectBodyState3DExtension.new() + assign(state) + state.free() + +func assign(state): + state.center_of_mass.x -= 1.0 diff --git a/modules/gdscript/tests/scripts/runtime/assign_to_read_only_property.out b/modules/gdscript/tests/scripts/runtime/assign_to_read_only_property.out new file mode 100644 index 0000000000..c181c5dd02 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/assign_to_read_only_property.out @@ -0,0 +1,6 @@ +GDTEST_RUNTIME_ERROR +>> SCRIPT ERROR +>> on function: assign() +>> runtime/assign_to_read_only_property.gd +>> 7 +>> Cannot set value into property "center_of_mass" (on base "PhysicsDirectBodyState3DExtension") because it is read-only. diff --git a/modules/gdscript/tests/scripts/runtime/assign_to_read_only_property_with_variable_index.gd b/modules/gdscript/tests/scripts/runtime/assign_to_read_only_property_with_variable_index.gd new file mode 100644 index 0000000000..f15f580272 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/assign_to_read_only_property_with_variable_index.gd @@ -0,0 +1,8 @@ +func test(): + var state = PhysicsDirectBodyState3DExtension.new() + var prop = &"center_of_mass" + assign(state, prop) + state.free() + +func assign(state, prop): + state[prop].x = 1.0 diff --git a/modules/gdscript/tests/scripts/runtime/assign_to_read_only_property_with_variable_index.out b/modules/gdscript/tests/scripts/runtime/assign_to_read_only_property_with_variable_index.out new file mode 100644 index 0000000000..2cdc81aacc --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/assign_to_read_only_property_with_variable_index.out @@ -0,0 +1,6 @@ +GDTEST_RUNTIME_ERROR +>> SCRIPT ERROR +>> on function: assign() +>> runtime/assign_to_read_only_property_with_variable_index.gd +>> 8 +>> Cannot set value into property "center_of_mass" (on base "PhysicsDirectBodyState3DExtension") because it is read-only. diff --git a/modules/gltf/editor/editor_import_blend_runner.cpp b/modules/gltf/editor/editor_import_blend_runner.cpp index c203a91834..659a60e6a1 100644 --- a/modules/gltf/editor/editor_import_blend_runner.cpp +++ b/modules/gltf/editor/editor_import_blend_runner.cpp @@ -181,7 +181,18 @@ Error EditorImportBlendRunner::start_blender(const String &p_python_script, bool Error EditorImportBlendRunner::do_import(const Dictionary &p_options) { if (is_using_rpc()) { - return do_import_rpc(p_options); + Error err = do_import_rpc(p_options); + if (err != OK) { + // Retry without using RPC (slow, but better than the import failing completely). + if (err == ERR_CONNECTION_ERROR) { + // Disable RPC if the connection could not be established. + print_error(vformat("Failed to connect to Blender via RPC, switching to direct imports of .blend files. Check your proxy and firewall settings, then RPC can be re-enabled by changing the editor setting `filesystem/import/blender/rpc_port` to %d.", rpc_port)); + EditorSettings::get_singleton()->set_manually("filesystem/import/blender/rpc_port", 0); + rpc_port = 0; + } + err = do_import_direct(p_options); + } + return err; } else { return do_import_direct(p_options); } diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 1a09b5bdcc..028028a103 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -54,6 +54,9 @@ #include "modules/modules_enabled.gen.h" // For csg, gridmap. +#ifdef TOOLS_ENABLED +#include "editor/editor_file_system.h" +#endif #ifdef MODULE_CSG_ENABLED #include "modules/csg/csg_shape.h" #endif // MODULE_CSG_ENABLED @@ -3232,54 +3235,38 @@ Error GLTFDocument::_parse_images(Ref<GLTFState> p_state, const String &p_base_p p_state->source_images.push_back(Ref<Image>()); } else { Error err = OK; - bool must_import = false; + bool must_import = true; + Vector<uint8_t> img_data = img->get_data(); + Dictionary generator_parameters; String file_path = p_state->get_base_path() + "/" + p_state->filename.get_basename() + "_" + img->get_name() + ".png"; - if (!FileAccess::exists(file_path + ".import")) { + if (FileAccess::exists(file_path + ".import")) { Ref<ConfigFile> config; config.instantiate(); - config->set_value("remap", "importer", "texture"); - config->set_value("remap", "type", "Texture2D"); - // Currently, it will likely use project defaults of Detect 3D, so textures will be reimported again. - if (!config->has_section_key("params", "mipmaps/generate")) { - config->set_value("params", "mipmaps/generate", true); + config->load(file_path + ".import"); + if (config->has_section_key("remap", "generator_parameters")) { + generator_parameters = (Dictionary)config->get_value("remap", "generator_parameters"); } - - if (ProjectSettings::get_singleton()->has_setting("importer_defaults/texture")) { - //use defaults if exist - Dictionary importer_defaults = GLOBAL_GET("importer_defaults/texture"); - List<Variant> importer_def_keys; - importer_defaults.get_key_list(&importer_def_keys); - for (const Variant &key : importer_def_keys) { - if (!config->has_section_key("params", (String)key)) { - config->set_value("params", (String)key, importer_defaults[key]); - } - } + if (!generator_parameters.has("md5")) { + must_import = false; // Didn't come form a gltf document; don't overwrite. } - err = config->save(file_path + ".import"); - ERR_FAIL_COND_V(err != OK, err); - must_import = true; - } - Vector<uint8_t> png_buffer = img->save_png_to_buffer(); - if (ResourceLoader::exists(file_path)) { - Ref<FileAccess> file = FileAccess::open(file_path, FileAccess::READ, &err); - if (err == OK && file.is_valid()) { - Vector<uint8_t> orig_png_buffer = file->get_buffer(file->get_length()); - if (png_buffer != orig_png_buffer) { - must_import = true; - } + String existing_md5 = generator_parameters["md5"]; + unsigned char md5_hash[16]; + CryptoCore::md5(img_data.ptr(), img_data.size(), md5_hash); + String new_md5 = String::hex_encode_buffer(md5_hash, 16); + generator_parameters["md5"] = new_md5; + if (new_md5 == existing_md5) { + must_import = false; } - } else { - must_import = true; } if (must_import) { - Ref<FileAccess> file = FileAccess::open(file_path, FileAccess::WRITE, &err); + err = img->save_png(file_path); ERR_FAIL_COND_V(err != OK, err); - ERR_FAIL_COND_V(file.is_null(), FAILED); - file->store_buffer(png_buffer); - file->flush(); - file.unref(); // ResourceLoader::import will crash if not is_editor_hint(), so this case is protected above and will fall through to uncompressed. - ResourceLoader::import(file_path); + HashMap<StringName, Variant> custom_options; + custom_options[SNAME("mipmaps/generate")] = true; + // Will only use project settings defaults if custom_importer is empty. + EditorFileSystem::get_singleton()->update_file(file_path); + EditorFileSystem::get_singleton()->reimport_append(file_path, custom_options, String(), generator_parameters); } Ref<Texture2D> saved_image = ResourceLoader::load(file_path, "Texture2D"); if (saved_image.is_valid()) { @@ -4907,13 +4894,11 @@ Error GLTFDocument::_serialize_animations(Ref<GLTFState> p_state) { return OK; } for (int32_t player_i = 0; player_i < p_state->animation_players.size(); player_i++) { - List<StringName> animation_names; AnimationPlayer *animation_player = p_state->animation_players[player_i]; - animation_player->get_animation_list(&animation_names); - if (animation_names.size()) { - for (int animation_name_i = 0; animation_name_i < animation_names.size(); animation_name_i++) { - _convert_animation(p_state, animation_player, animation_names[animation_name_i]); - } + List<StringName> animations; + animation_player->get_animation_list(&animations); + for (StringName animation_name : animations) { + _convert_animation(p_state, animation_player, animation_name); } } Array animations; @@ -6041,7 +6026,7 @@ void GLTFDocument::_import_animation(Ref<GLTFState> p_state, AnimationPlayer *p_ Node *root = p_animation_player->get_parent(); ERR_FAIL_COND(root == nullptr); HashMap<GLTFNodeIndex, Node *>::Iterator node_element = p_state->scene_nodes.find(node_index); - ERR_CONTINUE_MSG(!node_element, vformat("Unable to find node %d for animation", node_index)); + ERR_CONTINUE_MSG(!node_element, vformat("Unable to find node %d for animation.", node_index)); node_path = root->get_path_to(node_element->value); HashMap<GLTFNodeIndex, ImporterMeshInstance3D *>::Iterator mesh_instance_element = p_state->scene_mesh_instances.find(node_index); if (mesh_instance_element) { @@ -6463,58 +6448,179 @@ GLTFAnimation::Track GLTFDocument::_convert_animation_track(Ref<GLTFState> p_sta for (int32_t key_i = 0; key_i < key_count; key_i++) { times.write[key_i] = p_animation->track_get_key_time(p_track_i, key_i); } + double anim_end = p_animation->get_length(); if (track_type == Animation::TYPE_SCALE_3D) { - p_track.scale_track.times = times; - p_track.scale_track.interpolation = gltf_interpolation; - p_track.scale_track.values.resize(key_count); - for (int32_t key_i = 0; key_i < key_count; key_i++) { - Vector3 scale; - Error err = p_animation->scale_track_get_key(p_track_i, key_i, &scale); - ERR_CONTINUE(err != OK); - p_track.scale_track.values.write[key_i] = scale; + if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { + gltf_interpolation = GLTFAnimation::INTERP_LINEAR; + p_track.scale_track.times.clear(); + p_track.scale_track.values.clear(); + // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. + const double increment = 1.0 / BAKE_FPS; + double time = 0.0; + bool last = false; + while (true) { + Vector3 scale; + Error err = p_animation->scale_track_interpolate(p_track_i, time, &scale); + ERR_CONTINUE(err != OK); + p_track.scale_track.values.push_back(scale); + p_track.scale_track.times.push_back(time); + if (last) { + break; + } + time += increment; + if (time >= anim_end) { + last = true; + time = anim_end; + } + } + } else { + p_track.scale_track.times = times; + p_track.scale_track.interpolation = gltf_interpolation; + p_track.scale_track.values.resize(key_count); + for (int32_t key_i = 0; key_i < key_count; key_i++) { + Vector3 scale; + Error err = p_animation->scale_track_get_key(p_track_i, key_i, &scale); + ERR_CONTINUE(err != OK); + p_track.scale_track.values.write[key_i] = scale; + } } } else if (track_type == Animation::TYPE_POSITION_3D) { - p_track.position_track.times = times; - p_track.position_track.values.resize(key_count); - p_track.position_track.interpolation = gltf_interpolation; - for (int32_t key_i = 0; key_i < key_count; key_i++) { - Vector3 position; - Error err = p_animation->position_track_get_key(p_track_i, key_i, &position); - ERR_CONTINUE(err != OK); - p_track.position_track.values.write[key_i] = position; + if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { + gltf_interpolation = GLTFAnimation::INTERP_LINEAR; + p_track.position_track.times.clear(); + p_track.position_track.values.clear(); + // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. + const double increment = 1.0 / BAKE_FPS; + double time = 0.0; + bool last = false; + while (true) { + Vector3 scale; + Error err = p_animation->position_track_interpolate(p_track_i, time, &scale); + ERR_CONTINUE(err != OK); + p_track.position_track.values.push_back(scale); + p_track.position_track.times.push_back(time); + if (last) { + break; + } + time += increment; + if (time >= anim_end) { + last = true; + time = anim_end; + } + } + } else { + p_track.position_track.times = times; + p_track.position_track.values.resize(key_count); + p_track.position_track.interpolation = gltf_interpolation; + for (int32_t key_i = 0; key_i < key_count; key_i++) { + Vector3 position; + Error err = p_animation->position_track_get_key(p_track_i, key_i, &position); + ERR_CONTINUE(err != OK); + p_track.position_track.values.write[key_i] = position; + } } } else if (track_type == Animation::TYPE_ROTATION_3D) { - p_track.rotation_track.times = times; - p_track.rotation_track.interpolation = gltf_interpolation; - p_track.rotation_track.values.resize(key_count); - for (int32_t key_i = 0; key_i < key_count; key_i++) { - Quaternion rotation; - Error err = p_animation->rotation_track_get_key(p_track_i, key_i, &rotation); - ERR_CONTINUE(err != OK); - p_track.rotation_track.values.write[key_i] = rotation; + if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { + gltf_interpolation = GLTFAnimation::INTERP_LINEAR; + p_track.rotation_track.times.clear(); + p_track.rotation_track.values.clear(); + // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. + const double increment = 1.0 / BAKE_FPS; + double time = 0.0; + bool last = false; + while (true) { + Quaternion rotation; + Error err = p_animation->rotation_track_interpolate(p_track_i, time, &rotation); + ERR_CONTINUE(err != OK); + p_track.rotation_track.values.push_back(rotation); + p_track.rotation_track.times.push_back(time); + if (last) { + break; + } + time += increment; + if (time >= anim_end) { + last = true; + time = anim_end; + } + } + } else { + p_track.rotation_track.times = times; + p_track.rotation_track.values.resize(key_count); + p_track.rotation_track.interpolation = gltf_interpolation; + for (int32_t key_i = 0; key_i < key_count; key_i++) { + Quaternion rotation; + Error err = p_animation->rotation_track_get_key(p_track_i, key_i, &rotation); + ERR_CONTINUE(err != OK); + p_track.rotation_track.values.write[key_i] = rotation; + } } } else if (track_type == Animation::TYPE_VALUE) { if (path.contains(":position")) { - p_track.position_track.times = times; p_track.position_track.interpolation = gltf_interpolation; - + p_track.position_track.times = times; p_track.position_track.values.resize(key_count); - p_track.position_track.interpolation = gltf_interpolation; - for (int32_t key_i = 0; key_i < key_count; key_i++) { - Vector3 position = p_animation->track_get_key_value(p_track_i, key_i); - p_track.position_track.values.write[key_i] = position; + if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { + gltf_interpolation = GLTFAnimation::INTERP_LINEAR; + p_track.position_track.times.clear(); + p_track.position_track.values.clear(); + // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. + const double increment = 1.0 / BAKE_FPS; + double time = 0.0; + bool last = false; + while (true) { + Vector3 position; + Error err = p_animation->position_track_interpolate(p_track_i, time, &position); + ERR_CONTINUE(err != OK); + p_track.position_track.values.push_back(position); + p_track.position_track.times.push_back(time); + if (last) { + break; + } + time += increment; + if (time >= anim_end) { + last = true; + time = anim_end; + } + } + } else { + for (int32_t key_i = 0; key_i < key_count; key_i++) { + Vector3 position = p_animation->track_get_key_value(p_track_i, key_i); + p_track.position_track.values.write[key_i] = position; + } } } else if (path.contains(":rotation")) { - p_track.rotation_track.times = times; p_track.rotation_track.interpolation = gltf_interpolation; - + p_track.rotation_track.times = times; p_track.rotation_track.values.resize(key_count); - p_track.rotation_track.interpolation = gltf_interpolation; - - for (int32_t key_i = 0; key_i < key_count; key_i++) { - Vector3 rotation_radian = p_animation->track_get_key_value(p_track_i, key_i); - p_track.rotation_track.values.write[key_i] = Quaternion::from_euler(rotation_radian); + if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { + gltf_interpolation = GLTFAnimation::INTERP_LINEAR; + p_track.rotation_track.times.clear(); + p_track.rotation_track.values.clear(); + // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. + const double increment = 1.0 / BAKE_FPS; + double time = 0.0; + bool last = false; + while (true) { + Quaternion rotation; + Error err = p_animation->rotation_track_interpolate(p_track_i, time, &rotation); + ERR_CONTINUE(err != OK); + p_track.rotation_track.values.push_back(rotation); + p_track.rotation_track.times.push_back(time); + if (last) { + break; + } + time += increment; + if (time >= anim_end) { + last = true; + time = anim_end; + } + } + } else { + for (int32_t key_i = 0; key_i < key_count; key_i++) { + Vector3 rotation_radian = p_animation->track_get_key_value(p_track_i, key_i); + p_track.rotation_track.values.write[key_i] = Quaternion::from_euler(rotation_radian); + } } } else if (path.contains(":scale")) { p_track.scale_track.times = times; @@ -6523,68 +6629,115 @@ GLTFAnimation::Track GLTFDocument::_convert_animation_track(Ref<GLTFState> p_sta p_track.scale_track.values.resize(key_count); p_track.scale_track.interpolation = gltf_interpolation; - for (int32_t key_i = 0; key_i < key_count; key_i++) { - Vector3 scale_track = p_animation->track_get_key_value(p_track_i, key_i); - p_track.scale_track.values.write[key_i] = scale_track; + if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { + gltf_interpolation = GLTFAnimation::INTERP_LINEAR; + p_track.scale_track.times.clear(); + p_track.scale_track.values.clear(); + // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. + const double increment = 1.0 / BAKE_FPS; + double time = 0.0; + bool last = false; + while (true) { + Vector3 scale; + Error err = p_animation->scale_track_interpolate(p_track_i, time, &scale); + ERR_CONTINUE(err != OK); + p_track.scale_track.values.push_back(scale); + p_track.scale_track.times.push_back(time); + if (last) { + break; + } + time += increment; + if (time >= anim_end) { + last = true; + time = anim_end; + } + } + } else { + for (int32_t key_i = 0; key_i < key_count; key_i++) { + Vector3 scale_track = p_animation->track_get_key_value(p_track_i, key_i); + p_track.scale_track.values.write[key_i] = scale_track; + } } } } else if (track_type == Animation::TYPE_BEZIER) { - if (path.contains("/scale")) { - const int32_t keys = p_animation->track_get_key_time(p_track_i, key_count - 1) * BAKE_FPS; + const int32_t keys = anim_end * BAKE_FPS; + if (path.contains(":scale")) { if (!p_track.scale_track.times.size()) { + p_track.scale_track.interpolation = gltf_interpolation; Vector<real_t> new_times; new_times.resize(keys); for (int32_t key_i = 0; key_i < keys; key_i++) { new_times.write[key_i] = key_i / BAKE_FPS; } p_track.scale_track.times = new_times; - p_track.scale_track.interpolation = gltf_interpolation; p_track.scale_track.values.resize(keys); for (int32_t key_i = 0; key_i < keys; key_i++) { p_track.scale_track.values.write[key_i] = Vector3(1.0f, 1.0f, 1.0f); } - p_track.scale_track.interpolation = gltf_interpolation; - } - for (int32_t key_i = 0; key_i < keys; key_i++) { - Vector3 bezier_track = p_track.scale_track.values[key_i]; - if (path.contains("/scale:x")) { - bezier_track.x = p_animation->bezier_track_interpolate(p_track_i, key_i / BAKE_FPS); - } else if (path.contains("/scale:y")) { - bezier_track.y = p_animation->bezier_track_interpolate(p_track_i, key_i / BAKE_FPS); - } else if (path.contains("/scale:z")) { - bezier_track.z = p_animation->bezier_track_interpolate(p_track_i, key_i / BAKE_FPS); + for (int32_t key_i = 0; key_i < keys; key_i++) { + Vector3 bezier_track = p_track.scale_track.values[key_i]; + if (path.contains(":scale:x")) { + bezier_track.x = p_animation->bezier_track_interpolate(p_track_i, key_i / BAKE_FPS); + } else if (path.contains(":scale:y")) { + bezier_track.y = p_animation->bezier_track_interpolate(p_track_i, key_i / BAKE_FPS); + } else if (path.contains(":scale:z")) { + bezier_track.z = p_animation->bezier_track_interpolate(p_track_i, key_i / BAKE_FPS); + } + p_track.scale_track.values.write[key_i] = bezier_track; } - p_track.scale_track.values.write[key_i] = bezier_track; } - } else if (path.contains("/position")) { - const int32_t keys = p_animation->track_get_key_time(p_track_i, key_count - 1) * BAKE_FPS; + } else if (path.contains(":position")) { if (!p_track.position_track.times.size()) { + p_track.position_track.interpolation = gltf_interpolation; Vector<real_t> new_times; new_times.resize(keys); for (int32_t key_i = 0; key_i < keys; key_i++) { new_times.write[key_i] = key_i / BAKE_FPS; } p_track.position_track.times = new_times; - p_track.position_track.interpolation = gltf_interpolation; p_track.position_track.values.resize(keys); - p_track.position_track.interpolation = gltf_interpolation; } for (int32_t key_i = 0; key_i < keys; key_i++) { Vector3 bezier_track = p_track.position_track.values[key_i]; - if (path.contains("/position:x")) { + if (path.contains(":position:x")) { bezier_track.x = p_animation->bezier_track_interpolate(p_track_i, key_i / BAKE_FPS); - } else if (path.contains("/position:y")) { + } else if (path.contains(":position:y")) { bezier_track.y = p_animation->bezier_track_interpolate(p_track_i, key_i / BAKE_FPS); - } else if (path.contains("/position:z")) { + } else if (path.contains(":position:z")) { bezier_track.z = p_animation->bezier_track_interpolate(p_track_i, key_i / BAKE_FPS); } p_track.position_track.values.write[key_i] = bezier_track; } + } else if (path.contains(":rotation")) { + if (!p_track.rotation_track.times.size()) { + p_track.rotation_track.interpolation = gltf_interpolation; + Vector<real_t> new_times; + new_times.resize(keys); + for (int32_t key_i = 0; key_i < keys; key_i++) { + new_times.write[key_i] = key_i / BAKE_FPS; + } + p_track.rotation_track.times = new_times; + + p_track.rotation_track.values.resize(keys); + } + for (int32_t key_i = 0; key_i < keys; key_i++) { + Quaternion bezier_track = p_track.rotation_track.values[key_i]; + if (path.contains(":rotation:x")) { + bezier_track.x = p_animation->bezier_track_interpolate(p_track_i, key_i / BAKE_FPS); + } else if (path.contains(":rotation:y")) { + bezier_track.y = p_animation->bezier_track_interpolate(p_track_i, key_i / BAKE_FPS); + } else if (path.contains(":rotation:z")) { + bezier_track.z = p_animation->bezier_track_interpolate(p_track_i, key_i / BAKE_FPS); + } else if (path.contains(":rotation:w")) { + bezier_track.w = p_animation->bezier_track_interpolate(p_track_i, key_i / BAKE_FPS); + } + p_track.rotation_track.values.write[key_i] = bezier_track; + } } } return p_track; @@ -6595,16 +6748,18 @@ void GLTFDocument::_convert_animation(Ref<GLTFState> p_state, AnimationPlayer *p Ref<GLTFAnimation> gltf_animation; gltf_animation.instantiate(); gltf_animation->set_name(_gen_unique_name(p_state, p_animation_track_name)); - for (int32_t track_i = 0; track_i < animation->get_track_count(); track_i++) { if (!animation->track_is_enabled(track_i)) { continue; } - String orig_track_path = animation->track_get_path(track_i); - if (String(orig_track_path).contains(":position")) { - const Vector<String> node_suffix = String(orig_track_path).split(":position"); + String final_track_path = animation->track_get_path(track_i); + Node *animation_base_node = p_animation_player->get_parent(); + ERR_CONTINUE_MSG(!animation_base_node, "Cannot get the parent of the animation player."); + if (String(final_track_path).contains(":position")) { + const Vector<String> node_suffix = String(final_track_path).split(":position"); const NodePath path = node_suffix[0]; - const Node *node = p_animation_player->get_parent()->get_node_or_null(path); + const Node *node = animation_base_node->get_node_or_null(path); + ERR_CONTINUE_MSG(!node, "Cannot get the node from a position path."); for (const KeyValue<GLTFNodeIndex, Node *> &position_scene_node_i : p_state->scene_nodes) { if (position_scene_node_i.value == node) { GLTFNodeIndex node_index = position_scene_node_i.key; @@ -6617,10 +6772,11 @@ void GLTFDocument::_convert_animation(Ref<GLTFState> p_state, AnimationPlayer *p gltf_animation->get_tracks().insert(node_index, track); } } - } else if (String(orig_track_path).contains(":rotation_degrees")) { - const Vector<String> node_suffix = String(orig_track_path).split(":rotation_degrees"); + } else if (String(final_track_path).contains(":rotation_degrees")) { + const Vector<String> node_suffix = String(final_track_path).split(":rotation_degrees"); const NodePath path = node_suffix[0]; - const Node *node = p_animation_player->get_parent()->get_node_or_null(path); + const Node *node = animation_base_node->get_node_or_null(path); + ERR_CONTINUE_MSG(!node, "Cannot get the node from a rotation degrees path."); for (const KeyValue<GLTFNodeIndex, Node *> &rotation_degree_scene_node_i : p_state->scene_nodes) { if (rotation_degree_scene_node_i.value == node) { GLTFNodeIndex node_index = rotation_degree_scene_node_i.key; @@ -6633,10 +6789,11 @@ void GLTFDocument::_convert_animation(Ref<GLTFState> p_state, AnimationPlayer *p gltf_animation->get_tracks().insert(node_index, track); } } - } else if (String(orig_track_path).contains(":scale")) { - const Vector<String> node_suffix = String(orig_track_path).split(":scale"); + } else if (String(final_track_path).contains(":scale")) { + const Vector<String> node_suffix = String(final_track_path).split(":scale"); const NodePath path = node_suffix[0]; - const Node *node = p_animation_player->get_parent()->get_node_or_null(path); + const Node *node = animation_base_node->get_node_or_null(path); + ERR_CONTINUE_MSG(!node, "Cannot get the node from a scale path."); for (const KeyValue<GLTFNodeIndex, Node *> &scale_scene_node_i : p_state->scene_nodes) { if (scale_scene_node_i.value == node) { GLTFNodeIndex node_index = scale_scene_node_i.key; @@ -6649,10 +6806,11 @@ void GLTFDocument::_convert_animation(Ref<GLTFState> p_state, AnimationPlayer *p gltf_animation->get_tracks().insert(node_index, track); } } - } else if (String(orig_track_path).contains(":transform")) { - const Vector<String> node_suffix = String(orig_track_path).split(":transform"); + } else if (String(final_track_path).contains(":transform")) { + const Vector<String> node_suffix = String(final_track_path).split(":transform"); const NodePath path = node_suffix[0]; - const Node *node = p_animation_player->get_parent()->get_node_or_null(path); + const Node *node = animation_base_node->get_node_or_null(path); + ERR_CONTINUE_MSG(!node, "Cannot get the node from a transform path."); for (const KeyValue<GLTFNodeIndex, Node *> &transform_track_i : p_state->scene_nodes) { if (transform_track_i.value == node) { GLTFAnimation::Track track; @@ -6660,12 +6818,16 @@ void GLTFDocument::_convert_animation(Ref<GLTFState> p_state, AnimationPlayer *p gltf_animation->get_tracks().insert(transform_track_i.key, track); } } - } else if (String(orig_track_path).contains(":") && animation->track_get_type(track_i) == Animation::TYPE_BLEND_SHAPE) { - const Vector<String> node_suffix = String(orig_track_path).split(":"); + } else if (String(final_track_path).contains(":") && animation->track_get_type(track_i) == Animation::TYPE_BLEND_SHAPE) { + const Vector<String> node_suffix = String(final_track_path).split(":"); const NodePath path = node_suffix[0]; const String suffix = node_suffix[1]; - Node *node = p_animation_player->get_parent()->get_node_or_null(path); + Node *node = animation_base_node->get_node_or_null(path); + ERR_CONTINUE_MSG(!node, "Cannot get the node from a blend shape path."); MeshInstance3D *mi = cast_to<MeshInstance3D>(node); + if (!mi) { + continue; + } Ref<Mesh> mesh = mi->get_mesh(); ERR_CONTINUE(mesh.is_null()); int32_t mesh_index = -1; @@ -6716,14 +6878,20 @@ void GLTFDocument::_convert_animation(Ref<GLTFState> p_state, AnimationPlayer *p } tracks[mesh_index] = track; } - } else if (String(orig_track_path).contains(":")) { + } else if (String(final_track_path).contains(":")) { //Process skeleton - const Vector<String> node_suffix = String(orig_track_path).split(":"); + const Vector<String> node_suffix = String(final_track_path).split(":"); const String node = node_suffix[0]; const NodePath node_path = node; const String suffix = node_suffix[1]; - Node *godot_node = p_animation_player->get_parent()->get_node_or_null(node_path); - Skeleton3D *skeleton = nullptr; + Node *godot_node = animation_base_node->get_node_or_null(node_path); + if (!godot_node) { + continue; + } + Skeleton3D *skeleton = cast_to<Skeleton3D>(animation_base_node->get_node_or_null(node)); + if (!skeleton) { + continue; + } GLTFSkeletonIndex skeleton_gltf_i = -1; for (GLTFSkeletonIndex skeleton_i = 0; skeleton_i < p_state->skeletons.size(); skeleton_i++) { if (p_state->skeletons[skeleton_i]->godot_skeleton == cast_to<Skeleton3D>(godot_node)) { @@ -6732,7 +6900,7 @@ void GLTFDocument::_convert_animation(Ref<GLTFState> p_state, AnimationPlayer *p ERR_CONTINUE(!skeleton); Ref<GLTFSkeleton> skeleton_gltf = p_state->skeletons[skeleton_gltf_i]; int32_t bone = skeleton->find_bone(suffix); - ERR_CONTINUE(bone == -1); + ERR_CONTINUE_MSG(bone == -1, vformat("Cannot find the bone %s.", suffix)); if (!skeleton_gltf->godot_bone_node.has(bone)) { continue; } @@ -6746,9 +6914,10 @@ void GLTFDocument::_convert_animation(Ref<GLTFState> p_state, AnimationPlayer *p gltf_animation->get_tracks()[node_i] = track; } } - } else if (!String(orig_track_path).contains(":")) { - ERR_CONTINUE(!p_animation_player->get_parent()); - Node *godot_node = p_animation_player->get_parent()->get_node_or_null(orig_track_path); + } else if (!String(final_track_path).contains(":")) { + ERR_CONTINUE(!animation_base_node); + Node *godot_node = animation_base_node->get_node_or_null(final_track_path); + ERR_CONTINUE_MSG(!godot_node, vformat("Cannot get the node from a skeleton path %s.", final_track_path)); for (const KeyValue<GLTFNodeIndex, Node *> &scene_node_i : p_state->scene_nodes) { if (scene_node_i.value == godot_node) { GLTFNodeIndex node_i = scene_node_i.key; diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index b8b989bf89..ae19f67390 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -364,8 +364,7 @@ public: Ref<GLTFNode> p_gltf_node); GLTFMeshIndex _convert_mesh_to_gltf(Ref<GLTFState> p_state, MeshInstance3D *p_mesh_instance); - void _convert_animation(Ref<GLTFState> p_state, AnimationPlayer *p_animation_player, - String p_animation_track_name); + void _convert_animation(Ref<GLTFState> p_state, AnimationPlayer *p_animation_player, String p_animation_track_name); Error _serialize(Ref<GLTFState> p_state, const String &p_path); Error _parse(Ref<GLTFState> p_state, String p_path, Ref<FileAccess> p_file); }; diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs index a4d5e1a569..1e4fd2f09a 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs @@ -22,8 +22,7 @@ namespace GodotTools.Build // TODO Use List once we have proper serialization public Godot.Collections.Array CustomProperties { get; private set; } = new(); - public string LogsDirPath => - Path.Combine(GodotSharpDirs.BuildLogsDirs, $"{Solution.Md5Text()}_{Configuration}"); + public string LogsDirPath => GodotSharpDirs.LogsDirPathFor(Solution, Configuration); public override bool Equals(object? obj) { diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index ae0ffaf4cb..d6549c1b70 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using Godot; using GodotTools.BuildLogger; using GodotTools.Utils; @@ -22,9 +23,11 @@ namespace GodotTools.Build if (dotnetPath == null) throw new FileNotFoundException("Cannot find the dotnet executable."); + var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); + var startInfo = new ProcessStartInfo(dotnetPath); - BuildArguments(buildInfo, startInfo.ArgumentList); + BuildArguments(buildInfo, startInfo.ArgumentList, editorSettings); string launchMessage = startInfo.GetCommandLineDisplay(new StringBuilder("Running: ")).ToString(); stdOutHandler?.Invoke(launchMessage); @@ -35,6 +38,8 @@ namespace GodotTools.Build startInfo.RedirectStandardError = true; startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; + startInfo.EnvironmentVariables["DOTNET_CLI_UI_LANGUAGE"] + = ((string)editorSettings.GetSetting("interface/editor/editor_language")).Replace('_', '-'); // Needed when running from Developer Command Prompt for VS RemovePlatformVariable(startInfo.EnvironmentVariables); @@ -83,9 +88,11 @@ namespace GodotTools.Build if (dotnetPath == null) throw new FileNotFoundException("Cannot find the dotnet executable."); + var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); + var startInfo = new ProcessStartInfo(dotnetPath); - BuildPublishArguments(buildInfo, startInfo.ArgumentList); + BuildPublishArguments(buildInfo, startInfo.ArgumentList, editorSettings); string launchMessage = startInfo.GetCommandLineDisplay(new StringBuilder("Running: ")).ToString(); stdOutHandler?.Invoke(launchMessage); @@ -95,6 +102,8 @@ namespace GodotTools.Build startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = true; startInfo.UseShellExecute = false; + startInfo.EnvironmentVariables["DOTNET_CLI_UI_LANGUAGE"] + = ((string)editorSettings.GetSetting("interface/editor/editor_language")).Replace('_', '-'); // Needed when running from Developer Command Prompt for VS RemovePlatformVariable(startInfo.EnvironmentVariables); @@ -124,7 +133,8 @@ namespace GodotTools.Build } } - private static void BuildArguments(BuildInfo buildInfo, Collection<string> arguments) + private static void BuildArguments(BuildInfo buildInfo, Collection<string> arguments, + EditorSettings editorSettings) { // `dotnet clean` / `dotnet build` commands arguments.Add(buildInfo.OnlyClean ? "clean" : "build"); @@ -150,12 +160,14 @@ namespace GodotTools.Build arguments.Add(buildInfo.Configuration); // Verbosity - arguments.Add("-v"); - arguments.Add("normal"); + AddVerbosityArguments(buildInfo, arguments, editorSettings); // Logger AddLoggerArgument(buildInfo, arguments); + // Binary log + AddBinaryLogArgument(buildInfo, arguments, editorSettings); + // Custom properties foreach (var customProperty in buildInfo.CustomProperties) { @@ -163,7 +175,8 @@ namespace GodotTools.Build } } - private static void BuildPublishArguments(BuildInfo buildInfo, Collection<string> arguments) + private static void BuildPublishArguments(BuildInfo buildInfo, Collection<string> arguments, + EditorSettings editorSettings) { arguments.Add("publish"); // `dotnet publish` command @@ -193,12 +206,14 @@ namespace GodotTools.Build arguments.Add("true"); // Verbosity - arguments.Add("-v"); - arguments.Add("normal"); + AddVerbosityArguments(buildInfo, arguments, editorSettings); // Logger AddLoggerArgument(buildInfo, arguments); + // Binary log + AddBinaryLogArgument(buildInfo, arguments, editorSettings); + // Custom properties foreach (var customProperty in buildInfo.CustomProperties) { @@ -213,6 +228,25 @@ namespace GodotTools.Build } } + private static void AddVerbosityArguments(BuildInfo buildInfo, Collection<string> arguments, + EditorSettings editorSettings) + { + var verbosityLevel = + editorSettings.GetSetting(GodotSharpEditor.Settings.VerbosityLevel).As<VerbosityLevelId>(); + arguments.Add("-v"); + arguments.Add(verbosityLevel switch + { + VerbosityLevelId.Quiet => "quiet", + VerbosityLevelId.Minimal => "minimal", + VerbosityLevelId.Detailed => "detailed", + VerbosityLevelId.Diagnostic => "diagnostic", + _ => "normal", + }); + + if ((bool)editorSettings.GetSetting(GodotSharpEditor.Settings.NoConsoleLogging)) + arguments.Add("-noconlog"); + } + private static void AddLoggerArgument(BuildInfo buildInfo, Collection<string> arguments) { string buildLoggerPath = Path.Combine(Internals.GodotSharpDirs.DataEditorToolsDir, @@ -222,6 +256,16 @@ namespace GodotTools.Build $"-l:{typeof(GodotBuildLogger).FullName},{buildLoggerPath};{buildInfo.LogsDirPath}"); } + private static void AddBinaryLogArgument(BuildInfo buildInfo, Collection<string> arguments, + EditorSettings editorSettings) + { + if (!(bool)editorSettings.GetSetting(GodotSharpEditor.Settings.CreateBinaryLog)) + return; + + arguments.Add($"-bl:{Path.Combine(buildInfo.LogsDirPath, "msbuild.binlog")}"); + arguments.Add("-ds:False"); // Honestly never understood why -bl also switches -ds on. + } + private static void RemovePlatformVariable(StringDictionary environmentVariables) { // EnvironmentVariables is case sensitive? Seriously? diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs index 262de024ca..cf1b84e37f 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Godot; using GodotTools.Internals; using static GodotTools.Internals.Globals; @@ -14,6 +15,7 @@ namespace GodotTools.Build private Button _errorsBtn; private Button _warningsBtn; private Button _viewLogBtn; + private Button _openLogsFolderBtn; private void WarningsToggled(bool pressed) { @@ -93,6 +95,10 @@ namespace GodotTools.Build private void ViewLogToggled(bool pressed) => BuildOutputView.LogVisible = pressed; + private void OpenLogsFolderPressed() => OS.ShellOpen( + $"file://{GodotSharpDirs.LogsDirPathFor("Debug")}" + ); + private void BuildMenuOptionPressed(long id) { switch ((BuildMenuOptions)id) @@ -171,6 +177,22 @@ namespace GodotTools.Build _viewLogBtn.Toggled += ViewLogToggled; toolBarHBox.AddChild(_viewLogBtn); + // Horizontal spacer, push everything to the right. + toolBarHBox.AddChild(new Control + { + SizeFlagsHorizontal = SizeFlags.ExpandFill, + }); + + _openLogsFolderBtn = new Button + { + Text = "Show Logs in File Manager".TTR(), + Icon = GetThemeIcon("Filesystem", "EditorIcons"), + ExpandIcon = false, + FocusMode = FocusModeEnum.None, + }; + _openLogsFolderBtn.Pressed += OpenLogsFolderPressed; + toolBarHBox.AddChild(_openLogsFolderBtn); + BuildOutputView = new BuildOutputView(); AddChild(BuildOutputView); } diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index 019504ad66..a284451a35 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -24,19 +24,7 @@ namespace GodotTools.Export public void RegisterExportSettings() { // TODO: These would be better as export preset options, but that doesn't seem to be supported yet - - GlobalDef("mono/export/include_scripts_content", false); - - GlobalDef("mono/export/aot/enabled", false); - GlobalDef("mono/export/aot/full_aot", false); - GlobalDef("mono/export/aot/use_interpreter", true); - - // --aot or --aot=opt1,opt2 (use 'mono --aot=help AuxAssembly.dll' to list AOT options) - GlobalDef("mono/export/aot/extra_aot_options", Array.Empty<string>()); - // --optimize/-O=opt1,opt2 (use 'mono --list-opt'' to list optimize options) - GlobalDef("mono/export/aot/extra_optimizer_options", Array.Empty<string>()); - - GlobalDef("mono/export/aot/android_toolchain_path", ""); + GlobalDef("dotnet/export/include_scripts_content", false); } private string _maybeLastExportError; @@ -56,7 +44,7 @@ namespace GodotTools.Export // TODO What if the source file is not part of the game's C# project - bool includeScriptsContent = (bool)ProjectSettings.GetSetting("mono/export/include_scripts_content"); + bool includeScriptsContent = (bool)ProjectSettings.GetSetting("dotnet/export/include_scripts_content"); if (!includeScriptsContent) { @@ -185,7 +173,9 @@ namespace GodotTools.Export foreach (string file in Directory.GetFiles(publishOutputTempDir, "*", SearchOption.AllDirectories)) { - AddSharedObject(file, tags: null, projectDataDirName); + AddSharedObject(file, tags: null, + Path.Join(projectDataDirName, + Path.GetRelativePath(publishOutputTempDir, Path.GetDirectoryName(file)))); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index 5abbe8752c..43ead4af69 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -22,6 +22,14 @@ namespace GodotTools { public partial class GodotSharpEditor : EditorPlugin, ISerializationListener { + public static class Settings + { + public const string ExternalEditor = "dotnet/editor/external_editor"; + public const string VerbosityLevel = "dotnet/build/verbosity_level"; + public const string NoConsoleLogging = "dotnet/build/no_console_logging"; + public const string CreateBinaryLog = "dotnet/build/create_binary_log"; + } + private EditorSettings _editorSettings; private PopupMenu _menuPopup; @@ -171,7 +179,7 @@ namespace GodotTools [UsedImplicitly] public Error OpenInExternalEditor(Script script, int line, int col) { - var editorId = (ExternalEditorId)(int)_editorSettings.GetSetting("mono/editor/external_editor"); + var editorId = _editorSettings.GetSetting(Settings.ExternalEditor).As<ExternalEditorId>(); switch (editorId) { @@ -323,8 +331,7 @@ namespace GodotTools [UsedImplicitly] public bool OverridesExternalEditor() { - return (ExternalEditorId)(int)_editorSettings.GetSetting("mono/editor/external_editor") != - ExternalEditorId.None; + return _editorSettings.GetSetting(Settings.ExternalEditor).As<ExternalEditorId>() != ExternalEditorId.None; } public override bool _Build() @@ -453,7 +460,10 @@ namespace GodotTools _menuPopup.IdPressed += _MenuOptionPressed; // External editor settings - EditorDef("mono/editor/external_editor", Variant.From(ExternalEditorId.None)); + EditorDef(Settings.ExternalEditor, Variant.From(ExternalEditorId.None)); + EditorDef(Settings.VerbosityLevel, Variant.From(VerbosityLevelId.Normal)); + EditorDef(Settings.NoConsoleLogging, false); + EditorDef(Settings.CreateBinaryLog, false); string settingsHintStr = "Disabled"; @@ -481,11 +491,23 @@ namespace GodotTools _editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary { ["type"] = (int)Variant.Type.Int, - ["name"] = "mono/editor/external_editor", + ["name"] = Settings.ExternalEditor, ["hint"] = (int)PropertyHint.Enum, ["hint_string"] = settingsHintStr }); + var verbosityLevels = Enum.GetValues<VerbosityLevelId>().Select(level => $"{Enum.GetName(level)}:{(int)level}"); + _editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary + { + ["type"] = (int)Variant.Type.Int, + ["name"] = Settings.VerbosityLevel, + ["hint"] = (int)PropertyHint.Enum, + ["hint_string"] = string.Join(",", verbosityLevels), + }); + + OnSettingsChanged(); + _editorSettings.SettingsChanged += OnSettingsChanged; + // Export plugin var exportPlugin = new ExportPlugin(); AddExportPlugin(exportPlugin); @@ -510,6 +532,24 @@ namespace GodotTools AddChild(GodotIdeManager); } + public override void _DisablePlugin() + { + base._DisablePlugin(); + + _editorSettings.SettingsChanged -= OnSettingsChanged; + } + + private void OnSettingsChanged() + { + // We want to force NoConsoleLogging to true when the VerbosityLevel is at Detailed or above. + // At that point, there's so much info logged that it doesn't make sense to display it in + // the tiny editor window, and it'd make the editor hang or crash anyway. + var verbosityLevel = _editorSettings.GetSetting(Settings.VerbosityLevel).As<VerbosityLevelId>(); + var hideConsoleLog = (bool)_editorSettings.GetSetting(Settings.NoConsoleLogging); + if (verbosityLevel >= VerbosityLevelId.Detailed && !hideConsoleLog) + _editorSettings.SetSetting(Settings.NoConsoleLogging, Variant.From(true)); + } + protected override void Dispose(bool disposing) { if (disposing) diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs index 5de2c9833b..83621ce5af 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs @@ -21,7 +21,8 @@ namespace GodotTools.Ides return _messagingServer; _messagingServer?.Dispose(); - _messagingServer = new MessagingServer(OS.GetExecutablePath(), ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir), new GodotLogger()); + _messagingServer = new MessagingServer(OS.GetExecutablePath(), + ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir), new GodotLogger()); _ = _messagingServer.Listen(); @@ -76,8 +77,8 @@ namespace GodotTools.Ides public async Task<EditorPick?> LaunchIdeAsync(int millisecondsTimeout = 10000) { - var editorId = (ExternalEditorId)(int)GodotSharpEditor.Instance.GetEditorInterface() - .GetEditorSettings().GetSetting("mono/editor/external_editor"); + var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); + var editorId = editorSettings.GetSetting(GodotSharpEditor.Settings.ExternalEditor).As<ExternalEditorId>(); string editorIdentity = GetExternalEditorIdentity(editorId); var runningServer = GetRunningOrNewServer(); diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs index 60602a5847..f55ca4c7d7 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs @@ -9,7 +9,7 @@ namespace GodotTools.Ides.Rider { public static class RiderPathManager { - public static readonly string EditorPathSettingName = "mono/editor/editor_path_optional"; + public static readonly string EditorPathSettingName = "dotnet/editor/editor_path_optional"; private static string GetRiderPathFromSettings() { @@ -22,7 +22,7 @@ namespace GodotTools.Ides.Rider public static void Initialize() { var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var editor = (ExternalEditorId)(int)editorSettings.GetSetting("mono/editor/external_editor"); + var editor = editorSettings.GetSetting(GodotSharpEditor.Settings.ExternalEditor).As<ExternalEditorId>(); if (editor == ExternalEditorId.Rider) { if (!editorSettings.HasSetting(EditorPathSettingName)) diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs index 7624989092..fb68fcbae6 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs @@ -115,5 +115,11 @@ namespace GodotTools.Internals return _projectCsProjPath; } } + + public static string LogsDirPathFor(string solution, string configuration) + => Path.Combine(BuildLogsDirs, $"{solution.Md5Text()}_{configuration}"); + + public static string LogsDirPathFor(string configuration) + => LogsDirPathFor(ProjectSlnPath, configuration); } } diff --git a/modules/mono/editor/GodotTools/GodotTools/VerbosityLevelId.cs b/modules/mono/editor/GodotTools/GodotTools/VerbosityLevelId.cs new file mode 100644 index 0000000000..0e1afe6bbf --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/VerbosityLevelId.cs @@ -0,0 +1,11 @@ +namespace GodotTools +{ + public enum VerbosityLevelId : long + { + Quiet, + Minimal, + Normal, + Detailed, + Diagnostic, + } +} diff --git a/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs b/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs index 344b76a202..93baf4e51c 100644 --- a/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs +++ b/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs @@ -21,6 +21,26 @@ namespace GodotPlugins _resolver = new AssemblyDependencyResolver(pluginPath); _sharedAssemblies = sharedAssemblies; _mainLoadContext = mainLoadContext; + + if (string.IsNullOrEmpty(AppContext.BaseDirectory)) + { + // See https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/AppContext.AnyOS.cs#L17-L35 + // but Assembly.Location is unavailable, because we load assemblies from memory. + string? baseDirectory = Path.GetDirectoryName(pluginPath); + if (baseDirectory != null) + { + if (!Path.EndsInDirectorySeparator(baseDirectory)) + baseDirectory += Path.DirectorySeparatorChar; + // This SetData call effectively sets AppContext.BaseDirectory + // See https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/AppContext.cs#L21-L25 + AppDomain.CurrentDomain.SetData("APP_CONTEXT_BASE_DIRECTORY", baseDirectory); + } + else + { + // TODO: How to log from GodotPlugins? (delegate pointer?) + Console.Error.WriteLine("Failed to set AppContext.BaseDirectory. Dynamic loading of libraries may fail."); + } + } } protected override Assembly? Load(AssemblyName assemblyName) diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs index a61c5403b9..8598c32760 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.CompilerServices; using Godot.NativeInterop; @@ -174,7 +175,15 @@ namespace Godot.Collections } /// <summary> - /// Duplicates this <see cref="Array"/>. + /// Returns a copy of the <see cref="Array"/>. + /// If <paramref name="deep"/> is <see langword="true"/>, a deep copy if performed: + /// all nested arrays and dictionaries are duplicated and will not be shared with + /// the original array. If <see langword="false"/>, a shallow copy is made and + /// references to the original nested arrays and dictionaries are kept, so that + /// modifying a sub-array or dictionary in the copy will also impact those + /// referenced in the source array. Note that any <see cref="GodotObject"/> derived + /// elements will be shallow copied regardless of the <paramref name="deep"/> + /// setting. /// </summary> /// <param name="deep">If <see langword="true"/>, performs a deep copy.</param> /// <returns>A new Godot Array.</returns> @@ -187,7 +196,102 @@ namespace Godot.Collections } /// <summary> - /// Resizes this <see cref="Array"/> to the given size. + /// Assigns the given value to all elements in the array. This can typically be + /// used together with <see cref="Resize(int)"/> to create an array with a given + /// size and initialized elements. + /// Note: If <paramref name="value"/> is of a reference type (<see cref="GodotObject"/> + /// derived, <see cref="Array"/> or <see cref="Dictionary"/>, etc.) then the array + /// is filled with the references to the same object, i.e. no duplicates are + /// created. + /// </summary> + /// <example> + /// <code> + /// var array = new Godot.Collections.Array(); + /// array.Resize(10); + /// array.Fill(0); // Initialize the 10 elements to 0. + /// </code> + /// </example> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> + /// <param name="value">The value to fill the array with.</param> + public void Fill(Variant value) + { + ThrowIfReadOnly(); + + godot_variant variantValue = (godot_variant)value.NativeVar; + var self = (godot_array)NativeValue; + NativeFuncs.godotsharp_array_fill(ref self, variantValue); + } + + /// <summary> + /// Returns the maximum value contained in the array if all elements are of + /// comparable types. If the elements can't be compared, <see langword="null"/> + /// is returned. + /// </summary> + /// <returns>The maximum value contained in the array.</returns> + public Variant Max() + { + godot_variant resVariant; + var self = (godot_array)NativeValue; + NativeFuncs.godotsharp_array_max(ref self, out resVariant); + return Variant.CreateTakingOwnershipOfDisposableValue(resVariant); + } + + /// <summary> + /// Returns the minimum value contained in the array if all elements are of + /// comparable types. If the elements can't be compared, <see langword="null"/> + /// is returned. + /// </summary> + /// <returns>The minimum value contained in the array.</returns> + public Variant Min() + { + godot_variant resVariant; + var self = (godot_array)NativeValue; + NativeFuncs.godotsharp_array_min(ref self, out resVariant); + return Variant.CreateTakingOwnershipOfDisposableValue(resVariant); + } + + /// <summary> + /// Returns a random value from the target array. + /// </summary> + /// <example> + /// <code> + /// var array = new Godot.Collections.Array { 1, 2, 3, 4 }; + /// GD.Print(array.PickRandom()); // Prints either of the four numbers. + /// </code> + /// </example> + /// <returns>A random element from the array.</returns> + public Variant PickRandom() + { + godot_variant resVariant; + var self = (godot_array)NativeValue; + NativeFuncs.godotsharp_array_pick_random(ref self, out resVariant); + return Variant.CreateTakingOwnershipOfDisposableValue(resVariant); + } + + /// <summary> + /// Compares this <see cref="Array"/> against the <paramref name="other"/> + /// <see cref="Array"/> recursively. Returns <see langword="true"/> if the + /// sizes and contents of the arrays are equal, <see langword="false"/> + /// otherwise. + /// </summary> + /// <param name="other">The other array to compare against.</param> + /// <returns> + /// <see langword="true"/> if the sizes and contents of the arrays are equal, + /// <see langword="false"/> otherwise. + /// </returns> + public bool RecursiveEqual(Array other) + { + var self = (godot_array)NativeValue; + var otherVariant = (godot_array)other.NativeValue; + return NativeFuncs.godotsharp_array_recursive_equal(ref self, otherVariant).ToBool(); + } + + /// <summary> + /// Resizes the array to contain a different number of elements. If the array + /// size is smaller, elements are cleared, if bigger, new elements are + /// <see langword="null"/>. /// </summary> /// <exception cref="InvalidOperationException"> /// The array is read-only. @@ -203,7 +307,25 @@ namespace Godot.Collections } /// <summary> - /// Shuffles the contents of this <see cref="Array"/> into a random order. + /// Reverses the order of the elements in the array. + /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> + public void Reverse() + { + ThrowIfReadOnly(); + + var self = (godot_array)NativeValue; + NativeFuncs.godotsharp_array_reverse(ref self); + } + + /// <summary> + /// Shuffles the array such that the items will have a random order. + /// This method uses the global random number generator common to methods + /// such as <see cref="GD.Randi"/>. Call <see cref="GD.Randomize"/> to + /// ensure that a new seed will be used each time if you want + /// non-reproducible shuffling. /// </summary> /// <exception cref="InvalidOperationException"> /// The array is read-only. @@ -217,7 +339,104 @@ namespace Godot.Collections } /// <summary> - /// Concatenates these two <see cref="Array"/>s. + /// Creates a shallow copy of a range of elements in the source <see cref="Array"/>. + /// </summary> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="start"/> is less than 0 or greater than the array's size. + /// </exception> + /// <param name="start">The zero-based index at which the range starts.</param> + /// <returns>A new array that contains the elements inside the slice range.</returns> + public Array Slice(int start) + { + if (start < 0 || start > Count) + throw new ArgumentOutOfRangeException(nameof(start)); + + return GetSliceRange(start, Count, step: 1, deep: false); + } + + /// <summary> + /// Creates a shallow copy of a range of elements in the source <see cref="Array"/>. + /// </summary> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="start"/> is less than 0 or greater than the array's size. + /// -or- + /// <paramref name="length"/> is less than 0 or greater than the array's size. + /// </exception> + /// <param name="start">The zero-based index at which the range starts.</param> + /// <param name="length">The length of the range.</param> + /// <returns>A new array that contains the elements inside the slice range.</returns> + // The Slice method must have this signature to get implicit Range support. + public Array Slice(int start, int length) + { + if (start < 0 || start > Count) + throw new ArgumentOutOfRangeException(nameof(start)); + + if (length < 0 || length > Count) + throw new ArgumentOutOfRangeException(nameof(start)); + + return GetSliceRange(start, start + length, step: 1, deep: false); + } + + /// <summary> + /// Returns the slice of the <see cref="Array"/>, from <paramref name="start"/> + /// (inclusive) to <paramref name="end"/> (exclusive), as a new <see cref="Array"/>. + /// The absolute value of <paramref name="start"/> and <paramref name="end"/> + /// will be clamped to the array size. + /// If either <paramref name="start"/> or <paramref name="end"/> are negative, they + /// will be relative to the end of the array (i.e. <c>arr.GetSliceRange(0, -2)</c> + /// is a shorthand for <c>arr.GetSliceRange(0, arr.Count - 2)</c>). + /// If specified, <paramref name="step"/> is the relative index between source + /// elements. It can be negative, then <paramref name="start"/> must be higher than + /// <paramref name="end"/>. For example, <c>[0, 1, 2, 3, 4, 5].GetSliceRange(5, 1, -2)</c> + /// returns <c>[5, 3]</c>. + /// If <paramref name="deep"/> is true, each element will be copied by value + /// rather than by reference. + /// </summary> + /// <param name="start">The zero-based index at which the range starts.</param> + /// <param name="end">The zero-based index at which the range ends.</param> + /// <param name="step">The relative index between source elements to take.</param> + /// <param name="deep">If <see langword="true"/>, performs a deep copy.</param> + /// <returns>A new array that contains the elements inside the slice range.</returns> + public Array GetSliceRange(int start, int end, int step = 1, bool deep = false) + { + godot_array newArray; + var self = (godot_array)NativeValue; + NativeFuncs.godotsharp_array_slice(ref self, start, end, step, deep.ToGodotBool(), out newArray); + return CreateTakingOwnershipOfDisposableValue(newArray); + } + + /// <summary> + /// Sorts the array. + /// Note: The sorting algorithm used is not stable. This means that values + /// considered equal may have their order changed when using <see cref="Sort"/>. + /// Note: Strings are sorted in alphabetical order (as opposed to natural order). + /// This may lead to unexpected behavior when sorting an array of strings ending + /// with a sequence of numbers. + /// To sort with a custom predicate use + /// <see cref="Enumerable.OrderBy{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>. + /// </summary> + /// <example> + /// <code> + /// var strings = new Godot.Collections.Array { "string1", "string2", "string10", "string11" }; + /// strings.Sort(); + /// GD.Print(strings); // Prints [string1, string10, string11, string2] + /// </code> + /// </example> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> + public void Sort() + { + ThrowIfReadOnly(); + + var self = (godot_array)NativeValue; + NativeFuncs.godotsharp_array_sort(ref self); + } + + /// <summary> + /// Concatenates two <see cref="Array"/>s together, with the <paramref name="right"/> + /// being added to the end of the <see cref="Array"/> specified in <paramref name="left"/>. + /// For example, <c>[1, 2] + [3, 4]</c> results in <c>[1, 2, 3, 4]</c>. /// </summary> /// <param name="left">The first array.</param> /// <param name="right">The second array.</param> @@ -253,6 +472,9 @@ namespace Godot.Collections /// <exception cref="InvalidOperationException"> /// The property is assigned and the array is read-only. /// </exception> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="index"/> is less than 0 or greater than the array's size. + /// </exception> /// <value>The <see cref="Variant"/> item at the given <paramref name="index"/>.</value> public unsafe Variant this[int index] { @@ -294,14 +516,146 @@ namespace Godot.Collections } /// <summary> - /// Checks if this <see cref="Array"/> contains the given item. + /// Adds the elements of the specified collection to the end of this <see cref="Array"/>. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> + /// <exception cref="ArgumentNullException"> + /// The <paramref name="collection"/> is <see langword="null"/>. + /// </exception> + /// <param name="collection">Collection of <see cref="Variant"/> items to add.</param> + public void AddRange<[MustBeVariant] T>(IEnumerable<T> collection) + { + ThrowIfReadOnly(); + + if (collection == null) + throw new ArgumentNullException(nameof(collection), "Value cannot be null."); + + // If the collection is another Godot Array, we can add the items + // with a single interop call. + if (collection is Array array) + { + var self = (godot_array)NativeValue; + var collectionNative = (godot_array)array.NativeValue; + _ = NativeFuncs.godotsharp_array_add_range(ref self, collectionNative); + return; + } + if (collection is Array<T> typedArray) + { + var self = (godot_array)NativeValue; + var collectionNative = (godot_array)typedArray.NativeValue; + _ = NativeFuncs.godotsharp_array_add_range(ref self, collectionNative); + return; + } + + // If we can retrieve the count of the collection without enumerating it + // (e.g.: the collections is a List<T>), use it to resize the array once + // instead of growing it as we add items. + if (collection.TryGetNonEnumeratedCount(out int count)) + { + Resize(Count + count); + + using var enumerator = collection.GetEnumerator(); + + for (int i = 0; i < count; i++) + { + enumerator.MoveNext(); + this[count + i] = Variant.From(enumerator.Current); + } + + return; + } + + foreach (var item in collection) + { + Add(Variant.From(item)); + } + } + + /// <summary> + /// Finds the index of an existing value using binary search. + /// If the value is not present in the array, it returns the bitwise + /// complement of the insertion index that maintains sorting order. + /// Note: Calling <see cref="BinarySearch(int, int, Variant)"/> on an + /// unsorted array results in unexpected behavior. + /// </summary> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="index"/> is less than 0. + /// -or- + /// <paramref name="count"/> is less than 0. + /// </exception> + /// <exception cref="ArgumentException"> + /// <paramref name="index"/> and <paramref name="count"/> do not denote + /// a valid range in the <see cref="Array"/>. + /// </exception> + /// <param name="index">The starting index of the range to search.</param> + /// <param name="count">The length of the range to search.</param> + /// <param name="item">The object to locate.</param> + /// <returns> + /// The index of the item in the array, if <paramref name="item"/> is found; + /// otherwise, a negative number that is the bitwise complement of the index + /// of the next element that is larger than <paramref name="item"/> or, if + /// there is no larger element, the bitwise complement of <see cref="Count"/>. + /// </returns> + public int BinarySearch(int index, int count, Variant item) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index), "index cannot be negative."); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count), "count cannot be negative."); + if (Count - index < count) + throw new ArgumentException("length is out of bounds or count is greater than the number of elements."); + + if (Count == 0) + { + // Special case for empty array to avoid an interop call. + return -1; + } + + godot_variant variantValue = (godot_variant)item.NativeVar; + var self = (godot_array)NativeValue; + return NativeFuncs.godotsharp_array_binary_search(ref self, index, count, variantValue); + } + + /// <summary> + /// Finds the index of an existing value using binary search. + /// If the value is not present in the array, it returns the bitwise + /// complement of the insertion index that maintains sorting order. + /// Note: Calling <see cref="BinarySearch(Variant)"/> on an unsorted + /// array results in unexpected behavior. + /// </summary> + /// <param name="item">The object to locate.</param> + /// <returns> + /// The index of the item in the array, if <paramref name="item"/> is found; + /// otherwise, a negative number that is the bitwise complement of the index + /// of the next element that is larger than <paramref name="item"/> or, if + /// there is no larger element, the bitwise complement of <see cref="Count"/>. + /// </returns> + public int BinarySearch(Variant item) + { + return BinarySearch(0, Count, item); + } + + /// <summary> + /// Returns <see langword="true"/> if the array contains the given value. + /// </summary> + /// <example> + /// <code> + /// var arr = new Godot.Collections.Array { "inside", 7 }; + /// GD.Print(arr.Contains("inside")); // True + /// GD.Print(arr.Contains("outside")); // False + /// GD.Print(arr.Contains(7)); // True + /// GD.Print(arr.Contains("7")); // False + /// </code> + /// </example> /// <param name="item">The <see cref="Variant"/> item to look for.</param> /// <returns>Whether or not this array contains the given item.</returns> public bool Contains(Variant item) => IndexOf(item) != -1; /// <summary> - /// Erases all items from this <see cref="Array"/>. + /// Clears the array. This is the equivalent to using <see cref="Resize(int)"/> + /// with a size of <c>0</c> /// </summary> /// <exception cref="InvalidOperationException"> /// The array is read-only. @@ -309,27 +663,104 @@ namespace Godot.Collections public void Clear() => Resize(0); /// <summary> - /// Searches this <see cref="Array"/> for an item - /// and returns its index or -1 if not found. + /// Searches the array for a value and returns its index or <c>-1</c> if not found. /// </summary> /// <param name="item">The <see cref="Variant"/> item to search for.</param> /// <returns>The index of the item, or -1 if not found.</returns> public int IndexOf(Variant item) { + if (Count == 0) + { + // Special case for empty array to avoid an interop call. + return -1; + } + godot_variant variantValue = (godot_variant)item.NativeVar; var self = (godot_array)NativeValue; return NativeFuncs.godotsharp_array_index_of(ref self, variantValue); } /// <summary> - /// Inserts a new item at a given position in the array. - /// The position must be a valid position of an existing item, - /// or the position at the end of the array. + /// Searches the array for a value and returns its index or <c>-1</c> if not found. + /// </summary> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="index"/> is less than 0 or greater than the array's size. + /// </exception> + /// <param name="item">The <see cref="Variant"/> item to search for.</param> + /// <param name="index">The initial search index to start from.</param> + /// <returns>The index of the item, or -1 if not found.</returns> + public int IndexOf(Variant item, int index) + { + if (index < 0 || index > Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + if (Count == 0) + { + // Special case for empty array to avoid an interop call. + return -1; + } + + godot_variant variantValue = (godot_variant)item.NativeVar; + var self = (godot_array)NativeValue; + return NativeFuncs.godotsharp_array_index_of(ref self, variantValue, index); + } + + /// <summary> + /// Searches the array for a value in reverse order and returns its index + /// or <c>-1</c> if not found. + /// </summary> + /// <param name="item">The <see cref="Variant"/> item to search for.</param> + /// <returns>The index of the item, or -1 if not found.</returns> + public int LastIndexOf(Variant item) + { + if (Count == 0) + { + // Special case for empty array to avoid an interop call. + return -1; + } + + godot_variant variantValue = (godot_variant)item.NativeVar; + var self = (godot_array)NativeValue; + return NativeFuncs.godotsharp_array_last_index_of(ref self, variantValue, Count - 1); + } + + /// <summary> + /// Searches the array for a value in reverse order and returns its index + /// or <c>-1</c> if not found. + /// </summary> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="index"/> is less than 0 or greater than the array's size. + /// </exception> + /// <param name="item">The <see cref="Variant"/> item to search for.</param> + /// <param name="index">The initial search index to start from.</param> + /// <returns>The index of the item, or -1 if not found.</returns> + public int LastIndexOf(Variant item, int index) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + if (Count == 0) + { + // Special case for empty array to avoid an interop call. + return -1; + } + + godot_variant variantValue = (godot_variant)item.NativeVar; + var self = (godot_array)NativeValue; + return NativeFuncs.godotsharp_array_last_index_of(ref self, variantValue, index); + } + + /// <summary> + /// Inserts a new element at a given position in the array. The position + /// must be valid, or at the end of the array (<c>pos == Count - 1</c>). /// Existing items will be moved to the right. /// </summary> /// <exception cref="InvalidOperationException"> /// The array is read-only. /// </exception> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="index"/> is less than 0 or greater than the array's size. + /// </exception> /// <param name="index">The index to insert at.</param> /// <param name="item">The <see cref="Variant"/> item to insert.</param> public void Insert(int index, Variant item) @@ -367,11 +798,16 @@ namespace Godot.Collections } /// <summary> - /// Removes an element from this <see cref="Array"/> by index. + /// Removes an element from the array by index. + /// To remove an element by searching for its value, use + /// <see cref="Remove(Variant)"/> instead. /// </summary> /// <exception cref="InvalidOperationException"> /// The array is read-only. /// </exception> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="index"/> is less than 0 or greater than the array's size. + /// </exception> /// <param name="index">The index of the element to remove.</param> public void RemoveAt(int index) { @@ -424,6 +860,9 @@ namespace Godot.Collections /// Copies the elements of this <see cref="Array"/> to the given /// <see cref="Variant"/> C# array, starting at the given index. /// </summary> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="arrayIndex"/> is less than 0 or greater than the array's size. + /// </exception> /// <param name="array">The array to copy to.</param> /// <param name="arrayIndex">The index to start at.</param> public void CopyTo(Variant[] array, int arrayIndex) @@ -518,6 +957,9 @@ namespace Godot.Collections /// <summary> /// The variant returned via the <paramref name="elem"/> parameter is owned by the Array and must not be disposed. /// </summary> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="index"/> is less than 0 or greater than the array's size. + /// </exception> internal void GetVariantBorrowElementAt(int index, out godot_variant elem) { if (index < 0 || index >= Count) @@ -658,6 +1100,97 @@ namespace Godot.Collections } /// <summary> + /// Assigns the given value to all elements in the array. This can typically be + /// used together with <see cref="Resize(int)"/> to create an array with a given + /// size and initialized elements. + /// Note: If <paramref name="value"/> is of a reference type (<see cref="GodotObject"/> + /// derived, <see cref="Array"/> or <see cref="Dictionary"/>, etc.) then the array + /// is filled with the references to the same object, i.e. no duplicates are + /// created. + /// </summary> + /// <example> + /// <code> + /// var array = new Godot.Collections.Array<int>(); + /// array.Resize(10); + /// array.Fill(0); // Initialize the 10 elements to 0. + /// </code> + /// </example> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> + /// <param name="value">The value to fill the array with.</param> + public void Fill(T value) + { + ThrowIfReadOnly(); + + godot_variant variantValue = VariantUtils.CreateFrom(value); + var self = (godot_array)_underlyingArray.NativeValue; + NativeFuncs.godotsharp_array_fill(ref self, variantValue); + } + + /// <summary> + /// Returns the maximum value contained in the array if all elements are of + /// comparable types. If the elements can't be compared, <see langword="default"/> + /// is returned. + /// </summary> + /// <returns>The maximum value contained in the array.</returns> + public T Max() + { + godot_variant resVariant; + var self = (godot_array)_underlyingArray.NativeValue; + NativeFuncs.godotsharp_array_max(ref self, out resVariant); + return VariantUtils.ConvertTo<T>(resVariant); + } + + /// <summary> + /// Returns the minimum value contained in the array if all elements are of + /// comparable types. If the elements can't be compared, <see langword="default"/> + /// is returned. + /// </summary> + /// <returns>The minimum value contained in the array.</returns> + public T Min() + { + godot_variant resVariant; + var self = (godot_array)_underlyingArray.NativeValue; + NativeFuncs.godotsharp_array_min(ref self, out resVariant); + return VariantUtils.ConvertTo<T>(resVariant); + } + + /// <summary> + /// Returns a random value from the target array. + /// </summary> + /// <example> + /// <code> + /// var array = new Godot.Collections.Array<int> { 1, 2, 3, 4 }; + /// GD.Print(array.PickRandom()); // Prints either of the four numbers. + /// </code> + /// </example> + /// <returns>A random element from the array.</returns> + public T PickRandom() + { + godot_variant resVariant; + var self = (godot_array)_underlyingArray.NativeValue; + NativeFuncs.godotsharp_array_pick_random(ref self, out resVariant); + return VariantUtils.ConvertTo<T>(resVariant); + } + + /// <summary> + /// Compares this <see cref="Array{T}"/> against the <paramref name="other"/> + /// <see cref="Array{T}"/> recursively. Returns <see langword="true"/> if the + /// sizes and contents of the arrays are equal, <see langword="false"/> + /// otherwise. + /// </summary> + /// <param name="other">The other array to compare against.</param> + /// <returns> + /// <see langword="true"/> if the sizes and contents of the arrays are equal, + /// <see langword="false"/> otherwise. + /// </returns> + public bool RecursiveEqual(Array<T> other) + { + return _underlyingArray.RecursiveEqual(other._underlyingArray); + } + + /// <summary> /// Resizes this <see cref="Array{T}"/> to the given size. /// </summary> /// <exception cref="InvalidOperationException"> @@ -671,7 +1204,22 @@ namespace Godot.Collections } /// <summary> - /// Shuffles the contents of this <see cref="Array{T}"/> into a random order. + /// Reverses the order of the elements in the array. + /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> + public void Reverse() + { + _underlyingArray.Reverse(); + } + + /// <summary> + /// Shuffles the array such that the items will have a random order. + /// This method uses the global random number generator common to methods + /// such as <see cref="GD.Randi"/>. Call <see cref="GD.Randomize"/> to + /// ensure that a new seed will be used each time if you want + /// non-reproducible shuffling. /// </summary> /// <exception cref="InvalidOperationException"> /// The array is read-only. @@ -682,7 +1230,89 @@ namespace Godot.Collections } /// <summary> - /// Concatenates these two <see cref="Array{T}"/>s. + /// Creates a shallow copy of a range of elements in the source <see cref="Array{T}"/>. + /// </summary> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="start"/> is less than 0 or greater than the array's size. + /// </exception> + /// <param name="start">The zero-based index at which the range starts.</param> + /// <returns>A new array that contains the elements inside the slice range.</returns> + public Array<T> Slice(int start) + { + return GetSliceRange(start, Count, step: 1, deep: false); + } + + /// <summary> + /// Creates a shallow copy of a range of elements in the source <see cref="Array{T}"/>. + /// </summary> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="start"/> is less than 0 or greater than the array's size. + /// -or- + /// <paramref name="length"/> is less than 0 or greater than the array's size. + /// </exception> + /// <param name="start">The zero-based index at which the range starts.</param> + /// <param name="length">The length of the range.</param> + /// <returns>A new array that contains the elements inside the slice range.</returns> + // The Slice method must have this signature to get implicit Range support. + public Array<T> Slice(int start, int length) + { + return GetSliceRange(start, start + length, step: 1, deep: false); + } + + /// <summary> + /// Returns the slice of the <see cref="Array{T}"/>, from <paramref name="start"/> + /// (inclusive) to <paramref name="end"/> (exclusive), as a new <see cref="Array{T}"/>. + /// The absolute value of <paramref name="start"/> and <paramref name="end"/> + /// will be clamped to the array size. + /// If either <paramref name="start"/> or <paramref name="end"/> are negative, they + /// will be relative to the end of the array (i.e. <c>arr.GetSliceRange(0, -2)</c> + /// is a shorthand for <c>arr.GetSliceRange(0, arr.Count - 2)</c>). + /// If specified, <paramref name="step"/> is the relative index between source + /// elements. It can be negative, then <paramref name="start"/> must be higher than + /// <paramref name="end"/>. For example, <c>[0, 1, 2, 3, 4, 5].GetSliceRange(5, 1, -2)</c> + /// returns <c>[5, 3]</c>. + /// If <paramref name="deep"/> is true, each element will be copied by value + /// rather than by reference. + /// </summary> + /// <param name="start">The zero-based index at which the range starts.</param> + /// <param name="end">The zero-based index at which the range ends.</param> + /// <param name="step">The relative index between source elements to take.</param> + /// <param name="deep">If <see langword="true"/>, performs a deep copy.</param> + /// <returns>A new array that contains the elements inside the slice range.</returns> + public Array<T> GetSliceRange(int start, int end, int step = 1, bool deep = false) + { + return new Array<T>(_underlyingArray.GetSliceRange(start, end, step, deep)); + } + + /// <summary> + /// Sorts the array. + /// Note: The sorting algorithm used is not stable. This means that values + /// considered equal may have their order changed when using <see cref="Sort"/>. + /// Note: Strings are sorted in alphabetical order (as opposed to natural order). + /// This may lead to unexpected behavior when sorting an array of strings ending + /// with a sequence of numbers. + /// To sort with a custom predicate use + /// <see cref="Enumerable.OrderBy{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>. + /// </summary> + /// <example> + /// <code> + /// var strings = new Godot.Collections.Array<string> { "string1", "string2", "string10", "string11" }; + /// strings.Sort(); + /// GD.Print(strings); // Prints [string1, string10, string11, string2] + /// </code> + /// </example> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> + public void Sort() + { + _underlyingArray.Sort(); + } + + /// <summary> + /// Concatenates two <see cref="Array{T}"/>s together, with the <paramref name="right"/> + /// being added to the end of the <see cref="Array{T}"/> specified in <paramref name="left"/>. + /// For example, <c>[1, 2] + [3, 4]</c> results in <c>[1, 2, 3, 4]</c>. /// </summary> /// <param name="left">The first array.</param> /// <param name="right">The second array.</param> @@ -706,12 +1336,15 @@ namespace Godot.Collections // IList<T> /// <summary> - /// Returns the value at the given <paramref name="index"/>. + /// Returns the item at the given <paramref name="index"/>. /// </summary> /// <exception cref="InvalidOperationException"> /// The property is assigned and the array is read-only. /// </exception> - /// <value>The value at the given <paramref name="index"/>.</value> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="index"/> is less than 0 or greater than the array's size. + /// </exception> + /// <value>The <see cref="Variant"/> item at the given <paramref name="index"/>.</value> public unsafe T this[int index] { get @@ -735,29 +1368,106 @@ namespace Godot.Collections } /// <summary> - /// Searches this <see cref="Array{T}"/> for an item - /// and returns its index or -1 if not found. + /// Searches the array for a value and returns its index or <c>-1</c> if not found. /// </summary> - /// <param name="item">The item to search for.</param> + /// <param name="item">The <see cref="Variant"/> item to search for.</param> /// <returns>The index of the item, or -1 if not found.</returns> public int IndexOf(T item) { + if (Count == 0) + { + // Special case for empty array to avoid an interop call. + return -1; + } + using var variantValue = VariantUtils.CreateFrom(item); var self = (godot_array)_underlyingArray.NativeValue; return NativeFuncs.godotsharp_array_index_of(ref self, variantValue); } /// <summary> - /// Inserts a new item at a given position in the <see cref="Array{T}"/>. - /// The position must be a valid position of an existing item, - /// or the position at the end of the array. + /// Searches the array for a value and returns its index or <c>-1</c> if not found. + /// </summary> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="index"/> is less than 0 or greater than the array's size. + /// </exception> + /// <param name="item">The <see cref="Variant"/> item to search for.</param> + /// <param name="index">The initial search index to start from.</param> + /// <returns>The index of the item, or -1 if not found.</returns> + public int IndexOf(T item, int index) + { + if (index < 0 || index > Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + if (Count == 0) + { + // Special case for empty array to avoid an interop call. + return -1; + } + + godot_variant variantValue = VariantUtils.CreateFrom(item); + var self = (godot_array)_underlyingArray.NativeValue; + return NativeFuncs.godotsharp_array_index_of(ref self, variantValue, index); + } + + /// <summary> + /// Searches the array for a value in reverse order and returns its index + /// or <c>-1</c> if not found. + /// </summary> + /// <param name="item">The <see cref="Variant"/> item to search for.</param> + /// <returns>The index of the item, or -1 if not found.</returns> + public int LastIndexOf(Variant item) + { + if (Count == 0) + { + // Special case for empty array to avoid an interop call. + return -1; + } + + godot_variant variantValue = VariantUtils.CreateFrom(item); + var self = (godot_array)_underlyingArray.NativeValue; + return NativeFuncs.godotsharp_array_last_index_of(ref self, variantValue, Count - 1); + } + + /// <summary> + /// Searches the array for a value in reverse order and returns its index + /// or <c>-1</c> if not found. + /// </summary> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="index"/> is less than 0 or greater than the array's size. + /// </exception> + /// <param name="item">The <see cref="Variant"/> item to search for.</param> + /// <param name="index">The initial search index to start from.</param> + /// <returns>The index of the item, or -1 if not found.</returns> + public int LastIndexOf(Variant item, int index) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + if (Count == 0) + { + // Special case for empty array to avoid an interop call. + return -1; + } + + godot_variant variantValue = VariantUtils.CreateFrom(item); + var self = (godot_array)_underlyingArray.NativeValue; + return NativeFuncs.godotsharp_array_last_index_of(ref self, variantValue, index); + } + + /// <summary> + /// Inserts a new element at a given position in the array. The position + /// must be valid, or at the end of the array (<c>pos == Count - 1</c>). /// Existing items will be moved to the right. /// </summary> /// <exception cref="InvalidOperationException"> /// The array is read-only. /// </exception> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="index"/> is less than 0 or greater than the array's size. + /// </exception> /// <param name="index">The index to insert at.</param> - /// <param name="item">The item to insert.</param> + /// <param name="item">The <see cref="Variant"/> item to insert.</param> public void Insert(int index, T item) { ThrowIfReadOnly(); @@ -771,11 +1481,16 @@ namespace Godot.Collections } /// <summary> - /// Removes an element from this <see cref="Array{T}"/> by index. + /// Removes an element from the array by index. + /// To remove an element by searching for its value, use + /// <see cref="Remove(T)"/> instead. /// </summary> /// <exception cref="InvalidOperationException"> /// The array is read-only. /// </exception> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="index"/> is less than 0 or greater than the array's size. + /// </exception> /// <param name="index">The index of the element to remove.</param> public void RemoveAt(int index) { @@ -814,8 +1529,7 @@ namespace Godot.Collections /// <exception cref="InvalidOperationException"> /// The array is read-only. /// </exception> - /// <param name="item">The item to add.</param> - /// <returns>The new size after adding the item.</returns> + /// <param name="item">The <see cref="Variant"/> item to add.</param> public void Add(T item) { ThrowIfReadOnly(); @@ -826,7 +1540,130 @@ namespace Godot.Collections } /// <summary> - /// Erases all items from this <see cref="Array{T}"/>. + /// Adds the elements of the specified collection to the end of this <see cref="Array{T}"/>. + /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> + /// <exception cref="ArgumentNullException"> + /// The <paramref name="collection"/> is <see langword="null"/>. + /// </exception> + /// <param name="collection">Collection of <see cref="Variant"/> items to add.</param> + public void AddRange(IEnumerable<T> collection) + { + ThrowIfReadOnly(); + + if (collection == null) + throw new ArgumentNullException(nameof(collection), "Value cannot be null."); + + // If the collection is another Godot Array, we can add the items + // with a single interop call. + if (collection is Array array) + { + var self = (godot_array)_underlyingArray.NativeValue; + var collectionNative = (godot_array)array.NativeValue; + _ = NativeFuncs.godotsharp_array_add_range(ref self, collectionNative); + return; + } + if (collection is Array<T> typedArray) + { + var self = (godot_array)_underlyingArray.NativeValue; + var collectionNative = (godot_array)typedArray._underlyingArray.NativeValue; + _ = NativeFuncs.godotsharp_array_add_range(ref self, collectionNative); + return; + } + + // If we can retrieve the count of the collection without enumerating it + // (e.g.: the collections is a List<T>), use it to resize the array once + // instead of growing it as we add items. + if (collection.TryGetNonEnumeratedCount(out int count)) + { + Resize(Count + count); + + using var enumerator = collection.GetEnumerator(); + + for (int i = 0; i < count; i++) + { + enumerator.MoveNext(); + this[count + i] = enumerator.Current; + } + + return; + } + + foreach (var item in collection) + { + Add(item); + } + } + + /// <summary> + /// Finds the index of an existing value using binary search. + /// If the value is not present in the array, it returns the bitwise + /// complement of the insertion index that maintains sorting order. + /// Note: Calling <see cref="BinarySearch(int, int, T)"/> on an unsorted + /// array results in unexpected behavior. + /// </summary> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="index"/> is less than 0. + /// -or- + /// <paramref name="count"/> is less than 0. + /// </exception> + /// <exception cref="ArgumentException"> + /// <paramref name="index"/> and <paramref name="count"/> do not denote + /// a valid range in the <see cref="Array{T}"/>. + /// </exception> + /// <param name="index">The starting index of the range to search.</param> + /// <param name="count">The length of the range to search.</param> + /// <param name="item">The object to locate.</param> + /// <returns> + /// The index of the item in the array, if <paramref name="item"/> is found; + /// otherwise, a negative number that is the bitwise complement of the index + /// of the next element that is larger than <paramref name="item"/> or, if + /// there is no larger element, the bitwise complement of <see cref="Count"/>. + /// </returns> + public int BinarySearch(int index, int count, T item) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index), "index cannot be negative."); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count), "count cannot be negative."); + if (Count - index < count) + throw new ArgumentException("length is out of bounds or count is greater than the number of elements."); + + if (Count == 0) + { + // Special case for empty array to avoid an interop call. + return -1; + } + + using var variantValue = VariantUtils.CreateFrom(item); + var self = (godot_array)_underlyingArray.NativeValue; + return NativeFuncs.godotsharp_array_binary_search(ref self, index, count, variantValue); + } + + /// <summary> + /// Finds the index of an existing value using binary search. + /// If the value is not present in the array, it returns the bitwise + /// complement of the insertion index that maintains sorting order. + /// Note: Calling <see cref="BinarySearch(T)"/> on an unsorted + /// array results in unexpected behavior. + /// </summary> + /// <param name="item">The object to locate.</param> + /// <returns> + /// The index of the item in the array, if <paramref name="item"/> is found; + /// otherwise, a negative number that is the bitwise complement of the index + /// of the next element that is larger than <paramref name="item"/> or, if + /// there is no larger element, the bitwise complement of <see cref="Count"/>. + /// </returns> + public int BinarySearch(T item) + { + return BinarySearch(0, Count, item); + } + + /// <summary> + /// Clears the array. This is the equivalent to using <see cref="Resize(int)"/> + /// with a size of <c>0</c> /// </summary> /// <exception cref="InvalidOperationException"> /// The array is read-only. @@ -837,8 +1674,17 @@ namespace Godot.Collections } /// <summary> - /// Checks if this <see cref="Array{T}"/> contains the given item. + /// Returns <see langword="true"/> if the array contains the given value. /// </summary> + /// <example> + /// <code> + /// var arr = new Godot.Collections.Array<string> { "inside", "7" }; + /// GD.Print(arr.Contains("inside")); // True + /// GD.Print(arr.Contains("outside")); // False + /// GD.Print(arr.Contains(7)); // False + /// GD.Print(arr.Contains("7")); // True + /// </code> + /// </example> /// <param name="item">The item to look for.</param> /// <returns>Whether or not this array contains the given item.</returns> public bool Contains(T item) => IndexOf(item) != -1; @@ -847,6 +1693,9 @@ namespace Godot.Collections /// Copies the elements of this <see cref="Array{T}"/> to the given /// C# array, starting at the given index. /// </summary> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="arrayIndex"/> is less than 0 or greater than the array's size. + /// </exception> /// <param name="array">The C# array to copy to.</param> /// <param name="arrayIndex">The index to start at.</param> public void CopyTo(T[] array, int arrayIndex) @@ -876,7 +1725,7 @@ namespace Godot.Collections } /// <summary> - /// Removes the first occurrence of the specified value + /// Removes the first occurrence of the specified <paramref name="item"/> /// from this <see cref="Array{T}"/>. /// </summary> /// <exception cref="InvalidOperationException"> diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs index 1e23689c95..3d72ee0036 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs @@ -365,21 +365,44 @@ namespace Godot.NativeInterop public static partial int godotsharp_array_add(ref godot_array p_self, in godot_variant p_item); + public static partial int godotsharp_array_add_range(ref godot_array p_self, in godot_array p_collection); + + public static partial int godotsharp_array_binary_search(ref godot_array p_self, int p_index, int p_count, in godot_variant p_value); + public static partial void godotsharp_array_duplicate(ref godot_array p_self, godot_bool p_deep, out godot_array r_dest); - public static partial int godotsharp_array_index_of(ref godot_array p_self, in godot_variant p_item); + public static partial void godotsharp_array_fill(ref godot_array p_self, in godot_variant p_value); + + public static partial int godotsharp_array_index_of(ref godot_array p_self, in godot_variant p_item, int p_index = 0); public static partial void godotsharp_array_insert(ref godot_array p_self, int p_index, in godot_variant p_item); + public static partial int godotsharp_array_last_index_of(ref godot_array p_self, in godot_variant p_item, int p_index); + + public static partial void godotsharp_array_make_read_only(ref godot_array p_self); + + public static partial void godotsharp_array_max(ref godot_array p_self, out godot_variant r_value); + + public static partial void godotsharp_array_min(ref godot_array p_self, out godot_variant r_value); + + public static partial void godotsharp_array_pick_random(ref godot_array p_self, out godot_variant r_value); + + public static partial godot_bool godotsharp_array_recursive_equal(ref godot_array p_self, in godot_array p_other); + public static partial void godotsharp_array_remove_at(ref godot_array p_self, int p_index); public static partial Error godotsharp_array_resize(ref godot_array p_self, int p_new_size); - public static partial void godotsharp_array_make_read_only(ref godot_array p_self); + public static partial void godotsharp_array_reverse(ref godot_array p_self); public static partial void godotsharp_array_shuffle(ref godot_array p_self); + public static partial void godotsharp_array_slice(ref godot_array p_self, int p_start, int p_end, + int p_step, godot_bool p_deep, out godot_array r_dest); + + public static partial void godotsharp_array_sort(ref godot_array p_self); + public static partial void godotsharp_array_to_string(ref godot_array p_self, out godot_string r_str); // Dictionary @@ -459,6 +482,10 @@ namespace Godot.NativeInterop public static partial godot_bool godotsharp_node_path_is_absolute(in godot_node_path p_self); + public static partial godot_bool godotsharp_node_path_equals(in godot_node_path p_self, in godot_node_path p_other); + + public static partial int godotsharp_node_path_hash(in godot_node_path p_self); + // GD, etc internal static partial void godotsharp_bytes_to_var(in godot_packed_byte_array p_bytes, diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs index b02bd167a1..f216fb7ea3 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs @@ -39,7 +39,7 @@ namespace Godot /// new NodePath("/root/MyAutoload"); // If you have an autoloaded node or scene. /// </code> /// </example> - public sealed class NodePath : IDisposable + public sealed class NodePath : IDisposable, IEquatable<NodePath> { internal godot_node_path.movable NativeValue; @@ -288,5 +288,37 @@ namespace Godot /// </summary> /// <returns>If the <see cref="NodePath"/> is empty.</returns> public bool IsEmpty => NativeValue.DangerousSelfRef.IsEmpty; + + public static bool operator ==(NodePath left, NodePath right) + { + if (left is null) + return right is null; + return left.Equals(right); + } + + public static bool operator !=(NodePath left, NodePath right) + { + return !(left == right); + } + + public bool Equals(NodePath other) + { + if (other is null) + return false; + var self = (godot_node_path)NativeValue; + var otherNative = (godot_node_path)other.NativeValue; + return NativeFuncs.godotsharp_node_path_equals(self, otherNative).ToBool(); + } + + public override bool Equals(object obj) + { + return ReferenceEquals(this, obj) || (obj is NodePath other && Equals(other)); + } + + public override int GetHashCode() + { + var self = (godot_node_path)NativeValue; + return NativeFuncs.godotsharp_node_path_hash(self); + } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringName.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringName.cs index b9ee0bc278..97d28f9ee9 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringName.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringName.cs @@ -10,7 +10,7 @@ namespace Godot /// Comparing them is much faster than with regular strings, because only the pointers are compared, /// not the whole strings. /// </summary> - public sealed class StringName : IDisposable + public sealed class StringName : IDisposable, IEquatable<StringName> { internal godot_string_name.movable NativeValue; diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp index d17fe3e75f..306ac333eb 100644 --- a/modules/mono/glue/runtime_interop.cpp +++ b/modules/mono/glue/runtime_interop.cpp @@ -992,18 +992,78 @@ int32_t godotsharp_array_add(Array *p_self, const Variant *p_item) { return p_self->size(); } +int32_t godotsharp_array_add_range(Array *p_self, const Array *p_collection) { + p_self->append_array(*p_collection); + return p_self->size(); +} + +int32_t godotsharp_array_binary_search(const Array *p_self, int32_t p_index, int32_t p_length, const Variant *p_value) { + ERR_FAIL_COND_V(p_index < 0, -1); + ERR_FAIL_COND_V(p_length < 0, -1); + ERR_FAIL_COND_V(p_self->size() - p_index < p_length, -1); + + const Variant &value = *p_value; + const Array &array = *p_self; + + int lo = p_index; + int hi = p_index + p_length - 1; + while (lo <= hi) { + int mid = lo + ((hi - lo) >> 1); + const Variant &mid_item = array[mid]; + + if (mid_item == value) { + return mid; + } + if (mid_item < value) { + lo = mid + 1; + } else { + hi = mid - 1; + } + } + + return ~lo; +} + void godotsharp_array_duplicate(const Array *p_self, bool p_deep, Array *r_dest) { memnew_placement(r_dest, Array(p_self->duplicate(p_deep))); } -int32_t godotsharp_array_index_of(const Array *p_self, const Variant *p_item) { - return p_self->find(*p_item); +void godotsharp_array_fill(Array *p_self, const Variant *p_value) { + p_self->fill(*p_value); +} + +int32_t godotsharp_array_index_of(const Array *p_self, const Variant *p_item, int32_t p_index = 0) { + return p_self->find(*p_item, p_index); } void godotsharp_array_insert(Array *p_self, int32_t p_index, const Variant *p_item) { p_self->insert(p_index, *p_item); } +int32_t godotsharp_array_last_index_of(const Array *p_self, const Variant *p_item, int32_t p_index) { + return p_self->rfind(*p_item, p_index); +} + +void godotsharp_array_make_read_only(Array *p_self) { + p_self->make_read_only(); +} + +void godotsharp_array_max(const Array *p_self, Variant *r_value) { + *r_value = p_self->max(); +} + +void godotsharp_array_min(const Array *p_self, Variant *r_value) { + *r_value = p_self->min(); +} + +void godotsharp_array_pick_random(const Array *p_self, Variant *r_value) { + *r_value = p_self->pick_random(); +} + +bool godotsharp_array_recursive_equal(const Array *p_self, const Array *p_other) { + return p_self->recursive_equal(*p_other, 0); +} + void godotsharp_array_remove_at(Array *p_self, int32_t p_index) { p_self->remove_at(p_index); } @@ -1012,14 +1072,22 @@ int32_t godotsharp_array_resize(Array *p_self, int32_t p_new_size) { return (int32_t)p_self->resize(p_new_size); } -void godotsharp_array_make_read_only(Array *p_self) { - p_self->make_read_only(); +void godotsharp_array_reverse(Array *p_self) { + p_self->reverse(); } void godotsharp_array_shuffle(Array *p_self) { p_self->shuffle(); } +void godotsharp_array_slice(Array *p_self, int32_t p_start, int32_t p_end, int32_t p_step, bool p_deep, Array *r_dest) { + memnew_placement(r_dest, Array(p_self->slice(p_start, p_end, p_step, p_deep))); +} + +void godotsharp_array_sort(Array *p_self) { + p_self->sort(); +} + void godotsharp_array_to_string(const Array *p_self, String *r_str) { *r_str = Variant(*p_self).operator String(); } @@ -1141,6 +1209,14 @@ bool godotsharp_node_path_is_absolute(const NodePath *p_self) { return p_self->is_absolute(); } +bool godotsharp_node_path_equals(const NodePath *p_self, const NodePath *p_other) { + return *p_self == *p_other; +} + +int godotsharp_node_path_hash(const NodePath *p_self) { + return p_self->hash(); +} + void godotsharp_randomize() { Math::randomize(); } @@ -1442,13 +1518,24 @@ static const void *unmanaged_callbacks[]{ (void *)godotsharp_array_destroy, (void *)godotsharp_dictionary_destroy, (void *)godotsharp_array_add, + (void *)godotsharp_array_add_range, + (void *)godotsharp_array_binary_search, (void *)godotsharp_array_duplicate, + (void *)godotsharp_array_fill, (void *)godotsharp_array_index_of, (void *)godotsharp_array_insert, + (void *)godotsharp_array_last_index_of, + (void *)godotsharp_array_make_read_only, + (void *)godotsharp_array_max, + (void *)godotsharp_array_min, + (void *)godotsharp_array_pick_random, + (void *)godotsharp_array_recursive_equal, (void *)godotsharp_array_remove_at, (void *)godotsharp_array_resize, - (void *)godotsharp_array_make_read_only, + (void *)godotsharp_array_reverse, (void *)godotsharp_array_shuffle, + (void *)godotsharp_array_slice, + (void *)godotsharp_array_sort, (void *)godotsharp_array_to_string, (void *)godotsharp_dictionary_try_get_value, (void *)godotsharp_dictionary_set_value, @@ -1477,6 +1564,8 @@ static const void *unmanaged_callbacks[]{ (void *)godotsharp_node_path_get_subname, (void *)godotsharp_node_path_get_subname_count, (void *)godotsharp_node_path_is_absolute, + (void *)godotsharp_node_path_equals, + (void *)godotsharp_node_path_hash, (void *)godotsharp_bytes_to_var, (void *)godotsharp_convert, (void *)godotsharp_hash, diff --git a/modules/multiplayer/editor/editor_network_profiler.cpp b/modules/multiplayer/editor/editor_network_profiler.cpp index e320657ab5..f8e75d5ef5 100644 --- a/modules/multiplayer/editor/editor_network_profiler.cpp +++ b/modules/multiplayer/editor/editor_network_profiler.cpp @@ -253,7 +253,8 @@ EditorNetworkProfiler::EditorNetworkProfiler() { hb->add_spacer(); Label *lb = memnew(Label); - lb->set_text(TTR("Down")); + // TRANSLATORS: This is the label for the network profiler's incoming bandwidth. + lb->set_text(TTR("Down", "Network")); hb->add_child(lb); incoming_bandwidth_text = memnew(LineEdit); @@ -267,7 +268,8 @@ EditorNetworkProfiler::EditorNetworkProfiler() { hb->add_child(down_up_spacer); lb = memnew(Label); - lb->set_text(TTR("Up")); + // TRANSLATORS: This is the label for the network profiler's outgoing bandwidth. + lb->set_text(TTR("Up", "Network")); hb->add_child(lb); outgoing_bandwidth_text = memnew(LineEdit); diff --git a/modules/navigation/godot_navigation_server.cpp b/modules/navigation/godot_navigation_server.cpp index d546c5d3ba..c3cb1c5f13 100644 --- a/modules/navigation/godot_navigation_server.cpp +++ b/modules/navigation/godot_navigation_server.cpp @@ -262,7 +262,7 @@ TypedArray<RID> GodotNavigationServer::map_get_agents(RID p_map) const { const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_COND_V(map == nullptr, agents_rids); - const LocalVector<RvoAgent *> agents = map->get_agents(); + const LocalVector<NavAgent *> agents = map->get_agents(); agents_rids.resize(agents.size()); for (uint32_t i = 0; i < agents.size(); i++) { @@ -282,7 +282,7 @@ RID GodotNavigationServer::region_get_map(RID p_region) const { } RID GodotNavigationServer::agent_get_map(RID p_agent) const { - RvoAgent *agent = agent_owner.get_or_null(p_agent); + NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_COND_V(agent == nullptr, RID()); if (agent->get_map()) { @@ -579,13 +579,13 @@ RID GodotNavigationServer::agent_create() { MutexLock lock(operations_mutex); RID rid = agent_owner.make_rid(); - RvoAgent *agent = agent_owner.get_or_null(rid); + NavAgent *agent = agent_owner.get_or_null(rid); agent->set_self(rid); return rid; } COMMAND_2(agent_set_map, RID, p_agent, RID, p_map) { - RvoAgent *agent = agent_owner.get_or_null(p_agent); + NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_COND(agent == nullptr); if (agent->get_map()) { @@ -612,77 +612,77 @@ COMMAND_2(agent_set_map, RID, p_agent, RID, p_map) { } COMMAND_2(agent_set_neighbor_distance, RID, p_agent, real_t, p_distance) { - RvoAgent *agent = agent_owner.get_or_null(p_agent); + NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_COND(agent == nullptr); agent->get_agent()->neighborDist_ = p_distance; } COMMAND_2(agent_set_max_neighbors, RID, p_agent, int, p_count) { - RvoAgent *agent = agent_owner.get_or_null(p_agent); + NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_COND(agent == nullptr); agent->get_agent()->maxNeighbors_ = p_count; } COMMAND_2(agent_set_time_horizon, RID, p_agent, real_t, p_time) { - RvoAgent *agent = agent_owner.get_or_null(p_agent); + NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_COND(agent == nullptr); agent->get_agent()->timeHorizon_ = p_time; } COMMAND_2(agent_set_radius, RID, p_agent, real_t, p_radius) { - RvoAgent *agent = agent_owner.get_or_null(p_agent); + NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_COND(agent == nullptr); agent->get_agent()->radius_ = p_radius; } COMMAND_2(agent_set_max_speed, RID, p_agent, real_t, p_max_speed) { - RvoAgent *agent = agent_owner.get_or_null(p_agent); + NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_COND(agent == nullptr); agent->get_agent()->maxSpeed_ = p_max_speed; } COMMAND_2(agent_set_velocity, RID, p_agent, Vector3, p_velocity) { - RvoAgent *agent = agent_owner.get_or_null(p_agent); + NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_COND(agent == nullptr); agent->get_agent()->velocity_ = RVO::Vector3(p_velocity.x, p_velocity.y, p_velocity.z); } COMMAND_2(agent_set_target_velocity, RID, p_agent, Vector3, p_velocity) { - RvoAgent *agent = agent_owner.get_or_null(p_agent); + NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_COND(agent == nullptr); agent->get_agent()->prefVelocity_ = RVO::Vector3(p_velocity.x, p_velocity.y, p_velocity.z); } COMMAND_2(agent_set_position, RID, p_agent, Vector3, p_position) { - RvoAgent *agent = agent_owner.get_or_null(p_agent); + NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_COND(agent == nullptr); agent->get_agent()->position_ = RVO::Vector3(p_position.x, p_position.y, p_position.z); } COMMAND_2(agent_set_ignore_y, RID, p_agent, bool, p_ignore) { - RvoAgent *agent = agent_owner.get_or_null(p_agent); + NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_COND(agent == nullptr); agent->get_agent()->ignore_y_ = p_ignore; } bool GodotNavigationServer::agent_is_map_changed(RID p_agent) const { - RvoAgent *agent = agent_owner.get_or_null(p_agent); + NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_COND_V(agent == nullptr, false); return agent->is_map_changed(); } COMMAND_2(agent_set_callback, RID, p_agent, Callable, p_callback) { - RvoAgent *agent = agent_owner.get_or_null(p_agent); + NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_COND(agent == nullptr); agent->set_callback(p_callback); @@ -713,7 +713,7 @@ COMMAND_1(free, RID, p_object) { } // Remove any assigned agent - for (RvoAgent *agent : map->get_agents()) { + for (NavAgent *agent : map->get_agents()) { map->remove_agent(agent); agent->set_map(nullptr); } @@ -746,7 +746,7 @@ COMMAND_1(free, RID, p_object) { link_owner.free(p_object); } else if (agent_owner.owns(p_object)) { - RvoAgent *agent = agent_owner.get_or_null(p_object); + NavAgent *agent = agent_owner.get_or_null(p_object); // Removes this agent from the map if assigned if (agent->get_map() != nullptr) { diff --git a/modules/navigation/godot_navigation_server.h b/modules/navigation/godot_navigation_server.h index eea5713c40..0b113b77d4 100644 --- a/modules/navigation/godot_navigation_server.h +++ b/modules/navigation/godot_navigation_server.h @@ -36,10 +36,10 @@ #include "core/templates/rid_owner.h" #include "servers/navigation_server_3d.h" +#include "nav_agent.h" #include "nav_link.h" #include "nav_map.h" #include "nav_region.h" -#include "rvo_agent.h" /// The commands are functions executed during the `sync` phase. @@ -71,7 +71,7 @@ class GodotNavigationServer : public NavigationServer3D { mutable RID_Owner<NavLink> link_owner; mutable RID_Owner<NavMap> map_owner; mutable RID_Owner<NavRegion> region_owner; - mutable RID_Owner<RvoAgent> agent_owner; + mutable RID_Owner<NavAgent> agent_owner; bool active = true; LocalVector<NavMap *> active_maps; diff --git a/modules/navigation/rvo_agent.cpp b/modules/navigation/nav_agent.cpp index 40f1e925be..293544c0a5 100644 --- a/modules/navigation/rvo_agent.cpp +++ b/modules/navigation/nav_agent.cpp @@ -1,5 +1,5 @@ /**************************************************************************/ -/* rvo_agent.cpp */ +/* nav_agent.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,15 +28,15 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#include "rvo_agent.h" +#include "nav_agent.h" #include "nav_map.h" -void RvoAgent::set_map(NavMap *p_map) { +void NavAgent::set_map(NavMap *p_map) { map = p_map; } -bool RvoAgent::is_map_changed() { +bool NavAgent::is_map_changed() { if (map) { bool is_changed = map->get_map_update_id() != map_update_id; map_update_id = map->get_map_update_id(); @@ -46,15 +46,15 @@ bool RvoAgent::is_map_changed() { } } -void RvoAgent::set_callback(Callable p_callback) { +void NavAgent::set_callback(Callable p_callback) { callback = p_callback; } -bool RvoAgent::has_callback() const { +bool NavAgent::has_callback() const { return callback.is_valid(); } -void RvoAgent::dispatch_callback() { +void NavAgent::dispatch_callback() { if (!callback.is_valid()) { return; } diff --git a/modules/navigation/rvo_agent.h b/modules/navigation/nav_agent.h index 5f377b6079..f154ce14d9 100644 --- a/modules/navigation/rvo_agent.h +++ b/modules/navigation/nav_agent.h @@ -1,5 +1,5 @@ /**************************************************************************/ -/* rvo_agent.h */ +/* nav_agent.h */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,8 +28,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#ifndef RVO_AGENT_H -#define RVO_AGENT_H +#ifndef NAV_AGENT_H +#define NAV_AGENT_H #include "core/object/class_db.h" #include "nav_rid.h" @@ -38,7 +38,7 @@ class NavMap; -class RvoAgent : public NavRid { +class NavAgent : public NavRid { NavMap *map = nullptr; RVO::Agent agent; Callable callback = Callable(); @@ -62,4 +62,4 @@ public: void dispatch_callback(); }; -#endif // RVO_AGENT_H +#endif // NAV_AGENT_H diff --git a/modules/navigation/nav_map.cpp b/modules/navigation/nav_map.cpp index d763b1d3bc..b1674c8fc5 100644 --- a/modules/navigation/nav_map.cpp +++ b/modules/navigation/nav_map.cpp @@ -31,9 +31,9 @@ #include "nav_map.h" #include "core/object/worker_thread_pool.h" +#include "nav_agent.h" #include "nav_link.h" #include "nav_region.h" -#include "rvo_agent.h" #include <algorithm> #define THREE_POINTS_CROSS_PRODUCT(m_a, m_b, m_c) (((m_c) - (m_a)).cross((m_b) - (m_a))) @@ -568,18 +568,18 @@ void NavMap::remove_link(NavLink *p_link) { } } -bool NavMap::has_agent(RvoAgent *agent) const { +bool NavMap::has_agent(NavAgent *agent) const { return (agents.find(agent) != -1); } -void NavMap::add_agent(RvoAgent *agent) { +void NavMap::add_agent(NavAgent *agent) { if (!has_agent(agent)) { agents.push_back(agent); agents_dirty = true; } } -void NavMap::remove_agent(RvoAgent *agent) { +void NavMap::remove_agent(NavAgent *agent) { remove_agent_as_controlled(agent); int64_t agent_index = agents.find(agent); if (agent_index != -1) { @@ -588,7 +588,7 @@ void NavMap::remove_agent(RvoAgent *agent) { } } -void NavMap::set_agent_as_controlled(RvoAgent *agent) { +void NavMap::set_agent_as_controlled(NavAgent *agent) { const bool exist = (controlled_agents.find(agent) != -1); if (!exist) { ERR_FAIL_COND(!has_agent(agent)); @@ -596,7 +596,7 @@ void NavMap::set_agent_as_controlled(RvoAgent *agent) { } } -void NavMap::remove_agent_as_controlled(RvoAgent *agent) { +void NavMap::remove_agent_as_controlled(NavAgent *agent) { int64_t active_avoidance_agent_index = controlled_agents.find(agent); if (active_avoidance_agent_index != -1) { controlled_agents.remove_at_unordered(active_avoidance_agent_index); @@ -895,7 +895,7 @@ void NavMap::sync() { // cannot use LocalVector here as RVO library expects std::vector to build KdTree std::vector<RVO::Agent *> raw_agents; raw_agents.reserve(agents.size()); - for (RvoAgent *agent : agents) { + for (NavAgent *agent : agents) { raw_agents.push_back(agent->get_agent()); } rvo.buildAgentTree(raw_agents); @@ -916,7 +916,7 @@ void NavMap::sync() { pm_edge_free_count = _new_pm_edge_free_count; } -void NavMap::compute_single_step(uint32_t index, RvoAgent **agent) { +void NavMap::compute_single_step(uint32_t index, NavAgent **agent) { (*(agent + index))->get_agent()->computeNeighbors(&rvo); (*(agent + index))->get_agent()->computeNewVelocity(deltatime); } @@ -930,7 +930,7 @@ void NavMap::step(real_t p_deltatime) { } void NavMap::dispatch_callbacks() { - for (RvoAgent *agent : controlled_agents) { + for (NavAgent *agent : controlled_agents) { agent->dispatch_callback(); } } diff --git a/modules/navigation/nav_map.h b/modules/navigation/nav_map.h index fce7aff3ba..ab6a48dd70 100644 --- a/modules/navigation/nav_map.h +++ b/modules/navigation/nav_map.h @@ -42,7 +42,7 @@ class NavLink; class NavRegion; -class RvoAgent; +class NavAgent; class NavMap : public NavRid { /// Map Up @@ -78,10 +78,10 @@ class NavMap : public NavRid { bool agents_dirty = false; /// All the Agents (even the controlled one) - LocalVector<RvoAgent *> agents; + LocalVector<NavAgent *> agents; /// Controlled agents - LocalVector<RvoAgent *> controlled_agents; + LocalVector<NavAgent *> controlled_agents; /// Physics delta time real_t deltatime = 0.0; @@ -144,15 +144,15 @@ public: return links; } - bool has_agent(RvoAgent *agent) const; - void add_agent(RvoAgent *agent); - void remove_agent(RvoAgent *agent); - const LocalVector<RvoAgent *> &get_agents() const { + bool has_agent(NavAgent *agent) const; + void add_agent(NavAgent *agent); + void remove_agent(NavAgent *agent); + const LocalVector<NavAgent *> &get_agents() const { return agents; } - void set_agent_as_controlled(RvoAgent *agent); - void remove_agent_as_controlled(RvoAgent *agent); + void set_agent_as_controlled(NavAgent *agent); + void remove_agent_as_controlled(NavAgent *agent); uint32_t get_map_update_id() const { return map_update_id; @@ -173,7 +173,7 @@ public: int get_pm_edge_free_count() const { return pm_edge_free_count; } private: - void compute_single_step(uint32_t index, RvoAgent **agent); + void compute_single_step(uint32_t index, NavAgent **agent); void clip_path(const LocalVector<gd::NavigationPoly> &p_navigation_polys, Vector<Vector3> &path, const gd::NavigationPoly *from_poly, const Vector3 &p_to_point, const gd::NavigationPoly *p_to_poly, Vector<int32_t> *r_path_types, TypedArray<RID> *r_path_rids, Vector<int64_t> *r_path_owners) const; }; diff --git a/modules/openxr/SCsub b/modules/openxr/SCsub index 3b39967ba4..0dd41675b6 100644 --- a/modules/openxr/SCsub +++ b/modules/openxr/SCsub @@ -103,6 +103,7 @@ env_openxr.add_source_files(module_obj, "extensions/openxr_fb_passthrough_extens env_openxr.add_source_files(module_obj, "extensions/openxr_fb_display_refresh_rate_extension.cpp") env_openxr.add_source_files(module_obj, "extensions/openxr_pico_controller_extension.cpp") env_openxr.add_source_files(module_obj, "extensions/openxr_wmr_controller_extension.cpp") +env_openxr.add_source_files(module_obj, "extensions/openxr_ml2_controller_extension.cpp") env.modules_sources += module_obj diff --git a/modules/openxr/extensions/openxr_ml2_controller_extension.cpp b/modules/openxr/extensions/openxr_ml2_controller_extension.cpp new file mode 100644 index 0000000000..ae372f69b3 --- /dev/null +++ b/modules/openxr/extensions/openxr_ml2_controller_extension.cpp @@ -0,0 +1,71 @@ +/**************************************************************************/ +/* openxr_ml2_controller_extension.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 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 "openxr_ml2_controller_extension.h" +#include "../action_map/openxr_interaction_profile_meta_data.h" + +HashMap<String, bool *> OpenXRML2ControllerExtension::get_requested_extensions() { + HashMap<String, bool *> request_extensions; + + request_extensions[XR_ML_ML2_CONTROLLER_INTERACTION_EXTENSION_NAME] = &available; + + return request_extensions; +} + +bool OpenXRML2ControllerExtension::is_available() { + return available; +} + +void OpenXRML2ControllerExtension::on_register_metadata() { + OpenXRInteractionProfileMetaData *metadata = OpenXRInteractionProfileMetaData::get_singleton(); + ERR_FAIL_NULL(metadata); + + // Magic Leap 2 Controller + const String profile_path = "/interaction_profiles/ml/ml2_controller"; + metadata->register_interaction_profile("Magic Leap 2 controller", "/interaction_profiles/ml/ml2_controller", XR_ML_ML2_CONTROLLER_INTERACTION_EXTENSION_NAME); + for (const String user_path : { "/user/hand/left", "/user/hand/right" }) { + metadata->register_io_path(profile_path, "Grip pose", user_path, user_path + "/input/grip/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path(profile_path, "Aim pose", user_path, user_path + "/input/aim/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + + metadata->register_io_path(profile_path, "Menu click", user_path, user_path + "/input/menu/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path(profile_path, "Trigger", user_path, user_path + "/input/trigger/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path(profile_path, "Trigger click", user_path, user_path + "/input/trigger/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + + metadata->register_io_path(profile_path, "Shoulder click", user_path, user_path + "/input/shoulder/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + + metadata->register_io_path(profile_path, "Trackpad click", user_path, user_path + "/input/trackpad/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path(profile_path, "Trackpad force", user_path, user_path + "/input/trackpad/force", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path(profile_path, "Trackpad X", user_path, user_path + "/input/trackpad/x", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path(profile_path, "Trackpad Y", user_path, user_path + "/input/trackpad/y", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path(profile_path, "Trackpad touch", user_path, user_path + "/input/trackpad/touch", "", OpenXRAction::OPENXR_ACTION_VECTOR2); + + metadata->register_io_path(profile_path, "Haptic output", user_path, user_path + "/output/haptic", "", OpenXRAction::OPENXR_ACTION_HAPTIC); + } +} diff --git a/modules/openxr/extensions/openxr_ml2_controller_extension.h b/modules/openxr/extensions/openxr_ml2_controller_extension.h new file mode 100644 index 0000000000..216cd55a2f --- /dev/null +++ b/modules/openxr/extensions/openxr_ml2_controller_extension.h @@ -0,0 +1,48 @@ +/**************************************************************************/ +/* openxr_ml2_controller_extension.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 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. */ +/**************************************************************************/ + +#ifndef OPENXR_ML2_CONTROLLER_EXTENSION_H +#define OPENXR_ML2_CONTROLLER_EXTENSION_H + +#include "openxr_extension_wrapper.h" + +class OpenXRML2ControllerExtension : public OpenXRExtensionWrapper { +public: + virtual HashMap<String, bool *> get_requested_extensions() override; + + bool is_available(); + + virtual void on_register_metadata() override; + +private: + bool available = false; +}; + +#endif // OPENXR_ML2_CONTROLLER_EXTENSION_H diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp index 0a25cd68b7..ddb3114b59 100644 --- a/modules/openxr/openxr_api.cpp +++ b/modules/openxr/openxr_api.cpp @@ -483,6 +483,37 @@ bool OpenXRAPI::load_supported_view_configuration_types() { return true; } +bool OpenXRAPI::load_supported_environmental_blend_modes() { + // This queries the supported environmental blend modes. + + ERR_FAIL_COND_V(instance == XR_NULL_HANDLE, false); + + if (supported_environment_blend_modes != nullptr) { + // free previous results + memfree(supported_environment_blend_modes); + supported_environment_blend_modes = nullptr; + num_supported_environment_blend_modes = 0; + } + + XrResult result = xrEnumerateEnvironmentBlendModes(instance, system_id, view_configuration, 0, &num_supported_environment_blend_modes, nullptr); + if (XR_FAILED(result)) { + print_line("OpenXR: Failed to get supported environmental blend mode count [", get_error_string(result), "]"); + return false; + } + + supported_environment_blend_modes = (XrEnvironmentBlendMode *)memalloc(sizeof(XrEnvironmentBlendMode) * num_supported_environment_blend_modes); + ERR_FAIL_NULL_V(supported_environment_blend_modes, false); + + result = xrEnumerateEnvironmentBlendModes(instance, system_id, view_configuration, num_supported_environment_blend_modes, &num_supported_environment_blend_modes, supported_environment_blend_modes); + ERR_FAIL_COND_V_MSG(XR_FAILED(result), false, "OpenXR: Failed to enumerate environmental blend modes"); + + for (uint32_t i = 0; i < num_supported_environment_blend_modes; i++) { + print_verbose(String("OpenXR: Found environmental blend mode ") + OpenXRUtil::get_environment_blend_mode_name(supported_environment_blend_modes[i])); + } + + return true; +} + bool OpenXRAPI::is_view_configuration_supported(XrViewConfigurationType p_configuration_type) const { ERR_FAIL_NULL_V(supported_view_configuration_types, false); @@ -551,6 +582,12 @@ void OpenXRAPI::destroy_instance() { supported_view_configuration_types = nullptr; } + if (supported_environment_blend_modes != nullptr) { + memfree(supported_environment_blend_modes); + supported_environment_blend_modes = nullptr; + num_supported_environment_blend_modes = 0; + } + if (instance != XR_NULL_HANDLE) { for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { wrapper->on_instance_destroyed(); @@ -1205,6 +1242,7 @@ bool OpenXRAPI::resolve_instance_openxr_symbols() { OPENXR_API_INIT_XR_FUNC_V(xrDestroySwapchain); OPENXR_API_INIT_XR_FUNC_V(xrEndFrame); OPENXR_API_INIT_XR_FUNC_V(xrEndSession); + OPENXR_API_INIT_XR_FUNC_V(xrEnumerateEnvironmentBlendModes); OPENXR_API_INIT_XR_FUNC_V(xrEnumerateReferenceSpaces); OPENXR_API_INIT_XR_FUNC_V(xrEnumerateSwapchainFormats); OPENXR_API_INIT_XR_FUNC_V(xrEnumerateViewConfigurations); @@ -1312,6 +1350,11 @@ bool OpenXRAPI::initialize(const String &p_rendering_driver) { return false; } + if (!load_supported_environmental_blend_modes()) { + destroy_instance(); + return false; + } + return true; } @@ -1822,7 +1865,7 @@ void OpenXRAPI::end_frame() { XR_TYPE_FRAME_END_INFO, // type nullptr, // next frame_state.predictedDisplayTime, // displayTime - XR_ENVIRONMENT_BLEND_MODE_OPAQUE, // environmentBlendMode + environment_blend_mode, // environmentBlendMode 0, // layerCount nullptr // layers }; @@ -1874,7 +1917,7 @@ void OpenXRAPI::end_frame() { XR_TYPE_FRAME_END_INFO, // type nullptr, // next frame_state.predictedDisplayTime, // displayTime - XR_ENVIRONMENT_BLEND_MODE_OPAQUE, // environmentBlendMode + environment_blend_mode, // environmentBlendMode static_cast<uint32_t>(layers_list.size()), // layerCount layers_list.ptr() // layers }; @@ -2777,3 +2820,18 @@ void OpenXRAPI::register_composition_layer_provider(OpenXRCompositionLayerProvid void OpenXRAPI::unregister_composition_layer_provider(OpenXRCompositionLayerProvider *provider) { composition_layer_providers.erase(provider); } + +const XrEnvironmentBlendMode *OpenXRAPI::get_supported_environment_blend_modes(uint32_t &count) { + count = num_supported_environment_blend_modes; + return supported_environment_blend_modes; +} + +bool OpenXRAPI::set_environment_blend_mode(XrEnvironmentBlendMode mode) { + for (uint32_t i = 0; i < num_supported_environment_blend_modes; i++) { + if (supported_environment_blend_modes[i] == mode) { + environment_blend_mode = mode; + return true; + } + } + return false; +} diff --git a/modules/openxr/openxr_api.h b/modules/openxr/openxr_api.h index e1787c6da0..8c642c4ff4 100644 --- a/modules/openxr/openxr_api.h +++ b/modules/openxr/openxr_api.h @@ -99,9 +99,13 @@ private: XrFormFactor form_factor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; XrViewConfigurationType view_configuration = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; XrReferenceSpaceType reference_space = XR_REFERENCE_SPACE_TYPE_STAGE; - // XrEnvironmentBlendMode environment_blend_mode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; bool submit_depth_buffer = false; // if set to true we submit depth buffers to OpenXR if a suitable extension is enabled. + // blend mode + XrEnvironmentBlendMode environment_blend_mode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; + uint32_t num_supported_environment_blend_modes = 0; + XrEnvironmentBlendMode *supported_environment_blend_modes = nullptr; + // state XrInstance instance = XR_NULL_HANDLE; XrSystemId system_id = 0; @@ -182,6 +186,7 @@ private: EXT_PROTO_XRRESULT_FUNC2(xrEndFrame, (XrSession), session, (const XrFrameEndInfo *), frameEndInfo) EXT_PROTO_XRRESULT_FUNC1(xrEndSession, (XrSession), session) EXT_PROTO_XRRESULT_FUNC3(xrEnumerateApiLayerProperties, (uint32_t), propertyCapacityInput, (uint32_t *), propertyCountOutput, (XrApiLayerProperties *), properties) + EXT_PROTO_XRRESULT_FUNC6(xrEnumerateEnvironmentBlendModes, (XrInstance), instance, (XrSystemId), systemId, (XrViewConfigurationType), viewConfigurationType, (uint32_t), environmentBlendModeCapacityInput, (uint32_t *), environmentBlendModeCountOutput, (XrEnvironmentBlendMode *), environmentBlendModes) EXT_PROTO_XRRESULT_FUNC4(xrEnumerateInstanceExtensionProperties, (const char *), layerName, (uint32_t), propertyCapacityInput, (uint32_t *), propertyCountOutput, (XrExtensionProperties *), properties) EXT_PROTO_XRRESULT_FUNC4(xrEnumerateReferenceSpaces, (XrSession), session, (uint32_t), spaceCapacityInput, (uint32_t *), spaceCountOutput, (XrReferenceSpaceType *), spaces) EXT_PROTO_XRRESULT_FUNC4(xrEnumerateSwapchainFormats, (XrSession), session, (uint32_t), formatCapacityInput, (uint32_t *), formatCountOutput, (int64_t *), formats) @@ -210,6 +215,7 @@ private: bool create_instance(); bool get_system_info(); bool load_supported_view_configuration_types(); + bool load_supported_environmental_blend_modes(); bool is_view_configuration_supported(XrViewConfigurationType p_configuration_type) const; bool load_supported_view_configuration_views(XrViewConfigurationType p_configuration_type); void destroy_instance(); @@ -390,6 +396,9 @@ public: void register_composition_layer_provider(OpenXRCompositionLayerProvider *provider); void unregister_composition_layer_provider(OpenXRCompositionLayerProvider *provider); + const XrEnvironmentBlendMode *get_supported_environment_blend_modes(uint32_t &count); + bool set_environment_blend_mode(XrEnvironmentBlendMode mode); + OpenXRAPI(); ~OpenXRAPI(); }; diff --git a/modules/openxr/openxr_interface.cpp b/modules/openxr/openxr_interface.cpp index 702e56b410..51de9b913a 100644 --- a/modules/openxr/openxr_interface.cpp +++ b/modules/openxr/openxr_interface.cpp @@ -870,6 +870,60 @@ void OpenXRInterface::stop_passthrough() { } } +Array OpenXRInterface::get_supported_environment_blend_modes() { + Array modes; + + if (!openxr_api) { + return modes; + } + + uint32_t count = 0; + const XrEnvironmentBlendMode *env_blend_modes = openxr_api->get_supported_environment_blend_modes(count); + + if (!env_blend_modes) { + return modes; + } + + for (uint32_t i = 0; i < count; i++) { + switch (env_blend_modes[i]) { + case XR_ENVIRONMENT_BLEND_MODE_OPAQUE: + modes.push_back(XR_ENV_BLEND_MODE_OPAQUE); + break; + case XR_ENVIRONMENT_BLEND_MODE_ADDITIVE: + modes.push_back(XR_ENV_BLEND_MODE_ADDITIVE); + break; + case XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND: + modes.push_back(XR_ENV_BLEND_MODE_ALPHA_BLEND); + break; + default: + WARN_PRINT("Unsupported blend mode found: " + String::num_int64(int64_t(env_blend_modes[i]))); + } + } + return modes; +} + +bool OpenXRInterface::set_environment_blend_mode(XRInterface::EnvironmentBlendMode mode) { + if (openxr_api) { + XrEnvironmentBlendMode oxr_blend_mode; + switch (mode) { + case XR_ENV_BLEND_MODE_OPAQUE: + oxr_blend_mode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; + break; + case XR_ENV_BLEND_MODE_ADDITIVE: + oxr_blend_mode = XR_ENVIRONMENT_BLEND_MODE_ADDITIVE; + break; + case XR_ENV_BLEND_MODE_ALPHA_BLEND: + oxr_blend_mode = XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND; + break; + default: + WARN_PRINT("Unknown blend mode requested: " + String::num_int64(int64_t(mode))); + oxr_blend_mode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; + } + return openxr_api->set_environment_blend_mode(oxr_blend_mode); + } + return false; +} + void OpenXRInterface::on_state_ready() { emit_signal(SNAME("session_begun")); } diff --git a/modules/openxr/openxr_interface.h b/modules/openxr/openxr_interface.h index cce329d8e6..40ee95f02f 100644 --- a/modules/openxr/openxr_interface.h +++ b/modules/openxr/openxr_interface.h @@ -147,6 +147,10 @@ public: virtual bool start_passthrough() override; virtual void stop_passthrough() override; + /** environment blend mode. */ + virtual Array get_supported_environment_blend_modes() override; + virtual bool set_environment_blend_mode(XRInterface::EnvironmentBlendMode mode) override; + void on_state_ready(); void on_state_visible(); void on_state_focused(); diff --git a/modules/openxr/openxr_util.cpp b/modules/openxr/openxr_util.cpp index 50ebf468b9..926e918390 100644 --- a/modules/openxr/openxr_util.cpp +++ b/modules/openxr/openxr_util.cpp @@ -29,267 +29,38 @@ /**************************************************************************/ #include "openxr_util.h" +#include <openxr/openxr_reflection.h> -#define ENUM_TO_STRING_CASE(e) \ - case e: { \ - return String(#e); \ - } break; +#define XR_ENUM_CASE_STR(name, val) \ + case name: \ + return #name; +#define XR_ENUM_SWITCH(enumType, var) \ + switch (var) { \ + XR_LIST_ENUM_##enumType(XR_ENUM_CASE_STR) default : return "Unknown " #enumType ": " + String::num_int64(int64_t(var)); \ + } -// TODO see if we can generate this code further using the xml file with meta data supplied by OpenXR +String OpenXRUtil::get_view_configuration_name(XrViewConfigurationType p_view_configuration){ + XR_ENUM_SWITCH(XrViewConfigurationType, p_view_configuration) +} -String OpenXRUtil::get_view_configuration_name(XrViewConfigurationType p_view_configuration) { - switch (p_view_configuration) { - ENUM_TO_STRING_CASE(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MONO) - ENUM_TO_STRING_CASE(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO) - ENUM_TO_STRING_CASE(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_QUAD_VARJO) - ENUM_TO_STRING_CASE(XR_VIEW_CONFIGURATION_TYPE_SECONDARY_MONO_FIRST_PERSON_OBSERVER_MSFT) - ENUM_TO_STRING_CASE(XR_VIEW_CONFIGURATION_TYPE_MAX_ENUM) - default: { - return String("View Configuration ") + String::num_int64(int64_t(p_view_configuration)); - } break; - } +String OpenXRUtil::get_reference_space_name(XrReferenceSpaceType p_reference_space){ + XR_ENUM_SWITCH(XrReferenceSpaceType, p_reference_space) } -String OpenXRUtil::get_reference_space_name(XrReferenceSpaceType p_reference_space) { - switch (p_reference_space) { - ENUM_TO_STRING_CASE(XR_REFERENCE_SPACE_TYPE_VIEW) - ENUM_TO_STRING_CASE(XR_REFERENCE_SPACE_TYPE_LOCAL) - ENUM_TO_STRING_CASE(XR_REFERENCE_SPACE_TYPE_STAGE) - ENUM_TO_STRING_CASE(XR_REFERENCE_SPACE_TYPE_UNBOUNDED_MSFT) - ENUM_TO_STRING_CASE(XR_REFERENCE_SPACE_TYPE_COMBINED_EYE_VARJO) - ENUM_TO_STRING_CASE(XR_REFERENCE_SPACE_TYPE_MAX_ENUM) - default: { - return String("Reference space ") + String::num_int64(int64_t(p_reference_space)); - } break; - } +String OpenXRUtil::get_structure_type_name(XrStructureType p_structure_type){ + XR_ENUM_SWITCH(XrStructureType, p_structure_type) } -String OpenXRUtil::get_structure_type_name(XrStructureType p_structure_type) { - switch (p_structure_type) { - ENUM_TO_STRING_CASE(XR_TYPE_UNKNOWN) - ENUM_TO_STRING_CASE(XR_TYPE_API_LAYER_PROPERTIES) - ENUM_TO_STRING_CASE(XR_TYPE_EXTENSION_PROPERTIES) - ENUM_TO_STRING_CASE(XR_TYPE_INSTANCE_CREATE_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_SYSTEM_GET_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_SYSTEM_PROPERTIES) - ENUM_TO_STRING_CASE(XR_TYPE_VIEW_LOCATE_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_VIEW) - ENUM_TO_STRING_CASE(XR_TYPE_SESSION_CREATE_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_CREATE_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_SESSION_BEGIN_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_VIEW_STATE) - ENUM_TO_STRING_CASE(XR_TYPE_FRAME_END_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_HAPTIC_VIBRATION) - ENUM_TO_STRING_CASE(XR_TYPE_EVENT_DATA_BUFFER) - ENUM_TO_STRING_CASE(XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING) - ENUM_TO_STRING_CASE(XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) - ENUM_TO_STRING_CASE(XR_TYPE_ACTION_STATE_BOOLEAN) - ENUM_TO_STRING_CASE(XR_TYPE_ACTION_STATE_FLOAT) - ENUM_TO_STRING_CASE(XR_TYPE_ACTION_STATE_VECTOR2F) - ENUM_TO_STRING_CASE(XR_TYPE_ACTION_STATE_POSE) - ENUM_TO_STRING_CASE(XR_TYPE_ACTION_SET_CREATE_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_ACTION_CREATE_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_INSTANCE_PROPERTIES) - ENUM_TO_STRING_CASE(XR_TYPE_FRAME_WAIT_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_PROJECTION) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_QUAD) - ENUM_TO_STRING_CASE(XR_TYPE_REFERENCE_SPACE_CREATE_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_ACTION_SPACE_CREATE_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING) - ENUM_TO_STRING_CASE(XR_TYPE_VIEW_CONFIGURATION_VIEW) - ENUM_TO_STRING_CASE(XR_TYPE_SPACE_LOCATION) - ENUM_TO_STRING_CASE(XR_TYPE_SPACE_VELOCITY) - ENUM_TO_STRING_CASE(XR_TYPE_FRAME_STATE) - ENUM_TO_STRING_CASE(XR_TYPE_VIEW_CONFIGURATION_PROPERTIES) - ENUM_TO_STRING_CASE(XR_TYPE_FRAME_BEGIN_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW) - ENUM_TO_STRING_CASE(XR_TYPE_EVENT_DATA_EVENTS_LOST) - ENUM_TO_STRING_CASE(XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING) - ENUM_TO_STRING_CASE(XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED) - ENUM_TO_STRING_CASE(XR_TYPE_INTERACTION_PROFILE_STATE) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_ACTION_STATE_GET_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_HAPTIC_ACTION_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_ACTIONS_SYNC_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_BOUND_SOURCES_FOR_ACTION_ENUMERATE_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_INPUT_SOURCE_LOCALIZED_NAME_GET_INFO) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_CUBE_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_VULKAN_SWAPCHAIN_FORMAT_LIST_CREATE_INFO_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_EVENT_DATA_PERF_SETTINGS_EXT) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_EQUIRECT_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT) - ENUM_TO_STRING_CASE(XR_TYPE_DEBUG_UTILS_MESSENGER_CALLBACK_DATA_EXT) - ENUM_TO_STRING_CASE(XR_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT) - ENUM_TO_STRING_CASE(XR_TYPE_DEBUG_UTILS_LABEL_EXT) - ENUM_TO_STRING_CASE(XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_GRAPHICS_BINDING_OPENGL_XCB_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_GRAPHICS_BINDING_OPENGL_WAYLAND_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_GRAPHICS_BINDING_D3D11_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_IMAGE_D3D11_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_GRAPHICS_REQUIREMENTS_D3D11_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_GRAPHICS_BINDING_D3D12_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_IMAGE_D3D12_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_GRAPHICS_REQUIREMENTS_D3D12_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_SYSTEM_EYE_GAZE_INTERACTION_PROPERTIES_EXT) - ENUM_TO_STRING_CASE(XR_TYPE_EYE_GAZE_SAMPLE_TIME_EXT) - ENUM_TO_STRING_CASE(XR_TYPE_VISIBILITY_MASK_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_EVENT_DATA_VISIBILITY_MASK_CHANGED_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_SESSION_CREATE_INFO_OVERLAY_EXTX) - ENUM_TO_STRING_CASE(XR_TYPE_EVENT_DATA_MAIN_SESSION_VISIBILITY_CHANGED_EXTX) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_COLOR_SCALE_BIAS_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_SPATIAL_ANCHOR_CREATE_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SPATIAL_ANCHOR_SPACE_CREATE_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_IMAGE_LAYOUT_FB) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_ALPHA_BLEND_FB) - ENUM_TO_STRING_CASE(XR_TYPE_VIEW_CONFIGURATION_DEPTH_RANGE_EXT) - ENUM_TO_STRING_CASE(XR_TYPE_GRAPHICS_BINDING_EGL_MNDX) - ENUM_TO_STRING_CASE(XR_TYPE_SPATIAL_GRAPH_NODE_SPACE_CREATE_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SYSTEM_HAND_TRACKING_PROPERTIES_EXT) - ENUM_TO_STRING_CASE(XR_TYPE_HAND_TRACKER_CREATE_INFO_EXT) - ENUM_TO_STRING_CASE(XR_TYPE_HAND_JOINTS_LOCATE_INFO_EXT) - ENUM_TO_STRING_CASE(XR_TYPE_HAND_JOINT_LOCATIONS_EXT) - ENUM_TO_STRING_CASE(XR_TYPE_HAND_JOINT_VELOCITIES_EXT) - ENUM_TO_STRING_CASE(XR_TYPE_SYSTEM_HAND_TRACKING_MESH_PROPERTIES_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_HAND_MESH_SPACE_CREATE_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_HAND_MESH_UPDATE_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_HAND_MESH_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_HAND_POSE_TYPE_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SECONDARY_VIEW_CONFIGURATION_SESSION_BEGIN_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SECONDARY_VIEW_CONFIGURATION_STATE_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SECONDARY_VIEW_CONFIGURATION_FRAME_STATE_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SECONDARY_VIEW_CONFIGURATION_FRAME_END_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SECONDARY_VIEW_CONFIGURATION_LAYER_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SECONDARY_VIEW_CONFIGURATION_SWAPCHAIN_CREATE_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_CONTROLLER_MODEL_KEY_STATE_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_CONTROLLER_MODEL_NODE_PROPERTIES_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_CONTROLLER_MODEL_PROPERTIES_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_CONTROLLER_MODEL_NODE_STATE_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_CONTROLLER_MODEL_STATE_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_VIEW_CONFIGURATION_VIEW_FOV_EPIC) - ENUM_TO_STRING_CASE(XR_TYPE_HOLOGRAPHIC_WINDOW_ATTACHMENT_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_REPROJECTION_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_REPROJECTION_PLANE_OVERRIDE_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_ANDROID_SURFACE_SWAPCHAIN_CREATE_INFO_FB) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_SECURE_CONTENT_FB) - ENUM_TO_STRING_CASE(XR_TYPE_INTERACTION_PROFILE_ANALOG_THRESHOLD_VALVE) - ENUM_TO_STRING_CASE(XR_TYPE_HAND_JOINTS_MOTION_RANGE_INFO_EXT) - ENUM_TO_STRING_CASE(XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_VULKAN_INSTANCE_CREATE_INFO_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_VULKAN_DEVICE_CREATE_INFO_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_VULKAN_GRAPHICS_DEVICE_GET_INFO_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_OBSERVER_CREATE_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_CREATE_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_NEW_SCENE_COMPUTE_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_VISUAL_MESH_COMPUTE_LOD_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_COMPONENTS_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_COMPONENTS_GET_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_COMPONENT_LOCATIONS_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_COMPONENTS_LOCATE_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_OBJECTS_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_COMPONENT_PARENT_FILTER_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_OBJECT_TYPES_FILTER_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_PLANES_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_PLANE_ALIGNMENT_FILTER_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_MESHES_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_MESH_BUFFERS_GET_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_MESH_BUFFERS_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_MESH_VERTEX_BUFFER_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_MESH_INDICES_UINT32_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_MESH_INDICES_UINT16_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SERIALIZED_SCENE_FRAGMENT_DATA_GET_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SCENE_DESERIALIZE_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_EVENT_DATA_DISPLAY_REFRESH_RATE_CHANGED_FB) - ENUM_TO_STRING_CASE(XR_TYPE_VIVE_TRACKER_PATHS_HTCX) - ENUM_TO_STRING_CASE(XR_TYPE_EVENT_DATA_VIVE_TRACKER_CONNECTED_HTCX) - ENUM_TO_STRING_CASE(XR_TYPE_SYSTEM_COLOR_SPACE_PROPERTIES_FB) - ENUM_TO_STRING_CASE(XR_TYPE_HAND_TRACKING_MESH_FB) - ENUM_TO_STRING_CASE(XR_TYPE_HAND_TRACKING_SCALE_FB) - ENUM_TO_STRING_CASE(XR_TYPE_HAND_TRACKING_AIM_STATE_FB) - ENUM_TO_STRING_CASE(XR_TYPE_HAND_TRACKING_CAPSULES_STATE_FB) - ENUM_TO_STRING_CASE(XR_TYPE_FOVEATION_PROFILE_CREATE_INFO_FB) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_CREATE_INFO_FOVEATION_FB) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_STATE_FOVEATION_FB) - ENUM_TO_STRING_CASE(XR_TYPE_FOVEATION_LEVEL_PROFILE_CREATE_INFO_FB) - ENUM_TO_STRING_CASE(XR_TYPE_TRIANGLE_MESH_CREATE_INFO_FB) - ENUM_TO_STRING_CASE(XR_TYPE_SYSTEM_PASSTHROUGH_PROPERTIES_FB) - ENUM_TO_STRING_CASE(XR_TYPE_PASSTHROUGH_CREATE_INFO_FB) - ENUM_TO_STRING_CASE(XR_TYPE_PASSTHROUGH_LAYER_CREATE_INFO_FB) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_PASSTHROUGH_FB) - ENUM_TO_STRING_CASE(XR_TYPE_GEOMETRY_INSTANCE_CREATE_INFO_FB) - ENUM_TO_STRING_CASE(XR_TYPE_GEOMETRY_INSTANCE_TRANSFORM_FB) - ENUM_TO_STRING_CASE(XR_TYPE_PASSTHROUGH_STYLE_FB) - ENUM_TO_STRING_CASE(XR_TYPE_PASSTHROUGH_COLOR_MAP_MONO_TO_RGBA_FB) - ENUM_TO_STRING_CASE(XR_TYPE_PASSTHROUGH_COLOR_MAP_MONO_TO_MONO_FB) - ENUM_TO_STRING_CASE(XR_TYPE_EVENT_DATA_PASSTHROUGH_STATE_CHANGED_FB) - ENUM_TO_STRING_CASE(XR_TYPE_BINDING_MODIFICATIONS_KHR) - ENUM_TO_STRING_CASE(XR_TYPE_VIEW_LOCATE_FOVEATED_RENDERING_VARJO) - ENUM_TO_STRING_CASE(XR_TYPE_FOVEATED_VIEW_CONFIGURATION_VIEW_VARJO) - ENUM_TO_STRING_CASE(XR_TYPE_SYSTEM_FOVEATED_RENDERING_PROPERTIES_VARJO) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_DEPTH_TEST_VARJO) - ENUM_TO_STRING_CASE(XR_TYPE_SYSTEM_MARKER_TRACKING_PROPERTIES_VARJO) - ENUM_TO_STRING_CASE(XR_TYPE_EVENT_DATA_MARKER_TRACKING_UPDATE_VARJO) - ENUM_TO_STRING_CASE(XR_TYPE_MARKER_SPACE_CREATE_INFO_VARJO) - ENUM_TO_STRING_CASE(XR_TYPE_SPATIAL_ANCHOR_PERSISTENCE_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SPATIAL_ANCHOR_FROM_PERSISTED_ANCHOR_CREATE_INFO_MSFT) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_IMAGE_FOVEATION_VULKAN_FB) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_STATE_ANDROID_SURFACE_DIMENSIONS_FB) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_STATE_SAMPLER_OPENGL_ES_FB) - ENUM_TO_STRING_CASE(XR_TYPE_SWAPCHAIN_STATE_SAMPLER_VULKAN_FB) - ENUM_TO_STRING_CASE(XR_TYPE_COMPOSITION_LAYER_SPACE_WARP_INFO_FB) - ENUM_TO_STRING_CASE(XR_TYPE_SYSTEM_SPACE_WARP_PROPERTIES_FB) - ENUM_TO_STRING_CASE(XR_STRUCTURE_TYPE_MAX_ENUM) - default: { - return String("Structure type ") + String::num_int64(int64_t(p_structure_type)); - } break; - } +String OpenXRUtil::get_session_state_name(XrSessionState p_session_state){ + XR_ENUM_SWITCH(XrSessionState, p_session_state) } -String OpenXRUtil::get_session_state_name(XrSessionState p_session_state) { - switch (p_session_state) { - ENUM_TO_STRING_CASE(XR_SESSION_STATE_UNKNOWN) - ENUM_TO_STRING_CASE(XR_SESSION_STATE_IDLE) - ENUM_TO_STRING_CASE(XR_SESSION_STATE_READY) - ENUM_TO_STRING_CASE(XR_SESSION_STATE_SYNCHRONIZED) - ENUM_TO_STRING_CASE(XR_SESSION_STATE_VISIBLE) - ENUM_TO_STRING_CASE(XR_SESSION_STATE_FOCUSED) - ENUM_TO_STRING_CASE(XR_SESSION_STATE_STOPPING) - ENUM_TO_STRING_CASE(XR_SESSION_STATE_LOSS_PENDING) - ENUM_TO_STRING_CASE(XR_SESSION_STATE_EXITING) - ENUM_TO_STRING_CASE(XR_SESSION_STATE_MAX_ENUM) - default: { - return String("Session state ") + String::num_int64(int64_t(p_session_state)); - } break; - } +String OpenXRUtil::get_action_type_name(XrActionType p_action_type){ + XR_ENUM_SWITCH(XrActionType, p_action_type) } -String OpenXRUtil::get_action_type_name(XrActionType p_action_type) { - switch (p_action_type) { - ENUM_TO_STRING_CASE(XR_ACTION_TYPE_BOOLEAN_INPUT) - ENUM_TO_STRING_CASE(XR_ACTION_TYPE_FLOAT_INPUT) - ENUM_TO_STRING_CASE(XR_ACTION_TYPE_VECTOR2F_INPUT) - ENUM_TO_STRING_CASE(XR_ACTION_TYPE_POSE_INPUT) - ENUM_TO_STRING_CASE(XR_ACTION_TYPE_VIBRATION_OUTPUT) - ENUM_TO_STRING_CASE(XR_ACTION_TYPE_MAX_ENUM) - default: { - return String("Action type ") + String::num_int64(int64_t(p_action_type)); - } break; - } +String OpenXRUtil::get_environment_blend_mode_name(XrEnvironmentBlendMode p_blend_mode) { + XR_ENUM_SWITCH(XrEnvironmentBlendMode, p_blend_mode); } String OpenXRUtil::make_xr_version_string(XrVersion p_version) { diff --git a/modules/openxr/openxr_util.h b/modules/openxr/openxr_util.h index dfda537474..7e7a6a1880 100644 --- a/modules/openxr/openxr_util.h +++ b/modules/openxr/openxr_util.h @@ -41,6 +41,7 @@ public: static String get_structure_type_name(XrStructureType p_structure_type); static String get_session_state_name(XrSessionState p_session_state); static String get_action_type_name(XrActionType p_action_type); + static String get_environment_blend_mode_name(XrEnvironmentBlendMode p_blend_mode); static String make_xr_version_string(XrVersion p_version); }; diff --git a/modules/openxr/register_types.cpp b/modules/openxr/register_types.cpp index 306a2f1bbd..c39e49387a 100644 --- a/modules/openxr/register_types.cpp +++ b/modules/openxr/register_types.cpp @@ -52,6 +52,7 @@ #include "extensions/openxr_htc_controller_extension.h" #include "extensions/openxr_htc_vive_tracker_extension.h" #include "extensions/openxr_huawei_controller_extension.h" +#include "extensions/openxr_ml2_controller_extension.h" #include "extensions/openxr_palm_pose_extension.h" #include "extensions/openxr_pico_controller_extension.h" #include "extensions/openxr_wmr_controller_extension.h" @@ -102,6 +103,7 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) { OpenXRAPI::register_extension_wrapper(memnew(OpenXRFbPassthroughExtensionWrapper)); OpenXRAPI::register_extension_wrapper(memnew(OpenXRDisplayRefreshRateExtension)); OpenXRAPI::register_extension_wrapper(memnew(OpenXRWMRControllerExtension)); + OpenXRAPI::register_extension_wrapper(memnew(OpenXRML2ControllerExtension)); } if (OpenXRAPI::openxr_is_enabled()) { diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index 9b474bf2ce..b55188ce0c 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -6118,20 +6118,22 @@ int64_t TextServerAdvanced::_is_confusable(const String &p_string, const PackedS Vector<UChar *> skeletons; skeletons.resize(p_dict.size()); - USpoofChecker *sc = uspoof_open(&status); - uspoof_setChecks(sc, USPOOF_CONFUSABLE, &status); + if (sc_conf == nullptr) { + sc_conf = uspoof_open(&status); + uspoof_setChecks(sc_conf, USPOOF_CONFUSABLE, &status); + } for (int i = 0; i < p_dict.size(); i++) { Char16String word = p_dict[i].utf16(); - int32_t len = uspoof_getSkeleton(sc, 0, word.get_data(), -1, NULL, 0, &status); + int32_t len = uspoof_getSkeleton(sc_conf, 0, word.get_data(), -1, NULL, 0, &status); skeletons.write[i] = (UChar *)memalloc(++len * sizeof(UChar)); status = U_ZERO_ERROR; - uspoof_getSkeleton(sc, 0, word.get_data(), -1, skeletons.write[i], len, &status); + uspoof_getSkeleton(sc_conf, 0, word.get_data(), -1, skeletons.write[i], len, &status); } - int32_t len = uspoof_getSkeleton(sc, 0, utf16.get_data(), -1, NULL, 0, &status); + int32_t len = uspoof_getSkeleton(sc_conf, 0, utf16.get_data(), -1, NULL, 0, &status); UChar *skel = (UChar *)memalloc(++len * sizeof(UChar)); status = U_ZERO_ERROR; - uspoof_getSkeleton(sc, 0, utf16.get_data(), -1, skel, len, &status); + uspoof_getSkeleton(sc_conf, 0, utf16.get_data(), -1, skel, len, &status); for (int i = 0; i < skeletons.size(); i++) { if (u_strcmp(skel, skeletons[i]) == 0) { match_index = i; @@ -6143,7 +6145,6 @@ int64_t TextServerAdvanced::_is_confusable(const String &p_string, const PackedS for (int i = 0; i < skeletons.size(); i++) { memfree(skeletons.write[i]); } - uspoof_close(sc); ERR_FAIL_COND_V_MSG(U_FAILURE(status), -1, u_errorName(status)); @@ -6159,19 +6160,18 @@ bool TextServerAdvanced::_spoof_check(const String &p_string) const { UErrorCode status = U_ZERO_ERROR; Char16String utf16 = p_string.utf16(); - USet *allowed = uset_openEmpty(); - uset_addAll(allowed, uspoof_getRecommendedSet(&status)); - uset_addAll(allowed, uspoof_getInclusionSet(&status)); - - USpoofChecker *sc = uspoof_open(&status); - uspoof_setAllowedChars(sc, allowed, &status); - uspoof_setRestrictionLevel(sc, USPOOF_MODERATELY_RESTRICTIVE); - - int32_t bitmask = uspoof_check(sc, utf16.get_data(), -1, NULL, &status); - - uspoof_close(sc); - uset_close(allowed); + if (allowed == nullptr) { + allowed = uset_openEmpty(); + uset_addAll(allowed, uspoof_getRecommendedSet(&status)); + uset_addAll(allowed, uspoof_getInclusionSet(&status)); + } + if (sc_spoof == nullptr) { + sc_spoof = uspoof_open(&status); + uspoof_setAllowedChars(sc_spoof, allowed, &status); + uspoof_setRestrictionLevel(sc_spoof, USPOOF_MODERATELY_RESTRICTIVE); + } + int32_t bitmask = uspoof_check(sc_spoof, utf16.get_data(), -1, NULL, &status); ERR_FAIL_COND_V_MSG(U_FAILURE(status), false, u_errorName(status)); return (bitmask != 0); @@ -6587,5 +6587,17 @@ TextServerAdvanced::~TextServerAdvanced() { FT_Done_FreeType(ft_library); } #endif + if (sc_spoof != nullptr) { + uspoof_close(sc_spoof); + sc_spoof = nullptr; + } + if (sc_conf != nullptr) { + uspoof_close(sc_conf); + sc_conf = nullptr; + } + if (allowed != nullptr) { + uset_close(allowed); + allowed = nullptr; + } u_cleanup(); } diff --git a/modules/text_server_adv/text_server_adv.h b/modules/text_server_adv/text_server_adv.h index c7fe46d554..1acf5b21f0 100644 --- a/modules/text_server_adv/text_server_adv.h +++ b/modules/text_server_adv/text_server_adv.h @@ -159,6 +159,9 @@ class TextServerAdvanced : public TextServerExtension { // ICU support data. bool icu_data_loaded = false; + mutable USet *allowed = nullptr; + mutable USpoofChecker *sc_spoof = nullptr; + mutable USpoofChecker *sc_conf = nullptr; // Font cache data. |