Przeglądaj źródła

Merge pull request #68747 from rune-scape/rune-stringname-unification

GDScript: Unify StringName and String
Rémi Verschelde 2 lat temu
rodzic
commit
907298d673
28 zmienionych plików z 1463 dodań i 267 usunięć
  1. 67 29
      core/variant/array.cpp
  2. 19 4
      core/variant/container_type_validate.h
  3. 10 46
      core/variant/dictionary.cpp
  4. 13 0
      core/variant/variant.cpp
  5. 4 0
      core/variant/variant.h
  6. 240 101
      core/variant/variant_call.cpp
  7. 62 55
      core/variant/variant_op.cpp
  8. 50 23
      core/variant/variant_op.h
  9. 6 0
      doc/classes/String.xml
  10. 821 0
      doc/classes/StringName.xml
  11. 2 2
      editor/doc_tools.cpp
  12. 6 4
      modules/gdscript/gdscript_analyzer.cpp
  13. 21 0
      modules/gdscript/gdscript_compiler.cpp
  14. 9 0
      modules/gdscript/tests/scripts/analyzer/errors/dictionary_string_stringname_equivalent.gd
  15. 2 0
      modules/gdscript/tests/scripts/analyzer/errors/dictionary_string_stringname_equivalent.out
  16. 8 0
      modules/gdscript/tests/scripts/analyzer/features/array_string_stringname_equivalent.gd
  17. 3 0
      modules/gdscript/tests/scripts/analyzer/features/array_string_stringname_equivalent.out
  18. 35 0
      modules/gdscript/tests/scripts/runtime/features/array_string_stringname_equivalent.gd
  19. 11 0
      modules/gdscript/tests/scripts/runtime/features/array_string_stringname_equivalent.out
  20. 17 0
      modules/gdscript/tests/scripts/runtime/features/dictionary_string_stringname_equivalent.gd
  21. 6 0
      modules/gdscript/tests/scripts/runtime/features/dictionary_string_stringname_equivalent.out
  22. 14 0
      modules/gdscript/tests/scripts/runtime/features/match_string_stringname_equivalent.gd
  23. 3 0
      modules/gdscript/tests/scripts/runtime/features/match_string_stringname_equivalent.out
  24. 11 0
      modules/gdscript/tests/scripts/runtime/features/string_stringname_equivalent.gd
  25. 8 0
      modules/gdscript/tests/scripts/runtime/features/string_stringname_equivalent.out
  26. 1 1
      modules/multiplayer/scene_rpc_interface.cpp
  27. 1 2
      modules/regex/regex.cpp
  28. 13 0
      tests/core/variant/test_dictionary.h

+ 67 - 29
core/variant/array.cpp

@@ -38,6 +38,7 @@
 #include "core/templates/search_array.h"
 #include "core/templates/vector.h"
 #include "core/variant/callable.h"
+#include "core/variant/dictionary.h"
 #include "core/variant/variant.h"
 
 class ArrayPrivate {
@@ -201,16 +202,21 @@ uint32_t Array::recursive_hash(int recursion_count) const {
 }
 
 bool Array::_assign(const Array &p_array) {
+	bool can_convert = p_array._p->typed.type == Variant::NIL;
+	can_convert |= _p->typed.type == Variant::STRING && p_array._p->typed.type == Variant::STRING_NAME;
+	can_convert |= _p->typed.type == Variant::STRING_NAME && p_array._p->typed.type == Variant::STRING;
+
 	if (_p->typed.type != Variant::OBJECT && _p->typed.type == p_array._p->typed.type) {
 		//same type or untyped, just reference, should be fine
 		_ref(p_array);
 	} else if (_p->typed.type == Variant::NIL) { //from typed to untyped, must copy, but this is cheap anyway
 		_p->array = p_array._p->array;
-	} else if (p_array._p->typed.type == Variant::NIL) { //from untyped to typed, must try to check if they are all valid
+	} else if (can_convert) { //from untyped to typed, must try to check if they are all valid
 		if (_p->typed.type == Variant::OBJECT) {
 			//for objects, it needs full validation, either can be converted or fail
 			for (int i = 0; i < p_array._p->array.size(); i++) {
-				if (!_p->typed.validate(p_array._p->array[i], "assign")) {
+				const Variant &element = p_array._p->array[i];
+				if (element.get_type() != Variant::OBJECT || !_p->typed.validate_object(element, "assign")) {
 					return false;
 				}
 			}
@@ -255,16 +261,20 @@ void Array::operator=(const Array &p_array) {
 
 void Array::push_back(const Variant &p_value) {
 	ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
-	ERR_FAIL_COND(!_p->typed.validate(p_value, "push_back"));
-	_p->array.push_back(p_value);
+	Variant value = p_value;
+	ERR_FAIL_COND(!_p->typed.validate(value, "push_back"));
+	_p->array.push_back(value);
 }
 
 void Array::append_array(const Array &p_array) {
 	ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
-	for (int i = 0; i < p_array.size(); ++i) {
-		ERR_FAIL_COND(!_p->typed.validate(p_array[i], "append_array"));
+
+	Vector<Variant> validated_array = p_array._p->array;
+	for (int i = 0; i < validated_array.size(); ++i) {
+		ERR_FAIL_COND(!_p->typed.validate(validated_array.write[i], "append_array"));
 	}
-	_p->array.append_array(p_array._p->array);
+
+	_p->array.append_array(validated_array);
 }
 
 Error Array::resize(int p_new_size) {
@@ -274,20 +284,23 @@ Error Array::resize(int p_new_size) {
 
 Error Array::insert(int p_pos, const Variant &p_value) {
 	ERR_FAIL_COND_V_MSG(_p->read_only, ERR_LOCKED, "Array is in read-only state.");
-	ERR_FAIL_COND_V(!_p->typed.validate(p_value, "insert"), ERR_INVALID_PARAMETER);
-	return _p->array.insert(p_pos, p_value);
+	Variant value = p_value;
+	ERR_FAIL_COND_V(!_p->typed.validate(value, "insert"), ERR_INVALID_PARAMETER);
+	return _p->array.insert(p_pos, value);
 }
 
 void Array::fill(const Variant &p_value) {
 	ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
-	ERR_FAIL_COND(!_p->typed.validate(p_value, "fill"));
-	_p->array.fill(p_value);
+	Variant value = p_value;
+	ERR_FAIL_COND(!_p->typed.validate(value, "fill"));
+	_p->array.fill(value);
 }
 
 void Array::erase(const Variant &p_value) {
 	ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
-	ERR_FAIL_COND(!_p->typed.validate(p_value, "erase"));
-	_p->array.erase(p_value);
+	Variant value = p_value;
+	ERR_FAIL_COND(!_p->typed.validate(value, "erase"));
+	_p->array.erase(value);
 }
 
 Variant Array::front() const {
@@ -306,15 +319,34 @@ Variant Array::pick_random() const {
 }
 
 int Array::find(const Variant &p_value, int p_from) const {
-	ERR_FAIL_COND_V(!_p->typed.validate(p_value, "find"), -1);
-	return _p->array.find(p_value, p_from);
+	if (_p->array.size() == 0) {
+		return -1;
+	}
+	Variant value = p_value;
+	ERR_FAIL_COND_V(!_p->typed.validate(value, "find"), -1);
+
+	int ret = -1;
+
+	if (p_from < 0 || size() == 0) {
+		return ret;
+	}
+
+	for (int i = p_from; i < size(); i++) {
+		if (StringLikeVariantComparator::compare(_p->array[i], value)) {
+			ret = i;
+			break;
+		}
+	}
+
+	return ret;
 }
 
 int Array::rfind(const Variant &p_value, int p_from) const {
 	if (_p->array.size() == 0) {
 		return -1;
 	}
-	ERR_FAIL_COND_V(!_p->typed.validate(p_value, "rfind"), -1);
+	Variant value = p_value;
+	ERR_FAIL_COND_V(!_p->typed.validate(value, "rfind"), -1);
 
 	if (p_from < 0) {
 		// Relative offset from the end
@@ -326,7 +358,7 @@ int Array::rfind(const Variant &p_value, int p_from) const {
 	}
 
 	for (int i = p_from; i >= 0; i--) {
-		if (_p->array[i] == p_value) {
+		if (StringLikeVariantComparator::compare(_p->array[i], value)) {
 			return i;
 		}
 	}
@@ -335,14 +367,15 @@ int Array::rfind(const Variant &p_value, int p_from) const {
 }
 
 int Array::count(const Variant &p_value) const {
-	ERR_FAIL_COND_V(!_p->typed.validate(p_value, "count"), 0);
+	Variant value = p_value;
+	ERR_FAIL_COND_V(!_p->typed.validate(value, "count"), 0);
 	if (_p->array.size() == 0) {
 		return 0;
 	}
 
 	int amount = 0;
 	for (int i = 0; i < _p->array.size(); i++) {
-		if (_p->array[i] == p_value) {
+		if (StringLikeVariantComparator::compare(_p->array[i], value)) {
 			amount++;
 		}
 	}
@@ -351,9 +384,10 @@ int Array::count(const Variant &p_value) const {
 }
 
 bool Array::has(const Variant &p_value) const {
-	ERR_FAIL_COND_V(!_p->typed.validate(p_value, "use 'has'"), false);
+	Variant value = p_value;
+	ERR_FAIL_COND_V(!_p->typed.validate(value, "use 'has'"), false);
 
-	return _p->array.find(p_value, 0) != -1;
+	return find(value) != -1;
 }
 
 void Array::remove_at(int p_pos) {
@@ -363,9 +397,10 @@ void Array::remove_at(int p_pos) {
 
 void Array::set(int p_idx, const Variant &p_value) {
 	ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
-	ERR_FAIL_COND(!_p->typed.validate(p_value, "set"));
+	Variant value = p_value;
+	ERR_FAIL_COND(!_p->typed.validate(value, "set"));
 
-	operator[](p_idx) = p_value;
+	operator[](p_idx) = value;
 }
 
 const Variant &Array::get(int p_idx) const {
@@ -588,15 +623,17 @@ void Array::shuffle() {
 }
 
 int Array::bsearch(const Variant &p_value, bool p_before) {
-	ERR_FAIL_COND_V(!_p->typed.validate(p_value, "binary search"), -1);
+	Variant value = p_value;
+	ERR_FAIL_COND_V(!_p->typed.validate(value, "binary search"), -1);
 	SearchArray<Variant, _ArrayVariantSort> avs;
-	return avs.bisect(_p->array.ptrw(), _p->array.size(), p_value, p_before);
+	return avs.bisect(_p->array.ptrw(), _p->array.size(), value, p_before);
 }
 
 int Array::bsearch_custom(const Variant &p_value, const Callable &p_callable, bool p_before) {
-	ERR_FAIL_COND_V(!_p->typed.validate(p_value, "custom binary search"), -1);
+	Variant value = p_value;
+	ERR_FAIL_COND_V(!_p->typed.validate(value, "custom binary search"), -1);
 
-	return _p->array.bsearch_custom<CallableComparator>(p_value, p_before, p_callable);
+	return _p->array.bsearch_custom<CallableComparator>(value, p_before, p_callable);
 }
 
 void Array::reverse() {
@@ -606,8 +643,9 @@ void Array::reverse() {
 
 void Array::push_front(const Variant &p_value) {
 	ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
-	ERR_FAIL_COND(!_p->typed.validate(p_value, "push_front"));
-	_p->array.insert(0, p_value);
+	Variant value = p_value;
+	ERR_FAIL_COND(!_p->typed.validate(value, "push_front"));
+	_p->array.insert(0, value);
 }
 
 Variant Array::pop_back() {

+ 19 - 4
core/variant/container_type_validate.h

@@ -74,22 +74,37 @@ struct ContainerTypeValidate {
 		return true;
 	}
 
-	_FORCE_INLINE_ bool validate(const Variant &p_variant, const char *p_operation = "use") {
+	// Coerces String and StringName into each other when needed.
+	_FORCE_INLINE_ bool validate(Variant &inout_variant, const char *p_operation = "use") {
 		if (type == Variant::NIL) {
 			return true;
 		}
 
-		if (type != p_variant.get_type()) {
-			if (p_variant.get_type() == Variant::NIL && type == Variant::OBJECT) {
+		if (type != inout_variant.get_type()) {
+			if (inout_variant.get_type() == Variant::NIL && type == Variant::OBJECT) {
+				return true;
+			}
+			if (type == Variant::STRING && inout_variant.get_type() == Variant::STRING_NAME) {
+				inout_variant = String(inout_variant);
+				return true;
+			} else if (type == Variant::STRING_NAME && inout_variant.get_type() == Variant::STRING) {
+				inout_variant = StringName(inout_variant);
 				return true;
 			}
 
-			ERR_FAIL_V_MSG(false, "Attempted to " + String(p_operation) + " a variable of type '" + Variant::get_type_name(p_variant.get_type()) + "' into a " + where + " of type '" + Variant::get_type_name(type) + "'.");
+			ERR_FAIL_V_MSG(false, "Attempted to " + String(p_operation) + " a variable of type '" + Variant::get_type_name(inout_variant.get_type()) + "' into a " + where + " of type '" + Variant::get_type_name(type) + "'.");
 		}
 
 		if (type != Variant::OBJECT) {
 			return true;
 		}
+
+		return validate_object(inout_variant, p_operation);
+	}
+
+	_FORCE_INLINE_ bool validate_object(const Variant &p_variant, const char *p_operation = "use") {
+		ERR_FAIL_COND_V(p_variant.get_type() != Variant::OBJECT, false);
+
 #ifdef DEBUG_ENABLED
 		ObjectID object_id = p_variant;
 		if (object_id == ObjectID()) {

+ 10 - 46
core/variant/dictionary.cpp

@@ -42,7 +42,7 @@
 struct DictionaryPrivate {
 	SafeRefCount refcount;
 	Variant *read_only = nullptr; // If enabled, a pointer is used to a temporary value that is used to return read-only values.
-	HashMap<Variant, Variant, VariantHasher, VariantComparator> variant_map;
+	HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator> variant_map;
 };
 
 void Dictionary::get_key_list(List<Variant> *p_keys) const {
@@ -100,24 +100,12 @@ Variant &Dictionary::operator[](const Variant &p_key) {
 }
 
 const Variant &Dictionary::operator[](const Variant &p_key) const {
-	if (p_key.get_type() == Variant::STRING_NAME) {
-		const StringName *sn = VariantInternal::get_string_name(&p_key);
-		return _p->variant_map[sn->operator String()];
-	} else {
-		return _p->variant_map[p_key];
-	}
+	// Will not insert key, so no conversion is necessary.
+	return _p->variant_map[p_key];
 }
 
 const Variant *Dictionary::getptr(const Variant &p_key) const {
-	HashMap<Variant, Variant, VariantHasher, VariantComparator>::ConstIterator E;
-
-	if (p_key.get_type() == Variant::STRING_NAME) {
-		const StringName *sn = VariantInternal::get_string_name(&p_key);
-		E = ((const HashMap<Variant, Variant, VariantHasher, VariantComparator> *)&_p->variant_map)->find(sn->operator String());
-	} else {
-		E = ((const HashMap<Variant, Variant, VariantHasher, VariantComparator> *)&_p->variant_map)->find(p_key);
-	}
-
+	HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator>::ConstIterator E(_p->variant_map.find(p_key));
 	if (!E) {
 		return nullptr;
 	}
@@ -125,14 +113,7 @@ const Variant *Dictionary::getptr(const Variant &p_key) const {
 }
 
 Variant *Dictionary::getptr(const Variant &p_key) {
-	HashMap<Variant, Variant, VariantHasher, VariantComparator>::Iterator E;
-
-	if (p_key.get_type() == Variant::STRING_NAME) {
-		const StringName *sn = VariantInternal::get_string_name(&p_key);
-		E = ((HashMap<Variant, Variant, VariantHasher, VariantComparator> *)&_p->variant_map)->find(sn->operator String());
-	} else {
-		E = ((HashMap<Variant, Variant, VariantHasher, VariantComparator> *)&_p->variant_map)->find(p_key);
-	}
+	HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator>::Iterator E(_p->variant_map.find(p_key));
 	if (!E) {
 		return nullptr;
 	}
@@ -145,14 +126,7 @@ Variant *Dictionary::getptr(const Variant &p_key) {
 }
 
 Variant Dictionary::get_valid(const Variant &p_key) const {
-	HashMap<Variant, Variant, VariantHasher, VariantComparator>::ConstIterator E;
-
-	if (p_key.get_type() == Variant::STRING_NAME) {
-		const StringName *sn = VariantInternal::get_string_name(&p_key);
-		E = ((const HashMap<Variant, Variant, VariantHasher, VariantComparator> *)&_p->variant_map)->find(sn->operator String());
-	} else {
-		E = ((const HashMap<Variant, Variant, VariantHasher, VariantComparator> *)&_p->variant_map)->find(p_key);
-	}
+	HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator>::ConstIterator E(_p->variant_map.find(p_key));
 
 	if (!E) {
 		return Variant();
@@ -178,12 +152,7 @@ bool Dictionary::is_empty() const {
 }
 
 bool Dictionary::has(const Variant &p_key) const {
-	if (p_key.get_type() == Variant::STRING_NAME) {
-		const StringName *sn = VariantInternal::get_string_name(&p_key);
-		return _p->variant_map.has(sn->operator String());
-	} else {
-		return _p->variant_map.has(p_key);
-	}
+	return _p->variant_map.has(p_key);
 }
 
 bool Dictionary::has_all(const Array &p_keys) const {
@@ -206,12 +175,7 @@ Variant Dictionary::find_key(const Variant &p_value) const {
 
 bool Dictionary::erase(const Variant &p_key) {
 	ERR_FAIL_COND_V_MSG(_p->read_only, false, "Dictionary is in read-only state.");
-	if (p_key.get_type() == Variant::STRING_NAME) {
-		const StringName *sn = VariantInternal::get_string_name(&p_key);
-		return _p->variant_map.erase(sn->operator String());
-	} else {
-		return _p->variant_map.erase(p_key);
-	}
+	return _p->variant_map.erase(p_key);
 }
 
 bool Dictionary::operator==(const Dictionary &p_dictionary) const {
@@ -238,7 +202,7 @@ bool Dictionary::recursive_equal(const Dictionary &p_dictionary, int recursion_c
 	}
 	recursion_count++;
 	for (const KeyValue<Variant, Variant> &this_E : _p->variant_map) {
-		HashMap<Variant, Variant, VariantHasher, VariantComparator>::ConstIterator other_E = ((const HashMap<Variant, Variant, VariantHasher, VariantComparator> *)&p_dictionary._p->variant_map)->find(this_E.key);
+		HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator>::ConstIterator other_E(p_dictionary._p->variant_map.find(this_E.key));
 		if (!other_E || !this_E.value.hash_compare(other_E->value, recursion_count)) {
 			return false;
 		}
@@ -360,7 +324,7 @@ const Variant *Dictionary::next(const Variant *p_key) const {
 		}
 		return nullptr;
 	}
-	HashMap<Variant, Variant, VariantHasher, VariantComparator>::Iterator E = _p->variant_map.find(*p_key);
+	HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator>::Iterator E = _p->variant_map.find(*p_key);
 
 	if (!E) {
 		return nullptr;

+ 13 - 0
core/variant/variant.cpp

@@ -3491,6 +3491,19 @@ bool Variant::hash_compare(const Variant &p_variant, int recursion_count) const
 	}
 }
 
+bool StringLikeVariantComparator::compare(const Variant &p_lhs, const Variant &p_rhs) {
+	if (p_lhs.hash_compare(p_rhs)) {
+		return true;
+	}
+	if (p_lhs.get_type() == Variant::STRING && p_rhs.get_type() == Variant::STRING_NAME) {
+		return *VariantInternal::get_string(&p_lhs) == *VariantInternal::get_string_name(&p_rhs);
+	}
+	if (p_lhs.get_type() == Variant::STRING_NAME && p_rhs.get_type() == Variant::STRING) {
+		return *VariantInternal::get_string_name(&p_lhs) == *VariantInternal::get_string(&p_rhs);
+	}
+	return false;
+}
+
 bool Variant::is_ref_counted() const {
 	return type == OBJECT && _get_obj().id.is_ref_counted();
 }

+ 4 - 0
core/variant/variant.h

@@ -797,6 +797,10 @@ struct VariantComparator {
 	static _FORCE_INLINE_ bool compare(const Variant &p_lhs, const Variant &p_rhs) { return p_lhs.hash_compare(p_rhs); }
 };
 
+struct StringLikeVariantComparator {
+	static bool compare(const Variant &p_lhs, const Variant &p_rhs);
+};
+
 Variant::ObjData &Variant::_get_obj() {
 	return *reinterpret_cast<ObjData *>(&_data._mem[0]);
 }

+ 240 - 101
core/variant/variant_call.cpp

@@ -73,6 +73,30 @@ static _FORCE_INLINE_ void vc_method_call(void (T::*method)(P...) const, Variant
 	call_with_variant_argsc_dv(VariantGetInternalPtr<T>::get_ptr(base), method, p_args, p_argcount, r_error, p_defvals);
 }
 
+template <class From, class R, class T, class... P>
+static _FORCE_INLINE_ void vc_convert_method_call(R (T::*method)(P...), Variant *base, const Variant **p_args, int p_argcount, Variant &r_ret, const Vector<Variant> &p_defvals, Callable::CallError &r_error) {
+	T converted(static_cast<T>(*VariantGetInternalPtr<From>::get_ptr(base)));
+	call_with_variant_args_ret_dv(&converted, method, p_args, p_argcount, r_ret, r_error, p_defvals);
+}
+
+template <class From, class R, class T, class... P>
+static _FORCE_INLINE_ void vc_convert_method_call(R (T::*method)(P...) const, Variant *base, const Variant **p_args, int p_argcount, Variant &r_ret, const Vector<Variant> &p_defvals, Callable::CallError &r_error) {
+	T converted(static_cast<T>(*VariantGetInternalPtr<From>::get_ptr(base)));
+	call_with_variant_args_retc_dv(&converted, method, p_args, p_argcount, r_ret, r_error, p_defvals);
+}
+
+template <class From, class T, class... P>
+static _FORCE_INLINE_ void vc_convert_method_call(void (T::*method)(P...), Variant *base, const Variant **p_args, int p_argcount, Variant &r_ret, const Vector<Variant> &p_defvals, Callable::CallError &r_error) {
+	T converted(static_cast<T>(*VariantGetInternalPtr<From>::get_ptr(base)));
+	call_with_variant_args_dv(&converted, method, p_args, p_argcount, r_error, p_defvals);
+}
+
+template <class From, class T, class... P>
+static _FORCE_INLINE_ void vc_convert_method_call(void (T::*method)(P...) const, Variant *base, const Variant **p_args, int p_argcount, Variant &r_ret, const Vector<Variant> &p_defvals, Callable::CallError &r_error) {
+	T converted(static_cast<T>(*VariantGetInternalPtr<From>::get_ptr(base)));
+	call_with_variant_argsc_dv(&converted, method, p_args, p_argcount, r_error, p_defvals);
+}
+
 template <class R, class T, class... P>
 static _FORCE_INLINE_ void vc_method_call_static(R (*method)(T *, P...), Variant *base, const Variant **p_args, int p_argcount, Variant &r_ret, const Vector<Variant> &p_defvals, Callable::CallError &r_error) {
 	call_with_variant_args_retc_static_helper_dv(VariantGetInternalPtr<T>::get_ptr(base), method, p_args, p_argcount, r_ret, p_defvals, r_error);
@@ -102,6 +126,29 @@ static _FORCE_INLINE_ void vc_validated_call(void (T::*method)(P...) const, Vari
 	call_with_validated_variant_argsc(base, method, p_args);
 }
 
+template <class From, class R, class T, class... P>
+static _FORCE_INLINE_ void vc_convert_validated_call(R (T::*method)(P...), Variant *base, const Variant **p_args, Variant *r_ret) {
+	T converted(static_cast<T>(*VariantGetInternalPtr<From>::get_ptr(base)));
+	call_with_validated_variant_args_ret_helper<T, R, P...>(&converted, method, p_args, r_ret, BuildIndexSequence<sizeof...(P)>{});
+}
+
+template <class From, class R, class T, class... P>
+static _FORCE_INLINE_ void vc_convert_validated_call(R (T::*method)(P...) const, Variant *base, const Variant **p_args, Variant *r_ret) {
+	T converted(static_cast<T>(*VariantGetInternalPtr<From>::get_ptr(base)));
+	call_with_validated_variant_args_retc_helper<T, R, P...>(&converted, method, p_args, r_ret, BuildIndexSequence<sizeof...(P)>{});
+}
+template <class From, class T, class... P>
+static _FORCE_INLINE_ void vc_convert_validated_call(void (T::*method)(P...), Variant *base, const Variant **p_args, Variant *r_ret) {
+	T converted(static_cast<T>(*VariantGetInternalPtr<From>::get_ptr(base)));
+	call_with_validated_variant_args_helper<T, P...>(&converted, method, p_args, r_ret, BuildIndexSequence<sizeof...(P)>{});
+}
+
+template <class From, class T, class... P>
+static _FORCE_INLINE_ void vc_convert_validated_call(void (T::*method)(P...) const, Variant *base, const Variant **p_args, Variant *r_ret) {
+	T converted(static_cast<T>(*VariantGetInternalPtr<From>::get_ptr(base)));
+	call_with_validated_variant_argsc_helper<T, P...>(&converted, method, p_args, r_ret, BuildIndexSequence<sizeof...(P)>{});
+}
+
 template <class R, class T, class... P>
 static _FORCE_INLINE_ void vc_validated_call_static(R (*method)(T *, P...), Variant *base, const Variant **p_args, Variant *r_ret) {
 	call_with_validated_variant_args_static_retc(base, method, p_args, r_ret);
@@ -142,6 +189,30 @@ static _FORCE_INLINE_ void vc_ptrcall(void (T::*method)(P...) const, void *p_bas
 	call_with_ptr_argsc(reinterpret_cast<T *>(p_base), method, p_args);
 }
 
+template <class From, class R, class T, class... P>
+static _FORCE_INLINE_ void vc_convert_ptrcall(R (T::*method)(P...), void *p_base, const void **p_args, void *r_ret) {
+	T converted(*reinterpret_cast<From *>(p_base));
+	call_with_ptr_args_ret(&converted, method, p_args, r_ret);
+}
+
+template <class From, class R, class T, class... P>
+static _FORCE_INLINE_ void vc_convert_ptrcall(R (T::*method)(P...) const, void *p_base, const void **p_args, void *r_ret) {
+	T converted(*reinterpret_cast<From *>(p_base));
+	call_with_ptr_args_retc(&converted, method, p_args, r_ret);
+}
+
+template <class From, class T, class... P>
+static _FORCE_INLINE_ void vc_convert_ptrcall(void (T::*method)(P...), void *p_base, const void **p_args, void *r_ret) {
+	T converted(*reinterpret_cast<From *>(p_base));
+	call_with_ptr_args(&converted, method, p_args);
+}
+
+template <class From, class T, class... P>
+static _FORCE_INLINE_ void vc_convert_ptrcall(void (T::*method)(P...) const, void *p_base, const void **p_args, void *r_ret) {
+	T converted(*reinterpret_cast<From *>(p_base));
+	call_with_ptr_argsc(&converted, method, p_args);
+}
+
 template <class R, class T, class... P>
 static _FORCE_INLINE_ int vc_get_argument_count(R (T::*method)(P...)) {
 	return sizeof...(P);
@@ -337,6 +408,46 @@ static _FORCE_INLINE_ Variant::Type vc_get_base_type(void (T::*method)(P...) con
 		}                                                                                                                                                         \
 	};
 
+#define CONVERT_METHOD_CLASS(m_class, m_method_name, m_method_ptr)                                                                                                \
+	struct Method_##m_class##_##m_method_name {                                                                                                                   \
+		static void call(Variant *base, const Variant **p_args, int p_argcount, Variant &r_ret, const Vector<Variant> &p_defvals, Callable::CallError &r_error) { \
+			vc_convert_method_call<m_class>(m_method_ptr, base, p_args, p_argcount, r_ret, p_defvals, r_error);                                                   \
+		}                                                                                                                                                         \
+		static void validated_call(Variant *base, const Variant **p_args, int p_argcount, Variant *r_ret) {                                                       \
+			vc_convert_validated_call<m_class>(m_method_ptr, base, p_args, r_ret);                                                                                \
+		}                                                                                                                                                         \
+		static void ptrcall(void *p_base, const void **p_args, void *r_ret, int p_argcount) {                                                                     \
+			vc_convert_ptrcall<m_class>(m_method_ptr, p_base, p_args, r_ret);                                                                                     \
+		}                                                                                                                                                         \
+		static int get_argument_count() {                                                                                                                         \
+			return vc_get_argument_count(m_method_ptr);                                                                                                           \
+		}                                                                                                                                                         \
+		static Variant::Type get_argument_type(int p_arg) {                                                                                                       \
+			return vc_get_argument_type(m_method_ptr, p_arg);                                                                                                     \
+		}                                                                                                                                                         \
+		static Variant::Type get_return_type() {                                                                                                                  \
+			return vc_get_return_type(m_method_ptr);                                                                                                              \
+		}                                                                                                                                                         \
+		static bool has_return_type() {                                                                                                                           \
+			return vc_has_return_type(m_method_ptr);                                                                                                              \
+		}                                                                                                                                                         \
+		static bool is_const() {                                                                                                                                  \
+			return vc_is_const(m_method_ptr);                                                                                                                     \
+		}                                                                                                                                                         \
+		static bool is_static() {                                                                                                                                 \
+			return false;                                                                                                                                         \
+		}                                                                                                                                                         \
+		static bool is_vararg() {                                                                                                                                 \
+			return false;                                                                                                                                         \
+		}                                                                                                                                                         \
+		static Variant::Type get_base_type() {                                                                                                                    \
+			return GetTypeInfo<m_class>::VARIANT_TYPE;                                                                                                            \
+		}                                                                                                                                                         \
+		static StringName get_name() {                                                                                                                            \
+			return #m_method_name;                                                                                                                                \
+		}                                                                                                                                                         \
+	};
+
 template <class R, class... P>
 static _FORCE_INLINE_ void vc_static_ptrcall(R (*method)(P...), const void **p_args, void *r_ret) {
 	call_with_ptr_args_static_method_ret<R, P...>(method, p_args, r_ret);
@@ -1421,6 +1532,16 @@ int Variant::get_enum_value(Variant::Type p_type, StringName p_enum_name, String
 	register_builtin_method<Method_##m_type##_##m_method>(sarray(), m_default_args);
 #endif
 
+#ifdef DEBUG_METHODS_ENABLED
+#define bind_convert_method(m_type_from, m_type_to, m_method, m_arg_names, m_default_args) \
+	CONVERT_METHOD_CLASS(m_type_from, m_method, &m_type_to::m_method);                     \
+	register_builtin_method<Method_##m_type_from##_##m_method>(m_arg_names, m_default_args);
+#else
+#define bind_convert_method(m_type_from, m_type_to, m_method, m_arg_names, m_default_args) \
+	CONVERT_METHOD_CLASS(m_type_from, m_method, &m_type_to ::m_method);                    \
+	register_builtin_method<Method_##m_type_from##_##m_method>(sarray(), m_default_args);
+#endif
+
 #ifdef DEBUG_METHODS_ENABLED
 #define bind_static_method(m_type, m_method, m_arg_names, m_default_args) \
 	STATIC_METHOD_CLASS(m_type, m_method, m_type::m_method);              \
@@ -1441,6 +1562,16 @@ int Variant::get_enum_value(Variant::Type p_type, StringName p_enum_name, String
 	register_builtin_method<Method_##m_type##_##m_name>(sarray(), m_default_args);
 #endif
 
+#ifdef DEBUG_METHODS_ENABLED
+#define bind_convert_methodv(m_type_from, m_type_to, m_name, m_method, m_arg_names, m_default_args) \
+	CONVERT_METHOD_CLASS(m_type_from, m_name, m_method);                                            \
+	register_builtin_method<Method_##m_type_from##_##m_name>(m_arg_names, m_default_args);
+#else
+#define bind_convert_methodv(m_type_from, m_type_to, m_name, m_method, m_arg_names, m_default_args) \
+	CONVERT_METHOD_CLASS(m_type_from, m_name, m_method);                                            \
+	register_builtin_method<Method_##m_type_from##_##m_name>(sarray(), m_default_args);
+#endif
+
 #ifdef DEBUG_METHODS_ENABLED
 #define bind_function(m_type, m_name, m_method, m_arg_names, m_default_args) \
 	FUNCTION_CLASS(m_type, m_name, m_method, true);                          \
@@ -1461,6 +1592,14 @@ int Variant::get_enum_value(Variant::Type p_type, StringName p_enum_name, String
 	register_builtin_method<Method_##m_type##_##m_name>(sarray(), m_default_args);
 #endif
 
+#define bind_string_method(m_method, m_arg_names, m_default_args) \
+	bind_method(String, m_method, m_arg_names, m_default_args);   \
+	bind_convert_method(StringName, String, m_method, m_arg_names, m_default_args);
+
+#define bind_string_methodv(m_name, m_method, m_arg_names, m_default_args) \
+	bind_methodv(String, m_name, m_method, m_arg_names, m_default_args);   \
+	bind_convert_methodv(StringName, String, m_name, m_method, m_arg_names, m_default_args);
+
 #define bind_custom(m_type, m_name, m_method, m_has_return, m_ret_type) \
 	VARARG_CLASS(m_type, m_name, m_method, m_has_return, m_ret_type)    \
 	register_builtin_method<Method_##m_type##_##m_name>(sarray(), Vector<Variant>());
@@ -1477,108 +1616,108 @@ static void _register_variant_builtin_methods() {
 
 	/* String */
 
-	bind_method(String, casecmp_to, sarray("to"), varray());
-	bind_method(String, nocasecmp_to, sarray("to"), varray());
-	bind_method(String, naturalnocasecmp_to, sarray("to"), varray());
-	bind_method(String, length, sarray(), varray());
-	bind_method(String, substr, sarray("from", "len"), varray(-1));
-	bind_method(String, get_slice, sarray("delimiter", "slice"), varray());
-	bind_method(String, get_slicec, sarray("delimiter", "slice"), varray());
-	bind_method(String, get_slice_count, sarray("delimiter"), varray());
-	bind_methodv(String, find, static_cast<int (String::*)(const String &, int) const>(&String::find), sarray("what", "from"), varray(0));
-	bind_method(String, count, sarray("what", "from", "to"), varray(0, 0));
-	bind_method(String, countn, sarray("what", "from", "to"), varray(0, 0));
-	bind_method(String, findn, sarray("what", "from"), varray(0));
-	bind_method(String, rfind, sarray("what", "from"), varray(-1));
-	bind_method(String, rfindn, sarray("what", "from"), varray(-1));
-	bind_method(String, match, sarray("expr"), varray());
-	bind_method(String, matchn, sarray("expr"), varray());
-	bind_methodv(String, begins_with, static_cast<bool (String::*)(const String &) const>(&String::begins_with), sarray("text"), varray());
-	bind_method(String, ends_with, sarray("text"), varray());
-	bind_method(String, is_subsequence_of, sarray("text"), varray());
-	bind_method(String, is_subsequence_ofn, sarray("text"), varray());
-	bind_method(String, bigrams, sarray(), varray());
-	bind_method(String, similarity, sarray("text"), varray());
-
-	bind_method(String, format, sarray("values", "placeholder"), varray("{_}"));
-	bind_methodv(String, replace, static_cast<String (String::*)(const String &, const String &) const>(&String::replace), sarray("what", "forwhat"), varray());
-	bind_method(String, replacen, sarray("what", "forwhat"), varray());
-	bind_method(String, repeat, sarray("count"), varray());
-	bind_method(String, insert, sarray("position", "what"), varray());
-	bind_method(String, capitalize, sarray(), varray());
-	bind_method(String, to_camel_case, sarray(), varray());
-	bind_method(String, to_pascal_case, sarray(), varray());
-	bind_method(String, to_snake_case, sarray(), varray());
-	bind_method(String, split, sarray("delimiter", "allow_empty", "maxsplit"), varray("", true, 0));
-	bind_method(String, rsplit, sarray("delimiter", "allow_empty", "maxsplit"), varray("", true, 0));
-	bind_method(String, split_floats, sarray("delimiter", "allow_empty"), varray(true));
-	bind_method(String, join, sarray("parts"), varray());
-
-	bind_method(String, to_upper, sarray(), varray());
-	bind_method(String, to_lower, sarray(), varray());
-
-	bind_method(String, left, sarray("length"), varray());
-	bind_method(String, right, sarray("length"), varray());
-
-	bind_method(String, strip_edges, sarray("left", "right"), varray(true, true));
-	bind_method(String, strip_escapes, sarray(), varray());
-	bind_method(String, lstrip, sarray("chars"), varray());
-	bind_method(String, rstrip, sarray("chars"), varray());
-	bind_method(String, get_extension, sarray(), varray());
-	bind_method(String, get_basename, sarray(), varray());
-	bind_method(String, path_join, sarray("file"), varray());
-	bind_method(String, unicode_at, sarray("at"), varray());
-	bind_method(String, indent, sarray("prefix"), varray());
-	bind_method(String, dedent, sarray(), varray());
+	bind_string_method(casecmp_to, sarray("to"), varray());
+	bind_string_method(nocasecmp_to, sarray("to"), varray());
+	bind_string_method(naturalnocasecmp_to, sarray("to"), varray());
+	bind_string_method(length, sarray(), varray());
+	bind_string_method(substr, sarray("from", "len"), varray(-1));
+	bind_string_method(get_slice, sarray("delimiter", "slice"), varray());
+	bind_string_method(get_slicec, sarray("delimiter", "slice"), varray());
+	bind_string_method(get_slice_count, sarray("delimiter"), varray());
+	bind_string_methodv(find, static_cast<int (String::*)(const String &, int) const>(&String::find), sarray("what", "from"), varray(0));
+	bind_string_method(count, sarray("what", "from", "to"), varray(0, 0));
+	bind_string_method(countn, sarray("what", "from", "to"), varray(0, 0));
+	bind_string_method(findn, sarray("what", "from"), varray(0));
+	bind_string_method(rfind, sarray("what", "from"), varray(-1));
+	bind_string_method(rfindn, sarray("what", "from"), varray(-1));
+	bind_string_method(match, sarray("expr"), varray());
+	bind_string_method(matchn, sarray("expr"), varray());
+	bind_string_methodv(begins_with, static_cast<bool (String::*)(const String &) const>(&String::begins_with), sarray("text"), varray());
+	bind_string_method(ends_with, sarray("text"), varray());
+	bind_string_method(is_subsequence_of, sarray("text"), varray());
+	bind_string_method(is_subsequence_ofn, sarray("text"), varray());
+	bind_string_method(bigrams, sarray(), varray());
+	bind_string_method(similarity, sarray("text"), varray());
+
+	bind_string_method(format, sarray("values", "placeholder"), varray("{_}"));
+	bind_string_methodv(replace, static_cast<String (String::*)(const String &, const String &) const>(&String::replace), sarray("what", "forwhat"), varray());
+	bind_string_method(replacen, sarray("what", "forwhat"), varray());
+	bind_string_method(repeat, sarray("count"), varray());
+	bind_string_method(insert, sarray("position", "what"), varray());
+	bind_string_method(capitalize, sarray(), varray());
+	bind_string_method(to_camel_case, sarray(), varray());
+	bind_string_method(to_pascal_case, sarray(), varray());
+	bind_string_method(to_snake_case, sarray(), varray());
+	bind_string_method(split, sarray("delimiter", "allow_empty", "maxsplit"), varray("", true, 0));
+	bind_string_method(rsplit, sarray("delimiter", "allow_empty", "maxsplit"), varray("", true, 0));
+	bind_string_method(split_floats, sarray("delimiter", "allow_empty"), varray(true));
+	bind_string_method(join, sarray("parts"), varray());
+
+	bind_string_method(to_upper, sarray(), varray());
+	bind_string_method(to_lower, sarray(), varray());
+
+	bind_string_method(left, sarray("length"), varray());
+	bind_string_method(right, sarray("length"), varray());
+
+	bind_string_method(strip_edges, sarray("left", "right"), varray(true, true));
+	bind_string_method(strip_escapes, sarray(), varray());
+	bind_string_method(lstrip, sarray("chars"), varray());
+	bind_string_method(rstrip, sarray("chars"), varray());
+	bind_string_method(get_extension, sarray(), varray());
+	bind_string_method(get_basename, sarray(), varray());
+	bind_string_method(path_join, sarray("file"), varray());
+	bind_string_method(unicode_at, sarray("at"), varray());
+	bind_string_method(indent, sarray("prefix"), varray());
+	bind_string_method(dedent, sarray(), varray());
 	bind_method(String, hash, sarray(), varray());
-	bind_method(String, md5_text, sarray(), varray());
-	bind_method(String, sha1_text, sarray(), varray());
-	bind_method(String, sha256_text, sarray(), varray());
-	bind_method(String, md5_buffer, sarray(), varray());
-	bind_method(String, sha1_buffer, sarray(), varray());
-	bind_method(String, sha256_buffer, sarray(), varray());
-	bind_method(String, is_empty, sarray(), varray());
-	bind_methodv(String, contains, static_cast<bool (String::*)(const String &) const>(&String::contains), sarray("what"), varray());
-
-	bind_method(String, is_absolute_path, sarray(), varray());
-	bind_method(String, is_relative_path, sarray(), varray());
-	bind_method(String, simplify_path, sarray(), varray());
-	bind_method(String, get_base_dir, sarray(), varray());
-	bind_method(String, get_file, sarray(), varray());
-	bind_method(String, xml_escape, sarray("escape_quotes"), varray(false));
-	bind_method(String, xml_unescape, sarray(), varray());
-	bind_method(String, uri_encode, sarray(), varray());
-	bind_method(String, uri_decode, sarray(), varray());
-	bind_method(String, c_escape, sarray(), varray());
-	bind_method(String, c_unescape, sarray(), varray());
-	bind_method(String, json_escape, sarray(), varray());
-
-	bind_method(String, validate_node_name, sarray(), varray());
-
-	bind_method(String, is_valid_identifier, sarray(), varray());
-	bind_method(String, is_valid_int, sarray(), varray());
-	bind_method(String, is_valid_float, sarray(), varray());
-	bind_method(String, is_valid_hex_number, sarray("with_prefix"), varray(false));
-	bind_method(String, is_valid_html_color, sarray(), varray());
-	bind_method(String, is_valid_ip_address, sarray(), varray());
-	bind_method(String, is_valid_filename, sarray(), varray());
-
-	bind_method(String, to_int, sarray(), varray());
-	bind_method(String, to_float, sarray(), varray());
-	bind_method(String, hex_to_int, sarray(), varray());
-	bind_method(String, bin_to_int, sarray(), varray());
-
-	bind_method(String, lpad, sarray("min_length", "character"), varray(" "));
-	bind_method(String, rpad, sarray("min_length", "character"), varray(" "));
-	bind_method(String, pad_decimals, sarray("digits"), varray());
-	bind_method(String, pad_zeros, sarray("digits"), varray());
-	bind_method(String, trim_prefix, sarray("prefix"), varray());
-	bind_method(String, trim_suffix, sarray("suffix"), varray());
-
-	bind_method(String, to_ascii_buffer, sarray(), varray());
-	bind_method(String, to_utf8_buffer, sarray(), varray());
-	bind_method(String, to_utf16_buffer, sarray(), varray());
-	bind_method(String, to_utf32_buffer, sarray(), varray());
+	bind_string_method(md5_text, sarray(), varray());
+	bind_string_method(sha1_text, sarray(), varray());
+	bind_string_method(sha256_text, sarray(), varray());
+	bind_string_method(md5_buffer, sarray(), varray());
+	bind_string_method(sha1_buffer, sarray(), varray());
+	bind_string_method(sha256_buffer, sarray(), varray());
+	bind_string_method(is_empty, sarray(), varray());
+	bind_string_methodv(contains, static_cast<bool (String::*)(const String &) const>(&String::contains), sarray("what"), varray());
+
+	bind_string_method(is_absolute_path, sarray(), varray());
+	bind_string_method(is_relative_path, sarray(), varray());
+	bind_string_method(simplify_path, sarray(), varray());
+	bind_string_method(get_base_dir, sarray(), varray());
+	bind_string_method(get_file, sarray(), varray());
+	bind_string_method(xml_escape, sarray("escape_quotes"), varray(false));
+	bind_string_method(xml_unescape, sarray(), varray());
+	bind_string_method(uri_encode, sarray(), varray());
+	bind_string_method(uri_decode, sarray(), varray());
+	bind_string_method(c_escape, sarray(), varray());
+	bind_string_method(c_unescape, sarray(), varray());
+	bind_string_method(json_escape, sarray(), varray());
+
+	bind_string_method(validate_node_name, sarray(), varray());
+
+	bind_string_method(is_valid_identifier, sarray(), varray());
+	bind_string_method(is_valid_int, sarray(), varray());
+	bind_string_method(is_valid_float, sarray(), varray());
+	bind_string_method(is_valid_hex_number, sarray("with_prefix"), varray(false));
+	bind_string_method(is_valid_html_color, sarray(), varray());
+	bind_string_method(is_valid_ip_address, sarray(), varray());
+	bind_string_method(is_valid_filename, sarray(), varray());
+
+	bind_string_method(to_int, sarray(), varray());
+	bind_string_method(to_float, sarray(), varray());
+	bind_string_method(hex_to_int, sarray(), varray());
+	bind_string_method(bin_to_int, sarray(), varray());
+
+	bind_string_method(lpad, sarray("min_length", "character"), varray(" "));
+	bind_string_method(rpad, sarray("min_length", "character"), varray(" "));
+	bind_string_method(pad_decimals, sarray("digits"), varray());
+	bind_string_method(pad_zeros, sarray("digits"), varray());
+	bind_string_method(trim_prefix, sarray("prefix"), varray());
+	bind_string_method(trim_suffix, sarray("suffix"), varray());
+
+	bind_string_method(to_ascii_buffer, sarray(), varray());
+	bind_string_method(to_utf8_buffer, sarray(), varray());
+	bind_string_method(to_utf16_buffer, sarray(), varray());
+	bind_string_method(to_utf32_buffer, sarray(), varray());
 
 	bind_static_method(String, num_scientific, sarray("number"), varray());
 	bind_static_method(String, num, sarray("number", "decimals"), varray(-1));

+ 62 - 55
core/variant/variant_op.cpp

@@ -229,6 +229,20 @@ public:
 	static Variant::Type get_return_type() { return GetTypeInfo<Vector4>::VARIANT_TYPE; }
 };
 
+#define register_string_op(m_op_type, m_op_code)                                                               \
+	do {                                                                                                       \
+		register_op<m_op_type<String, String>>(m_op_code, Variant::STRING, Variant::STRING);                   \
+		register_op<m_op_type<String, StringName>>(m_op_code, Variant::STRING, Variant::STRING_NAME);          \
+		register_op<m_op_type<StringName, String>>(m_op_code, Variant::STRING_NAME, Variant::STRING);          \
+		register_op<m_op_type<StringName, StringName>>(m_op_code, Variant::STRING_NAME, Variant::STRING_NAME); \
+	} while (false)
+
+#define register_string_modulo_op(m_class, m_type)                                                                         \
+	do {                                                                                                                   \
+		register_op<OperatorEvaluatorStringFormat<String, m_class>>(Variant::OP_MODULE, Variant::STRING, m_type);          \
+		register_op<OperatorEvaluatorStringFormat<StringName, m_class>>(Variant::OP_MODULE, Variant::STRING_NAME, m_type); \
+	} while (false)
+
 void Variant::_register_variant_operators() {
 	memset(operator_return_type_table, 0, sizeof(operator_return_type_table));
 	memset(operator_evaluator_table, 0, sizeof(operator_evaluator_table));
@@ -239,7 +253,7 @@ void Variant::_register_variant_operators() {
 	register_op<OperatorEvaluatorAdd<double, int64_t, double>>(Variant::OP_ADD, Variant::INT, Variant::FLOAT);
 	register_op<OperatorEvaluatorAdd<double, double, int64_t>>(Variant::OP_ADD, Variant::FLOAT, Variant::INT);
 	register_op<OperatorEvaluatorAdd<double, double, double>>(Variant::OP_ADD, Variant::FLOAT, Variant::FLOAT);
-	register_op<OperatorEvaluatorAdd<String, String, String>>(Variant::OP_ADD, Variant::STRING, Variant::STRING);
+	register_string_op(OperatorEvaluatorStringConcat, Variant::OP_ADD);
 	register_op<OperatorEvaluatorAdd<Vector2, Vector2, Vector2>>(Variant::OP_ADD, Variant::VECTOR2, Variant::VECTOR2);
 	register_op<OperatorEvaluatorAdd<Vector2i, Vector2i, Vector2i>>(Variant::OP_ADD, Variant::VECTOR2I, Variant::VECTOR2I);
 	register_op<OperatorEvaluatorAdd<Vector3, Vector3, Vector3>>(Variant::OP_ADD, Variant::VECTOR3, Variant::VECTOR3);
@@ -415,46 +429,46 @@ void Variant::_register_variant_operators() {
 	register_op<OperatorEvaluatorModNZ<Vector4i, Vector4i, Vector4i>>(Variant::OP_MODULE, Variant::VECTOR4I, Variant::VECTOR4I);
 	register_op<OperatorEvaluatorModNZ<Vector4i, Vector4i, int64_t>>(Variant::OP_MODULE, Variant::VECTOR4I, Variant::INT);
 
-	register_op<OperatorEvaluatorStringModNil>(Variant::OP_MODULE, Variant::STRING, Variant::NIL);
-
-	register_op<OperatorEvaluatorStringModT<bool>>(Variant::OP_MODULE, Variant::STRING, Variant::BOOL);
-	register_op<OperatorEvaluatorStringModT<int64_t>>(Variant::OP_MODULE, Variant::STRING, Variant::INT);
-	register_op<OperatorEvaluatorStringModT<double>>(Variant::OP_MODULE, Variant::STRING, Variant::FLOAT);
-	register_op<OperatorEvaluatorStringModT<String>>(Variant::OP_MODULE, Variant::STRING, Variant::STRING);
-	register_op<OperatorEvaluatorStringModT<Vector2>>(Variant::OP_MODULE, Variant::STRING, Variant::VECTOR2);
-	register_op<OperatorEvaluatorStringModT<Vector2i>>(Variant::OP_MODULE, Variant::STRING, Variant::VECTOR2I);
-	register_op<OperatorEvaluatorStringModT<Rect2>>(Variant::OP_MODULE, Variant::STRING, Variant::RECT2);
-	register_op<OperatorEvaluatorStringModT<Rect2i>>(Variant::OP_MODULE, Variant::STRING, Variant::RECT2I);
-	register_op<OperatorEvaluatorStringModT<Vector3>>(Variant::OP_MODULE, Variant::STRING, Variant::VECTOR3);
-	register_op<OperatorEvaluatorStringModT<Vector3i>>(Variant::OP_MODULE, Variant::STRING, Variant::VECTOR3I);
-	register_op<OperatorEvaluatorStringModT<Vector4>>(Variant::OP_MODULE, Variant::STRING, Variant::VECTOR4);
-	register_op<OperatorEvaluatorStringModT<Vector4i>>(Variant::OP_MODULE, Variant::STRING, Variant::VECTOR4I);
-	register_op<OperatorEvaluatorStringModT<Transform2D>>(Variant::OP_MODULE, Variant::STRING, Variant::TRANSFORM2D);
-	register_op<OperatorEvaluatorStringModT<Plane>>(Variant::OP_MODULE, Variant::STRING, Variant::PLANE);
-	register_op<OperatorEvaluatorStringModT<Quaternion>>(Variant::OP_MODULE, Variant::STRING, Variant::QUATERNION);
-	register_op<OperatorEvaluatorStringModT<::AABB>>(Variant::OP_MODULE, Variant::STRING, Variant::AABB);
-	register_op<OperatorEvaluatorStringModT<Basis>>(Variant::OP_MODULE, Variant::STRING, Variant::BASIS);
-	register_op<OperatorEvaluatorStringModT<Transform3D>>(Variant::OP_MODULE, Variant::STRING, Variant::TRANSFORM3D);
-	register_op<OperatorEvaluatorStringModT<Projection>>(Variant::OP_MODULE, Variant::STRING, Variant::PROJECTION);
-
-	register_op<OperatorEvaluatorStringModT<Color>>(Variant::OP_MODULE, Variant::STRING, Variant::COLOR);
-	register_op<OperatorEvaluatorStringModT<StringName>>(Variant::OP_MODULE, Variant::STRING, Variant::STRING_NAME);
-	register_op<OperatorEvaluatorStringModT<NodePath>>(Variant::OP_MODULE, Variant::STRING, Variant::NODE_PATH);
-	register_op<OperatorEvaluatorStringModObject>(Variant::OP_MODULE, Variant::STRING, Variant::OBJECT);
-	register_op<OperatorEvaluatorStringModT<Callable>>(Variant::OP_MODULE, Variant::STRING, Variant::CALLABLE);
-	register_op<OperatorEvaluatorStringModT<Signal>>(Variant::OP_MODULE, Variant::STRING, Variant::SIGNAL);
-	register_op<OperatorEvaluatorStringModT<Dictionary>>(Variant::OP_MODULE, Variant::STRING, Variant::DICTIONARY);
-	register_op<OperatorEvaluatorStringModArray>(Variant::OP_MODULE, Variant::STRING, Variant::ARRAY);
-
-	register_op<OperatorEvaluatorStringModT<PackedByteArray>>(Variant::OP_MODULE, Variant::STRING, Variant::PACKED_BYTE_ARRAY);
-	register_op<OperatorEvaluatorStringModT<PackedInt32Array>>(Variant::OP_MODULE, Variant::STRING, Variant::PACKED_INT32_ARRAY);
-	register_op<OperatorEvaluatorStringModT<PackedInt64Array>>(Variant::OP_MODULE, Variant::STRING, Variant::PACKED_INT64_ARRAY);
-	register_op<OperatorEvaluatorStringModT<PackedFloat32Array>>(Variant::OP_MODULE, Variant::STRING, Variant::PACKED_FLOAT32_ARRAY);
-	register_op<OperatorEvaluatorStringModT<PackedFloat64Array>>(Variant::OP_MODULE, Variant::STRING, Variant::PACKED_FLOAT64_ARRAY);
-	register_op<OperatorEvaluatorStringModT<PackedStringArray>>(Variant::OP_MODULE, Variant::STRING, Variant::PACKED_STRING_ARRAY);
-	register_op<OperatorEvaluatorStringModT<PackedVector2Array>>(Variant::OP_MODULE, Variant::STRING, Variant::PACKED_VECTOR2_ARRAY);
-	register_op<OperatorEvaluatorStringModT<PackedVector3Array>>(Variant::OP_MODULE, Variant::STRING, Variant::PACKED_VECTOR3_ARRAY);
-	register_op<OperatorEvaluatorStringModT<PackedColorArray>>(Variant::OP_MODULE, Variant::STRING, Variant::PACKED_COLOR_ARRAY);
+	register_string_modulo_op(void, Variant::NIL);
+
+	register_string_modulo_op(bool, Variant::BOOL);
+	register_string_modulo_op(int64_t, Variant::INT);
+	register_string_modulo_op(double, Variant::FLOAT);
+	register_string_modulo_op(String, Variant::STRING);
+	register_string_modulo_op(Vector2, Variant::VECTOR2);
+	register_string_modulo_op(Vector2i, Variant::VECTOR2I);
+	register_string_modulo_op(Rect2, Variant::RECT2);
+	register_string_modulo_op(Rect2i, Variant::RECT2I);
+	register_string_modulo_op(Vector3, Variant::VECTOR3);
+	register_string_modulo_op(Vector3i, Variant::VECTOR3I);
+	register_string_modulo_op(Vector4, Variant::VECTOR4);
+	register_string_modulo_op(Vector4i, Variant::VECTOR4I);
+	register_string_modulo_op(Transform2D, Variant::TRANSFORM2D);
+	register_string_modulo_op(Plane, Variant::PLANE);
+	register_string_modulo_op(Quaternion, Variant::QUATERNION);
+	register_string_modulo_op(::AABB, Variant::AABB);
+	register_string_modulo_op(Basis, Variant::BASIS);
+	register_string_modulo_op(Transform3D, Variant::TRANSFORM3D);
+	register_string_modulo_op(Projection, Variant::PROJECTION);
+
+	register_string_modulo_op(Color, Variant::COLOR);
+	register_string_modulo_op(StringName, Variant::STRING_NAME);
+	register_string_modulo_op(NodePath, Variant::NODE_PATH);
+	register_string_modulo_op(Object, Variant::OBJECT);
+	register_string_modulo_op(Callable, Variant::CALLABLE);
+	register_string_modulo_op(Signal, Variant::SIGNAL);
+	register_string_modulo_op(Dictionary, Variant::DICTIONARY);
+	register_string_modulo_op(Array, Variant::ARRAY);
+
+	register_string_modulo_op(PackedByteArray, Variant::PACKED_BYTE_ARRAY);
+	register_string_modulo_op(PackedInt32Array, Variant::PACKED_INT32_ARRAY);
+	register_string_modulo_op(PackedInt64Array, Variant::PACKED_INT64_ARRAY);
+	register_string_modulo_op(PackedFloat32Array, Variant::PACKED_FLOAT32_ARRAY);
+	register_string_modulo_op(PackedFloat64Array, Variant::PACKED_FLOAT64_ARRAY);
+	register_string_modulo_op(PackedStringArray, Variant::PACKED_STRING_ARRAY);
+	register_string_modulo_op(PackedVector2Array, Variant::PACKED_VECTOR2_ARRAY);
+	register_string_modulo_op(PackedVector3Array, Variant::PACKED_VECTOR3_ARRAY);
+	register_string_modulo_op(PackedColorArray, Variant::PACKED_COLOR_ARRAY);
 
 	register_op<OperatorEvaluatorPow<int64_t, int64_t, int64_t>>(Variant::OP_POWER, Variant::INT, Variant::INT);
 	register_op<OperatorEvaluatorPow<double, int64_t, double>>(Variant::OP_POWER, Variant::INT, Variant::FLOAT);
@@ -498,7 +512,7 @@ void Variant::_register_variant_operators() {
 	register_op<OperatorEvaluatorEqual<int64_t, double>>(Variant::OP_EQUAL, Variant::INT, Variant::FLOAT);
 	register_op<OperatorEvaluatorEqual<double, int64_t>>(Variant::OP_EQUAL, Variant::FLOAT, Variant::INT);
 	register_op<OperatorEvaluatorEqual<double, double>>(Variant::OP_EQUAL, Variant::FLOAT, Variant::FLOAT);
-	register_op<OperatorEvaluatorEqual<String, String>>(Variant::OP_EQUAL, Variant::STRING, Variant::STRING);
+	register_string_op(OperatorEvaluatorEqual, Variant::OP_EQUAL);
 	register_op<OperatorEvaluatorEqual<Vector2, Vector2>>(Variant::OP_EQUAL, Variant::VECTOR2, Variant::VECTOR2);
 	register_op<OperatorEvaluatorEqual<Vector2i, Vector2i>>(Variant::OP_EQUAL, Variant::VECTOR2I, Variant::VECTOR2I);
 	register_op<OperatorEvaluatorEqual<Rect2, Rect2>>(Variant::OP_EQUAL, Variant::RECT2, Variant::RECT2);
@@ -516,10 +530,6 @@ void Variant::_register_variant_operators() {
 	register_op<OperatorEvaluatorEqual<Projection, Projection>>(Variant::OP_EQUAL, Variant::PROJECTION, Variant::PROJECTION);
 	register_op<OperatorEvaluatorEqual<Color, Color>>(Variant::OP_EQUAL, Variant::COLOR, Variant::COLOR);
 
-	register_op<OperatorEvaluatorEqual<StringName, String>>(Variant::OP_EQUAL, Variant::STRING_NAME, Variant::STRING);
-	register_op<OperatorEvaluatorEqual<String, StringName>>(Variant::OP_EQUAL, Variant::STRING, Variant::STRING_NAME);
-	register_op<OperatorEvaluatorEqual<StringName, StringName>>(Variant::OP_EQUAL, Variant::STRING_NAME, Variant::STRING_NAME);
-
 	register_op<OperatorEvaluatorEqual<NodePath, NodePath>>(Variant::OP_EQUAL, Variant::NODE_PATH, Variant::NODE_PATH);
 	register_op<OperatorEvaluatorEqual<::RID, ::RID>>(Variant::OP_EQUAL, Variant::RID, Variant::RID);
 
@@ -621,7 +631,7 @@ void Variant::_register_variant_operators() {
 	register_op<OperatorEvaluatorNotEqual<int64_t, double>>(Variant::OP_NOT_EQUAL, Variant::INT, Variant::FLOAT);
 	register_op<OperatorEvaluatorNotEqual<double, int64_t>>(Variant::OP_NOT_EQUAL, Variant::FLOAT, Variant::INT);
 	register_op<OperatorEvaluatorNotEqual<double, double>>(Variant::OP_NOT_EQUAL, Variant::FLOAT, Variant::FLOAT);
-	register_op<OperatorEvaluatorNotEqual<String, String>>(Variant::OP_NOT_EQUAL, Variant::STRING, Variant::STRING);
+	register_string_op(OperatorEvaluatorNotEqual, Variant::OP_NOT_EQUAL);
 	register_op<OperatorEvaluatorNotEqual<Vector2, Vector2>>(Variant::OP_NOT_EQUAL, Variant::VECTOR2, Variant::VECTOR2);
 	register_op<OperatorEvaluatorNotEqual<Vector2i, Vector2i>>(Variant::OP_NOT_EQUAL, Variant::VECTOR2I, Variant::VECTOR2I);
 	register_op<OperatorEvaluatorNotEqual<Rect2, Rect2>>(Variant::OP_NOT_EQUAL, Variant::RECT2, Variant::RECT2);
@@ -639,10 +649,6 @@ void Variant::_register_variant_operators() {
 	register_op<OperatorEvaluatorNotEqual<Projection, Projection>>(Variant::OP_NOT_EQUAL, Variant::PROJECTION, Variant::PROJECTION);
 	register_op<OperatorEvaluatorNotEqual<Color, Color>>(Variant::OP_NOT_EQUAL, Variant::COLOR, Variant::COLOR);
 
-	register_op<OperatorEvaluatorNotEqual<StringName, String>>(Variant::OP_NOT_EQUAL, Variant::STRING_NAME, Variant::STRING);
-	register_op<OperatorEvaluatorNotEqual<String, StringName>>(Variant::OP_NOT_EQUAL, Variant::STRING, Variant::STRING_NAME);
-	register_op<OperatorEvaluatorNotEqual<StringName, StringName>>(Variant::OP_NOT_EQUAL, Variant::STRING_NAME, Variant::STRING_NAME);
-
 	register_op<OperatorEvaluatorNotEqual<NodePath, NodePath>>(Variant::OP_NOT_EQUAL, Variant::NODE_PATH, Variant::NODE_PATH);
 	register_op<OperatorEvaluatorNotEqual<::RID, ::RID>>(Variant::OP_NOT_EQUAL, Variant::RID, Variant::RID);
 
@@ -895,10 +901,7 @@ void Variant::_register_variant_operators() {
 	register_op<OperatorEvaluatorNotFloat>(Variant::OP_NOT, Variant::FLOAT, Variant::NIL);
 	register_op<OperatorEvaluatorNotObject>(Variant::OP_NOT, Variant::OBJECT, Variant::NIL);
 
-	register_op<OperatorEvaluatorInStringFind<String>>(Variant::OP_IN, Variant::STRING, Variant::STRING);
-	register_op<OperatorEvaluatorInStringFind<StringName>>(Variant::OP_IN, Variant::STRING_NAME, Variant::STRING);
-	register_op<OperatorEvaluatorInStringNameFind<String>>(Variant::OP_IN, Variant::STRING, Variant::STRING_NAME);
-	register_op<OperatorEvaluatorInStringNameFind<StringName>>(Variant::OP_IN, Variant::STRING_NAME, Variant::STRING_NAME);
+	register_string_op(OperatorEvaluatorInStringFind, Variant::OP_IN);
 
 	register_op<OperatorEvaluatorInDictionaryHasNil>(Variant::OP_IN, Variant::NIL, Variant::DICTIONARY);
 	register_op<OperatorEvaluatorInDictionaryHas<bool>>(Variant::OP_IN, Variant::BOOL, Variant::DICTIONARY);
@@ -996,6 +999,7 @@ void Variant::_register_variant_operators() {
 	register_op<OperatorEvaluatorInArrayFind<float, PackedFloat64Array>>(Variant::OP_IN, Variant::FLOAT, Variant::PACKED_FLOAT64_ARRAY);
 
 	register_op<OperatorEvaluatorInArrayFind<String, PackedStringArray>>(Variant::OP_IN, Variant::STRING, Variant::PACKED_STRING_ARRAY);
+	register_op<OperatorEvaluatorInArrayFind<StringName, PackedStringArray>>(Variant::OP_IN, Variant::STRING_NAME, Variant::PACKED_STRING_ARRAY);
 
 	register_op<OperatorEvaluatorInArrayFind<Vector2, PackedVector2Array>>(Variant::OP_IN, Variant::VECTOR2, Variant::PACKED_VECTOR2_ARRAY);
 	register_op<OperatorEvaluatorInArrayFind<Vector3, PackedVector3Array>>(Variant::OP_IN, Variant::VECTOR3, Variant::PACKED_VECTOR3_ARRAY);
@@ -1006,6 +1010,9 @@ void Variant::_register_variant_operators() {
 	register_op<OperatorEvaluatorObjectHasPropertyStringName>(Variant::OP_IN, Variant::STRING_NAME, Variant::OBJECT);
 }
 
+#undef register_string_op
+#undef register_string_modulo_op
+
 void Variant::_unregister_variant_operators() {
 }
 

+ 50 - 23
core/variant/variant_op.h

@@ -875,7 +875,33 @@ public:
 	static Variant::Type get_return_type() { return GetTypeInfo<Vector<T>>::VARIANT_TYPE; }
 };
 
-class OperatorEvaluatorStringModNil {
+template <class Left, class Right>
+class OperatorEvaluatorStringConcat {
+public:
+	static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) {
+		const String a(*VariantGetInternalPtr<Left>::get_ptr(&p_left));
+		const String b(*VariantGetInternalPtr<Right>::get_ptr(&p_right));
+		*r_ret = a + b;
+		r_valid = true;
+	}
+	static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) {
+		const String a(*VariantGetInternalPtr<Left>::get_ptr(left));
+		const String b(*VariantGetInternalPtr<Right>::get_ptr(right));
+		*VariantGetInternalPtr<String>::get_ptr(r_ret) = a + b;
+	}
+	static void ptr_evaluate(const void *left, const void *right, void *r_ret) {
+		const String a(PtrToArg<Left>::convert(left));
+		const String b(PtrToArg<Right>::convert(right));
+		PtrToArg<String>::encode(a + b, r_ret);
+	}
+	static Variant::Type get_return_type() { return Variant::STRING; }
+};
+
+template <class S, class T>
+class OperatorEvaluatorStringFormat;
+
+template <class S>
+class OperatorEvaluatorStringFormat<S, void> {
 public:
 	_FORCE_INLINE_ static String do_mod(const String &s, bool *r_valid) {
 		Array values;
@@ -888,22 +914,22 @@ public:
 		return a;
 	}
 	static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) {
-		const String &a = *VariantGetInternalPtr<String>::get_ptr(&p_left);
-		*r_ret = do_mod(a, &r_valid);
+		*r_ret = do_mod(*VariantGetInternalPtr<S>::get_ptr(&p_left), &r_valid);
 	}
 	static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) {
 		bool valid = true;
-		String result = do_mod(*VariantGetInternalPtr<String>::get_ptr(left), &valid);
+		String result = do_mod(*VariantGetInternalPtr<S>::get_ptr(left), &valid);
 		ERR_FAIL_COND_MSG(!valid, result);
 		*VariantGetInternalPtr<String>::get_ptr(r_ret) = result;
 	}
 	static void ptr_evaluate(const void *left, const void *right, void *r_ret) {
-		PtrToArg<String>::encode(do_mod(PtrToArg<String>::convert(left), nullptr), r_ret);
+		PtrToArg<String>::encode(do_mod(PtrToArg<S>::convert(left), nullptr), r_ret);
 	}
 	static Variant::Type get_return_type() { return Variant::STRING; }
 };
 
-class OperatorEvaluatorStringModArray {
+template <class S>
+class OperatorEvaluatorStringFormat<S, Array> {
 public:
 	_FORCE_INLINE_ static String do_mod(const String &s, const Array &p_values, bool *r_valid) {
 		String a = s.sprintf(p_values, r_valid);
@@ -913,22 +939,22 @@ public:
 		return a;
 	}
 	static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) {
-		const String &a = *VariantGetInternalPtr<String>::get_ptr(&p_left);
-		*r_ret = do_mod(a, *VariantGetInternalPtr<Array>::get_ptr(&p_right), &r_valid);
+		*r_ret = do_mod(*VariantGetInternalPtr<S>::get_ptr(&p_left), *VariantGetInternalPtr<Array>::get_ptr(&p_right), &r_valid);
 	}
 	static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) {
 		bool valid = true;
-		String result = do_mod(*VariantGetInternalPtr<String>::get_ptr(left), *VariantGetInternalPtr<Array>::get_ptr(right), &valid);
+		String result = do_mod(*VariantGetInternalPtr<S>::get_ptr(left), *VariantGetInternalPtr<Array>::get_ptr(right), &valid);
 		ERR_FAIL_COND_MSG(!valid, result);
 		*VariantGetInternalPtr<String>::get_ptr(r_ret) = result;
 	}
 	static void ptr_evaluate(const void *left, const void *right, void *r_ret) {
-		PtrToArg<String>::encode(do_mod(PtrToArg<String>::convert(left), PtrToArg<Array>::convert(right), nullptr), r_ret);
+		PtrToArg<String>::encode(do_mod(PtrToArg<S>::convert(left), PtrToArg<Array>::convert(right), nullptr), r_ret);
 	}
 	static Variant::Type get_return_type() { return Variant::STRING; }
 };
 
-class OperatorEvaluatorStringModObject {
+template <class S>
+class OperatorEvaluatorStringFormat<S, Object> {
 public:
 	_FORCE_INLINE_ static String do_mod(const String &s, const Object *p_object, bool *r_valid) {
 		Array values;
@@ -941,23 +967,22 @@ public:
 		return a;
 	}
 	static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) {
-		const String &a = *VariantGetInternalPtr<String>::get_ptr(&p_left);
-		*r_ret = do_mod(a, p_right.get_validated_object(), &r_valid);
+		*r_ret = do_mod(*VariantGetInternalPtr<S>::get_ptr(&p_left), p_right.get_validated_object(), &r_valid);
 	}
 	static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) {
 		bool valid = true;
-		String result = do_mod(*VariantGetInternalPtr<String>::get_ptr(left), right->get_validated_object(), &valid);
+		String result = do_mod(*VariantGetInternalPtr<S>::get_ptr(left), right->get_validated_object(), &valid);
 		ERR_FAIL_COND_MSG(!valid, result);
 		*VariantGetInternalPtr<String>::get_ptr(r_ret) = result;
 	}
 	static void ptr_evaluate(const void *left, const void *right, void *r_ret) {
-		PtrToArg<String>::encode(do_mod(PtrToArg<String>::convert(left), PtrToArg<Object *>::convert(right), nullptr), r_ret);
+		PtrToArg<String>::encode(do_mod(PtrToArg<S>::convert(left), PtrToArg<Object *>::convert(right), nullptr), r_ret);
 	}
 	static Variant::Type get_return_type() { return Variant::STRING; }
 };
 
-template <class T>
-class OperatorEvaluatorStringModT {
+template <class S, class T>
+class OperatorEvaluatorStringFormat {
 public:
 	_FORCE_INLINE_ static String do_mod(const String &s, const T &p_value, bool *r_valid) {
 		Array values;
@@ -969,17 +994,16 @@ public:
 		return a;
 	}
 	static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) {
-		const String &a = *VariantGetInternalPtr<String>::get_ptr(&p_left);
-		*r_ret = do_mod(a, *VariantGetInternalPtr<T>::get_ptr(&p_right), &r_valid);
+		*r_ret = do_mod(*VariantGetInternalPtr<S>::get_ptr(&p_left), *VariantGetInternalPtr<T>::get_ptr(&p_right), &r_valid);
 	}
 	static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) {
 		bool valid = true;
-		String result = do_mod(*VariantGetInternalPtr<String>::get_ptr(left), *VariantGetInternalPtr<T>::get_ptr(right), &valid);
+		String result = do_mod(*VariantGetInternalPtr<S>::get_ptr(left), *VariantGetInternalPtr<T>::get_ptr(right), &valid);
 		ERR_FAIL_COND_MSG(!valid, result);
 		*VariantGetInternalPtr<String>::get_ptr(r_ret) = result;
 	}
 	static void ptr_evaluate(const void *left, const void *right, void *r_ret) {
-		PtrToArg<String>::encode(do_mod(PtrToArg<String>::convert(left), PtrToArg<T>::convert(right), nullptr), r_ret);
+		PtrToArg<String>::encode(do_mod(PtrToArg<S>::convert(left), PtrToArg<T>::convert(right), nullptr), r_ret);
 	}
 	static Variant::Type get_return_type() { return Variant::STRING; }
 };
@@ -1288,8 +1312,11 @@ public:
 
 ////
 
+template <class Left, class Right>
+class OperatorEvaluatorInStringFind;
+
 template <class Left>
-class OperatorEvaluatorInStringFind {
+class OperatorEvaluatorInStringFind<Left, String> {
 public:
 	static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) {
 		const Left &str_a = *VariantGetInternalPtr<Left>::get_ptr(&p_left);
@@ -1310,7 +1337,7 @@ public:
 };
 
 template <class Left>
-class OperatorEvaluatorInStringNameFind {
+class OperatorEvaluatorInStringFind<Left, StringName> {
 public:
 	static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) {
 		const Left &str_a = *VariantGetInternalPtr<Left>::get_ptr(&p_left);

+ 6 - 0
doc/classes/String.xml

@@ -1062,6 +1062,12 @@
 				Appends [param right] at the end of this [String], also known as a string concatenation.
 			</description>
 		</operator>
+		<operator name="operator +">
+			<return type="String" />
+			<param index="0" name="right" type="StringName" />
+			<description>
+			</description>
+		</operator>
 		<operator name="operator &lt;">
 			<return type="bool" />
 			<param index="0" name="right" type="String" />

+ 821 - 0
doc/classes/StringName.xml

@@ -33,12 +33,815 @@
 		</constructor>
 	</constructors>
 	<methods>
+		<method name="begins_with" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="text" type="String" />
+			<description>
+				Returns [code]true[/code] if the string begins with the given string.
+			</description>
+		</method>
+		<method name="bigrams" qualifiers="const">
+			<return type="PackedStringArray" />
+			<description>
+				Returns an array containing the bigrams (pairs of consecutive letters) of this string.
+				[codeblock]
+				print("Bigrams".bigrams()) # Prints "[Bi, ig, gr, ra, am, ms]"
+				[/codeblock]
+			</description>
+		</method>
+		<method name="bin_to_int" qualifiers="const">
+			<return type="int" />
+			<description>
+				Converts a string containing a binary number into an integer. Binary strings can either be prefixed with [code]0b[/code] or not, and they can also start with a [code]-[/code] before the optional prefix.
+				[codeblocks]
+				[gdscript]
+				print("0b101".bin_to_int()) # Prints "5".
+				print("101".bin_to_int()) # Prints "5".
+				[/gdscript]
+				[csharp]
+				GD.Print("0b101".BinToInt()); // Prints "5".
+				GD.Print("101".BinToInt()); // Prints "5".
+				[/csharp]
+				[/codeblocks]
+			</description>
+		</method>
+		<method name="c_escape" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns a copy of the string with special characters escaped using the C language standard.
+			</description>
+		</method>
+		<method name="c_unescape" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns a copy of the string with escaped characters replaced by their meanings. Supported escape sequences are [code]\'[/code], [code]\"[/code], [code]\\[/code], [code]\a[/code], [code]\b[/code], [code]\f[/code], [code]\n[/code], [code]\r[/code], [code]\t[/code], [code]\v[/code].
+				[b]Note:[/b] Unlike the GDScript parser, this method doesn't support the [code]\uXXXX[/code] escape sequence.
+			</description>
+		</method>
+		<method name="capitalize" qualifiers="const">
+			<return type="String" />
+			<description>
+				Changes the case of some letters. Replaces underscores with spaces, adds spaces before in-word uppercase characters, converts all letters to lowercase, then capitalizes the first letter and every letter following a space character. For [code]capitalize camelCase mixed_with_underscores[/code], it will return [code]Capitalize Camel Case Mixed With Underscores[/code].
+			</description>
+		</method>
+		<method name="casecmp_to" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="to" type="String" />
+			<description>
+				Performs a case-sensitive comparison to another string. Returns [code]-1[/code] if less than, [code]1[/code] if greater than, or [code]0[/code] if equal. "less than" or "greater than" are determined by the [url=https://en.wikipedia.org/wiki/List_of_Unicode_characters]Unicode code points[/url] of each string, which roughly matches the alphabetical order.
+				[b]Behavior with different string lengths:[/b] Returns [code]1[/code] if the "base" string is longer than the [param to] string or [code]-1[/code] if the "base" string is shorter than the [param to] string. Keep in mind this length is determined by the number of Unicode codepoints, [i]not[/i] the actual visible characters.
+				[b]Behavior with empty strings:[/b] Returns [code]-1[/code] if the "base" string is empty, [code]1[/code] if the [param to] string is empty or [code]0[/code] if both strings are empty.
+				To get a boolean result from a string comparison, use the [code]==[/code] operator instead. See also [method nocasecmp_to] and [method naturalnocasecmp_to].
+			</description>
+		</method>
+		<method name="contains" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="what" type="String" />
+			<description>
+				Returns [code]true[/code] if the string contains the given string.
+			</description>
+		</method>
+		<method name="count" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="what" type="String" />
+			<param index="1" name="from" type="int" default="0" />
+			<param index="2" name="to" type="int" default="0" />
+			<description>
+				Returns the number of occurrences of substring [param what] between [param from] and [param to] positions. If [param from] and [param to] equals 0 the whole string will be used. If only [param to] equals 0 the remained substring will be used.
+			</description>
+		</method>
+		<method name="countn" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="what" type="String" />
+			<param index="1" name="from" type="int" default="0" />
+			<param index="2" name="to" type="int" default="0" />
+			<description>
+				Returns the number of occurrences of substring [param what] (ignoring case) between [param from] and [param to] positions. If [param from] and [param to] equals 0 the whole string will be used. If only [param to] equals 0 the remained substring will be used.
+			</description>
+		</method>
+		<method name="dedent" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns a copy of the string with indentation (leading tabs and spaces) removed. See also [method indent] to add indentation.
+			</description>
+		</method>
+		<method name="ends_with" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="text" type="String" />
+			<description>
+				Returns [code]true[/code] if the string ends with the given string.
+			</description>
+		</method>
+		<method name="find" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="what" type="String" />
+			<param index="1" name="from" type="int" default="0" />
+			<description>
+				Returns the index of the [b]first[/b] case-sensitive occurrence of the specified string in this instance, or [code]-1[/code]. Optionally, the starting search index can be specified, continuing to the end of the string.
+				[b]Note:[/b] If you just want to know whether a string contains a substring, use the [code]in[/code] operator as follows:
+				[codeblocks]
+				[gdscript]
+				print("i" in "team") # Will print `false`.
+				[/gdscript]
+				[csharp]
+				// C# has no in operator, but we can use `Contains()`.
+				GD.Print("team".Contains("i")); // Will print `false`.
+				[/csharp]
+				[/codeblocks]
+			</description>
+		</method>
+		<method name="findn" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="what" type="String" />
+			<param index="1" name="from" type="int" default="0" />
+			<description>
+				Returns the index of the [b]first[/b] case-insensitive occurrence of the specified string in this instance, or [code]-1[/code]. Optionally, the starting search index can be specified, continuing to the end of the string.
+			</description>
+		</method>
+		<method name="format" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="values" type="Variant" />
+			<param index="1" name="placeholder" type="String" default="&quot;{_}&quot;" />
+			<description>
+				Formats the string by replacing all occurrences of [param placeholder] with the elements of [param values].
+				[param values] can be a [Dictionary] or an [Array]. Any underscores in [param placeholder] will be replaced with the corresponding keys in advance. Array elements use their index as keys.
+				[codeblock]
+				# Prints: Waiting for Godot is a play by Samuel Beckett, and Godot Engine is named after it.
+				var use_array_values = "Waiting for {0} is a play by {1}, and {0} Engine is named after it."
+				print(use_array_values.format(["Godot", "Samuel Beckett"]))
+
+				# Prints: User 42 is Godot.
+				print("User {id} is {name}.".format({"id": 42, "name": "Godot"}))
+				[/codeblock]
+				Some additional handling is performed when [param values] is an array. If [param placeholder] does not contain an underscore, the elements of the array will be used to replace one occurrence of the placeholder in turn; If an array element is another 2-element array, it'll be interpreted as a key-value pair.
+				[codeblock]
+				# Prints: User 42 is Godot.
+				print("User {} is {}.".format([42, "Godot"], "{}"))
+				print("User {id} is {name}.".format([["id", 42], ["name", "Godot"]]))
+				[/codeblock]
+			</description>
+		</method>
+		<method name="get_base_dir" qualifiers="const">
+			<return type="String" />
+			<description>
+				If the string is a valid file path, returns the base directory name.
+			</description>
+		</method>
+		<method name="get_basename" qualifiers="const">
+			<return type="String" />
+			<description>
+				If the string is a valid file path, returns the full file path without the extension.
+			</description>
+		</method>
+		<method name="get_extension" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns the extension without the leading period character ([code].[/code]) if the string is a valid file name or path. If the string does not contain an extension, returns an empty string instead.
+				[codeblock]
+				print("/path/to/file.txt".get_extension())  # "txt"
+				print("file.txt".get_extension())  # "txt"
+				print("file.sample.txt".get_extension())  # "txt"
+				print(".txt".get_extension())  # "txt"
+				print("file.txt.".get_extension())  # "" (empty string)
+				print("file.txt..".get_extension())  # "" (empty string)
+				print("txt".get_extension())  # "" (empty string)
+				print("".get_extension())  # "" (empty string)
+				[/codeblock]
+			</description>
+		</method>
+		<method name="get_file" qualifiers="const">
+			<return type="String" />
+			<description>
+				If the string is a valid file path, returns the filename.
+			</description>
+		</method>
+		<method name="get_slice" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="delimiter" type="String" />
+			<param index="1" name="slice" type="int" />
+			<description>
+				Splits a string using a [param delimiter] and returns a substring at index [param slice]. Returns an empty string if the index doesn't exist.
+				This is a more performant alternative to [method split] for cases when you need only one element from the array at a fixed index.
+				[b]Example:[/b]
+				[codeblock]
+				print("i/am/example/string".get_slice("/", 2)) # Prints 'example'.
+				[/codeblock]
+			</description>
+		</method>
+		<method name="get_slice_count" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="delimiter" type="String" />
+			<description>
+				Splits a string using a [param delimiter] and returns a number of slices.
+			</description>
+		</method>
+		<method name="get_slicec" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="delimiter" type="int" />
+			<param index="1" name="slice" type="int" />
+			<description>
+				Splits a string using a Unicode character with code [param delimiter] and returns a substring at index [param slice]. Returns an empty string if the index doesn't exist.
+				This is a more performant alternative to [method split] for cases when you need only one element from the array at a fixed index.
+			</description>
+		</method>
 		<method name="hash" qualifiers="const">
 			<return type="int" />
 			<description>
 				Returns the 32-bit hash value representing the [StringName]'s contents.
 			</description>
 		</method>
+		<method name="hex_to_int" qualifiers="const">
+			<return type="int" />
+			<description>
+				Converts a string containing a hexadecimal number into an integer. Hexadecimal strings can either be prefixed with [code]0x[/code] or not, and they can also start with a [code]-[/code] before the optional prefix.
+				[codeblocks]
+				[gdscript]
+				print("0xff".hex_to_int()) # Prints "255".
+				print("ab".hex_to_int()) # Prints "171".
+				[/gdscript]
+				[csharp]
+				GD.Print("0xff".HexToInt()); // Prints "255".
+				GD.Print("ab".HexToInt()); // Prints "171".
+				[/csharp]
+				[/codeblocks]
+			</description>
+		</method>
+		<method name="indent" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="prefix" type="String" />
+			<description>
+				Returns a copy of the string with lines indented with [param prefix].
+				For example, the string can be indented with two tabs using [code]"\t\t"[/code], or four spaces using [code]"    "[/code]. The prefix can be any string so it can also be used to comment out strings with e.g. [code]"# "[/code]. See also [method dedent] to remove indentation.
+				[b]Note:[/b] Empty lines are kept empty.
+			</description>
+		</method>
+		<method name="insert" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="position" type="int" />
+			<param index="1" name="what" type="String" />
+			<description>
+				Returns a copy of the string with the substring [param what] inserted at the given [param position].
+			</description>
+		</method>
+		<method name="is_absolute_path" qualifiers="const">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if the string is a path to a file or directory and its starting point is explicitly defined. This includes [code]res://[/code], [code]user://[/code], [code]C:\[/code], [code]/[/code], etc.
+			</description>
+		</method>
+		<method name="is_empty" qualifiers="const">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if the length of the string equals [code]0[/code].
+			</description>
+		</method>
+		<method name="is_relative_path" qualifiers="const">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if the string is a path to a file or directory and its starting point is implicitly defined within the context it is being used. The starting point may refer to the current directory ([code]./[/code]), or the current [Node].
+			</description>
+		</method>
+		<method name="is_subsequence_of" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="text" type="String" />
+			<description>
+				Returns [code]true[/code] if this string is a subsequence of the given string.
+			</description>
+		</method>
+		<method name="is_subsequence_ofn" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="text" type="String" />
+			<description>
+				Returns [code]true[/code] if this string is a subsequence of the given string, without considering case.
+			</description>
+		</method>
+		<method name="is_valid_filename" qualifiers="const">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if this string is free from characters that aren't allowed in file names, those being:
+				[code]: / \ ? * " | % &lt; &gt;[/code]
+			</description>
+		</method>
+		<method name="is_valid_float" qualifiers="const">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if this string contains a valid float. This is inclusive of integers, and also supports exponents:
+				[codeblock]
+				print("1.7".is_valid_float()) # Prints "true"
+				print("24".is_valid_float()) # Prints "true"
+				print("7e3".is_valid_float()) # Prints "true"
+				print("Hello".is_valid_float()) # Prints "false"
+				[/codeblock]
+			</description>
+		</method>
+		<method name="is_valid_hex_number" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="with_prefix" type="bool" default="false" />
+			<description>
+				Returns [code]true[/code] if this string contains a valid hexadecimal number. If [param with_prefix] is [code]true[/code], then a validity of the hexadecimal number is determined by the [code]0x[/code] prefix, for example: [code]0xDEADC0DE[/code].
+			</description>
+		</method>
+		<method name="is_valid_html_color" qualifiers="const">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if this string contains a valid color in hexadecimal HTML notation. Other HTML notations such as named colors or [code]hsl()[/code] colors aren't considered valid by this method and will return [code]false[/code].
+			</description>
+		</method>
+		<method name="is_valid_identifier" qualifiers="const">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if this string is a valid identifier. A valid identifier may contain only letters, digits and underscores ([code]_[/code]) and the first character may not be a digit.
+				[codeblock]
+				print("good_ident_1".is_valid_identifier()) # Prints "true"
+				print("1st_bad_ident".is_valid_identifier()) # Prints "false"
+				print("bad_ident_#2".is_valid_identifier()) # Prints "false"
+				[/codeblock]
+			</description>
+		</method>
+		<method name="is_valid_int" qualifiers="const">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if this string contains a valid integer.
+				[codeblock]
+				print("7".is_valid_int()) # Prints "true"
+				print("14.6".is_valid_int()) # Prints "false"
+				print("L".is_valid_int()) # Prints "false"
+				print("+3".is_valid_int()) # Prints "true"
+				print("-12".is_valid_int()) # Prints "true"
+				[/codeblock]
+			</description>
+		</method>
+		<method name="is_valid_ip_address" qualifiers="const">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if this string contains only a well-formatted IPv4 or IPv6 address. This method considers [url=https://en.wikipedia.org/wiki/Reserved_IP_addresses]reserved IP addresses[/url] such as [code]0.0.0.0[/code] as valid.
+			</description>
+		</method>
+		<method name="join" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="parts" type="PackedStringArray" />
+			<description>
+				Returns a [String] which is the concatenation of the [param parts]. The separator between elements is the string providing this method.
+				[b]Example:[/b]
+				[codeblocks]
+				[gdscript]
+				print(", ".join(["One", "Two", "Three", "Four"]))
+				[/gdscript]
+				[csharp]
+				GD.Print(String.Join(",", new string[] {"One", "Two", "Three", "Four"}));
+				[/csharp]
+				[/codeblocks]
+			</description>
+		</method>
+		<method name="json_escape" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns a copy of the string with special characters escaped using the JSON standard.
+			</description>
+		</method>
+		<method name="left" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="length" type="int" />
+			<description>
+				Returns a number of characters from the left of the string. If negative [param length] is used, the characters are counted downwards from [String]'s length.
+				[b]Example:[/b]
+				[codeblock]
+				print("sample text".left(3)) #prints "sam"
+				print("sample text".left(-3)) #prints "sample t"
+				[/codeblock]
+			</description>
+		</method>
+		<method name="length" qualifiers="const">
+			<return type="int" />
+			<description>
+				Returns the number of characters in the string.
+			</description>
+		</method>
+		<method name="lpad" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="min_length" type="int" />
+			<param index="1" name="character" type="String" default="&quot; &quot;" />
+			<description>
+				Formats a string to be at least [param min_length] long by adding [param character]s to the left of the string.
+			</description>
+		</method>
+		<method name="lstrip" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="chars" type="String" />
+			<description>
+				Returns a copy of the string with characters removed from the left. The [param chars] argument is a string specifying the set of characters to be removed.
+				[b]Note:[/b] The [param chars] is not a prefix. See [method trim_prefix] method that will remove a single prefix string rather than a set of characters.
+			</description>
+		</method>
+		<method name="match" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="expr" type="String" />
+			<description>
+				Does a simple case-sensitive expression match, where [code]"*"[/code] matches zero or more arbitrary characters and [code]"?"[/code] matches any single character except a period ([code]"."[/code]). An empty string or empty expression always evaluates to [code]false[/code].
+			</description>
+		</method>
+		<method name="matchn" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="expr" type="String" />
+			<description>
+				Does a simple case-insensitive expression match, where [code]"*"[/code] matches zero or more arbitrary characters and [code]"?"[/code] matches any single character except a period ([code]"."[/code]). An empty string or empty expression always evaluates to [code]false[/code].
+			</description>
+		</method>
+		<method name="md5_buffer" qualifiers="const">
+			<return type="PackedByteArray" />
+			<description>
+				Returns the MD5 hash of the string as an array of bytes.
+			</description>
+		</method>
+		<method name="md5_text" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns the MD5 hash of the string as a string.
+			</description>
+		</method>
+		<method name="naturalnocasecmp_to" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="to" type="String" />
+			<description>
+				Performs a case-insensitive [i]natural order[/i] comparison to another string. Returns [code]-1[/code] if less than, [code]1[/code] if greater than, or [code]0[/code] if equal. "less than" or "greater than" are determined by the [url=https://en.wikipedia.org/wiki/List_of_Unicode_characters]Unicode code points[/url] of each string, which roughly matches the alphabetical order. Internally, lowercase characters will be converted to uppercase during the comparison.
+				When used for sorting, natural order comparison will order suites of numbers as expected by most people. If you sort the numbers from 1 to 10 using natural order, you will get [code][1, 2, 3, ...][/code] instead of [code][1, 10, 2, 3, ...][/code].
+				[b]Behavior with different string lengths:[/b] Returns [code]1[/code] if the "base" string is longer than the [param to] string or [code]-1[/code] if the "base" string is shorter than the [param to] string. Keep in mind this length is determined by the number of Unicode codepoints, [i]not[/i] the actual visible characters.
+				[b]Behavior with empty strings:[/b] Returns [code]-1[/code] if the "base" string is empty, [code]1[/code] if the [param to] string is empty or [code]0[/code] if both strings are empty.
+				To get a boolean result from a string comparison, use the [code]==[/code] operator instead. See also [method nocasecmp_to] and [method casecmp_to].
+			</description>
+		</method>
+		<method name="nocasecmp_to" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="to" type="String" />
+			<description>
+				Performs a case-insensitive comparison to another string. Returns [code]-1[/code] if less than, [code]1[/code] if greater than, or [code]0[/code] if equal. "less than" or "greater than" are determined by the [url=https://en.wikipedia.org/wiki/List_of_Unicode_characters]Unicode code points[/url] of each string, which roughly matches the alphabetical order. Internally, lowercase characters will be converted to uppercase during the comparison.
+				[b]Behavior with different string lengths:[/b] Returns [code]1[/code] if the "base" string is longer than the [param to] string or [code]-1[/code] if the "base" string is shorter than the [param to] string. Keep in mind this length is determined by the number of Unicode codepoints, [i]not[/i] the actual visible characters.
+				[b]Behavior with empty strings:[/b] Returns [code]-1[/code] if the "base" string is empty, [code]1[/code] if the [param to] string is empty or [code]0[/code] if both strings are empty.
+				To get a boolean result from a string comparison, use the [code]==[/code] operator instead. See also [method casecmp_to] and [method naturalnocasecmp_to].
+			</description>
+		</method>
+		<method name="pad_decimals" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="digits" type="int" />
+			<description>
+				Formats a number to have an exact number of [param digits] after the decimal point.
+			</description>
+		</method>
+		<method name="pad_zeros" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="digits" type="int" />
+			<description>
+				Formats a number to have an exact number of [param digits] before the decimal point.
+			</description>
+		</method>
+		<method name="path_join" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="file" type="String" />
+			<description>
+				If the string is a path, this concatenates [param file] at the end of the string as a subpath. E.g. [code]"this/is".path_join("path") == "this/is/path"[/code].
+			</description>
+		</method>
+		<method name="repeat" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="count" type="int" />
+			<description>
+				Returns original string repeated a number of times. The number of repetitions is given by the argument.
+			</description>
+		</method>
+		<method name="replace" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="what" type="String" />
+			<param index="1" name="forwhat" type="String" />
+			<description>
+				Replaces occurrences of a case-sensitive substring with the given one inside the string.
+			</description>
+		</method>
+		<method name="replacen" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="what" type="String" />
+			<param index="1" name="forwhat" type="String" />
+			<description>
+				Replaces occurrences of a case-insensitive substring with the given one inside the string.
+			</description>
+		</method>
+		<method name="rfind" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="what" type="String" />
+			<param index="1" name="from" type="int" default="-1" />
+			<description>
+				Returns the index of the [b]last[/b] case-sensitive occurrence of the specified string in this instance, or [code]-1[/code]. Optionally, the starting search index can be specified, continuing to the beginning of the string.
+			</description>
+		</method>
+		<method name="rfindn" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="what" type="String" />
+			<param index="1" name="from" type="int" default="-1" />
+			<description>
+				Returns the index of the [b]last[/b] case-insensitive occurrence of the specified string in this instance, or [code]-1[/code]. Optionally, the starting search index can be specified, continuing to the beginning of the string.
+			</description>
+		</method>
+		<method name="right" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="length" type="int" />
+			<description>
+				Returns a number of characters from the right of the string. If negative [param length] is used, the characters are counted downwards from [String]'s length.
+				[b]Example:[/b]
+				[codeblock]
+				print("sample text".right(3)) #prints "ext"
+				print("sample text".right(-3)) #prints "ple text"
+				[/codeblock]
+			</description>
+		</method>
+		<method name="rpad" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="min_length" type="int" />
+			<param index="1" name="character" type="String" default="&quot; &quot;" />
+			<description>
+				Formats a string to be at least [param min_length] long by adding [param character]s to the right of the string.
+			</description>
+		</method>
+		<method name="rsplit" qualifiers="const">
+			<return type="PackedStringArray" />
+			<param index="0" name="delimiter" type="String" default="&quot;&quot;" />
+			<param index="1" name="allow_empty" type="bool" default="true" />
+			<param index="2" name="maxsplit" type="int" default="0" />
+			<description>
+				Splits the string by a [param delimiter] string and returns an array of the substrings, starting from right. If [param delimiter] is an empty string, each substring will be a single character.
+				The splits in the returned array are sorted in the same order as the original string, from left to right.
+				If [param allow_empty] is [code]true[/code], and there are two adjacent delimiters in the string, it will add an empty string to the array of substrings at this position.
+				If [param maxsplit] is specified, it defines the number of splits to do from the right up to [param maxsplit]. The default value of 0 means that all items are split, thus giving the same result as [method split].
+				[b]Example:[/b]
+				[codeblocks]
+				[gdscript]
+				var some_string = "One,Two,Three,Four"
+				var some_array = some_string.rsplit(",", true, 1)
+				print(some_array.size()) # Prints 2
+				print(some_array[0]) # Prints "One,Two,Three"
+				print(some_array[1]) # Prints "Four"
+				[/gdscript]
+				[csharp]
+				// There is no Rsplit.
+				[/csharp]
+				[/codeblocks]
+			</description>
+		</method>
+		<method name="rstrip" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="chars" type="String" />
+			<description>
+				Returns a copy of the string with characters removed from the right. The [param chars] argument is a string specifying the set of characters to be removed.
+				[b]Note:[/b] The [param chars] is not a suffix. See [method trim_suffix] method that will remove a single suffix string rather than a set of characters.
+			</description>
+		</method>
+		<method name="sha1_buffer" qualifiers="const">
+			<return type="PackedByteArray" />
+			<description>
+				Returns the SHA-1 hash of the string as an array of bytes.
+			</description>
+		</method>
+		<method name="sha1_text" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns the SHA-1 hash of the string as a string.
+			</description>
+		</method>
+		<method name="sha256_buffer" qualifiers="const">
+			<return type="PackedByteArray" />
+			<description>
+				Returns the SHA-256 hash of the string as an array of bytes.
+			</description>
+		</method>
+		<method name="sha256_text" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns the SHA-256 hash of the string as a string.
+			</description>
+		</method>
+		<method name="similarity" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="text" type="String" />
+			<description>
+				Returns the similarity index ([url=https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient]Sorensen-Dice coefficient[/url]) of this string compared to another. A result of 1.0 means totally similar, while 0.0 means totally dissimilar.
+				[codeblock]
+				print("ABC123".similarity("ABC123")) # Prints "1"
+				print("ABC123".similarity("XYZ456")) # Prints "0"
+				print("ABC123".similarity("123ABC")) # Prints "0.8"
+				print("ABC123".similarity("abc123")) # Prints "0.4"
+				[/codeblock]
+			</description>
+		</method>
+		<method name="simplify_path" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns a simplified canonical path.
+			</description>
+		</method>
+		<method name="split" qualifiers="const">
+			<return type="PackedStringArray" />
+			<param index="0" name="delimiter" type="String" default="&quot;&quot;" />
+			<param index="1" name="allow_empty" type="bool" default="true" />
+			<param index="2" name="maxsplit" type="int" default="0" />
+			<description>
+				Splits the string by a [param delimiter] string and returns an array of the substrings. The [param delimiter] can be of any length. If [param delimiter] is an empty string, each substring will be a single character.
+				If [param allow_empty] is [code]true[/code], and there are two adjacent delimiters in the string, it will add an empty string to the array of substrings at this position.
+				If [param maxsplit] is specified, it defines the number of splits to do from the left up to [param maxsplit]. The default value of [code]0[/code] means that all items are split.
+				If you need only one element from the array at a specific index, [method get_slice] is a more performant option.
+				[b]Example:[/b]
+				[codeblocks]
+				[gdscript]
+				var some_string = "One,Two,Three,Four"
+				var some_array = some_string.split(",", true, 1)
+				print(some_array.size()) # Prints 2
+				print(some_array[0]) # Prints "Four"
+				print(some_array[1]) # Prints "Three,Two,One"
+				[/gdscript]
+				[csharp]
+				var someString = "One,Two,Three,Four";
+				var someArray = someString.Split(",", true); // This is as close as it gets to Godots API.
+				GD.Print(someArray[0]); // Prints "Four"
+				GD.Print(someArray[1]); // Prints "Three,Two,One"
+				[/csharp]
+				[/codeblocks]
+				If you need to split strings with more complex rules, use the [RegEx] class instead.
+			</description>
+		</method>
+		<method name="split_floats" qualifiers="const">
+			<return type="PackedFloat64Array" />
+			<param index="0" name="delimiter" type="String" />
+			<param index="1" name="allow_empty" type="bool" default="true" />
+			<description>
+				Splits the string in floats by using a delimiter string and returns an array of the substrings.
+				For example, [code]"1,2.5,3"[/code] will return [code][1,2.5,3][/code] if split by [code]","[/code].
+				If [param allow_empty] is [code]true[/code], and there are two adjacent delimiters in the string, it will add an empty string to the array of substrings at this position.
+			</description>
+		</method>
+		<method name="strip_edges" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="left" type="bool" default="true" />
+			<param index="1" name="right" type="bool" default="true" />
+			<description>
+				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.
+			</description>
+		</method>
+		<method name="strip_escapes" qualifiers="const">
+			<return type="String" />
+			<description>
+				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 (&lt; 32), such as tabulation ([code]\t[/code] in C) and newline ([code]\n[/code] and [code]\r[/code]) characters, but not spaces.
+			</description>
+		</method>
+		<method name="substr" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="from" type="int" />
+			<param index="1" name="len" type="int" default="-1" />
+			<description>
+				Returns part of the string from the position [param from] with length [param len]. Argument [param len] is optional and using [code]-1[/code] will return remaining characters from given position.
+			</description>
+		</method>
+		<method name="to_ascii_buffer" qualifiers="const">
+			<return type="PackedByteArray" />
+			<description>
+				Converts the String (which is a character array) to ASCII/Latin-1 encoded [PackedByteArray] (which is an array of bytes). The conversion is faster compared to [method to_utf8_buffer], as this method assumes that all the characters in the String are ASCII/Latin-1 characters, unsupported characters are replaced with spaces.
+			</description>
+		</method>
+		<method name="to_camel_case" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns the string converted to [code]camelCase[/code].
+			</description>
+		</method>
+		<method name="to_float" qualifiers="const">
+			<return type="float" />
+			<description>
+				Converts a string containing a decimal number into a [code]float[/code]. The method will stop on the first non-number character except the first [code].[/code] (decimal point), and [code]e[/code] which is used for exponential.
+				[codeblock]
+				print("12.3".to_float()) # 12.3
+				print("1.2.3".to_float()) # 1.2
+				print("12ab3".to_float()) # 12
+				print("1e3".to_float()) # 1000
+				[/codeblock]
+			</description>
+		</method>
+		<method name="to_int" qualifiers="const">
+			<return type="int" />
+			<description>
+				Converts a string containing an integer number into an [code]int[/code]. The method will remove any non-number character and stop if it encounters a [code].[/code].
+				[codeblock]
+				print("123".to_int()) # 123
+				print("a1b2c3".to_int()) # 123
+				print("1.2.3".to_int()) # 1
+				[/codeblock]
+			</description>
+		</method>
+		<method name="to_lower" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns the string converted to lowercase.
+			</description>
+		</method>
+		<method name="to_pascal_case" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns the string converted to [code]PascalCase[/code].
+			</description>
+		</method>
+		<method name="to_snake_case" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns the string converted to [code]snake_case[/code].
+			</description>
+		</method>
+		<method name="to_upper" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns the string converted to uppercase.
+			</description>
+		</method>
+		<method name="to_utf16_buffer" qualifiers="const">
+			<return type="PackedByteArray" />
+			<description>
+				Converts the String (which is an array of characters) to UTF-16 encoded [PackedByteArray] (which is an array of bytes).
+			</description>
+		</method>
+		<method name="to_utf32_buffer" qualifiers="const">
+			<return type="PackedByteArray" />
+			<description>
+				Converts the String (which is an array of characters) to UTF-32 encoded [PackedByteArray] (which is an array of bytes).
+			</description>
+		</method>
+		<method name="to_utf8_buffer" qualifiers="const">
+			<return type="PackedByteArray" />
+			<description>
+				Converts the String (which is an array of characters) to UTF-8 encode [PackedByteArray] (which is an array of bytes). The conversion is a bit slower than [method to_ascii_buffer], but supports all UTF-8 characters. Therefore, you should prefer this function over [method to_ascii_buffer].
+			</description>
+		</method>
+		<method name="trim_prefix" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="prefix" type="String" />
+			<description>
+				Removes a given string from the start if it starts with it or leaves the string unchanged.
+			</description>
+		</method>
+		<method name="trim_suffix" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="suffix" type="String" />
+			<description>
+				Removes a given string from the end if it ends with it or leaves the string unchanged.
+			</description>
+		</method>
+		<method name="unicode_at" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="at" type="int" />
+			<description>
+				Returns the character code at position [param at].
+			</description>
+		</method>
+		<method name="uri_decode" qualifiers="const">
+			<return type="String" />
+			<description>
+				Decodes a string in URL encoded format. This is meant to decode parameters in a URL when receiving an HTTP request.
+				[codeblocks]
+				[gdscript]
+				print("https://example.org/?escaped=" + "Godot%20Engine%3A%27docs%27".uri_decode())
+				[/gdscript]
+				[csharp]
+				GD.Print("https://example.org/?escaped=" + "Godot%20Engine%3a%27Docs%27".URIDecode());
+				[/csharp]
+				[/codeblocks]
+			</description>
+		</method>
+		<method name="uri_encode" qualifiers="const">
+			<return type="String" />
+			<description>
+				Encodes a string to URL friendly format. This is meant to encode parameters in a URL when sending an HTTP request.
+				[codeblocks]
+				[gdscript]
+				print("https://example.org/?escaped=" + "Godot Engine:'docs'".uri_encode())
+				[/gdscript]
+				[csharp]
+				GD.Print("https://example.org/?escaped=" + "Godot Engine:'docs'".URIEncode());
+				[/csharp]
+				[/codeblocks]
+			</description>
+		</method>
+		<method name="validate_node_name" qualifiers="const">
+			<return type="String" />
+			<description>
+				Removes any characters from the string that are prohibited in [Node] names ([code].[/code] [code]:[/code] [code]@[/code] [code]/[/code] [code]"[/code]).
+			</description>
+		</method>
+		<method name="xml_escape" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="escape_quotes" type="bool" default="false" />
+			<description>
+				Returns a copy of the string with special characters escaped using the XML standard. If [param escape_quotes] is [code]true[/code], the single quote ([code]'[/code]) and double quote ([code]"[/code]) characters are also escaped.
+			</description>
+		</method>
+		<method name="xml_unescape" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns a copy of the string with escaped characters replaced by their meanings according to the XML standard.
+			</description>
+		</method>
 	</methods>
 	<operators>
 		<operator name="operator !=">
@@ -55,6 +858,24 @@
 				Returns [code]true[/code] if the [StringName] and [param right] do not refer to the same name. Comparisons between [StringName]s are much faster than regular [String] comparisons.
 			</description>
 		</operator>
+		<operator name="operator %">
+			<return type="String" />
+			<param index="0" name="right" type="Variant" />
+			<description>
+			</description>
+		</operator>
+		<operator name="operator +">
+			<return type="String" />
+			<param index="0" name="right" type="String" />
+			<description>
+			</description>
+		</operator>
+		<operator name="operator +">
+			<return type="String" />
+			<param index="0" name="right" type="StringName" />
+			<description>
+			</description>
+		</operator>
 		<operator name="operator &lt;">
 			<return type="bool" />
 			<param index="0" name="right" type="StringName" />

+ 2 - 2
editor/doc_tools.cpp

@@ -701,7 +701,7 @@ void DocTools::generate(bool p_basic_types) {
 				if (rt != Variant::NIL) { // Has operator.
 					// Skip String % operator as it's registered separately for each Variant arg type,
 					// we'll add it manually below.
-					if (i == Variant::STRING && Variant::Operator(j) == Variant::OP_MODULE) {
+					if ((i == Variant::STRING || i == Variant::STRING_NAME) && Variant::Operator(j) == Variant::OP_MODULE) {
 						continue;
 					}
 					MethodInfo mi;
@@ -718,7 +718,7 @@ void DocTools::generate(bool p_basic_types) {
 			}
 		}
 
-		if (i == Variant::STRING) {
+		if (i == Variant::STRING || i == Variant::STRING_NAME) {
 			// We skipped % operator above, and we register it manually once for Variant arg type here.
 			MethodInfo mi;
 			mi.name = "operator %";

+ 6 - 4
modules/gdscript/gdscript_analyzer.cpp

@@ -2676,7 +2676,7 @@ void GDScriptAnalyzer::reduce_cast(GDScriptParser::CastNode *p_cast) {
 }
 
 void GDScriptAnalyzer::reduce_dictionary(GDScriptParser::DictionaryNode *p_dictionary) {
-	HashMap<Variant, GDScriptParser::ExpressionNode *, VariantHasher, VariantComparator> elements;
+	HashMap<Variant, GDScriptParser::ExpressionNode *, VariantHasher, StringLikeVariantComparator> elements;
 
 	for (int i = 0; i < p_dictionary->elements.size(); i++) {
 		const GDScriptParser::DictionaryNode::Pair &element = p_dictionary->elements[i];
@@ -3432,7 +3432,7 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri
 							case Variant::QUATERNION:
 							case Variant::AABB:
 							case Variant::OBJECT:
-								error = index_type.builtin_type != Variant::STRING;
+								error = index_type.builtin_type != Variant::STRING && index_type.builtin_type != Variant::STRING_NAME;
 								break;
 							// Expect String or number.
 							case Variant::BASIS:
@@ -3446,11 +3446,11 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri
 							case Variant::TRANSFORM3D:
 							case Variant::PROJECTION:
 								error = index_type.builtin_type != Variant::INT && index_type.builtin_type != Variant::FLOAT &&
-										index_type.builtin_type != Variant::STRING;
+										index_type.builtin_type != Variant::STRING && index_type.builtin_type != Variant::STRING_NAME;
 								break;
 							// Expect String or int.
 							case Variant::COLOR:
-								error = index_type.builtin_type != Variant::INT && index_type.builtin_type != Variant::STRING;
+								error = index_type.builtin_type != Variant::INT && index_type.builtin_type != Variant::STRING && index_type.builtin_type != Variant::STRING_NAME;
 								break;
 							// Don't support indexing, but we will check it later.
 							case Variant::RID:
@@ -4164,6 +4164,8 @@ bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_targ
 
 	if (p_target.kind == GDScriptParser::DataType::BUILTIN) {
 		bool valid = p_source.kind == GDScriptParser::DataType::BUILTIN && p_target.builtin_type == p_source.builtin_type;
+		valid |= p_source.builtin_type == Variant::STRING && p_target.builtin_type == Variant::STRING_NAME;
+		valid |= p_source.builtin_type == Variant::STRING_NAME && p_target.builtin_type == Variant::STRING;
 		if (!valid && p_allow_implicit_conversion) {
 			valid = Variant::can_convert_strict(p_source.builtin_type, p_target.builtin_type);
 		}

+ 21 - 0
modules/gdscript/gdscript_compiler.cpp

@@ -1256,9 +1256,30 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_match_pattern(CodeGen &c
 			equality_type.kind = GDScriptDataType::BUILTIN;
 			equality_type.builtin_type = Variant::BOOL;
 
+			GDScriptCodeGenerator::Address type_string_addr = codegen.add_constant(Variant::STRING);
+			GDScriptCodeGenerator::Address type_string_name_addr = codegen.add_constant(Variant::STRING_NAME);
+
 			// Check type equality.
 			GDScriptCodeGenerator::Address type_equality_addr = codegen.add_temporary(equality_type);
 			codegen.generator->write_binary_operator(type_equality_addr, Variant::OP_EQUAL, p_type_addr, literal_type_addr);
+
+			// Check if StringName <-> String comparison is possible.
+			GDScriptCodeGenerator::Address type_comp_addr_1 = codegen.add_temporary(equality_type);
+			GDScriptCodeGenerator::Address type_comp_addr_2 = codegen.add_temporary(equality_type);
+
+			codegen.generator->write_binary_operator(type_comp_addr_1, Variant::OP_EQUAL, p_type_addr, type_string_addr);
+			codegen.generator->write_binary_operator(type_comp_addr_2, Variant::OP_EQUAL, literal_type_addr, type_string_name_addr);
+			codegen.generator->write_binary_operator(type_comp_addr_1, Variant::OP_AND, type_comp_addr_1, type_comp_addr_2);
+			codegen.generator->write_binary_operator(type_equality_addr, Variant::OP_OR, type_equality_addr, type_comp_addr_1);
+
+			codegen.generator->write_binary_operator(type_comp_addr_1, Variant::OP_EQUAL, p_type_addr, type_string_name_addr);
+			codegen.generator->write_binary_operator(type_comp_addr_2, Variant::OP_EQUAL, literal_type_addr, type_string_addr);
+			codegen.generator->write_binary_operator(type_comp_addr_1, Variant::OP_AND, type_comp_addr_1, type_comp_addr_2);
+			codegen.generator->write_binary_operator(type_equality_addr, Variant::OP_OR, type_equality_addr, type_comp_addr_1);
+
+			codegen.generator->pop_temporary(); // Remove type_comp_addr_2 from stack.
+			codegen.generator->pop_temporary(); // Remove type_comp_addr_1 from stack.
+
 			codegen.generator->write_and_left_operand(type_equality_addr);
 
 			// Get literal.

+ 9 - 0
modules/gdscript/tests/scripts/analyzer/errors/dictionary_string_stringname_equivalent.gd

@@ -0,0 +1,9 @@
+# https://github.com/godotengine/godot/issues/62957
+
+func test():
+	var dict = {
+		&"key": "StringName",
+		"key": "String"
+	}
+
+	print("Invalid dictionary: %s" % dict)

+ 2 - 0
modules/gdscript/tests/scripts/analyzer/errors/dictionary_string_stringname_equivalent.out

@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Key "key" was already used in this dictionary (at line 5).

+ 8 - 0
modules/gdscript/tests/scripts/analyzer/features/array_string_stringname_equivalent.gd

@@ -0,0 +1,8 @@
+func test():
+	# Converted to String when initialized
+	var string_array: Array[String] = [&"abc"]
+	print(string_array)
+
+	# Converted to StringName when initialized
+	var stringname_array: Array[StringName] = ["abc"]
+	print(stringname_array)

+ 3 - 0
modules/gdscript/tests/scripts/analyzer/features/array_string_stringname_equivalent.out

@@ -0,0 +1,3 @@
+GDTEST_OK
+["abc"]
+[&"abc"]

+ 35 - 0
modules/gdscript/tests/scripts/runtime/features/array_string_stringname_equivalent.gd

@@ -0,0 +1,35 @@
+# https://github.com/godotengine/godot/issues/63965
+
+func test():
+	var array_str: Array = []
+	array_str.push_back("godot")
+	print("StringName in Array: ", &"godot" in array_str)
+
+	var array_sname: Array = []
+	array_sname.push_back(&"godot")
+	print("String in Array: ", "godot" in array_sname)
+
+  # Not equal because the values are different types.
+	print("Arrays not equal: ", array_str != array_sname)
+
+	var string_array: Array[String] = []
+	var stringname_array: Array[StringName] = []
+
+	assert(!string_array.push_back(&"abc"))
+	print("Array[String] insert converted: ", typeof(string_array[0]) == TYPE_STRING)
+
+	assert(!stringname_array.push_back("abc"))
+	print("Array[StringName] insert converted: ", typeof(stringname_array[0]) == TYPE_STRING_NAME)
+
+	print("StringName in Array[String]: ", &"abc" in string_array)
+	print("String in Array[StringName]: ", "abc" in stringname_array)
+
+	var packed_string_array: PackedStringArray = []
+	assert(!packed_string_array.push_back("abc"))
+	print("StringName in PackedStringArray: ", &"abc" in packed_string_array)
+
+	assert(!string_array.push_back("abc"))
+	print("StringName finds String in Array: ", string_array.find(&"abc"))
+
+	assert(!stringname_array.push_back(&"abc"))
+	print("String finds StringName in Array: ", stringname_array.find("abc"))

+ 11 - 0
modules/gdscript/tests/scripts/runtime/features/array_string_stringname_equivalent.out

@@ -0,0 +1,11 @@
+GDTEST_OK
+StringName in Array: true
+String in Array: true
+Arrays not equal: true
+Array[String] insert converted: true
+Array[StringName] insert converted: true
+StringName in Array[String]: true
+String in Array[StringName]: true
+StringName in PackedStringArray: true
+StringName finds String in Array: 0
+String finds StringName in Array: 0

+ 17 - 0
modules/gdscript/tests/scripts/runtime/features/dictionary_string_stringname_equivalent.gd

@@ -0,0 +1,17 @@
+# https://github.com/godotengine/godot/issues/62957
+
+func test():
+	var string_dict = {}
+	string_dict["abc"] = 42
+	var stringname_dict = {}
+	stringname_dict[&"abc"] = 24
+
+	print("String key is TYPE_STRING: ", typeof(string_dict.keys()[0]) == TYPE_STRING)
+	print("StringName key is TYPE_STRING: ", typeof(stringname_dict.keys()[0]) == TYPE_STRING)
+
+	print("StringName gets String: ", string_dict.get(&"abc"))
+	print("String gets StringName: ", stringname_dict.get("abc"))
+
+	stringname_dict[&"abc"] = 42
+  # They compare equal because StringName keys are converted to String.
+	print("String Dictionary == StringName Dictionary: ", string_dict == stringname_dict)

+ 6 - 0
modules/gdscript/tests/scripts/runtime/features/dictionary_string_stringname_equivalent.out

@@ -0,0 +1,6 @@
+GDTEST_OK
+String key is TYPE_STRING: true
+StringName key is TYPE_STRING: true
+StringName gets String: 42
+String gets StringName: 24
+String Dictionary == StringName Dictionary: true

+ 14 - 0
modules/gdscript/tests/scripts/runtime/features/match_string_stringname_equivalent.gd

@@ -0,0 +1,14 @@
+# https://github.com/godotengine/godot/issues/60145
+
+func test():
+	match "abc":
+		&"abc":
+			print("String matched StringName")
+		_:
+			print("no match")
+
+	match &"abc":
+		"abc":
+			print("StringName matched String")
+		_:
+			print("no match")

+ 3 - 0
modules/gdscript/tests/scripts/runtime/features/match_string_stringname_equivalent.out

@@ -0,0 +1,3 @@
+GDTEST_OK
+String matched StringName
+StringName matched String

+ 11 - 0
modules/gdscript/tests/scripts/runtime/features/string_stringname_equivalent.gd

@@ -0,0 +1,11 @@
+# https://github.com/godotengine/godot/issues/64171
+
+func test():
+	print("Compare ==: ", "abc" == &"abc")
+	print("Compare ==: ", &"abc" == "abc")
+	print("Compare !=: ", "abc" != &"abc")
+	print("Compare !=: ", &"abc" != "abc")
+
+	print("Concat: ", "abc" + &"def")
+	print("Concat: ", &"abc" + "def")
+	print("Concat: ", &"abc" + &"def")

+ 8 - 0
modules/gdscript/tests/scripts/runtime/features/string_stringname_equivalent.out

@@ -0,0 +1,8 @@
+GDTEST_OK
+Compare ==: true
+Compare ==: true
+Compare !=: false
+Compare !=: false
+Concat: abcdef
+Concat: abcdef
+Concat: abcdef

+ 1 - 1
modules/multiplayer/scene_rpc_interface.cpp

@@ -82,7 +82,7 @@ void SceneRPCInterface::_parse_rpc_config(const Variant &p_config, bool p_for_no
 	Array names = config.keys();
 	names.sort(); // Ensure ID order
 	for (int i = 0; i < names.size(); i++) {
-		ERR_CONTINUE(names[i].get_type() != Variant::STRING);
+		ERR_CONTINUE(names[i].get_type() != Variant::STRING && names[i].get_type() != Variant::STRING_NAME);
 		String name = names[i].operator String();
 		ERR_CONTINUE(config[name].get_type() != Variant::DICTIONARY);
 		ERR_CONTINUE(!config[name].operator Dictionary().has("rpc_mode"));

+ 1 - 2
modules/regex/regex.cpp

@@ -50,8 +50,7 @@ int RegExMatch::_find(const Variant &p_name) const {
 			return -1;
 		}
 		return i;
-
-	} else if (p_name.get_type() == Variant::STRING) {
+	} else if (p_name.get_type() == Variant::STRING || p_name.get_type() == Variant::STRING_NAME) {
 		HashMap<String, int>::ConstIterator found = names.find((String)p_name);
 		if (found) {
 			return found->value;

+ 13 - 0
tests/core/variant/test_dictionary.h

@@ -64,6 +64,19 @@ TEST_CASE("[Dictionary] Assignment using bracket notation ([])") {
 	map["World!"] = 4;
 	CHECK(int(map["World!"]) == 4);
 
+	map[StringName("HelloName")] = 6;
+	CHECK(int(map[StringName("HelloName")]) == 6);
+	// Check that StringName key is converted to String.
+	CHECK(int(map.find_key(6).get_type()) == Variant::STRING);
+	map[StringName("HelloName")] = 7;
+	CHECK(int(map[StringName("HelloName")]) == 7);
+
+	// Test String and StringName are equivalent.
+	map[StringName("Hello")] = 8;
+	CHECK(int(map["Hello"]) == 8);
+	map["Hello"] = 9;
+	CHECK(int(map[StringName("Hello")]) == 9);
+
 	// Test non-string keys, since keys can be of any Variant type.
 	map[12345] = -5;
 	CHECK(int(map[12345]) == -5);