The C++ game engine that I’ve been working on stores game data in a custom binary archive that I mount using PhysicsFS in order to access individual files.
On the desktop version of the engine the archive lives in the same folder as the executable, and it can be easily mounted by using PHYSFS_getBaseDir to find its path. The Android port of the engine, on the other hand, stores the data archive in the APK assets, which cannot be accessed directly by PhysicsFS.
There are multiple ways to work around this problem:
Copy the archive to external storage when the game is launched for the first time and mount it from there. Alternatively, don’t ship the archive with the game and instead download it from a server at the first launch.
Read the whole archive to memory and then use PHYSFS_mountMemory to mount it.
Write a custom PHYSFS_Io interface to access android assets, and load the archive using PHYSFS_mountIo.
I decided to go with the third option since the first approach required coding too much new behaviour, and I didn’t like the idea of keeping the archive loaded in memory for the whole game.
SDL2 to the rescue
I have never done any Android development before porting my engine, so I wanted to avoid using the Android Asset Manager API and having to interact with Java. Luckily, my engine uses SDL2, which includes an I/O interface (SDL_RWops) that can read Android assets, so I just wrapped this structure in a custom PHYSFS_Io.
The only non-static function that we need is SDL_RW_Io, which takes as input the filename of the archive (relative to the assets folder), creates the SDL_RWops structure using SDL_RWFromFile, and wrapts it in a PHYSFS_Io.
Source
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include"sdl_rw.h"// header from before
#include"physfs.h"#include"SDL_rwops.h"//struct to hold the SDL_RWops pointer and other file information
structsdl_rw_file_info{SDL_RWops*RW;std::stringname;Sint64cursor_pos;Sint64size;};//forward declarations
staticPHYSFS_sint64io_read(PHYSFS_Io*io,void*buffer,PHYSFS_uint64len);staticintio_seek(PHYSFS_Io*io,PHYSFS_uint64offset);staticPHYSFS_sint64io_tell(PHYSFS_Io*io);staticPHYSFS_sint64io_tell(PHYSFS_Io*io);staticPHYSFS_Io*io_duplicate(PHYSFS_Io*io);staticvoidio_destroy(PHYSFS_Io*io);
The forward declarations are for the functions to assign to the fields of the PHYSFS_Io structure.
The function to create the PHYSFS_Io is pretty straightfoward: we use SDL_RWFromFile to create the SDL_RWops, populate the sdl_rw_file_info (which will be the opaque field of the PHYSFS_Io), and create the PHYSFS_Io.
If anything goes wrong when opening or reading the size of the archive any allocated memory is freed and the function returns a null pointer.
The io_read function uses SDL_RWread to read a certain number of bytes from the archive. The only thing to note is that SDL_RWread returns 0 whether there’s an error or it’s the end of the file, so we check SDL_GetError to distinguish the two cases and return -1 in case of error.
For io_duplicate we have to create a full independent copy of the PHYSFS_Io, so we create a new SDL_RWops by using SDL_RW_Io again with the filename that we stored.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
staticPHYSFS_Io*io_duplicate(PHYSFS_Io*io){autoorigfinfo=(sdl_rw_file_info*)io->opaque;autoretval=SDL_RW_Io(origfinfo->name);if(!retval){returnnullptr;}autofinfo=(sdl_rw_file_info*)retval->opaque;finfo->cursor_pos=origfinfo->cursor_pos;// seek the SDL_RWops to the appropriate position
if(SDL_RWseek(finfo->RW,finfo->cursor_pos,RW_SEEK_SET)<0){retval->destroy(retval);// free allocated memory
returnnullptr;}returnretval;}
autoio=SDL_RW_Io("data.pack");if(io){if(!PHYSFS_mountIo(io,"data.pack","/",false)){// PHYSFS_mountIo does not destroy the PHYSFS_Io if it fails
// so we do it manually
io->destroy(io);}}
Note that the PHYSFS_Io needs to be kept until the archive is unmounted. Calling PHYSFS_unmount will automatically call io->destroy(io) and free the associated memory.