Files
mcpe/platforms/android/main.cpp
iProgramInCpp 3b60855ad0 * Android: Determine feature availability at compile time.
Since assets are baked in to the APK, we can simply check for the existence of assets at compile time using CMake.
2023-11-05 21:30:33 +02:00

465 lines
14 KiB
C++

/********************************************************************
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 <jni.h>
#include <errno.h>
#include "android_native_app_glue.h"
#include <string.h>
#include <unistd.h>
#include <sys/resource.h>
#include <thread>
#include <android/sensor.h>
#include <android/log.h>
#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;
}