diff --git a/src/common/android.cpp b/src/common/android.cpp index 22a6ccefa..e6394847d 100644 --- a/src/common/android.cpp +++ b/src/common/android.cpp @@ -22,13 +22,20 @@ #ifdef LOVE_ANDROID -#include "SDL.h" -#include "jni.h" +#include +#include + +#include + +#include +#include +#include #include #include #include -#include + +#include "physfs.h" namespace love { @@ -326,6 +333,397 @@ void showRecordingPermissionMissingDialog() env->DeleteLocalRef(activity); } +/* + * Helper functions to aid new fusing method + */ +static AAssetManager *getAssetManager() +{ + JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv(); + jobject self = (jobject) SDL_AndroidGetActivity(); + jclass activity = env->GetObjectClass(self); + jmethodID method = env->GetMethodID(activity, "getAssetManager", "()Landroid/content/res/AssetManager;"); + + jobject assetManager = env->CallObjectMethod(self, method); + AAssetManager *assetManagerObject = AAssetManager_fromJava(env, assetManager); + + env->DeleteLocalRef(assetManager); + env->DeleteLocalRef(activity); + env->DeleteLocalRef(self); + + return assetManagerObject; +} + +namespace aasset +{ +namespace io +{ + +struct AssetInfo +{ + AAssetManager *assetManager; + AAsset *asset; + char *filename; + size_t size; +}; + +static std::unordered_map fileTree; + +// Workaround AAsset can't detect whetever something is a directory or doesn't exist +static void buildFileLookup( + AAssetManager *assetManager, + std::unordered_map &out, + const std::string &path = "" +) +{ + if (!path.empty()) + { + AAsset *test = AAssetManager_open(assetManager, path.c_str(), AASSET_MODE_STREAMING); + if (test) + { + AAsset_close(test); + out[path] = PHYSFS_FILETYPE_REGULAR; + return; + } + + out[path] = PHYSFS_FILETYPE_DIRECTORY; + } + + AAssetDir *dir = AAssetManager_openDir(assetManager, path.c_str()); + const char *file; + + while ((file = AAssetDir_getNextFileName(dir)) != nullptr) + buildFileLookup(assetManager, out, file); + + AAssetDir_close(dir); +} + +PHYSFS_sint64 read(PHYSFS_Io *io, void *buf, PHYSFS_uint64 len) +{ + AAsset *asset = ((AssetInfo *) io->opaque)->asset; + int readed = AAsset_read(asset, buf, (size_t) len); + + PHYSFS_setErrorCode(readed < 0 ? PHYSFS_ERR_OS_ERROR : PHYSFS_ERR_OK); + return (PHYSFS_sint64) readed; +} + +PHYSFS_sint64 write(PHYSFS_Io *io, const void *buf, PHYSFS_uint64 len) +{ + LOVE_UNUSED(io); + LOVE_UNUSED(buf); + LOVE_UNUSED(len); + + PHYSFS_setErrorCode(PHYSFS_ERR_READ_ONLY); + return -1; +} + +int seek(PHYSFS_Io *io, PHYSFS_uint64 offset) +{ + AAsset *asset = ((AssetInfo *) io->opaque)->asset; + int success = AAsset_seek64(asset, (off64_t) offset, SEEK_SET) != -1; + + PHYSFS_setErrorCode(success ? PHYSFS_ERR_OK : PHYSFS_ERR_OS_ERROR); + return success; +} + +PHYSFS_sint64 tell(PHYSFS_Io *io) +{ + AAsset *asset = ((AssetInfo *) io->opaque)->asset; + off64_t len = AAsset_getLength64(asset); + off64_t remain = AAsset_getRemainingLength64(asset); + + return len - remain; +} + +PHYSFS_sint64 length(PHYSFS_Io *io) +{ + AAsset *asset = ((AssetInfo *) io->opaque)->asset; + return AAsset_getLength64(asset); +} + +// Forward declaration +PHYSFS_Io *fromAAsset(AAssetManager *assetManager, const char *filename, AAsset *asset); + +PHYSFS_Io *duplicate(PHYSFS_Io *io) +{ + AssetInfo *assetInfo = (AssetInfo *) io->opaque; + AAsset *asset = AAssetManager_open(assetInfo->assetManager, assetInfo->filename, AASSET_MODE_RANDOM); + + if (asset == nullptr) + { + PHYSFS_setErrorCode(PHYSFS_ERR_OS_ERROR); + return nullptr; + } + + AAsset_seek64(asset, tell(io), SEEK_SET); + return fromAAsset(assetInfo->assetManager, assetInfo->filename, asset); +} + +void destroy(PHYSFS_Io *io) +{ + AssetInfo *assetInfo = (AssetInfo *) io->opaque; + AAsset_close(assetInfo->asset); + delete[] assetInfo->filename; + delete assetInfo; + delete io; +} + +PHYSFS_Io *fromAAsset(AAssetManager *assetManager, const char *filename, AAsset *asset) +{ + // Create AssetInfo + AssetInfo *assetInfo = new (std::nothrow) AssetInfo(); + assetInfo->assetManager = assetManager; + assetInfo->asset = asset; + assetInfo->size = strlen(filename) + 1; + assetInfo->filename = new (std::nothrow) char[assetInfo->size]; + memcpy(assetInfo->filename, filename, assetInfo->size); + + // Create PHYSFS_Io + PHYSFS_Io *io = new (std::nothrow) PHYSFS_Io(); + io->version = 0; + io->opaque = assetInfo; + io->read = read; + io->write = write; + io->seek = seek; + io->tell = tell; + io->length = length; + io->duplicate = duplicate; + io->flush = nullptr; + io->destroy = destroy; + + return io; +} + +} + +void *openArchive(PHYSFS_Io *io, const char *name, int forWrite, int *claimed) +{ + if (io->opaque == nullptr || memcmp(io->opaque, "ASET", 4) != 0) + return nullptr; + + // It's our archive + *claimed = 1; + AAssetManager *assetManager = getAssetManager(); + + if (io::fileTree.empty()) + io::buildFileLookup(assetManager, io::fileTree); + + return assetManager; +} + +PHYSFS_EnumerateCallbackResult enumerate( + void *opaque, + const char *dirname, + PHYSFS_EnumerateCallback cb, + const char *origdir, + void *callbackdata +) +{ + const char *path = dirname; + if (path == nullptr || (path[0] == '/' && path[1] == 0)) + path = ""; + + AAssetManager *assetManager = (AAssetManager *) opaque; + AAssetDir *dir = AAssetManager_openDir(assetManager, path); + + if (dir == nullptr) + { + PHYSFS_setErrorCode(PHYSFS_ERR_NOT_FOUND); + return PHYSFS_ENUM_ERROR; + } + + PHYSFS_EnumerateCallbackResult ret = PHYSFS_ENUM_OK; + + while (ret == PHYSFS_ENUM_OK) + { + const char *name = AAssetDir_getNextFileName(dir); + + // No more files? + if (name == nullptr) + break; + + ret = cb(callbackdata, origdir, name); + } + + AAssetDir_close(dir); + return ret; +} + +PHYSFS_Io *openRead(void *opaque, const char *name) +{ + AAssetManager *assetManager = (AAssetManager *) opaque; + AAsset *file = AAssetManager_open(assetManager, name, AASSET_MODE_UNKNOWN); + + if (file == nullptr) + { + PHYSFS_setErrorCode(PHYSFS_ERR_NOT_FOUND); + return nullptr; + } + + PHYSFS_setErrorCode(PHYSFS_ERR_OK); + return io::fromAAsset(assetManager, name, file); +} + +PHYSFS_Io *openWriteAppend(void *opaque, const char *name) +{ + LOVE_UNUSED(opaque); + LOVE_UNUSED(name); + + // AAsset doesn't support modification + PHYSFS_setErrorCode(PHYSFS_ERR_READ_ONLY); + return nullptr; +} + +int removeMkdir(void *opaque, const char *name) +{ + LOVE_UNUSED(opaque); + LOVE_UNUSED(name); + + // AAsset doesn't support modification + PHYSFS_setErrorCode(PHYSFS_ERR_READ_ONLY); + return 0; +} + +int stat(void *opaque, const char *name, PHYSFS_Stat *out) +{ + LOVE_UNUSED(opaque); + + auto result = io::fileTree.find(name); + + if (result != io::fileTree.end()) + { + out->filetype = result->second; + out->modtime = -1; + out->createtime = -1; + out->accesstime = -1; + out->readonly = 1; + + PHYSFS_setErrorCode(PHYSFS_ERR_OK); + return 1; + } + else + { + PHYSFS_setErrorCode(PHYSFS_ERR_NOT_FOUND); + return 0; + } +} + +void closeArchive(void *opaque) +{ + // Do nothing + LOVE_UNUSED(opaque); + PHYSFS_setErrorCode(PHYSFS_ERR_OK); +} + +static PHYSFS_Archiver g_AAssetArchiver = { + 0, + { + "AASSET", + "Android AAsset Wrapper", + "LOVE Development Team", + "https://developer.android.com/ndk/reference/group/asset", + 0 + }, + openArchive, + enumerate, + openRead, + openWriteAppend, + openWriteAppend, + removeMkdir, + removeMkdir, + stat, + closeArchive +}; + +static PHYSFS_sint64 dummyReturn0(PHYSFS_Io *io) +{ + LOVE_UNUSED(io); + PHYSFS_setErrorCode(PHYSFS_ERR_OK); + return 0; +} + +static PHYSFS_Io *getDummyIO(PHYSFS_Io *io); + +static char dummyOpaque[] = "ASET"; +static PHYSFS_Io dummyIo = { + 0, + dummyOpaque, + nullptr, + nullptr, + [](PHYSFS_Io *io, PHYSFS_uint64 offset) -> int + { + PHYSFS_setErrorCode(offset == 0 ? PHYSFS_ERR_OK : PHYSFS_ERR_PAST_EOF); + return offset == 0; + }, + dummyReturn0, + dummyReturn0, + getDummyIO, + nullptr, + [](PHYSFS_Io *io) { LOVE_UNUSED(io); } +}; + +static PHYSFS_Io *getDummyIO(PHYSFS_Io *io) +{ + return &dummyIo; +} + +} + +static bool isVirtualArchiveInitialized = false; + +bool initializeVirtualArchive() +{ + if (isVirtualArchiveInitialized) + return true; + + if (!PHYSFS_registerArchiver(&aasset::g_AAssetArchiver)) + return false; + if (!PHYSFS_mountIo(&aasset::dummyIo, "ASET.AASSET", nullptr, 0)) + { + PHYSFS_deregisterArchiver(aasset::g_AAssetArchiver.info.extension); + return false; + } + + isVirtualArchiveInitialized = true; + return true; +} + +void deinitializeVirtualArchive() +{ + if (isVirtualArchiveInitialized) + { + PHYSFS_deregisterArchiver(aasset::g_AAssetArchiver.info.extension); + isVirtualArchiveInitialized = false; + } +} + +bool checkFusedGame(void **physfsIO_Out) +{ + // TODO: Reorder the loading in 12.0 + PHYSFS_Io *&io = *(PHYSFS_Io **) physfsIO_Out; + AAssetManager *assetManager = getAssetManager(); + + // Prefer game.love inside assets/ folder + AAsset *asset = AAssetManager_open(assetManager, "game.love", AASSET_MODE_RANDOM); + if (asset) + { + io = aasset::io::fromAAsset(assetManager, "game.love", asset); + return true; + } + + // If there's no game.love inside assets/ try main.lua + asset = AAssetManager_open(assetManager, "main.lua", AASSET_MODE_STREAMING); + + if (asset) + { + AAsset_close(asset); + io = nullptr; + return true; + } + + // Not found + return false; +} + } // android } // love diff --git a/src/common/android.h b/src/common/android.h index 45b5acdea..9f066c2e8 100644 --- a/src/common/android.h +++ b/src/common/android.h @@ -80,6 +80,27 @@ void requestRecordingPermission(); void showRecordingPermissionMissingDialog(); +/** + * Initialize Android AAsset virtual archive. + * @return true if successful. + */ +bool initializeVirtualArchive(); + +/** + * Deinitialize Android AAsset virtual archive. + * @return true if successful. + */ +void deinitializeVirtualArchive(); + +/** + * Retrieve the fused game inside the APK + * @param physfsIO_Out Pointer to PHYSFS_Io* struct + * @return true if there's game inside the APK. If physfsIO_Out is not null, then it contains + * the game.love which needs to be mounted to root. false if it's not fused, in which case + * physfsIO_Out is undefined. + */ +bool checkFusedGame(void **physfsIO_Out); + } // android } // love diff --git a/src/common/runtime.cpp b/src/common/runtime.cpp index f09a80d88..54619bf33 100644 --- a/src/common/runtime.cpp +++ b/src/common/runtime.cpp @@ -93,11 +93,42 @@ static int w__eq(lua_State *L) return 1; } +typedef uint64 ObjectKey; + +static bool luax_isfulllightuserdatasupported(lua_State *L) +{ + // LuaJIT prior to commit e9af1abec542e6f9851ff2368e7f196b6382a44c doesn't + // support lightuserdata > 48 bits. This is not a problem with Android, + // Windows, macOS, and iOS as they'll use updated LuaJIT or won't use + // pointers > 48 bits, but this is not the case for Linux. So check for + // this capability first! + static bool checked = false; + static bool supported = false; + + if (!checked) + { + lua_pushcclosure(L, [](lua_State *L) -> int + { + // Try to push pointer with all bits set. + lua_pushlightuserdata(L, (void *) (~((size_t) 0))); + return 1; + }, 0); + + supported = lua_pcall(L, 0, 1, 0) == 0; + checked = true; + + lua_pop(L, 1); + } + + return supported; +} + // For use with the love object pointer -> Proxy pointer registry. // Using the pointer directly via lightuserdata would be ideal, but LuaJIT -// cannot use lightuserdata with more than 47 bits whereas some newer arm64 -// architectures allow pointers which use more than that. -static lua_Number luax_computeloveobjectkey(lua_State *L, love::Object *object) +// (before a commit to 2.1 in 2020) cannot use lightuserdata with more than 47 +// bits whereas some newer arm64 architectures allow pointers which use more +// than that. +static ObjectKey luax_computeloveobjectkey(lua_State *L, love::Object *object) { // love objects should be allocated on the heap, and thus are subject // to the alignment rules of operator new / malloc. Lua numbers (doubles) @@ -118,11 +149,20 @@ static lua_Number luax_computeloveobjectkey(lua_State *L, love::Object *object) key >>= shift; - // Make sure our key isn't larger than 2^53. - if (key > 0x20000000000000ULL) - luaL_error(L, "Cannot push love object to Lua: pointer value %p is too large", object); + return (ObjectKey) key; +} - return (lua_Number) key; +static void luax_pushloveobjectkey(lua_State *L, ObjectKey key) +{ + // If full 64-bit lightuserdata is supported, always use that. Otherwise, + // if the key is smaller than 2^53 (which is integer precision for double + // datatype), then push number. Otherwise, throw error. + if (luax_isfulllightuserdatasupported(L)) + lua_pushlightuserdata(L, (void *) key); + else if (key > 0x20000000000000ULL) // 2^53 + luaL_error(L, "Cannot push love object to Lua: pointer value %p is too large", key); + else + lua_pushnumber(L, (lua_Number) key); } static int w__release(lua_State *L) @@ -141,8 +181,8 @@ static int w__release(lua_State *L) if (lua_istable(L, -1)) { // loveobjects[object] = nil - lua_Number objectkey = luax_computeloveobjectkey(L, object); - lua_pushnumber(L, objectkey); + ObjectKey objectkey = luax_computeloveobjectkey(L, object); + luax_pushloveobjectkey(L, objectkey); lua_pushnil(L); lua_settable(L, -3); } @@ -622,10 +662,10 @@ void luax_pushtype(lua_State *L, love::Type &type, love::Object *object) return luax_rawnewtype(L, type, object); } - lua_Number objectkey = luax_computeloveobjectkey(L, object); + ObjectKey objectkey = luax_computeloveobjectkey(L, object); // Get the value of loveobjects[object] on the stack. - lua_pushnumber(L, objectkey); + luax_pushloveobjectkey(L, objectkey); lua_gettable(L, -2); // If the Proxy userdata isn't in the instantiated types table yet, add it. @@ -635,7 +675,7 @@ void luax_pushtype(lua_State *L, love::Type &type, love::Object *object) luax_rawnewtype(L, type, object); - lua_pushnumber(L, objectkey); + luax_pushloveobjectkey(L, objectkey); lua_pushvalue(L, -2); // loveobjects[object] = Proxy. diff --git a/src/modules/filesystem/physfs/Filesystem.cpp b/src/modules/filesystem/physfs/Filesystem.cpp index b329344ca..8768711dc 100644 --- a/src/modules/filesystem/physfs/Filesystem.cpp +++ b/src/modules/filesystem/physfs/Filesystem.cpp @@ -118,6 +118,10 @@ Filesystem::Filesystem() Filesystem::~Filesystem() { +#ifdef LOVE_ANDROID + love::android::deinitializeVirtualArchive(); +#endif + if (PHYSFS_isInit()) PHYSFS_deinit(); } @@ -232,30 +236,56 @@ bool Filesystem::setSource(const char *source) if (!love::android::createStorageDirectories()) SDL_Log("Error creating storage directories!"); - new_search_path = love::android::getSelectedGameFile(); + new_search_path = ""; - // try mounting first, if that fails, load to memory and mount - if (!PHYSFS_mount(new_search_path.c_str(), nullptr, 1)) + PHYSFS_Io *gameLoveIO; + bool hasFusedGame = love::android::checkFusedGame((void **) &gameLoveIO); + + if (hasFusedGame) { - // PHYSFS cannot yet mount a zip file inside an .apk - SDL_Log("Mounting %s did not work. Loading to memory.", - new_search_path.c_str()); - char* game_archive_ptr = NULL; - size_t game_archive_size = 0; - if (!love::android::loadGameArchiveToMemory( - new_search_path.c_str(), &game_archive_ptr, - &game_archive_size)) + if (gameLoveIO) { - SDL_Log("Failure memory loading archive %s", new_search_path.c_str()); - return false; + // Actually we should just be able to mount gameLoveIO, but that's experimental. + gameLoveIO->destroy(gameLoveIO); + goto oldschool; } - if (!PHYSFS_mountMemory( - game_archive_ptr, game_archive_size, - love::android::freeGameArchiveMemory, "archive.zip", "/", 0)) + else { - SDL_Log("Failure mounting in-memory archive."); - love::android::freeGameArchiveMemory(game_archive_ptr); - return false; + if (!love::android::initializeVirtualArchive()) + { + SDL_Log("Unable to mount AAsset: %s", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + return false; + } + } + } + else + { + oldschool: + new_search_path = love::android::getSelectedGameFile(); + + // try mounting first, if that fails, load to memory and mount + if (!PHYSFS_mount(new_search_path.c_str(), nullptr, 1)) + { + // PHYSFS cannot yet mount a zip file inside an .apk + SDL_Log("Mounting %s did not work. Loading to memory.", + new_search_path.c_str()); + char* game_archive_ptr = NULL; + size_t game_archive_size = 0; + if (!love::android::loadGameArchiveToMemory( + new_search_path.c_str(), &game_archive_ptr, + &game_archive_size)) + { + SDL_Log("Failure memory loading archive %s", new_search_path.c_str()); + return false; + } + if (!PHYSFS_mountMemory( + game_archive_ptr, game_archive_size, + love::android::freeGameArchiveMemory, "archive.zip", "/", 0)) + { + SDL_Log("Failure mounting in-memory archive."); + love::android::freeGameArchiveMemory(game_archive_ptr); + return false; + } } } #else