diff options
Diffstat (limited to 'platform')
92 files changed, 2133 insertions, 1434 deletions
diff --git a/platform/android/README.md b/platform/android/README.md new file mode 100644 index 0000000000..343e588553 --- /dev/null +++ b/platform/android/README.md @@ -0,0 +1,14 @@ +# Android platform port + +This folder contains the Java and C++ (JNI) code for the Android platform port, +using [Gradle](https://gradle.org/) as a build system. + +## Artwork license + +[`logo.png`](logo.png) and [`run_icon.png`](run_icon.png) are licensed under +[Creative Commons Attribution 3.0 Unported](https://developer.android.com/distribute/marketing-tools/brand-guidelines#android_robot) +per the Android logo usage guidelines: + +> The Android robot is reproduced or modified from work created and shared by +> Google and used according to terms described in the Creative Commons 3.0 +> Attribution License. diff --git a/platform/android/detect.py b/platform/android/detect.py index 7a993e9ca6..6f98dab2cc 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -27,8 +27,7 @@ def get_opts(): ("ANDROID_NDK_ROOT", "Path to the Android NDK", get_android_ndk_root()), ("ANDROID_SDK_ROOT", "Path to the Android SDK", get_android_sdk_root()), ("ndk_platform", 'Target platform (android-<api>, e.g. "android-24")', "android-24"), - EnumVariable("android_arch", "Target architecture", "armv7", ("armv7", "arm64v8", "x86", "x86_64")), - BoolVariable("android_neon", "Enable NEON support (armv7 only)", True), + EnumVariable("android_arch", "Target architecture", "arm64v8", ("armv7", "arm64v8", "x86", "x86_64")), ] @@ -93,7 +92,7 @@ def configure(env): install_ndk_if_needed(env) # Workaround for MinGW. See: - # http://www.scons.org/wiki/LongCmdLinesOnWin32 + # https://www.scons.org/wiki/LongCmdLinesOnWin32 if os.name == "nt": import subprocess @@ -143,10 +142,7 @@ def configure(env): if env["android_arch"] not in ["armv7", "arm64v8", "x86", "x86_64"]: env["android_arch"] = "armv7" - neon_text = "" - if env["android_arch"] == "armv7" and env["android_neon"]: - neon_text = " (with NEON)" - print("Building for Android, platform " + env["ndk_platform"] + " (" + env["android_arch"] + ")" + neon_text) + print("Building for Android, platform " + env["ndk_platform"] + " (" + env["android_arch"] + ")") can_vectorize = True if env["android_arch"] == "x86": @@ -174,10 +170,7 @@ def configure(env): target_subpath = "arm-linux-androideabi-4.9" abi_subpath = "arm-linux-androideabi" arch_subpath = "armeabi-v7a" - if env["android_neon"]: - env.extra_suffix = ".armv7.neon" + env.extra_suffix - else: - env.extra_suffix = ".armv7" + env.extra_suffix + env.extra_suffix = ".armv7" + env.extra_suffix elif env["android_arch"] == "arm64v8": if get_platform(env["ndk_platform"]) < 21: print( @@ -204,12 +197,10 @@ def configure(env): env.Append(CPPDEFINES=["NDEBUG"]) if can_vectorize: env.Append(CCFLAGS=["-ftree-vectorize"]) - if env["target"] == "release_debug": - env.Append(CPPDEFINES=["DEBUG_ENABLED"]) elif env["target"] == "debug": env.Append(LINKFLAGS=["-O0"]) env.Append(CCFLAGS=["-O0", "-g", "-fno-limit-debug-info"]) - env.Append(CPPDEFINES=["_DEBUG", "DEBUG_ENABLED"]) + env.Append(CPPDEFINES=["_DEBUG"]) env.Append(CPPFLAGS=["-UNDEBUG"]) # Compiler configuration @@ -288,7 +279,6 @@ def configure(env): if get_platform(env["ndk_platform"]) >= 24: env.Append(CPPDEFINES=[("_FILE_OFFSET_BITS", 64)]) - env["neon_enabled"] = False if env["android_arch"] == "x86": target_opts = ["-target", "i686-none-linux-android"] # The NDK adds this if targeting API < 21, so we can drop it when Godot targets it at least @@ -301,12 +291,9 @@ def configure(env): target_opts = ["-target", "armv7-none-linux-androideabi"] env.Append(CCFLAGS="-march=armv7-a -mfloat-abi=softfp".split()) env.Append(CPPDEFINES=["__ARM_ARCH_7__", "__ARM_ARCH_7A__"]) - if env["android_neon"]: - env["neon_enabled"] = True - env.Append(CCFLAGS=["-mfpu=neon"]) - env.Append(CPPDEFINES=["__ARM_NEON__"]) - else: - env.Append(CCFLAGS=["-mfpu=vfpv3-d16"]) + # Enable ARM NEON instructions to compile more optimized code. + env.Append(CCFLAGS=["-mfpu=neon"]) + env.Append(CPPDEFINES=["__ARM_NEON__"]) elif env["android_arch"] == "arm64v8": target_opts = ["-target", "aarch64-none-linux-android"] diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index 0bae090702..0eeee8215d 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -161,7 +161,7 @@ bool DirAccessJAndroid::dir_exists(String p_dir) { if (current_dir == "") sd = p_dir; else { - if (p_dir.is_rel_path()) + if (p_dir.is_relative_path()) sd = current_dir.plus_file(p_dir); else sd = fix_path(p_dir); diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index fc86abb6f1..8df61831c2 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -48,6 +48,8 @@ void register_android_exporter() { EDITOR_DEF("export/android/shutdown_adb_on_exit", true); + EDITOR_DEF("export/android/one_click_deploy_clear_previous_install", false); + Ref<EditorExportPlatformAndroid> exporter = Ref<EditorExportPlatformAndroid>(memnew(EditorExportPlatformAndroid)); EditorExport::get_singleton()->add_export_platform(exporter); } diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 8ed2d4e60a..6ef17faf06 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -221,6 +221,9 @@ static const LauncherIcon launcher_adaptive_icon_backgrounds[icon_densities_coun static const int EXPORT_FORMAT_APK = 0; static const int EXPORT_FORMAT_AAB = 1; +static const char *APK_ASSETS_DIRECTORY = "res://android/build/assets"; +static const char *AAB_ASSETS_DIRECTORY = "res://android/build/assetPacks/installTime/src/main/assets"; + void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { EditorExportPlatformAndroid *ea = (EditorExportPlatformAndroid *)ud; @@ -426,6 +429,10 @@ String EditorExportPlatformAndroid::get_package_name(const String &p_package) co return pname; } +String EditorExportPlatformAndroid::get_assets_directory(const Ref<EditorExportPreset> &p_preset, int p_export_format) const { + return p_export_format == EXPORT_FORMAT_AAB ? AAB_ASSETS_DIRECTORY : APK_ASSETS_DIRECTORY; +} + bool EditorExportPlatformAndroid::is_package_name_valid(const String &p_package, String *r_error) const { String pname = p_package; @@ -977,6 +984,11 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p Vector<int> feature_versions; if (xr_mode_index == 1 /* XRMode.OVR */) { + // Set degrees of freedom + feature_names.push_back("android.hardware.vr.headtracking"); + feature_required_list.push_back(true); + feature_versions.push_back(1); + // Check for hand tracking int hand_tracking_index = p_preset->get("xr_features/hand_tracking"); // 0: none, 1: optional, 2: required if (hand_tracking_index > 0) { @@ -1632,10 +1644,12 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio } plugins_changed.clear(); - Vector<String> abis = get_abis(); + const Vector<String> abis = get_abis(); for (int i = 0; i < abis.size(); ++i) { - String abi = abis[i]; - bool is_default = (abi == "armeabi-v7a" || abi == "arm64-v8a"); + const String abi = abis[i]; + // All Android devices supporting Vulkan run 64-bit Android, + // so there is usually no point in exporting for 32-bit Android. + const bool is_default = abi == "arm64-v8a"; r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "architectures/" + abi), is_default)); } @@ -1646,8 +1660,6 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_user"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_password"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "one_click_deploy/clear_previous_install"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/code", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 1)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "version/name"), "1.0")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/unique_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "ext.domain.name"), "org.godotengine.$genname")); @@ -1794,7 +1806,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, int rv; String output; - bool remove_prev = p_preset->get("one_click_deploy/clear_previous_install"); + bool remove_prev = EDITOR_GET("export/android/one_click_deploy_clear_previous_install"); String version_name = p_preset->get("version/name"); String package_name = p_preset->get("package/unique_name"); @@ -2332,11 +2344,21 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre void EditorExportPlatformAndroid::_clear_assets_directory() { DirAccessRef da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); - if (da_res->dir_exists("res://android/build/assets")) { - print_verbose("Clearing assets directory.."); - DirAccessRef da_assets = DirAccess::open("res://android/build/assets"); + + // Clear the APK assets directory + if (da_res->dir_exists(APK_ASSETS_DIRECTORY)) { + print_verbose("Clearing APK assets directory.."); + DirAccessRef da_assets = DirAccess::open(APK_ASSETS_DIRECTORY); + da_assets->erase_contents_recursive(); + da_res->remove(APK_ASSETS_DIRECTORY); + } + + // Clear the AAB assets directory + if (da_res->dir_exists(AAB_ASSETS_DIRECTORY)) { + print_verbose("Clearing AAB assets directory.."); + DirAccessRef da_assets = DirAccess::open(AAB_ASSETS_DIRECTORY); da_assets->erase_contents_recursive(); - da_res->remove("res://android/build/assets"); + da_res->remove(AAB_ASSETS_DIRECTORY); } } @@ -2456,6 +2478,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP return ERR_UNCONFIGURED; } } + const String assets_directory = get_assets_directory(p_preset, export_format); String sdk_path = EDITOR_GET("export/android/android_sdk_path"); ERR_FAIL_COND_V_MSG(sdk_path.is_empty(), ERR_UNCONFIGURED, "Android SDK path must be configured in Editor Settings at 'export/android/android_sdk_path'."); print_verbose("Android sdk path: " + sdk_path); @@ -2477,6 +2500,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP if (!apk_expansion) { print_verbose("Exporting project files.."); CustomExportData user_data; + user_data.assets_directory = assets_directory; user_data.debug = p_debug; err = export_project_files(p_preset, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so); if (err != OK) { @@ -2498,7 +2522,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP } } print_verbose("Storing command line flags.."); - store_file_at_path("res://android/build/assets/_cl_", command_line_flags); + store_file_at_path(assets_directory + "/_cl_", command_line_flags); print_verbose("Updating ANDROID_HOME environment to " + sdk_path); OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path); //set and overwrite if required @@ -2611,7 +2635,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP String export_filename = p_path.get_file(); String export_path = p_path.get_base_dir(); - if (export_path.is_rel_path()) { + if (export_path.is_relative_path()) { export_path = OS::get_singleton()->get_resource_dir().plus_file(export_path); } export_path = ProjectSettings::get_singleton()->globalize_path(export_path).simplify_path(); @@ -2939,7 +2963,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP void EditorExportPlatformAndroid::get_platform_features(List<String> *r_features) { r_features->push_back("mobile"); - r_features->push_back("Android"); + r_features->push_back("android"); } void EditorExportPlatformAndroid::resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) { diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h index b061ee4e04..e0ffaa718b 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -87,11 +87,6 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { EditorProgress *ep = nullptr; }; - struct CustomExportData { - bool debug; - Vector<String> libs; - }; - Vector<PluginConfigAndroid> plugins; String last_plugin_names; uint64_t last_custom_build_time = 0; @@ -109,6 +104,8 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { String get_package_name(const String &p_package) const; + String get_assets_directory(const Ref<EditorExportPreset> &p_preset, int p_export_format) const; + bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const; static bool _should_compress_asset(const String &p_path, const Vector<uint8_t> &p_data); diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index b9e28a7937..851bd0ac52 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -121,7 +121,8 @@ Error store_string_at_path(const String &p_path, const String &p_data) { // It's functionality mirrors that of the method save_apk_file. // This method will be called ONLY when custom build is enabled. Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { - String dst_path = p_path.replace_first("res://", "res://android/build/assets/"); + CustomExportData *export_data = (CustomExportData *)p_userdata; + String dst_path = p_path.replace_first("res://", export_data->assets_directory + "/"); print_verbose("Saving project files from " + p_path + " into " + dst_path); Error err = store_file_at_path(dst_path, p_data); return err; @@ -197,6 +198,8 @@ String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset) { String manifest_xr_features; bool uses_xr = (int)(p_preset->get("xr_features/xr_mode")) == 1; if (uses_xr) { + manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vr.headtracking\" android:required=\"true\" android:version=\"1\" />\n"; + int hand_tracking_index = p_preset->get("xr_features/hand_tracking"); // 0: none, 1: optional, 2: required if (hand_tracking_index == 1) { manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"oculus.software.handtracking\" android:required=\"false\" />\n"; @@ -228,7 +231,9 @@ String _get_activity_tag(const Ref<EditorExportPreset> &p_preset) { "tools:replace=\"android:screenOrientation\" " "android:screenOrientation=\"%s\">\n", orientation); - if (!uses_xr) { + if (uses_xr) { + manifest_activity_text += " <meta-data tools:node=\"replace\" android:name=\"com.oculus.vr.focusaware\" android:value=\"true\" />\n"; + } else { manifest_activity_text += " <meta-data tools:node=\"remove\" android:name=\"com.oculus.vr.focusaware\" />\n"; } manifest_activity_text += " </activity>\n"; diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index 8a93c25d79..744022f1f9 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -44,6 +44,12 @@ const String godot_project_name_xml_string = R"(<?xml version="1.0" encoding="ut </resources> )"; +struct CustomExportData { + String assets_directory; + bool debug; + Vector<String> libs; +}; + int _get_android_orientation_value(DisplayServer::ScreenOrientation screen_orientation); String _get_android_orientation_label(DisplayServer::ScreenOrientation screen_orientation); diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index 00e01884cf..d7bf6cef30 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -49,6 +49,10 @@ <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> + + <!-- Enable access to OpenXR on Oculus mobile devices, no-op on other Android + platforms. --> + <category android:name="com.oculus.intent.category.VR" /> </intent-filter> </activity> diff --git a/platform/android/java/app/assetPacks/installTime/build.gradle b/platform/android/java/app/assetPacks/installTime/build.gradle new file mode 100644 index 0000000000..b06faac374 --- /dev/null +++ b/platform/android/java/app/assetPacks/installTime/build.gradle @@ -0,0 +1,8 @@ +apply plugin: 'com.android.asset-pack' + +assetPack { + packName = "installTime" // Directory name for the asset pack + dynamicDelivery { + deliveryType = "install-time" // Delivery mode + } +} diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 18e07c3762..a391a3ca9a 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -34,9 +34,8 @@ allprojects { } dependencies { - implementation libraries.supportCoreUtils implementation libraries.kotlinStdLib - implementation libraries.v4Support + implementation libraries.androidxFragment if (rootProject.findProject(":lib")) { implementation project(":lib") @@ -73,6 +72,8 @@ android { targetCompatibility versions.javaVersion } + assetPacks = [":assetPacks:installTime"] + defaultConfig { // The default ignore pattern for the 'assets' directory includes hidden files and directories which are used by Godot projects. aaptOptions { diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index fad64c675f..3fafdf8573 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -1,23 +1,21 @@ ext.versions = [ - androidGradlePlugin: '4.2.2', + androidGradlePlugin: '7.0.3', compileSdk : 30, minSdk : 19, targetSdk : 30, buildTools : '30.0.3', - supportCoreUtils : '1.0.0', kotlinVersion : '1.5.10', - v4Support : '1.0.0', - javaVersion : 1.8, + fragmentVersion : '1.3.6', + javaVersion : 11, ndkVersion : '21.4.7075529' // Also update 'platform/android/detect.py#get_project_ndk_version()' when this is updated. ] ext.libraries = [ androidGradlePlugin: "com.android.tools.build:gradle:$versions.androidGradlePlugin", - supportCoreUtils : "androidx.legacy:legacy-support-core-utils:$versions.supportCoreUtils", kotlinGradlePlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlinVersion", kotlinStdLib : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlinVersion", - v4Support : "androidx.legacy:legacy-support-v4:$versions.v4Support" + androidxFragment : "androidx.fragment:fragment:$versions.fragmentVersion", ] ext.getExportPackageName = { -> diff --git a/platform/android/java/app/gradle.properties b/platform/android/java/app/gradle.properties index 19587bd81f..0ad8e611ca 100644 --- a/platform/android/java/app/gradle.properties +++ b/platform/android/java/app/gradle.properties @@ -4,7 +4,7 @@ # where otherwise specified. # For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html +# https://www.gradle.org/docs/current/userguide/build_environment.html android.enableJetifier=true android.useAndroidX=true @@ -15,7 +15,7 @@ org.gradle.jvmargs=-Xmx4536m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# https://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true org.gradle.warning.mode=all diff --git a/platform/android/java/app/settings.gradle b/platform/android/java/app/settings.gradle index 33b863c7bf..e38d7b2ba6 100644 --- a/platform/android/java/app/settings.gradle +++ b/platform/android/java/app/settings.gradle @@ -1,2 +1,2 @@ -// Empty settings.gradle file to denote this directory as being the root project -// of the Godot custom build. +// This is the root directory of the Godot custom build. +include ':assetPacks:installTime' diff --git a/platform/android/java/gradle.properties b/platform/android/java/gradle.properties index b51a19a005..5cd94e85d9 100644 --- a/platform/android/java/gradle.properties +++ b/platform/android/java/gradle.properties @@ -7,7 +7,7 @@ # any settings specified in this file. # For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html +# https://www.gradle.org/docs/current/userguide/build_environment.html android.enableJetifier=true android.useAndroidX=true @@ -18,7 +18,7 @@ org.gradle.jvmargs=-Xmx4536m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# https://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true org.gradle.warning.mode=all diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.jar b/platform/android/java/gradle/wrapper/gradle-wrapper.jar Binary files differindex f6b961fd5a..e708b1c023 100644 --- a/platform/android/java/gradle/wrapper/gradle-wrapper.jar +++ b/platform/android/java/gradle/wrapper/gradle-wrapper.jar diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.properties b/platform/android/java/gradle/wrapper/gradle-wrapper.properties index 74c5636f8a..ffed3a254e 100644 --- a/platform/android/java/gradle/wrapper/gradle-wrapper.properties +++ b/platform/android/java/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Wed Jun 23 23:42:22 PDT 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/platform/android/java/gradlew b/platform/android/java/gradlew index cccdd3d517..4f906e0c81 100755 --- a/platform/android/java/gradlew +++ b/platform/android/java/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -66,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -109,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -138,19 +156,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -159,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/platform/android/java/gradlew.bat b/platform/android/java/gradlew.bat index 11cc30edb0..107acd32c4 100644 --- a/platform/android/java/gradlew.bat +++ b/platform/android/java/gradlew.bat @@ -1,7 +1,23 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem Gradle startup script for Windows @rem @rem ########################################################################## @@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell @@ -75,7 +80,7 @@ if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index 663ba73d40..fbed4ed078 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -2,9 +2,8 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' dependencies { - implementation libraries.supportCoreUtils implementation libraries.kotlinStdLib - implementation libraries.v4Support + implementation libraries.androidxFragment } def pathToRootDir = "../../../../" diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Helpers.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Helpers.java index 2a72c9818d..9aa65fd786 100644 --- a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Helpers.java +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Helpers.java @@ -54,7 +54,7 @@ public class Helpers { /* * Parse the Content-Disposition HTTP Header. The format of the header is defined here: - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for + * https://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for * content that is going to be downloaded to the file system. We only support the attachment * type. */ diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java index 2926031bcc..70bc73b9ad 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -295,7 +295,11 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { View pluginView = plugin.onMainCreate(activity); if (pluginView != null) { - containerLayout.addView(pluginView); + if (plugin.shouldBeOnTop()) { + containerLayout.addView(pluginView); + } else { + containerLayout.addView(pluginView, 0); + } } } } @@ -317,7 +321,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC @SuppressLint("MissingPermission") @Keep private void vibrate(int durationMs) { - if (requestPermission("VIBRATE")) { + if (durationMs > 0 && requestPermission("VIBRATE")) { Vibrator v = (Vibrator)getContext().getSystemService(Context.VIBRATOR_SERVICE); if (v != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java index 2dc8359615..4536c21ed3 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java +++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java @@ -191,6 +191,9 @@ public abstract class GodotPlugin { * The plugin can return a non-null {@link View} layout in order to add it to the Godot view * hierarchy. * + * Use shouldBeOnTop() to set whether the plugin's {@link View} should be added on top or behind + * the main Godot view. + * * @see Activity#onCreate(Bundle) * @return the plugin's view to be included; null if no views should be included. */ @@ -311,6 +314,17 @@ public abstract class GodotPlugin { } /** + * Returns whether the plugin's {@link View} returned in onMainCreate() should be placed on + * top of the main Godot view. + * + * Returning false causes the plugin's {@link View} to be placed behind, which can be useful + * when used with transparency in order to let the Godot view handle inputs. + */ + public boolean shouldBeOnTop() { + return true; + } + + /** * Runs the specified action on the UI thread. If the current thread is the UI * thread, then the action is executed immediately. If the current thread is * not the UI thread, the action is posted to the event queue of the UI thread. diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle index 524031d93f..584b626900 100644 --- a/platform/android/java/settings.gradle +++ b/platform/android/java/settings.gradle @@ -4,3 +4,6 @@ rootProject.name = "Godot" include ':app' include ':lib' include ':nativeSrcsConfigs' + +include ':assetPacks:installTime' +project(':assetPacks:installTime').projectDir = file("app/assetPacks/installTime") diff --git a/platform/android/logo.png b/platform/android/logo.png Binary files differindex f44d360a25..9c8be93646 100644 --- a/platform/android/logo.png +++ b/platform/android/logo.png diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 21fb31d991..034885aa32 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -230,10 +230,13 @@ String OS_Android::get_user_data_dir() const { } String OS_Android::get_cache_path() const { + if (cache_dir_cache != String()) + return cache_dir_cache; + String cache_dir = godot_io_java->get_cache_dir(); if (cache_dir != "") { - cache_dir = _remove_symlink(cache_dir); - return cache_dir; + cache_dir_cache = _remove_symlink(cache_dir); + return cache_dir_cache; } return "."; } diff --git a/platform/android/os_android.h b/platform/android/os_android.h index c938297821..ce8083388f 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -57,6 +57,7 @@ private: #endif mutable String data_dir_cache; + mutable String cache_dir_cache; AudioDriverOpenSL audio_driver_android; diff --git a/platform/iphone/app_delegate.mm b/platform/iphone/app_delegate.mm index d10ea5c68c..c6f91665c5 100644 --- a/platform/iphone/app_delegate.mm +++ b/platform/iphone/app_delegate.mm @@ -44,7 +44,7 @@ extern int gargc; extern char **gargv; -extern int iphone_main(int, char **, String); +extern int iphone_main(int, char **, String, String); extern void iphone_finish(); @implementation AppDelegate @@ -67,8 +67,10 @@ static ViewController *mainViewController = nil; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; + paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + NSString *cacheDirectory = [paths objectAtIndex:0]; - int err = iphone_main(gargc, gargv, String::utf8([documentsDirectory UTF8String])); + int err = iphone_main(gargc, gargv, String::utf8([documentsDirectory UTF8String]), String::utf8([cacheDirectory UTF8String])); if (err != 0) { // bail, things did not go very well for us, should probably output a message on screen with our error code... diff --git a/platform/iphone/detect.py b/platform/iphone/detect.py index 05e24c5003..0d28aa2f06 100644 --- a/platform/iphone/detect.py +++ b/platform/iphone/detect.py @@ -53,12 +53,9 @@ def configure(env): env.Append(CCFLAGS=["-Os", "-ftree-vectorize"]) env.Append(LINKFLAGS=["-Os"]) - if env["target"] == "release_debug": - env.Append(CPPDEFINES=["DEBUG_ENABLED"]) - elif env["target"] == "debug": env.Append(CCFLAGS=["-gdwarf-2", "-O0"]) - env.Append(CPPDEFINES=["_DEBUG", ("DEBUG", 1), "DEBUG_ENABLED"]) + env.Append(CPPDEFINES=["_DEBUG", ("DEBUG", 1)]) if env["use_lto"]: env.Append(CCFLAGS=["-flto"]) diff --git a/platform/iphone/export/export_plugin.cpp b/platform/iphone/export/export_plugin.cpp index 69a8203e9f..a561da41c0 100644 --- a/platform/iphone/export/export_plugin.cpp +++ b/platform/iphone/export/export_plugin.cpp @@ -1427,7 +1427,6 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p } bool found_library = false; - int total_size = 0; const String project_file = "godot_ios.xcodeproj/project.pbxproj"; Set<String> files_to_parse; @@ -1523,7 +1522,6 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p file = file.replace("godot_ios", binary_name); print_line("ADDING: " + file + " size: " + itos(data.size())); - total_size += data.size(); /* write it into our folder structure */ file = dest_dir + file; diff --git a/platform/iphone/export/export_plugin.h b/platform/iphone/export/export_plugin.h index 8d3af6e057..359f855d86 100644 --- a/platform/iphone/export/export_plugin.h +++ b/platform/iphone/export/export_plugin.h @@ -204,7 +204,7 @@ public: virtual void get_platform_features(List<String> *r_features) override { r_features->push_back("mobile"); - r_features->push_back("iOS"); + r_features->push_back("ios"); } virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) override { diff --git a/platform/iphone/godot_iphone.mm b/platform/iphone/godot_iphone.mm index 6c3e1eabde..6f6f9d0708 100644 --- a/platform/iphone/godot_iphone.mm +++ b/platform/iphone/godot_iphone.mm @@ -74,7 +74,7 @@ int add_cmdline(int p_argc, char **p_args) { return p_argc; }; -int iphone_main(int argc, char **argv, String data_dir) { +int iphone_main(int argc, char **argv, String data_dir, String cache_dir) { size_t len = strlen(argv[0]); while (len--) { @@ -95,7 +95,7 @@ int iphone_main(int argc, char **argv, String data_dir) { char cwd[512]; getcwd(cwd, sizeof(cwd)); printf("cwd %s\n", cwd); - os = new OSIPhone(data_dir); + os = new OSIPhone(data_dir, cache_dir); // We must override main when testing is enabled TEST_MAIN_OVERRIDE diff --git a/platform/iphone/os_iphone.h b/platform/iphone/os_iphone.h index 248369369d..7a81d8f593 100644 --- a/platform/iphone/os_iphone.h +++ b/platform/iphone/os_iphone.h @@ -72,6 +72,7 @@ private: virtual void finalize() override; String user_data_dir; + String cache_dir; bool is_focused = false; @@ -80,7 +81,7 @@ private: public: static OSIPhone *get_singleton(); - OSIPhone(String p_data_dir); + OSIPhone(String p_data_dir, String p_cache_dir); ~OSIPhone(); void initialize_modules(); @@ -103,6 +104,8 @@ public: void set_user_data_dir(String p_dir); virtual String get_user_data_dir() const override; + virtual String get_cache_path() const override; + virtual String get_locale() const override; virtual String get_unique_id() const override; diff --git a/platform/iphone/os_iphone.mm b/platform/iphone/os_iphone.mm index c88d253691..fc07d321b7 100644 --- a/platform/iphone/os_iphone.mm +++ b/platform/iphone/os_iphone.mm @@ -87,7 +87,7 @@ OSIPhone *OSIPhone::get_singleton() { return (OSIPhone *)OS::get_singleton(); } -OSIPhone::OSIPhone(String p_data_dir) { +OSIPhone::OSIPhone(String p_data_dir, String p_cache_dir) { for (int i = 0; i < ios_init_callbacks_count; ++i) { ios_init_callbacks[i](); } @@ -101,6 +101,7 @@ OSIPhone::OSIPhone(String p_data_dir) { // can't call set_data_dir from here, since it requires DirAccess // which is initialized in initialize_core user_data_dir = p_data_dir; + cache_dir = p_cache_dir; Vector<Logger *> loggers; loggers.push_back(memnew(SyslogLogger)); @@ -266,6 +267,10 @@ String OSIPhone::get_user_data_dir() const { return user_data_dir; } +String OSIPhone::get_cache_path() const { + return cache_dir; +} + String OSIPhone::get_locale() const { NSString *preferedLanguage = [NSLocale preferredLanguages].firstObject; diff --git a/platform/javascript/.eslintrc.js b/platform/javascript/.eslintrc.js index 0ff9d67d26..2c81f1f02d 100644 --- a/platform/javascript/.eslintrc.js +++ b/platform/javascript/.eslintrc.js @@ -39,5 +39,13 @@ module.exports = { // Closure compiler (exported properties) "quote-props": ["error", "consistent"], "dot-notation": "off", + // No comma dangle for functions (it's madness, and ES2017) + "comma-dangle": ["error", { + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "never" + }], } }; diff --git a/platform/javascript/.eslintrc.libs.js b/platform/javascript/.eslintrc.libs.js index 81b1b8c864..8e579fd462 100644 --- a/platform/javascript/.eslintrc.libs.js +++ b/platform/javascript/.eslintrc.libs.js @@ -15,6 +15,7 @@ module.exports = { "IDBFS": true, "GodotOS": true, "GodotConfig": true, + "GodotEventListeners": true, "GodotRuntime": true, "GodotFS": true, "IDHandler": true, diff --git a/platform/javascript/README.md b/platform/javascript/README.md new file mode 100644 index 0000000000..f181bea9e0 --- /dev/null +++ b/platform/javascript/README.md @@ -0,0 +1,15 @@ +# HTML5 platform port + +This folder contains the C++ and JavaScript code for the HTML5/WebAssembly platform port, +compiled using [Emscripten](https://emscripten.org/). + +It also contains a ESLint linting setup (see [`package.json`](package.json)). + +See also [`misc/dist/html`](/misc/dist/html) folder for files used by this platform +such as the HTML5 shell. + +## Artwork license + +[`logo.png`](logo.png) and [`run_icon.png`](run_icon.png) are licensed under +[Creative Commons Attribution 3.0 Unported](https://www.w3.org/html/logo/faq.html#how-licenced) +per the HTML5 logo usage guidelines. diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index 62a8660ae4..fa9e6eed15 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -20,6 +20,7 @@ sys_env.AddJSLibraries( "js/libs/library_godot_fetch.js", "js/libs/library_godot_os.js", "js/libs/library_godot_runtime.js", + "js/libs/library_godot_input.js", ] ) diff --git a/platform/javascript/api/javascript_tools_editor_plugin.cpp b/platform/javascript/api/javascript_tools_editor_plugin.cpp index 54f541f607..df4c790755 100644 --- a/platform/javascript/api/javascript_tools_editor_plugin.cpp +++ b/platform/javascript/api/javascript_tools_editor_plugin.cpp @@ -35,6 +35,7 @@ #include "core/config/project_settings.h" #include "core/io/dir_access.h" #include "core/io/file_access.h" +#include "core/os/time.h" #include "editor/editor_node.h" #include <emscripten/emscripten.h> @@ -58,23 +59,40 @@ JavaScriptToolsEditorPlugin::JavaScriptToolsEditorPlugin(EditorNode *p_editor) { void JavaScriptToolsEditorPlugin::_download_zip(Variant p_v) { if (!Engine::get_singleton() || !Engine::get_singleton()->is_editor_hint()) { - WARN_PRINT("Project download is only available in Editor mode"); + ERR_PRINT("Downloading the project as a ZIP archive is only available in Editor mode."); return; } String resource_path = ProjectSettings::get_singleton()->get_resource_path(); FileAccess *src_f; zlib_filefunc_def io = zipio_create_io_from_file(&src_f); - zipFile zip = zipOpen2("/tmp/project.zip", APPEND_STATUS_CREATE, nullptr, &io); - String base_path = resource_path.substr(0, resource_path.rfind("/")) + "/"; + + // Name the downloded ZIP file to contain the project name and download date for easier organization. + // Replace characters not allowed (or risky) in Windows file names with safe characters. + // In the project name, all invalid characters become an empty string so that a name + // like "Platformer 2: Godette's Revenge" becomes "platformer_2-_godette-s_revenge". + const String project_name = GLOBAL_GET("application/config/name"); + const String project_name_safe = project_name.to_lower().replace(" ", "_"); + const String datetime_safe = + Time::get_singleton()->get_datetime_string_from_system(false, true).replace(" ", "_"); + const String output_name = OS::get_singleton()->get_safe_dir_name(vformat("%s_%s.zip")); + const String output_path = String("/tmp").plus_file(output_name); + + zipFile zip = zipOpen2(output_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io); + const String base_path = resource_path.substr(0, resource_path.rfind("/")) + "/"; _zip_recursive(resource_path, base_path, zip); zipClose(zip, nullptr); - FileAccess *f = FileAccess::open("/tmp/project.zip", FileAccess::READ); - ERR_FAIL_COND_MSG(!f, "Unable to create zip file"); + FileAccess *f = FileAccess::open(output_path, FileAccess::READ); + ERR_FAIL_COND_MSG(!f, "Unable to create ZIP file."); Vector<uint8_t> buf; buf.resize(f->get_length()); f->get_buffer(buf.ptrw(), buf.size()); - godot_js_os_download_buffer(buf.ptr(), buf.size(), "project.zip", "application/zip"); + godot_js_os_download_buffer(buf.ptr(), buf.size(), output_name.utf8().get_data(), "application/zip"); + + f->close(); + memdelete(f); + // Remove the temporary file since it was sent to the user's native filesystem as a download. + DirAccess::remove_file_or_error(output_path); } void JavaScriptToolsEditorPlugin::_zip_file(String p_path, String p_base_path, zipFile p_zip) { @@ -108,14 +126,15 @@ void JavaScriptToolsEditorPlugin::_zip_file(String p_path, String p_base_path, z void JavaScriptToolsEditorPlugin::_zip_recursive(String p_path, String p_base_path, zipFile p_zip) { DirAccess *dir = DirAccess::open(p_path); if (!dir) { - WARN_PRINT("Unable to open dir for zipping: " + p_path); + WARN_PRINT("Unable to open directory for zipping: " + p_path); return; } dir->list_dir_begin(); String cur = dir->get_next(); + String project_data_dir_name = ProjectSettings::get_singleton()->get_project_data_dir_name(); while (!cur.is_empty()) { String cs = p_path.plus_file(cur); - if (cur == "." || cur == ".." || cur == ".import") { + if (cur == "." || cur == ".." || cur == project_data_dir_name) { // Skip } else if (dir->current_is_dir()) { String path = cs.replace_first(p_base_path, "") + "/"; diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp index 478e848675..626aef3c60 100644 --- a/platform/javascript/audio_driver_javascript.cpp +++ b/platform/javascript/audio_driver_javascript.cpp @@ -34,22 +34,18 @@ #include <emscripten.h> -AudioDriverJavaScript *AudioDriverJavaScript::singleton = nullptr; +AudioDriverJavaScript::AudioContext AudioDriverJavaScript::audio_context; bool AudioDriverJavaScript::is_available() { return godot_audio_is_available() != 0; } -const char *AudioDriverJavaScript::get_name() const { - return "JavaScript"; -} - void AudioDriverJavaScript::_state_change_callback(int p_state) { - singleton->state = p_state; + AudioDriverJavaScript::audio_context.state = p_state; } void AudioDriverJavaScript::_latency_update_callback(float p_latency) { - singleton->output_latency = p_latency; + AudioDriverJavaScript::audio_context.output_latency = p_latency; } void AudioDriverJavaScript::_audio_driver_process(int p_from, int p_samples) { @@ -105,28 +101,31 @@ void AudioDriverJavaScript::_audio_driver_capture(int p_from, int p_samples) { } Error AudioDriverJavaScript::init() { - mix_rate = GLOBAL_GET("audio/driver/mix_rate"); int latency = GLOBAL_GET("audio/driver/output_latency"); - - channel_count = godot_audio_init(mix_rate, latency, &_state_change_callback, &_latency_update_callback); + if (!audio_context.inited) { + audio_context.mix_rate = GLOBAL_GET("audio/driver/mix_rate"); + audio_context.channel_count = godot_audio_init(&audio_context.mix_rate, latency, &_state_change_callback, &_latency_update_callback); + audio_context.inited = true; + } + mix_rate = audio_context.mix_rate; + channel_count = audio_context.channel_count; buffer_length = closest_power_of_2((latency * mix_rate / 1000)); -#ifndef NO_THREADS - node = memnew(WorkletNode); -#else - node = memnew(ScriptProcessorNode); -#endif - buffer_length = node->create(buffer_length, channel_count); + Error err = create(buffer_length, channel_count); + if (err != OK) { + return err; + } if (output_rb) { memdelete_arr(output_rb); } - output_rb = memnew_arr(float, buffer_length *channel_count); + const size_t array_size = buffer_length * (size_t)channel_count; + output_rb = memnew_arr(float, array_size); if (!output_rb) { return ERR_OUT_OF_MEMORY; } if (input_rb) { memdelete_arr(input_rb); } - input_rb = memnew_arr(float, buffer_length *channel_count); + input_rb = memnew_arr(float, array_size); if (!input_rb) { return ERR_OUT_OF_MEMORY; } @@ -134,19 +133,17 @@ Error AudioDriverJavaScript::init() { } void AudioDriverJavaScript::start() { - if (node) { - node->start(output_rb, memarr_len(output_rb), input_rb, memarr_len(input_rb)); - } + start(output_rb, memarr_len(output_rb), input_rb, memarr_len(input_rb)); } void AudioDriverJavaScript::resume() { - if (state == 0) { // 'suspended' + if (audio_context.state == 0) { // 'suspended' godot_audio_resume(); } } float AudioDriverJavaScript::get_latency() { - return output_latency + (float(buffer_length) / mix_rate); + return audio_context.output_latency + (float(buffer_length) / mix_rate); } int AudioDriverJavaScript::get_mix_rate() const { @@ -157,24 +154,8 @@ AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const { return get_speaker_mode_by_total_channels(channel_count); } -void AudioDriverJavaScript::lock() { - if (node) { - node->unlock(); - } -} - -void AudioDriverJavaScript::unlock() { - if (node) { - node->unlock(); - } -} - void AudioDriverJavaScript::finish() { - if (node) { - node->finish(); - memdelete(node); - node = nullptr; - } + finish_driver(); if (output_rb) { memdelete_arr(output_rb); output_rb = nullptr; @@ -203,41 +184,66 @@ Error AudioDriverJavaScript::capture_stop() { return OK; } -AudioDriverJavaScript::AudioDriverJavaScript() { - singleton = this; -} - #ifdef NO_THREADS /// ScriptProcessorNode implementation -void AudioDriverJavaScript::ScriptProcessorNode::_process_callback() { - AudioDriverJavaScript::singleton->_audio_driver_capture(); - AudioDriverJavaScript::singleton->_audio_driver_process(); +AudioDriverScriptProcessor *AudioDriverScriptProcessor::singleton = nullptr; + +void AudioDriverScriptProcessor::_process_callback() { + AudioDriverScriptProcessor::singleton->_audio_driver_capture(); + AudioDriverScriptProcessor::singleton->_audio_driver_process(); } -int AudioDriverJavaScript::ScriptProcessorNode::create(int p_buffer_samples, int p_channels) { - return godot_audio_script_create(p_buffer_samples, p_channels); +Error AudioDriverScriptProcessor::create(int &p_buffer_samples, int p_channels) { + if (!godot_audio_has_script_processor()) { + return ERR_UNAVAILABLE; + } + return (Error)godot_audio_script_create(&p_buffer_samples, p_channels); } -void AudioDriverJavaScript::ScriptProcessorNode::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) { +void AudioDriverScriptProcessor::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) { godot_audio_script_start(p_in_buf, p_in_buf_size, p_out_buf, p_out_buf_size, &_process_callback); } + +/// AudioWorkletNode implementation (no threads) +AudioDriverWorklet *AudioDriverWorklet::singleton = nullptr; + +Error AudioDriverWorklet::create(int &p_buffer_size, int p_channels) { + if (!godot_audio_has_worklet()) { + return ERR_UNAVAILABLE; + } + return (Error)godot_audio_worklet_create(p_channels); +} + +void AudioDriverWorklet::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) { + _audio_driver_process(); + godot_audio_worklet_start_no_threads(p_out_buf, p_out_buf_size, &_process_callback, p_in_buf, p_in_buf_size, &_capture_callback); +} + +void AudioDriverWorklet::_process_callback(int p_pos, int p_samples) { + AudioDriverWorklet *driver = AudioDriverWorklet::singleton; + driver->_audio_driver_process(p_pos, p_samples); +} + +void AudioDriverWorklet::_capture_callback(int p_pos, int p_samples) { + AudioDriverWorklet *driver = AudioDriverWorklet::singleton; + driver->_audio_driver_capture(p_pos, p_samples); +} #else -/// AudioWorkletNode implementation -void AudioDriverJavaScript::WorkletNode::_audio_thread_func(void *p_data) { - AudioDriverJavaScript::WorkletNode *obj = static_cast<AudioDriverJavaScript::WorkletNode *>(p_data); - AudioDriverJavaScript *driver = AudioDriverJavaScript::singleton; - const int out_samples = memarr_len(driver->output_rb); - const int in_samples = memarr_len(driver->input_rb); +/// AudioWorkletNode implementation (threads) +void AudioDriverWorklet::_audio_thread_func(void *p_data) { + AudioDriverWorklet *driver = static_cast<AudioDriverWorklet *>(p_data); + const int out_samples = memarr_len(driver->get_output_rb()); + const int in_samples = memarr_len(driver->get_input_rb()); int wpos = 0; int to_write = out_samples; int rpos = 0; int to_read = 0; int32_t step = 0; - while (!obj->quit) { + while (!driver->quit) { if (to_read) { driver->lock(); driver->_audio_driver_capture(rpos, to_read); - godot_audio_worklet_state_add(obj->state, STATE_SAMPLES_IN, -to_read); + godot_audio_worklet_state_add(driver->state, STATE_SAMPLES_IN, -to_read); driver->unlock(); rpos += to_read; if (rpos >= in_samples) { @@ -247,38 +253,40 @@ void AudioDriverJavaScript::WorkletNode::_audio_thread_func(void *p_data) { if (to_write) { driver->lock(); driver->_audio_driver_process(wpos, to_write); - godot_audio_worklet_state_add(obj->state, STATE_SAMPLES_OUT, to_write); + godot_audio_worklet_state_add(driver->state, STATE_SAMPLES_OUT, to_write); driver->unlock(); wpos += to_write; if (wpos >= out_samples) { wpos -= out_samples; } } - step = godot_audio_worklet_state_wait(obj->state, STATE_PROCESS, step, 1); - to_write = out_samples - godot_audio_worklet_state_get(obj->state, STATE_SAMPLES_OUT); - to_read = godot_audio_worklet_state_get(obj->state, STATE_SAMPLES_IN); + step = godot_audio_worklet_state_wait(driver->state, STATE_PROCESS, step, 1); + to_write = out_samples - godot_audio_worklet_state_get(driver->state, STATE_SAMPLES_OUT); + to_read = godot_audio_worklet_state_get(driver->state, STATE_SAMPLES_IN); } } -int AudioDriverJavaScript::WorkletNode::create(int p_buffer_size, int p_channels) { - godot_audio_worklet_create(p_channels); - return p_buffer_size; +Error AudioDriverWorklet::create(int &p_buffer_size, int p_channels) { + if (!godot_audio_has_worklet()) { + return ERR_UNAVAILABLE; + } + return (Error)godot_audio_worklet_create(p_channels); } -void AudioDriverJavaScript::WorkletNode::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) { +void AudioDriverWorklet::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) { godot_audio_worklet_start(p_in_buf, p_in_buf_size, p_out_buf, p_out_buf_size, state); thread.start(_audio_thread_func, this); } -void AudioDriverJavaScript::WorkletNode::lock() { +void AudioDriverWorklet::lock() { mutex.lock(); } -void AudioDriverJavaScript::WorkletNode::unlock() { +void AudioDriverWorklet::unlock() { mutex.unlock(); } -void AudioDriverJavaScript::WorkletNode::finish() { +void AudioDriverWorklet::finish_driver() { quit = true; // Ask thread to quit. thread.wait_to_finish(); } diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index 393693640f..6a0b4bcb0e 100644 --- a/platform/javascript/audio_driver_javascript.h +++ b/platform/javascript/audio_driver_javascript.h @@ -38,52 +38,15 @@ #include "godot_audio.h" class AudioDriverJavaScript : public AudioDriver { -public: - class AudioNode { - public: - virtual int create(int p_buffer_size, int p_output_channels) = 0; - virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) = 0; - virtual void finish() {} - virtual void lock() {} - virtual void unlock() {} - virtual ~AudioNode() {} - }; - - class WorkletNode : public AudioNode { - private: - enum { - STATE_LOCK, - STATE_PROCESS, - STATE_SAMPLES_IN, - STATE_SAMPLES_OUT, - STATE_MAX, - }; - Mutex mutex; - Thread thread; - bool quit = false; - int32_t state[STATE_MAX] = { 0 }; - - static void _audio_thread_func(void *p_data); - - public: - int create(int p_buffer_size, int p_output_channels) override; - void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override; - void finish() override; - void lock() override; - void unlock() override; - }; - - class ScriptProcessorNode : public AudioNode { - private: - static void _process_callback(); - - public: - int create(int p_buffer_samples, int p_channels) override; - void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override; - }; - private: - AudioNode *node = nullptr; + struct AudioContext { + bool inited = false; + float output_latency = 0.0; + int state = -1; + int channel_count = 0; + int mix_rate = 0; + }; + static AudioContext audio_context; float *output_rb = nullptr; float *input_rb = nullptr; @@ -91,36 +54,108 @@ private: int buffer_length = 0; int mix_rate = 0; int channel_count = 0; - int state = 0; - float output_latency = 0.0; static void _state_change_callback(int p_state); static void _latency_update_callback(float p_latency); + static AudioDriverJavaScript *singleton; + protected: void _audio_driver_process(int p_from = 0, int p_samples = 0); void _audio_driver_capture(int p_from = 0, int p_samples = 0); + float *get_output_rb() const { return output_rb; } + float *get_input_rb() const { return input_rb; } + + virtual Error create(int &p_buffer_samples, int p_channels) = 0; + virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) = 0; + virtual void finish_driver() {} public: static bool is_available(); - static AudioDriverJavaScript *singleton; + virtual Error init() final; + virtual void start() final; + virtual void finish() final; + + virtual float get_latency() override; + virtual int get_mix_rate() const override; + virtual SpeakerMode get_speaker_mode() const override; + + virtual Error capture_start() override; + virtual Error capture_stop() override; + + static void resume(); + + AudioDriverJavaScript() {} +}; + +#ifdef NO_THREADS +class AudioDriverScriptProcessor : public AudioDriverJavaScript { +private: + static void _process_callback(); + + static AudioDriverScriptProcessor *singleton; + +protected: + Error create(int &p_buffer_samples, int p_channels) override; + void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override; + +public: + virtual const char *get_name() const override { return "ScriptProcessor"; } + + virtual void lock() override {} + virtual void unlock() override {} + + AudioDriverScriptProcessor() { singleton = this; } +}; + +class AudioDriverWorklet : public AudioDriverJavaScript { +private: + static void _process_callback(int p_pos, int p_samples); + static void _capture_callback(int p_pos, int p_samples); + + static AudioDriverWorklet *singleton; + +protected: + virtual Error create(int &p_buffer_size, int p_output_channels) override; + virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override; - virtual const char *get_name() const; +public: + virtual const char *get_name() const override { return "AudioWorklet"; } + + virtual void lock() override {} + virtual void unlock() override {} - virtual Error init(); - virtual void start(); - void resume(); - virtual float get_latency(); - virtual int get_mix_rate() const; - virtual SpeakerMode get_speaker_mode() const; - virtual void lock(); - virtual void unlock(); - virtual void finish(); + AudioDriverWorklet() { singleton = this; } +}; +#else +class AudioDriverWorklet : public AudioDriverJavaScript { +private: + enum { + STATE_LOCK, + STATE_PROCESS, + STATE_SAMPLES_IN, + STATE_SAMPLES_OUT, + STATE_MAX, + }; + Mutex mutex; + Thread thread; + bool quit = false; + int32_t state[STATE_MAX] = { 0 }; - virtual Error capture_start(); - virtual Error capture_stop(); + static void _audio_thread_func(void *p_data); - AudioDriverJavaScript(); +protected: + virtual Error create(int &p_buffer_size, int p_output_channels) override; + virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override; + virtual void finish_driver() override; + +public: + virtual const char *get_name() const override { return "AudioWorklet"; } + + void lock() override; + void unlock() override; }; #endif + +#endif diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index 173b558b6d..891ae419bd 100644 --- a/platform/javascript/detect.py +++ b/platform/javascript/detect.py @@ -77,11 +77,9 @@ def configure(env): env.Append(LINKFLAGS=["-Os"]) if env["target"] == "release_debug": - env.Append(CPPDEFINES=["DEBUG_ENABLED"]) # Retain function names for backtraces at the cost of file size. env.Append(LINKFLAGS=["--profiling-funcs"]) else: # "debug" - env.Append(CPPDEFINES=["DEBUG_ENABLED"]) env.Append(CCFLAGS=["-O1", "-g"]) env.Append(LINKFLAGS=["-O1", "-g"]) env["use_assertions"] = True diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp index fda18a5c19..c2eb826db9 100644 --- a/platform/javascript/display_server_javascript.cpp +++ b/platform/javascript/display_server_javascript.cpp @@ -62,26 +62,13 @@ bool DisplayServerJavaScript::check_size_force_redraw() { return godot_js_display_size_update() != 0; } -Point2 DisplayServerJavaScript::compute_position_in_canvas(int p_x, int p_y) { - int point[2]; - godot_js_display_compute_position(p_x, p_y, point, point + 1); - return Point2(point[0], point[1]); -} - -EM_BOOL DisplayServerJavaScript::fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data) { +void DisplayServerJavaScript::fullscreen_change_callback(int p_fullscreen) { DisplayServerJavaScript *display = get_singleton(); - // Empty ID is canvas. - String target_id = String::utf8(p_event->id); - if (target_id.is_empty() || target_id == String::utf8(&(display->canvas_id[1]))) { - // This event property is the only reliable data on - // browser fullscreen state. - if (p_event->isFullscreen) { - display->window_mode = WINDOW_MODE_FULLSCREEN; - } else { - display->window_mode = WINDOW_MODE_WINDOWED; - } + if (p_fullscreen) { + display->window_mode = WINDOW_MODE_FULLSCREEN; + } else { + display->window_mode = WINDOW_MODE_WINDOWED; } - return false; } // Drag and drop callback. @@ -118,76 +105,51 @@ void DisplayServerJavaScript::request_quit_callback() { // Keys -template <typename T> -void DisplayServerJavaScript::dom2godot_mod(T *emscripten_event_ptr, Ref<InputEventWithModifiers> godot_event) { - godot_event->set_shift_pressed(emscripten_event_ptr->shiftKey); - godot_event->set_alt_pressed(emscripten_event_ptr->altKey); - godot_event->set_ctrl_pressed(emscripten_event_ptr->ctrlKey); - godot_event->set_meta_pressed(emscripten_event_ptr->metaKey); +void DisplayServerJavaScript::dom2godot_mod(Ref<InputEventWithModifiers> ev, int p_mod) { + ev->set_shift_pressed(p_mod & 1); + ev->set_alt_pressed(p_mod & 2); + ev->set_ctrl_pressed(p_mod & 4); + ev->set_meta_pressed(p_mod & 8); } -Ref<InputEventKey> DisplayServerJavaScript::setup_key_event(const EmscriptenKeyboardEvent *emscripten_event) { +void DisplayServerJavaScript::key_callback(int p_pressed, int p_repeat, int p_modifiers) { + DisplayServerJavaScript *ds = get_singleton(); + JSKeyEvent &key_event = ds->key_event; + // Resume audio context after input in case autoplay was denied. + OS_JavaScript::get_singleton()->resume_audio(); + Ref<InputEventKey> ev; ev.instantiate(); - ev->set_echo(emscripten_event->repeat); - dom2godot_mod(emscripten_event, ev); - ev->set_keycode(dom_code2godot_scancode(emscripten_event->code, emscripten_event->key, false)); - ev->set_physical_keycode(dom_code2godot_scancode(emscripten_event->code, emscripten_event->key, true)); - - String unicode = String::utf8(emscripten_event->key); - // Check if empty or multi-character (e.g. `CapsLock`). - if (unicode.length() != 1) { - // Might be empty as well, but better than nonsense. - unicode = String::utf8(emscripten_event->charValue); - } + ev->set_echo(p_repeat); + ev->set_keycode(dom_code2godot_scancode(key_event.code, key_event.key, false)); + ev->set_physical_keycode(dom_code2godot_scancode(key_event.code, key_event.key, true)); + ev->set_pressed(p_pressed); + dom2godot_mod(ev, p_modifiers); + + String unicode = String::utf8(key_event.key); if (unicode.length() == 1) { ev->set_unicode(unicode[0]); } - - return ev; -} - -EM_BOOL DisplayServerJavaScript::keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { - DisplayServerJavaScript *display = get_singleton(); - Ref<InputEventKey> ev = setup_key_event(p_event); - ev->set_pressed(true); - if (ev->get_unicode() == 0 && keycode_has_unicode(ev->get_keycode())) { - // Defer to keypress event for legacy unicode retrieval. - display->deferred_key_event = ev; - // Do not suppress keypress event. - return false; - } Input::get_singleton()->parse_input_event(ev); - return true; -} -EM_BOOL DisplayServerJavaScript::keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { - DisplayServerJavaScript *display = get_singleton(); - display->deferred_key_event->set_unicode(p_event->charCode); - Input::get_singleton()->parse_input_event(display->deferred_key_event); - return true; -} - -EM_BOOL DisplayServerJavaScript::keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { - Ref<InputEventKey> ev = setup_key_event(p_event); - ev->set_pressed(false); - Input::get_singleton()->parse_input_event(ev); - return ev->get_keycode() != KEY_UNKNOWN && ev->get_keycode() != (Key)0; + // Make sure to flush all events so we can call restricted APIs inside the event. + Input::get_singleton()->flush_buffered_events(); } // Mouse -EM_BOOL DisplayServerJavaScript::mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) { - DisplayServerJavaScript *display = get_singleton(); +int DisplayServerJavaScript::mouse_button_callback(int p_pressed, int p_button, double p_x, double p_y, int p_modifiers) { + DisplayServerJavaScript *ds = get_singleton(); Ref<InputEventMouseButton> ev; ev.instantiate(); - ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_MOUSEDOWN); - ev->set_position(compute_position_in_canvas(p_event->clientX, p_event->clientY)); + ev->set_pressed(p_pressed); + ev->set_position(Point2(p_x, p_y)); ev->set_global_position(ev->get_position()); - dom2godot_mod(p_event, ev); + ev->set_pressed(p_pressed); + dom2godot_mod(ev, p_modifiers); - switch (p_event->button) { + switch (p_button) { case DOM_BUTTON_LEFT: ev->set_button_index(MOUSE_BUTTON_LEFT); break; @@ -207,34 +169,30 @@ EM_BOOL DisplayServerJavaScript::mouse_button_callback(int p_event_type, const E return false; } - if (ev->is_pressed()) { - double diff = emscripten_get_now() - display->last_click_ms; + if (p_pressed) { + uint64_t diff = (OS::get_singleton()->get_ticks_usec() / 1000) - ds->last_click_ms; - if (ev->get_button_index() == display->last_click_button_index) { - if (diff < 400 && Point2(display->last_click_pos).distance_to(ev->get_position()) < 5) { - display->last_click_ms = 0; - display->last_click_pos = Point2(-100, -100); - display->last_click_button_index = -1; + if (ev->get_button_index() == ds->last_click_button_index) { + if (diff < 400 && Point2(ds->last_click_pos).distance_to(ev->get_position()) < 5) { + ds->last_click_ms = 0; + ds->last_click_pos = Point2(-100, -100); + ds->last_click_button_index = -1; ev->set_double_click(true); } } else { - display->last_click_button_index = ev->get_button_index(); + ds->last_click_button_index = ev->get_button_index(); } if (!ev->is_double_click()) { - display->last_click_ms += diff; - display->last_click_pos = ev->get_position(); + ds->last_click_ms += diff; + ds->last_click_pos = ev->get_position(); } } - Input *input = Input::get_singleton(); - int mask = input->get_mouse_button_mask(); - MouseButton button_flag = MouseButton(1 << (ev->get_button_index() - 1)); + int mask = Input::get_singleton()->get_mouse_button_mask(); + int button_flag = 1 << (ev->get_button_index() - 1); if (ev->is_pressed()) { - // Since the event is consumed, focus manually. The containing iframe, - // if exists, may not have focus yet, so focus even if already focused. - focus_canvas(); mask |= button_flag; } else if (mask & button_flag) { mask &= ~button_flag; @@ -244,37 +202,38 @@ EM_BOOL DisplayServerJavaScript::mouse_button_callback(int p_event_type, const E } ev->set_button_mask(mask); - input->parse_input_event(ev); + Input::get_singleton()->parse_input_event(ev); + // Resume audio context after input in case autoplay was denied. + OS_JavaScript::get_singleton()->resume_audio(); + + // Make sure to flush all events so we can call restricted APIs inside the event. + Input::get_singleton()->flush_buffered_events(); + // Prevent multi-click text selection and wheel-click scrolling anchor. // Context menu is prevented through contextmenu event. return true; } -EM_BOOL DisplayServerJavaScript::mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) { - DisplayServerJavaScript *ds = get_singleton(); - Input *input = Input::get_singleton(); - int input_mask = input->get_mouse_button_mask(); - Point2 pos = compute_position_in_canvas(p_event->clientX, p_event->clientY); +void DisplayServerJavaScript::mouse_move_callback(double p_x, double p_y, double p_rel_x, double p_rel_y, int p_modifiers) { + int input_mask = Input::get_singleton()->get_mouse_button_mask(); // For motion outside the canvas, only read mouse movement if dragging // started inside the canvas; imitating desktop app behaviour. - if (!ds->cursor_inside_canvas && !input_mask) - return false; + if (!get_singleton()->cursor_inside_canvas && !input_mask) + return; Ref<InputEventMouseMotion> ev; ev.instantiate(); - dom2godot_mod(p_event, ev); + dom2godot_mod(ev, p_modifiers); ev->set_button_mask(input_mask); - ev->set_position(pos); + ev->set_position(Point2(p_x, p_y)); ev->set_global_position(ev->get_position()); - ev->set_relative(Vector2(p_event->movementX, p_event->movementY)); - input->set_mouse_position(ev->get_position()); - ev->set_speed(input->get_last_mouse_speed()); + ev->set_relative(Vector2(p_rel_x, p_rel_y)); + Input::get_singleton()->set_mouse_position(ev->get_position()); + ev->set_speed(Input::get_singleton()->get_last_mouse_speed()); - input->parse_input_event(ev); - // Don't suppress mouseover/-leave events. - return false; + Input::get_singleton()->parse_input_event(ev); } // Cursor @@ -414,17 +373,15 @@ void DisplayServerJavaScript::mouse_set_mode(MouseMode p_mode) { if (p_mode == MOUSE_MODE_VISIBLE) { godot_js_display_cursor_set_visible(1); - emscripten_exit_pointerlock(); + godot_js_display_cursor_lock_set(0); } else if (p_mode == MOUSE_MODE_HIDDEN) { godot_js_display_cursor_set_visible(0); - emscripten_exit_pointerlock(); + godot_js_display_cursor_lock_set(0); } else if (p_mode == MOUSE_MODE_CAPTURED) { godot_js_display_cursor_set_visible(1); - EMSCRIPTEN_RESULT result = emscripten_request_pointerlock(canvas_id, false); - ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback."); - ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback."); + godot_js_display_cursor_lock_set(1); } } @@ -433,18 +390,17 @@ DisplayServer::MouseMode DisplayServerJavaScript::mouse_get_mode() const { return MOUSE_MODE_HIDDEN; } - EmscriptenPointerlockChangeEvent ev; - emscripten_get_pointerlock_status(&ev); - return (ev.isActive && String::utf8(ev.id) == String::utf8(&canvas_id[1])) ? MOUSE_MODE_CAPTURED : MOUSE_MODE_VISIBLE; + if (godot_js_display_cursor_is_locked()) { + return MOUSE_MODE_CAPTURED; + } + return MOUSE_MODE_VISIBLE; } // Wheel -EM_BOOL DisplayServerJavaScript::wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data) { - ERR_FAIL_COND_V(p_event_type != EMSCRIPTEN_EVENT_WHEEL, false); - DisplayServerJavaScript *ds = get_singleton(); - if (!is_canvas_focused()) { - if (ds->cursor_inside_canvas) { - focus_canvas(); +int DisplayServerJavaScript::mouse_wheel_callback(double p_delta_x, double p_delta_y) { + if (!godot_js_display_canvas_is_focused()) { + if (get_singleton()->cursor_inside_canvas) { + godot_js_display_canvas_focus(); } else { return false; } @@ -461,16 +417,17 @@ EM_BOOL DisplayServerJavaScript::wheel_callback(int p_event_type, const Emscript ev->set_ctrl_pressed(input->is_key_pressed(KEY_CTRL)); ev->set_meta_pressed(input->is_key_pressed(KEY_META)); - if (p_event->deltaY < 0) + if (p_delta_y < 0) { ev->set_button_index(MOUSE_BUTTON_WHEEL_UP); - else if (p_event->deltaY > 0) + } else if (p_delta_y > 0) { ev->set_button_index(MOUSE_BUTTON_WHEEL_DOWN); - else if (p_event->deltaX > 0) + } else if (p_delta_x > 0) { ev->set_button_index(MOUSE_BUTTON_WHEEL_LEFT); - else if (p_event->deltaX < 0) + } else if (p_delta_x < 0) { ev->set_button_index(MOUSE_BUTTON_WHEEL_RIGHT); - else + } else { return false; + } // Different browsers give wildly different delta values, and we can't // interpret deltaMode, so use default value for wheel events' factor. @@ -478,59 +435,55 @@ EM_BOOL DisplayServerJavaScript::wheel_callback(int p_event_type, const Emscript int button_flag = 1 << (ev->get_button_index() - 1); ev->set_pressed(true); - ev->set_button_mask(MouseButton(input->get_mouse_button_mask() | button_flag)); + ev->set_button_mask(input->get_mouse_button_mask() | button_flag); input->parse_input_event(ev); - ev->set_pressed(false); - ev->set_button_mask(MouseButton(input->get_mouse_button_mask() & ~button_flag)); - input->parse_input_event(ev); + Ref<InputEventMouseButton> release = ev->duplicate(); + release->set_pressed(false); + release->set_button_mask(MouseButton(input->get_mouse_button_mask() & ~button_flag)); + input->parse_input_event(release); return true; } // Touch -EM_BOOL DisplayServerJavaScript::touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) { - DisplayServerJavaScript *display = get_singleton(); - Ref<InputEventScreenTouch> ev; - ev.instantiate(); - int lowest_id_index = -1; - for (int i = 0; i < p_event->numTouches; ++i) { - const EmscriptenTouchPoint &touch = p_event->touches[i]; - if (lowest_id_index == -1 || touch.identifier < p_event->touches[lowest_id_index].identifier) - lowest_id_index = i; - if (!touch.isChanged) - continue; - ev->set_index(touch.identifier); - ev->set_position(compute_position_in_canvas(touch.clientX, touch.clientY)); - display->touches[i] = ev->get_position(); - ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_TOUCHSTART); +void DisplayServerJavaScript::touch_callback(int p_type, int p_count) { + DisplayServerJavaScript *ds = get_singleton(); - Input::get_singleton()->parse_input_event(ev); - } - // Resume audio context after input in case autoplay was denied. - return true; -} + const JSTouchEvent &touch_event = ds->touch_event; + for (int i = 0; i < p_count; i++) { + Point2 point(touch_event.coords[i * 2], touch_event.coords[i * 2 + 1]); + if (p_type == 2) { + // touchmove + Ref<InputEventScreenDrag> ev; + ev.instantiate(); + ev->set_index(touch_event.identifier[i]); + ev->set_position(point); + + Point2 &prev = ds->touches[i]; + ev->set_relative(ev->get_position() - prev); + prev = ev->get_position(); + + Input::get_singleton()->parse_input_event(ev); + } else { + // touchstart/touchend + Ref<InputEventScreenTouch> ev; -EM_BOOL DisplayServerJavaScript::touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) { - DisplayServerJavaScript *display = get_singleton(); - Ref<InputEventScreenDrag> ev; - ev.instantiate(); - int lowest_id_index = -1; - for (int i = 0; i < p_event->numTouches; ++i) { - const EmscriptenTouchPoint &touch = p_event->touches[i]; - if (lowest_id_index == -1 || touch.identifier < p_event->touches[lowest_id_index].identifier) - lowest_id_index = i; - if (!touch.isChanged) - continue; - ev->set_index(touch.identifier); - ev->set_position(compute_position_in_canvas(touch.clientX, touch.clientY)); - Point2 &prev = display->touches[i]; - ev->set_relative(ev->get_position() - prev); - prev = ev->get_position(); + // Resume audio context after input in case autoplay was denied. + OS_JavaScript::get_singleton()->resume_audio(); + + ev.instantiate(); + ev->set_index(touch_event.identifier[i]); + ev->set_position(point); + ev->set_pressed(p_type == 0); + ds->touches[i] = point; - Input::get_singleton()->parse_input_event(ev); + Input::get_singleton()->parse_input_event(ev); + + // Make sure to flush all events so we can call restricted APIs inside the event. + Input::get_singleton()->flush_buffered_events(); + } } - return true; } bool DisplayServerJavaScript::screen_is_touchscreen(int p_screen) const { @@ -574,6 +527,10 @@ void DisplayServerJavaScript::virtual_keyboard_hide() { godot_js_display_vk_hide(); } +void DisplayServerJavaScript::window_blur_callback() { + Input::get_singleton()->release_pressed_events(); +} + // Gamepad void DisplayServerJavaScript::gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid) { Input *input = Input::get_singleton(); @@ -586,14 +543,14 @@ void DisplayServerJavaScript::gamepad_callback(int p_index, int p_connected, con void DisplayServerJavaScript::process_joypads() { Input *input = Input::get_singleton(); - int32_t pads = godot_js_display_gamepad_sample_count(); + int32_t pads = godot_js_input_gamepad_sample_count(); int32_t s_btns_num = 0; int32_t s_axes_num = 0; int32_t s_standard = 0; float s_btns[16]; float s_axes[10]; for (int idx = 0; idx < pads; idx++) { - int err = godot_js_display_gamepad_sample_get(idx, s_btns, &s_btns_num, s_axes, &s_axes_num, &s_standard); + int err = godot_js_input_gamepad_sample_get(idx, s_btns, &s_btns_num, s_axes, &s_axes_num, &s_standard); if (err) { continue; } @@ -691,11 +648,6 @@ void DisplayServerJavaScript::set_icon(const Ref<Image> &p_icon) { } void DisplayServerJavaScript::_dispatch_input_event(const Ref<InputEvent> &p_event) { - OS_JavaScript *os = OS_JavaScript::get_singleton(); - - // Resume audio context after input in case autoplay was denied. - os->resume_audio(); - Callable cb = get_singleton()->input_event_callback; if (!cb.is_null()) { Variant ev = p_event; @@ -761,43 +713,24 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive video_driver_index = p_video_driver; #endif - EMSCRIPTEN_RESULT result; -#define EM_CHECK(ev) \ - if (result != EMSCRIPTEN_RESULT_SUCCESS) \ - ERR_PRINT("Error while setting " #ev " callback: Code " + itos(result)); -#define SET_EM_CALLBACK(target, ev, cb) \ - result = emscripten_set_##ev##_callback(target, nullptr, true, &cb); \ - EM_CHECK(ev) -#define SET_EM_WINDOW_CALLBACK(ev, cb) \ - result = emscripten_set_##ev##_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, false, &cb); \ - EM_CHECK(ev) - // These callbacks from Emscripten's html5.h suffice to access most - // JavaScript APIs. - SET_EM_CALLBACK(canvas_id, mousedown, mouse_button_callback) - SET_EM_WINDOW_CALLBACK(mousemove, mousemove_callback) - SET_EM_WINDOW_CALLBACK(mouseup, mouse_button_callback) - SET_EM_CALLBACK(canvas_id, wheel, wheel_callback) - SET_EM_CALLBACK(canvas_id, touchstart, touch_press_callback) - SET_EM_CALLBACK(canvas_id, touchmove, touchmove_callback) - SET_EM_CALLBACK(canvas_id, touchend, touch_press_callback) - SET_EM_CALLBACK(canvas_id, touchcancel, touch_press_callback) - SET_EM_CALLBACK(canvas_id, keydown, keydown_callback) - SET_EM_CALLBACK(canvas_id, keypress, keypress_callback) - SET_EM_CALLBACK(canvas_id, keyup, keyup_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, fullscreenchange, fullscreen_change_callback) -#undef SET_EM_CALLBACK -#undef EM_CHECK - - // For APIs that are not (sufficiently) exposed, a - // library is used below (implemented in library_godot_display.js). + // JS Input interface (js/libs/library_godot_input.js) + godot_js_input_mouse_button_cb(&DisplayServerJavaScript::mouse_button_callback); + godot_js_input_mouse_move_cb(&DisplayServerJavaScript::mouse_move_callback); + godot_js_input_mouse_wheel_cb(&DisplayServerJavaScript::mouse_wheel_callback); + godot_js_input_touch_cb(&DisplayServerJavaScript::touch_callback, touch_event.identifier, touch_event.coords); + godot_js_input_key_cb(&DisplayServerJavaScript::key_callback, key_event.code, key_event.key); + godot_js_input_paste_cb(update_clipboard_callback); + godot_js_input_drop_files_cb(drop_files_js_callback); + godot_js_input_gamepad_cb(&DisplayServerJavaScript::gamepad_callback); + + // JS Display interface (js/libs/library_godot_display.js) + godot_js_display_fullscreen_cb(&DisplayServerJavaScript::fullscreen_change_callback); + godot_js_display_window_blur_cb(&window_blur_callback); godot_js_display_notification_cb(&send_window_event_callback, WINDOW_EVENT_MOUSE_ENTER, WINDOW_EVENT_MOUSE_EXIT, WINDOW_EVENT_FOCUS_IN, WINDOW_EVENT_FOCUS_OUT); - godot_js_display_paste_cb(update_clipboard_callback); - godot_js_display_drop_files_cb(drop_files_js_callback); - godot_js_display_gamepad_cb(&DisplayServerJavaScript::gamepad_callback); godot_js_display_vk_cb(&vk_input_text_callback); Input::get_singleton()->set_event_dispatch_function(_dispatch_input_event); @@ -1019,7 +952,8 @@ bool DisplayServerJavaScript::can_any_window_draw() const { } void DisplayServerJavaScript::process_events() { - if (godot_js_display_gamepad_sample() == OK) { + Input::get_singleton()->flush_buffered_events(); + if (godot_js_input_gamepad_sample() == OK) { process_joypads(); } } diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h index bf5e229c9a..1aa600c421 100644 --- a/platform/javascript/display_server_javascript.h +++ b/platform/javascript/display_server_javascript.h @@ -38,6 +38,19 @@ class DisplayServerJavaScript : public DisplayServer { private: + struct JSTouchEvent { + uint32_t identifier[32] = { 0 }; + double coords[64] = { 0 }; + }; + JSTouchEvent touch_event; + + struct JSKeyEvent { + char code[32] = { 0 }; + char key[32] = { 0 }; + uint8_t modifiers[4] = { 0 }; + }; + JSKeyEvent key_event; + WindowMode window_mode = WINDOW_MODE_WINDOWED; ObjectID window_attached_instance_id = {}; @@ -47,44 +60,31 @@ private: Callable drop_files_callback; String clipboard; - Ref<InputEventKey> deferred_key_event; Point2 touches[32]; char canvas_id[256] = { 0 }; bool cursor_inside_canvas = true; CursorShape cursor_shape = CURSOR_ARROW; Point2i last_click_pos = Point2(-100, -100); // TODO check this again. - double last_click_ms = 0; + uint64_t last_click_ms = 0; int last_click_button_index = -1; bool swap_cancel_ok = false; // utilities - static Point2 compute_position_in_canvas(int p_x, int p_y); static void focus_canvas(); static bool is_canvas_focused(); - template <typename T> - static void dom2godot_mod(T *emscripten_event_ptr, Ref<InputEventWithModifiers> godot_event); - static Ref<InputEventKey> setup_key_event(const EmscriptenKeyboardEvent *emscripten_event); + static void dom2godot_mod(Ref<InputEventWithModifiers> ev, int p_mod); static const char *godot2dom_cursor(DisplayServer::CursorShape p_shape); // events - static EM_BOOL fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data); - - static EM_BOOL keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); - static EM_BOOL keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); - static EM_BOOL keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); - + static void fullscreen_change_callback(int p_fullscreen); + static int mouse_button_callback(int p_pressed, int p_button, double p_x, double p_y, int p_modifiers); + static void mouse_move_callback(double p_x, double p_y, double p_rel_x, double p_rel_y, int p_modifiers); + static int mouse_wheel_callback(double p_delta_x, double p_delta_y); + static void touch_callback(int p_type, int p_count); + static void key_callback(int p_pressed, int p_repeat, int p_modifiers); static void vk_input_text_callback(const char *p_text, int p_cursor); - - static EM_BOOL mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data); - static EM_BOOL mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data); - - static EM_BOOL wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data); - - static EM_BOOL touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data); - static EM_BOOL touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data); - static void gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid); void process_joypads(); @@ -94,6 +94,7 @@ private: static void _dispatch_input_event(const Ref<InputEvent> &p_event); static void request_quit_callback(); + static void window_blur_callback(); static void update_clipboard_callback(const char *p_text); static void send_window_event_callback(int p_notification); static void drop_files_js_callback(char **p_filev, int p_filec); diff --git a/platform/javascript/export/export_plugin.cpp b/platform/javascript/export/export_plugin.cpp index 5d285a6e60..c7bd172751 100644 --- a/platform/javascript/export/export_plugin.cpp +++ b/platform/javascript/export/export_plugin.cpp @@ -99,8 +99,8 @@ void EditorExportPlatformJavaScript::_replace_strings(Map<String, String> p_repl Vector<String> lines = str_template.split("\n"); for (int i = 0; i < lines.size(); i++) { String current_line = lines[i]; - for (Map<String, String>::Element *E = p_replaces.front(); E; E = E->next()) { - current_line = current_line.replace(E->key(), E->get()); + for (const KeyValue<String, String> &E : p_replaces) { + current_line = current_line.replace(E.key, E.value); } out += current_line + "\n"; } diff --git a/platform/javascript/export/export_plugin.h b/platform/javascript/export/export_plugin.h index 736edfe3a8..bdd1235259 100644 --- a/platform/javascript/export/export_plugin.h +++ b/platform/javascript/export/export_plugin.h @@ -134,7 +134,7 @@ public: virtual void get_platform_features(List<String> *r_features) override { r_features->push_back("web"); - r_features->push_back(get_os_name()); + r_features->push_back(get_os_name().to_lower()); } virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) override { diff --git a/platform/javascript/godot_audio.h b/platform/javascript/godot_audio.h index 54fc8fa3b5..eba025ab63 100644 --- a/platform/javascript/godot_audio.h +++ b/platform/javascript/godot_audio.h @@ -38,7 +38,9 @@ extern "C" { #include "stddef.h" extern int godot_audio_is_available(); -extern int godot_audio_init(int p_mix_rate, int p_latency, void (*_state_cb)(int), void (*_latency_cb)(float)); +extern int godot_audio_has_worklet(); +extern int godot_audio_has_script_processor(); +extern int godot_audio_init(int *p_mix_rate, int p_latency, void (*_state_cb)(int), void (*_latency_cb)(float)); extern void godot_audio_resume(); extern int godot_audio_capture_start(); @@ -46,14 +48,15 @@ extern void godot_audio_capture_stop(); // Worklet typedef int32_t GodotAudioState[4]; -extern void godot_audio_worklet_create(int p_channels); +extern int godot_audio_worklet_create(int p_channels); extern void godot_audio_worklet_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, GodotAudioState p_state); +extern void godot_audio_worklet_start_no_threads(float *p_out_buf, int p_out_size, void (*p_out_cb)(int p_pos, int p_frames), float *p_in_buf, int p_in_size, void (*p_in_cb)(int p_pos, int p_frames)); extern int godot_audio_worklet_state_add(GodotAudioState p_state, int p_idx, int p_value); extern int godot_audio_worklet_state_get(GodotAudioState p_state, int p_idx); extern int godot_audio_worklet_state_wait(int32_t *p_state, int p_idx, int32_t p_expected, int p_timeout); // Script -extern int godot_audio_script_create(int p_buffer_size, int p_channels); +extern int godot_audio_script_create(int *p_buffer_size, int p_channels); extern void godot_audio_script_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, void (*p_cb)()); #ifdef __cplusplus diff --git a/platform/javascript/godot_js.h b/platform/javascript/godot_js.h index d332af2c31..8051b270e6 100644 --- a/platform/javascript/godot_js.h +++ b/platform/javascript/godot_js.h @@ -50,12 +50,28 @@ extern int godot_js_os_execute(const char *p_json); extern void godot_js_os_shell_open(const char *p_uri); extern int godot_js_os_hw_concurrency_get(); +// Input +extern void godot_js_input_mouse_button_cb(int (*p_callback)(int p_pressed, int p_button, double p_x, double p_y, int p_modifiers)); +extern void godot_js_input_mouse_move_cb(void (*p_callback)(double p_x, double p_y, double p_rel_x, double p_rel_y, int p_modifiers)); +extern void godot_js_input_mouse_wheel_cb(int (*p_callback)(double p_delta_x, double p_delta_y)); +extern void godot_js_input_touch_cb(void (*p_callback)(int p_type, int p_count), uint32_t *r_identifiers, double *r_coords); +extern void godot_js_input_key_cb(void (*p_callback)(int p_type, int p_repeat, int p_modifiers), char r_code[32], char r_key[32]); + +// Input gamepad +extern void godot_js_input_gamepad_cb(void (*p_on_change)(int p_index, int p_connected, const char *p_id, const char *p_guid)); +extern int godot_js_input_gamepad_sample(); +extern int godot_js_input_gamepad_sample_count(); +extern int godot_js_input_gamepad_sample_get(int p_idx, float r_btns[16], int32_t *r_btns_num, float r_axes[10], int32_t *r_axes_num, int32_t *r_standard); +extern void godot_js_input_paste_cb(void (*p_callback)(const char *p_text)); +extern void godot_js_input_drop_files_cb(void (*p_callback)(char **p_filev, int p_filec)); + // Display extern int godot_js_display_screen_dpi_get(); extern double godot_js_display_pixel_ratio_get(); extern void godot_js_display_alert(const char *p_text); extern int godot_js_display_touchscreen_is_available(); extern int godot_js_display_is_swap_ok_cancel(); +extern void godot_js_display_setup_canvas(int p_width, int p_height, int p_fullscreen, int p_hidpi); // Display canvas extern void godot_js_display_canvas_focus(); @@ -68,7 +84,6 @@ extern void godot_js_display_window_size_get(int32_t *p_x, int32_t *p_y); extern void godot_js_display_screen_size_get(int32_t *p_x, int32_t *p_y); extern int godot_js_display_fullscreen_request(); extern int godot_js_display_fullscreen_exit(); -extern void godot_js_display_compute_position(int p_x, int p_y, int32_t *r_x, int32_t *r_y); extern void godot_js_display_window_title_set(const char *p_text); extern void godot_js_display_window_icon_set(const uint8_t *p_ptr, int p_len); extern int godot_js_display_has_webgl(int p_version); @@ -82,18 +97,13 @@ extern void godot_js_display_cursor_set_shape(const char *p_cursor); extern int godot_js_display_cursor_is_hidden(); extern void godot_js_display_cursor_set_custom_shape(const char *p_shape, const uint8_t *p_ptr, int p_len, int p_hotspot_x, int p_hotspot_y); extern void godot_js_display_cursor_set_visible(int p_visible); - -// Display gamepad -extern void godot_js_display_gamepad_cb(void (*p_on_change)(int p_index, int p_connected, const char *p_id, const char *p_guid)); -extern int godot_js_display_gamepad_sample(); -extern int godot_js_display_gamepad_sample_count(); -extern int godot_js_display_gamepad_sample_get(int p_idx, float r_btns[16], int32_t *r_btns_num, float r_axes[10], int32_t *r_axes_num, int32_t *r_standard); +extern void godot_js_display_cursor_lock_set(int p_lock); +extern int godot_js_display_cursor_is_locked(); // Display listeners +extern void godot_js_display_fullscreen_cb(void (*p_callback)(int p_fullscreen)); +extern void godot_js_display_window_blur_cb(void (*p_callback)()); extern void godot_js_display_notification_cb(void (*p_callback)(int p_notification), int p_enter, int p_exit, int p_in, int p_out); -extern void godot_js_display_paste_cb(void (*p_callback)(const char *p_text)); -extern void godot_js_display_drop_files_cb(void (*p_callback)(char **p_filev, int p_filec)); -extern void godot_js_display_setup_canvas(int p_width, int p_height, int p_fullscreen, int p_hidpi); // Display Virtual Keyboard extern int godot_js_display_vk_available(); diff --git a/platform/javascript/js/libs/audio.worklet.js b/platform/javascript/js/libs/audio.worklet.js index 866f845139..52b3aedf8c 100644 --- a/platform/javascript/js/libs/audio.worklet.js +++ b/platform/javascript/js/libs/audio.worklet.js @@ -29,15 +29,16 @@ /*************************************************************************/ class RingBuffer { - constructor(p_buffer, p_state) { + constructor(p_buffer, p_state, p_threads) { this.buffer = p_buffer; this.avail = p_state; + this.threads = p_threads; this.rpos = 0; this.wpos = 0; } data_left() { - return Atomics.load(this.avail, 0); + return this.threads ? Atomics.load(this.avail, 0) : this.avail; } space_left() { @@ -55,10 +56,16 @@ class RingBuffer { to_write -= high; this.rpos = 0; } - output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from); + if (to_write) { + output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from); + } this.rpos += to_write; - Atomics.add(this.avail, 0, -output.length); - Atomics.notify(this.avail, 0); + if (this.threads) { + Atomics.add(this.avail, 0, -output.length); + Atomics.notify(this.avail, 0); + } else { + this.avail -= output.length; + } } write(p_buffer) { @@ -66,25 +73,30 @@ class RingBuffer { const mw = this.buffer.length - this.wpos; if (mw >= to_write) { this.buffer.set(p_buffer, this.wpos); + this.wpos += to_write; + if (mw === to_write) { + this.wpos = 0; + } } else { - const high = p_buffer.subarray(0, to_write - mw); - const low = p_buffer.subarray(to_write - mw); + const high = p_buffer.subarray(0, mw); + const low = p_buffer.subarray(mw); this.buffer.set(high, this.wpos); this.buffer.set(low); + this.wpos = low.length; } - let diff = to_write; - if (this.wpos + diff >= this.buffer.length) { - diff -= this.buffer.length; + if (this.threads) { + Atomics.add(this.avail, 0, to_write); + Atomics.notify(this.avail, 0); + } else { + this.avail += to_write; } - this.wpos += diff; - Atomics.add(this.avail, 0, to_write); - Atomics.notify(this.avail, 0); } } class GodotProcessor extends AudioWorkletProcessor { constructor() { super(); + this.threads = false; this.running = true; this.lock = null; this.notifier = null; @@ -100,24 +112,31 @@ class GodotProcessor extends AudioWorkletProcessor { } process_notify() { - Atomics.add(this.notifier, 0, 1); - Atomics.notify(this.notifier, 0); + if (this.notifier) { + Atomics.add(this.notifier, 0, 1); + Atomics.notify(this.notifier, 0); + } } parse_message(p_cmd, p_data) { if (p_cmd === 'start' && p_data) { const state = p_data[0]; let idx = 0; + this.threads = true; this.lock = state.subarray(idx, ++idx); this.notifier = state.subarray(idx, ++idx); const avail_in = state.subarray(idx, ++idx); const avail_out = state.subarray(idx, ++idx); - this.input = new RingBuffer(p_data[1], avail_in); - this.output = new RingBuffer(p_data[2], avail_out); + this.input = new RingBuffer(p_data[1], avail_in, true); + this.output = new RingBuffer(p_data[2], avail_out, true); } else if (p_cmd === 'stop') { this.running = false; this.output = null; this.input = null; + } else if (p_cmd === 'start_nothreads') { + this.output = new RingBuffer(p_data[0], p_data[0].length, false); + } else if (p_cmd === 'chunk') { + this.output.write(p_data); } } @@ -139,7 +158,10 @@ class GodotProcessor extends AudioWorkletProcessor { if (this.input_buffer.length !== chunk) { this.input_buffer = new Float32Array(chunk); } - if (this.input.space_left() >= chunk) { + if (!this.threads) { + GodotProcessor.write_input(this.input_buffer, input); + this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer }); + } else if (this.input.space_left() >= chunk) { GodotProcessor.write_input(this.input_buffer, input); this.input.write(this.input_buffer); } else { @@ -156,6 +178,9 @@ class GodotProcessor extends AudioWorkletProcessor { if (this.output.data_left() >= chunk) { this.output.read(this.output_buffer); GodotProcessor.write_output(output, this.output_buffer); + if (!this.threads) { + this.port.postMessage({ 'cmd': 'read', 'data': chunk }); + } } else { this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); } diff --git a/platform/javascript/js/libs/library_godot_audio.js b/platform/javascript/js/libs/library_godot_audio.js index 45c3a3fe2e..6cbb0567f4 100644 --- a/platform/javascript/js/libs/library_godot_audio.js +++ b/platform/javascript/js/libs/library_godot_audio.js @@ -37,10 +37,14 @@ const GodotAudio = { interval: 0, init: function (mix_rate, latency, onstatechange, onlatencyupdate) { - const ctx = new (window.AudioContext || window.webkitAudioContext)({ - sampleRate: mix_rate, - // latencyHint: latency / 1000 // Do not specify, leave 'interactive' for good performance. - }); + const opts = {}; + // If mix_rate is 0, let the browser choose. + if (mix_rate) { + opts['sampleRate'] = mix_rate; + } + // Do not specify, leave 'interactive' for good performance. + // opts['latencyHint'] = latency / 1000; + const ctx = new (window.AudioContext || window.webkitAudioContext)(opts); GodotAudio.ctx = ctx; ctx.onstatechange = function () { let state = 0; @@ -155,11 +159,24 @@ const GodotAudio = { return 1; }, + godot_audio_has_worklet__sig: 'i', + godot_audio_has_worklet: function () { + return (GodotAudio.ctx && GodotAudio.ctx.audioWorklet) ? 1 : 0; + }, + + godot_audio_has_script_processor__sig: 'i', + godot_audio_has_script_processor: function () { + return (GodotAudio.ctx && GodotAudio.ctx.createScriptProcessor) ? 1 : 0; + }, + godot_audio_init__sig: 'iiiii', godot_audio_init: function (p_mix_rate, p_latency, p_state_change, p_latency_update) { const statechange = GodotRuntime.get_func(p_state_change); const latencyupdate = GodotRuntime.get_func(p_latency_update); - return GodotAudio.init(p_mix_rate, p_latency, statechange, latencyupdate); + const mix_rate = GodotRuntime.getHeapValue(p_mix_rate, 'i32'); + const channels = GodotAudio.init(mix_rate, p_latency, statechange, latencyupdate); + GodotRuntime.setHeapValue(p_mix_rate, GodotAudio.ctx.sampleRate, 'i32'); + return channels; }, godot_audio_resume__sig: 'v', @@ -202,6 +219,7 @@ const GodotAudioWorklet = { $GodotAudioWorklet: { promise: null, worklet: null, + ring_buffer: null, create: function (channels) { const path = GodotConfig.locate_file('godot.audio.worklet.js'); @@ -211,7 +229,7 @@ const GodotAudioWorklet = { 'godot-processor', { 'outputChannelCount': [channels], - }, + } ); return Promise.resolve(); }); @@ -232,6 +250,86 @@ const GodotAudioWorklet = { }); }, + start_no_threads: function (p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback) { + function RingBuffer() { + let wpos = 0; + let rpos = 0; + let pending_samples = 0; + const wbuf = new Float32Array(p_out_size); + + function send(port) { + if (pending_samples === 0) { + return; + } + const buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size); + const size = buffer.length; + const tot_sent = pending_samples; + out_callback(wpos, pending_samples); + if (wpos + pending_samples >= size) { + const high = size - wpos; + wbuf.set(buffer.subarray(wpos, size)); + pending_samples -= high; + wpos = 0; + } + if (pending_samples > 0) { + wbuf.set(buffer.subarray(wpos, wpos + pending_samples), tot_sent - pending_samples); + } + port.postMessage({ 'cmd': 'chunk', 'data': wbuf.subarray(0, tot_sent) }); + wpos += pending_samples; + pending_samples = 0; + } + this.receive = function (recv_buf) { + const buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size); + const from = rpos; + let to_write = recv_buf.length; + let high = 0; + if (rpos + to_write >= p_in_size) { + high = p_in_size - rpos; + buffer.set(recv_buf.subarray(0, high), rpos); + to_write -= high; + rpos = 0; + } + if (to_write) { + buffer.set(recv_buf.subarray(high, to_write), rpos); + } + in_callback(from, recv_buf.length); + rpos += to_write; + }; + this.consumed = function (size, port) { + pending_samples += size; + send(port); + }; + } + GodotAudioWorklet.ring_buffer = new RingBuffer(); + GodotAudioWorklet.promise.then(function () { + const node = GodotAudioWorklet.worklet; + const buffer = GodotRuntime.heapSlice(HEAPF32, p_out_buf, p_out_size); + node.connect(GodotAudio.ctx.destination); + node.port.postMessage({ + 'cmd': 'start_nothreads', + 'data': [buffer, p_in_size], + }); + node.port.onmessage = function (event) { + if (!GodotAudioWorklet.worklet) { + return; + } + if (event.data['cmd'] === 'read') { + const read = event.data['data']; + GodotAudioWorklet.ring_buffer.consumed(read, GodotAudioWorklet.worklet.port); + } else if (event.data['cmd'] === 'input') { + const buf = event.data['data']; + if (buf.length > p_in_size) { + GodotRuntime.error('Input chunk is too big'); + return; + } + GodotAudioWorklet.ring_buffer.receive(buf); + } else { + GodotRuntime.error(event.data); + } + }; + }); + }, + get_node: function () { return GodotAudioWorklet.worklet; }, @@ -255,9 +353,15 @@ const GodotAudioWorklet = { }, }, - godot_audio_worklet_create__sig: 'vi', + godot_audio_worklet_create__sig: 'ii', godot_audio_worklet_create: function (channels) { - GodotAudioWorklet.create(channels); + try { + GodotAudioWorklet.create(channels); + } catch (e) { + GodotRuntime.error('Error starting AudioDriverWorklet', e); + return 1; + } + return 0; }, godot_audio_worklet_start__sig: 'viiiii', @@ -268,6 +372,13 @@ const GodotAudioWorklet = { GodotAudioWorklet.start(in_buffer, out_buffer, state); }, + godot_audio_worklet_start_no_threads__sig: 'viiiiii', + godot_audio_worklet_start_no_threads: function (p_out_buf, p_out_size, p_out_callback, p_in_buf, p_in_size, p_in_callback) { + const out_callback = GodotRuntime.get_func(p_out_callback); + const in_callback = GodotRuntime.get_func(p_in_callback); + GodotAudioWorklet.start_no_threads(p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback); + }, + godot_audio_worklet_state_wait__sig: 'iiii', godot_audio_worklet_state_wait: function (p_state, p_idx, p_expected, p_timeout) { Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout); @@ -351,7 +462,15 @@ const GodotAudioScript = { godot_audio_script_create__sig: 'iii', godot_audio_script_create: function (buffer_length, channel_count) { - return GodotAudioScript.create(buffer_length, channel_count); + const buf_len = GodotRuntime.getHeapValue(buffer_length, 'i32'); + try { + const out_len = GodotAudioScript.create(buf_len, channel_count); + GodotRuntime.setHeapValue(buffer_length, out_len, 'i32'); + } catch (e) { + GodotRuntime.error('Error starting AudioDriverScriptProcessor', e); + return 1; + } + return 0; }, godot_audio_script_start__sig: 'viiiii', diff --git a/platform/javascript/js/libs/library_godot_display.js b/platform/javascript/js/libs/library_godot_display.js index affae90370..2689f1c22c 100644 --- a/platform/javascript/js/libs/library_godot_display.js +++ b/platform/javascript/js/libs/library_godot_display.js @@ -28,224 +28,9 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -/* - * Display Server listeners. - * Keeps track of registered event listeners so it can remove them on shutdown. - */ -const GodotDisplayListeners = { - $GodotDisplayListeners__deps: ['$GodotOS'], - $GodotDisplayListeners__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayListeners.clear(); resolve(); });', - $GodotDisplayListeners: { - handlers: [], - - has: function (target, event, method, capture) { - return GodotDisplayListeners.handlers.findIndex(function (e) { - return e.target === target && e.event === event && e.method === method && e.capture === capture; - }) !== -1; - }, - - add: function (target, event, method, capture) { - if (GodotDisplayListeners.has(target, event, method, capture)) { - return; - } - function Handler(p_target, p_event, p_method, p_capture) { - this.target = p_target; - this.event = p_event; - this.method = p_method; - this.capture = p_capture; - } - GodotDisplayListeners.handlers.push(new Handler(target, event, method, capture)); - target.addEventListener(event, method, capture); - }, - - clear: function () { - GodotDisplayListeners.handlers.forEach(function (h) { - h.target.removeEventListener(h.event, h.method, h.capture); - }); - GodotDisplayListeners.handlers.length = 0; - }, - }, -}; -mergeInto(LibraryManager.library, GodotDisplayListeners); - -/* - * Drag and drop handler. - * This is pretty big, but basically detect dropped files on GodotConfig.canvas, - * process them one by one (recursively for directories), and copies them to - * the temporary FS path '/tmp/drop-[random]/' so it can be emitted as a godot - * event (that requires a string array of paths). - * - * NOTE: The temporary files are removed after the callback. This means that - * deferred callbacks won't be able to access the files. - */ -const GodotDisplayDragDrop = { - $GodotDisplayDragDrop__deps: ['$FS', '$GodotFS'], - $GodotDisplayDragDrop: { - promises: [], - pending_files: [], - - add_entry: function (entry) { - if (entry.isDirectory) { - GodotDisplayDragDrop.add_dir(entry); - } else if (entry.isFile) { - GodotDisplayDragDrop.add_file(entry); - } else { - GodotRuntime.error('Unrecognized entry...', entry); - } - }, - - add_dir: function (entry) { - GodotDisplayDragDrop.promises.push(new Promise(function (resolve, reject) { - const reader = entry.createReader(); - reader.readEntries(function (entries) { - for (let i = 0; i < entries.length; i++) { - GodotDisplayDragDrop.add_entry(entries[i]); - } - resolve(); - }); - })); - }, - - add_file: function (entry) { - GodotDisplayDragDrop.promises.push(new Promise(function (resolve, reject) { - entry.file(function (file) { - const reader = new FileReader(); - reader.onload = function () { - const f = { - 'path': file.relativePath || file.webkitRelativePath, - 'name': file.name, - 'type': file.type, - 'size': file.size, - 'data': reader.result, - }; - if (!f['path']) { - f['path'] = f['name']; - } - GodotDisplayDragDrop.pending_files.push(f); - resolve(); - }; - reader.onerror = function () { - GodotRuntime.print('Error reading file'); - reject(); - }; - reader.readAsArrayBuffer(file); - }, function (err) { - GodotRuntime.print('Error!'); - reject(); - }); - })); - }, - - process: function (resolve, reject) { - if (GodotDisplayDragDrop.promises.length === 0) { - resolve(); - return; - } - GodotDisplayDragDrop.promises.pop().then(function () { - setTimeout(function () { - GodotDisplayDragDrop.process(resolve, reject); - }, 0); - }); - }, - - _process_event: function (ev, callback) { - ev.preventDefault(); - if (ev.dataTransfer.items) { - // Use DataTransferItemList interface to access the file(s) - for (let i = 0; i < ev.dataTransfer.items.length; i++) { - const item = ev.dataTransfer.items[i]; - let entry = null; - if ('getAsEntry' in item) { - entry = item.getAsEntry(); - } else if ('webkitGetAsEntry' in item) { - entry = item.webkitGetAsEntry(); - } - if (entry) { - GodotDisplayDragDrop.add_entry(entry); - } - } - } else { - GodotRuntime.error('File upload not supported'); - } - new Promise(GodotDisplayDragDrop.process).then(function () { - const DROP = `/tmp/drop-${parseInt(Math.random() * (1 << 30), 10)}/`; - const drops = []; - const files = []; - FS.mkdir(DROP); - GodotDisplayDragDrop.pending_files.forEach((elem) => { - const path = elem['path']; - GodotFS.copy_to_fs(DROP + path, elem['data']); - let idx = path.indexOf('/'); - if (idx === -1) { - // Root file - drops.push(DROP + path); - } else { - // Subdir - const sub = path.substr(0, idx); - idx = sub.indexOf('/'); - if (idx < 0 && drops.indexOf(DROP + sub) === -1) { - drops.push(DROP + sub); - } - } - files.push(DROP + path); - }); - GodotDisplayDragDrop.promises = []; - GodotDisplayDragDrop.pending_files = []; - callback(drops); - if (GodotConfig.persistent_drops) { - // Delay removal at exit. - GodotOS.atexit(function (resolve, reject) { - GodotDisplayDragDrop.remove_drop(files, DROP); - resolve(); - }); - } else { - GodotDisplayDragDrop.remove_drop(files, DROP); - } - }); - }, - - remove_drop: function (files, drop_path) { - const dirs = [drop_path.substr(0, drop_path.length - 1)]; - // Remove temporary files - files.forEach(function (file) { - FS.unlink(file); - let dir = file.replace(drop_path, ''); - let idx = dir.lastIndexOf('/'); - while (idx > 0) { - dir = dir.substr(0, idx); - if (dirs.indexOf(drop_path + dir) === -1) { - dirs.push(drop_path + dir); - } - idx = dir.lastIndexOf('/'); - } - }); - // Remove dirs. - dirs.sort(function (a, b) { - const al = (a.match(/\//g) || []).length; - const bl = (b.match(/\//g) || []).length; - if (al > bl) { - return -1; - } else if (al < bl) { - return 1; - } - return 0; - }).forEach(function (dir) { - FS.rmdir(dir); - }); - }, - - handler: function (callback) { - return function (ev) { - GodotDisplayDragDrop._process_event(ev, callback); - }; - }, - }, -}; -mergeInto(LibraryManager.library, GodotDisplayDragDrop); - const GodotDisplayVK = { - $GodotDisplayVK__deps: ['$GodotRuntime', '$GodotConfig', '$GodotDisplayListeners'], + $GodotDisplayVK__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners'], $GodotDisplayVK__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayVK.clear(); resolve(); });', $GodotDisplayVK: { textinput: null, @@ -271,12 +56,12 @@ const GodotDisplayVK = { elem.style.outline = 'none'; elem.readonly = true; elem.disabled = true; - GodotDisplayListeners.add(elem, 'input', function (evt) { + GodotEventListeners.add(elem, 'input', function (evt) { const c_str = GodotRuntime.allocString(elem.value); input_cb(c_str, elem.selectionEnd); GodotRuntime.free(c_str); }, false); - GodotDisplayListeners.add(elem, 'blur', function (evt) { + GodotEventListeners.add(elem, 'blur', function (evt) { elem.style.display = 'none'; elem.readonly = true; elem.disabled = true; @@ -376,136 +161,23 @@ const GodotDisplayCursor = { delete GodotDisplayCursor.cursors[key]; }); }, - }, -}; -mergeInto(LibraryManager.library, GodotDisplayCursor); - -/* - * Display Gamepad API helper. - */ -const GodotDisplayGamepads = { - $GodotDisplayGamepads__deps: ['$GodotRuntime', '$GodotDisplayListeners'], - $GodotDisplayGamepads: { - samples: [], - - get_pads: function () { - try { - // Will throw in iframe when permission is denied. - // Will throw/warn in the future for insecure contexts. - // See https://github.com/w3c/gamepad/pull/120 - const pads = navigator.getGamepads(); - if (pads) { - return pads; - } - return []; - } catch (e) { - return []; - } - }, - - get_samples: function () { - return GodotDisplayGamepads.samples; - }, - - get_sample: function (index) { - const samples = GodotDisplayGamepads.samples; - return index < samples.length ? samples[index] : null; - }, - - sample: function () { - const pads = GodotDisplayGamepads.get_pads(); - const samples = []; - for (let i = 0; i < pads.length; i++) { - const pad = pads[i]; - if (!pad) { - samples.push(null); - continue; - } - const s = { - standard: pad.mapping === 'standard', - buttons: [], - axes: [], - connected: pad.connected, - }; - for (let b = 0; b < pad.buttons.length; b++) { - s.buttons.push(pad.buttons[b].value); - } - for (let a = 0; a < pad.axes.length; a++) { - s.axes.push(pad.axes[a]); - } - samples.push(s); + lockPointer: function () { + const canvas = GodotConfig.canvas; + if (canvas.requestPointerLock) { + canvas.requestPointerLock(); } - GodotDisplayGamepads.samples = samples; }, - - init: function (onchange) { - GodotDisplayListeners.samples = []; - function add(pad) { - const guid = GodotDisplayGamepads.get_guid(pad); - const c_id = GodotRuntime.allocString(pad.id); - const c_guid = GodotRuntime.allocString(guid); - onchange(pad.index, 1, c_id, c_guid); - GodotRuntime.free(c_id); - GodotRuntime.free(c_guid); + releasePointer: function () { + if (document.exitPointerLock) { + document.exitPointerLock(); } - const pads = GodotDisplayGamepads.get_pads(); - for (let i = 0; i < pads.length; i++) { - // Might be reserved space. - if (pads[i]) { - add(pads[i]); - } - } - GodotDisplayListeners.add(window, 'gamepadconnected', function (evt) { - add(evt.gamepad); - }, false); - GodotDisplayListeners.add(window, 'gamepaddisconnected', function (evt) { - onchange(evt.gamepad.index, 0); - }, false); }, - - get_guid: function (pad) { - if (pad.mapping) { - return pad.mapping; - } - const ua = navigator.userAgent; - let os = 'Unknown'; - if (ua.indexOf('Android') >= 0) { - os = 'Android'; - } else if (ua.indexOf('Linux') >= 0) { - os = 'Linux'; - } else if (ua.indexOf('iPhone') >= 0) { - os = 'iOS'; - } else if (ua.indexOf('Macintosh') >= 0) { - // Updated iPads will fall into this category. - os = 'MacOSX'; - } else if (ua.indexOf('Windows') >= 0) { - os = 'Windows'; - } - - const id = pad.id; - // Chrom* style: NAME (Vendor: xxxx Product: xxxx) - const exp1 = /vendor: ([0-9a-f]{4}) product: ([0-9a-f]{4})/i; - // Firefox/Safari style (safari may remove leading zeores) - const exp2 = /^([0-9a-f]+)-([0-9a-f]+)-/i; - let vendor = ''; - let product = ''; - if (exp1.test(id)) { - const match = exp1.exec(id); - vendor = match[1].padStart(4, '0'); - product = match[2].padStart(4, '0'); - } else if (exp2.test(id)) { - const match = exp2.exec(id); - vendor = match[1].padStart(4, '0'); - product = match[2].padStart(4, '0'); - } - if (!vendor || !product) { - return `${os}Unknown`; - } - return os + vendor + product; + isPointerLocked: function () { + return document.pointerLockElement === GodotConfig.canvas; }, }, }; -mergeInto(LibraryManager.library, GodotDisplayGamepads); +mergeInto(LibraryManager.library, GodotDisplayCursor); const GodotDisplayScreen = { $GodotDisplayScreen__deps: ['$GodotConfig', '$GodotOS', '$GL', 'emscripten_webgl_get_current_context'], @@ -622,7 +294,7 @@ mergeInto(LibraryManager.library, GodotDisplayScreen); * Exposes all the functions needed by DisplayServer implementation. */ const GodotDisplay = { - $GodotDisplay__deps: ['$GodotConfig', '$GodotRuntime', '$GodotDisplayCursor', '$GodotDisplayListeners', '$GodotDisplayDragDrop', '$GodotDisplayGamepads', '$GodotDisplayScreen', '$GodotDisplayVK'], + $GodotDisplay__deps: ['$GodotConfig', '$GodotRuntime', '$GodotDisplayCursor', '$GodotEventListeners', '$GodotDisplayScreen', '$GodotDisplayVK'], $GodotDisplay: { window_icon: '', findDPI: function () { @@ -710,15 +382,6 @@ const GodotDisplay = { GodotRuntime.setHeapValue(p_height, GodotConfig.canvas.height, 'i32'); }, - godot_js_display_compute_position: function (x, y, r_x, r_y) { - const canvas = GodotConfig.canvas; - const rect = canvas.getBoundingClientRect(); - const rw = canvas.width / rect.width; - const rh = canvas.height / rect.height; - GodotRuntime.setHeapValue(r_x, (x - rect.x) * rw, 'i32'); - GodotRuntime.setHeapValue(r_y, (y - rect.y) * rh, 'i32'); - }, - godot_js_display_has_webgl__sig: 'ii', godot_js_display_has_webgl: function (p_version) { if (p_version !== 1 && p_version !== 2) { @@ -859,60 +522,64 @@ const GodotDisplay = { } }, + godot_js_display_cursor_lock_set__sig: 'vi', + godot_js_display_cursor_lock_set: function (p_lock) { + if (p_lock) { + GodotDisplayCursor.lockPointer(); + } else { + GodotDisplayCursor.releasePointer(); + } + }, + + godot_js_display_cursor_is_locked__sig: 'i', + godot_js_display_cursor_is_locked: function () { + return GodotDisplayCursor.isPointerLocked() ? 1 : 0; + }, + /* * Listeners */ - godot_js_display_notification_cb__sig: 'viiiii', - godot_js_display_notification_cb: function (callback, p_enter, p_exit, p_in, p_out) { + godot_js_display_fullscreen_cb__sig: 'vi', + godot_js_display_fullscreen_cb: function (callback) { const canvas = GodotConfig.canvas; const func = GodotRuntime.get_func(callback); - const notif = [p_enter, p_exit, p_in, p_out]; - ['mouseover', 'mouseleave', 'focus', 'blur'].forEach(function (evt_name, idx) { - GodotDisplayListeners.add(canvas, evt_name, function () { - func(notif[idx]); - }, true); - }); + function change_cb(evt) { + if (evt.target === canvas) { + func(GodotDisplayScreen.isFullscreen()); + } + } + GodotEventListeners.add(document, 'fullscreenchange', change_cb, false); + GodotEventListeners.add(document, 'mozfullscreenchange', change_cb, false); + GodotEventListeners.add(document, 'webkitfullscreenchange', change_cb, false); }, - godot_js_display_paste_cb__sig: 'vi', - godot_js_display_paste_cb: function (callback) { + godot_js_display_window_blur_cb__sig: 'vi', + godot_js_display_window_blur_cb: function (callback) { const func = GodotRuntime.get_func(callback); - GodotDisplayListeners.add(window, 'paste', function (evt) { - const text = evt.clipboardData.getData('text'); - const ptr = GodotRuntime.allocString(text); - func(ptr); - GodotRuntime.free(ptr); + GodotEventListeners.add(window, 'blur', function () { + func(); }, false); }, - godot_js_display_drop_files_cb__sig: 'vi', - godot_js_display_drop_files_cb: function (callback) { - const func = GodotRuntime.get_func(callback); - const dropFiles = function (files) { - const args = files || []; - if (!args.length) { - return; - } - const argc = args.length; - const argv = GodotRuntime.allocStringArray(args); - func(argv, argc); - GodotRuntime.freeStringArray(argv, argc); - }; + godot_js_display_notification_cb__sig: 'viiiii', + godot_js_display_notification_cb: function (callback, p_enter, p_exit, p_in, p_out) { const canvas = GodotConfig.canvas; - GodotDisplayListeners.add(canvas, 'dragover', function (ev) { - // Prevent default behavior (which would try to open the file(s)) - ev.preventDefault(); - }, false); - GodotDisplayListeners.add(canvas, 'drop', GodotDisplayDragDrop.handler(dropFiles)); + const func = GodotRuntime.get_func(callback); + const notif = [p_enter, p_exit, p_in, p_out]; + ['mouseover', 'mouseleave', 'focus', 'blur'].forEach(function (evt_name, idx) { + GodotEventListeners.add(canvas, evt_name, function () { + func(notif[idx]); + }, true); + }); }, godot_js_display_setup_canvas__sig: 'viiii', godot_js_display_setup_canvas: function (p_width, p_height, p_fullscreen, p_hidpi) { const canvas = GodotConfig.canvas; - GodotDisplayListeners.add(canvas, 'contextmenu', function (ev) { + GodotEventListeners.add(canvas, 'contextmenu', function (ev) { ev.preventDefault(); }, false); - GodotDisplayListeners.add(canvas, 'webglcontextlost', function (ev) { + GodotEventListeners.add(canvas, 'webglcontextlost', function (ev) { alert('WebGL context lost, please reload the page'); // eslint-disable-line no-alert ev.preventDefault(); }, false); @@ -965,49 +632,6 @@ const GodotDisplay = { GodotDisplayVK.init(input_cb); } }, - - /* - * Gamepads - */ - godot_js_display_gamepad_cb__sig: 'vi', - godot_js_display_gamepad_cb: function (change_cb) { - const onchange = GodotRuntime.get_func(change_cb); - GodotDisplayGamepads.init(onchange); - }, - - godot_js_display_gamepad_sample_count__sig: 'i', - godot_js_display_gamepad_sample_count: function () { - return GodotDisplayGamepads.get_samples().length; - }, - - godot_js_display_gamepad_sample__sig: 'i', - godot_js_display_gamepad_sample: function () { - GodotDisplayGamepads.sample(); - return 0; - }, - - godot_js_display_gamepad_sample_get__sig: 'iiiiiii', - godot_js_display_gamepad_sample_get: function (p_index, r_btns, r_btns_num, r_axes, r_axes_num, r_standard) { - const sample = GodotDisplayGamepads.get_sample(p_index); - if (!sample || !sample.connected) { - return 1; - } - const btns = sample.buttons; - const btns_len = btns.length < 16 ? btns.length : 16; - for (let i = 0; i < btns_len; i++) { - GodotRuntime.setHeapValue(r_btns + (i << 2), btns[i], 'float'); - } - GodotRuntime.setHeapValue(r_btns_num, btns_len, 'i32'); - const axes = sample.axes; - const axes_len = axes.length < 10 ? axes.length : 10; - for (let i = 0; i < axes_len; i++) { - GodotRuntime.setHeapValue(r_axes + (i << 2), axes[i], 'float'); - } - GodotRuntime.setHeapValue(r_axes_num, axes_len, 'i32'); - const is_standard = sample.standard ? 1 : 0; - GodotRuntime.setHeapValue(r_standard, is_standard, 'i32'); - return 0; - }, }; autoAddDeps(GodotDisplay, '$GodotDisplay'); diff --git a/platform/javascript/js/libs/library_godot_input.js b/platform/javascript/js/libs/library_godot_input.js new file mode 100644 index 0000000000..587c320f35 --- /dev/null +++ b/platform/javascript/js/libs/library_godot_input.js @@ -0,0 +1,526 @@ +/*************************************************************************/ +/* library_godot_input.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 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. */ +/*************************************************************************/ + +/* + * Gamepad API helper. + */ +const GodotInputGamepads = { + $GodotInputGamepads__deps: ['$GodotRuntime', '$GodotEventListeners'], + $GodotInputGamepads: { + samples: [], + + get_pads: function () { + try { + // Will throw in iframe when permission is denied. + // Will throw/warn in the future for insecure contexts. + // See https://github.com/w3c/gamepad/pull/120 + const pads = navigator.getGamepads(); + if (pads) { + return pads; + } + return []; + } catch (e) { + return []; + } + }, + + get_samples: function () { + return GodotInputGamepads.samples; + }, + + get_sample: function (index) { + const samples = GodotInputGamepads.samples; + return index < samples.length ? samples[index] : null; + }, + + sample: function () { + const pads = GodotInputGamepads.get_pads(); + const samples = []; + for (let i = 0; i < pads.length; i++) { + const pad = pads[i]; + if (!pad) { + samples.push(null); + continue; + } + const s = { + standard: pad.mapping === 'standard', + buttons: [], + axes: [], + connected: pad.connected, + }; + for (let b = 0; b < pad.buttons.length; b++) { + s.buttons.push(pad.buttons[b].value); + } + for (let a = 0; a < pad.axes.length; a++) { + s.axes.push(pad.axes[a]); + } + samples.push(s); + } + GodotInputGamepads.samples = samples; + }, + + init: function (onchange) { + GodotEventListeners.samples = []; + function add(pad) { + const guid = GodotInputGamepads.get_guid(pad); + const c_id = GodotRuntime.allocString(pad.id); + const c_guid = GodotRuntime.allocString(guid); + onchange(pad.index, 1, c_id, c_guid); + GodotRuntime.free(c_id); + GodotRuntime.free(c_guid); + } + const pads = GodotInputGamepads.get_pads(); + for (let i = 0; i < pads.length; i++) { + // Might be reserved space. + if (pads[i]) { + add(pads[i]); + } + } + GodotEventListeners.add(window, 'gamepadconnected', function (evt) { + add(evt.gamepad); + }, false); + GodotEventListeners.add(window, 'gamepaddisconnected', function (evt) { + onchange(evt.gamepad.index, 0); + }, false); + }, + + get_guid: function (pad) { + if (pad.mapping) { + return pad.mapping; + } + const ua = navigator.userAgent; + let os = 'Unknown'; + if (ua.indexOf('Android') >= 0) { + os = 'Android'; + } else if (ua.indexOf('Linux') >= 0) { + os = 'Linux'; + } else if (ua.indexOf('iPhone') >= 0) { + os = 'iOS'; + } else if (ua.indexOf('Macintosh') >= 0) { + // Updated iPads will fall into this category. + os = 'MacOSX'; + } else if (ua.indexOf('Windows') >= 0) { + os = 'Windows'; + } + + const id = pad.id; + // Chrom* style: NAME (Vendor: xxxx Product: xxxx) + const exp1 = /vendor: ([0-9a-f]{4}) product: ([0-9a-f]{4})/i; + // Firefox/Safari style (safari may remove leading zeores) + const exp2 = /^([0-9a-f]+)-([0-9a-f]+)-/i; + let vendor = ''; + let product = ''; + if (exp1.test(id)) { + const match = exp1.exec(id); + vendor = match[1].padStart(4, '0'); + product = match[2].padStart(4, '0'); + } else if (exp2.test(id)) { + const match = exp2.exec(id); + vendor = match[1].padStart(4, '0'); + product = match[2].padStart(4, '0'); + } + if (!vendor || !product) { + return `${os}Unknown`; + } + return os + vendor + product; + }, + }, +}; +mergeInto(LibraryManager.library, GodotInputGamepads); + +/* + * Drag and drop helper. + * This is pretty big, but basically detect dropped files on GodotConfig.canvas, + * process them one by one (recursively for directories), and copies them to + * the temporary FS path '/tmp/drop-[random]/' so it can be emitted as a godot + * event (that requires a string array of paths). + * + * NOTE: The temporary files are removed after the callback. This means that + * deferred callbacks won't be able to access the files. + */ +const GodotInputDragDrop = { + $GodotInputDragDrop__deps: ['$FS', '$GodotFS'], + $GodotInputDragDrop: { + promises: [], + pending_files: [], + + add_entry: function (entry) { + if (entry.isDirectory) { + GodotInputDragDrop.add_dir(entry); + } else if (entry.isFile) { + GodotInputDragDrop.add_file(entry); + } else { + GodotRuntime.error('Unrecognized entry...', entry); + } + }, + + add_dir: function (entry) { + GodotInputDragDrop.promises.push(new Promise(function (resolve, reject) { + const reader = entry.createReader(); + reader.readEntries(function (entries) { + for (let i = 0; i < entries.length; i++) { + GodotInputDragDrop.add_entry(entries[i]); + } + resolve(); + }); + })); + }, + + add_file: function (entry) { + GodotInputDragDrop.promises.push(new Promise(function (resolve, reject) { + entry.file(function (file) { + const reader = new FileReader(); + reader.onload = function () { + const f = { + 'path': file.relativePath || file.webkitRelativePath, + 'name': file.name, + 'type': file.type, + 'size': file.size, + 'data': reader.result, + }; + if (!f['path']) { + f['path'] = f['name']; + } + GodotInputDragDrop.pending_files.push(f); + resolve(); + }; + reader.onerror = function () { + GodotRuntime.print('Error reading file'); + reject(); + }; + reader.readAsArrayBuffer(file); + }, function (err) { + GodotRuntime.print('Error!'); + reject(); + }); + })); + }, + + process: function (resolve, reject) { + if (GodotInputDragDrop.promises.length === 0) { + resolve(); + return; + } + GodotInputDragDrop.promises.pop().then(function () { + setTimeout(function () { + GodotInputDragDrop.process(resolve, reject); + }, 0); + }); + }, + + _process_event: function (ev, callback) { + ev.preventDefault(); + if (ev.dataTransfer.items) { + // Use DataTransferItemList interface to access the file(s) + for (let i = 0; i < ev.dataTransfer.items.length; i++) { + const item = ev.dataTransfer.items[i]; + let entry = null; + if ('getAsEntry' in item) { + entry = item.getAsEntry(); + } else if ('webkitGetAsEntry' in item) { + entry = item.webkitGetAsEntry(); + } + if (entry) { + GodotInputDragDrop.add_entry(entry); + } + } + } else { + GodotRuntime.error('File upload not supported'); + } + new Promise(GodotInputDragDrop.process).then(function () { + const DROP = `/tmp/drop-${parseInt(Math.random() * (1 << 30), 10)}/`; + const drops = []; + const files = []; + FS.mkdir(DROP); + GodotInputDragDrop.pending_files.forEach((elem) => { + const path = elem['path']; + GodotFS.copy_to_fs(DROP + path, elem['data']); + let idx = path.indexOf('/'); + if (idx === -1) { + // Root file + drops.push(DROP + path); + } else { + // Subdir + const sub = path.substr(0, idx); + idx = sub.indexOf('/'); + if (idx < 0 && drops.indexOf(DROP + sub) === -1) { + drops.push(DROP + sub); + } + } + files.push(DROP + path); + }); + GodotInputDragDrop.promises = []; + GodotInputDragDrop.pending_files = []; + callback(drops); + if (GodotConfig.persistent_drops) { + // Delay removal at exit. + GodotOS.atexit(function (resolve, reject) { + GodotInputDragDrop.remove_drop(files, DROP); + resolve(); + }); + } else { + GodotInputDragDrop.remove_drop(files, DROP); + } + }); + }, + + remove_drop: function (files, drop_path) { + const dirs = [drop_path.substr(0, drop_path.length - 1)]; + // Remove temporary files + files.forEach(function (file) { + FS.unlink(file); + let dir = file.replace(drop_path, ''); + let idx = dir.lastIndexOf('/'); + while (idx > 0) { + dir = dir.substr(0, idx); + if (dirs.indexOf(drop_path + dir) === -1) { + dirs.push(drop_path + dir); + } + idx = dir.lastIndexOf('/'); + } + }); + // Remove dirs. + dirs.sort(function (a, b) { + const al = (a.match(/\//g) || []).length; + const bl = (b.match(/\//g) || []).length; + if (al > bl) { + return -1; + } else if (al < bl) { + return 1; + } + return 0; + }).forEach(function (dir) { + FS.rmdir(dir); + }); + }, + + handler: function (callback) { + return function (ev) { + GodotInputDragDrop._process_event(ev, callback); + }; + }, + }, +}; +mergeInto(LibraryManager.library, GodotInputDragDrop); + +/* + * Godot exposed input functions. + */ +const GodotInput = { + $GodotInput__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners', '$GodotInputGamepads', '$GodotInputDragDrop'], + $GodotInput: { + getModifiers: function (evt) { + return (evt.shiftKey + 0) + ((evt.altKey + 0) << 1) + ((evt.ctrlKey + 0) << 2) + ((evt.metaKey + 0) << 3); + }, + computePosition: function (evt, rect) { + const canvas = GodotConfig.canvas; + const rw = canvas.width / rect.width; + const rh = canvas.height / rect.height; + const x = (evt.clientX - rect.x) * rw; + const y = (evt.clientY - rect.y) * rh; + return [x, y]; + }, + }, + + /* + * Mouse API + */ + godot_js_input_mouse_move_cb__sig: 'vi', + godot_js_input_mouse_move_cb: function (callback) { + const func = GodotRuntime.get_func(callback); + const canvas = GodotConfig.canvas; + function move_cb(evt) { + const rect = canvas.getBoundingClientRect(); + const pos = GodotInput.computePosition(evt, rect); + // Scale movement + const rw = canvas.width / rect.width; + const rh = canvas.height / rect.height; + const rel_pos_x = evt.movementX * rw; + const rel_pos_y = evt.movementY * rh; + const modifiers = GodotInput.getModifiers(evt); + func(pos[0], pos[1], rel_pos_x, rel_pos_y, modifiers); + } + GodotEventListeners.add(window, 'mousemove', move_cb, false); + }, + + godot_js_input_mouse_wheel_cb__sig: 'vi', + godot_js_input_mouse_wheel_cb: function (callback) { + const func = GodotRuntime.get_func(callback); + function wheel_cb(evt) { + if (func(evt['deltaX'] || 0, evt['deltaY'] || 0)) { + evt.preventDefault(); + } + } + GodotEventListeners.add(GodotConfig.canvas, 'wheel', wheel_cb, false); + }, + + godot_js_input_mouse_button_cb__sig: 'vi', + godot_js_input_mouse_button_cb: function (callback) { + const func = GodotRuntime.get_func(callback); + const canvas = GodotConfig.canvas; + function button_cb(p_pressed, evt) { + const rect = canvas.getBoundingClientRect(); + const pos = GodotInput.computePosition(evt, rect); + const modifiers = GodotInput.getModifiers(evt); + if (func(p_pressed, evt.button, pos[0], pos[1], modifiers)) { + evt.preventDefault(); + } + } + GodotEventListeners.add(canvas, 'mousedown', button_cb.bind(null, 1), false); + GodotEventListeners.add(window, 'mouseup', button_cb.bind(null, 0), false); + }, + + /* + * Touch API + */ + godot_js_input_touch_cb__sig: 'viii', + godot_js_input_touch_cb: function (callback, ids, coords) { + const func = GodotRuntime.get_func(callback); + const canvas = GodotConfig.canvas; + function touch_cb(type, evt) { + const rect = canvas.getBoundingClientRect(); + const touches = evt.changedTouches; + for (let i = 0; i < touches.length; i++) { + const touch = touches[i]; + const pos = GodotInput.computePosition(touch, rect); + GodotRuntime.setHeapValue(coords + (i * 2), pos[0], 'double'); + GodotRuntime.setHeapValue(coords + (i * 2 + 8), pos[1], 'double'); + GodotRuntime.setHeapValue(ids + i, touch.identifier, 'i32'); + } + func(type, touches.length); + if (evt.cancelable) { + evt.preventDefault(); + } + } + GodotEventListeners.add(canvas, 'touchstart', touch_cb.bind(null, 0), false); + GodotEventListeners.add(canvas, 'touchend', touch_cb.bind(null, 1), false); + GodotEventListeners.add(canvas, 'touchcancel', touch_cb.bind(null, 1), false); + GodotEventListeners.add(canvas, 'touchmove', touch_cb.bind(null, 2), false); + }, + + /* + * Key API + */ + godot_js_input_key_cb__sig: 'viii', + godot_js_input_key_cb: function (callback, code, key) { + const func = GodotRuntime.get_func(callback); + function key_cb(pressed, evt) { + const modifiers = GodotInput.getModifiers(evt); + GodotRuntime.stringToHeap(evt.code, code, 32); + GodotRuntime.stringToHeap(evt.key, key, 32); + func(pressed, evt.repeat, modifiers); + evt.preventDefault(); + } + GodotEventListeners.add(GodotConfig.canvas, 'keydown', key_cb.bind(null, 1), false); + GodotEventListeners.add(GodotConfig.canvas, 'keyup', key_cb.bind(null, 0), false); + }, + + /* + * Gamepad API + */ + godot_js_input_gamepad_cb__sig: 'vi', + godot_js_input_gamepad_cb: function (change_cb) { + const onchange = GodotRuntime.get_func(change_cb); + GodotInputGamepads.init(onchange); + }, + + godot_js_input_gamepad_sample_count__sig: 'i', + godot_js_input_gamepad_sample_count: function () { + return GodotInputGamepads.get_samples().length; + }, + + godot_js_input_gamepad_sample__sig: 'i', + godot_js_input_gamepad_sample: function () { + GodotInputGamepads.sample(); + return 0; + }, + + godot_js_input_gamepad_sample_get__sig: 'iiiiiii', + godot_js_input_gamepad_sample_get: function (p_index, r_btns, r_btns_num, r_axes, r_axes_num, r_standard) { + const sample = GodotInputGamepads.get_sample(p_index); + if (!sample || !sample.connected) { + return 1; + } + const btns = sample.buttons; + const btns_len = btns.length < 16 ? btns.length : 16; + for (let i = 0; i < btns_len; i++) { + GodotRuntime.setHeapValue(r_btns + (i << 2), btns[i], 'float'); + } + GodotRuntime.setHeapValue(r_btns_num, btns_len, 'i32'); + const axes = sample.axes; + const axes_len = axes.length < 10 ? axes.length : 10; + for (let i = 0; i < axes_len; i++) { + GodotRuntime.setHeapValue(r_axes + (i << 2), axes[i], 'float'); + } + GodotRuntime.setHeapValue(r_axes_num, axes_len, 'i32'); + const is_standard = sample.standard ? 1 : 0; + GodotRuntime.setHeapValue(r_standard, is_standard, 'i32'); + return 0; + }, + + /* + * Drag/Drop API + */ + godot_js_input_drop_files_cb__sig: 'vi', + godot_js_input_drop_files_cb: function (callback) { + const func = GodotRuntime.get_func(callback); + const dropFiles = function (files) { + const args = files || []; + if (!args.length) { + return; + } + const argc = args.length; + const argv = GodotRuntime.allocStringArray(args); + func(argv, argc); + GodotRuntime.freeStringArray(argv, argc); + }; + const canvas = GodotConfig.canvas; + GodotEventListeners.add(canvas, 'dragover', function (ev) { + // Prevent default behavior (which would try to open the file(s)) + ev.preventDefault(); + }, false); + GodotEventListeners.add(canvas, 'drop', GodotInputDragDrop.handler(dropFiles)); + }, + + /* Paste API */ + godot_js_input_paste_cb__sig: 'vi', + godot_js_input_paste_cb: function (callback) { + const func = GodotRuntime.get_func(callback); + GodotEventListeners.add(window, 'paste', function (evt) { + const text = evt.clipboardData.getData('text'); + const ptr = GodotRuntime.allocString(text); + func(ptr); + GodotRuntime.free(ptr); + }, false); + }, +}; + +autoAddDeps(GodotInput, '$GodotInput'); +mergeInto(LibraryManager.library, GodotInput); diff --git a/platform/javascript/js/libs/library_godot_os.js b/platform/javascript/js/libs/library_godot_os.js index 99e7ee8b5f..c552e99415 100644 --- a/platform/javascript/js/libs/library_godot_os.js +++ b/platform/javascript/js/libs/library_godot_os.js @@ -328,3 +328,43 @@ const GodotOS = { autoAddDeps(GodotOS, '$GodotOS'); mergeInto(LibraryManager.library, GodotOS); + +/* + * Godot event listeners. + * Keeps track of registered event listeners so it can remove them on shutdown. + */ +const GodotEventListeners = { + $GodotEventListeners__deps: ['$GodotOS'], + $GodotEventListeners__postset: 'GodotOS.atexit(function(resolve, reject) { GodotEventListeners.clear(); resolve(); });', + $GodotEventListeners: { + handlers: [], + + has: function (target, event, method, capture) { + return GodotEventListeners.handlers.findIndex(function (e) { + return e.target === target && e.event === event && e.method === method && e.capture === capture; + }) !== -1; + }, + + add: function (target, event, method, capture) { + if (GodotEventListeners.has(target, event, method, capture)) { + return; + } + function Handler(p_target, p_event, p_method, p_capture) { + this.target = p_target; + this.event = p_event; + this.method = p_method; + this.capture = p_capture; + } + GodotEventListeners.handlers.push(new Handler(target, event, method, capture)); + target.addEventListener(event, method, capture); + }, + + clear: function () { + GodotEventListeners.handlers.forEach(function (h) { + h.target.removeEventListener(h.event, h.method, h.capture); + }); + GodotEventListeners.handlers.length = 0; + }, + }, +}; +mergeInto(LibraryManager.library, GodotEventListeners); diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index 76102d941b..4431bd5f1b 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -63,9 +63,7 @@ void OS_JavaScript::initialize() { } void OS_JavaScript::resume_audio() { - if (audio_driver_javascript) { - audio_driver_javascript->resume(); - } + AudioDriverJavaScript::resume(); } void OS_JavaScript::set_main_loop(MainLoop *p_main_loop) { @@ -101,10 +99,10 @@ void OS_JavaScript::delete_main_loop() { void OS_JavaScript::finalize() { delete_main_loop(); - if (audio_driver_javascript) { - memdelete(audio_driver_javascript); - audio_driver_javascript = nullptr; + for (AudioDriverJavaScript *driver : audio_drivers) { + memdelete(driver); } + audio_drivers.clear(); } // Miscellaneous @@ -137,12 +135,12 @@ int OS_JavaScript::get_processor_count() const { } bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) { - if (p_feature == "HTML5" || p_feature == "web") { + if (p_feature == "html5" || p_feature == "web") { return true; } #ifdef JAVASCRIPT_EVAL_ENABLED - if (p_feature == "JavaScript") { + if (p_feature == "javascript") { return true; } #endif @@ -229,8 +227,13 @@ OS_JavaScript::OS_JavaScript() { setenv("LANG", locale_ptr, true); if (AudioDriverJavaScript::is_available()) { - audio_driver_javascript = memnew(AudioDriverJavaScript); - AudioDriverManager::add_driver(audio_driver_javascript); +#ifdef NO_THREADS + audio_drivers.push_back(memnew(AudioDriverScriptProcessor)); +#endif + audio_drivers.push_back(memnew(AudioDriverWorklet)); + } + for (int i = 0; i < audio_drivers.size(); i++) { + AudioDriverManager::add_driver(audio_drivers[i]); } idb_available = godot_js_os_fs_is_persistent(); diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index efac2dbca7..d053082d92 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -40,7 +40,7 @@ class OS_JavaScript : public OS_Unix { MainLoop *main_loop = nullptr; - AudioDriverJavaScript *audio_driver_javascript = nullptr; + List<AudioDriverJavaScript *> audio_drivers; bool idb_is_syncing = false; bool idb_available = false; diff --git a/platform/linuxbsd/README.md b/platform/linuxbsd/README.md new file mode 100644 index 0000000000..0d3fb37be5 --- /dev/null +++ b/platform/linuxbsd/README.md @@ -0,0 +1,11 @@ +# Linux/*BSD platform port + +This folder contains the C++ code for the Linux/*BSD platform port. + +## Artwork license + +[`logo.png`](logo.png) is derived from the [Linux logo](https://isc.tamu.edu/~lewing/linux/): + +> Permission to use and/or modify this image is granted provided you acknowledge me + <lewing@isc.tamu.edu> and [The GIMP](https://isc.tamu.edu/~lewing/gimp/) + if someone asks. diff --git a/platform/linuxbsd/crash_handler_linuxbsd.cpp b/platform/linuxbsd/crash_handler_linuxbsd.cpp index ea0222cb19..0e98af71fa 100644 --- a/platform/linuxbsd/crash_handler_linuxbsd.cpp +++ b/platform/linuxbsd/crash_handler_linuxbsd.cpp @@ -32,6 +32,8 @@ #include "core/config/project_settings.h" #include "core/os/os.h" +#include "core/version.h" +#include "core/version_hash.gen.h" #include "main/main.h" #ifdef DEBUG_ENABLED @@ -61,12 +63,19 @@ static void handle_crash(int sig) { } // Dump the backtrace to stderr with a message to the user + fprintf(stderr, "\n================================================================\n"); fprintf(stderr, "%s: Program crashed with signal %d\n", __FUNCTION__, sig); if (OS::get_singleton()->get_main_loop()) { OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); } + // Print the engine version just before, so that people are reminded to include the version in backtrace reports. + if (String(VERSION_HASH).length() != 0) { + fprintf(stderr, "Engine version: " VERSION_FULL_NAME " (" VERSION_HASH ")\n"); + } else { + fprintf(stderr, "Engine version: " VERSION_FULL_NAME "\n"); + } fprintf(stderr, "Dumping the backtrace. %s\n", msg.utf8().get_data()); char **strings = backtrace_symbols(bt_buffer, size); if (strings) { @@ -115,6 +124,7 @@ static void handle_crash(int sig) { free(strings); } fprintf(stderr, "-- END OF BACKTRACE --\n"); + fprintf(stderr, "================================================================\n"); // Abort to pass the error to the OS abort(); diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index 3e3ed469ed..1b9dd1e8f3 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -18,40 +18,42 @@ def can_build(): # Check the minimal dependencies x11_error = os.system("pkg-config --version > /dev/null") if x11_error: + print("Error: pkg-config not found. Aborting.") return False - x11_error = os.system("pkg-config x11 --modversion > /dev/null ") + x11_error = os.system("pkg-config x11 --modversion > /dev/null") if x11_error: + print("Error: X11 libraries not found. Aborting.") return False - x11_error = os.system("pkg-config xcursor --modversion > /dev/null ") + x11_error = os.system("pkg-config xcursor --modversion > /dev/null") if x11_error: - print("xcursor not found.. x11 disabled.") + print("Error: Xcursor library not found. Aborting.") return False - x11_error = os.system("pkg-config xinerama --modversion > /dev/null ") + x11_error = os.system("pkg-config xinerama --modversion > /dev/null") if x11_error: - print("xinerama not found.. x11 disabled.") + print("Error: Xinerama library not found. Aborting.") return False - x11_error = os.system("pkg-config xext --modversion > /dev/null ") + x11_error = os.system("pkg-config xext --modversion > /dev/null") if x11_error: - print("xext not found.. x11 disabled.") + print("Error: Xext library not found. Aborting.") return False - x11_error = os.system("pkg-config xrandr --modversion > /dev/null ") + x11_error = os.system("pkg-config xrandr --modversion > /dev/null") if x11_error: - print("xrandr not found.. x11 disabled.") + print("Error: XrandR library not found. Aborting.") return False - x11_error = os.system("pkg-config xrender --modversion > /dev/null ") + x11_error = os.system("pkg-config xrender --modversion > /dev/null") if x11_error: - print("xrender not found.. x11 disabled.") + print("Error: XRender library not found. Aborting.") return False - x11_error = os.system("pkg-config xi --modversion > /dev/null ") + x11_error = os.system("pkg-config xi --modversion > /dev/null") if x11_error: - print("xi not found.. Aborting.") + print("Error: Xi library not found. Aborting.") return False return True @@ -103,14 +105,12 @@ def configure(env): env.Prepend(CCFLAGS=["-O2"]) elif env["optimize"] == "size": # optimize for size env.Prepend(CCFLAGS=["-Os"]) - env.Prepend(CPPDEFINES=["DEBUG_ENABLED"]) if env["debug_symbols"]: env.Prepend(CCFLAGS=["-g2"]) elif env["target"] == "debug": env.Prepend(CCFLAGS=["-g3"]) - env.Prepend(CPPDEFINES=["DEBUG_ENABLED"]) env.Append(LINKFLAGS=["-rdynamic"]) ## Architecture @@ -138,7 +138,7 @@ def configure(env): # A convenience so you don't need to write use_lto too when using SCons env["use_lto"] = True else: - print("Using LLD with GCC is not supported yet, try compiling with 'use_llvm=yes'.") + print("Using LLD with GCC is not supported yet. Try compiling with 'use_llvm=yes'.") sys.exit(255) if env["use_coverage"]: @@ -201,11 +201,6 @@ def configure(env): env.Append(CCFLAGS=["-pipe"]) env.Append(LINKFLAGS=["-pipe"]) - # -fpie and -no-pie is supported on GCC 6+ and Clang 4+, both below our - # minimal requirements. - env.Append(CCFLAGS=["-fpie"]) - env.Append(LINKFLAGS=["-no-pie"]) - ## Dependencies env.ParseConfig("pkg-config x11 --cflags --libs") @@ -293,17 +288,10 @@ def configure(env): if any(platform.machine() in s for s in list_of_x86): env["x86_libtheora_opt_gcc"] = True - if not env["builtin_libvpx"]: - env.ParseConfig("pkg-config vpx --cflags --libs") - if not env["builtin_libvorbis"]: env["builtin_libogg"] = False # Needed to link against system libvorbis env.ParseConfig("pkg-config vorbis vorbisfile --cflags --libs") - if not env["builtin_opus"]: - env["builtin_libogg"] = False # Needed to link against system opus - env.ParseConfig("pkg-config opus opusfile --cflags --libs") - if not env["builtin_libogg"]: env.ParseConfig("pkg-config ogg --cflags --libs") @@ -334,36 +322,32 @@ def configure(env): ## Flags if os.system("pkg-config --exists alsa") == 0: # 0 means found - print("Enabling ALSA") env["alsa"] = True env.Append(CPPDEFINES=["ALSA_ENABLED", "ALSAMIDI_ENABLED"]) else: - print("ALSA libraries not found, disabling driver") + print("Warning: ALSA libraries not found. Disabling the ALSA audio driver.") if env["pulseaudio"]: if os.system("pkg-config --exists libpulse") == 0: # 0 means found - print("Enabling PulseAudio") env.Append(CPPDEFINES=["PULSEAUDIO_ENABLED"]) env.ParseConfig("pkg-config --cflags libpulse") else: - print("PulseAudio development libraries not found, disabling driver") + print("Warning: PulseAudio development libraries not found. Disabling the PulseAudio audio driver.") if env["dbus"]: if os.system("pkg-config --exists dbus-1") == 0: # 0 means found - print("Enabling D-Bus") env.Append(CPPDEFINES=["DBUS_ENABLED"]) env.ParseConfig("pkg-config --cflags --libs dbus-1") else: - print("D-Bus development libraries not found, disabling dependent features") + print("Warning: D-Bus development libraries not found. Disabling screensaver prevention.") if platform.system() == "Linux": env.Append(CPPDEFINES=["JOYDEV_ENABLED"]) if env["udev"]: if os.system("pkg-config --exists libudev") == 0: # 0 means found - print("Enabling udev support") env.Append(CPPDEFINES=["UDEV_ENABLED"]) else: - print("libudev development libraries not found, disabling udev support") + print("Warning: libudev development libraries not found. Disabling controller hotplugging support.") else: env["udev"] = False # Linux specific @@ -412,7 +396,7 @@ def configure(env): gnu_ld_version = re.search("^GNU ld [^$]*(\d+\.\d+)$", linker_version_str, re.MULTILINE) if not gnu_ld_version: print( - "Warning: Creating template binaries enabled for PCK embedding is currently only supported with GNU ld" + "Warning: Creating template binaries enabled for PCK embedding is currently only supported with GNU ld, not gold or LLD." ) else: if float(gnu_ld_version.group(1)) >= 2.30: diff --git a/platform/linuxbsd/display_server_x11.cpp b/platform/linuxbsd/display_server_x11.cpp index a39941339a..f2cd336b39 100644 --- a/platform/linuxbsd/display_server_x11.cpp +++ b/platform/linuxbsd/display_server_x11.cpp @@ -306,11 +306,11 @@ void DisplayServerX11::mouse_set_mode(MouseMode p_mode) { // The only modes that show a cursor are VISIBLE and CONFINED bool showCursor = (p_mode == MOUSE_MODE_VISIBLE || p_mode == MOUSE_MODE_CONFINED); - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { + for (const KeyValue<WindowID, WindowData> &E : windows) { if (showCursor) { - XDefineCursor(x11_display, E->get().x11_window, cursors[current_cursor]); // show cursor + XDefineCursor(x11_display, E.value.x11_window, cursors[current_cursor]); // show cursor } else { - XDefineCursor(x11_display, E->get().x11_window, null_cursor); // hide cursor + XDefineCursor(x11_display, E.value.x11_window, null_cursor); // hide cursor } } mouse_mode = p_mode; @@ -785,8 +785,8 @@ Vector<DisplayServer::WindowID> DisplayServerX11::get_window_list() const { _THREAD_SAFE_METHOD_ Vector<int> ret; - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - ret.push_back(E->key()); + for (const KeyValue<WindowID, WindowData> &E : windows) { + ret.push_back(E.key); } return ret; } @@ -864,8 +864,8 @@ DisplayServerX11::WindowID DisplayServerX11::get_window_at_screen_position(const WindowID found_window = INVALID_WINDOW_ID; WindowID parent_window = INVALID_WINDOW_ID; unsigned int focus_order = 0; - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - const WindowData &wd = E->get(); + for (const KeyValue<WindowID, WindowData> &E : windows) { + const WindowData &wd = E.value; // Discard windows with no focus. if (wd.focus_order == 0) { @@ -873,7 +873,7 @@ DisplayServerX11::WindowID DisplayServerX11::get_window_at_screen_position(const } // Find topmost window which contains the given position. - WindowID window_id = E->key(); + WindowID window_id = E.key; Rect2i win_rect = Rect2i(window_get_position(window_id), window_get_size(window_id)); if (win_rect.has_point(p_position)) { // For siblings, pick the window which was focused last. @@ -1768,8 +1768,8 @@ bool DisplayServerX11::window_can_draw(WindowID p_window) const { bool DisplayServerX11::can_any_window_draw() const { _THREAD_SAFE_METHOD_ - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - if (window_get_mode(E->key()) != WINDOW_MODE_MINIMIZED) { + for (const KeyValue<WindowID, WindowData> &E : windows) { + if (window_get_mode(E.key) != WINDOW_MODE_MINIMIZED) { return true; } } @@ -1841,12 +1841,12 @@ void DisplayServerX11::cursor_set_shape(CursorShape p_shape) { if (mouse_mode == MOUSE_MODE_VISIBLE || mouse_mode == MOUSE_MODE_CONFINED) { if (cursors[p_shape] != None) { - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - XDefineCursor(x11_display, E->get().x11_window, cursors[p_shape]); + for (const KeyValue<WindowID, WindowData> &E : windows) { + XDefineCursor(x11_display, E.value.x11_window, cursors[p_shape]); } } else if (cursors[CURSOR_ARROW] != None) { - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - XDefineCursor(x11_display, E->get().x11_window, cursors[CURSOR_ARROW]); + for (const KeyValue<WindowID, WindowData> &E : windows) { + XDefineCursor(x11_display, E.value.x11_window, cursors[CURSOR_ARROW]); } } } @@ -1944,8 +1944,8 @@ void DisplayServerX11::cursor_set_custom_image(const RES &p_cursor, CursorShape if (p_shape == current_cursor) { if (mouse_mode == MOUSE_MODE_VISIBLE || mouse_mode == MOUSE_MODE_CONFINED) { - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - XDefineCursor(x11_display, E->get().x11_window, cursors[p_shape]); + for (const KeyValue<WindowID, WindowData> &E : windows) { + XDefineCursor(x11_display, E.value.x11_window, cursors[p_shape]); } } } @@ -2068,6 +2068,24 @@ String DisplayServerX11::keyboard_get_layout_name(int p_index) const { return ret; } +Key DisplayServerX11::keyboard_get_keycode_from_physical(Key p_keycode) const { + unsigned int modifiers = p_keycode & KEY_MODIFIER_MASK; + unsigned int keycode_no_mod = p_keycode & KEY_CODE_MASK; + unsigned int xkeycode = KeyMappingX11::get_xlibcode((Key)keycode_no_mod); + KeySym xkeysym = XkbKeycodeToKeysym(x11_display, xkeycode, 0, 0); + if (xkeysym >= 'a' && xkeysym <= 'z') { + xkeysym -= ('a' - 'A'); + } + + Key key = KeyMappingX11::get_keycode(xkeysym); + // If not found, fallback to QWERTY. + // This should match the behavior of the event pump + if (key == KEY_NONE) { + return p_keycode; + } + return (Key)(key | modifiers); +} + DisplayServerX11::Property DisplayServerX11::_read_property(Display *p_display, Window p_window, Atom p_property) { Atom actual_type = None; int actual_format = 0; @@ -2517,8 +2535,8 @@ void DisplayServerX11::_xim_destroy_callback(::XIM im, ::XPointer client_data, DisplayServerX11 *ds = reinterpret_cast<DisplayServerX11 *>(client_data); ds->xim = nullptr; - for (Map<WindowID, WindowData>::Element *E = ds->windows.front(); E; E = E->next()) { - E->get().xic = nullptr; + for (KeyValue<WindowID, WindowData> &E : ds->windows) { + E.value.xic = nullptr; } } @@ -2526,9 +2544,9 @@ void DisplayServerX11::_window_changed(XEvent *event) { WindowID window_id = MAIN_WINDOW_ID; // Assign the event to the relevant window - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - if (event->xany.window == E->get().x11_window) { - window_id = E->key(); + for (const KeyValue<WindowID, WindowData> &E : windows) { + if (event->xany.window == E.value.x11_window) { + window_id = E.key; break; } } @@ -2602,8 +2620,8 @@ void DisplayServerX11::_dispatch_input_event(const Ref<InputEvent> &p_event) { callable.call((const Variant **)&evp, 1, ret, ce); } else { //send to all windows - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - Callable callable = E->get().input_event_callback; + for (KeyValue<WindowID, WindowData> &E : windows) { + Callable callable = E.value.input_event_callback; if (callable.is_null()) { continue; } @@ -2703,8 +2721,8 @@ void DisplayServerX11::process_events() { if (app_focused) { //verify that one of the windows has focus, else send focus out notification bool focus_found = false; - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - if (E->get().focused) { + for (const KeyValue<WindowID, WindowData> &E : windows) { + if (E.value.focused) { focus_found = true; break; } @@ -2749,9 +2767,9 @@ void DisplayServerX11::process_events() { WindowID window_id = MAIN_WINDOW_ID; // Assign the event to the relevant window - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - if (event.xany.window == E->get().x11_window) { - window_id = E->key(); + for (const KeyValue<WindowID, WindowData> &E : windows) { + if (event.xany.window == E.value.x11_window) { + window_id = E.key; break; } } @@ -2990,17 +3008,17 @@ void DisplayServerX11::process_events() { if (mouse_mode_grab) { // Show and update the cursor if confined and the window regained focus. - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { + for (const KeyValue<WindowID, WindowData> &E : windows) { if (mouse_mode == MOUSE_MODE_CONFINED) { - XUndefineCursor(x11_display, E->get().x11_window); + XUndefineCursor(x11_display, E.value.x11_window); } else if (mouse_mode == MOUSE_MODE_CAPTURED || mouse_mode == MOUSE_MODE_CONFINED_HIDDEN) { // Or re-hide it. - XDefineCursor(x11_display, E->get().x11_window, null_cursor); + XDefineCursor(x11_display, E.value.x11_window, null_cursor); } XGrabPointer( - x11_display, E->get().x11_window, True, + x11_display, E.value.x11_window, True, ButtonPressMask | ButtonReleaseMask | PointerMotionMask, - GrabModeAsync, GrabModeAsync, E->get().x11_window, None, CurrentTime); + GrabModeAsync, GrabModeAsync, E.value.x11_window, None, CurrentTime); } } #ifdef TOUCH_ENABLED @@ -3036,11 +3054,11 @@ void DisplayServerX11::process_events() { _send_window_event(wd, WINDOW_EVENT_FOCUS_OUT); if (mouse_mode_grab) { - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { + for (const KeyValue<WindowID, WindowData> &E : windows) { //dear X11, I try, I really try, but you never work, you do whathever you want. if (mouse_mode == MOUSE_MODE_CAPTURED) { // Show the cursor if we're in captured mode so it doesn't look weird. - XUndefineCursor(x11_display, E->get().x11_window); + XUndefineCursor(x11_display, E.value.x11_window); } } XUngrabPointer(x11_display, CurrentTime); @@ -3052,12 +3070,12 @@ void DisplayServerX11::process_events() { }*/ // Release every pointer to avoid sticky points - for (Map<int, Vector2>::Element *E = xi.state.front(); E; E = E->next()) { + for (const KeyValue<int, Vector2> &E : xi.state) { Ref<InputEventScreenTouch> st; st.instantiate(); - st->set_index(E->key()); + st->set_index(E.key); st->set_window_id(window_id); - st->set_position(E->get()); + st->set_position(E.value); Input::get_singleton()->parse_input_event(st); } xi.state.clear(); @@ -3144,9 +3162,9 @@ void DisplayServerX11::process_events() { // Note: This is needed for drag & drop to work between windows, // because the engine expects events to keep being processed // on the same window dragging started. - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - const WindowData &wd_other = E->get(); - WindowID window_id_other = E->key(); + for (const KeyValue<WindowID, WindowData> &E : windows) { + const WindowData &wd_other = E.value; + WindowID window_id_other = E.key; if (wd_other.focused) { if (window_id_other != window_id) { int x, y; @@ -3287,8 +3305,8 @@ void DisplayServerX11::process_events() { // Note: This is needed for drag & drop to work between windows, // because the engine expects events to keep being processed // on the same window dragging started. - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - const WindowData &wd_other = E->get(); + for (const KeyValue<WindowID, WindowData> &E : windows) { + const WindowData &wd_other = E.value; if (wd_other.focused) { int x, y; Window child; @@ -3296,7 +3314,7 @@ void DisplayServerX11::process_events() { Point2i pos_focused(x, y); - mm->set_window_id(E->key()); + mm->set_window_id(E.key); mm->set_position(pos_focused); mm->set_global_position(pos_focused); mm->set_speed(Input::get_singleton()->get_last_mouse_speed()); @@ -3487,8 +3505,8 @@ void DisplayServerX11::set_context(Context p_context) { context = p_context; - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - _update_context(E->get()); + for (KeyValue<WindowID, WindowData> &E : windows) { + _update_context(E.value); } } @@ -3614,7 +3632,8 @@ DisplayServer *DisplayServerX11::create_func(const String &p_rendering_driver, W DisplayServer *ds = memnew(DisplayServerX11(p_rendering_driver, p_mode, p_vsync_mode, p_flags, p_resolution, r_error)); if (r_error != OK) { OS::get_singleton()->alert("Your video card driver does not support any of the supported Vulkan versions.\n" - "Please update your drivers or if you have a very old or integrated GPU upgrade it.", + "Please update your drivers or if you have a very old or integrated GPU, upgrade it.\n" + "If you have updated your graphics drivers recently, try rebooting.", "Unable to initialize Video driver"); } return ds; @@ -4267,14 +4286,14 @@ DisplayServerX11::~DisplayServerX11() { events_thread.wait_to_finish(); //destroy all windows - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { + for (KeyValue<WindowID, WindowData> &E : windows) { #ifdef VULKAN_ENABLED if (rendering_driver == "vulkan") { - context_vulkan->window_destroy(E->key()); + context_vulkan->window_destroy(E.key); } #endif - WindowData &wd = E->get(); + WindowData &wd = E.value; if (wd.xic) { XDestroyIC(wd.xic); wd.xic = nullptr; diff --git a/platform/linuxbsd/display_server_x11.h b/platform/linuxbsd/display_server_x11.h index 052c6d6b7b..1887c7105b 100644 --- a/platform/linuxbsd/display_server_x11.h +++ b/platform/linuxbsd/display_server_x11.h @@ -372,6 +372,7 @@ public: virtual void keyboard_set_current_layout(int p_index) override; virtual String keyboard_get_layout_language(int p_index) const override; virtual String keyboard_get_layout_name(int p_index) const override; + virtual Key keyboard_get_keycode_from_physical(Key p_keycode) const override; virtual void process_events() override; diff --git a/platform/linuxbsd/export/export.cpp b/platform/linuxbsd/export/export.cpp index 5c6be2d7d4..965a38ef4e 100644 --- a/platform/linuxbsd/export/export.cpp +++ b/platform/linuxbsd/export/export.cpp @@ -53,7 +53,7 @@ void register_linuxbsd_exporter() { platform->set_debug_32("linux_x11_32_debug"); platform->set_release_64("linux_x11_64_release"); platform->set_debug_64("linux_x11_64_debug"); - platform->set_os_name("X11"); + platform->set_os_name("LinuxBSD"); platform->set_chmod_flags(0755); platform->set_fixup_embedded_pck_func(&fixup_embedded_pck); diff --git a/platform/linuxbsd/freedesktop_screensaver.cpp b/platform/linuxbsd/freedesktop_screensaver.cpp index a6a3b27d76..3973d43d49 100644 --- a/platform/linuxbsd/freedesktop_screensaver.cpp +++ b/platform/linuxbsd/freedesktop_screensaver.cpp @@ -50,6 +50,7 @@ void FreeDesktopScreenSaver::inhibit() { DBusConnection *bus = dbus_bus_get(DBUS_BUS_SESSION, &error); if (dbus_error_is_set(&error)) { + dbus_error_free(&error); unsupported = true; return; } @@ -72,6 +73,7 @@ void FreeDesktopScreenSaver::inhibit() { DBusMessage *reply = dbus_connection_send_with_reply_and_block(bus, message, 50, &error); dbus_message_unref(message); if (dbus_error_is_set(&error)) { + dbus_error_free(&error); dbus_connection_unref(bus); unsupported = false; return; @@ -96,6 +98,7 @@ void FreeDesktopScreenSaver::uninhibit() { DBusConnection *bus = dbus_bus_get(DBUS_BUS_SESSION, &error); if (dbus_error_is_set(&error)) { + dbus_error_free(&error); unsupported = true; return; } @@ -110,6 +113,7 @@ void FreeDesktopScreenSaver::uninhibit() { DBusMessage *reply = dbus_connection_send_with_reply_and_block(bus, message, 50, &error); if (dbus_error_is_set(&error)) { + dbus_error_free(&error); dbus_connection_unref(bus); unsupported = true; return; diff --git a/platform/linuxbsd/key_mapping_x11.cpp b/platform/linuxbsd/key_mapping_x11.cpp index a1ef28234d..829feda40a 100644 --- a/platform/linuxbsd/key_mapping_x11.cpp +++ b/platform/linuxbsd/key_mapping_x11.cpp @@ -309,6 +309,18 @@ unsigned int KeyMappingX11::get_scancode(unsigned int p_code) { return keycode; } +unsigned int KeyMappingX11::get_xlibcode(unsigned int p_keysym) { + unsigned int code = 0; + for (int i = 0; _scancode_to_keycode[i].keysym != KEY_UNKNOWN; i++) { + if (_scancode_to_keycode[i].keysym == p_keysym) { + code = _scancode_to_keycode[i].keycode; + break; + } + } + + return code; +} + Key KeyMappingX11::get_keycode(KeySym p_keysym) { // kinda bruteforce.. could optimize. diff --git a/platform/linuxbsd/key_mapping_x11.h b/platform/linuxbsd/key_mapping_x11.h index 598db1c45a..d4f1554671 100644 --- a/platform/linuxbsd/key_mapping_x11.h +++ b/platform/linuxbsd/key_mapping_x11.h @@ -45,6 +45,7 @@ class KeyMappingX11 { public: static Key get_keycode(KeySym p_keysym); + static unsigned int get_xlibcode(unsigned int p_keysym); static unsigned int get_scancode(unsigned int p_code); static KeySym get_keysym(Key p_code); static unsigned int get_unicode_from_keysym(KeySym p_keysym); diff --git a/platform/linuxbsd/os_linuxbsd.cpp b/platform/linuxbsd/os_linuxbsd.cpp index 2c9801f512..69474c6dec 100644 --- a/platform/linuxbsd/os_linuxbsd.cpp +++ b/platform/linuxbsd/os_linuxbsd.cpp @@ -463,7 +463,10 @@ Error OS_LinuxBSD::move_to_trash(const String &p_path) { // The trash can is successfully created, now we check that we don't exceed our file name length limit. // If the file name is too long trim it so we can add the identifying number and ".trashinfo". // Assumes that the file name length limit is 255 characters. - String file_name = basename(p_path.utf8().get_data()); + String file_name = p_path.get_file(); + if (file_name.length() == 0) { + file_name = p_path.get_base_dir().get_file(); + } if (file_name.length() > 240) { file_name = file_name.substr(0, file_name.length() - 15); } diff --git a/platform/osx/crash_handler_osx.mm b/platform/osx/crash_handler_osx.mm index 0f128d504f..31228b10b4 100644 --- a/platform/osx/crash_handler_osx.mm +++ b/platform/osx/crash_handler_osx.mm @@ -32,6 +32,8 @@ #include "core/config/project_settings.h" #include "core/os/os.h" +#include "core/version.h" +#include "core/version_hash.gen.h" #include "main/main.h" #include <string.h> @@ -85,11 +87,18 @@ static void handle_crash(int sig) { } // Dump the backtrace to stderr with a message to the user + fprintf(stderr, "\n================================================================\n"); fprintf(stderr, "%s: Program crashed with signal %d\n", __FUNCTION__, sig); if (OS::get_singleton()->get_main_loop()) OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); + // Print the engine version just before, so that people are reminded to include the version in backtrace reports. + if (String(VERSION_HASH).length() != 0) { + fprintf(stderr, "Engine version: " VERSION_FULL_NAME " (" VERSION_HASH ")\n"); + } else { + fprintf(stderr, "Engine version: " VERSION_FULL_NAME "\n"); + } fprintf(stderr, "Dumping the backtrace. %s\n", msg.utf8().get_data()); char **strings = backtrace_symbols(bt_buffer, size); if (strings) { @@ -148,6 +157,7 @@ static void handle_crash(int sig) { free(strings); } fprintf(stderr, "-- END OF BACKTRACE --\n"); + fprintf(stderr, "================================================================\n"); // Abort to pass the error to the OS abort(); diff --git a/platform/osx/detect.py b/platform/osx/detect.py index 6b25daf05d..36fff4e12a 100644 --- a/platform/osx/detect.py +++ b/platform/osx/detect.py @@ -57,13 +57,11 @@ def configure(env): env.Prepend(CCFLAGS=["-O2"]) elif env["optimize"] == "size": # optimize for size env.Prepend(CCFLAGS=["-Os"]) - env.Prepend(CPPDEFINES=["DEBUG_ENABLED"]) if env["debug_symbols"]: env.Prepend(CCFLAGS=["-g2"]) elif env["target"] == "debug": env.Prepend(CCFLAGS=["-g3"]) - env.Prepend(CPPDEFINES=["DEBUG_ENABLED"]) env.Prepend(LINKFLAGS=["-Xlinker", "-no_deduplicate"]) ## Architecture @@ -96,7 +94,6 @@ def configure(env): env["AR"] = mpprefix + "/libexec/llvm-" + mpclangver + "/bin/llvm-ar" env["RANLIB"] = mpprefix + "/libexec/llvm-" + mpclangver + "/bin/llvm-ranlib" env["AS"] = mpprefix + "/libexec/llvm-" + mpclangver + "/bin/llvm-as" - env.Append(CPPDEFINES=["__MACPORTS__"]) # hack to fix libvpx MM256_BROADCASTSI128_SI256 define else: env["CC"] = "clang" env["CXX"] = "clang++" @@ -124,7 +121,6 @@ def configure(env): env["AR"] = basecmd + "ar" env["RANLIB"] = basecmd + "ranlib" env["AS"] = basecmd + "as" - env.Append(CPPDEFINES=["__MACPORTS__"]) # hack to fix libvpx MM256_BROADCASTSI128_SI256 define if env["use_ubsan"] or env["use_asan"] or env["use_tsan"]: env.extra_suffix += "s" diff --git a/platform/osx/display_server_osx.h b/platform/osx/display_server_osx.h index 06b14f1c14..e8f2858489 100644 --- a/platform/osx/display_server_osx.h +++ b/platform/osx/display_server_osx.h @@ -305,6 +305,7 @@ public: virtual void keyboard_set_current_layout(int p_index) override; virtual String keyboard_get_layout_language(int p_index) const override; virtual String keyboard_get_layout_name(int p_index) const override; + virtual Key keyboard_get_keycode_from_physical(Key p_keycode) const override; virtual void process_events() override; virtual void force_process_and_drop_events() override; diff --git a/platform/osx/display_server_osx.mm b/platform/osx/display_server_osx.mm index 43b7d7c1e0..3fe055a511 100644 --- a/platform/osx/display_server_osx.mm +++ b/platform/osx/display_server_osx.mm @@ -190,6 +190,8 @@ static NSCursor *_cursorFromSelector(SEL selector, SEL fallback = nil) { [wd.window_object setContentMinSize:NSMakeSize(0, 0)]; [wd.window_object setContentMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)]; + // Force window resize event. + [self windowDidResize:notification]; } - (void)windowDidExitFullScreen:(NSNotification *)notification { @@ -217,6 +219,8 @@ static NSCursor *_cursorFromSelector(SEL selector, SEL fallback = nil) { if (wd.on_top) { [wd.window_object setLevel:NSFloatingWindowLevel]; } + // Force window resize event. + [self windowDidResize:notification]; } - (void)windowDidChangeBackingProperties:(NSNotification *)notification { @@ -926,146 +930,155 @@ static bool isNumpadKey(unsigned int key) { return false; } +// Keyboard symbol translation table +static const Key _osx_to_godot_table[128] = { + /* 00 */ KEY_A, + /* 01 */ KEY_S, + /* 02 */ KEY_D, + /* 03 */ KEY_F, + /* 04 */ KEY_H, + /* 05 */ KEY_G, + /* 06 */ KEY_Z, + /* 07 */ KEY_X, + /* 08 */ KEY_C, + /* 09 */ KEY_V, + /* 0a */ KEY_SECTION, /* ISO Section */ + /* 0b */ KEY_B, + /* 0c */ KEY_Q, + /* 0d */ KEY_W, + /* 0e */ KEY_E, + /* 0f */ KEY_R, + /* 10 */ KEY_Y, + /* 11 */ KEY_T, + /* 12 */ KEY_1, + /* 13 */ KEY_2, + /* 14 */ KEY_3, + /* 15 */ KEY_4, + /* 16 */ KEY_6, + /* 17 */ KEY_5, + /* 18 */ KEY_EQUAL, + /* 19 */ KEY_9, + /* 1a */ KEY_7, + /* 1b */ KEY_MINUS, + /* 1c */ KEY_8, + /* 1d */ KEY_0, + /* 1e */ KEY_BRACERIGHT, + /* 1f */ KEY_O, + /* 20 */ KEY_U, + /* 21 */ KEY_BRACELEFT, + /* 22 */ KEY_I, + /* 23 */ KEY_P, + /* 24 */ KEY_ENTER, + /* 25 */ KEY_L, + /* 26 */ KEY_J, + /* 27 */ KEY_APOSTROPHE, + /* 28 */ KEY_K, + /* 29 */ KEY_SEMICOLON, + /* 2a */ KEY_BACKSLASH, + /* 2b */ KEY_COMMA, + /* 2c */ KEY_SLASH, + /* 2d */ KEY_N, + /* 2e */ KEY_M, + /* 2f */ KEY_PERIOD, + /* 30 */ KEY_TAB, + /* 31 */ KEY_SPACE, + /* 32 */ KEY_QUOTELEFT, + /* 33 */ KEY_BACKSPACE, + /* 34 */ KEY_UNKNOWN, + /* 35 */ KEY_ESCAPE, + /* 36 */ KEY_META, + /* 37 */ KEY_META, + /* 38 */ KEY_SHIFT, + /* 39 */ KEY_CAPSLOCK, + /* 3a */ KEY_ALT, + /* 3b */ KEY_CTRL, + /* 3c */ KEY_SHIFT, + /* 3d */ KEY_ALT, + /* 3e */ KEY_CTRL, + /* 3f */ KEY_UNKNOWN, /* Function */ + /* 40 */ KEY_UNKNOWN, /* F17 */ + /* 41 */ KEY_KP_PERIOD, + /* 42 */ KEY_UNKNOWN, + /* 43 */ KEY_KP_MULTIPLY, + /* 44 */ KEY_UNKNOWN, + /* 45 */ KEY_KP_ADD, + /* 46 */ KEY_UNKNOWN, + /* 47 */ KEY_NUMLOCK, /* Really KeypadClear... */ + /* 48 */ KEY_VOLUMEUP, /* VolumeUp */ + /* 49 */ KEY_VOLUMEDOWN, /* VolumeDown */ + /* 4a */ KEY_VOLUMEMUTE, /* Mute */ + /* 4b */ KEY_KP_DIVIDE, + /* 4c */ KEY_KP_ENTER, + /* 4d */ KEY_UNKNOWN, + /* 4e */ KEY_KP_SUBTRACT, + /* 4f */ KEY_UNKNOWN, /* F18 */ + /* 50 */ KEY_UNKNOWN, /* F19 */ + /* 51 */ KEY_EQUAL, /* KeypadEqual */ + /* 52 */ KEY_KP_0, + /* 53 */ KEY_KP_1, + /* 54 */ KEY_KP_2, + /* 55 */ KEY_KP_3, + /* 56 */ KEY_KP_4, + /* 57 */ KEY_KP_5, + /* 58 */ KEY_KP_6, + /* 59 */ KEY_KP_7, + /* 5a */ KEY_UNKNOWN, /* F20 */ + /* 5b */ KEY_KP_8, + /* 5c */ KEY_KP_9, + /* 5d */ KEY_YEN, /* JIS Yen */ + /* 5e */ KEY_UNDERSCORE, /* JIS Underscore */ + /* 5f */ KEY_COMMA, /* JIS KeypadComma */ + /* 60 */ KEY_F5, + /* 61 */ KEY_F6, + /* 62 */ KEY_F7, + /* 63 */ KEY_F3, + /* 64 */ KEY_F8, + /* 65 */ KEY_F9, + /* 66 */ KEY_UNKNOWN, /* JIS Eisu */ + /* 67 */ KEY_F11, + /* 68 */ KEY_UNKNOWN, /* JIS Kana */ + /* 69 */ KEY_F13, + /* 6a */ KEY_F16, + /* 6b */ KEY_F14, + /* 6c */ KEY_UNKNOWN, + /* 6d */ KEY_F10, + /* 6e */ KEY_MENU, + /* 6f */ KEY_F12, + /* 70 */ KEY_UNKNOWN, + /* 71 */ KEY_F15, + /* 72 */ KEY_INSERT, /* Really Help... */ + /* 73 */ KEY_HOME, + /* 74 */ KEY_PAGEUP, + /* 75 */ KEY_DELETE, + /* 76 */ KEY_F4, + /* 77 */ KEY_END, + /* 78 */ KEY_F2, + /* 79 */ KEY_PAGEDOWN, + /* 7a */ KEY_F1, + /* 7b */ KEY_LEFT, + /* 7c */ KEY_RIGHT, + /* 7d */ KEY_DOWN, + /* 7e */ KEY_UP, + /* 7f */ KEY_UNKNOWN, +}; + // Translates a OS X keycode to a Godot keycode -// static Key translateKey(unsigned int key) { - // Keyboard symbol translation table - static const Key table[128] = { - /* 00 */ KEY_A, - /* 01 */ KEY_S, - /* 02 */ KEY_D, - /* 03 */ KEY_F, - /* 04 */ KEY_H, - /* 05 */ KEY_G, - /* 06 */ KEY_Z, - /* 07 */ KEY_X, - /* 08 */ KEY_C, - /* 09 */ KEY_V, - /* 0a */ KEY_SECTION, /* ISO Section */ - /* 0b */ KEY_B, - /* 0c */ KEY_Q, - /* 0d */ KEY_W, - /* 0e */ KEY_E, - /* 0f */ KEY_R, - /* 10 */ KEY_Y, - /* 11 */ KEY_T, - /* 12 */ KEY_1, - /* 13 */ KEY_2, - /* 14 */ KEY_3, - /* 15 */ KEY_4, - /* 16 */ KEY_6, - /* 17 */ KEY_5, - /* 18 */ KEY_EQUAL, - /* 19 */ KEY_9, - /* 1a */ KEY_7, - /* 1b */ KEY_MINUS, - /* 1c */ KEY_8, - /* 1d */ KEY_0, - /* 1e */ KEY_BRACERIGHT, - /* 1f */ KEY_O, - /* 20 */ KEY_U, - /* 21 */ KEY_BRACELEFT, - /* 22 */ KEY_I, - /* 23 */ KEY_P, - /* 24 */ KEY_ENTER, - /* 25 */ KEY_L, - /* 26 */ KEY_J, - /* 27 */ KEY_APOSTROPHE, - /* 28 */ KEY_K, - /* 29 */ KEY_SEMICOLON, - /* 2a */ KEY_BACKSLASH, - /* 2b */ KEY_COMMA, - /* 2c */ KEY_SLASH, - /* 2d */ KEY_N, - /* 2e */ KEY_M, - /* 2f */ KEY_PERIOD, - /* 30 */ KEY_TAB, - /* 31 */ KEY_SPACE, - /* 32 */ KEY_QUOTELEFT, - /* 33 */ KEY_BACKSPACE, - /* 34 */ KEY_UNKNOWN, - /* 35 */ KEY_ESCAPE, - /* 36 */ KEY_META, - /* 37 */ KEY_META, - /* 38 */ KEY_SHIFT, - /* 39 */ KEY_CAPSLOCK, - /* 3a */ KEY_ALT, - /* 3b */ KEY_CTRL, - /* 3c */ KEY_SHIFT, - /* 3d */ KEY_ALT, - /* 3e */ KEY_CTRL, - /* 3f */ KEY_UNKNOWN, /* Function */ - /* 40 */ KEY_UNKNOWN, /* F17 */ - /* 41 */ KEY_KP_PERIOD, - /* 42 */ KEY_UNKNOWN, - /* 43 */ KEY_KP_MULTIPLY, - /* 44 */ KEY_UNKNOWN, - /* 45 */ KEY_KP_ADD, - /* 46 */ KEY_UNKNOWN, - /* 47 */ KEY_NUMLOCK, /* Really KeypadClear... */ - /* 48 */ KEY_VOLUMEUP, /* VolumeUp */ - /* 49 */ KEY_VOLUMEDOWN, /* VolumeDown */ - /* 4a */ KEY_VOLUMEMUTE, /* Mute */ - /* 4b */ KEY_KP_DIVIDE, - /* 4c */ KEY_KP_ENTER, - /* 4d */ KEY_UNKNOWN, - /* 4e */ KEY_KP_SUBTRACT, - /* 4f */ KEY_UNKNOWN, /* F18 */ - /* 50 */ KEY_UNKNOWN, /* F19 */ - /* 51 */ KEY_EQUAL, /* KeypadEqual */ - /* 52 */ KEY_KP_0, - /* 53 */ KEY_KP_1, - /* 54 */ KEY_KP_2, - /* 55 */ KEY_KP_3, - /* 56 */ KEY_KP_4, - /* 57 */ KEY_KP_5, - /* 58 */ KEY_KP_6, - /* 59 */ KEY_KP_7, - /* 5a */ KEY_UNKNOWN, /* F20 */ - /* 5b */ KEY_KP_8, - /* 5c */ KEY_KP_9, - /* 5d */ KEY_YEN, /* JIS Yen */ - /* 5e */ KEY_UNDERSCORE, /* JIS Underscore */ - /* 5f */ KEY_COMMA, /* JIS KeypadComma */ - /* 60 */ KEY_F5, - /* 61 */ KEY_F6, - /* 62 */ KEY_F7, - /* 63 */ KEY_F3, - /* 64 */ KEY_F8, - /* 65 */ KEY_F9, - /* 66 */ KEY_UNKNOWN, /* JIS Eisu */ - /* 67 */ KEY_F11, - /* 68 */ KEY_UNKNOWN, /* JIS Kana */ - /* 69 */ KEY_F13, - /* 6a */ KEY_F16, - /* 6b */ KEY_F14, - /* 6c */ KEY_UNKNOWN, - /* 6d */ KEY_F10, - /* 6e */ KEY_MENU, - /* 6f */ KEY_F12, - /* 70 */ KEY_UNKNOWN, - /* 71 */ KEY_F15, - /* 72 */ KEY_INSERT, /* Really Help... */ - /* 73 */ KEY_HOME, - /* 74 */ KEY_PAGEUP, - /* 75 */ KEY_DELETE, - /* 76 */ KEY_F4, - /* 77 */ KEY_END, - /* 78 */ KEY_F2, - /* 79 */ KEY_PAGEDOWN, - /* 7a */ KEY_F1, - /* 7b */ KEY_LEFT, - /* 7c */ KEY_RIGHT, - /* 7d */ KEY_DOWN, - /* 7e */ KEY_UP, - /* 7f */ KEY_UNKNOWN, - }; - if (key >= 128) { return KEY_UNKNOWN; } - return table[key]; + return _osx_to_godot_table[key]; +} + +// Translates a Godot keycode back to a OSX keycode +static unsigned int unmapKey(Key key) { + for (int i = 0; i <= 126; i++) { + if (_osx_to_godot_table[i] == key) { + return i; + } + } + return 127; } struct _KeyCodeMap { @@ -3001,7 +3014,7 @@ void DisplayServerOSX::cursor_set_custom_image(const RES &p_cursor, CursorShape ERR_FAIL_COND(!image.is_valid()); NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] - initWithBitmapDataPlanes:NULL + initWithBitmapDataPlanes:nullptr pixelsWide:int(texture_size.width) pixelsHigh:int(texture_size.height) bitsPerSample:8 @@ -3196,6 +3209,17 @@ String DisplayServerOSX::keyboard_get_layout_name(int p_index) const { return kbd_layouts[p_index].name; } +Key DisplayServerOSX::keyboard_get_keycode_from_physical(Key p_keycode) const { + if (p_keycode == KEY_PAUSE) { + return p_keycode; + } + + unsigned int modifiers = p_keycode & KEY_MODIFIER_MASK; + unsigned int keycode_no_mod = p_keycode & KEY_CODE_MASK; + unsigned int osx_keycode = unmapKey((Key)keycode_no_mod); + return (Key)(remapKey(osx_keycode, 0) | modifiers); +} + void DisplayServerOSX::_push_input(const Ref<InputEvent> &p_event) { Ref<InputEvent> ev = p_event; Input::get_singleton()->parse_input_event(ev); @@ -3394,7 +3418,7 @@ void DisplayServerOSX::set_icon(const Ref<Image> &p_icon) { img = img->duplicate(); img->convert(Image::FORMAT_RGBA8); NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] - initWithBitmapDataPlanes:NULL + initWithBitmapDataPlanes:nullptr pixelsWide:img->get_width() pixelsHigh:img->get_height() bitsPerSample:8 diff --git a/platform/osx/export/export_plugin.cpp b/platform/osx/export/export_plugin.cpp index 5b959d6da4..60a878d644 100644 --- a/platform/osx/export/export_plugin.cpp +++ b/platform/osx/export/export_plugin.cpp @@ -95,6 +95,7 @@ void EditorExportPlatformOSX::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_pictures", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_music", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_movies", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0)); + r_options->push_back(ExportOption(PropertyInfo(Variant::ARRAY, "codesign/entitlements/app_sandbox/helper_executables", PROPERTY_HINT_ARRAY_TYPE, itos(Variant::STRING) + "/" + itos(PROPERTY_HINT_GLOBAL_FILE) + ":"), Array())); r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray())); @@ -535,6 +536,8 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p err = ERR_CANT_CREATE; } + Array helpers = p_preset->get("codesign/entitlements/app_sandbox/helper_executables"); + // Create our folder structure. if (err == OK) { print_line("Creating " + tmp_app_path_name + "/Contents/MacOS"); @@ -546,6 +549,11 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Frameworks"); } + if ((err == OK) && helpers.size() > 0) { + print_line("Creating " + tmp_app_path_name + "/Contents/Helpers"); + err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Helpers"); + } + if (err == OK) { print_line("Creating " + tmp_app_path_name + "/Contents/Resources"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Resources"); @@ -553,7 +561,6 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p // Now process our template. bool found_binary = false; - int total_size = 0; Vector<String> dylibs_found; while (ret == UNZ_OK && err == OK) { @@ -641,7 +648,6 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p } print_line("ADDING: " + file + " size: " + itos(data.size())); - total_size += data.size(); // Write it into our application bundle. file = tmp_app_path_name.plus_file(file); @@ -688,6 +694,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p bool sign_enabled = p_preset->get("codesign/enable"); String ent_path = p_preset->get("codesign/entitlements/custom_file"); + String hlp_ent_path = EditorPaths::get_singleton()->get_cache_dir().plus_file(pkg_name + "_helper.entitlements"); if (sign_enabled && (ent_path == "")) { ent_path = EditorPaths::get_singleton()->get_cache_dir().plus_file(pkg_name + ".entitlements"); @@ -819,10 +826,43 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p } else { err = ERR_CANT_CREATE; } + + if ((err == OK) && helpers.size() > 0) { + ent_f = FileAccess::open(hlp_ent_path, FileAccess::WRITE); + if (ent_f) { + ent_f->store_line("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); + ent_f->store_line("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"); + ent_f->store_line("<plist version=\"1.0\">"); + ent_f->store_line("<dict>"); + ent_f->store_line("<key>com.apple.security.app-sandbox</key>"); + ent_f->store_line("<true/>"); + ent_f->store_line("<key>com.apple.security.inherit</key>"); + ent_f->store_line("<true/>"); + ent_f->store_line("</dict>"); + ent_f->store_line("</plist>"); + + ent_f->close(); + memdelete(ent_f); + } else { + err = ERR_CANT_CREATE; + } + } + } + + if ((err == OK) && helpers.size() > 0) { + DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + for (int i = 0; i < helpers.size(); i++) { + String hlp_path = helpers[i]; + err = da->copy(hlp_path, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file()); + if (err == OK && sign_enabled) { + err = _code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path); + } + FileAccess::set_unix_permissions(tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), 0755); + } } if (err == OK) { - DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); for (int i = 0; i < shared_objects.size(); i++) { String src_path = ProjectSettings::get_singleton()->globalize_path(shared_objects[i].path); if (da->dir_exists(src_path)) { @@ -842,7 +882,6 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p err = _code_sign(p_preset, tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file(), ent_path); } } - memdelete(da); } if (sign_enabled) { @@ -903,6 +942,9 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p err = _notarize(p_preset, p_path); } + // Clean up temporary entitlements files. + DirAccess::remove_file_or_error(hlp_ent_path); + // Clean up temporary .app dir. tmp_app_dir->change_dir(tmp_app_path_name); tmp_app_dir->erase_contents_recursive(); @@ -916,7 +958,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p void EditorExportPlatformOSX::_zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name) { String dir = p_root_path.plus_file(p_folder); - DirAccess *da = DirAccess::open(dir); + DirAccessRef da = DirAccess::open(dir); da->list_dir_begin(); String f; while ((f = da->get_next()) != "") { @@ -931,7 +973,7 @@ void EditorExportPlatformOSX::_zip_folder_recursive(zipFile &p_zip, const String zipfi.tmz_date.tm_hour = time.hour; zipfi.tmz_date.tm_mday = date.day; zipfi.tmz_date.tm_min = time.minute; - zipfi.tmz_date.tm_mon = date.month - 1; // Note: "tm" month range - 0..11, Godot month range - 1..12, http://www.cplusplus.com/reference/ctime/tm/ + zipfi.tmz_date.tm_mon = date.month - 1; // Note: "tm" month range - 0..11, Godot month range - 1..12, https://www.cplusplus.com/reference/ctime/tm/ zipfi.tmz_date.tm_sec = time.second; zipfi.tmz_date.tm_year = date.year; zipfi.dosDate = 0; @@ -967,7 +1009,7 @@ void EditorExportPlatformOSX::_zip_folder_recursive(zipFile &p_zip, const String } else if (da->current_is_dir()) { _zip_folder_recursive(p_zip, p_root_path, p_folder.plus_file(f), p_pkg_name); } else { - bool is_executable = (p_folder.ends_with("MacOS") && (f == p_pkg_name)); + bool is_executable = (p_folder.ends_with("MacOS") && (f == p_pkg_name)) || p_folder.ends_with("Helpers"); OS::Time time = OS::get_singleton()->get_time(); OS::Date date = OS::get_singleton()->get_date(); @@ -976,7 +1018,7 @@ void EditorExportPlatformOSX::_zip_folder_recursive(zipFile &p_zip, const String zipfi.tmz_date.tm_hour = time.hour; zipfi.tmz_date.tm_mday = date.day; zipfi.tmz_date.tm_min = time.minute; - zipfi.tmz_date.tm_mon = date.month - 1; // Note: "tm" month range - 0..11, Godot month range - 1..12, http://www.cplusplus.com/reference/ctime/tm/ + zipfi.tmz_date.tm_mon = date.month - 1; // Note: "tm" month range - 0..11, Godot month range - 1..12, https://www.cplusplus.com/reference/ctime/tm/ zipfi.tmz_date.tm_sec = time.second; zipfi.tmz_date.tm_year = date.year; zipfi.dosDate = 0; @@ -1012,7 +1054,6 @@ void EditorExportPlatformOSX::_zip_folder_recursive(zipFile &p_zip, const String } } da->list_dir_end(); - memdelete(da); } bool EditorExportPlatformOSX::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { diff --git a/platform/osx/export/export_plugin.h b/platform/osx/export/export_plugin.h index cd85ce2aad..ca5086622e 100644 --- a/platform/osx/export/export_plugin.h +++ b/platform/osx/export/export_plugin.h @@ -115,7 +115,7 @@ public: virtual void get_platform_features(List<String> *r_features) override { r_features->push_back("pc"); r_features->push_back("s3tc"); - r_features->push_back("macOS"); + r_features->push_back("macos"); } virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) override { diff --git a/platform/osx/os_osx.h b/platform/osx/os_osx.h index df41ccd892..a52436a70a 100644 --- a/platform/osx/os_osx.h +++ b/platform/osx/os_osx.h @@ -82,6 +82,7 @@ public: virtual String get_data_path() const override; virtual String get_cache_path() const override; virtual String get_bundle_resource_dir() const override; + virtual String get_bundle_icon_path() const override; virtual String get_godot_dir_name() const override; virtual String get_system_dir(SystemDir p_dir, bool p_shared_storage = true) const override; diff --git a/platform/osx/os_osx.mm b/platform/osx/os_osx.mm index c6e35fee83..e9cb46ed21 100644 --- a/platform/osx/os_osx.mm +++ b/platform/osx/os_osx.mm @@ -314,17 +314,27 @@ String OS_OSX::get_name() const { return "macOS"; } +_FORCE_INLINE_ String _get_framework_executable(const String p_path) { + // Append framework executable name, or return as is if p_path is not a framework. + DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (da->dir_exists(p_path) && da->file_exists(p_path.plus_file(p_path.get_file().get_basename()))) { + return p_path.plus_file(p_path.get_file().get_basename()); + } else { + return p_path; + } +} + Error OS_OSX::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { - String path = p_path; + String path = _get_framework_executable(p_path); if (!FileAccess::exists(path)) { - //this code exists so gdnative can load .dylib files from within the executable path - path = get_executable_path().get_base_dir().plus_file(p_path.get_file()); + // This code exists so gdnative can load .dylib files from within the executable path. + path = _get_framework_executable(get_executable_path().get_base_dir().plus_file(p_path.get_file())); } if (!FileAccess::exists(path)) { - //this code exists so gdnative can load .dylib files from a standard macOS location - path = get_executable_path().get_base_dir().plus_file("../Frameworks").plus_file(p_path.get_file()); + // This code exists so gdnative can load .dylib files from a standard macOS location. + path = _get_framework_executable(get_executable_path().get_base_dir().plus_file("../Frameworks").plus_file(p_path.get_file())); } p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW); @@ -379,14 +389,26 @@ String OS_OSX::get_cache_path() const { } String OS_OSX::get_bundle_resource_dir() const { + String ret; + NSBundle *main = [NSBundle mainBundle]; - NSString *resourcePath = [main resourcePath]; + if (main) { + NSString *resourcePath = [main resourcePath]; + ret.parse_utf8([resourcePath UTF8String]); + } + return ret; +} - char *utfs = strdup([resourcePath UTF8String]); +String OS_OSX::get_bundle_icon_path() const { String ret; - ret.parse_utf8(utfs); - free(utfs); + NSBundle *main = [NSBundle mainBundle]; + if (main) { + NSString *iconPath = [[main infoDictionary] objectForKey:@"CFBundleIconFile"]; + if (iconPath) { + ret.parse_utf8([iconPath UTF8String]); + } + } return ret; } diff --git a/platform/uwp/SCsub b/platform/uwp/SCsub index 71c402358f..8726d32d61 100644 --- a/platform/uwp/SCsub +++ b/platform/uwp/SCsub @@ -7,7 +7,7 @@ files = [ "#platform/windows/windows_terminal_logger.cpp", "joypad_uwp.cpp", "context_egl_uwp.cpp", - "app.cpp", + "app_uwp.cpp", "os_uwp.cpp", ] diff --git a/platform/uwp/app.cpp b/platform/uwp/app_uwp.cpp index 1da17ffc5d..50e33e6c49 100644 --- a/platform/uwp/app.cpp +++ b/platform/uwp/app_uwp.cpp @@ -1,5 +1,5 @@ /*************************************************************************/ -/* app.cpp */ +/* app_uwp.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -32,7 +32,7 @@ // This file demonstrates how to initialize EGL in a Windows Store app, using ICoreWindow. // -#include "app.h" +#include "app_uwp.h" #include "core/io/dir_access.h" #include "core/io/file_access.h" diff --git a/platform/uwp/app.h b/platform/uwp/app_uwp.h index 0b02527dae..8d4a0b90c3 100644 --- a/platform/uwp/app.h +++ b/platform/uwp/app_uwp.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* app.h */ +/* app_uwp.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,7 +28,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#pragma once +#ifndef APP_UWP_H +#define APP_UWP_H #include <string> @@ -111,3 +112,4 @@ namespace GodotUWP } /* clang-format on */ +#endif // APP_UWP_H diff --git a/platform/uwp/detect.py b/platform/uwp/detect.py index 28922a4f59..9c91378b22 100644 --- a/platform/uwp/detect.py +++ b/platform/uwp/detect.py @@ -64,14 +64,12 @@ def configure(env): env.Append(CCFLAGS=["/MD"]) env.Append(LINKFLAGS=["/SUBSYSTEM:CONSOLE"]) env.AppendUnique(CPPDEFINES=["WINDOWS_SUBSYSTEM_CONSOLE"]) - env.Append(CPPDEFINES=["DEBUG_ENABLED"]) if env["optimize"] != "none": env.Append(CCFLAGS=["/O2", "/Zi"]) elif env["target"] == "debug": env.Append(CCFLAGS=["/Zi"]) env.Append(CCFLAGS=["/MDd"]) - env.Append(CPPDEFINES=["DEBUG_ENABLED"]) env.Append(LINKFLAGS=["/SUBSYSTEM:CONSOLE"]) env.AppendUnique(CPPDEFINES=["WINDOWS_SUBSYSTEM_CONSOLE"]) env.Append(LINKFLAGS=["/DEBUG"]) diff --git a/platform/uwp/export/export_plugin.cpp b/platform/uwp/export/export_plugin.cpp index 5bd00b1549..a54b85a803 100644 --- a/platform/uwp/export/export_plugin.cpp +++ b/platform/uwp/export/export_plugin.cpp @@ -485,7 +485,7 @@ Error EditorExportPlatformUWP::export_project(const Ref<EditorExportPreset> &p_p void EditorExportPlatformUWP::get_platform_features(List<String> *r_features) { r_features->push_back("pc"); - r_features->push_back("UWP"); + r_features->push_back("uwp"); } void EditorExportPlatformUWP::resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) { diff --git a/platform/uwp/os_uwp.cpp b/platform/uwp/os_uwp.cpp index 6ac5b55156..daf3c14f3b 100644 --- a/platform/uwp/os_uwp.cpp +++ b/platform/uwp/os_uwp.cpp @@ -28,14 +28,12 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -// Must include Winsock before windows.h (included by os_uwp.h) -#include "drivers/unix/net_socket_posix.h" - #include "os_uwp.h" #include "core/config/project_settings.h" #include "core/io/marshalls.h" #include "drivers/unix/ip_unix.h" +#include "drivers/unix/net_socket_posix.h" #include "drivers/windows/dir_access_windows.h" #include "drivers/windows/file_access_windows.h" #include "drivers/windows/mutex_windows.h" diff --git a/platform/uwp/os_uwp.h b/platform/uwp/os_uwp.h index c9b2600c8e..7945f409a1 100644 --- a/platform/uwp/os_uwp.h +++ b/platform/uwp/os_uwp.h @@ -45,6 +45,7 @@ #include <fcntl.h> #include <io.h> #include <stdio.h> +#define WIN32_LEAN_AND_MEAN #include <windows.h> class OS_UWP : public OS { diff --git a/platform/windows/context_gl_windows.h b/platform/windows/context_gl_windows.h index c8e8a0891d..feff1d825b 100644 --- a/platform/windows/context_gl_windows.h +++ b/platform/windows/context_gl_windows.h @@ -38,6 +38,7 @@ #include "core/error/error_list.h" #include "core/os/os.h" +#define WIN32_LEAN_AND_MEAN #include <windows.h> typedef bool(APIENTRY *PFNWGLSWAPINTERVALEXTPROC)(int interval); diff --git a/platform/windows/crash_handler_windows.cpp b/platform/windows/crash_handler_windows.cpp index e2d507eddd..1b4dae207f 100644 --- a/platform/windows/crash_handler_windows.cpp +++ b/platform/windows/crash_handler_windows.cpp @@ -32,6 +32,8 @@ #include "core/config/project_settings.h" #include "core/os/os.h" +#include "core/version.h" +#include "core/version_hash.gen.h" #include "main/main.h" #ifdef CRASH_HANDLER_EXCEPTION @@ -127,6 +129,7 @@ DWORD CrashHandlerException(EXCEPTION_POINTERS *ep) { return EXCEPTION_CONTINUE_SEARCH; } + fprintf(stderr, "\n================================================================\n"); fprintf(stderr, "%s: Program crashed\n", __FUNCTION__); if (OS::get_singleton()->get_main_loop()) @@ -175,6 +178,12 @@ DWORD CrashHandlerException(EXCEPTION_POINTERS *ep) { msg = proj_settings->get("debug/settings/crash_handler/message"); } + // Print the engine version just before, so that people are reminded to include the version in backtrace reports. + if (String(VERSION_HASH).length() != 0) { + fprintf(stderr, "Engine version: " VERSION_FULL_NAME " (" VERSION_HASH ")\n"); + } else { + fprintf(stderr, "Engine version: " VERSION_FULL_NAME "\n"); + } fprintf(stderr, "Dumping the backtrace. %s\n", msg.utf8().get_data()); int n = 0; @@ -200,6 +209,7 @@ DWORD CrashHandlerException(EXCEPTION_POINTERS *ep) { } while (frame.AddrReturn.Offset != 0 && n < 256); fprintf(stderr, "-- END OF BACKTRACE --\n"); + fprintf(stderr, "================================================================\n"); SymCleanup(process); diff --git a/platform/windows/crash_handler_windows.h b/platform/windows/crash_handler_windows.h index e1ec8e6787..5cdc6d3e05 100644 --- a/platform/windows/crash_handler_windows.h +++ b/platform/windows/crash_handler_windows.h @@ -31,6 +31,7 @@ #ifndef CRASH_HANDLER_WINDOWS_H #define CRASH_HANDLER_WINDOWS_H +#define WIN32_LEAN_AND_MEAN #include <windows.h> // Crash handler exception only enabled with MSVC diff --git a/platform/windows/detect.py b/platform/windows/detect.py index 9154f7749e..6752885f37 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -203,11 +203,9 @@ def configure_msvc(env, manual_msvc_config): elif env["optimize"] == "size": # optimize for size env.Append(CCFLAGS=["/O1"]) env.Append(LINKFLAGS=["/OPT:REF"]) - env.AppendUnique(CPPDEFINES=["DEBUG_ENABLED"]) elif env["target"] == "debug": env.AppendUnique(CCFLAGS=["/Zi", "/FS", "/Od", "/EHsc"]) - env.AppendUnique(CPPDEFINES=["DEBUG_ENABLED"]) env.Append(LINKFLAGS=["/DEBUG"]) if env["debug_symbols"]: @@ -321,7 +319,7 @@ def configure_msvc(env, manual_msvc_config): def configure_mingw(env): # Workaround for MinGW. See: - # http://www.scons.org/wiki/LongCmdLinesOnWin32 + # https://www.scons.org/wiki/LongCmdLinesOnWin32 env.use_windows_spawn_fix() ## Build type @@ -351,7 +349,6 @@ def configure_mingw(env): elif env["target"] == "release_debug": env.Append(CCFLAGS=["-O2"]) - env.Append(CPPDEFINES=["DEBUG_ENABLED"]) if env["debug_symbols"]: env.Prepend(CCFLAGS=["-g2"]) if env["optimize"] == "speed": # optimize for speed (default) @@ -361,7 +358,6 @@ def configure_mingw(env): elif env["target"] == "debug": env.Append(CCFLAGS=["-g3"]) - env.Append(CPPDEFINES=["DEBUG_ENABLED"]) if env["windows_subsystem"] == "gui": env.Append(LINKFLAGS=["-Wl,--subsystem,windows"]) diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index b6489e7a95..905c4142a8 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -38,7 +38,6 @@ #include <avrt.h> -#ifdef DEBUG_ENABLED static String format_error_message(DWORD id) { LPWSTR messageBuffer = nullptr; size_t size = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, @@ -50,7 +49,6 @@ static String format_error_message(DWORD id) { return msg; } -#endif // DEBUG_ENABLED bool DisplayServerWindows::has_feature(Feature p_feature) const { switch (p_feature) { @@ -80,7 +78,7 @@ String DisplayServerWindows::get_name() const { } void DisplayServerWindows::_set_mouse_mode_impl(MouseMode p_mode) { - if (p_mode == MOUSE_MODE_CAPTURED || p_mode == MOUSE_MODE_CONFINED || p_mode == MOUSE_MODE_CONFINED_HIDDEN) { + if (windows.has(MAIN_WINDOW_ID) && (p_mode == MOUSE_MODE_CAPTURED || p_mode == MOUSE_MODE_CONFINED || p_mode == MOUSE_MODE_CONFINED_HIDDEN)) { // Mouse is grabbed (captured or confined). WindowData &wd = windows[MAIN_WINDOW_ID]; @@ -118,8 +116,10 @@ void DisplayServerWindows::_set_mouse_mode_impl(MouseMode p_mode) { void DisplayServerWindows::mouse_set_mode(MouseMode p_mode) { _THREAD_SAFE_METHOD_ - if (mouse_mode == p_mode) + if (mouse_mode == p_mode) { + // Already in the same mode; do nothing. return; + } mouse_mode = p_mode; @@ -134,7 +134,7 @@ void DisplayServerWindows::mouse_warp_to_position(const Point2i &p_to) { _THREAD_SAFE_METHOD_ if (!windows.has(last_focused_window)) { - return; //no window focused? + return; // No focused window? } if (mouse_mode == MOUSE_MODE_CAPTURED) { @@ -154,7 +154,6 @@ Point2i DisplayServerWindows::mouse_get_position() const { POINT p; GetCursorPos(&p); return Point2i(p.x, p.y); - //return Point2(old_x, old_y); } MouseButton DisplayServerWindows::mouse_get_button_state() const { @@ -165,12 +164,12 @@ void DisplayServerWindows::clipboard_set(const String &p_text) { _THREAD_SAFE_METHOD_ if (!windows.has(last_focused_window)) { - return; //no window focused? + return; // No focused window? } - // Convert LF line endings to CRLF in clipboard content - // Otherwise, line endings won't be visible when pasted in other software - String text = p_text.replace("\r\n", "\n").replace("\n", "\r\n"); // avoid \r\r\n + // Convert LF line endings to CRLF in clipboard content. + // Otherwise, line endings won't be visible when pasted in other software. + String text = p_text.replace("\r\n", "\n").replace("\n", "\r\n"); // Avoid \r\r\n. if (!OpenClipboard(windows[last_focused_window].hWnd)) { ERR_FAIL_MSG("Unable to open clipboard."); @@ -187,7 +186,7 @@ void DisplayServerWindows::clipboard_set(const String &p_text) { SetClipboardData(CF_UNICODETEXT, mem); - // set the CF_TEXT version (not needed?) + // Set the CF_TEXT version (not needed?). CharString utf8 = text.utf8(); mem = GlobalAlloc(GMEM_MOVEABLE, utf8.length() + 1); ERR_FAIL_COND_MSG(mem == nullptr, "Unable to allocate memory for clipboard contents."); @@ -206,7 +205,7 @@ String DisplayServerWindows::clipboard_get() const { _THREAD_SAFE_METHOD_ if (!windows.has(last_focused_window)) { - return String(); //no window focused? + return String(); // No focused window? } String ret; @@ -453,8 +452,8 @@ Vector<DisplayServer::WindowID> DisplayServerWindows::get_window_list() const { _THREAD_SAFE_METHOD_ Vector<DisplayServer::WindowID> ret; - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - ret.push_back(E->key()); + for (const KeyValue<WindowID, WindowData> &E : windows) { + ret.push_back(E.key); } return ret; } @@ -464,9 +463,9 @@ DisplayServer::WindowID DisplayServerWindows::get_window_at_screen_position(cons p.x = p_position.x; p.y = p_position.y; HWND hwnd = WindowFromPoint(p); - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - if (E->get().hWnd == hwnd) { - return E->key(); + for (const KeyValue<WindowID, WindowData> &E : windows) { + if (E.value.hWnd == hwnd) { + return E.key; } } @@ -498,16 +497,18 @@ DisplayServer::WindowID DisplayServerWindows::create_sub_window(WindowMode p_mod } void DisplayServerWindows::show_window(WindowID p_id) { + ERR_FAIL_COND(!windows.has(p_id)); + WindowData &wd = windows[p_id]; if (p_id != MAIN_WINDOW_ID) { _update_window_style(p_id); } - ShowWindow(wd.hWnd, wd.no_focus ? SW_SHOWNOACTIVATE : SW_SHOW); // Show The Window + ShowWindow(wd.hWnd, wd.no_focus ? SW_SHOWNOACTIVATE : SW_SHOW); // Show the window. if (!wd.no_focus) { - SetForegroundWindow(wd.hWnd); // Slightly Higher Priority - SetFocus(wd.hWnd); // Sets Keyboard Focus To + SetForegroundWindow(wd.hWnd); // Slightly higher priority. + SetFocus(wd.hWnd); // Set keyboard focus. } } @@ -606,6 +607,8 @@ void DisplayServerWindows::window_set_mouse_passthrough(const Vector<Vector2> &p } void DisplayServerWindows::_update_window_mouse_passthrough(WindowID p_window) { + ERR_FAIL_COND(!windows.has(p_window)); + if (windows[p_window].mpath.size() == 0) { SetWindowRgn(windows[p_window].hWnd, nullptr, TRUE); } else { @@ -664,16 +667,11 @@ Point2i DisplayServerWindows::window_get_position(WindowID p_window) const { ClientToScreen(wd.hWnd, &point); return Point2i(point.x, point.y); - -#if 0 - //do not use this method, as it includes windows decorations - RECT r; - GetWindowRect(wd.hWnd, &r); - return Point2(r.left, r.top); -#endif } void DisplayServerWindows::_update_real_mouse_position(WindowID p_window) { + ERR_FAIL_COND(!windows.has(p_window)); + POINT mouse_pos; if (GetCursorPos(&mouse_pos) && ScreenToClient(windows[p_window].hWnd, &mouse_pos)) { if (mouse_pos.x > 0 && mouse_pos.y > 0 && mouse_pos.x <= windows[p_window].width && mouse_pos.y <= windows[p_window].height) { @@ -691,14 +689,9 @@ void DisplayServerWindows::window_set_position(const Point2i &p_position, Window ERR_FAIL_COND(!windows.has(p_window)); WindowData &wd = windows[p_window]; - if (wd.fullscreen) + if (wd.fullscreen) { return; -#if 0 - //wrong needs to account properly for decorations - RECT r; - GetWindowRect(wd.hWnd, &r); - MoveWindow(wd.hWnd, p_position.x, p_position.y, r.right - r.left, r.bottom - r.top, TRUE); -#else + } RECT rc; rc.left = p_position.x; @@ -711,8 +704,8 @@ void DisplayServerWindows::window_set_position(const Point2i &p_position, Window AdjustWindowRectEx(&rc, style, false, exStyle); MoveWindow(wd.hWnd, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, TRUE); -#endif - // Don't let the mouse leave the window when moved + + // Don't let the mouse leave the window when moved. if (mouse_mode == MOUSE_MODE_CONFINED || mouse_mode == MOUSE_MODE_CONFINED_HIDDEN) { RECT rect; GetClientRect(wd.hWnd, &rect); @@ -729,16 +722,15 @@ void DisplayServerWindows::window_set_transient(WindowID p_window, WindowID p_pa _THREAD_SAFE_METHOD_ ERR_FAIL_COND(p_window == p_parent); - ERR_FAIL_COND(!windows.has(p_window)); + WindowData &wd_window = windows[p_window]; ERR_FAIL_COND(wd_window.transient_parent == p_parent); - ERR_FAIL_COND_MSG(wd_window.always_on_top, "Windows with the 'on top' can't become transient."); if (p_parent == INVALID_WINDOW_ID) { - //remove transient + // Remove transient. ERR_FAIL_COND(wd_window.transient_parent == INVALID_WINDOW_ID); ERR_FAIL_COND(!windows.has(wd_window.transient_parent)); @@ -838,7 +830,7 @@ void DisplayServerWindows::window_set_size(const Size2i p_size, WindowID p_windo MoveWindow(wd.hWnd, rect.left, rect.top, w, h, TRUE); - // Don't let the mouse leave the window when resizing to a smaller resolution + // Don't let the mouse leave the window when resizing to a smaller resolution. if (mouse_mode == MOUSE_MODE_CONFINED || mouse_mode == MOUSE_MODE_CONFINED_HIDDEN) { RECT crect; GetClientRect(wd.hWnd, &crect); @@ -854,12 +846,13 @@ Size2i DisplayServerWindows::window_get_size(WindowID p_window) const { ERR_FAIL_COND_V(!windows.has(p_window), Size2i()); const WindowData &wd = windows[p_window]; + // GetClientRect() returns a zero rect for a minimized window, so we need to get the size in another way. if (wd.minimized) { return Size2(wd.width, wd.height); } RECT r; - if (GetClientRect(wd.hWnd, &r)) { // Only area inside of window border + if (GetClientRect(wd.hWnd, &r)) { // Retrieves area inside of window border, including decoration. return Size2(r.right - r.left, r.bottom - r.top); } return Size2(); @@ -872,13 +865,17 @@ Size2i DisplayServerWindows::window_get_real_size(WindowID p_window) const { const WindowData &wd = windows[p_window]; RECT r; - if (GetWindowRect(wd.hWnd, &r)) { // Includes area of the window border + if (GetWindowRect(wd.hWnd, &r)) { // Retrieves area inside of window border, including decoration. return Size2(r.right - r.left, r.bottom - r.top); } return Size2(); } void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_fullscreen, bool p_borderless, bool p_resizable, bool p_maximized, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex) { + // Windows docs for window styles: + // https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles + // https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles + r_style = 0; r_style_ex = WS_EX_WINDOWEDGE; if (p_main_window) { @@ -886,10 +883,7 @@ void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_fullscre } if (p_fullscreen || p_borderless) { - r_style |= WS_POPUP; - //if (p_borderless) { - // r_style_ex |= WS_EX_TOOLWINDOW; - //} + r_style |= WS_POPUP; // p_borderless was WS_EX_TOOLWINDOW in the past. } else { if (p_resizable) { if (p_maximized) { @@ -959,6 +953,11 @@ void DisplayServerWindows::window_set_mode(WindowMode p_mode, WindowID p_window) _update_window_style(p_window, false); MoveWindow(wd.hWnd, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE); + + if (restore_mouse_trails > 1) { + SystemParametersInfoA(SPI_SETMOUSETRAILS, restore_mouse_trails, 0, 0); + restore_mouse_trails = 0; + } } else if (p_mode == WINDOW_MODE_WINDOWED) { ShowWindow(wd.hWnd, SW_RESTORE); wd.maximized = false; @@ -998,6 +997,13 @@ void DisplayServerWindows::window_set_mode(WindowMode p_mode, WindowID p_window) _update_window_style(false); MoveWindow(wd.hWnd, pos.x, pos.y, size.width, size.height, TRUE); + + // If the user has mouse trails enabled in windows, then sometimes the cursor disappears in fullscreen mode. + // Save number of trails so we can restore when exiting, then turn off mouse trails + SystemParametersInfoA(SPI_GETMOUSETRAILS, 0, &restore_mouse_trails, 0); + if (restore_mouse_trails > 1) { + SystemParametersInfoA(SPI_SETMOUSETRAILS, 0, 0, 0); + } } } @@ -1025,7 +1031,7 @@ bool DisplayServerWindows::window_is_maximize_allowed(WindowID p_window) const { // FIXME: Implement this, or confirm that it should always be true. - return true; //no idea + return true; } void DisplayServerWindows::window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window) { @@ -1123,8 +1129,8 @@ bool DisplayServerWindows::window_can_draw(WindowID p_window) const { bool DisplayServerWindows::can_any_window_draw() const { _THREAD_SAFE_METHOD_ - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - if (!E->get().minimized) { + for (const KeyValue<WindowID, WindowData> &E : windows) { + if (!E.value.minimized) { return true; } } @@ -1170,8 +1176,9 @@ void DisplayServerWindows::window_set_ime_position(const Point2i &p_pos, WindowI void DisplayServerWindows::console_set_visible(bool p_enabled) { _THREAD_SAFE_METHOD_ - if (console_visible == p_enabled) + if (console_visible == p_enabled) { return; + } ShowWindow(GetConsoleWindow(), p_enabled ? SW_SHOW : SW_HIDE); console_visible = p_enabled; } @@ -1185,8 +1192,9 @@ void DisplayServerWindows::cursor_set_shape(CursorShape p_shape) { ERR_FAIL_INDEX(p_shape, CURSOR_MAX); - if (cursor_shape == p_shape) + if (cursor_shape == p_shape) { return; + } if (mouse_mode != MOUSE_MODE_VISIBLE && mouse_mode != MOUSE_MODE_CONFINED) { cursor_shape = p_shape; @@ -1196,11 +1204,11 @@ void DisplayServerWindows::cursor_set_shape(CursorShape p_shape) { static const LPCTSTR win_cursors[CURSOR_MAX] = { IDC_ARROW, IDC_IBEAM, - IDC_HAND, //finger + IDC_HAND, // Finger. IDC_CROSS, IDC_WAIT, IDC_APPSTARTING, - IDC_ARROW, + IDC_SIZEALL, IDC_ARROW, IDC_NO, IDC_SIZENS, @@ -1227,49 +1235,49 @@ DisplayServer::CursorShape DisplayServerWindows::cursor_get_shape() const { } void DisplayServerWindows::GetMaskBitmaps(HBITMAP hSourceBitmap, COLORREF clrTransparent, OUT HBITMAP &hAndMaskBitmap, OUT HBITMAP &hXorMaskBitmap) { - // Get the system display DC + // Get the system display DC. HDC hDC = GetDC(nullptr); - // Create helper DC + // Create helper DC. HDC hMainDC = CreateCompatibleDC(hDC); HDC hAndMaskDC = CreateCompatibleDC(hDC); HDC hXorMaskDC = CreateCompatibleDC(hDC); - // Get the dimensions of the source bitmap + // Get the dimensions of the source bitmap. BITMAP bm; GetObject(hSourceBitmap, sizeof(BITMAP), &bm); - // Create the mask bitmaps - hAndMaskBitmap = CreateCompatibleBitmap(hDC, bm.bmWidth, bm.bmHeight); // color - hXorMaskBitmap = CreateCompatibleBitmap(hDC, bm.bmWidth, bm.bmHeight); // color + // Create the mask bitmaps. + hAndMaskBitmap = CreateCompatibleBitmap(hDC, bm.bmWidth, bm.bmHeight); // Color. + hXorMaskBitmap = CreateCompatibleBitmap(hDC, bm.bmWidth, bm.bmHeight); // Color. - // Release the system display DC + // Release the system display DC. ReleaseDC(nullptr, hDC); - // Select the bitmaps to helper DC + // Select the bitmaps to helper DC. HBITMAP hOldMainBitmap = (HBITMAP)SelectObject(hMainDC, hSourceBitmap); HBITMAP hOldAndMaskBitmap = (HBITMAP)SelectObject(hAndMaskDC, hAndMaskBitmap); HBITMAP hOldXorMaskBitmap = (HBITMAP)SelectObject(hXorMaskDC, hXorMaskBitmap); // Assign the monochrome AND mask bitmap pixels so that the pixels of the source bitmap - // with 'clrTransparent' will be white pixels of the monochrome bitmap + // with 'clrTransparent' will be white pixels of the monochrome bitmap. SetBkColor(hMainDC, clrTransparent); BitBlt(hAndMaskDC, 0, 0, bm.bmWidth, bm.bmHeight, hMainDC, 0, 0, SRCCOPY); // Assign the color XOR mask bitmap pixels so that the pixels of the source bitmap // with 'clrTransparent' will be black and rest the pixels same as corresponding - // pixels of the source bitmap + // pixels of the source bitmap. SetBkColor(hXorMaskDC, RGB(0, 0, 0)); SetTextColor(hXorMaskDC, RGB(255, 255, 255)); BitBlt(hXorMaskDC, 0, 0, bm.bmWidth, bm.bmHeight, hAndMaskDC, 0, 0, SRCCOPY); BitBlt(hXorMaskDC, 0, 0, bm.bmWidth, bm.bmHeight, hMainDC, 0, 0, SRCAND); - // Deselect bitmaps from the helper DC + // Deselect bitmaps from the helper DC. SelectObject(hMainDC, hOldMainBitmap); SelectObject(hAndMaskDC, hOldAndMaskBitmap); SelectObject(hXorMaskDC, hOldXorMaskBitmap); - // Delete the helper DC + // Delete the helper DC. DeleteDC(hXorMaskDC); DeleteDC(hAndMaskDC); DeleteDC(hMainDC); @@ -1326,7 +1334,7 @@ void DisplayServerWindows::cursor_set_custom_image(const RES &p_cursor, CursorSh UINT image_size = texture_size.width * texture_size.height; - // Create the BITMAP with alpha channel + // Create the BITMAP with alpha channel. COLORREF *buffer = (COLORREF *)memalloc(sizeof(COLORREF) * image_size); for (UINT index = 0; index < image_size; index++) { @@ -1341,11 +1349,11 @@ void DisplayServerWindows::cursor_set_custom_image(const RES &p_cursor, CursorSh *(buffer + index) = image->get_pixel(column_index, row_index).to_argb32(); } - // Using 4 channels, so 4 * 8 bits + // Using 4 channels, so 4 * 8 bits. HBITMAP bitmap = CreateBitmap(texture_size.width, texture_size.height, 1, 4 * 8, buffer); COLORREF clrTransparent = -1; - // Create the AND and XOR masks for the bitmap + // Create the AND and XOR masks for the bitmap. HBITMAP hAndMask = nullptr; HBITMAP hXorMask = nullptr; @@ -1357,7 +1365,7 @@ void DisplayServerWindows::cursor_set_custom_image(const RES &p_cursor, CursorSh return; } - // Finally, create the icon + // Finally, create the icon. ICONINFO iconinfo; iconinfo.fIcon = FALSE; iconinfo.xHotspot = p_hotspot.x; @@ -1365,8 +1373,9 @@ void DisplayServerWindows::cursor_set_custom_image(const RES &p_cursor, CursorSh iconinfo.hbmMask = hAndMask; iconinfo.hbmColor = hXorMask; - if (cursors[p_shape]) + if (cursors[p_shape]) { DestroyIcon(cursors[p_shape]); + } cursors[p_shape] = CreateIconIndirect(&iconinfo); @@ -1392,7 +1401,7 @@ void DisplayServerWindows::cursor_set_custom_image(const RES &p_cursor, CursorSh memfree(buffer); DeleteObject(bitmap); } else { - // Reset to default system cursor + // Reset to default system cursor. if (cursors[p_shape]) { DestroyIcon(cursors[p_shape]); cursors[p_shape] = nullptr; @@ -1465,6 +1474,42 @@ String DisplayServerWindows::keyboard_get_layout_language(int p_index) const { return String::utf16((const char16_t *)buf).substr(0, 2); } +Key DisplayServerWindows::keyboard_get_keycode_from_physical(Key p_keycode) const { + unsigned int modifiers = p_keycode & KEY_MODIFIER_MASK; + Key keycode_no_mod = (Key)(p_keycode & KEY_CODE_MASK); + + if (keycode_no_mod == KEY_PRINT || + keycode_no_mod == KEY_KP_ADD || + keycode_no_mod == KEY_KP_5 || + (keycode_no_mod >= KEY_0 && keycode_no_mod <= KEY_9)) { + return p_keycode; + } + + unsigned int scancode = KeyMappingWindows::get_scancode(keycode_no_mod); + if (scancode == 0) { + return p_keycode; + } + + HKL current_layout = GetKeyboardLayout(0); + UINT vk = MapVirtualKeyEx(scancode, MAPVK_VSC_TO_VK, current_layout); + if (vk == 0) { + return p_keycode; + } + + UINT char_code = MapVirtualKeyEx(vk, MAPVK_VK_TO_CHAR, current_layout) & 0x7FFF; + // Unlike a similar Linux/BSD check which matches full Latin-1 range, + // we limit these to ASCII to fix some layouts, including Arabic ones + if (char_code >= 32 && char_code <= 127) { + // Godot uses 'braces' instead of 'brackets' + if (char_code == KEY_BRACKETLEFT || char_code == KEY_BRACKETRIGHT) { + char_code += 32; + } + return (Key)(char_code | modifiers); + } + + return (Key)(KeyMappingWindows::get_keysym(vk) | modifiers); +} + String _get_full_layout_name_from_registry(HKL p_layout) { String id = "SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\" + String::num_int64((int64_t)p_layout, 16, false).lpad(8, "0"); String ret; @@ -1574,9 +1619,9 @@ void DisplayServerWindows::set_native_icon(const String &p_filename) { icon_dir = (ICONDIR *)memrealloc(icon_dir, 3 * sizeof(WORD) + icon_dir->idCount * sizeof(ICONDIRENTRY)); f->get_buffer((uint8_t *)&icon_dir->idEntries[0], icon_dir->idCount * sizeof(ICONDIRENTRY)); - int small_icon_index = -1; // Select 16x16 with largest color count + int small_icon_index = -1; // Select 16x16 with largest color count. int small_icon_cc = 0; - int big_icon_index = -1; // Select largest + int big_icon_index = -1; // Select largest. int big_icon_width = 16; int big_icon_cc = 0; @@ -1606,7 +1651,7 @@ void DisplayServerWindows::set_native_icon(const String &p_filename) { small_icon_cc = big_icon_cc; } - // Read the big icon + // Read the big icon. DWORD bytecount_big = icon_dir->idEntries[big_icon_index].dwBytesInRes; Vector<uint8_t> data_big; data_big.resize(bytecount_big); @@ -1616,7 +1661,7 @@ void DisplayServerWindows::set_native_icon(const String &p_filename) { HICON icon_big = CreateIconFromResource((PBYTE)&data_big.write[0], bytecount_big, TRUE, 0x00030000); ERR_FAIL_COND_MSG(!icon_big, "Could not create " + itos(big_icon_width) + "x" + itos(big_icon_width) + " @" + itos(big_icon_cc) + " icon, error: " + format_error_message(GetLastError()) + "."); - // Read the small icon + // Read the small icon. DWORD bytecount_small = icon_dir->idEntries[small_icon_index].dwBytesInRes; Vector<uint8_t> data_small; data_small.resize(bytecount_small); @@ -1626,7 +1671,7 @@ void DisplayServerWindows::set_native_icon(const String &p_filename) { HICON icon_small = CreateIconFromResource((PBYTE)&data_small.write[0], bytecount_small, TRUE, 0x00030000); ERR_FAIL_COND_MSG(!icon_small, "Could not create 16x16 @" + itos(small_icon_cc) + " icon, error: " + format_error_message(GetLastError()) + "."); - // Online tradition says to be sure last error is cleared and set the small icon first + // Online tradition says to be sure last error is cleared and set the small icon first. int err = 0; SetLastError(err); @@ -1647,12 +1692,13 @@ void DisplayServerWindows::set_icon(const Ref<Image> &p_icon) { ERR_FAIL_COND(!p_icon.is_valid()); Ref<Image> icon = p_icon->duplicate(); - if (icon->get_format() != Image::FORMAT_RGBA8) + if (icon->get_format() != Image::FORMAT_RGBA8) { icon->convert(Image::FORMAT_RGBA8); + } int w = icon->get_width(); int h = icon->get_height(); - /* Create temporary bitmap buffer */ + // Create temporary bitmap buffer. int icon_len = 40 + h * w * 4; Vector<BYTE> v; v.resize(icon_len); @@ -1686,10 +1732,10 @@ void DisplayServerWindows::set_icon(const Ref<Image> &p_icon) { HICON hicon = CreateIconFromResource(icon_bmp, icon_len, TRUE, 0x00030000); - /* Set the icon for the window */ + // Set the icon for the window. SendMessage(windows[MAIN_WINDOW_ID].hWnd, WM_SETICON, ICON_SMALL, (LPARAM)hicon); - /* Set the icon in the task manager (should we do this?) */ + // Set the icon in the task manager (should we do this?). SendMessage(windows[MAIN_WINDOW_ID].hWnd, WM_SETICON, ICON_BIG, (LPARAM)hicon); } @@ -1717,13 +1763,13 @@ void DisplayServerWindows::set_context(Context p_context) { // Keeping the name suggested by Microsoft, but this macro really answers: // Is this mouse event emulated from touch or pen input? #define IsPenEvent(dw) (((dw)&SIGNATURE_MASK) == MI_WP_SIGNATURE) -// This one tells whether the event comes from touchscreen (and not from pen) +// This one tells whether the event comes from touchscreen (and not from pen). #define IsTouchEvent(dw) (IsPenEvent(dw) && ((dw)&0x80)) void DisplayServerWindows::_touch_event(WindowID p_window, bool p_pressed, float p_x, float p_y, int idx) { - // Defensive - if (touch_state.has(idx) == p_pressed) + if (touch_state.has(idx) == p_pressed) { return; + } if (p_pressed) { touch_state.insert(idx, Vector2(p_x, p_y)); @@ -1743,12 +1789,13 @@ void DisplayServerWindows::_touch_event(WindowID p_window, bool p_pressed, float void DisplayServerWindows::_drag_event(WindowID p_window, float p_x, float p_y, int idx) { Map<int, Vector2>::Element *curr = touch_state.find(idx); - // Defensive - if (!curr) + if (!curr) { return; + } - if (curr->get() == Vector2(p_x, p_y)) + if (curr->get() == Vector2(p_x, p_y)) { return; + } Ref<InputEventScreenDrag> event; event.instantiate(); @@ -1790,7 +1837,7 @@ void DisplayServerWindows::_dispatch_input_event(const Ref<InputEvent> &p_event) Ref<InputEventFromWindow> event_from_window = p_event; if (event_from_window.is_valid() && event_from_window->get_window_id() != INVALID_WINDOW_ID) { - //send to a window + // Send to a single window. if (!windows.has(event_from_window->get_window_id())) { in_dispatch_input_event = false; ERR_FAIL_MSG("DisplayServerWindows: Invalid window id in input event."); @@ -1802,9 +1849,9 @@ void DisplayServerWindows::_dispatch_input_event(const Ref<InputEvent> &p_event) } callable.call((const Variant **)&evp, 1, ret, ce); } else { - //send to all windows - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - Callable callable = E->get().input_event_callback; + // Send to all windows. + for (const KeyValue<WindowID, WindowData> &E : windows) { + const Callable callable = E.value.input_event_callback; if (callable.is_null()) { continue; } @@ -1815,6 +1862,9 @@ void DisplayServerWindows::_dispatch_input_event(const Ref<InputEvent> &p_event) in_dispatch_input_event = false; } +// Our default window procedure to handle processing of window-related system messages/events. +// Also known as DefProc or DefWindowProc. +// See: https://docs.microsoft.com/en-us/windows/win32/winmsg/window-procedures LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { if (drop_events) { if (user_proc) { @@ -1827,27 +1877,28 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA WindowID window_id = INVALID_WINDOW_ID; bool window_created = false; - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - if (E->get().hWnd == hWnd) { - window_id = E->key(); + // Check whether window exists. + for (const KeyValue<WindowID, WindowData> &E : windows) { + if (E.value.hWnd == hWnd) { + window_id = E.key; window_created = true; break; } } + // Window doesn't exist or creation in progress, don't handle messages yet. if (!window_created) { - // Window creation in progress. window_id = window_id_counter; ERR_FAIL_COND_V(!windows.has(window_id), 0); } - switch (uMsg) // Check For Windows Messages - { + // Process window messages. + switch (uMsg) { case WM_SETFOCUS: { windows[window_id].window_has_focus = true; last_focused_window = window_id; - // Restore mouse mode + // Restore mouse mode. _set_mouse_mode_impl(mouse_mode); if (!app_focused) { @@ -1856,18 +1907,17 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } app_focused = true; } - break; - } + } break; case WM_KILLFOCUS: { windows[window_id].window_has_focus = false; last_focused_window = window_id; - // Release capture unconditionally because it can be set due to dragging, in addition to captured mode + // Release capture unconditionally because it can be set due to dragging, in addition to captured mode. ReleaseCapture(); - // Release every touch to avoid sticky points - for (Map<int, Vector2>::Element *E = touch_state.front(); E; E = E->next()) { - _touch_event(window_id, false, E->get().x, E->get().y, E->key()); + // Release every touch to avoid sticky points. + for (const KeyValue<int, Vector2> &E : touch_state) { + _touch_event(window_id, false, E.value.x, E.value.y, E.key); } touch_state.clear(); @@ -1883,10 +1933,8 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } app_focused = false; } - - break; - } - case WM_ACTIVATE: { // Watch For Window Activate Message + } break; + case WM_ACTIVATE: { // Watch for window activate message. if (!windows[window_id].window_focused) { _process_activate_event(window_id, wParam, lParam); } else { @@ -1896,11 +1944,13 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA // Run a timer to prevent event catching warning if the focused window is closing. windows[window_id].focus_timer_id = SetTimer(windows[window_id].hWnd, 2, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); } - return 0; // Return To The Message Loop - } + return 0; // Return to the message loop. + } break; case WM_GETMINMAXINFO: { if (windows[window_id].resizable && !windows[window_id].fullscreen) { - Size2 decor = window_get_size(window_id) - window_get_real_size(window_id); // Size of window decorations + // Size of window decorations. + Size2 decor = window_get_real_size(window_id) - window_get_size(window_id); + MINMAXINFO *min_max_info = (MINMAXINFO *)lParam; if (windows[window_id].min_size != Size2()) { min_max_info->ptMinTrackSize.x = windows[window_id].min_size.x + decor.x; @@ -1911,37 +1961,31 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA min_max_info->ptMaxTrackSize.y = windows[window_id].max_size.y + decor.y; } return 0; - } else { - break; } - } - case WM_PAINT: - + } break; + case WM_PAINT: { Main::force_redraw(); - break; - - case WM_SYSCOMMAND: // Intercept System Commands + } break; + case WM_SYSCOMMAND: // Intercept system commands. { - switch (wParam) // Check System Calls + switch (wParam) // Check system calls. { - case SC_SCREENSAVE: // Screensaver Trying To Start? - case SC_MONITORPOWER: // Monitor Trying To Enter Powersave? - return 0; // Prevent From Happening + case SC_SCREENSAVE: // Screensaver trying to start? + case SC_MONITORPOWER: // Monitor trying to enter powersave? + return 0; // Prevent from happening. case SC_KEYMENU: if ((lParam >> 16) <= 0) return 0; } - break; // Exit - } - - case WM_CLOSE: // Did We Receive A Close Message? + } break; + case WM_CLOSE: // Did we receive a close message? { if (windows[window_id].focus_timer_id != 0U) { KillTimer(windows[window_id].hWnd, windows[window_id].focus_timer_id); } _send_window_event(windows[window_id], WINDOW_EVENT_CLOSE_REQUEST); - return 0; // Jump Back + return 0; // Jump back. } case WM_MOUSELEAVE: { old_invalid = true; @@ -1983,7 +2027,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA Point2i c(windows[window_id].width / 2, windows[window_id].height / 2); - // centering just so it works as before + // Centering just so it works as before. POINT pos = { (int)c.x, (int)c.y }; ClientToScreen(windows[window_id].hWnd, &pos); SetCursorPos(pos.x, pos.y); @@ -2006,7 +2050,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA (double(raw->data.mouse.lLastX) - 65536.0 / (nScreenWidth)) * nScreenWidth / 65536.0 + nScreenLeft, (double(raw->data.mouse.lLastY) - 65536.0 / (nScreenHeight)) * nScreenHeight / 65536.0 + nScreenTop); - POINT coords; //client coords + POINT coords; // Client coords. coords.x = abs_pos.x; coords.y = abs_pos.y; @@ -2015,14 +2059,11 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA mm->set_relative(Vector2(coords.x - old_x, coords.y - old_y)); old_x = coords.x; old_y = coords.y; - - /*Input.mi.dx = (int)((((double)(pos.x)-nScreenLeft) * 65536) / nScreenWidth + 65536 / (nScreenWidth)); - Input.mi.dy = (int)((((double)(pos.y)-nScreenTop) * 65536) / nScreenHeight + 65536 / (nScreenHeight)); - */ } - if (windows[window_id].window_has_focus && mm->get_relative() != Vector2()) + if (windows[window_id].window_has_focus && mm->get_relative() != Vector2()) { Input::get_singleton()->parse_input_event(mm); + } } delete[] lpb; } break; @@ -2167,7 +2208,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } if (Input::get_singleton()->is_emulating_mouse_from_touch()) { - // Universal translation enabled; ignore OS translation + // Universal translation enabled; ignore OS translation. LPARAM extra = GetMessageExtraInfo(); if (IsTouchEvent(extra)) { break; @@ -2175,7 +2216,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } if (outside) { - //mouse enter + // Mouse enter. if (mouse_mode != MOUSE_MODE_CAPTURED) { _send_window_event(windows[window_id], WINDOW_EVENT_MOUSE_ENTER); @@ -2186,7 +2227,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA cursor_set_shape(c); outside = false; - //Once-Off notification, must call again.... + // Once-off notification, must call again. TRACKMOUSEEVENT tme; tme.cbSize = sizeof(TRACKMOUSEEVENT); tme.dwFlags = TME_LEAVE; @@ -2219,7 +2260,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA mm->set_button_mask(last_button_state); - POINT coords; //client coords + POINT coords; // Client coords. coords.x = GET_X_LPARAM(lParam); coords.y = GET_Y_LPARAM(lParam); @@ -2261,7 +2302,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA Input::get_singleton()->parse_input_event(mm); } - return 0; //Pointer event handled return 0 to avoid duplicate WM_MOUSEMOVE event + return 0; // Pointer event handled return 0 to avoid duplicate WM_MOUSEMOVE event. } break; case WM_MOUSEMOVE: { if (windows[window_id].block_mm) { @@ -2273,7 +2314,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } if (Input::get_singleton()->is_emulating_mouse_from_touch()) { - // Universal translation enabled; ignore OS translation + // Universal translation enabled; ignore OS translation. LPARAM extra = GetMessageExtraInfo(); if (IsTouchEvent(extra)) { break; @@ -2281,7 +2322,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } if (outside) { - //mouse enter + // Mouse enter. if (mouse_mode != MOUSE_MODE_CAPTURED) { _send_window_event(windows[window_id], WINDOW_EVENT_MOUSE_ENTER); @@ -2292,7 +2333,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA cursor_set_shape(c); outside = false; - //Once-Off notification, must call again.... + // Once-off notification, must call again. TRACKMOUSEEVENT tme; tme.cbSize = sizeof(TRACKMOUSEEVENT); tme.dwFlags = TME_LEAVE; @@ -2370,7 +2411,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA case WM_LBUTTONDOWN: case WM_LBUTTONUP: if (Input::get_singleton()->is_emulating_mouse_from_touch()) { - // Universal translation enabled; ignore OS translations for left button + // Universal translation enabled; ignore OS translations for left button. LPARAM extra = GetMessageExtraInfo(); if (IsTouchEvent(extra)) { break; @@ -2494,7 +2535,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA mb->set_ctrl_pressed((wParam & MK_CONTROL) != 0); mb->set_shift_pressed((wParam & MK_SHIFT) != 0); mb->set_alt_pressed(alt_mem); - //mb->is_alt_pressed()=(wParam&MK_MENU)!=0; + // mb->is_alt_pressed()=(wParam&MK_MENU)!=0; if (mb->is_pressed()) { last_button_state |= MouseButton(1 << (mb->get_button_index() - 1)); } else { @@ -2521,7 +2562,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } } } else { - // for reasons unknown to mankind, wheel comes in screen coordinates + // For reasons unknown to mankind, wheel comes in screen coordinates. POINT coords; coords.x = mb->get_position().x; coords.y = mb->get_position().y; @@ -2535,7 +2576,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA Input::get_singleton()->parse_input_event(mb); if (mb->is_pressed() && mb->get_button_index() > 3 && mb->get_button_index() < 8) { - //send release for mouse wheel + // Send release for mouse wheel. Ref<InputEventMouseButton> mbd = mb->duplicate(); mbd->set_window_id(window_id); last_button_state &= (MouseButton) ~(1 << (mbd->get_button_index() - 1)); @@ -2545,7 +2586,6 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } } break; - case WM_MOVE: { if (!IsIconic(windows[window_id].hWnd)) { int x = int16_t(LOWORD(lParam)); @@ -2561,12 +2601,14 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } } } break; - case WM_SIZE: { - // Ignore size when a SIZE_MINIMIZED event is triggered + // Ignore window size change when a SIZE_MINIMIZED event is triggered. if (wParam != SIZE_MINIMIZED) { + // The new width and height of the client area. int window_w = LOWORD(lParam); int window_h = HIWORD(lParam); + + // Set new value to the size if it isn't preserved. if (window_w > 0 && window_h > 0 && !windows[window_id].preserve_window_size) { windows[window_id].width = window_w; windows[window_id].height = window_h; @@ -2577,29 +2619,38 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } #endif - } else { + } else { // If the size is preserved. windows[window_id].preserve_window_size = false; + + // Restore the old size. window_set_size(Size2(windows[window_id].width, windows[window_id].height), window_id); } - } else { + } else { // When the window has been minimized, preserve its size. windows[window_id].preserve_window_size = true; } + // Call windows rect change callback. if (!windows[window_id].rect_changed_callback.is_null()) { Variant size = Rect2i(windows[window_id].last_pos.x, windows[window_id].last_pos.y, windows[window_id].width, windows[window_id].height); - Variant *sizep = &size; + Variant *size_ptr = &size; Variant ret; Callable::CallError ce; - windows[window_id].rect_changed_callback.call((const Variant **)&sizep, 1, ret, ce); + windows[window_id].rect_changed_callback.call((const Variant **)&size_ptr, 1, ret, ce); } + // The window has been maximized. if (wParam == SIZE_MAXIMIZED) { windows[window_id].maximized = true; windows[window_id].minimized = false; - } else if (wParam == SIZE_MINIMIZED) { + } + // The window has been minimized. + else if (wParam == SIZE_MINIMIZED) { windows[window_id].maximized = false; windows[window_id].minimized = true; - } else if (wParam == SIZE_RESTORED) { + windows[window_id].preserve_window_size = false; + } + // The window has been resized, but neither the SIZE_MINIMIZED nor SIZE_MAXIMIZED value applies. + else if (wParam == SIZE_RESTORED) { windows[window_id].maximized = false; windows[window_id].minimized = false; } @@ -2626,9 +2677,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA ZeroMemory(dib_data, dib_size.x * dib_size.y * 4); } #endif - //return 0; // Jump Back } break; - case WM_ENTERSIZEMOVE: { Input::get_singleton()->release_pressed_events(); windows[window_id].move_timer_id = SetTimer(windows[window_id].hWnd, 1, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); @@ -2648,7 +2697,6 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA windows[window_id].focus_timer_id = 0U; } } break; - case WM_SYSKEYDOWN: case WM_SYSKEYUP: case WM_KEYUP: @@ -2700,7 +2748,6 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA case WM_INPUTLANGCHANGEREQUEST: { // FIXME: Do something? } break; - case WM_TOUCH: { BOOL bHandled = FALSE; UINT cInputs = LOWORD(wParam); @@ -2714,7 +2761,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA TOUCH_COORD_TO_PIXEL(ti.y), }; ScreenToClient(hWnd, &touch_pos); - //do something with each touch input entry + // Do something with each touch input entry. if (ti.dwFlags & TOUCHEVENTF_MOVE) { _drag_event(window_id, touch_pos.x, touch_pos.y, ti.dwID); } else if (ti.dwFlags & (TOUCHEVENTF_UP | TOUCHEVENTF_DOWN)) { @@ -2723,11 +2770,11 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } bHandled = TRUE; } else { - /* handle the error here */ + // TODO: Handle the error here. } memdelete_arr(pInputs); } else { - /* handle the error here, probably out of memory */ + // TODO: Handle the error here, probably out of memory. } if (bHandled) { CloseTouchInputHandle((HTOUCHINPUT)lParam); @@ -2735,14 +2782,13 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA }; } break; - case WM_DEVICECHANGE: { joypad->probe_joypads(); } break; case WM_SETCURSOR: { if (LOWORD(lParam) == HTCLIENT) { if (windows[window_id].window_has_focus && (mouse_mode == MOUSE_MODE_HIDDEN || mouse_mode == MOUSE_MODE_CAPTURED || mouse_mode == MOUSE_MODE_CONFINED_HIDDEN)) { - //Hide the cursor + // Hide the cursor. if (hCursor == nullptr) { hCursor = SetCursor(nullptr); } else { @@ -2757,7 +2803,6 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } } } - } break; case WM_DROPFILES: { HDROP hDropInfo = (HDROP)wParam; @@ -2781,9 +2826,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA Callable::CallError ce; windows[window_id].drop_files_callback.call((const Variant **)&vp, 1, ret, ce); } - } break; - default: { if (user_proc) { return CallWindowProcW(user_proc, hWnd, uMsg, wParam, lParam); @@ -2809,7 +2852,7 @@ void DisplayServerWindows::_process_activate_event(WindowID p_window_id, WPARAM alt_mem = false; control_mem = false; shift_mem = false; - } else { // WM_INACTIVE + } else { // WM_INACTIVE. Input::get_singleton()->release_pressed_events(); _send_window_event(windows[p_window_id], WINDOW_EVENT_FOCUS_OUT); windows[p_window_id].window_focused = false; @@ -2826,7 +2869,7 @@ void DisplayServerWindows::_process_key_events() { KeyEvent &ke = key_event_buffer[i]; switch (ke.uMsg) { case WM_CHAR: { - // extended keys should only be processed as WM_KEYDOWN message. + // Extended keys should only be processed as WM_KEYDOWN message. if (!KeyMappingWindows::is_extended_key(ke.wParam) && ((i == 0 && ke.uMsg == WM_CHAR) || (i > 0 && key_event_buffer[i - 1].uMsg == WM_CHAR))) { static char32_t prev_wc = 0; char32_t unicode = ke.wParam; @@ -2867,9 +2910,9 @@ void DisplayServerWindows::_process_key_events() { k->set_unicode(0); Input::get_singleton()->parse_input_event(k); + } else { + // Do nothing. } - - //do nothing } break; case WM_KEYUP: case WM_KEYDOWN: { @@ -2885,7 +2928,7 @@ void DisplayServerWindows::_process_key_events() { k->set_pressed(ke.uMsg == WM_KEYDOWN); if ((ke.lParam & (1 << 24)) && (ke.wParam == VK_RETURN)) { - // Special case for Numpad Enter key + // Special case for Numpad Enter key. k->set_keycode(KEY_KP_ENTER); } else { k->set_keycode((Key)KeyMappingWindows::get_keysym(ke.wParam)); @@ -2934,8 +2977,8 @@ void DisplayServerWindows::_process_key_events() { } void DisplayServerWindows::_update_tablet_ctx(const String &p_old_driver, const String &p_new_driver) { - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - WindowData &wd = E->get(); + for (KeyValue<WindowID, WindowData> &E : windows) { + WindowData &wd = E.value; wd.block_mm = false; if ((p_old_driver == "wintab") && wintab_available && wd.wtctx) { wintab_WTEnable(wd.wtctx, false); @@ -3031,8 +3074,8 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, if (p_mode != WINDOW_MODE_FULLSCREEN) { wd.pre_fs_valid = true; } -#ifdef VULKAN_ENABLED +#ifdef VULKAN_ENABLED if (rendering_driver == "vulkan") { if (context_vulkan->window_create(id, p_vsync_mode, wd.hWnd, hInstance, WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top) == -1) { memdelete(context_vulkan); @@ -3087,7 +3130,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, wd.last_pressure_update = 0; wd.last_tilt = Vector2(); - // IME + // IME. wd.im_himc = ImmGetContext(wd.hWnd); ImmReleaseContext(wd.hWnd, wd.im_himc); @@ -3102,7 +3145,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, return id; } -// WinTab API +// WinTab API. bool DisplayServerWindows::wintab_available = false; WTOpenPtr DisplayServerWindows::wintab_WTOpen = nullptr; WTClosePtr DisplayServerWindows::wintab_WTClose = nullptr; @@ -3110,7 +3153,7 @@ WTInfoPtr DisplayServerWindows::wintab_WTInfo = nullptr; WTPacketPtr DisplayServerWindows::wintab_WTPacket = nullptr; WTEnablePtr DisplayServerWindows::wintab_WTEnable = nullptr; -// Windows Ink API +// Windows Ink API. bool DisplayServerWindows::winink_available = false; GetPointerTypePtr DisplayServerWindows::win8p_GetPointerType = nullptr; GetPointerPenInfoPtr DisplayServerWindows::win8p_GetPointerPenInfo = nullptr; @@ -3173,7 +3216,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win outside = true; - //Note: Wacom WinTab driver API for pen input, for devices incompatible with Windows Ink. + // Note: Wacom WinTab driver API for pen input, for devices incompatible with Windows Ink. HMODULE wintab_lib = LoadLibraryW(L"wintab32.dll"); if (wintab_lib) { wintab_WTOpen = (WTOpenPtr)GetProcAddress(wintab_lib, "WTOpenW"); @@ -3189,7 +3232,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win tablet_drivers.push_back("wintab"); } - //Note: Windows Ink API for pen input, available on Windows 8+ only. + // Note: Windows Ink API for pen input, available on Windows 8+ only. HMODULE user32_lib = LoadLibraryW(L"user32.dll"); if (user32_lib) { win8p_GetPointerType = (GetPointerTypePtr)GetProcAddress(user32_lib, "GetPointerType"); @@ -3222,7 +3265,6 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win wc.lpfnWndProc = (WNDPROC)::WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; - //wc.hInstance = hInstance; wc.hInstance = hInstance ? hInstance : GetModuleHandle(nullptr); wc.hIcon = LoadIcon(nullptr, IDI_WINLOGO); wc.hCursor = nullptr; //LoadCursor(nullptr, IDC_ARROW); @@ -3246,7 +3288,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win Rid[0].hwndTarget = 0; if (RegisterRawInputDevices(Rid, 1, sizeof(Rid[0])) == FALSE) { - //registration failed. + // Registration failed. use_raw_input = false; } @@ -3263,6 +3305,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win } } #endif + #if defined(OPENGL_ENABLED) if (rendering_driver_index == VIDEO_DRIVER_GLES2) { context_gles2 = memnew(ContextGL_Windows(hWnd, false)); @@ -3285,6 +3328,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win } } #endif + Point2i window_position( (screen_get_size(0).width - p_resolution.width) / 2, (screen_get_size(0).height - p_resolution.height) / 2); @@ -3313,7 +3357,6 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win //set_ime_active(false); if (!OS::get_singleton()->is_in_low_processor_usage_mode()) { - //SetPriorityClass(GetCurrentProcess(), ABOVE_NORMAL_PRIORITY_CLASS); SetPriorityClass(GetCurrentProcess(), ABOVE_NORMAL_PRIORITY_CLASS); DWORD index = 0; HANDLE handle = AvSetMmThreadCharacteristics("Games", &index); @@ -3321,7 +3364,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win AvSetMmThreadPriority(handle, AVRT_PRIORITY_CRITICAL); // This is needed to make sure that background work does not starve the main thread. - // This is only setting priority of this thread, not the whole process. + // This is only setting the priority of this thread, not the whole process. SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); } @@ -3398,4 +3441,8 @@ DisplayServerWindows::~DisplayServerWindows() { memdelete(context_vulkan); } #endif + + if (restore_mouse_trails > 1) { + SystemParametersInfoA(SPI_SETMOUSETRAILS, restore_mouse_trails, 0, 0); + } } diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 06014fbabe..22c4f96a90 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -63,6 +63,7 @@ #include <fcntl.h> #include <io.h> #include <stdio.h> +#define WIN32_LEAN_AND_MEAN #include <windows.h> #include <windowsx.h> @@ -403,6 +404,7 @@ class DisplayServerWindows : public DisplayServer { void _get_window_style(bool p_main_window, bool p_fullscreen, bool p_borderless, bool p_resizable, bool p_maximized, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex); MouseMode mouse_mode; + int restore_mouse_trails = 0; bool alt_mem = false; bool gr_mem = false; bool shift_mem = false; @@ -460,7 +462,7 @@ public: virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual void screen_set_orientation(ScreenOrientation p_orientation, int p_screen = SCREEN_OF_MAIN_WINDOW) override; - ScreenOrientation screen_get_orientation(int p_screen = SCREEN_OF_MAIN_WINDOW) const; + virtual ScreenOrientation screen_get_orientation(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual void screen_set_keep_on(bool p_enable) override; //disable screensaver virtual bool screen_is_kept_on() const override; @@ -542,6 +544,7 @@ public: virtual void keyboard_set_current_layout(int p_index) override; virtual String keyboard_get_layout_language(int p_index) const override; virtual String keyboard_get_layout_name(int p_index) const override; + virtual Key keyboard_get_keycode_from_physical(Key p_keycode) const override; virtual int tablet_get_driver_count() const override; virtual String tablet_get_driver_name(int p_driver) const override; diff --git a/platform/windows/key_mapping_windows.cpp b/platform/windows/key_mapping_windows.cpp index db99d6c122..8016d20470 100644 --- a/platform/windows/key_mapping_windows.cpp +++ b/platform/windows/key_mapping_windows.cpp @@ -345,6 +345,16 @@ unsigned int KeyMappingWindows::get_keysym(unsigned int p_code) { return KEY_UNKNOWN; } +unsigned int KeyMappingWindows::get_scancode(Key p_keycode) { + for (int i = 0; _scancode_to_keycode[i].keysym != KEY_UNKNOWN; i++) { + if (_scancode_to_keycode[i].keysym == p_keycode) { + return _scancode_to_keycode[i].keycode; + } + } + + return 0; +} + unsigned int KeyMappingWindows::get_scansym(unsigned int p_code, bool p_extended) { unsigned int keycode = KEY_UNKNOWN; for (int i = 0; _scancode_to_keycode[i].keysym != KEY_UNKNOWN; i++) { diff --git a/platform/windows/key_mapping_windows.h b/platform/windows/key_mapping_windows.h index fb07227014..d056e88f06 100644 --- a/platform/windows/key_mapping_windows.h +++ b/platform/windows/key_mapping_windows.h @@ -33,8 +33,8 @@ #include "core/os/keyboard.h" +#define WIN32_LEAN_AND_MEAN #include <windows.h> - #include <winuser.h> class KeyMappingWindows { @@ -42,6 +42,7 @@ class KeyMappingWindows { public: static unsigned int get_keysym(unsigned int p_code); + static unsigned int get_scancode(Key p_keycode); static unsigned int get_scansym(unsigned int p_code, bool p_extended); static bool is_extended_key(unsigned int p_code); }; diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 2a0a509d43..6c02bf1e16 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -28,15 +28,13 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -// Must include Winsock before windows.h (included by os_windows.h) -#include "drivers/unix/net_socket_posix.h" - #include "os_windows.h" #include "core/debugger/engine_debugger.h" #include "core/debugger/script_debugger.h" #include "core/io/marshalls.h" #include "core/version_generated.gen.h" +#include "drivers/unix/net_socket_posix.h" #include "drivers/windows/dir_access_windows.h" #include "drivers/windows/file_access_windows.h" #include "joypad_windows.h" @@ -75,7 +73,6 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; #define GetProcAddress (void *)GetProcAddress #endif -#ifdef DEBUG_ENABLED static String format_error_message(DWORD id) { LPWSTR messageBuffer = nullptr; size_t size = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, @@ -87,7 +84,6 @@ static String format_error_message(DWORD id) { return msg; } -#endif // DEBUG_ENABLED void RedirectIOToConsole() { int hConHandle; @@ -557,8 +553,27 @@ String OS_Windows::get_stdin_string(bool p_block) { } Error OS_Windows::shell_open(String p_uri) { - ShellExecuteW(nullptr, nullptr, (LPCWSTR)(p_uri.utf16().get_data()), nullptr, nullptr, SW_SHOWNORMAL); - return OK; + INT_PTR ret = (INT_PTR)ShellExecuteW(nullptr, nullptr, (LPCWSTR)(p_uri.utf16().get_data()), nullptr, nullptr, SW_SHOWNORMAL); + if (ret > 32) { + return OK; + } else { + switch (ret) { + case ERROR_FILE_NOT_FOUND: + case SE_ERR_DLLNOTFOUND: + return ERR_FILE_NOT_FOUND; + case ERROR_PATH_NOT_FOUND: + return ERR_FILE_BAD_PATH; + case ERROR_BAD_FORMAT: + return ERR_FILE_CORRUPT; + case SE_ERR_ACCESSDENIED: + return ERR_UNAUTHORIZED; + case 0: + case SE_ERR_OOM: + return ERR_OUT_OF_MEMORY; + default: + return FAILED; + } + } } String OS_Windows::get_locale() const { @@ -662,18 +677,27 @@ String OS_Windows::get_data_path() const { } String OS_Windows::get_cache_path() const { - // The XDG Base Directory specification technically only applies on Linux/*BSD, but it doesn't hurt to support it on Windows as well. - if (has_environment("XDG_CACHE_HOME")) { - if (get_environment("XDG_CACHE_HOME").is_absolute_path()) { - return get_environment("XDG_CACHE_HOME").replace("\\", "/"); - } else { - WARN_PRINT_ONCE("`XDG_CACHE_HOME` is a relative path. Ignoring its value and falling back to `%TEMP%` or `get_config_path()` per the XDG Base Directory specification."); + static String cache_path_cache; + if (cache_path_cache.is_empty()) { + // The XDG Base Directory specification technically only applies on Linux/*BSD, but it doesn't hurt to support it on Windows as well. + if (has_environment("XDG_CACHE_HOME")) { + if (get_environment("XDG_CACHE_HOME").is_absolute_path()) { + cache_path_cache = get_environment("XDG_CACHE_HOME").replace("\\", "/"); + } else { + WARN_PRINT_ONCE("`XDG_CACHE_HOME` is a relative path. Ignoring its value and falling back to `%LOCALAPPDATA%\\cache`, `%TEMP%` or `get_config_path()` per the XDG Base Directory specification."); + } + } + if (cache_path_cache.is_empty() && has_environment("LOCALAPPDATA")) { + cache_path_cache = get_environment("LOCALAPPDATA").replace("\\", "/"); + } + if (cache_path_cache.is_empty() && has_environment("TEMP")) { + cache_path_cache = get_environment("TEMP").replace("\\", "/"); + } + if (cache_path_cache.is_empty()) { + cache_path_cache = get_config_path(); } } - if (has_environment("TEMP")) { - return get_environment("TEMP").replace("\\", "/"); - } - return get_config_path(); + return cache_path_cache; } // Get properly capitalized engine name for system paths diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h index c4a2eda8f4..d24afa91eb 100644 --- a/platform/windows/os_windows.h +++ b/platform/windows/os_windows.h @@ -57,7 +57,9 @@ #include <fcntl.h> #include <io.h> +#include <shellapi.h> #include <stdio.h> +#define WIN32_LEAN_AND_MEAN #include <windows.h> #include <windowsx.h> diff --git a/platform/windows/vulkan_context_win.h b/platform/windows/vulkan_context_win.h index 39dd2641fd..61e66b8ae0 100644 --- a/platform/windows/vulkan_context_win.h +++ b/platform/windows/vulkan_context_win.h @@ -32,6 +32,8 @@ #define VULKAN_DEVICE_WIN_H #include "drivers/vulkan/vulkan_context.h" + +#define WIN32_LEAN_AND_MEAN #include <windows.h> class VulkanContextWindows : public VulkanContext { diff --git a/platform/windows/windows_terminal_logger.cpp b/platform/windows/windows_terminal_logger.cpp index 8cab7ca521..e54a61fdfd 100644 --- a/platform/windows/windows_terminal_logger.cpp +++ b/platform/windows/windows_terminal_logger.cpp @@ -33,6 +33,7 @@ #ifdef WINDOWS_ENABLED #include <stdio.h> +#define WIN32_LEAN_AND_MEAN #include <windows.h> void WindowsTerminalLogger::logv(const char *p_format, va_list p_list, bool p_err) { |