From b0c2516d5b9f9d7b8b23483a800ef3d45b444ff2 Mon Sep 17 00:00:00 2001 From: bofeng-song Date: Tue, 19 Apr 2022 16:43:33 +0800 Subject: [PATCH] integrate gamesdk source code (#248) * integrate gamesdk source code * refine --- sources/CMakeLists.txt | 15 + .../include/game-activity/GameActivity.cpp | 1304 +++++++++++++++ .../include/game-activity/GameActivity.h | 769 +++++++++ .../native_app_glue/android_native_app_glue.c | 571 +++++++ .../native_app_glue/android_native_app_glue.h | 486 ++++++ .../include/game-text-input/gamecommon.h | 41 + .../include/game-text-input/gametextinput.cpp | 366 +++++ .../include/game-text-input/gametextinput.h | 290 ++++ .../GameActivity/game-activity/module.json | 1 + .../GameController/CMakeLists.txt | 44 + .../GameController/GameController.cpp | 458 ++++++ .../GameController/GameController.h | 165 ++ .../GameControllerDeviceInfo.cpp | 42 + .../GameController/GameControllerDeviceInfo.h | 72 + .../GameControllerGameActivityMirror.h | 72 + .../GameControllerInternalConstants.h | 31 + .../GameController/GameControllerLog.cpp | 122 ++ .../GameController/GameControllerLog.h | 27 + .../GameController/GameControllerLogStrings.h | 391 +++++ .../GameController/GameControllerManager.cpp | 1443 +++++++++++++++++ .../GameController/GameControllerManager.h | 223 +++ .../GameControllerMappingUtils.cpp | 179 ++ .../GameControllerMappingUtils.h | 60 + .../InternalControllerTable.cpp | 1005 ++++++++++++ .../GameController/InternalControllerTable.h | 27 + sources/android-gamesdk/GameController/Log.h | 40 + .../GameController/ThreadUtil.h | 47 + .../paddleboat/include/paddleboat.h | 1067 ++++++++++++ .../GameController/paddleboat_c.cpp | 182 +++ 29 files changed, 9540 insertions(+) create mode 100644 sources/android-gamesdk/GameActivity/game-activity/include/game-activity/GameActivity.cpp create mode 100644 sources/android-gamesdk/GameActivity/game-activity/include/game-activity/GameActivity.h create mode 100644 sources/android-gamesdk/GameActivity/game-activity/include/game-activity/native_app_glue/android_native_app_glue.c create mode 100644 sources/android-gamesdk/GameActivity/game-activity/include/game-activity/native_app_glue/android_native_app_glue.h create mode 100644 sources/android-gamesdk/GameActivity/game-activity/include/game-text-input/gamecommon.h create mode 100644 sources/android-gamesdk/GameActivity/game-activity/include/game-text-input/gametextinput.cpp create mode 100644 sources/android-gamesdk/GameActivity/game-activity/include/game-text-input/gametextinput.h create mode 100644 sources/android-gamesdk/GameActivity/game-activity/module.json create mode 100644 sources/android-gamesdk/GameController/CMakeLists.txt create mode 100644 sources/android-gamesdk/GameController/GameController.cpp create mode 100644 sources/android-gamesdk/GameController/GameController.h create mode 100644 sources/android-gamesdk/GameController/GameControllerDeviceInfo.cpp create mode 100644 sources/android-gamesdk/GameController/GameControllerDeviceInfo.h create mode 100644 sources/android-gamesdk/GameController/GameControllerGameActivityMirror.h create mode 100644 sources/android-gamesdk/GameController/GameControllerInternalConstants.h create mode 100644 sources/android-gamesdk/GameController/GameControllerLog.cpp create mode 100644 sources/android-gamesdk/GameController/GameControllerLog.h create mode 100644 sources/android-gamesdk/GameController/GameControllerLogStrings.h create mode 100644 sources/android-gamesdk/GameController/GameControllerManager.cpp create mode 100644 sources/android-gamesdk/GameController/GameControllerManager.h create mode 100644 sources/android-gamesdk/GameController/GameControllerMappingUtils.cpp create mode 100644 sources/android-gamesdk/GameController/GameControllerMappingUtils.h create mode 100644 sources/android-gamesdk/GameController/InternalControllerTable.cpp create mode 100644 sources/android-gamesdk/GameController/InternalControllerTable.h create mode 100644 sources/android-gamesdk/GameController/Log.h create mode 100644 sources/android-gamesdk/GameController/ThreadUtil.h create mode 100644 sources/android-gamesdk/GameController/paddleboat/include/paddleboat.h create mode 100644 sources/android-gamesdk/GameController/paddleboat_c.cpp diff --git a/sources/CMakeLists.txt b/sources/CMakeLists.txt index 3ce8042f..b0faf6de 100644 --- a/sources/CMakeLists.txt +++ b/sources/CMakeLists.txt @@ -47,6 +47,21 @@ endif() # add dependent boost libs include(${CMAKE_CURRENT_LIST_DIR}/boost-source/boost.cmake) + +if(ANDROID) + set(CC_GAME_ACTIVITY_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/android-gamesdk/GameActivity/game-activity/include/game-activity/GameActivity.cpp + ${CMAKE_CURRENT_LIST_DIR}/android-gamesdk/GameActivity/game-activity/include/game-activity/native_app_glue/android_native_app_glue.c + ${CMAKE_CURRENT_LIST_DIR}/android-gamesdk/GameActivity/game-activity/include/game-text-input/gametextinput.cpp + ) + + include(${CMAKE_CURRENT_LIST_DIR}/android-gamesdk/GameController/CMakeLists.txt) + list(APPEND CC_EXTERNAL_SOURCES + ${CC_GAME_ACTIVITY_SOURCES} + ) + include_directories(${CMAKE_CURRENT_LIST_DIR}/android-gamesdk/GameActivity/game-activity/include) +endif() + if(ANDROID OR OHOS) include(${CMAKE_CURRENT_LIST_DIR}/pvmp3dec/CMakeLists.txt) diff --git a/sources/android-gamesdk/GameActivity/game-activity/include/game-activity/GameActivity.cpp b/sources/android-gamesdk/GameActivity/game-activity/include/game-activity/GameActivity.cpp new file mode 100644 index 00000000..d20cd6be --- /dev/null +++ b/sources/android-gamesdk/GameActivity/game-activity/include/game-activity/GameActivity.cpp @@ -0,0 +1,1304 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#define LOG_TAG "GameActivity" + +#include "GameActivity.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +// TODO(b/187147166): these functions were extracted from the Game SDK +// (gamesdk/src/common/system_utils.h). system_utils.h/cpp should be used +// instead. +namespace { + +#if __ANDROID_API__ >= 26 +std::string getSystemPropViaCallback(const char *key, + const char *default_value = "") { + const prop_info *prop = __system_property_find(key); + if (prop == nullptr) { + return default_value; + } + std::string return_value; + auto thunk = [](void *cookie, const char * /*name*/, const char *value, + uint32_t /*serial*/) { + if (value != nullptr) { + std::string *r = static_cast(cookie); + *r = value; + } + }; + __system_property_read_callback(prop, thunk, &return_value); + return return_value; +} +#else +std::string getSystemPropViaGet(const char *key, + const char *default_value = "") { + char buffer[PROP_VALUE_MAX + 1] = ""; // +1 for terminator + int bufferLen = __system_property_get(key, buffer); + if (bufferLen > 0) + return buffer; + else + return ""; +} +#endif + +std::string GetSystemProp(const char *key, const char *default_value = "") { +#if __ANDROID_API__ >= 26 + return getSystemPropViaCallback(key, default_value); +#else + return getSystemPropViaGet(key, default_value); +#endif +} + +int GetSystemPropAsInt(const char *key, int default_value = 0) { + std::string prop = GetSystemProp(key); + return prop == "" ? default_value : strtoll(prop.c_str(), nullptr, 10); +} + +struct OwnedGameTextInputState { + OwnedGameTextInputState &operator=(const GameTextInputState &rhs) { + inner = rhs; + owned_string = std::string(rhs.text_UTF8, rhs.text_length); + inner.text_UTF8 = owned_string.data(); + return *this; + } + GameTextInputState inner; + std::string owned_string; +}; + +} // anonymous namespace + +#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__); +#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__); +#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__); +#ifdef NDEBUG +#define ALOGV(...) +#else +#define ALOGV(...) \ + __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__); +#endif + +/* Returns 2nd arg. Used to substitute default value if caller's vararg list + * is empty. + */ +#define __android_second(first, second, ...) second + +/* If passed multiple args, returns ',' followed by all but 1st arg, otherwise + * returns nothing. + */ +#define __android_rest(first, ...) , ##__VA_ARGS__ + +#define android_printAssert(cond, tag, fmt...) \ + __android_log_assert(cond, tag, \ + __android_second(0, ##fmt, NULL) __android_rest(fmt)) + +#define CONDITION(cond) (__builtin_expect((cond) != 0, 0)) + +#ifndef LOG_ALWAYS_FATAL_IF +#define LOG_ALWAYS_FATAL_IF(cond, ...) \ + ((CONDITION(cond)) \ + ? ((void)android_printAssert(#cond, LOG_TAG, ##__VA_ARGS__)) \ + : (void)0) +#endif + +#ifndef LOG_ALWAYS_FATAL +#define LOG_ALWAYS_FATAL(...) \ + (((void)android_printAssert(NULL, LOG_TAG, ##__VA_ARGS__))) +#endif + +/* + * Simplified macro to send a warning system log message using current LOG_TAG. + */ +#ifndef SLOGW +#define SLOGW(...) \ + ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef SLOGW_IF +#define SLOGW_IF(cond, ...) \ + ((__predict_false(cond)) \ + ? ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)) \ + : (void)0) +#endif + +/* + * Versions of LOG_ALWAYS_FATAL_IF and LOG_ALWAYS_FATAL that + * are stripped out of release builds. + */ +#if LOG_NDEBUG + +#ifndef LOG_FATAL_IF +#define LOG_FATAL_IF(cond, ...) ((void)0) +#endif +#ifndef LOG_FATAL +#define LOG_FATAL(...) ((void)0) +#endif + +#else + +#ifndef LOG_FATAL_IF +#define LOG_FATAL_IF(cond, ...) LOG_ALWAYS_FATAL_IF(cond, ##__VA_ARGS__) +#endif +#ifndef LOG_FATAL +#define LOG_FATAL(...) LOG_ALWAYS_FATAL(__VA_ARGS__) +#endif + +#endif + +/* + * Assertion that generates a log message when the assertion fails. + * Stripped out of release builds. Uses the current LOG_TAG. + */ +#ifndef ALOG_ASSERT +#define ALOG_ASSERT(cond, ...) LOG_FATAL_IF(!(cond), ##__VA_ARGS__) +#endif + +#define LOG_TRACE(...) + +#ifndef NELEM +#define NELEM(x) ((int)(sizeof(x) / sizeof((x)[0]))) +#endif + +/* + * JNI methods of the GameActivity Java class. + */ +static struct { + jmethodID finish; + jmethodID setWindowFlags; + jmethodID getWindowInsets; + jmethodID getWaterfallInsets; + jmethodID setImeEditorInfoFields; +} gGameActivityClassInfo; + +/* + * JNI fields of the androidx.core.graphics.Insets Java class. + */ +static struct { + jfieldID left; + jfieldID right; + jfieldID top; + jfieldID bottom; +} gInsetsClassInfo; + +/* + * JNI methods of the WindowInsetsCompat.Type Java class. + */ +static struct { + jmethodID methods[GAMECOMMON_INSETS_TYPE_COUNT]; + jclass clazz; +} gWindowInsetsCompatTypeClassInfo; + +/* + * Contains a command to be executed by the GameActivity + * on the application main thread. + */ +struct ActivityWork { + int32_t cmd; + int64_t arg1; + int64_t arg2; +}; + +/* + * The type of commands that can be passed to the GameActivity and that + * are executed on the application main thread. + */ +enum { + CMD_FINISH = 1, + CMD_SET_WINDOW_FORMAT, + CMD_SET_WINDOW_FLAGS, + CMD_SHOW_SOFT_INPUT, + CMD_HIDE_SOFT_INPUT, + CMD_SET_SOFT_INPUT_STATE +}; + +/* + * Write a command to be executed by the GameActivity on the application main + * thread. + */ +static void write_work(int fd, int32_t cmd, int64_t arg1 = 0, + int64_t arg2 = 0) { + ActivityWork work; + work.cmd = cmd; + work.arg1 = arg1; + work.arg2 = arg2; + + LOG_TRACE("write_work: cmd=%d", cmd); +restart: + int res = write(fd, &work, sizeof(work)); + if (res < 0 && errno == EINTR) { + goto restart; + } + + if (res == sizeof(work)) return; + + if (res < 0) { + ALOGW("Failed writing to work fd: %s", strerror(errno)); + } else { + ALOGW("Truncated writing to work fd: %d", res); + } +} + +/* + * Read commands to be executed by the GameActivity on the application main + * thread. + */ +static bool read_work(int fd, ActivityWork *outWork) { + int res = read(fd, outWork, sizeof(ActivityWork)); + // no need to worry about EINTR, poll loop will just come back again. + if (res == sizeof(ActivityWork)) return true; + + if (res < 0) { + ALOGW("Failed reading work fd: %s", strerror(errno)); + } else { + ALOGW("Truncated reading work fd: %d", res); + } + return false; +} + +/* + * Native state for interacting with the GameActivity class. + */ +struct NativeCode : public GameActivity { + NativeCode(void *_dlhandle, GameActivity_createFunc *_createFunc) { + memset((GameActivity *)this, 0, sizeof(GameActivity)); + memset(&callbacks, 0, sizeof(callbacks)); + memset(&insetsState, 0, sizeof(insetsState)); + dlhandle = _dlhandle; + createActivityFunc = _createFunc; + nativeWindow = NULL; + mainWorkRead = mainWorkWrite = -1; + gameTextInput = NULL; + } + + ~NativeCode() { + if (callbacks.onDestroy != NULL) { + callbacks.onDestroy(this); + } + if (env != NULL) { + if (javaGameActivity != NULL) { + env->DeleteGlobalRef(javaGameActivity); + } + if (javaAssetManager != NULL) { + env->DeleteGlobalRef(javaAssetManager); + } + } + GameTextInput_destroy(gameTextInput); + if (looper != NULL && mainWorkRead >= 0) { + ALooper_removeFd(looper, mainWorkRead); + } + ALooper_release(looper); + looper = NULL; + + setSurface(NULL); + if (mainWorkRead >= 0) close(mainWorkRead); + if (mainWorkWrite >= 0) close(mainWorkWrite); + if (dlhandle != NULL) { + // for now don't unload... we probably should clean this + // up and only keep one open dlhandle per proc, since there + // is really no benefit to unloading the code. + // dlclose(dlhandle); + } + } + + void setSurface(jobject _surface) { + if (nativeWindow != NULL) { + ANativeWindow_release(nativeWindow); + } + if (_surface != NULL) { + nativeWindow = ANativeWindow_fromSurface(env, _surface); + } else { + nativeWindow = NULL; + } + } + + GameActivityCallbacks callbacks; + + void *dlhandle; + GameActivity_createFunc *createActivityFunc; + + std::string internalDataPathObj; + std::string externalDataPathObj; + std::string obbPathObj; + + ANativeWindow *nativeWindow; + int32_t lastWindowWidth; + int32_t lastWindowHeight; + + // These are used to wake up the main thread to process work. + int mainWorkRead; + int mainWorkWrite; + ALooper *looper; + + // Need to hold on to a reference here in case the upper layers destroy our + // AssetManager. + jobject javaAssetManager; + + GameTextInput *gameTextInput; + // Set by users in GameActivity_setTextInputState, then passed to + // GameTextInput. + OwnedGameTextInputState gameTextInputState; + std::mutex gameTextInputStateMutex; + + ARect insetsState[GAMECOMMON_INSETS_TYPE_COUNT]; +}; + +extern "C" void GameActivity_finish(GameActivity *activity) { + NativeCode *code = static_cast(activity); + write_work(code->mainWorkWrite, CMD_FINISH, 0); +} + +extern "C" void GameActivity_setWindowFlags(GameActivity *activity, + uint32_t values, uint32_t mask) { + NativeCode *code = static_cast(activity); + write_work(code->mainWorkWrite, CMD_SET_WINDOW_FLAGS, values, mask); +} + +extern "C" void GameActivity_showSoftInput(GameActivity *activity, + uint32_t flags) { + NativeCode *code = static_cast(activity); + write_work(code->mainWorkWrite, CMD_SHOW_SOFT_INPUT, flags); +} + +extern "C" void GameActivity_setTextInputState( + GameActivity *activity, const GameTextInputState *state) { + NativeCode *code = static_cast(activity); + std::lock_guard lock(code->gameTextInputStateMutex); + code->gameTextInputState = *state; + write_work(code->mainWorkWrite, CMD_SET_SOFT_INPUT_STATE); +} + +extern "C" void GameActivity_getTextInputState( + GameActivity *activity, GameTextInputGetStateCallback callback, + void *context) { + NativeCode *code = static_cast(activity); + return GameTextInput_getState(code->gameTextInput, callback, context); +} + +extern "C" void GameActivity_hideSoftInput(GameActivity *activity, + uint32_t flags) { + NativeCode *code = static_cast(activity); + write_work(code->mainWorkWrite, CMD_HIDE_SOFT_INPUT, flags); +} + +extern "C" void GameActivity_getWindowInsets(GameActivity *activity, + GameCommonInsetsType type, + ARect *insets) { + if (type < 0 || type >= GAMECOMMON_INSETS_TYPE_COUNT) return; + NativeCode *code = static_cast(activity); + *insets = code->insetsState[type]; +} + +extern "C" GameTextInput *GameActivity_getTextInput( + const GameActivity *activity) { + const NativeCode *code = static_cast(activity); + return code->gameTextInput; +} + +/* + * Log the JNI exception, if any. + */ +static void checkAndClearException(JNIEnv *env, const char *methodName) { + if (env->ExceptionCheck()) { + ALOGE("Exception while running %s", methodName); + env->ExceptionDescribe(); + env->ExceptionClear(); + } +} + +/* + * Callback for handling native events on the application's main thread. + */ +static int mainWorkCallback(int fd, int events, void *data) { + ALOGD("************** mainWorkCallback *********"); + NativeCode *code = (NativeCode *)data; + if ((events & POLLIN) == 0) { + return 1; + } + + ActivityWork work; + if (!read_work(code->mainWorkRead, &work)) { + return 1; + } + LOG_TRACE("mainWorkCallback: cmd=%d", work.cmd); + switch (work.cmd) { + case CMD_FINISH: { + code->env->CallVoidMethod(code->javaGameActivity, + gGameActivityClassInfo.finish); + checkAndClearException(code->env, "finish"); + } break; + case CMD_SET_WINDOW_FLAGS: { + code->env->CallVoidMethod(code->javaGameActivity, + gGameActivityClassInfo.setWindowFlags, + work.arg1, work.arg2); + checkAndClearException(code->env, "setWindowFlags"); + } break; + case CMD_SHOW_SOFT_INPUT: { + GameTextInput_showIme(code->gameTextInput, work.arg1); + } break; + case CMD_SET_SOFT_INPUT_STATE: { + std::lock_guard lock(code->gameTextInputStateMutex); + GameTextInput_setState(code->gameTextInput, + &code->gameTextInputState.inner); + checkAndClearException(code->env, "setTextInputState"); + } break; + case CMD_HIDE_SOFT_INPUT: { + GameTextInput_hideIme(code->gameTextInput, work.arg1); + } break; + default: + ALOGW("Unknown work command: %d", work.cmd); + break; + } + + return 1; +} + +// ------------------------------------------------------------------------ + +static thread_local std::string g_error_msg; + +static jlong loadNativeCode_native(JNIEnv *env, jobject javaGameActivity, + jstring path, jstring funcName, + jstring internalDataDir, jstring obbDir, + jstring externalDataDir, jobject jAssetMgr, + jbyteArray savedState) { + LOG_TRACE("loadNativeCode_native"); + const char *pathStr = env->GetStringUTFChars(path, NULL); + NativeCode *code = NULL; + + void *handle = dlopen(pathStr, RTLD_LAZY); + + env->ReleaseStringUTFChars(path, pathStr); + + if (handle == nullptr) { + g_error_msg = dlerror(); + ALOGE("GameActivity dlopen(\"%s\") failed: %s", pathStr, + g_error_msg.c_str()); + return 0; + } + + const char *funcStr = env->GetStringUTFChars(funcName, NULL); + code = new NativeCode(handle, + (GameActivity_createFunc *)dlsym(handle, funcStr)); + env->ReleaseStringUTFChars(funcName, funcStr); + + if (code->createActivityFunc == nullptr) { + g_error_msg = dlerror(); + ALOGW("GameActivity_onCreate not found: %s", g_error_msg.c_str()); + delete code; + return 0; + } + + code->looper = ALooper_forThread(); + if (code->looper == nullptr) { + g_error_msg = "Unable to retrieve native ALooper"; + ALOGW("%s", g_error_msg.c_str()); + delete code; + return 0; + } + ALooper_acquire(code->looper); + + int msgpipe[2]; + if (pipe(msgpipe)) { + g_error_msg = "could not create pipe: "; + g_error_msg += strerror(errno); + + ALOGW("%s", g_error_msg.c_str()); + delete code; + return 0; + } + code->mainWorkRead = msgpipe[0]; + code->mainWorkWrite = msgpipe[1]; + int result = fcntl(code->mainWorkRead, F_SETFL, O_NONBLOCK); + SLOGW_IF(result != 0, + "Could not make main work read pipe " + "non-blocking: %s", + strerror(errno)); + result = fcntl(code->mainWorkWrite, F_SETFL, O_NONBLOCK); + SLOGW_IF(result != 0, + "Could not make main work write pipe " + "non-blocking: %s", + strerror(errno)); + ALooper_addFd(code->looper, code->mainWorkRead, 0, ALOOPER_EVENT_INPUT, + mainWorkCallback, code); + + code->GameActivity::callbacks = &code->callbacks; + if (env->GetJavaVM(&code->vm) < 0) { + ALOGW("GameActivity GetJavaVM failed"); + delete code; + return 0; + } + code->env = env; + code->javaGameActivity = env->NewGlobalRef(javaGameActivity); + + const char *dirStr = + internalDataDir ? env->GetStringUTFChars(internalDataDir, NULL) : ""; + code->internalDataPathObj = dirStr; + code->internalDataPath = code->internalDataPathObj.c_str(); + if (internalDataDir) env->ReleaseStringUTFChars(internalDataDir, dirStr); + + dirStr = + externalDataDir ? env->GetStringUTFChars(externalDataDir, NULL) : ""; + code->externalDataPathObj = dirStr; + code->externalDataPath = code->externalDataPathObj.c_str(); + if (externalDataDir) env->ReleaseStringUTFChars(externalDataDir, dirStr); + + code->javaAssetManager = env->NewGlobalRef(jAssetMgr); + code->assetManager = AAssetManager_fromJava(env, jAssetMgr); + + dirStr = obbDir ? env->GetStringUTFChars(obbDir, NULL) : ""; + code->obbPathObj = dirStr; + code->obbPath = code->obbPathObj.c_str(); + if (obbDir) env->ReleaseStringUTFChars(obbDir, dirStr); + + jbyte *rawSavedState = NULL; + jsize rawSavedSize = 0; + if (savedState != NULL) { + rawSavedState = env->GetByteArrayElements(savedState, NULL); + rawSavedSize = env->GetArrayLength(savedState); + } + code->createActivityFunc(code, rawSavedState, rawSavedSize); + + code->gameTextInput = GameTextInput_init(env, 0); + GameTextInput_setEventCallback(code->gameTextInput, + reinterpret_cast( + code->callbacks.onTextInputEvent), + code); + + if (rawSavedState != NULL) { + env->ReleaseByteArrayElements(savedState, rawSavedState, 0); + } + + return reinterpret_cast(code); +} + +static jstring getDlError_native(JNIEnv *env, jobject javaGameActivity) { + jstring result = env->NewStringUTF(g_error_msg.c_str()); + g_error_msg.clear(); + return result; +} + +static void unloadNativeCode_native(JNIEnv *env, jobject javaGameActivity, + jlong handle) { + LOG_TRACE("unloadNativeCode_native"); + if (handle != 0) { + NativeCode *code = (NativeCode *)handle; + delete code; + } +} + +static void onStart_native(JNIEnv *env, jobject javaGameActivity, + jlong handle) { + ALOGV("onStart_native"); + if (handle != 0) { + NativeCode *code = (NativeCode *)handle; + if (code->callbacks.onStart != NULL) { + code->callbacks.onStart(code); + } + } +} + +static void onResume_native(JNIEnv *env, jobject javaGameActivity, + jlong handle) { + LOG_TRACE("onResume_native"); + if (handle != 0) { + NativeCode *code = (NativeCode *)handle; + if (code->callbacks.onResume != NULL) { + code->callbacks.onResume(code); + } + } +} + +struct SaveInstanceLocals { + JNIEnv *env; + jbyteArray array; +}; + +static jbyteArray onSaveInstanceState_native(JNIEnv *env, + jobject javaGameActivity, + jlong handle) { + LOG_TRACE("onSaveInstanceState_native"); + + SaveInstanceLocals locals{ + env, NULL}; // Passed through the user's state prep function. + + if (handle != 0) { + NativeCode *code = (NativeCode *)handle; + if (code->callbacks.onSaveInstanceState != NULL) { + code->callbacks.onSaveInstanceState( + code, + [](const char *bytes, int len, void *context) { + auto locals = static_cast(context); + if (len > 0) { + locals->array = locals->env->NewByteArray(len); + if (locals->array != NULL) { + locals->env->SetByteArrayRegion( + locals->array, 0, len, (const jbyte *)bytes); + } + } + }, + &locals); + } + } + return locals.array; +} + +static void onPause_native(JNIEnv *env, jobject javaGameActivity, + jlong handle) { + LOG_TRACE("onPause_native"); + if (handle != 0) { + NativeCode *code = (NativeCode *)handle; + if (code->callbacks.onPause != NULL) { + code->callbacks.onPause(code); + } + } +} + +static void onStop_native(JNIEnv *env, jobject javaGameActivity, jlong handle) { + LOG_TRACE("onStop_native"); + if (handle != 0) { + NativeCode *code = (NativeCode *)handle; + if (code->callbacks.onStop != NULL) { + code->callbacks.onStop(code); + } + } +} + +static void onConfigurationChanged_native(JNIEnv *env, jobject javaGameActivity, + jlong handle) { + LOG_TRACE("onConfigurationChanged_native"); + if (handle != 0) { + NativeCode *code = (NativeCode *)handle; + if (code->callbacks.onConfigurationChanged != NULL) { + code->callbacks.onConfigurationChanged(code); + } + } +} + +static void onTrimMemory_native(JNIEnv *env, jobject javaGameActivity, + jlong handle, jint level) { + LOG_TRACE("onTrimMemory_native"); + if (handle != 0) { + NativeCode *code = (NativeCode *)handle; + if (code->callbacks.onTrimMemory != NULL) { + code->callbacks.onTrimMemory(code, level); + } + } +} + +static void onWindowFocusChanged_native(JNIEnv *env, jobject javaGameActivity, + jlong handle, jboolean focused) { + LOG_TRACE("onWindowFocusChanged_native"); + if (handle != 0) { + NativeCode *code = (NativeCode *)handle; + if (code->callbacks.onWindowFocusChanged != NULL) { + code->callbacks.onWindowFocusChanged(code, focused ? 1 : 0); + } + } +} + +static void onSurfaceCreated_native(JNIEnv *env, jobject javaGameActivity, + jlong handle, jobject surface) { + ALOGV("onSurfaceCreated_native"); + LOG_TRACE("onSurfaceCreated_native"); + if (handle != 0) { + NativeCode *code = (NativeCode *)handle; + code->setSurface(surface); + + if (code->nativeWindow != NULL && + code->callbacks.onNativeWindowCreated != NULL) { + code->callbacks.onNativeWindowCreated(code, code->nativeWindow); + } + } +} + +static void onSurfaceChanged_native(JNIEnv *env, jobject javaGameActivity, + jlong handle, jobject surface, jint format, + jint width, jint height) { + LOG_TRACE("onSurfaceChanged_native"); + if (handle != 0) { + NativeCode *code = (NativeCode *)handle; + ANativeWindow *oldNativeWindow = code->nativeWindow; + // Fix for window being destroyed behind the scenes on older Android + // versions. + if (oldNativeWindow != NULL) { + ANativeWindow_acquire(oldNativeWindow); + } + code->setSurface(surface); + if (oldNativeWindow != code->nativeWindow) { + if (oldNativeWindow != NULL && + code->callbacks.onNativeWindowDestroyed != NULL) { + code->callbacks.onNativeWindowDestroyed(code, oldNativeWindow); + } + if (code->nativeWindow != NULL) { + if (code->callbacks.onNativeWindowCreated != NULL) { + code->callbacks.onNativeWindowCreated(code, + code->nativeWindow); + } + + code->lastWindowWidth = + ANativeWindow_getWidth(code->nativeWindow); + code->lastWindowHeight = + ANativeWindow_getHeight(code->nativeWindow); + } + } else { + // Maybe it was resized? + int32_t newWidth = ANativeWindow_getWidth(code->nativeWindow); + int32_t newHeight = ANativeWindow_getHeight(code->nativeWindow); + if (newWidth != code->lastWindowWidth || + newHeight != code->lastWindowHeight) { + if (code->callbacks.onNativeWindowResized != NULL) { + code->callbacks.onNativeWindowResized( + code, code->nativeWindow, newWidth, newHeight); + } + } + } + // Release the window we acquired earlier. + if (oldNativeWindow != NULL) { + ANativeWindow_release(oldNativeWindow); + } + } +} + +static void onSurfaceRedrawNeeded_native(JNIEnv *env, jobject javaGameActivity, + jlong handle) { + LOG_TRACE("onSurfaceRedrawNeeded_native"); + if (handle != 0) { + NativeCode *code = (NativeCode *)handle; + if (code->nativeWindow != NULL && + code->callbacks.onNativeWindowRedrawNeeded != NULL) { + code->callbacks.onNativeWindowRedrawNeeded(code, + code->nativeWindow); + } + } +} + +static void onSurfaceDestroyed_native(JNIEnv *env, jobject javaGameActivity, + jlong handle) { + LOG_TRACE("onSurfaceDestroyed_native"); + if (handle != 0) { + NativeCode *code = (NativeCode *)handle; + if (code->nativeWindow != NULL && + code->callbacks.onNativeWindowDestroyed != NULL) { + code->callbacks.onNativeWindowDestroyed(code, code->nativeWindow); + } + code->setSurface(NULL); + } +} + +static bool enabledAxes[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT] = { + /* AMOTION_EVENT_AXIS_X */ true, + /* AMOTION_EVENT_AXIS_Y */ true, + // Disable all other axes by default (they can be enabled using + // `GameActivityPointerAxes_enableAxis`). + false}; + +extern "C" void GameActivityPointerAxes_enableAxis(int32_t axis) { + if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) { + return; + } + + enabledAxes[axis] = true; +} + +extern "C" void GameActivityPointerAxes_disableAxis(int32_t axis) { + if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) { + return; + } + + enabledAxes[axis] = false; +} + +extern "C" void GameActivity_setImeEditorInfo(GameActivity *activity, + int inputType, int actionId, + int imeOptions) { + JNIEnv *env; + if (activity->vm->AttachCurrentThread(&env, NULL) == JNI_OK) { + env->CallVoidMethod(activity->javaGameActivity, + gGameActivityClassInfo.setImeEditorInfoFields, + inputType, actionId, imeOptions); + } +} + +static struct { + jmethodID getDeviceId; + jmethodID getSource; + jmethodID getAction; + + jmethodID getEventTime; + jmethodID getDownTime; + + jmethodID getFlags; + jmethodID getMetaState; + + jmethodID getActionButton; + jmethodID getButtonState; + jmethodID getClassification; + jmethodID getEdgeFlags; + + jmethodID getPointerCount; + jmethodID getPointerId; + jmethodID getRawX; + jmethodID getRawY; + jmethodID getXPrecision; + jmethodID getYPrecision; + jmethodID getAxisValue; +} gMotionEventClassInfo; + +extern "C" void GameActivityMotionEvent_fromJava( + JNIEnv *env, jobject motionEvent, GameActivityMotionEvent *out_event) { + static bool gMotionEventClassInfoInitialized = false; + if (!gMotionEventClassInfoInitialized) { + int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk"); + gMotionEventClassInfo = {0}; + jclass motionEventClass = env->FindClass("android/view/MotionEvent"); + gMotionEventClassInfo.getDeviceId = + env->GetMethodID(motionEventClass, "getDeviceId", "()I"); + gMotionEventClassInfo.getSource = + env->GetMethodID(motionEventClass, "getSource", "()I"); + gMotionEventClassInfo.getAction = + env->GetMethodID(motionEventClass, "getAction", "()I"); + gMotionEventClassInfo.getEventTime = + env->GetMethodID(motionEventClass, "getEventTime", "()J"); + gMotionEventClassInfo.getDownTime = + env->GetMethodID(motionEventClass, "getDownTime", "()J"); + gMotionEventClassInfo.getFlags = + env->GetMethodID(motionEventClass, "getFlags", "()I"); + gMotionEventClassInfo.getMetaState = + env->GetMethodID(motionEventClass, "getMetaState", "()I"); + if (sdkVersion >= 23) { + gMotionEventClassInfo.getActionButton = + env->GetMethodID(motionEventClass, "getActionButton", "()I"); + } + if (sdkVersion >= 14) { + gMotionEventClassInfo.getButtonState = + env->GetMethodID(motionEventClass, "getButtonState", "()I"); + } + if (sdkVersion >= 29) { + gMotionEventClassInfo.getClassification = + env->GetMethodID(motionEventClass, "getClassification", "()I"); + } + gMotionEventClassInfo.getEdgeFlags = + env->GetMethodID(motionEventClass, "getEdgeFlags", "()I"); + gMotionEventClassInfo.getPointerCount = + env->GetMethodID(motionEventClass, "getPointerCount", "()I"); + gMotionEventClassInfo.getPointerId = + env->GetMethodID(motionEventClass, "getPointerId", "(I)I"); + if (sdkVersion >= 29) { + gMotionEventClassInfo.getRawX = + env->GetMethodID(motionEventClass, "getRawX", "(I)F"); + gMotionEventClassInfo.getRawY = + env->GetMethodID(motionEventClass, "getRawY", "(I)F"); + } + gMotionEventClassInfo.getXPrecision = + env->GetMethodID(motionEventClass, "getXPrecision", "()F"); + gMotionEventClassInfo.getYPrecision = + env->GetMethodID(motionEventClass, "getYPrecision", "()F"); + gMotionEventClassInfo.getAxisValue = + env->GetMethodID(motionEventClass, "getAxisValue", "(II)F"); + + gMotionEventClassInfoInitialized = true; + } + + int pointerCount = + env->CallIntMethod(motionEvent, gMotionEventClassInfo.getPointerCount); + pointerCount = + std::min(pointerCount, GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT); + out_event->pointerCount = pointerCount; + for (int i = 0; i < pointerCount; ++i) { + out_event->pointers[i] = { + /*id=*/env->CallIntMethod(motionEvent, + gMotionEventClassInfo.getPointerId, i), + /*axisValues=*/{0}, + /*rawX=*/gMotionEventClassInfo.getRawX + ? env->CallFloatMethod(motionEvent, + gMotionEventClassInfo.getRawX, i) + : 0, + /*rawY=*/gMotionEventClassInfo.getRawY + ? env->CallFloatMethod(motionEvent, + gMotionEventClassInfo.getRawY, i) + : 0, + }; + + for (int axisIndex = 0; + axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT; ++axisIndex) { + if (enabledAxes[axisIndex]) { + out_event->pointers[i].axisValues[axisIndex] = + env->CallFloatMethod(motionEvent, + gMotionEventClassInfo.getAxisValue, + axisIndex, i); + } + } + } + + out_event->deviceId = + env->CallIntMethod(motionEvent, gMotionEventClassInfo.getDeviceId); + out_event->source = + env->CallIntMethod(motionEvent, gMotionEventClassInfo.getSource); + out_event->action = + env->CallIntMethod(motionEvent, gMotionEventClassInfo.getAction); + out_event->eventTime = + env->CallLongMethod(motionEvent, gMotionEventClassInfo.getEventTime) * + 1000000; + out_event->downTime = + env->CallLongMethod(motionEvent, gMotionEventClassInfo.getDownTime) * + 1000000; + out_event->flags = + env->CallIntMethod(motionEvent, gMotionEventClassInfo.getFlags); + out_event->metaState = + env->CallIntMethod(motionEvent, gMotionEventClassInfo.getMetaState); + out_event->actionButton = + gMotionEventClassInfo.getActionButton + ? env->CallIntMethod(motionEvent, + gMotionEventClassInfo.getActionButton) + : 0; + out_event->buttonState = + gMotionEventClassInfo.getButtonState + ? env->CallIntMethod(motionEvent, + gMotionEventClassInfo.getButtonState) + : 0; + out_event->classification = + gMotionEventClassInfo.getClassification + ? env->CallIntMethod(motionEvent, + gMotionEventClassInfo.getClassification) + : 0; + out_event->edgeFlags = + env->CallIntMethod(motionEvent, gMotionEventClassInfo.getEdgeFlags); + out_event->precisionX = + env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getXPrecision); + out_event->precisionY = + env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getYPrecision); +} + +static struct { + jmethodID getDeviceId; + jmethodID getSource; + jmethodID getAction; + + jmethodID getEventTime; + jmethodID getDownTime; + + jmethodID getFlags; + jmethodID getMetaState; + + jmethodID getModifiers; + jmethodID getRepeatCount; + jmethodID getKeyCode; +} gKeyEventClassInfo; + +extern "C" void GameActivityKeyEvent_fromJava(JNIEnv *env, jobject keyEvent, + GameActivityKeyEvent *out_event) { + static bool gKeyEventClassInfoInitialized = false; + if (!gKeyEventClassInfoInitialized) { + int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk"); + gKeyEventClassInfo = {0}; + jclass keyEventClass = env->FindClass("android/view/KeyEvent"); + gKeyEventClassInfo.getDeviceId = + env->GetMethodID(keyEventClass, "getDeviceId", "()I"); + gKeyEventClassInfo.getSource = + env->GetMethodID(keyEventClass, "getSource", "()I"); + gKeyEventClassInfo.getAction = + env->GetMethodID(keyEventClass, "getAction", "()I"); + gKeyEventClassInfo.getEventTime = + env->GetMethodID(keyEventClass, "getEventTime", "()J"); + gKeyEventClassInfo.getDownTime = + env->GetMethodID(keyEventClass, "getDownTime", "()J"); + gKeyEventClassInfo.getFlags = + env->GetMethodID(keyEventClass, "getFlags", "()I"); + gKeyEventClassInfo.getMetaState = + env->GetMethodID(keyEventClass, "getMetaState", "()I"); + if (sdkVersion >= 13) { + gKeyEventClassInfo.getModifiers = + env->GetMethodID(keyEventClass, "getModifiers", "()I"); + } + gKeyEventClassInfo.getRepeatCount = + env->GetMethodID(keyEventClass, "getRepeatCount", "()I"); + gKeyEventClassInfo.getKeyCode = + env->GetMethodID(keyEventClass, "getKeyCode", "()I"); + + gKeyEventClassInfoInitialized = true; + } + + *out_event = { + /*deviceId=*/env->CallIntMethod(keyEvent, + gKeyEventClassInfo.getDeviceId), + /*source=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getSource), + /*action=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getAction), + // TODO: introduce a millisecondsToNanoseconds helper: + /*eventTime=*/ + env->CallLongMethod(keyEvent, gKeyEventClassInfo.getEventTime) * + 1000000, + /*downTime=*/ + env->CallLongMethod(keyEvent, gKeyEventClassInfo.getDownTime) * 1000000, + /*flags=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getFlags), + /*metaState=*/ + env->CallIntMethod(keyEvent, gKeyEventClassInfo.getMetaState), + /*modifiers=*/gKeyEventClassInfo.getModifiers + ? env->CallIntMethod(keyEvent, gKeyEventClassInfo.getModifiers) + : 0, + /*repeatCount=*/ + env->CallIntMethod(keyEvent, gKeyEventClassInfo.getRepeatCount), + /*keyCode=*/ + env->CallIntMethod(keyEvent, gKeyEventClassInfo.getKeyCode)}; +} + +static bool onTouchEvent_native(JNIEnv *env, jobject javaGameActivity, + jlong handle, jobject motionEvent) { + if (handle == 0) return false; + NativeCode *code = (NativeCode *)handle; + if (code->callbacks.onTouchEvent == nullptr) return false; + + static GameActivityMotionEvent c_event; + GameActivityMotionEvent_fromJava(env, motionEvent, &c_event); + return code->callbacks.onTouchEvent(code, &c_event); +} + +static bool onKeyUp_native(JNIEnv *env, jobject javaGameActivity, jlong handle, + jobject keyEvent) { + if (handle == 0) return false; + NativeCode *code = (NativeCode *)handle; + if (code->callbacks.onKeyUp == nullptr) return false; + + static GameActivityKeyEvent c_event; + GameActivityKeyEvent_fromJava(env, keyEvent, &c_event); + return code->callbacks.onKeyUp(code, &c_event); +} + +static bool onKeyDown_native(JNIEnv *env, jobject javaGameActivity, + jlong handle, jobject keyEvent) { + if (handle == 0) return false; + NativeCode *code = (NativeCode *)handle; + if (code->callbacks.onKeyDown == nullptr) return false; + + static GameActivityKeyEvent c_event; + GameActivityKeyEvent_fromJava(env, keyEvent, &c_event); + return code->callbacks.onKeyDown(code, &c_event); +} + +static void onTextInput_native(JNIEnv *env, jobject activity, jlong handle, + jobject textInputEvent) { + if (handle == 0) return; + NativeCode *code = (NativeCode *)handle; + GameTextInput_processEvent(code->gameTextInput, textInputEvent); +} + +static void onWindowInsetsChanged_native(JNIEnv *env, jobject activity, + jlong handle) { + if (handle == 0) return; + NativeCode *code = (NativeCode *)handle; + if (code->callbacks.onWindowInsetsChanged == nullptr) return; + for (int type = 0; type < GAMECOMMON_INSETS_TYPE_COUNT; ++type) { + jobject jinsets; + // Note that waterfall insets are handled differently on the Java side. + if (type == GAMECOMMON_INSETS_TYPE_WATERFALL) { + jinsets = env->CallObjectMethod( + code->javaGameActivity, + gGameActivityClassInfo.getWaterfallInsets); + } else { + jint jtype = env->CallStaticIntMethod( + gWindowInsetsCompatTypeClassInfo.clazz, + gWindowInsetsCompatTypeClassInfo.methods[type]); + jinsets = env->CallObjectMethod( + code->javaGameActivity, gGameActivityClassInfo.getWindowInsets, + jtype); + } + ARect &insets = code->insetsState[type]; + if (jinsets == nullptr) { + insets.left = 0; + insets.right = 0; + insets.top = 0; + insets.bottom = 0; + } else { + insets.left = env->GetIntField(jinsets, gInsetsClassInfo.left); + insets.right = env->GetIntField(jinsets, gInsetsClassInfo.right); + insets.top = env->GetIntField(jinsets, gInsetsClassInfo.top); + insets.bottom = env->GetIntField(jinsets, gInsetsClassInfo.bottom); + } + } + GameTextInput_processImeInsets( + code->gameTextInput, &code->insetsState[GAMECOMMON_INSETS_TYPE_IME]); + code->callbacks.onWindowInsetsChanged(code); +} + +static void setInputConnection_native(JNIEnv *env, jobject activity, + jlong handle, jobject inputConnection) { + NativeCode *code = (NativeCode *)handle; + GameTextInput_setInputConnection(code->gameTextInput, inputConnection); +} + +static const JNINativeMethod g_methods[] = { + {"loadNativeCode", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/" + "String;Ljava/lang/String;Landroid/content/res/AssetManager;[B)J", + (void *)loadNativeCode_native}, + {"getDlError", "()Ljava/lang/String;", (void *)getDlError_native}, + {"unloadNativeCode", "(J)V", (void *)unloadNativeCode_native}, + {"onStartNative", "(J)V", (void *)onStart_native}, + {"onResumeNative", "(J)V", (void *)onResume_native}, + {"onSaveInstanceStateNative", "(J)[B", (void *)onSaveInstanceState_native}, + {"onPauseNative", "(J)V", (void *)onPause_native}, + {"onStopNative", "(J)V", (void *)onStop_native}, + {"onConfigurationChangedNative", "(J)V", + (void *)onConfigurationChanged_native}, + {"onTrimMemoryNative", "(JI)V", (void *)onTrimMemory_native}, + {"onWindowFocusChangedNative", "(JZ)V", + (void *)onWindowFocusChanged_native}, + {"onSurfaceCreatedNative", "(JLandroid/view/Surface;)V", + (void *)onSurfaceCreated_native}, + {"onSurfaceChangedNative", "(JLandroid/view/Surface;III)V", + (void *)onSurfaceChanged_native}, + {"onSurfaceRedrawNeededNative", "(JLandroid/view/Surface;)V", + (void *)onSurfaceRedrawNeeded_native}, + {"onSurfaceDestroyedNative", "(J)V", (void *)onSurfaceDestroyed_native}, + {"onTouchEventNative", "(JLandroid/view/MotionEvent;)Z", + (void *)onTouchEvent_native}, + {"onKeyDownNative", "(JLandroid/view/KeyEvent;)Z", + (void *)onKeyDown_native}, + {"onKeyUpNative", "(JLandroid/view/KeyEvent;)Z", (void *)onKeyUp_native}, + {"onTextInputEventNative", + "(JLcom/google/androidgamesdk/gametextinput/State;)V", + (void *)onTextInput_native}, + {"onWindowInsetsChangedNative", "(J)V", + (void *)onWindowInsetsChanged_native}, + {"setInputConnectionNative", + "(JLcom/google/androidgamesdk/gametextinput/InputConnection;)V", + (void *)setInputConnection_native}, +}; + +static const char *const kGameActivityPathName = + "com/google/androidgamesdk/GameActivity"; + +static const char *const kInsetsPathName = "androidx/core/graphics/Insets"; + +static const char *const kWindowInsetsCompatTypePathName = + "androidx/core/view/WindowInsetsCompat$Type"; + +#define FIND_CLASS(var, className) \ + var = env->FindClass(className); \ + LOG_FATAL_IF(!var, "Unable to find class %s", className); + +#define GET_METHOD_ID(var, clazz, methodName, fieldDescriptor) \ + var = env->GetMethodID(clazz, methodName, fieldDescriptor); \ + LOG_FATAL_IF(!var, "Unable to find method %s", methodName); + +#define GET_STATIC_METHOD_ID(var, clazz, methodName, fieldDescriptor) \ + var = env->GetStaticMethodID(clazz, methodName, fieldDescriptor); \ + LOG_FATAL_IF(!var, "Unable to find static method %s", methodName); + +#define GET_FIELD_ID(var, clazz, fieldName, fieldDescriptor) \ + var = env->GetFieldID(clazz, fieldName, fieldDescriptor); \ + LOG_FATAL_IF(!var, "Unable to find field %s", fieldName); + +static int jniRegisterNativeMethods(JNIEnv *env, const char *className, + const JNINativeMethod *methods, + int numMethods) { + ALOGV("Registering %s's %d native methods...", className, numMethods); + jclass clazz = env->FindClass(className); + LOG_FATAL_IF(clazz == nullptr, + "Native registration unable to find class '%s'; aborting...", + className); + int result = env->RegisterNatives(clazz, methods, numMethods); + env->DeleteLocalRef(clazz); + if (result == 0) { + return 0; + } + + // Failure to register natives is fatal. Try to report the corresponding + // exception, otherwise abort with generic failure message. + jthrowable thrown = env->ExceptionOccurred(); + if (thrown != NULL) { + env->ExceptionDescribe(); + env->DeleteLocalRef(thrown); + } + LOG_FATAL("RegisterNatives failed for '%s'; aborting...", className); +} + +extern "C" int GameActivity_register(JNIEnv *env) { + ALOGD("GameActivity_register"); + jclass activity_class; + FIND_CLASS(activity_class, kGameActivityPathName); + GET_METHOD_ID(gGameActivityClassInfo.finish, activity_class, "finish", + "()V"); + GET_METHOD_ID(gGameActivityClassInfo.setWindowFlags, activity_class, + "setWindowFlags", "(II)V"); + GET_METHOD_ID(gGameActivityClassInfo.getWindowInsets, activity_class, + "getWindowInsets", "(I)Landroidx/core/graphics/Insets;"); + GET_METHOD_ID(gGameActivityClassInfo.getWaterfallInsets, activity_class, + "getWaterfallInsets", "()Landroidx/core/graphics/Insets;"); + GET_METHOD_ID(gGameActivityClassInfo.setImeEditorInfoFields, activity_class, + "setImeEditorInfoFields", "(III)V"); + jclass insets_class; + FIND_CLASS(insets_class, kInsetsPathName); + GET_FIELD_ID(gInsetsClassInfo.left, insets_class, "left", "I"); + GET_FIELD_ID(gInsetsClassInfo.right, insets_class, "right", "I"); + GET_FIELD_ID(gInsetsClassInfo.top, insets_class, "top", "I"); + GET_FIELD_ID(gInsetsClassInfo.bottom, insets_class, "bottom", "I"); + jclass windowInsetsCompatType_class; + FIND_CLASS(windowInsetsCompatType_class, kWindowInsetsCompatTypePathName); + gWindowInsetsCompatTypeClassInfo.clazz = + (jclass)env->NewGlobalRef(windowInsetsCompatType_class); + // These names must match, in order, the GameCommonInsetsType enum fields + // Note that waterfall is handled differently by the insets API, so we + // exclude it here. + const char *methodNames[GAMECOMMON_INSETS_TYPE_WATERFALL] = { + "captionBar", + "displayCutout", + "ime", + "mandatorySystemGestures", + "navigationBars", + "statusBars", + "systemBars", + "systemGestures", + "tappableElement"}; + for (int i = 0; i < GAMECOMMON_INSETS_TYPE_WATERFALL; ++i) { + GET_STATIC_METHOD_ID(gWindowInsetsCompatTypeClassInfo.methods[i], + windowInsetsCompatType_class, methodNames[i], + "()I"); + } + return jniRegisterNativeMethods(env, kGameActivityPathName, g_methods, + NELEM(g_methods)); +} + +// Register this method so that GameActiviy_register does not need to be called +// manually. +extern "C" jlong Java_com_google_androidgamesdk_GameActivity_loadNativeCode( + JNIEnv *env, jobject javaGameActivity, jstring path, jstring funcName, + jstring internalDataDir, jstring obbDir, jstring externalDataDir, + jobject jAssetMgr, jbyteArray savedState) { + GameActivity_register(env); + jlong nativeCode = loadNativeCode_native( + env, javaGameActivity, path, funcName, internalDataDir, obbDir, + externalDataDir, jAssetMgr, savedState); + return nativeCode; +} diff --git a/sources/android-gamesdk/GameActivity/game-activity/include/game-activity/GameActivity.h b/sources/android-gamesdk/GameActivity/game-activity/include/game-activity/GameActivity.h new file mode 100644 index 00000000..da102bcb --- /dev/null +++ b/sources/android-gamesdk/GameActivity/game-activity/include/game-activity/GameActivity.h @@ -0,0 +1,769 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @addtogroup GameActivity Game Activity + * The interface to use GameActivity. + * @{ + */ + +/** + * @file GameActivity.h + */ + +#ifndef ANDROID_GAME_SDK_GAME_ACTIVITY_H +#define ANDROID_GAME_SDK_GAME_ACTIVITY_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "game-text-input/gametextinput.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * {@link GameActivityCallbacks} + */ +struct GameActivityCallbacks; + +/** + * This structure defines the native side of an android.app.GameActivity. + * It is created by the framework, and handed to the application's native + * code as it is being launched. + */ +typedef struct GameActivity { + /** + * Pointer to the callback function table of the native application. + * You can set the functions here to your own callbacks. The callbacks + * pointer itself here should not be changed; it is allocated and managed + * for you by the framework. + */ + struct GameActivityCallbacks* callbacks; + + /** + * The global handle on the process's Java VM. + */ + JavaVM* vm; + + /** + * JNI context for the main thread of the app. Note that this field + * can ONLY be used from the main thread of the process; that is, the + * thread that calls into the GameActivityCallbacks. + */ + JNIEnv* env; + + /** + * The GameActivity object handle. + */ + jobject javaGameActivity; + + /** + * Path to this application's internal data directory. + */ + const char* internalDataPath; + + /** + * Path to this application's external (removable/mountable) data directory. + */ + const char* externalDataPath; + + /** + * The platform's SDK version code. + */ + int32_t sdkVersion; + + /** + * This is the native instance of the application. It is not used by + * the framework, but can be set by the application to its own instance + * state. + */ + void* instance; + + /** + * Pointer to the Asset Manager instance for the application. The + * application uses this to access binary assets bundled inside its own .apk + * file. + */ + AAssetManager* assetManager; + + /** + * Available starting with Honeycomb: path to the directory containing + * the application's OBB files (if any). If the app doesn't have any + * OBB files, this directory may not exist. + */ + const char* obbPath; +} GameActivity; + +/** + * The maximum number of axes supported in an Android MotionEvent. + * See https://developer.android.com/ndk/reference/group/input. + */ +#define GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT 48 + +/** + * \brief Describe information about a pointer, found in a + * GameActivityMotionEvent. + * + * You can read values directly from this structure, or use helper functions + * (`GameActivityPointerAxes_getX`, `GameActivityPointerAxes_getY` and + * `GameActivityPointerAxes_getAxisValue`). + * + * The X axis and Y axis are enabled by default but any other axis that you want + * to read **must** be enabled first, using + * `GameActivityPointerAxes_enableAxis`. + * + * \see GameActivityMotionEvent + */ +typedef struct GameActivityPointerAxes { + int32_t id; + float axisValues[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT]; + float rawX; + float rawY; +} GameActivityPointerAxes; + +/** \brief Get the current X coordinate of the pointer. */ +inline float GameActivityPointerAxes_getX( + const GameActivityPointerAxes* pointerInfo) { + return pointerInfo->axisValues[AMOTION_EVENT_AXIS_X]; +} + +/** \brief Get the current Y coordinate of the pointer. */ +inline float GameActivityPointerAxes_getY( + const GameActivityPointerAxes* pointerInfo) { + return pointerInfo->axisValues[AMOTION_EVENT_AXIS_Y]; +} + +/** + * \brief Enable the specified axis, so that its value is reported in the + * GameActivityPointerAxes structures stored in a motion event. + * + * You must enable any axis that you want to read, apart from + * `AMOTION_EVENT_AXIS_X` and `AMOTION_EVENT_AXIS_Y` that are enabled by + * default. + * + * If the axis index is out of range, nothing is done. + */ +void GameActivityPointerAxes_enableAxis(int32_t axis); + +/** + * \brief Disable the specified axis. Its value won't be reported in the + * GameActivityPointerAxes structures stored in a motion event anymore. + * + * Apart from X and Y, any axis that you want to read **must** be enabled first, + * using `GameActivityPointerAxes_enableAxis`. + * + * If the axis index is out of range, nothing is done. + */ +void GameActivityPointerAxes_disableAxis(int32_t axis); + +/** + * \brief Get the value of the requested axis. + * + * Apart from X and Y, any axis that you want to read **must** be enabled first, + * using `GameActivityPointerAxes_enableAxis`. + * + * Find the valid enums for the axis (`AMOTION_EVENT_AXIS_X`, + * `AMOTION_EVENT_AXIS_Y`, `AMOTION_EVENT_AXIS_PRESSURE`...) + * in https://developer.android.com/ndk/reference/group/input. + * + * @param pointerInfo The structure containing information about the pointer, + * obtained from GameActivityMotionEvent. + * @param axis The axis to get the value from + * @return The value of the axis, or 0 if the axis is invalid or was not + * enabled. + */ +inline float GameActivityPointerAxes_getAxisValue( + GameActivityPointerAxes* pointerInfo, int32_t axis) { + if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) { + return 0; + } + + return pointerInfo->axisValues[axis]; +} + +/** + * The maximum number of pointers returned inside a motion event. + */ +#if (defined GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE) +#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT \ + GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE +#else +#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT 8 +#endif + +/** + * \brief Describe a motion event that happened on the GameActivity SurfaceView. + * + * This is 1:1 mapping to the information contained in a Java `MotionEvent` + * (see https://developer.android.com/reference/android/view/MotionEvent). + */ +typedef struct GameActivityMotionEvent { + int32_t deviceId; + int32_t source; + int32_t action; + + int64_t eventTime; + int64_t downTime; + + int32_t flags; + int32_t metaState; + + int32_t actionButton; + int32_t buttonState; + int32_t classification; + int32_t edgeFlags; + + uint32_t pointerCount; + GameActivityPointerAxes + pointers[GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT]; + + float precisionX; + float precisionY; +} GameActivityMotionEvent; + +/** + * \brief Describe a key event that happened on the GameActivity SurfaceView. + * + * This is 1:1 mapping to the information contained in a Java `KeyEvent` + * (see https://developer.android.com/reference/android/view/KeyEvent). + */ +typedef struct GameActivityKeyEvent { + int32_t deviceId; + int32_t source; + int32_t action; + + int64_t eventTime; + int64_t downTime; + + int32_t flags; + int32_t metaState; + + int32_t modifiers; + int32_t repeatCount; + int32_t keyCode; +} GameActivityKeyEvent; + +/** + * A function the user should call from their callback with the data, its length + * and the library- supplied context. + */ +typedef void (*SaveInstanceStateRecallback)(const char* bytes, int len, + void* context); + +/** + * These are the callbacks the framework makes into a native application. + * All of these callbacks happen on the main thread of the application. + * By default, all callbacks are NULL; set to a pointer to your own function + * to have it called. + */ +typedef struct GameActivityCallbacks { + /** + * GameActivity has started. See Java documentation for Activity.onStart() + * for more information. + */ + void (*onStart)(GameActivity* activity); + + /** + * GameActivity has resumed. See Java documentation for Activity.onResume() + * for more information. + */ + void (*onResume)(GameActivity* activity); + + /** + * The framework is asking GameActivity to save its current instance state. + * See the Java documentation for Activity.onSaveInstanceState() for more + * information. The user should call the recallback with their data, its + * length and the provided context; they retain ownership of the data. Note + * that the saved state will be persisted, so it can not contain any active + * entities (pointers to memory, file descriptors, etc). + */ + void (*onSaveInstanceState)(GameActivity* activity, + SaveInstanceStateRecallback recallback, + void* context); + + /** + * GameActivity has paused. See Java documentation for Activity.onPause() + * for more information. + */ + void (*onPause)(GameActivity* activity); + + /** + * GameActivity has stopped. See Java documentation for Activity.onStop() + * for more information. + */ + void (*onStop)(GameActivity* activity); + + /** + * GameActivity is being destroyed. See Java documentation for + * Activity.onDestroy() for more information. + */ + void (*onDestroy)(GameActivity* activity); + + /** + * Focus has changed in this GameActivity's window. This is often used, + * for example, to pause a game when it loses input focus. + */ + void (*onWindowFocusChanged)(GameActivity* activity, bool hasFocus); + + /** + * The drawing window for this native activity has been created. You + * can use the given native window object to start drawing. + */ + void (*onNativeWindowCreated)(GameActivity* activity, + ANativeWindow* window); + + /** + * The drawing window for this native activity has been resized. You should + * retrieve the new size from the window and ensure that your rendering in + * it now matches. + */ + void (*onNativeWindowResized)(GameActivity* activity, ANativeWindow* window, + int32_t newWidth, int32_t newHeight); + + /** + * The drawing window for this native activity needs to be redrawn. To + * avoid transient artifacts during screen changes (such resizing after + * rotation), applications should not return from this function until they + * have finished drawing their window in its current state. + */ + void (*onNativeWindowRedrawNeeded)(GameActivity* activity, + ANativeWindow* window); + + /** + * The drawing window for this native activity is going to be destroyed. + * You MUST ensure that you do not touch the window object after returning + * from this function: in the common case of drawing to the window from + * another thread, that means the implementation of this callback must + * properly synchronize with the other thread to stop its drawing before + * returning from here. + */ + void (*onNativeWindowDestroyed)(GameActivity* activity, + ANativeWindow* window); + + /** + * The current device AConfiguration has changed. The new configuration can + * be retrieved from assetManager. + */ + void (*onConfigurationChanged)(GameActivity* activity); + + /** + * The system is running low on memory. Use this callback to release + * resources you do not need, to help the system avoid killing more + * important processes. + */ + void (*onTrimMemory)(GameActivity* activity, int level); + + /** + * Callback called for every MotionEvent done on the GameActivity + * SurfaceView. Ownership of `event` is maintained by the library and it is + * only valid during the callback. + */ + bool (*onTouchEvent)(GameActivity* activity, + const GameActivityMotionEvent* event); + + /** + * Callback called for every key down event on the GameActivity SurfaceView. + * Ownership of `event` is maintained by the library and it is only valid + * during the callback. + */ + bool (*onKeyDown)(GameActivity* activity, + const GameActivityKeyEvent* event); + + /** + * Callback called for every key up event on the GameActivity SurfaceView. + * Ownership of `event` is maintained by the library and it is only valid + * during the callback. + */ + bool (*onKeyUp)(GameActivity* activity, const GameActivityKeyEvent* event); + + /** + * Callback called for every soft-keyboard text input event. + * Ownership of `state` is maintained by the library and it is only valid + * during the callback. + */ + void (*onTextInputEvent)(GameActivity* activity, + const GameTextInputState* state); + + /** + * Callback called when WindowInsets of the main app window have changed. + * Call GameActivity_getWindowInsets to retrieve the insets themselves. + */ + void (*onWindowInsetsChanged)(GameActivity* activity); +} GameActivityCallbacks; + +/** + * \brief Convert a Java `MotionEvent` to a `GameActivityMotionEvent`. + * + * This is done automatically by the GameActivity: see `onTouchEvent` to set + * a callback to consume the received events. + * This function can be used if you re-implement events handling in your own + * activity. + * Ownership of out_event is maintained by the caller. + */ +void GameActivityMotionEvent_fromJava(JNIEnv* env, jobject motionEvent, + GameActivityMotionEvent* out_event); + +/** + * \brief Convert a Java `KeyEvent` to a `GameActivityKeyEvent`. + * + * This is done automatically by the GameActivity: see `onKeyUp` and `onKeyDown` + * to set a callback to consume the received events. + * This function can be used if you re-implement events handling in your own + * activity. + * Ownership of out_event is maintained by the caller. + */ +void GameActivityKeyEvent_fromJava(JNIEnv* env, jobject motionEvent, + GameActivityKeyEvent* out_event); + +/** + * This is the function that must be in the native code to instantiate the + * application's native activity. It is called with the activity instance (see + * above); if the code is being instantiated from a previously saved instance, + * the savedState will be non-NULL and point to the saved data. You must make + * any copy of this data you need -- it will be released after you return from + * this function. + */ +typedef void GameActivity_createFunc(GameActivity* activity, void* savedState, + size_t savedStateSize); + +/** + * The name of the function that NativeInstance looks for when launching its + * native code. This is the default function that is used, you can specify + * "android.app.func_name" string meta-data in your manifest to use a different + * function. + */ +extern GameActivity_createFunc GameActivity_onCreate; + +/** + * Finish the given activity. Its finish() method will be called, causing it + * to be stopped and destroyed. Note that this method can be called from + * *any* thread; it will send a message to the main thread of the process + * where the Java finish call will take place. + */ +void GameActivity_finish(GameActivity* activity); + +/** + * Flags for GameActivity_setWindowFlags, + * as per the Java API at android.view.WindowManager.LayoutParams. + */ +enum GameActivitySetWindowFlags { + /** + * As long as this window is visible to the user, allow the lock + * screen to activate while the screen is on. This can be used + * independently, or in combination with {@link + * GAMEACTIVITY_FLAG_KEEP_SCREEN_ON} and/or {@link + * GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED} + */ + GAMEACTIVITY_FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001, + /** Everything behind this window will be dimmed. */ + GAMEACTIVITY_FLAG_DIM_BEHIND = 0x00000002, + /** + * Blur everything behind this window. + * @deprecated Blurring is no longer supported. + */ + GAMEACTIVITY_FLAG_BLUR_BEHIND = 0x00000004, + /** + * This window won't ever get key input focus, so the + * user can not send key or other button events to it. Those will + * instead go to whatever focusable window is behind it. This flag + * will also enable {@link GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL} whether or not + * that is explicitly set. + * + * Setting this flag also implies that the window will not need to + * interact with + * a soft input method, so it will be Z-ordered and positioned + * independently of any active input method (typically this means it + * gets Z-ordered on top of the input method, so it can use the full + * screen for its content and cover the input method if needed. You + * can use {@link GAMEACTIVITY_FLAG_ALT_FOCUSABLE_IM} to modify this + * behavior. + */ + GAMEACTIVITY_FLAG_NOT_FOCUSABLE = 0x00000008, + /** This window can never receive touch events. */ + GAMEACTIVITY_FLAG_NOT_TOUCHABLE = 0x00000010, + /** + * Even when this window is focusable (its + * {@link GAMEACTIVITY_FLAG_NOT_FOCUSABLE} is not set), allow any pointer + * events outside of the window to be sent to the windows behind it. + * Otherwise it will consume all pointer events itself, regardless of + * whether they are inside of the window. + */ + GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL = 0x00000020, + /** + * When set, if the device is asleep when the touch + * screen is pressed, you will receive this first touch event. Usually + * the first touch event is consumed by the system since the user can + * not see what they are pressing on. + * + * @deprecated This flag has no effect. + */ + GAMEACTIVITY_FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040, + /** + * As long as this window is visible to the user, keep + * the device's screen turned on and bright. + */ + GAMEACTIVITY_FLAG_KEEP_SCREEN_ON = 0x00000080, + /** + * Place the window within the entire screen, ignoring + * decorations around the border (such as the status bar). The + * window must correctly position its contents to take the screen + * decoration into account. + */ + GAMEACTIVITY_FLAG_LAYOUT_IN_SCREEN = 0x00000100, + /** Allows the window to extend outside of the screen. */ + GAMEACTIVITY_FLAG_LAYOUT_NO_LIMITS = 0x00000200, + /** + * Hide all screen decorations (such as the status + * bar) while this window is displayed. This allows the window to + * use the entire display space for itself -- the status bar will + * be hidden when an app window with this flag set is on the top + * layer. A fullscreen window will ignore a value of {@link + * GAMEACTIVITY_SOFT_INPUT_ADJUST_RESIZE}; the window will stay + * fullscreen and will not resize. + */ + GAMEACTIVITY_FLAG_FULLSCREEN = 0x00000400, + /** + * Override {@link GAMEACTIVITY_FLAG_FULLSCREEN} and force the + * screen decorations (such as the status bar) to be shown. + */ + GAMEACTIVITY_FLAG_FORCE_NOT_FULLSCREEN = 0x00000800, + /** + * Turn on dithering when compositing this window to + * the screen. + * @deprecated This flag is no longer used. + */ + GAMEACTIVITY_FLAG_DITHER = 0x00001000, + /** + * Treat the content of the window as secure, preventing + * it from appearing in screenshots or from being viewed on non-secure + * displays. + */ + GAMEACTIVITY_FLAG_SECURE = 0x00002000, + /** + * A special mode where the layout parameters are used + * to perform scaling of the surface when it is composited to the + * screen. + */ + GAMEACTIVITY_FLAG_SCALED = 0x00004000, + /** + * Intended for windows that will often be used when the user is + * holding the screen against their face, it will aggressively + * filter the event stream to prevent unintended presses in this + * situation that may not be desired for a particular window, when + * such an event stream is detected, the application will receive + * a {@link AMOTION_EVENT_ACTION_CANCEL} to indicate this so + * applications can handle this accordingly by taking no action on + * the event until the finger is released. + */ + GAMEACTIVITY_FLAG_IGNORE_CHEEK_PRESSES = 0x00008000, + /** + * A special option only for use in combination with + * {@link GAMEACTIVITY_FLAG_LAYOUT_IN_SCREEN}. When requesting layout in + * the screen your window may appear on top of or behind screen decorations + * such as the status bar. By also including this flag, the window + * manager will report the inset rectangle needed to ensure your + * content is not covered by screen decorations. + */ + GAMEACTIVITY_FLAG_LAYOUT_INSET_DECOR = 0x00010000, + /** + * Invert the state of {@link GAMEACTIVITY_FLAG_NOT_FOCUSABLE} with + * respect to how this window interacts with the current method. + * That is, if FLAG_NOT_FOCUSABLE is set and this flag is set, + * then the window will behave as if it needs to interact with the + * input method and thus be placed behind/away from it; if {@link + * GAMEACTIVITY_FLAG_NOT_FOCUSABLE} is not set and this flag is set, + * then the window will behave as if it doesn't need to interact + * with the input method and can be placed to use more space and + * cover the input method. + */ + GAMEACTIVITY_FLAG_ALT_FOCUSABLE_IM = 0x00020000, + /** + * If you have set {@link GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL}, you + * can set this flag to receive a single special MotionEvent with + * the action + * {@link AMOTION_EVENT_ACTION_OUTSIDE} for + * touches that occur outside of your window. Note that you will not + * receive the full down/move/up gesture, only the location of the + * first down as an {@link AMOTION_EVENT_ACTION_OUTSIDE}. + */ + GAMEACTIVITY_FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000, + /** + * Special flag to let windows be shown when the screen + * is locked. This will let application windows take precedence over + * key guard or any other lock screens. Can be used with + * {@link GAMEACTIVITY_FLAG_KEEP_SCREEN_ON} to turn screen on and display + * windows directly before showing the key guard window. Can be used with + * {@link GAMEACTIVITY_FLAG_DISMISS_KEYGUARD} to automatically fully + * dismisss non-secure keyguards. This flag only applies to the top-most + * full-screen window. + */ + GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED = 0x00080000, + /** + * Ask that the system wallpaper be shown behind + * your window. The window surface must be translucent to be able + * to actually see the wallpaper behind it; this flag just ensures + * that the wallpaper surface will be there if this window actually + * has translucent regions. + */ + GAMEACTIVITY_FLAG_SHOW_WALLPAPER = 0x00100000, + /** + * When set as a window is being added or made + * visible, once the window has been shown then the system will + * poke the power manager's user activity (as if the user had woken + * up the device) to turn the screen on. + */ + GAMEACTIVITY_FLAG_TURN_SCREEN_ON = 0x00200000, + /** + * When set the window will cause the keyguard to + * be dismissed, only if it is not a secure lock keyguard. Because such + * a keyguard is not needed for security, it will never re-appear if + * the user navigates to another window (in contrast to + * {@link GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED}, which will only temporarily + * hide both secure and non-secure keyguards but ensure they reappear + * when the user moves to another UI that doesn't hide them). + * If the keyguard is currently active and is secure (requires an + * unlock pattern) than the user will still need to confirm it before + * seeing this window, unless {@link GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED} has + * also been set. + */ + GAMEACTIVITY_FLAG_DISMISS_KEYGUARD = 0x00400000, +}; + +/** + * Change the window flags of the given activity. Calls getWindow().setFlags() + * of the given activity. + * Note that some flags must be set before the window decoration is created, + * see + * https://developer.android.com/reference/android/view/Window#setFlags(int,%20int). + * Note also that this method can be called from + * *any* thread; it will send a message to the main thread of the process + * where the Java finish call will take place. + */ +void GameActivity_setWindowFlags(GameActivity* activity, uint32_t addFlags, + uint32_t removeFlags); + +/** + * Flags for GameActivity_showSoftInput; see the Java InputMethodManager + * API for documentation. + */ +enum GameActivityShowSoftInputFlags { + /** + * Implicit request to show the input window, not as the result + * of a direct request by the user. + */ + GAMEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT = 0x0001, + + /** + * The user has forced the input method open (such as by + * long-pressing menu) so it should not be closed until they + * explicitly do so. + */ + GAMEACTIVITY_SHOW_SOFT_INPUT_FORCED = 0x0002, +}; + +/** + * Show the IME while in the given activity. Calls + * InputMethodManager.showSoftInput() for the given activity. Note that this + * method can be called from *any* thread; it will send a message to the main + * thread of the process where the Java call will take place. + */ +void GameActivity_showSoftInput(GameActivity* activity, uint32_t flags); + +/** + * Set the text entry state (see documentation of the GameTextInputState struct + * in the Game Text Input library reference). + * + * Ownership of the state is maintained by the caller. + */ +void GameActivity_setTextInputState(GameActivity* activity, + const GameTextInputState* state); + +/** + * Get the last-received text entry state (see documentation of the + * GameTextInputState struct in the Game Text Input library reference). + * + */ +void GameActivity_getTextInputState(GameActivity* activity, + GameTextInputGetStateCallback callback, + void* context); + +/** + * Get a pointer to the GameTextInput library instance. + */ +GameTextInput* GameActivity_getTextInput(const GameActivity* activity); + +/** + * Flags for GameActivity_hideSoftInput; see the Java InputMethodManager + * API for documentation. + */ +enum GameActivityHideSoftInputFlags { + /** + * The soft input window should only be hidden if it was not + * explicitly shown by the user. + */ + GAMEACTIVITY_HIDE_SOFT_INPUT_IMPLICIT_ONLY = 0x0001, + /** + * The soft input window should normally be hidden, unless it was + * originally shown with {@link GAMEACTIVITY_SHOW_SOFT_INPUT_FORCED}. + */ + GAMEACTIVITY_HIDE_SOFT_INPUT_NOT_ALWAYS = 0x0002, +}; + +/** + * Hide the IME while in the given activity. Calls + * InputMethodManager.hideSoftInput() for the given activity. Note that this + * method can be called from *any* thread; it will send a message to the main + * thread of the process where the Java finish call will take place. + */ +void GameActivity_hideSoftInput(GameActivity* activity, uint32_t flags); + +/** + * Get the current window insets of the particular component. See + * https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat.Type + * for more details. + * You can use these insets to influence what you show on the screen. + */ +void GameActivity_getWindowInsets(GameActivity* activity, + GameCommonInsetsType type, ARect* insets); + +/** + * Set options on how the IME behaves when it is requested for text input. + * See + * https://developer.android.com/reference/android/view/inputmethod/EditorInfo + * for the meaning of inputType, actionId and imeOptions. + * + * Note that this function will attach the current thread to the JVM if it is + * not already attached, so the caller must detach the thread from the JVM + * before the thread is destroyed using DetachCurrentThread. + */ +void GameActivity_setImeEditorInfo(GameActivity* activity, int inputType, + int actionId, int imeOptions); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif // ANDROID_GAME_SDK_GAME_ACTIVITY_H diff --git a/sources/android-gamesdk/GameActivity/game-activity/include/game-activity/native_app_glue/android_native_app_glue.c b/sources/android-gamesdk/GameActivity/game-activity/include/game-activity/native_app_glue/android_native_app_glue.c new file mode 100644 index 00000000..f1b385ff --- /dev/null +++ b/sources/android-gamesdk/GameActivity/game-activity/include/game-activity/native_app_glue/android_native_app_glue.c @@ -0,0 +1,571 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "android_native_app_glue.h" + +#include +#include +#include +#include +#include +#include +#include + +#define LOGI(...) \ + ((void)__android_log_print(ANDROID_LOG_INFO, "threaded_app", __VA_ARGS__)) +#define LOGE(...) \ + ((void)__android_log_print(ANDROID_LOG_ERROR, "threaded_app", __VA_ARGS__)) + +/* For debug builds, always enable the debug traces in this library */ +#ifndef NDEBUG +#define LOGV(...) \ + ((void)__android_log_print(ANDROID_LOG_VERBOSE, "threaded_app", \ + __VA_ARGS__)) +#else +#define LOGV(...) ((void)0) +#endif + +static void free_saved_state(struct android_app* android_app) { + pthread_mutex_lock(&android_app->mutex); + if (android_app->savedState != NULL) { + free(android_app->savedState); + android_app->savedState = NULL; + android_app->savedStateSize = 0; + } + pthread_mutex_unlock(&android_app->mutex); +} + +int8_t android_app_read_cmd(struct android_app* android_app) { + int8_t cmd; + if (read(android_app->msgread, &cmd, sizeof(cmd)) != sizeof(cmd)) { + LOGE("No data on command pipe!"); + return -1; + } + if (cmd == APP_CMD_SAVE_STATE) free_saved_state(android_app); + return cmd; +} + +static void print_cur_config(struct android_app* android_app) { + char lang[2], country[2]; + AConfiguration_getLanguage(android_app->config, lang); + AConfiguration_getCountry(android_app->config, country); + + LOGV( + "Config: mcc=%d mnc=%d lang=%c%c cnt=%c%c orien=%d touch=%d dens=%d " + "keys=%d nav=%d keysHid=%d navHid=%d sdk=%d size=%d long=%d " + "modetype=%d modenight=%d", + AConfiguration_getMcc(android_app->config), + AConfiguration_getMnc(android_app->config), lang[0], lang[1], + country[0], country[1], + AConfiguration_getOrientation(android_app->config), + AConfiguration_getTouchscreen(android_app->config), + AConfiguration_getDensity(android_app->config), + AConfiguration_getKeyboard(android_app->config), + AConfiguration_getNavigation(android_app->config), + AConfiguration_getKeysHidden(android_app->config), + AConfiguration_getNavHidden(android_app->config), + AConfiguration_getSdkVersion(android_app->config), + AConfiguration_getScreenSize(android_app->config), + AConfiguration_getScreenLong(android_app->config), + AConfiguration_getUiModeType(android_app->config), + AConfiguration_getUiModeNight(android_app->config)); +} + +void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) { + switch (cmd) { + case UNUSED_APP_CMD_INPUT_CHANGED: + LOGV("UNUSED_APP_CMD_INPUT_CHANGED"); + // Do nothing. This can be used in the future to handle AInputQueue + // natively, like done in NativeActivity. + break; + + case APP_CMD_INIT_WINDOW: + LOGV("APP_CMD_INIT_WINDOW"); + pthread_mutex_lock(&android_app->mutex); + android_app->window = android_app->pendingWindow; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + break; + + case APP_CMD_TERM_WINDOW: + LOGV("APP_CMD_TERM_WINDOW"); + pthread_cond_broadcast(&android_app->cond); + break; + + case APP_CMD_RESUME: + case APP_CMD_START: + case APP_CMD_PAUSE: + case APP_CMD_STOP: + LOGV("activityState=%d", cmd); + pthread_mutex_lock(&android_app->mutex); + android_app->activityState = cmd; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + break; + + case APP_CMD_CONFIG_CHANGED: + LOGV("APP_CMD_CONFIG_CHANGED"); + AConfiguration_fromAssetManager( + android_app->config, android_app->activity->assetManager); + print_cur_config(android_app); + break; + + case APP_CMD_DESTROY: + LOGV("APP_CMD_DESTROY"); + android_app->destroyRequested = 1; + break; + } +} + +void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) { + switch (cmd) { + case APP_CMD_TERM_WINDOW: + LOGV("APP_CMD_TERM_WINDOW"); + pthread_mutex_lock(&android_app->mutex); + android_app->window = NULL; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + break; + + case APP_CMD_SAVE_STATE: + LOGV("APP_CMD_SAVE_STATE"); + pthread_mutex_lock(&android_app->mutex); + android_app->stateSaved = 1; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + break; + + case APP_CMD_RESUME: + free_saved_state(android_app); + break; + } +} + +void app_dummy() {} + +static void android_app_destroy(struct android_app* android_app) { + LOGV("android_app_destroy!"); + free_saved_state(android_app); + pthread_mutex_lock(&android_app->mutex); + + AConfiguration_delete(android_app->config); + android_app->destroyed = 1; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + // Can't touch android_app object after this. +} + +static void process_cmd(struct android_app* app, + struct android_poll_source* source) { + int8_t cmd = android_app_read_cmd(app); + android_app_pre_exec_cmd(app, cmd); + if (app->onAppCmd != NULL) app->onAppCmd(app, cmd); + android_app_post_exec_cmd(app, cmd); +} + +// This is run on a separate thread (i.e: not the main thread). +static void* android_app_entry(void* param) { + struct android_app* android_app = (struct android_app*)param; + + LOGV("android_app_entry called"); + android_app->config = AConfiguration_new(); + LOGV("android_app = %p", android_app); + LOGV("config = %p", android_app->config); + LOGV("activity = %p", android_app->activity); + LOGV("assetmanager = %p", android_app->activity->assetManager); + AConfiguration_fromAssetManager(android_app->config, + android_app->activity->assetManager); + + print_cur_config(android_app); + + android_app->cmdPollSource.id = LOOPER_ID_MAIN; + android_app->cmdPollSource.app = android_app; + android_app->cmdPollSource.process = process_cmd; + + ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS); + ALooper_addFd(looper, android_app->msgread, LOOPER_ID_MAIN, + ALOOPER_EVENT_INPUT, NULL, &android_app->cmdPollSource); + android_app->looper = looper; + + pthread_mutex_lock(&android_app->mutex); + android_app->running = 1; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + + android_main(android_app); + + android_app_destroy(android_app); + return NULL; +} + +// Codes from https://developer.android.com/reference/android/view/KeyEvent +#define KEY_EVENT_KEYCODE_VOLUME_DOWN 25 +#define KEY_EVENT_KEYCODE_VOLUME_MUTE 164 +#define KEY_EVENT_KEYCODE_VOLUME_UP 24 +#define KEY_EVENT_KEYCODE_CAMERA 27 +#define KEY_EVENT_KEYCODE_ZOOM_IN 168 +#define KEY_EVENT_KEYCODE_ZOOM_OUT 169 + +// Double-buffer the key event filter to avoid race condition. +static bool default_key_filter(const GameActivityKeyEvent* event) { + // Ignore camera, volume, etc. buttons + return !(event->keyCode == KEY_EVENT_KEYCODE_VOLUME_DOWN || + event->keyCode == KEY_EVENT_KEYCODE_VOLUME_MUTE || + event->keyCode == KEY_EVENT_KEYCODE_VOLUME_UP || + event->keyCode == KEY_EVENT_KEYCODE_CAMERA || + event->keyCode == KEY_EVENT_KEYCODE_ZOOM_IN || + event->keyCode == KEY_EVENT_KEYCODE_ZOOM_OUT); +} + +// See +// https://developer.android.com/reference/android/view/InputDevice#SOURCE_TOUCHSCREEN +#define SOURCE_TOUCHSCREEN 0x00001002 + +static bool default_motion_filter(const GameActivityMotionEvent* event) { + // Ignore any non-touch events. + return event->source == SOURCE_TOUCHSCREEN; +} + +// -------------------------------------------------------------------- +// Native activity interaction (called from main thread) +// -------------------------------------------------------------------- + +static struct android_app* android_app_create(GameActivity* activity, + void* savedState, + size_t savedStateSize) { + // struct android_app* android_app = calloc(1, sizeof(struct android_app)); + struct android_app* android_app = + (struct android_app*)malloc(sizeof(struct android_app)); + memset(android_app, 0, sizeof(struct android_app)); + android_app->activity = activity; + + pthread_mutex_init(&android_app->mutex, NULL); + pthread_cond_init(&android_app->cond, NULL); + + if (savedState != NULL) { + android_app->savedState = malloc(savedStateSize); + android_app->savedStateSize = savedStateSize; + memcpy(android_app->savedState, savedState, savedStateSize); + } + + int msgpipe[2]; + if (pipe(msgpipe)) { + LOGE("could not create pipe: %s", strerror(errno)); + return NULL; + } + android_app->msgread = msgpipe[0]; + android_app->msgwrite = msgpipe[1]; + + android_app->keyEventFilter = default_key_filter; + android_app->motionEventFilter = default_motion_filter; + + LOGV("Launching android_app_entry in a thread"); + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + pthread_create(&android_app->thread, &attr, android_app_entry, android_app); + + // Wait for thread to start. + pthread_mutex_lock(&android_app->mutex); + while (!android_app->running) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + pthread_mutex_unlock(&android_app->mutex); + + return android_app; +} + +static void android_app_write_cmd(struct android_app* android_app, int8_t cmd) { + if (write(android_app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) { + LOGE("Failure writing android_app cmd: %s", strerror(errno)); + } +} + +static void android_app_set_window(struct android_app* android_app, + ANativeWindow* window) { + LOGV("android_app_set_window called"); + pthread_mutex_lock(&android_app->mutex); + if (android_app->pendingWindow != NULL) { + android_app_write_cmd(android_app, APP_CMD_TERM_WINDOW); + } + android_app->pendingWindow = window; + if (window != NULL) { + android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW); + } + while (android_app->window != android_app->pendingWindow) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + pthread_mutex_unlock(&android_app->mutex); +} + +static void android_app_set_activity_state(struct android_app* android_app, + int8_t cmd) { + pthread_mutex_lock(&android_app->mutex); + android_app_write_cmd(android_app, cmd); + while (android_app->activityState != cmd) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + pthread_mutex_unlock(&android_app->mutex); +} + +static void android_app_free(struct android_app* android_app) { + pthread_mutex_lock(&android_app->mutex); + android_app_write_cmd(android_app, APP_CMD_DESTROY); + while (!android_app->destroyed) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + pthread_mutex_unlock(&android_app->mutex); + + close(android_app->msgread); + close(android_app->msgwrite); + pthread_cond_destroy(&android_app->cond); + pthread_mutex_destroy(&android_app->mutex); + free(android_app); +} + +static inline struct android_app* ToApp(GameActivity* activity) { + return (struct android_app*)activity->instance; +} + +static void onDestroy(GameActivity* activity) { + LOGV("Destroy: %p", activity); + android_app_free(ToApp(activity)); +} + +static void onStart(GameActivity* activity) { + LOGV("Start: %p", activity); + android_app_set_activity_state(ToApp(activity), APP_CMD_START); +} + +static void onResume(GameActivity* activity) { + LOGV("Resume: %p", activity); + android_app_set_activity_state(ToApp(activity), APP_CMD_RESUME); +} + +static void onSaveInstanceState(GameActivity* activity, + SaveInstanceStateRecallback recallback, + void* context) { + LOGV("SaveInstanceState: %p", activity); + + struct android_app* android_app = ToApp(activity); + void* savedState = NULL; + pthread_mutex_lock(&android_app->mutex); + android_app->stateSaved = 0; + android_app_write_cmd(android_app, APP_CMD_SAVE_STATE); + while (!android_app->stateSaved) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + + if (android_app->savedState != NULL) { + // Tell the Java side about our state. + recallback((const char*)android_app->savedState, + android_app->savedStateSize, context); + // Now we can free it. + free(android_app->savedState); + android_app->savedState = NULL; + android_app->savedStateSize = 0; + } + + pthread_mutex_unlock(&android_app->mutex); +} + +static void onPause(GameActivity* activity) { + LOGV("Pause: %p", activity); + android_app_set_activity_state(ToApp(activity), APP_CMD_PAUSE); +} + +static void onStop(GameActivity* activity) { + LOGV("Stop: %p", activity); + android_app_set_activity_state(ToApp(activity), APP_CMD_STOP); +} + +static void onConfigurationChanged(GameActivity* activity) { + LOGV("ConfigurationChanged: %p", activity); + android_app_write_cmd(ToApp(activity), APP_CMD_CONFIG_CHANGED); +} + +static void onTrimMemory(GameActivity* activity, int level) { + LOGV("TrimMemory: %p %d", activity, level); + android_app_write_cmd(ToApp(activity), APP_CMD_LOW_MEMORY); +} + +static void onWindowFocusChanged(GameActivity* activity, bool focused) { + LOGV("WindowFocusChanged: %p -- %d", activity, focused); + android_app_write_cmd(ToApp(activity), + focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS); +} + +static void onNativeWindowCreated(GameActivity* activity, + ANativeWindow* window) { + LOGV("NativeWindowCreated: %p -- %p", activity, window); + android_app_set_window(ToApp(activity), window); +} + +static void onNativeWindowDestroyed(GameActivity* activity, + ANativeWindow* window) { + LOGV("NativeWindowDestroyed: %p -- %p", activity, window); + android_app_set_window(ToApp(activity), NULL); +} + +static void onNativeWindowRedrawNeeded(GameActivity* activity, + ANativeWindow* window) { + LOGV("NativeWindowRedrawNeeded: %p -- %p", activity, window); + android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_REDRAW_NEEDED); +} + +static void onNativeWindowResized(GameActivity* activity, ANativeWindow* window, + int32_t width, int32_t height) { + LOGV("NativeWindowResized: %p -- %p ( %d x %d )", activity, window, width, + height); + android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_RESIZED); +} + +void android_app_set_motion_event_filter(struct android_app* app, + android_motion_event_filter filter) { + pthread_mutex_lock(&app->mutex); + app->motionEventFilter = filter; + pthread_mutex_unlock(&app->mutex); +} + +static bool onTouchEvent(GameActivity* activity, + const GameActivityMotionEvent* event) { + struct android_app* android_app = ToApp(activity); + pthread_mutex_lock(&android_app->mutex); + + if (android_app->motionEventFilter != NULL && + !android_app->motionEventFilter(event)) { + pthread_mutex_unlock(&android_app->mutex); + return false; + } + + struct android_input_buffer* inputBuffer = + &android_app->inputBuffers[android_app->currentInputBuffer]; + + // Add to the list of active motion events + if (inputBuffer->motionEventsCount < + NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS) { + int new_ix = inputBuffer->motionEventsCount; + memcpy(&inputBuffer->motionEvents[new_ix], event, + sizeof(GameActivityMotionEvent)); + ++inputBuffer->motionEventsCount; + } + pthread_mutex_unlock(&android_app->mutex); + return true; +} + +struct android_input_buffer* android_app_swap_input_buffers( + struct android_app* android_app) { + pthread_mutex_lock(&android_app->mutex); + + struct android_input_buffer* inputBuffer = + &android_app->inputBuffers[android_app->currentInputBuffer]; + + if (inputBuffer->motionEventsCount == 0 && + inputBuffer->keyEventsCount == 0) { + inputBuffer = NULL; + } else { + android_app->currentInputBuffer = + (android_app->currentInputBuffer + 1) % + NATIVE_APP_GLUE_MAX_INPUT_BUFFERS; + } + + pthread_mutex_unlock(&android_app->mutex); + + return inputBuffer; +} + +void android_app_clear_motion_events(struct android_input_buffer* inputBuffer) { + inputBuffer->motionEventsCount = 0; +} + +void android_app_set_key_event_filter(struct android_app* app, + android_key_event_filter filter) { + pthread_mutex_lock(&app->mutex); + app->keyEventFilter = filter; + pthread_mutex_unlock(&app->mutex); +} + +static bool onKey(GameActivity* activity, const GameActivityKeyEvent* event) { + struct android_app* android_app = ToApp(activity); + pthread_mutex_lock(&android_app->mutex); + + if (android_app->keyEventFilter != NULL && + !android_app->keyEventFilter(event)) { + pthread_mutex_unlock(&android_app->mutex); + return false; + } + + struct android_input_buffer* inputBuffer = + &android_app->inputBuffers[android_app->currentInputBuffer]; + + // Add to the list of active key down events + if (inputBuffer->keyEventsCount < NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS) { + int new_ix = inputBuffer->keyEventsCount; + memcpy(&inputBuffer->keyEvents[new_ix], event, + sizeof(GameActivityKeyEvent)); + ++inputBuffer->keyEventsCount; + } + + pthread_mutex_unlock(&android_app->mutex); + return true; +} + +void android_app_clear_key_events(struct android_input_buffer* inputBuffer) { + inputBuffer->keyEventsCount = 0; +} + +static void onTextInputEvent(GameActivity* activity, + const GameTextInputState* state) { + struct android_app* android_app = ToApp(activity); + pthread_mutex_lock(&android_app->mutex); + + android_app->textInputState = 1; + pthread_mutex_unlock(&android_app->mutex); +} + +static void onWindowInsetsChanged(GameActivity* activity) { + LOGV("WindowInsetsChanged: %p", activity); + android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_INSETS_CHANGED); +} + +JNIEXPORT +void GameActivity_onCreate(GameActivity* activity, void* savedState, + size_t savedStateSize) { + LOGV("Creating: %p", activity); + activity->callbacks->onDestroy = onDestroy; + activity->callbacks->onStart = onStart; + activity->callbacks->onResume = onResume; + activity->callbacks->onSaveInstanceState = onSaveInstanceState; + activity->callbacks->onPause = onPause; + activity->callbacks->onStop = onStop; + activity->callbacks->onTouchEvent = onTouchEvent; + activity->callbacks->onKeyDown = onKey; + activity->callbacks->onKeyUp = onKey; + activity->callbacks->onTextInputEvent = onTextInputEvent; + activity->callbacks->onConfigurationChanged = onConfigurationChanged; + activity->callbacks->onTrimMemory = onTrimMemory; + activity->callbacks->onWindowFocusChanged = onWindowFocusChanged; + activity->callbacks->onNativeWindowCreated = onNativeWindowCreated; + activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed; + activity->callbacks->onNativeWindowRedrawNeeded = + onNativeWindowRedrawNeeded; + activity->callbacks->onNativeWindowResized = onNativeWindowResized; + activity->callbacks->onWindowInsetsChanged = onWindowInsetsChanged; + LOGV("Callbacks set: %p", activity->callbacks); + + activity->instance = + android_app_create(activity, savedState, savedStateSize); +} diff --git a/sources/android-gamesdk/GameActivity/game-activity/include/game-activity/native_app_glue/android_native_app_glue.h b/sources/android-gamesdk/GameActivity/game-activity/include/game-activity/native_app_glue/android_native_app_glue.h new file mode 100644 index 00000000..9dfe3a53 --- /dev/null +++ b/sources/android-gamesdk/GameActivity/game-activity/include/game-activity/native_app_glue/android_native_app_glue.h @@ -0,0 +1,486 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +/** + * @addtogroup android_native_app_glue Native App Glue library + * The glue library to interface your game loop with GameActivity. + * @{ + */ + +#include +#include +#include +#include +#include + +#include "game-activity/GameActivity.h" + +#if (defined NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS_OVERRIDE) +#define NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS \ + NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS_OVERRIDE +#else +#define NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS 16 +#endif + +#if (defined NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS_OVERRIDE) +#define NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS \ + NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS_OVERRIDE +#else +#define NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS 4 +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * The GameActivity interface provided by + * is based on a set of application-provided callbacks that will be called + * by the Activity's main thread when certain events occur. + * + * This means that each one of this callbacks _should_ _not_ block, or they + * risk having the system force-close the application. This programming + * model is direct, lightweight, but constraining. + * + * The 'android_native_app_glue' static library is used to provide a different + * execution model where the application can implement its own main event + * loop in a different thread instead. Here's how it works: + * + * 1/ The application must provide a function named "android_main()" that + * will be called when the activity is created, in a new thread that is + * distinct from the activity's main thread. + * + * 2/ android_main() receives a pointer to a valid "android_app" structure + * that contains references to other important objects, e.g. the + * GameActivity obejct instance the application is running in. + * + * 3/ the "android_app" object holds an ALooper instance that already + * listens to activity lifecycle events (e.g. "pause", "resume"). + * See APP_CMD_XXX declarations below. + * + * This corresponds to an ALooper identifier returned by + * ALooper_pollOnce with value LOOPER_ID_MAIN. + * + * Your application can use the same ALooper to listen to additional + * file-descriptors. They can either be callback based, or with return + * identifiers starting with LOOPER_ID_USER. + * + * 4/ Whenever you receive a LOOPER_ID_MAIN event, + * the returned data will point to an android_poll_source structure. You + * can call the process() function on it, and fill in android_app->onAppCmd + * to be called for your own processing of the event. + * + * Alternatively, you can call the low-level functions to read and process + * the data directly... look at the process_cmd() and process_input() + * implementations in the glue to see how to do this. + * + * See the sample named "native-activity" that comes with the NDK with a + * full usage example. Also look at the documentation of GameActivity. + */ + +struct android_app; + +/** + * Data associated with an ALooper fd that will be returned as the "outData" + * when that source has data ready. + */ +struct android_poll_source { + /** + * The identifier of this source. May be LOOPER_ID_MAIN or + * LOOPER_ID_INPUT. + */ + int32_t id; + + /** The android_app this ident is associated with. */ + struct android_app* app; + + /** + * Function to call to perform the standard processing of data from + * this source. + */ + void (*process)(struct android_app* app, + struct android_poll_source* source); +}; + +struct android_input_buffer { + /** + * Pointer to a read-only array of pointers to GameActivityMotionEvent. + * Only the first motionEventsCount events are valid. + */ + GameActivityMotionEvent motionEvents[NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS]; + + /** + * The number of valid motion events in `motionEvents`. + */ + uint64_t motionEventsCount; + + /** + * Pointer to a read-only array of pointers to GameActivityKeyEvent. + * Only the first keyEventsCount events are valid. + */ + GameActivityKeyEvent keyEvents[NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS]; + + /** + * The number of valid "Key" events in `keyEvents`. + */ + uint64_t keyEventsCount; +}; + +/** + * Function pointer declaration for the filtering of key events. + * A function with this signature should be passed to + * android_app_set_key_event_filter and return false for any events that should + * not be handled by android_native_app_glue. These events will be handled by + * the system instead. + */ +typedef bool (*android_key_event_filter)(const GameActivityKeyEvent*); + +/** + * Function pointer definition for the filtering of motion events. + * A function with this signature should be passed to + * android_app_set_motion_event_filter and return false for any events that + * should not be handled by android_native_app_glue. These events will be + * handled by the system instead. + */ +typedef bool (*android_motion_event_filter)(const GameActivityMotionEvent*); + +/** + * This is the interface for the standard glue code of a threaded + * application. In this model, the application's code is running + * in its own thread separate from the main thread of the process. + * It is not required that this thread be associated with the Java + * VM, although it will need to be in order to make JNI calls any + * Java objects. + */ +struct android_app { + /** + * An optional pointer to application-defined state. + */ + void* userData; + + /** + * A required callback for processing main app commands (`APP_CMD_*`). + * This is called each frame if there are app commands that need processing. + */ + void (*onAppCmd)(struct android_app* app, int32_t cmd); + + /** The GameActivity object instance that this app is running in. */ + GameActivity* activity; + + /** The current configuration the app is running in. */ + AConfiguration* config; + + /** + * The last activity saved state, as provided at creation time. + * It is NULL if there was no state. You can use this as you need; the + * memory will remain around until you call android_app_exec_cmd() for + * APP_CMD_RESUME, at which point it will be freed and savedState set to + * NULL. These variables should only be changed when processing a + * APP_CMD_SAVE_STATE, at which point they will be initialized to NULL and + * you can malloc your state and place the information here. In that case + * the memory will be freed for you later. + */ + void* savedState; + + /** + * The size of the activity saved state. It is 0 if `savedState` is NULL. + */ + size_t savedStateSize; + + /** The ALooper associated with the app's thread. */ + ALooper* looper; + + /** When non-NULL, this is the window surface that the app can draw in. */ + ANativeWindow* window; + + /** + * Current content rectangle of the window; this is the area where the + * window's content should be placed to be seen by the user. + */ + ARect contentRect; + + /** + * Current state of the app's activity. May be either APP_CMD_START, + * APP_CMD_RESUME, APP_CMD_PAUSE, or APP_CMD_STOP. + */ + int activityState; + + /** + * This is non-zero when the application's GameActivity is being + * destroyed and waiting for the app thread to complete. + */ + int destroyRequested; + +#define NATIVE_APP_GLUE_MAX_INPUT_BUFFERS 2 + + /** + * This is used for buffering input from GameActivity. Once ready, the + * application thread switches the buffers and processes what was + * accumulated. + */ + struct android_input_buffer inputBuffers[NATIVE_APP_GLUE_MAX_INPUT_BUFFERS]; + + int currentInputBuffer; + + /** + * 0 if no text input event is outstanding, 1 if it is. + * Use `GameActivity_getTextInputState` to get information + * about the text entered by the user. + */ + int textInputState; + + // Below are "private" implementation of the glue code. + /** @cond INTERNAL */ + + pthread_mutex_t mutex; + pthread_cond_t cond; + + int msgread; + int msgwrite; + + pthread_t thread; + + struct android_poll_source cmdPollSource; + + int running; + int stateSaved; + int destroyed; + int redrawNeeded; + ANativeWindow* pendingWindow; + ARect pendingContentRect; + + android_key_event_filter keyEventFilter; + android_motion_event_filter motionEventFilter; + + /** @endcond */ +}; + +/** + * Looper ID of commands coming from the app's main thread, an AInputQueue or + * user-defined sources. + */ +enum NativeAppGlueLooperId { + /** + * Looper data ID of commands coming from the app's main thread, which + * is returned as an identifier from ALooper_pollOnce(). The data for this + * identifier is a pointer to an android_poll_source structure. + * These can be retrieved and processed with android_app_read_cmd() + * and android_app_exec_cmd(). + */ + LOOPER_ID_MAIN = 1, + + /** + * Unused. Reserved for future use when usage of AInputQueue will be + * supported. + */ + LOOPER_ID_INPUT = 2, + + /** + * Start of user-defined ALooper identifiers. + */ + LOOPER_ID_USER = 3, +}; + +/** + * Commands passed from the application's main Java thread to the game's thread. + */ +enum NativeAppGlueAppCmd { + /** + * Unused. Reserved for future use when usage of AInputQueue will be + * supported. + */ + UNUSED_APP_CMD_INPUT_CHANGED, + + /** + * Command from main thread: a new ANativeWindow is ready for use. Upon + * receiving this command, android_app->window will contain the new window + * surface. + */ + APP_CMD_INIT_WINDOW, + + /** + * Command from main thread: the existing ANativeWindow needs to be + * terminated. Upon receiving this command, android_app->window still + * contains the existing window; after calling android_app_exec_cmd + * it will be set to NULL. + */ + APP_CMD_TERM_WINDOW, + + /** + * Command from main thread: the current ANativeWindow has been resized. + * Please redraw with its new size. + */ + APP_CMD_WINDOW_RESIZED, + + /** + * Command from main thread: the system needs that the current ANativeWindow + * be redrawn. You should redraw the window before handing this to + * android_app_exec_cmd() in order to avoid transient drawing glitches. + */ + APP_CMD_WINDOW_REDRAW_NEEDED, + + /** + * Command from main thread: the content area of the window has changed, + * such as from the soft input window being shown or hidden. You can + * find the new content rect in android_app::contentRect. + */ + APP_CMD_CONTENT_RECT_CHANGED, + + /** + * Command from main thread: the app's activity window has gained + * input focus. + */ + APP_CMD_GAINED_FOCUS, + + /** + * Command from main thread: the app's activity window has lost + * input focus. + */ + APP_CMD_LOST_FOCUS, + + /** + * Command from main thread: the current device configuration has changed. + */ + APP_CMD_CONFIG_CHANGED, + + /** + * Command from main thread: the system is running low on memory. + * Try to reduce your memory use. + */ + APP_CMD_LOW_MEMORY, + + /** + * Command from main thread: the app's activity has been started. + */ + APP_CMD_START, + + /** + * Command from main thread: the app's activity has been resumed. + */ + APP_CMD_RESUME, + + /** + * Command from main thread: the app should generate a new saved state + * for itself, to restore from later if needed. If you have saved state, + * allocate it with malloc and place it in android_app.savedState with + * the size in android_app.savedStateSize. The will be freed for you + * later. + */ + APP_CMD_SAVE_STATE, + + /** + * Command from main thread: the app's activity has been paused. + */ + APP_CMD_PAUSE, + + /** + * Command from main thread: the app's activity has been stopped. + */ + APP_CMD_STOP, + + /** + * Command from main thread: the app's activity is being destroyed, + * and waiting for the app thread to clean up and exit before proceeding. + */ + APP_CMD_DESTROY, + + /** + * Command from main thread: the app's insets have changed. + */ + APP_CMD_WINDOW_INSETS_CHANGED, + +}; + +/** + * Call when ALooper_pollAll() returns LOOPER_ID_MAIN, reading the next + * app command message. + */ +int8_t android_app_read_cmd(struct android_app* android_app); + +/** + * Call with the command returned by android_app_read_cmd() to do the + * initial pre-processing of the given command. You can perform your own + * actions for the command after calling this function. + */ +void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd); + +/** + * Call with the command returned by android_app_read_cmd() to do the + * final post-processing of the given command. You must have done your own + * actions for the command before calling this function. + */ +void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd); + +/** + * Call this before processing input events to get the events buffer. + * The function returns NULL if there are no events to process. + */ +struct android_input_buffer* android_app_swap_input_buffers( + struct android_app* android_app); + +/** + * Clear the array of motion events that were waiting to be handled, and release + * each of them. + * + * This method should be called after you have processed the motion events in + * your game loop. You should handle events at each iteration of your game loop. + */ +void android_app_clear_motion_events(struct android_input_buffer* inputBuffer); + +/** + * Clear the array of key events that were waiting to be handled, and release + * each of them. + * + * This method should be called after you have processed the key up events in + * your game loop. You should handle events at each iteration of your game loop. + */ +void android_app_clear_key_events(struct android_input_buffer* inputBuffer); + +/** + * This is the function that application code must implement, representing + * the main entry to the app. + */ +extern void android_main(struct android_app* app); + +/** + * Set the filter to use when processing key events. + * Any events for which the filter returns false will be ignored by + * android_native_app_glue. If filter is set to NULL, no filtering is done. + * + * The default key filter will filter out volume and camera button presses. + */ +void android_app_set_key_event_filter(struct android_app* app, + android_key_event_filter filter); + +/** + * Set the filter to use when processing touch and motion events. + * Any events for which the filter returns false will be ignored by + * android_native_app_glue. If filter is set to NULL, no filtering is done. + * + * Note that the default motion event filter will only allow touchscreen events + * through, in order to mimic NativeActivity's behaviour, so for controller + * events to be passed to the app, set the filter to NULL. + */ +void android_app_set_motion_event_filter(struct android_app* app, + android_motion_event_filter filter); + +#ifdef __cplusplus +} +#endif + +/** @} */ diff --git a/sources/android-gamesdk/GameActivity/game-activity/include/game-text-input/gamecommon.h b/sources/android-gamesdk/GameActivity/game-activity/include/game-text-input/gamecommon.h new file mode 100644 index 00000000..38bffff2 --- /dev/null +++ b/sources/android-gamesdk/GameActivity/game-activity/include/game-text-input/gamecommon.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @defgroup game_common Game Common + * Common structures and functions used within AGDK + * @{ + */ + +#pragma once + +/** + * The type of a component for which to retrieve insets. See + * https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat.Type + */ +typedef enum GameCommonInsetsType { + GAMECOMMON_INSETS_TYPE_CAPTION_BAR = 0, + GAMECOMMON_INSETS_TYPE_DISPLAY_CUTOUT, + GAMECOMMON_INSETS_TYPE_IME, + GAMECOMMON_INSETS_TYPE_MANDATORY_SYSTEM_GESTURES, + GAMECOMMON_INSETS_TYPE_NAVIGATION_BARS, + GAMECOMMON_INSETS_TYPE_STATUS_BARS, + GAMECOMMON_INSETS_TYPE_SYSTEM_BARS, + GAMECOMMON_INSETS_TYPE_SYSTEM_GESTURES, + GAMECOMMON_INSETS_TYPE_TAPABLE_ELEMENT, + GAMECOMMON_INSETS_TYPE_WATERFALL, + GAMECOMMON_INSETS_TYPE_COUNT +} GameCommonInsetsType; diff --git a/sources/android-gamesdk/GameActivity/game-activity/include/game-text-input/gametextinput.cpp b/sources/android-gamesdk/GameActivity/game-activity/include/game-text-input/gametextinput.cpp new file mode 100644 index 00000000..25bfcec0 --- /dev/null +++ b/sources/android-gamesdk/GameActivity/game-activity/include/game-text-input/gametextinput.cpp @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "game-text-input/gametextinput.h" + +#include +#include +#include +#include + +#include +#include +#include + +#define LOG_TAG "GameTextInput" + +static constexpr int32_t DEFAULT_MAX_STRING_SIZE = 1 << 16; + +// Cache of field ids in the Java GameTextInputState class +struct StateClassInfo { + jfieldID text; + jfieldID selectionStart; + jfieldID selectionEnd; + jfieldID composingRegionStart; + jfieldID composingRegionEnd; +}; + +// Main GameTextInput object. +struct GameTextInput { + public: + GameTextInput(JNIEnv *env, uint32_t max_string_size); + ~GameTextInput(); + void setState(const GameTextInputState &state); + const GameTextInputState &getState() const { return currentState_; } + void setInputConnection(jobject inputConnection); + void processEvent(jobject textInputEvent); + void showIme(uint32_t flags); + void hideIme(uint32_t flags); + void setEventCallback(GameTextInputEventCallback callback, void *context); + jobject stateToJava(const GameTextInputState &state) const; + void stateFromJava(jobject textInputEvent, + GameTextInputGetStateCallback callback, + void *context) const; + void setImeInsetsCallback(GameTextInputImeInsetsCallback callback, + void *context); + void processImeInsets(const ARect *insets); + const ARect &getImeInsets() const { return currentInsets_; } + + private: + // Copy string and set other fields + void setStateInner(const GameTextInputState &state); + static void processCallback(void *context, const GameTextInputState *state); + JNIEnv *env_ = nullptr; + // Cached at initialization from + // com/google/androidgamesdk/gametextinput/State. + jclass stateJavaClass_ = nullptr; + // The latest text input update. + GameTextInputState currentState_ = {}; + // An instance of gametextinput.InputConnection. + jclass inputConnectionClass_ = nullptr; + jobject inputConnection_ = nullptr; + jmethodID inputConnectionSetStateMethod_; + jmethodID setSoftKeyboardActiveMethod_; + void (*eventCallback_)(void *context, + const struct GameTextInputState *state) = nullptr; + void *eventCallbackContext_ = nullptr; + void (*insetsCallback_)(void *context, + const struct ARect *insets) = nullptr; + ARect currentInsets_ = {}; + void *insetsCallbackContext_ = nullptr; + StateClassInfo stateClassInfo_ = {}; + // Constant-sized buffer used to store state text. + std::vector stateStringBuffer_; +}; + +std::unique_ptr s_gameTextInput; + +extern "C" { + +/////////////////////////////////////////////////////////// +/// GameTextInputState C Functions +/////////////////////////////////////////////////////////// + +// Convert to a Java structure. +jobject currentState_toJava(const GameTextInput *gameTextInput, + const GameTextInputState *state) { + if (state == nullptr) return NULL; + return gameTextInput->stateToJava(*state); +} + +// Convert from Java structure. +void currentState_fromJava(const GameTextInput *gameTextInput, + jobject textInputEvent, + GameTextInputGetStateCallback callback, + void *context) { + gameTextInput->stateFromJava(textInputEvent, callback, context); +} + +/////////////////////////////////////////////////////////// +/// GameTextInput C Functions +/////////////////////////////////////////////////////////// + +struct GameTextInput *GameTextInput_init(JNIEnv *env, + uint32_t max_string_size) { + if (s_gameTextInput.get() != nullptr) { + __android_log_print(ANDROID_LOG_WARN, LOG_TAG, + "Warning: called GameTextInput_init twice without " + "calling GameTextInput_destroy"); + return s_gameTextInput.get(); + } + // Don't use make_unique, for C++11 compatibility + s_gameTextInput = + std::unique_ptr(new GameTextInput(env, max_string_size)); + return s_gameTextInput.get(); +} + +void GameTextInput_destroy(GameTextInput *input) { + if (input == nullptr || s_gameTextInput.get() == nullptr) return; + s_gameTextInput.reset(); +} + +void GameTextInput_setState(GameTextInput *input, + const GameTextInputState *state) { + if (state == nullptr) return; + input->setState(*state); +} + +void GameTextInput_getState(GameTextInput *input, + GameTextInputGetStateCallback callback, + void *context) { + callback(context, &input->getState()); +} + +void GameTextInput_setInputConnection(GameTextInput *input, + jobject inputConnection) { + input->setInputConnection(inputConnection); +} + +void GameTextInput_processEvent(GameTextInput *input, jobject textInputEvent) { + input->processEvent(textInputEvent); +} + +void GameTextInput_processImeInsets(GameTextInput *input, const ARect *insets) { + input->processImeInsets(insets); +} + +void GameTextInput_showIme(struct GameTextInput *input, uint32_t flags) { + input->showIme(flags); +} + +void GameTextInput_hideIme(struct GameTextInput *input, uint32_t flags) { + input->hideIme(flags); +} + +void GameTextInput_setEventCallback(struct GameTextInput *input, + GameTextInputEventCallback callback, + void *context) { + input->setEventCallback(callback, context); +} + +void GameTextInput_setImeInsetsCallback(struct GameTextInput *input, + GameTextInputImeInsetsCallback callback, + void *context) { + input->setImeInsetsCallback(callback, context); +} + +void GameTextInput_getImeInsets(const GameTextInput *input, ARect *insets) { + *insets = input->getImeInsets(); +} + +} // extern "C" + +/////////////////////////////////////////////////////////// +/// GameTextInput C++ class Implementation +/////////////////////////////////////////////////////////// + +GameTextInput::GameTextInput(JNIEnv *env, uint32_t max_string_size) + : env_(env), + stateStringBuffer_(max_string_size == 0 ? DEFAULT_MAX_STRING_SIZE + : max_string_size) { + stateJavaClass_ = (jclass)env_->NewGlobalRef( + env_->FindClass("com/google/androidgamesdk/gametextinput/State")); + inputConnectionClass_ = (jclass)env_->NewGlobalRef(env_->FindClass( + "com/google/androidgamesdk/gametextinput/InputConnection")); + inputConnectionSetStateMethod_ = + env_->GetMethodID(inputConnectionClass_, "setState", + "(Lcom/google/androidgamesdk/gametextinput/State;)V"); + setSoftKeyboardActiveMethod_ = env_->GetMethodID( + inputConnectionClass_, "setSoftKeyboardActive", "(ZI)V"); + + stateClassInfo_.text = + env_->GetFieldID(stateJavaClass_, "text", "Ljava/lang/String;"); + stateClassInfo_.selectionStart = + env_->GetFieldID(stateJavaClass_, "selectionStart", "I"); + stateClassInfo_.selectionEnd = + env_->GetFieldID(stateJavaClass_, "selectionEnd", "I"); + stateClassInfo_.composingRegionStart = + env_->GetFieldID(stateJavaClass_, "composingRegionStart", "I"); + stateClassInfo_.composingRegionEnd = + env_->GetFieldID(stateJavaClass_, "composingRegionEnd", "I"); + + s_gameTextInput.get(); +} + +GameTextInput::~GameTextInput() { + if (stateJavaClass_ != NULL) { + env_->DeleteGlobalRef(stateJavaClass_); + stateJavaClass_ = NULL; + } + if (inputConnectionClass_ != NULL) { + env_->DeleteGlobalRef(inputConnectionClass_); + inputConnectionClass_ = NULL; + } + if (inputConnection_ != NULL) { + env_->DeleteGlobalRef(inputConnection_); + inputConnection_ = NULL; + } +} + +void GameTextInput::setState(const GameTextInputState &state) { + if (inputConnection_ == nullptr) return; + jobject jstate = stateToJava(state); + env_->CallVoidMethod(inputConnection_, inputConnectionSetStateMethod_, + jstate); + env_->DeleteLocalRef(jstate); + setStateInner(state); +} + +void GameTextInput::setStateInner(const GameTextInputState &state) { + // Check if we're setting using our own string (other parts may be + // different) + if (state.text_UTF8 == currentState_.text_UTF8) { + currentState_ = state; + return; + } + // Otherwise, copy across the string. + auto bytes_needed = + std::min(static_cast(state.text_length + 1), + static_cast(stateStringBuffer_.size())); + currentState_.text_UTF8 = stateStringBuffer_.data(); + std::copy(state.text_UTF8, state.text_UTF8 + bytes_needed - 1, + stateStringBuffer_.data()); + currentState_.text_length = state.text_length; + currentState_.selection = state.selection; + currentState_.composingRegion = state.composingRegion; + stateStringBuffer_[bytes_needed - 1] = 0; +} + +void GameTextInput::setInputConnection(jobject inputConnection) { + if (inputConnection_ != NULL) { + env_->DeleteGlobalRef(inputConnection_); + } + inputConnection_ = env_->NewGlobalRef(inputConnection); +} + +/*static*/ void GameTextInput::processCallback( + void *context, const GameTextInputState *state) { + auto thiz = static_cast(context); + if (state != nullptr) thiz->setStateInner(*state); +} + +void GameTextInput::processEvent(jobject textInputEvent) { + stateFromJava(textInputEvent, processCallback, this); + if (eventCallback_) { + eventCallback_(eventCallbackContext_, ¤tState_); + } +} + +void GameTextInput::showIme(uint32_t flags) { + if (inputConnection_ == nullptr) return; + env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, true, + flags); +} + +void GameTextInput::setEventCallback(GameTextInputEventCallback callback, + void *context) { + eventCallback_ = callback; + eventCallbackContext_ = context; +} + +void GameTextInput::setImeInsetsCallback( + GameTextInputImeInsetsCallback callback, void *context) { + insetsCallback_ = callback; + insetsCallbackContext_ = context; +} + +void GameTextInput::processImeInsets(const ARect *insets) { + currentInsets_ = *insets; + if (insetsCallback_) { + insetsCallback_(insetsCallbackContext_, ¤tInsets_); + } +} + +void GameTextInput::hideIme(uint32_t flags) { + if (inputConnection_ == nullptr) return; + env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, false, + flags); +} + +jobject GameTextInput::stateToJava(const GameTextInputState &state) const { + static jmethodID constructor = nullptr; + if (constructor == nullptr) { + constructor = env_->GetMethodID(stateJavaClass_, "", + "(Ljava/lang/String;IIII)V"); + if (constructor == nullptr) { + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, + "Can't find gametextinput.State constructor"); + return nullptr; + } + } + const char *text = state.text_UTF8; + if (text == nullptr) { + static char empty_string[] = ""; + text = empty_string; + } + // Note that this expects 'modified' UTF-8 which is not the same as UTF-8 + // https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8 + jstring jtext = env_->NewStringUTF(text); + jobject jobj = + env_->NewObject(stateJavaClass_, constructor, jtext, + state.selection.start, state.selection.end, + state.composingRegion.start, state.composingRegion.end); + env_->DeleteLocalRef(jtext); + return jobj; +} + +void GameTextInput::stateFromJava(jobject textInputEvent, + GameTextInputGetStateCallback callback, + void *context) const { + jstring text = + (jstring)env_->GetObjectField(textInputEvent, stateClassInfo_.text); + // Note this is 'modified' UTF-8, not true UTF-8. It has no NULLs in it, + // except at the end. It's actually not specified whether the value returned + // by GetStringUTFChars includes a null at the end, but it *seems to* on + // Android. + const char *text_chars = env_->GetStringUTFChars(text, NULL); + int text_len = env_->GetStringUTFLength( + text); // Length in bytes, *not* including the null. + int selectionStart = + env_->GetIntField(textInputEvent, stateClassInfo_.selectionStart); + int selectionEnd = + env_->GetIntField(textInputEvent, stateClassInfo_.selectionEnd); + int composingRegionStart = + env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionStart); + int composingRegionEnd = + env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionEnd); + GameTextInputState state{text_chars, + text_len, + {selectionStart, selectionEnd}, + {composingRegionStart, composingRegionEnd}}; + callback(context, &state); + env_->ReleaseStringUTFChars(text, text_chars); + env_->DeleteLocalRef(text); +} diff --git a/sources/android-gamesdk/GameActivity/game-activity/include/game-text-input/gametextinput.h b/sources/android-gamesdk/GameActivity/game-activity/include/game-text-input/gametextinput.h new file mode 100644 index 00000000..a85265ee --- /dev/null +++ b/sources/android-gamesdk/GameActivity/game-activity/include/game-text-input/gametextinput.h @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @defgroup game_text_input Game Text Input + * The interface to use GameTextInput. + * @{ + */ + +#pragma once + +#include +#include +#include + +#include "gamecommon.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * This struct holds a span within a region of text from start (inclusive) to + * end (exclusive). An empty span or cursor position is specified with + * start==end. An undefined span is specified with start = end = SPAN_UNDEFINED. + */ +typedef struct GameTextInputSpan { + /** The start of the region (inclusive). */ + int32_t start; + /** The end of the region (exclusive). */ + int32_t end; +} GameTextInputSpan; + +/** + * Values with special meaning in a GameTextInputSpan. + */ +enum GameTextInputSpanFlag { SPAN_UNDEFINED = -1 }; + +/** + * This struct holds the state of an editable section of text. + * The text can have a selection and a composing region defined on it. + * A composing region is used by IMEs that allow input using multiple steps to + * compose a glyph or word. Use functions GameTextInput_getState and + * GameTextInput_setState to read and modify the state that an IME is editing. + */ +typedef struct GameTextInputState { + /** + * Text owned by the state, as a modified UTF-8 string. Null-terminated. + * https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8 + */ + const char *text_UTF8; + /** + * Length in bytes of text_UTF8, *not* including the null at end. + */ + int32_t text_length; + /** + * A selection defined on the text. + */ + GameTextInputSpan selection; + /** + * A composing region defined on the text. + */ + GameTextInputSpan composingRegion; +} GameTextInputState; + +/** + * A callback called by GameTextInput_getState. + * @param context User-defined context. + * @param state State, owned by the library, that will be valid for the duration + * of the callback. + */ +typedef void (*GameTextInputGetStateCallback)( + void *context, const struct GameTextInputState *state); + +/** + * Opaque handle to the GameTextInput API. + */ +typedef struct GameTextInput GameTextInput; + +/** + * Initialize the GameTextInput library. + * If called twice without GameTextInput_destroy being called, the same pointer + * will be returned and a warning will be issued. + * @param env A JNI env valid on the calling thread. + * @param max_string_size The maximum length of a string that can be edited. If + * zero, the maximum defaults to 65536 bytes. A buffer of this size is allocated + * at initialization. + * @return A handle to the library. + */ +GameTextInput *GameTextInput_init(JNIEnv *env, uint32_t max_string_size); + +/** + * When using GameTextInput, you need to create a gametextinput.InputConnection + * on the Java side and pass it using this function to the library, unless using + * GameActivity in which case this will be done for you. See the GameActivity + * source code or GameTextInput samples for examples of usage. + * @param input A valid GameTextInput library handle. + * @param inputConnection A gametextinput.InputConnection object. + */ +void GameTextInput_setInputConnection(GameTextInput *input, + jobject inputConnection); + +/** + * Unless using GameActivity, it is required to call this function from your + * Java gametextinput.Listener.stateChanged method to convert eventState and + * trigger any event callbacks. When using GameActivity, this does not need to + * be called as event processing is handled by the Activity. + * @param input A valid GameTextInput library handle. + * @param eventState A Java gametextinput.State object. + */ +void GameTextInput_processEvent(GameTextInput *input, jobject eventState); + +/** + * Free any resources owned by the GameTextInput library. + * Any subsequent calls to the library will fail until GameTextInput_init is + * called again. + * @param input A valid GameTextInput library handle. + */ +void GameTextInput_destroy(GameTextInput *input); + +/** + * Flags to be passed to GameTextInput_showIme. + */ +enum ShowImeFlags { + SHOW_IME_UNDEFINED = 0, // Default value. + SHOW_IMPLICIT = + 1, // Indicates that the user has forced the input method open so it + // should not be closed until they explicitly do so. + SHOW_FORCED = 2 // Indicates that this is an implicit request to show the + // input window, not as the result of a direct request by + // the user. The window may not be shown in this case. +}; + +/** + * Show the IME. Calls InputMethodManager.showSoftInput(). + * @param input A valid GameTextInput library handle. + * @param flags Defined in ShowImeFlags above. For more information see: + * https://developer.android.com/reference/android/view/inputmethod/InputMethodManager + */ +void GameTextInput_showIme(GameTextInput *input, uint32_t flags); + +/** + * Flags to be passed to GameTextInput_hideIme. + */ +enum HideImeFlags { + HIDE_IME_UNDEFINED = 0, // Default value. + HIDE_IMPLICIT_ONLY = + 1, // Indicates that the soft input window should only be hidden if it + // was not explicitly shown by the user. + HIDE_NOT_ALWAYS = + 2, // Indicates that the soft input window should normally be hidden, + // unless it was originally shown with SHOW_FORCED. +}; + +/** + * Show the IME. Calls InputMethodManager.hideSoftInputFromWindow(). + * @param input A valid GameTextInput library handle. + * @param flags Defined in HideImeFlags above. For more information see: + * https://developer.android.com/reference/android/view/inputmethod/InputMethodManager + */ +void GameTextInput_hideIme(GameTextInput *input, uint32_t flags); + +/** + * Call a callback with the current GameTextInput state, which may have been + * modified by changes in the IME and calls to GameTextInput_setState. We use a + * callback rather than returning the state in order to simplify ownership of + * text_UTF8 strings. These strings are only valid during the calling of the + * callback. + * @param input A valid GameTextInput library handle. + * @param callback A function that will be called with valid state. + * @param context Context used by the callback. + */ +void GameTextInput_getState(GameTextInput *input, + GameTextInputGetStateCallback callback, + void *context); + +/** + * Set the current GameTextInput state. This state is reflected to any active + * IME. + * @param input A valid GameTextInput library handle. + * @param state The state to set. Ownership is maintained by the caller and must + * remain valid for the duration of the call. + */ +void GameTextInput_setState(GameTextInput *input, + const GameTextInputState *state); + +/** + * Type of the callback needed by GameTextInput_setEventCallback that will be + * called every time the IME state changes. + * @param context User-defined context set in GameTextInput_setEventCallback. + * @param current_state Current IME state, owned by the library and valid during + * the callback. + */ +typedef void (*GameTextInputEventCallback)( + void *context, const GameTextInputState *current_state); + +/** + * Optionally set a callback to be called whenever the IME state changes. + * Not necessary if you are using GameActivity, which handles these callbacks + * for you. + * @param input A valid GameTextInput library handle. + * @param callback Called by the library when the IME state changes. + * @param context Context passed as first argument to the callback. + */ +void GameTextInput_setEventCallback(GameTextInput *input, + GameTextInputEventCallback callback, + void *context); + +/** + * Type of the callback needed by GameTextInput_setImeInsetsCallback that will + * be called every time the IME window insets change. + * @param context User-defined context set in + * GameTextInput_setImeWIndowInsetsCallback. + * @param current_insets Current IME insets, owned by the library and valid + * during the callback. + */ +typedef void (*GameTextInputImeInsetsCallback)(void *context, + const ARect *current_insets); + +/** + * Optionally set a callback to be called whenever the IME insets change. + * Not necessary if you are using GameActivity, which handles these callbacks + * for you. + * @param input A valid GameTextInput library handle. + * @param callback Called by the library when the IME insets change. + * @param context Context passed as first argument to the callback. + */ +void GameTextInput_setImeInsetsCallback(GameTextInput *input, + GameTextInputImeInsetsCallback callback, + void *context); + +/** + * Get the current window insets for the IME. + * @param input A valid GameTextInput library handle. + * @param insets Filled with the current insets by this function. + */ +void GameTextInput_getImeInsets(const GameTextInput *input, ARect *insets); + +/** + * Unless using GameActivity, it is required to call this function from your + * Java gametextinput.Listener.onImeInsetsChanged method to + * trigger any event callbacks. When using GameActivity, this does not need to + * be called as insets processing is handled by the Activity. + * @param input A valid GameTextInput library handle. + * @param eventState A Java gametextinput.State object. + */ +void GameTextInput_processImeInsets(GameTextInput *input, const ARect *insets); + +/** + * Convert a GameTextInputState struct to a Java gametextinput.State object. + * Don't forget to delete the returned Java local ref when you're done. + * @param input A valid GameTextInput library handle. + * @param state Input state to convert. + * @return A Java object of class gametextinput.State. The caller is required to + * delete this local reference. + */ +jobject GameTextInputState_toJava(const GameTextInput *input, + const GameTextInputState *state); + +/** + * Convert from a Java gametextinput.State object into a C GameTextInputState + * struct. + * @param input A valid GameTextInput library handle. + * @param state A Java gametextinput.State object. + * @param callback A function called with the C struct, valid for the duration + * of the call. + * @param context Context passed to the callback. + */ +void GameTextInputState_fromJava(const GameTextInput *input, jobject state, + GameTextInputGetStateCallback callback, + void *context); + +#ifdef __cplusplus +} +#endif + +/** @} */ diff --git a/sources/android-gamesdk/GameActivity/game-activity/module.json b/sources/android-gamesdk/GameActivity/game-activity/module.json new file mode 100644 index 00000000..b571794d --- /dev/null +++ b/sources/android-gamesdk/GameActivity/game-activity/module.json @@ -0,0 +1 @@ +{"export_libraries":[],"library_name":null,"android":{"export_libraries":null,"library_name":null}} \ No newline at end of file diff --git a/sources/android-gamesdk/GameController/CMakeLists.txt b/sources/android-gamesdk/GameController/CMakeLists.txt new file mode 100644 index 00000000..ef09d633 --- /dev/null +++ b/sources/android-gamesdk/GameController/CMakeLists.txt @@ -0,0 +1,44 @@ +# +# Copyright (C) 2021 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +project(paddleboat C CXX) + +include_directories(${CMAKE_CURRENT_LIST_DIR}/paddleboat/include) + +set( PADDLEBOAT_SRCS + ${CMAKE_CURRENT_LIST_DIR}/InternalControllerTable.cpp + ${CMAKE_CURRENT_LIST_DIR}/GameController.cpp + ${CMAKE_CURRENT_LIST_DIR}/GameControllerDeviceInfo.cpp + ${CMAKE_CURRENT_LIST_DIR}/GameControllerLog.cpp + ${CMAKE_CURRENT_LIST_DIR}/GameControllerManager.cpp + ${CMAKE_CURRENT_LIST_DIR}/GameControllerMappingUtils.cpp + ${CMAKE_CURRENT_LIST_DIR}/paddleboat_c.cpp) + +# set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror -Os") +# set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions -fno-rtti") +# set( CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -g0") + +add_library(paddleboat_static STATIC ${PADDLEBOAT_SRCS}) + +set_target_properties( paddleboat_static PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/build ) + +add_library(paddleboat SHARED ${CMAKE_CURRENT_LIST_DIR}/paddleboat_c.cpp) + +target_link_libraries(paddleboat + paddleboat_static + android + atomic + log) diff --git a/sources/android-gamesdk/GameController/GameController.cpp b/sources/android-gamesdk/GameController/GameController.cpp new file mode 100644 index 00000000..a0909f2d --- /dev/null +++ b/sources/android-gamesdk/GameController/GameController.cpp @@ -0,0 +1,458 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "GameController.h" + +#include +#include + +#include +#include +#include + +#include "GameControllerInternalConstants.h" + +namespace paddleboat { +struct ControllerButtonMap { + int32_t buttonKeycodes[PADDLEBOAT_BUTTON_COUNT]; +}; + +const ControllerButtonMap defaultButtonMap = {{ + AKEYCODE_DPAD_UP, // PADDLEBOAT_BUTTON_DPAD_UP + AKEYCODE_DPAD_LEFT, // PADDLEBOAT_BUTTON_DPAD_LEFT + AKEYCODE_DPAD_DOWN, // PADDLEBOAT_BUTTON_DPAD_DOWN + AKEYCODE_DPAD_RIGHT, // PADDLEBOAT_BUTTON_DPAD_RIGHT + AKEYCODE_BUTTON_A, // PADDLEBOAT_BUTTON_A + AKEYCODE_BUTTON_B, // PADDLEBOAT_BUTTON_B + AKEYCODE_BUTTON_X, // PADDLEBOAT_BUTTON_X + AKEYCODE_BUTTON_Y, // PADDLEBOAT_BUTTON_Y + AKEYCODE_BUTTON_L1, // PADDLEBOAT_BUTTON_L1 + AKEYCODE_BUTTON_L2, // PADDLEBOAT_BUTTON_L2 + AKEYCODE_BUTTON_THUMBL, // PADDLEBOAT_BUTTON_L3 + AKEYCODE_BUTTON_R1, // PADDLEBOAT_BUTTON_R1 + AKEYCODE_BUTTON_R2, // PADDLEBOAT_BUTTON_R2 + AKEYCODE_BUTTON_THUMBR, // PADDLEBOAT_BUTTON_R3 + AKEYCODE_BUTTON_SELECT, // PADDLEBOAT_BUTTON_SELECT + AKEYCODE_BUTTON_START, // PADDLEBOAT_BUTTON_START + AKEYCODE_BUTTON_MODE, // PADDLEBOAT_BUTTON_SYSTEM + 0, // PADDLEBOAT_BUTTON_TOUCHPAD + 0, // PADDLEBOAT_BUTTON_AUX1 + 0, // PADDLEBOAT_BUTTON_AUX2 + 0, // PADDLEBOAT_BUTTON_AUX3 + 0 // PADDLEBOAT_BUTTON_AUX4 +}}; + +// Axis must be at least this value to trigger a mapped button press +constexpr float AXIS_BUTTON_THRESHOLD = 0.1f; + +void resetData(Paddleboat_Controller_Data &pbData) { + pbData.timestamp = 0; + pbData.buttonsDown = 0; + pbData.leftStick.stickX = 0.0f; + pbData.leftStick.stickY = 0.0f; + pbData.rightStick.stickX = 0.0f; + pbData.rightStick.stickY = 0.0f; + pbData.triggerL1 = 0.0f; + pbData.triggerL2 = 0.0f; + pbData.triggerR1 = 0.0f; + pbData.triggerR2 = 0.0f; + pbData.virtualPointer.pointerX = 0.0f; + pbData.virtualPointer.pointerY = 0.0f; +} + +void resetInfo(Paddleboat_Controller_Info &pbInfo) { + pbInfo.controllerNumber = -1; + pbInfo.controllerFlags = 0; + pbInfo.vendorId = 0; + pbInfo.productId = 0; + pbInfo.deviceId = -1; + pbInfo.leftStickPrecision.stickFlatX = 0.0f; + pbInfo.leftStickPrecision.stickFlatY = 0.0f; + pbInfo.leftStickPrecision.stickFuzzX = 0.0f; + pbInfo.leftStickPrecision.stickFuzzY = 0.0f; + pbInfo.rightStickPrecision.stickFlatX = 0.0f; + pbInfo.rightStickPrecision.stickFlatY = 0.0f; + pbInfo.rightStickPrecision.stickFuzzX = 0.0f; + pbInfo.rightStickPrecision.stickFuzzY = 0.0f; +} + +GameController::GameController() + : mConnectionIndex(-1), + mControllerData(), + mControllerInfo(), + mAxisInfo(), + mDeviceInfo(), + mControllerDataDirty(true) { + memset(mButtonKeycodes, 0, sizeof(mButtonKeycodes)); + resetControllerData(); +} + +void GameController::resetControllerData() { + resetData(mControllerData); + resetInfo(mControllerInfo); +} + +void GameController::setupController( + const Paddleboat_Controller_Mapping_Data *mappingData) { + const GameControllerDeviceInfo::InfoFields &infoFields = + *(mDeviceInfo.getInfo()); + uint64_t axisLow = static_cast(infoFields.mAxisBitsLow); + uint64_t axisHigh = static_cast(infoFields.mAxisBitsHigh); + mControllerAxisMask = axisLow | (axisHigh << 32ULL); + mControllerInfo.controllerFlags = infoFields.mControllerFlags; + mControllerInfo.controllerNumber = infoFields.mControllerNumber; + mControllerInfo.deviceId = infoFields.mDeviceId; + mControllerInfo.productId = infoFields.mProductId; + mControllerInfo.vendorId = infoFields.mVendorId; + + if (mappingData != nullptr) { + mControllerInfo.controllerFlags |= mappingData->flags; + for (int32_t i = 0; i < PADDLEBOAT_BUTTON_COUNT; ++i) { + if (mappingData->buttonMapping[i] != PADDLEBOAT_BUTTON_IGNORED) { + mButtonKeycodes[i] = mappingData->buttonMapping[i]; + } + } + + for (int32_t i = 0; i < PADDLEBOAT_MAPPING_AXIS_COUNT; ++i) { + if (mappingData->axisMapping[i] != PADDLEBOAT_AXIS_IGNORED) { + const GameControllerAxis gcAxis = + static_cast(i); + const int32_t nativeAxisId = mappingData->axisMapping[i]; + const int32_t positiveButton = + (mappingData->axisPositiveButtonMapping[i] == + PADDLEBOAT_AXIS_BUTTON_IGNORED) + ? 0 + : (1 << mappingData->axisPositiveButtonMapping[i]); + const int32_t negativeButton = + (mappingData->axisNegativeButtonMapping[i] == + PADDLEBOAT_AXIS_BUTTON_IGNORED) + ? 0 + : (1 << mappingData->axisNegativeButtonMapping[i]); + setupAxis(gcAxis, nativeAxisId, nativeAxisId, positiveButton, + negativeButton); + } + } + adjustAxisConstants(); + } else { + // Fallback defaults if there wasn't mapping data provided + initializeDefaultAxisMapping(); + memcpy(mButtonKeycodes, &defaultButtonMap, sizeof(ControllerButtonMap)); + mControllerInfo.controllerFlags |= + PADDLEBOAT_CONTROLLER_FLAG_GENERIC_PROFILE; + } +} + +void GameController::initializeDefaultAxisMapping() { + setupAxis(GAMECONTROLLER_AXIS_LSTICK_X, AMOTION_EVENT_AXIS_X, + AMOTION_EVENT_AXIS_X, 0, 0); + setupAxis(GAMECONTROLLER_AXIS_LSTICK_Y, AMOTION_EVENT_AXIS_Y, + AMOTION_EVENT_AXIS_Y, 0, 0); + setupAxis(GAMECONTROLLER_AXIS_RSTICK_X, AMOTION_EVENT_AXIS_Z, + AMOTION_EVENT_AXIS_RX, 0, 0); + setupAxis(GAMECONTROLLER_AXIS_RSTICK_Y, AMOTION_EVENT_AXIS_RZ, + AMOTION_EVENT_AXIS_RY, 0, 0); + setupAxis(GAMECONTROLLER_AXIS_L2, AMOTION_EVENT_AXIS_LTRIGGER, + AMOTION_EVENT_AXIS_BRAKE, PADDLEBOAT_BUTTON_L2, 0); + setupAxis(GAMECONTROLLER_AXIS_R2, AMOTION_EVENT_AXIS_RTRIGGER, + AMOTION_EVENT_AXIS_GAS, PADDLEBOAT_BUTTON_R2, 0); + setupAxis(GAMECONTROLLER_AXIS_HAT_X, AMOTION_EVENT_AXIS_HAT_X, + AMOTION_EVENT_AXIS_HAT_X, PADDLEBOAT_BUTTON_DPAD_RIGHT, + PADDLEBOAT_BUTTON_DPAD_LEFT); + setupAxis(GAMECONTROLLER_AXIS_HAT_Y, AMOTION_EVENT_AXIS_HAT_Y, + AMOTION_EVENT_AXIS_HAT_Y, PADDLEBOAT_BUTTON_DPAD_DOWN, + PADDLEBOAT_BUTTON_DPAD_UP); + adjustAxisConstants(); +} + +void GameController::adjustAxisConstants() { + if (mAxisInfo[GAMECONTROLLER_AXIS_LSTICK_X].axisIndex >= 0) { + const bool stickAxisAdjust = + ((mAxisInfo[GAMECONTROLLER_AXIS_LSTICK_X].axisFlags & + GAMECONTROLLER_AXIS_FLAG_APPLY_ADJUSTMENTS) != 0); + + mControllerInfo.leftStickPrecision.stickFlatX = + mDeviceInfo.getFlatArray()[mAxisInfo[GAMECONTROLLER_AXIS_LSTICK_X] + .axisIndex]; + mControllerInfo.leftStickPrecision.stickFlatY = + mDeviceInfo.getFlatArray()[mAxisInfo[GAMECONTROLLER_AXIS_LSTICK_Y] + .axisIndex]; + mControllerInfo.leftStickPrecision.stickFuzzX = + mDeviceInfo.getFuzzArray()[mAxisInfo[GAMECONTROLLER_AXIS_LSTICK_X] + .axisIndex]; + mControllerInfo.leftStickPrecision.stickFuzzY = + mDeviceInfo.getFuzzArray()[mAxisInfo[GAMECONTROLLER_AXIS_LSTICK_Y] + .axisIndex]; + if (stickAxisAdjust) { + // We are adjusting the raw axis values, so we also adjust the + // 'flat' and 'fuzz' values for the sticks + mControllerInfo.leftStickPrecision.stickFlatX *= + mAxisInfo[GAMECONTROLLER_AXIS_LSTICK_X].axisMultiplier; + mControllerInfo.leftStickPrecision.stickFlatY *= + mAxisInfo[GAMECONTROLLER_AXIS_LSTICK_Y].axisMultiplier; + mControllerInfo.leftStickPrecision.stickFuzzX *= + mAxisInfo[GAMECONTROLLER_AXIS_LSTICK_X].axisMultiplier; + mControllerInfo.leftStickPrecision.stickFuzzY *= + mAxisInfo[GAMECONTROLLER_AXIS_LSTICK_Y].axisMultiplier; + } + } + + if (mAxisInfo[GAMECONTROLLER_AXIS_RSTICK_X].axisIndex >= 0) { + const bool stickAxisAdjust = + ((mAxisInfo[GAMECONTROLLER_AXIS_RSTICK_X].axisFlags & + GAMECONTROLLER_AXIS_FLAG_APPLY_ADJUSTMENTS) != 0); + + mControllerInfo.rightStickPrecision.stickFlatX = + mDeviceInfo.getFlatArray()[mAxisInfo[GAMECONTROLLER_AXIS_RSTICK_X] + .axisIndex]; + mControllerInfo.rightStickPrecision.stickFlatY = + mDeviceInfo.getFlatArray()[mAxisInfo[GAMECONTROLLER_AXIS_RSTICK_Y] + .axisIndex]; + mControllerInfo.rightStickPrecision.stickFuzzX = + mDeviceInfo.getFuzzArray()[mAxisInfo[GAMECONTROLLER_AXIS_RSTICK_X] + .axisIndex]; + mControllerInfo.rightStickPrecision.stickFuzzY = + mDeviceInfo.getFuzzArray()[mAxisInfo[GAMECONTROLLER_AXIS_RSTICK_Y] + .axisIndex]; + if (stickAxisAdjust) { + // We are adjusting the raw axis values, so we also adjust the + // 'flat' and 'fuzz' values for the sticks + mControllerInfo.rightStickPrecision.stickFlatX *= + mAxisInfo[GAMECONTROLLER_AXIS_RSTICK_X].axisMultiplier; + mControllerInfo.rightStickPrecision.stickFlatY *= + mAxisInfo[GAMECONTROLLER_AXIS_RSTICK_Y].axisMultiplier; + mControllerInfo.rightStickPrecision.stickFuzzX *= + mAxisInfo[GAMECONTROLLER_AXIS_RSTICK_X].axisMultiplier; + mControllerInfo.rightStickPrecision.stickFuzzY *= + mAxisInfo[GAMECONTROLLER_AXIS_RSTICK_Y].axisMultiplier; + } + } +} + +void GameController::setupAxis(const GameControllerAxis gcAxis, + const int32_t preferredNativeAxisId, + const int32_t secondaryNativeAxisId, + const int32_t buttonMask, + const int32_t buttonNegativeMask) { + mAxisInfo[gcAxis].resetInfo(); + mAxisInfo[gcAxis].axisButtonMask = buttonMask; + mAxisInfo[gcAxis].axisButtonNegativeMask = buttonNegativeMask; + + // Do we have a mapping for the preferred native axis? + if (preferredNativeAxisId < MAX_AXIS_COUNT && + secondaryNativeAxisId < MAX_AXIS_COUNT) { + const int32_t preferredMask = (1U << preferredNativeAxisId); + const int32_t secondaryMask = (1U << secondaryNativeAxisId); + if ((mControllerAxisMask & preferredMask) != 0) { + // preferred axis is mapped + mAxisInfo[gcAxis].axisIndex = preferredNativeAxisId; + } else if ((preferredNativeAxisId != secondaryNativeAxisId) && + ((mControllerAxisMask & secondaryMask) != 0)) { + // secondary axis is mapped + mAxisInfo[gcAxis].axisIndex = secondaryNativeAxisId; + } else if (buttonMask) { + // There wasn't a matching axis, + // but we will fake this axis using a digital button mapping + mAxisInfo[gcAxis].axisFlags |= + GAMECONTROLLER_AXIS_FLAG_DIGITAL_TRIGGER; + } + + // If we found a native axis, check its ranges and see if we need to set + // up adjustment values if the ranges aren't -1.0 to 1.0 for a stick or + // 0.0 to 1.0 for a trigger + if (mAxisInfo[gcAxis].axisIndex >= 0) { + bool isStickAxis = (!(gcAxis >= GAMECONTROLLER_AXIS_L1 && + gcAxis <= GAMECONTROLLER_AXIS_R2)); + const float minAdjust = isStickAxis ? 1.0f : 0.0f; + const float rawMin = + mDeviceInfo.getMinArray()[mAxisInfo[gcAxis].axisIndex]; + const float rawMax = + mDeviceInfo.getMaxArray()[mAxisInfo[gcAxis].axisIndex]; + const float diffMin = fabsf(rawMin + minAdjust); + const float diffMax = fabsf(1.0f - rawMax); + if (!(diffMin <= FLT_MIN && diffMax <= FLT_MIN)) { + mAxisInfo[gcAxis].axisFlags |= + GAMECONTROLLER_AXIS_FLAG_APPLY_ADJUSTMENTS; + if (isStickAxis) { + // normalize min and max axis to 1.0 + mAxisInfo[gcAxis].axisMultiplier = 1.0f / rawMax; + // Make sure center of stick is 0.0 + const float rawCenter = ((rawMax - rawMin) * 0.5f) + rawMin; + if (rawCenter >= FLT_MIN) { + mAxisInfo[gcAxis].axisAdjust = + -(rawCenter * mAxisInfo[gcAxis].axisMultiplier); + } + } else { + // This case is hit on PS5 API <= 30 having weird trigger + // axis mappings of: L2=RX R2=RY with a -1.0 to 1.0 range + mAxisInfo[gcAxis].axisMultiplier = 1.0f / (rawMax - rawMin); + mAxisInfo[gcAxis].axisAdjust = + (-rawMin) * mAxisInfo[gcAxis].axisMultiplier; + } + } + } + } +} + +int32_t GameController::processGameActivityKeyEvent( + const Paddleboat_GameActivityKeyEvent *event, const size_t eventSize) { + return processKeyEventInternal(event->keyCode, event->action); +} + +int32_t GameController::processGameActivityMotionEvent( + const Paddleboat_GameActivityMotionEvent *event, const size_t eventSize) { + int32_t handledEvent = IGNORED_EVENT; + if (event->pointerCount > 0) { + handledEvent = + processMotionEventInternal(event->pointers->axisValues, nullptr); + } + return handledEvent; +} + +int32_t GameController::processKeyEvent(const AInputEvent *event) { + const int32_t eventKeyCode = AKeyEvent_getKeyCode(event); + const int32_t eventKeyAction = AKeyEvent_getAction(event); + return processKeyEventInternal(eventKeyCode, eventKeyAction); +} + +int32_t GameController::processKeyEventInternal(const int32_t eventKeyCode, + const int32_t eventKeyAction) { + int32_t handledEvent = IGNORED_EVENT; + int32_t buttonMask = 0; + const bool bDown = (eventKeyAction == AKEY_EVENT_ACTION_DOWN); + + for (uint32_t i = 0; i < PADDLEBOAT_BUTTON_COUNT; ++i) { + if (eventKeyCode == mButtonKeycodes[i]) { + int32_t newButtonFlag = (1 << i); + buttonMask |= newButtonFlag; + + // If we have no analog axis mapped for a trigger, allow the + // button events to set the analog trigger data + switch (newButtonFlag) { + case PADDLEBOAT_BUTTON_L1: + if (mAxisInfo[GAMECONTROLLER_AXIS_L1].axisIndex == -1) { + mControllerData.triggerL1 = bDown ? 1.0f : 0.0f; + } + break; + case PADDLEBOAT_BUTTON_L2: + if (mAxisInfo[GAMECONTROLLER_AXIS_L2].axisIndex == -1) { + mControllerData.triggerL2 = bDown ? 1.0f : 0.0f; + } + break; + case PADDLEBOAT_BUTTON_R1: + if (mAxisInfo[GAMECONTROLLER_AXIS_R1].axisIndex == -1) { + mControllerData.triggerR1 = bDown ? 1.0f : 0.0f; + } + break; + case PADDLEBOAT_BUTTON_R2: + if (mAxisInfo[GAMECONTROLLER_AXIS_R2].axisIndex == -1) { + mControllerData.triggerR2 = bDown ? 1.0f : 0.0f; + } + break; + default: + break; + } + + break; + } + } + + if (buttonMask != 0) { + if (bDown) { + mControllerData.buttonsDown |= buttonMask; + setControllerDataDirty(true); + } else { + mControllerData.buttonsDown &= (~buttonMask); + setControllerDataDirty(true); + } + handledEvent = HANDLED_EVENT; + } + return handledEvent; +} + +int32_t GameController::processMotionEvent(const AInputEvent *event) { + return processMotionEventInternal(nullptr, event); +} + +int32_t GameController::processMotionEventInternal(const float *axisArray, + const AInputEvent *event) { + int32_t handledEvent = IGNORED_EVENT; + + for (uint32_t axis = GAMECONTROLLER_AXIS_LSTICK_X; + axis < GAMECONTROLLER_AXIS_COUNT; ++axis) { + if (mAxisInfo[axis].axisIndex >= 0 && + mAxisInfo[axis].axisIndex < + PADDLEBOAT_GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) { + float axisValue = 0.0f; + if (axisArray != nullptr) { + axisValue = axisArray[mAxisInfo[axis].axisIndex]; + } else if (event != nullptr) { + axisValue = AMotionEvent_getAxisValue( + event, mAxisInfo[axis].axisIndex, 0); + } + if ((mAxisInfo[axis].axisFlags & + GAMECONTROLLER_AXIS_FLAG_APPLY_ADJUSTMENTS) != 0) { + axisValue = ((axisValue * mAxisInfo[axis].axisMultiplier) + + mAxisInfo[axis].axisAdjust); + } + + // We take advantage of the GameControllerAxis matching the axis + // order in the Paddleboat_Controller_Data struct to use the current + // axis as an index into the axis entries in the + // Paddleboat_Controller_Data struct + if (axis < GAMECONTROLLER_AXIS_HAT_X) { + float *axisData = &mControllerData.leftStick.stickX; + axisData[axis] = axisValue; + } + + // If this axis has a button associated with it, set/clear the flags + // as appropriate + if (mAxisInfo[axis].axisButtonMask != 0 || + mAxisInfo[axis].axisButtonNegativeMask) { + if (axisValue > -AXIS_BUTTON_THRESHOLD && + axisValue < AXIS_BUTTON_THRESHOLD) { + const uint32_t buttonMask = + mAxisInfo[axis].axisButtonMask | + mAxisInfo[axis].axisButtonNegativeMask; + mControllerData.buttonsDown &= (~buttonMask); + } else if (axisValue > AXIS_BUTTON_THRESHOLD) { + mControllerData.buttonsDown |= + mAxisInfo[axis].axisButtonMask; + } else if (axisValue < -AXIS_BUTTON_THRESHOLD) { + mControllerData.buttonsDown |= + mAxisInfo[axis].axisButtonNegativeMask; + } + } + + setControllerDataDirty(true); + handledEvent = HANDLED_EVENT; + } + } + return handledEvent; +} + +void GameController::setControllerDataDirty(const bool dirty) { + mControllerDataDirty = dirty; + if (dirty) { + // update the timestamp any time we mark dirty + const auto timestamp = + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); + mControllerData.timestamp = static_cast(timestamp); + } +} + +} // namespace paddleboat \ No newline at end of file diff --git a/sources/android-gamesdk/GameController/GameController.h b/sources/android-gamesdk/GameController/GameController.h new file mode 100644 index 00000000..eeb03c65 --- /dev/null +++ b/sources/android-gamesdk/GameController/GameController.h @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "GameControllerDeviceInfo.h" +#include "GameControllerGameActivityMirror.h" +#include "paddleboat.h" + +namespace paddleboat { + +// set if axisMultiplier/axisAdjustment should be applied (raw device axis isn't +// -1.0 to 1.0) +inline constexpr uint32_t GAMECONTROLLER_AXIS_FLAG_APPLY_ADJUSTMENTS = + (1U << 0); +// set if trigger is being faked as an analog axis (device just has a on/off +// button flag) +inline constexpr uint32_t GAMECONTROLLER_AXIS_FLAG_DIGITAL_TRIGGER = (1U << 1); + +class GameController { + public: + enum GameControllerAxis { + GAMECONTROLLER_AXIS_LSTICK_X = 0, + GAMECONTROLLER_AXIS_LSTICK_Y, + GAMECONTROLLER_AXIS_RSTICK_X, + GAMECONTROLLER_AXIS_RSTICK_Y, + GAMECONTROLLER_AXIS_L1, + GAMECONTROLLER_AXIS_L2, + GAMECONTROLLER_AXIS_R1, + GAMECONTROLLER_AXIS_R2, + GAMECONTROLLER_AXIS_HAT_X, + GAMECONTROLLER_AXIS_HAT_Y, + GAMECONTROLLER_AXIS_COUNT + }; + + struct GameControllerAxisInfo { + // Index into the device axis array, -1 is unmapped + int32_t axisIndex = -1; + // See GAMECONTROLLER_AXIS_FLAG constants + uint32_t axisFlags = 0; + // Button mask flag, if backed/shadowed by digital button when axis > + // 0.0 + uint32_t axisButtonMask = 0; + // Button mask flag, if backed/shadowed by digital button when axis < + // 0.0 + uint32_t axisButtonNegativeMask = 0; + // Multiplier to normalize to a 0.0 center -> 1.0 edge + float axisMultiplier = 1.0f; + // Adjustment to bring center to 0.0 if necessary + float axisAdjust = 0.0f; + + void resetInfo() { + axisIndex = -1; + axisFlags = 0; + axisButtonMask = 0; + axisButtonNegativeMask = 0; + axisMultiplier = 1.0f; + axisAdjust = 0.0f; + } + }; + + GameController(); + + void setupController(const Paddleboat_Controller_Mapping_Data *mappingData); + + void initializeDefaultAxisMapping(); + + int32_t processGameActivityKeyEvent( + const Paddleboat_GameActivityKeyEvent *event, const size_t eventSize); + + int32_t processGameActivityMotionEvent( + const Paddleboat_GameActivityMotionEvent *event, + const size_t eventSize); + + int32_t processKeyEvent(const AInputEvent *event); + + int32_t processMotionEvent(const AInputEvent *event); + + Paddleboat_ControllerStatus getControllerStatus() const { + return mControllerStatus; + } + + void setControllerStatus( + const Paddleboat_ControllerStatus controllerStatus) { + mControllerStatus = controllerStatus; + } + + int32_t getConnectionIndex() const { return mConnectionIndex; } + + void setConnectionIndex(const int32_t connectionIndex) { + mConnectionIndex = connectionIndex; + } + + uint64_t getControllerAxisMask() const { return mControllerAxisMask; } + + Paddleboat_Controller_Data &getControllerData() { return mControllerData; } + + const Paddleboat_Controller_Data &getControllerData() const { + return mControllerData; + } + + Paddleboat_Controller_Info &getControllerInfo() { return mControllerInfo; } + + const Paddleboat_Controller_Info &getControllerInfo() const { + return mControllerInfo; + } + + GameControllerDeviceInfo &getDeviceInfo() { return mDeviceInfo; } + + const GameControllerDeviceInfo &getDeviceInfo() const { + return mDeviceInfo; + } + + GameControllerAxisInfo *getAxisInfo() { return mAxisInfo; } + + const GameControllerAxisInfo *getAxisInfo() const { return mAxisInfo; } + + bool getControllerDataDirty() const { return mControllerDataDirty; } + + void setControllerDataDirty(const bool dirty); + + void resetControllerData(); + + private: + int32_t processKeyEventInternal(const int32_t eventKeyCode, + const int32_t eventKeyAction); + + int32_t processMotionEventInternal(const float *axisArray, + const AInputEvent *event); + + void setupAxis(const GameControllerAxis gcAxis, + const int32_t preferredNativeAxisId, + const int32_t secondaryNativeAxisId, + const int32_t buttonMask, const int32_t buttonNegativeMask); + + void adjustAxisConstants(); + + uint64_t mControllerAxisMask = 0; + Paddleboat_ControllerStatus mControllerStatus = + PADDLEBOAT_CONTROLLER_INACTIVE; + int32_t mConnectionIndex = -1; + Paddleboat_Controller_Data mControllerData; + Paddleboat_Controller_Info mControllerInfo; + int32_t mButtonKeycodes[PADDLEBOAT_BUTTON_COUNT]; + GameControllerAxisInfo mAxisInfo[GAMECONTROLLER_AXIS_COUNT]; + GameControllerDeviceInfo mDeviceInfo; + // Controller data has been updated since the last time it was read + bool mControllerDataDirty; +}; +} // namespace paddleboat diff --git a/sources/android-gamesdk/GameController/GameControllerDeviceInfo.cpp b/sources/android-gamesdk/GameController/GameControllerDeviceInfo.cpp new file mode 100644 index 00000000..571572c3 --- /dev/null +++ b/sources/android-gamesdk/GameController/GameControllerDeviceInfo.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "GameControllerDeviceInfo.h" + +#include + +namespace paddleboat { +GameControllerDeviceInfo::GameControllerDeviceInfo() { + mName[0] = '\0'; + mInfo.mDeviceId = -1; + mInfo.mProductId = -1; + mInfo.mVendorId = -1; + mInfo.mAxisBitsLow = 0; + mInfo.mAxisBitsHigh = 0; + mInfo.mControllerNumber = -1; + mInfo.mControllerFlags = 0; + + for (size_t i = 0; i < paddleboat::MAX_AXIS_COUNT; ++i) { + mAxisMinArray[i] = 0.0f; + mAxisMaxArray[i] = 0.0f; + mAxisFlatArray[i] = 0.0f; + mAxisFuzzArray[i] = 0.0f; + } +} + +void GameControllerDeviceInfo::setName(const char *name) { + strncpy(mName, name, DEVICEINFO_MAX_NAME_LENGTH); +} +} // namespace paddleboat \ No newline at end of file diff --git a/sources/android-gamesdk/GameController/GameControllerDeviceInfo.h b/sources/android-gamesdk/GameController/GameControllerDeviceInfo.h new file mode 100644 index 00000000..d9fd456a --- /dev/null +++ b/sources/android-gamesdk/GameController/GameControllerDeviceInfo.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "GameControllerInternalConstants.h" + +namespace paddleboat { + +class GameControllerDeviceInfo { + public: + // These are copied directly from the int[] mGameControllerDeviceInfoArray, + // the layout should match the field definitions in + // Paddleboat_Controller_Info.java + struct InfoFields { + int32_t mDeviceId; + int32_t mVendorId; + int32_t mProductId; + int32_t mAxisBitsLow; + int32_t mAxisBitsHigh; + int32_t mControllerNumber; + int32_t mControllerFlags; + }; + + GameControllerDeviceInfo(); + + void setName(const char *name); + + const char *getName() const { return mName; } + + const InfoFields &getInfo() const { return mInfo; } + + const float *getMinArray() const { return mAxisMinArray; } + + const float *getMaxArray() const { return mAxisMaxArray; } + + const float *getFlatArray() const { return mAxisFlatArray; } + + const float *getFuzzArray() const { return mAxisFuzzArray; } + + InfoFields *getInfo() { return &mInfo; } + + float *getMinArray() { return mAxisMinArray; } + + float *getMaxArray() { return mAxisMaxArray; } + + float *getFlatArray() { return mAxisFlatArray; } + + float *getFuzzArray() { return mAxisFuzzArray; } + + private: + char mName[paddleboat::DEVICEINFO_MAX_NAME_LENGTH]; + InfoFields mInfo; + float mAxisMinArray[paddleboat::MAX_AXIS_COUNT]; + float mAxisMaxArray[paddleboat::MAX_AXIS_COUNT]; + float mAxisFlatArray[paddleboat::MAX_AXIS_COUNT]; + float mAxisFuzzArray[paddleboat::MAX_AXIS_COUNT]; +}; +} // namespace paddleboat diff --git a/sources/android-gamesdk/GameController/GameControllerGameActivityMirror.h b/sources/android-gamesdk/GameController/GameControllerGameActivityMirror.h new file mode 100644 index 00000000..74d3f0f3 --- /dev/null +++ b/sources/android-gamesdk/GameController/GameControllerGameActivityMirror.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace paddleboat { +// GameActivity needs to use its own event data structures for key and motion +// events. We do not want to have a dependency on GameActivity. Unfortunately, +// this means we need to internally mirror the relevant structures. +// Paddleboat_processGameActivityInputEvent includes a struct size parameter, +// and we know that the GameActivity structures will only ever add fields, so we +// can determine if we are being passed a later version of the struct than the +// mirrored internal version. + +// The following should mirror GameActivity.h +#define PADDLEBOAT_GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT 48 +#define PADDLEBOAT_MAX_NUM_POINTERS_IN_MOTION_EVENT 8 + +typedef struct Paddleboat_GameActivityPointerInfo { + int32_t id; + float axisValues[PADDLEBOAT_GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT]; + float rawX; + float rawY; +} Paddleboat_GameActivityPointerInfo; + +typedef struct Paddleboat_GameActivityMotionEvent { + int32_t deviceId; + int32_t source; + int32_t action; + int64_t eventTime; + int64_t downTime; + int32_t flags; + int32_t metaState; + int32_t actionButton; + int32_t buttonState; + int32_t classification; + int32_t edgeFlags; + uint32_t pointerCount; + Paddleboat_GameActivityPointerInfo + pointers[PADDLEBOAT_MAX_NUM_POINTERS_IN_MOTION_EVENT]; + float precisionX; + float precisionY; +} Paddleboat_GameActivityMotionEvent; + +typedef struct Paddleboat_GameActivityKeyEvent { + int32_t deviceId; + int32_t source; + int32_t action; + int64_t eventTime; + int64_t downTime; + int32_t flags; + int32_t metaState; + int32_t modifiers; + int32_t repeatCount; + int32_t keyCode; +} Paddleboat_GameActivityKeyEvent; +} // namespace paddleboat diff --git a/sources/android-gamesdk/GameController/GameControllerInternalConstants.h b/sources/android-gamesdk/GameController/GameControllerInternalConstants.h new file mode 100644 index 00000000..d943947b --- /dev/null +++ b/sources/android-gamesdk/GameController/GameControllerInternalConstants.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace paddleboat { +inline constexpr size_t MAX_AXIS_COUNT = 48; +// Must match GameControllerManager.DEVICEINFO_ARRAY_SIZE +inline constexpr size_t DEVICEINFO_ARRAY_SIZE = 7; +inline constexpr size_t DEVICEINFO_ARRAY_BYTESIZE = + sizeof(int32_t) * DEVICEINFO_ARRAY_SIZE; +inline constexpr size_t DEVICEINFO_MAX_NAME_LENGTH = 128; + +inline constexpr int32_t IGNORED_EVENT = 0; +inline constexpr int32_t HANDLED_EVENT = 1; +} // namespace paddleboat diff --git a/sources/android-gamesdk/GameController/GameControllerLog.cpp b/sources/android-gamesdk/GameController/GameControllerLog.cpp new file mode 100644 index 00000000..396759b9 --- /dev/null +++ b/sources/android-gamesdk/GameController/GameControllerLog.cpp @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "GameControllerLog.h" + +#include + +#include "GameControllerLogStrings.h" +#include "Log.h" + +#define LOG_TAG "GameControllerManager" +// Filter input event logging to qualifying 'gamecontroller' event sources +#define LOG_FILTER_PADDLEBOAT_SOURCES + +#define ELEMENTS_OF(x) (sizeof(x) / sizeof(x[0])) + +namespace paddleboat { +const char *LogGetInputSourceString(const int32_t eventSource) { + const char *inputSourceString = "AINPUT_SOURCE_UNKNOWN"; + + switch (eventSource) { + case AINPUT_SOURCE_KEYBOARD: + inputSourceString = "AINPUT_SOURCE_KEYBOARD"; + break; + case AINPUT_SOURCE_DPAD: + inputSourceString = "AINPUT_SOURCE_DPAD"; + break; + case AINPUT_SOURCE_GAMEPAD: + inputSourceString = "AINPUT_SOURCE_GAMEPAD"; + break; + case AINPUT_SOURCE_TOUCHSCREEN: + inputSourceString = "AINPUT_SOURCE_TOUCHSCREEN"; + break; + case AINPUT_SOURCE_MOUSE: + inputSourceString = "AINPUT_SOURCE_MOUSE"; + break; + case AINPUT_SOURCE_STYLUS: + inputSourceString = "AINPUT_SOURCE_STYLUS"; + break; + case AINPUT_SOURCE_BLUETOOTH_STYLUS: + inputSourceString = "AINPUT_SOURCE_BLUETOOTH_STYLUS"; + break; + case AINPUT_SOURCE_MOUSE_RELATIVE: + inputSourceString = "AINPUT_SOURCE_MOUSE_RELATIVE"; + break; + case AINPUT_SOURCE_TOUCHPAD: + inputSourceString = "AINPUT_SOURCE_TOUCHPAD"; + break; + case AINPUT_SOURCE_TOUCH_NAVIGATION: + inputSourceString = "AINPUT_SOURCE_TOUCH_NAVIGATION"; + break; + case AINPUT_SOURCE_JOYSTICK: + inputSourceString = "AINPUT_SOURCE_JOYSTICK"; + break; + case AINPUT_SOURCE_ROTARY_ENCODER: + inputSourceString = "AINPUT_SOURCE_ROTARY_ENCODER"; + break; + default: + break; + } + return inputSourceString; +} + +void LogInputEvent(const AInputEvent *event) { + const int32_t eventSource = AInputEvent_getSource(event); +#if defined LOG_FILTER_PADDLEBOAT_SOURCES + if (!(eventSource == AINPUT_SOURCE_DPAD || + eventSource == AINPUT_SOURCE_GAMEPAD || + eventSource == AINPUT_SOURCE_JOYSTICK)) { + return; + } +#endif + const int32_t eventDeviceId = AInputEvent_getDeviceId(event); + const int32_t eventType = AInputEvent_getType(event); + const char *inputSourceString = LogGetInputSourceString(eventSource); + + if (eventType == AINPUT_EVENT_TYPE_KEY) { + const int32_t eventAction = AKeyEvent_getAction(event); + const int32_t eventFlags = AKeyEvent_getFlags(event); + const int32_t eventKeycode = AKeyEvent_getKeyCode(event); + const char *actionString = + (eventAction < ELEMENTS_OF(AKEY_ACTION_STRINGS) && eventAction >= 0) + ? AKEY_ACTION_STRINGS[eventAction] + : "AKEY_ACTION out of range"; + const char *keycodeString = + (eventKeycode < ELEMENTS_OF(AKEYCODE_STRINGS) && eventKeycode >= 0) + ? AKEYCODE_STRINGS[eventKeycode] + : "AKEYCODE out of range"; + ALOGI( + "LogInputEvent\nAINPUT_EVENT_TYPE_KEY deviceId %d source %s\n%s %s " + "%08x", + eventDeviceId, inputSourceString, actionString, keycodeString, + eventFlags); + } else if (eventType == AINPUT_EVENT_TYPE_MOTION) { + const int32_t eventAction = + AMotionEvent_getAction(event) & AMOTION_EVENT_ACTION_MASK; + const int32_t eventFlags = AMotionEvent_getFlags(event); + const char *actionString = + (eventAction < ELEMENTS_OF(AMOTION_ACTION_STRINGS) && + eventAction >= 0) + ? AMOTION_ACTION_STRINGS[eventAction] + : "AMOTION_ACTION out of range"; + ALOGI( + "LogInputEvent\nAINPUT_EVENT_TYPE_MOTION deviceId %d source %s\n%s " + "%08x", + eventDeviceId, inputSourceString, actionString, eventFlags); + } +} +} // namespace paddleboat \ No newline at end of file diff --git a/sources/android-gamesdk/GameController/GameControllerLog.h b/sources/android-gamesdk/GameController/GameControllerLog.h new file mode 100644 index 00000000..9ecddbcd --- /dev/null +++ b/sources/android-gamesdk/GameController/GameControllerLog.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +namespace paddleboat { +const char *LogGetInputSourceString(const int32_t eventSource); + +void LogInputEvent(const AInputEvent *event); +} // namespace paddleboat diff --git a/sources/android-gamesdk/GameController/GameControllerLogStrings.h b/sources/android-gamesdk/GameController/GameControllerLogStrings.h new file mode 100644 index 00000000..b9fd24e4 --- /dev/null +++ b/sources/android-gamesdk/GameController/GameControllerLogStrings.h @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +namespace paddleboat { +// String tables for debug logging purposes that must exactly map +// to the enum tables in input.h and keycode.h +const char *AINPUT_EVENT_STRINGS[3] = { + "AINPUT_EVENT_NULL", // 0 + "AINPUT_EVENT_TYPE_KEY", // 1 + "AINPUT_EVENT_TYPE_MOTION" // 2 +}; + +const char *AKEY_ACTION_STRINGS[3] = { + "AKEY_EVENT_ACTION_DOWN", // 0 + "AKEY_EVENT_ACTION_UP", // 1 + "AKEY_EVENT_ACTION_MULTIPLE" // 2 +}; + +const char *AKEYCODE_STRINGS[289] = { + "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 + "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 + "AKEYCODE_ESCAPE", // 111 + "AKEYCODE_FORWARD_DEL", // 112 + "AKEYCODE_CTRL_LEFT", // 113 + "AKEYCODE_CTRL_RIGHT", // 114 + "AKEYCODE_CAPS_LOCK", // 115 + "AKEYCODE_SCROLL_LOCK", // 116 + "AKEYCODE_META_LEFT", // 117 + "AKEYCODE_META_RIGHT", // 118 + "AKEYCODE_FUNCTION", // 119 + "AKEYCODE_SYSRQ", // 120 + "AKEYCODE_BREAK", // 121 + "AKEYCODE_MOVE_HOME", // 122 + "AKEYCODE_MOVE_END", // 123 + "AKEYCODE_INSERT", // 124 + "AKEYCODE_FORWARD", // 125 + "AKEYCODE_MEDIA_PLAY", // 126 + "AKEYCODE_MEDIA_PAUSE", // 127 + "AKEYCODE_MEDIA_CLOSE", // 128 + "AKEYCODE_MEDIA_EJECT", // 129 + "AKEYCODE_MEDIA_RECORD", // 130 + "AKEYCODE_F1", // 131 + "AKEYCODE_F2", // 132 + "AKEYCODE_F3", // 133 + "AKEYCODE_F4", // 134 + "AKEYCODE_F5", // 135 + "AKEYCODE_F6", // 136 + "AKEYCODE_F7", // 137 + "AKEYCODE_F8", // 138 + "AKEYCODE_F9", // 139 + "AKEYCODE_F10", // 140 + "AKEYCODE_F11", // 141 + "AKEYCODE_F12", // 142 + "AKEYCODE_NUM_LOCK", // 143 + "AKEYCODE_NUMPAD_0", // 144 + "AKEYCODE_NUMPAD_1", // 145 + "AKEYCODE_NUMPAD_2", // 146 + "AKEYCODE_NUMPAD_3", // 147 + "AKEYCODE_NUMPAD_4", // 148 + "AKEYCODE_NUMPAD_5", // 149 + "AKEYCODE_NUMPAD_6", // 150 + "AKEYCODE_NUMPAD_7", // 151 + "AKEYCODE_NUMPAD_8", // 152 + "AKEYCODE_NUMPAD_9", // 153 + "AKEYCODE_NUMPAD_DIVIDE", // 154 + "AKEYCODE_NUMPAD_MULTIPLY", // 155 + "AKEYCODE_NUMPAD_SUBTRACT", // 156 + "AKEYCODE_NUMPAD_ADD", // 157 + "AKEYCODE_NUMPAD_DOT", // 158 + "AKEYCODE_NUMPAD_COMMA", // 159 + "AKEYCODE_NUMPAD_ENTER", // 160 + "AKEYCODE_NUMPAD_EQUALS", // 161 + "AKEYCODE_NUMPAD_LEFT_PAREN", // 162 + "AKEYCODE_NUMPAD_RIGHT_PAREN", // 163 + "AKEYCODE_VOLUME_MUTE", // 164 + "AKEYCODE_INFO", // 165 + "AKEYCODE_CHANNEL_UP", // 166 + "AKEYCODE_CHANNEL_DOWN", // 167 + "AKEYCODE_ZOOM_IN", // 168 + "AKEYCODE_ZOOM_OUT", // 169 + "AKEYCODE_TV", // 170 + "AKEYCODE_WINDOW", // 171 + "AKEYCODE_GUIDE", // 172 + "AKEYCODE_DVR", // 173 + "AKEYCODE_BOOKMARK", // 174 + "AKEYCODE_CAPTIONS", // 175 + "AKEYCODE_SETTINGS", // 176 + "AKEYCODE_TV_POWER", // 177 + "AKEYCODE_TV_INPUT", // 178 + "AKEYCODE_STB_POWER", // 179 + "AKEYCODE_STB_INPUT", // 180 + "AKEYCODE_AVR_POWER", // 181 + "AKEYCODE_AVR_INPUT", // 182 + "AKEYCODE_PROG_RED", // 183 + "AKEYCODE_PROG_GREEN", // 184 + "AKEYCODE_PROG_YELLOW", // 185 + "AKEYCODE_PROG_BLUE", // 186 + "AKEYCODE_APP_SWITCH", // 187 + "AKEYCODE_BUTTON_1", // 188 + "AKEYCODE_BUTTON_2", // 189 + "AKEYCODE_BUTTON_3", // 190 + "AKEYCODE_BUTTON_4", // 191 + "AKEYCODE_BUTTON_5", // 192 + "AKEYCODE_BUTTON_6", // 193 + "AKEYCODE_BUTTON_7", // 194 + "AKEYCODE_BUTTON_8", // 195 + "AKEYCODE_BUTTON_9", // 196 + "AKEYCODE_BUTTON_10", // 197 + "AKEYCODE_BUTTON_11", // 198 + "AKEYCODE_BUTTON_12", // 199 + "AKEYCODE_BUTTON_13", // 200 + "AKEYCODE_BUTTON_14", // 201 + "AKEYCODE_BUTTON_15", // 202 + "AKEYCODE_BUTTON_16", // 203 + "AKEYCODE_LANGUAGE_SWITCH", // 204 + "AKEYCODE_MANNER_MODE", // 205 + "AKEYCODE_3D_MODE", // 206 + "AKEYCODE_CONTACTS", // 207 + "AKEYCODE_CALENDAR", // 208 + "AKEYCODE_MUSIC", // 209 + "AKEYCODE_CALCULATOR", // 210 + "AKEYCODE_ZENKAKU_HANKAKU", // 211 + "AKEYCODE_EISU", // 212 + "AKEYCODE_MUHENKAN", // 213 + "AKEYCODE_HENKAN", // 214 + "AKEYCODE_KATAKANA_HIRAGANA", // 215 + "AKEYCODE_YEN", // 216 + "AKEYCODE_RO", // 217 + "AKEYCODE_KANA", // 218 + "AKEYCODE_ASSIST", // 219 + "AKEYCODE_BRIGHTNESS_DOWN", // 220 + "AKEYCODE_BRIGHTNESS_UP", // 221 + "AKEYCODE_MEDIA_AUDIO_TRACK", // 222 + "AKEYCODE_SLEEP", // 223 + "AKEYCODE_WAKEUP", // 224 + "AKEYCODE_PAIRING", // 225 + "AKEYCODE_MEDIA_TOP_MENU", // 226 + "AKEYCODE_11", // 227 + "AKEYCODE_12", // 228 + "AKEYCODE_LAST_CHANNEL", // 229 + "AKEYCODE_TV_DATA_SERVICE", // 230 + "AKEYCODE_VOICE_ASSIST", // 231 + "AKEYCODE_TV_RADIO_SERVICE", // 232 + "AKEYCODE_TV_TELETEXT", // 233 + "AKEYCODE_TV_NUMBER_ENTRY", // 234 + "AKEYCODE_TV_TERRESTRIAL_ANALOG", // 235 + "AKEYCODE_TV_TERRESTRIAL_DIGITAL", // 236 + "AKEYCODE_TV_SATELLITE", // 237 + "AKEYCODE_TV_SATELLITE_BS", // 238 + "AKEYCODE_TV_SATELLITE_CS", // 239 + "AKEYCODE_TV_SATELLITE_SERVICE", // 240 + "AKEYCODE_TV_NETWORK", // 241 + "AKEYCODE_TV_ANTENNA_CABLE", // 242 + "AKEYCODE_TV_INPUT_HDMI_1", // 243 + "AKEYCODE_TV_INPUT_HDMI_2", // 244 + "AKEYCODE_TV_INPUT_HDMI_3", // 245 + "AKEYCODE_TV_INPUT_HDMI_4", // 246 + "AKEYCODE_TV_INPUT_COMPOSITE_1", // 247 + "AKEYCODE_TV_INPUT_COMPOSITE_2", // 248 + "AKEYCODE_TV_INPUT_COMPONENT_1", // 249 + "AKEYCODE_TV_INPUT_COMPONENT_2", // 250 + "AKEYCODE_TV_INPUT_VGA_1", // 251 + "AKEYCODE_TV_AUDIO_DESCRIPTION", // 252 + "AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP", // 253 + "AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN", // 254 + "AKEYCODE_TV_ZOOM_MODE", // 255 + "AKEYCODE_TV_CONTENTS_MENU", // 256 + "AKEYCODE_TV_MEDIA_CONTEXT_MENU", // 257 + "AKEYCODE_TV_TIMER_PROGRAMMING", // 258 + "AKEYCODE_HELP", // 259 + "AKEYCODE_NAVIGATE_PREVIOUS", // 260 + "AKEYCODE_NAVIGATE_NEXT", // 261 + "AKEYCODE_NAVIGATE_IN", // 262 + "AKEYCODE_NAVIGATE_OUT", // 263 + "AKEYCODE_STEM_PRIMARY", // 264 + "AKEYCODE_STEM_1", // 265 + "AKEYCODE_STEM_2", // 266 + "AKEYCODE_STEM_3", // 267 + "AKEYCODE_DPAD_UP_LEFT", // 268 + "AKEYCODE_DPAD_DOWN_LEFT", // 269 + "AKEYCODE_DPAD_UP_RIGHT", // 270 + "AKEYCODE_DPAD_DOWN_RIGHT", // 271 + "AKEYCODE_MEDIA_SKIP_FORWARD", // 272 + "AKEYCODE_MEDIA_SKIP_BACKWARD", // 273 + "AKEYCODE_MEDIA_STEP_FORWARD", // 274 + "AKEYCODE_MEDIA_STEP_BACKWARD", // 275 + "AKEYCODE_SOFT_SLEEP", // 276 + "AKEYCODE_CUT", // 277 + "AKEYCODE_COPY", // 278 + "AKEYCODE_PASTE", // 279 + "AKEYCODE_SYSTEM_NAVIGATION_UP", // 280 + "AKEYCODE_SYSTEM_NAVIGATION_DOWN", // 281 + "AKEYCODE_SYSTEM_NAVIGATION_LEFT", // 282 + "AKEYCODE_SYSTEM_NAVIGATION_RIGHT", // 283 + "AKEYCODE_ALL_APPS", // 284 + "AKEYCODE_REFRESH", // 285 + "AKEYCODE_THUMBS_UP", // 286 + "AKEYCODE_THUMBS_DOWN", // 287 + "AKEYCODE_PROFILE_SWITCH", // 288 +}; + +const char *AMOTION_AXIS_STRINGS[48] = { + "AMOTION_EVENT_AXIS_X", // 0 + "AMOTION_EVENT_AXIS_Y", // 1 + "AMOTION_EVENT_AXIS_PRESSURE", // 2 + "AMOTION_EVENT_AXIS_SIZE", // 3 + "AMOTION_EVENT_AXIS_TOUCH_MAJOR", // 4 + "AMOTION_EVENT_AXIS_TOUCH_MINOR", // 5 + "AMOTION_EVENT_AXIS_TOOL_MAJOR", // 6 + "AMOTION_EVENT_AXIS_TOOL_MINOR", // 7 + "AMOTION_EVENT_AXIS_ORIENTATION", // 8 + "AMOTION_EVENT_AXIS_VSCROLL", // 9 + "AMOTION_EVENT_AXIS_HSCROLL", // 10 + "AMOTION_EVENT_AXIS_Z", // 11 + "AMOTION_EVENT_AXIS_RX", // 12 + "AMOTION_EVENT_AXIS_RY", // 13 + "AMOTION_EVENT_AXIS_RZ", // 14 + "AMOTION_EVENT_AXIS_HAT_X", // 15 + "AMOTION_EVENT_AXIS_HAT_Y", // 16 + "AMOTION_EVENT_AXIS_LTRIGGER", // 17 + "AMOTION_EVENT_AXIS_RTRIGGER", // 18 + "AMOTION_EVENT_AXIS_THROTTLE", // 19 + "AMOTION_EVENT_AXIS_RUDDER", // 20 + "AMOTION_EVENT_AXIS_WHEEL", // 21 + "AMOTION_EVENT_AXIS_GAS", // 22 + "AMOTION_EVENT_AXIS_BRAKE", // 23 + "AMOTION_EVENT_AXIS_DISTANCE", // 24 + "AMOTION_EVENT_AXIS_TILT", // 25 + "AMOTION_EVENT_AXIS_SCROLL", // 26 + "AMOTION_EVENT_AXIS_RELATIVE_X", // 27 + "AMOTION_EVENT_AXIS_RELATIVE_Y", // 28 + "AMOTION_EVENT_UNDEFINED", // 29 + "AMOTION_EVENT_UNDEFINED", // 30 + "AMOTION_EVENT_UNDEFINED", // 31 + "AMOTION_EVENT_AXIS_GENERIC_1", // 32 + "AMOTION_EVENT_AXIS_GENERIC_2", // 33 + "AMOTION_EVENT_AXIS_GENERIC_3", // 34 + "AMOTION_EVENT_AXIS_GENERIC_4", // 35 + "AMOTION_EVENT_AXIS_GENERIC_5", // 36 + "AMOTION_EVENT_AXIS_GENERIC_6", // 37 + "AMOTION_EVENT_AXIS_GENERIC_7", // 38 + "AMOTION_EVENT_AXIS_GENERIC_8", // 39 + "AMOTION_EVENT_AXIS_GENERIC_9", // 40 + "AMOTION_EVENT_AXIS_GENERIC_10", // 41 + "AMOTION_EVENT_AXIS_GENERIC_11", // 42 + "AMOTION_EVENT_AXIS_GENERIC_12", // 43 + "AMOTION_EVENT_AXIS_GENERIC_13", // 44 + "AMOTION_EVENT_AXIS_GENERIC_14", // 45 + "AMOTION_EVENT_AXIS_GENERIC_15", // 46 + "AMOTION_EVENT_AXIS_GENERIC_16" // 47 +}; + +const char *AMOTION_ACTION_STRINGS[13] = { + "AMOTION_EVENT_ACTION_DOWN", // 0 + "AMOTION_EVENT_ACTION_UP", // 1, + "AMOTION_EVENT_ACTION_MOVE", // 2 + "AMOTION_EVENT_ACTION_CANCEL", // 3 + "AMOTION_EVENT_ACTION_OUTSIDE", // 4 + "AMOTION_EVENT_ACTION_POINTER_DOWN", // 5 + "AMOTION_EVENT_ACTION_POINTER_UP", // 6 + "AMOTION_EVENT_ACTION_HOVER_MOVE", // 7 + "AMOTION_EVENT_ACTION_SCROLL", // 8 + "AMOTION_EVENT_ACTION_HOVER_ENTER", // 9 + "AMOTION_EVENT_ACTION_HOVER_EXIT", // 10 + "AMOTION_EVENT_ACTION_BUTTON_PRESS", // 11 + "AMOTION_EVENT_ACTION_BUTTON_RELEASE" // 12 _ +}; +} // namespace paddleboat \ No newline at end of file diff --git a/sources/android-gamesdk/GameController/GameControllerManager.cpp b/sources/android-gamesdk/GameController/GameControllerManager.cpp new file mode 100644 index 00000000..6666a29d --- /dev/null +++ b/sources/android-gamesdk/GameController/GameControllerManager.cpp @@ -0,0 +1,1443 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "GameControllerManager.h" + +#include + +#include +#include + +#include "GameControllerInternalConstants.h" +#include "GameControllerLog.h" +#include "GameControllerMappingUtils.h" +#include "InternalControllerTable.h" +#include "Log.h" + +#define ARRAY_COUNTOF(array) (sizeof(array) / sizeof(array[0])) +#define LOG_TAG "GameControllerManager" +// If defined, output information about qualifying game controller +// event data to log +//#define LOG_INPUT_EVENTS + +// JNI interface functions + +extern "C" { + +void Java_com_google_android_games_paddleboat_GameControllerManager_onControllerConnected( + JNIEnv *env, jobject gcmObject, jintArray deviceInfoArray, + jfloatArray axisMinArray, jfloatArray axisMaxArray, + jfloatArray axisFlatArray, jfloatArray axisFuzzArray) { + paddleboat::GameControllerDeviceInfo *deviceInfo = + paddleboat::GameControllerManager::onConnection(); + if (deviceInfo != nullptr) { + // Copy the array contents into the DeviceInfo array equivalents + const jsize infoArraySize = env->GetArrayLength(deviceInfoArray); + if ((infoArraySize * sizeof(int32_t)) == + sizeof(paddleboat::GameControllerDeviceInfo::InfoFields)) { + env->GetIntArrayRegion( + deviceInfoArray, 0, infoArraySize, + reinterpret_cast(deviceInfo->getInfo())); + } else { + ALOGE( + "deviceInfoArray/GameControllerDeviceInfo::InfoFields size " + "mismatch"); + } +#if defined LOG_INPUT_EVENTS + ALOGI("onControllerConnected deviceId %d", + deviceInfo->getInfo()->mDeviceId); +#endif + + // all axis arrays are presumed to be the same size + const jsize axisArraySize = env->GetArrayLength(axisMinArray); + if ((axisArraySize * sizeof(float)) == + (sizeof(float) * paddleboat::MAX_AXIS_COUNT)) { + env->GetFloatArrayRegion(axisMinArray, 0, axisArraySize, + deviceInfo->getMinArray()); + env->GetFloatArrayRegion(axisMaxArray, 0, axisArraySize, + deviceInfo->getMaxArray()); + env->GetFloatArrayRegion(axisFlatArray, 0, axisArraySize, + deviceInfo->getFlatArray()); + env->GetFloatArrayRegion(axisFuzzArray, 0, axisArraySize, + deviceInfo->getFuzzArray()); + } else { + ALOGE( + "axisArray/GameControllerDeviceInfo::axisArray size mismatch"); + } + + // Retrieve the device name string + const jint deviceId = deviceInfo->getInfo()->mDeviceId; + jmethodID getDeviceNameById = env->GetMethodID( + paddleboat::GameControllerManager::getGameControllerClass(), + "getDeviceNameById", "(I)Ljava/lang/String;"); + if (getDeviceNameById != NULL) { + jstring deviceNameJstring = reinterpret_cast< + jstring>(env->CallObjectMethod( + paddleboat::GameControllerManager::getGameControllerObject(), + getDeviceNameById, deviceId)); + const char *deviceName = + env->GetStringUTFChars(deviceNameJstring, NULL); + if (deviceName != nullptr) { + deviceInfo->setName(deviceName); + } + env->ReleaseStringUTFChars(deviceNameJstring, deviceName); + } + } +} + +void Java_com_google_android_games_paddleboat_GameControllerManager_onControllerDisconnected( + JNIEnv *env, jobject gcmObject, jint deviceId) { + paddleboat::GameControllerManager::onDisconnection(deviceId); +} + +void Java_com_google_android_games_paddleboat_GameControllerManager_onMotionData( + JNIEnv *env, jobject gcmObject, jint deviceId, jint motionType, + jlong timestamp, jfloat dataX, jfloat dataY, jfloat dataZ) { + paddleboat::GameControllerManager::onMotionData( + deviceId, motionType, timestamp, dataX, dataY, dataZ); +} + +void Java_com_google_android_games_paddleboat_GameControllerManager_onMouseConnected( + JNIEnv *env, jobject gcmObject, jint deviceId) { + paddleboat::GameControllerManager::onMouseConnection(deviceId); +} + +void Java_com_google_android_games_paddleboat_GameControllerManager_onMouseDisconnected( + JNIEnv *env, jobject gcmObject, jint deviceId) { + paddleboat::GameControllerManager::onMouseDisconnection(deviceId); +} + +} // extern "C" + +namespace paddleboat { +constexpr const char *CLASSLOADER_CLASS = "java/lang/ClassLoader"; +constexpr const char *CLASSLOADER_GETCLASSLOADER_METHOD_NAME = "getClassLoader"; +constexpr const char *CLASSLOADER_GETCLASSLOADER_METHOD_SIG = + "()Ljava/lang/ClassLoader;"; +constexpr const char *CLASSLOADER_LOADCLASS_METHOD_NAME = "loadClass"; +constexpr const char *CLASSLOADER_LOADCLASS_METHOD_SIG = + "(Ljava/lang/String;)Ljava/lang/Class;"; + +constexpr const char *GCM_CLASSNAME = + "com/google/android/games/paddleboat/GameControllerManager"; +constexpr const char *GCM_INIT_METHOD_NAME = ""; +constexpr const char *GCM_INIT_METHOD_SIGNATURE = + "(Landroid/content/Context;Z)V"; +constexpr const char *GCM_ONSTOP_METHOD_NAME = "onStop"; +constexpr const char *GCM_ONSTART_METHOD_NAME = "onStart"; +constexpr const char *GCM_GETAPILEVEL_METHOD_NAME = "getApiLevel"; +constexpr const char *GCM_GETAPILEVEL_METHOD_SIGNATURE = "()I"; +constexpr const char *GCM_GETBATTERYLEVEL_METHOD_NAME = "getBatteryLevel"; +constexpr const char *GCM_GETBATTERYLEVEL_METHOD_SIGNATURE = "(I)F"; +constexpr const char *GCM_GETBATTERYSTATUS_METHOD_NAME = "getBatteryStatus"; +constexpr const char *GCM_GETBATTERYSTATUS_METHOD_SIGNATURE = "(I)I"; +constexpr const char *GCM_SETLIGHT_METHOD_NAME = "setLight"; +constexpr const char *GCM_SETLIGHT_METHOD_SIGNATURE = "(III)V"; +constexpr const char *GCM_SETNATIVEREADY_METHOD_NAME = "setNativeReady"; +constexpr const char *GCM_SETREPORTMOTIONEVENTS_METHOD_NAME = + "setReportMotionEvents"; +constexpr const char *GCM_SETVIBRATION_METHOD_NAME = "setVibration"; +constexpr const char *GCM_SETVIBRATION_METHOD_SIGNATURE = "(IIIII)V"; + +constexpr const char *VOID_METHOD_SIGNATURE = "()V"; + +constexpr float VIBRATION_INTENSITY_SCALE = 255.0f; + +typedef struct MethodTableEntry { + const char *methodName; + const char *methodSignature; + jmethodID *methodID; +} MethodTableEntry; + +const JNINativeMethod GCM_NATIVE_METHODS[] = { + {"onControllerConnected", "([I[F[F[F[F)V", + reinterpret_cast( + Java_com_google_android_games_paddleboat_GameControllerManager_onControllerConnected)}, + {"onControllerDisconnected", "(I)V", + reinterpret_cast( + Java_com_google_android_games_paddleboat_GameControllerManager_onControllerDisconnected)}, + {"onMotionData", "(IIJFFF)V", + reinterpret_cast( + Java_com_google_android_games_paddleboat_GameControllerManager_onMotionData)}, + {"onMouseConnected", "(I)V", + reinterpret_cast( + Java_com_google_android_games_paddleboat_GameControllerManager_onMouseConnected)}, + {"onMouseDisconnected", "(I)V", + reinterpret_cast( + Java_com_google_android_games_paddleboat_GameControllerManager_onMouseDisconnected)}, +}; + +std::mutex GameControllerManager::sInstanceMutex; +std::unique_ptr GameControllerManager::sInstance; + +Paddleboat_ErrorCode GameControllerManager::init(JNIEnv *env, + jobject jcontext) { + std::lock_guard lock(sInstanceMutex); + if (sInstance) { + ALOGE("Attempted to initialize Paddleboat twice"); + return PADDLEBOAT_ERROR_ALREADY_INITIALIZED; + } + + sInstance = std::make_unique(env, jcontext, + ConstructorTag{}); + if (!sInstance->mInitialized) { + ALOGE("Failed to initialize Paddleboat"); + return PADDLEBOAT_ERROR_INIT_GCM_FAILURE; + } + GameControllerManager *gcm = sInstance.get(); + if (gcm == nullptr) { + ALOGE("Failed to initialize Paddleboat"); + return PADDLEBOAT_ERROR_INIT_GCM_FAILURE; + } + + // Load our GameControllerManager class + jclass activityClass = env->GetObjectClass(jcontext); + jmethodID getClassLoaderID = + env->GetMethodID(activityClass, CLASSLOADER_GETCLASSLOADER_METHOD_NAME, + CLASSLOADER_GETCLASSLOADER_METHOD_SIG); + jobject classLoaderObject = + env->CallObjectMethod(jcontext, getClassLoaderID); + jclass classLoader = env->FindClass(CLASSLOADER_CLASS); + jmethodID loadClassID = + env->GetMethodID(classLoader, CLASSLOADER_LOADCLASS_METHOD_NAME, + CLASSLOADER_LOADCLASS_METHOD_SIG); + jstring gcmClassName = env->NewStringUTF(GCM_CLASSNAME); + jclass gcmClass = jclass( + env->CallObjectMethod(classLoaderObject, loadClassID, gcmClassName)); + if (gcmClass == NULL) { + ALOGE("Failed to find GameControllerManager class"); + return PADDLEBOAT_ERROR_INIT_GCM_FAILURE; + } + + // Register our native methods, in case we were linked as a static library + int rc = env->RegisterNatives( + gcmClass, GCM_NATIVE_METHODS, + sizeof(GCM_NATIVE_METHODS) / sizeof(JNINativeMethod)); + if (rc != JNI_OK) { + ALOGE("Failed to register native methods. %d", rc); + return PADDLEBOAT_ERROR_INIT_GCM_FAILURE; + } + + jclass global_gcmClass = static_cast(env->NewGlobalRef(gcmClass)); + + // Retrieve our JNI methodIDs to the game controller manager class object + gcm->mGameControllerClass = global_gcmClass; + Paddleboat_ErrorCode methodResult = gcm->initMethods(env); + if (methodResult != PADDLEBOAT_NO_ERROR) { + return methodResult; + } + +#if _DEBUG + jboolean printControllerInfo = JNI_TRUE; +#else + jboolean printControllerInfo = JNI_FALSE; +#endif + jobject gcmObject = env->NewObject(global_gcmClass, gcm->mInitMethodId, + jcontext, printControllerInfo); + if (gcmObject == NULL) { + ALOGE("Failed to create GameControllerManager"); + return PADDLEBOAT_ERROR_INIT_GCM_FAILURE; + } + jobject global_gcmObject = env->NewGlobalRef(gcmObject); + env->DeleteLocalRef(gcmObject); + + gcm->mGameControllerObject = global_gcmObject; + + return PADDLEBOAT_NO_ERROR; +} + +void GameControllerManager::destroyInstance(JNIEnv *env) { + std::lock_guard lock(sInstanceMutex); + sInstance.get()->releaseGlobals(env); + sInstance.reset(); +} + +void GameControllerManager::releaseGlobals(JNIEnv *env) { + if (mGameControllerClass != NULL) { + env->DeleteGlobalRef(mGameControllerClass); + mGameControllerClass = NULL; + } + if (mGameControllerObject != NULL) { + env->DeleteGlobalRef(mGameControllerObject); + mGameControllerObject = NULL; + } +} + +GameControllerManager *GameControllerManager::getInstance() { + std::lock_guard lock(sInstanceMutex); + return sInstance.get(); +} + +bool GameControllerManager::isInitialized() { + GameControllerManager *gcm = getInstance(); + if (!gcm) { + // This is a case of error. + // We do not log anything here, so that we do not spam + // the user when this function is called each frame. + return false; + } + return gcm->mGCMClassInitialized; +} + +Paddleboat_ErrorCode GameControllerManager::initMethods(JNIEnv *env) { + const MethodTableEntry methodTable[] = { + {GCM_INIT_METHOD_NAME, GCM_INIT_METHOD_SIGNATURE, &mInitMethodId}, + {GCM_GETAPILEVEL_METHOD_NAME, GCM_GETAPILEVEL_METHOD_SIGNATURE, + &mGetApiLevelMethodId}, + {GCM_GETBATTERYLEVEL_METHOD_NAME, GCM_GETBATTERYLEVEL_METHOD_SIGNATURE, + &mGetBatteryLevelMethodId}, + {GCM_GETBATTERYSTATUS_METHOD_NAME, + GCM_GETBATTERYSTATUS_METHOD_SIGNATURE, &mGetBatteryStatusMethodId}, + {GCM_SETVIBRATION_METHOD_NAME, GCM_SETVIBRATION_METHOD_SIGNATURE, + &mSetVibrationMethodId}, + {GCM_SETLIGHT_METHOD_NAME, GCM_SETLIGHT_METHOD_SIGNATURE, + &mSetLightMethodId}, + {GCM_SETNATIVEREADY_METHOD_NAME, VOID_METHOD_SIGNATURE, + &mSetNativeReadyMethodId}, + {GCM_SETREPORTMOTIONEVENTS_METHOD_NAME, VOID_METHOD_SIGNATURE, + &mSetReportMotionEventsMethodId}}; + const size_t methodTableCount = ARRAY_COUNTOF(methodTable); + + for (size_t i = 0; i < methodTableCount; ++i) { + const MethodTableEntry &entry = methodTable[i]; + jmethodID methodID = env->GetMethodID( + mGameControllerClass, entry.methodName, entry.methodSignature); + if (methodID == NULL) { + ALOGE("Failed to find %s init method", entry.methodName); + return PADDLEBOAT_ERROR_INIT_GCM_FAILURE; + } else { + *entry.methodID = methodID; + } + } + return PADDLEBOAT_NO_ERROR; +} + +GameControllerManager::GameControllerManager(JNIEnv *env, jobject jcontext, + ConstructorTag) { + mContext = jcontext; + mMouseData.timestamp = 0; + mMouseData.buttonsDown = 0; + mMouseData.mouseScrollDeltaH = 0; + mMouseData.mouseScrollDeltaV = 0; + mMouseData.mouseX = 0.0f; + mMouseData.mouseY = 0.0f; + mInitialized = true; + memset(mMappingTable, 0, sizeof(mMappingTable)); + mRemapEntryCount = GetInternalControllerDataCount(); + const Paddleboat_Controller_Mapping_Data *mappingData = + GetInternalControllerData(); + const size_t copySize = + mRemapEntryCount * sizeof(Paddleboat_Controller_Mapping_Data); + memcpy(mMappingTable, mappingData, copySize); + // Our minimum supported API level, we will retrieve the actual runtime API + // level later on calling getApiLevel + mApiLevel = 16; +} + +GameControllerManager::~GameControllerManager() { mInitialized = false; } + +int32_t GameControllerManager::processInputEvent(const AInputEvent *event) { + int32_t handledEvent = IGNORED_EVENT; + if (event != nullptr) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + std::lock_guard lock(gcm->mUpdateMutex); + const int32_t eventSource = AInputEvent_getSource(event); + const int32_t dpadSource = eventSource & AINPUT_SOURCE_DPAD; + const int32_t gamepadSource = eventSource & AINPUT_SOURCE_GAMEPAD; + const int32_t joystickSource = eventSource & AINPUT_SOURCE_JOYSTICK; + const int32_t mouseSource = eventSource & AINPUT_SOURCE_MOUSE; + if (dpadSource == AINPUT_SOURCE_DPAD || + gamepadSource == AINPUT_SOURCE_GAMEPAD || + joystickSource == AINPUT_SOURCE_JOYSTICK) { + const int32_t eventDeviceId = AInputEvent_getDeviceId(event); + for (size_t i = 0; i < PADDLEBOAT_MAX_CONTROLLERS; ++i) { + if (gcm->mGameControllers[i].getConnectionIndex() >= 0) { + if (gcm->mGameControllers[i].getControllerStatus() == + PADDLEBOAT_CONTROLLER_ACTIVE) { + const GameControllerDeviceInfo &deviceInfo = + gcm->mGameControllers[i].getDeviceInfo(); + if (deviceInfo.getInfo().mDeviceId == + eventDeviceId) { + const int32_t eventType = + AInputEvent_getType(event); + if (eventType == AINPUT_EVENT_TYPE_KEY) { + handledEvent = + gcm->processControllerKeyEvent( + event, gcm->mGameControllers[i]); + } else if (eventType == + AINPUT_EVENT_TYPE_MOTION) { + handledEvent = + gcm->mGameControllers[i] + .processMotionEvent(event); + } + break; + } + } + } + } + } else if (mouseSource == AINPUT_SOURCE_MOUSE) { + handledEvent = gcm->processMouseEvent(event); + } +#if defined LOG_INPUT_EVENTS + LogInputEvent(event); +#endif + } + } + return handledEvent; +} + +int32_t GameControllerManager::processGameActivityKeyInputEvent( + const void *event, const size_t eventSize) { + int32_t handledEvent = IGNORED_EVENT; + if (event != nullptr) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + std::lock_guard lock(gcm->mUpdateMutex); + const Paddleboat_GameActivityKeyEvent *keyEvent = + reinterpret_cast( + event); + const int32_t eventSource = keyEvent->source; + const int32_t eventDeviceId = keyEvent->deviceId; + const int32_t dpadSource = eventSource & AINPUT_SOURCE_DPAD; + const int32_t gamepadSource = eventSource & AINPUT_SOURCE_GAMEPAD; + const int32_t joystickSource = eventSource & AINPUT_SOURCE_JOYSTICK; + if (dpadSource == AINPUT_SOURCE_DPAD || + gamepadSource == AINPUT_SOURCE_GAMEPAD || + joystickSource == AINPUT_SOURCE_JOYSTICK) { + for (size_t i = 0; i < PADDLEBOAT_MAX_CONTROLLERS; ++i) { + if (gcm->mGameControllers[i].getConnectionIndex() >= 0) { + if (gcm->mGameControllers[i].getControllerStatus() == + PADDLEBOAT_CONTROLLER_ACTIVE) { + const GameControllerDeviceInfo &deviceInfo = + gcm->mGameControllers[i].getDeviceInfo(); + if (deviceInfo.getInfo().mDeviceId == + eventDeviceId) { + handledEvent = + gcm->processControllerGameActivityKeyEvent( + keyEvent, eventSize, + gcm->mGameControllers[i]); + break; + } + } + } + } + } + } + } + return handledEvent; +} + +int32_t GameControllerManager::processGameActivityMotionInputEvent( + const void *event, const size_t eventSize) { + int32_t handledEvent = IGNORED_EVENT; + if (event != nullptr) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + std::lock_guard lock(gcm->mUpdateMutex); + const Paddleboat_GameActivityMotionEvent *motionEvent = + reinterpret_cast( + event); + const int32_t eventSource = motionEvent->source; + const int32_t eventDeviceId = motionEvent->deviceId; + const int32_t dpadSource = eventSource & AINPUT_SOURCE_DPAD; + const int32_t gamepadSource = eventSource & AINPUT_SOURCE_GAMEPAD; + const int32_t joystickSource = eventSource & AINPUT_SOURCE_JOYSTICK; + const int32_t mouseSource = eventSource & AINPUT_SOURCE_MOUSE; + if (dpadSource == AINPUT_SOURCE_DPAD || + gamepadSource == AINPUT_SOURCE_GAMEPAD || + joystickSource == AINPUT_SOURCE_JOYSTICK) { + for (size_t i = 0; i < PADDLEBOAT_MAX_CONTROLLERS; ++i) { + if (gcm->mGameControllers[i].getConnectionIndex() >= 0) { + if (gcm->mGameControllers[i].getControllerStatus() == + PADDLEBOAT_CONTROLLER_ACTIVE) { + const GameControllerDeviceInfo &deviceInfo = + gcm->mGameControllers[i].getDeviceInfo(); + if (deviceInfo.getInfo().mDeviceId == + eventDeviceId) { + handledEvent = + gcm->mGameControllers[i] + .processGameActivityMotionEvent( + motionEvent, eventSize); + break; + } + } + } + } + } else if (mouseSource == AINPUT_SOURCE_MOUSE) { + handledEvent = gcm->processGameActivityMouseEvent( + event, eventSize, eventDeviceId); + } + } + } + return handledEvent; +} + +int32_t GameControllerManager::processControllerKeyEvent( + const AInputEvent *event, GameController &gameController) { + int32_t handledEvent = HANDLED_EVENT; + const int32_t eventKeycode = AKeyEvent_getKeyCode(event); + + mLastKeyEventKeyCode = eventKeycode; + if (eventKeycode == AKEYCODE_BACK) { + if (!mBackButtonConsumed) { + handledEvent = IGNORED_EVENT; + } + } else { + handledEvent = gameController.processKeyEvent(event); + } + return handledEvent; +} + +int32_t GameControllerManager::processControllerGameActivityKeyEvent( + const Paddleboat_GameActivityKeyEvent *event, const size_t eventSize, + GameController &gameController) { + int32_t handledEvent = HANDLED_EVENT; + const int32_t eventKeyCode = event->keyCode; + + mLastKeyEventKeyCode = eventKeyCode; + if (eventKeyCode == AKEYCODE_BACK) { + if (!mBackButtonConsumed) { + handledEvent = IGNORED_EVENT; + } + } else { + handledEvent = + gameController.processGameActivityKeyEvent(event, eventSize); + } + return handledEvent; +} + +int32_t GameControllerManager::processMouseEvent(const AInputEvent *event) { + int32_t handledEvent = IGNORED_EVENT; + const int32_t eventDeviceId = AInputEvent_getDeviceId(event); + const int32_t eventType = AInputEvent_getType(event); + + // Always update the virtual pointer data in the appropriate controller data + // structures + if (eventType == AINPUT_EVENT_TYPE_MOTION) { + for (size_t i = 0; i < PADDLEBOAT_MAX_CONTROLLERS; ++i) { + if (mGameControllers[i].getConnectionIndex() >= 0) { + if (mGameControllers[i].getControllerStatus() == + PADDLEBOAT_CONTROLLER_ACTIVE) { + const Paddleboat_Controller_Info &controllerInfo = + mGameControllers[i].getControllerInfo(); + if (controllerInfo.deviceId == eventDeviceId) { + Paddleboat_Controller_Data &controllerData = + mGameControllers[i].getControllerData(); + controllerData.virtualPointer.pointerX = + AMotionEvent_getAxisValue(event, + AMOTION_EVENT_AXIS_X, 0); + controllerData.virtualPointer.pointerY = + AMotionEvent_getAxisValue(event, + AMOTION_EVENT_AXIS_Y, 0); + const float axisP = AMotionEvent_getAxisValue( + event, AMOTION_EVENT_AXIS_PRESSURE, 0); + + const bool hasTouchpadButton = + ((controllerInfo.controllerFlags & + PADDLEBOAT_CONTROLLER_FLAG_TOUCHPAD) != 0); + if (hasTouchpadButton) { + if (axisP > 0.0f) { + controllerData.buttonsDown |= + PADDLEBOAT_BUTTON_TOUCHPAD; + } else { + controllerData.buttonsDown &= + (~PADDLEBOAT_BUTTON_TOUCHPAD); + } + } + mGameControllers[i].setControllerDataDirty(true); + + // If this controller is our 'active' virtual mouse, + // update the mouse data + if (mMouseStatus == + PADDLEBOAT_MOUSE_CONTROLLER_EMULATED && + mMouseControllerIndex == static_cast(i)) { + mMouseData.mouseX = + controllerData.virtualPointer.pointerX; + mMouseData.mouseY = + controllerData.virtualPointer.pointerY; + mMouseData.buttonsDown = static_cast( + AMotionEvent_getButtonState(event)); + mMouseData.buttonsDown |= axisP > 0.0f ? 1 : 0; + updateMouseDataTimestamp(); + } + break; + } + } + } + } + } + + if (mMouseStatus == PADDLEBOAT_MOUSE_PHYSICAL) { + for (size_t i = 0; i < MAX_MOUSE_DEVICES; ++i) { + if (mMouseDeviceIds[i] == eventDeviceId) { + if (eventType == AINPUT_EVENT_TYPE_MOTION) { + mMouseData.mouseX = AMotionEvent_getAxisValue( + event, AMOTION_EVENT_AXIS_X, 0); + mMouseData.mouseY = AMotionEvent_getAxisValue( + event, AMOTION_EVENT_AXIS_Y, 0); + const int32_t buttonState = + AMotionEvent_getButtonState(event); + mMouseData.buttonsDown = static_cast(buttonState); + const float axisHScroll = AMotionEvent_getAxisValue( + event, AMOTION_EVENT_AXIS_HSCROLL, 0); + const float axisVScroll = AMotionEvent_getAxisValue( + event, AMOTION_EVENT_AXIS_VSCROLL, 0); + // These are treated as cumulative deltas and reset when the + // calling application requests the mouse data. + mMouseData.mouseScrollDeltaH += + static_cast(axisHScroll); + mMouseData.mouseScrollDeltaV += + static_cast(axisVScroll); + updateMouseDataTimestamp(); + } + handledEvent = HANDLED_EVENT; + break; + } + } + } + + return handledEvent; +} + +int32_t GameControllerManager::processGameActivityMouseEvent( + const void *event, const size_t eventSize, const int32_t eventDeviceId) { + int32_t handledEvent = IGNORED_EVENT; + + // Always update the virtual pointer data in the appropriate controller data + // structures + const Paddleboat_GameActivityMotionEvent *motionEvent = + reinterpret_cast(event); + if (motionEvent->pointerCount > 0) { + const Paddleboat_GameActivityPointerInfo *pointerInfo = + motionEvent->pointers; + for (size_t i = 0; i < PADDLEBOAT_MAX_CONTROLLERS; ++i) { + if (mGameControllers[i].getConnectionIndex() >= 0) { + if (mGameControllers[i].getControllerStatus() == + PADDLEBOAT_CONTROLLER_ACTIVE) { + const Paddleboat_Controller_Info &controllerInfo = + mGameControllers[i].getControllerInfo(); + if (controllerInfo.deviceId == eventDeviceId) { + Paddleboat_Controller_Data &controllerData = + mGameControllers[i].getControllerData(); + controllerData.virtualPointer.pointerX = + pointerInfo->axisValues[AMOTION_EVENT_AXIS_X]; + controllerData.virtualPointer.pointerY = + pointerInfo->axisValues[AMOTION_EVENT_AXIS_Y]; + const float axisP = + pointerInfo + ->axisValues[AMOTION_EVENT_AXIS_PRESSURE]; + + const bool hasTouchpadButton = + ((controllerInfo.controllerFlags & + PADDLEBOAT_CONTROLLER_FLAG_TOUCHPAD) != 0); + if (hasTouchpadButton) { + if (axisP > 0.0f) { + controllerData.buttonsDown |= + PADDLEBOAT_BUTTON_TOUCHPAD; + } else { + controllerData.buttonsDown &= + (~PADDLEBOAT_BUTTON_TOUCHPAD); + } + } + mGameControllers[i].setControllerDataDirty(true); + + // If this controller is our 'active' virtual mouse, + // update the mouse data + if (mMouseStatus == + PADDLEBOAT_MOUSE_CONTROLLER_EMULATED && + mMouseControllerIndex == static_cast(i)) { + mMouseData.mouseX = + controllerData.virtualPointer.pointerX; + mMouseData.mouseY = + controllerData.virtualPointer.pointerY; + mMouseData.buttonsDown = motionEvent->buttonState; + mMouseData.buttonsDown |= axisP > 0.0f ? 1 : 0; + updateMouseDataTimestamp(); + } + break; + } + } + } + } + } + + if (mMouseStatus == PADDLEBOAT_MOUSE_PHYSICAL) { + for (size_t i = 0; i < MAX_MOUSE_DEVICES; ++i) { + if (mMouseDeviceIds[i] == eventDeviceId) { + if (motionEvent->pointerCount > 0) { + const Paddleboat_GameActivityPointerInfo *pointerInfo = + motionEvent->pointers; + + mMouseData.mouseX = + pointerInfo->axisValues[AMOTION_EVENT_AXIS_X]; + mMouseData.mouseY = + pointerInfo->axisValues[AMOTION_EVENT_AXIS_Y]; + const int32_t buttonState = motionEvent->buttonState; + mMouseData.buttonsDown = static_cast(buttonState); + const float axisHScroll = + pointerInfo->axisValues[AMOTION_EVENT_AXIS_HSCROLL]; + const float axisVScroll = + pointerInfo->axisValues[AMOTION_EVENT_AXIS_VSCROLL]; + // These are treated as cumulative deltas and reset when the + // calling application requests the mouse data. + mMouseData.mouseScrollDeltaH += + static_cast(axisHScroll); + mMouseData.mouseScrollDeltaV += + static_cast(axisVScroll); + updateMouseDataTimestamp(); + } + handledEvent = HANDLED_EVENT; + break; + } + } + } + + return handledEvent; +} + +uint64_t GameControllerManager::getActiveAxisMask() { + uint64_t returnMask = 0; + GameControllerManager *gcm = getInstance(); + if (gcm) { + returnMask = gcm->mActiveAxisMask; + } + return returnMask; +} + +void GameControllerManager::update(JNIEnv *env) { + GameControllerManager *gcm = getInstance(); + if (!gcm) { + return; + } + if (!gcm->mGCMClassInitialized && gcm->mGameControllerObject != NULL) { + gcm->mApiLevel = env->CallIntMethod(gcm->mGameControllerObject, + gcm->mGetApiLevelMethodId); + + // Tell the GCM class we are ready to receive information about + // controllers + gcm->mGCMClassInitialized = true; + jmethodID setNativeReady = env->GetMethodID( + gcm->mGameControllerClass, GCM_SETNATIVEREADY_METHOD_NAME, + VOID_METHOD_SIGNATURE); + if (setNativeReady != NULL) { + env->CallVoidMethod(gcm->mGameControllerObject, setNativeReady); + } + } + + if (gcm->mMotionDataCallback != nullptr && + gcm->mMotionEventReporting == false) { + // If a motion data callback is registered, tell the managed side to + // start reporting motion event data + env->CallVoidMethod(gcm->mGameControllerObject, + gcm->mSetReportMotionEventsMethodId); + gcm->mMotionEventReporting = true; + } + + std::lock_guard lock(gcm->mUpdateMutex); + + // Process pending connections/disconnections + if (gcm->mStatusCallback != nullptr) { + for (size_t i = 0; i < PADDLEBOAT_MAX_CONTROLLERS; ++i) { + if (gcm->mGameControllers[i].getConnectionIndex() >= 0) { + if (gcm->mGameControllers[i].getControllerStatus() == + PADDLEBOAT_CONTROLLER_JUST_CONNECTED) { + // Look for a mapping table entry for this controller + // device, if one does not exist, nullptr is passed and + // GameController will fallback to default axis and button + // mapping. + const Paddleboat_Controller_Mapping_Data *mapData = + gcm->getMapForController(gcm->mGameControllers[i]); +#if defined LOG_INPUT_EVENTS + if (mapData != nullptr) { + ALOGI("Found controller map for vId/pId: %x %x", + gcm->mGameControllers[i] + .getDeviceInfo() + .getInfo() + ->mVendorId, + gcm->mGameControllers[i] + .getDeviceInfo() + .getInfo() + ->mProductId); + } else { + ALOGI( + "No controller map found, using defaults for " + "vId/pId: %x %x", + gcm->mGameControllers[i] + .getDeviceInfo() + .getInfo() + ->mVendorId, + gcm->mGameControllers[i] + .getDeviceInfo() + .getInfo() + ->mProductId); + } +#endif + gcm->mGameControllers[i].setupController(mapData); + // Update the active axis mask to include any new axis used + // by the new controller + gcm->mActiveAxisMask |= + gcm->mGameControllers[i].getControllerAxisMask(); + gcm->mGameControllers[i].setControllerStatus( + PADDLEBOAT_CONTROLLER_ACTIVE); + if (gcm->mStatusCallback != nullptr) { +#if defined LOG_INPUT_EVENTS + ALOGI( + "statusCallback " + "PADDLEBOAT_CONTROLLER_JUST_CONNECTED on %d", + static_cast(i)); +#endif + gcm->mStatusCallback( + i, PADDLEBOAT_CONTROLLER_JUST_CONNECTED, + gcm->mStatusCallbackUserData); + } + gcm->rescanVirtualMouseControllers(); + } else if (gcm->mGameControllers[i].getControllerStatus() == + PADDLEBOAT_CONTROLLER_JUST_DISCONNECTED) { + gcm->mGameControllers[i].setControllerStatus( + PADDLEBOAT_CONTROLLER_INACTIVE); + // free the controller for reuse + gcm->mGameControllers[i].setConnectionIndex(-1); + if (gcm->mStatusCallback != nullptr) { +#if defined LOG_INPUT_EVENTS + ALOGI( + "statusCallback " + "PADDLEBOAT_CONTROLLER_JUST_DISCONNECTED on %d", + static_cast(i)); +#endif + gcm->mStatusCallback( + i, PADDLEBOAT_CONTROLLER_JUST_DISCONNECTED, + gcm->mStatusCallbackUserData); + } + gcm->rescanVirtualMouseControllers(); + } + } + } + } + gcm->updateBattery(env); +} + +Paddleboat_ErrorCode GameControllerManager::getControllerData( + const int32_t controllerIndex, Paddleboat_Controller_Data *controllerData) { + Paddleboat_ErrorCode errorCode = PADDLEBOAT_NO_ERROR; + if (controllerData != nullptr) { + if (controllerIndex >= 0 && + controllerIndex < PADDLEBOAT_MAX_CONTROLLERS) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + if (gcm->mGameControllers[controllerIndex] + .getConnectionIndex() == controllerIndex) { + if (gcm->mGameControllers[controllerIndex] + .getControllerDataDirty()) { + gcm->mGameControllers[controllerIndex] + .setControllerDataDirty(false); + } + memcpy(controllerData, + &gcm->mGameControllers[controllerIndex] + .getControllerData(), + sizeof(Paddleboat_Controller_Data)); + } else { + errorCode = PADDLEBOAT_ERROR_NO_CONTROLLER; + } + } else { + errorCode = PADDLEBOAT_ERROR_NOT_INITIALIZED; + } + } else { + errorCode = PADDLEBOAT_ERROR_INVALID_CONTROLLER_INDEX; + } + } else { + errorCode = PADDLEBOAT_ERROR_INVALID_PARAMETER; + } + return errorCode; +} + +Paddleboat_ErrorCode GameControllerManager::getControllerInfo( + const int32_t controllerIndex, Paddleboat_Controller_Info *controllerInfo) { + Paddleboat_ErrorCode errorCode = PADDLEBOAT_NO_ERROR; + if (controllerInfo != nullptr) { + if (controllerIndex >= 0 && + controllerIndex < PADDLEBOAT_MAX_CONTROLLERS) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + if (gcm->mGameControllers[controllerIndex] + .getConnectionIndex() == controllerIndex) { + memcpy(controllerInfo, + &gcm->mGameControllers[controllerIndex] + .getControllerInfo(), + sizeof(Paddleboat_Controller_Info)); + } else { + errorCode = PADDLEBOAT_ERROR_NO_CONTROLLER; + } + } else { + errorCode = PADDLEBOAT_ERROR_NOT_INITIALIZED; + } + } else { + errorCode = PADDLEBOAT_ERROR_INVALID_CONTROLLER_INDEX; + } + } else { + errorCode = PADDLEBOAT_ERROR_INVALID_PARAMETER; + } + return errorCode; +} + +Paddleboat_ErrorCode GameControllerManager::getControllerName( + const int32_t controllerIndex, const size_t bufferSize, + char *controllerName) { + Paddleboat_ErrorCode errorCode = PADDLEBOAT_NO_ERROR; + if (controllerName != nullptr) { + if (controllerIndex >= 0 && + controllerIndex < PADDLEBOAT_MAX_CONTROLLERS) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + if (gcm->mGameControllers[controllerIndex] + .getConnectionIndex() == controllerIndex) { + const GameControllerDeviceInfo &deviceInfo = + gcm->mGameControllers[controllerIndex].getDeviceInfo(); + strncpy(controllerName, deviceInfo.getName(), bufferSize); + // Manually zero-terminate if the string was too long to fit + const size_t nameLength = strlen(deviceInfo.getName()); + if (nameLength >= bufferSize) { + controllerName[bufferSize - 1] = '\0'; + } + } else { + errorCode = PADDLEBOAT_ERROR_NO_CONTROLLER; + } + } else { + errorCode = PADDLEBOAT_ERROR_NOT_INITIALIZED; + } + } else { + errorCode = PADDLEBOAT_ERROR_INVALID_CONTROLLER_INDEX; + } + } else { + errorCode = PADDLEBOAT_ERROR_INVALID_PARAMETER; + } + return errorCode; +} + +Paddleboat_ControllerStatus GameControllerManager::getControllerStatus( + const int32_t controllerIndex) { + Paddleboat_ControllerStatus controllerStatus = + PADDLEBOAT_CONTROLLER_INACTIVE; + if (controllerIndex >= 0 && controllerIndex < PADDLEBOAT_MAX_CONTROLLERS) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + controllerStatus = + gcm->mGameControllers[controllerIndex].getControllerStatus(); + } + } + return controllerStatus; +} + +bool GameControllerManager::isLightTypeSupported( + const Paddleboat_Controller_Info &controllerInfo, + const Paddleboat_LightType lightType) { + bool isSupported = false; + + if (mGameControllerObject != NULL && mSetLightMethodId != NULL) { + if (lightType == PADDLEBOAT_LIGHT_RGB) { + if ((controllerInfo.controllerFlags & + PADDLEBOAT_CONTROLLER_FLAG_LIGHT_RGB) != 0) { + isSupported = true; + } + } else if (lightType == PADDLEBOAT_LIGHT_PLAYER_NUMBER) { + if ((controllerInfo.controllerFlags & + PADDLEBOAT_CONTROLLER_FLAG_LIGHT_PLAYER) != 0) { + isSupported = true; + } + } + } + return isSupported; +} + +Paddleboat_ErrorCode GameControllerManager::setControllerLight( + const int32_t controllerIndex, const Paddleboat_LightType lightType, + const uint32_t lightData, JNIEnv *env) { + Paddleboat_ErrorCode errorCode = PADDLEBOAT_NO_ERROR; + + if (controllerIndex >= 0 && controllerIndex < PADDLEBOAT_MAX_CONTROLLERS) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + if (gcm->mGameControllers[controllerIndex].getConnectionIndex() == + controllerIndex) { + const Paddleboat_Controller_Info &controllerInfo = + gcm->mGameControllers[controllerIndex].getControllerInfo(); + if (gcm->isLightTypeSupported(controllerInfo, lightType)) { + const jint jLightType = static_cast(lightType); + const jint jLightData = static_cast(lightData); + env->CallVoidMethod( + gcm->mGameControllerObject, gcm->mSetLightMethodId, + controllerInfo.deviceId, jLightType, jLightData); + } else { + errorCode = PADDLEBOAT_ERROR_FEATURE_NOT_SUPPORTED; + } + } else { + errorCode = PADDLEBOAT_ERROR_NO_CONTROLLER; + } + } else { + errorCode = PADDLEBOAT_ERROR_NOT_INITIALIZED; + } + } else { + errorCode = PADDLEBOAT_ERROR_INVALID_CONTROLLER_INDEX; + } + return errorCode; +} + +Paddleboat_ErrorCode GameControllerManager::setControllerVibrationData( + const int32_t controllerIndex, + const Paddleboat_Vibration_Data *vibrationData, JNIEnv *env) { + Paddleboat_ErrorCode errorCode = PADDLEBOAT_NO_ERROR; + + if (vibrationData != nullptr) { + if (controllerIndex >= 0 && + controllerIndex < PADDLEBOAT_MAX_CONTROLLERS) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + if (gcm->mGameControllers[controllerIndex] + .getConnectionIndex() == controllerIndex) { + const Paddleboat_Controller_Info &controllerInfo = + gcm->mGameControllers[controllerIndex] + .getControllerInfo(); + if ((controllerInfo.controllerFlags & + PADDLEBOAT_CONTROLLER_FLAG_VIBRATION) != 0) { + if (gcm->mGameControllerObject != NULL && + gcm->mSetVibrationMethodId != NULL) { + const jint intensityLeft = + static_cast(vibrationData->intensityLeft * + VIBRATION_INTENSITY_SCALE); + const jint intensityRight = static_cast( + vibrationData->intensityRight * + VIBRATION_INTENSITY_SCALE); + const jint durationLeft = + static_cast(vibrationData->durationLeft); + const jint durationRight = + static_cast(vibrationData->durationRight); + env->CallVoidMethod(gcm->mGameControllerObject, + gcm->mSetVibrationMethodId, + controllerInfo.deviceId, + intensityLeft, durationLeft, + intensityRight, durationRight); + } + } else { + errorCode = PADDLEBOAT_ERROR_FEATURE_NOT_SUPPORTED; + } + } else { + errorCode = PADDLEBOAT_ERROR_NO_CONTROLLER; + } + } else { + errorCode = PADDLEBOAT_ERROR_NOT_INITIALIZED; + } + } else { + errorCode = PADDLEBOAT_ERROR_INVALID_CONTROLLER_INDEX; + } + } else { + errorCode = PADDLEBOAT_ERROR_INVALID_PARAMETER; + } + return errorCode; +} + +GameControllerDeviceInfo *GameControllerManager::onConnection() { + GameControllerDeviceInfo *deviceInfo = nullptr; + GameControllerManager *gcm = getInstance(); + if (gcm) { + std::lock_guard lock(gcm->mUpdateMutex); + for (size_t i = 0; i < PADDLEBOAT_MAX_CONTROLLERS; ++i) { + if (gcm->mGameControllers[i].getConnectionIndex() < 0) { + gcm->mGameControllers[i].setConnectionIndex(i); + gcm->mGameControllers[i].resetControllerData(); + deviceInfo = &gcm->mGameControllers[i].getDeviceInfo(); + gcm->mGameControllers[i].setControllerStatus( + PADDLEBOAT_CONTROLLER_JUST_CONNECTED); +#if defined LOG_INPUT_EVENTS + ALOGI( + "Setting PADDLEBOAT_CONTROLLER_JUST_CONNECTED on index %d", + static_cast(i)); +#endif + break; + } + } + } + return deviceInfo; +} + +void GameControllerManager::onDisconnection(const int32_t deviceId) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + std::lock_guard lock(gcm->mUpdateMutex); + for (size_t i = 0; i < PADDLEBOAT_MAX_CONTROLLERS; ++i) { + if (gcm->mGameControllers[i].getConnectionIndex() >= 0) { + const GameControllerDeviceInfo &deviceInfo = + gcm->mGameControllers[i].getDeviceInfo(); + if (deviceInfo.getInfo().mDeviceId == deviceId) { + gcm->mGameControllers[i].setControllerStatus( + PADDLEBOAT_CONTROLLER_JUST_DISCONNECTED); +#if defined LOG_INPUT_EVENTS + ALOGI( + "Setting PADDLEBOAT_CONTROLLER_JUST_DISCONNECTED on " + "index %d", + static_cast(i)); +#endif + } + } + } + } +} + +Paddleboat_ErrorCode GameControllerManager::getMouseData( + Paddleboat_Mouse_Data *mouseData) { + Paddleboat_ErrorCode errorCode = PADDLEBOAT_NO_ERROR; + if (mouseData != nullptr) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + if (gcm->mMouseStatus != PADDLEBOAT_MOUSE_NONE) { + memcpy(mouseData, &gcm->mMouseData, + sizeof(Paddleboat_Mouse_Data)); + // We reset the scroll wheel(s) values after each read + gcm->mMouseData.mouseScrollDeltaH = 0; + gcm->mMouseData.mouseScrollDeltaV = 0; + } else { + errorCode = PADDLEBOAT_ERROR_NO_MOUSE; + } + } else { + errorCode = PADDLEBOAT_ERROR_NOT_INITIALIZED; + } + } else { + errorCode = PADDLEBOAT_ERROR_INVALID_PARAMETER; + } + return errorCode; +} + +Paddleboat_MouseStatus GameControllerManager::getMouseStatus() { + GameControllerManager *gcm = getInstance(); + if (gcm) { + return gcm->mMouseStatus; + } + return PADDLEBOAT_MOUSE_NONE; +} + +void GameControllerManager::onMotionData(const int32_t deviceId, + const int32_t motionType, + const uint64_t timestamp, + const float dataX, const float dataY, + const float dataZ) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + if (gcm->mMotionDataCallback != nullptr) { + for (size_t i = 0; i < PADDLEBOAT_MAX_CONTROLLERS; ++i) { + if (gcm->mGameControllers[i].getConnectionIndex() >= 0) { + const GameControllerDeviceInfo &deviceInfo = + gcm->mGameControllers[i].getDeviceInfo(); + if (deviceInfo.getInfo().mDeviceId == deviceId) { + Paddleboat_Motion_Data motionData; + motionData.motionType = + static_cast(motionType); + motionData.timestamp = timestamp; + motionData.motionX = dataX; + motionData.motionY = dataY; + motionData.motionZ = dataZ; + gcm->mMotionDataCallback( + i, &motionData, gcm->mMotionDataCallbackUserData); + return; + } + } + } + } + } +} + +void GameControllerManager::onMouseConnection(const int32_t deviceId) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + for (size_t i = 0; i < MAX_MOUSE_DEVICES; ++i) { + if (gcm->mMouseDeviceIds[i] == INVALID_MOUSE_ID) { + gcm->mMouseDeviceIds[i] = deviceId; + break; + } + } + if (gcm->mMouseStatus != PADDLEBOAT_MOUSE_PHYSICAL) { + gcm->mMouseStatus = PADDLEBOAT_MOUSE_PHYSICAL; + if (gcm->mMouseCallback != nullptr) { + gcm->mMouseCallback(gcm->mMouseStatus, + gcm->mMouseCallbackUserData); + } + } + } +} + +void GameControllerManager::onMouseDisconnection(const int32_t deviceId) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + int mouseDeviceCount = 0; + for (size_t i = 0; i < MAX_MOUSE_DEVICES; ++i) { + if (gcm->mMouseDeviceIds[i] == deviceId) { + gcm->mMouseDeviceIds[i] = INVALID_MOUSE_ID; + } else if (gcm->mMouseDeviceIds[i] != INVALID_MOUSE_ID) { + ++mouseDeviceCount; + } + } + + // If no other physical mice are connected, see if we downgrade to + // a controller virtual mouse or no mouse at all + if (mouseDeviceCount == 0) { + gcm->mMouseStatus = (gcm->mMouseControllerIndex == INVALID_MOUSE_ID) + ? PADDLEBOAT_MOUSE_NONE + : PADDLEBOAT_MOUSE_CONTROLLER_EMULATED; + if (gcm->mMouseCallback != nullptr) { + gcm->mMouseCallback(gcm->mMouseStatus, + gcm->mMouseCallbackUserData); + } + } + } +} + +// Make sure the 'virtual' mouse is the first active controller index +// which supports a virtual pointer. If no mouse is currently active, +// upgrade to a virtual mouse and send a mouse status callback. +// If only a virtual mouse was active, and it vanished, set no mouse and +// send a mouse status callback. +void GameControllerManager::rescanVirtualMouseControllers() { + mMouseControllerIndex = INVALID_MOUSE_ID; + for (size_t i = 0; i < PADDLEBOAT_MAX_CONTROLLERS; ++i) { + if (mGameControllers[i].getControllerStatus() == + PADDLEBOAT_CONTROLLER_ACTIVE) { + if ((mGameControllers[i].getControllerInfo().controllerFlags & + PADDLEBOAT_CONTROLLER_FLAG_VIRTUAL_MOUSE) != 0) { + mMouseControllerIndex = i; + if (mMouseStatus == PADDLEBOAT_MOUSE_NONE) { + mMouseStatus = PADDLEBOAT_MOUSE_CONTROLLER_EMULATED; + if (mMouseCallback != nullptr) { + mMouseCallback(mMouseStatus, mMouseCallbackUserData); + } + } + break; + } + } + } + + // If no virtual mouse exists, downgrade to no mouse and send a mouse status + // callback. + if (mMouseControllerIndex == INVALID_MOUSE_ID && + mMouseStatus == PADDLEBOAT_MOUSE_CONTROLLER_EMULATED) { + mMouseStatus = PADDLEBOAT_MOUSE_NONE; + if (mMouseCallback != nullptr) { + mMouseCallback(mMouseStatus, mMouseCallbackUserData); + } + } +} + +void GameControllerManager::setMotionDataCallback( + Paddleboat_MotionDataCallback motionDataCallback, void *userData) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + gcm->mMotionDataCallback = motionDataCallback; + gcm->mMotionDataCallbackUserData = userData; + } +} + +void GameControllerManager::setMouseStatusCallback( + Paddleboat_MouseStatusCallback statusCallback, void *userData) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + gcm->mMouseCallback = statusCallback; + gcm->mMouseCallbackUserData = userData; + } +} + +jclass GameControllerManager::getGameControllerClass() { + GameControllerManager *gcm = getInstance(); + if (!gcm) { + return NULL; + } + return gcm->mGameControllerClass; +} + +jobject GameControllerManager::getGameControllerObject() { + GameControllerManager *gcm = getInstance(); + if (!gcm) { + return NULL; + } + return gcm->mGameControllerObject; +} + +bool GameControllerManager::getBackButtonConsumed() { + GameControllerManager *gcm = getInstance(); + if (gcm) { + return gcm->mBackButtonConsumed; + } + return false; +} + +void GameControllerManager::setBackButtonConsumed(bool consumed) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + gcm->mBackButtonConsumed = consumed; + } +} + +void GameControllerManager::setControllerStatusCallback( + Paddleboat_ControllerStatusCallback statusCallback, void *userData) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + gcm->mStatusCallback = statusCallback; + gcm->mStatusCallbackUserData = userData; + } +} + +// device debug helper function +int32_t GameControllerManager::getLastKeycode() { + GameControllerManager *gcm = getInstance(); + if (gcm) { + return gcm->mLastKeyEventKeyCode; + } + return 0; +} + +void GameControllerManager::onStop(JNIEnv *env) { + GameControllerManager *gcm = getInstance(); + if (!gcm) { + return; + } + if (gcm->mGameControllerObject != NULL) { + jmethodID onPauseID = + env->GetMethodID(gcm->mGameControllerClass, GCM_ONSTOP_METHOD_NAME, + VOID_METHOD_SIGNATURE); + if (onPauseID != NULL) { + env->CallVoidMethod(gcm->mGameControllerObject, onPauseID); + } + } +} + +void GameControllerManager::onStart(JNIEnv *env) { + GameControllerManager *gcm = getInstance(); + if (!gcm) { + return; + } + if (gcm->mGameControllerObject != NULL) { + jmethodID onResumeID = + env->GetMethodID(gcm->mGameControllerClass, GCM_ONSTART_METHOD_NAME, + VOID_METHOD_SIGNATURE); + if (onResumeID != NULL) { + env->CallVoidMethod(gcm->mGameControllerObject, onResumeID); + } + } +} + +void GameControllerManager::updateBattery(JNIEnv *env) { + if (mBatteryWait <= 0) { + mBatteryWait = BATTERY_REFRESH_WAIT; + + for (size_t i = 0; i < PADDLEBOAT_MAX_CONTROLLERS; ++i) { + if (mGameControllers[i].getControllerStatus() == + PADDLEBOAT_CONTROLLER_ACTIVE) { + const Paddleboat_Controller_Info &controllerInfo = + mGameControllers[i].getControllerInfo(); + if ((controllerInfo.controllerFlags & + PADDLEBOAT_CONTROLLER_FLAG_BATTERY) != 0) { + Paddleboat_Controller_Data &controllerData = + mGameControllers[i].getControllerData(); + const jint deviceId = controllerInfo.deviceId; + jfloat batteryLevel = env->CallFloatMethod( + mGameControllerObject, mGetBatteryLevelMethodId, + deviceId); + jint batteryStatus = + env->CallIntMethod(mGameControllerObject, + mGetBatteryStatusMethodId, deviceId); + controllerData.battery.batteryLevel = batteryLevel; + // Java 'enum' starts at 1, not 0. + controllerData.battery.batteryStatus = + static_cast(batteryStatus - + 1); + } + } + } + } else { + --mBatteryWait; + } +} + +void GameControllerManager::updateMouseDataTimestamp() { + const auto timestamp = + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); + mMouseData.timestamp = static_cast(timestamp); +} + +void GameControllerManager::addControllerRemapData( + const Paddleboat_Remap_Addition_Mode addMode, + const int32_t remapTableEntryCount, + const Paddleboat_Controller_Mapping_Data *mappingData) { + GameControllerManager *gcm = getInstance(); + if (gcm) { + if (addMode == PADDLEBOAT_REMAP_ADD_MODE_REPLACE_ALL) { + gcm->mRemapEntryCount = + (remapTableEntryCount < MAX_REMAP_TABLE_SIZE) + ? remapTableEntryCount + : MAX_REMAP_TABLE_SIZE; + const size_t copySize = gcm->mRemapEntryCount * + sizeof(Paddleboat_Controller_Mapping_Data); + memcpy(gcm->mMappingTable, mappingData, copySize); + } else if (addMode == PADDLEBOAT_REMAP_ADD_MODE_DEFAULT) { + for (int32_t i = 0; i < remapTableEntryCount; ++i) { + MappingTableSearch mapSearch(&gcm->mMappingTable[0], + gcm->mRemapEntryCount); + mapSearch.initSearchParameters( + mappingData[i].vendorId, mappingData[i].productId, + mappingData[i].minimumEffectiveApiLevel, + mappingData[i].maximumEffectiveApiLevel); + GameControllerMappingUtils::findMatchingMapEntry(&mapSearch); + bool success = GameControllerMappingUtils::insertMapEntry( + &mappingData[i], &mapSearch); + if (!success) { + break; + } + gcm->mRemapEntryCount += 1; + } + } + } +} + +int32_t GameControllerManager::getControllerRemapTableData( + const int32_t destRemapTableEntryCount, + Paddleboat_Controller_Mapping_Data *mappingData) { + GameControllerManager *gcm = getInstance(); + if (!gcm) { + return 0; + } + if (mappingData != nullptr) { + size_t copySize = (gcm->mRemapEntryCount < destRemapTableEntryCount) + ? gcm->mRemapEntryCount + : destRemapTableEntryCount; + copySize *= sizeof(Paddleboat_Controller_Mapping_Data); + memcpy(mappingData, gcm->mMappingTable, copySize); + } + return gcm->mRemapEntryCount; +} + +const Paddleboat_Controller_Mapping_Data * +GameControllerManager::getMapForController( + const GameController &gameController) { + const Paddleboat_Controller_Mapping_Data *returnMap = nullptr; + const GameControllerDeviceInfo &deviceInfo = gameController.getDeviceInfo(); + MappingTableSearch mapSearch(&mMappingTable[0], mRemapEntryCount); + mapSearch.initSearchParameters(deviceInfo.getInfo().mVendorId, + deviceInfo.getInfo().mProductId, mApiLevel, + mApiLevel); + bool success = GameControllerMappingUtils::findMatchingMapEntry(&mapSearch); + if (success) { + returnMap = &mapSearch.mappingRoot[mapSearch.tableIndex]; + } + return returnMap; +} +} // namespace paddleboat diff --git a/sources/android-gamesdk/GameController/GameControllerManager.h b/sources/android-gamesdk/GameController/GameControllerManager.h new file mode 100644 index 00000000..baeceaa2 --- /dev/null +++ b/sources/android-gamesdk/GameController/GameControllerManager.h @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +#include "GameController.h" +#include "ThreadUtil.h" + +namespace paddleboat { + +class GameControllerManager { + private: + // Allows construction with std::unique_ptr from a static method, but + // disallows construction outside of the class since no one else can + // construct a ConstructorTag + struct ConstructorTag {}; + + static constexpr int32_t MAX_MOUSE_DEVICES = 2; + static constexpr int32_t INVALID_MOUSE_ID = -1; + static constexpr int32_t MAX_REMAP_TABLE_SIZE = 256; + + // Assuming update is getting called at 60Hz, wait one minute in between + // checking battery status + static constexpr int32_t BATTERY_REFRESH_WAIT = 60 * 60; + + public: + GameControllerManager(JNIEnv *env, jobject jcontext, ConstructorTag); + + ~GameControllerManager(); + + static inline int32_t getRemapTableSize() { return MAX_REMAP_TABLE_SIZE; } + + static Paddleboat_ErrorCode init(JNIEnv *env, jobject jcontext); + + static void destroyInstance(JNIEnv *env); + + static bool isInitialized(); + + // Get/Set whether AKEYCODE_BACK is 'eaten' or allowed to pass through to + // the system This can be used to block the OS backing out of the game, or + // allowing it if the game is in an appropriate state (i.e. the title + // screen) + static bool getBackButtonConsumed(); + + static void setBackButtonConsumed(bool consumed); + + static Paddleboat_ErrorCode setControllerLight( + const int32_t controllerIndex, const Paddleboat_LightType lightType, + const uint32_t lightData, JNIEnv *env); + + static void setControllerStatusCallback( + Paddleboat_ControllerStatusCallback statusCallback, void *userData); + + static void setMotionDataCallback( + Paddleboat_MotionDataCallback motionDataCallback, void *userData); + + static void setMouseStatusCallback( + Paddleboat_MouseStatusCallback statusCallback, void *userData); + + static void onStop(JNIEnv *env); + + static void onStart(JNIEnv *env); + + static void update(JNIEnv *env); + + static Paddleboat_ErrorCode getControllerData( + const int32_t controllerIndex, + Paddleboat_Controller_Data *controllerData); + + static Paddleboat_ErrorCode getControllerInfo( + const int32_t controllerIndex, Paddleboat_Controller_Info *deviceInfo); + + static Paddleboat_ErrorCode getControllerName(const int32_t controllerIndex, + const size_t bufferSize, + char *controllerName); + + static Paddleboat_ControllerStatus getControllerStatus( + const int32_t controllerIndex); + + static Paddleboat_ErrorCode setControllerVibrationData( + const int32_t controllerIndex, + const Paddleboat_Vibration_Data *vibrationData, JNIEnv *env); + + static Paddleboat_ErrorCode getMouseData(Paddleboat_Mouse_Data *mouseData); + + static Paddleboat_MouseStatus getMouseStatus(); + + static int32_t processInputEvent(const AInputEvent *event); + + static int32_t processGameActivityKeyInputEvent(const void *event, + const size_t eventSize); + + static int32_t processGameActivityMotionInputEvent(const void *event, + const size_t eventSize); + + static uint64_t getActiveAxisMask(); + + static void addControllerRemapData( + const Paddleboat_Remap_Addition_Mode addMode, + const int32_t remapTableEntryCount, + const Paddleboat_Controller_Mapping_Data *mappingData); + + static int32_t getControllerRemapTableData( + const int32_t destRemapTableEntryCount, + Paddleboat_Controller_Mapping_Data *mappingData); + + // Called from the JNI bridge functions + static GameControllerDeviceInfo *onConnection(); + + static void onDisconnection(const int32_t deviceId); + + static void onMotionData(const int32_t deviceId, const int32_t motionType, + const uint64_t timestamp, const float dataX, + const float dataY, const float dataZ); + + static void onMouseConnection(const int32_t deviceId); + + static void onMouseDisconnection(const int32_t deviceId); + + static jclass getGameControllerClass(); + + static jobject getGameControllerObject(); + + // device debug helper function + static int32_t getLastKeycode(); + + private: + static GameControllerManager *getInstance(); + + Paddleboat_ErrorCode initMethods(JNIEnv *env); + + bool isLightTypeSupported(const Paddleboat_Controller_Info &controllerInfo, + const Paddleboat_LightType lightType); + + int32_t processControllerKeyEvent(const AInputEvent *event, + GameController &gameController); + + int32_t processControllerGameActivityKeyEvent( + const Paddleboat_GameActivityKeyEvent *event, const size_t eventSize, + GameController &gameController); + + int32_t processMouseEvent(const AInputEvent *event); + + int32_t processGameActivityMouseEvent(const void *event, + const size_t eventSize, + const int32_t eventDeviceId); + + void rescanVirtualMouseControllers(); + + void updateBattery(JNIEnv *env); + + void updateMouseDataTimestamp(); + + void releaseGlobals(JNIEnv *env); + + const Paddleboat_Controller_Mapping_Data *getMapForController( + const GameController &gameController); + + bool mInitialized = false; + bool mGCMClassInitialized = false; + bool mBackButtonConsumed = true; + bool mMotionEventReporting = false; + + int32_t mApiLevel = 16; + int32_t mBatteryWait = BATTERY_REFRESH_WAIT; + jobject mContext = NULL; + jclass mGameControllerClass = NULL; + jobject mGameControllerObject = NULL; + jmethodID mInitMethodId = NULL; + jmethodID mGetApiLevelMethodId = NULL; + jmethodID mGetBatteryLevelMethodId = NULL; + jmethodID mGetBatteryStatusMethodId = NULL; + jmethodID mSetLightMethodId = NULL; + jmethodID mSetNativeReadyMethodId = NULL; + jmethodID mSetReportMotionEventsMethodId = NULL; + jmethodID mSetVibrationMethodId = NULL; + + uint64_t mActiveAxisMask = 0; + + int32_t mRemapEntryCount = 0; + Paddleboat_Controller_Mapping_Data mMappingTable[MAX_REMAP_TABLE_SIZE]; + + Paddleboat_MotionDataCallback mMotionDataCallback = nullptr; + void *mMotionDataCallbackUserData = nullptr; + + GameController mGameControllers[PADDLEBOAT_MAX_CONTROLLERS]; + Paddleboat_ControllerStatusCallback mStatusCallback = nullptr; + void *mStatusCallbackUserData = nullptr; + // device debug helper + int32_t mLastKeyEventKeyCode = 0; + + Paddleboat_MouseStatus mMouseStatus = PADDLEBOAT_MOUSE_NONE; + int32_t mMouseDeviceIds[MAX_MOUSE_DEVICES] = {INVALID_MOUSE_ID, + INVALID_MOUSE_ID}; + int32_t mMouseControllerIndex = INVALID_MOUSE_ID; + Paddleboat_Mouse_Data mMouseData; + Paddleboat_MouseStatusCallback mMouseCallback = nullptr; + void *mMouseCallbackUserData = nullptr; + + std::mutex mUpdateMutex; + static std::mutex sInstanceMutex; + static std::unique_ptr sInstance + GUARDED_BY(sInstanceMutex); +}; +} // namespace paddleboat diff --git a/sources/android-gamesdk/GameController/GameControllerMappingUtils.cpp b/sources/android-gamesdk/GameController/GameControllerMappingUtils.cpp new file mode 100644 index 00000000..ca09e8d9 --- /dev/null +++ b/sources/android-gamesdk/GameController/GameControllerMappingUtils.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "GameControllerMappingUtils.h" + +#include "GameControllerManager.h" + +namespace paddleboat { +MappingTableSearch::MappingTableSearch() + : mappingRoot(nullptr), + vendorId(0), + productId(0), + minApi(0), + maxApi(0), + tableIndex(0), + mapEntryCount(0), + tableEntryCount(0), + tableMaxEntryCount(GameControllerManager::getRemapTableSize()) {} + +MappingTableSearch::MappingTableSearch( + Paddleboat_Controller_Mapping_Data *mapRoot, int32_t entryCount) + : mappingRoot(mapRoot), + vendorId(0), + productId(0), + minApi(0), + maxApi(0), + tableIndex(0), + mapEntryCount(0), + tableEntryCount(entryCount), + tableMaxEntryCount(GameControllerManager::getRemapTableSize()) {} + +void MappingTableSearch::initSearchParameters(const int32_t newVendorId, + const int32_t newProductId, + const int32_t newMinApi, + const int32_t newMaxApi) { + vendorId = newVendorId; + productId = newProductId; + minApi = newMinApi; + maxApi = newMaxApi; + tableIndex = 0; +} + +bool GameControllerMappingUtils::findMatchingMapEntry( + MappingTableSearch *searchEntry) { + int32_t currentIndex = 0; + + // Starting out with a linear search. Updating the map table is something + // that should only ever be done once at startup, if it actually takes an + // appreciable time to execute when working with a big remap dictionary, + // this is low-hanging fruit to optimize. + const Paddleboat_Controller_Mapping_Data *mapRoot = + searchEntry->mappingRoot; + while (currentIndex < searchEntry->tableEntryCount) { + const Paddleboat_Controller_Mapping_Data &mapEntry = + mapRoot[currentIndex]; + if (mapEntry.vendorId > searchEntry->vendorId) { + // Passed by the search vendorId value, so we don't already exist in + // the table, set the current index as the insert point and bail + searchEntry->tableIndex = currentIndex; + return false; + } else if (searchEntry->vendorId == mapEntry.vendorId) { + if (mapEntry.productId > searchEntry->productId) { + // Passed by the search productId value, so we don't already + // exist in the table, set the current index as the insert point + // and bail + searchEntry->tableIndex = currentIndex; + return false; + } else if (searchEntry->productId == mapEntry.productId) { + // Any overlap of the min/max API range is treated as matching + // an existing entry + if ((searchEntry->minApi >= mapEntry.minimumEffectiveApiLevel && + searchEntry->minApi <= + mapEntry.maximumEffectiveApiLevel) || + (searchEntry->minApi >= mapEntry.minimumEffectiveApiLevel && + mapEntry.maximumEffectiveApiLevel == 0)) { + searchEntry->tableIndex = currentIndex; + return true; + } + } + } + ++currentIndex; + } + searchEntry->tableIndex = currentIndex; + return false; +} + +bool GameControllerMappingUtils::insertMapEntry( + const Paddleboat_Controller_Mapping_Data *mappingData, + MappingTableSearch *searchEntry) { + bool insertSuccess = false; + // Verify there is room in the table for another entry + if (searchEntry->tableEntryCount < searchEntry->tableMaxEntryCount && + searchEntry->tableIndex < searchEntry->tableMaxEntryCount) { + // Empty table, or inserting at the end, no relocation of existing data + // required, otherwise shift existing data down starting at the insert + // index. + if (!(searchEntry->tableEntryCount == 0 || + searchEntry->tableIndex == searchEntry->tableEntryCount)) { + const size_t copySize = + (searchEntry->tableEntryCount - searchEntry->tableIndex) * + sizeof(Paddleboat_Controller_Mapping_Data); + memmove(&searchEntry->mappingRoot[searchEntry->tableIndex + 1], + &searchEntry->mappingRoot[searchEntry->tableIndex], + copySize); + } + // Insert the new data + memcpy(&searchEntry->mappingRoot[searchEntry->tableIndex], mappingData, + sizeof(Paddleboat_Controller_Mapping_Data)); + insertSuccess = true; + } + return insertSuccess; +} + +const Paddleboat_Controller_Mapping_Data * +GameControllerMappingUtils::validateMapTable( + const Paddleboat_Controller_Mapping_Data *mappingRoot, + const int32_t tableEntryCount) { + // The map table is always assumed to be sorted by increasing vendorId. Each + // sequence of entries with the same vendorId are sorted by increasing + // productId. Multiple entries with the same vendorId/productId range are + // sorted by increasing min/max API ranges. vendorId + // productId + // minApi + int32_t currentIndex = 0; + int32_t previousVendorId = -1; + while (currentIndex < tableEntryCount) { + if (mappingRoot[currentIndex].vendorId < previousVendorId) { + // failure in vendorId order, return the offending entry + return &mappingRoot[currentIndex]; + } + + int32_t previousProductId = mappingRoot[currentIndex].productId; + int32_t previousMinApi = + mappingRoot[currentIndex].minimumEffectiveApiLevel; + int32_t previousMaxApi = + mappingRoot[currentIndex].maximumEffectiveApiLevel; + previousVendorId = mappingRoot[currentIndex++].vendorId; + + while (currentIndex < tableEntryCount && + mappingRoot[currentIndex].vendorId == previousVendorId) { + while (currentIndex < tableEntryCount && + mappingRoot[currentIndex].productId == previousProductId) { + if (mappingRoot[currentIndex].minimumEffectiveApiLevel < + previousMinApi || + mappingRoot[currentIndex].minimumEffectiveApiLevel < + previousMaxApi) { + // failure in API order, return the offending entry + return &mappingRoot[currentIndex]; + } + previousMinApi = + mappingRoot[currentIndex].minimumEffectiveApiLevel; + previousMaxApi = + mappingRoot[currentIndex++].maximumEffectiveApiLevel; + } + if (mappingRoot[currentIndex].productId < previousProductId) { + // failure in productId order, return the offending entry + return &mappingRoot[currentIndex]; + } + previousProductId = mappingRoot[currentIndex++].productId; + } + } + + // Validation success, return nullptr (no offending entries to return) + return nullptr; +} +} // namespace paddleboat diff --git a/sources/android-gamesdk/GameController/GameControllerMappingUtils.h b/sources/android-gamesdk/GameController/GameControllerMappingUtils.h new file mode 100644 index 00000000..dbd53287 --- /dev/null +++ b/sources/android-gamesdk/GameController/GameControllerMappingUtils.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#ifndef PADDLEBOAT_H +#include "paddleboat.h" +#endif + +namespace paddleboat { +class MappingTableSearch { + public: + MappingTableSearch(); + + MappingTableSearch(Paddleboat_Controller_Mapping_Data *mapRoot, + int32_t entryCount); + + void initSearchParameters(const int32_t newVendorId, + const int32_t newProductId, + const int32_t newMinApi, const int32_t newMaxApi); + + Paddleboat_Controller_Mapping_Data *mappingRoot; + int32_t vendorId; + int32_t productId; + int32_t minApi; + int32_t maxApi; + int32_t tableIndex; + int32_t mapEntryCount; + int32_t tableEntryCount; + int32_t tableMaxEntryCount; +}; + +class GameControllerMappingUtils { + public: + static bool findMatchingMapEntry(MappingTableSearch *searchEntry); + + static bool insertMapEntry( + const Paddleboat_Controller_Mapping_Data *mappingData, + MappingTableSearch *searchEntry); + + static const Paddleboat_Controller_Mapping_Data *validateMapTable( + const Paddleboat_Controller_Mapping_Data *mappingRoot, + const int32_t tableEntryCount); +}; +} // namespace paddleboat diff --git a/sources/android-gamesdk/GameController/InternalControllerTable.cpp b/sources/android-gamesdk/GameController/InternalControllerTable.cpp new file mode 100644 index 00000000..e970377c --- /dev/null +++ b/sources/android-gamesdk/GameController/InternalControllerTable.cpp @@ -0,0 +1,1005 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "InternalControllerTable.h" + +#include +#include + +#define ARRAY_COUNTOF(array) (sizeof(array) / sizeof(array[0])) + +#define PADDLEBOAT_AXIS_BUTTON_DPAD_UP 0 +#define PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT 1 +#define PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN 2 +#define PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT 3 +#define PADDLEBOAT_AXIS_BUTTON_L2 9 +#define PADDLEBOAT_AXIS_BUTTON_R2 12 + +namespace paddleboat { +static const Paddleboat_Controller_Mapping_Data pb_internal_controller_map[] = { + + // Microsoft Xbox 360 controller (usb) + {16, + 0, + 0x0045e, + 0x028e, + PADDLEBOAT_CONTROLLER_LAYOUT_STANDARD, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_BRAKE, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_GAS, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_L2, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_R2, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_A, + /* B */ AKEYCODE_BUTTON_B, + /* X */ AKEYCODE_BUTTON_X, + /* Y */ AKEYCODE_BUTTON_Y, + /* L1 */ AKEYCODE_BUTTON_L1, + /* L2 */ AKEYCODE_BUTTON_L2, + /* L3 */ AKEYCODE_BUTTON_THUMBL, + /* R1 */ AKEYCODE_BUTTON_R1, + /* R2 */ AKEYCODE_BUTTON_R2, + /* R3 */ AKEYCODE_BUTTON_THUMBR, + /* SELECT */ AKEYCODE_BUTTON_SELECT, + /* START */ AKEYCODE_BUTTON_START, + /* SYSTEM */ AKEYCODE_BUTTON_MODE, + /* TOUCHP */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX1 */ AKEYCODE_MEDIA_RECORD, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}, + // Microsoft Xbox One non-BT controller (usb) + {16, + 0, + 0x0045e, + 0x02d1, + PADDLEBOAT_CONTROLLER_LAYOUT_STANDARD, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_BRAKE, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_GAS, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_L2, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_R2, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_A, + /* B */ AKEYCODE_BUTTON_B, + /* X */ AKEYCODE_BUTTON_X, + /* Y */ AKEYCODE_BUTTON_Y, + /* L1 */ AKEYCODE_BUTTON_L1, + /* L2 */ AKEYCODE_BUTTON_L2, + /* L3 */ AKEYCODE_BUTTON_THUMBL, + /* R1 */ AKEYCODE_BUTTON_R1, + /* R2 */ AKEYCODE_BUTTON_R2, + /* R3 */ AKEYCODE_BUTTON_THUMBR, + /* SELECT */ AKEYCODE_BUTTON_SELECT, + /* START */ AKEYCODE_BUTTON_START, + /* SYSTEM */ AKEYCODE_BUTTON_MODE, + /* TOUCHP */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX1 */ AKEYCODE_MEDIA_RECORD, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}, + // 8BitDo Arcade Stick, XInput mode, Bluetooth + {16, + 0, + 0x0045e, + 0x02e0, + PADDLEBOAT_CONTROLLER_LAYOUT_ARCADE_STICK, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_BRAKE, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_GAS, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* LY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_L2, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_R2, + /* HX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* LY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_A, + /* B */ AKEYCODE_BUTTON_B, + /* X */ AKEYCODE_BUTTON_X, + /* Y */ AKEYCODE_BUTTON_Y, + /* L1 */ AKEYCODE_BUTTON_L1, + /* L2 */ AKEYCODE_BUTTON_L2, + /* L3 */ PADDLEBOAT_BUTTON_IGNORED, + /* R1 */ AKEYCODE_BUTTON_R1, + /* R2 */ AKEYCODE_BUTTON_R2, + /* R3 */ PADDLEBOAT_BUTTON_IGNORED, + /* SELECT */ AKEYCODE_BUTTON_SELECT, + /* START */ AKEYCODE_BUTTON_START, + /* SYSTEM */ AKEYCODE_BUTTON_MODE, + /* TOUCHP */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX1 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}, + // Microsoft Xbox One BT controller (usb) + {16, + 0, + 0x0045e, + 0x02ea, + PADDLEBOAT_CONTROLLER_LAYOUT_STANDARD, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_BRAKE, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_GAS, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_L2, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_R2, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_A, + /* B */ AKEYCODE_BUTTON_B, + /* X */ AKEYCODE_BUTTON_X, + /* Y */ AKEYCODE_BUTTON_Y, + /* L1 */ AKEYCODE_BUTTON_L1, + /* L2 */ AKEYCODE_BUTTON_L2, + /* L3 */ AKEYCODE_BUTTON_THUMBL, + /* R1 */ AKEYCODE_BUTTON_R1, + /* R2 */ AKEYCODE_BUTTON_R2, + /* R3 */ AKEYCODE_BUTTON_THUMBR, + /* SELECT */ AKEYCODE_BUTTON_SELECT, + /* START */ AKEYCODE_BUTTON_START, + /* SYSTEM */ AKEYCODE_BUTTON_MODE, + /* TOUCHP */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX1 */ AKEYCODE_MEDIA_RECORD, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}, + // Microsoft Xbox One BT controller (bluetooth) + {16, + 0, + 0x0045e, + 0x02fd, + PADDLEBOAT_CONTROLLER_LAYOUT_STANDARD, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_BRAKE, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_GAS, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_L2, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_R2, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_A, + /* B */ AKEYCODE_BUTTON_B, + /* X */ AKEYCODE_BUTTON_X, + /* Y */ AKEYCODE_BUTTON_Y, + /* L1 */ AKEYCODE_BUTTON_L1, + /* L2 */ AKEYCODE_BUTTON_L2, + /* L3 */ AKEYCODE_BUTTON_THUMBL, + /* R1 */ AKEYCODE_BUTTON_R1, + /* R2 */ AKEYCODE_BUTTON_R2, + /* R3 */ AKEYCODE_BUTTON_THUMBR, + /* SELECT */ AKEYCODE_BUTTON_SELECT, + /* START */ AKEYCODE_BUTTON_START, + /* SYSTEM */ AKEYCODE_BUTTON_MODE, + /* TOUCHP */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX1 */ AKEYCODE_MEDIA_RECORD, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}, + // Microsoft Xbox Series controller (usb) + {16, + 0, + 0x0045e, + 0x0b12, + PADDLEBOAT_CONTROLLER_LAYOUT_STANDARD, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_BRAKE, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_GAS, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_L2, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_R2, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_A, + /* B */ AKEYCODE_BUTTON_B, + /* X */ AKEYCODE_BUTTON_X, + /* Y */ AKEYCODE_BUTTON_Y, + /* L1 */ AKEYCODE_BUTTON_L1, + /* L2 */ AKEYCODE_BUTTON_L2, + /* L3 */ AKEYCODE_BUTTON_THUMBL, + /* R1 */ AKEYCODE_BUTTON_R1, + /* R2 */ AKEYCODE_BUTTON_R2, + /* R3 */ AKEYCODE_BUTTON_THUMBR, + /* SELECT */ AKEYCODE_BUTTON_SELECT, + /* START */ AKEYCODE_BUTTON_START, + /* SYSTEM */ AKEYCODE_BUTTON_MODE, + /* TOUCHP */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX1 */ AKEYCODE_MEDIA_RECORD, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}, + // Microsoft Xbox Series controller (bluetooth) + {16, + 0, + 0x0045e, + 0x0b13, + PADDLEBOAT_CONTROLLER_LAYOUT_STANDARD, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_BRAKE, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_GAS, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_L2, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_R2, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_A, + /* B */ AKEYCODE_BUTTON_B, + /* X */ AKEYCODE_BUTTON_X, + /* Y */ AKEYCODE_BUTTON_Y, + /* L1 */ AKEYCODE_BUTTON_L1, + /* L2 */ AKEYCODE_BUTTON_L2, + /* L3 */ AKEYCODE_BUTTON_THUMBL, + /* R1 */ AKEYCODE_BUTTON_R1, + /* R2 */ AKEYCODE_BUTTON_R2, + /* R3 */ AKEYCODE_BUTTON_THUMBR, + /* SELECT */ AKEYCODE_BUTTON_SELECT, + /* START */ AKEYCODE_BUTTON_START, + /* SYSTEM */ AKEYCODE_BUTTON_MODE, + /* TOUCHP */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX1 */ AKEYCODE_MEDIA_RECORD, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}, + + // Sony PlayStation 4 controller (usb/bluetooth) + {16, + 0, + 0x054C, + 0x05C4, + PADDLEBOAT_CONTROLLER_LAYOUT_SHAPES | PADDLEBOAT_CONTROLLER_FLAG_TOUCHPAD, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_BRAKE, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_GAS, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_L2, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_R2, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_A, + /* B */ AKEYCODE_BUTTON_B, + /* X */ AKEYCODE_BUTTON_X, + /* Y */ AKEYCODE_BUTTON_Y, + /* L1 */ AKEYCODE_BUTTON_L1, + /* L2 */ AKEYCODE_BUTTON_L2, + /* L3 */ AKEYCODE_BUTTON_THUMBL, + /* R1 */ AKEYCODE_BUTTON_R1, + /* R2 */ AKEYCODE_BUTTON_R2, + /* R3 */ AKEYCODE_BUTTON_THUMBR, + /* SELECT */ AKEYCODE_BUTTON_SELECT, + /* START */ AKEYCODE_BUTTON_START, + /* SYSTEM */ AKEYCODE_BUTTON_MODE, + /* TOUCHP */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX1 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}, + // Sony PlayStation 4 controller (usb/bluetooth) - alternate deviceId + {16, + 0, + 0x054C, + 0x09CC, + PADDLEBOAT_CONTROLLER_LAYOUT_SHAPES | PADDLEBOAT_CONTROLLER_FLAG_TOUCHPAD, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_BRAKE, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_GAS, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_L2, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_R2, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_A, + /* B */ AKEYCODE_BUTTON_B, + /* X */ AKEYCODE_BUTTON_X, + /* Y */ AKEYCODE_BUTTON_Y, + /* L1 */ AKEYCODE_BUTTON_L1, + /* L2 */ AKEYCODE_BUTTON_L2, + /* L3 */ AKEYCODE_BUTTON_THUMBL, + /* R1 */ AKEYCODE_BUTTON_R1, + /* R2 */ AKEYCODE_BUTTON_R2, + /* R3 */ AKEYCODE_BUTTON_THUMBR, + /* SELECT */ AKEYCODE_BUTTON_SELECT, + /* START */ AKEYCODE_BUTTON_START, + /* SYSTEM */ AKEYCODE_BUTTON_MODE, + /* TOUCHP */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX1 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}, + // Sony PlayStation 5 controller (usb/bluetooth) API <= 30 + {16, + 30, + 0x054C, + 0x0ce6, + PADDLEBOAT_CONTROLLER_LAYOUT_SHAPES, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_RX, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_RY, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_L2, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_R2, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_B, + /* B */ AKEYCODE_BUTTON_C, + /* X */ AKEYCODE_BUTTON_A, + /* Y */ AKEYCODE_BUTTON_X, + /* L1 */ AKEYCODE_BUTTON_Y, + /* L2 */ PADDLEBOAT_BUTTON_IGNORED, + /* L3 */ AKEYCODE_BUTTON_SELECT, + /* R1 */ AKEYCODE_BUTTON_Z, + /* R2 */ PADDLEBOAT_BUTTON_IGNORED, + /* R3 */ AKEYCODE_BUTTON_START, + /* SELECT */ AKEYCODE_BUTTON_L2, + /* START */ AKEYCODE_BUTTON_R2, + /* SYSTEM */ AKEYCODE_BUTTON_MODE, + /* TOUCHP */ AKEYCODE_BUTTON_THUMBL, + /* AUX1 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}, + // Sony PlayStation 5 controller (usb/bluetooth) API >= 31 + {31, + 0, + 0x054C, + 0x0ce6, + PADDLEBOAT_CONTROLLER_LAYOUT_SHAPES | PADDLEBOAT_CONTROLLER_FLAG_TOUCHPAD, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_BRAKE, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_GAS, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_L2, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_R2, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_A, + /* B */ AKEYCODE_BUTTON_B, + /* X */ AKEYCODE_BUTTON_X, + /* Y */ AKEYCODE_BUTTON_Y, + /* L1 */ AKEYCODE_BUTTON_L1, + /* L2 */ AKEYCODE_BUTTON_L2, + /* L3 */ AKEYCODE_BUTTON_THUMBL, + /* R1 */ AKEYCODE_BUTTON_R1, + /* R2 */ AKEYCODE_BUTTON_R2, + /* R3 */ AKEYCODE_BUTTON_THUMBR, + /* SELECT */ AKEYCODE_BUTTON_SELECT, + /* START */ AKEYCODE_BUTTON_START, + /* SYSTEM */ AKEYCODE_BUTTON_MODE, + /* TOUCHP */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX1 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}, + // Nintendo Switch Pro controller (usb/bluetooth) + {16, + 0, + 0x0057e, + 0x2009, + PADDLEBOAT_CONTROLLER_LAYOUT_REVERSE, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_BRAKE, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_GAS, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_L2, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_R2, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_B, + /* B */ AKEYCODE_BUTTON_A, + /* X */ AKEYCODE_BUTTON_Y, + /* Y */ AKEYCODE_BUTTON_X, + /* L1 */ AKEYCODE_BUTTON_L1, + /* L2 */ AKEYCODE_BUTTON_L2, + /* L3 */ AKEYCODE_BUTTON_THUMBL, + /* R1 */ AKEYCODE_BUTTON_R1, + /* R2 */ AKEYCODE_BUTTON_R2, + /* R3 */ AKEYCODE_BUTTON_THUMBR, + /* SELECT */ AKEYCODE_BUTTON_SELECT, + /* START */ AKEYCODE_BUTTON_START, + /* SYSTEM */ AKEYCODE_BUTTON_MODE, + /* TOUCHP */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX1 */ AKEYCODE_MEDIA_RECORD, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}, + // Nvidia Shield TV controller (usb) + {16, + 0, + 0x0955, + 0x7210, + PADDLEBOAT_CONTROLLER_LAYOUT_STANDARD, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_BRAKE, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_GAS, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_L2, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_R2, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_A, + /* B */ AKEYCODE_BUTTON_B, + /* X */ AKEYCODE_BUTTON_X, + /* Y */ AKEYCODE_BUTTON_Y, + /* L1 */ AKEYCODE_BUTTON_L1, + /* L2 */ AKEYCODE_BUTTON_L2, + /* L3 */ AKEYCODE_BUTTON_THUMBL, + /* R1 */ AKEYCODE_BUTTON_R1, + /* R2 */ AKEYCODE_BUTTON_R2, + /* R3 */ AKEYCODE_BUTTON_THUMBR, + /* SELECT */ AKEYCODE_BUTTON_SELECT, + /* START */ AKEYCODE_BUTTON_START, + /* SYSTEM */ AKEYCODE_BUTTON_MODE, + /* TOUCHP */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX1 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}, + + // Google Stadia controller (usb) + {16, + 0, + 0x18d1, + 0x9400, + PADDLEBOAT_CONTROLLER_LAYOUT_STANDARD, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_BRAKE, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_GAS, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_L2, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_R2, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_A, + /* B */ AKEYCODE_BUTTON_B, + /* X */ AKEYCODE_BUTTON_X, + /* Y */ AKEYCODE_BUTTON_Y, + /* L1 */ AKEYCODE_BUTTON_L1, + /* L2 */ AKEYCODE_BUTTON_L2, + /* L3 */ AKEYCODE_BUTTON_THUMBL, + /* R1 */ AKEYCODE_BUTTON_R1, + /* R2 */ AKEYCODE_BUTTON_R2, + /* R3 */ AKEYCODE_BUTTON_THUMBR, + /* SELECT */ AKEYCODE_BUTTON_SELECT, + /* START */ AKEYCODE_BUTTON_START, + /* SYSTEM */ AKEYCODE_BUTTON_MODE, + /* TOUCHP */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX1 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}, + // NeoGeo Pro, PC 2 setting, USB + {16, + 0, + 0x20bc, + 0x5501, + PADDLEBOAT_CONTROLLER_LAYOUT_ARCADE_STICK, + { + /* LX */ AMOTION_EVENT_AXIS_X, + /* LY */ AMOTION_EVENT_AXIS_Y, + /* RX */ AMOTION_EVENT_AXIS_Z, + /* RY */ AMOTION_EVENT_AXIS_RZ, + /* L1 */ PADDLEBOAT_AXIS_IGNORED, + /* L2 */ AMOTION_EVENT_AXIS_BRAKE, + /* R1 */ PADDLEBOAT_AXIS_IGNORED, + /* R2 */ AMOTION_EVENT_AXIS_GAS, + /* HX */ AMOTION_EVENT_AXIS_HAT_X, + /* HY */ AMOTION_EVENT_AXIS_HAT_Y, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_RIGHT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_DOWN, + }, + { + /* LX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* LY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RX */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* RY */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* L2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R1 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* R2 */ PADDLEBOAT_AXIS_BUTTON_IGNORED, + /* HX */ PADDLEBOAT_AXIS_BUTTON_DPAD_LEFT, + /* HY */ PADDLEBOAT_AXIS_BUTTON_DPAD_UP, + }, + {/* UP */ AKEYCODE_DPAD_UP, + /* LEFT */ AKEYCODE_DPAD_LEFT, + /* DOWN */ AKEYCODE_DPAD_DOWN, + /* RIGHT */ AKEYCODE_DPAD_RIGHT, + /* A */ AKEYCODE_BUTTON_A, + /* B */ AKEYCODE_BUTTON_B, + /* X */ AKEYCODE_BUTTON_X, + /* Y */ AKEYCODE_BUTTON_Y, + /* L1 */ AKEYCODE_BUTTON_L1, + /* L2 */ AKEYCODE_BUTTON_R1, + /* L3 */ PADDLEBOAT_BUTTON_IGNORED, + /* R1 */ AKEYCODE_BUTTON_Z, + /* R2 */ AKEYCODE_BUTTON_C, + /* R3 */ PADDLEBOAT_BUTTON_IGNORED, + /* SELECT */ AKEYCODE_BUTTON_SELECT, + /* START */ AKEYCODE_BUTTON_START, + /* SYSTEM */ PADDLEBOAT_BUTTON_IGNORED, + /* TOUCHP */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX1 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX2 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX3 */ PADDLEBOAT_BUTTON_IGNORED, + /* AUX4 */ PADDLEBOAT_BUTTON_IGNORED}}}; + +const Paddleboat_Controller_Mapping_Data *GetInternalControllerData() { + return pb_internal_controller_map; +} + +int32_t GetInternalControllerDataCount() { + const size_t count = ARRAY_COUNTOF(pb_internal_controller_map); + return (static_cast(count)); +} +} // namespace paddleboat \ No newline at end of file diff --git a/sources/android-gamesdk/GameController/InternalControllerTable.h b/sources/android-gamesdk/GameController/InternalControllerTable.h new file mode 100644 index 00000000..a88babd0 --- /dev/null +++ b/sources/android-gamesdk/GameController/InternalControllerTable.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "paddleboat.h" + +namespace paddleboat { +const Paddleboat_Controller_Mapping_Data *GetInternalControllerData(); + +int32_t GetInternalControllerDataCount(); +} // namespace paddleboat \ No newline at end of file diff --git a/sources/android-gamesdk/GameController/Log.h b/sources/android-gamesdk/GameController/Log.h new file mode 100644 index 00000000..5b5a780b --- /dev/null +++ b/sources/android-gamesdk/GameController/Log.h @@ -0,0 +1,40 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define ALOGW_ONCE_IF(cond, ...) \ + do { \ + static bool alogw_once##__FILE__##__LINE__##__ = true; \ + if (cond && alogw_once##__FILE__##__LINE__##__) { \ + alogw_once##__FILE__##__LINE__##__ = false; \ + ALOGW(__VA_ARGS__); \ + } \ + } while (0) + +#ifndef NDEBUG +#define ALOGV(...) \ + __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) +#else +#define ALOGV(...) +#endif diff --git a/sources/android-gamesdk/GameController/ThreadUtil.h b/sources/android-gamesdk/GameController/ThreadUtil.h new file mode 100644 index 00000000..be48fc28 --- /dev/null +++ b/sources/android-gamesdk/GameController/ThreadUtil.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +// Enable thread safety attributes only with clang. +// The attributes can be safely erased when compiling with other compilers. +#if defined(__clang__) && (!defined(SWIG)) +#define THREAD_ANNOTATION_ATTRIBUTE__(x) __attribute__((x)) +#else +#define THREAD_ANNOTATION_ATTRIBUTE__(x) // no-op +#endif + +#if !defined GAMESDK_THREAD_CHECKS +#define GAMESDK_THREAD_CHECKS 1 +#endif + +#if GAMESDK_THREAD_CHECKS +#define GUARDED_BY(x) THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x)) + +#define REQUIRES(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(requires_capability(__VA_ARGS__)) + +#define NO_THREAD_SAFETY_ANALYSIS \ + THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis) +#else +#define GUARDED_BY(x) +#define REQUIRES(...) +#define NO_THREAD_SAFETY_ANALYSIS +#endif diff --git a/sources/android-gamesdk/GameController/paddleboat/include/paddleboat.h b/sources/android-gamesdk/GameController/paddleboat/include/paddleboat.h new file mode 100644 index 00000000..f4db25a9 --- /dev/null +++ b/sources/android-gamesdk/GameController/paddleboat/include/paddleboat.h @@ -0,0 +1,1067 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * This is the main interface to the Android Game Controller library, also known + as Paddleboat. + + See the documentation at + https://developer.android.com/games/sdk/game-controller for more information + on using this library in a native Android game. + */ + +/** + * @defgroup paddleboat Game Controller main interface + * The main interface to use the Game Controller library. + * @{ + */ + +#ifndef PADDLEBOAT_H +#define PADDLEBOAT_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Maximum number of simultaneously connected controllers. + */ +#define PADDLEBOAT_MAX_CONTROLLERS 8 + +/** + * @brief Paddleboat error code results. + */ +enum Paddleboat_ErrorCode { + /** + * @brief No error. Function call was successful. + */ + PADDLEBOAT_NO_ERROR = 0, + /** + * @brief ::Paddleboat_init was called a second time without a call to + * ::Paddleboat_destroy in between. + */ + PADDLEBOAT_ERROR_ALREADY_INITIALIZED = -2000, + /** + * @brief Paddleboat was not successfully initialized. Either + * ::Paddleboat_init was not called or returned an error. + */ + PADDLEBOAT_ERROR_NOT_INITIALIZED = -2001, + /** + * @brief Paddleboat could not be successfully initialized. Instantiation + * of the GameControllerManager class failed. + */ + PADDLEBOAT_ERROR_INIT_GCM_FAILURE = -2002, + /** + * @brief Invalid controller index specified. Valid index range is from 0 + * to PADDLEBOAT_MAX_CONTROLLERS - 1 + */ + PADDLEBOAT_ERROR_INVALID_CONTROLLER_INDEX = -2003, + /** + * @brief No controller is connected at the specified controller index. + */ + PADDLEBOAT_ERROR_NO_CONTROLLER = -2004, + /** + * @brief No virtual or physical mouse device is connected. + */ + PADDLEBOAT_ERROR_NO_MOUSE = -2005, + /** + * @brief The feature is not supported by the specified controller. + * Example: Calling ::Paddleboat_setControllerVibrationData on a controller + * that does not have the `PADDLEBOAT_CONTROLLER_FLAG_VIBRATION` bit + * set in `Paddleboat_Controller_Info.controllerFlags`. + */ + PADDLEBOAT_ERROR_FEATURE_NOT_SUPPORTED = -2006, + /** + * @brief An invalid parameter was specified. This usually means NULL or + * nullptr was passed in a parameter that requires a valid pointer. + */ + PADDLEBOAT_ERROR_INVALID_PARAMETER = -2007 +}; + +/** + * @brief Paddleboat controller buttons defined as bitmask values. + * AND against `Paddleboat_Controller_Data.buttonsDown` to check for button + * status. + */ +enum Paddleboat_Buttons { + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` direction pad + * up button. + */ + PADDLEBOAT_BUTTON_DPAD_UP = (1U << 0), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` direction pad + * left button. + */ + PADDLEBOAT_BUTTON_DPAD_LEFT = (1U << 1), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` direction pad + * down button. + */ + PADDLEBOAT_BUTTON_DPAD_DOWN = (1U << 2), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` direction pad + * right button. + */ + PADDLEBOAT_BUTTON_DPAD_RIGHT = (1U << 3), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` A button. + */ + PADDLEBOAT_BUTTON_A = (1U << 4), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` B button. + */ + PADDLEBOAT_BUTTON_B = (1U << 5), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` X button. + */ + PADDLEBOAT_BUTTON_X = (1U << 6), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` Y button. + */ + PADDLEBOAT_BUTTON_Y = (1U << 7), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` L1 trigger + * button. + */ + PADDLEBOAT_BUTTON_L1 = (1U << 8), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` L2 trigger + * button. + */ + PADDLEBOAT_BUTTON_L2 = (1U << 9), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` L3 thumbstick + * button. + */ + PADDLEBOAT_BUTTON_L3 = (1U << 10), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` R1 trigger + * button. + */ + PADDLEBOAT_BUTTON_R1 = (1U << 11), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` R2 trigger + * button. + */ + PADDLEBOAT_BUTTON_R2 = (1U << 12), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` R3 thumbstick + * button. + */ + PADDLEBOAT_BUTTON_R3 = (1U << 13), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` Select + * button. + */ + PADDLEBOAT_BUTTON_SELECT = (1U << 14), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` Start button. + */ + PADDLEBOAT_BUTTON_START = (1U << 15), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` System + * button. + */ + PADDLEBOAT_BUTTON_SYSTEM = (1U << 16), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` Touchpad + * pressed. + */ + PADDLEBOAT_BUTTON_TOUCHPAD = (1U << 17), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` AUX1 button. + */ + PADDLEBOAT_BUTTON_AUX1 = (1U << 18), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` AUX2 button. + */ + PADDLEBOAT_BUTTON_AUX2 = (1U << 19), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` AUX3 button. + */ + PADDLEBOAT_BUTTON_AUX3 = (1U << 20), + /** + * @brief Bitmask for `Paddleboat_Controller_Data.buttonsDown` AUX4 button. + */ + PADDLEBOAT_BUTTON_AUX4 = (1U << 21), + /** + * @brief Count of defined controller buttons. + */ + PADDLEBOAT_BUTTON_COUNT = 22 +}; + +/** + * @brief Paddleboat controller device feature flags as bitmask values + * AND against `Paddleboat_Controller_Info.controllerFlags` to determine feature + * availability. + */ +enum Paddleboat_Controller_Flags { + /** + * @brief Bitmask for `Paddleboat_Controller_Info.controllerFlags` + * If set, this controller device wasn't found in the internal + * controller database and a generic button and axis mapping + * profile is being used + */ + PADDLEBOAT_CONTROLLER_FLAG_GENERIC_PROFILE = (0x0000000010), + /** + * @brief Bitmask for `Paddleboat_Controller_Info.controllerFlags` + * If set, this controller device supports reporting accelerometer + * motion axis data + */ + PADDLEBOAT_CONTROLLER_FLAG_ACCELEROMETER = (0x00400000), + /** + * @brief Bitmask for `Paddleboat_Controller_Info.controllerFlags` + * If set, this controller device supports reporting gyroscope + * motion axis data + */ + PADDLEBOAT_CONTROLLER_FLAG_GYROSCOPE = (0x00800000), + /** + * @brief Bitmask for `Paddleboat_Controller_Info.controllerFlags` + * If set, this controller device supports setting a player number + * light using the ::Paddleboat_setControllerLight function + * with the `PADDLEBOAT_LIGHT_PLAYER_NUMBER` light type + */ + PADDLEBOAT_CONTROLLER_FLAG_LIGHT_PLAYER = (0x01000000), + /** + * @brief Bitmask for `Paddleboat_Controller_Info.controllerFlags` + * If set, this controller device supports setting a RGB light + * color using the ::Paddleboat_setControllerLight function + * with the `PADDLEBOAT_LIGHT_RGB` light type + */ + PADDLEBOAT_CONTROLLER_FLAG_LIGHT_RGB = (0x02000000), + /** + * @brief Bitmask for `Paddleboat_Controller_Info.controllerFlags` + * If set, this controller device supports reporting the battery + * status information in the controller data structure + */ + PADDLEBOAT_CONTROLLER_FLAG_BATTERY = (0x04000000), + /** + * @brief Bitmask for `Paddleboat_Controller_Info.controllerFlags` + * If set, this controller device supports vibration effects + * using the ::Paddleboat_setControllerVibrationData function + */ + PADDLEBOAT_CONTROLLER_FLAG_VIBRATION = (0x08000000), + /** + * @brief Bitmask for `Paddleboat_Controller_Info.controllerFlags` + * If set, this controller device supports both left and right + * vibration motor data for vibration effects + */ + PADDLEBOAT_CONTROLLER_FLAG_VIBRATION_DUAL_MOTOR = (0x10000000), + /** + * @brief Bitmask for `Paddleboat_Controller_Info.controllerFlags` + * If set, this controller device has a touchpad that will register + * the `PADDLEBOAT_BUTTON_TOUCHPAD` button when pressed + */ + PADDLEBOAT_CONTROLLER_FLAG_TOUCHPAD = (0x20000000), + /** + * @brief Bitmask for `Paddleboat_Controller_Info.controllerFlags` + * If set, this controller device can simulate a virtual mouse and + * will report coordinates in `Paddleboat_Controller_Data.virtualPointer` + */ + PADDLEBOAT_CONTROLLER_FLAG_VIRTUAL_MOUSE = (0x40000000) +}; + +/** + * @brief Paddleboat mouse buttons as bitmask values + * AND against `Paddleboat_Mouse_Data.buttonsDown` to determine button status. + */ +enum Paddleboat_Mouse_Buttons { + /** + * @brief Bitmask for `Paddleboat_Mouse_Data.buttonsDown` left mouse button. + */ + PADDLEBOAT_MOUSE_BUTTON_LEFT = (1U << 0), + /** + * @brief Bitmask for `Paddleboat_Mouse_Data.buttonsDown` right mouse + * button. + */ + PADDLEBOAT_MOUSE_BUTTON_RIGHT = (1U << 1), + /** + * @brief Bitmask for `Paddleboat_Mouse_Data.buttonsDown` middle mouse + * button. + */ + PADDLEBOAT_MOUSE_BUTTON_MIDDLE = (1U << 2), + /** + * @brief Bitmask for `Paddleboat_Mouse_Data.buttonsDown` back mouse button. + */ + PADDLEBOAT_MOUSE_BUTTON_BACK = (1U << 3), + /** + * @brief Bitmask for `Paddleboat_Mouse_Data.buttonsDown` forward mouse + * button. + */ + PADDLEBOAT_MOUSE_BUTTON_FORWARD = (1U << 4), + /** + * @brief Bitmask for `Paddleboat_Mouse_Data.buttonsDown` mouse button 6. + */ + PADDLEBOAT_MOUSE_BUTTON_6 = (1U << 5), + /** + * @brief Bitmask for `Paddleboat_Mouse_Data.buttonsDown` mouse button 7. + */ + PADDLEBOAT_MOUSE_BUTTON_7 = (1U << 6), + /** + * @brief Bitmask for `Paddleboat_Mouse_Data.buttonsDown` mouse button 8. + */ + PADDLEBOAT_MOUSE_BUTTON_8 = (1U << 7) +}; + +//* Axis order: LStickX, LStickY, RStickX, RStickY, L1, L2, R1, R2, HatX, HatY\n + +/** + * @brief Paddleboat axis mapping table axis order. + */ +enum Paddleboat_Mapping_Axis { + /** + * @brief Paddleboat internal mapping index for left thumbstick X axis. */ + PADDLEBOAT_MAPPING_AXIS_LEFTSTICK_X = 0, + /** + * @brief Paddleboat internal mapping index for left thumbstick Y axis. */ + PADDLEBOAT_MAPPING_AXIS_LEFTSTICK_Y, + /** + * @brief Paddleboat internal mapping index for right thumbstick X axis. */ + PADDLEBOAT_MAPPING_AXIS_RIGHTSTICK_X, + /** + * @brief Paddleboat internal mapping index for right thumbstick Y axis. */ + PADDLEBOAT_MAPPING_AXIS_RIGHTSTICK_Y, + /** + * @brief Paddleboat internal mapping index for L1 trigger axis. */ + PADDLEBOAT_MAPPING_AXIS_L1, + /** + * @brief Paddleboat internal mapping index for L2 trigger axis. */ + PADDLEBOAT_MAPPING_AXIS_L2, + /** + * @brief Paddleboat internal mapping index for R1 trigger axis. */ + PADDLEBOAT_MAPPING_AXIS_R1, + /** + * @brief Paddleboat internal mapping index for R2 trigger axis. */ + PADDLEBOAT_MAPPING_AXIS_R2, + /** + * @brief Paddleboat internal mapping index for HatX trigger axis. + * This is usually the dpad left/right. */ + PADDLEBOAT_MAPPING_AXIS_HATX, + /** + * @brief Paddleboat internal mapping index for HatY trigger axis. + * This is usually the dpad up/down. */ + PADDLEBOAT_MAPPING_AXIS_HATY, + /** + * @brief Number of axis used for controller mapping configuration. */ + PADDLEBOAT_MAPPING_AXIS_COUNT = 10 +}; + +/** + * @brief Special constants to specify an axis or axis button mapping is ignored + * by the controller. + */ +enum Paddleboat_Ignored_Axis { + /** + * @brief Constant that signifies an axis in the + * `Paddleboat_Controller_Mapping_Data.axisPositiveButtonMapping` array + * and/or `Paddleboat_Controller_Mapping_Data.axisNegativeButtonMapping` + * array does not have a mapping to a button + */ + PADDLEBOAT_AXIS_BUTTON_IGNORED = 0xFE, + /** + * @brief Constant that signifies an axis in the + * `Paddleboat_Controller_Mapping_Data.axisMapping` array is unused by + * the controller + */ + PADDLEBOAT_AXIS_IGNORED = 0xFFFE +}; + +/** + * @brief Special constant to specify a button is ignored by the controller. + */ +enum Paddleboat_Ignored_Buttons { + /** + * @brief Constant that signifies a button in the + * `Paddleboat_Controller_Mapping_Data.buttonMapping` array + * does not map to a button on the controller + */ + PADDLEBOAT_BUTTON_IGNORED = 0xFFFE +}; + +/** + * @brief Battery status of a controller + */ +enum Paddleboat_BatteryStatus { + PADDLEBOAT_CONTROLLER_BATTERY_UNKNOWN = 0, ///< Battery status is unknown + PADDLEBOAT_CONTROLLER_BATTERY_CHARGING = + 1, ///< Controller battery is charging + PADDLEBOAT_CONTROLLER_BATTERY_DISCHARGING = + 2, ///< Controller battery is discharging + PADDLEBOAT_CONTROLLER_BATTERY_NOT_CHARGING = + 3, ///< Controller battery is not charging + PADDLEBOAT_CONTROLLER_BATTERY_FULL = + 4 ///< Controller battery is completely charged +}; + +/** + * @brief Current status of a controller (at a specified controller index) + */ +enum Paddleboat_ControllerStatus { + PADDLEBOAT_CONTROLLER_INACTIVE = 0, ///< No controller is connected + PADDLEBOAT_CONTROLLER_ACTIVE = 1, ///< Controller is connected and active + PADDLEBOAT_CONTROLLER_JUST_CONNECTED = + 2, ///< Controller has just connected, + ///< only seen in a controller status callback + PADDLEBOAT_CONTROLLER_JUST_DISCONNECTED = + 3 ///< Controller has just disconnected, + ///< only seen in a controller status callback +}; + +/** + * @brief The button layout and iconography of the controller buttons + */ +enum Paddleboat_ControllerButtonLayout { + //!  Y \n + //! X B\n + //!  A  + PADDLEBOAT_CONTROLLER_LAYOUT_STANDARD = 0, + //!  △ \n + //! □ ○\n + //!  x \n + //! x = A, ○ = B, □ = X, △ = Y + PADDLEBOAT_CONTROLLER_LAYOUT_SHAPES = 1, + //!  X \n + //! Y A\n + //!  B  + PADDLEBOAT_CONTROLLER_LAYOUT_REVERSE = 2, + //! X Y R1 L1\n + //! A B R2 L2 + PADDLEBOAT_CONTROLLER_LAYOUT_ARCADE_STICK = 3, + //! Mask value, AND with + //! `Paddleboat_Controller_Info.controllerFlags` + //! to get the `Paddleboat_ControllerButtonLayout` value + PADDLEBOAT_CONTROLLER_LAYOUT_MASK = 3 +}; + +/** + * @brief The type of light being specified by a call to + * ::Paddleboat_setControllerLight + */ +enum Paddleboat_LightType { + PADDLEBOAT_LIGHT_PLAYER_NUMBER = 0, ///< Light is a player index, + ///< `lightData` is the player number + PADDLEBOAT_LIGHT_RGB = 1 ///< Light is a color light, + ///< `lightData` is a ARGB (8888) light value. +}; + +/** + * @brief The type of motion data being reported in a Paddleboat_Motion_Data + * structure + */ +enum Paddleboat_Motion_Type { + PADDLEBOAT_MOTION_ACCELEROMETER = 0, ///< Accelerometer motion data + PADDLEBOAT_MOTION_GYROSCOPE = 1 ///< Gyroscope motion data +}; + +/** + * @brief The status of the mouse device + */ +enum Paddleboat_MouseStatus { + PADDLEBOAT_MOUSE_NONE = 0, ///< No mouse device is connected + PADDLEBOAT_MOUSE_CONTROLLER_EMULATED = + 1, ///< A virtual mouse is connected + ///< The virtual mouse is being simulated + ///< by a game controller + PADDLEBOAT_MOUSE_PHYSICAL ///< A physical mouse or trackpad device is + ///< connected +}; + +/** + * @brief The addition mode to use when passing new controller mapping data + * to ::Paddleboat_addControllerRemapData + */ +enum Paddleboat_Remap_Addition_Mode { + PADDLEBOAT_REMAP_ADD_MODE_DEFAULT = + 0, ///< If a vendorId/productId controller entry in the + ///< new remap table exists in the current database, + ///< and the min/max API ranges overlap, replace the + ///< existing entry, otherwise add a new entry. + ///< Always adds a new entry if the vendorId/productId + ///< does not exist in the current database. These + ///< changes only persist for the current session. + PADDLEBOAT_REMAP_ADD_MODE_REPLACE_ALL ///< The current controller database + ///< will be erased and entirely + ///< replaced by the new remap table. + ///< This change only persists for + ///< the current session. +}; + +/** + * @brief A structure that describes the current battery state of a controller. + * This structure will only be populated if a controller has + * `PADDLEBOAT_CONTROLLER_FLAG_BATTERY` set in + * `Paddleboat_Controller_Info.controllerFlags` + */ +typedef struct Paddleboat_Controller_Battery { + Paddleboat_BatteryStatus + batteryStatus; /** @brief The current status of the battery */ + float batteryLevel; /** @brief The current charge level of the battery, from + 0.0 to 1.0 */ +} Paddleboat_Controller_Battery; + +/** + * @brief A structure that contains virtual pointer position data. + * X and Y coordinates are pixel based and range from 0,0 to window + * width,height. + */ +typedef struct Paddleboat_Controller_Pointer { + /** @brief X pointer position in window space pixel coordinates */ + float pointerX; + /** @brief Y pointer position in window space pixel coordinates */ + float pointerY; +} Paddleboat_Controller_Pointer; + +/** + * @brief A structure that contains X and Y axis data for an analog thumbstick. + * Axis ranges from -1.0 to 1.0. + */ +typedef struct Paddleboat_Controller_Thumbstick { + /** @brief X axis data for the thumbstick */ + float stickX; + /** @brief X axis data for the thumbstick */ + float stickY; +} Paddleboat_Controller_Thumbstick; + +/** + * @brief A structure that contains axis precision data for a thumbstick in the + * X and Y axis. Value ranges from 0.0 to 1.0. Flat is the extent of a center + * flat (deadzone) area of a thumbstick axis Fuzz is the error tolerance + * (deviation) of a thumbstick axis + */ +typedef struct Paddleboat_Controller_Thumbstick_Precision { + /** @brief X axis flat value for the thumbstick */ + float stickFlatX; + /** @brief Y axis flat value for the thumbstick */ + float stickFlatY; + /** @brief X axis fuzz value for the thumbstick */ + float stickFuzzX; + /** @brief Y axis fuzz value for the thumbstick */ + float stickFuzzY; +} Paddleboat_Controller_Thumbstick_Precision; + +/** + * @brief A structure that contains the current data + * for a controller's inputs and sensors. + */ +typedef struct Paddleboat_Controller_Data { + /** @brief Timestamp of most recent controller data update, timestamp is + * microseconds elapsed since clock epoch. */ + uint64_t timestamp; + /** @brief Bit-per-button bitfield array */ + uint32_t buttonsDown; + /** @brief Left analog thumbstick axis data */ + Paddleboat_Controller_Thumbstick leftStick; + /** @brief Right analog thumbstick axis data */ + Paddleboat_Controller_Thumbstick rightStick; + /** @brief L1 trigger axis data. Axis range is 0.0 to 1.0. */ + float triggerL1; + /** @brief L2 trigger axis data. Axis range is 0.0 to 1.0. */ + float triggerL2; + /** @brief R1 trigger axis data. Axis range is 0.0 to 1.0. */ + float triggerR1; + /** @brief R2 trigger axis data. Axis range is 0.0 to 1.0. */ + float triggerR2; + /** + * @brief Virtual pointer pixel coordinates in window space. + * If `Paddleboat_Controller_Info.controllerFlags` has the + * `PADDLEBOAT_CONTROLLER_FLAG_VIRTUAL_MOUSE` bit set, pointer coordinates + * are valid. If this bit is not set, pointer coordinates will always be + * 0,0. + */ + Paddleboat_Controller_Pointer virtualPointer; + /** + * @brief Battery status. This structure will only be populated if the + * controller has `PADDLEBOAT_CONTROLLER_FLAG_BATTERY` set in + * `Paddleboat_Controller_Info.controllerFlags` + */ + Paddleboat_Controller_Battery battery; +} Paddleboat_Controller_Data; + +/** + * @brief A structure that contains information + * about a particular controller device. Several fields + * are populated by the value of the corresponding fields from InputDevice. + */ +typedef struct Paddleboat_Controller_Info { + /** @brief Controller feature flag bits */ + uint32_t controllerFlags; + /** @brief Controller number, maps to InputDevice.getControllerNumber() */ + int32_t controllerNumber; + /** @brief Vendor ID, maps to InputDevice.getVendorId() */ + int32_t vendorId; + /** @brief Product ID, maps to InputDevice.getProductId() */ + int32_t productId; + /** @brief Device ID, maps to InputDevice.getId() */ + int32_t deviceId; + /** @brief the flat and fuzz precision values of the left thumbstick */ + Paddleboat_Controller_Thumbstick_Precision leftStickPrecision; + /** @brief the flat and fuzz precision values of the right thumbstick */ + Paddleboat_Controller_Thumbstick_Precision rightStickPrecision; +} Paddleboat_Controller_Info; + +/** + * @brief A structure that contains motion data reported by a controller. + */ +typedef struct Paddleboat_Motion_Data { + /** @brief Timestamp of when the motion data event occurred, timestamp is + * nanoseconds elapsed since clock epoch. */ + uint64_t timestamp; + /** @brief The type of motion event data */ + Paddleboat_Motion_Type motionType; + /** @brief Motion X axis data. */ + float motionX; + /** @brief Motion Y axis data. */ + float motionY; + /** @brief Motion Z axis data. */ + float motionZ; +} Paddleboat_Motion_Data; + +/** + * @brief A structure that contains input data for the mouse device. + */ +typedef struct Paddleboat_Mouse_Data { + /** @brief Timestamp of most recent mouse data update, timestamp is + * microseconds elapsed since clock epoch. */ + uint64_t timestamp; + /** @brief Bit-per-button bitfield array of mouse button status. */ + uint32_t buttonsDown; + /** @brief Number of horizontal mouse wheel movements since previous + * read of mouse data. Can be positive or negative depending + * on direction of scrolling. + */ + int32_t mouseScrollDeltaH; + /** @brief Number of vertical mouse wheel movements since previous + * read of mouse data. Can be positive or negative depending + * on direction of scrolling. + */ + int32_t mouseScrollDeltaV; + /** @brief Current mouse X coordinates in window space pixels. */ + float mouseX; + /** @brief Current mouse Y coordinates in window space pixels. */ + float mouseY; +} Paddleboat_Mouse_Data; + +/** + * @brief A structure that describes the parameters of a vibration effect. + */ +typedef struct Paddleboat_Vibration_Data { + /** @brief Duration to vibrate the left motor in milliseconds. */ + int32_t durationLeft; + /** @brief Duration to vibrate the right motor in milliseconds. */ + int32_t durationRight; + /** @brief Intensity of vibration of left motor, valid range is 0.0 to 1.0. + */ + float intensityLeft; + /** @brief Intensity of vibration of right motor, valid range is 0.0 to 1.0. + */ + float intensityRight; +} Paddleboat_Vibration_Data; + +/** + * @brief A structure that describes the button and axis mappings + * for a specified controller device running on a specified range of Android API + * levels.\n See `Paddleboat_Mapping_Axis` for axis order. Hat axis should be + * mapped to dpad buttons. + */ +typedef struct Paddleboat_Controller_Mapping_Data { + /** @brief Minimum API level required for this entry */ + int16_t minimumEffectiveApiLevel; + /** @brief Maximum API level required for this entry, 0 = no max */ + int16_t maximumEffectiveApiLevel; + /** @brief VendorID of the controller device for this entry */ + int32_t vendorId; + /** @brief ProductID of the controller device for this entry */ + int32_t productId; + /** @brief Flag bits, will be ORed with + * `Paddleboat_Controller_Info.controllerFlags` + */ + int32_t flags; + /** @brief AMOTION_EVENT_AXIS value for + * the corresponding Paddleboat control axis, or PADDLEBOAT_AXIS_IGNORED if + * unsupported. + */ + uint16_t axisMapping[PADDLEBOAT_MAPPING_AXIS_COUNT]; + /** @brief Button to set on + * positive axis value, PADDLEBOAT_AXIS_BUTTON_IGNORED if none. + */ + uint8_t axisPositiveButtonMapping[PADDLEBOAT_MAPPING_AXIS_COUNT]; + /** @brief Button to set on + * negative axis value, PADDLEBOAT_AXIS_BUTTON_IGNORED if none. + */ + uint8_t axisNegativeButtonMapping[PADDLEBOAT_MAPPING_AXIS_COUNT]; + /** @brief AKEYCODE_ value corresponding + * with the corresponding Paddleboat button. + * PADDLEBOAT_BUTTON_IGNORED if unsupported. + */ + uint16_t buttonMapping[PADDLEBOAT_BUTTON_COUNT]; +} Paddleboat_Controller_Mapping_Data; + +/** + * @brief Signature of a function that can be passed to + * ::Paddleboat_setControllerStatusCallback to receive information about + controller + * connections and disconnections. + + * @param controllerIndex Index of the controller that has registered a status + change, + * will range from 0 to PADDLEBOAT_MAX_CONTROLLERS - 1. + * @param controllerStatus New status of the controller. + * @param userData The value of the userData parameter passed + * to ::Paddleboat_setControllerStatusCallback + * + * Function will be called on the same thread that calls ::Paddleboat_update. + */ +typedef void (*Paddleboat_ControllerStatusCallback)( + const int32_t controllerIndex, + const Paddleboat_ControllerStatus controllerStatus, void *userData); + +/** + * @brief Signature of a function that can be passed to + * ::Paddleboat_setMouseStatusCallback to receive information about mouse + * device status changes. + * @param mouseStatus Current status of the mouse. + * @param userData The value of the userData parameter passed + * to ::Paddleboat_setMouseStatusCallback + * + * Function will be called on the same thread that calls ::Paddleboat_update. + */ +typedef void (*Paddleboat_MouseStatusCallback)( + const Paddleboat_MouseStatus mouseStatus, void *userData); + +/** + * @brief Signature of a function that can be passed to + * ::Paddleboat_setMotionDataCallback to receive information about motion data + events + * sent by connected controllers + + * @param controllerIndex Index of the controller reporting the motion event, + * will range from 0 to PADDLEBOAT_MAX_CONTROLLERS - 1. + * @param motionData The motion data. Pointer is only valid until the callback + returns. + * @param userData The value of the userData parameter passed + * to ::Paddleboat_setMotionDataCallback + * + */ +typedef void (*Paddleboat_MotionDataCallback)( + const int32_t controllerIndex, const Paddleboat_Motion_Data *motionData, + void *userData); + +/** + * @brief Initialize Paddleboat, constructing internal resources via JNI. This + * may be called after calling ::Paddleboat_destroy to reinitialize the library. + * @param env The JNIEnv attached to the thread calling the function. + * @param jcontext A Context derived object used by the game. This can be an + * Activity derived class. + * @return `PADDLEBOAT_NO_ERROR` if successful, otherwise an error code relating + * to initialization failure. + * @see Paddleboat_destroy + */ +Paddleboat_ErrorCode Paddleboat_init(JNIEnv *env, jobject jcontext); + +/** + * @brief Check if Paddleboat was successfully initialized. + * @return false if the initialization failed or was not called. + */ +bool Paddleboat_isInitialized(); + +/** + * @brief Destroy resources that Paddleboat has created. + * @param env The JNIEnv attached to the thread calling the function. + * @see Paddleboat_init + */ +void Paddleboat_destroy(JNIEnv *env); + +/** + * @brief Inform Paddleboat that a stop event was sent to the application. + * @param env The JNIEnv attached to the thread calling the function. + */ +void Paddleboat_onStop(JNIEnv *env); + +/** + * @brief Inform Paddleboat that a start event was sent to the application. + * @param env The JNIEnv attached to the thread calling the function. + */ +void Paddleboat_onStart(JNIEnv *env); + +/** + * @brief Process an input event to see if it is from a device being + * managed by Paddleboat. + * @param event the input event received by the application. + * @return 0 if the event was ignored, 1 if the event was processed/consumed by + * Paddleboat. + */ +int32_t Paddleboat_processInputEvent(const AInputEvent *event); + +/** + * @brief Process a GameActivityKeyEvent input event to see if it is from a + * device being managed by Paddleboat. At least once per game frame, the + * game should iterate through reported GameActivityKeyEvents and pass them to + * ::Paddleboat_processGameActivityKeyInputEvent for evaluation. + * @param event the GameActivityKeyEvent input event received by the + * application. + * @param eventSize the size of the GameActivityKeyEvent struct being passed in + * bytes. + * @return 0 if the event was ignored, 1 if the event was processed/consumed by + * Paddleboat. + */ +int32_t Paddleboat_processGameActivityKeyInputEvent(const void *event, + const size_t eventSize); + +/** + * @brief Process a GameActivityMotionEvent input event to see if it is from a + * device being managed by Paddleboat. At least once per game frame, the + * game should iterate through reported GameActivityMotionEvents and pass them + * to + * ::Paddleboat_processGameActivityMotionInputEvent for evaluation. + * @param event the GameActivityMotionEvent input event received by the + * application. + * @param eventSize the size of the GameActivityMotionEvent struct being passed + * in bytes. + * @return 0 if the event was ignored, 1 if the event was processed/consumed by + * Paddleboat. + */ +int32_t Paddleboat_processGameActivityMotionInputEvent(const void *event, + const size_t eventSize); + +/** + * @brief Retrieve the active axis ids being used by connected devices. This can + * be used to determine what axis values to provide to + * GameActivityPointerInfo_enableAxis when GameActivity is being used. + * @return A bitmask of the active axis ids that have been used by connected + * devices during the current application session. + */ +uint64_t Paddleboat_getActiveAxisMask(); + +/** + * @brief Get whether Paddleboat consumes AKEYCODE_BACK key events from devices + * being managed by Paddleboat. The default at initialization is true. + * @return If true, Paddleboat will consume AKEYCODE_BACK key events, if false + * it will pass them through. + */ +bool Paddleboat_getBackButtonConsumed(); + +/** + * @brief Set whether Paddleboat consumes AKEYCODE_BACK key events from devices + * being managed by Paddleboat. The default at initialization is true. This can + * be set to false to allow exiting the application from a back button press + * when the application is in an appropriate state (i.e. the title screen). + * @param consumeBackButton If true, Paddleboat will consume AKEYCODE_BACK key + * events, if false it will pass them through. + */ +void Paddleboat_setBackButtonConsumed(bool consumeBackButton); + +/** + * @brief Set a callback to be called whenever a controller managed by + * Paddleboat changes status. This is used to inform of controller connections + * and disconnections. + * @param statusCallback function pointer to the controllers status change + * callback, passing NULL or nullptr will remove any currently registered + * callback. + * @param userData optional pointer (may be NULL or nullptr) to user data that + * will be passed as a parameter to the status callback. A reference to this + * pointer will be retained internally until changed by a future call to + * ::Paddleboat_setControllerStatusCallback + */ +void Paddleboat_setControllerStatusCallback( + Paddleboat_ControllerStatusCallback statusCallback, void *userData); + +/** + * @brief Set a callback which is called whenever a controller managed by + * Paddleboat reports a motion data event. + * @param motionDataCallback function pointer to the motion data callback, + * passing NULL or nullptr will remove any currently registered callback. + * @param userData optional pointer (may be NULL or nullptr) to user data + * that will be passed as a parameter to the status callback. A reference + * to this pointer will be retained internally until changed by a future + * call to ::Paddleboat_setMotionDataCallback + */ +void Paddleboat_setMotionDataCallback( + Paddleboat_MotionDataCallback motionDataCallback, void *userData); + +/** + * @brief Set a callback to be called when the mouse status changes. This is + * used to inform of physical or virual mouse device connections and + * disconnections. + * @param statusCallback function pointer to the controllers status change + * callback, passing NULL or nullptr will remove any currently registered + * callback. + * @param userData optional pointer (may be NULL or nullptr) to user data that + * will be passed as a parameter to the status callback. A reference to this + * pointer will be retained internally until changed by a future call to + * ::Paddleboat_setMouseStatusCallback + */ +void Paddleboat_setMouseStatusCallback( + Paddleboat_MouseStatusCallback statusCallback, void *userData); + +/** + * @brief Retrieve the current controller data from the controller with the + * specified index. + * @param controllerIndex The index of the controller to read from, must be + * between 0 and PADDLEBOAT_MAX_CONTROLLERS - 1 + * @param[out] controllerData a pointer to the controller data struct to + * populate. + * @return `PADDLEBOAT_NO_ERROR` if data was successfully read. + */ +Paddleboat_ErrorCode Paddleboat_getControllerData( + const int32_t controllerIndex, Paddleboat_Controller_Data *controllerData); + +/** + * @brief Retrieve the current controller device info from the controller with + * the specified index. + * @param controllerIndex The index of the controller to read from, must be + * between 0 and PADDLEBOAT_MAX_CONTROLLERS - 1 + * @param[out] controllerInfo a pointer to the controller device info struct to + * populate. + * @return true if the data was read, false if there was no connected controller + * at the specified index. + */ +Paddleboat_ErrorCode Paddleboat_getControllerInfo( + const int32_t controllerIndex, Paddleboat_Controller_Info *controllerInfo); + +/** + * @brief Retrieve the current controller name from the controller with the + * specified index. This name is retrieved from InputDevice.getName(). + * @param controllerIndex The index of the controller to read from, must be + * between 0 and PADDLEBOAT_MAX_CONTROLLERS - 1 + * @param bufferSize The capacity in bytes of the string buffer passed in + * controllerName + * @param[out] controllerName A pointer to a buffer that will be populated with + * the name string. The name string is a C string in UTF-8 format. If the length + * of the string is greater than bufferSize the string will be truncated to fit. + * @return `PADDLEBOAT_NO_ERROR` if data was successfully read. + */ +Paddleboat_ErrorCode Paddleboat_getControllerName(const int32_t controllerIndex, + const size_t bufferSize, + char *controllerName); +/** + * @brief Retrieve the current controller device info from the controller with + * the specified index. + * @param controllerIndex The index of the controller to read from, must be + * between 0 and PADDLEBOAT_MAX_CONTROLLERS - 1. + * @return Paddleboat_ControllerStatus enum value of the current controller + * status of the specified controller index. + */ +Paddleboat_ControllerStatus Paddleboat_getControllerStatus( + const int32_t controllerIndex); + +/** + * @brief Configures a light on the controller with the specified index. + * @param controllerIndex The index of the controller to read from, must be + * between 0 and PADDLEBOAT_MAX_CONTROLLERS - 1 + * @param lightType Specifies the type of light on the controller to configure. + * @param lightData Light configuration data. For player index lights, this is + * a number indicating the player index (usually between 1 and 4). For RGB + * lights, this is a 8888 ARGB value. + * @param env The JNIEnv attached to the thread calling the function. + * @return `PADDLEBOAT_NO_ERROR` if successful, otherwise an error code. + */ +Paddleboat_ErrorCode Paddleboat_setControllerLight( + const int32_t controllerIndex, const Paddleboat_LightType lightType, + const uint32_t lightData, JNIEnv *env); + +/** + * @brief Set vibration data for the controller with the specified index. + * @param controllerIndex The index of the controller to read from, must be + * between 0 and PADDLEBOAT_MAX_CONTROLLERS - 1. + * @param vibrationData The intensity and duration data for the vibration + * effect. Valid intensity range is from 0.0 to 1.0. Intensity of 0.0 will turn + * off vibration if it is active. Duration is specified in milliseconds. The + * pointer passed in vibrationData is not retained and does not need to persist + * after function return. + * @param env The JNIEnv attached to the thread calling the function. + * @return true if the vibration data was set, false if there was no connected + * controller or the connected controller does not support vibration. + */ +Paddleboat_ErrorCode Paddleboat_setControllerVibrationData( + const int32_t controllerIndex, + const Paddleboat_Vibration_Data *vibrationData, JNIEnv *env); +/** + * @brief Retrieve the current mouse data. + * @param[out] mouseData pointer to the mouse data struct to populate. + * @return true if the data was read, false if there was no connected mouse + * device. + */ +Paddleboat_ErrorCode Paddleboat_getMouseData(Paddleboat_Mouse_Data *mouseData); + +/** + * @brief Retrieve the current controller device info from the controller with + * the specified index. + * @return Paddleboat_MouseStatus enum value of the current mouse status. + */ +Paddleboat_MouseStatus Paddleboat_getMouseStatus(); + +/** + * @brief Add new controller remap information to the internal remapping table. + * @param addMode The addition mode for the new data. See the + * `Paddleboat_Remap_Addition_Mode` enum for details on each mode. + * @param remapTableEntryCount the number of remap elements in the mappingData + * array. + * @param mappingData An array of controller mapping structs to be added to the + * internal remapping table. The pointer passed in mappingData is not retained + * and does not need to persist after function return. + */ +void Paddleboat_addControllerRemapData( + const Paddleboat_Remap_Addition_Mode addMode, + const int32_t remapTableEntryCount, + const Paddleboat_Controller_Mapping_Data *mappingData); + +/** + * @brief Retrieve the current table of controller remap entries. + * @param destRemapTableEntryCount the number of + * `Paddleboat_Controller_Mapping_Data` entries in the array passed in the + * mappingData parameter. Paddleboat will not copy more than this number of + * entries out of the internal array to avoid overflowing the buffer. + * @param[out] mappingData pointer to an array of + * `Paddleboat_Controller_Mapping_Data` structures. this should contain at least + * destRemapTableEntryCount elements. Passing nullptr is valid, and can be used + * to get the number of elements in the internal remap table. + * @return The number of elements in the internal remap table. + */ +int32_t Paddleboat_getControllerRemapTableData( + const int32_t destRemapTableEntryCount, + Paddleboat_Controller_Mapping_Data *mappingData); + +/** + * @brief Updates internal Paddleboat status and processes pending + * connection/disconnections. Paddleboat_update is responsible for triggering + * any registered controller or mouse status callbacks, those callbacks will + * fire on the same thread that called Paddleboat_update. This function should + * be called once per game frame. It is recommended to call Paddleboat_update + * before sending any new input events or reading controller inputs for a + * particular game frame. + * @param env The JNIEnv attached to the thread calling the function. + */ +void Paddleboat_update(JNIEnv *env); + +/** + * @brief An function that returns the last keycode seen in a key event + * coming from a controller owned by Paddleboat. Useful for debugging unknown + * buttons in new devices in the sample app + * @return keycode from last controller key event. + */ +int32_t Paddleboat_getLastKeycode(); + +#ifdef __cplusplus +} +#endif + +#endif +/** @} */ diff --git a/sources/android-gamesdk/GameController/paddleboat_c.cpp b/sources/android-gamesdk/GameController/paddleboat_c.cpp new file mode 100644 index 00000000..22967ebc --- /dev/null +++ b/sources/android-gamesdk/GameController/paddleboat_c.cpp @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "GameControllerManager.h" +#include "paddleboat.h" + +using namespace paddleboat; + +extern "C" { + +// Internal macros to track Paddleboat version, do not use directly. +#define PADDLEBOAT_MAJOR_VERSION 1 +#define PADDLEBOAT_MINOR_VERSION 1 +#define PADDLEBOAT_BUGFIX_VERSION 0 + +#define PADDLEBOAT_PACKED_VERSION \ + ((PADDLEBOAT_MAJOR_VERSION << 24) | (PADDLEBOAT_MINOR_VERSION << 16) | \ + (PADDLEBOAT_BUGFIX_VERSION)) + +#define PADDLEBOAT_VERSION_CONCAT_NX(PREFIX, MAJOR, MINOR, BUGFIX) \ + PREFIX##_##MAJOR##_##MINOR##_##BUGFIX +#define PADDLEBOAT_VERSION_CONCAT(PREFIX, MAJOR, MINOR, BUGFIX) \ + PADDLEBOAT_VERSION_CONCAT_NX(PREFIX, MAJOR, MINOR, BUGFIX) +#define PADDLEBOAT_VERSION_SYMBOL \ + PADDLEBOAT_VERSION_CONCAT(PADDLEBOAT_version, PADDLEBOAT_MAJOR_VERSION, \ + PADDLEBOAT_MINOR_VERSION, \ + PADDLEBOAT_BUGFIX_VERSION) + +void PADDLEBOAT_VERSION_SYMBOL(); + +Paddleboat_ErrorCode Paddleboat_init(JNIEnv *env, jobject jcontext) { + PADDLEBOAT_VERSION_SYMBOL(); + Paddleboat_ErrorCode errorCode = GameControllerManager::init(env, jcontext); + if (errorCode == PADDLEBOAT_NO_ERROR) { + GameControllerManager::update(env); + } + return errorCode; +} + +bool Paddleboat_isInitialized() { + return GameControllerManager::isInitialized(); +} + +void Paddleboat_destroy(JNIEnv *env) { + GameControllerManager::destroyInstance(env); +} + +void Paddleboat_onStop(JNIEnv *env) { GameControllerManager::onStop(env); } + +void Paddleboat_onStart(JNIEnv *env) { GameControllerManager::onStart(env); } + +int32_t Paddleboat_processInputEvent(const AInputEvent *event) { + return GameControllerManager::processInputEvent(event); +} + +int32_t Paddleboat_processGameActivityKeyInputEvent(const void *event, + const size_t eventSize) { + return GameControllerManager::processGameActivityKeyInputEvent(event, + eventSize); +} + +int32_t Paddleboat_processGameActivityMotionInputEvent(const void *event, + const size_t eventSize) { + return GameControllerManager::processGameActivityMotionInputEvent( + event, eventSize); +} + +uint64_t Paddleboat_getActiveAxisMask() { + return GameControllerManager::getActiveAxisMask(); +} + +bool Paddleboat_getBackButtonConsumed() { + return GameControllerManager::getBackButtonConsumed(); +} + +void Paddleboat_setBackButtonConsumed(bool consumeBackButton) { + GameControllerManager::setBackButtonConsumed(consumeBackButton); +} + +void Paddleboat_setControllerStatusCallback( + Paddleboat_ControllerStatusCallback statusCallback, void *userData) { + GameControllerManager::setControllerStatusCallback(statusCallback, + userData); +} + +void Paddleboat_setMotionDataCallback( + Paddleboat_MotionDataCallback motionDataCallback, void *userData) { + GameControllerManager::setMotionDataCallback(motionDataCallback, userData); +} + +void Paddleboat_setMouseStatusCallback( + Paddleboat_MouseStatusCallback statusCallback, void *userData) { + GameControllerManager::setMouseStatusCallback(statusCallback, userData); +} + +Paddleboat_ErrorCode Paddleboat_getControllerData( + const int32_t controllerIndex, Paddleboat_Controller_Data *controllerData) { + return GameControllerManager::getControllerData(controllerIndex, + controllerData); +} + +Paddleboat_ErrorCode Paddleboat_getControllerInfo( + const int32_t controllerIndex, Paddleboat_Controller_Info *controllerInfo) { + return GameControllerManager::getControllerInfo(controllerIndex, + controllerInfo); +} + +Paddleboat_ErrorCode Paddleboat_getControllerName(const int32_t controllerIndex, + const size_t bufferSize, + char *controllerName) { + return GameControllerManager::getControllerName(controllerIndex, bufferSize, + controllerName); +} + +Paddleboat_ControllerStatus Paddleboat_getControllerStatus( + const int32_t controllerIndex) { + return GameControllerManager::getControllerStatus(controllerIndex); +} + +Paddleboat_ErrorCode Paddleboat_setControllerLight( + const int32_t controllerIndex, const Paddleboat_LightType lightType, + const uint32_t lightData, JNIEnv *env) { + return GameControllerManager::setControllerLight(controllerIndex, lightType, + lightData, env); +} + +Paddleboat_ErrorCode Paddleboat_setControllerVibrationData( + const int32_t controllerIndex, + const Paddleboat_Vibration_Data *vibrationData, JNIEnv *env) { + return GameControllerManager::setControllerVibrationData( + controllerIndex, vibrationData, env); +} + +Paddleboat_ErrorCode Paddleboat_getMouseData(Paddleboat_Mouse_Data *mouseData) { + return GameControllerManager::getMouseData(mouseData); +} + +Paddleboat_MouseStatus Paddleboat_getMouseStatus() { + return GameControllerManager::getMouseStatus(); +} + +void Paddleboat_addControllerRemapData( + const Paddleboat_Remap_Addition_Mode addMode, + const int32_t remapTableEntryCount, + const Paddleboat_Controller_Mapping_Data *mappingData) { + GameControllerManager::addControllerRemapData(addMode, remapTableEntryCount, + mappingData); +} + +int32_t Paddleboat_getControllerRemapTableData( + const int32_t destRemapTableEntryCount, + Paddleboat_Controller_Mapping_Data *mappingData) { + return GameControllerManager::getControllerRemapTableData( + destRemapTableEntryCount, mappingData); +} + +void Paddleboat_update(JNIEnv *env) { GameControllerManager::update(env); } + +int32_t Paddleboat_getLastKeycode() { + return GameControllerManager::getLastKeycode(); +} + +void PADDLEBOAT_VERSION_SYMBOL() { + // Intentionally empty: this function is used to ensure that the proper + // version of the library is linked against the proper headers. + // In case of mismatch, a linker error will be triggered because of an + // undefined symbol, as the name of the function depends on the version. +} +} // extern "C" { \ No newline at end of file