summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/javascript_builds.yml2
-rw-r--r--core/local_vector.h2
-rw-r--r--doc/classes/Button.xml12
-rw-r--r--doc/classes/File.xml1
-rw-r--r--doc/classes/JSON.xml3
-rw-r--r--doc/classes/JSONParseResult.xml10
-rw-r--r--doc/classes/TreeItem.xml12
-rwxr-xr-xdoc/tools/makerst.py118
-rw-r--r--editor/editor_help.cpp49
-rw-r--r--editor/editor_settings.cpp2
-rw-r--r--editor/plugins/script_editor_plugin.cpp8
-rw-r--r--editor/plugins/visual_shader_editor_plugin.cpp8
-rw-r--r--modules/gdscript/doc_classes/@GDScript.xml25
-rw-r--r--platform/linuxbsd/display_server_x11.cpp214
-rw-r--r--platform/linuxbsd/display_server_x11.h1
-rw-r--r--platform/linuxbsd/joypad_linux.cpp11
-rw-r--r--scene/main/window.cpp4
-rw-r--r--scene/resources/visual_shader.cpp10
-rw-r--r--scene/resources/visual_shader.h1
-rw-r--r--tests/test_astar.cpp409
-rw-r--r--tests/test_astar.h333
-rw-r--r--tests/test_color.h213
-rw-r--r--tests/test_main.cpp7
23 files changed, 812 insertions, 643 deletions
diff --git a/.github/workflows/javascript_builds.yml b/.github/workflows/javascript_builds.yml
index 8167a48eae..421906c77f 100644
--- a/.github/workflows/javascript_builds.yml
+++ b/.github/workflows/javascript_builds.yml
@@ -5,7 +5,7 @@ on: [push, pull_request]
env:
GODOT_BASE_BRANCH: master
SCONS_CACHE_LIMIT: 4096
- EM_VERSION: latest
+ EM_VERSION: 1.39.20
EM_CACHE_FOLDER: 'emsdk-cache'
jobs:
diff --git a/core/local_vector.h b/core/local_vector.h
index d97f3330dc..b0dbd22b29 100644
--- a/core/local_vector.h
+++ b/core/local_vector.h
@@ -73,10 +73,10 @@ public:
void remove(U p_index) {
ERR_FAIL_UNSIGNED_INDEX(p_index, count);
+ count--;
for (U i = p_index; i < count; i++) {
data[i] = data[i + 1];
}
- count--;
if (!__has_trivial_destructor(T) && !force_trivial) {
data[count].~T();
}
diff --git a/doc/classes/Button.xml b/doc/classes/Button.xml
index 675441d842..de05cfcd13 100644
--- a/doc/classes/Button.xml
+++ b/doc/classes/Button.xml
@@ -5,6 +5,18 @@
</brief_description>
<description>
Button is the standard themed button. It can contain text and an icon, and will display them according to the current [Theme].
+ [b]Example of creating a button and assigning an action when pressed by code:[/b]
+ [codeblock]
+ func _ready():
+ var button = Button.new()
+ button.text = "Click me"
+ button.connect("pressed", self, "_button_pressed")
+ add_child(button)
+
+ func _button_pressed():
+ print("Hello world!")
+ [/codeblock]
+ Buttons (like all Control nodes) can also be created in the editor, but some situations may require creating them from code.
</description>
<tutorials>
</tutorials>
diff --git a/doc/classes/File.xml b/doc/classes/File.xml
index 20bc39ef1f..d91203d91f 100644
--- a/doc/classes/File.xml
+++ b/doc/classes/File.xml
@@ -257,6 +257,7 @@
</argument>
<description>
Opens an encrypted file in write or read mode. You need to pass a binary key to encrypt/decrypt it.
+ [b]Note:[/b] The provided key must be 32 bytes long.
</description>
</method>
<method name="open_encrypted_with_pass">
diff --git a/doc/classes/JSON.xml b/doc/classes/JSON.xml
index 7bd2edcb1c..2e837dea1d 100644
--- a/doc/classes/JSON.xml
+++ b/doc/classes/JSON.xml
@@ -15,7 +15,7 @@
<argument index="0" name="json" type="String">
</argument>
<description>
- Parses a JSON encoded string and returns a [JSONParseResult] containing the result.
+ Parses a JSON-encoded string and returns a [JSONParseResult] containing the result.
</description>
</method>
<method name="print">
@@ -29,6 +29,7 @@
</argument>
<description>
Converts a [Variant] var to JSON text and returns the result. Useful for serializing data to store or send over the network.
+ [b]Note:[/b] The JSON specification does not define integer or float types, but only a [i]number[/i] type. Therefore, converting a Variant to JSON text will convert all numerical values to [float] types.
</description>
</method>
</methods>
diff --git a/doc/classes/JSONParseResult.xml b/doc/classes/JSONParseResult.xml
index 4444e08593..4dbceb35e9 100644
--- a/doc/classes/JSONParseResult.xml
+++ b/doc/classes/JSONParseResult.xml
@@ -15,21 +15,21 @@
The error type if the JSON source was not successfully parsed. See the [enum Error] constants.
</member>
<member name="error_line" type="int" setter="set_error_line" getter="get_error_line" default="-1">
- The line number where the error occurred if JSON source was not successfully parsed.
+ The line number where the error occurred if the JSON source was not successfully parsed.
</member>
<member name="error_string" type="String" setter="set_error_string" getter="get_error_string" default="&quot;&quot;">
- The error message if JSON source was not successfully parsed. See the [enum Error] constants.
+ The error message if the JSON source was not successfully parsed. See the [enum Error] constants.
</member>
<member name="result" type="Variant" setter="set_result" getter="get_result">
- A [Variant] containing the parsed JSON. Use [method @GDScript.typeof] or the [code]is[/code] keyword to check if it is what you expect. For example, if the JSON source starts with curly braces ([code]{}[/code]), a [Dictionary] will be returned. If the JSON source starts with braces ([code][][/code]), an [Array] will be returned.
- [b]Note:[/b] The JSON specification does not define integer or float types, but only a number type. Therefore, parsing a JSON text will convert all numerical values to float types.
+ A [Variant] containing the parsed JSON. Use [method @GDScript.typeof] or the [code]is[/code] keyword to check if it is what you expect. For example, if the JSON source starts with curly braces ([code]{}[/code]), a [Dictionary] will be returned. If the JSON source starts with brackets ([code][][/code]), an [Array] will be returned.
+ [b]Note:[/b] The JSON specification does not define integer or float types, but only a [i]number[/i] type. Therefore, parsing a JSON text will convert all numerical values to [float] types.
[b]Note:[/b] JSON objects do not preserve key order like Godot dictionaries, thus, you should not rely on keys being in a certain order if a dictionary is constructed from JSON. In contrast, JSON arrays retain the order of their elements:
[codeblock]
var p = JSON.parse('["hello", "world", "!"]')
if typeof(p.result) == TYPE_ARRAY:
print(p.result[0]) # Prints "hello"
else:
- print("unexpected results")
+ push_error("Unexpected results.")
[/codeblock]
</member>
</members>
diff --git a/doc/classes/TreeItem.xml b/doc/classes/TreeItem.xml
index 126d6b4180..22e643a51d 100644
--- a/doc/classes/TreeItem.xml
+++ b/doc/classes/TreeItem.xml
@@ -118,7 +118,7 @@
<return type="TreeItem">
</return>
<description>
- Returns the TreeItem's child items.
+ Returns the TreeItem's first child item or a null object if there is none.
</description>
</method>
<method name="get_custom_bg_color" qualifiers="const">
@@ -196,7 +196,7 @@
<return type="TreeItem">
</return>
<description>
- Returns the next TreeItem in the tree.
+ Returns the next TreeItem in the tree or a null object if there is none.
</description>
</method>
<method name="get_next_visible">
@@ -205,7 +205,7 @@
<argument index="0" name="wrap" type="bool" default="false">
</argument>
<description>
- Returns the next visible TreeItem in the tree.
+ Returns the next visible TreeItem in the tree or a null object if there is none.
If [code]wrap[/code] is enabled, the method will wrap around to the first visible element in the tree when called on the last visible element, otherwise it returns [code]null[/code].
</description>
</method>
@@ -213,14 +213,14 @@
<return type="TreeItem">
</return>
<description>
- Returns the parent TreeItem.
+ Returns the parent TreeItem or a null object if there is none.
</description>
</method>
<method name="get_prev">
<return type="TreeItem">
</return>
<description>
- Returns the previous TreeItem in the tree.
+ Returns the previous TreeItem in the tree or a null object if there is none.
</description>
</method>
<method name="get_prev_visible">
@@ -229,7 +229,7 @@
<argument index="0" name="wrap" type="bool" default="false">
</argument>
<description>
- Returns the previous visible TreeItem in the tree.
+ Returns the previous visible TreeItem in the tree or a null object if there is none.
If [code]wrap[/code] is enabled, the method will wrap around to the last visible element in the tree when called on the first visible element, otherwise it returns [code]null[/code].
</description>
</method>
diff --git a/doc/tools/makerst.py b/doc/tools/makerst.py
index c0bba38799..b594f652b3 100755
--- a/doc/tools/makerst.py
+++ b/doc/tools/makerst.py
@@ -565,6 +565,8 @@ def make_rst_class(class_def, state, dry_run, output_dir): # type: (ClassDef, S
index += 1
+ f.write(make_footer())
+
def escape_rst(text, until_pos=-1): # type: (str) -> str
# Escape \ character, otherwise it ends up as an escape character in rst
@@ -600,6 +602,43 @@ def escape_rst(text, until_pos=-1): # type: (str) -> str
return text
+def format_codeblock(code_type, post_text, indent_level, state): # types: str, str, int, state
+ end_pos = post_text.find("[/" + code_type + "]")
+ if end_pos == -1:
+ print_error("[" + code_type + "] without a closing tag, file: {}".format(state.current_class), state)
+ return None
+
+ code_text = post_text[len("[" + code_type + "]") : end_pos]
+ post_text = post_text[end_pos:]
+
+ # Remove extraneous tabs
+ code_pos = 0
+ while True:
+ code_pos = code_text.find("\n", code_pos)
+ if code_pos == -1:
+ break
+
+ to_skip = 0
+ while code_pos + to_skip + 1 < len(code_text) and code_text[code_pos + to_skip + 1] == "\t":
+ to_skip += 1
+
+ if to_skip > indent_level:
+ print_error(
+ "Four spaces should be used for indentation within ["
+ + code_type
+ + "], file: {}".format(state.current_class),
+ state,
+ )
+
+ if len(code_text[code_pos + to_skip + 1 :]) == 0:
+ code_text = code_text[:code_pos] + "\n"
+ code_pos += 1
+ else:
+ code_text = code_text[:code_pos] + "\n " + code_text[code_pos + to_skip + 1 :]
+ code_pos += 5 - to_skip
+ return ["\n[" + code_type + "]" + code_text + post_text, len("\n[" + code_type + "]" + code_text)]
+
+
def rstize_text(text, state): # type: (str, State) -> str
# Linebreak + tabs in the XML should become two line breaks unless in a "codeblock"
pos = 0
@@ -616,43 +655,17 @@ def rstize_text(text, state): # type: (str, State) -> str
post_text = text[pos + 1 :]
# Handle codeblocks
- if post_text.startswith("[codeblock]"):
- end_pos = post_text.find("[/codeblock]")
- if end_pos == -1:
- print_error("[codeblock] without a closing tag, file: {}".format(state.current_class), state)
+ if (
+ post_text.startswith("[codeblock]")
+ or post_text.startswith("[gdscript]")
+ or post_text.startswith("[csharp]")
+ ):
+ block_type = post_text[1:].split("]")[0]
+ result = format_codeblock(block_type, post_text, indent_level, state)
+ if result is None:
return ""
-
- code_text = post_text[len("[codeblock]") : end_pos]
- post_text = post_text[end_pos:]
-
- # Remove extraneous tabs
- code_pos = 0
- while True:
- code_pos = code_text.find("\n", code_pos)
- if code_pos == -1:
- break
-
- to_skip = 0
- while code_pos + to_skip + 1 < len(code_text) and code_text[code_pos + to_skip + 1] == "\t":
- to_skip += 1
-
- if to_skip > indent_level:
- print_error(
- "Four spaces should be used for indentation within [codeblock], file: {}".format(
- state.current_class
- ),
- state,
- )
-
- if len(code_text[code_pos + to_skip + 1 :]) == 0:
- code_text = code_text[:code_pos] + "\n"
- code_pos += 1
- else:
- code_text = code_text[:code_pos] + "\n " + code_text[code_pos + to_skip + 1 :]
- code_pos += 5 - to_skip
-
- text = pre_text + "\n[codeblock]" + code_text + post_text
- pos += len("\n[codeblock]" + code_text)
+ text = pre_text + result[0]
+ pos += result[1]
# Handle normal text
else:
@@ -697,7 +710,7 @@ def rstize_text(text, state): # type: (str, State) -> str
else: # command
cmd = tag_text
space_pos = tag_text.find(" ")
- if cmd == "/codeblock":
+ if cmd == "/codeblock" or cmd == "/gdscript" or cmd == "/csharp":
tag_text = ""
tag_depth -= 1
inside_code = False
@@ -813,6 +826,20 @@ def rstize_text(text, state): # type: (str, State) -> str
tag_depth += 1
tag_text = "\n::\n"
inside_code = True
+ elif cmd == "gdscript":
+ tag_depth += 1
+ tag_text = "\n .. code-tab:: gdscript GDScript\n"
+ inside_code = True
+ elif cmd == "csharp":
+ tag_depth += 1
+ tag_text = "\n .. code-tab:: csharp\n"
+ inside_code = True
+ elif cmd == "codeblocks":
+ tag_depth += 1
+ tag_text = "\n.. tabs::"
+ elif cmd == "/codeblocks":
+ tag_depth -= 1
+ tag_text = ""
elif cmd == "br":
# Make a new paragraph instead of a linebreak, rst is not so linebreak friendly
tag_text = "\n\n"
@@ -995,7 +1022,10 @@ def make_method_signature(
out += " **)**"
if isinstance(method_def, MethodDef) and method_def.qualifiers is not None:
- out += " " + method_def.qualifiers
+ # Use substitutions for abbreviations. This is used to display tooltips on hover.
+ # See `make_footer()` for descriptions.
+ for qualifier in method_def.qualifiers.split():
+ out += " |" + qualifier + "|"
return ret_type, out
@@ -1004,6 +1034,18 @@ def make_heading(title, underline): # type: (str, str) -> str
return title + "\n" + (underline * len(title)) + "\n\n"
+def make_footer(): # type: () -> str
+ # Generate reusable abbreviation substitutions.
+ # This way, we avoid bloating the generated rST with duplicate abbreviations.
+ # fmt: off
+ return (
+ ".. |virtual| replace:: :abbr:`virtual (This method should typically be overridden by the user to have any effect.)`\n"
+ ".. |const| replace:: :abbr:`const (This method has no side effects. It doesn't modify any of the instance's member variables.)`\n"
+ ".. |vararg| replace:: :abbr:`vararg (This method accepts any number of arguments after the ones described here.)`"
+ )
+ # fmt: on
+
+
def make_url(link): # type: (str) -> str
match = GODOT_DOCS_PATTERN.search(link)
if match:
diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp
index 2a59aadd61..bce34db740 100644
--- a/editor/editor_help.cpp
+++ b/editor/editor_help.cpp
@@ -1252,6 +1252,55 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt) {
String bbcode = p_bbcode.dedent().replace("\t", "").replace("\r", "").strip_edges();
+ // Select the correct code examples
+ switch ((int)EDITOR_GET("text_editor/help/class_reference_examples")) {
+ case 0: // GDScript
+ bbcode = bbcode.replace("[gdscript]", "[codeblock]");
+ bbcode = bbcode.replace("[/gdscript]", "[/codeblock]");
+
+ for (int pos = bbcode.find("[csharp]"); pos != -1; pos = bbcode.find("[csharp]")) {
+ if (bbcode.find("[/csharp]") == -1) {
+ WARN_PRINT("Unclosed [csharp] block or parse fail in code (search for tag errors)");
+ break;
+ }
+
+ bbcode.erase(pos, bbcode.find("[/csharp]") + 9 - pos);
+ while (bbcode[pos] == '\n') {
+ bbcode.erase(pos, 1);
+ }
+ }
+ break;
+ case 1: // C#
+ bbcode = bbcode.replace("[csharp]", "[codeblock]");
+ bbcode = bbcode.replace("[/csharp]", "[/codeblock]");
+
+ for (int pos = bbcode.find("[gdscript]"); pos != -1; pos = bbcode.find("[gdscript]")) {
+ if (bbcode.find("[/gdscript]") == -1) {
+ WARN_PRINT("Unclosed [gdscript] block or parse fail in code (search for tag errors)");
+ break;
+ }
+
+ bbcode.erase(pos, bbcode.find("[/gdscript]") + 11 - pos);
+ while (bbcode[pos] == '\n') {
+ bbcode.erase(pos, 1);
+ }
+ }
+ break;
+ case 2: // GDScript and C#
+ bbcode = bbcode.replace("[csharp]", "[b]C#:[/b]\n[codeblock]");
+ bbcode = bbcode.replace("[gdscript]", "[b]GDScript:[/b]\n[codeblock]");
+
+ bbcode = bbcode.replace("[/csharp]", "[/codeblock]");
+ bbcode = bbcode.replace("[/gdscript]", "[/codeblock]");
+ break;
+ }
+
+ // Remove codeblocks (they would be printed otherwise)
+ bbcode = bbcode.replace("[codeblocks]\n", "");
+ bbcode = bbcode.replace("\n[/codeblocks]", "");
+ bbcode = bbcode.replace("[codeblocks]", "");
+ bbcode = bbcode.replace("[/codeblocks]", "");
+
// remove extra new lines around code blocks
bbcode = bbcode.replace("[codeblock]\n", "[codeblock]");
bbcode = bbcode.replace("\n[/codeblock]", "[/codeblock]");
diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp
index 3a1a0d5b01..a3438b3601 100644
--- a/editor/editor_settings.cpp
+++ b/editor/editor_settings.cpp
@@ -495,6 +495,8 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
hints["text_editor/help/help_source_font_size"] = PropertyInfo(Variant::INT, "text_editor/help/help_source_font_size", PROPERTY_HINT_RANGE, "8,48,1");
_initial_set("text_editor/help/help_title_font_size", 23);
hints["text_editor/help/help_title_font_size"] = PropertyInfo(Variant::INT, "text_editor/help/help_title_font_size", PROPERTY_HINT_RANGE, "8,48,1");
+ _initial_set("text_editor/help/class_reference_examples", 0);
+ hints["text_editor/help/class_reference_examples"] = PropertyInfo(Variant::INT, "text_editor/help/class_reference_examples", PROPERTY_HINT_ENUM, "GDScript,C#,GDScript and C#");
/* Editors */
diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp
index 6c7f4eb908..20eef1cebd 100644
--- a/editor/plugins/script_editor_plugin.cpp
+++ b/editor/plugins/script_editor_plugin.cpp
@@ -1922,14 +1922,18 @@ void ScriptEditor::_update_script_names() {
Vector<String> disambiguated_script_names;
Vector<String> full_script_paths;
for (int j = 0; j < sedata.size(); j++) {
- disambiguated_script_names.append(sedata[j].name);
+ disambiguated_script_names.append(sedata[j].name.replace("(*)", ""));
full_script_paths.append(sedata[j].tooltip);
}
EditorNode::disambiguate_filenames(full_script_paths, disambiguated_script_names);
for (int j = 0; j < sedata.size(); j++) {
- sedata.write[j].name = disambiguated_script_names[j];
+ if (sedata[j].name.ends_with("(*)")) {
+ sedata.write[j].name = disambiguated_script_names[j] + "(*)";
+ } else {
+ sedata.write[j].name = disambiguated_script_names[j];
+ }
}
EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_child(i));
diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp
index 0af2b120bd..53bd1150ec 100644
--- a/editor/plugins/visual_shader_editor_plugin.cpp
+++ b/editor/plugins/visual_shader_editor_plugin.cpp
@@ -874,6 +874,8 @@ void VisualShaderEditor::_update_graph() {
Color comment_color = EDITOR_GET("text_editor/highlighting/comment_color");
Color symbol_color = EDITOR_GET("text_editor/highlighting/symbol_color");
Color function_color = EDITOR_GET("text_editor/highlighting/function_color");
+ Color number_color = EDITOR_GET("text_editor/highlighting/number_color");
+ Color members_color = EDITOR_GET("text_editor/highlighting/member_variable_color");
expression_box->set_syntax_highlighter(expression_syntax_highlighter);
expression_box->add_theme_color_override("background_color", background_color);
@@ -884,8 +886,10 @@ void VisualShaderEditor::_update_graph() {
expression_box->add_theme_font_override("font", get_theme_font("expression", "EditorFonts"));
expression_box->add_theme_color_override("font_color", text_color);
+ expression_syntax_highlighter->set_number_color(number_color);
expression_syntax_highlighter->set_symbol_color(symbol_color);
expression_syntax_highlighter->set_function_color(function_color);
+ expression_syntax_highlighter->set_member_variable_color(members_color);
expression_syntax_highlighter->add_color_region("/*", "*/", comment_color, false);
expression_syntax_highlighter->add_color_region("//", "", comment_color, true);
@@ -1743,6 +1747,8 @@ void VisualShaderEditor::_notification(int p_what) {
Color comment_color = EDITOR_GET("text_editor/highlighting/comment_color");
Color symbol_color = EDITOR_GET("text_editor/highlighting/symbol_color");
Color function_color = EDITOR_GET("text_editor/highlighting/function_color");
+ Color number_color = EDITOR_GET("text_editor/highlighting/number_color");
+ Color members_color = EDITOR_GET("text_editor/highlighting/member_variable_color");
preview_text->add_theme_color_override("background_color", background_color);
@@ -1752,8 +1758,10 @@ void VisualShaderEditor::_notification(int p_what) {
preview_text->add_theme_font_override("font", get_theme_font("expression", "EditorFonts"));
preview_text->add_theme_color_override("font_color", text_color);
+ syntax_highlighter->set_number_color(number_color);
syntax_highlighter->set_symbol_color(symbol_color);
syntax_highlighter->set_function_color(function_color);
+ syntax_highlighter->set_member_variable_color(members_color);
syntax_highlighter->clear_color_regions();
syntax_highlighter->add_color_region("/*", "*/", comment_color, false);
syntax_highlighter->add_color_region("//", "", comment_color, true);
diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml
index 1ac47884c0..9e40a69712 100644
--- a/modules/gdscript/doc_classes/@GDScript.xml
+++ b/modules/gdscript/doc_classes/@GDScript.xml
@@ -735,16 +735,17 @@
<argument index="0" name="json" type="String">
</argument>
<description>
- Parse JSON text to a Variant (use [method typeof] to check if it is what you expect).
- Be aware that the JSON specification does not define integer or float types, but only a number type. Therefore, parsing a JSON text will convert all numerical values to [float] types.
- Note that JSON objects do not preserve key order like Godot dictionaries, thus you should not rely on keys being in a certain order if a dictionary is constructed from JSON. In contrast, JSON arrays retain the order of their elements:
+ Parse JSON text to a Variant. (Use [method typeof] to check if the Variant's type is what you expect.)
+ [b]Note:[/b] The JSON specification does not define integer or float types, but only a [i]number[/i] type. Therefore, parsing a JSON text will convert all numerical values to [float] types.
+ [b]Note:[/b] JSON objects do not preserve key order like Godot dictionaries, thus, you should not rely on keys being in a certain order if a dictionary is constructed from JSON. In contrast, JSON arrays retain the order of their elements:
[codeblock]
- p = parse_json('["a", "b", "c"]')
- if typeof(p) == TYPE_ARRAY:
- print(p[0]) # Prints a
+ var p = JSON.parse('["hello", "world", "!"]')
+ if typeof(p.result) == TYPE_ARRAY:
+ print(p.result[0]) # Prints "hello"
else:
- print("unexpected results")
+ push_error("Unexpected results.")
[/codeblock]
+ See also [JSON] for an alternative way to parse JSON text.
</description>
</method>
<method name="polar2cartesian">
@@ -1220,12 +1221,16 @@
<argument index="0" name="var" type="Variant">
</argument>
<description>
- Converts a Variant [code]var[/code] to JSON text and return the result. Useful for serializing data to store or send over the network.
+ Converts a [Variant] [code]var[/code] to JSON text and return the result. Useful for serializing data to store or send over the network.
[codeblock]
+ # Both numbers below are integers.
a = { "a": 1, "b": 2 }
b = to_json(a)
print(b) # {"a":1, "b":2}
+ # Both numbers above are floats, even if they display without any decimal places.
[/codeblock]
+ [b]Note:[/b] The JSON specification does not define integer or float types, but only a [i]number[/i] type. Therefore, converting a [Variant] to JSON text will convert all numerical values to [float] types.
+ See also [JSON] for an alternative way to convert a [Variant] to JSON text.
</description>
</method>
<method name="type_exists">
@@ -1268,9 +1273,9 @@
j = to_json([1, 2, 3])
v = validate_json(j)
if not v:
- print("valid")
+ print("Valid JSON.")
else:
- prints("invalid", v)
+ push_error("Invalid JSON: " + v)
[/codeblock]
</description>
</method>
diff --git a/platform/linuxbsd/display_server_x11.cpp b/platform/linuxbsd/display_server_x11.cpp
index 4aec6d256c..d35941bdcd 100644
--- a/platform/linuxbsd/display_server_x11.cpp
+++ b/platform/linuxbsd/display_server_x11.cpp
@@ -881,6 +881,46 @@ void DisplayServerX11::window_set_transient(WindowID p_window, WindowID p_parent
}
}
+// Helper method. Assumes that the window id has already been checked and exists.
+void DisplayServerX11::_update_size_hints(WindowID p_window) {
+ WindowData &wd = windows[p_window];
+ WindowMode window_mode = window_get_mode(p_window);
+ XSizeHints *xsh = XAllocSizeHints();
+
+ // Always set the position and size hints - they should be synchronized with the actual values after the window is mapped anyway
+ xsh->flags |= PPosition | PSize;
+ xsh->x = wd.position.x;
+ xsh->y = wd.position.y;
+ xsh->width = wd.size.width;
+ xsh->height = wd.size.height;
+
+ if (window_mode == WINDOW_MODE_FULLSCREEN) {
+ // Do not set any other hints to prevent the window manager from ignoring the fullscreen flags
+ } else if (window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) {
+ // If resizing is disabled, use the forced size
+ xsh->flags |= PMinSize | PMaxSize;
+ xsh->min_width = wd.size.x;
+ xsh->max_width = wd.size.x;
+ xsh->min_height = wd.size.y;
+ xsh->max_height = wd.size.y;
+ } else {
+ // Otherwise, just respect min_size and max_size
+ if (wd.min_size != Size2i()) {
+ xsh->flags |= PMinSize;
+ xsh->min_width = wd.min_size.x;
+ xsh->min_height = wd.min_size.y;
+ }
+ if (wd.max_size != Size2i()) {
+ xsh->flags |= PMaxSize;
+ xsh->max_width = wd.max_size.x;
+ xsh->max_height = wd.max_size.y;
+ }
+ }
+
+ XSetWMNormalHints(x11_display, wd.x11_window, xsh);
+ XFree(xsh);
+}
+
Point2i DisplayServerX11::window_get_position(WindowID p_window) const {
_THREAD_SAFE_METHOD_
@@ -934,25 +974,8 @@ void DisplayServerX11::window_set_max_size(const Size2i p_size, WindowID p_windo
}
wd.max_size = p_size;
- if (!window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) {
- XSizeHints *xsh;
- xsh = XAllocSizeHints();
- xsh->flags = 0L;
- if (wd.min_size != Size2i()) {
- xsh->flags |= PMinSize;
- xsh->min_width = wd.min_size.x;
- xsh->min_height = wd.min_size.y;
- }
- if (wd.max_size != Size2i()) {
- xsh->flags |= PMaxSize;
- xsh->max_width = wd.max_size.x;
- xsh->max_height = wd.max_size.y;
- }
- XSetWMNormalHints(x11_display, wd.x11_window, xsh);
- XFree(xsh);
-
- XFlush(x11_display);
- }
+ _update_size_hints(p_window);
+ XFlush(x11_display);
}
Size2i DisplayServerX11::window_get_max_size(WindowID p_window) const {
@@ -976,25 +999,8 @@ void DisplayServerX11::window_set_min_size(const Size2i p_size, WindowID p_windo
}
wd.min_size = p_size;
- if (!window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) {
- XSizeHints *xsh;
- xsh = XAllocSizeHints();
- xsh->flags = 0L;
- if (wd.min_size != Size2i()) {
- xsh->flags |= PMinSize;
- xsh->min_width = wd.min_size.x;
- xsh->min_height = wd.min_size.y;
- }
- if (wd.max_size != Size2i()) {
- xsh->flags |= PMaxSize;
- xsh->max_width = wd.max_size.x;
- xsh->max_height = wd.max_size.y;
- }
- XSetWMNormalHints(x11_display, wd.x11_window, xsh);
- XFree(xsh);
-
- XFlush(x11_display);
- }
+ _update_size_hints(p_window);
+ XFlush(x11_display);
}
Size2i DisplayServerX11::window_get_min_size(WindowID p_window) const {
@@ -1027,37 +1033,15 @@ void DisplayServerX11::window_set_size(const Size2i p_size, WindowID p_window) {
int old_w = xwa.width;
int old_h = xwa.height;
- // If window resizable is disabled we need to update the attributes first
- XSizeHints *xsh;
- xsh = XAllocSizeHints();
- if (!window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) {
- xsh->flags = PMinSize | PMaxSize;
- xsh->min_width = size.x;
- xsh->max_width = size.x;
- xsh->min_height = size.y;
- xsh->max_height = size.y;
- } else {
- xsh->flags = 0L;
- if (wd.min_size != Size2i()) {
- xsh->flags |= PMinSize;
- xsh->min_width = wd.min_size.x;
- xsh->min_height = wd.min_size.y;
- }
- if (wd.max_size != Size2i()) {
- xsh->flags |= PMaxSize;
- xsh->max_width = wd.max_size.x;
- xsh->max_height = wd.max_size.y;
- }
- }
- XSetWMNormalHints(x11_display, wd.x11_window, xsh);
- XFree(xsh);
+ // Update our videomode width and height
+ wd.size = size;
+
+ // Update the size hints first to make sure the window size can be set
+ _update_size_hints(p_window);
// Resize the window
XResizeWindow(x11_display, wd.x11_window, size.x, size.y);
- // Update our videomode width and height
- wd.size = size;
-
for (int timeout = 0; timeout < 50; ++timeout) {
XSync(x11_display, False);
XGetWindowAttributes(x11_display, wd.x11_window, &xwa);
@@ -1213,14 +1197,9 @@ void DisplayServerX11::_set_wm_fullscreen(WindowID p_window, bool p_enabled) {
XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5);
}
- if (p_enabled && window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) {
+ if (p_enabled) {
// Set the window as resizable to prevent window managers to ignore the fullscreen state flag.
- XSizeHints *xsh;
-
- xsh = XAllocSizeHints();
- xsh->flags = 0L;
- XSetWMNormalHints(x11_display, wd.x11_window, xsh);
- XFree(xsh);
+ _update_size_hints(p_window);
}
// Using EWMH -- Extended Window Manager Hints
@@ -1248,30 +1227,7 @@ void DisplayServerX11::_set_wm_fullscreen(WindowID p_window, bool p_enabled) {
if (!p_enabled) {
// Reset the non-resizable flags if we un-set these before.
- Size2i size = window_get_size(p_window);
- XSizeHints *xsh;
- xsh = XAllocSizeHints();
- if (window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) {
- xsh->flags = PMinSize | PMaxSize;
- xsh->min_width = size.x;
- xsh->max_width = size.x;
- xsh->min_height = size.y;
- xsh->max_height = size.y;
- } else {
- xsh->flags = 0L;
- if (wd.min_size != Size2i()) {
- xsh->flags |= PMinSize;
- xsh->min_width = wd.min_size.x;
- xsh->min_height = wd.min_size.y;
- }
- if (wd.max_size != Size2i()) {
- xsh->flags |= PMaxSize;
- xsh->max_width = wd.max_size.x;
- xsh->max_height = wd.max_size.y;
- }
- }
- XSetWMNormalHints(x11_display, wd.x11_window, xsh);
- XFree(xsh);
+ _update_size_hints(p_window);
// put back or remove decorations according to the last set borderless state
Hints hints;
@@ -1329,13 +1285,13 @@ void DisplayServerX11::window_set_mode(WindowMode p_mode, WindowID p_window) {
} break;
case WINDOW_MODE_FULLSCREEN: {
//Remove full-screen
+ wd.fullscreen = false;
+
_set_wm_fullscreen(p_window, false);
//un-maximize required for always on top
bool on_top = window_get_flag(WINDOW_FLAG_ALWAYS_ON_TOP, p_window);
- wd.fullscreen = false;
-
window_set_position(wd.last_position_before_fs, p_window);
if (on_top) {
@@ -1381,15 +1337,16 @@ void DisplayServerX11::window_set_mode(WindowMode p_mode, WindowID p_window) {
} break;
case WINDOW_MODE_FULLSCREEN: {
wd.last_position_before_fs = wd.position;
+
if (window_get_flag(WINDOW_FLAG_ALWAYS_ON_TOP, p_window)) {
_set_wm_maximized(p_window, true);
}
- _set_wm_fullscreen(p_window, true);
+
wd.fullscreen = true;
+ _set_wm_fullscreen(p_window, true);
} break;
case WINDOW_MODE_MAXIMIZED: {
_set_wm_maximized(p_window, true);
-
} break;
}
}
@@ -1456,37 +1413,11 @@ void DisplayServerX11::window_set_flag(WindowFlags p_flag, bool p_enabled, Windo
switch (p_flag) {
case WINDOW_FLAG_RESIZE_DISABLED: {
- XSizeHints *xsh;
- xsh = XAllocSizeHints();
- if (p_enabled) {
- Size2i size = window_get_size(p_window);
-
- xsh->flags = PMinSize | PMaxSize;
- xsh->min_width = size.x;
- xsh->max_width = size.x;
- xsh->min_height = size.y;
- xsh->max_height = size.y;
- } else {
- xsh->flags = 0L;
- if (wd.min_size != Size2i()) {
- xsh->flags |= PMinSize;
- xsh->min_width = wd.min_size.x;
- xsh->min_height = wd.min_size.y;
- }
- if (wd.max_size != Size2i()) {
- xsh->flags |= PMaxSize;
- xsh->max_width = wd.max_size.x;
- xsh->max_height = wd.max_size.y;
- }
- }
-
- XSetWMNormalHints(x11_display, wd.x11_window, xsh);
- XFree(xsh);
-
wd.resize_disabled = p_enabled;
- XFlush(x11_display);
+ _update_size_hints(p_window);
+ XFlush(x11_display);
} break;
case WINDOW_FLAG_BORDERLESS: {
Hints hints;
@@ -3305,20 +3236,6 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u
windows[id] = wd;
{
- if (p_flags & WINDOW_FLAG_RESIZE_DISABLED_BIT) {
- XSizeHints *xsh;
- xsh = XAllocSizeHints();
-
- xsh->flags = PMinSize | PMaxSize;
- xsh->min_width = p_rect.size.width;
- xsh->max_width = p_rect.size.width;
- xsh->min_height = p_rect.size.height;
- xsh->max_height = p_rect.size.height;
-
- XSetWMNormalHints(x11_display, wd.x11_window, xsh);
- XFree(xsh);
- }
-
bool make_utility = false;
if (p_flags & WINDOW_FLAG_BORDERLESS_BIT) {
@@ -3368,18 +3285,7 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u
}
}
- if (id != MAIN_WINDOW_ID) {
- XSizeHints my_hints = XSizeHints();
-
- my_hints.flags = PPosition | PSize; /* I want to specify position and size */
- my_hints.x = p_rect.position.x; /* The origin and size coords I want */
- my_hints.y = p_rect.position.y;
- my_hints.width = p_rect.size.width;
- my_hints.height = p_rect.size.height;
-
- XSetNormalHints(x11_display, wd.x11_window, &my_hints);
- XMoveWindow(x11_display, wd.x11_window, p_rect.position.x, p_rect.position.y);
- }
+ _update_size_hints(id);
#if defined(VULKAN_ENABLED)
if (context_vulkan) {
diff --git a/platform/linuxbsd/display_server_x11.h b/platform/linuxbsd/display_server_x11.h
index 0ba1359145..8e1f941bbf 100644
--- a/platform/linuxbsd/display_server_x11.h
+++ b/platform/linuxbsd/display_server_x11.h
@@ -235,6 +235,7 @@ class DisplayServerX11 : public DisplayServer {
void _update_real_mouse_position(const WindowData &wd);
bool _window_maximize_check(WindowID p_window, const char *p_atom_name) const;
+ void _update_size_hints(WindowID p_window);
void _set_wm_fullscreen(WindowID p_window, bool p_enabled);
void _set_wm_maximized(WindowID p_window, bool p_enabled);
diff --git a/platform/linuxbsd/joypad_linux.cpp b/platform/linuxbsd/joypad_linux.cpp
index 5edaf35c50..fda1358dfd 100644
--- a/platform/linuxbsd/joypad_linux.cpp
+++ b/platform/linuxbsd/joypad_linux.cpp
@@ -311,16 +311,9 @@ void JoypadLinux::open_joypad(const char *p_path) {
return;
}
- //check if the device supports basic gamepad events, prevents certain keyboards from
- //being detected as joypads
+ // Check if the device supports basic gamepad events
if (!(test_bit(EV_KEY, evbit) && test_bit(EV_ABS, evbit) &&
- (test_bit(ABS_X, absbit) || test_bit(ABS_Y, absbit) || test_bit(ABS_HAT0X, absbit) ||
- test_bit(ABS_GAS, absbit) || test_bit(ABS_RUDDER, absbit)) &&
- (test_bit(BTN_A, keybit) || test_bit(BTN_THUMBL, keybit) ||
- test_bit(BTN_TRIGGER, keybit) || test_bit(BTN_1, keybit))) &&
- !(test_bit(EV_ABS, evbit) &&
- test_bit(ABS_X, absbit) && test_bit(ABS_Y, absbit) &&
- test_bit(ABS_RX, absbit) && test_bit(ABS_RY, absbit))) {
+ test_bit(ABS_X, absbit) && test_bit(ABS_Y, absbit))) {
close(fd);
return;
}
diff --git a/scene/main/window.cpp b/scene/main/window.cpp
index 8c985242f1..a5c5be8a44 100644
--- a/scene/main/window.cpp
+++ b/scene/main/window.cpp
@@ -527,11 +527,11 @@ void Window::_update_window_size() {
size.x = MAX(size_limit.x, size.x);
size.y = MAX(size_limit.y, size.y);
- if (max_size.x > 0 && max_size.x > min_size.x && max_size.x > size.x) {
+ if (max_size.x > 0 && max_size.x > min_size.x && size.x > max_size.x) {
size.x = max_size.x;
}
- if (max_size.y > 0 && max_size.y > min_size.y && max_size.y > size.y) {
+ if (max_size.y > 0 && max_size.y > min_size.y && size.y > max_size.y) {
size.y = max_size.y;
}
diff --git a/scene/resources/visual_shader.cpp b/scene/resources/visual_shader.cpp
index 8b84ce9e8c..e4851ad9f7 100644
--- a/scene/resources/visual_shader.cpp
+++ b/scene/resources/visual_shader.cpp
@@ -71,7 +71,15 @@ bool VisualShaderNode::is_output_port_connected(int p_port) const {
}
void VisualShaderNode::set_output_port_connected(int p_port, bool p_connected) {
- connected_output_ports[p_port] = p_connected;
+ if (p_connected) {
+ connected_output_ports[p_port] = true;
+ ++connected_output_count;
+ } else {
+ --connected_output_count;
+ if (connected_output_count == 0) {
+ connected_output_ports[p_port] = false;
+ }
+ }
}
bool VisualShaderNode::is_input_port_connected(int p_port) const {
diff --git a/scene/resources/visual_shader.h b/scene/resources/visual_shader.h
index d74269cfc6..05d8950be9 100644
--- a/scene/resources/visual_shader.h
+++ b/scene/resources/visual_shader.h
@@ -184,6 +184,7 @@ class VisualShaderNode : public Resource {
Map<int, Variant> default_input_values;
Map<int, bool> connected_input_ports;
Map<int, bool> connected_output_ports;
+ int connected_output_count = 0;
protected:
bool simple_decl;
diff --git a/tests/test_astar.cpp b/tests/test_astar.cpp
deleted file mode 100644
index cb5fcfe37b..0000000000
--- a/tests/test_astar.cpp
+++ /dev/null
@@ -1,409 +0,0 @@
-/*************************************************************************/
-/* test_astar.cpp */
-/*************************************************************************/
-/* This file is part of: */
-/* GODOT ENGINE */
-/* https://godotengine.org */
-/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
-/* */
-/* Permission is hereby granted, free of charge, to any person obtaining */
-/* a copy of this software and associated documentation files (the */
-/* "Software"), to deal in the Software without restriction, including */
-/* without limitation the rights to use, copy, modify, merge, publish, */
-/* distribute, sublicense, and/or sell copies of the Software, and to */
-/* permit persons to whom the Software is furnished to do so, subject to */
-/* the following conditions: */
-/* */
-/* The above copyright notice and this permission notice shall be */
-/* included in all copies or substantial portions of the Software. */
-/* */
-/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
-/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
-/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
-/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
-/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
-/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
-/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
-/*************************************************************************/
-
-#include "test_astar.h"
-
-#include "core/math/a_star.h"
-#include "core/math/math_funcs.h"
-#include "core/os/os.h"
-
-#include <math.h>
-#include <stdio.h>
-
-namespace TestAStar {
-
-class ABCX : public AStar {
-public:
- enum { A,
- B,
- C,
- X };
-
- ABCX() {
- add_point(A, Vector3(0, 0, 0));
- add_point(B, Vector3(1, 0, 0));
- add_point(C, Vector3(0, 1, 0));
- add_point(X, Vector3(0, 0, 1));
- connect_points(A, B);
- connect_points(A, C);
- connect_points(B, C);
- connect_points(X, A);
- }
-
- // Disable heuristic completely
- float _compute_cost(int p_from, int p_to) {
- if (p_from == A && p_to == C) {
- return 1000;
- }
- return 100;
- }
-};
-
-bool test_abc() {
- ABCX abcx;
- Vector<int> path = abcx.get_id_path(ABCX::A, ABCX::C);
- bool ok = path.size() == 3;
- int i = 0;
- ok = ok && path[i++] == ABCX::A;
- ok = ok && path[i++] == ABCX::B;
- ok = ok && path[i++] == ABCX::C;
- return ok;
-}
-
-bool test_abcx() {
- ABCX abcx;
- Vector<int> path = abcx.get_id_path(ABCX::X, ABCX::C);
- bool ok = path.size() == 4;
- int i = 0;
- ok = ok && path[i++] == ABCX::X;
- ok = ok && path[i++] == ABCX::A;
- ok = ok && path[i++] == ABCX::B;
- ok = ok && path[i++] == ABCX::C;
- return ok;
-}
-
-bool test_add_remove() {
- AStar a;
- bool ok = true;
-
- // Manual tests
- a.add_point(1, Vector3(0, 0, 0));
- a.add_point(2, Vector3(0, 1, 0));
- a.add_point(3, Vector3(1, 1, 0));
- a.add_point(4, Vector3(2, 0, 0));
- a.connect_points(1, 2, true);
- a.connect_points(1, 3, true);
- a.connect_points(1, 4, false);
-
- ok = ok && (a.are_points_connected(2, 1));
- ok = ok && (a.are_points_connected(4, 1));
- ok = ok && (a.are_points_connected(2, 1, false));
- ok = ok && (a.are_points_connected(4, 1, false) == false);
-
- a.disconnect_points(1, 2, true);
- ok = ok && (a.get_point_connections(1).size() == 2); // 3, 4
- ok = ok && (a.get_point_connections(2).size() == 0);
-
- a.disconnect_points(4, 1, false);
- ok = ok && (a.get_point_connections(1).size() == 2); // 3, 4
- ok = ok && (a.get_point_connections(4).size() == 0);
-
- a.disconnect_points(4, 1, true);
- ok = ok && (a.get_point_connections(1).size() == 1); // 3
- ok = ok && (a.get_point_connections(4).size() == 0);
-
- a.connect_points(2, 3, false);
- ok = ok && (a.get_point_connections(2).size() == 1); // 3
- ok = ok && (a.get_point_connections(3).size() == 1); // 1
-
- a.connect_points(2, 3, true);
- ok = ok && (a.get_point_connections(2).size() == 1); // 3
- ok = ok && (a.get_point_connections(3).size() == 2); // 1, 2
-
- a.disconnect_points(2, 3, false);
- ok = ok && (a.get_point_connections(2).size() == 0);
- ok = ok && (a.get_point_connections(3).size() == 2); // 1, 2
-
- a.connect_points(4, 3, true);
- ok = ok && (a.get_point_connections(3).size() == 3); // 1, 2, 4
- ok = ok && (a.get_point_connections(4).size() == 1); // 3
-
- a.disconnect_points(3, 4, false);
- ok = ok && (a.get_point_connections(3).size() == 2); // 1, 2
- ok = ok && (a.get_point_connections(4).size() == 1); // 3
-
- a.remove_point(3);
- ok = ok && (a.get_point_connections(1).size() == 0);
- ok = ok && (a.get_point_connections(2).size() == 0);
- ok = ok && (a.get_point_connections(4).size() == 0);
-
- a.add_point(0, Vector3(0, -1, 0));
- a.add_point(3, Vector3(2, 1, 0));
- // 0: (0, -1)
- // 1: (0, 0)
- // 2: (0, 1)
- // 3: (2, 1)
- // 4: (2, 0)
-
- // Tests for get_closest_position_in_segment
- a.connect_points(2, 3);
- ok = ok && (a.get_closest_position_in_segment(Vector3(0.5, 0.5, 0)) == Vector3(0.5, 1, 0));
-
- a.connect_points(3, 4);
- a.connect_points(0, 3);
- a.connect_points(1, 4);
- a.disconnect_points(1, 4, false);
- a.disconnect_points(4, 3, false);
- a.disconnect_points(3, 4, false);
- // Remaining edges: <2, 3>, <0, 3>, <1, 4> (directed)
- ok = ok && (a.get_closest_position_in_segment(Vector3(2, 0.5, 0)) == Vector3(1.75, 0.75, 0));
- ok = ok && (a.get_closest_position_in_segment(Vector3(-1, 0.2, 0)) == Vector3(0, 0, 0));
- ok = ok && (a.get_closest_position_in_segment(Vector3(3, 2, 0)) == Vector3(2, 1, 0));
-
- Math::seed(0);
-
- // Random tests for connectivity checks
- for (int i = 0; i < 20000; i++) {
- int u = Math::rand() % 5;
- int v = Math::rand() % 4;
- if (u == v) {
- v = 4;
- }
- if (Math::rand() % 2 == 1) {
- // Add a (possibly existing) directed edge and confirm connectivity
- a.connect_points(u, v, false);
- ok = ok && (a.are_points_connected(u, v, false));
- } else {
- // Remove a (possibly nonexistent) directed edge and confirm disconnectivity
- a.disconnect_points(u, v, false);
- ok = ok && (a.are_points_connected(u, v, false) == false);
- }
- }
-
- // Random tests for point removal
- for (int i = 0; i < 20000; i++) {
- a.clear();
- for (int j = 0; j < 5; j++) {
- a.add_point(j, Vector3(0, 0, 0));
- }
-
- // Add or remove random edges
- for (int j = 0; j < 10; j++) {
- int u = Math::rand() % 5;
- int v = Math::rand() % 4;
- if (u == v) {
- v = 4;
- }
- if (Math::rand() % 2 == 1) {
- a.connect_points(u, v, false);
- } else {
- a.disconnect_points(u, v, false);
- }
- }
-
- // Remove point 0
- a.remove_point(0);
- // White box: this will check all edges remaining in the segments set
- for (int j = 1; j < 5; j++) {
- ok = ok && (a.are_points_connected(0, j, true) == false);
- }
- }
-
- // It's been great work, cheers \(^ ^)/
- return ok;
-}
-
-bool test_solutions() {
- // Random stress tests with Floyd-Warshall
-
- const int N = 30;
- Math::seed(0);
-
- for (int test = 0; test < 1000; test++) {
- AStar a;
- Vector3 p[N];
- bool adj[N][N] = { { false } };
-
- // Assign initial coordinates
- for (int u = 0; u < N; u++) {
- p[u].x = Math::rand() % 100;
- p[u].y = Math::rand() % 100;
- p[u].z = Math::rand() % 100;
- a.add_point(u, p[u]);
- }
-
- // Generate a random sequence of operations
- for (int i = 0; i < 1000; i++) {
- // Pick two different vertices
- int u, v;
- u = Math::rand() % N;
- v = Math::rand() % (N - 1);
- if (u == v) {
- v = N - 1;
- }
-
- // Pick a random operation
- int op = Math::rand();
- switch (op % 9) {
- case 0:
- case 1:
- case 2:
- case 3:
- case 4:
- case 5:
- // Add edge (u, v); possibly bidirectional
- a.connect_points(u, v, op % 2);
- adj[u][v] = true;
- if (op % 2) {
- adj[v][u] = true;
- }
- break;
- case 6:
- case 7:
- // Remove edge (u, v); possibly bidirectional
- a.disconnect_points(u, v, op % 2);
- adj[u][v] = false;
- if (op % 2) {
- adj[v][u] = false;
- }
- break;
- case 8:
- // Remove point u and add it back; clears adjacent edges and changes coordinates
- a.remove_point(u);
- p[u].x = Math::rand() % 100;
- p[u].y = Math::rand() % 100;
- p[u].z = Math::rand() % 100;
- a.add_point(u, p[u]);
- for (v = 0; v < N; v++) {
- adj[u][v] = adj[v][u] = false;
- }
- break;
- }
- }
-
- // Floyd-Warshall
- float d[N][N];
- for (int u = 0; u < N; u++) {
- for (int v = 0; v < N; v++) {
- d[u][v] = (u == v || adj[u][v]) ? p[u].distance_to(p[v]) : INFINITY;
- }
- }
-
- for (int w = 0; w < N; w++) {
- for (int u = 0; u < N; u++) {
- for (int v = 0; v < N; v++) {
- if (d[u][v] > d[u][w] + d[w][v]) {
- d[u][v] = d[u][w] + d[w][v];
- }
- }
- }
- }
-
- // Display statistics
- int count = 0;
- for (int u = 0; u < N; u++) {
- for (int v = 0; v < N; v++) {
- if (adj[u][v]) {
- count++;
- }
- }
- }
- printf("Test #%4d: %3d edges, ", test + 1, count);
- count = 0;
- for (int u = 0; u < N; u++) {
- for (int v = 0; v < N; v++) {
- if (!Math::is_inf(d[u][v])) {
- count++;
- }
- }
- }
- printf("%3d/%d pairs of reachable points\n", count - N, N * (N - 1));
-
- // Check A*'s output
- bool match = true;
- for (int u = 0; u < N; u++) {
- for (int v = 0; v < N; v++) {
- if (u != v) {
- Vector<int> route = a.get_id_path(u, v);
- if (!Math::is_inf(d[u][v])) {
- // Reachable
- if (route.size() == 0) {
- printf("From %d to %d: A* did not find a path\n", u, v);
- match = false;
- goto exit;
- }
- float astar_dist = 0;
- for (int i = 1; i < route.size(); i++) {
- if (!adj[route[i - 1]][route[i]]) {
- printf("From %d to %d: edge (%d, %d) does not exist\n",
- u, v, route[i - 1], route[i]);
- match = false;
- goto exit;
- }
- astar_dist += p[route[i - 1]].distance_to(p[route[i]]);
- }
- if (!Math::is_equal_approx(astar_dist, d[u][v])) {
- printf("From %d to %d: Floyd-Warshall gives %.6f, A* gives %.6f\n",
- u, v, d[u][v], astar_dist);
- match = false;
- goto exit;
- }
- } else {
- // Unreachable
- if (route.size() > 0) {
- printf("From %d to %d: A* somehow found a nonexistent path\n", u, v);
- match = false;
- goto exit;
- }
- }
- }
- }
- }
-
- exit:
- if (!match) {
- return false;
- }
- }
- return true;
-}
-
-typedef bool (*TestFunc)();
-
-TestFunc test_funcs[] = {
- test_abc,
- test_abcx,
- test_add_remove,
- test_solutions,
- nullptr
-};
-
-MainLoop *test() {
- int count = 0;
- int passed = 0;
-
- while (true) {
- if (!test_funcs[count]) {
- break;
- }
- bool pass = test_funcs[count]();
- if (pass) {
- passed++;
- }
- OS::get_singleton()->print("\t%s\n", pass ? "PASS" : "FAILED");
-
- count++;
- }
- OS::get_singleton()->print("\n");
- OS::get_singleton()->print("Passed %i of %i tests\n", passed, count);
- return nullptr;
-}
-
-} // namespace TestAStar
diff --git a/tests/test_astar.h b/tests/test_astar.h
index 0992812c18..bef6127471 100644
--- a/tests/test_astar.h
+++ b/tests/test_astar.h
@@ -31,11 +31,338 @@
#ifndef TEST_ASTAR_H
#define TEST_ASTAR_H
-#include "core/os/main_loop.h"
+#include "core/math/a_star.h"
+#include "core/math/math_funcs.h"
+#include "core/os/os.h"
+
+#include <math.h>
+#include <stdio.h>
+
+#include "tests/test_macros.h"
namespace TestAStar {
-MainLoop *test();
+class ABCX : public AStar {
+public:
+ enum {
+ A,
+ B,
+ C,
+ X,
+ };
+
+ ABCX() {
+ add_point(A, Vector3(0, 0, 0));
+ add_point(B, Vector3(1, 0, 0));
+ add_point(C, Vector3(0, 1, 0));
+ add_point(X, Vector3(0, 0, 1));
+ connect_points(A, B);
+ connect_points(A, C);
+ connect_points(B, C);
+ connect_points(X, A);
+ }
+
+ // Disable heuristic completely.
+ float _compute_cost(int p_from, int p_to) {
+ if (p_from == A && p_to == C) {
+ return 1000;
+ }
+ return 100;
+ }
+};
+
+TEST_CASE("[AStar] ABC path") {
+ ABCX abcx;
+ Vector<int> path = abcx.get_id_path(ABCX::A, ABCX::C);
+ REQUIRE(path.size() == 3);
+ CHECK(path[0] == ABCX::A);
+ CHECK(path[1] == ABCX::B);
+ CHECK(path[2] == ABCX::C);
+}
+
+TEST_CASE("[AStar] ABCX path") {
+ ABCX abcx;
+ Vector<int> path = abcx.get_id_path(ABCX::X, ABCX::C);
+ REQUIRE(path.size() == 4);
+ CHECK(path[0] == ABCX::X);
+ CHECK(path[1] == ABCX::A);
+ CHECK(path[2] == ABCX::B);
+ CHECK(path[3] == ABCX::C);
+}
+
+TEST_CASE("[AStar] Add/Remove") {
+ AStar a;
+
+ // Manual tests.
+ a.add_point(1, Vector3(0, 0, 0));
+ a.add_point(2, Vector3(0, 1, 0));
+ a.add_point(3, Vector3(1, 1, 0));
+ a.add_point(4, Vector3(2, 0, 0));
+ a.connect_points(1, 2, true);
+ a.connect_points(1, 3, true);
+ a.connect_points(1, 4, false);
+
+ CHECK(a.are_points_connected(2, 1));
+ CHECK(a.are_points_connected(4, 1));
+ CHECK(a.are_points_connected(2, 1, false));
+ CHECK_FALSE(a.are_points_connected(4, 1, false));
+
+ a.disconnect_points(1, 2, true);
+ CHECK(a.get_point_connections(1).size() == 2); // 3, 4
+ CHECK(a.get_point_connections(2).size() == 0);
+
+ a.disconnect_points(4, 1, false);
+ CHECK(a.get_point_connections(1).size() == 2); // 3, 4
+ CHECK(a.get_point_connections(4).size() == 0);
+
+ a.disconnect_points(4, 1, true);
+ CHECK(a.get_point_connections(1).size() == 1); // 3
+ CHECK(a.get_point_connections(4).size() == 0);
+
+ a.connect_points(2, 3, false);
+ CHECK(a.get_point_connections(2).size() == 1); // 3
+ CHECK(a.get_point_connections(3).size() == 1); // 1
+
+ a.connect_points(2, 3, true);
+ CHECK(a.get_point_connections(2).size() == 1); // 3
+ CHECK(a.get_point_connections(3).size() == 2); // 1, 2
+
+ a.disconnect_points(2, 3, false);
+ CHECK(a.get_point_connections(2).size() == 0);
+ CHECK(a.get_point_connections(3).size() == 2); // 1, 2
+
+ a.connect_points(4, 3, true);
+ CHECK(a.get_point_connections(3).size() == 3); // 1, 2, 4
+ CHECK(a.get_point_connections(4).size() == 1); // 3
+
+ a.disconnect_points(3, 4, false);
+ CHECK(a.get_point_connections(3).size() == 2); // 1, 2
+ CHECK(a.get_point_connections(4).size() == 1); // 3
+
+ a.remove_point(3);
+ CHECK(a.get_point_connections(1).size() == 0);
+ CHECK(a.get_point_connections(2).size() == 0);
+ CHECK(a.get_point_connections(4).size() == 0);
+
+ a.add_point(0, Vector3(0, -1, 0));
+ a.add_point(3, Vector3(2, 1, 0));
+ // 0: (0, -1)
+ // 1: (0, 0)
+ // 2: (0, 1)
+ // 3: (2, 1)
+ // 4: (2, 0)
+
+ // Tests for get_closest_position_in_segment.
+ a.connect_points(2, 3);
+ CHECK(a.get_closest_position_in_segment(Vector3(0.5, 0.5, 0)) == Vector3(0.5, 1, 0));
+
+ a.connect_points(3, 4);
+ a.connect_points(0, 3);
+ a.connect_points(1, 4);
+ a.disconnect_points(1, 4, false);
+ a.disconnect_points(4, 3, false);
+ a.disconnect_points(3, 4, false);
+ // Remaining edges: <2, 3>, <0, 3>, <1, 4> (directed).
+ CHECK(a.get_closest_position_in_segment(Vector3(2, 0.5, 0)) == Vector3(1.75, 0.75, 0));
+ CHECK(a.get_closest_position_in_segment(Vector3(-1, 0.2, 0)) == Vector3(0, 0, 0));
+ CHECK(a.get_closest_position_in_segment(Vector3(3, 2, 0)) == Vector3(2, 1, 0));
+
+ Math::seed(0);
+
+ // Random tests for connectivity checks
+ for (int i = 0; i < 20000; i++) {
+ int u = Math::rand() % 5;
+ int v = Math::rand() % 4;
+ if (u == v) {
+ v = 4;
+ }
+ if (Math::rand() % 2 == 1) {
+ // Add a (possibly existing) directed edge and confirm connectivity.
+ a.connect_points(u, v, false);
+ CHECK(a.are_points_connected(u, v, false));
+ } else {
+ // Remove a (possibly nonexistent) directed edge and confirm disconnectivity.
+ a.disconnect_points(u, v, false);
+ CHECK_FALSE(a.are_points_connected(u, v, false));
+ }
+ }
+
+ // Random tests for point removal.
+ for (int i = 0; i < 20000; i++) {
+ a.clear();
+ for (int j = 0; j < 5; j++) {
+ a.add_point(j, Vector3(0, 0, 0));
+ }
+
+ // Add or remove random edges.
+ for (int j = 0; j < 10; j++) {
+ int u = Math::rand() % 5;
+ int v = Math::rand() % 4;
+ if (u == v) {
+ v = 4;
+ }
+ if (Math::rand() % 2 == 1) {
+ a.connect_points(u, v, false);
+ } else {
+ a.disconnect_points(u, v, false);
+ }
+ }
+
+ // Remove point 0.
+ a.remove_point(0);
+ // White box: this will check all edges remaining in the segments set.
+ for (int j = 1; j < 5; j++) {
+ CHECK_FALSE(a.are_points_connected(0, j, true));
+ }
+ }
+ // It's been great work, cheers. \(^ ^)/
+}
+
+TEST_CASE("[Stress][AStar] Find paths") {
+ // Random stress tests with Floyd-Warshall.
+ const int N = 30;
+ Math::seed(0);
+
+ for (int test = 0; test < 1000; test++) {
+ AStar a;
+ Vector3 p[N];
+ bool adj[N][N] = { { false } };
+
+ // Assign initial coordinates.
+ for (int u = 0; u < N; u++) {
+ p[u].x = Math::rand() % 100;
+ p[u].y = Math::rand() % 100;
+ p[u].z = Math::rand() % 100;
+ a.add_point(u, p[u]);
+ }
+ // Generate a random sequence of operations.
+ for (int i = 0; i < 1000; i++) {
+ // Pick two different vertices.
+ int u, v;
+ u = Math::rand() % N;
+ v = Math::rand() % (N - 1);
+ if (u == v) {
+ v = N - 1;
+ }
+ // Pick a random operation.
+ int op = Math::rand();
+ switch (op % 9) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ // Add edge (u, v); possibly bidirectional.
+ a.connect_points(u, v, op % 2);
+ adj[u][v] = true;
+ if (op % 2) {
+ adj[v][u] = true;
+ }
+ break;
+ case 6:
+ case 7:
+ // Remove edge (u, v); possibly bidirectional.
+ a.disconnect_points(u, v, op % 2);
+ adj[u][v] = false;
+ if (op % 2) {
+ adj[v][u] = false;
+ }
+ break;
+ case 8:
+ // Remove point u and add it back; clears adjacent edges and changes coordinates.
+ a.remove_point(u);
+ p[u].x = Math::rand() % 100;
+ p[u].y = Math::rand() % 100;
+ p[u].z = Math::rand() % 100;
+ a.add_point(u, p[u]);
+ for (v = 0; v < N; v++) {
+ adj[u][v] = adj[v][u] = false;
+ }
+ break;
+ }
+ }
+ // Floyd-Warshall.
+ float d[N][N];
+ for (int u = 0; u < N; u++) {
+ for (int v = 0; v < N; v++) {
+ d[u][v] = (u == v || adj[u][v]) ? p[u].distance_to(p[v]) : INFINITY;
+ }
+ }
+ for (int w = 0; w < N; w++) {
+ for (int u = 0; u < N; u++) {
+ for (int v = 0; v < N; v++) {
+ if (d[u][v] > d[u][w] + d[w][v]) {
+ d[u][v] = d[u][w] + d[w][v];
+ }
+ }
+ }
+ }
+ // Display statistics.
+ int count = 0;
+ for (int u = 0; u < N; u++) {
+ for (int v = 0; v < N; v++) {
+ if (adj[u][v]) {
+ count++;
+ }
+ }
+ }
+ print_verbose(vformat("Test #%4d: %3d edges, ", test + 1, count));
+ count = 0;
+ for (int u = 0; u < N; u++) {
+ for (int v = 0; v < N; v++) {
+ if (!Math::is_inf(d[u][v])) {
+ count++;
+ }
+ }
+ }
+ print_verbose(vformat("%3d/%d pairs of reachable points\n", count - N, N * (N - 1)));
+
+ // Check A*'s output.
+ bool match = true;
+ for (int u = 0; u < N; u++) {
+ for (int v = 0; v < N; v++) {
+ if (u != v) {
+ Vector<int> route = a.get_id_path(u, v);
+ if (!Math::is_inf(d[u][v])) {
+ // Reachable.
+ if (route.size() == 0) {
+ print_verbose(vformat("From %d to %d: A* did not find a path\n", u, v));
+ match = false;
+ goto exit;
+ }
+ float astar_dist = 0;
+ for (int i = 1; i < route.size(); i++) {
+ if (!adj[route[i - 1]][route[i]]) {
+ print_verbose(vformat("From %d to %d: edge (%d, %d) does not exist\n",
+ u, v, route[i - 1], route[i]));
+ match = false;
+ goto exit;
+ }
+ astar_dist += p[route[i - 1]].distance_to(p[route[i]]);
+ }
+ if (!Math::is_equal_approx(astar_dist, d[u][v])) {
+ print_verbose(vformat("From %d to %d: Floyd-Warshall gives %.6f, A* gives %.6f\n",
+ u, v, d[u][v], astar_dist));
+ match = false;
+ goto exit;
+ }
+ } else {
+ // Unreachable.
+ if (route.size() > 0) {
+ print_verbose(vformat("From %d to %d: A* somehow found a nonexistent path\n", u, v));
+ match = false;
+ goto exit;
+ }
+ }
+ }
+ }
+ }
+ exit:
+ CHECK_MESSAGE(match, "Found all paths.");
+ }
}
-#endif
+} // namespace TestAStar
+
+#endif // TEST_ASTAR_H
diff --git a/tests/test_color.h b/tests/test_color.h
new file mode 100644
index 0000000000..3633f2746d
--- /dev/null
+++ b/tests/test_color.h
@@ -0,0 +1,213 @@
+/*************************************************************************/
+/* test_color.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TEST_COLOR_H
+#define TEST_COLOR_H
+
+#include "core/color.h"
+
+#include "thirdparty/doctest/doctest.h"
+
+namespace TestColor {
+
+TEST_CASE("[Color] Constructor methods") {
+ const Color blue_rgba = Color(0.25098, 0.376471, 1, 0.501961);
+ // HTML currently uses ARGB notation, which is contrary to the CSS standard.
+ // This may be changed to RGBA in 4.0.
+ const Color blue_html = Color::html("#804060ff");
+ const Color blue_hex = Color::hex(0x4060ff80);
+ const Color blue_hex64 = Color::hex64(0x4040'6060'ffff'8080);
+
+ CHECK_MESSAGE(
+ blue_rgba.is_equal_approx(blue_html),
+ "Creation with HTML notation should result in components approximately equal to the default constructor.");
+ CHECK_MESSAGE(
+ blue_rgba.is_equal_approx(blue_hex),
+ "Creation with a 32-bit hexadecimal number should result in components approximately equal to the default constructor.");
+ CHECK_MESSAGE(
+ blue_rgba.is_equal_approx(blue_hex64),
+ "Creation with a 64-bit hexadecimal number should result in components approximately equal to the default constructor.");
+
+ ERR_PRINT_OFF;
+ const Color html_invalid = Color::html("invalid");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ html_invalid.is_equal_approx(Color()),
+ "Creation with invalid HTML notation should result in a Color with the default values.");
+
+ const Color green_rgba = Color(0, 1, 0, 0.25);
+ const Color green_hsva = Color(0, 0, 0).from_hsv(120 / 360.0, 1, 1, 0.25);
+
+ CHECK_MESSAGE(
+ green_rgba.is_equal_approx(green_hsva),
+ "Creation with HSV notation should result in components approximately equal to the default constructor.");
+}
+
+TEST_CASE("[Color] Operators") {
+ const Color blue = Color(0.2, 0.2, 1);
+ const Color dark_red = Color(0.3, 0.1, 0.1);
+
+ // Color components may be negative. Also, the alpha component may be greater than 1.0.
+ CHECK_MESSAGE(
+ (blue + dark_red).is_equal_approx(Color(0.5, 0.3, 1.1, 2)),
+ "Color addition should behave as expected.");
+ CHECK_MESSAGE(
+ (blue - dark_red).is_equal_approx(Color(-0.1, 0.1, 0.9, 0)),
+ "Color subtraction should behave as expected.");
+ CHECK_MESSAGE(
+ (blue * 2).is_equal_approx(Color(0.4, 0.4, 2, 2)),
+ "Color multiplication with a scalar should behave as expected.");
+ CHECK_MESSAGE(
+ (blue / 2).is_equal_approx(Color(0.1, 0.1, 0.5, 0.5)),
+ "Color division with a scalar should behave as expected.");
+ CHECK_MESSAGE(
+ (blue * dark_red).is_equal_approx(Color(0.06, 0.02, 0.1)),
+ "Color multiplication with another Color should behave as expected.");
+ CHECK_MESSAGE(
+ (blue / dark_red).is_equal_approx(Color(0.666667, 2, 10)),
+ "Color division with another Color should behave as expected.");
+ CHECK_MESSAGE(
+ (-blue).is_equal_approx(Color(0.8, 0.8, 0, 0)),
+ "Color negation should behave as expected (affecting the alpha channel, unlike `invert()`).");
+}
+
+TEST_CASE("[Color] Reading methods") {
+ const Color dark_blue = Color(0, 0, 0.5, 0.4);
+
+ CHECK_MESSAGE(
+ Math::is_equal_approx(dark_blue.get_h(), 240 / 360.0),
+ "The returned HSV hue should match the expected value.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(dark_blue.get_s(), 1),
+ "The returned HSV saturation should match the expected value.");
+ CHECK_MESSAGE(
+ Math::is_equal_approx(dark_blue.get_v(), 0.5),
+ "The returned HSV value should match the expected value.");
+}
+
+TEST_CASE("[Color] Conversion methods") {
+ const Color cyan = Color(0, 1, 1);
+ const Color cyan_transparent = Color(0, 1, 1, 0);
+
+ CHECK_MESSAGE(
+ cyan.to_html() == "ff00ffff",
+ "The returned RGB HTML color code should match the expected value.");
+ CHECK_MESSAGE(
+ cyan_transparent.to_html() == "0000ffff",
+ "The returned RGBA HTML color code should match the expected value.");
+ CHECK_MESSAGE(
+ cyan.to_argb32() == 0xff00ffff,
+ "The returned 32-bit RGB number should match the expected value.");
+ CHECK_MESSAGE(
+ cyan.to_abgr32() == 0xffffff00,
+ "The returned 32-bit BGR number should match the expected value.");
+ CHECK_MESSAGE(
+ cyan.to_rgba32() == 0x00ffffff,
+ "The returned 32-bit BGR number should match the expected value.");
+ CHECK_MESSAGE(
+ cyan.to_argb64() == 0xffff'0000'ffff'ffff,
+ "The returned 64-bit RGB number should match the expected value.");
+ CHECK_MESSAGE(
+ cyan.to_abgr64() == 0xffff'ffff'ffff'0000,
+ "The returned 64-bit BGR number should match the expected value.");
+ CHECK_MESSAGE(
+ cyan.to_rgba64() == 0x0000'ffff'ffff'ffff,
+ "The returned 64-bit BGR number should match the expected value.");
+ CHECK_MESSAGE(
+ String(cyan) == "0, 1, 1, 1",
+ "The string representation should match the expected value.");
+}
+
+TEST_CASE("[Color] Named colors") {
+ CHECK_MESSAGE(
+ Color::named("red").is_equal_approx(Color(1, 0, 0)),
+ "The named color \"red\" should match the expected value.");
+
+ // Named colors have their names automatically normalized.
+ CHECK_MESSAGE(
+ Color::named("white_smoke").is_equal_approx(Color(0.96, 0.96, 0.96)),
+ "The named color \"white_smoke\" should match the expected value.");
+ CHECK_MESSAGE(
+ Color::named("Slate Blue").is_equal_approx(Color(0.42, 0.35, 0.80)),
+ "The named color \"Slate Blue\" should match the expected value.");
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ Color::named("doesn't exist").is_equal_approx(Color()),
+ "The invalid named color \"doesn't exist\" should result in a Color with the default values.");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[Color] Validation methods") {
+ CHECK_MESSAGE(
+ Color::html_is_valid("#4080ff"),
+ "Valid HTML color (with leading #) should be considered valid.");
+ CHECK_MESSAGE(
+ Color::html_is_valid("4080ff"),
+ "Valid HTML color (without leading #) should be considered valid.");
+ CHECK_MESSAGE(
+ !Color::html_is_valid("12345"),
+ "Invalid HTML color should be considered invalid.");
+ CHECK_MESSAGE(
+ !Color::html_is_valid("#fuf"),
+ "Invalid HTML color should be considered invalid.");
+}
+
+TEST_CASE("[Color] Manipulation methods") {
+ const Color blue = Color(0, 0, 1, 0.4);
+
+ CHECK_MESSAGE(
+ blue.inverted().is_equal_approx(Color(1, 1, 0, 0.4)),
+ "Inverted color should have its red, green and blue components inverted.");
+ CHECK_MESSAGE(
+ blue.contrasted().is_equal_approx(Color(0.5, 0.5, 0.5, 0.4)),
+ "Contrasted pure blue should be fully gray.");
+
+ const Color purple = Color(0.5, 0.2, 0.5, 0.25);
+
+ CHECK_MESSAGE(
+ purple.lightened(0.2).is_equal_approx(Color(0.6, 0.36, 0.6, 0.25)),
+ "Color should be lightened by the expected amount.");
+ CHECK_MESSAGE(
+ purple.darkened(0.2).is_equal_approx(Color(0.4, 0.16, 0.4, 0.25)),
+ "Color should be darkened by the expected amount.");
+
+ const Color red = Color(1, 0, 0, 0.2);
+ const Color yellow = Color(1, 1, 0, 0.8);
+
+ CHECK_MESSAGE(
+ red.lerp(yellow, 0.5).is_equal_approx(Color(1, 0.5, 0, 0.5)),
+ "Red interpolated with yellow should be orange (with interpolated alpha).");
+}
+
+} // namespace TestColor
+
+#endif // TEST_COLOR_H
diff --git a/tests/test_main.cpp b/tests/test_main.cpp
index 0d83c3ced3..137882841c 100644
--- a/tests/test_main.cpp
+++ b/tests/test_main.cpp
@@ -35,6 +35,7 @@
#include "test_astar.h"
#include "test_basis.h"
#include "test_class_db.h"
+#include "test_color.h"
#include "test_gdscript.h"
#include "test_gui.h"
#include "test_math.h"
@@ -69,7 +70,8 @@ int test_main(int argc, char *argv[]) {
char **args = new char *[valid_arguments.size()];
for (int x = 0; x < valid_arguments.size(); x++) {
// Operation to convert Godot string to non wchar string.
- const char *str = valid_arguments[x].utf8().ptr();
+ CharString cs = valid_arguments[x].utf8();
+ const char *str = cs.get_data();
// Allocate the string copy.
args[x] = new char[strlen(str) + 1];
// Copy this into memory.
@@ -82,6 +84,9 @@ int test_main(int argc, char *argv[]) {
test_context.setOption("abort-after", 5);
test_context.setOption("no-breaks", true);
+ for (int x = 0; x < valid_arguments.size(); x++) {
+ delete[] args[x];
+ }
delete[] args;
return test_context.run();