diff options
Diffstat (limited to 'platform')
89 files changed, 3762 insertions, 2530 deletions
diff --git a/platform/android/SCsub b/platform/android/SCsub index fd2a774c71..46f0703a65 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -18,6 +18,10 @@ android_files = [ 'java_class_wrapper.cpp', 'java_godot_wrapper.cpp', 'java_godot_io_wrapper.cpp', + 'jni_utils.cpp', + 'android_keys_utils.cpp', + 'vulkan/vk_renderer_jni.cpp', + 'plugin/godot_plugin_jni.cpp' ] env_android = env.Clone() diff --git a/platform/osx/semaphore_osx.h b/platform/android/android_keys_utils.cpp index 9aa2b47bc8..88874ba2c7 100644 --- a/platform/osx/semaphore_osx.h +++ b/platform/android/android_keys_utils.cpp @@ -1,5 +1,5 @@ /*************************************************************************/ -/* semaphore_osx.h */ +/* android_keys_utils.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,32 +28,16 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef SEMAPHORE_OSX_H -#define SEMAPHORE_OSX_H +#include "android_keys_utils.h" -struct cgsem { - int pipefd[2]; -}; +unsigned int android_get_keysym(unsigned int p_code) { + for (int i = 0; _ak_to_keycode[i].keysym != KEY_UNKNOWN; i++) { -typedef struct cgsem cgsem_t; + if (_ak_to_keycode[i].keycode == p_code) { -#include "core/os/semaphore.h" + return _ak_to_keycode[i].keysym; + } + } -class SemaphoreOSX : public SemaphoreOld { - - mutable cgsem_t sem; - - static SemaphoreOld *create_semaphore_osx(); - -public: - virtual Error wait(); - virtual Error post(); - virtual int get() const; - - static void make_default(); - SemaphoreOSX(); - - ~SemaphoreOSX(); -}; - -#endif // SEMAPHORE_OSX_H + return KEY_UNKNOWN; +} diff --git a/platform/android/android_keys_utils.h b/platform/android/android_keys_utils.h new file mode 100644 index 0000000000..f076688ac8 --- /dev/null +++ b/platform/android/android_keys_utils.h @@ -0,0 +1,280 @@ +/*************************************************************************/ +/* android_keys_utils.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef ANDROID_KEYS_UTILS_H +#define ANDROID_KEYS_UTILS_H + +#include <core/os/keyboard.h> + +/* + * Android Key codes. + */ +enum { + AKEYCODE_UNKNOWN = 0, + AKEYCODE_SOFT_LEFT = 1, + AKEYCODE_SOFT_RIGHT = 2, + AKEYCODE_HOME = 3, + AKEYCODE_BACK = 4, + AKEYCODE_CALL = 5, + AKEYCODE_ENDCALL = 6, + AKEYCODE_0 = 7, + AKEYCODE_1 = 8, + AKEYCODE_2 = 9, + AKEYCODE_3 = 10, + AKEYCODE_4 = 11, + AKEYCODE_5 = 12, + AKEYCODE_6 = 13, + AKEYCODE_7 = 14, + AKEYCODE_8 = 15, + AKEYCODE_9 = 16, + AKEYCODE_STAR = 17, + AKEYCODE_POUND = 18, + AKEYCODE_DPAD_UP = 19, + AKEYCODE_DPAD_DOWN = 20, + AKEYCODE_DPAD_LEFT = 21, + AKEYCODE_DPAD_RIGHT = 22, + AKEYCODE_DPAD_CENTER = 23, + AKEYCODE_VOLUME_UP = 24, + AKEYCODE_VOLUME_DOWN = 25, + AKEYCODE_POWER = 26, + AKEYCODE_CAMERA = 27, + AKEYCODE_CLEAR = 28, + AKEYCODE_A = 29, + AKEYCODE_B = 30, + AKEYCODE_C = 31, + AKEYCODE_D = 32, + AKEYCODE_E = 33, + AKEYCODE_F = 34, + AKEYCODE_G = 35, + AKEYCODE_H = 36, + AKEYCODE_I = 37, + AKEYCODE_J = 38, + AKEYCODE_K = 39, + AKEYCODE_L = 40, + AKEYCODE_M = 41, + AKEYCODE_N = 42, + AKEYCODE_O = 43, + AKEYCODE_P = 44, + AKEYCODE_Q = 45, + AKEYCODE_R = 46, + AKEYCODE_S = 47, + AKEYCODE_T = 48, + AKEYCODE_U = 49, + AKEYCODE_V = 50, + AKEYCODE_W = 51, + AKEYCODE_X = 52, + AKEYCODE_Y = 53, + AKEYCODE_Z = 54, + AKEYCODE_COMMA = 55, + AKEYCODE_PERIOD = 56, + AKEYCODE_ALT_LEFT = 57, + AKEYCODE_ALT_RIGHT = 58, + AKEYCODE_SHIFT_LEFT = 59, + AKEYCODE_SHIFT_RIGHT = 60, + AKEYCODE_TAB = 61, + AKEYCODE_SPACE = 62, + AKEYCODE_SYM = 63, + AKEYCODE_EXPLORER = 64, + AKEYCODE_ENVELOPE = 65, + AKEYCODE_ENTER = 66, + AKEYCODE_DEL = 67, + AKEYCODE_GRAVE = 68, + AKEYCODE_MINUS = 69, + AKEYCODE_EQUALS = 70, + AKEYCODE_LEFT_BRACKET = 71, + AKEYCODE_RIGHT_BRACKET = 72, + AKEYCODE_BACKSLASH = 73, + AKEYCODE_SEMICOLON = 74, + AKEYCODE_APOSTROPHE = 75, + AKEYCODE_SLASH = 76, + AKEYCODE_AT = 77, + AKEYCODE_NUM = 78, + AKEYCODE_HEADSETHOOK = 79, + AKEYCODE_FOCUS = 80, // *Camera* focus + AKEYCODE_PLUS = 81, + AKEYCODE_MENU = 82, + AKEYCODE_NOTIFICATION = 83, + AKEYCODE_SEARCH = 84, + AKEYCODE_MEDIA_PLAY_PAUSE = 85, + AKEYCODE_MEDIA_STOP = 86, + AKEYCODE_MEDIA_NEXT = 87, + AKEYCODE_MEDIA_PREVIOUS = 88, + AKEYCODE_MEDIA_REWIND = 89, + AKEYCODE_MEDIA_FAST_FORWARD = 90, + AKEYCODE_MUTE = 91, + AKEYCODE_PAGE_UP = 92, + AKEYCODE_PAGE_DOWN = 93, + AKEYCODE_PICTSYMBOLS = 94, + AKEYCODE_SWITCH_CHARSET = 95, + AKEYCODE_BUTTON_A = 96, + AKEYCODE_BUTTON_B = 97, + AKEYCODE_BUTTON_C = 98, + AKEYCODE_BUTTON_X = 99, + AKEYCODE_BUTTON_Y = 100, + AKEYCODE_BUTTON_Z = 101, + AKEYCODE_BUTTON_L1 = 102, + AKEYCODE_BUTTON_R1 = 103, + AKEYCODE_BUTTON_L2 = 104, + AKEYCODE_BUTTON_R2 = 105, + AKEYCODE_BUTTON_THUMBL = 106, + AKEYCODE_BUTTON_THUMBR = 107, + AKEYCODE_BUTTON_START = 108, + AKEYCODE_BUTTON_SELECT = 109, + AKEYCODE_BUTTON_MODE = 110, + + // NOTE: If you add a new keycode here you must also add it to several other files. + // Refer to frameworks/base/core/java/android/view/KeyEvent.java for the full list. +}; + +struct _WinTranslatePair { + + unsigned int keysym; + unsigned int keycode; +}; + +static _WinTranslatePair _ak_to_keycode[] = { + { KEY_TAB, AKEYCODE_TAB }, + { KEY_ENTER, AKEYCODE_ENTER }, + { KEY_SHIFT, AKEYCODE_SHIFT_LEFT }, + { KEY_SHIFT, AKEYCODE_SHIFT_RIGHT }, + { KEY_ALT, AKEYCODE_ALT_LEFT }, + { KEY_ALT, AKEYCODE_ALT_RIGHT }, + { KEY_MENU, AKEYCODE_MENU }, + { KEY_PAUSE, AKEYCODE_MEDIA_PLAY_PAUSE }, + { KEY_ESCAPE, AKEYCODE_BACK }, + { KEY_SPACE, AKEYCODE_SPACE }, + { KEY_PAGEUP, AKEYCODE_PAGE_UP }, + { KEY_PAGEDOWN, AKEYCODE_PAGE_DOWN }, + { KEY_HOME, AKEYCODE_HOME }, //(0x24) + { KEY_LEFT, AKEYCODE_DPAD_LEFT }, + { KEY_UP, AKEYCODE_DPAD_UP }, + { KEY_RIGHT, AKEYCODE_DPAD_RIGHT }, + { KEY_DOWN, AKEYCODE_DPAD_DOWN }, + { KEY_PERIODCENTERED, AKEYCODE_DPAD_CENTER }, + { KEY_BACKSPACE, AKEYCODE_DEL }, + { KEY_0, AKEYCODE_0 }, ////0 key + { KEY_1, AKEYCODE_1 }, ////1 key + { KEY_2, AKEYCODE_2 }, ////2 key + { KEY_3, AKEYCODE_3 }, ////3 key + { KEY_4, AKEYCODE_4 }, ////4 key + { KEY_5, AKEYCODE_5 }, ////5 key + { KEY_6, AKEYCODE_6 }, ////6 key + { KEY_7, AKEYCODE_7 }, ////7 key + { KEY_8, AKEYCODE_8 }, ////8 key + { KEY_9, AKEYCODE_9 }, ////9 key + { KEY_A, AKEYCODE_A }, ////A key + { KEY_B, AKEYCODE_B }, ////B key + { KEY_C, AKEYCODE_C }, ////C key + { KEY_D, AKEYCODE_D }, ////D key + { KEY_E, AKEYCODE_E }, ////E key + { KEY_F, AKEYCODE_F }, ////F key + { KEY_G, AKEYCODE_G }, ////G key + { KEY_H, AKEYCODE_H }, ////H key + { KEY_I, AKEYCODE_I }, ////I key + { KEY_J, AKEYCODE_J }, ////J key + { KEY_K, AKEYCODE_K }, ////K key + { KEY_L, AKEYCODE_L }, ////L key + { KEY_M, AKEYCODE_M }, ////M key + { KEY_N, AKEYCODE_N }, ////N key + { KEY_O, AKEYCODE_O }, ////O key + { KEY_P, AKEYCODE_P }, ////P key + { KEY_Q, AKEYCODE_Q }, ////Q key + { KEY_R, AKEYCODE_R }, ////R key + { KEY_S, AKEYCODE_S }, ////S key + { KEY_T, AKEYCODE_T }, ////T key + { KEY_U, AKEYCODE_U }, ////U key + { KEY_V, AKEYCODE_V }, ////V key + { KEY_W, AKEYCODE_W }, ////W key + { KEY_X, AKEYCODE_X }, ////X key + { KEY_Y, AKEYCODE_Y }, ////Y key + { KEY_Z, AKEYCODE_Z }, ////Z key + { KEY_HOMEPAGE, AKEYCODE_EXPLORER }, + { KEY_LAUNCH0, AKEYCODE_BUTTON_A }, + { KEY_LAUNCH1, AKEYCODE_BUTTON_B }, + { KEY_LAUNCH2, AKEYCODE_BUTTON_C }, + { KEY_LAUNCH3, AKEYCODE_BUTTON_X }, + { KEY_LAUNCH4, AKEYCODE_BUTTON_Y }, + { KEY_LAUNCH5, AKEYCODE_BUTTON_Z }, + { KEY_LAUNCH6, AKEYCODE_BUTTON_L1 }, + { KEY_LAUNCH7, AKEYCODE_BUTTON_R1 }, + { KEY_LAUNCH8, AKEYCODE_BUTTON_L2 }, + { KEY_LAUNCH9, AKEYCODE_BUTTON_R2 }, + { KEY_LAUNCHA, AKEYCODE_BUTTON_THUMBL }, + { KEY_LAUNCHB, AKEYCODE_BUTTON_THUMBR }, + { KEY_LAUNCHC, AKEYCODE_BUTTON_START }, + { KEY_LAUNCHD, AKEYCODE_BUTTON_SELECT }, + { KEY_LAUNCHE, AKEYCODE_BUTTON_MODE }, + { KEY_VOLUMEMUTE, AKEYCODE_MUTE }, + { KEY_VOLUMEDOWN, AKEYCODE_VOLUME_DOWN }, + { KEY_VOLUMEUP, AKEYCODE_VOLUME_UP }, + { KEY_BACK, AKEYCODE_MEDIA_REWIND }, + { KEY_FORWARD, AKEYCODE_MEDIA_FAST_FORWARD }, + { KEY_MEDIANEXT, AKEYCODE_MEDIA_NEXT }, + { KEY_MEDIAPREVIOUS, AKEYCODE_MEDIA_PREVIOUS }, + { KEY_MEDIASTOP, AKEYCODE_MEDIA_STOP }, + { KEY_PLUS, AKEYCODE_PLUS }, + { KEY_EQUAL, AKEYCODE_EQUALS }, // the '+' key + { KEY_COMMA, AKEYCODE_COMMA }, // the ',' key + { KEY_MINUS, AKEYCODE_MINUS }, // the '-' key + { KEY_SLASH, AKEYCODE_SLASH }, // the '/?' key + { KEY_BACKSLASH, AKEYCODE_BACKSLASH }, + { KEY_BRACKETLEFT, AKEYCODE_LEFT_BRACKET }, + { KEY_BRACKETRIGHT, AKEYCODE_RIGHT_BRACKET }, + { KEY_UNKNOWN, 0 } +}; +/* +TODO: map these android key: + AKEYCODE_SOFT_LEFT = 1, + AKEYCODE_SOFT_RIGHT = 2, + AKEYCODE_CALL = 5, + AKEYCODE_ENDCALL = 6, + AKEYCODE_STAR = 17, + AKEYCODE_POUND = 18, + AKEYCODE_POWER = 26, + AKEYCODE_CAMERA = 27, + AKEYCODE_CLEAR = 28, + AKEYCODE_SYM = 63, + AKEYCODE_ENVELOPE = 65, + AKEYCODE_GRAVE = 68, + AKEYCODE_SEMICOLON = 74, + AKEYCODE_APOSTROPHE = 75, + AKEYCODE_AT = 77, + AKEYCODE_NUM = 78, + AKEYCODE_HEADSETHOOK = 79, + AKEYCODE_FOCUS = 80, // *Camera* focus + AKEYCODE_NOTIFICATION = 83, + AKEYCODE_SEARCH = 84, + AKEYCODE_PICTSYMBOLS = 94, + AKEYCODE_SWITCH_CHARSET = 95, +*/ + +unsigned int android_get_keysym(unsigned int p_code); + +#endif // ANDROID_KEYS_UTILS_H diff --git a/platform/android/audio_driver_jandroid.cpp b/platform/android/audio_driver_jandroid.cpp index 5a8e3b94da..e94dad9ac6 100644 --- a/platform/android/audio_driver_jandroid.cpp +++ b/platform/android/audio_driver_jandroid.cpp @@ -48,7 +48,7 @@ int AudioDriverAndroid::mix_rate = 44100; bool AudioDriverAndroid::quit = false; jobject AudioDriverAndroid::audioBuffer = NULL; void *AudioDriverAndroid::audioBufferPinned = NULL; -Mutex *AudioDriverAndroid::mutex = NULL; +Mutex AudioDriverAndroid::mutex; int32_t *AudioDriverAndroid::audioBuffer32 = NULL; const char *AudioDriverAndroid::get_name() const { @@ -58,7 +58,6 @@ const char *AudioDriverAndroid::get_name() const { Error AudioDriverAndroid::init() { - mutex = Mutex::create(); /* // TODO: pass in/return a (Java) device ID, also whether we're opening for input or output this->spec.samples = Android_JNI_OpenAudioDevice(this->spec.freq, this->spec.format == AUDIO_U8 ? 0 : 1, this->spec.channels, this->spec.samples); @@ -133,7 +132,7 @@ void AudioDriverAndroid::thread_func(JNIEnv *env) { int16_t *ptr = (int16_t *)audioBufferPinned; int fc = audioBufferFrames; - if (!s_ad->active || mutex->try_lock() != OK) { + if (!s_ad->active || mutex.try_lock() != OK) { for (int i = 0; i < fc; i++) { ptr[i] = 0; @@ -143,7 +142,7 @@ void AudioDriverAndroid::thread_func(JNIEnv *env) { s_ad->audio_server_process(fc / 2, audioBuffer32); - mutex->unlock(); + mutex.unlock(); for (int i = 0; i < fc; i++) { @@ -167,14 +166,12 @@ AudioDriver::SpeakerMode AudioDriverAndroid::get_speaker_mode() const { void AudioDriverAndroid::lock() { - if (mutex) - mutex->lock(); + mutex.lock(); } void AudioDriverAndroid::unlock() { - if (mutex) - mutex->unlock(); + mutex.unlock(); } void AudioDriverAndroid::finish() { diff --git a/platform/android/audio_driver_jandroid.h b/platform/android/audio_driver_jandroid.h index d3d1641c20..b1cc3f9aa0 100644 --- a/platform/android/audio_driver_jandroid.h +++ b/platform/android/audio_driver_jandroid.h @@ -37,7 +37,7 @@ class AudioDriverAndroid : public AudioDriver { - static Mutex *mutex; + static Mutex mutex; static AudioDriverAndroid *s_ad; static jobject io; static jmethodID _init_audio; diff --git a/platform/android/audio_driver_opensl.cpp b/platform/android/audio_driver_opensl.cpp index 6e9864c803..222120f81f 100644 --- a/platform/android/audio_driver_opensl.cpp +++ b/platform/android/audio_driver_opensl.cpp @@ -44,8 +44,8 @@ void AudioDriverOpenSL::_buffer_callback( if (pause) { mix = false; - } else if (mutex) { - mix = mutex->try_lock() == OK; + } else { + mix = mutex.try_lock() == OK; } if (mix) { @@ -58,8 +58,8 @@ void AudioDriverOpenSL::_buffer_callback( } } - if (mutex && mix) - mutex->unlock(); + if (mix) + mutex.unlock(); const int32_t *src_buff = mixdown_buffer; @@ -107,7 +107,6 @@ Error AudioDriverOpenSL::init() { void AudioDriverOpenSL::start() { - mutex = Mutex::create(); active = false; SLresult res; @@ -329,14 +328,14 @@ AudioDriver::SpeakerMode AudioDriverOpenSL::get_speaker_mode() const { void AudioDriverOpenSL::lock() { - if (active && mutex) - mutex->lock(); + if (active) + mutex.lock(); } void AudioDriverOpenSL::unlock() { - if (active && mutex) - mutex->unlock(); + if (active) + mutex.unlock(); } void AudioDriverOpenSL::finish() { @@ -359,7 +358,6 @@ void AudioDriverOpenSL::set_pause(bool p_pause) { AudioDriverOpenSL::AudioDriverOpenSL() { s_ad = this; - mutex = Mutex::create(); //NULL; pause = false; active = false; } diff --git a/platform/android/audio_driver_opensl.h b/platform/android/audio_driver_opensl.h index 57d9b30af6..569e2aa54b 100644 --- a/platform/android/audio_driver_opensl.h +++ b/platform/android/audio_driver_opensl.h @@ -40,7 +40,7 @@ class AudioDriverOpenSL : public AudioDriver { bool active; - Mutex *mutex; + Mutex mutex; enum { diff --git a/platform/android/detect.py b/platform/android/detect.py index 8b62360888..8f74ae0ef0 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -205,7 +205,6 @@ def configure(env): env.Append(CPPFLAGS=["-isystem", env["ANDROID_NDK_ROOT"] + "/sources/cxx-stl/llvm-libc++/include"]) env.Append(CPPFLAGS=["-isystem", env["ANDROID_NDK_ROOT"] + "/sources/cxx-stl/llvm-libc++abi/include"]) - env.Append(CXXFLAGS=["-std=gnu++14"]) # Disable exceptions and rtti on non-tools (template) builds if env['tools']: diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index f52b511522..ebcac884db 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -144,7 +144,7 @@ Error DirAccessJAndroid::change_dir(String p_dir) { return OK; } -String DirAccessJAndroid::get_current_dir() { +String DirAccessJAndroid::get_current_dir(bool p_include_drive) { return "res://" + current_dir; } diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h index caeb4b58b9..8dab3e50ce 100644 --- a/platform/android/dir_access_jandroid.h +++ b/platform/android/dir_access_jandroid.h @@ -65,7 +65,7 @@ public: virtual String get_drive(int p_drive); virtual Error change_dir(String p_dir); ///< can be relative or absolute, return false on success - virtual String get_current_dir(); ///< return current dir location + virtual String get_current_dir(bool p_include_drive = true); ///< return current dir location virtual bool file_exists(String p_file); virtual bool dir_exists(String p_dir); diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index 50bf671a84..e7d4bc6c51 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -257,7 +257,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { Vector<Device> devices; volatile bool devices_changed; - Mutex *device_lock; + Mutex device_lock; Thread *device_thread; volatile bool quit_request; @@ -288,7 +288,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { ldevices.push_back(d); } - ea->device_lock->lock(); + MutexLock lock(ea->device_lock); bool different = false; @@ -381,8 +381,6 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { ea->devices = ndevices; ea->devices_changed = true; } - - ea->device_lock->unlock(); } uint64_t sleep = 200; @@ -690,6 +688,8 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { int xr_mode_index = p_preset->get("xr_features/xr_mode"); + String plugins = p_preset->get("custom_template/plugins"); + Vector<String> perms; const char **aperms = android_perms; @@ -853,6 +853,11 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { } } + if (tname == "meta-data" && attrname == "value" && value == "custom_template_plugins_value") { + // Update the meta-data 'android:value' attribute with the list of enabled plugins. + string_table.write[attr_value] = plugins; + } + iofs += 20; } @@ -1365,6 +1370,7 @@ public: r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "custom_template/use_custom_build"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/plugins", PROPERTY_HINT_PLACEHOLDER_TEXT, "Plugin1,Plugin2,..."), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "command_line/extra_args"), "")); 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")); @@ -1432,11 +1438,8 @@ public: virtual int get_options_count() const { - device_lock->lock(); - int dc = devices.size(); - device_lock->unlock(); - - return dc; + MutexLock lock(device_lock); + return devices.size(); } virtual String get_options_tooltip() const { @@ -1447,16 +1450,14 @@ public: virtual String get_option_label(int p_index) const { ERR_FAIL_INDEX_V(p_index, devices.size(), ""); - device_lock->lock(); - String s = devices[p_index].name; - device_lock->unlock(); - return s; + MutexLock lock(device_lock); + return devices[p_index].name; } virtual String get_option_tooltip(int p_index) const { ERR_FAIL_INDEX_V(p_index, devices.size(), ""); - device_lock->lock(); + MutexLock lock(device_lock); String s = devices[p_index].description; if (devices.size() == 1) { // Tooltip will be: @@ -1464,7 +1465,6 @@ public: // Description s = devices[p_index].name + "\n\n" + s; } - device_lock->unlock(); return s; } @@ -1479,7 +1479,7 @@ public: return ERR_UNCONFIGURED; } - device_lock->lock(); + MutexLock lock(device_lock); EditorProgress ep("run", "Running on " + devices[p_device].name, 3); @@ -1487,7 +1487,6 @@ public: // Export_temp APK. if (ep.step("Exporting APK...", 0)) { - device_lock->unlock(); return ERR_SKIP; } @@ -1502,7 +1501,6 @@ public: #define CLEANUP_AND_RETURN(m_err) \ { \ DirAccess::remove_file_or_error(tmp_export_path); \ - device_lock->unlock(); \ return m_err; \ } @@ -1753,260 +1751,6 @@ public: return list; } - void _update_custom_build_project() { - - DirAccessRef da = DirAccess::open("res://android"); - - ERR_FAIL_COND_MSG(!da, "Cannot open directory 'res://android'."); - Map<String, List<String> > directory_paths; - Map<String, List<String> > manifest_sections; - Map<String, List<String> > gradle_sections; - da->list_dir_begin(); - String d = da->get_next(); - while (d != String()) { - - if (!d.begins_with(".") && d != "build" && da->current_is_dir()) { //a dir and not the build dir - //add directories found - DirAccessRef ds = DirAccess::open(String("res://android").plus_file(d)); - if (ds) { - ds->list_dir_begin(); - String sd = ds->get_next(); - while (sd != String()) { - - if (!sd.begins_with(".") && ds->current_is_dir()) { - String key = sd.to_upper(); - if (!directory_paths.has(key)) { - directory_paths[key] = List<String>(); - } - String path = ProjectSettings::get_singleton()->get_resource_path().plus_file("android").plus_file(d).plus_file(sd); - directory_paths[key].push_back(path); - print_line("Add: " + sd + ":" + path); - } - - sd = ds->get_next(); - } - ds->list_dir_end(); - } - //parse manifest - { - FileAccessRef f = FileAccess::open(String("res://android").plus_file(d).plus_file("AndroidManifest.conf"), FileAccess::READ); - if (f) { - - String section; - while (!f->eof_reached()) { - String l = f->get_line(); - String k = l.strip_edges(); - if (k.begins_with("[")) { - section = k.substr(1, k.length() - 2).strip_edges().to_upper(); - print_line("Section: " + section); - } else if (k != String()) { - if (!manifest_sections.has(section)) { - manifest_sections[section] = List<String>(); - } - manifest_sections[section].push_back(l); - } - } - - f->close(); - } - } - //parse gradle - { - FileAccessRef f = FileAccess::open(String("res://android").plus_file(d).plus_file("gradle.conf"), FileAccess::READ); - if (f) { - - String section; - while (!f->eof_reached()) { - String l = f->get_line().strip_edges(); - String k = l.strip_edges(); - if (k.begins_with("[")) { - section = k.substr(1, k.length() - 2).strip_edges().to_upper(); - print_line("Section: " + section); - } else if (k != String()) { - if (!gradle_sections.has(section)) { - gradle_sections[section] = List<String>(); - } - gradle_sections[section].push_back(l); - } - } - } - } - } - d = da->get_next(); - } - da->list_dir_end(); - - { //fix gradle build - - String new_file; - { - FileAccessRef f = FileAccess::open("res://android/build/build.gradle", FileAccess::READ); - if (f) { - - while (!f->eof_reached()) { - String l = f->get_line(); - - if (l.begins_with("//CHUNK_")) { - String text = l.replace_first("//CHUNK_", ""); - int begin_pos = text.find("_BEGIN"); - if (begin_pos != -1) { - text = text.substr(0, begin_pos); - text = text.to_upper(); //just in case - - String end_marker = "//CHUNK_" + text + "_END"; - size_t pos = f->get_position(); - bool found = false; - while (!f->eof_reached()) { - l = f->get_line(); - if (l.begins_with(end_marker)) { - found = true; - break; - } - } - - new_file += "//CHUNK_" + text + "_BEGIN\n"; - - if (!found) { - ERR_PRINT("No end marker found in build.gradle for chunk: " + text); - f->seek(pos); - } else { - - //add chunk lines - if (gradle_sections.has(text)) { - for (List<String>::Element *E = gradle_sections[text].front(); E; E = E->next()) { - new_file += E->get() + "\n"; - } - } - new_file += end_marker + "\n"; - } - } else { - new_file += l + "\n"; //pass line by - } - } else if (l.begins_with("//DIR_")) { - String text = l.replace_first("//DIR_", ""); - int begin_pos = text.find("_BEGIN"); - if (begin_pos != -1) { - text = text.substr(0, begin_pos); - text = text.to_upper(); //just in case - - String end_marker = "//DIR_" + text + "_END"; - size_t pos = f->get_position(); - bool found = false; - while (!f->eof_reached()) { - l = f->get_line(); - if (l.begins_with(end_marker)) { - found = true; - break; - } - } - - new_file += "//DIR_" + text + "_BEGIN\n"; - - if (!found) { - ERR_PRINT("No end marker found in build.gradle for dir: " + text); - f->seek(pos); - } else { - //add chunk lines - if (directory_paths.has(text)) { - for (List<String>::Element *E = directory_paths[text].front(); E; E = E->next()) { - new_file += ",'" + E->get().replace("'", "\'") + "'"; - new_file += "\n"; - } - } - new_file += end_marker + "\n"; - } - } else { - new_file += l + "\n"; //pass line by - } - - } else { - new_file += l + "\n"; - } - } - } - } - - FileAccessRef f = FileAccess::open("res://android/build/build.gradle", FileAccess::WRITE); - f->store_string(new_file); - f->close(); - } - - { //fix manifest - - String new_file; - { - FileAccessRef f = FileAccess::open("res://android/build/AndroidManifest.xml", FileAccess::READ); - if (f) { - - while (!f->eof_reached()) { - String l = f->get_line(); - - if (l.begins_with("<!--CHUNK_")) { - String text = l.replace_first("<!--CHUNK_", ""); - int begin_pos = text.find("_BEGIN-->"); - if (begin_pos != -1) { - text = text.substr(0, begin_pos); - text = text.to_upper(); //just in case - - String end_marker = "<!--CHUNK_" + text + "_END-->"; - size_t pos = f->get_position(); - bool found = false; - while (!f->eof_reached()) { - l = f->get_line(); - if (l.begins_with(end_marker)) { - found = true; - break; - } - } - - new_file += "<!--CHUNK_" + text + "_BEGIN-->\n"; - - if (!found) { - ERR_PRINT("No end marker found in AndroidManifest.xml for chunk: " + text); - f->seek(pos); - } else { - //add chunk lines - if (manifest_sections.has(text)) { - for (List<String>::Element *E = manifest_sections[text].front(); E; E = E->next()) { - new_file += E->get() + "\n"; - } - } - new_file += end_marker + "\n"; - } - } else { - new_file += l + "\n"; //pass line by - } - - } else if (l.strip_edges().begins_with("<application")) { - String last_tag = "android:icon=\"@mipmap/icon\""; - int last_tag_pos = l.find(last_tag); - if (last_tag_pos == -1) { - ERR_PRINT("Not adding application attributes as the expected tag was not found in '<application': " + last_tag); - new_file += l + "\n"; - } else { - String base = l.substr(0, last_tag_pos + last_tag.length()); - if (manifest_sections.has("application_attribs")) { - for (List<String>::Element *E = manifest_sections["application_attribs"].front(); E; E = E->next()) { - String to_add = E->get().strip_edges(); - base += " " + to_add + " "; - } - } - base += ">\n"; - new_file += base; - } - } else { - new_file += l + "\n"; - } - } - } - } - - FileAccessRef f = FileAccess::open("res://android/build/AndroidManifest.xml", FileAccess::WRITE); - f->store_string(new_file); - f->close(); - } - } - virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) { ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); @@ -2035,8 +1779,6 @@ public: ERR_FAIL_COND_V_MSG(sdk_path == "", ERR_UNCONFIGURED, "Android SDK path must be configured in Editor Settings at 'export/android/custom_build_sdk_path'."); - _update_custom_build_project(); - OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path); //set and overwrite if required String build_command; @@ -2047,14 +1789,18 @@ public: #endif String build_path = ProjectSettings::get_singleton()->get_resource_path().plus_file("android/build"); + String plugins_dir = ProjectSettings::get_singleton()->get_resource_path().plus_file("android/plugins"); build_command = build_path.plus_file(build_command); String package_name = get_package_name(p_preset->get("package/unique_name")); + String plugins = p_preset->get("custom_template/plugins"); List<String> cmdline; cmdline.push_back("build"); cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name. + cmdline.push_back("-Pcustom_template_plugins_dir=" + plugins_dir); // argument to specify the plugins directory. + cmdline.push_back("-Pcustom_template_plugins=" + plugins); // argument to specify the list of plugins to enable. cmdline.push_back("-p"); // argument to specify the start directory. cmdline.push_back(build_path); // start directory. /*{ used for debug @@ -2570,7 +2316,6 @@ public: run_icon.instance(); run_icon->create_from_image(img); - device_lock = Mutex::create(); devices_changed = true; quit_request = false; device_thread = Thread::create(_device_poll_thread, this); @@ -2579,7 +2324,6 @@ public: ~EditorExportPlatformAndroid() { quit_request = true; Thread::wait_to_finish(device_thread); - memdelete(device_lock); memdelete(device_thread); } }; diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index 4d2eb1ef65..cc480d1c84 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -6,9 +6,6 @@ android:versionName="1.0" android:installLocation="auto" > - <!-- Adding custom text to the manifest is fine, but do it outside the custom USER and APPLICATION BEGIN/END comments, --> - <!-- as that gets rewritten. --> - <supports-screens android:smallScreens="true" android:normalScreens="true" @@ -19,14 +16,11 @@ android:glEsVersion="0x00020000" android:required="true" /> -<!-- Custom user permissions XML added by add-ons. It's recommended to add them from the export preset, though. --> -<!--CHUNK_USER_PERMISSIONS_BEGIN--> -<!--CHUNK_USER_PERMISSIONS_END--> - - <!-- Any tag in this line after android:icon will be erased when doing custom builds. --> - <!-- If you want to add tags manually, do before it. --> - <!-- WARNING: This should stay on a single line until the parsing code is improved. See GH-32414. --> - <application android:label="@string/godot_project_name_string" android:allowBackup="false" tools:ignore="GoogleAppIndexingWarning" android:icon="@mipmap/icon" > + <application + android:label="@string/godot_project_name_string" + android:allowBackup="false" + tools:ignore="GoogleAppIndexingWarning" + android:icon="@mipmap/icon" > <!-- The following metadata values are replaced when Godot exports, modifying them here has no effect. --> <!-- Do these changes in the export preset. Adding new ones is fine. --> @@ -36,6 +30,11 @@ android:name="xr_mode_metadata_name" android:value="xr_mode_metadata_value" /> + <!-- Metadata populated at export time and used by Godot to figure out which plugins must be enabled. --> + <meta-data + android:name="custom_template_plugins" + android:value="custom_template_plugins_value"/> + <activity android:name=".GodotApp" android:label="@string/godot_project_name_string" @@ -52,10 +51,6 @@ </intent-filter> </activity> -<!-- Custom application XML added by add-ons. --> -<!--CHUNK_APPLICATION_BEGIN--> -<!--CHUNK_APPLICATION_END--> - </application> </manifest> diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 2e4f2ffab0..5e37f538e9 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -1,7 +1,4 @@ // Gradle build config for Godot Engine's Android port. -// -// Do not remove/modify comments ending with BEGIN/END, they are used to inject -// addon-specific configuration. apply from: 'config.gradle' buildscript { @@ -10,13 +7,10 @@ buildscript { repositories { google() jcenter() -//CHUNK_BUILDSCRIPT_REPOSITORIES_BEGIN -//CHUNK_BUILDSCRIPT_REPOSITORIES_END } dependencies { classpath libraries.androidGradlePlugin -//CHUNK_BUILDSCRIPT_DEPENDENCIES_BEGIN -//CHUNK_BUILDSCRIPT_DEPENDENCIES_END + classpath libraries.kotlinGradlePlugin } } @@ -27,24 +21,35 @@ allprojects { mavenCentral() google() jcenter() -//CHUNK_ALLPROJECTS_REPOSITORIES_BEGIN -//CHUNK_ALLPROJECTS_REPOSITORIES_END } } dependencies { implementation libraries.supportCoreUtils + implementation libraries.kotlinStdLib + implementation libraries.v4Support if (rootProject.findProject(":lib")) { implementation project(":lib") + } else if (rootProject.findProject(":godot:lib")) { + implementation project(":godot:lib") } else { // Custom build mode. In this scenario this project is the only one around and the Godot // library is available through the pre-generated godot-lib.*.aar android archive files. debugImplementation fileTree(dir: 'libs/debug', include: ['*.jar', '*.aar']) releaseImplementation fileTree(dir: 'libs/release', include: ['*.jar', '*.aar']) } -//CHUNK_DEPENDENCIES_BEGIN -//CHUNK_DEPENDENCIES_END + + // Godot prebuilt plugins + implementation fileTree(dir: 'libs/plugins', include: ["GodotPayment*.aar"]) + + // Godot user plugins dependencies + String pluginsDir = getGodotPluginsDirectory() + String[] pluginsBinaries = getGodotPluginsBinaries() + if (pluginsDir != null && !pluginsDir.isEmpty() && + pluginsBinaries != null && pluginsBinaries.size() > 0) { + implementation fileTree(dir: pluginsDir, include: pluginsBinaries) + } } android { @@ -56,8 +61,6 @@ android { applicationId getExportPackageName() minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk -//CHUNK_ANDROID_DEFAULTCONFIG_BEGIN -//CHUNK_ANDROID_DEFAULTCONFIG_END } lintOptions { @@ -79,37 +82,13 @@ android { sourceSets { main { manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = [ - 'src' -//DIR_SRC_BEGIN -//DIR_SRC_END - ] - res.srcDirs = [ - 'res' -//DIR_RES_BEGIN -//DIR_RES_END - ] - aidl.srcDirs = [ - 'aidl' -//DIR_AIDL_BEGIN -//DIR_AIDL_END - ] - assets.srcDirs = [ - 'assets' -//DIR_ASSETS_BEGIN -//DIR_ASSETS_END - ] + java.srcDirs = ['src'] + res.srcDirs = ['res'] + aidl.srcDirs = ['aidl'] + assets.srcDirs = ['assets'] } - debug.jniLibs.srcDirs = [ - 'libs/debug' -//DIR_JNI_DEBUG_BEGIN -//DIR_JNI_DEBUG_END - ] - release.jniLibs.srcDirs = [ - 'libs/release' -//DIR_JNI_RELEASE_BEGIN -//DIR_JNI_RELEASE_END - ] + debug.jniLibs.srcDirs = ['libs/debug'] + release.jniLibs.srcDirs = ['libs/release'] } applicationVariants.all { variant -> @@ -118,6 +97,3 @@ android { } } } - -//CHUNK_GLOBAL_BEGIN -//CHUNK_GLOBAL_END diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index 5550d3099d..eaaefcbccb 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -1,24 +1,66 @@ ext.versions = [ - androidGradlePlugin : '3.4.2', - compileSdk : 28, - minSdk : 18, - targetSdk : 28, - buildTools : '28.0.3', - supportCoreUtils : '28.0.0' + androidGradlePlugin: '3.6.0', + compileSdk : 29, + minSdk : 18, + targetSdk : 29, + buildTools : '29.0.1', + supportCoreUtils : '28.0.0', + kotlinVersion : '1.3.61', + v4Support : '28.0.0' ] ext.libraries = [ - androidGradlePlugin : "com.android.tools.build:gradle:$versions.androidGradlePlugin", - supportCoreUtils : "com.android.support:support-core-utils:$versions.supportCoreUtils" + androidGradlePlugin: "com.android.tools.build:gradle:$versions.androidGradlePlugin", + supportCoreUtils : "com.android.support:support-core-utils:$versions.supportCoreUtils", + kotlinGradlePlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlinVersion", + kotlinStdLib : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlinVersion", + v4Support : "com.android.support:support-v4:$versions.v4Support" ] ext.getExportPackageName = { -> - // Retrieve the app id from the project property set by the Godot build command. - String appId = project.hasProperty("export_package_name") ? project.property("export_package_name") : "" - // Check if the app id is valid, otherwise use the default. - if (appId == null || appId.isEmpty()) { - appId = "com.godot.game" - } - return appId + // Retrieve the app id from the project property set by the Godot build command. + String appId = project.hasProperty("export_package_name") ? project.property("export_package_name") : "" + // Check if the app id is valid, otherwise use the default. + if (appId == null || appId.isEmpty()) { + appId = "com.godot.game" + } + return appId +} + +/** + * Parse the project properties for the 'custom_template_plugins' property and return + * their binaries for inclusion in the build dependencies. + * + * The listed plugins must have their binaries in the project plugins directory. + */ +ext.getGodotPluginsBinaries = { -> + String[] binDeps = [] + + // Retrieve the list of enabled plugins. + if (project.hasProperty("custom_template_plugins")) { + String pluginsList = project.property("custom_template_plugins") + if (pluginsList != null && !pluginsList.trim().isEmpty()) { + for (String plugin : pluginsList.split(",")) { + binDeps += plugin.trim() + "*.aar" + } + } + } + + return binDeps +} + +/** + * Parse the project properties for the 'custom_template_plugins_dir' property and return + * its value. + * + * The returned value is the directory containing user plugins. + */ +ext.getGodotPluginsDirectory = { -> + // The plugins directory is provided by the 'custom_template_plugins_dir' property. + String pluginsDir = project.hasProperty("custom_template_plugins_dir") + ? project.property("custom_template_plugins_dir") + : "" + + return pluginsDir } diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index 2052017888..976a5bda99 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -9,6 +9,7 @@ buildscript { } dependencies { classpath libraries.androidGradlePlugin + classpath libraries.kotlinGradlePlugin } } @@ -24,7 +25,7 @@ ext { sconsExt = org.gradle.internal.os.OperatingSystem.current().isWindows() ? ".bat" : "" supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"] - supportedTargets = ['release':"release", 'debug':"release_debug"] + supportedTargets = ['release': "release", 'debug': "release_debug"] // Used by gradle to specify which architecture to build for by default when running `./gradlew build`. // This command is usually used by Android Studio. @@ -64,10 +65,10 @@ task copyReleaseBinaryToBin(type: Copy) { } /** - * Copy the Godot android library archive debug file into the app debug libs directory. + * Copy the Godot android library archive debug file into the app module debug libs directory. * Depends on the library build task to ensure the AAR file is generated prior to copying. */ -task copyDebugAAR(type: Copy) { +task copyDebugAARToAppModule(type: Copy) { dependsOn ':lib:assembleDebug' from('lib/build/outputs/aar') into('app/libs/debug') @@ -75,16 +76,45 @@ task copyDebugAAR(type: Copy) { } /** - * Copy the Godot android library archive release file into the app release libs directory. + * Copy the Godot android library archive debug file into the root bin directory. * Depends on the library build task to ensure the AAR file is generated prior to copying. */ -task copyReleaseAAR(type: Copy) { +task copyDebugAARToBin(type: Copy) { + dependsOn ':lib:assembleDebug' + from('lib/build/outputs/aar') + into(binDir) + include('godot-lib.debug.aar') +} + +/** + * Copy the Godot android library archive release file into the app module release libs directory. + * Depends on the library build task to ensure the AAR file is generated prior to copying. + */ +task copyReleaseAARToAppModule(type: Copy) { dependsOn ':lib:assembleRelease' from('lib/build/outputs/aar') into('app/libs/release') include('godot-lib.release.aar') } +task copyGodotPaymentPluginToAppModule(type: Copy) { + dependsOn ':plugins:godotpayment:assembleRelease' + from('plugins/godotpayment/build/outputs/aar') + into('app/libs/plugins') + include('GodotPayment.release.aar') +} + +/** + * Copy the Godot android library archive release file into the root bin directory. + * Depends on the library build task to ensure the AAR file is generated prior to copying. + */ +task copyReleaseAARToBin(type: Copy) { + dependsOn ':lib:assembleRelease' + from('lib/build/outputs/aar') + into(binDir) + include('godot-lib.release.aar') +} + /** * Generate Godot custom build template by zipping the source files from the app directory, as well * as the AAR files generated by 'copyDebugAAR' and 'copyReleaseAAR'. @@ -95,7 +125,7 @@ task zipCustomBuild(type: Zip) { doFirst { logger.lifecycle("Generating Godot custom build template") } - from(fileTree(dir: 'app', excludes: ['**/build/**', '**/.gradle/**', '**/*.iml']), fileTree(dir: '.', includes: ['gradle.properties','gradlew', 'gradlew.bat', 'gradle/**'])) + from(fileTree(dir: 'app', excludes: ['**/build/**', '**/.gradle/**', '**/*.iml']), fileTree(dir: '.', includes: ['gradle.properties', 'gradlew', 'gradlew.bat', 'gradle/**'])) include '**/*' archiveName 'android_source.zip' destinationDir(file(binDir)) @@ -110,19 +140,24 @@ task generateGodotTemplates(type: GradleBuild) { startParameter.excludedTaskNames += ":lib:" + getSconsTaskName(buildType) } - tasks = [] + tasks = ["copyGodotPaymentPluginToAppModule"] // Only build the apks and aar files for which we have native shared libraries. for (String target : supportedTargets.keySet()) { File targetLibs = new File("lib/libs/" + target) - if (targetLibs != null && targetLibs.isDirectory()) { - File[] targetLibsContents = targetLibs.listFiles() - if (targetLibsContents != null && targetLibsContents.length > 0) { - // Copy the generated aar library files to the custom build directory. - tasks += "copy" + target.capitalize() + "AAR" - // Copy the prebuilt binary templates to the bin directory. - tasks += "copy" + target.capitalize() + "BinaryToBin" - } + if (targetLibs != null + && targetLibs.isDirectory() + && targetLibs.listFiles() != null + && targetLibs.listFiles().length > 0) { + String capitalizedTarget = target.capitalize() + // Copy the generated aar library files to the custom build directory. + tasks += "copy" + capitalizedTarget + "AARToAppModule" + // Copy the generated aar library files to the bin directory. + tasks += "copy" + capitalizedTarget + "AARToBin" + // Copy the prebuilt binary templates to the bin directory. + tasks += "copy" + capitalizedTarget + "BinaryToBin" + } else { + logger.lifecycle("No native shared libs for target $target. Skipping build.") } } @@ -133,20 +168,28 @@ task generateGodotTemplates(type: GradleBuild) { * Clean the generated artifacts. */ task cleanGodotTemplates(type: Delete) { - // Delete the generated native libs - delete("lib/libs") + // Delete the generated native libs + delete("lib/libs") + + // Delete the library generated AAR files + delete("lib/build/outputs/aar") + + // Delete the godotpayment libs directory contents + delete("plugins/godotpayment/libs") - // Delete the library generated AAR files - delete("lib/build/outputs/aar") + // Delete the generated godotpayment aar + delete("plugins/godotpayment/build/outputs/aar") - // Delete the app libs directory contents - delete("app/libs") + // Delete the app libs directory contents + delete("app/libs") - // Delete the generated binary apks - delete("app/build/outputs/apk") + // Delete the generated binary apks + delete("app/build/outputs/apk") - // Delete the Godot templates in the Godot bin directory - delete("$binDir/android_debug.apk") - delete("$binDir/android_release.apk") - delete("$binDir/android_source.zip") + // Delete the Godot templates in the Godot bin directory + delete("$binDir/android_debug.apk") + delete("$binDir/android_release.apk") + delete("$binDir/android_source.zip") + delete("$binDir/godot-lib.debug.aar") + delete("$binDir/godot-lib.release.aar") } diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.properties b/platform/android/java/gradle/wrapper/gradle-wrapper.properties index bf50265715..f56b0f6a5e 100644 --- a/platform/android/java/gradle/wrapper/gradle-wrapper.properties +++ b/platform/android/java/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index eb97484b9c..ca8aaf8af0 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -2,6 +2,8 @@ apply plugin: 'com.android.library' dependencies { implementation libraries.supportCoreUtils + implementation libraries.kotlinStdLib + implementation libraries.v4Support } def pathToRootDir = "../../../../" 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 021214b627..7db4aa6597 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -61,8 +61,11 @@ import android.os.Messenger; import android.os.VibrationEffect; import android.os.Vibrator; import android.provider.Settings.Secure; +import android.support.annotation.CallSuper; import android.support.annotation.Keep; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.app.FragmentActivity; import android.view.Display; import android.view.KeyEvent; import android.view.MotionEvent; @@ -87,22 +90,20 @@ import com.google.android.vending.expansion.downloader.IStub; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; -import java.lang.reflect.Method; import java.security.MessageDigest; -import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Locale; -import javax.microedition.khronos.opengles.GL10; import org.godotengine.godot.input.GodotEditText; import org.godotengine.godot.payments.PaymentsManager; +import org.godotengine.godot.plugin.GodotPlugin; +import org.godotengine.godot.plugin.GodotPluginRegistry; import org.godotengine.godot.utils.GodotNetUtils; import org.godotengine.godot.utils.PermissionsUtil; import org.godotengine.godot.xr.XRMode; -public abstract class Godot extends Activity implements SensorEventListener, IDownloaderClient { +public abstract class Godot extends FragmentActivity implements SensorEventListener, IDownloaderClient { - static final int MAX_SINGLETONS = 64; private IStub mDownloaderClientStub; private TextView mStatusText; private TextView mProgressFraction; @@ -126,8 +127,7 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo private boolean activityResumed; private int mState; - // Used to dispatch events to the main thread. - private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + private GodotPluginRegistry pluginRegistry; static private Intent mCurrentIntent; @@ -154,83 +154,6 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo mPauseButton.setText(stringResourceID); } - static public class SingletonBase { - - protected void registerClass(String p_name, String[] p_methods) { - - GodotLib.singleton(p_name, this); - - Class clazz = getClass(); - Method[] methods = clazz.getDeclaredMethods(); - for (Method method : methods) { - boolean found = false; - - for (String s : p_methods) { - if (s.equals(method.getName())) { - found = true; - break; - } - } - if (!found) - continue; - - List<String> ptr = new ArrayList<String>(); - - Class[] paramTypes = method.getParameterTypes(); - for (Class c : paramTypes) { - ptr.add(c.getName()); - } - - String[] pt = new String[ptr.size()]; - ptr.toArray(pt); - - GodotLib.method(p_name, method.getName(), method.getReturnType().getName(), pt); - } - - Godot.singletons[Godot.singleton_count++] = this; - } - - /** - * Invoked once during the Godot Android initialization process after creation of the - * {@link GodotView} view. - * <p> - * This method should be overridden by descendants of this class that would like to add - * their view/layout to the Godot view hierarchy. - * - * @return the view to be included; null if no views should be included. - */ - @Nullable - protected View onMainCreateView(Activity activity) { - return null; - } - - protected void onMainActivityResult(int requestCode, int resultCode, Intent data) { - } - - protected void onMainRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - } - - protected void onMainPause() {} - protected void onMainResume() {} - protected void onMainDestroy() {} - protected boolean onMainBackPressed() { return false; } - - protected void onGLDrawFrame(GL10 gl) {} - protected void onGLSurfaceChanged(GL10 gl, int width, int height) {} // singletons will always miss first onGLSurfaceChanged call - //protected void onGLSurfaceCreated(GL10 gl, EGLConfig config) {} // singletons won't be ready until first GodotLib.step() - - public void registerMethods() {} - } - - /* - protected List<SingletonBase> singletons = new ArrayList<SingletonBase>(); - protected void instanceSingleton(SingletonBase s) { - - s.registerMethods(); - singletons.add(s); - } - */ - private String[] command_line; private boolean use_apk_expansion; @@ -246,9 +169,6 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo public static GodotIO io; public static GodotNetUtils netUtils; - static SingletonBase[] singletons = new SingletonBase[MAX_SINGLETONS]; - static int singleton_count = 0; - public interface ResultCallback { public void callback(int requestCode, int resultCode, Intent data); } @@ -265,16 +185,15 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo result_callback = null; }; - for (int i = 0; i < singleton_count; i++) { - - singletons[i].onMainActivityResult(requestCode, resultCode, data); + for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { + plugin.onMainActivityResult(requestCode, resultCode, data); } }; @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - for (int i = 0; i < singleton_count; i++) { - singletons[i].onMainRequestPermissionsResult(requestCode, permissions, grantResults); + for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { + plugin.onMainRequestPermissionsResult(requestCode, permissions, grantResults); } for (int i = 0; i < permissions.length; i++) { @@ -283,6 +202,16 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo }; /** + * Invoked on the GL thread when the Godot main loop has started. + */ + @CallSuper + protected void onGLGodotMainLoopStarted() { + for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { + plugin.onGLGodotMainLoopStarted(); + } + } + + /** * Used by the native code (java_godot_lib_jni.cpp) to complete initialization of the GLSurfaceView view and renderer. */ @Keep @@ -302,14 +231,13 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo edittext.setView(mView); io.setEdit(edittext); - final Godot godot = this; mView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { Point fullSize = new Point(); - godot.getWindowManager().getDefaultDisplay().getSize(fullSize); + getWindowManager().getDefaultDisplay().getSize(fullSize); Rect gameSize = new Rect(); - godot.mView.getWindowVisibleDisplayFrame(gameSize); + mView.getWindowVisibleDisplayFrame(gameSize); final int keyboardHeight = fullSize.y - gameSize.bottom; GodotLib.setVirtualKeyboardHeight(keyboardHeight); @@ -321,23 +249,23 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo @Override public void run() { GodotLib.setup(current_command_line); - setKeepScreenOn("True".equals(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on"))); - // The Godot Android plugins are setup on completion of GodotLib.setup - mainThreadHandler.post(new Runnable() { - @Override - public void run() { - // Include the non-null views returned in the Godot view hierarchy. - for (int i = 0; i < singleton_count; i++) { - View view = singletons[i].onMainCreateView(Godot.this); - if (view != null) { - layout.addView(view); - } - } - } - }); + // Must occur after GodotLib.setup has completed. + for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { + plugin.onGLRegisterPluginWithGodotNative(); + } + + setKeepScreenOn("True".equals(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on"))); } }); + + // Include the returned non-null views in the Godot view hierarchy. + for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { + View pluginView = plugin.onMainCreateView(this); + if (pluginView != null) { + layout.addView(pluginView); + } + } } public void setKeepScreenOn(final boolean p_enabled) { @@ -535,6 +463,7 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo Window window = getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); mClipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); + pluginRegistry = GodotPluginRegistry.initializePluginRegistry(this); //check for apk expansion API if (true) { @@ -675,8 +604,8 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo protected void onDestroy() { if (mPaymentsManager != null) mPaymentsManager.destroy(); - for (int i = 0; i < singleton_count; i++) { - singletons[i].onMainDestroy(); + for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { + plugin.onMainDestroy(); } GodotLib.ondestroy(this); @@ -703,8 +632,8 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo mSensorManager.unregisterListener(this); - for (int i = 0; i < singleton_count; i++) { - singletons[i].onMainPause(); + for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { + plugin.onMainPause(); } } @@ -755,9 +684,8 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); } - for (int i = 0; i < singleton_count; i++) { - - singletons[i].onMainResume(); + for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { + plugin.onMainResume(); } } @@ -850,8 +778,8 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo public void onBackPressed() { boolean shouldQuit = true; - for (int i = 0; i < singleton_count; i++) { - if (singletons[i].onMainBackPressed()) { + for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { + if (plugin.onMainBackPressed()) { shouldQuit = false; } } @@ -866,6 +794,17 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo } } + /** + * Queue a runnable to be run on the GL thread. + * <p> + * This must be called after the GL thread has started. + */ + public final void runOnGLThread(@NonNull Runnable action) { + if (mView != null) { + mView.queueEvent(action); + } + } + private void forceQuit() { System.exit(0); } @@ -990,8 +929,8 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo int keyCode; if ((keyCode = cc[i]) != 0) { // Simulate key down and up... - GodotLib.key(0, keyCode, true); - GodotLib.key(0, keyCode, false); + GodotLib.key(0, 0, keyCode, true); + GodotLib.key(0, 0, keyCode, false); } } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index e0b46673ba..89a65aea24 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -136,7 +136,7 @@ public class GodotLib { /** * Forward regular key events from the main thread to the GL thread. */ - public static native void key(int p_scancode, int p_unicode_char, boolean p_pressed); + public static native void key(int p_keycode, int p_scancode, int p_unicode_char, boolean p_pressed); /** * Forward game device's key events from the main thread to the GL thread. @@ -176,22 +176,6 @@ public class GodotLib { public static native void audio(); /** - * Used to setup a {@link org.godotengine.godot.Godot.SingletonBase} instance. - * @param p_name Name of the instance. - * @param p_object Reference to the singleton instance. - */ - public static native void singleton(String p_name, Object p_object); - - /** - * Used to complete registration of the {@link org.godotengine.godot.Godot.SingletonBase} instance's methods. - * @param p_sname Name of the instance - * @param p_name Name of the method to register - * @param p_ret Return type of the registered method - * @param p_params Method parameters types - */ - public static native void method(String p_sname, String p_name, String p_ret, String[] p_params); - - /** * Used to access Godot global properties. * @param p_key Property key * @return String value of the property diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java index 26fa033f12..ee9a2aee4f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java @@ -30,9 +30,12 @@ package org.godotengine.godot; +import android.content.Context; import android.opengl.GLSurfaceView; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; +import org.godotengine.godot.plugin.GodotPlugin; +import org.godotengine.godot.plugin.GodotPluginRegistry; import org.godotengine.godot.utils.GLUtils; /** @@ -40,8 +43,13 @@ import org.godotengine.godot.utils.GLUtils; */ class GodotRenderer implements GLSurfaceView.Renderer { + private final GodotPluginRegistry pluginRegistry; private boolean activityJustResumed = false; + GodotRenderer() { + this.pluginRegistry = GodotPluginRegistry.getPluginRegistry(); + } + public void onDrawFrame(GL10 gl) { if (activityJustResumed) { GodotLib.onRendererResumed(); @@ -49,21 +57,23 @@ class GodotRenderer implements GLSurfaceView.Renderer { } GodotLib.step(); - for (int i = 0; i < Godot.singleton_count; i++) { - Godot.singletons[i].onGLDrawFrame(gl); + for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { + plugin.onGLDrawFrame(gl); } } public void onSurfaceChanged(GL10 gl, int width, int height) { - GodotLib.resize(width, height); - for (int i = 0; i < Godot.singleton_count; i++) { - Godot.singletons[i].onGLSurfaceChanged(gl, width, height); + for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { + plugin.onGLSurfaceChanged(gl, width, height); } } public void onSurfaceCreated(GL10 gl, EGLConfig config) { GodotLib.newcontext(GLUtils.use_32); + for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { + plugin.onGLSurfaceCreated(gl, config); + } } void onActivityResumed() { diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java index b2b88088e8..e00ca86c41 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -98,11 +98,12 @@ public class GodotInputHandler implements InputDeviceListener { }); } } else { + final int scanCode = event.getScanCode(); final int chr = event.getUnicodeChar(0); queueEvent(new Runnable() { @Override public void run() { - GodotLib.key(keyCode, chr, false); + GodotLib.key(keyCode, scanCode, chr, false); } }); }; @@ -143,11 +144,12 @@ public class GodotInputHandler implements InputDeviceListener { }); } } else { + final int scanCode = event.getScanCode(); final int chr = event.getUnicodeChar(0); queueEvent(new Runnable() { @Override public void run() { - GodotLib.key(keyCode, chr, true); + GodotLib.key(keyCode, scanCode, chr, true); } }); }; diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java index 3a154f1bf3..8d9b5461a1 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java @@ -91,8 +91,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene @Override public void run() { for (int i = 0; i < count; ++i) { - GodotLib.key(KeyEvent.KEYCODE_DEL, 0, true); - GodotLib.key(KeyEvent.KEYCODE_DEL, 0, false); + GodotLib.key(KeyEvent.KEYCODE_DEL, KeyEvent.KEYCODE_DEL, 0, true); + GodotLib.key(KeyEvent.KEYCODE_DEL, KeyEvent.KEYCODE_DEL, 0, false); } } }); @@ -110,8 +110,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene @Override public void run() { for (int i = 0; i < count; ++i) { - GodotLib.key(0, newChars[i], true); - GodotLib.key(0, newChars[i], false); + GodotLib.key(0, 0, newChars[i], true); + GodotLib.key(0, 0, newChars[i], false); } } }); @@ -127,8 +127,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene public void run() { for (int i = 0; i < characters.length(); i++) { final int ch = characters.codePointAt(i); - GodotLib.key(0, ch, true); - GodotLib.key(0, ch, false); + GodotLib.key(0, 0, ch, true); + GodotLib.key(0, 0, ch, false); } } }); diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/GodotPaymentInterface.java b/platform/android/java/lib/src/org/godotengine/godot/payments/GodotPaymentInterface.java new file mode 100644 index 0000000000..6ac7338b30 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/GodotPaymentInterface.java @@ -0,0 +1,97 @@ +/*************************************************************************/ +/* GodotPaymentInterface.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2019 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. */ +/*************************************************************************/ + +package org.godotengine.godot.payments; + +public interface GodotPaymentInterface { + void purchase(String sku, String transactionId); + + void consumeUnconsumedPurchases(); + + String getSignature(); + + void callbackSuccess(String ticket, String signature, String sku); + + void callbackSuccessProductMassConsumed(String ticket, String signature, String sku); + + void callbackSuccessNoUnconsumedPurchases(); + + void callbackFailConsume(String message); + + void callbackFail(String message); + + void callbackCancel(); + + void callbackAlreadyOwned(String sku); + + int getPurchaseCallbackId(); + + void setPurchaseCallbackId(int purchaseCallbackId); + + String getPurchaseValidationUrlPrefix(); + + void setPurchaseValidationUrlPrefix(String url); + + String getAccessToken(); + + void setAccessToken(String accessToken); + + void setTransactionId(String transactionId); + + String getTransactionId(); + + // request purchased items are not consumed + void requestPurchased(); + + // callback for requestPurchased() + void callbackPurchased(String receipt, String signature, String sku); + + void callbackDisconnected(); + + void callbackConnected(); + + // true if connected, false otherwise + boolean isConnected(); + + // consume item automatically after purchase. default is true. + void setAutoConsume(boolean autoConsume); + + // consume a specific item + void consume(String sku); + + // query in app item detail info + void querySkuDetails(String[] list); + + void addSkuDetail(String itemJson); + + void completeSkuDetail(); + + void errorSkuDetail(String errorMessage); +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java b/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java index 90b958266b..9bf6650f84 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java @@ -43,7 +43,6 @@ import android.util.Log; import com.android.vending.billing.IInAppBillingService; import java.util.ArrayList; import java.util.Arrays; -import org.godotengine.godot.GodotPaymentV3; import org.json.JSONException; import org.json.JSONObject; @@ -90,9 +89,9 @@ public class PaymentsManager { public void onServiceDisconnected(ComponentName name) { mService = null; - // At this stage, godotPaymentV3 might not have been initialized yet. - if (godotPaymentV3 != null) { - godotPaymentV3.callbackDisconnected(); + // At this stage, godotPayment might not have been initialized yet. + if (godotPayment != null) { + godotPayment.callbackDisconnected(); } } @@ -100,9 +99,9 @@ public class PaymentsManager { public void onServiceConnected(ComponentName name, IBinder service) { mService = IInAppBillingService.Stub.asInterface(service); - // At this stage, godotPaymentV3 might not have been initialized yet. - if (godotPaymentV3 != null) { - godotPaymentV3.callbackConnected(); + // At this stage, godotPayment might not have been initialized yet. + if (godotPayment != null) { + godotPayment.callbackConnected(); } } }; @@ -111,17 +110,17 @@ public class PaymentsManager { new PurchaseTask(mService, activity) { @Override protected void error(String message) { - godotPaymentV3.callbackFail(message); + godotPayment.callbackFail(message); } @Override protected void canceled() { - godotPaymentV3.callbackCancel(); + godotPayment.callbackCancel(); } @Override protected void alreadyOwned() { - godotPaymentV3.callbackAlreadyOwned(sku); + godotPayment.callbackAlreadyOwned(sku); } } .purchase(sku, transactionId); @@ -135,19 +134,19 @@ public class PaymentsManager { new ReleaseAllConsumablesTask(mService, activity) { @Override protected void success(String sku, String receipt, String signature, String token) { - godotPaymentV3.callbackSuccessProductMassConsumed(receipt, signature, sku); + godotPayment.callbackSuccessProductMassConsumed(receipt, signature, sku); } @Override protected void error(String message) { Log.d("godot", "consumeUnconsumedPurchases :" + message); - godotPaymentV3.callbackFailConsume(message); + godotPayment.callbackFailConsume(message); } @Override protected void notRequired() { Log.d("godot", "callbackSuccessNoUnconsumedPurchases :"); - godotPaymentV3.callbackSuccessNoUnconsumedPurchases(); + godotPayment.callbackSuccessNoUnconsumedPurchases(); } } .consumeItAll(); @@ -168,7 +167,7 @@ public class PaymentsManager { final ArrayList<String> mySignatures = bundle.getStringArrayList("INAPP_DATA_SIGNATURE_LIST"); if (myPurchases == null || myPurchases.size() == 0) { - godotPaymentV3.callbackPurchased("", "", ""); + godotPayment.callbackPurchased("", "", ""); return; } @@ -186,7 +185,7 @@ public class PaymentsManager { pc.setConsumableFlag("block", sku, true); pc.setConsumableValue("token", sku, token); - godotPaymentV3.callbackPurchased(receipt, signature, sku); + godotPayment.callbackPurchased(receipt, signature, sku); } catch (JSONException e) { } } @@ -203,7 +202,7 @@ public class PaymentsManager { new HandlePurchaseTask(activity) { @Override protected void success(final String sku, final String signature, final String ticket) { - godotPaymentV3.callbackSuccess(ticket, signature, sku); + godotPayment.callbackSuccess(ticket, signature, sku); if (auto_consume) { new ConsumeTask(mService, activity) { @@ -213,7 +212,7 @@ public class PaymentsManager { @Override protected void error(String message) { - godotPaymentV3.callbackFail(message); + godotPayment.callbackFail(message); } } .consume(sku); @@ -222,12 +221,12 @@ public class PaymentsManager { @Override protected void error(String message) { - godotPaymentV3.callbackFail(message); + godotPayment.callbackFail(message); } @Override protected void canceled() { - godotPaymentV3.callbackCancel(); + godotPayment.callbackCancel(); } } .handlePurchaseRequest(resultCode, data); @@ -235,19 +234,19 @@ public class PaymentsManager { public void validatePurchase(String purchaseToken, final String sku) { - new ValidateTask(activity, godotPaymentV3) { + new ValidateTask(activity, godotPayment) { @Override protected void success() { new ConsumeTask(mService, activity) { @Override protected void success(String ticket) { - godotPaymentV3.callbackSuccess(ticket, null, sku); + godotPayment.callbackSuccess(ticket, null, sku); } @Override protected void error(String message) { - godotPaymentV3.callbackFail(message); + godotPayment.callbackFail(message); } } .consume(sku); @@ -255,12 +254,12 @@ public class PaymentsManager { @Override protected void error(String message) { - godotPaymentV3.callbackFail(message); + godotPayment.callbackFail(message); } @Override protected void canceled() { - godotPaymentV3.callbackCancel(); + godotPayment.callbackCancel(); } } .validatePurchase(sku); @@ -274,12 +273,12 @@ public class PaymentsManager { new ConsumeTask(mService, activity) { @Override protected void success(String ticket) { - godotPaymentV3.callbackSuccessProductMassConsumed(ticket, "", sku); + godotPayment.callbackSuccessProductMassConsumed(ticket, "", sku); } @Override protected void error(String message) { - godotPaymentV3.callbackFailConsume(message); + godotPayment.callbackFailConsume(message); } } .consume(sku); @@ -387,9 +386,9 @@ public class PaymentsManager { if (!skuDetails.containsKey("DETAILS_LIST")) { int response = getResponseCodeFromBundle(skuDetails); if (response != BILLING_RESPONSE_RESULT_OK) { - godotPaymentV3.errorSkuDetail(getResponseDesc(response)); + godotPayment.errorSkuDetail(getResponseDesc(response)); } else { - godotPaymentV3.errorSkuDetail("No error but no detail list."); + godotPayment.errorSkuDetail("No error but no detail list."); } return; } @@ -398,22 +397,22 @@ public class PaymentsManager { for (String thisResponse : responseList) { Log.d("godot", "response = " + thisResponse); - godotPaymentV3.addSkuDetail(thisResponse); + godotPayment.addSkuDetail(thisResponse); } } catch (RemoteException e) { e.printStackTrace(); - godotPaymentV3.errorSkuDetail("RemoteException error!"); + godotPayment.errorSkuDetail("RemoteException error!"); } } - godotPaymentV3.completeSkuDetail(); + godotPayment.completeSkuDetail(); } })) .start(); } - private GodotPaymentV3 godotPaymentV3; + private GodotPaymentInterface godotPayment; - public void setBaseSingleton(GodotPaymentV3 godotPaymentV3) { - this.godotPaymentV3 = godotPaymentV3; + public void setBaseSingleton(GodotPaymentInterface godotPaymentInterface) { + this.godotPayment = godotPaymentInterface; } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java index dbb6b8a783..10c314aecf 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java @@ -34,7 +34,6 @@ import android.app.Activity; import android.app.ProgressDialog; import android.os.AsyncTask; import java.lang.ref.WeakReference; -import org.godotengine.godot.GodotPaymentV3; import org.godotengine.godot.utils.HttpRequester; import org.godotengine.godot.utils.RequestParams; import org.json.JSONException; @@ -43,7 +42,7 @@ import org.json.JSONObject; abstract public class ValidateTask { private Activity context; - private GodotPaymentV3 godotPaymentsV3; + private GodotPaymentInterface godotPayments; private ProgressDialog dialog; private String mSku; @@ -80,9 +79,9 @@ abstract public class ValidateTask { } } - public ValidateTask(Activity context, GodotPaymentV3 godotPaymentsV3) { + public ValidateTask(Activity context, GodotPaymentInterface godotPayments) { this.context = context; - this.godotPaymentsV3 = godotPaymentsV3; + this.godotPayments = godotPayments; } public void validatePurchase(final String sku) { @@ -96,7 +95,7 @@ abstract public class ValidateTask { private String doInBackground(String... params) { PaymentsCache pc = new PaymentsCache(context); - String url = godotPaymentsV3.getPurchaseValidationUrlPrefix(); + String url = godotPayments.getPurchaseValidationUrlPrefix(); RequestParams param = new RequestParams(); param.setUrl(url); param.put("ticket", pc.getConsumableValue("ticket", mSku)); 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 new file mode 100644 index 0000000000..d5bf4fc70e --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java @@ -0,0 +1,256 @@ +/*************************************************************************/ +/* GodotPlugin.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2019 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. */ +/*************************************************************************/ + +package org.godotengine.godot.plugin; + +import android.app.Activity; +import android.content.Intent; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; +import org.godotengine.godot.Godot; + +/** + * Base class for the Godot Android plugins. + * <p> + * A Godot Android plugin is a regular Android library packaged as an aar archive file with the following caveats: + * <p> + * - The library must have a dependency on the Godot Android library (godot-lib.aar). + * A stable version is available for each release. + * <p> + * - The library must include a <meta-data> tag in its manifest file setup as follow: + * <meta-data android:name="org.godotengine.plugin.v1.[PluginName]" android:value="[plugin.init.ClassFullName]" /> + * Where: + * - 'PluginName' is the name of the plugin. + * - 'plugin.init.ClassFullName' is the full name (package + class name) of the plugin class + * extending {@link GodotPlugin}. + * + * A plugin can also define and provide c/c++ gdnative libraries and nativescripts for the target + * app/game to leverage. + * The shared library for the gdnative library will be automatically bundled by the aar build + * system. + * Godot '*.gdnlib' and '*.gdns' resource files must however be manually defined in the project + * 'assets' directory. The recommended path for these resources in the 'assets' directory should be: + * 'godot/plugin/v1/[PluginName]/' + */ +public abstract class GodotPlugin { + + private final Godot godot; + + public GodotPlugin(Godot godot) { + this.godot = godot; + } + + /** + * Provides access to the Godot engine. + */ + protected Godot getGodot() { + return godot; + } + + /** + * Register the plugin with Godot native code. + */ + public final void onGLRegisterPluginWithGodotNative() { + nativeRegisterSingleton(getPluginName()); + + Class clazz = getClass(); + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + boolean found = false; + + for (String s : getPluginMethods()) { + if (s.equals(method.getName())) { + found = true; + break; + } + } + if (!found) + continue; + + List<String> ptr = new ArrayList<String>(); + + Class[] paramTypes = method.getParameterTypes(); + for (Class c : paramTypes) { + ptr.add(c.getName()); + } + + String[] pt = new String[ptr.size()]; + ptr.toArray(pt); + + nativeRegisterMethod(getPluginName(), method.getName(), method.getReturnType().getName(), pt); + } + + // Get the list of gdnative libraries to register. + Set<String> gdnativeLibrariesPaths = getPluginGDNativeLibrariesPaths(); + if (!gdnativeLibrariesPaths.isEmpty()) { + nativeRegisterGDNativeLibraries(gdnativeLibrariesPaths.toArray(new String[0])); + } + } + + /** + * Invoked once during the Godot Android initialization process after creation of the + * {@link org.godotengine.godot.GodotView} view. + * <p> + * This method should be overridden by descendants of this class that would like to add + * their view/layout to the Godot view hierarchy. + * + * @return the view to be included; null if no views should be included. + */ + @Nullable + public View onMainCreateView(Activity activity) { + return null; + } + + /** + * @see Activity#onActivityResult(int, int, Intent) + */ + public void onMainActivityResult(int requestCode, int resultCode, Intent data) { + } + + /** + * @see Activity#onRequestPermissionsResult(int, String[], int[]) + */ + public void onMainRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + } + + /** + * @see Activity#onPause() + */ + public void onMainPause() {} + + /** + * @see Activity#onResume() + */ + public void onMainResume() {} + + /** + * @see Activity#onDestroy() + */ + public void onMainDestroy() {} + + /** + * @see Activity#onBackPressed() + */ + public boolean onMainBackPressed() { return false; } + + /** + * Invoked on the GL thread when the Godot main loop has started. + */ + public void onGLGodotMainLoopStarted() {} + + /** + * Invoked once per frame on the GL thread after the frame is drawn. + */ + public void onGLDrawFrame(GL10 gl) {} + + /** + * Called on the GL thread after the surface is created and whenever the OpenGL ES surface size + * changes. + */ + public void onGLSurfaceChanged(GL10 gl, int width, int height) {} + + /** + * Called on the GL thread when the surface is created or recreated. + */ + public void onGLSurfaceCreated(GL10 gl, EGLConfig config) {} + + /** + * Returns the name of the plugin. + * <p> + * This value must match the one listed in the plugin '<meta-data>' manifest entry. + */ + @NonNull + public abstract String getPluginName(); + + /** + * Returns the list of methods to be exposed to Godot. + */ + @NonNull + public abstract List<String> getPluginMethods(); + + /** + * Returns the paths for the plugin's gdnative libraries. + * + * The paths must be relative to the 'assets' directory and point to a '*.gdnlib' file. + */ + @NonNull + protected Set<String> getPluginGDNativeLibrariesPaths() { + return Collections.emptySet(); + } + + /** + * 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. + * + * @param action the action to run on the UI thread + */ + protected void runOnUiThread(Runnable action) { + godot.runOnUiThread(action); + } + + /** + * Queue the specified action to be run on the GL thread. + * + * @param action the action to run on the GL thread + */ + protected void runOnGLThread(Runnable action) { + godot.runOnGLThread(action); + } + + /** + * Used to setup a {@link GodotPlugin} instance. + * @param p_name Name of the instance. + */ + private native void nativeRegisterSingleton(String p_name); + + /** + * Used to complete registration of the {@link GodotPlugin} instance's methods. + * @param p_sname Name of the instance + * @param p_name Name of the method to register + * @param p_ret Return type of the registered method + * @param p_params Method parameters types + */ + private native void nativeRegisterMethod(String p_sname, String p_name, String p_ret, String[] p_params); + + /** + * Used to register gdnative libraries bundled by the plugin. + * @param gdnlibPaths Paths to the libraries relative to the 'assets' directory. + */ + private native void nativeRegisterGDNativeLibraries(String[] gdnlibPaths); +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java new file mode 100644 index 0000000000..b6d949b7bf --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java @@ -0,0 +1,199 @@ +/*************************************************************************/ +/* GodotPluginRegistry.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2019 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. */ +/*************************************************************************/ + +package org.godotengine.godot.plugin; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.godotengine.godot.Godot; + +/** + * Registry used to load and access the registered Godot Android plugins. + */ +public final class GodotPluginRegistry { + + private static final String TAG = GodotPluginRegistry.class.getSimpleName(); + + private static final String GODOT_PLUGIN_V1_NAME_PREFIX = "org.godotengine.plugin.v1."; + + /** + * Name for the metadata containing the list of Godot plugins to enable. + */ + private static final String GODOT_ENABLED_PLUGINS_LABEL = "custom_template_plugins"; + + private static GodotPluginRegistry instance; + private final ConcurrentHashMap<String, GodotPlugin> registry; + + private GodotPluginRegistry(Godot godot) { + registry = new ConcurrentHashMap<>(); + loadPlugins(godot); + } + + /** + * Retrieve the plugin tied to the given plugin name. + * @param pluginName Name of the plugin + * @return {@link GodotPlugin} handle if it exists, null otherwise. + */ + @Nullable + public GodotPlugin getPlugin(String pluginName) { + return registry.get(pluginName); + } + + /** + * Retrieve the full set of loaded plugins. + */ + public Collection<GodotPlugin> getAllPlugins() { + return registry.values(); + } + + /** + * Parse the manifest file and load all included Godot Android plugins. + * <p> + * A plugin manifest entry is a '<meta-data>' tag setup as described in the {@link GodotPlugin} + * documentation. + * + * @param godot Godot instance + * @return A singleton instance of {@link GodotPluginRegistry}. This ensures that only one instance + * of each Godot Android plugins is available at runtime. + */ + public static GodotPluginRegistry initializePluginRegistry(Godot godot) { + if (instance == null) { + instance = new GodotPluginRegistry(godot); + } + + return instance; + } + + /** + * Return the plugin registry if it's initialized. + * Throws a {@link IllegalStateException} exception if not. + * + * @throws IllegalStateException if {@link GodotPluginRegistry#initializePluginRegistry(Godot)} has not been called prior to calling this method. + */ + public static GodotPluginRegistry getPluginRegistry() throws IllegalStateException { + if (instance == null) { + throw new IllegalStateException("Plugin registry hasn't been initialized."); + } + + return instance; + } + + private void loadPlugins(Godot godot) { + try { + ApplicationInfo appInfo = godot + .getPackageManager() + .getApplicationInfo(godot.getPackageName(), PackageManager.GET_META_DATA); + Bundle metaData = appInfo.metaData; + if (metaData == null || metaData.isEmpty()) { + return; + } + + // When using the Godot editor for building and exporting the apk, this is used to check + // which plugins to enable since the custom build template may contain prebuilt plugins. + // When using a custom process to generate the apk, the metadata is not needed since + // it's assumed that the developer is aware of the dependencies included in the apk. + final Set<String> enabledPluginsSet; + if (metaData.containsKey(GODOT_ENABLED_PLUGINS_LABEL)) { + String enabledPlugins = metaData.getString(GODOT_ENABLED_PLUGINS_LABEL, ""); + String[] enabledPluginsList = enabledPlugins.split(","); + if (enabledPluginsList.length == 0) { + // No plugins to enable. Aborting early. + return; + } + + enabledPluginsSet = new HashSet<>(); + for (String enabledPlugin : enabledPluginsList) { + enabledPluginsSet.add(enabledPlugin.trim()); + } + } else { + enabledPluginsSet = null; + } + + int godotPluginV1NamePrefixLength = GODOT_PLUGIN_V1_NAME_PREFIX.length(); + for (String metaDataName : metaData.keySet()) { + // Parse the meta-data looking for entry with the Godot plugin name prefix. + if (metaDataName.startsWith(GODOT_PLUGIN_V1_NAME_PREFIX)) { + String pluginName = metaDataName.substring(godotPluginV1NamePrefixLength).trim(); + if (enabledPluginsSet != null && !enabledPluginsSet.contains(pluginName)) { + Log.w(TAG, "Plugin " + pluginName + " is listed in the dependencies but is not enabled."); + continue; + } + + // Retrieve the plugin class full name. + String pluginHandleClassFullName = metaData.getString(metaDataName); + if (!TextUtils.isEmpty(pluginHandleClassFullName)) { + try { + // Attempt to create the plugin init class via reflection. + @SuppressWarnings("unchecked") + Class<GodotPlugin> pluginClass = (Class<GodotPlugin>)Class + .forName(pluginHandleClassFullName); + Constructor<GodotPlugin> pluginConstructor = pluginClass + .getConstructor(Godot.class); + GodotPlugin pluginHandle = pluginConstructor.newInstance(godot); + + // Load the plugin initializer into the registry using the plugin name + // as key. + if (!pluginName.equals(pluginHandle.getPluginName())) { + Log.w(TAG, + "Meta-data plugin name does not match the value returned by the plugin handle: " + pluginName + " =/= " + pluginHandle.getPluginName()); + } + registry.put(pluginName, pluginHandle); + } catch (ClassNotFoundException e) { + Log.w(TAG, "Unable to load Godot plugin " + pluginName, e); + } catch (IllegalAccessException e) { + Log.w(TAG, "Unable to load Godot plugin " + pluginName, e); + } catch (InstantiationException e) { + Log.w(TAG, "Unable to load Godot plugin " + pluginName, e); + } catch (NoSuchMethodException e) { + Log.w(TAG, "Unable to load Godot plugin " + pluginName, e); + } catch (InvocationTargetException e) { + Log.w(TAG, "Unable to load Godot plugin " + pluginName, e); + } + } else { + Log.w(TAG, "Invalid plugin loader class for " + pluginName); + } + } + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Unable load Godot Android plugins from the manifest file.", e); + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt new file mode 100644 index 0000000000..67faad8ddd --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt @@ -0,0 +1,99 @@ +/*************************************************************************/ +/* VkRenderer.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +@file:JvmName("VkRenderer") +package org.godotengine.godot.vulkan + +import android.view.Surface + +/** + * Responsible to setting up and driving the Vulkan rendering logic. + * + * <h3>Threading</h3> + * The renderer will be called on a separate thread, so that rendering + * performance is decoupled from the UI thread. Clients typically need to + * communicate with the renderer from the UI thread, because that's where + * input events are received. Clients can communicate using any of the + * standard Java techniques for cross-thread communication, or they can + * use the [VkSurfaceView.queueOnVkThread] convenience method. + * + * @see [VkSurfaceView.startRenderer] + */ +internal class VkRenderer { + + /** + * Called when the surface is created and signals the beginning of rendering. + */ + fun onVkSurfaceCreated(surface: Surface) { + nativeOnVkSurfaceCreated(surface) + } + + /** + * Called after the surface is created and whenever its size changes. + */ + fun onVkSurfaceChanged(surface: Surface, width: Int, height: Int) { + nativeOnVkSurfaceChanged(surface, width, height) + } + + /** + * Called to draw the current frame. + */ + fun onVkDrawFrame() { + nativeOnVkDrawFrame() + } + + /** + * Called when the rendering thread is resumed. + */ + fun onVkResume() { + nativeOnVkResume() + } + + /** + * Called when the rendering thread is paused. + */ + fun onVkPause() { + nativeOnVkPause() + } + + /** + * Called when the rendering thread is destroyed and used as signal to tear down the Vulkan logic. + */ + fun onVkDestroy() { + nativeOnVkDestroy() + } + + private external fun nativeOnVkSurfaceCreated(surface: Surface) + private external fun nativeOnVkSurfaceChanged(surface: Surface, width: Int, height: Int) + private external fun nativeOnVkResume() + private external fun nativeOnVkDrawFrame() + private external fun nativeOnVkPause() + private external fun nativeOnVkDestroy() +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt new file mode 100644 index 0000000000..1c594f3201 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt @@ -0,0 +1,136 @@ +/*************************************************************************/ +/* VkSurfaceView.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +@file:JvmName("VkSurfaceView") +package org.godotengine.godot.vulkan + +import android.content.Context +import android.view.SurfaceHolder +import android.view.SurfaceView + +/** + * An implementation of SurfaceView that uses the dedicated surface for + * displaying Vulkan rendering. + * <p> + * A [VkSurfaceView] provides the following features: + * <p> + * <ul> + * <li>Manages a surface, which is a special piece of memory that can be + * composited into the Android view system. + * <li>Accepts a user-provided [VkRenderer] object that does the actual rendering. + * <li>Renders on a dedicated [VkThread] thread to decouple rendering performance from the + * UI thread. + * </ul> + */ +internal class VkSurfaceView(context: Context) : SurfaceView(context), SurfaceHolder.Callback { + + companion object { + fun checkState(expression: Boolean, errorMessage: Any) { + check(expression) { errorMessage.toString() } + } + } + + /** + * Thread used to drive the vulkan logic. + */ + private val vkThread: VkThread by lazy { + VkThread(this, renderer) + } + + /** + * Performs the actual rendering. + */ + private lateinit var renderer: VkRenderer + + init { + isClickable = true + holder.addCallback(this) + } + + /** + * Set the [VkRenderer] associated with the view, and starts the thread that will drive the vulkan + * rendering. + * + * This method should be called once and only once in the life-cycle of [VkSurfaceView]. + */ + fun startRenderer(renderer: VkRenderer) { + checkState(!this::renderer.isInitialized, "startRenderer must only be invoked once") + this.renderer = renderer + vkThread.start() + } + + /** + * Queues a runnable to be run on the Vulkan rendering thread. + * + * Must not be called before a [VkRenderer] has been set. + */ + fun queueOnVkThread(runnable: Runnable) { + vkThread.queueEvent(runnable) + } + + /** + * Resumes the rendering thread. + * + * Must not be called before a [VkRenderer] has been set. + */ + fun onResume() { + vkThread.onResume() + } + + /** + * Pauses the rendering thread. + * + * Must not be called before a [VkRenderer] has been set. + */ + fun onPause() { + vkThread.onPause() + } + + /** + * Tear down the rendering thread. + * + * Must not be called before a [VkRenderer] has been set. + */ + fun onDestroy() { + vkThread.blockingExit() + } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + vkThread.onSurfaceChanged(width, height) + } + + override fun surfaceDestroyed(holder: SurfaceHolder) { + vkThread.onSurfaceDestroyed() + } + + override fun surfaceCreated(holder: SurfaceHolder) { + vkThread.onSurfaceCreated() + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt new file mode 100644 index 0000000000..2e332840bf --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt @@ -0,0 +1,230 @@ +/*************************************************************************/ +/* VkThread.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +@file:JvmName("VkThread") +package org.godotengine.godot.vulkan + +import android.util.Log +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +/** + * Thread implementation for the [VkSurfaceView] onto which the vulkan logic is ran. + * + * The implementation is modeled after [android.opengl.GLSurfaceView]'s GLThread. + */ +internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vkRenderer: VkRenderer) : Thread(TAG) { + + companion object { + private val TAG = VkThread::class.java.simpleName + } + + /** + * Used to run events scheduled on the thread. + */ + private val eventQueue = ArrayList<Runnable>() + + /** + * Used to synchronize interaction with other threads (e.g: main thread). + */ + private val lock = ReentrantLock() + private val lockCondition = lock.newCondition() + + private var shouldExit = false + private var exited = false + private var rendererInitialized = false + private var rendererResumed = false + private var resumed = false + private var hasSurface = false + private var width = 0 + private var height = 0 + + /** + * Determine when drawing can occur on the thread. This usually occurs after the + * [android.view.Surface] is available, the app is in a resumed state. + */ + private val readyToDraw + get() = hasSurface && resumed + + private fun threadExiting() { + lock.withLock { + exited = true + lockCondition.signalAll() + } + } + + /** + * Queue an event on the [VkThread]. + */ + fun queueEvent(event: Runnable) { + lock.withLock { + eventQueue.add(event) + lockCondition.signalAll() + } + } + + /** + * Request the thread to exit and block until it's done. + */ + fun blockingExit() { + lock.withLock { + shouldExit = true + lockCondition.signalAll() + while (!exited) { + try { + Log.i(TAG, "Waiting on exit for $name") + lockCondition.await() + } catch (ex: InterruptedException) { + currentThread().interrupt() + } + } + } + } + + /** + * Invoked when the app resumes. + */ + fun onResume() { + lock.withLock { + resumed = true + lockCondition.signalAll() + } + } + + /** + * Invoked when the app pauses. + */ + fun onPause() { + lock.withLock { + resumed = false + lockCondition.signalAll() + } + } + + /** + * Invoked when the [android.view.Surface] has been created. + */ + fun onSurfaceCreated() { + // This is a no op because surface creation will always be followed by surfaceChanged() + // which provide all the needed information. + } + + /** + * Invoked following structural updates to [android.view.Surface]. + */ + fun onSurfaceChanged(width: Int, height: Int) { + lock.withLock { + hasSurface = true + this.width = width + this.height = height + lockCondition.signalAll() + } + } + + /** + * Invoked when the [android.view.Surface] is no longer available. + */ + fun onSurfaceDestroyed() { + lock.withLock { + hasSurface = false + lockCondition.signalAll() + } + } + + /** + * Thread loop modeled after [android.opengl.GLSurfaceView]'s GLThread. + */ + override fun run() { + try { + while (true) { + var event: Runnable? = null + lock.withLock { + while (true) { + // Code path for exiting the thread loop. + if (shouldExit) { + vkRenderer.onVkDestroy() + return + } + + // Check for events and execute them outside of the loop if found to avoid + // blocking the thread lifecycle by holding onto the lock. + if (eventQueue.isNotEmpty()) { + event = eventQueue.removeAt(0) + break; + } + + if (readyToDraw) { + if (!rendererResumed) { + rendererResumed = true + vkRenderer.onVkResume() + + if (!rendererInitialized) { + rendererInitialized = true + vkRenderer.onVkSurfaceCreated(vkSurfaceView.holder.surface) + } + + vkRenderer.onVkSurfaceChanged(vkSurfaceView.holder.surface, width, height) + } + + // Break out of the loop so drawing can occur without holding onto the lock. + break; + } else if (rendererResumed) { + // If we aren't ready to draw but are resumed, that means we either lost a surface + // or the app was paused. + rendererResumed = false + vkRenderer.onVkPause() + } + // We only reach this state if we are not ready to draw and have no queued events, so + // we wait. + // On state change, the thread will be awoken using the [lock] and [lockCondition], and + // we will resume execution. + lockCondition.await() + } + } + + // Run queued event. + if (event != null) { + event?.run() + continue + } + + // Draw only when there no more queued events. + vkRenderer.onVkDrawFrame() + } + } catch (ex: InterruptedException) { + Log.i(TAG, ex.message) + } catch (ex: IllegalStateException) { + Log.i(TAG, ex.message) + } finally { + threadExiting() + } + } + +} diff --git a/platform/android/java/plugins/godotpayment/build.gradle b/platform/android/java/plugins/godotpayment/build.gradle new file mode 100644 index 0000000000..4f376c4587 --- /dev/null +++ b/platform/android/java/plugins/godotpayment/build.gradle @@ -0,0 +1,31 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + + defaultConfig { + minSdkVersion versions.minSdk + targetSdkVersion versions.targetSdk + } + + libraryVariants.all { variant -> + variant.outputs.all { output -> + output.outputFileName = "GodotPayment.${variant.name}.aar" + } + } + +} + +dependencies { + implementation libraries.supportCoreUtils + implementation libraries.v4Support + + if (rootProject.findProject(":lib")) { + compileOnly project(":lib") + } else if (rootProject.findProject(":godot:lib")) { + compileOnly project(":godot:lib") + } else { + compileOnly fileTree(dir: 'libs', include: ['godot-lib*.aar']) + } +} diff --git a/platform/android/java/plugins/godotpayment/src/main/AndroidManifest.xml b/platform/android/java/plugins/godotpayment/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..61afa03799 --- /dev/null +++ b/platform/android/java/plugins/godotpayment/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.godotengine.godot.plugin.payment"> + + <application> + + <meta-data + android:name="org.godotengine.plugin.v1.GodotPayment" + android:value="org.godotengine.godot.plugin.payment.GodotPayment" /> + + </application> +</manifest> diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotPaymentV3.java b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/GodotPayment.java index 93265d509f..4a6b611c4d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotPaymentV3.java +++ b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/GodotPayment.java @@ -1,5 +1,5 @@ /*************************************************************************/ -/* GodotPaymentV3.java */ +/* GodotPayment.java */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,20 +28,24 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -package org.godotengine.godot; +package org.godotengine.godot.plugin.payment; -import android.app.Activity; +import android.support.annotation.NonNull; import android.util.Log; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.godotengine.godot.Dictionary; +import org.godotengine.godot.Godot; +import org.godotengine.godot.GodotLib; +import org.godotengine.godot.payments.GodotPaymentInterface; import org.godotengine.godot.payments.PaymentsManager; +import org.godotengine.godot.plugin.GodotPlugin; import org.json.JSONException; import org.json.JSONObject; -public class GodotPaymentV3 extends Godot.SingletonBase { +public class GodotPayment extends GodotPlugin implements GodotPaymentInterface { - private Godot activity; private Integer purchaseCallbackId = 0; private String accessToken; private String purchaseValidationUrlPrefix; @@ -49,8 +53,15 @@ public class GodotPaymentV3 extends Godot.SingletonBase { private PaymentsManager mPaymentManager; private Dictionary mSkuDetails = new Dictionary(); + public GodotPayment(Godot godot) { + super(godot); + mPaymentManager = godot.getPaymentsManager(); + mPaymentManager.setBaseSingleton(this); + } + + @Override public void purchase(final String sku, final String transactionId) { - activity.runOnUiThread(new Runnable() { + runOnUiThread(new Runnable() { @Override public void run() { mPaymentManager.requestPurchase(sku, transactionId); @@ -58,21 +69,9 @@ public class GodotPaymentV3 extends Godot.SingletonBase { }); } - static public Godot.SingletonBase initialize(Activity p_activity) { - - return new GodotPaymentV3(p_activity); - } - - public GodotPaymentV3(Activity p_activity) { - - registerClass("GodotPayments", new String[] { "purchase", "setPurchaseCallbackId", "setPurchaseValidationUrlPrefix", "setTransactionId", "getSignature", "consumeUnconsumedPurchases", "requestPurchased", "setAutoConsume", "consume", "querySkuDetails", "isConnected" }); - activity = (Godot)p_activity; - mPaymentManager = activity.getPaymentsManager(); - mPaymentManager.setBaseSingleton(this); - } - + @Override public void consumeUnconsumedPurchases() { - activity.runOnUiThread(new Runnable() { + runOnUiThread(new Runnable() { @Override public void run() { mPaymentManager.consumeUnconsumedPurchases(); @@ -82,74 +81,91 @@ public class GodotPaymentV3 extends Godot.SingletonBase { private String signature; + @Override public String getSignature() { return this.signature; } + @Override public void callbackSuccess(String ticket, String signature, String sku) { GodotLib.calldeferred(purchaseCallbackId, "purchase_success", new Object[] { ticket, signature, sku }); } + @Override public void callbackSuccessProductMassConsumed(String ticket, String signature, String sku) { Log.d(this.getClass().getName(), "callbackSuccessProductMassConsumed > " + ticket + "," + signature + "," + sku); GodotLib.calldeferred(purchaseCallbackId, "consume_success", new Object[] { ticket, signature, sku }); } + @Override public void callbackSuccessNoUnconsumedPurchases() { GodotLib.calldeferred(purchaseCallbackId, "consume_not_required", new Object[] {}); } + @Override public void callbackFailConsume(String message) { GodotLib.calldeferred(purchaseCallbackId, "consume_fail", new Object[] { message }); } + @Override public void callbackFail(String message) { GodotLib.calldeferred(purchaseCallbackId, "purchase_fail", new Object[] { message }); } + @Override public void callbackCancel() { GodotLib.calldeferred(purchaseCallbackId, "purchase_cancel", new Object[] {}); } + @Override public void callbackAlreadyOwned(String sku) { GodotLib.calldeferred(purchaseCallbackId, "purchase_owned", new Object[] { sku }); } + @Override public int getPurchaseCallbackId() { return purchaseCallbackId; } + @Override public void setPurchaseCallbackId(int purchaseCallbackId) { this.purchaseCallbackId = purchaseCallbackId; } + @Override public String getPurchaseValidationUrlPrefix() { return this.purchaseValidationUrlPrefix; } + @Override public void setPurchaseValidationUrlPrefix(String url) { this.purchaseValidationUrlPrefix = url; } + @Override public String getAccessToken() { return accessToken; } + @Override public void setAccessToken(String accessToken) { this.accessToken = accessToken; } + @Override public void setTransactionId(String transactionId) { this.transactionId = transactionId; } + @Override public String getTransactionId() { return this.transactionId; } // request purchased items are not consumed + @Override public void requestPurchased() { - activity.runOnUiThread(new Runnable() { + runOnUiThread(new Runnable() { @Override public void run() { mPaymentManager.requestPurchased(); @@ -158,34 +174,41 @@ public class GodotPaymentV3 extends Godot.SingletonBase { } // callback for requestPurchased() + @Override public void callbackPurchased(String receipt, String signature, String sku) { GodotLib.calldeferred(purchaseCallbackId, "has_purchased", new Object[] { receipt, signature, sku }); } + @Override public void callbackDisconnected() { GodotLib.calldeferred(purchaseCallbackId, "iap_disconnected", new Object[] {}); } + @Override public void callbackConnected() { GodotLib.calldeferred(purchaseCallbackId, "iap_connected", new Object[] {}); } // true if connected, false otherwise + @Override public boolean isConnected() { return mPaymentManager.isConnected(); } // consume item automatically after purchase. default is true. + @Override public void setAutoConsume(boolean autoConsume) { mPaymentManager.setAutoConsume(autoConsume); } // consume a specific item + @Override public void consume(String sku) { mPaymentManager.consume(sku); } // query in app item detail info + @Override public void querySkuDetails(String[] list) { List<String> nKeys = Arrays.asList(list); List<String> cKeys = Arrays.asList(mSkuDetails.get_keys()); @@ -202,6 +225,7 @@ public class GodotPaymentV3 extends Godot.SingletonBase { } } + @Override public void addSkuDetail(String itemJson) { JSONObject o = null; try { @@ -220,11 +244,25 @@ public class GodotPaymentV3 extends Godot.SingletonBase { } } + @Override public void completeSkuDetail() { GodotLib.calldeferred(purchaseCallbackId, "sku_details_complete", new Object[] { mSkuDetails }); } + @Override public void errorSkuDetail(String errorMessage) { GodotLib.calldeferred(purchaseCallbackId, "sku_details_error", new Object[] { errorMessage }); } + + @NonNull + @Override + public String getPluginName() { + return "GodotPayment"; + } + + @NonNull + @Override + public List<String> getPluginMethods() { + return Arrays.asList("purchase", "setPurchaseCallbackId", "setPurchaseValidationUrlPrefix", "setTransactionId", "getSignature", "consumeUnconsumedPurchases", "requestPurchased", "setAutoConsume", "consume", "querySkuDetails", "isConnected"); + } } diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle index f6921c70aa..9536d3de6d 100644 --- a/platform/android/java/settings.gradle +++ b/platform/android/java/settings.gradle @@ -3,3 +3,4 @@ rootProject.name = "Godot" include ':app' include ':lib' +include ':plugins:godotpayment' diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 900a452024..0b1d070441 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -33,14 +33,15 @@ #include "java_godot_wrapper.h" #include "android/asset_manager_jni.h" +#include "android_keys_utils.h" #include "api/java_class_wrapper.h" #include "audio_driver_jandroid.h" #include "core/engine.h" -#include "core/os/keyboard.h" #include "core/project_settings.h" #include "dir_access_jandroid.h" #include "file_access_android.h" #include "file_access_jandroid.h" +#include "jni_utils.h" #include "main/input_default.h" #include "main/main.h" #include "net_socket_android.h" @@ -54,551 +55,6 @@ static OS_Android *os_android = NULL; static GodotJavaWrapper *godot_java = NULL; static GodotIOJavaWrapper *godot_io_java = NULL; -struct jvalret { - - jobject obj; - jvalue val; - jvalret() { obj = NULL; } -}; - -jvalret _variant_to_jvalue(JNIEnv *env, Variant::Type p_type, const Variant *p_arg, bool force_jobject = false) { - - jvalret v; - - switch (p_type) { - - case Variant::BOOL: { - - if (force_jobject) { - jclass bclass = env->FindClass("java/lang/Boolean"); - jmethodID ctor = env->GetMethodID(bclass, "<init>", "(Z)V"); - jvalue val; - val.z = (bool)(*p_arg); - jobject obj = env->NewObjectA(bclass, ctor, &val); - v.val.l = obj; - v.obj = obj; - env->DeleteLocalRef(bclass); - } else { - v.val.z = *p_arg; - }; - } break; - case Variant::INT: { - - if (force_jobject) { - - jclass bclass = env->FindClass("java/lang/Integer"); - jmethodID ctor = env->GetMethodID(bclass, "<init>", "(I)V"); - jvalue val; - val.i = (int)(*p_arg); - jobject obj = env->NewObjectA(bclass, ctor, &val); - v.val.l = obj; - v.obj = obj; - env->DeleteLocalRef(bclass); - - } else { - v.val.i = *p_arg; - }; - } break; - case Variant::FLOAT: { - - if (force_jobject) { - - jclass bclass = env->FindClass("java/lang/Double"); - jmethodID ctor = env->GetMethodID(bclass, "<init>", "(D)V"); - jvalue val; - val.d = (double)(*p_arg); - jobject obj = env->NewObjectA(bclass, ctor, &val); - v.val.l = obj; - v.obj = obj; - env->DeleteLocalRef(bclass); - - } else { - v.val.f = *p_arg; - }; - } break; - case Variant::STRING: { - - String s = *p_arg; - jstring jStr = env->NewStringUTF(s.utf8().get_data()); - v.val.l = jStr; - v.obj = jStr; - } break; - case Variant::PACKED_STRING_ARRAY: { - - Vector<String> sarray = *p_arg; - jobjectArray arr = env->NewObjectArray(sarray.size(), env->FindClass("java/lang/String"), env->NewStringUTF("")); - - for (int j = 0; j < sarray.size(); j++) { - - jstring str = env->NewStringUTF(sarray[j].utf8().get_data()); - env->SetObjectArrayElement(arr, j, str); - env->DeleteLocalRef(str); - } - v.val.l = arr; - v.obj = arr; - - } break; - - case Variant::DICTIONARY: { - - Dictionary dict = *p_arg; - jclass dclass = env->FindClass("org/godotengine/godot/Dictionary"); - jmethodID ctor = env->GetMethodID(dclass, "<init>", "()V"); - jobject jdict = env->NewObject(dclass, ctor); - - Array keys = dict.keys(); - - jobjectArray jkeys = env->NewObjectArray(keys.size(), env->FindClass("java/lang/String"), env->NewStringUTF("")); - for (int j = 0; j < keys.size(); j++) { - jstring str = env->NewStringUTF(String(keys[j]).utf8().get_data()); - env->SetObjectArrayElement(jkeys, j, str); - env->DeleteLocalRef(str); - }; - - jmethodID set_keys = env->GetMethodID(dclass, "set_keys", "([Ljava/lang/String;)V"); - jvalue val; - val.l = jkeys; - env->CallVoidMethodA(jdict, set_keys, &val); - env->DeleteLocalRef(jkeys); - - jobjectArray jvalues = env->NewObjectArray(keys.size(), env->FindClass("java/lang/Object"), NULL); - - for (int j = 0; j < keys.size(); j++) { - Variant var = dict[keys[j]]; - jvalret v = _variant_to_jvalue(env, var.get_type(), &var, true); - env->SetObjectArrayElement(jvalues, j, v.val.l); - if (v.obj) { - env->DeleteLocalRef(v.obj); - } - }; - - jmethodID set_values = env->GetMethodID(dclass, "set_values", "([Ljava/lang/Object;)V"); - val.l = jvalues; - env->CallVoidMethodA(jdict, set_values, &val); - env->DeleteLocalRef(jvalues); - env->DeleteLocalRef(dclass); - - v.val.l = jdict; - v.obj = jdict; - } break; - - case Variant::PACKED_INT32_ARRAY: { - - Vector<int> array = *p_arg; - jintArray arr = env->NewIntArray(array.size()); - const int *r = array.ptr(); - env->SetIntArrayRegion(arr, 0, array.size(), r.ptr()); - v.val.l = arr; - v.obj = arr; - - } break; - case Variant::PACKED_BYTE_ARRAY: { - Vector<uint8_t> array = *p_arg; - jbyteArray arr = env->NewByteArray(array.size()); - const uint8_t *r = array.ptr(); - env->SetByteArrayRegion(arr, 0, array.size(), reinterpret_cast<const signed char *>(r.ptr())); - v.val.l = arr; - v.obj = arr; - - } break; - case Variant::PACKED_FLOAT32_ARRAY: { - - Vector<float> array = *p_arg; - jfloatArray arr = env->NewFloatArray(array.size()); - const float *r = array.ptr(); - env->SetFloatArrayRegion(arr, 0, array.size(), r.ptr()); - v.val.l = arr; - v.obj = arr; - - } break; -#ifndef _MSC_VER -#warning This is missing 64 bits arrays, I have no idea how to do it in JNI -#endif - - default: { - - v.val.i = 0; - } break; - } - return v; -} - -String _get_class_name(JNIEnv *env, jclass cls, bool *array) { - - jclass cclass = env->FindClass("java/lang/Class"); - jmethodID getName = env->GetMethodID(cclass, "getName", "()Ljava/lang/String;"); - jstring clsName = (jstring)env->CallObjectMethod(cls, getName); - - if (array) { - jmethodID isArray = env->GetMethodID(cclass, "isArray", "()Z"); - jboolean isarr = env->CallBooleanMethod(cls, isArray); - (*array) = isarr ? true : false; - } - String name = jstring_to_string(clsName, env); - env->DeleteLocalRef(clsName); - - return name; -} - -Variant _jobject_to_variant(JNIEnv *env, jobject obj) { - - if (obj == NULL) { - return Variant(); - } - - jclass c = env->GetObjectClass(obj); - bool array; - String name = _get_class_name(env, c, &array); - - if (name == "java.lang.String") { - - return jstring_to_string((jstring)obj, env); - }; - - if (name == "[Ljava.lang.String;") { - - jobjectArray arr = (jobjectArray)obj; - int stringCount = env->GetArrayLength(arr); - Vector<String> sarr; - - for (int i = 0; i < stringCount; i++) { - jstring string = (jstring)env->GetObjectArrayElement(arr, i); - sarr.push_back(jstring_to_string(string, env)); - env->DeleteLocalRef(string); - } - - return sarr; - }; - - if (name == "java.lang.Boolean") { - - jmethodID boolValue = env->GetMethodID(c, "booleanValue", "()Z"); - bool ret = env->CallBooleanMethod(obj, boolValue); - return ret; - }; - - if (name == "java.lang.Integer" || name == "java.lang.Long") { - - jclass nclass = env->FindClass("java/lang/Number"); - jmethodID longValue = env->GetMethodID(nclass, "longValue", "()J"); - jlong ret = env->CallLongMethod(obj, longValue); - return ret; - }; - - if (name == "[I") { - - jintArray arr = (jintArray)obj; - int fCount = env->GetArrayLength(arr); - Vector<int> sarr; - sarr.resize(fCount); - - int *w = sarr.ptrw(); - env->GetIntArrayRegion(arr, 0, fCount, w.ptr()); - w.release(); - return sarr; - }; - - if (name == "[B") { - - jbyteArray arr = (jbyteArray)obj; - int fCount = env->GetArrayLength(arr); - Vector<uint8_t> sarr; - sarr.resize(fCount); - - uint8_t *w = sarr.ptrw(); - env->GetByteArrayRegion(arr, 0, fCount, reinterpret_cast<signed char *>(w.ptr())); - w.release(); - return sarr; - }; - - if (name == "java.lang.Float" || name == "java.lang.Double") { - - jclass nclass = env->FindClass("java/lang/Number"); - jmethodID doubleValue = env->GetMethodID(nclass, "doubleValue", "()D"); - double ret = env->CallDoubleMethod(obj, doubleValue); - return ret; - }; - - if (name == "[D") { - - jdoubleArray arr = (jdoubleArray)obj; - int fCount = env->GetArrayLength(arr); - PackedFloat32Array sarr; - sarr.resize(fCount); - - real_t *w = sarr.ptrw(); - - for (int i = 0; i < fCount; i++) { - - double n; - env->GetDoubleArrayRegion(arr, i, 1, &n); - w.ptr()[i] = n; - }; - return sarr; - }; - - if (name == "[F") { - - jfloatArray arr = (jfloatArray)obj; - int fCount = env->GetArrayLength(arr); - PackedFloat32Array sarr; - sarr.resize(fCount); - - real_t *w = sarr.ptrw(); - - for (int i = 0; i < fCount; i++) { - - float n; - env->GetFloatArrayRegion(arr, i, 1, &n); - w.ptr()[i] = n; - }; - return sarr; - }; - - if (name == "[Ljava.lang.Object;") { - - jobjectArray arr = (jobjectArray)obj; - int objCount = env->GetArrayLength(arr); - Array varr; - - for (int i = 0; i < objCount; i++) { - jobject jobj = env->GetObjectArrayElement(arr, i); - Variant v = _jobject_to_variant(env, jobj); - varr.push_back(v); - env->DeleteLocalRef(jobj); - } - - return varr; - }; - - if (name == "java.util.HashMap" || name == "org.godotengine.godot.Dictionary") { - - Dictionary ret; - jclass oclass = c; - jmethodID get_keys = env->GetMethodID(oclass, "get_keys", "()[Ljava/lang/String;"); - jobjectArray arr = (jobjectArray)env->CallObjectMethod(obj, get_keys); - - PackedStringArray keys = _jobject_to_variant(env, arr); - env->DeleteLocalRef(arr); - - jmethodID get_values = env->GetMethodID(oclass, "get_values", "()[Ljava/lang/Object;"); - arr = (jobjectArray)env->CallObjectMethod(obj, get_values); - - Array vals = _jobject_to_variant(env, arr); - env->DeleteLocalRef(arr); - - for (int i = 0; i < keys.size(); i++) { - - ret[keys[i]] = vals[i]; - }; - - return ret; - }; - - env->DeleteLocalRef(c); - - return Variant(); -} - -class JNISingleton : public Object { - - GDCLASS(JNISingleton, Object); - - struct MethodData { - - jmethodID method; - Variant::Type ret_type; - Vector<Variant::Type> argtypes; - }; - - jobject instance; - Map<StringName, MethodData> method_map; - -public: - virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { - - ERR_FAIL_COND_V(!instance, Variant()); - - r_error.error = Callable::CallError::CALL_OK; - - Map<StringName, MethodData>::Element *E = method_map.find(p_method); - if (!E) { - - r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; - return Variant(); - } - - int ac = E->get().argtypes.size(); - if (ac < p_argcount) { - - r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS; - r_error.argument = ac; - return Variant(); - } - - if (ac > p_argcount) { - - r_error.error = Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS; - r_error.argument = ac; - return Variant(); - } - - for (int i = 0; i < p_argcount; i++) { - - if (!Variant::can_convert(p_args[i]->get_type(), E->get().argtypes[i])) { - - r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT; - r_error.argument = i; - r_error.expected = E->get().argtypes[i]; - } - } - - jvalue *v = NULL; - - if (p_argcount) { - - v = (jvalue *)alloca(sizeof(jvalue) * p_argcount); - } - - JNIEnv *env = ThreadAndroid::get_env(); - - int res = env->PushLocalFrame(16); - - ERR_FAIL_COND_V(res != 0, Variant()); - - List<jobject> to_erase; - for (int i = 0; i < p_argcount; i++) { - - jvalret vr = _variant_to_jvalue(env, E->get().argtypes[i], p_args[i]); - v[i] = vr.val; - if (vr.obj) - to_erase.push_back(vr.obj); - } - - Variant ret; - - switch (E->get().ret_type) { - - case Variant::NIL: { - - env->CallVoidMethodA(instance, E->get().method, v); - } break; - case Variant::BOOL: { - - ret = env->CallBooleanMethodA(instance, E->get().method, v) == JNI_TRUE; - } break; - case Variant::INT: { - - ret = env->CallIntMethodA(instance, E->get().method, v); - } break; - case Variant::FLOAT: { - - ret = env->CallFloatMethodA(instance, E->get().method, v); - } break; - case Variant::STRING: { - - jobject o = env->CallObjectMethodA(instance, E->get().method, v); - ret = jstring_to_string((jstring)o, env); - env->DeleteLocalRef(o); - } break; - case Variant::PACKED_STRING_ARRAY: { - - jobjectArray arr = (jobjectArray)env->CallObjectMethodA(instance, E->get().method, v); - - ret = _jobject_to_variant(env, arr); - - env->DeleteLocalRef(arr); - } break; - case Variant::PACKED_INT32_ARRAY: { - - jintArray arr = (jintArray)env->CallObjectMethodA(instance, E->get().method, v); - - int fCount = env->GetArrayLength(arr); - Vector<int> sarr; - sarr.resize(fCount); - - int *w = sarr.ptrw(); - env->GetIntArrayRegion(arr, 0, fCount, w.ptr()); - w.release(); - ret = sarr; - env->DeleteLocalRef(arr); - } break; - case Variant::PACKED_FLOAT32_ARRAY: { - - jfloatArray arr = (jfloatArray)env->CallObjectMethodA(instance, E->get().method, v); - - int fCount = env->GetArrayLength(arr); - Vector<float> sarr; - sarr.resize(fCount); - - float *w = sarr.ptrw(); - env->GetFloatArrayRegion(arr, 0, fCount, w.ptr()); - w.release(); - ret = sarr; - env->DeleteLocalRef(arr); - } break; - -#ifndef _MSC_VER -#warning This is missing 64 bits arrays, I have no idea how to do it in JNI -#endif - case Variant::DICTIONARY: { - - jobject obj = env->CallObjectMethodA(instance, E->get().method, v); - ret = _jobject_to_variant(env, obj); - env->DeleteLocalRef(obj); - - } break; - default: { - - env->PopLocalFrame(NULL); - ERR_FAIL_V(Variant()); - } break; - } - - while (to_erase.size()) { - env->DeleteLocalRef(to_erase.front()->get()); - to_erase.pop_front(); - } - - env->PopLocalFrame(NULL); - - return ret; - } - - jobject get_instance() const { - - return instance; - } - void set_instance(jobject p_instance) { - - instance = p_instance; - } - - void add_method(const StringName &p_name, jmethodID p_method, const Vector<Variant::Type> &p_args, Variant::Type p_ret_type) { - - MethodData md; - md.method = p_method; - md.argtypes = p_args; - md.ret_type = p_ret_type; - method_map[p_name] = md; - } - - JNISingleton() { - instance = NULL; - } -}; - -struct TST { - - int a; - TST() { - - a = 5; - } -}; - -TST tst; - static bool initialized = false; static int step = 0; @@ -607,20 +63,16 @@ static Vector3 accelerometer; static Vector3 gravity; static Vector3 magnetometer; static Vector3 gyroscope; -static HashMap<String, JNISingleton *> jni_singletons; -// virtual Error native_video_play(String p_path); -// virtual bool native_video_is_playing(); -// virtual void native_video_pause(); -// virtual void native_video_stop(); +extern "C" { -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jobject obj, jint p_height) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jclass clazz, jint p_height) { if (godot_io_java) { godot_io_java->set_vk_height(p_height); } } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jobject obj, jobject activity, jobject p_asset_manager, jboolean p_use_apk_expansion) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject activity, jobject p_asset_manager, jboolean p_use_apk_expansion) { initialized = true; @@ -653,7 +105,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *en godot_java->on_video_init(env); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jobject obj, jobject activity) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz, jobject activity) { // lets cleanup if (godot_io_java) { delete godot_io_java; @@ -666,47 +118,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env } } -static void _initialize_java_modules() { - - if (!ProjectSettings::get_singleton()->has_setting("android/modules")) { - return; - } - - String modules = ProjectSettings::get_singleton()->get("android/modules"); - modules = modules.strip_edges(); - if (modules == String()) { - return; - } - Vector<String> mods = modules.split(",", false); - - if (mods.size()) { - jobject cls = godot_java->get_class_loader(); - - // TODO create wrapper for class loader - - JNIEnv *env = ThreadAndroid::get_env(); - jclass classLoader = env->FindClass("java/lang/ClassLoader"); - jmethodID findClass = env->GetMethodID(classLoader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); - - for (int i = 0; i < mods.size(); i++) { - - String m = mods[i]; - - print_line("Loading Android module: " + m); - jstring strClassName = env->NewStringUTF(m.utf8().get_data()); - jclass singletonClass = (jclass)env->CallObjectMethod(cls, findClass, strClassName); - ERR_CONTINUE_MSG(!singletonClass, "Couldn't find singleton for class: " + m + "."); - - jmethodID initialize = env->GetStaticMethodID(singletonClass, "initialize", "(Landroid/app/Activity;)Lorg/godotengine/godot/Godot$SingletonBase;"); - ERR_CONTINUE_MSG(!initialize, "Couldn't find proper initialize function 'public static Godot.SingletonBase Class::initialize(Activity p_activity)' initializer for singleton class: " + m + "."); - - jobject obj = env->CallStaticObjectMethod(singletonClass, initialize, godot_java->get_activity()); - env->NewGlobalRef(obj); - } - } -} - -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jobject obj, jobjectArray p_cmdline) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline) { ThreadAndroid::setup_thread(); const char **cmdline = NULL; @@ -746,16 +158,15 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jo } java_class_wrapper = memnew(JavaClassWrapper(godot_java->get_activity())); - _initialize_java_modules(); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jobject obj, jint width, jint height) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jclass clazz, jint width, jint height) { if (os_android) os_android->set_display_size(Size2(width, height)); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *env, jobject obj, bool p_32_bits) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *env, jclass clazz, jboolean p_32_bits) { if (os_android) { if (step == 0) { @@ -770,14 +181,14 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *en } } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jobject obj) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jclass clazz) { if (step == 0) return; os_android->main_loop_request_go_back(); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jobject obj) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz) { if (step == -1) return; @@ -796,6 +207,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, job } os_android->main_loop_begin(); + godot_java->on_gl_godot_main_loop_started(env); ++step; } @@ -810,7 +222,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, job } } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch(JNIEnv *env, jobject obj, jint ev, jint pointer, jint count, jintArray positions) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint count, jintArray positions) { if (step == 0) return; @@ -834,282 +246,28 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch(JNIEnv *env, jo */ } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_hover(JNIEnv *env, jobject obj, jint p_type, jint p_x, jint p_y) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_hover(JNIEnv *env, jclass clazz, jint p_type, jint p_x, jint p_y) { if (step == 0) return; os_android->process_hover(p_type, Point2(p_x, p_y)); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_doubletap(JNIEnv *env, jobject obj, jint p_x, jint p_y) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_doubletap(JNIEnv *env, jclass clazz, jint p_x, jint p_y) { if (step == 0) return; os_android->process_double_tap(Point2(p_x, p_y)); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_scroll(JNIEnv *env, jobject obj, jint p_x, jint p_y) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_scroll(JNIEnv *env, jclass clazz, jint p_x, jint p_y) { if (step == 0) return; os_android->process_scroll(Point2(p_x, p_y)); } -/* - * Android Key codes. - */ -enum { - AKEYCODE_UNKNOWN = 0, - AKEYCODE_SOFT_LEFT = 1, - AKEYCODE_SOFT_RIGHT = 2, - AKEYCODE_HOME = 3, - AKEYCODE_BACK = 4, - AKEYCODE_CALL = 5, - AKEYCODE_ENDCALL = 6, - AKEYCODE_0 = 7, - AKEYCODE_1 = 8, - AKEYCODE_2 = 9, - AKEYCODE_3 = 10, - AKEYCODE_4 = 11, - AKEYCODE_5 = 12, - AKEYCODE_6 = 13, - AKEYCODE_7 = 14, - AKEYCODE_8 = 15, - AKEYCODE_9 = 16, - AKEYCODE_STAR = 17, - AKEYCODE_POUND = 18, - AKEYCODE_DPAD_UP = 19, - AKEYCODE_DPAD_DOWN = 20, - AKEYCODE_DPAD_LEFT = 21, - AKEYCODE_DPAD_RIGHT = 22, - AKEYCODE_DPAD_CENTER = 23, - AKEYCODE_VOLUME_UP = 24, - AKEYCODE_VOLUME_DOWN = 25, - AKEYCODE_POWER = 26, - AKEYCODE_CAMERA = 27, - AKEYCODE_CLEAR = 28, - AKEYCODE_A = 29, - AKEYCODE_B = 30, - AKEYCODE_C = 31, - AKEYCODE_D = 32, - AKEYCODE_E = 33, - AKEYCODE_F = 34, - AKEYCODE_G = 35, - AKEYCODE_H = 36, - AKEYCODE_I = 37, - AKEYCODE_J = 38, - AKEYCODE_K = 39, - AKEYCODE_L = 40, - AKEYCODE_M = 41, - AKEYCODE_N = 42, - AKEYCODE_O = 43, - AKEYCODE_P = 44, - AKEYCODE_Q = 45, - AKEYCODE_R = 46, - AKEYCODE_S = 47, - AKEYCODE_T = 48, - AKEYCODE_U = 49, - AKEYCODE_V = 50, - AKEYCODE_W = 51, - AKEYCODE_X = 52, - AKEYCODE_Y = 53, - AKEYCODE_Z = 54, - AKEYCODE_COMMA = 55, - AKEYCODE_PERIOD = 56, - AKEYCODE_ALT_LEFT = 57, - AKEYCODE_ALT_RIGHT = 58, - AKEYCODE_SHIFT_LEFT = 59, - AKEYCODE_SHIFT_RIGHT = 60, - AKEYCODE_TAB = 61, - AKEYCODE_SPACE = 62, - AKEYCODE_SYM = 63, - AKEYCODE_EXPLORER = 64, - AKEYCODE_ENVELOPE = 65, - AKEYCODE_ENTER = 66, - AKEYCODE_DEL = 67, - AKEYCODE_GRAVE = 68, - AKEYCODE_MINUS = 69, - AKEYCODE_EQUALS = 70, - AKEYCODE_LEFT_BRACKET = 71, - AKEYCODE_RIGHT_BRACKET = 72, - AKEYCODE_BACKSLASH = 73, - AKEYCODE_SEMICOLON = 74, - AKEYCODE_APOSTROPHE = 75, - AKEYCODE_SLASH = 76, - AKEYCODE_AT = 77, - AKEYCODE_NUM = 78, - AKEYCODE_HEADSETHOOK = 79, - AKEYCODE_FOCUS = 80, // *Camera* focus - AKEYCODE_PLUS = 81, - AKEYCODE_MENU = 82, - AKEYCODE_NOTIFICATION = 83, - AKEYCODE_SEARCH = 84, - AKEYCODE_MEDIA_PLAY_PAUSE = 85, - AKEYCODE_MEDIA_STOP = 86, - AKEYCODE_MEDIA_NEXT = 87, - AKEYCODE_MEDIA_PREVIOUS = 88, - AKEYCODE_MEDIA_REWIND = 89, - AKEYCODE_MEDIA_FAST_FORWARD = 90, - AKEYCODE_MUTE = 91, - AKEYCODE_PAGE_UP = 92, - AKEYCODE_PAGE_DOWN = 93, - AKEYCODE_PICTSYMBOLS = 94, - AKEYCODE_SWITCH_CHARSET = 95, - AKEYCODE_BUTTON_A = 96, - AKEYCODE_BUTTON_B = 97, - AKEYCODE_BUTTON_C = 98, - AKEYCODE_BUTTON_X = 99, - AKEYCODE_BUTTON_Y = 100, - AKEYCODE_BUTTON_Z = 101, - AKEYCODE_BUTTON_L1 = 102, - AKEYCODE_BUTTON_R1 = 103, - AKEYCODE_BUTTON_L2 = 104, - AKEYCODE_BUTTON_R2 = 105, - AKEYCODE_BUTTON_THUMBL = 106, - AKEYCODE_BUTTON_THUMBR = 107, - AKEYCODE_BUTTON_START = 108, - AKEYCODE_BUTTON_SELECT = 109, - AKEYCODE_BUTTON_MODE = 110, - - // NOTE: If you add a new keycode here you must also add it to several other files. - // Refer to frameworks/base/core/java/android/view/KeyEvent.java for the full list. -}; - -struct _WinTranslatePair { - - unsigned int keysym; - unsigned int keycode; -}; - -static _WinTranslatePair _ak_to_keycode[] = { - { KEY_TAB, AKEYCODE_TAB }, - { KEY_ENTER, AKEYCODE_ENTER }, - { KEY_SHIFT, AKEYCODE_SHIFT_LEFT }, - { KEY_SHIFT, AKEYCODE_SHIFT_RIGHT }, - { KEY_ALT, AKEYCODE_ALT_LEFT }, - { KEY_ALT, AKEYCODE_ALT_RIGHT }, - { KEY_MENU, AKEYCODE_MENU }, - { KEY_PAUSE, AKEYCODE_MEDIA_PLAY_PAUSE }, - { KEY_ESCAPE, AKEYCODE_BACK }, - { KEY_SPACE, AKEYCODE_SPACE }, - { KEY_PAGEUP, AKEYCODE_PAGE_UP }, - { KEY_PAGEDOWN, AKEYCODE_PAGE_DOWN }, - { KEY_HOME, AKEYCODE_HOME }, //(0x24) - { KEY_LEFT, AKEYCODE_DPAD_LEFT }, - { KEY_UP, AKEYCODE_DPAD_UP }, - { KEY_RIGHT, AKEYCODE_DPAD_RIGHT }, - { KEY_DOWN, AKEYCODE_DPAD_DOWN }, - { KEY_PERIODCENTERED, AKEYCODE_DPAD_CENTER }, - { KEY_BACKSPACE, AKEYCODE_DEL }, - { KEY_0, AKEYCODE_0 }, ////0 key - { KEY_1, AKEYCODE_1 }, ////1 key - { KEY_2, AKEYCODE_2 }, ////2 key - { KEY_3, AKEYCODE_3 }, ////3 key - { KEY_4, AKEYCODE_4 }, ////4 key - { KEY_5, AKEYCODE_5 }, ////5 key - { KEY_6, AKEYCODE_6 }, ////6 key - { KEY_7, AKEYCODE_7 }, ////7 key - { KEY_8, AKEYCODE_8 }, ////8 key - { KEY_9, AKEYCODE_9 }, ////9 key - { KEY_A, AKEYCODE_A }, ////A key - { KEY_B, AKEYCODE_B }, ////B key - { KEY_C, AKEYCODE_C }, ////C key - { KEY_D, AKEYCODE_D }, ////D key - { KEY_E, AKEYCODE_E }, ////E key - { KEY_F, AKEYCODE_F }, ////F key - { KEY_G, AKEYCODE_G }, ////G key - { KEY_H, AKEYCODE_H }, ////H key - { KEY_I, AKEYCODE_I }, ////I key - { KEY_J, AKEYCODE_J }, ////J key - { KEY_K, AKEYCODE_K }, ////K key - { KEY_L, AKEYCODE_L }, ////L key - { KEY_M, AKEYCODE_M }, ////M key - { KEY_N, AKEYCODE_N }, ////N key - { KEY_O, AKEYCODE_O }, ////O key - { KEY_P, AKEYCODE_P }, ////P key - { KEY_Q, AKEYCODE_Q }, ////Q key - { KEY_R, AKEYCODE_R }, ////R key - { KEY_S, AKEYCODE_S }, ////S key - { KEY_T, AKEYCODE_T }, ////T key - { KEY_U, AKEYCODE_U }, ////U key - { KEY_V, AKEYCODE_V }, ////V key - { KEY_W, AKEYCODE_W }, ////W key - { KEY_X, AKEYCODE_X }, ////X key - { KEY_Y, AKEYCODE_Y }, ////Y key - { KEY_Z, AKEYCODE_Z }, ////Z key - { KEY_HOMEPAGE, AKEYCODE_EXPLORER }, - { KEY_LAUNCH0, AKEYCODE_BUTTON_A }, - { KEY_LAUNCH1, AKEYCODE_BUTTON_B }, - { KEY_LAUNCH2, AKEYCODE_BUTTON_C }, - { KEY_LAUNCH3, AKEYCODE_BUTTON_X }, - { KEY_LAUNCH4, AKEYCODE_BUTTON_Y }, - { KEY_LAUNCH5, AKEYCODE_BUTTON_Z }, - { KEY_LAUNCH6, AKEYCODE_BUTTON_L1 }, - { KEY_LAUNCH7, AKEYCODE_BUTTON_R1 }, - { KEY_LAUNCH8, AKEYCODE_BUTTON_L2 }, - { KEY_LAUNCH9, AKEYCODE_BUTTON_R2 }, - { KEY_LAUNCHA, AKEYCODE_BUTTON_THUMBL }, - { KEY_LAUNCHB, AKEYCODE_BUTTON_THUMBR }, - { KEY_LAUNCHC, AKEYCODE_BUTTON_START }, - { KEY_LAUNCHD, AKEYCODE_BUTTON_SELECT }, - { KEY_LAUNCHE, AKEYCODE_BUTTON_MODE }, - { KEY_VOLUMEMUTE, AKEYCODE_MUTE }, - { KEY_VOLUMEDOWN, AKEYCODE_VOLUME_DOWN }, - { KEY_VOLUMEUP, AKEYCODE_VOLUME_UP }, - { KEY_BACK, AKEYCODE_MEDIA_REWIND }, - { KEY_FORWARD, AKEYCODE_MEDIA_FAST_FORWARD }, - { KEY_MEDIANEXT, AKEYCODE_MEDIA_NEXT }, - { KEY_MEDIAPREVIOUS, AKEYCODE_MEDIA_PREVIOUS }, - { KEY_MEDIASTOP, AKEYCODE_MEDIA_STOP }, - { KEY_PLUS, AKEYCODE_PLUS }, - { KEY_EQUAL, AKEYCODE_EQUALS }, // the '+' key - { KEY_COMMA, AKEYCODE_COMMA }, // the ',' key - { KEY_MINUS, AKEYCODE_MINUS }, // the '-' key - { KEY_SLASH, AKEYCODE_SLASH }, // the '/?' key - { KEY_BACKSLASH, AKEYCODE_BACKSLASH }, - { KEY_BRACKETLEFT, AKEYCODE_LEFT_BRACKET }, - { KEY_BRACKETRIGHT, AKEYCODE_RIGHT_BRACKET }, - { KEY_UNKNOWN, 0 } -}; -/* -TODO: map these android key: - AKEYCODE_SOFT_LEFT = 1, - AKEYCODE_SOFT_RIGHT = 2, - AKEYCODE_CALL = 5, - AKEYCODE_ENDCALL = 6, - AKEYCODE_STAR = 17, - AKEYCODE_POUND = 18, - AKEYCODE_POWER = 26, - AKEYCODE_CAMERA = 27, - AKEYCODE_CLEAR = 28, - AKEYCODE_SYM = 63, - AKEYCODE_ENVELOPE = 65, - AKEYCODE_GRAVE = 68, - AKEYCODE_SEMICOLON = 74, - AKEYCODE_APOSTROPHE = 75, - AKEYCODE_AT = 77, - AKEYCODE_NUM = 78, - AKEYCODE_HEADSETHOOK = 79, - AKEYCODE_FOCUS = 80, // *Camera* focus - AKEYCODE_NOTIFICATION = 83, - AKEYCODE_SEARCH = 84, - AKEYCODE_PICTSYMBOLS = 94, - AKEYCODE_SWITCH_CHARSET = 95, -*/ - -static unsigned int android_get_keysym(unsigned int p_code) { - for (int i = 0; _ak_to_keycode[i].keysym != KEY_UNKNOWN; i++) { - - if (_ak_to_keycode[i].keycode == p_code) { - - return _ak_to_keycode[i].keysym; - } - } - - return KEY_UNKNOWN; -} - -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jobject obj, jint p_device, jint p_button, jboolean p_pressed) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jclass clazz, jint p_device, jint p_button, jboolean p_pressed) { if (step == 0) return; @@ -1122,7 +280,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env os_android->process_joy_event(jevent); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, jobject obj, jint p_device, jint p_axis, jfloat p_value) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, jclass clazz, jint p_device, jint p_axis, jfloat p_value) { if (step == 0) return; @@ -1135,7 +293,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, os_android->process_joy_event(jevent); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv *env, jobject obj, jint p_device, jint p_hat_x, jint p_hat_y) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv *env, jclass clazz, jint p_device, jint p_hat_x, jint p_hat_y) { if (step == 0) return; @@ -1160,58 +318,59 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv *env, j os_android->process_joy_event(jevent); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyconnectionchanged(JNIEnv *env, jobject obj, jint p_device, jboolean p_connected, jstring p_name) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyconnectionchanged(JNIEnv *env, jclass clazz, jint p_device, jboolean p_connected, jstring p_name) { if (os_android) { String name = jstring_to_string(p_name, env); os_android->joy_connection_changed(p_device, p_connected, name); } } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jobject obj, jint p_scancode, jint p_unicode_char, jboolean p_pressed) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_keycode, jint p_scancode, jint p_unicode_char, jboolean p_pressed) { if (step == 0) return; Ref<InputEventKey> ievent; ievent.instance(); int val = p_unicode_char; - int scancode = android_get_keysym(p_scancode); - ievent->set_scancode(scancode); + int keycode = android_get_keysym(p_keycode); + int phy_keycode = android_get_keysym(p_scancode); + ievent->set_keycode(keycode); + ievent->set_physical_keycode(phy_keycode); ievent->set_unicode(val); ievent->set_pressed(p_pressed); if (val == '\n') { - ievent->set_scancode(KEY_ENTER); + ievent->set_keycode(KEY_ENTER); } else if (val == 61448) { - ievent->set_scancode(KEY_BACKSPACE); + ievent->set_keycode(KEY_BACKSPACE); ievent->set_unicode(KEY_BACKSPACE); } else if (val == 61453) { - ievent->set_scancode(KEY_ENTER); + ievent->set_keycode(KEY_ENTER); ievent->set_unicode(KEY_ENTER); - } else if (p_scancode == 4) { - + } else if (p_keycode == 4) { os_android->main_loop_request_go_back(); } os_android->process_event(ievent); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_accelerometer(JNIEnv *env, jobject obj, jfloat x, jfloat y, jfloat z) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_accelerometer(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z) { accelerometer = Vector3(x, y, z); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gravity(JNIEnv *env, jobject obj, jfloat x, jfloat y, jfloat z) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gravity(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z) { gravity = Vector3(x, y, z); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnetometer(JNIEnv *env, jobject obj, jfloat x, jfloat y, jfloat z) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnetometer(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z) { magnetometer = Vector3(x, y, z); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gyroscope(JNIEnv *env, jobject obj, jfloat x, jfloat y, jfloat z) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gyroscope(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z) { gyroscope = Vector3(x, y, z); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env, jobject obj) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env, jclass clazz) { if (step == 0) return; @@ -1219,7 +378,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env, os_android->main_loop_focusin(); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, jobject obj) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, jclass clazz) { if (step == 0) return; @@ -1227,134 +386,22 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, os_android->main_loop_focusout(); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_audio(JNIEnv *env, jobject obj) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_audio(JNIEnv *env, jclass clazz) { ThreadAndroid::setup_thread(); AudioDriverAndroid::thread_func(env); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_singleton(JNIEnv *env, jobject obj, jstring name, jobject p_object) { - - String singname = jstring_to_string(name, env); - JNISingleton *s = memnew(JNISingleton); - s->set_instance(env->NewGlobalRef(p_object)); - jni_singletons[singname] = s; - - Engine::get_singleton()->add_singleton(Engine::Singleton(singname, s)); - ProjectSettings::get_singleton()->set(singname, s); -} - -static Variant::Type get_jni_type(const String &p_type) { - - static struct { - const char *name; - Variant::Type type; - } _type_to_vtype[] = { - { "void", Variant::NIL }, - { "boolean", Variant::BOOL }, - { "int", Variant::INT }, - { "float", Variant::FLOAT }, - { "double", Variant::FLOAT }, - { "java.lang.String", Variant::STRING }, - { "[I", Variant::PACKED_INT32_ARRAY }, - { "[B", Variant::PACKED_BYTE_ARRAY }, - { "[F", Variant::PACKED_FLOAT32_ARRAY }, - { "[Ljava.lang.String;", Variant::PACKED_STRING_ARRAY }, - { "org.godotengine.godot.Dictionary", Variant::DICTIONARY }, - { NULL, Variant::NIL } - }; - - int idx = 0; - - while (_type_to_vtype[idx].name) { - - if (p_type == _type_to_vtype[idx].name) - return _type_to_vtype[idx].type; - - idx++; - } - - return Variant::NIL; -} - -static const char *get_jni_sig(const String &p_type) { - - static struct { - const char *name; - const char *sig; - } _type_to_vtype[] = { - { "void", "V" }, - { "boolean", "Z" }, - { "int", "I" }, - { "float", "F" }, - { "double", "D" }, - { "java.lang.String", "Ljava/lang/String;" }, - { "org.godotengine.godot.Dictionary", "Lorg/godotengine/godot/Dictionary;" }, - { "[I", "[I" }, - { "[B", "[B" }, - { "[F", "[F" }, - { "[Ljava.lang.String;", "[Ljava/lang/String;" }, - { NULL, "V" } - }; - - int idx = 0; - - while (_type_to_vtype[idx].name) { - - if (p_type == _type_to_vtype[idx].name) - return _type_to_vtype[idx].sig; - - idx++; - } - - return "Ljava/lang/Object;"; -} - -JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv *env, jobject obj, jstring path) { +JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv *env, jclass clazz, jstring path) { String js = jstring_to_string(path, env); return env->NewStringUTF(ProjectSettings::get_singleton()->get(js).operator String().utf8().get_data()); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_method(JNIEnv *env, jobject obj, jstring sname, jstring name, jstring ret, jobjectArray args) { - - String singname = jstring_to_string(sname, env); - - ERR_FAIL_COND(!jni_singletons.has(singname)); - - JNISingleton *s = jni_singletons.get(singname); - - String mname = jstring_to_string(name, env); - String retval = jstring_to_string(ret, env); - Vector<Variant::Type> types; - String cs = "("; - - int stringCount = env->GetArrayLength(args); - - for (int i = 0; i < stringCount; i++) { - - jstring string = (jstring)env->GetObjectArrayElement(args, i); - const String rawString = jstring_to_string(string, env); - types.push_back(get_jni_type(rawString)); - cs += get_jni_sig(rawString); - } - - cs += ")"; - cs += get_jni_sig(retval); - jclass cls = env->GetObjectClass(s->get_instance()); - jmethodID mid = env->GetMethodID(cls, mname.ascii().get_data(), cs.ascii().get_data()); - if (!mid) { - - print_line("Failed getting method ID " + mname); - } - - s->add_method(mname, mid, types, get_jni_type(retval)); -} - -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *env, jobject p_obj, jint ID, jstring method, jobjectArray params) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *env, jclass clazz, jint ID, jstring method, jobjectArray params) { - Object *obj = ObjectDB::get_instance(ID); + Object *obj = ObjectDB::get_instance(ObjectID((uint64_t)ID)); ERR_FAIL_COND(!obj); int res = env->PushLocalFrame(16); @@ -1384,9 +431,9 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *en env->PopLocalFrame(NULL); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jobject p_obj, jint ID, jstring method, jobjectArray params) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jclass clazz, jint ID, jstring method, jobjectArray params) { - Object *obj = ObjectDB::get_instance(ID); + Object *obj = ObjectDB::get_instance(ObjectID((uint64_t)ID)); ERR_FAIL_COND(!obj); int res = env->PushLocalFrame(16); @@ -1410,7 +457,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv * env->PopLocalFrame(NULL); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jobject p_obj, jstring p_permission, jboolean p_result) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result) { String permission = jstring_to_string(p_permission, env); if (permission == "android.permission.RECORD_AUDIO" && p_result) { AudioDriver::get_singleton()->capture_start(); @@ -1438,3 +485,4 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIE os_android->get_main_loop()->notification(MainLoop::NOTIFICATION_APP_PAUSED); } } +} diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index 71d4547f65..a7a5970440 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -37,36 +37,34 @@ // These functions can be called from within JAVA and are the means by which our JAVA implementation calls back into our C++ code. // See java/src/org/godotengine/godot/GodotLib.java for the JAVA side of this (yes that's why we have the long names) extern "C" { -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jobject obj, jobject activity, jobject p_asset_manager, jboolean p_use_apk_expansion); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jobject obj, jobject activity); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jobject obj, jobjectArray p_cmdline); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jobject obj, jint width, jint height); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *env, jobject obj, bool p_32_bits); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jobject obj); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jobject obj); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch(JNIEnv *env, jobject obj, jint ev, jint pointer, jint count, jintArray positions); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_hover(JNIEnv *env, jobject obj, jint p_type, jint p_x, jint p_y); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_doubletap(JNIEnv *env, jobject obj, jint p_x, jint p_y); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_scroll(JNIEnv *env, jobject obj, jint p_x, jint p_y); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jobject obj, jint p_scancode, jint p_unicode_char, jboolean p_pressed); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jobject obj, jint p_device, jint p_button, jboolean p_pressed); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, jobject obj, jint p_device, jint p_axis, jfloat p_value); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv *env, jobject obj, jint p_device, jint p_hat_x, jint p_hat_y); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyconnectionchanged(JNIEnv *env, jobject obj, jint p_device, jboolean p_connected, jstring p_name); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_audio(JNIEnv *env, jobject obj); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_accelerometer(JNIEnv *env, jobject obj, jfloat x, jfloat y, jfloat z); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gravity(JNIEnv *env, jobject obj, jfloat x, jfloat y, jfloat z); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnetometer(JNIEnv *env, jobject obj, jfloat x, jfloat y, jfloat z); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gyroscope(JNIEnv *env, jobject obj, jfloat x, jfloat y, jfloat z); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env, jobject obj); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, jobject obj); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_singleton(JNIEnv *env, jobject obj, jstring name, jobject p_object); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_method(JNIEnv *env, jobject obj, jstring sname, jstring name, jstring ret, jobjectArray args); -JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv *env, jobject obj, jstring path); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *env, jobject p_obj, jint ID, jstring method, jobjectArray params); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jobject p_obj, jint ID, jstring method, jobjectArray params); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jobject obj, jint p_height); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jobject p_obj, jstring p_permission, jboolean p_result); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject activity, jobject p_asset_manager, jboolean p_use_apk_expansion); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz, jobject activity); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jclass clazz, jint width, jint height); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *env, jclass clazz, jboolean p_32_bits); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jclass clazz); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint count, jintArray positions); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_hover(JNIEnv *env, jclass clazz, jint p_type, jint p_x, jint p_y); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_doubletap(JNIEnv *env, jclass clazz, jint p_x, jint p_y); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_scroll(JNIEnv *env, jclass clazz, jint p_x, jint p_y); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_keycode, jint p_scancode, jint p_unicode_char, jboolean p_pressed); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jclass clazz, jint p_device, jint p_button, jboolean p_pressed); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, jclass clazz, jint p_device, jint p_axis, jfloat p_value); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv *env, jclass clazz, jint p_device, jint p_hat_x, jint p_hat_y); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyconnectionchanged(JNIEnv *env, jclass clazz, jint p_device, jboolean p_connected, jstring p_name); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_audio(JNIEnv *env, jclass clazz); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_accelerometer(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gravity(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnetometer(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gyroscope(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env, jclass clazz); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, jclass clazz); +JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv *env, jclass clazz, jstring path); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *env, jclass clazz, jint ID, jstring method, jobjectArray params); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jclass clazz, jint ID, jstring method, jobjectArray params); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jclass clazz, jint p_height); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz); } diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 9ac91b8ef6..7b677c186e 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -66,6 +66,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance) { _is_activity_resumed = p_env->GetMethodID(cls, "isActivityResumed", "()Z"); _vibrate = p_env->GetMethodID(cls, "vibrate", "(I)V"); _get_input_fallback_mapping = p_env->GetMethodID(cls, "getInputFallbackMapping", "()Ljava/lang/String;"); + _on_gl_godot_main_loop_started = p_env->GetMethodID(cls, "onGLGodotMainLoopStarted", "()V"); } GodotJavaWrapper::~GodotJavaWrapper() { @@ -107,6 +108,15 @@ void GodotJavaWrapper::on_video_init(JNIEnv *p_env) { p_env->CallVoidMethod(godot_instance, _on_video_init); } +void GodotJavaWrapper::on_gl_godot_main_loop_started(JNIEnv *p_env) { + if (_on_gl_godot_main_loop_started) { + if (p_env == NULL) { + p_env = ThreadAndroid::get_env(); + } + } + p_env->CallVoidMethod(godot_instance, _on_gl_godot_main_loop_started); +} + void GodotJavaWrapper::restart(JNIEnv *p_env) { if (_restart) if (p_env == NULL) diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index f378b1ea38..cdab2ecc9c 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -61,6 +61,7 @@ private: jmethodID _is_activity_resumed = 0; jmethodID _vibrate = 0; jmethodID _get_input_fallback_mapping = 0; + jmethodID _on_gl_godot_main_loop_started = 0; public: GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance); @@ -72,6 +73,7 @@ public: jobject get_class_loader(); void on_video_init(JNIEnv *p_env = NULL); + void on_gl_godot_main_loop_started(JNIEnv *p_env = NULL); void restart(JNIEnv *p_env = NULL); void force_quit(JNIEnv *p_env = NULL); void set_keep_screen_on(bool p_enabled); diff --git a/platform/android/jni_utils.cpp b/platform/android/jni_utils.cpp new file mode 100644 index 0000000000..3fa4e80884 --- /dev/null +++ b/platform/android/jni_utils.cpp @@ -0,0 +1,434 @@ +/*************************************************************************/ +/* jni_utils.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "jni_utils.h" + +jvalret _variant_to_jvalue(JNIEnv *env, Variant::Type p_type, const Variant *p_arg, bool force_jobject) { + + jvalret v; + + switch (p_type) { + + case Variant::BOOL: { + + if (force_jobject) { + jclass bclass = env->FindClass("java/lang/Boolean"); + jmethodID ctor = env->GetMethodID(bclass, "<init>", "(Z)V"); + jvalue val; + val.z = (bool)(*p_arg); + jobject obj = env->NewObjectA(bclass, ctor, &val); + v.val.l = obj; + v.obj = obj; + env->DeleteLocalRef(bclass); + } else { + v.val.z = *p_arg; + }; + } break; + case Variant::INT: { + + if (force_jobject) { + + jclass bclass = env->FindClass("java/lang/Integer"); + jmethodID ctor = env->GetMethodID(bclass, "<init>", "(I)V"); + jvalue val; + val.i = (int)(*p_arg); + jobject obj = env->NewObjectA(bclass, ctor, &val); + v.val.l = obj; + v.obj = obj; + env->DeleteLocalRef(bclass); + + } else { + v.val.i = *p_arg; + }; + } break; + case Variant::FLOAT: { + + if (force_jobject) { + + jclass bclass = env->FindClass("java/lang/Double"); + jmethodID ctor = env->GetMethodID(bclass, "<init>", "(D)V"); + jvalue val; + val.d = (double)(*p_arg); + jobject obj = env->NewObjectA(bclass, ctor, &val); + v.val.l = obj; + v.obj = obj; + env->DeleteLocalRef(bclass); + + } else { + v.val.f = *p_arg; + }; + } break; + case Variant::STRING: { + + String s = *p_arg; + jstring jStr = env->NewStringUTF(s.utf8().get_data()); + v.val.l = jStr; + v.obj = jStr; + } break; + case Variant::PACKED_STRING_ARRAY: { + + Vector<String> sarray = *p_arg; + jobjectArray arr = env->NewObjectArray(sarray.size(), env->FindClass("java/lang/String"), env->NewStringUTF("")); + + for (int j = 0; j < sarray.size(); j++) { + + jstring str = env->NewStringUTF(sarray[j].utf8().get_data()); + env->SetObjectArrayElement(arr, j, str); + env->DeleteLocalRef(str); + } + v.val.l = arr; + v.obj = arr; + + } break; + + case Variant::DICTIONARY: { + + Dictionary dict = *p_arg; + jclass dclass = env->FindClass("org/godotengine/godot/Dictionary"); + jmethodID ctor = env->GetMethodID(dclass, "<init>", "()V"); + jobject jdict = env->NewObject(dclass, ctor); + + Array keys = dict.keys(); + + jobjectArray jkeys = env->NewObjectArray(keys.size(), env->FindClass("java/lang/String"), env->NewStringUTF("")); + for (int j = 0; j < keys.size(); j++) { + jstring str = env->NewStringUTF(String(keys[j]).utf8().get_data()); + env->SetObjectArrayElement(jkeys, j, str); + env->DeleteLocalRef(str); + }; + + jmethodID set_keys = env->GetMethodID(dclass, "set_keys", "([Ljava/lang/String;)V"); + jvalue val; + val.l = jkeys; + env->CallVoidMethodA(jdict, set_keys, &val); + env->DeleteLocalRef(jkeys); + + jobjectArray jvalues = env->NewObjectArray(keys.size(), env->FindClass("java/lang/Object"), NULL); + + for (int j = 0; j < keys.size(); j++) { + Variant var = dict[keys[j]]; + jvalret v = _variant_to_jvalue(env, var.get_type(), &var, true); + env->SetObjectArrayElement(jvalues, j, v.val.l); + if (v.obj) { + env->DeleteLocalRef(v.obj); + } + }; + + jmethodID set_values = env->GetMethodID(dclass, "set_values", "([Ljava/lang/Object;)V"); + val.l = jvalues; + env->CallVoidMethodA(jdict, set_values, &val); + env->DeleteLocalRef(jvalues); + env->DeleteLocalRef(dclass); + + v.val.l = jdict; + v.obj = jdict; + } break; + + case Variant::PACKED_INT32_ARRAY: { + + Vector<int> array = *p_arg; + jintArray arr = env->NewIntArray(array.size()); + const int *r = array.ptr(); + env->SetIntArrayRegion(arr, 0, array.size(), r); + v.val.l = arr; + v.obj = arr; + + } break; + case Variant::PACKED_BYTE_ARRAY: { + Vector<uint8_t> array = *p_arg; + jbyteArray arr = env->NewByteArray(array.size()); + const uint8_t *r = array.ptr(); + env->SetByteArrayRegion(arr, 0, array.size(), reinterpret_cast<const signed char *>(r)); + v.val.l = arr; + v.obj = arr; + + } break; + case Variant::PACKED_FLOAT32_ARRAY: { + + Vector<float> array = *p_arg; + jfloatArray arr = env->NewFloatArray(array.size()); + const float *r = array.ptr(); + env->SetFloatArrayRegion(arr, 0, array.size(), r); + v.val.l = arr; + v.obj = arr; + + } break; +#ifndef _MSC_VER +#warning This is missing 64 bits arrays, I have no idea how to do it in JNI +#endif + + default: { + + v.val.i = 0; + } break; + } + return v; +} + +String _get_class_name(JNIEnv *env, jclass cls, bool *array) { + + jclass cclass = env->FindClass("java/lang/Class"); + jmethodID getName = env->GetMethodID(cclass, "getName", "()Ljava/lang/String;"); + jstring clsName = (jstring)env->CallObjectMethod(cls, getName); + + if (array) { + jmethodID isArray = env->GetMethodID(cclass, "isArray", "()Z"); + jboolean isarr = env->CallBooleanMethod(cls, isArray); + (*array) = isarr ? true : false; + } + String name = jstring_to_string(clsName, env); + env->DeleteLocalRef(clsName); + + return name; +} + +Variant _jobject_to_variant(JNIEnv *env, jobject obj) { + + if (obj == NULL) { + return Variant(); + } + + jclass c = env->GetObjectClass(obj); + bool array; + String name = _get_class_name(env, c, &array); + + if (name == "java.lang.String") { + + return jstring_to_string((jstring)obj, env); + }; + + if (name == "[Ljava.lang.String;") { + + jobjectArray arr = (jobjectArray)obj; + int stringCount = env->GetArrayLength(arr); + Vector<String> sarr; + + for (int i = 0; i < stringCount; i++) { + jstring string = (jstring)env->GetObjectArrayElement(arr, i); + sarr.push_back(jstring_to_string(string, env)); + env->DeleteLocalRef(string); + } + + return sarr; + }; + + if (name == "java.lang.Boolean") { + + jmethodID boolValue = env->GetMethodID(c, "booleanValue", "()Z"); + bool ret = env->CallBooleanMethod(obj, boolValue); + return ret; + }; + + if (name == "java.lang.Integer" || name == "java.lang.Long") { + + jclass nclass = env->FindClass("java/lang/Number"); + jmethodID longValue = env->GetMethodID(nclass, "longValue", "()J"); + jlong ret = env->CallLongMethod(obj, longValue); + return ret; + }; + + if (name == "[I") { + + jintArray arr = (jintArray)obj; + int fCount = env->GetArrayLength(arr); + Vector<int> sarr; + sarr.resize(fCount); + + int *w = sarr.ptrw(); + env->GetIntArrayRegion(arr, 0, fCount, w); + return sarr; + }; + + if (name == "[B") { + + jbyteArray arr = (jbyteArray)obj; + int fCount = env->GetArrayLength(arr); + Vector<uint8_t> sarr; + sarr.resize(fCount); + + uint8_t *w = sarr.ptrw(); + env->GetByteArrayRegion(arr, 0, fCount, reinterpret_cast<signed char *>(w)); + return sarr; + }; + + if (name == "java.lang.Float" || name == "java.lang.Double") { + + jclass nclass = env->FindClass("java/lang/Number"); + jmethodID doubleValue = env->GetMethodID(nclass, "doubleValue", "()D"); + double ret = env->CallDoubleMethod(obj, doubleValue); + return ret; + }; + + if (name == "[D") { + + jdoubleArray arr = (jdoubleArray)obj; + int fCount = env->GetArrayLength(arr); + PackedFloat32Array sarr; + sarr.resize(fCount); + + real_t *w = sarr.ptrw(); + + for (int i = 0; i < fCount; i++) { + + double n; + env->GetDoubleArrayRegion(arr, i, 1, &n); + w[i] = n; + }; + return sarr; + }; + + if (name == "[F") { + + jfloatArray arr = (jfloatArray)obj; + int fCount = env->GetArrayLength(arr); + PackedFloat32Array sarr; + sarr.resize(fCount); + + real_t *w = sarr.ptrw(); + + for (int i = 0; i < fCount; i++) { + + float n; + env->GetFloatArrayRegion(arr, i, 1, &n); + w[i] = n; + }; + return sarr; + }; + + if (name == "[Ljava.lang.Object;") { + + jobjectArray arr = (jobjectArray)obj; + int objCount = env->GetArrayLength(arr); + Array varr; + + for (int i = 0; i < objCount; i++) { + jobject jobj = env->GetObjectArrayElement(arr, i); + Variant v = _jobject_to_variant(env, jobj); + varr.push_back(v); + env->DeleteLocalRef(jobj); + } + + return varr; + }; + + if (name == "java.util.HashMap" || name == "org.godotengine.godot.Dictionary") { + + Dictionary ret; + jclass oclass = c; + jmethodID get_keys = env->GetMethodID(oclass, "get_keys", "()[Ljava/lang/String;"); + jobjectArray arr = (jobjectArray)env->CallObjectMethod(obj, get_keys); + + PackedStringArray keys = _jobject_to_variant(env, arr); + env->DeleteLocalRef(arr); + + jmethodID get_values = env->GetMethodID(oclass, "get_values", "()[Ljava/lang/Object;"); + arr = (jobjectArray)env->CallObjectMethod(obj, get_values); + + Array vals = _jobject_to_variant(env, arr); + env->DeleteLocalRef(arr); + + for (int i = 0; i < keys.size(); i++) { + + ret[keys[i]] = vals[i]; + }; + + return ret; + }; + + env->DeleteLocalRef(c); + + return Variant(); +} + +Variant::Type get_jni_type(const String &p_type) { + + static struct { + const char *name; + Variant::Type type; + } _type_to_vtype[] = { + { "void", Variant::NIL }, + { "boolean", Variant::BOOL }, + { "int", Variant::INT }, + { "float", Variant::FLOAT }, + { "double", Variant::FLOAT }, + { "java.lang.String", Variant::STRING }, + { "[I", Variant::PACKED_INT32_ARRAY }, + { "[B", Variant::PACKED_BYTE_ARRAY }, + { "[F", Variant::PACKED_FLOAT32_ARRAY }, + { "[Ljava.lang.String;", Variant::PACKED_STRING_ARRAY }, + { "org.godotengine.godot.Dictionary", Variant::DICTIONARY }, + { NULL, Variant::NIL } + }; + + int idx = 0; + + while (_type_to_vtype[idx].name) { + + if (p_type == _type_to_vtype[idx].name) + return _type_to_vtype[idx].type; + + idx++; + } + + return Variant::NIL; +} + +const char *get_jni_sig(const String &p_type) { + + static struct { + const char *name; + const char *sig; + } _type_to_vtype[] = { + { "void", "V" }, + { "boolean", "Z" }, + { "int", "I" }, + { "float", "F" }, + { "double", "D" }, + { "java.lang.String", "Ljava/lang/String;" }, + { "org.godotengine.godot.Dictionary", "Lorg/godotengine/godot/Dictionary;" }, + { "[I", "[I" }, + { "[B", "[B" }, + { "[F", "[F" }, + { "[Ljava.lang.String;", "[Ljava/lang/String;" }, + { NULL, "V" } + }; + + int idx = 0; + + while (_type_to_vtype[idx].name) { + + if (p_type == _type_to_vtype[idx].name) + return _type_to_vtype[idx].sig; + + idx++; + } + + return "Ljava/lang/Object;"; +} diff --git a/platform/android/jni_utils.h b/platform/android/jni_utils.h new file mode 100644 index 0000000000..925340a680 --- /dev/null +++ b/platform/android/jni_utils.h @@ -0,0 +1,242 @@ +/*************************************************************************/ +/* jni_utils.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef JNI_UTILS_H +#define JNI_UTILS_H + +#include "string_android.h" +#include <core/engine.h> +#include <core/variant.h> +#include <jni.h> + +struct jvalret { + + jobject obj; + jvalue val; + jvalret() { obj = NULL; } +}; + +jvalret _variant_to_jvalue(JNIEnv *env, Variant::Type p_type, const Variant *p_arg, bool force_jobject = false); + +String _get_class_name(JNIEnv *env, jclass cls, bool *array); + +Variant _jobject_to_variant(JNIEnv *env, jobject obj); + +Variant::Type get_jni_type(const String &p_type); + +const char *get_jni_sig(const String &p_type); + +class JNISingleton : public Object { + + GDCLASS(JNISingleton, Object); + + struct MethodData { + + jmethodID method; + Variant::Type ret_type; + Vector<Variant::Type> argtypes; + }; + + jobject instance; + Map<StringName, MethodData> method_map; + +public: + virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { + + ERR_FAIL_COND_V(!instance, Variant()); + + r_error.error = Callable::CallError::CALL_OK; + + Map<StringName, MethodData>::Element *E = method_map.find(p_method); + if (!E) { + + r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; + return Variant(); + } + + int ac = E->get().argtypes.size(); + if (ac < p_argcount) { + + r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS; + r_error.argument = ac; + return Variant(); + } + + if (ac > p_argcount) { + + r_error.error = Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS; + r_error.argument = ac; + return Variant(); + } + + for (int i = 0; i < p_argcount; i++) { + + if (!Variant::can_convert(p_args[i]->get_type(), E->get().argtypes[i])) { + + r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT; + r_error.argument = i; + r_error.expected = E->get().argtypes[i]; + } + } + + jvalue *v = NULL; + + if (p_argcount) { + + v = (jvalue *)alloca(sizeof(jvalue) * p_argcount); + } + + JNIEnv *env = ThreadAndroid::get_env(); + + int res = env->PushLocalFrame(16); + + ERR_FAIL_COND_V(res != 0, Variant()); + + List<jobject> to_erase; + for (int i = 0; i < p_argcount; i++) { + + jvalret vr = _variant_to_jvalue(env, E->get().argtypes[i], p_args[i]); + v[i] = vr.val; + if (vr.obj) + to_erase.push_back(vr.obj); + } + + Variant ret; + + switch (E->get().ret_type) { + + case Variant::NIL: { + + env->CallVoidMethodA(instance, E->get().method, v); + } break; + case Variant::BOOL: { + + ret = env->CallBooleanMethodA(instance, E->get().method, v) == JNI_TRUE; + } break; + case Variant::INT: { + + ret = env->CallIntMethodA(instance, E->get().method, v); + } break; + case Variant::FLOAT: { + + ret = env->CallFloatMethodA(instance, E->get().method, v); + } break; + case Variant::STRING: { + + jobject o = env->CallObjectMethodA(instance, E->get().method, v); + ret = jstring_to_string((jstring)o, env); + env->DeleteLocalRef(o); + } break; + case Variant::PACKED_STRING_ARRAY: { + + jobjectArray arr = (jobjectArray)env->CallObjectMethodA(instance, E->get().method, v); + + ret = _jobject_to_variant(env, arr); + + env->DeleteLocalRef(arr); + } break; + case Variant::PACKED_INT32_ARRAY: { + + jintArray arr = (jintArray)env->CallObjectMethodA(instance, E->get().method, v); + + int fCount = env->GetArrayLength(arr); + Vector<int> sarr; + sarr.resize(fCount); + + int *w = sarr.ptrw(); + env->GetIntArrayRegion(arr, 0, fCount, w); + ret = sarr; + env->DeleteLocalRef(arr); + } break; + case Variant::PACKED_FLOAT32_ARRAY: { + + jfloatArray arr = (jfloatArray)env->CallObjectMethodA(instance, E->get().method, v); + + int fCount = env->GetArrayLength(arr); + Vector<float> sarr; + sarr.resize(fCount); + + float *w = sarr.ptrw(); + env->GetFloatArrayRegion(arr, 0, fCount, w); + ret = sarr; + env->DeleteLocalRef(arr); + } break; + +#ifndef _MSC_VER +#warning This is missing 64 bits arrays, I have no idea how to do it in JNI +#endif + case Variant::DICTIONARY: { + + jobject obj = env->CallObjectMethodA(instance, E->get().method, v); + ret = _jobject_to_variant(env, obj); + env->DeleteLocalRef(obj); + + } break; + default: { + + env->PopLocalFrame(NULL); + ERR_FAIL_V(Variant()); + } break; + } + + while (to_erase.size()) { + env->DeleteLocalRef(to_erase.front()->get()); + to_erase.pop_front(); + } + + env->PopLocalFrame(NULL); + + return ret; + } + + jobject get_instance() const { + + return instance; + } + void set_instance(jobject p_instance) { + + instance = p_instance; + } + + void add_method(const StringName &p_name, jmethodID p_method, const Vector<Variant::Type> &p_args, Variant::Type p_ret_type) { + + MethodData md; + md.method = p_method; + md.argtypes = p_args; + md.ret_type = p_ret_type; + method_map[p_name] = md; + } + + JNISingleton() { + instance = NULL; + } +}; + +#endif // JNI_UTILS_H diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 15e3ac48c7..7e2b0d948e 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -32,7 +32,9 @@ #include "core/io/file_access_buffered_fa.h" #include "core/project_settings.h" +#if defined(OPENGL_ENABLED) #include "drivers/gles2/rasterizer_gles2.h" +#endif #include "drivers/unix/dir_access_unix.h" #include "drivers/unix/file_access_unix.h" #include "file_access_android.h" @@ -120,23 +122,27 @@ int OS_Android::get_current_video_driver() const { Error OS_Android::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) { - bool gl_initialization_error = false; - // FIXME: Add Vulkan support. Readd fallback code from Vulkan to GLES2? - if (RasterizerGLES2::is_viable() == OK) { - RasterizerGLES2::register_config(); - RasterizerGLES2::make_current(); - } else { - gl_initialization_error = true; - } - - if (gl_initialization_error) { - OS::get_singleton()->alert("Your device does not support any of the supported OpenGL versions.\n" - "Please try updating your Android version.", - "Unable to initialize video driver"); - return ERR_UNAVAILABLE; +#if defined(OPENGL_ENABLED) + if (video_driver_index == VIDEO_DRIVER_GLES2) { + bool gl_initialization_error = false; + + if (RasterizerGLES2::is_viable() == OK) { + RasterizerGLES2::register_config(); + RasterizerGLES2::make_current(); + } else { + gl_initialization_error = true; + } + + if (gl_initialization_error) { + OS::get_singleton()->alert("Your device does not support any of the supported OpenGL versions.\n" + "Please try updating your Android version.", + "Unable to initialize video driver"); + return ERR_UNAVAILABLE; + } } +#endif video_driver_index = p_video_driver; @@ -753,6 +759,8 @@ OS_Android::OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_god //rasterizer = NULL; use_gl2 = false; + visual_server = NULL; + godot_java = p_godot_java; godot_io_java = p_godot_io_java; diff --git a/platform/android/plugin/godot_plugin_jni.cpp b/platform/android/plugin/godot_plugin_jni.cpp new file mode 100644 index 0000000000..7413236e5d --- /dev/null +++ b/platform/android/plugin/godot_plugin_jni.cpp @@ -0,0 +1,115 @@ +/*************************************************************************/ +/* godot_plugin_jni.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "godot_plugin_jni.h" + +#include <core/engine.h> +#include <core/error_macros.h> +#include <core/project_settings.h> +#include <platform/android/jni_utils.h> +#include <platform/android/string_android.h> + +static HashMap<String, JNISingleton *> jni_singletons; + +extern "C" { + +JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterSingleton(JNIEnv *env, jobject obj, jstring name) { + + String singname = jstring_to_string(name, env); + JNISingleton *s = memnew(JNISingleton); + s->set_instance(env->NewGlobalRef(obj)); + jni_singletons[singname] = s; + + Engine::get_singleton()->add_singleton(Engine::Singleton(singname, s)); + ProjectSettings::get_singleton()->set(singname, s); +} + +JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterMethod(JNIEnv *env, jobject obj, jstring sname, jstring name, jstring ret, jobjectArray args) { + + String singname = jstring_to_string(sname, env); + + ERR_FAIL_COND(!jni_singletons.has(singname)); + + JNISingleton *s = jni_singletons.get(singname); + + String mname = jstring_to_string(name, env); + String retval = jstring_to_string(ret, env); + Vector<Variant::Type> types; + String cs = "("; + + int stringCount = env->GetArrayLength(args); + + for (int i = 0; i < stringCount; i++) { + + jstring string = (jstring)env->GetObjectArrayElement(args, i); + const String rawString = jstring_to_string(string, env); + types.push_back(get_jni_type(rawString)); + cs += get_jni_sig(rawString); + } + + cs += ")"; + cs += get_jni_sig(retval); + jclass cls = env->GetObjectClass(s->get_instance()); + jmethodID mid = env->GetMethodID(cls, mname.ascii().get_data(), cs.ascii().get_data()); + if (!mid) { + + print_line("Failed getting method ID " + mname); + } + + s->add_method(mname, mid, types, get_jni_type(retval)); +} + +JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterGDNativeLibraries(JNIEnv *env, jobject obj, jobjectArray gdnlib_paths) { + int gdnlib_count = env->GetArrayLength(gdnlib_paths); + if (gdnlib_count == 0) { + return; + } + + // Retrieve the current list of gdnative libraries. + Array singletons = Array(); + if (ProjectSettings::get_singleton()->has_setting("gdnative/singletons")) { + singletons = ProjectSettings::get_singleton()->get("gdnative/singletons"); + } + + // Insert the libraries provided by the plugin + for (int i = 0; i < gdnlib_count; i++) { + jstring relative_path = (jstring)env->GetObjectArrayElement(gdnlib_paths, i); + + String path = "res://" + jstring_to_string(relative_path, env); + if (!singletons.has(path)) { + singletons.push_back(path); + } + env->DeleteLocalRef(relative_path); + } + + // Insert the updated list back into project settings. + ProjectSettings::get_singleton()->set("gdnative/singletons", singletons); +} +} diff --git a/platform/iphone/semaphore_iphone.h b/platform/android/plugin/godot_plugin_jni.h index 54ff3c17f9..0d613d3bfe 100644 --- a/platform/iphone/semaphore_iphone.h +++ b/platform/android/plugin/godot_plugin_jni.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* semaphore_iphone.h */ +/* godot_plugin_jni.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,32 +28,16 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef SEMAPHORE_IPHONE_H -#define SEMAPHORE_IPHONE_H +#ifndef GODOT_PLUGIN_JNI_H +#define GODOT_PLUGIN_JNI_H -struct cgsem { - int pipefd[2]; -}; +#include <android/log.h> +#include <jni.h> -typedef struct cgsem cgsem_t; +extern "C" { +JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterSingleton(JNIEnv *env, jobject obj, jstring name); +JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterMethod(JNIEnv *env, jobject obj, jstring sname, jstring name, jstring ret, jobjectArray args); +JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterGDNativeLibraries(JNIEnv *env, jobject obj, jobjectArray gdnlib_paths); +} -#include "core/os/semaphore.h" - -class SemaphoreIphone : public SemaphoreOld { - - mutable cgsem_t sem; - - static SemaphoreOld *create_semaphore_iphone(); - -public: - virtual Error wait(); - virtual Error post(); - virtual int get() const; - - static void make_default(); - SemaphoreIphone(); - - ~SemaphoreIphone(); -}; - -#endif // SEMAPHORE_IPHONE_H +#endif // GODOT_PLUGIN_JNI_H diff --git a/platform/osx/semaphore_osx.cpp b/platform/android/vulkan/vk_renderer_jni.cpp index e4e5991637..3026e7daad 100644 --- a/platform/osx/semaphore_osx.cpp +++ b/platform/android/vulkan/vk_renderer_jni.cpp @@ -1,5 +1,5 @@ /*************************************************************************/ -/* semaphore_osx.cpp */ +/* vk_renderer_jni.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,80 +28,31 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "semaphore_osx.h" +#include "vk_renderer_jni.h" -#include <fcntl.h> -#include <unistd.h> +extern "C" { -void cgsem_init(cgsem_t *cgsem) { - int flags, fd, i; - - pipe(cgsem->pipefd); - - /* Make the pipes FD_CLOEXEC to allow them to close should we call - * execv on restart. */ - for (i = 0; i < 2; i++) { - fd = cgsem->pipefd[i]; - flags = fcntl(fd, F_GETFD, 0); - flags |= FD_CLOEXEC; - fcntl(fd, F_SETFD, flags); - } -} - -void cgsem_post(cgsem_t *cgsem) { - const char buf = 1; - - write(cgsem->pipefd[1], &buf, 1); -} - -void cgsem_wait(cgsem_t *cgsem) { - char buf; - - read(cgsem->pipefd[0], &buf, 1); -} - -void cgsem_destroy(cgsem_t *cgsem) { - close(cgsem->pipefd[1]); - close(cgsem->pipefd[0]); -} - -#include "core/os/memory.h" - -#include <errno.h> - -Error SemaphoreOSX::wait() { - - cgsem_wait(&sem); - return OK; +JNIEXPORT void JNICALL Java_org_godotengine_godot_vulkan_VkRenderer_nativeOnVkSurfaceCreated(JNIEnv *env, jobject obj, jobject j_surface) { + // TODO: complete } -Error SemaphoreOSX::post() { - - cgsem_post(&sem); - - return OK; +JNIEXPORT void JNICALL Java_org_godotengine_godot_vulkan_VkRenderer_nativeOnVkSurfaceChanged(JNIEnv *env, jobject object, jobject j_surface, jint width, jint height) { + // TODO: complete } -int SemaphoreOSX::get() const { - return 0; +JNIEXPORT void JNICALL Java_org_godotengine_godot_vulkan_VkRenderer_nativeOnVkResume(JNIEnv *env, jobject obj) { + // TODO: complete } -SemaphoreOld *SemaphoreOSX::create_semaphore_osx() { - - return memnew(SemaphoreOSX); +JNIEXPORT void JNICALL Java_org_godotengine_godot_vulkan_VkRenderer_nativeOnVkDrawFrame(JNIEnv *env, jobject obj) { + // TODO: complete } -void SemaphoreOSX::make_default() { - - create_func = create_semaphore_osx; +JNIEXPORT void JNICALL Java_org_godotengine_godot_vulkan_VkRenderer_nativeOnVkPause(JNIEnv *env, jobject obj) { + // TODO: complete } -SemaphoreOSX::SemaphoreOSX() { - - cgsem_init(&sem); +JNIEXPORT void JNICALL Java_org_godotengine_godot_vulkan_VkRenderer_nativeOnVkDestroy(JNIEnv *env, jobject obj) { + // TODO: complete } - -SemaphoreOSX::~SemaphoreOSX() { - - cgsem_destroy(&sem); } diff --git a/platform/iphone/semaphore_iphone.cpp b/platform/android/vulkan/vk_renderer_jni.h index 0461f58c40..017766fea2 100644 --- a/platform/iphone/semaphore_iphone.cpp +++ b/platform/android/vulkan/vk_renderer_jni.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* semaphore_iphone.cpp */ +/* vk_renderer_jni.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,85 +28,19 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "semaphore_iphone.h" +#ifndef VK_RENDERER_JNI_H +#define VK_RENDERER_JNI_H -#include <fcntl.h> -#include <unistd.h> +#include <android/log.h> +#include <jni.h> -void cgsem_init(cgsem_t *); -void cgsem_post(cgsem_t *); -void cgsem_wait(cgsem_t *); -void cgsem_destroy(cgsem_t *); - -void cgsem_init(cgsem_t *cgsem) { - int flags, fd, i; - - pipe(cgsem->pipefd); - - /* Make the pipes FD_CLOEXEC to allow them to close should we call - * execv on restart. */ - for (i = 0; i < 2; i++) { - fd = cgsem->pipefd[i]; - flags = fcntl(fd, F_GETFD, 0); - flags |= FD_CLOEXEC; - fcntl(fd, F_SETFD, flags); - } -} - -void cgsem_post(cgsem_t *cgsem) { - const char buf = 1; - - write(cgsem->pipefd[1], &buf, 1); -} - -void cgsem_wait(cgsem_t *cgsem) { - char buf; - - read(cgsem->pipefd[0], &buf, 1); +extern "C" { +JNIEXPORT void JNICALL Java_org_godotengine_godot_vulkan_VkRenderer_nativeOnVkSurfaceCreated(JNIEnv *env, jobject obj, jobject j_surface); +JNIEXPORT void JNICALL Java_org_godotengine_godot_vulkan_VkRenderer_nativeOnVkSurfaceChanged(JNIEnv *env, jobject object, jobject j_surface, jint width, jint height); +JNIEXPORT void JNICALL Java_org_godotengine_godot_vulkan_VkRenderer_nativeOnVkResume(JNIEnv *env, jobject obj); +JNIEXPORT void JNICALL Java_org_godotengine_godot_vulkan_VkRenderer_nativeOnVkDrawFrame(JNIEnv *env, jobject obj); +JNIEXPORT void JNICALL Java_org_godotengine_godot_vulkan_VkRenderer_nativeOnVkPause(JNIEnv *env, jobject obj); +JNIEXPORT void JNICALL Java_org_godotengine_godot_vulkan_VkRenderer_nativeOnVkDestroy(JNIEnv *env, jobject obj); } -void cgsem_destroy(cgsem_t *cgsem) { - close(cgsem->pipefd[1]); - close(cgsem->pipefd[0]); -} - -#include "core/os/memory.h" - -#include <errno.h> - -Error SemaphoreIphone::wait() { - - cgsem_wait(&sem); - return OK; -} - -Error SemaphoreIphone::post() { - - cgsem_post(&sem); - - return OK; -} -int SemaphoreIphone::get() const { - - return 0; -} - -SemaphoreOld *SemaphoreIphone::create_semaphore_iphone() { - - return memnew(SemaphoreIphone); -} - -void SemaphoreIphone::make_default() { - - create_func = create_semaphore_iphone; -} - -SemaphoreIphone::SemaphoreIphone() { - - cgsem_init(&sem); -} - -SemaphoreIphone::~SemaphoreIphone() { - - cgsem_destroy(&sem); -} +#endif // VK_RENDERER_JNI_H diff --git a/platform/haiku/audio_driver_media_kit.cpp b/platform/haiku/audio_driver_media_kit.cpp index b7f6b57244..0a5df14743 100644 --- a/platform/haiku/audio_driver_media_kit.cpp +++ b/platform/haiku/audio_driver_media_kit.cpp @@ -67,7 +67,6 @@ Error AudioDriverMediaKit::init() { ERR_FAIL_COND_V(player == NULL, ERR_CANT_OPEN); } - mutex = Mutex::create(); player->Start(); return OK; @@ -108,14 +107,14 @@ void AudioDriverMediaKit::lock() { if (!mutex) return; - mutex->lock(); + mutex.lock(); } void AudioDriverMediaKit::unlock() { if (!mutex) return; - mutex->unlock(); + mutex.unlock(); } void AudioDriverMediaKit::finish() { @@ -124,15 +123,9 @@ void AudioDriverMediaKit::finish() { if (samples_in) { memdelete_arr(samples_in); }; - - if (mutex) { - memdelete(mutex); - mutex = NULL; - } } AudioDriverMediaKit::AudioDriverMediaKit() { - mutex = NULL; player = NULL; } diff --git a/platform/haiku/audio_driver_media_kit.h b/platform/haiku/audio_driver_media_kit.h index 06a362a89e..8272780fa7 100644 --- a/platform/haiku/audio_driver_media_kit.h +++ b/platform/haiku/audio_driver_media_kit.h @@ -40,7 +40,7 @@ #include <SoundPlayer.h> class AudioDriverMediaKit : public AudioDriver { - Mutex *mutex; + Mutex mutex; BSoundPlayer *player; static int32_t *samples_in; diff --git a/platform/haiku/haiku_direct_window.cpp b/platform/haiku/haiku_direct_window.cpp index 3c2b7f8d10..2d7efe6b61 100644 --- a/platform/haiku/haiku_direct_window.cpp +++ b/platform/haiku/haiku_direct_window.cpp @@ -273,7 +273,8 @@ void HaikuDirectWindow::HandleKeyboardEvent(BMessage *message) { event.instance(); GetKeyModifierState(event, modifiers); event->set_pressed(message->what == B_KEY_DOWN); - event->set_scancode(KeyMappingHaiku::get_keysym(raw_char, key)); + event->set_keycode(KeyMappingHaiku::get_keysym(raw_char, key)); + event->set_physical_keycode(KeyMappingHaiku::get_keysym(raw_char, key)); event->set_echo(message->HasInt32("be:key_repeat")); event->set_unicode(0); @@ -283,8 +284,9 @@ void HaikuDirectWindow::HandleKeyboardEvent(BMessage *message) { } //make it consistent across platforms. - if (event->get_scancode() == KEY_BACKTAB) { - event->set_scancode(KEY_TAB); + if (event->get_keycode() == KEY_BACKTAB) { + event->set_keycode(KEY_TAB); + event->set_physical_keycode(KEY_TAB); event->set_shift(true); } diff --git a/platform/iphone/os_iphone.cpp b/platform/iphone/os_iphone.cpp index db203ff2b3..497f2f747d 100644 --- a/platform/iphone/os_iphone.cpp +++ b/platform/iphone/os_iphone.cpp @@ -100,7 +100,6 @@ String OSIPhone::get_unique_id() const { void OSIPhone::initialize_core() { OS_Unix::initialize_core(); - SemaphoreIphone::make_default(); set_data_dir(data_dir); }; @@ -135,7 +134,6 @@ Error OSIPhone::initialize(const VideoMode &p_desired, int p_video_driver, int p RasterizerRD::make_current(); #endif - visual_server = memnew(VisualServerRaster); // FIXME: Reimplement threaded rendering if (get_render_thread_mode() != RENDER_THREAD_UNSAFE) { @@ -212,7 +210,8 @@ void OSIPhone::key(uint32_t p_key, bool p_pressed) { ev.instance(); ev->set_echo(false); ev->set_pressed(p_pressed); - ev->set_scancode(p_key); + ev->set_keycode(p_key); + ev->set_physical_keycode(p_key); ev->set_unicode(p_key); queue_event(ev); }; diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index 85a633442e..d3cd8f76b7 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -10,8 +10,11 @@ javascript_files = [ 'os_javascript.cpp', ] -build = env.add_program(['#bin/godot${PROGSUFFIX}.js', '#bin/godot${PROGSUFFIX}.wasm'], javascript_files); -js, wasm = build +build_targets = ['#bin/godot${PROGSUFFIX}.js', '#bin/godot${PROGSUFFIX}.wasm'] +if env['threads_enabled']: + build_targets.append('#bin/godot${PROGSUFFIX}.worker.js') + +build = env.add_program(build_targets, javascript_files) js_libraries = [ 'http_request.js', @@ -27,18 +30,38 @@ for module in js_modules: env.Append(LINKFLAGS=['--pre-js', env.File(module).path]) env.Depends(build, js_modules) -wrapper_start = env.File('pre.js') -wrapper_end = env.File('engine.js') -js_wrapped = env.Textfile('#bin/godot', [wrapper_start, js, wrapper_end], TEXTFILESUFFIX='${PROGSUFFIX}.wrapped.js') +engine = [ + 'engine/preloader.js', + 'engine/loader.js', + 'engine/utils.js', + 'engine/engine.js', +] +externs = [ + env.File('#platform/javascript/engine/externs.js') +] +js_engine = env.CreateEngineFile('#bin/godot${PROGSUFFIX}.engine.js', engine, externs) +env.Depends(js_engine, externs) + +wrap_list = [ + build[0], + js_engine, +] +js_wrapped = env.Textfile('#bin/godot', [env.File(f) for f in wrap_list], TEXTFILESUFFIX='${PROGSUFFIX}.wrapped.js') zip_dir = env.Dir('#bin/.javascript_zip') -zip_files = env.InstallAs([ +out_files = [ zip_dir.File('godot.js'), zip_dir.File('godot.wasm'), zip_dir.File('godot.html') -], [ +] +in_files = [ js_wrapped, - wasm, + build[1], '#misc/dist/html/full-size.html' -]) +] +if env['threads_enabled']: + in_files.append(build[2]) + out_files.append(zip_dir.File('godot.worker.js')) + +zip_files = env.InstallAs(out_files, in_files) env.Zip('#bin/godot', zip_files, ZIPROOT=zip_dir, ZIPSUFFIX='${PROGSUFFIX}${ZIPSUFFIX}', ZIPCOMSTR='Archving $SOURCES as $TARGET') diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp index f1bc7c4382..d63c6a40a5 100644 --- a/platform/javascript/audio_driver_javascript.cpp +++ b/platform/javascript/audio_driver_javascript.cpp @@ -69,31 +69,37 @@ void AudioDriverJavaScript::process_capture(float sample) { Error AudioDriverJavaScript::init() { /* clang-format off */ - EM_ASM({ - _audioDriver_audioContext = new (window.AudioContext || window.webkitAudioContext); - _audioDriver_audioInput = null; - _audioDriver_inputStream = null; - _audioDriver_scriptNode = null; + _driver_id = EM_ASM_INT({ + return Module.IDHandler.add({ + 'context': new (window.AudioContext || window.webkitAudioContext), + 'input': null, + 'stream': null, + 'script': null + }); }); /* clang-format on */ int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode()); /* clang-format off */ buffer_length = EM_ASM_INT({ - var CHANNEL_COUNT = $0; + var ref = Module.IDHandler.get($0); + var ctx = ref['context']; + var CHANNEL_COUNT = $1; - var channelCount = _audioDriver_audioContext.destination.channelCount; + var channelCount = ctx.destination.channelCount; + var script = null; try { // Try letting the browser recommend a buffer length. - _audioDriver_scriptNode = _audioDriver_audioContext.createScriptProcessor(0, 2, channelCount); + script = ctx.createScriptProcessor(0, 2, channelCount); } catch (e) { // ...otherwise, default to 4096. - _audioDriver_scriptNode = _audioDriver_audioContext.createScriptProcessor(4096, 2, channelCount); + script = ctx.createScriptProcessor(4096, 2, channelCount); } - _audioDriver_scriptNode.connect(_audioDriver_audioContext.destination); + script.connect(ctx.destination); + ref['script'] = script; - return _audioDriver_scriptNode.bufferSize; - }, channel_count); + return script.bufferSize; + }, _driver_id, channel_count); /* clang-format on */ if (!buffer_length) { return FAILED; @@ -112,11 +118,12 @@ void AudioDriverJavaScript::start() { /* clang-format off */ EM_ASM({ - var INTERNAL_BUFFER_PTR = $0; + const ref = Module.IDHandler.get($0); + var INTERNAL_BUFFER_PTR = $1; var audioDriverMixFunction = cwrap('audio_driver_js_mix'); var audioDriverProcessCapture = cwrap('audio_driver_process_capture', null, ['number']); - _audioDriver_scriptNode.onaudioprocess = function(audioProcessingEvent) { + ref['script'].onaudioprocess = function(audioProcessingEvent) { audioDriverMixFunction(); var input = audioProcessingEvent.inputBuffer; @@ -133,7 +140,7 @@ void AudioDriverJavaScript::start() { } } - if (_audioDriver_audioInput) { + if (ref['input']) { var inputDataL = input.getChannelData(0); var inputDataR = input.getChannelData(1); for (var i = 0; i < inputDataL.length; i++) { @@ -142,34 +149,37 @@ void AudioDriverJavaScript::start() { } } }; - }, internal_buffer); + }, _driver_id, internal_buffer); /* clang-format on */ } void AudioDriverJavaScript::resume() { /* clang-format off */ EM_ASM({ - if (_audioDriver_audioContext.resume) - _audioDriver_audioContext.resume(); - }); + const ref = Module.IDHandler.get($0); + if (ref && ref['context'] && ref['context'].resume) + ref['context'].resume(); + }, _driver_id); /* clang-format on */ } int AudioDriverJavaScript::get_mix_rate() const { /* clang-format off */ - return EM_ASM_INT_V({ - return _audioDriver_audioContext.sampleRate; - }); + return EM_ASM_INT({ + const ref = Module.IDHandler.get($0); + return ref && ref['context'] ? ref['context'].sampleRate : 0; + }, _driver_id); /* clang-format on */ } AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const { /* clang-format off */ - return get_speaker_mode_by_total_channels(EM_ASM_INT_V({ - return _audioDriver_audioContext.destination.channelCount; - })); + return get_speaker_mode_by_total_channels(EM_ASM_INT({ + const ref = Module.IDHandler.get($0); + return ref && ref['context'] ? ref['context'].destination.channelCount : 0; + }, _driver_id)); /* clang-format on */ } @@ -184,16 +194,15 @@ void AudioDriverJavaScript::finish() { /* clang-format off */ EM_ASM({ - _audioDriver_audioContext = null; - _audioDriver_audioInput = null; - _audioDriver_scriptNode = null; - }); + Module.IDHandler.remove($0); + }, _driver_id); /* clang-format on */ if (internal_buffer) { memdelete_arr(internal_buffer); internal_buffer = NULL; } + _driver_id = 0; } Error AudioDriverJavaScript::capture_start() { @@ -203,9 +212,10 @@ Error AudioDriverJavaScript::capture_start() { /* clang-format off */ EM_ASM({ function gotMediaInput(stream) { - _audioDriver_inputStream = stream; - _audioDriver_audioInput = _audioDriver_audioContext.createMediaStreamSource(stream); - _audioDriver_audioInput.connect(_audioDriver_scriptNode); + var ref = Module.IDHandler.get($0); + ref['stream'] = stream; + ref['input'] = ref['context'].createMediaStreamSource(stream); + ref['input'].connect(ref['script']); } function gotMediaInputError(e) { @@ -219,7 +229,7 @@ Error AudioDriverJavaScript::capture_start() { navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; navigator.getUserMedia({"audio": true}, gotMediaInput, gotMediaInputError); } - }); + }, _driver_id); /* clang-format on */ return OK; @@ -229,20 +239,21 @@ Error AudioDriverJavaScript::capture_stop() { /* clang-format off */ EM_ASM({ - if (_audioDriver_inputStream) { - const tracks = _audioDriver_inputStream.getTracks(); + var ref = Module.IDHandler.get($0); + if (ref['stream']) { + const tracks = ref['stream'].getTracks(); for (var i = 0; i < tracks.length; i++) { tracks[i].stop(); } - _audioDriver_inputStream = null; + ref['stream'] = null; } - if (_audioDriver_audioInput) { - _audioDriver_audioInput.disconnect(); - _audioDriver_audioInput = null; + if (ref['input']) { + ref['input'].disconnect(); + ref['input'] = null; } - }); + }, _driver_id); /* clang-format on */ input_buffer.clear(); @@ -252,7 +263,9 @@ Error AudioDriverJavaScript::capture_stop() { AudioDriverJavaScript::AudioDriverJavaScript() { + _driver_id = 0; internal_buffer = NULL; + buffer_length = 0; singleton = this; } diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index 2bb97ba192..f6f2dacd4e 100644 --- a/platform/javascript/audio_driver_javascript.h +++ b/platform/javascript/audio_driver_javascript.h @@ -37,6 +37,7 @@ class AudioDriverJavaScript : public AudioDriver { float *internal_buffer; + int _driver_id; int buffer_length; public: diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index 1766833364..fb02752aa7 100644 --- a/platform/javascript/detect.py +++ b/platform/javascript/detect.py @@ -1,5 +1,6 @@ import os +from emscripten_helpers import parse_config, run_closure_compiler, create_engine_file def is_active(): return True @@ -18,6 +19,8 @@ def get_opts(): return [ # eval() can be a security concern, so it can be disabled. BoolVariable('javascript_eval', 'Enable JavaScript eval interface', True), + BoolVariable('threads_enabled', 'Enable WebAssembly Threads support (limited browser support)', False), + BoolVariable('use_closure_compiler', 'Use closure compiler to minimize Javascript code', False), ] @@ -37,7 +40,7 @@ def configure(env): ## Build type - if env['target'] != 'debug': + if env['target'] == 'release': # Use -Os to prioritize optimizing for reduced file size. This is # particularly valuable for the web platform because it directly # decreases download time. @@ -46,38 +49,55 @@ def configure(env): # run-time performance. env.Append(CCFLAGS=['-Os']) 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: + elif env['target'] == 'release_debug': + env.Append(CCFLAGS=['-Os']) + env.Append(LINKFLAGS=['-Os']) + 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.Append(LINKFLAGS=['-s', 'ASSERTIONS=1']) - ## Compiler configuration + if env['tools']: + if not env['threads_enabled']: + raise RuntimeError("Threads must be enabled to build the editor. Please add the 'threads_enabled=yes' option") + # Tools need more memory. Initial stack memory in bytes. See `src/settings.js` in emscripten repository (will be renamed to INITIAL_MEMORY). + env.Append(LINKFLAGS=['-s', 'TOTAL_MEMORY=33554432']) + else: + # Disable exceptions and rtti on non-tools (template) builds + # These flags help keep the file size down. + env.Append(CCFLAGS=['-fno-exceptions', '-fno-rtti']) + # Don't use dynamic_cast, necessary with no-rtti. + env.Append(CPPDEFINES=['NO_SAFE_CAST']) + ## Copy env variables. env['ENV'] = os.environ - em_config_file = os.getenv('EM_CONFIG') or os.path.expanduser('~/.emscripten') - if not os.path.exists(em_config_file): - raise RuntimeError("Emscripten configuration file '%s' does not exist" % em_config_file) - with open(em_config_file) as f: - em_config = {} - try: - # Emscripten configuration file is a Python file with simple assignments. - exec(f.read(), em_config) - except StandardError as e: - raise RuntimeError("Emscripten configuration file '%s' is invalid:\n%s" % (em_config_file, e)) - if 'BINARYEN_ROOT' in em_config and os.path.isdir(os.path.join(em_config.get('BINARYEN_ROOT'), 'emscripten')): - # New style, emscripten path as a subfolder of BINARYEN_ROOT - env.PrependENVPath('PATH', os.path.join(em_config.get('BINARYEN_ROOT'), 'emscripten')) - elif 'EMSCRIPTEN_ROOT' in em_config: - # Old style (but can be there as a result from previous activation, so do last) - env.PrependENVPath('PATH', em_config.get('EMSCRIPTEN_ROOT')) - else: - raise RuntimeError("'BINARYEN_ROOT' or 'EMSCRIPTEN_ROOT' missing in Emscripten configuration file '%s'" % em_config_file) + # LTO + if env['use_lto']: + env.Append(CCFLAGS=['-s', 'WASM_OBJECT_FILES=0']) + env.Append(LINKFLAGS=['-s', 'WASM_OBJECT_FILES=0']) + env.Append(LINKFLAGS=['--llvm-lto', '1']) + + # Closure compiler + if env['use_closure_compiler']: + # For emscripten support code. + env.Append(LINKFLAGS=['--closure', '1']) + # Register builder for our Engine files + jscc = env.Builder(generator=run_closure_compiler, suffix='.cc.js', src_suffix='.js') + env.Append(BUILDERS = {'BuildJS' : jscc}) + + # Add method that joins/compiles our Engine files. + env.AddMethod(create_engine_file, "CreateEngineFile") + + # Closure compiler extern and support for ecmascript specs (const, let, etc). + env['ENV']['EMCC_CLOSURE_ARGS'] = '--language_in ECMASCRIPT6' + + em_config = parse_config() + env.PrependENVPath('PATH', em_config['EMCC_ROOT']) env['CC'] = 'emcc' env['CXX'] = 'em++' @@ -104,44 +124,31 @@ def configure(env): env['LIBPREFIXES'] = ['$LIBPREFIX'] env['LIBSUFFIXES'] = ['$LIBSUFFIX'] - ## Compile flags - env.Prepend(CPPPATH=['#platform/javascript']) env.Append(CPPDEFINES=['JAVASCRIPT_ENABLED', 'UNIX_ENABLED']) - # No multi-threading (SharedArrayBuffer) available yet, - # once feasible also consider memory buffer size issues. - env.Append(CPPDEFINES=['NO_THREADS']) - - # Disable exceptions and rtti on non-tools (template) builds - if not env['tools']: - # These flags help keep the file size down. - env.Append(CCFLAGS=['-fno-exceptions', '-fno-rtti']) - # Don't use dynamic_cast, necessary with no-rtti. - env.Append(CPPDEFINES=['NO_SAFE_CAST']) - if env['javascript_eval']: env.Append(CPPDEFINES=['JAVASCRIPT_EVAL_ENABLED']) - ## Link flags + # Thread support (via SharedArrayBuffer). + if env['threads_enabled']: + env.Append(CPPDEFINES=['PTHREAD_NO_RENAME']) + env.Append(CCFLAGS=['-s', 'USE_PTHREADS=1']) + env.Append(LINKFLAGS=['-s', 'USE_PTHREADS=1']) + env.Append(LINKFLAGS=['-s', 'PTHREAD_POOL_SIZE=4']) + env.Append(LINKFLAGS=['-s', 'WASM_MEM_MAX=2048MB']) + else: + env.Append(CPPDEFINES=['NO_THREADS']) + + # Reduce code size by generating less support code (e.g. skip NodeJS support). + env.Append(LINKFLAGS=['-s', 'ENVIRONMENT=web,worker']) # We use IDBFS in javascript_main.cpp. Since Emscripten 1.39.1 it needs to # be linked explicitly. env.Append(LIBS=['idbfs.js']) env.Append(LINKFLAGS=['-s', 'BINARYEN=1']) - - # Only include the JavaScript support code for the web environment - # (i.e. exclude Node.js and other unused environments). - # This makes the JavaScript support code about 4 KB smaller. - env.Append(LINKFLAGS=['-s', 'ENVIRONMENT=web']) - - # This needs to be defined for Emscripten using 'fastcomp' (default pre-1.39.0) - # and undefined if using 'upstream'. And to make things simple, earlier - # Emscripten versions didn't include 'fastcomp' in their path, so we check - # against the presence of 'upstream' to conditionally add the flag. - if not "upstream" in em_config['EMSCRIPTEN_ROOT']: - env.Append(LINKFLAGS=['-s', 'BINARYEN_TRAP_MODE=\'clamp\'']) + env.Append(LINKFLAGS=['-s', 'MODULARIZE=1', '-s', 'EXPORT_NAME="Godot"']) # Allow increasing memory buffer size during runtime. This is efficient # when using WebAssembly (in comparison to asm.js) and works well for @@ -153,8 +160,5 @@ def configure(env): env.Append(LINKFLAGS=['-s', 'INVOKE_RUN=0']) - # TODO: Reevaluate usage of this setting now that engine.js manages engine runtime. - env.Append(LINKFLAGS=['-s', 'NO_EXIT_RUNTIME=1']) - - #adding flag due to issue with emscripten 1.38.41 callMain method https://github.com/emscripten-core/emscripten/blob/incoming/ChangeLog.md#v13841-08072019 - env.Append(LINKFLAGS=['-s', 'EXTRA_EXPORTED_RUNTIME_METHODS=["callMain"]']) + # callMain for manual start, FS for preloading. + env.Append(LINKFLAGS=['-s', 'EXTRA_EXPORTED_RUNTIME_METHODS=["callMain", "FS"]']) diff --git a/platform/javascript/dom_keys.inc b/platform/javascript/dom_keys.inc index 25e88f99d1..fd9df765d2 100644 --- a/platform/javascript/dom_keys.inc +++ b/platform/javascript/dom_keys.inc @@ -218,7 +218,7 @@ #define DOM_VK_PA1 0xFD #define DOM_VK_WIN_OEM_CLEAR 0xFE -int dom2godot_scancode(int dom_keycode) { +int dom2godot_keycode(int dom_keycode) { if (DOM_VK_0 <= dom_keycode && dom_keycode <= DOM_VK_Z) { // ASCII intersection diff --git a/platform/javascript/emscripten_helpers.py b/platform/javascript/emscripten_helpers.py new file mode 100644 index 0000000000..bda5b40a74 --- /dev/null +++ b/platform/javascript/emscripten_helpers.py @@ -0,0 +1,37 @@ +import os + +def parse_config(): + em_config_file = os.getenv('EM_CONFIG') or os.path.expanduser('~/.emscripten') + if not os.path.exists(em_config_file): + raise RuntimeError("Emscripten configuration file '%s' does not exist" % em_config_file) + + normalized = {} + em_config = {} + with open(em_config_file) as f: + try: + # Emscripten configuration file is a Python file with simple assignments. + exec(f.read(), em_config) + except StandardError as e: + raise RuntimeError("Emscripten configuration file '%s' is invalid:\n%s" % (em_config_file, e)) + normalized['EMCC_ROOT'] = em_config.get('EMSCRIPTEN_ROOT') + normalized['NODE_JS'] = em_config.get('NODE_JS') + normalized['CLOSURE_BIN'] = os.path.join(normalized['EMCC_ROOT'], 'node_modules', '.bin', 'google-closure-compiler') + return normalized + + +def run_closure_compiler(target, source, env, for_signature): + cfg = parse_config() + cmd = [cfg['NODE_JS'], cfg['CLOSURE_BIN']] + cmd.extend(['--compilation_level', 'ADVANCED_OPTIMIZATIONS']) + for f in env['JSEXTERNS']: + cmd.extend(['--externs', f.get_abspath()]) + for f in source: + cmd.extend(['--js', f.get_abspath()]) + cmd.extend(['--js_output_file', target[0].get_abspath()]) + return ' '.join(cmd) + + +def create_engine_file(env, target, source, externs): + if env['use_closure_compiler']: + return env.BuildJS(target, source, JSEXTERNS=externs) + return env.Textfile(target, [env.File(s) for s in source]) diff --git a/platform/javascript/engine.js b/platform/javascript/engine.js deleted file mode 100644 index 227accadb0..0000000000 --- a/platform/javascript/engine.js +++ /dev/null @@ -1,411 +0,0 @@ - // The following is concatenated with generated code, and acts as the end - // of a wrapper for said code. See pre.js for the other part of the - // wrapper. - exposedLibs['PATH'] = PATH; - exposedLibs['FS'] = FS; - return Module; - }, -}; - -(function() { - var engine = Engine; - - var DOWNLOAD_ATTEMPTS_MAX = 4; - - var basePath = null; - var wasmFilenameExtensionOverride = null; - var engineLoadPromise = null; - - var loadingFiles = {}; - - function getPathLeaf(path) { - - while (path.endsWith('/')) - path = path.slice(0, -1); - return path.slice(path.lastIndexOf('/') + 1); - } - - function getBasePath(path) { - - if (path.endsWith('/')) - path = path.slice(0, -1); - if (path.lastIndexOf('.') > path.lastIndexOf('/')) - path = path.slice(0, path.lastIndexOf('.')); - return path; - } - - function getBaseName(path) { - - return getPathLeaf(getBasePath(path)); - } - - Engine = function Engine() { - - this.rtenv = null; - - var LIBS = {}; - - var initPromise = null; - var unloadAfterInit = true; - - var preloadedFiles = []; - - var resizeCanvasOnStart = true; - var progressFunc = null; - var preloadProgressTracker = {}; - var lastProgress = { loaded: 0, total: 0 }; - - var canvas = null; - var executableName = null; - var locale = null; - var stdout = null; - var stderr = null; - - this.init = function(newBasePath) { - - if (!initPromise) { - initPromise = Engine.load(newBasePath).then( - instantiate.bind(this) - ); - requestAnimationFrame(animateProgress); - if (unloadAfterInit) - initPromise.then(Engine.unloadEngine); - } - return initPromise; - }; - - function instantiate(wasmBuf) { - - var rtenvProps = { - engine: this, - ENV: {}, - }; - if (typeof stdout === 'function') - rtenvProps.print = stdout; - if (typeof stderr === 'function') - rtenvProps.printErr = stderr; - rtenvProps.instantiateWasm = function(imports, onSuccess) { - WebAssembly.instantiate(wasmBuf, imports).then(function(result) { - onSuccess(result.instance); - }); - return {}; - }; - - return new Promise(function(resolve, reject) { - rtenvProps.onRuntimeInitialized = resolve; - rtenvProps.onAbort = reject; - rtenvProps.thisProgram = executableName; - rtenvProps.engine.rtenv = Engine.RuntimeEnvironment(rtenvProps, LIBS); - }); - } - - this.preloadFile = function(pathOrBuffer, destPath) { - - if (pathOrBuffer instanceof ArrayBuffer) { - pathOrBuffer = new Uint8Array(pathOrBuffer); - } else if (ArrayBuffer.isView(pathOrBuffer)) { - pathOrBuffer = new Uint8Array(pathOrBuffer.buffer); - } - if (pathOrBuffer instanceof Uint8Array) { - preloadedFiles.push({ - path: destPath, - buffer: pathOrBuffer - }); - return Promise.resolve(); - } else if (typeof pathOrBuffer === 'string') { - return loadPromise(pathOrBuffer, preloadProgressTracker).then(function(xhr) { - preloadedFiles.push({ - path: destPath || pathOrBuffer, - buffer: xhr.response - }); - }); - } else { - throw Promise.reject("Invalid object for preloading"); - } - }; - - this.start = function() { - - return this.init().then( - Function.prototype.apply.bind(synchronousStart, this, arguments) - ); - }; - - this.startGame = function(execName, mainPack) { - - executableName = execName; - var mainArgs = [ '--main-pack', getPathLeaf(mainPack) ]; - - return Promise.all([ - this.init(getBasePath(execName)), - this.preloadFile(mainPack, getPathLeaf(mainPack)) - ]).then( - Function.prototype.apply.bind(synchronousStart, this, mainArgs) - ); - }; - - function synchronousStart() { - - if (canvas instanceof HTMLCanvasElement) { - this.rtenv.canvas = canvas; - } else { - var firstCanvas = document.getElementsByTagName('canvas')[0]; - if (firstCanvas instanceof HTMLCanvasElement) { - this.rtenv.canvas = firstCanvas; - } else { - throw new Error("No canvas found"); - } - } - - var actualCanvas = this.rtenv.canvas; - // canvas can grab focus on click - if (actualCanvas.tabIndex < 0) { - actualCanvas.tabIndex = 0; - } - // necessary to calculate cursor coordinates correctly - actualCanvas.style.padding = 0; - actualCanvas.style.borderWidth = 0; - actualCanvas.style.borderStyle = 'none'; - // disable right-click context menu - actualCanvas.addEventListener('contextmenu', function(ev) { - ev.preventDefault(); - }, false); - // until context restoration is implemented - actualCanvas.addEventListener('webglcontextlost', function(ev) { - alert("WebGL context lost, please reload the page"); - ev.preventDefault(); - }, false); - - if (locale) { - this.rtenv.locale = locale; - } else { - this.rtenv.locale = navigator.languages ? navigator.languages[0] : navigator.language; - } - this.rtenv.locale = this.rtenv.locale.split('.')[0]; - this.rtenv.resizeCanvasOnStart = resizeCanvasOnStart; - - preloadedFiles.forEach(function(file) { - var dir = LIBS.PATH.dirname(file.path); - try { - LIBS.FS.stat(dir); - } catch (e) { - if (e.code !== 'ENOENT') { - throw e; - } - LIBS.FS.mkdirTree(dir); - } - // With memory growth, canOwn should be false. - LIBS.FS.createDataFile(file.path, null, new Uint8Array(file.buffer), true, true, false); - }, this); - - preloadedFiles = null; - initPromise = null; - this.rtenv.callMain(arguments); - } - - this.setProgressFunc = function(func) { - progressFunc = func; - }; - - this.setResizeCanvasOnStart = function(enabled) { - resizeCanvasOnStart = enabled; - }; - - function animateProgress() { - - var loaded = 0; - var total = 0; - var totalIsValid = true; - var progressIsFinal = true; - - [loadingFiles, preloadProgressTracker].forEach(function(tracker) { - Object.keys(tracker).forEach(function(file) { - if (!tracker[file].final) - progressIsFinal = false; - if (!totalIsValid || tracker[file].total === 0) { - totalIsValid = false; - total = 0; - } else { - total += tracker[file].total; - } - loaded += tracker[file].loaded; - }); - }); - if (loaded !== lastProgress.loaded || total !== lastProgress.total) { - lastProgress.loaded = loaded; - lastProgress.total = total; - if (typeof progressFunc === 'function') - progressFunc(loaded, total); - } - if (!progressIsFinal) - requestAnimationFrame(animateProgress); - } - - this.setCanvas = function(elem) { - canvas = elem; - }; - - this.setExecutableName = function(newName) { - - executableName = newName; - }; - - this.setLocale = function(newLocale) { - - locale = newLocale; - }; - - this.setUnloadAfterInit = function(enabled) { - - if (enabled && !unloadAfterInit && initPromise) { - initPromise.then(Engine.unloadEngine); - } - unloadAfterInit = enabled; - }; - - this.setStdoutFunc = function(func) { - - var print = function(text) { - if (arguments.length > 1) { - text = Array.prototype.slice.call(arguments).join(" "); - } - func(text); - }; - if (this.rtenv) - this.rtenv.print = print; - stdout = print; - }; - - this.setStderrFunc = function(func) { - - var printErr = function(text) { - if (arguments.length > 1) - text = Array.prototype.slice.call(arguments).join(" "); - func(text); - }; - if (this.rtenv) - this.rtenv.printErr = printErr; - stderr = printErr; - }; - - - }; // Engine() - - Engine.RuntimeEnvironment = engine.RuntimeEnvironment; - - Engine.isWebGLAvailable = function(majorVersion = 1) { - - var testContext = false; - try { - var testCanvas = document.createElement('canvas'); - if (majorVersion === 1) { - testContext = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl'); - } else if (majorVersion === 2) { - testContext = testCanvas.getContext('webgl2') || testCanvas.getContext('experimental-webgl2'); - } - } catch (e) {} - return !!testContext; - }; - - Engine.setWebAssemblyFilenameExtension = function(override) { - - if (String(override).length === 0) { - throw new Error('Invalid WebAssembly filename extension override'); - } - wasmFilenameExtensionOverride = String(override); - } - - Engine.load = function(newBasePath) { - - if (newBasePath !== undefined) basePath = getBasePath(newBasePath); - if (engineLoadPromise === null) { - if (typeof WebAssembly !== 'object') - return Promise.reject(new Error("Browser doesn't support WebAssembly")); - // TODO cache/retrieve module to/from idb - engineLoadPromise = loadPromise(basePath + '.' + (wasmFilenameExtensionOverride || 'wasm')).then(function(xhr) { - return xhr.response; - }); - engineLoadPromise = engineLoadPromise.catch(function(err) { - engineLoadPromise = null; - throw err; - }); - } - return engineLoadPromise; - }; - - Engine.unload = function() { - engineLoadPromise = null; - }; - - function loadPromise(file, tracker) { - if (tracker === undefined) - tracker = loadingFiles; - return new Promise(function(resolve, reject) { - loadXHR(resolve, reject, file, tracker); - }); - } - - function loadXHR(resolve, reject, file, tracker) { - - var xhr = new XMLHttpRequest; - xhr.open('GET', file); - if (!file.endsWith('.js')) { - xhr.responseType = 'arraybuffer'; - } - ['loadstart', 'progress', 'load', 'error', 'abort'].forEach(function(ev) { - xhr.addEventListener(ev, onXHREvent.bind(xhr, resolve, reject, file, tracker)); - }); - xhr.send(); - } - - function onXHREvent(resolve, reject, file, tracker, ev) { - - if (this.status >= 400) { - - if (this.status < 500 || ++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) { - reject(new Error("Failed loading file '" + file + "': " + this.statusText)); - this.abort(); - return; - } else { - setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000); - } - } - - switch (ev.type) { - case 'loadstart': - if (tracker[file] === undefined) { - tracker[file] = { - total: ev.total, - loaded: ev.loaded, - attempts: 0, - final: false, - }; - } - break; - - case 'progress': - tracker[file].loaded = ev.loaded; - tracker[file].total = ev.total; - break; - - case 'load': - tracker[file].final = true; - resolve(this); - break; - - case 'error': - if (++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) { - tracker[file].final = true; - reject(new Error("Failed loading file '" + file + "'")); - } else { - setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000); - } - break; - - case 'abort': - tracker[file].final = true; - reject(new Error("Loading file '" + file + "' was aborted.")); - break; - } - } -})(); diff --git a/platform/javascript/engine/engine.js b/platform/javascript/engine/engine.js new file mode 100644 index 0000000000..6d7509377f --- /dev/null +++ b/platform/javascript/engine/engine.js @@ -0,0 +1,184 @@ +Function('return this')()['Engine'] = (function() { + + var unloadAfterInit = true; + var canvas = null; + var resizeCanvasOnStart = false; + var customLocale = 'en_US'; + var wasmExt = '.wasm'; + + var preloader = new Preloader(); + var loader = new Loader(); + var rtenv = null; + + var executableName = ''; + var loadPath = ''; + var loadPromise = null; + var initPromise = null; + var stderr = null; + var stdout = null; + var progressFunc = null; + + function load(basePath) { + if (loadPromise == null) { + loadPath = basePath; + loadPromise = preloader.loadPromise(basePath + wasmExt); + preloader.setProgressFunc(progressFunc); + requestAnimationFrame(preloader.animateProgress); + } + return loadPromise; + }; + + function unload() { + loadPromise = null; + }; + + /** @constructor */ + function Engine() {}; + + Engine.prototype.init = /** @param {string=} basePath */ function(basePath) { + if (initPromise) { + return initPromise; + } + if (!loadPromise) { + if (!basePath) { + initPromise = Promise.reject(new Error("A base path must be provided when calling `init` and the engine is not loaded.")); + return initPromise; + } + load(basePath); + } + var config = {} + if (typeof stdout === 'function') + config.print = stdout; + if (typeof stderr === 'function') + config.printErr = stderr; + initPromise = loader.init(loadPromise, loadPath, config).then(function() { + return new Promise(function(resolve, reject) { + rtenv = loader.env; + if (unloadAfterInit) { + loadPromise = null; + } + resolve(); + }); + }); + return initPromise; + }; + + /** @type {function(string, string):Object} */ + Engine.prototype.preloadFile = function(file, path) { + return preloader.preload(file, path); + }; + + /** @type {function(...string):Object} */ + Engine.prototype.start = function() { + // Start from arguments. + var args = []; + for (var i = 0; i < arguments.length; i++) { + args.push(arguments[i]); + } + var me = this; + return new Promise(function(resolve, reject) { + return me.init().then(function() { + if (!(canvas instanceof HTMLCanvasElement)) { + canvas = Utils.findCanvas(); + } + rtenv['locale'] = customLocale; + rtenv['canvas'] = canvas; + rtenv['thisProgram'] = executableName; + rtenv['resizeCanvasOnStart'] = resizeCanvasOnStart; + loader.start(preloader.preloadedFiles, args).then(function() { + loader = null; + initPromise = null; + resolve(); + }); + }); + }); + }; + + Engine.prototype.startGame = function(execName, mainPack) { + // Start and init with execName as loadPath if not inited. + executableName = execName; + var me = this; + return Promise.all([ + this.init(execName), + this.preloadFile(mainPack, mainPack) + ]).then(function() { + return me.start('--main-pack', mainPack); + }); + }; + + Engine.prototype.setWebAssemblyFilenameExtension = function(override) { + if (String(override).length === 0) { + throw new Error('Invalid WebAssembly filename extension override'); + } + wasmExt = String(override); + }; + + Engine.prototype.setUnloadAfterInit = function(enabled) { + unloadAfterInit = enabled; + }; + + Engine.prototype.setCanvas = function(canvasElem) { + canvas = canvasElem; + }; + + Engine.prototype.setCanvasResizedOnStart = function(enabled) { + resizeCanvasOnStart = enabled; + }; + + Engine.prototype.setLocale = function(locale) { + customLocale = locale; + }; + + Engine.prototype.setExecutableName = function(newName) { + executableName = newName; + }; + + Engine.prototype.setProgressFunc = function(func) { + progressFunc = func; + } + + Engine.prototype.setStdoutFunc = function(func) { + + var print = function(text) { + if (arguments.length > 1) { + text = Array.prototype.slice.call(arguments).join(" "); + } + func(text); + }; + if (rtenv) + rtenv.print = print; + stdout = print; + }; + + Engine.prototype.setStderrFunc = function(func) { + + var printErr = function(text) { + if (arguments.length > 1) + text = Array.prototype.slice.call(arguments).join(" "); + func(text); + }; + if (rtenv) + rtenv.printErr = printErr; + stderr = printErr; + }; + + // Closure compiler exported engine methods. + /** @export */ + Engine['isWebGLAvailable'] = Utils.isWebGLAvailable; + Engine['load'] = load; + Engine['unload'] = unload; + Engine.prototype['init'] = Engine.prototype.init + Engine.prototype['preloadFile'] = Engine.prototype.preloadFile + Engine.prototype['start'] = Engine.prototype.start + Engine.prototype['startGame'] = Engine.prototype.startGame + Engine.prototype['setWebAssemblyFilenameExtension'] = Engine.prototype.setWebAssemblyFilenameExtension + Engine.prototype['setUnloadAfterInit'] = Engine.prototype.setUnloadAfterInit + Engine.prototype['setCanvas'] = Engine.prototype.setCanvas + Engine.prototype['setCanvasResizedOnStart'] = Engine.prototype.setCanvasResizedOnStart + Engine.prototype['setLocale'] = Engine.prototype.setLocale + Engine.prototype['setExecutableName'] = Engine.prototype.setExecutableName + Engine.prototype['setProgressFunc'] = Engine.prototype.setProgressFunc + Engine.prototype['setStdoutFunc'] = Engine.prototype.setStdoutFunc + Engine.prototype['setStderrFunc'] = Engine.prototype.setStderrFunc + return Engine; +})(); diff --git a/platform/javascript/engine/externs.js b/platform/javascript/engine/externs.js new file mode 100644 index 0000000000..1a94dd15ec --- /dev/null +++ b/platform/javascript/engine/externs.js @@ -0,0 +1,3 @@ +var Godot; +var WebAssembly = {}; +WebAssembly.instantiate = function(buffer, imports) {}; diff --git a/platform/javascript/engine/loader.js b/platform/javascript/engine/loader.js new file mode 100644 index 0000000000..d27fbf612e --- /dev/null +++ b/platform/javascript/engine/loader.js @@ -0,0 +1,33 @@ +var Loader = /** @constructor */ function() { + + this.env = null; + + this.init = function(loadPromise, basePath, config) { + var me = this; + return new Promise(function(resolve, reject) { + var cfg = config || {}; + cfg['locateFile'] = Utils.createLocateRewrite(basePath); + cfg['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise); + loadPromise = null; + Godot(cfg).then(function(module) { + me.env = module; + resolve(); + }); + }); + } + + this.start = function(preloadedFiles, args) { + var me = this; + return new Promise(function(resolve, reject) { + if (!me.env) { + reject(new Error('The engine must be initialized before it can be started')); + } + preloadedFiles.forEach(function(file) { + Utils.copyToFS(me.env['FS'], file.path, file.buffer); + }); + preloadedFiles.length = 0; // Clear memory + me.env['callMain'](args); + resolve(); + }); + } +}; diff --git a/platform/javascript/engine/preloader.js b/platform/javascript/engine/preloader.js new file mode 100644 index 0000000000..17918eae38 --- /dev/null +++ b/platform/javascript/engine/preloader.js @@ -0,0 +1,139 @@ +var Preloader = /** @constructor */ function() { + + var DOWNLOAD_ATTEMPTS_MAX = 4; + var progressFunc = null; + var lastProgress = { loaded: 0, total: 0 }; + + var loadingFiles = {}; + this.preloadedFiles = []; + + function loadXHR(resolve, reject, file, tracker) { + var xhr = new XMLHttpRequest; + xhr.open('GET', file); + if (!file.endsWith('.js')) { + xhr.responseType = 'arraybuffer'; + } + ['loadstart', 'progress', 'load', 'error', 'abort'].forEach(function(ev) { + xhr.addEventListener(ev, onXHREvent.bind(xhr, resolve, reject, file, tracker)); + }); + xhr.send(); + } + + function onXHREvent(resolve, reject, file, tracker, ev) { + + if (this.status >= 400) { + + if (this.status < 500 || ++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) { + reject(new Error("Failed loading file '" + file + "': " + this.statusText)); + this.abort(); + return; + } else { + setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000); + } + } + + switch (ev.type) { + case 'loadstart': + if (tracker[file] === undefined) { + tracker[file] = { + total: ev.total, + loaded: ev.loaded, + attempts: 0, + final: false, + }; + } + break; + + case 'progress': + tracker[file].loaded = ev.loaded; + tracker[file].total = ev.total; + break; + + case 'load': + tracker[file].final = true; + resolve(this); + break; + + case 'error': + if (++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) { + tracker[file].final = true; + reject(new Error("Failed loading file '" + file + "'")); + } else { + setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000); + } + break; + + case 'abort': + tracker[file].final = true; + reject(new Error("Loading file '" + file + "' was aborted.")); + break; + } + } + + this.loadPromise = function(file) { + return new Promise(function(resolve, reject) { + loadXHR(resolve, reject, file, loadingFiles); + }); + } + + this.preload = function(pathOrBuffer, destPath) { + if (pathOrBuffer instanceof ArrayBuffer) { + pathOrBuffer = new Uint8Array(pathOrBuffer); + } else if (ArrayBuffer.isView(pathOrBuffer)) { + pathOrBuffer = new Uint8Array(pathOrBuffer.buffer); + } + if (pathOrBuffer instanceof Uint8Array) { + this.preloadedFiles.push({ + path: destPath, + buffer: pathOrBuffer + }); + return Promise.resolve(); + } else if (typeof pathOrBuffer === 'string') { + var me = this; + return this.loadPromise(pathOrBuffer).then(function(xhr) { + me.preloadedFiles.push({ + path: destPath || pathOrBuffer, + buffer: xhr.response + }); + return Promise.resolve(); + }); + } else { + throw Promise.reject("Invalid object for preloading"); + } + }; + + var animateProgress = function() { + + var loaded = 0; + var total = 0; + var totalIsValid = true; + var progressIsFinal = true; + + Object.keys(loadingFiles).forEach(function(file) { + const stat = loadingFiles[file]; + if (!stat.final) { + progressIsFinal = false; + } + if (!totalIsValid || stat.total === 0) { + totalIsValid = false; + total = 0; + } else { + total += stat.total; + } + loaded += stat.loaded; + }); + if (loaded !== lastProgress.loaded || total !== lastProgress.total) { + lastProgress.loaded = loaded; + lastProgress.total = total; + if (typeof progressFunc === 'function') + progressFunc(loaded, total); + } + if (!progressIsFinal) + requestAnimationFrame(animateProgress); + } + this.animateProgress = animateProgress; // Also exposed to start it. + + this.setProgressFunc = function(callback) { + progressFunc = callback; + } +}; diff --git a/platform/javascript/engine/utils.js b/platform/javascript/engine/utils.js new file mode 100644 index 0000000000..fdff90a923 --- /dev/null +++ b/platform/javascript/engine/utils.js @@ -0,0 +1,69 @@ +var Utils = { + + createLocateRewrite: function(execName) { + function rw(path) { + if (path.endsWith('.worker.js')) { + return execName + '.worker.js'; + } else if (path.endsWith('.js')) { + return execName + '.js'; + } else if (path.endsWith('.wasm')) { + return execName + '.wasm'; + } + } + return rw; + }, + + createInstantiatePromise: function(wasmLoader) { + function instantiateWasm(imports, onSuccess) { + wasmLoader.then(function(xhr) { + WebAssembly.instantiate(xhr.response, imports).then(function(result) { + onSuccess(result['instance'], result['module']); + }); + }); + wasmLoader = null; + return {}; + }; + + return instantiateWasm; + }, + + copyToFS: function(fs, path, buffer) { + var p = path.lastIndexOf("/"); + var dir = "/"; + if (p > 0) { + dir = path.slice(0, path.lastIndexOf("/")); + } + try { + fs.stat(dir); + } catch (e) { + if (e.errno !== 44) { // 'ENOENT', see https://github.com/emscripten-core/emscripten/blob/master/system/lib/libc/musl/arch/emscripten/bits/errno.h + throw e; + } + fs['mkdirTree'](dir); + } + // With memory growth, canOwn should be false. + fs['writeFile'](path, new Uint8Array(buffer), {'flags': 'wx+'}); + }, + + findCanvas: function() { + var nodes = document.getElementsByTagName('canvas'); + if (nodes.length && nodes[0] instanceof HTMLCanvasElement) { + return nodes[0]; + } + throw new Error("No canvas found"); + }, + + isWebGLAvailable: function(majorVersion = 1) { + + var testContext = false; + try { + var testCanvas = document.createElement('canvas'); + if (majorVersion === 1) { + testContext = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl'); + } else if (majorVersion === 2) { + testContext = testCanvas.getContext('webgl2') || testCanvas.getContext('experimental-webgl2'); + } + } catch (e) {} + return !!testContext; + } +}; diff --git a/platform/javascript/export/export.cpp b/platform/javascript/export/export.cpp index c44a0270ab..da61425747 100644 --- a/platform/javascript/export/export.cpp +++ b/platform/javascript/export/export.cpp @@ -94,6 +94,9 @@ public: } else if (req[1] == basereq + ".js") { filepath += ".js"; ctype = "application/javascript"; + } else if (req[1] == basereq + ".worker.js") { + filepath += ".worker.js"; + ctype = "application/javascript"; } else if (req[1] == basereq + ".pck") { filepath += ".pck"; ctype = "application/octet-stream"; @@ -200,7 +203,7 @@ class EditorExportPlatformJavaScript : public EditorExportPlatform { private: Ref<EditorHTTPServer> server; bool server_quit; - Mutex *server_lock; + Mutex server_lock; Thread *server_thread; static void _server_thread_poll(void *data); @@ -432,6 +435,10 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese } else if (file == "godot.js") { file = p_path.get_file().get_basename() + ".js"; + } else if (file == "godot.worker.js") { + + file = p_path.get_file().get_basename() + ".worker.js"; + } else if (file == "godot.wasm") { file = p_path.get_file().get_basename() + ".wasm"; @@ -531,9 +538,8 @@ bool EditorExportPlatformJavaScript::poll_export() { menu_options = preset.is_valid(); if (server->is_listening()) { if (menu_options == 0) { - server_lock->lock(); + MutexLock lock(server_lock); server->stop(); - server_lock->unlock(); } else { menu_options += 1; } @@ -553,9 +559,8 @@ int EditorExportPlatformJavaScript::get_options_count() const { Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) { if (p_option == 1) { - server_lock->lock(); + MutexLock lock(server_lock); server->stop(); - server_lock->unlock(); return OK; } @@ -565,6 +570,7 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese // Export generates several files, clean them up on failure. DirAccess::remove_file_or_error(basepath + ".html"); DirAccess::remove_file_or_error(basepath + ".js"); + DirAccess::remove_file_or_error(basepath + ".worker.js"); DirAccess::remove_file_or_error(basepath + ".pck"); DirAccess::remove_file_or_error(basepath + ".png"); DirAccess::remove_file_or_error(basepath + ".wasm"); @@ -584,10 +590,12 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese ERR_FAIL_COND_V_MSG(!bind_ip.is_valid(), ERR_INVALID_PARAMETER, "Invalid editor setting 'export/web/http_host': '" + bind_host + "'. Try using '127.0.0.1'."); // Restart server. - server_lock->lock(); - server->stop(); - err = server->listen(bind_port, bind_ip); - server_lock->unlock(); + { + MutexLock lock(server_lock); + + server->stop(); + err = server->listen(bind_port, bind_ip); + } ERR_FAIL_COND_V_MSG(err != OK, err, "Unable to start HTTP server."); OS::get_singleton()->shell_open(String("http://" + bind_host + ":" + itos(bind_port) + "/tmp_js_export.html")); @@ -605,9 +613,10 @@ void EditorExportPlatformJavaScript::_server_thread_poll(void *data) { EditorExportPlatformJavaScript *ej = (EditorExportPlatformJavaScript *)data; while (!ej->server_quit) { OS::get_singleton()->delay_usec(1000); - ej->server_lock->lock(); - ej->server->poll(); - ej->server_lock->unlock(); + { + MutexLock lock(ej->server_lock); + ej->server->poll(); + } } } @@ -615,7 +624,6 @@ EditorExportPlatformJavaScript::EditorExportPlatformJavaScript() { server.instance(); server_quit = false; - server_lock = Mutex::create(); server_thread = Thread::create(_server_thread_poll, this); Ref<Image> img = memnew(Image(_javascript_logo)); @@ -639,7 +647,6 @@ EditorExportPlatformJavaScript::~EditorExportPlatformJavaScript() { server->stop(); server_quit = true; Thread::wait_to_finish(server_thread); - memdelete(server_lock); memdelete(server_thread); } diff --git a/platform/javascript/http_client.h.inc b/platform/javascript/http_client.h.inc index 03e2ce8b8a..ac275aadbc 100644 --- a/platform/javascript/http_client.h.inc +++ b/platform/javascript/http_client.h.inc @@ -45,7 +45,7 @@ String password; int polled_response_code; String polled_response_header; -PoolByteArray polled_response; +PackedByteArray polled_response; #ifdef DEBUG_ENABLED bool has_polled; diff --git a/platform/javascript/http_client_javascript.cpp b/platform/javascript/http_client_javascript.cpp index d7796cc4f4..472384cf30 100644 --- a/platform/javascript/http_client_javascript.cpp +++ b/platform/javascript/http_client_javascript.cpp @@ -108,8 +108,7 @@ Error HTTPClient::request_raw(Method p_method, const String &p_url, const Vector Error err = prepare_request(p_method, p_url, p_headers); if (err != OK) return err; - const uint8_t *read = p_body.ptr(); - godot_xhr_send_data(xhr_id, read.ptr(), p_body.size()); + godot_xhr_send_data(xhr_id, p_body.ptr(), p_body.size()); return OK; } @@ -180,11 +179,7 @@ PackedByteArray HTTPClient::read_response_body_chunk() { int to_read = MIN(read_limit, polled_response.size() - response_read_offset); PackedByteArray chunk; chunk.resize(to_read); - uint8_t *write = chunk.ptrw(); - const uint8_t *read = polled_response.ptr(); - memcpy(write.ptr(), read.ptr() + response_read_offset, to_read); - write = uint8_t * (); - read = const uint8_t * (); + memcpy(chunk.ptrw(), polled_response.ptr() + response_read_offset, to_read); response_read_offset += to_read; if (response_read_offset == polled_response.size()) { @@ -267,19 +262,13 @@ Error HTTPClient::poll() { int len = godot_xhr_get_response_headers_length(xhr_id); bytes.resize(len + 1); - uint8_t *write = bytes.ptrw(); - godot_xhr_get_response_headers(xhr_id, reinterpret_cast<char *>(write.ptr()), len); - write[len] = 0; - write = uint8_t * (); + godot_xhr_get_response_headers(xhr_id, reinterpret_cast<char *>(bytes.ptrw()), len); + bytes.ptrw()[len] = 0; - const uint8_t *read = bytes.ptr(); - polled_response_header = String::utf8(reinterpret_cast<const char *>(read.ptr())); - read = const uint8_t * (); + polled_response_header = String::utf8(reinterpret_cast<const char *>(bytes.ptr())); polled_response.resize(godot_xhr_get_response_length(xhr_id)); - write = polled_response.ptrw(); - godot_xhr_get_response(xhr_id, write.ptr(), polled_response.size()); - write = uint8_t * (); + godot_xhr_get_response(xhr_id, polled_response.ptrw(), polled_response.size()); break; } diff --git a/platform/javascript/id_handler.js b/platform/javascript/id_handler.js index 3851123ed1..67d29075b8 100644 --- a/platform/javascript/id_handler.js +++ b/platform/javascript/id_handler.js @@ -28,7 +28,7 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -var IDHandler = function() { +var IDHandler = /** @constructor */ function() { var ids = {}; var size = 0; diff --git a/platform/javascript/javascript_eval.cpp b/platform/javascript/javascript_eval.cpp index 44cce28d57..db8050b90e 100644 --- a/platform/javascript/javascript_eval.cpp +++ b/platform/javascript/javascript_eval.cpp @@ -33,11 +33,11 @@ #include "api/javascript_eval.h" #include "emscripten.h" -extern "C" EMSCRIPTEN_KEEPALIVE uint8_t *resize_PackedByteArray_and_open_write(PackedByteArray *p_arr, uint8_t **r_write, int p_len) { +extern "C" EMSCRIPTEN_KEEPALIVE uint8_t *resize_PackedByteArray_and_open_write(PackedByteArray *p_arr, VectorWriteProxy<uint8_t> *r_write, int p_len) { p_arr->resize(p_len); - *r_write = p_arr->write(); - return r_write->ptr(); + *r_write = p_arr->write; + return p_arr->ptrw(); } Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { @@ -49,7 +49,7 @@ Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { } js_data; PackedByteArray arr; - uint8_t *arr_write; + VectorWriteProxy<uint8_t> arr_write; /* clang-format off */ Variant::Type return_type = static_cast<Variant::Type>(EM_ASM_INT({ @@ -138,7 +138,7 @@ Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { return str; } case Variant::PACKED_BYTE_ARRAY: - arr_write = uint8_t * (); + arr_write = VectorWriteProxy<uint8_t>(); return arr; default: return Variant(); diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index 5acdc5f602..1d7a16db80 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -31,7 +31,8 @@ #include "os_javascript.h" #include "core/io/file_access_buffered_fa.h" -#include "drivers/gles2/rasterizer_gles2.h" +//#include "drivers/gles2/rasterizer_gles2.h" +#include "drivers/dummy/rasterizer_dummy.h" #include "drivers/unix/dir_access_unix.h" #include "drivers/unix/file_access_unix.h" #include "main/main.h" @@ -250,7 +251,8 @@ static Ref<InputEventKey> setup_key_event(const EmscriptenKeyboardEvent *emscrip ev.instance(); ev->set_echo(emscripten_event->repeat); dom2godot_mod(emscripten_event, ev); - ev->set_scancode(dom2godot_scancode(emscripten_event->keyCode)); + ev->set_keycode(dom2godot_keycode(emscripten_event->keyCode)); + ev->set_physical_keycode(dom2godot_keycode(emscripten_event->keyCode)); String unicode = String::utf8(emscripten_event->key); // Check if empty or multi-character (e.g. `CapsLock`). @@ -270,7 +272,7 @@ EM_BOOL OS_JavaScript::keydown_callback(int p_event_type, const EmscriptenKeyboa OS_JavaScript *os = get_singleton(); Ref<InputEventKey> ev = setup_key_event(p_event); ev->set_pressed(true); - if (ev->get_unicode() == 0 && keycode_has_unicode(ev->get_scancode())) { + if (ev->get_unicode() == 0 && keycode_has_unicode(ev->get_keycode())) { // Defer to keypress event for legacy unicode retrieval. os->deferred_key_event = ev; // Do not suppress keypress event. @@ -295,7 +297,7 @@ EM_BOOL OS_JavaScript::keyup_callback(int p_event_type, const EmscriptenKeyboard Ref<InputEventKey> ev = setup_key_event(p_event); ev->set_pressed(false); get_singleton()->input->parse_input_event(ev); - return ev->get_scancode() != KEY_UNKNOWN && ev->get_scancode() != 0; + return ev->get_keycode() != KEY_UNKNOWN && ev->get_keycode() != 0; } // Mouse @@ -538,15 +540,11 @@ void OS_JavaScript::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_s PackedByteArray png; size_t len; - const uint8_t *r = image->get_data().ptr(); - ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, r.ptr(), 0, NULL)); + PackedByteArray data = image->get_data(); + ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, NULL)); png.resize(len); - uint8_t *w = png.ptrw(); - ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, w.ptr(), &len, 0, r.ptr(), 0, NULL)); - w = uint8_t * (); - - r = png.ptr(); + ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, NULL)); char *object_url; /* clang-format off */ @@ -561,9 +559,8 @@ void OS_JavaScript::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_s var string_on_wasm_heap = _malloc(length_bytes); setValue(PTR, string_on_wasm_heap, '*'); stringToUTF8(url, string_on_wasm_heap, length_bytes); - }, r.ptr(), len, &object_url); + }, png.ptr(), len, &object_url); /* clang-format on */ - r = const uint8_t * (); String url = String::utf8(object_url) + "?" + itos(p_hotspot.x) + " " + itos(p_hotspot.y); @@ -895,6 +892,7 @@ void OS_JavaScript::initialize_core() { Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) { +#if 0 EmscriptenWebGLContextAttributes attributes; emscripten_webgl_init_context_attributes(&attributes); attributes.alpha = GLOBAL_GET("display/window/per_pixel_transparency/allowed"); @@ -937,6 +935,7 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, if (p_desired.fullscreen) { /* clang-format off */ EM_ASM({ + const canvas = Module.canvas; (canvas.requestFullscreen || canvas.msRequestFullscreen || canvas.mozRequestFullScreen || canvas.mozRequestFullscreen || canvas.webkitRequestFullscreen @@ -951,6 +950,8 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, } else { set_window_size(get_window_size()); } +#endif + RasterizerDummy::make_current(); // TODO GLES2 in Godot 4.0... or webgpu? char locale_ptr[16]; /* clang-format off */ @@ -981,10 +982,10 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, mousedown, mouse_button_callback) SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mouseup, mouse_button_callback) SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, wheel, wheel_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, touchstart, touch_press_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, touchmove, touchmove_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, touchend, touch_press_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, touchcancel, touch_press_callback) + SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchstart, touch_press_callback) + SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchmove, touchmove_callback) + SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchend, touch_press_callback) + SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchcancel, touch_press_callback) SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keydown, keydown_callback) SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keypress, keypress_callback) SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keyup, keyup_callback) @@ -1180,15 +1181,12 @@ void OS_JavaScript::set_icon(const Ref<Image> &p_icon) { PackedByteArray png; size_t len; - const uint8_t *r = icon->get_data().ptr(); - ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, r.ptr(), 0, NULL)); + PackedByteArray data = icon->get_data(); + ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, NULL)); png.resize(len); - uint8_t *w = png.ptrw(); - ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, w.ptr(), &len, 0, r.ptr(), 0, NULL)); - w = uint8_t * (); + ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, NULL)); - r = png.ptr(); /* clang-format off */ EM_ASM_ARGS({ var PNG_PTR = $0; @@ -1204,7 +1202,7 @@ void OS_JavaScript::set_icon(const Ref<Image> &p_icon) { document.head.appendChild(link); } link.href = url; - }, r.ptr(), len); + }, png.ptr(), len); /* clang-format on */ } diff --git a/platform/javascript/pre.js b/platform/javascript/pre.js deleted file mode 100644 index a870e676ea..0000000000 --- a/platform/javascript/pre.js +++ /dev/null @@ -1,5 +0,0 @@ -var Engine = { - RuntimeEnvironment: function(Module, exposedLibs) { - // The above is concatenated with generated code, and acts as the start of - // a wrapper for said code. See engine.js for the other part of the - // wrapper. diff --git a/platform/osx/SCsub b/platform/osx/SCsub index d764ac4b50..0a4e0a45e1 100644 --- a/platform/osx/SCsub +++ b/platform/osx/SCsub @@ -9,7 +9,6 @@ files = [ 'crash_handler_osx.mm', 'os_osx.mm', 'godot_main_osx.mm', - 'semaphore_osx.cpp', 'dir_access_osx.mm', 'joypad_osx.cpp', 'vulkan_context_osx.mm', diff --git a/platform/osx/joypad_osx.cpp b/platform/osx/joypad_osx.cpp index 13ece678f3..e9f46fb5a4 100644 --- a/platform/osx/joypad_osx.cpp +++ b/platform/osx/joypad_osx.cpp @@ -208,9 +208,8 @@ void joypad::add_hid_elements(CFArrayRef p_array) { CFArrayApplyFunction(p_array, range, hid_element_added, this); } -static void joypad_removed_callback(void *ctx, IOReturn result, void *sender) { - int id = (intptr_t)ctx; - self->_device_removed(id); +static void joypad_removed_callback(void *ctx, IOReturn res, void *sender, IOHIDDeviceRef ioHIDDeviceObject) { + self->_device_removed(res, ioHIDDeviceObject); } static void joypad_added_callback(void *ctx, IOReturn res, void *sender, IOHIDDeviceRef ioHIDDeviceObject) { @@ -261,16 +260,15 @@ void JoypadOSX::_device_added(IOReturn p_res, IOHIDDeviceRef p_device) { #endif device_list.push_back(new_joypad); } - IOHIDDeviceRegisterRemovalCallback(p_device, joypad_removed_callback, (void *)(intptr_t)new_joypad.id); IOHIDDeviceScheduleWithRunLoop(p_device, CFRunLoopGetCurrent(), GODOT_JOY_LOOP_RUN_MODE); } -void JoypadOSX::_device_removed(int p_id) { +void JoypadOSX::_device_removed(IOReturn p_res, IOHIDDeviceRef p_device) { - int device = get_joy_index(p_id); + int device = get_joy_ref(p_device); ERR_FAIL_COND(device == -1); - input->joy_connection_changed(p_id, false, ""); + input->joy_connection_changed(device_list[device].id, false, ""); device_list.write[device].free(); device_list.remove(device); } @@ -516,6 +514,13 @@ int JoypadOSX::get_joy_index(int p_id) const { return -1; } +int JoypadOSX::get_joy_ref(IOHIDDeviceRef p_device) const { + for (int i = 0; i < device_list.size(); i++) { + if (device_list[i].device_ref == p_device) return i; + } + return -1; +} + bool JoypadOSX::have_device(IOHIDDeviceRef p_device) const { for (int i = 0; i < device_list.size(); i++) { if (device_list[i].device_ref == p_device) { @@ -558,6 +563,7 @@ void JoypadOSX::config_hid_manager(CFArrayRef p_matching_array) const { IOHIDManagerSetDeviceMatchingMultiple(hid_manager, p_matching_array); IOHIDManagerRegisterDeviceMatchingCallback(hid_manager, joypad_added_callback, NULL); + IOHIDManagerRegisterDeviceRemovalCallback(hid_manager, joypad_removed_callback, NULL); IOHIDManagerScheduleWithRunLoop(hid_manager, runloop, GODOT_JOY_LOOP_RUN_MODE); while (CFRunLoopRunInMode(GODOT_JOY_LOOP_RUN_MODE, 0, TRUE) == kCFRunLoopRunHandledSource) { diff --git a/platform/osx/joypad_osx.h b/platform/osx/joypad_osx.h index 388251016b..2c076b3680 100644 --- a/platform/osx/joypad_osx.h +++ b/platform/osx/joypad_osx.h @@ -103,6 +103,7 @@ private: bool configure_joypad(IOHIDDeviceRef p_device_ref, joypad *p_joy); int get_joy_index(int p_id) const; + int get_joy_ref(IOHIDDeviceRef p_device) const; void poll_joypads() const; void setup_joypad_objects(); @@ -115,7 +116,7 @@ public: void process_joypads(); void _device_added(IOReturn p_res, IOHIDDeviceRef p_device); - void _device_removed(int p_id); + void _device_removed(IOReturn p_res, IOHIDDeviceRef p_device); JoypadOSX(); ~JoypadOSX(); diff --git a/platform/osx/os_osx.h b/platform/osx/os_osx.h index 75a56bd82c..3140d9bac4 100644 --- a/platform/osx/os_osx.h +++ b/platform/osx/os_osx.h @@ -69,7 +69,8 @@ public: bool pressed; bool echo; bool raw; - uint32_t scancode; + uint32_t keycode; + uint32_t physical_keycode; uint32_t unicode; }; diff --git a/platform/osx/os_osx.mm b/platform/osx/os_osx.mm index f2e5f9369c..4c70beee00 100644 --- a/platform/osx/os_osx.mm +++ b/platform/osx/os_osx.mm @@ -44,7 +44,6 @@ #endif #include "main/main.h" -#include "semaphore_osx.h" #include "servers/visual/visual_server_raster.h" #include "servers/visual/visual_server_wrap_mt.h" @@ -157,7 +156,8 @@ static NSCursor *cursorFromSelector(SEL selector, SEL fallback = nil) { get_key_modifier_state([event modifierFlags], k); k->set_pressed(true); - k->set_scancode(KEY_PERIOD); + k->set_keycode(KEY_PERIOD); + k->set_physical_keycode(KEY_PERIOD); k->set_echo([event isARepeat]); OS_OSX::singleton->push_input(k); @@ -635,7 +635,8 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; ke.pressed = true; ke.echo = false; ke.raw = false; // IME input event - ke.scancode = 0; + ke.keycode = 0; + ke.physical_keycode = 0; ke.unicode = codepoint; push_to_key_event_buffer(ke); @@ -1158,7 +1159,8 @@ static int remapKey(unsigned int key, unsigned int state) { ke.osx_state = [event modifierFlags]; ke.pressed = true; ke.echo = [event isARepeat]; - ke.scancode = remapKey([event keyCode], [event modifierFlags]); + ke.keycode = remapKey([event keyCode], [event modifierFlags]); + ke.physical_keycode = translateKey([event keyCode]); ke.raw = true; ke.unicode = [characters characterAtIndex:i]; @@ -1170,7 +1172,8 @@ static int remapKey(unsigned int key, unsigned int state) { ke.osx_state = [event modifierFlags]; ke.pressed = true; ke.echo = [event isARepeat]; - ke.scancode = remapKey([event keyCode], [event modifierFlags]); + ke.keycode = remapKey([event keyCode], [event modifierFlags]); + ke.physical_keycode = translateKey([event keyCode]); ke.raw = false; ke.unicode = 0; @@ -1228,7 +1231,8 @@ static int remapKey(unsigned int key, unsigned int state) { } ke.osx_state = mod; - ke.scancode = remapKey(key, mod); + ke.keycode = remapKey(key, mod); + ke.physical_keycode = translateKey(key); ke.unicode = 0; push_to_key_event_buffer(ke); @@ -1250,7 +1254,8 @@ static int remapKey(unsigned int key, unsigned int state) { ke.osx_state = [event modifierFlags]; ke.pressed = false; ke.echo = [event isARepeat]; - ke.scancode = remapKey([event keyCode], [event modifierFlags]); + ke.keycode = remapKey([event keyCode], [event modifierFlags]); + ke.physical_keycode = translateKey([event keyCode]); ke.raw = true; ke.unicode = [characters characterAtIndex:i]; @@ -1262,7 +1267,8 @@ static int remapKey(unsigned int key, unsigned int state) { ke.osx_state = [event modifierFlags]; ke.pressed = false; ke.echo = [event isARepeat]; - ke.scancode = remapKey([event keyCode], [event modifierFlags]); + ke.keycode = remapKey([event keyCode], [event modifierFlags]); + ke.physical_keycode = translateKey([event keyCode]); ke.raw = true; ke.unicode = 0; @@ -1457,8 +1463,6 @@ void OS_OSX::initialize_core() { DirAccess::make_default<DirAccessOSX>(DirAccess::ACCESS_RESOURCES); DirAccess::make_default<DirAccessOSX>(DirAccess::ACCESS_USERDATA); DirAccess::make_default<DirAccessOSX>(DirAccess::ACCESS_FILESYSTEM); - - SemaphoreOSX::make_default(); } static bool keyboard_layout_dirty = true; @@ -2845,32 +2849,35 @@ void OS_OSX::process_key_events() { get_key_modifier_state(ke.osx_state, k); k->set_pressed(ke.pressed); k->set_echo(ke.echo); - k->set_scancode(ke.scancode); + k->set_keycode(ke.keycode); + k->set_physical_keycode(ke.physical_keycode); k->set_unicode(ke.unicode); push_input(k); } else { // IME input - if ((i == 0 && ke.scancode == 0) || (i > 0 && key_event_buffer[i - 1].scancode == 0)) { + if ((i == 0 && ke.keycode == 0) || (i > 0 && key_event_buffer[i - 1].keycode == 0)) { k.instance(); get_key_modifier_state(ke.osx_state, k); k->set_pressed(ke.pressed); k->set_echo(ke.echo); - k->set_scancode(0); + k->set_keycode(0); + k->set_physical_keycode(0); k->set_unicode(ke.unicode); push_input(k); } - if (ke.scancode != 0) { + if (ke.keycode != 0) { k.instance(); get_key_modifier_state(ke.osx_state, k); k->set_pressed(ke.pressed); k->set_echo(ke.echo); - k->set_scancode(ke.scancode); + k->set_keycode(ke.keycode); + k->set_physical_keycode(ke.physical_keycode); - if (i + 1 < key_event_pos && key_event_buffer[i + 1].scancode == 0) { + if (i + 1 < key_event_pos && key_event_buffer[i + 1].keycode == 0) { k->set_unicode(key_event_buffer[i + 1].unicode); } diff --git a/platform/server/SCsub b/platform/server/SCsub index e8538f03a6..8364164114 100644 --- a/platform/server/SCsub +++ b/platform/server/SCsub @@ -10,7 +10,6 @@ common_server = [\ if sys.platform == "darwin": common_server.append("#platform/osx/crash_handler_osx.mm") - common_server.append("#platform/osx/semaphore_osx.cpp") else: common_server.append("#platform/x11/crash_handler_x11.cpp") diff --git a/platform/server/detect.py b/platform/server/detect.py index ef94dc436c..db9ba8d036 100644 --- a/platform/server/detect.py +++ b/platform/server/detect.py @@ -163,6 +163,10 @@ def configure(env): sys.exit(255) env.ParseConfig('pkg-config bullet --cflags --libs') + if False: # not env['builtin_assimp']: + # FIXME: Add min version check + env.ParseConfig('pkg-config assimp --cflags --libs') + if not env['builtin_enet']: env.ParseConfig('pkg-config libenet --cflags --libs') diff --git a/platform/server/os_server.cpp b/platform/server/os_server.cpp index 8a66332ff1..3257ec261c 100644 --- a/platform/server/os_server.cpp +++ b/platform/server/os_server.cpp @@ -68,10 +68,6 @@ void OS_Server::initialize_core() { crash_handler.initialize(); OS_Unix::initialize_core(); - -#ifdef __APPLE__ - SemaphoreOSX::make_default(); -#endif } Error OS_Server::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) { diff --git a/platform/uwp/app.cpp b/platform/uwp/app.cpp index a47fe96c1b..ccb4b43373 100644 --- a/platform/uwp/app.cpp +++ b/platform/uwp/app.cpp @@ -410,14 +410,16 @@ void App::key_event(Windows::UI::Core::CoreWindow ^ sender, bool p_pressed, Wind ke.type = OS_UWP::KeyEvent::MessageType::KEY_EVENT_MESSAGE; ke.unicode = 0; - ke.scancode = KeyMappingWindows::get_keysym((unsigned int)key_args->VirtualKey); + ke.keycode = KeyMappingWindows::get_keysym((unsigned int)key_args->VirtualKey); + ke.physical_keycode = KeyMappingWindows::get_scansym((unsigned int)key_args->KeyStatus.ScanCode); ke.echo = (!p_pressed && !key_args->KeyStatus.IsKeyReleased) || (p_pressed && key_args->KeyStatus.WasKeyDown); } else { ke.type = OS_UWP::KeyEvent::MessageType::CHAR_EVENT_MESSAGE; ke.unicode = char_args->KeyCode; - ke.scancode = 0; + ke.keycode = 0; + ke.physical_keycode = 0; ke.echo = (!p_pressed && !char_args->KeyStatus.IsKeyReleased) || (p_pressed && char_args->KeyStatus.WasKeyDown); } diff --git a/platform/uwp/os_uwp.cpp b/platform/uwp/os_uwp.cpp index 3cd7a02a94..4ddb5463d0 100644 --- a/platform/uwp/os_uwp.cpp +++ b/platform/uwp/os_uwp.cpp @@ -140,8 +140,6 @@ void OS_UWP::initialize_core() { //RedirectIOToConsole(); ThreadUWP::make_default(); - SemaphoreWindows::make_default(); - MutexWindows::make_default(); RWLockWindows::make_default(); FileAccess::make_default<FileAccessWindows>(FileAccess::ACCESS_RESOURCES); @@ -606,7 +604,8 @@ void OS_UWP::process_key_events() { key_event->set_shift(kev.shift); key_event->set_control(kev.control); key_event->set_echo(kev.echo); - key_event->set_scancode(kev.scancode); + key_event->set_keycode(kev.keycode); + key_event->set_physical_keycode(kev.physical_keycode); key_event->set_unicode(kev.unicode); key_event->set_pressed(kev.pressed); diff --git a/platform/uwp/os_uwp.h b/platform/uwp/os_uwp.h index 32b899c0da..ac6e0f3dd5 100644 --- a/platform/uwp/os_uwp.h +++ b/platform/uwp/os_uwp.h @@ -61,7 +61,8 @@ public: bool alt, shift, control; MessageType type; bool pressed; - unsigned int scancode; + unsigned int keycode; + unsigned int physical_keycode; unsigned int unicode; bool echo; CorePhysicalKeyStatus status; diff --git a/platform/windows/key_mapping_windows.cpp b/platform/windows/key_mapping_windows.cpp index c76b31ca9c..da63e92622 100644 --- a/platform/windows/key_mapping_windows.cpp +++ b/platform/windows/key_mapping_windows.cpp @@ -238,6 +238,104 @@ VK_PA1 (0xFD) VK_OEM_CLEAR (0xFE) */ +static _WinTranslatePair _scancode_to_keycode[] = { + + { KEY_ESCAPE, 0x01 }, + { KEY_1, 0x02 }, + { KEY_2, 0x03 }, + { KEY_3, 0x04 }, + { KEY_4, 0x05 }, + { KEY_5, 0x06 }, + { KEY_6, 0x07 }, + { KEY_7, 0x08 }, + { KEY_8, 0x09 }, + { KEY_9, 0x0A }, + { KEY_0, 0x0B }, + { KEY_MINUS, 0x0C }, + { KEY_EQUAL, 0x0D }, + { KEY_BACKSPACE, 0x0E }, + { KEY_TAB, 0x0F }, + { KEY_Q, 0x10 }, + { KEY_W, 0x11 }, + { KEY_E, 0x12 }, + { KEY_R, 0x13 }, + { KEY_T, 0x14 }, + { KEY_Y, 0x15 }, + { KEY_U, 0x16 }, + { KEY_I, 0x17 }, + { KEY_O, 0x18 }, + { KEY_P, 0x19 }, + { KEY_BRACELEFT, 0x1A }, + { KEY_BRACERIGHT, 0x1B }, + { KEY_ENTER, 0x1C }, + { KEY_CONTROL, 0x1D }, + { KEY_A, 0x1E }, + { KEY_S, 0x1F }, + { KEY_D, 0x20 }, + { KEY_F, 0x21 }, + { KEY_G, 0x22 }, + { KEY_H, 0x23 }, + { KEY_J, 0x24 }, + { KEY_K, 0x25 }, + { KEY_L, 0x26 }, + { KEY_SEMICOLON, 0x27 }, + { KEY_APOSTROPHE, 0x28 }, + { KEY_QUOTELEFT, 0x29 }, + { KEY_SHIFT, 0x2A }, + { KEY_BACKSLASH, 0x2B }, + { KEY_Z, 0x2C }, + { KEY_X, 0x2D }, + { KEY_C, 0x2E }, + { KEY_V, 0x2F }, + { KEY_B, 0x30 }, + { KEY_N, 0x31 }, + { KEY_M, 0x32 }, + { KEY_COMMA, 0x33 }, + { KEY_PERIOD, 0x34 }, + { KEY_SLASH, 0x35 }, + { KEY_SHIFT, 0x36 }, + { KEY_PRINT, 0x37 }, + { KEY_ALT, 0x38 }, + { KEY_SPACE, 0x39 }, + { KEY_CAPSLOCK, 0x3A }, + { KEY_F1, 0x3B }, + { KEY_F2, 0x3C }, + { KEY_F3, 0x3D }, + { KEY_F4, 0x3E }, + { KEY_F5, 0x3F }, + { KEY_F6, 0x40 }, + { KEY_F7, 0x41 }, + { KEY_F8, 0x42 }, + { KEY_F9, 0x43 }, + { KEY_F10, 0x44 }, + { KEY_NUMLOCK, 0x45 }, + { KEY_SCROLLLOCK, 0x46 }, + { KEY_HOME, 0x47 }, + { KEY_UP, 0x48 }, + { KEY_PAGEUP, 0x49 }, + { KEY_KP_SUBTRACT, 0x4A }, + { KEY_LEFT, 0x4B }, + { KEY_KP_5, 0x4C }, + { KEY_RIGHT, 0x4D }, + { KEY_KP_ADD, 0x4E }, + { KEY_END, 0x4F }, + { KEY_DOWN, 0x50 }, + { KEY_PAGEDOWN, 0x51 }, + { KEY_INSERT, 0x52 }, + { KEY_DELETE, 0x53 }, + //{ KEY_???, 0x56 }, //NON US BACKSLASH + { KEY_F11, 0x57 }, + { KEY_F12, 0x58 }, + { KEY_META, 0x5B }, + { KEY_META, 0x5C }, + { KEY_MENU, 0x5D }, + { KEY_F13, 0x64 }, + { KEY_F14, 0x65 }, + { KEY_F15, 0x66 }, + { KEY_F16, 0x67 }, + { KEY_UNKNOWN, 0 } +}; + unsigned int KeyMappingWindows::get_keysym(unsigned int p_code) { for (int i = 0; _vk_to_keycode[i].keysym != KEY_UNKNOWN; i++) { @@ -251,3 +349,69 @@ unsigned int KeyMappingWindows::get_keysym(unsigned int p_code) { return KEY_UNKNOWN; } + +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++) { + + if (_scancode_to_keycode[i].keycode == p_code) { + keycode = _scancode_to_keycode[i].keysym; + break; + } + } + + if (p_extended) { + switch (keycode) { + case KEY_ENTER: { + keycode = KEY_KP_ENTER; + } break; + case KEY_SLASH: { + keycode = KEY_KP_DIVIDE; + } break; + case KEY_CAPSLOCK: { + keycode = KEY_KP_ADD; + } break; + } + } else { + switch (keycode) { + case KEY_NUMLOCK: { + keycode = KEY_PAUSE; + } break; + case KEY_HOME: { + keycode = KEY_KP_7; + } break; + case KEY_UP: { + keycode = KEY_KP_8; + } break; + case KEY_PAGEUP: { + keycode = KEY_KP_9; + } break; + case KEY_LEFT: { + keycode = KEY_KP_4; + } break; + case KEY_RIGHT: { + keycode = KEY_KP_6; + } break; + case KEY_END: { + keycode = KEY_KP_1; + } break; + case KEY_DOWN: { + keycode = KEY_KP_2; + } break; + case KEY_PAGEDOWN: { + keycode = KEY_KP_3; + } break; + case KEY_INSERT: { + keycode = KEY_KP_0; + } break; + case KEY_DELETE: { + keycode = KEY_KP_PERIOD; + } break; + case KEY_PRINT: { + keycode = KEY_KP_MULTIPLY; + } break; + } + } + + return keycode; +} diff --git a/platform/windows/key_mapping_windows.h b/platform/windows/key_mapping_windows.h index 0f9bdecde1..3361ad397f 100644 --- a/platform/windows/key_mapping_windows.h +++ b/platform/windows/key_mapping_windows.h @@ -43,6 +43,7 @@ class KeyMappingWindows { public: static unsigned int get_keysym(unsigned int p_code); + static unsigned int get_scansym(unsigned int p_code, bool p_extended); }; #endif // KEY_MAPPING_WINDOWS_H diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 716a637993..a112f26ac4 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -33,8 +33,9 @@ #include "os_windows.h" +#include "core/debugger/engine_debugger.h" +#include "core/debugger/script_debugger.h" #include "core/io/marshalls.h" -#include "core/script_language.h" #include "core/version_generated.gen.h" #if defined(OPENGL_ENABLED) @@ -47,9 +48,7 @@ #include "drivers/windows/dir_access_windows.h" #include "drivers/windows/file_access_windows.h" -#include "drivers/windows/mutex_windows.h" #include "drivers/windows/rw_lock_windows.h" -#include "drivers/windows/semaphore_windows.h" #include "drivers/windows/thread_windows.h" #include "joypad_windows.h" #include "lang_table.h" @@ -196,13 +195,13 @@ void RedirectIOToConsole() { } BOOL WINAPI HandlerRoutine(_In_ DWORD dwCtrlType) { - if (ScriptDebugger::get_singleton() == NULL) + if (!EngineDebugger::is_active()) return FALSE; switch (dwCtrlType) { case CTRL_C_EVENT: - ScriptDebugger::get_singleton()->set_depth(-1); - ScriptDebugger::get_singleton()->set_lines_left(1); + EngineDebugger::get_script_debugger()->set_depth(-1); + EngineDebugger::get_script_debugger()->set_lines_left(1); return TRUE; default: return FALSE; @@ -229,8 +228,6 @@ void OS_Windows::initialize_core() { borderless = false; ThreadWindows::make_default(); - SemaphoreWindows::make_default(); - MutexWindows::make_default(); RWLockWindows::make_default(); FileAccess::make_default<FileAccessWindows>(FileAccess::ACCESS_RESOURCES); @@ -1135,7 +1132,8 @@ void OS_Windows::process_key_events() { k->set_control(ke.control); k->set_metakey(ke.meta); k->set_pressed(true); - k->set_scancode(KeyMappingWindows::get_keysym(ke.wParam)); + k->set_keycode(KeyMappingWindows::get_keysym(ke.wParam)); + k->set_physical_keycode(KeyMappingWindows::get_scansym((ke.lParam >> 16) & 0xFF, ke.lParam & (1 << 24))); k->set_unicode(ke.wParam); if (k->get_unicode() && gr_mem) { k->set_alt(false); @@ -1165,11 +1163,13 @@ void OS_Windows::process_key_events() { if ((ke.lParam & (1 << 24)) && (ke.wParam == VK_RETURN)) { // Special case for Numpad Enter key - k->set_scancode(KEY_KP_ENTER); + k->set_keycode(KEY_KP_ENTER); } else { - k->set_scancode(KeyMappingWindows::get_keysym(ke.wParam)); + k->set_keycode(KeyMappingWindows::get_keysym(ke.wParam)); } + k->set_physical_keycode(KeyMappingWindows::get_scansym((ke.lParam >> 16) & 0xFF, ke.lParam & (1 << 24))); + if (i + 1 < key_event_pos && key_event_buffer[i + 1].uMsg == WM_CHAR) { k->set_unicode(key_event_buffer[i + 1].wParam); } @@ -2723,7 +2723,7 @@ Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, if (p_pipe_mutex) { p_pipe_mutex->lock(); } - (*r_pipe) += buf; + (*r_pipe) += String::utf8(buf); if (p_pipe_mutex) { p_pipe_mutex->unlock(); } diff --git a/platform/x11/detect.py b/platform/x11/detect.py index cd22ee9ff6..9a9ab86068 100644 --- a/platform/x11/detect.py +++ b/platform/x11/detect.py @@ -1,7 +1,6 @@ import os import platform import sys -from methods import get_compiler_version, using_gcc, using_clang def is_active(): @@ -226,6 +225,10 @@ def configure(env): sys.exit(255) env.ParseConfig('pkg-config bullet --cflags --libs') + if False: # not env['builtin_assimp']: + # FIXME: Add min version check + env.ParseConfig('pkg-config assimp --cflags --libs') + if not env['builtin_enet']: env.ParseConfig('pkg-config libenet --cflags --libs') diff --git a/platform/x11/detect_prime.cpp b/platform/x11/detect_prime.cpp index 98a51ff27c..a0e5f835c0 100644 --- a/platform/x11/detect_prime.cpp +++ b/platform/x11/detect_prime.cpp @@ -62,6 +62,7 @@ vendor vendormap[] = { { "NVIDIA Corporation", 30 }, { "X.Org", 30 }, { "Intel Open Source Technology Center", 20 }, + { "Intel", 20 }, { "nouveau", 10 }, { "Mesa Project", 0 }, { NULL, 0 } diff --git a/platform/x11/joypad_linux.cpp b/platform/x11/joypad_linux.cpp index a64a25aeee..a9fe7275c2 100644 --- a/platform/x11/joypad_linux.cpp +++ b/platform/x11/joypad_linux.cpp @@ -83,7 +83,6 @@ void JoypadLinux::Joypad::reset() { JoypadLinux::JoypadLinux(InputDefault *in) { exit_udev = false; input = in; - joy_mutex = Mutex::create(); joy_thread = Thread::create(joy_thread_func, this); } @@ -91,7 +90,6 @@ JoypadLinux::~JoypadLinux() { exit_udev = true; Thread::wait_to_finish(joy_thread); memdelete(joy_thread); - memdelete(joy_mutex); close_joypad(); } @@ -137,9 +135,8 @@ void JoypadLinux::enumerate_joypads(udev *p_udev) { String devnode_str = devnode; if (devnode_str.find(ignore_str) == -1) { - joy_mutex->lock(); + MutexLock lock(joy_mutex); open_joypad(devnode); - joy_mutex->unlock(); } } udev_device_unref(dev); @@ -176,7 +173,7 @@ void JoypadLinux::monitor_joypads(udev *p_udev) { if (dev && udev_device_get_devnode(dev) != 0) { - joy_mutex->lock(); + MutexLock lock(joy_mutex); String action = udev_device_get_action(dev); const char *devnode = udev_device_get_devnode(dev); if (devnode) { @@ -192,7 +189,6 @@ void JoypadLinux::monitor_joypads(udev *p_udev) { } udev_device_unref(dev); - joy_mutex->unlock(); } } usleep(50000); @@ -204,15 +200,17 @@ void JoypadLinux::monitor_joypads(udev *p_udev) { void JoypadLinux::monitor_joypads() { while (!exit_udev) { - joy_mutex->lock(); - for (int i = 0; i < 32; i++) { - char fname[64]; - sprintf(fname, "/dev/input/event%d", i); - if (attached_devices.find(fname) == -1) { - open_joypad(fname); + { + MutexLock lock(joy_mutex); + + for (int i = 0; i < 32; i++) { + char fname[64]; + sprintf(fname, "/dev/input/event%d", i); + if (attached_devices.find(fname) == -1) { + open_joypad(fname); + } } } - joy_mutex->unlock(); usleep(1000000); // 1s } } @@ -457,7 +455,7 @@ InputDefault::JoyAxis JoypadLinux::axis_correct(const input_absinfo *p_abs, int void JoypadLinux::process_joypads() { - if (joy_mutex->try_lock() != OK) { + if (joy_mutex.try_lock() != OK) { return; } for (int i = 0; i < JOYPADS_MAX; i++) { @@ -548,6 +546,6 @@ void JoypadLinux::process_joypads() { } } } - joy_mutex->unlock(); + joy_mutex.unlock(); } #endif diff --git a/platform/x11/joypad_linux.h b/platform/x11/joypad_linux.h index e5638899bf..d5719b6dbe 100644 --- a/platform/x11/joypad_linux.h +++ b/platform/x11/joypad_linux.h @@ -72,7 +72,7 @@ private: }; bool exit_udev; - Mutex *joy_mutex; + Mutex joy_mutex; Thread *joy_thread; InputDefault *input; Joypad joypads[JOYPADS_MAX]; diff --git a/platform/x11/key_mapping_x11.cpp b/platform/x11/key_mapping_x11.cpp index 54e1e1d357..78bd2b71a0 100644 --- a/platform/x11/key_mapping_x11.cpp +++ b/platform/x11/key_mapping_x11.cpp @@ -180,6 +180,140 @@ static _XTranslatePair _xkeysym_to_keycode[] = { { 0, 0 } }; +struct _TranslatePair { + + unsigned int keysym; + unsigned int keycode; +}; + +static _TranslatePair _scancode_to_keycode[] = { + + { KEY_ESCAPE, 0x09 }, + { KEY_1, 0x0A }, + { KEY_2, 0x0B }, + { KEY_3, 0x0C }, + { KEY_4, 0x0D }, + { KEY_5, 0x0E }, + { KEY_6, 0x0F }, + { KEY_7, 0x10 }, + { KEY_8, 0x11 }, + { KEY_9, 0x12 }, + { KEY_0, 0x13 }, + { KEY_MINUS, 0x14 }, + { KEY_EQUAL, 0x15 }, + { KEY_BACKSPACE, 0x16 }, + { KEY_TAB, 0x17 }, + { KEY_Q, 0x18 }, + { KEY_W, 0x19 }, + { KEY_E, 0x1A }, + { KEY_R, 0x1B }, + { KEY_T, 0x1C }, + { KEY_Y, 0x1D }, + { KEY_U, 0x1E }, + { KEY_I, 0x1F }, + { KEY_O, 0x20 }, + { KEY_P, 0x21 }, + { KEY_BRACELEFT, 0x22 }, + { KEY_BRACERIGHT, 0x23 }, + { KEY_ENTER, 0x24 }, + { KEY_CONTROL, 0x25 }, + { KEY_A, 0x26 }, + { KEY_S, 0x27 }, + { KEY_D, 0x28 }, + { KEY_F, 0x29 }, + { KEY_G, 0x2A }, + { KEY_H, 0x2B }, + { KEY_J, 0x2C }, + { KEY_K, 0x2D }, + { KEY_L, 0x2E }, + { KEY_SEMICOLON, 0x2F }, + { KEY_APOSTROPHE, 0x30 }, + { KEY_QUOTELEFT, 0x31 }, + { KEY_SHIFT, 0x32 }, + { KEY_BACKSLASH, 0x33 }, + { KEY_Z, 0x34 }, + { KEY_X, 0x35 }, + { KEY_C, 0x36 }, + { KEY_V, 0x37 }, + { KEY_B, 0x38 }, + { KEY_N, 0x39 }, + { KEY_M, 0x3A }, + { KEY_COMMA, 0x3B }, + { KEY_PERIOD, 0x3C }, + { KEY_SLASH, 0x3D }, + { KEY_SHIFT, 0x3E }, + { KEY_KP_MULTIPLY, 0x3F }, + { KEY_ALT, 0x40 }, + { KEY_SPACE, 0x41 }, + { KEY_CAPSLOCK, 0x42 }, + { KEY_F1, 0x43 }, + { KEY_F2, 0x44 }, + { KEY_F3, 0x45 }, + { KEY_F4, 0x46 }, + { KEY_F5, 0x47 }, + { KEY_F6, 0x48 }, + { KEY_F7, 0x49 }, + { KEY_F8, 0x4A }, + { KEY_F9, 0x4B }, + { KEY_F10, 0x4C }, + { KEY_NUMLOCK, 0x4D }, + { KEY_SCROLLLOCK, 0x4E }, + { KEY_KP_7, 0x4F }, + { KEY_KP_8, 0x50 }, + { KEY_KP_9, 0x51 }, + { KEY_KP_SUBTRACT, 0x52 }, + { KEY_KP_4, 0x53 }, + { KEY_KP_5, 0x54 }, + { KEY_KP_6, 0x55 }, + { KEY_KP_ADD, 0x56 }, + { KEY_KP_1, 0x57 }, + { KEY_KP_2, 0x58 }, + { KEY_KP_3, 0x59 }, + { KEY_KP_0, 0x5A }, + { KEY_KP_PERIOD, 0x5B }, + //{ KEY_???, 0x5E }, //NON US BACKSLASH + { KEY_F11, 0x5F }, + { KEY_F12, 0x60 }, + { KEY_KP_ENTER, 0x68 }, + { KEY_CONTROL, 0x69 }, + { KEY_KP_DIVIDE, 0x6A }, + { KEY_PRINT, 0x6B }, + { KEY_ALT, 0x6C }, + { KEY_ENTER, 0x6D }, + { KEY_HOME, 0x6E }, + { KEY_UP, 0x6F }, + { KEY_PAGEUP, 0x70 }, + { KEY_LEFT, 0x71 }, + { KEY_RIGHT, 0x72 }, + { KEY_END, 0x73 }, + { KEY_DOWN, 0x74 }, + { KEY_PAGEDOWN, 0x75 }, + { KEY_INSERT, 0x76 }, + { KEY_DELETE, 0x77 }, + { KEY_VOLUMEMUTE, 0x79 }, + { KEY_VOLUMEDOWN, 0x7A }, + { KEY_VOLUMEUP, 0x7B }, + { KEY_PAUSE, 0x7F }, + { KEY_SUPER_L, 0x85 }, + { KEY_SUPER_R, 0x86 }, + { KEY_MENU, 0x87 }, + { KEY_UNKNOWN, 0 } +}; + +unsigned int KeyMappingX11::get_scancode(unsigned int p_code) { + + unsigned int keycode = KEY_UNKNOWN; + for (int i = 0; _scancode_to_keycode[i].keysym != KEY_UNKNOWN; i++) { + + if (_scancode_to_keycode[i].keycode == p_code) { + keycode = _scancode_to_keycode[i].keysym; + break; + } + } + + return keycode; +} + unsigned int KeyMappingX11::get_keycode(KeySym p_keysym) { // kinda bruteforce.. could optimize. diff --git a/platform/x11/key_mapping_x11.h b/platform/x11/key_mapping_x11.h index e99bf1694b..10db43bcc4 100644 --- a/platform/x11/key_mapping_x11.h +++ b/platform/x11/key_mapping_x11.h @@ -45,6 +45,7 @@ class KeyMappingX11 { public: static unsigned int get_keycode(KeySym p_keysym); + static unsigned int get_scancode(unsigned int p_code); static KeySym get_keysym(unsigned int p_code); static unsigned int get_unicode_from_keysym(KeySym p_keysym); static KeySym get_keysym_from_unicode(unsigned int p_unicode); diff --git a/platform/x11/os_x11.cpp b/platform/x11/os_x11.cpp index 36e9681f5f..ddb5237d1b 100644 --- a/platform/x11/os_x11.cpp +++ b/platform/x11/os_x11.cpp @@ -1888,6 +1888,8 @@ void OS_X11::handle_key_event(XKeyEvent *p_event, bool p_echo) { if (status == XLookupChars) { bool keypress = xkeyevent->type == KeyPress; unsigned int keycode = KeyMappingX11::get_keycode(keysym_keycode); + unsigned int physical_keycode = KeyMappingX11::get_scancode(xkeyevent->keycode); + if (keycode >= 'a' && keycode <= 'z') keycode -= 'a' - 'A'; @@ -1896,23 +1898,29 @@ void OS_X11::handle_key_event(XKeyEvent *p_event, bool p_echo) { for (int i = 0; i < tmp.length(); i++) { Ref<InputEventKey> k; k.instance(); - if (keycode == 0 && tmp[i] == 0) { + if (physical_keycode == 0 && keycode == 0 && tmp[i] == 0) { continue; } + if (keycode == 0) + keycode = physical_keycode; + get_key_modifier_state(xkeyevent->state, k); k->set_unicode(tmp[i]); k->set_pressed(keypress); - k->set_scancode(keycode); + k->set_keycode(keycode); + + k->set_physical_keycode(physical_keycode); k->set_echo(false); - if (k->get_scancode() == KEY_BACKTAB) { + if (k->get_keycode() == KEY_BACKTAB) { //make it consistent across platforms. - k->set_scancode(KEY_TAB); + k->set_keycode(KEY_TAB); + k->set_physical_keycode(KEY_TAB); k->set_shift(true); } @@ -1942,6 +1950,7 @@ void OS_X11::handle_key_event(XKeyEvent *p_event, bool p_echo) { // keysym, so it works in all platforms the same. unsigned int keycode = KeyMappingX11::get_keycode(keysym_keycode); + unsigned int physical_keycode = KeyMappingX11::get_scancode(xkeyevent->keycode); /* Phase 3, obtain a unicode character from the keysym */ @@ -1961,9 +1970,12 @@ void OS_X11::handle_key_event(XKeyEvent *p_event, bool p_echo) { bool keypress = xkeyevent->type == KeyPress; - if (keycode == 0 && unicode == 0) + if (physical_keycode == 0 && keycode == 0 && unicode == 0) return; + if (keycode == 0) + keycode = physical_keycode; + /* Phase 5, determine modifier mask */ // No problems here, except I had no way to @@ -2000,8 +2012,10 @@ void OS_X11::handle_key_event(XKeyEvent *p_event, bool p_echo) { // is correct, but the xorg developers are // not very helpful today. - ::Time tresh = ABSDIFF(peek_event.xkey.time, xkeyevent->time); - if (peek_event.type == KeyPress && tresh < 5) { +#define ABSDIFF(x, y) (((x) < (y)) ? ((y) - (x)) : ((x) - (y))) + ::Time threshold = ABSDIFF(peek_event.xkey.time, xkeyevent->time); +#undef ABSDIFF + if (peek_event.type == KeyPress && threshold < 5) { KeySym rk; XLookupString((XKeyEvent *)&peek_event, str, 256, &rk, NULL); if (rk == keysym_keycode) { @@ -2025,37 +2039,39 @@ void OS_X11::handle_key_event(XKeyEvent *p_event, bool p_echo) { if (keycode >= 'a' && keycode <= 'z') keycode -= 'a' - 'A'; - k->set_scancode(keycode); + k->set_keycode(keycode); + k->set_physical_keycode(physical_keycode); k->set_unicode(unicode); k->set_echo(p_echo); - if (k->get_scancode() == KEY_BACKTAB) { + if (k->get_keycode() == KEY_BACKTAB) { //make it consistent across platforms. - k->set_scancode(KEY_TAB); + k->set_keycode(KEY_TAB); + k->set_physical_keycode(KEY_TAB); k->set_shift(true); } //don't set mod state if modifier keys are released by themselves //else event.is_action() will not work correctly here if (!k->is_pressed()) { - if (k->get_scancode() == KEY_SHIFT) + if (k->get_keycode() == KEY_SHIFT) k->set_shift(false); - else if (k->get_scancode() == KEY_CONTROL) + else if (k->get_keycode() == KEY_CONTROL) k->set_control(false); - else if (k->get_scancode() == KEY_ALT) + else if (k->get_keycode() == KEY_ALT) k->set_alt(false); - else if (k->get_scancode() == KEY_META) + else if (k->get_keycode() == KEY_META) k->set_metakey(false); } - bool last_is_pressed = Input::get_singleton()->is_key_pressed(k->get_scancode()); + bool last_is_pressed = Input::get_singleton()->is_key_pressed(k->get_keycode()); if (k->is_pressed()) { if (last_is_pressed) { k->set_echo(true); } } - //printf("key: %x\n",k->get_scancode()); + //printf("key: %x\n",k->get_keycode()); input->accumulate_input_event(k); } |