/******************************************************************** Minecraft: Pocket Edition - Decompilation Project Copyright (C) 2023 iProgramInCpp The following code is licensed under the BSD 1 clause license. SPDX-License-Identifier: BSD-1-Clause ********************************************************************/ #include #include #include "android_native_app_glue.h" #include #include #include #include #include #include #include "compat/KeyCodes.hpp" #include "thirdparty/GL/GL.hpp" #include "platforms/android/AppPlatform_android.hpp" #include "client/app/NinecraftApp.hpp" #include "client/gui/screens/ProgressScreen.hpp" #include "client/player/input/Controller.hpp" #include "client/player/input/Mouse.hpp" #include "client/player/input/Multitouch.hpp" AppPlatform_android g_AppPlatform; bool g_LButtonDown, g_RButtonDown; int g_MousePosX, g_MousePosY; struct engine { struct android_app* androidApp; EGLDisplay display = nullptr; EGLSurface surface = nullptr; EGLContext context = nullptr; int animating = 0; bool initted = false; NinecraftApp* ninecraftApp = nullptr; }; static float mapStick(AInputEvent* event, int32_t axis) { const float deadZone = .265f; float value = AMotionEvent_getAxisValue(event, axis, 0); if (value > deadZone) return (value - deadZone) / (1.f - deadZone); else if (value < -deadZone) return (value + deadZone) / (1.f - deadZone); else return 0.f; } static float mapTrigger(AInputEvent* event, int32_t axis) { const float deadZone = .1f; float value = AMotionEvent_getAxisValue(event, axis, 0); if (value > deadZone) return (value - deadZone) / (1.f - deadZone); else return 0.f; } static bool s_lastR = false; static bool s_lastL = false; static char getCharFromKey(int32_t keyCode, int32_t metaState) { bool bShiftPressed = metaState & AMETA_SHIFT_ON; // well you can't really press alt-X or ctrl-X and expect an actual character if (metaState & (AMETA_ALT_ON | AMETA_CTRL_ON)) return '\0'; // Alphabet if (keyCode >= AKEYCODE_A && keyCode <= AKEYCODE_Z) return char((keyCode - AKEYCODE_A) + (bShiftPressed ? 'A' : 'a')); // Digits if (keyCode >= AKEYCODE_0 && keyCode <= AKEYCODE_9) { static const char* shiftmap = ")!@#$%^&*("; return char(bShiftPressed ? shiftmap[keyCode - AKEYCODE_0] : (keyCode - AKEYCODE_0 + '0')); } // NumPad if (keyCode >= AKEYCODE_NUMPAD_0 && keyCode <= AKEYCODE_NUMPAD_9) return char(keyCode + '0' - AKEYCODE_NUMPAD_0); switch (keyCode) { case AKEYCODE_DEL: return '\b'; case AKEYCODE_FORWARD_DEL: return '\001'; case AKEYCODE_ARROW_LEFT: return '\002'; case AKEYCODE_ARROW_RIGHT: return '\003'; case AKEYCODE_SPACE: return ' '; case AKEYCODE_COMMA: return bShiftPressed ? '<' : ','; case AKEYCODE_PERIOD: return bShiftPressed ? '>' : '.'; case AKEYCODE_EQUALS: return bShiftPressed ? '+' : '='; case AKEYCODE_MINUS: return bShiftPressed ? '_' : '-'; case AKEYCODE_SEMICOLON: return bShiftPressed ? ':' : ';'; case AKEYCODE_SLASH: return bShiftPressed ? '?' : '/'; case AKEYCODE_GRAVE: return bShiftPressed ? '~' : '`'; case AKEYCODE_BACKSLASH: return bShiftPressed ? '|' : '\\'; case AKEYCODE_APOSTROPHE: return bShiftPressed ? '"' : '\''; case AKEYCODE_LEFT_BRACKET: return bShiftPressed ? '{' : '['; case AKEYCODE_RIGHT_BRACKET: return bShiftPressed ? '}' : ']'; } return '\0'; } static int evalKeyInput(struct engine* engine, AInputEvent* event) { bool keyDown = AKeyEvent_getAction(event) == AKEY_EVENT_ACTION_DOWN; int keyCode = AKeyEvent_getKeyCode(event); bool isBack = true; if (keyCode != AKEYCODE_BACK || AInputEvent_getDeviceId(event) != 0) isBack = false; int repeatCount = AKeyEvent_getRepeatCount(event); if (repeatCount <= 0 && !isBack) Keyboard::feed(keyDown ? Keyboard::DOWN : Keyboard::UP, keyCode); // We define HANDLE_CHARS_SEPARATELY but have to fake it if (keyDown) { char chr = getCharFromKey(keyCode, AKeyEvent_getMetaState(event)); if (chr != '\0') engine->ninecraftApp->handleCharInput(chr); } if (keyCode == AKEYCODE_BACK) { if (repeatCount == 0) engine->ninecraftApp->handleBack(keyDown); return 1; } // let Android handle those if (keyCode >= AKEYCODE_VOLUME_UP && keyCode <= AKEYCODE_VOLUME_DOWN) return 0; return 1; } #define COMPARE_SOURCE(v, s) ((v & s) == s) static void nativeMouseDown(int id, int x, int y) { Mouse::feed(BUTTON_LEFT, true, x, y); Multitouch::feed(BUTTON_LEFT, true, x, y, id); } static void nativeMouseUp(int id, int x, int y) { Mouse::feed(BUTTON_LEFT, false, x, y); Multitouch::feed(BUTTON_LEFT, false, x, y, id); } static void nativeMouseMove(int id, int x, int y) { Mouse::feed(BUTTON_NONE, false, x, y); Multitouch::feed(BUTTON_NONE, false, x, y, id); } static int32_t evalMotionInput(struct engine* engine, AInputEvent* event, int32_t source) { size_t pointerCount = AMotionEvent_getPointerCount(event); //! NOTE: Android documentation clearly states that pointerCount is always >=1. if (pointerCount <= 0) return 1; int actionp = AMotionEvent_getAction(event); int action = actionp & AMOTION_EVENT_ACTION_MASK; int pointerIndex = (actionp & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT; int pointerId = AMotionEvent_getPointerId(event, pointerIndex); int x = AMotionEvent_getX(event, pointerIndex); int y = AMotionEvent_getY(event, pointerIndex); switch (action) { case AMOTION_EVENT_ACTION_DOWN: case AMOTION_EVENT_ACTION_POINTER_DOWN: nativeMouseDown(pointerId, x, y); break; case AMOTION_EVENT_ACTION_UP: case AMOTION_EVENT_ACTION_POINTER_UP: nativeMouseUp(pointerId, x, y); break; case AMOTION_EVENT_ACTION_MOVE: { int pc = AMotionEvent_getPointerCount(event); for (int i = 0; i < pc; i++) { nativeMouseMove(AMotionEvent_getPointerId(event, i), AMotionEvent_getX(event, i), AMotionEvent_getY(event, i)); } break; } } return 1; } static int32_t engine_handle_input(struct android_app* app, AInputEvent* event) { struct engine* engine = (struct engine*)app->userData; int32_t type = AInputEvent_getType(event); int32_t source = AInputEvent_getSource(event); switch (type) { case AINPUT_EVENT_TYPE_KEY: return evalKeyInput(engine, event); case AINPUT_EVENT_TYPE_MOTION: return evalMotionInput(engine, event, source); } return 0; } static std::string getExternalStorageDir(struct engine* engine) { // Normally we would use the external storage directory. However, late Android is being // very bitchy about giving us the permission. And that's probably a good thing, actually. // This returns a directory path that looks something like the following, which is still // exposed to the user, therefore we get the benefits of having it exposed, aside from the // fact that removing the app will delete all your worlds. // /.../Android/data/com.minecraftcpp/files #ifndef USE_EXTERNAL_STORAGE return std::string(engine->androidApp->activity->externalDataPath); #else ANativeActivity* pActivity = engine->androidApp->activity; JNIEnv* pJNIEnv = pActivity->env; pActivity->vm->AttachCurrentThread(&pJNIEnv, nullptr); jclass Environment = pJNIEnv->FindClass("android/os/Environment"); jmethodID GetDirId = pJNIEnv->GetStaticMethodID(Environment, "getExternalStorageDirectory", "()Ljava/io/File;"); if (pJNIEnv->ExceptionOccurred()) pJNIEnv->ExceptionDescribe(); // Since it takes a String jobject FileObject = pJNIEnv->CallStaticObjectMethod(Environment, GetDirId); jclass FileClass = pJNIEnv->GetObjectClass(FileObject); jmethodID AbsPathId = pJNIEnv->GetMethodID(FileClass, "getAbsolutePath", "()Ljava/lang/String;"); jobject PathObject = pJNIEnv->CallObjectMethod(FileObject, AbsPathId); const char* PathString = pJNIEnv->GetStringUTFChars((jstring) PathObject, nullptr); std::string result(PathString); pJNIEnv->ReleaseStringUTFChars((jstring) PathObject, PathString); pActivity->vm->DetachCurrentThread(); return result; #endif } /** * Process the next main command. */ static void initWindow(struct engine* engine, struct android_app* app) { if (engine->androidApp->window == NULL) { LOG_E("no active window?"); return; } EGLint w, h, format; //EGLint numConfigs; engine->display = eglGetDisplay(EGL_DEFAULT_DISPLAY); if (!eglInitialize(engine->display, 0, 0)) { LOG_E("eglInitialize failed %i", eglGetError()); return; } EGLConfig config; EGLint attribs[] = { EGL_SURFACE_TYPE, EGL_OPENGL_ES2_BIT, EGL_DEPTH_SIZE, 0x10, EGL_RENDERABLE_TYPE, EGL_VERSION_1_3, EGL_NONE }; EGLint numConfigs = 0; if (!(eglChooseConfig(engine->display, attribs, &config, 1, &numConfigs) && numConfigs > 0)) { LOG_E("eglChooseConfig failed %i", eglGetError()); return; } eglGetConfigAttrib(engine->display, config, EGL_NATIVE_VISUAL_ID, &format); ANativeWindow_setBuffersGeometry(app->window, 0, 0, format); engine->surface = eglCreateWindowSurface(engine->display, config, app->window, NULL); engine->context = eglCreateContext(engine->display, config, NULL, NULL); if (!engine->context) { LOG_E("cant create egl context %x", eglGetError()); return; } if (eglMakeCurrent(engine->display, engine->surface, engine->surface, engine->context) == EGL_FALSE) { LOG_E("Unable to eglMakeCurrent"); return; } eglQuerySurface(engine->display, engine->surface, EGL_WIDTH, &w); eglQuerySurface(engine->display, engine->surface, EGL_HEIGHT, &h); g_AppPlatform.initConsts(); g_AppPlatform.setScreenSize(w, h); g_AppPlatform.initAndroidApp(app); engine->ninecraftApp->width = w; engine->ninecraftApp->height = h; if (!engine->initted) { engine->ninecraftApp->m_externalStorageDir = getExternalStorageDir(engine); g_AppPlatform.setExternalStoragePath(engine->ninecraftApp->m_externalStorageDir); engine->ninecraftApp->init(); } else { engine->ninecraftApp->onGraphicsReset(); } engine->ninecraftApp->sizeUpdate(w, h); engine->initted = true; LOG_I("finished initializing"); engine->animating = 1; } static void engine_handle_cmd(struct android_app* app, int32_t cmd) { struct engine* engine = (struct engine*)app->userData; switch (cmd) { case APP_CMD_CONFIG_CHANGED: LOG_I("APP_CMD_CONFIG_CHANGED"); break; case APP_CMD_SAVE_STATE: //ninecraftApp saveState not implemented do nothing break; case APP_CMD_INIT_WINDOW: LOG_I("APP_CMD_INIT_WINDOW"); initWindow(engine, app); break; case APP_CMD_TERM_WINDOW: LOG_I("APP_CMD_TERM_WINDOW"); engine->ninecraftApp->saveOptions(); if (engine->display) { eglMakeCurrent(engine->display, 0, 0, 0); if (engine->context) eglDestroyContext(engine->display, engine->context); if (engine->surface) eglDestroySurface(engine->display, engine->surface); eglTerminate(engine->display); } break; case APP_CMD_GAINED_FOCUS: engine->animating = 1; break; case APP_CMD_LOST_FOCUS: engine->animating = 0; engine->ninecraftApp->saveOptions(); break; } } extern bool g_bIsMenuBackgroundAvailable; // client/gui/Screen.cpp extern bool g_bAreCloudsAvailable; // client/renderer/LevelRenderer.cpp extern bool g_bIsGrassColorAvailable; // world/level/GrassColor.cpp extern bool g_bIsFoliageColorAvailable; // world/level/FoliageColor.cpp static void CheckOptionalTextureAvailability() { #ifdef FEATURE_MENU_BACKGROUND g_bIsMenuBackgroundAvailable = true; #endif #ifdef FEATURE_CLOUDS g_bAreCloudsAvailable = true; #endif #ifdef FEATURE_GRASS_COLOR g_bIsGrassColorAvailable = true; #endif #ifdef FEATURE_FOLIAGE_COLOR g_bIsFoliageColorAvailable = true; #endif } void android_main(struct android_app* state) { struct engine engine; CheckOptionalTextureAvailability(); memset(&engine, 0, sizeof(engine)); state->userData = &engine; state->onAppCmd = engine_handle_cmd; state->onInputEvent = engine_handle_input; engine.androidApp = state; engine.ninecraftApp = new NinecraftApp; engine.ninecraftApp->m_pPlatform = &g_AppPlatform; while (1) { int ident; int events; struct android_poll_source* source; while ((ident = ALooper_pollAll(engine.animating ? 0 : -1, NULL, &events, (void**)&source)) >= 0) { if (source != NULL) { source->process(state, source); } } if (engine.animating) { if (engine.ninecraftApp->wantToQuit()) break; engine.ninecraftApp->update(); eglSwapBuffers(engine.display, engine.surface); } } engine.ninecraftApp->saveOptions(); delete engine.ninecraftApp; }