diff --git a/README.md b/README.md index dd3f84a..494a050 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,12 @@ Try these non-SPACEBOX demos for verifying the Raspberry Pi OpenGL ES setup with The [fill_screen demo][] has a working example of how to build for Android. It may be worthwhile to read the [SDL wiki Android page][] and [SDL docs Android README][] and compile an SDL example for Linux before doing a SPACEBOX Android build. The source distributions for SDL, SDL image, SDL ttf, and SDL mixer, and the Android SDK are required. +After building the demo, see the following for further information on using SDL on Android. + +* [SDL wiki](https://wiki.libsdl.org/Android) +* [SDL Android README](https://github.com/libsdl-org/SDL/blob/main/docs/README-android.md) +* In the SDL source package, `SDL_android.h` and the Android section of `SDL_system.h` + #### Building an SDL example for Linux * Install Java packages @@ -255,6 +261,10 @@ These steps were taken to build the fill_screen demo for Android. The Android SD $ ANDROID_SDK_ROOT=$HOME/local/Android ./gradlew build +#### Screen rotation + +Note that `SDL_WINDOW_RESIZABLE` is [required for screen rotation](https://discourse.libsdl.org/t/screen-orientation-not-changing-when-rotating-android-device/26676) to work + ### OS X, Windows Builds for these platforms have only passed the proof of concept phase. An early version of SPACEBOX was compiled for each of them, but none of the demos have been compiled for them in their current form, so there is only some broken code available in the box demo Makefile. @@ -383,9 +393,37 @@ This builds the local, WASM, and Android libraries by downloading OpenCV 4.7.0 a $ python3 platforms/android/build_sdk.py --sdk_path ~/local/Android/ --ndk_path ~/local/Android/ndk/22.1.7171670/ \ --extra_modules_path ../opencv_contrib-4.7.0-subset/ --shared --no_samples_build build_android/ -### ZBar +### curl -Note that ZBar has been replaced with OpenCV's barcode contrib module in the original project this was compiled for. The instructions may still work, so they are left in case they are useful for another project, but they have not been used in a while. +#### Linux + +Install from the package manager + + sudo apt install libcurl4-openssl-dev + +#### Emscripten + +Use Emscripten's [Fetch API](https://emscripten.org/docs/api_reference/fetch.html) instead of curl + +#### Android + +There are instructions on how to [build for Android](https://curl.se/docs/install.html#android) in the curl documentation. There is also a project [libcurl-android](https://github.com/ibaoger/libcurl-android) that facilitates the build process. + + $ git clone --recursive https://github.com/ibaoger/libcurl-android + +Add `x86` to the `APP_ABI` parameter in `build_for_android.sh`. Then run the build script. + + $ NDK_ROOT=/path/to/NDK ./build_for_android.sh + +The libraries should be written the `jni/build` folder. The folder `libs/x86-64` should actually be named `libs/x86_64`, so rename it. + + $ mv jni/build/zlib/x86-64 jni/build/zlib/x86_64 + $ mv jni/build/curl/x86-64 jni/build/curl/x86_64 + $ mv jni/build/openssl/x86-64/ jni/build/openssl/x86_64 + +See how the OpenCV libraries are included in an Android NDK project in the [camera demo](demo/camera) for an example of how to use pre-built shared libraries. + +### ZBar #### Linux diff --git a/demo/camera/camera.cpp b/demo/camera/camera.cpp index bfd40a5..0e33ba2 100644 --- a/demo/camera/camera.cpp +++ b/demo/camera/camera.cpp @@ -111,10 +111,10 @@ public: void open() { + std::ostringstream message; /* Open the OpenCV capture, using device ID #0 to get the default attached camera. */ int device_id = configuration()["scan"]["camera-device-id"]; capture.open(device_id); - std::ostringstream message; if (capture.isOpened()) { message << "Opened and initialized " << capture.get(cv::CAP_PROP_FRAME_WIDTH) << "x" << diff --git a/src/Box.hpp b/src/Box.hpp index 06d84b2..c01eb54 100644 --- a/src/Box.hpp +++ b/src/Box.hpp @@ -8,8 +8,7 @@ | ~~~~~~~ BOX |/ +-------------*/ -#ifndef SB_BOX_H_ -#define SB_BOX_H_ +#pragma once #include #include @@ -110,5 +109,3 @@ namespace std #include "extension.hpp" #include "Segment.hpp" - -#endif diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 279bd36..371b76d 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -1,32 +1,29 @@ -/* /\ +------------------------------------------------------------+ - ____/ \____ /| zlib/MIT/Unlicenced game framework licensed to freely use, | - \ / / | copy, modify and sell without restriction | - +--\ ^__^ /--+ | | - | ~/ \~ | | Learn more about [SPACE BOX] at [shampoo.ooo] | - | ~~~~~~~~~~~~ | +------------------------------------------------------------+ - | SPACE ~~~~~ | / - | ~~~~~~~ BOX |/ - +-------------*/ + /* +------------------------------------------------------+ + ____/ \____ /| - Open source game framework licensed to freely use, | + \ / / | copy, modify and sell without restriction | ++--\ ^__^ /--+ | | +| ~/ \~ | | - created for | +| ~~~~~~~~~~~~ | +------------------------------------------------------+ +| SPACE ~~~~~ | / +| ~~~~~~~ BOX |/ ++-------------*/ #include "Configuration.hpp" -/* Initialize a Configuration object. The path argument is the location where the config file is stored. - * If there is no file located at the path, it will be created if the write method is called. System level - * default assignments defined in this file can be added to and overwritten by user supplied JSON file at - * the specified path or at a path passed to the load function. */ -Configuration::Configuration(Node *parent, fs::path path) : Node(parent) +Configuration::Configuration(Node* parent) : Node(parent) { - config_path = path; set_defaults(); - load(); + merge(USER_CONFIG_PATH); +#ifdef __ANDROID__ + merge(ANDROID_CONFIG_PATH); +#endif auto_refresher.set_frame_length(config["configuration"]["auto-refresh-interval"].get()); auto_refresh(config["configuration"]["auto-refresh"]); } -/* Fill the system level config JSON dict with default values set by the framework */ void Configuration::set_defaults() { - sys_config["keys"] = { + config["keys"] = { {"record", {"CTRL", "SHIFT", "i"}}, {"save-current-stash", {"CTRL", "SHIFT", "v"}}, {"screenshot", {"CTRL", "i"}}, @@ -40,14 +37,14 @@ void Configuration::set_defaults() {"toggle-framerate", {"CTRL", "f"}}, {"reset", {"CTRL", "r"}} }; - sys_config["input"] = { + config["input"] = { {"suppress-any-key-on-mods", true}, {"system-any-key-ignore-commands", {"fullscreen", "screenshot", "toggle-framerate", "record", "quit"}}, {"any-key-ignore-commands", nlohmann::json::array()}, {"default-unsuppress-delay", 700}, {"ignore-repeat-keypress", true} }; - sys_config["display"] = { + config["display"] = { {"dimensions", {960, 540}}, {"framerate", 60}, {"title", "[SPACEBOX]"}, @@ -56,11 +53,11 @@ void Configuration::set_defaults() {"render-test-spacing", 2}, {"render driver", "opengl"} }; - sys_config["audio"] = { + config["audio"] = { {"default-sfx-root", "resource/sfx"}, {"default-bgm-root", "resource/bgm"} }; - sys_config["gl"] = { + config["gl"] = { {"depth-size", 16}, {"red-size", 8}, {"green-size", 8}, @@ -70,7 +67,7 @@ void Configuration::set_defaults() {"major-version", 3}, {"minor-version", 2} }, - sys_config["recording"] = { + config["recording"] = { {"enabled", false}, {"screenshot-prefix", "screenshot-"}, {"screenshot-extension", ".png"}, @@ -85,16 +82,16 @@ void Configuration::set_defaults() {"max-video-memory", 1000}, {"mp4-pixel-format", "yuv444p"} }; - sys_config["fps-indicator"] = { + config["fps-indicator"] = { {"width", .05}, {"height", .04}, {"background", {255, 255, 255}}, {"foreground", {0, 0, 0}} }; - sys_config["animation"] = { + config["animation"] = { {"all-frames-frameset-name", "all"} }; - sys_config["log"] = { + config["log"] = { {"enabled", false}, {"debug-to-stdout", false}, {"debug-to-file", false}, @@ -103,37 +100,33 @@ void Configuration::set_defaults() {"debug-file-name", "space_box_debug_log.txt"}, {"short-name", "spacebox"} }; - sys_config["configuration"] = { + config["configuration"] = { {"auto-refresh", false}, {"auto-refresh-interval", 1000} }; - config = sys_config; } -/* Load the configuration file at path */ -void Configuration::load(fs::path path) +nlohmann::json& Configuration::operator[](const std::string& key) { - /* read contents of path into the game level config JSON dict */ - user_config = nlohmann::json::parse(sb::file_to_string(path)); - - /* merge into the full config JSON dict */ - merge(); + return config[key]; } -/* Load the configuration file at Configuration::config_path */ -void Configuration::load() +const nlohmann::json& Configuration::operator[](const std::string& key) const { - load(config_path); + return config[key]; } -/* Merge the system level config JSON dict (hard-coded in this file) with the user level config JSON - * dict (loaded from disk by the load function) */ -void Configuration::merge() +const nlohmann::json& Configuration::operator()() const { - if (!user_config.empty()) + return config; +} + +void Configuration::merge(const nlohmann::json& incoming) +{ + if (!incoming.empty()) { /* loop over first level key/value pairs */ - for (auto& item: user_config.items()) + for (auto& item: incoming.items()) { /* if the value is an object (dict), merge it into the config, overwriting keys already in the config */ if (item.value().is_object()) @@ -147,41 +140,75 @@ void Configuration::merge() } } } + else + { + sb::Log::log("Attempted to merge empty JSON into configuration", sb::Log::WARN); + } +} + +void Configuration::merge(const fs::path& path) +{ +#ifndef __ANDROID__ + /* Can't check for file existence in an Android APK */ + if (fs::exists(path)) + { +#endif + /* Load JSON to a string and check for validity. */ + std::string contents = sb::file_to_string(path); + if (nlohmann::json::accept(contents)) + { + merge(nlohmann::json::parse(contents)); + } + else + { + std::ostringstream message; + message << "Invalid JSON at " << path; + sb::Log::log(message, sb::Log::WARN); + } +#ifndef __ANDROID__ + } + else + { + std::ostringstream message; + message << "File not found: " << path; + sb::Log::log(message, sb::Log::WARN); + } +#endif +} + +void Configuration::merge(const std::string& path) +{ + merge(fs::path(path)); +} + +void Configuration::merge(const char* path) +{ + merge(fs::path(path)); } -/* Set auto refresh to on or off */ void Configuration::auto_refresh(bool on) { on ? auto_refresher.play() : auto_refresher.pause(); } -/* Refresh the config contents by calling the default load function */ void Configuration::refresh() { - if (fs::exists(config_path) && fs::last_write_time(config_path) > config_file_modification_time) + if (fs::exists(USER_CONFIG_PATH) && fs::last_write_time(USER_CONFIG_PATH) > config_file_modification_time) { std::ostringstream message; - message << "config file modified, reloading " << config_path; + message << "config file modified, reloading " << USER_CONFIG_PATH; sb::Log::log(message, sb::Log::DEBUG); - load(); + merge(USER_CONFIG_PATH); } } -/* Write configuration to specified path in JSON format */ -void Configuration::write(fs::path path) -{ - std::ofstream output(path); - output << std::setw(tab_width) << user_config << std::endl; -} - -/* Write configuration to config_path (set at initialization) */ -void Configuration::write() -{ - write(config_path); -} - -/* Updates the auto refresher */ void Configuration::update() { auto_refresher.update(); } + +std::ostream& std::operator<<(std::ostream& out, const Configuration& configuration) +{ + out << configuration(); + return out; +} diff --git a/src/Configuration.hpp b/src/Configuration.hpp index 53cf907..ff853a8 100644 --- a/src/Configuration.hpp +++ b/src/Configuration.hpp @@ -1,15 +1,14 @@ - /* +--------------------------------------------------------------+ - ____/ \____ /| - zlib/MIT/Unlicenced game framework licensed to freely use, | - \ / / | copy, modify and sell without restriction | - +--\ ^__^ /--+ | | - | ~/ \~ | | - originally created at [http://nugget.fun] | - | ~~~~~~~~~~~~ | +--------------------------------------------------------------+ - | SPACE ~~~~~ | / - | ~~~~~~~ BOX |/ - +-------------*/ + /* +------------------------------------------------------+ + ____/ \____ /| - Open source game framework licensed to freely use, | + \ / / | copy, modify and sell without restriction | ++--\ ^__^ /--+ | | +| ~/ \~ | | - created for | +| ~~~~~~~~~~~~ | +------------------------------------------------------+ +| SPACE ~~~~~ | / +| ~~~~~~~ BOX |/ ++-------------*/ -#ifndef SB_CONFIGURATION_H_ -#define SB_CONFIGURATION_H_ +#pragma once #include #include @@ -25,29 +24,101 @@ class Configuration : public Node { private: - - nlohmann::json sys_config, user_config; - fs::path config_path; - int tab_width = 4; + + inline static const std::string USER_CONFIG_PATH = "config.json"; + inline static const std::string ANDROID_CONFIG_PATH = "config_android.json"; + Animation auto_refresher = Animation(&Configuration::refresh, this); fs::file_time_type config_file_modification_time; + nlohmann::json config; + /*! + * Fill the config JSON with default values set by the framework. + */ void set_defaults(); - void merge(); public: - nlohmann::json config; + /*! + * Construct a Configuration object. The path argument is the location to look for a user configuration. If valid JSON is found, + * it will be merged into the hard-coded default assignments, overwriting any existing key/value pairs. The path will be watched + * for changes if auto refresh is turned on. + * + * @param parent SPACEBOX Node rooted at the Game object + */ + Configuration(Node* parent); - Configuration(Node*, fs::path = "config.json"); - void load(fs::path path); - void load(); + /*! + * Get a writable JSON object corresponding to a key in the configuration. + * + * The JSON object's value can be used directly if it can be implictly type-cast, and it can be assigned a new value. See + * https://nlohmann.github.io/json/api/basic_json/ for further information on how to read and write the JSON object. + * + * @param key Top level key corresponding to a section of the SPACEBOX configuration JSON + * @return + */ + nlohmann::json& operator[](const std::string& key); + + /*! + * Get a read-only JSON object corresponding to a key in configuration. + * + * The JSON object's value can be used directly if it can be implictly type-cast. Otherwise, use the JSON object's + * get method with a template parameter. See https://nlohmann.github.io/json/api/basic_json/ for further information. + * + * @param key Top level key corresponding to a section of the SPACEBOX configuration JSON + * @return Read-only JSON object + */ + const nlohmann::json& operator[](const std::string& key) const; + + /*! + * Get a read-only JSON object reference to the entire configuration. Can be used, for example, for iterating over the + * configuration keys. + * + * @return Read-only JSON object reference to the full configuration + */ + const nlohmann::json& operator()() const; + + /*! + * Merge new SPACEBOX configuration JSON with the JSON already in memory, overwriting any existing key/value pairs. + * + * @param incoming JSON object populated with SPACEBOX configuration settings + */ + void merge(const nlohmann::json& incoming); + + /*! + * Merge new SPACEBOX configuration from a given path to a JSON file. + * + * @param path JSON file path with new configuration values + */ + void merge(const fs::path& path); + + /*! + * @overload void Configuration::merge(fs::path path) + */ + void merge(const std::string& path); + + /*! + * @overload void Configuration::merge(fs::path path) + */ + void merge(const char* path); + + /*! + * Set auto refresh to on or off. Auto refresh watches the file at Configuration::user_path (set in the contructor) for changes + * and loads them automatically at the interval set at Configuration::config["configuration"]["auto-refresh-interval"]. + * + * @param bool true to refresh automatically, false to turn off automatic refresh + */ void auto_refresh(bool); + + /*! + * Check if the user config file was modified and merge if so. + */ void refresh(); - void write(fs::path path); - void write(); + + /*! + * Update the auto refresher. + */ void update(); - virtual std::string class_name() const { return "Configuration"; } }; @@ -88,4 +159,14 @@ namespace std::filesystem } } -#endif +namespace std +{ + /*! + * Add the entire JSON when the stream operator is called. + * + * @param out Output stream + * @param configuration Configuration object being output to the stream + * @return A reference to the input stream + */ + std::ostream& operator<<(std::ostream& out, const Configuration& configuration); +} diff --git a/src/Game.cpp b/src/Game.cpp index d299f47..b7a77b1 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -30,21 +30,22 @@ Game::Game() /* Log the current working directory as seen by std::filesystem */ std::ostringstream log_message; - log_message << "Current path is " << std::filesystem::current_path(); + log_message << "Current path as seen by std::filesystem is " << std::filesystem::current_path(); sb::Log::log(log_message); /* Log Android storage paths as determined by SDL */ #if defined(__ANDROID__) || defined(ANDROID) log_message = std::ostringstream(); + log_message << "Using Android SDK version " << SDL_GetAndroidSDKVersion() << std::endl; log_message << "SDL_AndroidGetInternalStoragePath() is " << SDL_AndroidGetInternalStoragePath() << std::endl; - log_message << "SDL_AndroidGetExternalStorageState() is " << SDL_AndroidGetExternalStorageState() << std::endl; + log_message << "SDL_AndroidGetExternalStorageState() is " << SDL_AndroidGetExternalStorageState() << " (1=read, 2=write, 3=r/w)" << std::endl; log_message << "SDL_AndroidGetExternalStoragePath() is " << SDL_AndroidGetExternalStoragePath(); sb::Log::log(log_message); #endif /* Pretty print config JSON to debug log */ log_message = std::ostringstream(); - log_message << std::setw(4) << configuration() << std::endl; + log_message << std::setw(4) << configuration()() << std::endl; sb::Log::log(log_message, sb::Log::DEBUG); /* Tell SDL which render driver you will be requesting when calling SDL_CreateRenderer */ @@ -460,14 +461,14 @@ void Game::log_surface_format(SDL_Surface* surface, std::string preface) format->Amask, pixel_format.c_str()); } -const nlohmann::json& Game::configuration() const +const Configuration& Game::configuration() const { - return _configuration.config; + return _configuration; } -nlohmann::json& Game::configuration() +Configuration& Game::configuration() { - return _configuration.config; + return _configuration; } const SDL_Window* Game::window() const diff --git a/src/Game.hpp b/src/Game.hpp index 8fd486e..53ebc6e 100644 --- a/src/Game.hpp +++ b/src/Game.hpp @@ -71,7 +71,6 @@ private: int ticks; float frame_length = 1000.0 / 60.0; - Configuration _configuration {this}; SDL_Window* _window; /*! @@ -108,6 +107,7 @@ public: int frame_count_this_second = 0, last_frame_length; float frame_time_overflow = 0, last_frame_timestamp, last_frame_count_timestamp; bool done = false, show_framerate = true, is_gl_context = true; + Configuration _configuration {this}; Delegate delegate {this}; sb::Display display {this}; Recorder recorder {this}; @@ -138,8 +138,8 @@ public: void log_gl_properties() const; void log_surface_format(SDL_Surface*, std::string = "surface"); - const nlohmann::json& configuration() const; - nlohmann::json& configuration(); + const Configuration& configuration() const; + Configuration& configuration(); const SDL_Window* window() const; SDL_Window* window(); const SDL_Renderer* get_renderer() const; diff --git a/src/Input.cpp b/src/Input.cpp index 7cb8b30..ba6d509 100644 --- a/src/Input.cpp +++ b/src/Input.cpp @@ -30,7 +30,7 @@ void Input::print_key_combination(const KeyCombination &combination) const void Input::load_key_map() { - nlohmann::json &config = configuration(); + const nlohmann::json& config = configuration()(); for (auto& entry : config.at("keys").items()) { bool ctrl = false, alt = false, shift = false; diff --git a/src/Log.hpp b/src/Log.hpp index 0bedd4f..abd459d 100644 --- a/src/Log.hpp +++ b/src/Log.hpp @@ -17,8 +17,7 @@ * settings. */ -#ifndef SB_LOG_H_ -#define SB_LOG_H_ +#pragma once /* include Open GL */ #if defined(__EMSCRIPTEN__) @@ -69,5 +68,3 @@ namespace sb /* Log log = Log(&Log::record); */ } - -#endif diff --git a/src/Node.cpp b/src/Node.cpp index 2532320..520d72d 100644 --- a/src/Node.cpp +++ b/src/Node.cpp @@ -44,12 +44,12 @@ bool Node::is_active() const return active; } -const nlohmann::json& Node::configuration() const +const Configuration& Node::configuration() const { return get_root()->configuration(); } -nlohmann::json& Node::configuration() +Configuration& Node::configuration() { return get_root()->configuration(); } diff --git a/src/Node.hpp b/src/Node.hpp index cc6e483..e967cc6 100644 --- a/src/Node.hpp +++ b/src/Node.hpp @@ -23,6 +23,7 @@ class Game; class Delegate; class Input; class Box; +class Configuration; struct Audio; namespace sb @@ -44,8 +45,8 @@ public: void set_canvas(SDL_Texture*); SDL_Texture* get_canvas(); bool is_active() const; - const nlohmann::json& configuration() const; - nlohmann::json& configuration(); + const Configuration& configuration() const; + Configuration& configuration(); Delegate& get_delegate(); const sb::Display& get_display() const; const SDL_Renderer* get_renderer() const; diff --git a/src/Recorder.cpp b/src/Recorder.cpp index ab5c52c..499d3db 100644 --- a/src/Recorder.cpp +++ b/src/Recorder.cpp @@ -64,7 +64,7 @@ void Recorder::respond(SDL_Event& event) * screenshots found in the output directory. */ void Recorder::capture_screen() { - nlohmann::json config = configuration(); + const nlohmann::json& config = configuration()(); SDL_Surface* surface = get_display().screen_surface(); fs::path directory = config["recording"]["screenshot-directory"]; fs::create_directories(directory); @@ -233,8 +233,7 @@ int Recorder::get_memory_size() void Recorder::make_directory() { - nlohmann::json config = configuration(); - fs::path root = config["recording"]["video-directory"]; + fs::path root = configuration()["recording"]["video-directory"]; fs::create_directories(root); fs::path directory = sb::get_next_file_name(root, 5, "video-"); fs::create_directories(directory); diff --git a/src/android/revise_skeleton.sh b/src/android/revise_skeleton.sh index 550221d..073b57a 100755 --- a/src/android/revise_skeleton.sh +++ b/src/android/revise_skeleton.sh @@ -38,8 +38,8 @@ sed -i "s/^#.*\(org.gradle.parallel\)/\1/" "$ANDROID_BUILD_DIR/gradle.properties sed -i 's/^LOCAL_SHARED_LIBRARIES.*/& SDL2_image SDL2_mixer SDL2_ttf/' "$ANDROID_BUILD_DIR/$ANDROID_MK" sed -i "s#^LOCAL_C_INCLUDES.*#& \$(LOCAL_PATH)/../../../../../../$SB_LIB_SRC \$(LOCAL_PATH)/../../../../../../$SB_SRC#" \ "$ANDROID_BUILD_DIR/$ANDROID_MK" -sed -i "s#YourSourceHere.c#\$(wildcard \$(LOCAL_PATH)/../../../../../../*.cpp)#" "$ANDROID_BUILD_DIR/$ANDROID_MK" -sed -i 's#^LOCAL_SRC_FILES.*#& $(wildcard $(LOCAL_PATH)/../../../../../../../../src/*.cpp)#' "$ANDROID_BUILD_DIR/$ANDROID_MK" -sed -i 's#^LOCAL_SRC_FILES.*#& $(wildcard $(LOCAL_PATH)/../../../../../../../../lib/sdl2-gfx/*.c)#' "$ANDROID_BUILD_DIR/$ANDROID_MK" +sed -i "s#YourSourceHere.c#\$(wildcard \$(LOCAL_PATH)/../../../../../../$SRC/*.cpp)#" "$ANDROID_BUILD_DIR/$ANDROID_MK" +sed -i "s#^LOCAL_SRC_FILES.*#& \$(wildcard \$(LOCAL_PATH)/../../../../../../$SB_SRC/*.cpp)#" "$ANDROID_BUILD_DIR/$ANDROID_MK" +sed -i "s#^LOCAL_SRC_FILES.*#& \$(wildcard \$(LOCAL_PATH)/../../../../../../$SB_LIB_SRC/sdl2-gfx/*.c)#" "$ANDROID_BUILD_DIR/$ANDROID_MK" sed -i "s/\(name=\)\"SDLActivity\"/\1\"$ANDROID_CLASS\"/" "$ANDROID_BUILD_DIR/$ANDROID_MANIFEST" sed -i "s/Game/$ANDROID_APP_NAME/" "$ANDROID_BUILD_DIR/app/src/main/res/values/strings.xml" diff --git a/src/extension.cpp b/src/extension.cpp index 54363ad..cb4f607 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -671,6 +671,73 @@ std::string sb::file_to_string(const fs::path& path) return contents; } +fs::path sb::copy_file(fs::path from, fs::path to, bool overwrite_ok) +{ + /* Open source */ + SDL_RWops* source_rw; + fs::path destination; + if ((source_rw = SDL_RWFromFile(from.c_str(), "r")) != nullptr) + { + std::ostringstream message; + message << "Copying " << (SDL_RWsize(source_rw) / 1000) << " KB from " << from << " to " << to; + sb::Log::log(message); + + /* Allocate storage for source contents */ + std::string content; + content.resize(SDL_RWsize(source_rw)); + + /* Read entire contents in one call and ensure the entire size in bytes was read. */ + if (SDL_RWread(source_rw, content.data(), 1, SDL_RWsize(source_rw)) == static_cast(SDL_RWsize(source_rw))) + { + SDL_RWclose(source_rw); + + /* Append the filename of the source file to the destination path if destination is a directory. */ + if (fs::is_directory(to)) + { + to /= from.filename(); + } + + if (!fs::exists(to) || overwrite_ok) + { + /* Open destination, write entire contents in one call and ensure the entire size in bytes was written. */ + SDL_RWops* to_rw; + if ((to_rw = SDL_RWFromFile(to.c_str(), "w")) != nullptr) + { + if ((SDL_RWwrite(to_rw, content.data(), 1, content.size())) == content.size()) + { + std::ostringstream message; + message << "Wrote CA bundle to internal storage at " << to; + sb::Log::log(message); + destination = to; + } + else + { + sb::Log::sdl_error("Error writing to internal storage"); + } + SDL_RWclose(to_rw); + } + else + { + sb::Log::sdl_error("Error opening internal storage for writing"); + } + } + else + { + sb::Log::log("Could not copy file: destination already exists and overwrite was not set", sb::Log::WARN); + } + } + else + { + sb::Log::sdl_error("Error reading file"); + } + } + else + { + sb::Log::sdl_error("Error getting file handle"); + } + return destination; +} + int SDL_SetRenderDrawColor(SDL_Renderer* renderer, const Color& color) { return SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); diff --git a/src/extension.hpp b/src/extension.hpp index 7f49ddf..c1e89ef 100644 --- a/src/extension.hpp +++ b/src/extension.hpp @@ -8,8 +8,7 @@ | ~~~~~~~ BOX |/ +-------------*/ -#ifndef SB_EXTENSION_H_ -#define SB_EXTENSION_H_ +#pragma once /* For logging pre-SPACEBOX messages in sb::file_to_string */ #if defined(__ANDROID__) || defined(ANDROID) @@ -90,9 +89,32 @@ namespace sb * the build directory. * @return std::string containing file contents */ - std::string file_to_string(const fs::path&); + std::string file_to_string(const fs::path& path); - /* Returns an unsorted vector of keys from the passed map */ + /*! + * Copy a file from one path to another. + * + * If the destination is a directory, the file will be copied into the directory with the same filename. If the destination exists, the + * `overwrite_ok` argument controls whether or not it is overwritten. + * + * If the source or destination can't be opened, read, or written, information will be written to the log, and an empty fs::path object + * will be returned. + * + * On Android, `source` can be a file in an APK's `assets/` directory if the destination is a writable path on the Android internal + * or external filesystem, meaning this can be used to copy assets from an APK onto the Android filesystem. + * + * This uses SDL's RWops to support cross platform copying. + * + * @param to Path to a file to be copied + * @param from Path to the destination, either a file or directory + * @param overwrite_ok Only overwrite an existing destination if this is `true` + * @return Return the path to copied file if successful, or an empty fs::path otherwise + */ + fs::path copy_file(fs::path to, fs::path from, bool overwrite_ok = false); + + /*! + * Return an unsorted vector of keys from the passed map. + */ template class Map> std::vector get_keys(const Map& map) { @@ -269,5 +291,3 @@ namespace std return out; } } - -#endif