diff options
Diffstat (limited to 'modules')
30 files changed, 784 insertions, 386 deletions
diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index a6840b54b8..95e577c140 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -494,8 +494,8 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type result = ref->get_parser()->head->get_datatype(); } else { result.kind = GDScriptParser::DataType::SCRIPT; - result.native_type = ScriptServer::get_global_class_native_base(first); result.script_type = ResourceLoader::load(path, "Script"); + result.native_type = result.script_type->get_instance_base_type(); result.script_path = path; result.is_constant = true; result.is_meta_type = false; @@ -2733,21 +2733,13 @@ GDScriptParser::DataType GDScriptAnalyzer::make_global_class_meta_type(const Str return type; } - type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; - type.kind = GDScriptParser::DataType::CLASS; - type.builtin_type = Variant::OBJECT; - type.native_type = ScriptServer::get_global_class_native_base(p_class_name); - type.class_type = ref->get_parser()->head; - type.script_path = ref->get_parser()->script_path; - type.is_constant = true; - type.is_meta_type = true; - return type; + return ref->get_parser()->head->get_datatype(); } else { type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; type.kind = GDScriptParser::DataType::SCRIPT; type.builtin_type = Variant::OBJECT; - type.native_type = ScriptServer::get_global_class_native_base(p_class_name); type.script_type = ResourceLoader::load(path, "Script"); + type.native_type = type.script_type->get_instance_base_type(); type.script_path = path; type.is_constant = true; type.is_meta_type = true; diff --git a/modules/gdscript/gdscript_cache.cpp b/modules/gdscript/gdscript_cache.cpp index 2e7263b652..021504f242 100644 --- a/modules/gdscript/gdscript_cache.cpp +++ b/modules/gdscript/gdscript_cache.cpp @@ -260,7 +260,7 @@ Ref<GDScript> GDScriptCache::get_full_script(const String &p_path, Error &r_erro Ref<GDScript> script; r_error = OK; if (singleton->full_gdscript_cache.has(p_path)) { - script = Ref<GDScript>(singleton->full_gdscript_cache[p_path]); + script = singleton->full_gdscript_cache[p_path]; if (!p_update_from_disk) { return script; } @@ -360,7 +360,6 @@ Ref<PackedScene> GDScriptCache::get_packed_scene(const String &p_path, Error &r_ singleton->packed_scene_cache[p_path] = scene; singleton->packed_scene_dependencies[p_path].insert(p_owner); - scene->recreate_state(); scene->reload_from_file(); return scene; } diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp index f0ceb42f89..103eb60da9 100644 --- a/modules/gdscript/gdscript_compiler.cpp +++ b/modules/gdscript/gdscript_compiler.cpp @@ -2284,7 +2284,7 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri _set_error(vformat(R"(Could not find class "%s" in "%s".)", base->fully_qualified_name, base->path), nullptr); return ERR_COMPILATION_FAILED; } - ERR_FAIL_COND_V(!base->is_valid(), ERR_BUG); + ERR_FAIL_COND_V(!base->is_valid() && !base->reloading, ERR_BUG); } p_script->base = base; diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 7628bffd22..c02ee99a86 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -3265,15 +3265,6 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co } } - // Need special checks for assert and preload as they are technically - // keywords, so are not registered in GDScriptUtilityFunctions. - if (GDScriptUtilityFunctions::function_exists(p_symbol) || "assert" == p_symbol || "preload" == p_symbol) { - r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD; - r_result.class_name = "@GDScript"; - r_result.class_member = p_symbol; - return OK; - } - if ("PI" == p_symbol || "TAU" == p_symbol || "INF" == p_symbol || "NAN" == p_symbol) { r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT; r_result.class_name = "@GDScript"; @@ -3283,11 +3274,24 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co GDScriptParser parser; parser.parse(p_code, p_path, true); - GDScriptAnalyzer analyzer(&parser); - analyzer.analyze(); GDScriptParser::CompletionContext context = parser.get_completion_context(); + // Allows class functions with the names like built-ins to be handled properly. + if (context.type != GDScriptParser::COMPLETION_ATTRIBUTE) { + // Need special checks for assert and preload as they are technically + // keywords, so are not registered in GDScriptUtilityFunctions. + if (GDScriptUtilityFunctions::function_exists(p_symbol) || "assert" == p_symbol || "preload" == p_symbol) { + r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD; + r_result.class_name = "@GDScript"; + r_result.class_member = p_symbol; + return OK; + } + } + + GDScriptAnalyzer analyzer(&parser); + analyzer.analyze(); + if (context.current_class && context.current_class->extends.size() > 0) { bool success = false; ClassDB::get_integer_constant(context.current_class->extends[0], p_symbol, &success); diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 84f6fb230c..f8fe015c6b 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -3801,16 +3801,19 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node break; case GDScriptParser::DataType::SCRIPT: { StringName class_name; - if (export_type.script_type != nullptr && export_type.script_type.is_valid()) { + StringName native_base; + if (export_type.script_type.is_valid()) { class_name = export_type.script_type->get_language()->get_global_class_name(export_type.script_type->get_path()); + native_base = export_type.script_type->get_instance_base_type(); } if (class_name == StringName()) { Ref<Script> script = ResourceLoader::load(export_type.script_path, SNAME("Script")); if (script.is_valid()) { class_name = script->get_language()->get_global_class_name(export_type.script_path); + native_base = script->get_instance_base_type(); } } - if (class_name != StringName() && ClassDB::is_parent_class(ScriptServer::get_global_class_native_base(class_name), SNAME("Resource"))) { + if (class_name != StringName() && native_base != StringName() && ClassDB::is_parent_class(native_base, SNAME("Resource"))) { variable->export_info.type = Variant::OBJECT; variable->export_info.hint = PROPERTY_HINT_RESOURCE_TYPE; variable->export_info.hint_string = class_name; diff --git a/modules/gdscript/language_server/gdscript_language_protocol.cpp b/modules/gdscript/language_server/gdscript_language_protocol.cpp index 39f4c976a4..551973140d 100644 --- a/modules/gdscript/language_server/gdscript_language_protocol.cpp +++ b/modules/gdscript/language_server/gdscript_language_protocol.cpp @@ -32,6 +32,7 @@ #include "core/config/project_settings.h" #include "editor/doc_tools.h" +#include "editor/editor_help.h" #include "editor/editor_log.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" diff --git a/modules/gltf/doc_classes/GLTFDocument.xml b/modules/gltf/doc_classes/GLTFDocument.xml index 588015de62..f313f4b28f 100644 --- a/modules/gltf/doc_classes/GLTFDocument.xml +++ b/modules/gltf/doc_classes/GLTFDocument.xml @@ -14,7 +14,6 @@ <param index="1" name="base_path" type="String" /> <param index="2" name="state" type="GLTFState" /> <param index="3" name="flags" type="int" default="0" /> - <param index="4" name="bake_fps" type="int" default="30" /> <description> Takes a [PackedByteArray] defining a gLTF and returns a [GLTFState] object through the [param state] parameter. [b]Note:[/b] The [param base_path] tells [method append_from_buffer] where to find dependencies and can be empty. @@ -25,8 +24,7 @@ <param index="0" name="path" type="String" /> <param index="1" name="state" type="GLTFState" /> <param index="2" name="flags" type="int" default="0" /> - <param index="3" name="bake_fps" type="int" default="30" /> - <param index="4" name="base_path" type="String" default="""" /> + <param index="3" name="base_path" type="String" default="""" /> <description> Takes a path to a gLTF file and returns a [GLTFState] object through the [param state] parameter. [b]Note:[/b] The [param base_path] tells [method append_from_file] where to find dependencies and can be empty. @@ -37,7 +35,6 @@ <param index="0" name="node" type="Node" /> <param index="1" name="state" type="GLTFState" /> <param index="2" name="flags" type="int" default="0" /> - <param index="3" name="bake_fps" type="int" default="30" /> <description> Takes a Godot Engine scene node and returns a [GLTFState] object through the [param state] parameter. </description> @@ -52,7 +49,8 @@ <method name="generate_scene"> <return type="Node" /> <param index="0" name="state" type="GLTFState" /> - <param index="1" name="bake_fps" type="int" default="30" /> + <param index="1" name="bake_fps" type="float" default="30" /> + <param index="2" name="trimming" type="bool" default="false" /> <description> Takes a [GLTFState] object through the [param state] parameter and returns a Godot Engine scene node. </description> diff --git a/modules/gltf/editor/editor_scene_exporter_gltf_plugin.cpp b/modules/gltf/editor/editor_scene_exporter_gltf_plugin.cpp index 95db1c0965..0c0b134bd1 100644 --- a/modules/gltf/editor/editor_scene_exporter_gltf_plugin.cpp +++ b/modules/gltf/editor/editor_scene_exporter_gltf_plugin.cpp @@ -85,7 +85,7 @@ void SceneExporterGLTFPlugin::_gltf2_dialog_action(String p_file) { state.instantiate(); int32_t flags = 0; flags |= EditorSceneFormatImporter::IMPORT_USE_NAMED_SKIN_BINDS; - Error err = doc->append_from_scene(root, state, flags, 30.0f); + Error err = doc->append_from_scene(root, state, flags); if (err != OK) { ERR_PRINT(vformat("glTF2 save scene error %s.", itos(err))); } diff --git a/modules/gltf/editor/editor_scene_importer_blend.cpp b/modules/gltf/editor/editor_scene_importer_blend.cpp index 20c9508474..7007ea5d13 100644 --- a/modules/gltf/editor/editor_scene_importer_blend.cpp +++ b/modules/gltf/editor/editor_scene_importer_blend.cpp @@ -41,6 +41,7 @@ #include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "main/main.h" +#include "scene/gui/line_edit.h" #include "scene/main/node.h" #include "scene/resources/animation.h" @@ -58,7 +59,7 @@ void EditorSceneFormatImporterBlend::get_extensions(List<String> *r_extensions) } Node *EditorSceneFormatImporterBlend::import_scene(const String &p_path, uint32_t p_flags, - const HashMap<StringName, Variant> &p_options, int p_bake_fps, + const HashMap<StringName, Variant> &p_options, List<String> *r_missing_deps, Error *r_err) { // Get global paths for source and sink. @@ -227,14 +228,14 @@ Node *EditorSceneFormatImporterBlend::import_scene(const String &p_path, uint32_ if (p_options.has(SNAME("blender/materials/unpack_enabled")) && p_options[SNAME("blender/materials/unpack_enabled")]) { base_dir = sink.get_base_dir(); } - Error err = gltf->append_from_file(sink.get_basename() + ".gltf", state, p_flags, p_bake_fps, base_dir); + Error err = gltf->append_from_file(sink.get_basename() + ".gltf", state, p_flags, base_dir); if (err != OK) { if (r_err) { *r_err = FAILED; } return nullptr; } - return gltf->generate_scene(state, p_bake_fps); + return gltf->generate_scene(state, (float)p_options["animation/fps"], (bool)p_options["animation/trimming"]); } Variant EditorSceneFormatImporterBlend::get_option_visibility(const String &p_path, bool p_for_animation, const String &p_option, diff --git a/modules/gltf/editor/editor_scene_importer_blend.h b/modules/gltf/editor/editor_scene_importer_blend.h index a1485ff82e..fe687f19fc 100644 --- a/modules/gltf/editor/editor_scene_importer_blend.h +++ b/modules/gltf/editor/editor_scene_importer_blend.h @@ -66,7 +66,7 @@ public: virtual uint32_t get_import_flags() const override; virtual void get_extensions(List<String> *r_extensions) const override; virtual Node *import_scene(const String &p_path, uint32_t p_flags, - const HashMap<StringName, Variant> &p_options, int p_bake_fps, + const HashMap<StringName, Variant> &p_options, List<String> *r_missing_deps, Error *r_err = nullptr) override; virtual void get_import_options(const String &p_path, List<ResourceImporter::ImportOption> *r_options) override; diff --git a/modules/gltf/editor/editor_scene_importer_fbx.cpp b/modules/gltf/editor/editor_scene_importer_fbx.cpp index 017a44cccf..14f2117413 100644 --- a/modules/gltf/editor/editor_scene_importer_fbx.cpp +++ b/modules/gltf/editor/editor_scene_importer_fbx.cpp @@ -49,7 +49,7 @@ void EditorSceneFormatImporterFBX::get_extensions(List<String> *r_extensions) co } Node *EditorSceneFormatImporterFBX::import_scene(const String &p_path, uint32_t p_flags, - const HashMap<StringName, Variant> &p_options, int p_bake_fps, + const HashMap<StringName, Variant> &p_options, List<String> *r_missing_deps, Error *r_err) { // Get global paths for source and sink. @@ -95,14 +95,14 @@ Node *EditorSceneFormatImporterFBX::import_scene(const String &p_path, uint32_t Ref<GLTFState> state; state.instantiate(); print_verbose(vformat("glTF path: %s", sink)); - Error err = gltf->append_from_file(sink, state, p_flags, p_bake_fps); + Error err = gltf->append_from_file(sink, state, p_flags); if (err != OK) { if (r_err) { *r_err = FAILED; } return nullptr; } - return gltf->generate_scene(state, p_bake_fps); + return gltf->generate_scene(state, (float)p_options["animation/fps"], (bool)p_options["animation/trimming"]); } Variant EditorSceneFormatImporterFBX::get_option_visibility(const String &p_path, bool p_for_animation, diff --git a/modules/gltf/editor/editor_scene_importer_fbx.h b/modules/gltf/editor/editor_scene_importer_fbx.h index b0039b1c8f..6bf9f3e033 100644 --- a/modules/gltf/editor/editor_scene_importer_fbx.h +++ b/modules/gltf/editor/editor_scene_importer_fbx.h @@ -45,7 +45,7 @@ public: virtual uint32_t get_import_flags() const override; virtual void get_extensions(List<String> *r_extensions) const override; virtual Node *import_scene(const String &p_path, uint32_t p_flags, - const HashMap<StringName, Variant> &p_options, int p_bake_fps, + const HashMap<StringName, Variant> &p_options, List<String> *r_missing_deps, Error *r_err = nullptr) override; virtual void get_import_options(const String &p_path, List<ResourceImporter::ImportOption> *r_options) override; diff --git a/modules/gltf/editor/editor_scene_importer_gltf.cpp b/modules/gltf/editor/editor_scene_importer_gltf.cpp index 161808aade..3cf49a3046 100644 --- a/modules/gltf/editor/editor_scene_importer_gltf.cpp +++ b/modules/gltf/editor/editor_scene_importer_gltf.cpp @@ -47,13 +47,13 @@ void EditorSceneFormatImporterGLTF::get_extensions(List<String> *r_extensions) c } Node *EditorSceneFormatImporterGLTF::import_scene(const String &p_path, uint32_t p_flags, - const HashMap<StringName, Variant> &p_options, int p_bake_fps, + const HashMap<StringName, Variant> &p_options, List<String> *r_missing_deps, Error *r_err) { Ref<GLTFDocument> doc; doc.instantiate(); Ref<GLTFState> state; state.instantiate(); - Error err = doc->append_from_file(p_path, state, p_flags, p_bake_fps); + Error err = doc->append_from_file(p_path, state, p_flags); if (err != OK) { if (r_err) { *r_err = err; @@ -63,7 +63,7 @@ Node *EditorSceneFormatImporterGLTF::import_scene(const String &p_path, uint32_t if (p_options.has("animation/import")) { state->set_create_animations(bool(p_options["animation/import"])); } - return doc->generate_scene(state, p_bake_fps); + return doc->generate_scene(state, (float)p_options["animation/fps"], (bool)p_options["animation/trimming"]); } #endif // TOOLS_ENABLED diff --git a/modules/gltf/editor/editor_scene_importer_gltf.h b/modules/gltf/editor/editor_scene_importer_gltf.h index edca038532..c0582b26c1 100644 --- a/modules/gltf/editor/editor_scene_importer_gltf.h +++ b/modules/gltf/editor/editor_scene_importer_gltf.h @@ -45,7 +45,7 @@ public: virtual uint32_t get_import_flags() const override; virtual void get_extensions(List<String> *r_extensions) const override; virtual Node *import_scene(const String &p_path, uint32_t p_flags, - const HashMap<StringName, Variant> &p_options, int p_bake_fps, + const HashMap<StringName, Variant> &p_options, List<String> *r_missing_deps, Error *r_err = nullptr) override; }; diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index eb8f7e5ebc..735e35ac1e 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -5860,7 +5860,7 @@ T GLTFDocument::_interpolate_track(const Vector<real_t> &p_times, const Vector<T ERR_FAIL_V(p_values[0]); } -void GLTFDocument::_import_animation(Ref<GLTFState> state, AnimationPlayer *ap, const GLTFAnimationIndex index, const int bake_fps) { +void GLTFDocument::_import_animation(Ref<GLTFState> state, AnimationPlayer *ap, const GLTFAnimationIndex index, const float bake_fps, const bool trimming) { Ref<GLTFAnimation> anim = state->animations[index]; String anim_name = anim->get_name(); @@ -5877,7 +5877,8 @@ void GLTFDocument::_import_animation(Ref<GLTFState> state, AnimationPlayer *ap, animation->set_loop_mode(Animation::LOOP_LINEAR); } - float length = 0.0; + double anim_start = trimming ? INFINITY : 0.0; + double anim_end = 0.0; for (const KeyValue<int, GLTFAnimation::Track> &track_i : anim->get_tracks()) { const GLTFAnimation::Track &track = track_i.value; @@ -5907,19 +5908,40 @@ void GLTFDocument::_import_animation(Ref<GLTFState> state, AnimationPlayer *ap, transform_node_path = node_path; } - for (int i = 0; i < track.rotation_track.times.size(); i++) { - length = MAX(length, track.rotation_track.times[i]); - } - for (int i = 0; i < track.position_track.times.size(); i++) { - length = MAX(length, track.position_track.times[i]); - } - for (int i = 0; i < track.scale_track.times.size(); i++) { - length = MAX(length, track.scale_track.times[i]); - } - - for (int i = 0; i < track.weight_tracks.size(); i++) { - for (int j = 0; j < track.weight_tracks[i].times.size(); j++) { - length = MAX(length, track.weight_tracks[i].times[j]); + if (trimming) { + for (int i = 0; i < track.rotation_track.times.size(); i++) { + anim_start = MIN(anim_start, track.rotation_track.times[i]); + anim_end = MAX(anim_end, track.rotation_track.times[i]); + } + for (int i = 0; i < track.position_track.times.size(); i++) { + anim_start = MIN(anim_start, track.position_track.times[i]); + anim_end = MAX(anim_end, track.position_track.times[i]); + } + for (int i = 0; i < track.scale_track.times.size(); i++) { + anim_start = MIN(anim_start, track.scale_track.times[i]); + anim_end = MAX(anim_end, track.scale_track.times[i]); + } + for (int i = 0; i < track.weight_tracks.size(); i++) { + for (int j = 0; j < track.weight_tracks[i].times.size(); j++) { + anim_start = MIN(anim_start, track.weight_tracks[i].times[j]); + anim_end = MAX(anim_end, track.weight_tracks[i].times[j]); + } + } + } else { + // If you don't use trimming and the first key time is not at 0.0, fake keys will be inserted. + for (int i = 0; i < track.rotation_track.times.size(); i++) { + anim_end = MAX(anim_end, track.rotation_track.times[i]); + } + for (int i = 0; i < track.position_track.times.size(); i++) { + anim_end = MAX(anim_end, track.position_track.times[i]); + } + for (int i = 0; i < track.scale_track.times.size(); i++) { + anim_end = MAX(anim_end, track.scale_track.times[i]); + } + for (int i = 0; i < track.weight_tracks.size(); i++) { + for (int j = 0; j < track.weight_tracks[i].times.size(); j++) { + anim_end = MAX(anim_end, track.weight_tracks[i].times[j]); + } } } @@ -5988,10 +6010,8 @@ void GLTFDocument::_import_animation(Ref<GLTFState> state, AnimationPlayer *ap, } } - //first determine animation length - const double increment = 1.0 / bake_fps; - double time = 0.0; + double time = anim_start; Vector3 base_pos; Quaternion base_rot; @@ -6017,26 +6037,26 @@ void GLTFDocument::_import_animation(Ref<GLTFState> state, AnimationPlayer *ap, if (position_idx >= 0) { pos = _interpolate_track<Vector3>(track.position_track.times, track.position_track.values, time, track.position_track.interpolation); - animation->position_track_insert_key(position_idx, time, pos); + animation->position_track_insert_key(position_idx, time - anim_start, pos); } if (rotation_idx >= 0) { rot = _interpolate_track<Quaternion>(track.rotation_track.times, track.rotation_track.values, time, track.rotation_track.interpolation); - animation->rotation_track_insert_key(rotation_idx, time, rot); + animation->rotation_track_insert_key(rotation_idx, time - anim_start, rot); } if (scale_idx >= 0) { scale = _interpolate_track<Vector3>(track.scale_track.times, track.scale_track.values, time, track.scale_track.interpolation); - animation->scale_track_insert_key(scale_idx, time, scale); + animation->scale_track_insert_key(scale_idx, time - anim_start, scale); } if (last) { break; } time += increment; - if (time >= length) { + if (time >= anim_end) { last = true; - time = length; + time = anim_end; } } } @@ -6071,21 +6091,21 @@ void GLTFDocument::_import_animation(Ref<GLTFState> state, AnimationPlayer *ap, bool last = false; while (true) { real_t blend = _interpolate_track<real_t>(track.weight_tracks[i].times, track.weight_tracks[i].values, time, gltf_interp); - animation->blend_shape_track_insert_key(track_idx, time, blend); + animation->blend_shape_track_insert_key(track_idx, time - anim_start, blend); if (last) { break; } time += increment; - if (time >= length) { + if (time >= anim_end) { last = true; - time = length; + time = anim_end; } } } } } - animation->set_length(length); + animation->set_length(anim_end - anim_start); Ref<AnimationLibrary> library; if (!ap->has_animation_library("")) { @@ -6585,7 +6605,7 @@ void GLTFDocument::_convert_animation(Ref<GLTFState> state, AnimationPlayer *ap, } } -Error GLTFDocument::_parse(Ref<GLTFState> state, String p_path, Ref<FileAccess> f, int p_bake_fps) { +Error GLTFDocument::_parse(Ref<GLTFState> state, String p_path, Ref<FileAccess> f) { Error err; if (f.is_null()) { return FAILED; @@ -6636,7 +6656,7 @@ Error GLTFDocument::_parse(Ref<GLTFState> state, String p_path, Ref<FileAccess> } } - err = _parse_gltf_state(state, p_path, p_bake_fps); + err = _parse_gltf_state(state, p_path); ERR_FAIL_COND_V(err != OK, err); return OK; @@ -6754,14 +6774,14 @@ Error GLTFDocument::_serialize_file(Ref<GLTFState> state, const String p_path) { } void GLTFDocument::_bind_methods() { - ClassDB::bind_method(D_METHOD("append_from_file", "path", "state", "flags", "bake_fps", "base_path"), - &GLTFDocument::append_from_file, DEFVAL(0), DEFVAL(30), DEFVAL(String())); - ClassDB::bind_method(D_METHOD("append_from_buffer", "bytes", "base_path", "state", "flags", "bake_fps"), - &GLTFDocument::append_from_buffer, DEFVAL(0), DEFVAL(30)); - ClassDB::bind_method(D_METHOD("append_from_scene", "node", "state", "flags", "bake_fps"), - &GLTFDocument::append_from_scene, DEFVAL(0), DEFVAL(30)); - ClassDB::bind_method(D_METHOD("generate_scene", "state", "bake_fps"), - &GLTFDocument::generate_scene, DEFVAL(30)); + ClassDB::bind_method(D_METHOD("append_from_file", "path", "state", "flags", "base_path"), + &GLTFDocument::append_from_file, DEFVAL(0), DEFVAL(String())); + ClassDB::bind_method(D_METHOD("append_from_buffer", "bytes", "base_path", "state", "flags"), + &GLTFDocument::append_from_buffer, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("append_from_scene", "node", "state", "flags"), + &GLTFDocument::append_from_scene, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("generate_scene", "state", "bake_fps", "trimming"), + &GLTFDocument::generate_scene, DEFVAL(30), DEFVAL(false)); ClassDB::bind_method(D_METHOD("generate_buffer", "state"), &GLTFDocument::generate_buffer); ClassDB::bind_method(D_METHOD("write_to_filesystem", "state", "path"), @@ -6870,7 +6890,7 @@ Error GLTFDocument::write_to_filesystem(Ref<GLTFState> state, const String &p_pa return OK; } -Node *GLTFDocument::generate_scene(Ref<GLTFState> state, int32_t p_bake_fps) { +Node *GLTFDocument::generate_scene(Ref<GLTFState> state, float p_bake_fps, bool p_trimming) { ERR_FAIL_NULL_V(state, nullptr); ERR_FAIL_INDEX_V(0, state->root_nodes.size(), nullptr); Error err = OK; @@ -6884,7 +6904,7 @@ Node *GLTFDocument::generate_scene(Ref<GLTFState> state, int32_t p_bake_fps) { root->add_child(ap, true); ap->set_owner(root); for (int i = 0; i < state->animations.size(); i++) { - _import_animation(state, ap, i, p_bake_fps); + _import_animation(state, ap, i, p_bake_fps, p_trimming); } } for (KeyValue<GLTFNodeIndex, Node *> E : state->scene_nodes) { @@ -6910,7 +6930,7 @@ Node *GLTFDocument::generate_scene(Ref<GLTFState> state, int32_t p_bake_fps) { return root; } -Error GLTFDocument::append_from_scene(Node *p_node, Ref<GLTFState> state, uint32_t p_flags, int32_t p_bake_fps) { +Error GLTFDocument::append_from_scene(Node *p_node, Ref<GLTFState> state, uint32_t p_flags) { ERR_FAIL_COND_V(state.is_null(), FAILED); state->use_named_skin_binds = p_flags & GLTF_IMPORT_USE_NAMED_SKIN_BINDS; state->discard_meshes_and_materials = p_flags & GLTF_IMPORT_DISCARD_MESHES_AND_MATERIALS; @@ -6930,7 +6950,7 @@ Error GLTFDocument::append_from_scene(Node *p_node, Ref<GLTFState> state, uint32 return OK; } -Error GLTFDocument::append_from_buffer(PackedByteArray p_bytes, String p_base_path, Ref<GLTFState> state, uint32_t p_flags, int32_t p_bake_fps) { +Error GLTFDocument::append_from_buffer(PackedByteArray p_bytes, String p_base_path, Ref<GLTFState> state, uint32_t p_flags) { ERR_FAIL_COND_V(state.is_null(), FAILED); // TODO Add missing texture and missing .bin file paths to r_missing_deps 2021-09-10 fire Error err = FAILED; @@ -6941,7 +6961,7 @@ Error GLTFDocument::append_from_buffer(PackedByteArray p_bytes, String p_base_pa file_access.instantiate(); file_access->open_custom(p_bytes.ptr(), p_bytes.size()); state->base_path = p_base_path.get_base_dir(); - err = _parse(state, state->base_path, file_access, p_bake_fps); + err = _parse(state, state->base_path, file_access); ERR_FAIL_COND_V(err != OK, err); for (Ref<GLTFDocumentExtension> ext : document_extensions) { ERR_CONTINUE(ext.is_null()); @@ -6951,7 +6971,7 @@ Error GLTFDocument::append_from_buffer(PackedByteArray p_bytes, String p_base_pa return OK; } -Error GLTFDocument::_parse_gltf_state(Ref<GLTFState> state, const String &p_search_path, float p_bake_fps) { +Error GLTFDocument::_parse_gltf_state(Ref<GLTFState> state, const String &p_search_path) { Error err; /* PARSE EXTENSIONS */ @@ -7047,7 +7067,7 @@ Error GLTFDocument::_parse_gltf_state(Ref<GLTFState> state, const String &p_sear return OK; } -Error GLTFDocument::append_from_file(String p_path, Ref<GLTFState> r_state, uint32_t p_flags, int32_t p_bake_fps, String p_base_path) { +Error GLTFDocument::append_from_file(String p_path, Ref<GLTFState> r_state, uint32_t p_flags, String p_base_path) { // TODO Add missing texture and missing .bin file paths to r_missing_deps 2021-09-10 fire if (r_state == Ref<GLTFState>()) { r_state.instantiate(); @@ -7064,7 +7084,7 @@ Error GLTFDocument::append_from_file(String p_path, Ref<GLTFState> r_state, uint base_path = p_path.get_base_dir(); } r_state->base_path = base_path; - err = _parse(r_state, base_path, f, p_bake_fps); + err = _parse(r_state, base_path, f); ERR_FAIL_COND_V(err != OK, err); for (Ref<GLTFDocumentExtension> ext : document_extensions) { ERR_CONTINUE(ext.is_null()); diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index 5a0e4ff498..6eb38354a2 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -292,17 +292,17 @@ private: static float get_max_component(const Color &p_color); public: - Error append_from_file(String p_path, Ref<GLTFState> r_state, uint32_t p_flags = 0, int32_t p_bake_fps = 30, String p_base_path = String()); - Error append_from_buffer(PackedByteArray p_bytes, String p_base_path, Ref<GLTFState> r_state, uint32_t p_flags = 0, int32_t p_bake_fps = 30); - Error append_from_scene(Node *p_node, Ref<GLTFState> r_state, uint32_t p_flags = 0, int32_t p_bake_fps = 30); + Error append_from_file(String p_path, Ref<GLTFState> r_state, uint32_t p_flags = 0, String p_base_path = String()); + Error append_from_buffer(PackedByteArray p_bytes, String p_base_path, Ref<GLTFState> r_state, uint32_t p_flags = 0); + Error append_from_scene(Node *p_node, Ref<GLTFState> r_state, uint32_t p_flags = 0); public: - Node *generate_scene(Ref<GLTFState> state, int32_t p_bake_fps = 30.0f); + Node *generate_scene(Ref<GLTFState> state, float p_bake_fps = 30.0f, bool p_trimming = false); PackedByteArray generate_buffer(Ref<GLTFState> state); Error write_to_filesystem(Ref<GLTFState> state, const String &p_path); public: - Error _parse_gltf_state(Ref<GLTFState> state, const String &p_search_path, float p_bake_fps); + Error _parse_gltf_state(Ref<GLTFState> state, const String &p_search_path); Error _parse_gltf_extensions(Ref<GLTFState> state); void _process_mesh_instances(Ref<GLTFState> state, Node *scene_root); void _generate_scene_node(Ref<GLTFState> state, Node *scene_parent, @@ -310,7 +310,7 @@ public: const GLTFNodeIndex node_index); void _generate_skeleton_bone_node(Ref<GLTFState> state, Node *scene_parent, Node3D *scene_root, const GLTFNodeIndex node_index); void _import_animation(Ref<GLTFState> state, AnimationPlayer *ap, - const GLTFAnimationIndex index, const int bake_fps); + const GLTFAnimationIndex index, const float bake_fps, const bool trimming); void _convert_mesh_instances(Ref<GLTFState> state); GLTFCameraIndex _convert_camera(Ref<GLTFState> state, Camera3D *p_camera); void _convert_light_to_gltf(Light3D *light, Ref<GLTFState> state, Ref<GLTFNode> gltf_node); @@ -368,7 +368,7 @@ public: void _convert_animation(Ref<GLTFState> state, AnimationPlayer *ap, String p_animation_track_name); Error _serialize(Ref<GLTFState> state, const String &p_path); - Error _parse(Ref<GLTFState> state, String p_path, Ref<FileAccess> f, int p_bake_fps); + Error _parse(Ref<GLTFState> state, String p_path, Ref<FileAccess> f); }; #endif // GLTF_DOCUMENT_H diff --git a/modules/gridmap/editor/grid_map_editor_plugin.cpp b/modules/gridmap/editor/grid_map_editor_plugin.cpp index c8aedc8b92..89ef9ddd90 100644 --- a/modules/gridmap/editor/grid_map_editor_plugin.cpp +++ b/modules/gridmap/editor/grid_map_editor_plugin.cpp @@ -40,6 +40,8 @@ #include "editor/editor_undo_redo_manager.h" #include "editor/plugins/node_3d_editor_plugin.h" #include "scene/3d/camera_3d.h" +#include "scene/gui/menu_button.h" +#include "scene/gui/separator.h" #include "scene/main/window.h" void GridMapEditor::_node_removed(Node *p_node) { diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index a4bffc1e3c..cfb9a0fbfb 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -46,6 +46,7 @@ #include "editor/editor_internal_calls.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" +#include "editor/inspector_dock.h" #include "editor/node_dock.h" #include "editor/script_templates/templates.gen.h" #endif diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs index 3020cfbc50..eb83833b40 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs @@ -12,6 +12,95 @@ namespace Godot.SourceGenerators.Sample [SuppressMessage("ReSharper", "InconsistentNaming")] public partial class ExportedProperties : Godot.Object { + // Do not generate default value + private String _notGenerate_Property_String = new string("not generate"); + [Export] + public String NotGenerate_Complex_Lamda_Property + { + get => _notGenerate_Property_String + Convert.ToInt32("1"); + set => _notGenerate_Property_String = value; + } + + [Export] + public String NotGenerate_Lamda_NoField_Property + { + get => new string("not generate"); + set => _notGenerate_Property_String = value; + } + + [Export] + public String NotGenerate_Complex_Return_Property + { + get + { + return _notGenerate_Property_String + Convert.ToInt32("1"); + } + set + { + _notGenerate_Property_String = value; + } + } + + private int _notGenerate_Property_Int = 1; + [Export] + public string NotGenerate_Returns_Property + { + get + { + if (_notGenerate_Property_Int == 1) + { + return "a"; + } + else + { + return "b"; + } + } + set + { + _notGenerate_Property_Int = value == "a" ? 1 : 2; + } + } + + // Full Property + private String _fullProperty_String = "FullProperty_String"; + [Export] + public String FullProperty_String + { + get + { + return _fullProperty_String; + } + set + { + _fullProperty_String = value; + } + } + + private String _fullProperty_String_Complex = new string("FullProperty_String_Complex") + Convert.ToInt32("1"); + [Export] + public String FullProperty_String_Complex + { + get + { + return _fullProperty_String_Complex; + } + set + { + _fullProperty_String_Complex = value; + } + } + + // Lamda Property + private String _lamdaProperty_String = "LamdaProperty_String"; + [Export] + public String LamdaProperty_String + { + get => _lamdaProperty_String; + set => _lamdaProperty_String = value; + } + + // Auto Property [Export] private Boolean property_Boolean { get; set; } = true; [Export] private Char property_Char { get; set; } = 'f'; [Export] private SByte property_SByte { get; set; } = 10; diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs index 5dd079bb17..252f162b0c 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs @@ -292,7 +292,7 @@ namespace Godot.SourceGenerators source.Append("if (name == PropertyName.") .Append(propertyMemberName) .Append(") {\n") - .Append(" ") + .Append(" this.") .Append(propertyMemberName) .Append(" = ") .AppendNativeVariantToManagedExpr("value", propertyTypeSymbol, propertyMarshalType) @@ -317,7 +317,7 @@ namespace Godot.SourceGenerators .Append(propertyMemberName) .Append(") {\n") .Append(" value = ") - .AppendManagedToNativeVariantExpr(propertyMemberName, propertyMarshalType) + .AppendManagedToNativeVariantExpr("this." + propertyMemberName, propertyMarshalType) .Append(";\n") .Append(" return true;\n") .Append(" }\n"); diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs index 3d0055759e..3f588a4c90 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; @@ -163,19 +164,68 @@ namespace Godot.SourceGenerators continue; } - // TODO: Detect default value from simple property getters (currently we only detect from initializers) - - EqualsValueClauseSyntax? initializer = property.DeclaringSyntaxReferences - .Select(r => r.GetSyntax() as PropertyDeclarationSyntax) - .Select(s => s?.Initializer ?? null) - .FirstOrDefault(); + var propertyDeclarationSyntax = property.DeclaringSyntaxReferences + .Select(r => r.GetSyntax() as PropertyDeclarationSyntax).FirstOrDefault(); // Fully qualify the value to avoid issues with namespaces. string? value = null; - if (initializer != null) + if (propertyDeclarationSyntax != null) { - var sm = context.Compilation.GetSemanticModel(initializer.SyntaxTree); - value = initializer.Value.FullQualifiedSyntax(sm); + if (propertyDeclarationSyntax.Initializer != null) + { + var sm = context.Compilation.GetSemanticModel(propertyDeclarationSyntax.Initializer.SyntaxTree); + value = propertyDeclarationSyntax.Initializer.Value.FullQualifiedSyntax(sm); + } + else + { + var propertyGet = propertyDeclarationSyntax.AccessorList?.Accessors.Where(a => a.Keyword.IsKind(SyntaxKind.GetKeyword)).FirstOrDefault(); + if (propertyGet != null) + { + if (propertyGet.ExpressionBody != null) + { + if (propertyGet.ExpressionBody.Expression is IdentifierNameSyntax identifierNameSyntax) + { + var sm = context.Compilation.GetSemanticModel(identifierNameSyntax.SyntaxTree); + var fieldSymbol = sm.GetSymbolInfo(identifierNameSyntax).Symbol as IFieldSymbol; + EqualsValueClauseSyntax? initializer = fieldSymbol?.DeclaringSyntaxReferences + .Select(r => r.GetSyntax()) + .OfType<VariableDeclaratorSyntax>() + .Select(s => s.Initializer) + .FirstOrDefault(i => i != null); + + if (initializer != null) + { + sm = context.Compilation.GetSemanticModel(initializer.SyntaxTree); + value = initializer.Value.FullQualifiedSyntax(sm); + } + } + } + else + { + var returns = propertyGet.DescendantNodes().OfType<ReturnStatementSyntax>(); + if (returns.Count() == 1) + {// Generate only single return + var returnStatementSyntax = returns.Single(); + if (returnStatementSyntax.Expression is IdentifierNameSyntax identifierNameSyntax) + { + var sm = context.Compilation.GetSemanticModel(identifierNameSyntax.SyntaxTree); + var fieldSymbol = sm.GetSymbolInfo(identifierNameSyntax).Symbol as IFieldSymbol; + EqualsValueClauseSyntax? initializer = fieldSymbol?.DeclaringSyntaxReferences + .Select(r => r.GetSyntax()) + .OfType<VariableDeclaratorSyntax>() + .Select(s => s.Initializer) + .FirstOrDefault(i => i != null); + + if (initializer != null) + { + sm = context.Compilation.GetSemanticModel(initializer.SyntaxTree); + value = initializer.Value.FullQualifiedSyntax(sm); + } + } + } + } + } + } } exportedMembers.Add(new ExportedPropertyMetadata( diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs index e3b7ac297d..1b9f63c22d 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs @@ -474,6 +474,11 @@ namespace Godot.Collections } } + internal interface IGenericGodotArray + { + public Array UnderlyingArray { get; } + } + /// <summary> /// Typed wrapper around Godot's Array class, an array of Variant /// typed elements allocated in the engine in C++. Useful when @@ -487,7 +492,8 @@ namespace Godot.Collections IList<T>, IReadOnlyList<T>, ICollection<T>, - IEnumerable<T> + IEnumerable<T>, + IGenericGodotArray { private static godot_variant ToVariantFunc(in Array<T> godotArray) => VariantUtils.CreateFromArray(godotArray); @@ -503,6 +509,8 @@ namespace Godot.Collections private readonly Array _underlyingArray; + Array IGenericGodotArray.UnderlyingArray => _underlyingArray; + internal ref godot_array.movable NativeValue { [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs index f14790a218..cf25e1f0ae 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs @@ -344,6 +344,11 @@ namespace Godot.Collections } } + internal interface IGenericGodotDictionary + { + public Dictionary UnderlyingDictionary { get; } + } + /// <summary> /// Typed wrapper around Godot's Dictionary class, a dictionary of Variant /// typed elements allocated in the engine in C++. Useful when @@ -354,7 +359,8 @@ namespace Godot.Collections /// <typeparam name="TValue">The type of the dictionary's values.</typeparam> public class Dictionary<[MustBeVariant] TKey, [MustBeVariant] TValue> : IDictionary<TKey, TValue>, - IReadOnlyDictionary<TKey, TValue> + IReadOnlyDictionary<TKey, TValue>, + IGenericGodotDictionary { private static godot_variant ToVariantFunc(in Dictionary<TKey, TValue> godotDictionary) => VariantUtils.CreateFromDictionary(godotDictionary); @@ -370,6 +376,8 @@ namespace Godot.Collections private readonly Dictionary _underlyingDict; + Dictionary IGenericGodotDictionary.UnderlyingDictionary => _underlyingDict; + internal ref godot_dictionary.movable NativeValue { [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs index 649661ee06..d2a80d0e92 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs @@ -293,6 +293,10 @@ namespace Godot.NativeInterop return VariantUtils.CreateFromDictionary(godotDictionary); case Collections.Array godotArray: return VariantUtils.CreateFromArray(godotArray); + case Collections.IGenericGodotDictionary godotDictionary: + return VariantUtils.CreateFromDictionary(godotDictionary.UnderlyingDictionary); + case Collections.IGenericGodotArray godotArray: + return VariantUtils.CreateFromArray(godotArray.UnderlyingArray); case Variant variant: return NativeFuncs.godotsharp_variant_new_copy((godot_variant)variant.NativeVar); } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs index b30b6a0752..c7deb6423b 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs @@ -414,21 +414,6 @@ namespace Godot.NativeInterop // StringExtensions - public static partial void godotsharp_string_md5_buffer(in godot_string p_self, - out godot_packed_byte_array r_md5_buffer); - - public static partial void godotsharp_string_md5_text(in godot_string p_self, out godot_string r_md5_text); - - public static partial int godotsharp_string_rfind(in godot_string p_self, in godot_string p_what, int p_from); - - public static partial int godotsharp_string_rfindn(in godot_string p_self, in godot_string p_what, int p_from); - - public static partial void godotsharp_string_sha256_buffer(in godot_string p_self, - out godot_packed_byte_array r_sha256_buffer); - - public static partial void godotsharp_string_sha256_text(in godot_string p_self, - out godot_string r_sha256_text); - public static partial void godotsharp_string_simplify_path(in godot_string p_self, out godot_string r_simplified_path); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs index f511233fcc..d4329d78c1 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Security; +using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using Godot.NativeInterop; @@ -67,30 +69,13 @@ namespace Godot } /// <summary> - /// If the string is a path to a file, return the path to the file without the extension. - /// </summary> - /// <seealso cref="GetExtension(string)"/> - /// <seealso cref="GetBaseDir(string)"/> - /// <seealso cref="GetFile(string)"/> - /// <param name="instance">The path to a file.</param> - /// <returns>The path to the file without the extension.</returns> - public static string GetBaseName(this string instance) - { - int index = instance.LastIndexOf('.'); - - if (index > 0) - return instance.Substring(0, index); - - return instance; - } - - /// <summary> /// Returns <see langword="true"/> if the strings begins /// with the given string <paramref name="text"/>. /// </summary> /// <param name="instance">The string to check.</param> /// <param name="text">The beginning string.</param> /// <returns>If the string begins with the given string.</returns> + [Obsolete("Use string.StartsWith instead.")] public static bool BeginsWith(this string instance, string text) { return instance.StartsWith(text); @@ -144,15 +129,15 @@ namespace Godot } /// <summary> - /// Returns the amount of substrings <paramref name="what"/> in the string. + /// Returns the number of occurrences of substring <paramref name="what"/> in the string. /// </summary> /// <param name="instance">The string where the substring will be searched.</param> /// <param name="what">The substring that will be counted.</param> - /// <param name="caseSensitive">If the search is case sensitive.</param> /// <param name="from">Index to start searching from.</param> /// <param name="to">Index to stop searching at.</param> - /// <returns>Amount of substrings in the string.</returns> - public static int Count(this string instance, string what, bool caseSensitive = true, int from = 0, int to = 0) + /// <param name="caseSensitive">If the search is case sensitive.</param> + /// <returns>Number of occurrences of the substring in the string.</returns> + public static int Count(this string instance, string what, int from = 0, int to = 0, bool caseSensitive = true) { if (what.Length == 0) { @@ -211,6 +196,82 @@ namespace Godot } /// <summary> + /// Returns the number of occurrences of substring <paramref name="what"/> (ignoring case) + /// between <paramref name="from"/> and <paramref name="to"/> positions. If <paramref name="from"/> + /// and <paramref name="to"/> equals 0 the whole string will be used. If only <paramref name="to"/> + /// equals 0 the remained substring will be used. + /// </summary> + /// <param name="instance">The string where the substring will be searched.</param> + /// <param name="what">The substring that will be counted.</param> + /// <param name="from">Index to start searching from.</param> + /// <param name="to">Index to stop searching at.</param> + /// <returns>Number of occurrences of the substring in the string.</returns> + public static int CountN(this string instance, string what, int from = 0, int to = 0) + { + return instance.Count(what, from, to, caseSensitive: false); + } + + /// <summary> + /// Returns a copy of the string with indentation (leading tabs and spaces) removed. + /// See also <see cref="Indent"/> to add indentation. + /// </summary> + /// <param name="instance">The string to remove the indentation from.</param> + /// <returns>The string with the indentation removed.</returns> + public static string Dedent(this string instance) + { + var sb = new StringBuilder(); + string indent = ""; + bool hasIndent = false; + bool hasText = false; + int lineStart = 0; + int indentStop = -1; + + for (int i = 0; i < instance.Length; i++) + { + char c = instance[i]; + if (c == '\n') + { + if (hasText) + { + sb.Append(instance.Substring(indentStop, i - indentStop)); + } + sb.Append('\n'); + hasText = false; + lineStart = i + 1; + indentStop = -1; + } + else if (!hasText) + { + if (c > 32) + { + hasText = true; + if (!hasIndent) + { + hasIndent = true; + indent = instance.Substring(lineStart, i - lineStart); + indentStop = i; + } + } + if (hasIndent && indentStop < 0) + { + int j = i - lineStart; + if (j >= indent.Length || c != indent[j]) + { + indentStop = i; + } + } + } + } + + if (hasText) + { + sb.Append(instance.Substring(indentStop, instance.Length - indentStop)); + } + + return sb.ToString(); + } + + /// <summary> /// Returns a copy of the string with special characters escaped using the C language standard. /// </summary> /// <param name="instance">The string to escape.</param> @@ -443,29 +504,6 @@ namespace Godot } /// <summary> - /// Returns <see langword="true"/> if the strings ends - /// with the given string <paramref name="text"/>. - /// </summary> - /// <param name="instance">The string to check.</param> - /// <param name="text">The ending string.</param> - /// <returns>If the string ends with the given string.</returns> - public static bool EndsWith(this string instance, string text) - { - return instance.EndsWith(text); - } - - /// <summary> - /// Erase <paramref name="chars"/> characters from the string starting from <paramref name="pos"/>. - /// </summary> - /// <param name="instance">The string to modify.</param> - /// <param name="pos">Starting position from which to erase.</param> - /// <param name="chars">Amount of characters to erase.</param> - public static void Erase(this StringBuilder instance, int pos, int chars) - { - instance.Remove(pos, chars); - } - - /// <summary> /// Returns the extension without the leading period character (<c>.</c>) /// if the string is a valid file name or path. If the string does not contain /// an extension, returns an empty string instead. @@ -489,7 +527,7 @@ namespace Godot /// <returns>The extension of the file or an empty string.</returns> public static string GetExtension(this string instance) { - int pos = instance.FindLast("."); + int pos = instance.RFind("."); if (pos < 0) return instance; @@ -498,12 +536,16 @@ namespace Godot } /// <summary> - /// Find the first occurrence of a substring. Optionally, the search starting position can be passed. + /// Returns the index of the first occurrence of the specified string in this instance, + /// or <c>-1</c>. Optionally, the starting search index can be specified, continuing + /// to the end of the string. + /// Note: If you just want to know whether a string contains a substring, use the + /// <see cref="string.Contains(string)"/> method. /// </summary> /// <seealso cref="Find(string, char, int, bool)"/> - /// <seealso cref="FindLast(string, string, bool)"/> - /// <seealso cref="FindLast(string, string, int, bool)"/> /// <seealso cref="FindN(string, string, int)"/> + /// <seealso cref="RFind(string, string, int, bool)"/> + /// <seealso cref="RFindN(string, string, int)"/> /// <param name="instance">The string that will be searched.</param> /// <param name="what">The substring to find.</param> /// <param name="from">The search starting position.</param> @@ -519,9 +561,9 @@ namespace Godot /// Find the first occurrence of a char. Optionally, the search starting position can be passed. /// </summary> /// <seealso cref="Find(string, string, int, bool)"/> - /// <seealso cref="FindLast(string, string, bool)"/> - /// <seealso cref="FindLast(string, string, int, bool)"/> /// <seealso cref="FindN(string, string, int)"/> + /// <seealso cref="RFind(string, string, int, bool)"/> + /// <seealso cref="RFindN(string, string, int)"/> /// <param name="instance">The string that will be searched.</param> /// <param name="what">The substring to find.</param> /// <param name="from">The search starting position.</param> @@ -529,50 +571,21 @@ namespace Godot /// <returns>The first instance of the char, or -1 if not found.</returns> public static int Find(this string instance, char what, int from = 0, bool caseSensitive = true) { - // TODO: Could be more efficient if we get a char version of `IndexOf`. - // See https://github.com/dotnet/runtime/issues/44116 - return instance.IndexOf(what.ToString(), from, - caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); - } + if (caseSensitive) + return instance.IndexOf(what, from); - /// <summary>Find the last occurrence of a substring.</summary> - /// <seealso cref="Find(string, string, int, bool)"/> - /// <seealso cref="Find(string, char, int, bool)"/> - /// <seealso cref="FindLast(string, string, int, bool)"/> - /// <seealso cref="FindN(string, string, int)"/> - /// <param name="instance">The string that will be searched.</param> - /// <param name="what">The substring to find.</param> - /// <param name="caseSensitive">If <see langword="true"/>, the search is case sensitive.</param> - /// <returns>The starting position of the substring, or -1 if not found.</returns> - public static int FindLast(this string instance, string what, bool caseSensitive = true) - { - return instance.FindLast(what, instance.Length - 1, caseSensitive); - } - - /// <summary>Find the last occurrence of a substring specifying the search starting position.</summary> - /// <seealso cref="Find(string, string, int, bool)"/> - /// <seealso cref="Find(string, char, int, bool)"/> - /// <seealso cref="FindLast(string, string, bool)"/> - /// <seealso cref="FindN(string, string, int)"/> - /// <param name="instance">The string that will be searched.</param> - /// <param name="what">The substring to find.</param> - /// <param name="from">The search starting position.</param> - /// <param name="caseSensitive">If <see langword="true"/>, the search is case sensitive.</param> - /// <returns>The starting position of the substring, or -1 if not found.</returns> - public static int FindLast(this string instance, string what, int from, bool caseSensitive = true) - { - return instance.LastIndexOf(what, from, - caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); + return CultureInfo.InvariantCulture.CompareInfo.IndexOf(instance, what, from, CompareOptions.OrdinalIgnoreCase); } /// <summary> - /// Find the first occurrence of a substring but search as case-insensitive. - /// Optionally, the search starting position can be passed. + /// Returns the index of the first case-insensitive occurrence of the specified string in this instance, + /// or <c>-1</c>. Optionally, the starting search index can be specified, continuing + /// to the end of the string. /// </summary> /// <seealso cref="Find(string, string, int, bool)"/> /// <seealso cref="Find(string, char, int, bool)"/> - /// <seealso cref="FindLast(string, string, bool)"/> - /// <seealso cref="FindLast(string, string, int, bool)"/> + /// <seealso cref="RFind(string, string, int, bool)"/> + /// <seealso cref="RFindN(string, string, int)"/> /// <param name="instance">The string that will be searched.</param> /// <param name="what">The substring to find.</param> /// <param name="from">The search starting position.</param> @@ -616,7 +629,7 @@ namespace Godot } } - int sep = Mathf.Max(rs.FindLast("/"), rs.FindLast("\\")); + int sep = Mathf.Max(rs.RFind("/"), rs.RFind("\\")); if (sep == -1) return directory; @@ -625,6 +638,24 @@ namespace Godot } /// <summary> + /// If the string is a path to a file, return the path to the file without the extension. + /// </summary> + /// <seealso cref="GetExtension(string)"/> + /// <seealso cref="GetBaseDir(string)"/> + /// <seealso cref="GetFile(string)"/> + /// <param name="instance">The path to a file.</param> + /// <returns>The path to the file without the extension.</returns> + public static string GetBaseName(this string instance) + { + int index = instance.RFind("."); + + if (index > 0) + return instance.Substring(0, index); + + return instance; + } + + /// <summary> /// If the string is a path to a file, return the file and ignore the base directory. /// </summary> /// <seealso cref="GetBaseName(string)"/> @@ -634,7 +665,7 @@ namespace Godot /// <returns>The file name.</returns> public static string GetFile(this string instance) { - int sep = Mathf.Max(instance.FindLast("/"), instance.FindLast("\\")); + int sep = Mathf.Max(instance.RFind("/"), instance.RFind("\\")); if (sep == -1) return instance; @@ -643,8 +674,8 @@ namespace Godot } /// <summary> - /// Converts the given byte array of ASCII encoded text to a string. - /// Faster alternative to <see cref="GetStringFromUTF8"/> if the + /// Converts ASCII encoded array to string. + /// Fast alternative to <see cref="GetStringFromUTF8"/> if the /// content is ASCII-only. Unlike the UTF-8 function this function /// maps every byte to a character in the array. Multibyte sequences /// will not be interpreted correctly. For parsing user input always @@ -658,13 +689,35 @@ namespace Godot } /// <summary> - /// Converts the given byte array of UTF-8 encoded text to a string. + /// Converts UTF-16 encoded array to string using the little endian byte order. + /// </summary> + /// <param name="bytes">A byte array of UTF-16 characters.</param> + /// <returns>A string created from the bytes.</returns> + public static string GetStringFromUTF16(this byte[] bytes) + { + return Encoding.Unicode.GetString(bytes); + } + + /// <summary> + /// Converts UTF-32 encoded array to string using the little endian byte order. + /// </summary> + /// <param name="bytes">A byte array of UTF-32 characters.</param> + /// <returns>A string created from the bytes.</returns> + public static string GetStringFromUTF32(this byte[] bytes) + { + return Encoding.UTF32.GetString(bytes); + } + + /// <summary> + /// Converts UTF-8 encoded array to string. /// Slower than <see cref="GetStringFromASCII"/> but supports UTF-8 /// encoded data. Use this function if you are unsure about the /// source of the data. For user input this function /// should always be preferred. /// </summary> - /// <param name="bytes">A byte array of UTF-8 characters (a character may take up multiple bytes).</param> + /// <param name="bytes"> + /// A byte array of UTF-8 characters (a character may take up multiple bytes). + /// </param> /// <returns>A string created from the bytes.</returns> public static string GetStringFromUTF8(this byte[] bytes) { @@ -766,18 +819,44 @@ namespace Godot } /// <summary> - /// Inserts a substring at a given position. + /// Returns a copy of the string with lines indented with <paramref name="prefix"/>. + /// For example, the string can be indented with two tabs using <c>"\t\t"</c>, + /// or four spaces using <c>" "</c>. The prefix can be any string so it can + /// also be used to comment out strings with e.g. <c>"// </c>. + /// See also <see cref="Dedent"/> to remove indentation. + /// Note: Empty lines are kept empty. /// </summary> - /// <param name="instance">The string to modify.</param> - /// <param name="pos">Position at which to insert the substring.</param> - /// <param name="what">Substring to insert.</param> - /// <returns> - /// The string with <paramref name="what"/> inserted at the given - /// position <paramref name="pos"/>. - /// </returns> - public static string Insert(this string instance, int pos, string what) + /// <param name="instance">The string to add indentation to.</param> + /// <param name="prefix">The string to use as indentation.</param> + /// <returns>The string with indentation added.</returns> + public static string Indent(this string instance, string prefix) { - return instance.Insert(pos, what); + var sb = new StringBuilder(); + int lineStart = 0; + + for (int i = 0; i < instance.Length; i++) + { + char c = instance[i]; + if (c == '\n') + { + if (i == lineStart) + { + sb.Append(c); // Leave empty lines empty. + } + else + { + sb.Append(prefix); + sb.Append(instance.Substring(lineStart, i - lineStart + 1)); + } + lineStart = i + 1; + } + } + if (lineStart != instance.Length) + { + sb.Append(prefix); + sb.Append(instance.Substring(lineStart)); + } + return sb.ToString(); } /// <summary> @@ -873,19 +952,94 @@ namespace Godot return instance.IsSubsequenceOf(text, caseSensitive: false); } + private static readonly char[] _invalidFileNameCharacters = { ':', '/', '\\', '?', '*', '"', '|', '%', '<', '>' }; + + /// <summary> + /// Returns <see langword="true"/> if this string is free from characters that + /// aren't allowed in file names. + /// </summary> + /// <param name="instance">The string to check.</param> + /// <returns>If the string contains a valid file name.</returns> + public static bool IsValidFileName(this string instance) + { + var stripped = instance.Trim(); + if (instance != stripped) + return false; + + if (string.IsNullOrEmpty(stripped)) + return false; + + return instance.IndexOfAny(_invalidFileNameCharacters) == -1; + } + /// <summary> - /// Check whether the string contains a valid <see langword="float"/>. + /// Returns <see langword="true"/> if this string contains a valid <see langword="float"/>. + /// This is inclusive of integers, and also supports exponents. /// </summary> + /// <example> + /// <code> + /// GD.Print("1.7".IsValidFloat()) // Prints "True" + /// GD.Print("24".IsValidFloat()) // Prints "True" + /// GD.Print("7e3".IsValidFloat()) // Prints "True" + /// GD.Print("Hello".IsValidFloat()) // Prints "False" + /// </code> + /// </example> /// <param name="instance">The string to check.</param> /// <returns>If the string contains a valid floating point number.</returns> public static bool IsValidFloat(this string instance) { - float f; - return float.TryParse(instance, out f); + return float.TryParse(instance, out _); + } + + /// <summary> + /// Returns <see langword="true"/> if this string contains a valid hexadecimal number. + /// If <paramref name="withPrefix"/> is <see langword="true"/>, then a validity of the + /// hexadecimal number is determined by <c>0x</c> prefix, for instance: <c>0xDEADC0DE</c>. + /// </summary> + /// <param name="instance">The string to check.</param> + /// <param name="withPrefix">If the string must contain the <c>0x</c> prefix to be valid.</param> + /// <returns>If the string contains a valid hexadecimal number.</returns> + public static bool IsValidHexNumber(this string instance, bool withPrefix = false) + { + if (string.IsNullOrEmpty(instance)) + return false; + + int from = 0; + if (instance.Length != 1 && instance[0] == '+' || instance[0] == '-') + { + from++; + } + + if (withPrefix) + { + if (instance.Length < 3) + return false; + if (instance[from] != '0' || instance[from + 1] != 'x') + return false; + from += 2; + } + + for (int i = from; i < instance.Length; i++) + { + char c = instance[i]; + if (IsHexDigit(c)) + continue; + + return false; + } + + return true; + + static bool IsHexDigit(char c) + { + return char.IsDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } } /// <summary> - /// Check whether the string contains a valid color in HTML notation. + /// Returns <see langword="true"/> if this string contains a valid color in hexadecimal + /// HTML notation. Other HTML notations such as named colors or <c>hsl()</c> aren't + /// considered valid by this method and will return <see langword="false"/>. /// </summary> /// <param name="instance">The string to check.</param> /// <returns>If the string contains a valid HTML color.</returns> @@ -895,10 +1049,17 @@ namespace Godot } /// <summary> - /// Check whether the string is a valid identifier. As is common in - /// programming languages, a valid identifier may contain only letters, - /// digits and underscores (_) and the first character may not be a digit. + /// Returns <see langword="true"/> if this string is a valid identifier. + /// A valid identifier may contain only letters, digits and underscores (<c>_</c>) + /// and the first character may not be a digit. /// </summary> + /// <example> + /// <code> + /// GD.Print("good_ident_1".IsValidIdentifier()) // Prints "True" + /// GD.Print("1st_bad_ident".IsValidIdentifier()) // Prints "False" + /// GD.Print("bad_ident_#2".IsValidIdentifier()) // Prints "False" + /// </code> + /// </example> /// <param name="instance">The string to check.</param> /// <returns>If the string contains a valid identifier.</returns> public static bool IsValidIdentifier(this string instance) @@ -926,38 +1087,73 @@ namespace Godot } /// <summary> - /// Check whether the string contains a valid integer. + /// Returns <see langword="true"/> if this string contains a valid <see langword="int"/>. /// </summary> + /// <example> + /// <code> + /// GD.Print("7".IsValidInt()) // Prints "True" + /// GD.Print("14.6".IsValidInt()) // Prints "False" + /// GD.Print("L".IsValidInt()) // Prints "False" + /// GD.Print("+3".IsValidInt()) // Prints "True" + /// GD.Print("-12".IsValidInt()) // Prints "True" + /// </code> + /// </example> /// <param name="instance">The string to check.</param> /// <returns>If the string contains a valid integer.</returns> - public static bool IsValidInteger(this string instance) + public static bool IsValidInt(this string instance) { - int f; - return int.TryParse(instance, out f); + return int.TryParse(instance, out _); } /// <summary> - /// Check whether the string contains a valid IP address. + /// Returns <see langword="true"/> if this string contains only a well-formatted + /// IPv4 or IPv6 address. This method considers reserved IP addresses such as + /// <c>0.0.0.0</c> as valid. /// </summary> /// <param name="instance">The string to check.</param> /// <returns>If the string contains a valid IP address.</returns> public static bool IsValidIPAddress(this string instance) { - // TODO: Support IPv6 addresses - string[] ip = instance.Split("."); + if (instance.Contains(':')) + { + string[] ip = instance.Split(':'); - if (ip.Length != 4) - return false; + for (int i = 0; i < ip.Length; i++) + { + string n = ip[i]; + if (n.Length == 0) + continue; + + if (n.IsValidHexNumber(withPrefix: false)) + { + long nint = n.HexToInt(); + if (nint < 0 || nint > 0xffff) + return false; - for (int i = 0; i < ip.Length; i++) + continue; + } + + if (!n.IsValidIPAddress()) + return false; + } + } + else { - string n = ip[i]; - if (!n.IsValidInteger()) - return false; + string[] ip = instance.Split('.'); - int val = n.ToInt(); - if (val < 0 || val > 255) + if (ip.Length != 4) return false; + + for (int i = 0; i < ip.Length; i++) + { + string n = ip[i]; + if (!n.IsValidInt()) + return false; + + int val = n.ToInt(); + if (val < 0 || val > 255) + return false; + } } return true; @@ -1003,41 +1199,20 @@ namespace Godot } /// <summary> - /// Returns the length of the string in characters. - /// </summary> - /// <param name="instance">The string to check.</param> - /// <returns>The length of the string.</returns> - public static int Length(this string instance) - { - return instance.Length; - } - - /// <summary> /// Returns a copy of the string with characters removed from the left. + /// The <paramref name="chars"/> argument is a string specifying the set of characters + /// to be removed. + /// Note: The <paramref name="chars"/> is not a prefix. See <see cref="TrimPrefix"/> + /// method that will remove a single prefix string rather than a set of characters. /// </summary> /// <seealso cref="RStrip(string, string)"/> /// <param name="instance">The string to remove characters from.</param> /// <param name="chars">The characters to be removed.</param> /// <returns>A copy of the string with characters removed from the left.</returns> + [Obsolete("Use string.TrimStart instead.")] public static string LStrip(this string instance, string chars) { - int len = instance.Length; - int beg; - - for (beg = 0; beg < len; beg++) - { - if (chars.Find(instance[beg]) == -1) - { - break; - } - } - - if (beg == 0) - { - return instance; - } - - return instance.Substr(beg, len - beg); + return instance.TrimStart(chars.ToCharArray()); } /// <summary> @@ -1117,10 +1292,9 @@ namespace Godot /// <returns>The MD5 hash of the string.</returns> public static byte[] MD5Buffer(this string instance) { - using godot_string instanceStr = Marshaling.ConvertStringToNative(instance); - NativeFuncs.godotsharp_string_md5_buffer(instanceStr, out var md5Buffer); - using (md5Buffer) - return Marshaling.ConvertNativePackedByteArrayToSystemArray(md5Buffer); +#pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms + return MD5.HashData(Encoding.UTF8.GetBytes(instance)); +#pragma warning restore CA5351 } /// <summary> @@ -1131,10 +1305,7 @@ namespace Godot /// <returns>The MD5 hash of the string.</returns> public static string MD5Text(this string instance) { - using godot_string instanceStr = Marshaling.ConvertStringToNative(instance); - NativeFuncs.godotsharp_string_md5_text(instanceStr, out var md5Text); - using (md5Text) - return Marshaling.ConvertStringToManaged(md5Text); + return instance.MD5Buffer().HexEncode(); } /// <summary> @@ -1151,17 +1322,6 @@ namespace Godot } /// <summary> - /// Returns the character code at position <paramref name="at"/>. - /// </summary> - /// <param name="instance">The string to check.</param> - /// <param name="at">The position int the string for the character to check.</param> - /// <returns>The character code.</returns> - public static int OrdAt(this string instance, int at) - { - return instance[at]; - } - - /// <summary> /// Format a number to have an exact number of <paramref name="digits"/> /// after the decimal point. /// </summary> @@ -1282,34 +1442,47 @@ namespace Godot } /// <summary> - /// Perform a search for a substring, but start from the end of the string instead of the beginning. + /// Returns the index of the last occurrence of the specified string in this instance, + /// or <c>-1</c>. Optionally, the starting search index can be specified, continuing to + /// the beginning of the string. /// </summary> + /// <seealso cref="Find(string, string, int, bool)"/> + /// <seealso cref="Find(string, char, int, bool)"/> + /// <seealso cref="FindN(string, string, int)"/> /// <seealso cref="RFindN(string, string, int)"/> /// <param name="instance">The string that will be searched.</param> /// <param name="what">The substring to search in the string.</param> /// <param name="from">The position at which to start searching.</param> + /// <param name="caseSensitive">If <see langword="true"/>, the search is case sensitive.</param> /// <returns>The position at which the substring was found, or -1 if not found.</returns> - public static int RFind(this string instance, string what, int from = -1) + public static int RFind(this string instance, string what, int from = -1, bool caseSensitive = true) { - using godot_string instanceStr = Marshaling.ConvertStringToNative(instance); - using godot_string whatStr = Marshaling.ConvertStringToNative(instance); - return NativeFuncs.godotsharp_string_rfind(instanceStr, whatStr, from); + if (from == -1) + from = instance.Length - 1; + + return instance.LastIndexOf(what, from, + caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); } /// <summary> - /// Perform a search for a substring, but start from the end of the string instead of the beginning. - /// Also search case-insensitive. + /// Returns the index of the last case-insensitive occurrence of the specified string in this instance, + /// or <c>-1</c>. Optionally, the starting search index can be specified, continuing to + /// the beginning of the string. /// </summary> - /// <seealso cref="RFind(string, string, int)"/> + /// <seealso cref="Find(string, string, int, bool)"/> + /// <seealso cref="Find(string, char, int, bool)"/> + /// <seealso cref="FindN(string, string, int)"/> + /// <seealso cref="RFind(string, string, int, bool)"/> /// <param name="instance">The string that will be searched.</param> /// <param name="what">The substring to search in the string.</param> /// <param name="from">The position at which to start searching.</param> /// <returns>The position at which the substring was found, or -1 if not found.</returns> public static int RFindN(this string instance, string what, int from = -1) { - using godot_string instanceStr = Marshaling.ConvertStringToNative(instance); - using godot_string whatStr = Marshaling.ConvertStringToNative(instance); - return NativeFuncs.godotsharp_string_rfindn(instanceStr, whatStr, from); + if (from == -1) + from = instance.Length - 1; + + return instance.LastIndexOf(what, from, StringComparison.OrdinalIgnoreCase); } /// <summary> @@ -1332,30 +1505,43 @@ namespace Godot /// <summary> /// Returns a copy of the string with characters removed from the right. + /// The <paramref name="chars"/> argument is a string specifying the set of characters + /// to be removed. + /// Note: The <paramref name="chars"/> is not a suffix. See <see cref="TrimSuffix"/> + /// method that will remove a single suffix string rather than a set of characters. /// </summary> /// <seealso cref="LStrip(string, string)"/> /// <param name="instance">The string to remove characters from.</param> /// <param name="chars">The characters to be removed.</param> /// <returns>A copy of the string with characters removed from the right.</returns> + [Obsolete("Use string.TrimEnd instead.")] public static string RStrip(this string instance, string chars) { - int len = instance.Length; - int end; - - for (end = len - 1; end >= 0; end--) - { - if (chars.Find(instance[end]) == -1) - { - break; - } - } + return instance.TrimEnd(chars.ToCharArray()); + } - if (end == len - 1) - { - return instance; - } + /// <summary> + /// Returns the SHA-1 hash of the string as an array of bytes. + /// </summary> + /// <seealso cref="SHA1Text(string)"/> + /// <param name="instance">The string to hash.</param> + /// <returns>The SHA-1 hash of the string.</returns> + public static byte[] SHA1Buffer(this string instance) + { +#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms + return SHA1.HashData(Encoding.UTF8.GetBytes(instance)); +#pragma warning restore CA5350 + } - return instance.Substr(0, end + 1); + /// <summary> + /// Returns the SHA-1 hash of the string as a string. + /// </summary> + /// <seealso cref="SHA1Buffer(string)"/> + /// <param name="instance">The string to hash.</param> + /// <returns>The SHA-1 hash of the string.</returns> + public static string SHA1Text(this string instance) + { + return instance.SHA1Buffer().HexEncode(); } /// <summary> @@ -1366,10 +1552,7 @@ namespace Godot /// <returns>The SHA-256 hash of the string.</returns> public static byte[] SHA256Buffer(this string instance) { - using godot_string instanceStr = Marshaling.ConvertStringToNative(instance); - NativeFuncs.godotsharp_string_sha256_buffer(instanceStr, out var sha256Buffer); - using (sha256Buffer) - return Marshaling.ConvertNativePackedByteArrayToSystemArray(sha256Buffer); + return SHA256.HashData(Encoding.UTF8.GetBytes(instance)); } /// <summary> @@ -1380,10 +1563,7 @@ namespace Godot /// <returns>The SHA-256 hash of the string.</returns> public static string SHA256Text(this string instance) { - using godot_string instanceStr = Marshaling.ConvertStringToNative(instance); - NativeFuncs.godotsharp_string_sha256_text(instanceStr, out var sha256Text); - using (sha256Text) - return Marshaling.ConvertStringToManaged(sha256Text); + return instance.SHA256Buffer().HexEncode(); } /// <summary> @@ -1455,7 +1635,7 @@ namespace Godot /// <returns>The array of strings split from the string.</returns> public static string[] Split(this string instance, string divisor, bool allowEmpty = true) { - return instance.Split(new[] { divisor }, + return instance.Split(divisor, allowEmpty ? StringSplitOptions.None : StringSplitOptions.RemoveEmptyEntries); } @@ -1503,8 +1683,10 @@ namespace Godot }; /// <summary> - /// Returns a copy of the string stripped of any non-printable character at the beginning and the end. - /// The optional arguments are used to toggle stripping on the left and right edges respectively. + /// Returns a copy of the string stripped of any non-printable character + /// (including tabulations, spaces and line breaks) at the beginning and the end. + /// The optional arguments are used to toggle stripping on the left and right + /// edges respectively. /// </summary> /// <param name="instance">The string to strip.</param> /// <param name="left">If the left side should be stripped.</param> @@ -1522,6 +1704,30 @@ namespace Godot return instance.TrimEnd(_nonPrintable); } + + /// <summary> + /// Returns a copy of the string stripped of any escape character. + /// These include all non-printable control characters of the first page + /// of the ASCII table (< 32), such as tabulation (<c>\t</c>) and + /// newline (<c>\n</c> and <c>\r</c>) characters, but not spaces. + /// </summary> + /// <param name="instance">The string to strip.</param> + /// <returns>The string stripped of any escape characters.</returns> + public static string StripEscapes(this string instance) + { + var sb = new StringBuilder(); + for (int i = 0; i < instance.Length; i++) + { + // Escape characters on first page of the ASCII table, before 32 (Space). + if (instance[i] < 32) + continue; + + sb.Append(instance[i]); + } + + return sb.ToString(); + } + /// <summary> /// Returns part of the string from the position <paramref name="from"/>, with length <paramref name="len"/>. /// </summary> @@ -1539,13 +1745,15 @@ namespace Godot /// <summary> /// Converts the String (which is a character array) to PackedByteArray (which is an array of bytes). - /// The conversion is speeded up in comparison to <see cref="ToUTF8(string)"/> with the assumption - /// that all the characters the String contains are only ASCII characters. + /// The conversion is faster compared to <see cref="ToUTF8Buffer(string)"/>, + /// as this method assumes that all the characters in the String are ASCII characters. /// </summary> - /// <seealso cref="ToUTF8(string)"/> + /// <seealso cref="ToUTF8Buffer(string)"/> + /// <seealso cref="ToUTF16Buffer(string)"/> + /// <seealso cref="ToUTF32Buffer(string)"/> /// <param name="instance">The string to convert.</param> /// <returns>The string as ASCII encoded bytes.</returns> - public static byte[] ToAscii(this string instance) + public static byte[] ToASCIIBuffer(this string instance) { return Encoding.ASCII.GetBytes(instance); } @@ -1573,41 +1781,76 @@ namespace Godot } /// <summary> - /// Returns the string converted to lowercase. + /// Converts the string (which is an array of characters) to an UTF-16 encoded array of bytes. /// </summary> - /// <seealso cref="ToUpper(string)"/> + /// <seealso cref="ToASCIIBuffer(string)"/> + /// <seealso cref="ToUTF32Buffer(string)"/> + /// <seealso cref="ToUTF8Buffer(string)"/> /// <param name="instance">The string to convert.</param> - /// <returns>The string converted to lowercase.</returns> - public static string ToLower(this string instance) + /// <returns>The string as UTF-16 encoded bytes.</returns> + public static byte[] ToUTF16Buffer(this string instance) { - return instance.ToLower(); + return Encoding.Unicode.GetBytes(instance); } /// <summary> - /// Returns the string converted to uppercase. + /// Converts the string (which is an array of characters) to an UTF-32 encoded array of bytes. /// </summary> - /// <seealso cref="ToLower(string)"/> + /// <seealso cref="ToASCIIBuffer(string)"/> + /// <seealso cref="ToUTF16Buffer(string)"/> + /// <seealso cref="ToUTF8Buffer(string)"/> /// <param name="instance">The string to convert.</param> - /// <returns>The string converted to uppercase.</returns> - public static string ToUpper(this string instance) + /// <returns>The string as UTF-32 encoded bytes.</returns> + public static byte[] ToUTF32Buffer(this string instance) { - return instance.ToUpper(); + return Encoding.UTF32.GetBytes(instance); } /// <summary> - /// Converts the String (which is an array of characters) to PackedByteArray (which is an array of bytes). - /// The conversion is a bit slower than <see cref="ToAscii(string)"/>, but supports all UTF-8 characters. - /// Therefore, you should prefer this function over <see cref="ToAscii(string)"/>. + /// Converts the string (which is an array of characters) to an UTF-8 encoded array of bytes. + /// The conversion is a bit slower than <see cref="ToASCIIBuffer(string)"/>, + /// but supports all UTF-8 characters. Therefore, you should prefer this function + /// over <see cref="ToASCIIBuffer(string)"/>. /// </summary> - /// <seealso cref="ToAscii(string)"/> + /// <seealso cref="ToASCIIBuffer(string)"/> + /// <seealso cref="ToUTF16Buffer(string)"/> + /// <seealso cref="ToUTF32Buffer(string)"/> /// <param name="instance">The string to convert.</param> /// <returns>The string as UTF-8 encoded bytes.</returns> - public static byte[] ToUTF8(this string instance) + public static byte[] ToUTF8Buffer(this string instance) { return Encoding.UTF8.GetBytes(instance); } /// <summary> + /// Removes a given string from the start if it starts with it or leaves the string unchanged. + /// </summary> + /// <param name="instance">The string to remove the prefix from.</param> + /// <param name="prefix">The string to remove from the start.</param> + /// <returns>A copy of the string with the prefix string removed from the start.</returns> + public static string TrimPrefix(this string instance, string prefix) + { + if (instance.StartsWith(prefix)) + return instance.Substring(prefix.Length); + + return instance; + } + + /// <summary> + /// Removes a given string from the end if it ends with it or leaves the string unchanged. + /// </summary> + /// <param name="instance">The string to remove the suffix from.</param> + /// <param name="suffix">The string to remove from the end.</param> + /// <returns>A copy of the string with the suffix string removed from the end.</returns> + public static string TrimSuffix(this string instance, string suffix) + { + if (instance.EndsWith(suffix)) + return instance.Substring(0, instance.Length - suffix.Length); + + return instance; + } + + /// <summary> /// Decodes a string in URL encoded format. This is meant to /// decode parameters in a URL when receiving an HTTP request. /// This mostly wraps around <see cref="Uri.UnescapeDataString"/>, @@ -1634,6 +1877,25 @@ namespace Godot return Uri.EscapeDataString(instance); } + private const string _uniqueNodePrefix = "%"; + private static readonly string[] _invalidNodeNameCharacters = { ".", ":", "@", "/", "\"", _uniqueNodePrefix }; + + /// <summary> + /// Removes any characters from the string that are prohibited in + /// <see cref="Node"/> names (<c>.</c> <c>:</c> <c>@</c> <c>/</c> <c>"</c>). + /// </summary> + /// <param name="instance">The string to sanitize.</param> + /// <returns>The string sanitized as a valid node name.</returns> + public static string ValidateNodeName(this string instance) + { + string name = instance.Replace(_invalidNodeNameCharacters[0], ""); + for (int i = 1; i < _invalidNodeNameCharacters.Length; i++) + { + name = name.Replace(_invalidNodeNameCharacters[i], ""); + } + return name; + } + /// <summary> /// Returns a copy of the string with special characters escaped using the XML standard. /// </summary> diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp index e20a88076a..338e5a0147 100644 --- a/modules/mono/glue/runtime_interop.cpp +++ b/modules/mono/glue/runtime_interop.cpp @@ -1067,30 +1067,6 @@ void godotsharp_dictionary_to_string(const Dictionary *p_self, String *r_str) { *r_str = Variant(*p_self).operator String(); } -void godotsharp_string_md5_buffer(const String *p_self, PackedByteArray *r_md5_buffer) { - memnew_placement(r_md5_buffer, PackedByteArray(p_self->md5_buffer())); -} - -void godotsharp_string_md5_text(const String *p_self, String *r_md5_text) { - memnew_placement(r_md5_text, String(p_self->md5_text())); -} - -int32_t godotsharp_string_rfind(const String *p_self, const String *p_what, int32_t p_from) { - return p_self->rfind(*p_what, p_from); -} - -int32_t godotsharp_string_rfindn(const String *p_self, const String *p_what, int32_t p_from) { - return p_self->rfindn(*p_what, p_from); -} - -void godotsharp_string_sha256_buffer(const String *p_self, PackedByteArray *r_sha256_buffer) { - memnew_placement(r_sha256_buffer, PackedByteArray(p_self->sha256_buffer())); -} - -void godotsharp_string_sha256_text(const String *p_self, String *r_sha256_text) { - memnew_placement(r_sha256_text, String(p_self->sha256_text())); -} - void godotsharp_string_simplify_path(const String *p_self, String *r_simplified_path) { memnew_placement(r_simplified_path, String(p_self->simplify_path())); } @@ -1473,12 +1449,6 @@ static const void *unmanaged_callbacks[]{ (void *)godotsharp_dictionary_duplicate, (void *)godotsharp_dictionary_remove_key, (void *)godotsharp_dictionary_to_string, - (void *)godotsharp_string_md5_buffer, - (void *)godotsharp_string_md5_text, - (void *)godotsharp_string_rfind, - (void *)godotsharp_string_rfindn, - (void *)godotsharp_string_sha256_buffer, - (void *)godotsharp_string_sha256_text, (void *)godotsharp_string_simplify_path, (void *)godotsharp_string_to_camel_case, (void *)godotsharp_string_to_pascal_case, diff --git a/modules/noise/editor/noise_editor_plugin.cpp b/modules/noise/editor/noise_editor_plugin.cpp index 47f5f8f819..a8e376e142 100644 --- a/modules/noise/editor/noise_editor_plugin.cpp +++ b/modules/noise/editor/noise_editor_plugin.cpp @@ -34,6 +34,7 @@ #include "editor/editor_inspector.h" #include "editor/editor_scale.h" +#include "scene/gui/button.h" #include "scene/gui/texture_rect.h" #include "modules/noise/noise.h" diff --git a/modules/openxr/extensions/openxr_opengl_extension.cpp b/modules/openxr/extensions/openxr_opengl_extension.cpp index 569030cc11..234c5f8391 100644 --- a/modules/openxr/extensions/openxr_opengl_extension.cpp +++ b/modules/openxr/extensions/openxr_opengl_extension.cpp @@ -138,7 +138,7 @@ void *OpenXROpenGLExtension::set_session_create_and_get_next_pointer(void *p_nex graphics_binding_gl.display = eglGetCurrentDisplay(); graphics_binding_gl.config = (EGLConfig)0; // https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/master/src/tests/hello_xr/graphicsplugin_opengles.cpp#L122 - graphics_binding_gl.context = eglGetCurrentContext(); + graphics_binding_gl.context = (void *)display_server->window_get_native_handle(DisplayServer::OPENGL_CONTEXT); #else graphics_binding_gl.type = XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR; graphics_binding_gl.next = p_next_pointer; diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp index d6580ebfa6..59d3865acd 100644 --- a/modules/openxr/openxr_api.cpp +++ b/modules/openxr/openxr_api.cpp @@ -51,7 +51,7 @@ #define XR_USE_GRAPHICS_API_VULKAN #endif #ifdef GLES3_ENABLED -#ifdef ANDROID +#ifdef ANDROID_ENABLED #define XR_USE_GRAPHICS_API_OPENGL_ES #include <EGL/egl.h> #include <EGL/eglext.h> @@ -59,7 +59,7 @@ #include <GLES3/gl3ext.h> #else #define XR_USE_GRAPHICS_API_OPENGL -#endif // ANDROID +#endif // ANDROID_ENABLED #ifdef X11_ENABLED #include OPENGL_INCLUDE_H #define GL_GLEXT_PROTOTYPES 1 |