diff --git a/README.md b/README.md index 569bf331..1ef298a1 100644 --- a/README.md +++ b/README.md @@ -108,3 +108,15 @@ Currently there is no plug-in interface. If you want to work towards implementing such a system, please check [PLUGIN LICENSE ADDENDUM.md](PLUGIN%20LICENSE%20ADDENDUM.md) for licensing considerations. + +## User Overrides +Users can override files from packs by creating a folder with the same file +structure as in the pack, named `.../user-override/` where `...` is +any one of `Documents/PopTracker`, `%home%/PopTracker` or `AppPath`. + +## Portable Mode +When creating a file called `portable.txt` next to the program (not macos) or +next to poptracker **inside** the AppBundle (macos-only), the app runs in +portable mode, which changes the default pack folder to be next to the program +(not in home folder) and disables asset and pack overrides from home folder +(only allows overrides from program folder). \ No newline at end of file diff --git a/doc/PACKS.md b/doc/PACKS.md index c277bd18..2100bb5b 100644 --- a/doc/PACKS.md +++ b/doc/PACKS.md @@ -75,7 +75,7 @@ Configures behavior of the pack. "smooth_map_scaling": true|false|null, // configure the image scaling method for maps. null = default = smooth } -Currently **not** user overridable. +NOTE: User overrides for settings are merged with the pack, replacing individual keys, not the whole file. ## Lua Interface diff --git a/src/core/pack.cpp b/src/core/pack.cpp index 0ee1c64f..9001fcb9 100644 --- a/src/core/pack.cpp +++ b/src/core/pack.cpp @@ -4,6 +4,7 @@ #include #include #include "sha256.h" +#include "util.h" using nlohmann::json; @@ -103,9 +104,10 @@ static bool dirNewerThan(const char* path, const std::chrono::system_clock::time std::vector Pack::_searchPaths; +std::vector Pack::_overrideSearchPaths; -Pack::Pack(const std::string& path) : _zip(nullptr), _path(path) +Pack::Pack(const std::string& path) : _zip(nullptr), _path(path), _override(nullptr) { _loaded = std::chrono::system_clock::now(); std::string s; @@ -134,10 +136,23 @@ Pack::Pack(const std::string& path) : _zip(nullptr), _path(path) _minPopTrackerVersion = to_string(_manifest, "min_poptracker_version", ""); _targetPopTrackerVersion = to_string(_manifest, "target_poptracker_version", ""); } + + if (!_uid.empty()) { + for (const auto& path: _overrideSearchPaths) { + std::string overridePath = os_pathcat(path, _uid); + if (dirExists(overridePath)) { + _override = new Override(overridePath); + break; + } + } + } } Pack::~Pack() { + if (_override) + delete _override; + _override = nullptr; if (_zip) delete _zip; _zip = nullptr; @@ -201,8 +216,23 @@ bool Pack::hasFile(const std::string& userfile) const bool Pack::ReadFile(const std::string& userfile, std::string& out) const { std::string file; - if (!sanitizePath(userfile, file)) return false; - + if (!sanitizePath(userfile, file)) + return false; + + if (_override && userfile != "settings.json") { + bool packHasVariantFile = false; + if (!_variant.empty()) { + if (_zip) + packHasVariantFile = _zip->hasFile(_variant+"/"+file); + else + packHasVariantFile = fileExists(os_pathcat(_path,_variant,file)); + } + if (packHasVariantFile && _override->ReadFile(_variant+"/"+file, out)) + return true; + else if ((!packHasVariantFile || _variant.empty()) && _override->ReadFile(file, out)) + return true; + } + if (_zip) { if (!_variant.empty() && _zip->readFile(_variant+"/"+file, out)) return true; @@ -252,14 +282,26 @@ void Pack::setVariant(const std::string& variant) std::string s; if (ReadFile("settings.json", s)) { - // TODO: allow extending from pack overrides _settings = parse_jsonc(s); - if (_settings.type() != json::value_t::object) + if (!_settings.is_object()) fprintf(stderr, "WARNING: invalid settings.json\n"); } if (_settings.type() != json::value_t::object) _settings = json::object(); + + if (_override) { + printf("overriding from %s\n", sanitize_print(_override->getPath()).c_str()); + if (_override->ReadFile("settings.json", s)) { + json overrides = parse_jsonc(s); + if (!overrides.is_object()) { + fprintf(stderr, "WARNING: invalid settings.json override\n"); + } else { + printf("settings.json overridden\n"); + _settings.update(overrides); // extend/override + } + } + } } std::string Pack::getPlatform() const @@ -322,6 +364,8 @@ std::set Pack::getVariantFlags() const bool Pack::hasFilesChanged() const { + if (_override && _override->hasFilesChanged(_loaded)) + return true; if (_zip) return fileNewerThan(_path, _loaded); else @@ -409,34 +453,39 @@ Pack::Info Pack::Find(const std::string& uid, const std::string& version, const return {}; } -void Pack::addSearchPath(const std::string& path) +static void addPath(const std::string& path, std::vector& paths) { #if !defined WIN32 && !defined _WIN32 char* tmp = realpath(path.c_str(), NULL); if (tmp) { std::string real = tmp; free(tmp); - if (std::find(_searchPaths.begin(), _searchPaths.end(), real) != _searchPaths.end()) + if (std::find(paths.begin(), paths.end(), real) != paths.end()) return; - _searchPaths.push_back(real); + paths.push_back(real); } else #else char* tmp = _fullpath(NULL, path.c_str(), 1024); if (tmp) { auto cmp = [tmp](const std::string& s) { return strcasecmp(tmp, s.c_str()) == 0; }; - if (std::find_if(_searchPaths.begin(), _searchPaths.end(), cmp) != _searchPaths.end()) + if (std::find_if(paths.begin(), paths.end(), cmp) != paths.end()) return; - _searchPaths.push_back(tmp); + paths.push_back(tmp); free(tmp); } else #endif { - if (std::find(_searchPaths.begin(), _searchPaths.end(), path) != _searchPaths.end()) + if (std::find(paths.begin(), paths.end(), path) != paths.end()) return; - _searchPaths.push_back(path); + paths.push_back(path); } } +void Pack::addSearchPath(const std::string& path) +{ + addPath(path, _searchPaths); +} + bool Pack::isInSearchPath(const std::string& uncleanPath) { std::string path = cleanUpPath(uncleanPath); @@ -452,3 +501,51 @@ const std::vector& Pack::getSearchPaths() { return _searchPaths; } + +void Pack::addOverrideSearchPath(const std::string& path) +{ + addPath(path, _overrideSearchPaths); +} + +Pack::Override::Override(const std::string& path) + : _path(path) +{ +} + +Pack::Override::~Override() +{ +} + +bool Pack::Override::ReadFile(const std::string& userfile, std::string& out) const +{ + std::string file; + if (!sanitizePath(userfile, file)) + return false; + + out.clear(); + FILE* f = nullptr; + if (!f) { + f = fopen(os_pathcat(_path,file).c_str(), "rb"); + } + if (!f) { + return false; + } + while (!feof(f)) { + char buf[4096]; + size_t sz = fread(buf, 1, sizeof(buf), f); + if (ferror(f)) goto read_err; + out += std::string(buf, sz); + } + fclose(f); + return true; + +read_err: + fclose(f); + out.clear(); + return false; +} + +bool Pack::Override::hasFilesChanged(std::chrono::system_clock::time_point since) const +{ + return dirNewerThan(_path.c_str(), since); +} diff --git a/src/core/pack.h b/src/core/pack.h index cfa40507..939e6dc0 100644 --- a/src/core/pack.h +++ b/src/core/pack.h @@ -67,7 +67,22 @@ class Pack final { static bool isInSearchPath(const std::string& path); static const std::vector& getSearchPaths(); + static void addOverrideSearchPath(const std::string& path); + private: + class Override { + public: + Override(const std::string& path); + virtual ~Override(); + + bool ReadFile(const std::string& file, std::string& out) const; + bool hasFilesChanged(std::chrono::system_clock::time_point since) const; + const std::string& getPath() const { return _path; } + + private: + std::string _path; + }; + Zip* _zip; std::string _path; std::string _variant; @@ -80,10 +95,12 @@ class Pack final { Version _targetPopTrackerVersion; nlohmann::json _manifest; nlohmann::json _settings; + Override* _override; std::chrono::system_clock::time_point _loaded; static std::vector _searchPaths; + static std::vector _overrideSearchPaths; }; #endif // _CORE_PACK_H diff --git a/src/poptracker.cpp b/src/poptracker.cpp index fab56320..cbd61a1f 100644 --- a/src/poptracker.cpp +++ b/src/poptracker.cpp @@ -170,28 +170,45 @@ PopTracker::PopTracker(int argc, char** argv, bool cli, const json& args) _ui = nullptr; // UI init moved to start() Pack::addSearchPath("packs"); // current directory + Pack::addOverrideSearchPath("user-override"); std::string cwdPath = getCwd(); std::string documentsPath = getDocumentsPath(); std::string homePath = getHomePath(); - _homePackDir = os_pathcat(homePath, "PopTracker", "packs"); + std::string homePopTrackerPath = os_pathcat(homePath, "PopTracker"); + _homePackDir = os_pathcat(homePopTrackerPath, "packs"); _appPackDir = os_pathcat(appPath, "packs"); if (!homePath.empty()) { Pack::addSearchPath(_homePackDir); // default user packs - if (!_isPortable) - Assets::addSearchPath(os_pathcat(homePath, "PopTracker", "assets")); // default user overrides + if (!_isPortable) { + // default user overrides + std::string homeUserOverrides = os_pathcat(homePopTrackerPath, "user-override"); + if (dirExists(homePopTrackerPath)) + mkdir_recursive(homeUserOverrides); + Pack::addOverrideSearchPath(homeUserOverrides); + Assets::addSearchPath(os_pathcat(homePopTrackerPath, "assets")); + } } if (!documentsPath.empty() && documentsPath != ".") { - Pack::addSearchPath(os_pathcat(documentsPath, "PopTracker", "packs")); // alternative user packs + std::string documentsPopTrackerPath = os_pathcat(documentsPath, "PopTracker"); + Pack::addSearchPath(os_pathcat(documentsPopTrackerPath, "packs")); // alternative user packs + if (!_isPortable) { + std::string documentsUserOverrides = os_pathcat(documentsPopTrackerPath, "user-override"); + Pack::addOverrideSearchPath(documentsUserOverrides); + if (dirExists(documentsPopTrackerPath)) + mkdir_recursive(documentsUserOverrides); + } if (_config.value("add_emo_packs", false)) { Pack::addSearchPath(os_pathcat(documentsPath, "EmoTracker", "packs")); // "old" packs } } + if (!appPath.empty() && appPath != "." && appPath != cwdPath) { Pack::addSearchPath(_appPackDir); // system packs - Assets::addSearchPath(os_pathcat(appPath,"assets")); // system assets + Pack::addOverrideSearchPath(os_pathcat(appPath, "user-override")); // portable/system overrides + Assets::addSearchPath(os_pathcat(appPath, "assets")); // system assets } _asio = new asio::io_service();