diff options
Diffstat (limited to 'modules/mono')
28 files changed, 492 insertions, 173 deletions
diff --git a/modules/mono/.gitignore b/modules/mono/.gitignore index fa6d00cbbb..2d62f9f88a 100644 --- a/modules/mono/.gitignore +++ b/modules/mono/.gitignore @@ -1,2 +1,5 @@ # Do not ignore solution files inside the mono module. Overrides Godot's global gitignore. !*.sln + +# Generated by build_assemblies.py. +SdkPackageVersions.props diff --git a/modules/mono/Directory.Build.targets b/modules/mono/Directory.Build.targets index 98410b93ae..e666b3ac9d 100644 --- a/modules/mono/Directory.Build.targets +++ b/modules/mono/Directory.Build.targets @@ -2,6 +2,8 @@ <PropertyGroup> <_HasNuGetPackage Condition=" '$(_HasNuGetPackage)' == '' And '$(PackageId)' != '' And '$(GeneratePackageOnBuild.ToLower())' == 'true' ">true</_HasNuGetPackage> <_HasNuGetPackage Condition=" '$(_HasNuGetPackage)' == '' ">false</_HasNuGetPackage> + <_HasSymbolsNuGetPackage Condition=" '$(_HasSymbolsNuGetPackage)' == '' And '$(PackageId)' != '' And '$(IncludeSymbols.ToLower())' == 'true' And '$(SymbolPackageFormat)' == 'snupkg' ">true</_HasSymbolsNuGetPackage> + <_HasSymbolsNuGetPackage Condition=" '$(_HasSymbolsNuGetPackage)' == '' ">false</_HasSymbolsNuGetPackage> </PropertyGroup> <Target Name="CopyNupkgToSConsOutputDir" AfterTargets="Pack" Condition=" '$(_HasNuGetPackage)' == 'true' "> @@ -10,13 +12,15 @@ <GodotOutputDataDir>$(GodotSourceRootPath)\bin\GodotSharp\</GodotOutputDataDir> </PropertyGroup> <Copy SourceFiles="$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg" DestinationFolder="$(GodotOutputDataDir)Tools\nupkgs\" /> + <Copy Condition=" '$(_HasSymbolsNuGetPackage)' == 'true' " SourceFiles="$(PackageOutputPath)$(PackageId).$(PackageVersion).snupkg" DestinationFolder="$(GodotOutputDataDir)Tools\nupkgs\" /> </Target> <Target Name="PushNuGetPackagesToLocalSource" BeforeTargets="Pack" Condition=" '$(_HasNuGetPackage)' == 'true' And '$(PushNuGetToLocalSource)' != '' "> <Copy SourceFiles="$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg" DestinationFolder="$(PushNuGetToLocalSource)\" /> + <Copy Condition=" '$(_HasSymbolsNuGetPackage)' == 'true' " SourceFiles="$(PackageOutputPath)$(PackageId).$(PackageVersion).snupkg" DestinationFolder="$(PushNuGetToLocalSource)\" /> </Target> <Target Name="ClearNuGetLocalPackageCache" BeforeTargets="Pack" Condition=" '$(_HasNuGetPackage)' == 'true' And '$(ClearNuGetLocalCache.ToLower())' == 'true' "> - <RemoveDir Directories="$(NugetPackageRoot)/$(PackageId.ToLower())/$(PackageVersion)"/> + <RemoveDir Directories="$(NugetPackageRoot)/$(PackageId.ToLower())/$(PackageVersion)" /> </Target> </Project> diff --git a/modules/mono/SCsub b/modules/mono/SCsub index 7764ba0b45..a4667f784d 100644 --- a/modules/mono/SCsub +++ b/modules/mono/SCsub @@ -26,6 +26,6 @@ if env["platform"] in ["macos", "ios"]: elif env["platform"] == "android": env_mono.add_source_files(env.modules_sources, "mono_gd/android_mono_config.gen.cpp") -if env["tools"]: +if env.editor_build: env_mono.add_source_files(env.modules_sources, "editor/*.cpp") SConscript("editor/script_templates/SCsub") diff --git a/modules/mono/SdkPackageVersions.props b/modules/mono/SdkPackageVersions.props deleted file mode 100644 index 65094aa34f..0000000000 --- a/modules/mono/SdkPackageVersions.props +++ /dev/null @@ -1,8 +0,0 @@ -<Project> - <PropertyGroup> - <PackageFloatingVersion_Godot>4.0.*-*</PackageFloatingVersion_Godot> - <PackageVersion_GodotSharp>4.0.0-dev</PackageVersion_GodotSharp> - <PackageVersion_Godot_NET_Sdk>4.0.0-dev8</PackageVersion_Godot_NET_Sdk> - <PackageVersion_Godot_SourceGenerators>4.0.0-dev8</PackageVersion_Godot_SourceGenerators> - </PropertyGroup> -</Project> diff --git a/modules/mono/build_scripts/build_assemblies.py b/modules/mono/build_scripts/build_assemblies.py index d78a9c7db8..7343af0b39 100755 --- a/modules/mono/build_scripts/build_assemblies.py +++ b/modules/mono/build_scripts/build_assemblies.py @@ -5,6 +5,7 @@ import os.path import shlex import subprocess from dataclasses import dataclass +from typing import Optional, List def find_dotnet_cli(): @@ -150,10 +151,7 @@ def find_any_msbuild_tool(mono_prefix): return None -def run_msbuild(tools: ToolsLocation, sln: str, msbuild_args: [str] = None): - if msbuild_args is None: - msbuild_args = [] - +def run_msbuild(tools: ToolsLocation, sln: str, msbuild_args: Optional[List[str]] = None): using_msbuild_mono = False # Preference order: dotnet CLI > Standalone MSBuild > Mono's MSBuild @@ -169,7 +167,7 @@ def run_msbuild(tools: ToolsLocation, sln: str, msbuild_args: [str] = None): args += [sln] - if len(msbuild_args) > 0: + if msbuild_args: args += msbuild_args print("Running MSBuild: ", " ".join(shlex.quote(arg) for arg in args), flush=True) @@ -258,7 +256,57 @@ def build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, flo return 0 +def generate_sdk_package_versions(): + # I can't believe importing files in Python is so convoluted when not + # following the golden standard for packages/modules. + import os + import sys + from os.path import dirname + + # We want ../../../methods.py. + script_path = dirname(os.path.abspath(__file__)) + root_path = dirname(dirname(dirname(script_path))) + + sys.path.insert(0, root_path) + from methods import get_version_info + + version_info = get_version_info("") + sys.path.remove(root_path) + + version_str = "{major}.{minor}.{patch}".format(**version_info) + version_status = version_info["status"] + if version_status != "stable": # Pre-release + # If version was overridden to be e.g. "beta3", we insert a dot between + # "beta" and "3" to follow SemVer 2.0. + import re + + match = re.search(r"[\d]+$", version_status) + if match: + pos = match.start() + version_status = version_status[:pos] + "." + version_status[pos:] + version_str += "-" + version_status + + props = """<Project> + <PropertyGroup> + <PackageVersion_GodotSharp>{0}</PackageVersion_GodotSharp> + <PackageVersion_Godot_NET_Sdk>{0}</PackageVersion_Godot_NET_Sdk> + <PackageVersion_Godot_SourceGenerators>{0}</PackageVersion_Godot_SourceGenerators> + </PropertyGroup> +</Project> +""".format( + version_str + ) + + # We write in ../SdkPackageVersions.props. + with open(os.path.join(dirname(script_path), "SdkPackageVersions.props"), "w") as f: + f.write(props) + f.close() + + def build_all(msbuild_tool, module_dir, output_dir, godot_platform, dev_debug, push_nupkgs_local, float_size): + # Generate SdkPackageVersions.props + generate_sdk_package_versions() + # Godot API exit_code = build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, float_size) if exit_code != 0: diff --git a/modules/mono/build_scripts/mono_configure.py b/modules/mono/build_scripts/mono_configure.py index 5d63773096..5cec8f41f5 100644 --- a/modules/mono/build_scripts/mono_configure.py +++ b/modules/mono/build_scripts/mono_configure.py @@ -20,10 +20,7 @@ def configure(env, env_mono): # is_ios = env["platform"] == "ios" # is_ios_sim = is_ios and env["arch"] in ["x86_32", "x86_64"] - tools_enabled = env["tools"] - - if tools_enabled and not module_supports_tools_on(env["platform"]): - raise RuntimeError("This module does not currently support building for this platform with tools enabled") - - if env["tools"]: + if env.editor_build: + if not module_supports_tools_on(env["platform"]): + raise RuntimeError("This module does not currently support building for this platform for editor builds.") env_mono.Append(CPPDEFINES=["GD_MONO_HOT_RELOAD"]) diff --git a/modules/mono/class_db_api_json.cpp b/modules/mono/class_db_api_json.cpp index c4547b4323..1f4b085bfb 100644 --- a/modules/mono/class_db_api_json.cpp +++ b/modules/mono/class_db_api_json.cpp @@ -227,8 +227,7 @@ void class_db_api_to_json(const String &p_output_file, ClassDB::APIType p_api) { Ref<FileAccess> f = FileAccess::open(p_output_file, FileAccess::WRITE); ERR_FAIL_COND_MSG(f.is_null(), "Cannot open file '" + p_output_file + "'."); - JSON json; - f->store_string(json.stringify(classes_dict, "\t")); + f->store_string(JSON::stringify(classes_dict, "\t")); print_line(String() + "ClassDB API JSON written to: " + ProjectSettings::get_singleton()->globalize_path(p_output_file)); } diff --git a/modules/mono/config.py b/modules/mono/config.py index 15fe79ef8c..a36083b64b 100644 --- a/modules/mono/config.py +++ b/modules/mono/config.py @@ -7,7 +7,7 @@ def can_build(env, platform): if env["arch"].startswith("rv"): return False - if env["tools"]: + if env.editor_build: env.module_add_dependencies("mono", ["regex"]) return True diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 8fd3626a20..345d2e4694 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -1299,7 +1299,7 @@ GDNativeBool CSharpLanguage::_instance_binding_reference_callback(void *p_token, MonoGCHandleData &gchandle = script_binding.gchandle; - int refcount = rc_owner->reference_get_count(); + int refcount = rc_owner->get_reference_count(); if (!script_binding.inited) { return refcount == 0; @@ -1818,7 +1818,7 @@ void CSharpInstance::refcount_incremented() { RefCounted *rc_owner = Object::cast_to<RefCounted>(owner); - if (rc_owner->reference_get_count() > 1 && gchandle.is_weak()) { // The managed side also holds a reference, hence 1 instead of 0 + if (rc_owner->get_reference_count() > 1 && gchandle.is_weak()) { // The managed side also holds a reference, hence 1 instead of 0 // The reference count was increased after the managed side was the only one referencing our owner. // This means the owner is being referenced again by the unmanaged side, // so the owner must hold the managed side alive again to avoid it from being GCed. @@ -1849,7 +1849,7 @@ bool CSharpInstance::refcount_decremented() { RefCounted *rc_owner = Object::cast_to<RefCounted>(owner); - int refcount = rc_owner->reference_get_count(); + int refcount = rc_owner->get_reference_count(); if (refcount == 1 && !gchandle.is_weak()) { // The managed side also holds a reference, hence 1 instead of 0 // If owner owner is no longer referenced by the unmanaged side, @@ -1995,7 +1995,7 @@ CSharpInstance::~CSharpInstance() { #ifdef DEBUG_ENABLED // The "instance binding" holds a reference so the refcount should be at least 2 before `scope_keep_owner_alive` goes out of scope - CRASH_COND(rc_owner->reference_get_count() <= 1); + CRASH_COND(rc_owner->get_reference_count() <= 1); #endif } @@ -2398,9 +2398,9 @@ ScriptInstance *CSharpScript::instance_create(Object *p_this) { if (EngineDebugger::is_active()) { CSharpLanguage::get_singleton()->debug_break_parse(get_path(), 0, "Script inherits from native type '" + String(native_name) + - "', so it can't be instantiated in object of type: '" + p_this->get_class() + "'"); + "', so it can't be assigned to an object of type: '" + p_this->get_class() + "'"); } - ERR_FAIL_V_MSG(nullptr, "Script inherits from native type '" + String(native_name) + "', so it can't be instantiated in object of type: '" + p_this->get_class() + "'."); + ERR_FAIL_V_MSG(nullptr, "Script inherits from native type '" + String(native_name) + "', so it can't be assigned to an object of type: '" + p_this->get_class() + "'."); } Callable::CallError unchecked_error; diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h index d469c28d4a..e5e53acb07 100644 --- a/modules/mono/csharp_script.h +++ b/modules/mono/csharp_script.h @@ -48,20 +48,10 @@ class CSharpScript; class CSharpInstance; class CSharpLanguage; -#ifdef NO_SAFE_CAST -template <typename TScriptInstance, typename TScriptLanguage> -TScriptInstance *cast_script_instance(ScriptInstance *p_inst) { - if (!p_inst) { - return nullptr; - } - return p_inst->get_language() == TScriptLanguage::get_singleton() ? static_cast<TScriptInstance *>(p_inst) : nullptr; -} -#else template <typename TScriptInstance, typename TScriptLanguage> TScriptInstance *cast_script_instance(ScriptInstance *p_inst) { return dynamic_cast<TScriptInstance *>(p_inst); } -#endif #define CAST_CSHARP_INSTANCE(m_inst) (cast_script_instance<CSharpInstance, CSharpLanguage>(m_inst)) diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/EventHandlerSuffixSuppressor.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/EventHandlerSuffixSuppressor.cs new file mode 100644 index 0000000000..ddde730fa2 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/EventHandlerSuffixSuppressor.cs @@ -0,0 +1,53 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Godot.SourceGenerators +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class EventHandlerSuffixSuppressor : DiagnosticSuppressor + { + private static readonly SuppressionDescriptor _descriptor = new( + id: "GDSP0001", + suppressedDiagnosticId: "CA1711", + justification: "Signal delegates are used in events so the naming follows the guidelines."); + + public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => + ImmutableArray.Create(_descriptor); + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + foreach (var diagnostic in context.ReportedDiagnostics) + { + AnalyzeDiagnostic(context, diagnostic, context.CancellationToken); + } + } + + private static void AnalyzeDiagnostic(SuppressionAnalysisContext context, Diagnostic diagnostic, CancellationToken cancellationToken = default) + { + var location = diagnostic.Location; + var root = location.SourceTree?.GetRoot(cancellationToken); + var dds = root? + .FindNode(location.SourceSpan) + .DescendantNodesAndSelf() + .OfType<DelegateDeclarationSyntax>() + .FirstOrDefault(); + + if (dds == null) + return; + + var semanticModel = context.GetSemanticModel(dds.SyntaxTree); + var delegateSymbol = semanticModel.GetDeclaredSymbol(dds, cancellationToken); + if (delegateSymbol == null) + return; + + if (delegateSymbol.GetAttributes().Any(a => a.AttributeClass?.IsGodotSignalAttribute() ?? false)) + { + context.ReportSuppression(Suppression.Create(_descriptor, diagnostic)); + } + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs index de3b6c862a..8de12de23b 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs @@ -29,7 +29,7 @@ namespace Godot.SourceGenerators { while (symbol != null) { - if (symbol.ContainingAssembly.Name == assemblyName && + if (symbol.ContainingAssembly?.Name == assemblyName && symbol.ToString() == typeFullName) { return true; @@ -47,7 +47,7 @@ namespace Godot.SourceGenerators while (symbol != null) { - if (symbol.ContainingAssembly.Name == "GodotSharp") + if (symbol.ContainingAssembly?.Name == "GodotSharp") return symbol; symbol = symbol.BaseType; diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs index f31ded4d77..bd40675fd3 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs @@ -124,8 +124,8 @@ namespace Godot.SourceGenerators if (typeKind == TypeKind.Struct) { - if (type.ContainingAssembly.Name == "GodotSharp" && - type.ContainingNamespace.Name == "Godot") + if (type.ContainingAssembly?.Name == "GodotSharp" && + type.ContainingNamespace?.Name == "Godot") { return type switch { @@ -208,9 +208,9 @@ namespace Godot.SourceGenerators if (type.SimpleDerivesFrom(typeCache.GodotObjectType)) return MarshalType.GodotObjectOrDerived; - if (type.ContainingAssembly.Name == "GodotSharp") + if (type.ContainingAssembly?.Name == "GodotSharp") { - switch (type.ContainingNamespace.Name) + switch (type.ContainingNamespace?.Name) { case "Godot": return type switch @@ -220,7 +220,7 @@ namespace Godot.SourceGenerators _ => null }; case "Collections" - when type.ContainingNamespace.FullQualifiedName() == "Godot.Collections": + when type.ContainingNamespace?.FullQualifiedName() == "Godot.Collections": return type switch { { Name: "Dictionary" } => diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs index eeda1042ca..ea7cc3fe38 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs @@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis.Text; // TODO: // Determine a proper way to emit the signal. -// 'Emit(nameof(TheEvent))' creates a StringName everytime and has the overhead of string marshaling. +// 'Emit(nameof(TheEvent))' creates a StringName every time and has the overhead of string marshaling. // I haven't decided on the best option yet. Some possibilities: // - Expose the generated StringName fields to the user, for use with 'Emit(...)'. // - Generate a 'EmitSignalName' method for each event signal. diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs index ad4fce8daa..4d40724a83 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs @@ -69,51 +69,41 @@ namespace GodotTools.Build private void LoadIssuesFromFile(string csvFile) { - using (var file = new Godot.File()) + using var file = FileAccess.Open(csvFile, FileAccess.ModeFlags.Read); + + if (file == null) + return; + + while (!file.EofReached()) { - try - { - Error openError = file.Open(csvFile, Godot.File.ModeFlags.Read); + string[] csvColumns = file.GetCsvLine(); - if (openError != Error.Ok) - return; + if (csvColumns.Length == 1 && string.IsNullOrEmpty(csvColumns[0])) + return; - while (!file.EofReached()) - { - string[] csvColumns = file.GetCsvLine(); - - if (csvColumns.Length == 1 && string.IsNullOrEmpty(csvColumns[0])) - return; - - if (csvColumns.Length != 7) - { - GD.PushError($"Expected 7 columns, got {csvColumns.Length}"); - continue; - } - - var issue = new BuildIssue - { - Warning = csvColumns[0] == "warning", - File = csvColumns[1], - Line = int.Parse(csvColumns[2]), - Column = int.Parse(csvColumns[3]), - Code = csvColumns[4], - Message = csvColumns[5], - ProjectFile = csvColumns[6] - }; - - if (issue.Warning) - WarningCount += 1; - else - ErrorCount += 1; - - _issues.Add(issue); - } - } - finally + if (csvColumns.Length != 7) { - file.Close(); // Disposing it is not enough. We need to call Close() + GD.PushError($"Expected 7 columns, got {csvColumns.Length}"); + continue; } + + var issue = new BuildIssue + { + Warning = csvColumns[0] == "warning", + File = csvColumns[1], + Line = int.Parse(csvColumns[2]), + Column = int.Parse(csvColumns[3]), + Code = csvColumns[4], + Message = csvColumns[5], + ProjectFile = csvColumns[6] + }; + + if (issue.Warning) + WarningCount += 1; + else + ErrorCount += 1; + + _issues.Add(issue); } } diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index 95a44d3b7e..d29e0d69ab 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -3326,11 +3326,11 @@ bool BindingsGenerator::_arg_default_value_from_variant(const Variant &p_val, Ar r_iarg.def_param_mode = ArgumentInterface::NULLABLE_VAL; } break; case Variant::PROJECTION: { - Projection transform = p_val.operator Projection(); - if (transform == Projection()) { + Projection projection = p_val.operator Projection(); + if (projection == Projection()) { r_iarg.default_argument = "Projection.Identity"; } else { - r_iarg.default_argument = "new Projection(new Vector4" + transform.matrix[0].operator String() + ", new Vector4" + transform.matrix[1].operator String() + ", new Vector4" + transform.matrix[2].operator String() + ", new Vector4" + transform.matrix[3].operator String() + ")"; + r_iarg.default_argument = "new Projection(new Vector4" + projection.columns[0].operator String() + ", new Vector4" + projection.columns[1].operator String() + ", new Vector4" + projection.columns[2].operator String() + ", new Vector4" + projection.columns[3].operator String() + ")"; } r_iarg.def_param_mode = ArgumentInterface::NULLABLE_VAL; } break; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs index fbd59d649f..9c3bc51c44 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs @@ -4,6 +4,21 @@ using System.Runtime.InteropServices; namespace Godot { /// <summary> + /// Specifies which order Euler angle rotations should be in. + /// When composing, the order is the same as the letters. When decomposing, + /// the order is reversed (ex: YXZ decomposes Z first, then X, and Y last). + /// </summary> + public enum EulerOrder + { + XYZ, + XZY, + YXZ, + YZX, + ZXY, + ZYX + }; + + /// <summary> /// 3×3 matrix used for 3D rotation and scale. /// Almost always used as an orthogonal basis for a Transform. /// @@ -244,50 +259,258 @@ namespace Godot } /// <summary> - /// Returns the basis's rotation in the form of Euler angles - /// (in the YXZ convention: when *decomposing*, first Z, then X, and Y last). - /// The returned vector contains the rotation angles in - /// the format (X angle, Y angle, Z angle). + /// Returns the basis's rotation in the form of Euler angles. + /// The Euler order depends on the [param order] parameter, + /// by default it uses the YXZ convention: when decomposing, + /// first Z, then X, and Y last. The returned vector contains + /// the rotation angles in the format (X angle, Y angle, Z angle). /// /// Consider using the <see cref="GetRotationQuaternion"/> method instead, which /// returns a <see cref="Quaternion"/> quaternion instead of Euler angles. /// </summary> + /// <param name="order">The Euler order to use. By default, use YXZ order (most common).</param> /// <returns>A <see cref="Vector3"/> representing the basis rotation in Euler angles.</returns> - public Vector3 GetEuler() + public Vector3 GetEuler(EulerOrder order = EulerOrder.YXZ) { - Basis m = Orthonormalized(); - - Vector3 euler; - euler.z = 0.0f; - - real_t mzy = m.Row1[2]; - - if (mzy < 1.0f) + switch (order) { - if (mzy > -1.0f) + case EulerOrder.XYZ: { - euler.x = Mathf.Asin(-mzy); - euler.y = Mathf.Atan2(m.Row0[2], m.Row2[2]); - euler.z = Mathf.Atan2(m.Row1[0], m.Row1[1]); + // Euler angles in XYZ convention. + // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix + // + // rot = cy*cz -cy*sz sy + // cz*sx*sy+cx*sz cx*cz-sx*sy*sz -cy*sx + // -cx*cz*sy+sx*sz cz*sx+cx*sy*sz cx*cy + Vector3 euler; + real_t sy = Row0[2]; + if (sy < (1.0f - Mathf.Epsilon)) + { + if (sy > -(1.0f - Mathf.Epsilon)) + { + // is this a pure Y rotation? + if (Row1[0] == 0 && Row0[1] == 0 && Row1[2] == 0 && Row2[1] == 0 && Row1[1] == 1) + { + // return the simplest form (human friendlier in editor and scripts) + euler.x = 0; + euler.y = Mathf.Atan2(Row0[2], Row0[0]); + euler.z = 0; + } + else + { + euler.x = Mathf.Atan2(-Row1[2], Row2[2]); + euler.y = Mathf.Asin(sy); + euler.z = Mathf.Atan2(-Row0[1], Row0[0]); + } + } + else + { + euler.x = Mathf.Atan2(Row2[1], Row1[1]); + euler.y = -Mathf.Tau / 4.0f; + euler.z = 0.0f; + } + } + else + { + euler.x = Mathf.Atan2(Row2[1], Row1[1]); + euler.y = Mathf.Tau / 4.0f; + euler.z = 0.0f; + } + return euler; } - else + case EulerOrder.XZY: { - euler.x = Mathf.Pi * 0.5f; - euler.y = -Mathf.Atan2(-m.Row0[1], m.Row0[0]); + // Euler angles in XZY convention. + // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix + // + // rot = cz*cy -sz cz*sy + // sx*sy+cx*cy*sz cx*cz cx*sz*sy-cy*sx + // cy*sx*sz cz*sx cx*cy+sx*sz*sy + Vector3 euler; + real_t sz = Row0[1]; + if (sz < (1.0f - Mathf.Epsilon)) + { + if (sz > -(1.0f - Mathf.Epsilon)) + { + euler.x = Mathf.Atan2(Row2[1], Row1[1]); + euler.y = Mathf.Atan2(Row0[2], Row0[0]); + euler.z = Mathf.Asin(-sz); + } + else + { + // It's -1 + euler.x = -Mathf.Atan2(Row1[2], Row2[2]); + euler.y = 0.0f; + euler.z = Mathf.Tau / 4.0f; + } + } + else + { + // It's 1 + euler.x = -Mathf.Atan2(Row1[2], Row2[2]); + euler.y = 0.0f; + euler.z = -Mathf.Tau / 4.0f; + } + return euler; } - } - else - { - euler.x = -Mathf.Pi * 0.5f; - euler.y = -Mathf.Atan2(-m.Row0[1], m.Row0[0]); - } + case EulerOrder.YXZ: + { + // Euler angles in YXZ convention. + // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix + // + // rot = cy*cz+sy*sx*sz cz*sy*sx-cy*sz cx*sy + // cx*sz cx*cz -sx + // cy*sx*sz-cz*sy cy*cz*sx+sy*sz cy*cx + Vector3 euler; + real_t m12 = Row1[2]; + if (m12 < (1 - Mathf.Epsilon)) + { + if (m12 > -(1 - Mathf.Epsilon)) + { + // is this a pure X rotation? + if (Row1[0] == 0 && Row0[1] == 0 && Row0[2] == 0 && Row2[0] == 0 && Row0[0] == 1) + { + // return the simplest form (human friendlier in editor and scripts) + euler.x = Mathf.Atan2(-m12, Row1[1]); + euler.y = 0; + euler.z = 0; + } + else + { + euler.x = Mathf.Asin(-m12); + euler.y = Mathf.Atan2(Row0[2], Row2[2]); + euler.z = Mathf.Atan2(Row1[0], Row1[1]); + } + } + else + { // m12 == -1 + euler.x = Mathf.Tau / 4.0f; + euler.y = Mathf.Atan2(Row0[1], Row0[0]); + euler.z = 0; + } + } + else + { // m12 == 1 + euler.x = -Mathf.Tau / 4.0f; + euler.y = -Mathf.Atan2(Row0[1], Row0[0]); + euler.z = 0; + } - return euler; + return euler; + } + case EulerOrder.YZX: + { + // Euler angles in YZX convention. + // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix + // + // rot = cy*cz sy*sx-cy*cx*sz cx*sy+cy*sz*sx + // sz cz*cx -cz*sx + // -cz*sy cy*sx+cx*sy*sz cy*cx-sy*sz*sx + Vector3 euler; + real_t sz = Row1[0]; + if (sz < (1.0f - Mathf.Epsilon)) + { + if (sz > -(1.0f - Mathf.Epsilon)) + { + euler.x = Mathf.Atan2(-Row1[2], Row1[1]); + euler.y = Mathf.Atan2(-Row2[0], Row0[0]); + euler.z = Mathf.Asin(sz); + } + else + { + // It's -1 + euler.x = Mathf.Atan2(Row2[1], Row2[2]); + euler.y = 0.0f; + euler.z = -Mathf.Tau / 4.0f; + } + } + else + { + // It's 1 + euler.x = Mathf.Atan2(Row2[1], Row2[2]); + euler.y = 0.0f; + euler.z = Mathf.Tau / 4.0f; + } + return euler; + } + case EulerOrder.ZXY: + { + // Euler angles in ZXY convention. + // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix + // + // rot = cz*cy-sz*sx*sy -cx*sz cz*sy+cy*sz*sx + // cy*sz+cz*sx*sy cz*cx sz*sy-cz*cy*sx + // -cx*sy sx cx*cy + Vector3 euler; + real_t sx = Row2[1]; + if (sx < (1.0f - Mathf.Epsilon)) + { + if (sx > -(1.0f - Mathf.Epsilon)) + { + euler.x = Mathf.Asin(sx); + euler.y = Mathf.Atan2(-Row2[0], Row2[2]); + euler.z = Mathf.Atan2(-Row0[1], Row1[1]); + } + else + { + // It's -1 + euler.x = -Mathf.Tau / 4.0f; + euler.y = Mathf.Atan2(Row0[2], Row0[0]); + euler.z = 0; + } + } + else + { + // It's 1 + euler.x = Mathf.Tau / 4.0f; + euler.y = Mathf.Atan2(Row0[2], Row0[0]); + euler.z = 0; + } + return euler; + } + case EulerOrder.ZYX: + { + // Euler angles in ZYX convention. + // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix + // + // rot = cz*cy cz*sy*sx-cx*sz sz*sx+cz*cx*cy + // cy*sz cz*cx+sz*sy*sx cx*sz*sy-cz*sx + // -sy cy*sx cy*cx + Vector3 euler; + real_t sy = Row2[0]; + if (sy < (1.0f - Mathf.Epsilon)) + { + if (sy > -(1.0f - Mathf.Epsilon)) + { + euler.x = Mathf.Atan2(Row2[1], Row2[2]); + euler.y = Mathf.Asin(-sy); + euler.z = Mathf.Atan2(Row1[0], Row0[0]); + } + else + { + // It's -1 + euler.x = 0; + euler.y = Mathf.Tau / 4.0f; + euler.z = -Mathf.Atan2(Row0[1], Row1[1]); + } + } + else + { + // It's 1 + euler.x = 0; + euler.y = -Mathf.Tau / 4.0f; + euler.z = -Mathf.Atan2(Row0[1], Row1[1]); + } + return euler; + } + default: + throw new ArgumentOutOfRangeException(nameof(order)); + } } /// <summary> /// Returns the basis's rotation in the form of a quaternion. - /// See <see cref="GetEuler()"/> if you need Euler angles, but keep in + /// See <see cref="GetEuler"/> if you need Euler angles, but keep in /// mind that quaternions should generally be preferred to Euler angles. /// </summary> /// <returns>A <see cref="Quaternion"/> representing the basis's rotation.</returns> @@ -712,35 +935,6 @@ namespace Godot } /// <summary> - /// Constructs a pure rotation basis matrix from the given Euler angles - /// (in the YXZ convention: when *composing*, first Y, then X, and Z last), - /// given in the vector format as (X angle, Y angle, Z angle). - /// - /// Consider using the <see cref="Basis(Quaternion)"/> constructor instead, which - /// uses a <see cref="Quaternion"/> quaternion instead of Euler angles. - /// </summary> - /// <param name="eulerYXZ">The Euler angles to create the basis from.</param> - public Basis(Vector3 eulerYXZ) - { - real_t c; - real_t s; - - c = Mathf.Cos(eulerYXZ.x); - s = Mathf.Sin(eulerYXZ.x); - var xmat = new Basis(1, 0, 0, 0, c, -s, 0, s, c); - - c = Mathf.Cos(eulerYXZ.y); - s = Mathf.Sin(eulerYXZ.y); - var ymat = new Basis(c, 0, s, 0, 1, 0, -s, 0, c); - - c = Mathf.Cos(eulerYXZ.z); - s = Mathf.Sin(eulerYXZ.z); - var zmat = new Basis(c, -s, 0, s, c, 0, 0, 0, 1); - - this = ymat * xmat * zmat; - } - - /// <summary> /// Constructs a pure rotation basis matrix, rotated around the given <paramref name="axis"/> /// by <paramref name="angle"/> (in radians). The axis must be a normalized vector. /// </summary> @@ -800,6 +994,46 @@ namespace Godot } /// <summary> + /// Constructs a Basis matrix from Euler angles in the specified rotation order. By default, use YXZ order (most common). + /// </summary> + /// <param name="euler">The Euler angles to use.</param> + /// <param name="order">The order to compose the Euler angles.</param> + public static Basis FromEuler(Vector3 euler, EulerOrder order = EulerOrder.YXZ) + { + real_t c, s; + + c = Mathf.Cos(euler.x); + s = Mathf.Sin(euler.x); + Basis xmat = new Basis(new Vector3(1, 0, 0), new Vector3(0, c, s), new Vector3(0, -s, c)); + + c = Mathf.Cos(euler.y); + s = Mathf.Sin(euler.y); + Basis ymat = new Basis(new Vector3(c, 0, -s), new Vector3(0, 1, 0), new Vector3(s, 0, c)); + + c = Mathf.Cos(euler.z); + s = Mathf.Sin(euler.z); + Basis zmat = new Basis(new Vector3(c, s, 0), new Vector3(-s, c, 0), new Vector3(0, 0, 1)); + + switch (order) + { + case EulerOrder.XYZ: + return xmat * ymat * zmat; + case EulerOrder.XZY: + return xmat * zmat * ymat; + case EulerOrder.YXZ: + return ymat * xmat * zmat; + case EulerOrder.YZX: + return ymat * zmat * xmat; + case EulerOrder.ZXY: + return zmat * xmat * ymat; + case EulerOrder.ZYX: + return zmat * ymat * xmat; + default: + throw new ArgumentOutOfRangeException(nameof(order)); + } + } + + /// <summary> /// Constructs a pure scale basis matrix with no rotation or shearing. /// The scale values are set as the main diagonal of the matrix, /// and all of the other parts of the matrix are zero. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.cs index 1b7f5158fd..bdedd2e87a 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.cs @@ -72,7 +72,7 @@ namespace Godot /// <param name="delegate">Delegate method that will be called.</param> public Callable(Delegate @delegate) { - _target = null; + _target = @delegate?.Target as Object; _method = null; _delegate = @delegate; } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs index 3483a04c83..ee9e59f9fa 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs @@ -597,7 +597,7 @@ namespace Godot /// <exception name="ArgumentOutOfRangeException"> /// <paramref name="rgba"/> color code is invalid. /// </exception> - private static Color FromHTML(string rgba) + private static Color FromHTML(ReadOnlySpan<char> rgba) { Color c; if (rgba.Length == 0) @@ -611,7 +611,7 @@ namespace Godot if (rgba[0] == '#') { - rgba = rgba.Substring(1); + rgba = rgba.Slice(1); } // If enabled, use 1 hex digit per channel instead of 2. @@ -665,22 +665,22 @@ namespace Godot if (c.r < 0) { - throw new ArgumentOutOfRangeException("Invalid color code. Red part is not valid hexadecimal: " + rgba); + throw new ArgumentOutOfRangeException($"Invalid color code. Red part is not valid hexadecimal: {rgba}"); } if (c.g < 0) { - throw new ArgumentOutOfRangeException("Invalid color code. Green part is not valid hexadecimal: " + rgba); + throw new ArgumentOutOfRangeException($"Invalid color code. Green part is not valid hexadecimal: {rgba}"); } if (c.b < 0) { - throw new ArgumentOutOfRangeException("Invalid color code. Blue part is not valid hexadecimal: " + rgba); + throw new ArgumentOutOfRangeException($"Invalid color code. Blue part is not valid hexadecimal: {rgba}"); } if (c.a < 0) { - throw new ArgumentOutOfRangeException("Invalid color code. Alpha part is not valid hexadecimal: " + rgba); + throw new ArgumentOutOfRangeException($"Invalid color code. Alpha part is not valid hexadecimal: {rgba}"); } return c; } @@ -817,9 +817,9 @@ namespace Godot value = max; } - private static int ParseCol4(string str, int ofs) + private static int ParseCol4(ReadOnlySpan<char> str, int index) { - char character = str[ofs]; + char character = str[index]; if (character >= '0' && character <= '9') { @@ -836,9 +836,9 @@ namespace Godot return -1; } - private static int ParseCol8(string str, int ofs) + private static int ParseCol8(ReadOnlySpan<char> str, int index) { - return ParseCol4(str, ofs) * 16 + ParseCol4(str, ofs + 1); + return ParseCol4(str, index) * 16 + ParseCol4(str, index + 1); } private static string ToHex32(float val) @@ -847,16 +847,16 @@ namespace Godot return b.HexEncode(); } - internal static bool HtmlIsValid(string color) + internal static bool HtmlIsValid(ReadOnlySpan<char> color) { - if (string.IsNullOrEmpty(color)) + if (color.IsEmpty) { return false; } if (color[0] == '#') { - color = color.Substring(1); + color = color.Slice(1); } // Check if the amount of hex digits is valid. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs index 140fc167ba..76b186cd15 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs @@ -721,8 +721,9 @@ namespace Godot.NativeInterop if (p_managed_callable.Delegate != null) { var gcHandle = CustomGCHandle.AllocStrong(p_managed_callable.Delegate); + IntPtr objectPtr = p_managed_callable.Target != null ? Object.GetPtr(p_managed_callable.Target) : IntPtr.Zero; NativeFuncs.godotsharp_callable_new_with_delegate( - GCHandle.ToIntPtr(gcHandle), out godot_callable callable); + GCHandle.ToIntPtr(gcHandle), objectPtr, out godot_callable callable); return callable; } else diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs index bd00611383..20ede9f0dd 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs @@ -141,7 +141,7 @@ namespace Godot.NativeInterop public static partial void godotsharp_packed_string_array_add(ref godot_packed_string_array r_dest, in godot_string p_element); - public static partial void godotsharp_callable_new_with_delegate(IntPtr p_delegate_handle, + public static partial void godotsharp_callable_new_with_delegate(IntPtr p_delegate_handle, IntPtr p_object, out godot_callable r_callable); internal static partial godot_bool godotsharp_callable_get_data_for_marshalling(in godot_callable p_callable, diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs index 44f951e314..d77baab24b 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs @@ -1245,12 +1245,12 @@ namespace Godot /// <summary> /// If the string is a path, this concatenates <paramref name="file"/> /// at the end of the string as a subpath. - /// E.g. <c>"this/is".PlusFile("path") == "this/is/path"</c>. + /// E.g. <c>"this/is".PathJoin("path") == "this/is/path"</c>. /// </summary> /// <param name="instance">The path that will be concatenated.</param> /// <param name="file">File name to concatenate with the path.</param> /// <returns>The concatenated path with the given file name.</returns> - public static string PlusFile(this string instance, string file) + public static string PathJoin(this string instance, string file) { if (instance.Length > 0 && instance[instance.Length - 1] == '/') return instance + file; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index 5827d3e591..a63b668387 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -27,6 +27,8 @@ <PackageLicenseExpression>MIT</PackageLicenseExpression> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> + <IncludeSymbols>true</IncludeSymbols> + <SymbolPackageFormat>snupkg</SymbolPackageFormat> </PropertyGroup> <ItemGroup> <!-- SdkPackageVersions.props for easy access --> diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj index 5d69ad8ec6..8f623625fc 100644 --- a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj @@ -22,6 +22,8 @@ <PackageLicenseExpression>MIT</PackageLicenseExpression> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> + <IncludeSymbols>true</IncludeSymbols> + <SymbolPackageFormat>snupkg</SymbolPackageFormat> </PropertyGroup> <PropertyGroup> <DefineConstants>$(DefineConstants);GODOT</DefineConstants> diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp index 276701cdaa..2717b945f6 100644 --- a/modules/mono/glue/runtime_interop.cpp +++ b/modules/mono/glue/runtime_interop.cpp @@ -447,9 +447,10 @@ void godotsharp_packed_string_array_add(PackedStringArray *r_dest, const String r_dest->append(*p_element); } -void godotsharp_callable_new_with_delegate(GCHandleIntPtr p_delegate_handle, Callable *r_callable) { +void godotsharp_callable_new_with_delegate(GCHandleIntPtr p_delegate_handle, const Object *p_object, Callable *r_callable) { // TODO: Use pooling for ManagedCallable instances. - CallableCustom *managed_callable = memnew(ManagedCallable(p_delegate_handle)); + ObjectID objid = p_object ? p_object->get_instance_id() : ObjectID(); + CallableCustom *managed_callable = memnew(ManagedCallable(p_delegate_handle, objid)); memnew_placement(r_callable, Callable(managed_callable)); } diff --git a/modules/mono/managed_callable.cpp b/modules/mono/managed_callable.cpp index 9305dc645a..0c2c533090 100644 --- a/modules/mono/managed_callable.cpp +++ b/modules/mono/managed_callable.cpp @@ -79,7 +79,9 @@ CallableCustom::CompareLessFunc ManagedCallable::get_compare_less_func() const { } ObjectID ManagedCallable::get_object() const { - // TODO: If the delegate target extends Godot.Object, use that instead! + if (object_id != ObjectID()) { + return object_id; + } return CSharpLanguage::get_singleton()->get_managed_callable_middleman()->get_instance_id(); } @@ -104,7 +106,7 @@ void ManagedCallable::release_delegate_handle() { // Why you do this clang-format... /* clang-format off */ -ManagedCallable::ManagedCallable(GCHandleIntPtr p_delegate_handle) : delegate_handle(p_delegate_handle) { +ManagedCallable::ManagedCallable(GCHandleIntPtr p_delegate_handle, ObjectID p_object_id) : delegate_handle(p_delegate_handle), object_id(p_object_id) { #ifdef GD_MONO_HOT_RELOAD { MutexLock lock(instances_mutex); diff --git a/modules/mono/managed_callable.h b/modules/mono/managed_callable.h index aa3344f4d5..26cd164fb6 100644 --- a/modules/mono/managed_callable.h +++ b/modules/mono/managed_callable.h @@ -40,6 +40,7 @@ class ManagedCallable : public CallableCustom { friend class CSharpLanguage; GCHandleIntPtr delegate_handle; + ObjectID object_id; #ifdef GD_MONO_HOT_RELOAD SelfList<ManagedCallable> self_instance = this; @@ -66,7 +67,7 @@ public: void release_delegate_handle(); - ManagedCallable(GCHandleIntPtr p_delegate_handle); + ManagedCallable(GCHandleIntPtr p_delegate_handle, ObjectID p_object_id); ~ManagedCallable(); }; diff --git a/modules/mono/utils/macos_utils.h b/modules/mono/utils/macos_utils.h index ca4957f5a7..0b74114685 100644 --- a/modules/mono/utils/macos_utils.h +++ b/modules/mono/utils/macos_utils.h @@ -28,13 +28,13 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "core/string/ustring.h" - #ifndef MONO_MACOS_UTILS_H #define MONO_MACOS_UTILS_H #ifdef MACOS_ENABLED +#include "core/string/ustring.h" + bool macos_is_app_bundle_installed(const String &p_bundle_id); #endif |