diff options
Diffstat (limited to 'NorthstarDLL/mods')
-rw-r--r-- | NorthstarDLL/mods/autodownload/moddownloader.cpp | 638 | ||||
-rw-r--r-- | NorthstarDLL/mods/autodownload/moddownloader.h | 151 | ||||
-rw-r--r-- | NorthstarDLL/mods/compiled/kb_act.cpp | 44 | ||||
-rw-r--r-- | NorthstarDLL/mods/compiled/modkeyvalues.cpp | 106 | ||||
-rw-r--r-- | NorthstarDLL/mods/compiled/modpdef.cpp | 118 | ||||
-rw-r--r-- | NorthstarDLL/mods/compiled/modscriptsrson.cpp | 65 | ||||
-rw-r--r-- | NorthstarDLL/mods/modmanager.cpp | 1150 | ||||
-rw-r--r-- | NorthstarDLL/mods/modmanager.h | 187 | ||||
-rw-r--r-- | NorthstarDLL/mods/modsavefiles.cpp | 572 | ||||
-rw-r--r-- | NorthstarDLL/mods/modsavefiles.h | 16 |
10 files changed, 0 insertions, 3047 deletions
diff --git a/NorthstarDLL/mods/autodownload/moddownloader.cpp b/NorthstarDLL/mods/autodownload/moddownloader.cpp deleted file mode 100644 index 165399e3..00000000 --- a/NorthstarDLL/mods/autodownload/moddownloader.cpp +++ /dev/null @@ -1,638 +0,0 @@ -#include "moddownloader.h" -#include <rapidjson/fwd.h> -#include <mz_strm_mem.h> -#include <mz.h> -#include <mz_strm.h> -#include <mz_zip.h> -#include <mz_compat.h> -#include <thread> -#include <future> -#include <bcrypt.h> -#include <winternl.h> -#include <fstream> - -ModDownloader* g_pModDownloader; - -ModDownloader::ModDownloader() -{ - spdlog::info("Mod downloader initialized"); - - // Initialise mods list URI - char* clachar = strstr(GetCommandLineA(), CUSTOM_MODS_URL_FLAG); - if (clachar) - { - std::string url; - int iFlagStringLength = strlen(CUSTOM_MODS_URL_FLAG); - std::string cla = std::string(clachar); - if (strncmp(cla.substr(iFlagStringLength, 1).c_str(), "\"", 1)) - { - int space = cla.find(" "); - url = cla.substr(iFlagStringLength, space - iFlagStringLength); - } - else - { - std::string quote = "\""; - int quote1 = cla.find(quote); - int quote2 = (cla.substr(quote1 + 1)).find(quote); - url = cla.substr(quote1 + 1, quote2); - } - spdlog::info("Found custom verified mods URL in command line argument: {}", url); - modsListUrl = strdup(url.c_str()); - } - else - { - spdlog::info("Custom verified mods URL not found in command line arguments, using default URL."); - modsListUrl = strdup(DEFAULT_MODS_LIST_URL); - } -} - -size_t WriteToString(void* ptr, size_t size, size_t count, void* stream) -{ - ((std::string*)stream)->append((char*)ptr, 0, size * count); - return size * count; -} - -void ModDownloader::FetchModsListFromAPI() -{ - std::thread requestThread( - [this]() - { - CURLcode result; - CURL* easyhandle; - rapidjson::Document verifiedModsJson; - std::string url = modsListUrl; - - curl_global_init(CURL_GLOBAL_ALL); - easyhandle = curl_easy_init(); - std::string readBuffer; - - // Fetching mods list from GitHub repository - curl_easy_setopt(easyhandle, CURLOPT_CUSTOMREQUEST, "GET"); - curl_easy_setopt(easyhandle, CURLOPT_TIMEOUT, 30L); - curl_easy_setopt(easyhandle, CURLOPT_URL, url.c_str()); - curl_easy_setopt(easyhandle, CURLOPT_FAILONERROR, 1L); - curl_easy_setopt(easyhandle, CURLOPT_WRITEDATA, &readBuffer); - curl_easy_setopt(easyhandle, CURLOPT_WRITEFUNCTION, WriteToString); - result = curl_easy_perform(easyhandle); - - if (result == CURLcode::CURLE_OK) - { - spdlog::info("Mods list successfully fetched."); - } - else - { - spdlog::error("Fetching mods list failed: {}", curl_easy_strerror(result)); - goto REQUEST_END_CLEANUP; - } - - // Load mods list into local state - spdlog::info("Loading mods configuration..."); - verifiedModsJson.Parse(readBuffer); - for (auto i = verifiedModsJson.MemberBegin(); i != verifiedModsJson.MemberEnd(); ++i) - { - std::string name = i->name.GetString(); - std::string dependency = i->value["DependencyPrefix"].GetString(); - - std::unordered_map<std::string, VerifiedModVersion> modVersions; - rapidjson::Value& versions = i->value["Versions"]; - assert(versions.IsArray()); - for (auto& attribute : versions.GetArray()) - { - assert(attribute.IsObject()); - std::string version = attribute["Version"].GetString(); - std::string checksum = attribute["Checksum"].GetString(); - modVersions.insert({version, {.checksum = checksum}}); - } - - VerifiedModDetails modConfig = {.dependencyPrefix = dependency, .versions = modVersions}; - verifiedMods.insert({name, modConfig}); - spdlog::info("==> Loaded configuration for mod \"" + name + "\""); - } - - spdlog::info("Done loading verified mods list."); - - REQUEST_END_CLEANUP: - curl_easy_cleanup(easyhandle); - }); - requestThread.detach(); -} - -size_t WriteData(void* ptr, size_t size, size_t nmemb, FILE* stream) -{ - size_t written; - written = fwrite(ptr, size, nmemb, stream); - return written; -} - -int ModDownloader::ModFetchingProgressCallback( - void* ptr, curl_off_t totalDownloadSize, curl_off_t finishedDownloadSize, curl_off_t totalToUpload, curl_off_t nowUploaded) -{ - if (totalDownloadSize != 0 && finishedDownloadSize != 0) - { - ModDownloader* instance = static_cast<ModDownloader*>(ptr); - auto currentDownloadProgress = roundf(static_cast<float>(finishedDownloadSize) / totalDownloadSize * 100); - instance->modState.progress = finishedDownloadSize; - instance->modState.total = totalDownloadSize; - instance->modState.ratio = currentDownloadProgress; - } - - return 0; -} - -std::optional<fs::path> ModDownloader::FetchModFromDistantStore(std::string_view modName, std::string_view modVersion) -{ - // Retrieve mod prefix from local mods list, or use mod name as mod prefix if bypass flag is set - std::string modPrefix = strstr(GetCommandLineA(), VERIFICATION_FLAG) ? modName.data() : verifiedMods[modName.data()].dependencyPrefix; - // Build archive distant URI - std::string archiveName = std::format("{}-{}.zip", modPrefix, modVersion.data()); - std::string url = STORE_URL + archiveName; - spdlog::info(std::format("Fetching mod archive from {}", url)); - - // Download destination - std::filesystem::path downloadPath = std::filesystem::temp_directory_path() / archiveName; - spdlog::info(std::format("Downloading archive to {}", downloadPath.generic_string())); - - // Update state - modState.state = DOWNLOADING; - - // Download the actual archive - bool failed = false; - FILE* fp = fopen(downloadPath.generic_string().c_str(), "wb"); - CURLcode result; - CURL* easyhandle; - easyhandle = curl_easy_init(); - - curl_easy_setopt(easyhandle, CURLOPT_URL, url.data()); - curl_easy_setopt(easyhandle, CURLOPT_FAILONERROR, 1L); - curl_easy_setopt(easyhandle, CURLOPT_WRITEDATA, fp); - curl_easy_setopt(easyhandle, CURLOPT_WRITEFUNCTION, WriteData); - curl_easy_setopt(easyhandle, CURLOPT_NOPROGRESS, 0L); - curl_easy_setopt(easyhandle, CURLOPT_XFERINFOFUNCTION, ModDownloader::ModFetchingProgressCallback); - curl_easy_setopt(easyhandle, CURLOPT_XFERINFODATA, this); - result = curl_easy_perform(easyhandle); - - if (result == CURLcode::CURLE_OK) - { - spdlog::info("Mod archive successfully fetched."); - goto REQUEST_END_CLEANUP; - } - else - { - spdlog::error("Fetching mod archive failed: {}", curl_easy_strerror(result)); - failed = true; - goto REQUEST_END_CLEANUP; - } - -REQUEST_END_CLEANUP: - curl_easy_cleanup(easyhandle); - fclose(fp); - return failed ? std::optional<fs::path>() : std::optional<fs::path>(downloadPath); -} - -bool ModDownloader::IsModLegit(fs::path modPath, std::string_view expectedChecksum) -{ - if (strstr(GetCommandLineA(), VERIFICATION_FLAG)) - { - spdlog::info("Bypassing mod verification due to flag set up."); - return true; - } - - // Update state - modState.state = CHECKSUMING; - - NTSTATUS status; - BCRYPT_ALG_HANDLE algorithmHandle = NULL; - BCRYPT_HASH_HANDLE hashHandle = NULL; - std::vector<uint8_t> hash; - DWORD hashLength = 0; - DWORD resultLength = 0; - std::stringstream ss; - - constexpr size_t bufferSize {1 << 12}; - std::vector<char> buffer(bufferSize, '\0'); - std::ifstream fp(modPath.generic_string(), std::ios::binary); - - // Open an algorithm handle - // This sample passes BCRYPT_HASH_REUSABLE_FLAG with BCryptAlgorithmProvider(...) to load a provider which supports reusable hash - status = BCryptOpenAlgorithmProvider( - &algorithmHandle, // Alg Handle pointer - BCRYPT_SHA256_ALGORITHM, // Cryptographic Algorithm name (null terminated unicode string) - NULL, // Provider name; if null, the default provider is loaded - BCRYPT_HASH_REUSABLE_FLAG); // Flags; Loads a provider which supports reusable hash - if (!NT_SUCCESS(status)) - { - modState.state = MOD_CORRUPTED; - goto cleanup; - } - - // Obtain the length of the hash - status = BCryptGetProperty( - algorithmHandle, // Handle to a CNG object - BCRYPT_HASH_LENGTH, // Property name (null terminated unicode string) - (PBYTE)&hashLength, // Address of the output buffer which recieves the property value - sizeof(hashLength), // Size of the buffer in bytes - &resultLength, // Number of bytes that were copied into the buffer - 0); // Flags - if (!NT_SUCCESS(status)) - { - // goto cleanup; - modState.state = MOD_CORRUPTED; - return false; - } - - // Create a hash handle - status = BCryptCreateHash( - algorithmHandle, // Handle to an algorithm provider - &hashHandle, // A pointer to a hash handle - can be a hash or hmac object - NULL, // Pointer to the buffer that recieves the hash/hmac object - 0, // Size of the buffer in bytes - NULL, // A pointer to a key to use for the hash or MAC - 0, // Size of the key in bytes - 0); // Flags - if (!NT_SUCCESS(status)) - { - modState.state = MOD_CORRUPTED; - goto cleanup; - } - - // Hash archive content - if (!fp.is_open()) - { - spdlog::error("Unable to open archive."); - modState.state = FAILED_READING_ARCHIVE; - return false; - } - fp.seekg(0, fp.beg); - while (fp.good()) - { - fp.read(buffer.data(), bufferSize); - std::streamsize bytesRead = fp.gcount(); - if (bytesRead > 0) - { - status = BCryptHashData(hashHandle, (PBYTE)buffer.data(), bytesRead, 0); - if (!NT_SUCCESS(status)) - { - modState.state = MOD_CORRUPTED; - goto cleanup; - } - } - } - - hash = std::vector<uint8_t>(hashLength); - - // Obtain the hash of the message(s) into the hash buffer - status = BCryptFinishHash( - hashHandle, // Handle to the hash or MAC object - hash.data(), // A pointer to a buffer that receives the hash or MAC value - hashLength, // Size of the buffer in bytes - 0); // Flags - if (!NT_SUCCESS(status)) - { - modState.state = MOD_CORRUPTED; - goto cleanup; - } - - // Convert hash to string using bytes raw values - ss << std::hex << std::setfill('0'); - for (int i = 0; i < hashLength; i++) - { - ss << std::hex << std::setw(2) << static_cast<int>(hash.data()[i]); - } - - spdlog::info("Expected checksum: {}", expectedChecksum.data()); - spdlog::info("Computed checksum: {}", ss.str()); - return expectedChecksum.compare(ss.str()) == 0; - -cleanup: - if (NULL != hashHandle) - { - BCryptDestroyHash(hashHandle); // Handle to hash/MAC object which needs to be destroyed - } - - if (NULL != algorithmHandle) - { - BCryptCloseAlgorithmProvider( - algorithmHandle, // Handle to the algorithm provider which needs to be closed - 0); // Flags - } - - return false; -} - -bool ModDownloader::IsModAuthorized(std::string_view modName, std::string_view modVersion) -{ - if (strstr(GetCommandLineA(), VERIFICATION_FLAG)) - { - spdlog::info("Bypassing mod verification due to flag set up."); - return true; - } - - if (!verifiedMods.contains(modName.data())) - { - return false; - } - - std::unordered_map<std::string, VerifiedModVersion> versions = verifiedMods[modName.data()].versions; - return versions.count(modVersion.data()) != 0; -} - -int GetModArchiveSize(unzFile file, unz_global_info64 info) -{ - int totalSize = 0; - - for (int i = 0; i < info.number_entry; i++) - { - char zipFilename[256]; - unz_file_info64 fileInfo; - unzGetCurrentFileInfo64(file, &fileInfo, zipFilename, sizeof(zipFilename), NULL, 0, NULL, 0); - - totalSize += fileInfo.uncompressed_size; - - if ((i + 1) < info.number_entry) - { - unzGoToNextFile(file); - } - } - - // Reset file pointer for archive extraction - unzGoToFirstFile(file); - - return totalSize; -} - -void ModDownloader::ExtractMod(fs::path modPath) -{ - unzFile file; - std::string name; - fs::path modDirectory; - - file = unzOpen(modPath.generic_string().c_str()); - if (file == NULL) - { - spdlog::error("Cannot open archive located at {}.", modPath.generic_string()); - modState.state = FAILED_READING_ARCHIVE; - goto EXTRACTION_CLEANUP; - } - - unz_global_info64 gi; - int status; - status = unzGetGlobalInfo64(file, &gi); - if (status != UNZ_OK) - { - spdlog::error("Failed getting information from archive (error code: {})", status); - modState.state = FAILED_READING_ARCHIVE; - goto EXTRACTION_CLEANUP; - } - - // Update state - modState.state = EXTRACTING; - modState.total = GetModArchiveSize(file, gi); - modState.progress = 0; - - // Mod directory name (removing the ".zip" fom the archive name) - name = modPath.filename().string(); - name = name.substr(0, name.length() - 4); - modDirectory = GetRemoteModFolderPath() / name; - - for (int i = 0; i < gi.number_entry; i++) - { - char zipFilename[256]; - unz_file_info64 fileInfo; - status = unzGetCurrentFileInfo64(file, &fileInfo, zipFilename, sizeof(zipFilename), NULL, 0, NULL, 0); - - // Extract file - { - std::error_code ec; - fs::path fileDestination = modDirectory / zipFilename; - spdlog::info("=> {}", fileDestination.generic_string()); - - // Create parent directory if needed - if (!std::filesystem::exists(fileDestination.parent_path())) - { - spdlog::info("Parent directory does not exist, creating it.", fileDestination.generic_string()); - if (!std::filesystem::create_directories(fileDestination.parent_path(), ec) && ec.value() != 0) - { - spdlog::error("Parent directory ({}) creation failed.", fileDestination.parent_path().generic_string()); - modState.state = FAILED_WRITING_TO_DISK; - goto EXTRACTION_CLEANUP; - } - } - - // If current file is a directory, create directory... - if (fileDestination.generic_string().back() == '/') - { - // Create directory - if (!std::filesystem::create_directory(fileDestination, ec) && ec.value() != 0) - { - spdlog::error("Directory creation failed: {}", ec.message()); - modState.state = FAILED_WRITING_TO_DISK; - goto EXTRACTION_CLEANUP; - } - } - // ...else create file - else - { - // Ensure file is in zip archive - if (unzLocateFile(file, zipFilename, 0) != UNZ_OK) - { - spdlog::error("File \"{}\" was not found in archive.", zipFilename); - modState.state = FAILED_READING_ARCHIVE; - goto EXTRACTION_CLEANUP; - } - - // Create file - const int bufferSize = 8192; - void* buffer; - int err = UNZ_OK; - FILE* fout = NULL; - - // Open zip file to prepare its extraction - status = unzOpenCurrentFile(file); - if (status != UNZ_OK) - { - spdlog::error("Could not open file {} from archive.", zipFilename); - modState.state = FAILED_READING_ARCHIVE; - goto EXTRACTION_CLEANUP; - } - - // Create destination file - fout = fopen(fileDestination.generic_string().c_str(), "wb"); - if (fout == NULL) - { - spdlog::error("Failed creating destination file."); - modState.state = FAILED_WRITING_TO_DISK; - goto EXTRACTION_CLEANUP; - } - - // Allocate memory for buffer - buffer = (void*)malloc(bufferSize); - if (buffer == NULL) - { - spdlog::error("Error while allocating memory."); - modState.state = FAILED_WRITING_TO_DISK; - goto EXTRACTION_CLEANUP; - } - - // Extract file to destination - do - { - err = unzReadCurrentFile(file, buffer, bufferSize); - if (err < 0) - { - spdlog::error("error {} with zipfile in unzReadCurrentFile", err); - break; - } - if (err > 0) - { - if (fwrite(buffer, (unsigned)err, 1, fout) != 1) - { - spdlog::error("error in writing extracted file\n"); - err = UNZ_ERRNO; - break; - } - } - - // Update extraction stats - modState.progress += bufferSize; - modState.ratio = roundf(static_cast<float>(modState.progress) / modState.total * 100); - } while (err > 0); - - if (err != UNZ_OK) - { - spdlog::error("An error occurred during file extraction (code: {})", err); - modState.state = FAILED_WRITING_TO_DISK; - goto EXTRACTION_CLEANUP; - } - err = unzCloseCurrentFile(file); - if (err != UNZ_OK) - { - spdlog::error("error {} with zipfile in unzCloseCurrentFile", err); - } - - // Cleanup - if (fout) - fclose(fout); - } - } - - // Go to next file - if ((i + 1) < gi.number_entry) - { - status = unzGoToNextFile(file); - if (status != UNZ_OK) - { - spdlog::error("Error while browsing archive files (error code: {}).", status); - goto EXTRACTION_CLEANUP; - } - } - } - -EXTRACTION_CLEANUP: - if (unzClose(file) != MZ_OK) - { - spdlog::error("Failed closing mod archive after extraction."); - } -} - -void ModDownloader::DownloadMod(std::string modName, std::string modVersion) -{ - // Check if mod can be auto-downloaded - if (!IsModAuthorized(std::string_view(modName), std::string_view(modVersion))) - { - spdlog::warn("Tried to download a mod that is not verified, aborting."); - return; - } - - std::thread requestThread( - [this, modName, modVersion]() - { - fs::path archiveLocation; - - // Download mod archive - std::string expectedHash = verifiedMods[modName].versions[modVersion].checksum; - std::optional<fs::path> fetchingResult = FetchModFromDistantStore(std::string_view(modName), std::string_view(modVersion)); - if (!fetchingResult.has_value()) - { - spdlog::error("Something went wrong while fetching archive, aborting."); - modState.state = MOD_FETCHING_FAILED; - goto REQUEST_END_CLEANUP; - } - archiveLocation = fetchingResult.value(); - if (!IsModLegit(archiveLocation, std::string_view(expectedHash))) - { - spdlog::warn("Archive hash does not match expected checksum, aborting."); - modState.state = MOD_CORRUPTED; - goto REQUEST_END_CLEANUP; - } - - // Extract downloaded mod archive - ExtractMod(archiveLocation); - - REQUEST_END_CLEANUP: - try - { - remove(archiveLocation); - } - catch (const std::exception& a) - { - spdlog::error("Error while removing downloaded archive: {}", a.what()); - } - - modState.state = DONE; - spdlog::info("Done downloading {}.", modName); - }); - - requestThread.detach(); -} - -ON_DLL_LOAD_RELIESON("engine.dll", ModDownloader, (ConCommand), (CModule module)) -{ - g_pModDownloader = new ModDownloader(); - g_pModDownloader->FetchModsListFromAPI(); -} - -ADD_SQFUNC( - "bool", NSIsModDownloadable, "string name, string version", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - g_pSquirrel<context>->newarray(sqvm, 0); - - const SQChar* modName = g_pSquirrel<context>->getstring(sqvm, 1); - const SQChar* modVersion = g_pSquirrel<context>->getstring(sqvm, 2); - - bool result = g_pModDownloader->IsModAuthorized(modName, modVersion); - g_pSquirrel<context>->pushbool(sqvm, result); - - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("void", NSDownloadMod, "string name, string version", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - const SQChar* modName = g_pSquirrel<context>->getstring(sqvm, 1); - const SQChar* modVersion = g_pSquirrel<context>->getstring(sqvm, 2); - g_pModDownloader->DownloadMod(modName, modVersion); - - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("ModInstallState", NSGetModInstallState, "", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - g_pSquirrel<context>->pushnewstructinstance(sqvm, 4); - - // state - g_pSquirrel<context>->pushinteger(sqvm, g_pModDownloader->modState.state); - g_pSquirrel<context>->sealstructslot(sqvm, 0); - - // progress - g_pSquirrel<context>->pushinteger(sqvm, g_pModDownloader->modState.progress); - g_pSquirrel<context>->sealstructslot(sqvm, 1); - - // total - g_pSquirrel<context>->pushinteger(sqvm, g_pModDownloader->modState.total); - g_pSquirrel<context>->sealstructslot(sqvm, 2); - - // ratio - g_pSquirrel<context>->pushfloat(sqvm, g_pModDownloader->modState.ratio); - g_pSquirrel<context>->sealstructslot(sqvm, 3); - - return SQRESULT_NOTNULL; -} diff --git a/NorthstarDLL/mods/autodownload/moddownloader.h b/NorthstarDLL/mods/autodownload/moddownloader.h deleted file mode 100644 index 5302c21e..00000000 --- a/NorthstarDLL/mods/autodownload/moddownloader.h +++ /dev/null @@ -1,151 +0,0 @@ -class ModDownloader -{ -private: - const char* VERIFICATION_FLAG = "-disablemodverification"; - const char* CUSTOM_MODS_URL_FLAG = "-customverifiedurl="; - const char* STORE_URL = "https://gcdn.thunderstore.io/live/repository/packages/"; - const char* DEFAULT_MODS_LIST_URL = "https://raw.githubusercontent.com/R2Northstar/VerifiedMods/master/verified-mods.json"; - char* modsListUrl; - - struct VerifiedModVersion - { - std::string checksum; - }; - struct VerifiedModDetails - { - std::string dependencyPrefix; - std::unordered_map<std::string, VerifiedModVersion> versions = {}; - }; - std::unordered_map<std::string, VerifiedModDetails> verifiedMods = {}; - - /** - * Mod archive download callback. - * - * This function is called by curl as it's downloading the mod archive; this - * will retrieve the current `ModDownloader` instance and update its `modState` - * member accordingly. - */ - static int ModFetchingProgressCallback( - void* ptr, curl_off_t totalDownloadSize, curl_off_t finishedDownloadSize, curl_off_t totalToUpload, curl_off_t nowUploaded); - - /** - * Downloads a mod archive from distant store. - * - * This rebuilds the URI of the mod archive using both a predefined store URI - * and the mod dependency string from the `verifiedMods` variable, or using - * input mod name as mod dependency string if bypass flag is set up; fetched - * archive is then stored in a temporary location. - * - * If something went wrong during archive download, this will return an empty - * optional object. - * - * @param modName name of the mod to be downloaded - * @param modVersion version of the mod to be downloaded - * @returns location of the downloaded archive - */ - std::optional<fs::path> FetchModFromDistantStore(std::string_view modName, std::string_view modVersion); - - /** - * Tells if a mod archive has not been corrupted. - * - * The mod validation procedure includes computing the SHA256 hash of the final - * archive, which is stored in the verified mods list. This hash is used by this - * very method to ensure the archive downloaded from the Internet is the exact - * same that has been manually verified. - * - * @param modPath path of the archive to check - * @param expectedChecksum checksum the archive should have - * @returns whether archive is legit - */ - bool IsModLegit(fs::path modPath, std::string_view expectedChecksum); - - /** - * Extracts a mod archive to the game folder. - * - * This extracts a downloaded mod archive from its original location to the - * current game profile, in the remote mods folder. - * - * @param modPath location of the downloaded archive - * @returns nothing - */ - void ExtractMod(fs::path modPath); - -public: - ModDownloader(); - - /** - * Retrieves the verified mods list from the central authority. - * - * The Northstar auto-downloading feature does NOT allow automatically installing - * all mods for various (notably security) reasons; mods that are candidate to - * auto-downloading are rather listed on a GitHub repository - * (https://raw.githubusercontent.com/R2Northstar/VerifiedMods/master/verified-mods.json), - * which this method gets via a HTTP call to load into local state. - * - * If list fetching fails, local mods list will be initialized as empty, thus - * preventing any mod from being auto-downloaded. - * - * @returns nothing - */ - void FetchModsListFromAPI(); - - /** - * Checks whether a mod is verified. - * - * A mod is deemed verified/authorized through a manual validation process that is - * described here: https://github.com/R2Northstar/VerifiedMods; in practice, a mod - * is considered authorized if their name AND exact version appear in the - * `verifiedMods` variable. - * - * @param modName name of the mod to be checked - * @param modVersion version of the mod to be checked, must follow semantic versioning - * @returns whether the mod is authorized and can be auto-downloaded - */ - bool IsModAuthorized(std::string_view modName, std::string_view modVersion); - - /** - * Downloads a given mod from Thunderstore API to local game profile. - * - * @param modName name of the mod to be downloaded - * @param modVersion version of the mod to be downloaded - * @returns nothing - **/ - void DownloadMod(std::string modName, std::string modVersion); - - enum ModInstallState - { - // Normal installation process - DOWNLOADING, - CHECKSUMING, - EXTRACTING, - DONE, // Everything went great, mod can be used in-game - - // Errors - FAILED, // Generic error message, should be avoided as much as possible - FAILED_READING_ARCHIVE, - FAILED_WRITING_TO_DISK, - MOD_FETCHING_FAILED, - MOD_CORRUPTED, // Downloaded archive checksum does not match verified hash - NO_DISK_SPACE_AVAILABLE, - NOT_FOUND // Mod is not currently being auto-downloaded - }; - - struct MOD_STATE - { - ModInstallState state; - int progress; - int total; - float ratio; - } modState = {}; - - /** - * Cancels installation of the mod. - * - * Prevents installation of the mod currently being installed, no matter the install - * progress (downloading, checksuming, extracting), and frees all resources currently - * being used in this purpose. - * - * @returns nothing - */ - void CancelDownload(); -}; diff --git a/NorthstarDLL/mods/compiled/kb_act.cpp b/NorthstarDLL/mods/compiled/kb_act.cpp deleted file mode 100644 index 6117fd28..00000000 --- a/NorthstarDLL/mods/compiled/kb_act.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "mods/modmanager.h" -#include "core/filesystem/filesystem.h" - -#include <fstream> - -const char* KB_ACT_PATH = "scripts\\kb_act.lst"; - -// compiles the file kb_act.lst, that defines entries for keybindings in the options menu -void ModManager::BuildKBActionsList() -{ - spdlog::info("Building kb_act.lst"); - - fs::create_directories(GetCompiledAssetsPath() / "scripts"); - std::ofstream soCompiledKeys(GetCompiledAssetsPath() / KB_ACT_PATH, std::ios::binary); - - // write vanilla file's content to compiled file - soCompiledKeys << ReadVPKOriginalFile(KB_ACT_PATH); - - for (Mod& mod : m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - // write content of each modded file to compiled file - std::ifstream siModKeys(mod.m_ModDirectory / "kb_act.lst"); - - if (siModKeys.good()) - soCompiledKeys << siModKeys.rdbuf() << std::endl; - - siModKeys.close(); - } - - soCompiledKeys.close(); - - // push to overrides - ModOverrideFile overrideFile; - overrideFile.m_pOwningMod = nullptr; - overrideFile.m_Path = KB_ACT_PATH; - - if (m_ModFiles.find(KB_ACT_PATH) == m_ModFiles.end()) - m_ModFiles.insert(std::make_pair(KB_ACT_PATH, overrideFile)); - else - m_ModFiles[KB_ACT_PATH] = overrideFile; -} diff --git a/NorthstarDLL/mods/compiled/modkeyvalues.cpp b/NorthstarDLL/mods/compiled/modkeyvalues.cpp deleted file mode 100644 index e44a81d3..00000000 --- a/NorthstarDLL/mods/compiled/modkeyvalues.cpp +++ /dev/null @@ -1,106 +0,0 @@ -#include "mods/modmanager.h" -#include "core/filesystem/filesystem.h" - -#include <fstream> - -AUTOHOOK_INIT() - -void ModManager::TryBuildKeyValues(const char* filename) -{ - spdlog::info("Building KeyValues for file {}", filename); - - std::string normalisedPath = g_pModManager->NormaliseModFilePath(fs::path(filename)); - fs::path compiledPath = GetCompiledAssetsPath() / filename; - fs::path compiledDir = compiledPath.parent_path(); - fs::create_directories(compiledDir); - - fs::path kvPath(filename); - std::string ogFilePath = "mod_original_"; - ogFilePath += kvPath.filename().string(); - - std::string newKvs = "// AUTOGENERATED: MOD PATCH KV\n"; - - int patchNum = 0; - - // copy over patch kv files, and add #bases to new file, last mods' patches should be applied first - // note: #include should be identical but it's actually just broken, thanks respawn - for (int64_t i = m_LoadedMods.size() - 1; i > -1; i--) - { - if (!m_LoadedMods[i].m_bEnabled) - continue; - - size_t fileHash = STR_HASH(normalisedPath); - auto modKv = m_LoadedMods[i].KeyValues.find(fileHash); - if (modKv != m_LoadedMods[i].KeyValues.end()) - { - // should result in smth along the lines of #include "mod_patch_5_mp_weapon_car.txt" - - std::string patchFilePath = "mod_patch_"; - patchFilePath += std::to_string(patchNum++); - patchFilePath += "_"; - patchFilePath += kvPath.filename().string(); - - newKvs += "#base \""; - newKvs += patchFilePath; - newKvs += "\"\n"; - - fs::remove(compiledDir / patchFilePath); - - fs::copy_file(m_LoadedMods[i].m_ModDirectory / "keyvalues" / filename, compiledDir / patchFilePath); - } - } - - // add original #base last, #bases don't override preexisting keys, including the ones we've just done - newKvs += "#base \""; - newKvs += ogFilePath; - newKvs += "\"\n"; - - // load original file, so we can parse out the name of the root obj (e.g. WeaponData for weapons) - std::string originalFile = ReadVPKOriginalFile(filename); - - if (!originalFile.length()) - { - spdlog::warn("Tried to patch kv {} but no base kv was found!", ogFilePath); - return; - } - - char rootName[64]; - memset(rootName, 0, sizeof(rootName)); - - // iterate until we hit an ascii char that isn't in a # command or comment to get root obj name - int i = 0; - while (!(originalFile[i] >= 65 && originalFile[i] <= 122)) - { - // if we hit a comment or # thing, iterate until end of line - if (originalFile[i] == '/' || originalFile[i] == '#') - while (originalFile[i] != '\n') - i++; - - i++; - } - - int j = 0; - for (int j = 0; originalFile[i] >= 65 && originalFile[i] <= 122; j++) - rootName[j] = originalFile[i++]; - - // empty kv, all the other stuff gets #base'd - newKvs += rootName; - newKvs += "\n{\n}\n"; - - std::ofstream originalFileWriteStream(compiledDir / ogFilePath, std::ios::binary); - originalFileWriteStream << originalFile; - originalFileWriteStream.close(); - - std::ofstream writeStream(compiledPath, std::ios::binary); - writeStream << newKvs; - writeStream.close(); - - ModOverrideFile overrideFile; - overrideFile.m_pOwningMod = nullptr; - overrideFile.m_Path = normalisedPath; - - if (m_ModFiles.find(normalisedPath) == m_ModFiles.end()) - m_ModFiles.insert(std::make_pair(normalisedPath, overrideFile)); - else - m_ModFiles[normalisedPath] = overrideFile; -} diff --git a/NorthstarDLL/mods/compiled/modpdef.cpp b/NorthstarDLL/mods/compiled/modpdef.cpp deleted file mode 100644 index d268a063..00000000 --- a/NorthstarDLL/mods/compiled/modpdef.cpp +++ /dev/null @@ -1,118 +0,0 @@ -#include "mods/modmanager.h" -#include "core/filesystem/filesystem.h" - -#include <map> -#include <sstream> -#include <fstream> - -const fs::path MOD_PDEF_SUFFIX = "cfg/server/persistent_player_data_version_231.pdef"; -const char* VPK_PDEF_PATH = "cfg/server/persistent_player_data_version_231.pdef"; - -void ModManager::BuildPdef() -{ - spdlog::info("Building persistent_player_data_version_231.pdef..."); - - fs::path MOD_PDEF_PATH = fs::path(GetCompiledAssetsPath() / MOD_PDEF_SUFFIX); - - fs::remove(MOD_PDEF_PATH); - std::string pdef = ReadVPKOriginalFile(VPK_PDEF_PATH); - - for (Mod& mod : m_LoadedMods) - { - if (!mod.m_bEnabled || !mod.Pdiff.size()) - continue; - - // this code probably isn't going to be pretty lol - // refer to shared/pjson.js for an actual okish parser of the pdiff format - // but pretty much, $ENUM_ADD blocks define members added to preexisting enums - // $PROP_START ends the custom stuff, and from there it's just normal props we append to the pdef - - std::map<std::string, std::vector<std::string>> enumAdds; - - // read pdiff - bool inEnum = false; - bool inProp = false; - std::string currentEnum; - std::string currentLine; - std::istringstream pdiffStream(mod.Pdiff); - - while (std::getline(pdiffStream, currentLine)) - { - if (inProp) - { - // just append to pdef here - pdef += currentLine; - pdef += '\n'; - continue; - } - - // trim leading whitespace - size_t start = currentLine.find_first_not_of(" \n\r\t\f\v"); - size_t end = currentLine.find("//"); - if (end == std::string::npos) - end = currentLine.size() - 1; // last char - - if (!currentLine.size() || !currentLine.compare(start, 2, "//")) - continue; - - if (inEnum) - { - if (!currentLine.compare(start, 9, "$ENUM_END")) - inEnum = false; - else - enumAdds[currentEnum].push_back(currentLine); // only need to push_back current line, if there's syntax errors then game - // pdef parser will handle them - } - else if (!currentLine.compare(start, 9, "$ENUM_ADD")) - { - inEnum = true; - currentEnum = currentLine.substr(start + 10 /*$ENUM_ADD + 1*/, currentLine.size() - end - (start + 10)); - enumAdds.insert(std::make_pair(currentEnum, std::vector<std::string>())); - } - else if (!currentLine.compare(start, 11, "$PROP_START")) - { - inProp = true; - pdef += "\n// $PROP_START "; - pdef += mod.Name; - pdef += "\n"; - } - } - - // add new members to preexisting enums - // note: this code could 100% be messed up if people put //$ENUM_START comments and the like - // could make it protect against this, but honestly not worth atm - for (auto enumAdd : enumAdds) - { - std::string addStr; - for (std::string enumMember : enumAdd.second) - { - addStr += enumMember; - addStr += '\n'; - } - - // start of enum we're adding to - std::string startStr = "$ENUM_START "; - startStr += enumAdd.first; - - // insert enum values into enum - size_t insertIdx = pdef.find("$ENUM_END", pdef.find(startStr)); - pdef.reserve(addStr.size()); - pdef.insert(insertIdx, addStr); - } - } - - fs::create_directories(MOD_PDEF_PATH.parent_path()); - - std::ofstream writeStream(MOD_PDEF_PATH, std::ios::binary); - writeStream << pdef; - writeStream.close(); - - ModOverrideFile overrideFile; - overrideFile.m_pOwningMod = nullptr; - overrideFile.m_Path = VPK_PDEF_PATH; - - if (m_ModFiles.find(VPK_PDEF_PATH) == m_ModFiles.end()) - m_ModFiles.insert(std::make_pair(VPK_PDEF_PATH, overrideFile)); - else - m_ModFiles[VPK_PDEF_PATH] = overrideFile; -} diff --git a/NorthstarDLL/mods/compiled/modscriptsrson.cpp b/NorthstarDLL/mods/compiled/modscriptsrson.cpp deleted file mode 100644 index d130745f..00000000 --- a/NorthstarDLL/mods/compiled/modscriptsrson.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "mods/modmanager.h" -#include "core/filesystem/filesystem.h" -#include "squirrel/squirrel.h" - -#include <fstream> - -const std::string MOD_SCRIPTS_RSON_SUFFIX = "scripts/vscripts/scripts.rson"; -const char* VPK_SCRIPTS_RSON_PATH = "scripts\\vscripts\\scripts.rson"; - -void ModManager::BuildScriptsRson() -{ - spdlog::info("Building custom scripts.rson"); - fs::path MOD_SCRIPTS_RSON_PATH = fs::path(GetCompiledAssetsPath() / MOD_SCRIPTS_RSON_SUFFIX); - fs::remove(MOD_SCRIPTS_RSON_PATH); - - std::string scriptsRson = ReadVPKOriginalFile(VPK_SCRIPTS_RSON_PATH); - scriptsRson += "\n\n// START MODDED SCRIPT CONTENT\n\n"; // newline before we start custom stuff - - for (Mod& mod : m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - // this isn't needed at all, just nice to have imo - scriptsRson += "// MOD: "; - scriptsRson += mod.Name; - scriptsRson += ":\n\n"; - - for (ModScript& script : mod.Scripts) - { - /* should create something with this format for each script - When: "CONTEXT" - Scripts: - [ - _coolscript.gnut - ]*/ - - scriptsRson += "When: \""; - scriptsRson += script.RunOn; - scriptsRson += "\"\n"; - - scriptsRson += "Scripts:\n[\n\t"; - scriptsRson += script.Path; - scriptsRson += "\n]\n\n"; - } - } - - fs::create_directories(MOD_SCRIPTS_RSON_PATH.parent_path()); - - std::ofstream writeStream(MOD_SCRIPTS_RSON_PATH, std::ios::binary); - writeStream << scriptsRson; - writeStream.close(); - - ModOverrideFile overrideFile; - overrideFile.m_pOwningMod = nullptr; - overrideFile.m_Path = VPK_SCRIPTS_RSON_PATH; - - if (m_ModFiles.find(VPK_SCRIPTS_RSON_PATH) == m_ModFiles.end()) - m_ModFiles.insert(std::make_pair(VPK_SCRIPTS_RSON_PATH, overrideFile)); - else - m_ModFiles[VPK_SCRIPTS_RSON_PATH] = overrideFile; - - // todo: for preventing dupe scripts in scripts.rson, we could actually parse when conditions with the squirrel vm, just need a way to - // get a result out of squirrelmanager.ExecuteCode this would probably be the best way to do this, imo -} diff --git a/NorthstarDLL/mods/modmanager.cpp b/NorthstarDLL/mods/modmanager.cpp deleted file mode 100644 index 8a0eb71d..00000000 --- a/NorthstarDLL/mods/modmanager.cpp +++ /dev/null @@ -1,1150 +0,0 @@ -#include "modmanager.h" -#include "core/convar/convar.h" -#include "core/convar/concommand.h" -#include "client/audio.h" -#include "masterserver/masterserver.h" -#include "core/filesystem/filesystem.h" -#include "core/filesystem/rpakfilesystem.h" -#include "config/profile.h" - -#include "rapidjson/error/en.h" -#include "rapidjson/document.h" -#include "rapidjson/ostreamwrapper.h" -#include "rapidjson/prettywriter.h" -#include <filesystem> -#include <fstream> -#include <string> -#include <sstream> -#include <vector> -#include <regex> - -ModManager* g_pModManager; - -Mod::Mod(fs::path modDir, char* jsonBuf) -{ - m_bWasReadSuccessfully = false; - - m_ModDirectory = modDir; - - rapidjson_document modJson; - modJson.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(jsonBuf); - - spdlog::info("Loading mod file at path '{}'", modDir.string()); - - // fail if parse error - if (modJson.HasParseError()) - { - spdlog::error( - "Failed reading mod file {}: encountered parse error \"{}\" at offset {}", - (modDir / "mod.json").string(), - GetParseError_En(modJson.GetParseError()), - modJson.GetErrorOffset()); - return; - } - - // fail if it's not a json obj (could be an array, string, etc) - if (!modJson.IsObject()) - { - spdlog::error("Failed reading mod file {}: file is not a JSON object", (modDir / "mod.json").string()); - return; - } - - // basic mod info - // name is required - if (!modJson.HasMember("Name")) - { - spdlog::error("Failed reading mod file {}: missing required member \"Name\"", (modDir / "mod.json").string()); - return; - } - - Name = modJson["Name"].GetString(); - spdlog::info("Loading mod '{}'", Name); - - // Don't load blacklisted mods - if (!strstr(GetCommandLineA(), "-nomodblacklist") && MODS_BLACKLIST.find(Name) != std::end(MODS_BLACKLIST)) - { - spdlog::warn("Skipping blacklisted mod \"{}\"!", Name); - return; - } - - if (modJson.HasMember("Description")) - Description = modJson["Description"].GetString(); - else - Description = ""; - - if (modJson.HasMember("Version")) - Version = modJson["Version"].GetString(); - else - { - Version = "0.0.0"; - spdlog::warn("Mod file {} is missing a version, consider adding a version", (modDir / "mod.json").string()); - } - - if (modJson.HasMember("DownloadLink")) - DownloadLink = modJson["DownloadLink"].GetString(); - else - DownloadLink = ""; - - if (modJson.HasMember("RequiredOnClient")) - RequiredOnClient = modJson["RequiredOnClient"].GetBool(); - else - RequiredOnClient = false; - - if (modJson.HasMember("LoadPriority")) - LoadPriority = modJson["LoadPriority"].GetInt(); - else - { - spdlog::info("Mod file {} is missing a LoadPriority, consider adding one", (modDir / "mod.json").string()); - LoadPriority = 0; - } - - // Parse all array fields - ParseConVars(modJson); - ParseConCommands(modJson); - ParseScripts(modJson); - ParseLocalization(modJson); - ParseDependencies(modJson); - ParsePluginDependencies(modJson); - ParseInitScript(modJson); - - // A mod is remote if it's located in the remote mods folder - m_bIsRemote = m_ModDirectory.generic_string().find(GetRemoteModFolderPath().generic_string()) != std::string::npos; - - m_bWasReadSuccessfully = true; -} - -void Mod::ParseConVars(rapidjson_document& json) -{ - if (!json.HasMember("ConVars")) - return; - - if (!json["ConVars"].IsArray()) - { - spdlog::warn("'ConVars' field is not an array, skipping..."); - return; - } - - for (auto& convarObj : json["ConVars"].GetArray()) - { - if (!convarObj.IsObject()) - { - spdlog::warn("ConVar is not an object, skipping..."); - continue; - } - if (!convarObj.HasMember("Name")) - { - spdlog::warn("ConVar does not have a Name, skipping..."); - continue; - } - // from here on, the ConVar can be referenced by name in logs - if (!convarObj.HasMember("DefaultValue")) - { - spdlog::warn("ConVar '{}' does not have a DefaultValue, skipping...", convarObj["Name"].GetString()); - continue; - } - - // have to allocate this manually, otherwise convar registration will break - // unfortunately this causes us to leak memory on reload, unsure of a way around this rn - ModConVar* convar = new ModConVar; - convar->Name = convarObj["Name"].GetString(); - convar->DefaultValue = convarObj["DefaultValue"].GetString(); - - if (convarObj.HasMember("HelpString")) - convar->HelpString = convarObj["HelpString"].GetString(); - else - convar->HelpString = ""; - - convar->Flags = FCVAR_NONE; - - if (convarObj.HasMember("Flags")) - { - // read raw integer flags - if (convarObj["Flags"].IsInt()) - convar->Flags = convarObj["Flags"].GetInt(); - else if (convarObj["Flags"].IsString()) - { - // parse cvar flags from string - // example string: ARCHIVE_PLAYERPROFILE | GAMEDLL - convar->Flags |= ParseConVarFlagsString(convar->Name, convarObj["Flags"].GetString()); - } - } - - ConVars.push_back(convar); - - spdlog::info("'{}' contains ConVar '{}'", Name, convar->Name); - } -} - -void Mod::ParseConCommands(rapidjson_document& json) -{ - if (!json.HasMember("ConCommands")) - return; - - if (!json["ConCommands"].IsArray()) - { - spdlog::warn("'ConCommands' field is not an array, skipping..."); - return; - } - - for (auto& concommandObj : json["ConCommands"].GetArray()) - { - if (!concommandObj.IsObject()) - { - spdlog::warn("ConCommand is not an object, skipping..."); - continue; - } - if (!concommandObj.HasMember("Name")) - { - spdlog::warn("ConCommand does not have a Name, skipping..."); - continue; - } - // from here on, the ConCommand can be referenced by name in logs - if (!concommandObj.HasMember("Function")) - { - spdlog::warn("ConCommand '{}' does not have a Function, skipping...", concommandObj["Name"].GetString()); - continue; - } - if (!concommandObj.HasMember("Context")) - { - spdlog::warn("ConCommand '{}' does not have a Context, skipping...", concommandObj["Name"].GetString()); - continue; - } - - // have to allocate this manually, otherwise concommand registration will break - // unfortunately this causes us to leak memory on reload, unsure of a way around this rn - ModConCommand* concommand = new ModConCommand; - concommand->Name = concommandObj["Name"].GetString(); - concommand->Function = concommandObj["Function"].GetString(); - concommand->Context = ScriptContextFromString(concommandObj["Context"].GetString()); - if (concommand->Context == ScriptContext::INVALID) - { - spdlog::warn("ConCommand '{}' has invalid context '{}', skipping...", concommand->Name, concommandObj["Context"].GetString()); - continue; - } - - if (concommandObj.HasMember("HelpString")) - concommand->HelpString = concommandObj["HelpString"].GetString(); - else - concommand->HelpString = ""; - - concommand->Flags = FCVAR_NONE; - - if (concommandObj.HasMember("Flags")) - { - // read raw integer flags - if (concommandObj["Flags"].IsInt()) - { - concommand->Flags = concommandObj["Flags"].GetInt(); - } - else if (concommandObj["Flags"].IsString()) - { - // parse cvar flags from string - // example string: ARCHIVE_PLAYERPROFILE | GAMEDLL - concommand->Flags |= ParseConVarFlagsString(concommand->Name, concommandObj["Flags"].GetString()); - } - } - - ConCommands.push_back(concommand); - - spdlog::info("'{}' contains ConCommand '{}'", Name, concommand->Name); - } -} - -void Mod::ParseScripts(rapidjson_document& json) -{ - if (!json.HasMember("Scripts")) - return; - - if (!json["Scripts"].IsArray()) - { - spdlog::warn("'Scripts' field is not an array, skipping..."); - return; - } - - for (auto& scriptObj : json["Scripts"].GetArray()) - { - if (!scriptObj.IsObject()) - { - spdlog::warn("Script is not an object, skipping..."); - continue; - } - if (!scriptObj.HasMember("Path")) - { - spdlog::warn("Script does not have a Path, skipping..."); - continue; - } - // from here on, the Path for a script is used as it's name in logs - if (!scriptObj.HasMember("RunOn")) - { - // "a RunOn" sounds dumb but anything else doesn't match the format of the warnings... - // this is the best i could think of within 20 seconds - spdlog::warn("Script '{}' does not have a RunOn field, skipping...", scriptObj["Path"].GetString()); - continue; - } - - ModScript script; - - script.Path = scriptObj["Path"].GetString(); - script.RunOn = scriptObj["RunOn"].GetString(); - - if (scriptObj.HasMember("ServerCallback")) - { - if (scriptObj["ServerCallback"].IsObject()) - { - ModScriptCallback callback; - callback.Context = ScriptContext::SERVER; - - if (scriptObj["ServerCallback"].HasMember("Before")) - { - if (scriptObj["ServerCallback"]["Before"].IsString()) - callback.BeforeCallback = scriptObj["ServerCallback"]["Before"].GetString(); - else - spdlog::warn("'Before' ServerCallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - if (scriptObj["ServerCallback"].HasMember("After")) - { - if (scriptObj["ServerCallback"]["After"].IsString()) - callback.AfterCallback = scriptObj["ServerCallback"]["After"].GetString(); - else - spdlog::warn("'After' ServerCallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - if (scriptObj["ServerCallback"].HasMember("Destroy")) - { - if (scriptObj["ServerCallback"]["Destroy"].IsString()) - callback.DestroyCallback = scriptObj["ServerCallback"]["Destroy"].GetString(); - else - spdlog::warn( - "'Destroy' ServerCallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - script.Callbacks.push_back(callback); - } - else - { - spdlog::warn("ServerCallback for script '{}' is not an object, skipping...", scriptObj["Path"].GetString()); - } - } - - if (scriptObj.HasMember("ClientCallback")) - { - if (scriptObj["ClientCallback"].IsObject()) - { - ModScriptCallback callback; - callback.Context = ScriptContext::CLIENT; - - if (scriptObj["ClientCallback"].HasMember("Before")) - { - if (scriptObj["ClientCallback"]["Before"].IsString()) - callback.BeforeCallback = scriptObj["ClientCallback"]["Before"].GetString(); - else - spdlog::warn("'Before' ClientCallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - if (scriptObj["ClientCallback"].HasMember("After")) - { - if (scriptObj["ClientCallback"]["After"].IsString()) - callback.AfterCallback = scriptObj["ClientCallback"]["After"].GetString(); - else - spdlog::warn("'After' ClientCallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - if (scriptObj["ClientCallback"].HasMember("Destroy")) - { - if (scriptObj["ClientCallback"]["Destroy"].IsString()) - callback.DestroyCallback = scriptObj["ClientCallback"]["Destroy"].GetString(); - else - spdlog::warn( - "'Destroy' ClientCallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - script.Callbacks.push_back(callback); - } - else - { - spdlog::warn("ClientCallback for script '{}' is not an object, skipping...", scriptObj["Path"].GetString()); - } - } - - if (scriptObj.HasMember("UICallback")) - { - if (scriptObj["UICallback"].IsObject()) - { - ModScriptCallback callback; - callback.Context = ScriptContext::UI; - - if (scriptObj["UICallback"].HasMember("Before")) - { - if (scriptObj["UICallback"]["Before"].IsString()) - callback.BeforeCallback = scriptObj["UICallback"]["Before"].GetString(); - else - spdlog::warn("'Before' UICallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - if (scriptObj["UICallback"].HasMember("After")) - { - if (scriptObj["UICallback"]["After"].IsString()) - callback.AfterCallback = scriptObj["UICallback"]["After"].GetString(); - else - spdlog::warn("'After' UICallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - if (scriptObj["UICallback"].HasMember("Destroy")) - { - if (scriptObj["UICallback"]["Destroy"].IsString()) - callback.DestroyCallback = scriptObj["UICallback"]["Destroy"].GetString(); - else - spdlog::warn("'Destroy' UICallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - script.Callbacks.push_back(callback); - } - else - { - spdlog::warn("UICallback for script '{}' is not an object, skipping...", scriptObj["Path"].GetString()); - } - } - - Scripts.push_back(script); - - spdlog::info("'{}' contains Script '{}'", Name, script.Path); - } -} - -void Mod::ParseLocalization(rapidjson_document& json) -{ - if (!json.HasMember("Localisation")) - return; - - if (!json["Localisation"].IsArray()) - { - spdlog::warn("'Localisation' field is not an array, skipping..."); - return; - } - - for (auto& localisationStr : json["Localisation"].GetArray()) - { - if (!localisationStr.IsString()) - { - // not a string but we still GetString() to log it :trol: - spdlog::warn("Localisation '{}' is not a string, skipping...", localisationStr.GetString()); - continue; - } - - LocalisationFiles.push_back(localisationStr.GetString()); - - spdlog::info("'{}' registered Localisation '{}'", Name, localisationStr.GetString()); - } -} - -void Mod::ParseDependencies(rapidjson_document& json) -{ - if (!json.HasMember("Dependencies")) - return; - - if (!json["Dependencies"].IsObject()) - { - spdlog::warn("'Dependencies' field is not an object, skipping..."); - return; - } - - for (auto v = json["Dependencies"].MemberBegin(); v != json["Dependencies"].MemberEnd(); v++) - { - if (!v->name.IsString()) - { - spdlog::warn("Dependency constant '{}' is not a string, skipping...", v->name.GetString()); - continue; - } - if (!v->value.IsString()) - { - spdlog::warn("Dependency constant '{}' is not a string, skipping...", v->value.GetString()); - continue; - } - - if (DependencyConstants.find(v->name.GetString()) != DependencyConstants.end() && - v->value.GetString() != DependencyConstants[v->name.GetString()]) - { - // this is fatal because otherwise the mod will probably try to use functions that dont exist, - // which will cause errors further down the line that are harder to debug - spdlog::error( - "'{}' attempted to register a dependency constant '{}' for '{}' that already exists for '{}'. " - "Change the constant name.", - Name, - v->name.GetString(), - v->value.GetString(), - DependencyConstants[v->name.GetString()]); - return; - } - - if (DependencyConstants.find(v->name.GetString()) == DependencyConstants.end()) - DependencyConstants.emplace(v->name.GetString(), v->value.GetString()); - - spdlog::info("'{}' registered dependency constant '{}' for mod '{}'", Name, v->name.GetString(), v->value.GetString()); - } -} - -void Mod::ParsePluginDependencies(rapidjson_document& json) -{ - if (!json.HasMember("PluginDependencies")) - return; - - if (!json["PluginDependencies"].IsArray()) - { - spdlog::warn("'PluginDependencies' field is not an object, skipping..."); - return; - } - - for (auto& name : json["PluginDependencies"].GetArray()) - { - if (!name.IsString()) - continue; - - spdlog::info("Plugin Constant {} defined by {}", name.GetString(), Name); - - PluginDependencyConstants.push_back(name.GetString()); - } -} - -void Mod::ParseInitScript(rapidjson_document& json) -{ - if (!json.HasMember("InitScript")) - return; - - if (!json["InitScript"].IsString()) - { - spdlog::warn("'InitScript' field is not a string, skipping..."); - return; - } - - initScript = json["InitScript"].GetString(); -} - -ModManager::ModManager() -{ - // precaculated string hashes - // note: use backslashes for these, since we use lexically_normal for file paths which uses them - m_hScriptsRsonHash = STR_HASH("scripts\\vscripts\\scripts.rson"); - m_hPdefHash = STR_HASH( - "cfg\\server\\persistent_player_data_version_231.pdef" // this can have multiple versions, but we use 231 so that's what we hash - ); - m_hKBActHash = STR_HASH("scripts\\kb_act.lst"); - - LoadMods(); -} - -struct Test -{ - std::string funcName; - ScriptContext context; -}; - -template <ScriptContext context> auto ModConCommandCallback_Internal(std::string name, const CCommand& command) -{ - if (g_pSquirrel<context>->m_pSQVM && g_pSquirrel<context>->m_pSQVM) - { - if (command.ArgC() == 1) - { - g_pSquirrel<context>->AsyncCall(name); - } - else - { - std::vector<std::string> args; - args.reserve(command.ArgC()); - for (int i = 1; i < command.ArgC(); i++) - args.push_back(command.Arg(i)); - g_pSquirrel<context>->AsyncCall(name, args); - } - } - else - { - spdlog::warn("ConCommand `{}` was called while the associated Squirrel VM `{}` was unloaded", name, GetContextName(context)); - } -} - -auto ModConCommandCallback(const CCommand& command) -{ - ModConCommand* found = nullptr; - auto commandString = std::string(command.GetCommandString()); - - // Finding the first space to remove the command's name - auto firstSpace = commandString.find(' '); - if (firstSpace) - { - commandString = commandString.substr(0, firstSpace); - } - - // Find the mod this command belongs to - for (auto& mod : g_pModManager->m_LoadedMods) - { - auto res = std::find_if( - mod.ConCommands.begin(), - mod.ConCommands.end(), - [&commandString](const ModConCommand* other) { return other->Name == commandString; }); - if (res != mod.ConCommands.end()) - { - found = *res; - break; - } - } - if (!found) - return; - - switch (found->Context) - { - case ScriptContext::CLIENT: - ModConCommandCallback_Internal<ScriptContext::CLIENT>(found->Function, command); - break; - case ScriptContext::SERVER: - ModConCommandCallback_Internal<ScriptContext::SERVER>(found->Function, command); - break; - case ScriptContext::UI: - ModConCommandCallback_Internal<ScriptContext::UI>(found->Function, command); - break; - }; -} - -void ModManager::LoadMods() -{ - if (m_bHasLoadedMods) - UnloadMods(); - - std::vector<fs::path> modDirs; - - // ensure dirs exist - fs::remove_all(GetCompiledAssetsPath()); - fs::create_directories(GetModFolderPath()); - fs::create_directories(GetThunderstoreModFolderPath()); - fs::create_directories(GetRemoteModFolderPath()); - - m_DependencyConstants.clear(); - - // read enabled mods cfg - std::ifstream enabledModsStream(GetNorthstarPrefix() + "/enabledmods.json"); - std::stringstream enabledModsStringStream; - - if (!enabledModsStream.fail()) - { - while (enabledModsStream.peek() != EOF) - enabledModsStringStream << (char)enabledModsStream.get(); - - enabledModsStream.close(); - m_EnabledModsCfg.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>( - enabledModsStringStream.str().c_str()); - - m_bHasEnabledModsCfg = m_EnabledModsCfg.IsObject(); - } - - // get mod directories - std::filesystem::directory_iterator classicModsDir = fs::directory_iterator(GetModFolderPath()); - std::filesystem::directory_iterator remoteModsDir = fs::directory_iterator(GetRemoteModFolderPath()); - std::filesystem::directory_iterator thunderstoreModsDir = fs::directory_iterator(GetThunderstoreModFolderPath()); - - for (fs::directory_entry dir : classicModsDir) - if (fs::exists(dir.path() / "mod.json")) - modDirs.push_back(dir.path()); - - // Special case for Thunderstore and remote mods directories - // Set up regex for `AUTHOR-MOD-VERSION` pattern - std::regex pattern(R"(.*\\([a-zA-Z0-9_]+)-([a-zA-Z0-9_]+)-(\d+\.\d+\.\d+))"); - - for (fs::directory_iterator dirIterator : {thunderstoreModsDir, remoteModsDir}) - { - for (fs::directory_entry dir : dirIterator) - { - fs::path modsDir = dir.path() / "mods"; // Check for mods folder in the Thunderstore mod - // Use regex to match `AUTHOR-MOD-VERSION` pattern - if (!std::regex_match(dir.path().string(), pattern)) - { - spdlog::warn("The following directory did not match 'AUTHOR-MOD-VERSION': {}", dir.path().string()); - continue; // skip loading mod that doesn't match - } - if (fs::exists(modsDir) && fs::is_directory(modsDir)) - { - for (fs::directory_entry subDir : fs::directory_iterator(modsDir)) - { - if (fs::exists(subDir.path() / "mod.json")) - { - modDirs.push_back(subDir.path()); - } - } - } - } - } - - for (fs::path modDir : modDirs) - { - // read mod json file - std::ifstream jsonStream(modDir / "mod.json"); - std::stringstream jsonStringStream; - - // fail if no mod json - if (jsonStream.fail()) - { - spdlog::warn( - "Mod file at '{}' does not exist or could not be read, is it installed correctly?", (modDir / "mod.json").string()); - continue; - } - - while (jsonStream.peek() != EOF) - jsonStringStream << (char)jsonStream.get(); - - jsonStream.close(); - - Mod mod(modDir, (char*)jsonStringStream.str().c_str()); - - for (auto& pair : mod.DependencyConstants) - { - if (m_DependencyConstants.find(pair.first) != m_DependencyConstants.end() && m_DependencyConstants[pair.first] != pair.second) - { - spdlog::error( - "'{}' attempted to register a dependency constant '{}' for '{}' that already exists for '{}'. " - "Change the constant name.", - mod.Name, - pair.first, - pair.second, - m_DependencyConstants[pair.first]); - mod.m_bWasReadSuccessfully = false; - break; - } - if (m_DependencyConstants.find(pair.first) == m_DependencyConstants.end()) - m_DependencyConstants.emplace(pair); - } - - for (std::string& dependency : mod.PluginDependencyConstants) - { - m_PluginDependencyConstants.insert(dependency); - } - - if (m_bHasEnabledModsCfg && m_EnabledModsCfg.HasMember(mod.Name.c_str())) - mod.m_bEnabled = m_EnabledModsCfg[mod.Name.c_str()].IsTrue(); - else - mod.m_bEnabled = true; - - if (mod.m_bWasReadSuccessfully) - { - if (mod.m_bEnabled) - spdlog::info("'{}' loaded successfully, version {}", mod.Name, mod.Version); - else - spdlog::info("'{}' loaded successfully, version {} (DISABLED)", mod.Name, mod.Version); - - m_LoadedMods.push_back(mod); - } - else - spdlog::warn("Mod file at '{}' failed to load", (modDir / "mod.json").string()); - } - - // sort by load prio, lowest-highest - std::sort(m_LoadedMods.begin(), m_LoadedMods.end(), [](Mod& a, Mod& b) { return a.LoadPriority < b.LoadPriority; }); - - // This is used to check if some mods have a folder but no entry in enabledmods.json - bool newModsDetected = false; - - for (Mod& mod : m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - // Add mod entry to enabledmods.json if it doesn't exist - if (!mod.m_bIsRemote && !m_EnabledModsCfg.HasMember(mod.Name.c_str())) - { - m_EnabledModsCfg.AddMember(rapidjson_document::StringRefType(mod.Name.c_str()), true, m_EnabledModsCfg.GetAllocator()); - newModsDetected = true; - } - - // register convars - // for reloads, this is sorta barebones, when we have a good findconvar method, we could probably reset flags and stuff on - // preexisting convars note: we don't delete convars if they already exist because they're used for script stuff, unfortunately this - // causes us to leak memory on reload, but not much, potentially find a way to not do this at some point - for (ModConVar* convar : mod.ConVars) - { - // make sure convar isn't registered yet, unsure if necessary but idk what - // behaviour is for defining same convar multiple times - if (!g_pCVar->FindVar(convar->Name.c_str())) - { - new ConVar(convar->Name.c_str(), convar->DefaultValue.c_str(), convar->Flags, convar->HelpString.c_str()); - } - } - - for (ModConCommand* command : mod.ConCommands) - { - // make sure command isnt't registered multiple times. - if (!g_pCVar->FindCommand(command->Name.c_str())) - { - ConCommand* newCommand = new ConCommand(); - std::string funcName = command->Function; - RegisterConCommand(command->Name.c_str(), ModConCommandCallback, command->HelpString.c_str(), command->Flags); - } - } - - // read vpk paths - if (fs::exists(mod.m_ModDirectory / "vpk")) - { - // read vpk cfg - std::ifstream vpkJsonStream(mod.m_ModDirectory / "vpk/vpk.json"); - std::stringstream vpkJsonStringStream; - - bool bUseVPKJson = false; - rapidjson::Document dVpkJson; - - if (!vpkJsonStream.fail()) - { - while (vpkJsonStream.peek() != EOF) - vpkJsonStringStream << (char)vpkJsonStream.get(); - - vpkJsonStream.close(); - dVpkJson.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>( - vpkJsonStringStream.str().c_str()); - - bUseVPKJson = !dVpkJson.HasParseError() && dVpkJson.IsObject(); - } - - for (fs::directory_entry file : fs::directory_iterator(mod.m_ModDirectory / "vpk")) - { - // a bunch of checks to make sure we're only adding dir vpks and their paths are good - // note: the game will literally only load vpks with the english prefix - if (fs::is_regular_file(file) && file.path().extension() == ".vpk" && - file.path().string().find("english") != std::string::npos && - file.path().string().find(".bsp.pak000_dir") != std::string::npos) - { - std::string formattedPath = file.path().filename().string(); - - // this really fucking sucks but it'll work - std::string vpkName = formattedPath.substr(strlen("english"), formattedPath.find(".bsp") - 3); - - ModVPKEntry& modVpk = mod.Vpks.emplace_back(); - modVpk.m_bAutoLoad = !bUseVPKJson || (dVpkJson.HasMember("Preload") && dVpkJson["Preload"].IsObject() && - dVpkJson["Preload"].HasMember(vpkName) && dVpkJson["Preload"][vpkName].IsTrue()); - modVpk.m_sVpkPath = (file.path().parent_path() / vpkName).string(); - - if (m_bHasLoadedMods && modVpk.m_bAutoLoad) - (*g_pFilesystem)->m_vtable->MountVPK(*g_pFilesystem, vpkName.c_str()); - } - } - } - - // read rpak paths - if (fs::exists(mod.m_ModDirectory / "paks")) - { - // read rpak cfg - std::ifstream rpakJsonStream(mod.m_ModDirectory / "paks/rpak.json"); - std::stringstream rpakJsonStringStream; - - bool bUseRpakJson = false; - rapidjson::Document dRpakJson; - - if (!rpakJsonStream.fail()) - { - while (rpakJsonStream.peek() != EOF) - rpakJsonStringStream << (char)rpakJsonStream.get(); - - rpakJsonStream.close(); - dRpakJson.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>( - rpakJsonStringStream.str().c_str()); - - bUseRpakJson = !dRpakJson.HasParseError() && dRpakJson.IsObject(); - } - - // read pak aliases - if (bUseRpakJson && dRpakJson.HasMember("Aliases") && dRpakJson["Aliases"].IsObject()) - { - for (rapidjson::Value::ConstMemberIterator iterator = dRpakJson["Aliases"].MemberBegin(); - iterator != dRpakJson["Aliases"].MemberEnd(); - iterator++) - { - if (!iterator->name.IsString() || !iterator->value.IsString()) - continue; - - mod.RpakAliases.insert(std::make_pair(iterator->name.GetString(), iterator->value.GetString())); - } - } - - for (fs::directory_entry file : fs::directory_iterator(mod.m_ModDirectory / "paks")) - { - // ensure we're only loading rpaks - if (fs::is_regular_file(file) && file.path().extension() == ".rpak") - { - std::string pakName(file.path().filename().string()); - - ModRpakEntry& modPak = mod.Rpaks.emplace_back(); - modPak.m_bAutoLoad = - !bUseRpakJson || (dRpakJson.HasMember("Preload") && dRpakJson["Preload"].IsObject() && - dRpakJson["Preload"].HasMember(pakName) && dRpakJson["Preload"][pakName].IsTrue()); - - // postload things - if (!bUseRpakJson || - (dRpakJson.HasMember("Postload") && dRpakJson["Postload"].IsObject() && dRpakJson["Postload"].HasMember(pakName))) - modPak.m_sLoadAfterPak = dRpakJson["Postload"][pakName].GetString(); - - modPak.m_sPakName = pakName; - - // read header of file and get the starpak paths - // this is done here as opposed to on starpak load because multiple rpaks can load a starpak - // and there is seemingly no good way to tell which rpak is causing the load of a starpak :/ - - std::ifstream rpakStream(file.path(), std::ios::binary); - - // seek to the point in the header where the starpak reference size is - rpakStream.seekg(0x38, std::ios::beg); - int starpaksSize = 0; - rpakStream.read((char*)&starpaksSize, 2); - - // seek to just after the header - rpakStream.seekg(0x58, std::ios::beg); - // read the starpak reference(s) - std::vector<char> buf(starpaksSize); - rpakStream.read(buf.data(), starpaksSize); - - rpakStream.close(); - - // split the starpak reference(s) into strings to hash - std::string str = ""; - for (int i = 0; i < starpaksSize; i++) - { - // if the current char is null, that signals the end of the current starpak path - if (buf[i] != 0x00) - { - str += buf[i]; - } - else - { - // only add the string we are making if it isnt empty - if (!str.empty()) - { - mod.StarpakPaths.push_back(STR_HASH(str)); - spdlog::info("Mod {} registered starpak '{}'", mod.Name, str); - str = ""; - } - } - } - - // not using atm because we need to resolve path to rpak - // if (m_hasLoadedMods && modPak.m_bAutoLoad) - // g_pPakLoadManager->LoadPakAsync(pakName.c_str()); - } - } - } - - // read keyvalues paths - if (fs::exists(mod.m_ModDirectory / "keyvalues")) - { - for (fs::directory_entry file : fs::recursive_directory_iterator(mod.m_ModDirectory / "keyvalues")) - { - if (fs::is_regular_file(file)) - { - std::string kvStr = - g_pModManager->NormaliseModFilePath(file.path().lexically_relative(mod.m_ModDirectory / "keyvalues")); - mod.KeyValues.emplace(STR_HASH(kvStr), kvStr); - } - } - } - - // read pdiff - if (fs::exists(mod.m_ModDirectory / "mod.pdiff")) - { - std::ifstream pdiffStream(mod.m_ModDirectory / "mod.pdiff"); - - if (!pdiffStream.fail()) - { - std::stringstream pdiffStringStream; - while (pdiffStream.peek() != EOF) - pdiffStringStream << (char)pdiffStream.get(); - - pdiffStream.close(); - - mod.Pdiff = pdiffStringStream.str(); - } - } - - // read bink video paths - if (fs::exists(mod.m_ModDirectory / "media")) - { - for (fs::directory_entry file : fs::recursive_directory_iterator(mod.m_ModDirectory / "media")) - if (fs::is_regular_file(file) && file.path().extension() == ".bik") - mod.BinkVideos.push_back(file.path().filename().string()); - } - - // try to load audio - if (fs::exists(mod.m_ModDirectory / "audio")) - { - for (fs::directory_entry file : fs::directory_iterator(mod.m_ModDirectory / "audio")) - { - if (fs::is_regular_file(file) && file.path().extension().string() == ".json") - { - if (!g_CustomAudioManager.TryLoadAudioOverride(file.path())) - { - spdlog::warn("Mod {} has an invalid audio def {}", mod.Name, file.path().filename().string()); - continue; - } - } - } - } - } - - // If there are new mods, we write entries accordingly in enabledmods.json - if (newModsDetected) - { - std::ofstream writeStream(GetNorthstarPrefix() + "/enabledmods.json"); - rapidjson::OStreamWrapper writeStreamWrapper(writeStream); - rapidjson::PrettyWriter<rapidjson::OStreamWrapper> writer(writeStreamWrapper); - m_EnabledModsCfg.Accept(writer); - } - - // in a seperate loop because we register mod files in reverse order, since mods loaded later should have their files prioritised - for (int64_t i = m_LoadedMods.size() - 1; i > -1; i--) - { - if (!m_LoadedMods[i].m_bEnabled) - continue; - - if (fs::exists(m_LoadedMods[i].m_ModDirectory / MOD_OVERRIDE_DIR)) - { - for (fs::directory_entry file : fs::recursive_directory_iterator(m_LoadedMods[i].m_ModDirectory / MOD_OVERRIDE_DIR)) - { - std::string path = - g_pModManager->NormaliseModFilePath(file.path().lexically_relative(m_LoadedMods[i].m_ModDirectory / MOD_OVERRIDE_DIR)); - if (file.is_regular_file() && m_ModFiles.find(path) == m_ModFiles.end()) - { - ModOverrideFile modFile; - modFile.m_pOwningMod = &m_LoadedMods[i]; - modFile.m_Path = path; - m_ModFiles.insert(std::make_pair(path, modFile)); - } - } - } - } - - // build modinfo obj for masterserver - rapidjson_document modinfoDoc; - auto& alloc = modinfoDoc.GetAllocator(); - modinfoDoc.SetObject(); - modinfoDoc.AddMember("Mods", rapidjson::kArrayType, alloc); - - int currentModIndex = 0; - for (Mod& mod : m_LoadedMods) - { - if (!mod.m_bEnabled || (!mod.RequiredOnClient && !mod.Pdiff.size())) - continue; - - modinfoDoc["Mods"].PushBack(rapidjson::kObjectType, modinfoDoc.GetAllocator()); - modinfoDoc["Mods"][currentModIndex].AddMember("Name", rapidjson::StringRef(&mod.Name[0]), modinfoDoc.GetAllocator()); - modinfoDoc["Mods"][currentModIndex].AddMember("Version", rapidjson::StringRef(&mod.Version[0]), modinfoDoc.GetAllocator()); - modinfoDoc["Mods"][currentModIndex].AddMember("RequiredOnClient", mod.RequiredOnClient, modinfoDoc.GetAllocator()); - modinfoDoc["Mods"][currentModIndex].AddMember("Pdiff", rapidjson::StringRef(&mod.Pdiff[0]), modinfoDoc.GetAllocator()); - - currentModIndex++; - } - - rapidjson::StringBuffer buffer; - buffer.Clear(); - rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); - modinfoDoc.Accept(writer); - g_pMasterServerManager->m_sOwnModInfoJson = std::string(buffer.GetString()); - - m_bHasLoadedMods = true; -} - -void ModManager::UnloadMods() -{ - // clean up stuff from mods before we unload - m_ModFiles.clear(); - fs::remove_all(GetCompiledAssetsPath()); - - g_CustomAudioManager.ClearAudioOverrides(); - - if (!m_bHasEnabledModsCfg) - m_EnabledModsCfg.SetObject(); - - for (Mod& mod : m_LoadedMods) - { - // remove all built kvs - for (std::pair<size_t, std::string> kvPaths : mod.KeyValues) - fs::remove(GetCompiledAssetsPath() / fs::path(kvPaths.second).lexically_relative(mod.m_ModDirectory)); - - mod.KeyValues.clear(); - - // write to m_enabledModsCfg - // should we be doing this here or should scripts be doing this manually? - // main issue with doing this here is when we reload mods for connecting to a server, we write enabled mods, which isn't necessarily - // what we wanna do - if (!m_EnabledModsCfg.HasMember(mod.Name.c_str())) - m_EnabledModsCfg.AddMember(rapidjson_document::StringRefType(mod.Name.c_str()), false, m_EnabledModsCfg.GetAllocator()); - - m_EnabledModsCfg[mod.Name.c_str()].SetBool(mod.m_bEnabled); - } - - std::ofstream writeStream(GetNorthstarPrefix() + "/enabledmods.json"); - rapidjson::OStreamWrapper writeStreamWrapper(writeStream); - rapidjson::PrettyWriter<rapidjson::OStreamWrapper> writer(writeStreamWrapper); - m_EnabledModsCfg.Accept(writer); - - // do we need to dealloc individual entries in m_loadedMods? idk, rework - m_LoadedMods.clear(); -} - -std::string ModManager::NormaliseModFilePath(const fs::path path) -{ - std::string str = path.lexically_normal().string(); - - // force to lowercase - for (char& c : str) - if (c <= 'Z' && c >= 'A') - c = c - ('Z' - 'z'); - - return str; -} - -void ModManager::CompileAssetsForFile(const char* filename) -{ - size_t fileHash = STR_HASH(NormaliseModFilePath(fs::path(filename))); - - if (fileHash == m_hScriptsRsonHash) - BuildScriptsRson(); - else if (fileHash == m_hPdefHash) - BuildPdef(); - else if (fileHash == m_hKBActHash) - BuildKBActionsList(); - else - { - // check if we should build keyvalues, depending on whether any of our mods have patch kvs for this file - for (Mod& mod : m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - if (mod.KeyValues.find(fileHash) != mod.KeyValues.end()) - { - TryBuildKeyValues(filename); - return; - } - } - } -} - -void ConCommand_reload_mods(const CCommand& args) -{ - g_pModManager->LoadMods(); -} - -fs::path GetModFolderPath() -{ - return fs::path(GetNorthstarPrefix() + MOD_FOLDER_SUFFIX); -} -fs::path GetThunderstoreModFolderPath() -{ - return fs::path(GetNorthstarPrefix() + THUNDERSTORE_MOD_FOLDER_SUFFIX); -} -fs::path GetRemoteModFolderPath() -{ - return fs::path(GetNorthstarPrefix() + REMOTE_MOD_FOLDER_SUFFIX); -} -fs::path GetCompiledAssetsPath() -{ - return fs::path(GetNorthstarPrefix() + COMPILED_ASSETS_SUFFIX); -} - -ON_DLL_LOAD_RELIESON("engine.dll", ModManager, (ConCommand, MasterServer), (CModule module)) -{ - g_pModManager = new ModManager; - - RegisterConCommand("reload_mods", ConCommand_reload_mods, "reloads mods", FCVAR_NONE); -} diff --git a/NorthstarDLL/mods/modmanager.h b/NorthstarDLL/mods/modmanager.h deleted file mode 100644 index 233f004d..00000000 --- a/NorthstarDLL/mods/modmanager.h +++ /dev/null @@ -1,187 +0,0 @@ -#pragma once -#include "core/convar/convar.h" -#include "core/memalloc.h" -#include "squirrel/squirrel.h" - -#include "rapidjson/document.h" -#include <string> -#include <vector> -#include <filesystem> -#include <unordered_set> - -const std::string MOD_FOLDER_SUFFIX = "\\mods"; -const std::string THUNDERSTORE_MOD_FOLDER_SUFFIX = "\\packages"; -const std::string REMOTE_MOD_FOLDER_SUFFIX = "\\runtime\\remote\\mods"; -const fs::path MOD_OVERRIDE_DIR = "mod"; -const std::string COMPILED_ASSETS_SUFFIX = "\\runtime\\compiled"; - -const std::set<std::string> MODS_BLACKLIST = {"Mod Settings"}; - -struct ModConVar -{ -public: - std::string Name; - std::string DefaultValue; - std::string HelpString; - int Flags; -}; - -struct ModConCommand -{ -public: - std::string Name; - std::string Function; - std::string HelpString; - ScriptContext Context; - int Flags; -}; - -struct ModScriptCallback -{ -public: - ScriptContext Context; - - // called before the codecallback is executed - std::string BeforeCallback; - // called after the codecallback has finished executing - std::string AfterCallback; - // called right before the vm is destroyed. - std::string DestroyCallback; -}; - -struct ModScript -{ -public: - std::string Path; - std::string RunOn; - - std::vector<ModScriptCallback> Callbacks; -}; - -// these are pretty much identical, could refactor to use the same stuff? -struct ModVPKEntry -{ -public: - bool m_bAutoLoad; - std::string m_sVpkPath; -}; - -struct ModRpakEntry -{ -public: - bool m_bAutoLoad; - std::string m_sPakName; - std::string m_sLoadAfterPak; -}; - -class Mod -{ -public: - // runtime stuff - bool m_bEnabled = true; - bool m_bWasReadSuccessfully = false; - fs::path m_ModDirectory; - bool m_bIsRemote; - - // mod.json stuff: - - // the mod's name - std::string Name; - // the mod's description - std::string Description; - // the mod's version, should be in semver - std::string Version; - // a download link to the mod, for clients that try to join without the mod - std::string DownloadLink; - - // whether clients need the mod to join servers running this mod - bool RequiredOnClient; - // the priority for this mod's files, mods with prio 0 are loaded first, then 1, then 2, etc - int LoadPriority; - - // custom scripts used by the mod - std::vector<ModScript> Scripts; - // convars created by the mod - std::vector<ModConVar*> ConVars; - // concommands created by the mod - std::vector<ModConCommand*> ConCommands; - // custom localisation files created by the mod - std::vector<std::string> LocalisationFiles; - // custom script init.nut - std::string initScript; - - // other files: - - std::vector<ModVPKEntry> Vpks; - std::unordered_map<size_t, std::string> KeyValues; - std::vector<std::string> BinkVideos; - std::string Pdiff; // only need one per mod - - std::vector<ModRpakEntry> Rpaks; - std::unordered_map<std::string, std::string> - RpakAliases; // paks we alias to other rpaks, e.g. to load sp_crashsite paks on the map mp_crashsite - std::vector<size_t> StarpakPaths; // starpaks that this mod contains - // there seems to be no nice way to get the rpak that is causing the load of a starpak? - // hashed with STR_HASH - - std::unordered_map<std::string, std::string> DependencyConstants; - std::vector<std::string> PluginDependencyConstants; - -public: - Mod(fs::path modPath, char* jsonBuf); - -private: - void ParseConVars(rapidjson_document& json); - void ParseConCommands(rapidjson_document& json); - void ParseScripts(rapidjson_document& json); - void ParseLocalization(rapidjson_document& json); - void ParseDependencies(rapidjson_document& json); - void ParsePluginDependencies(rapidjson_document& json); - void ParseInitScript(rapidjson_document& json); -}; - -struct ModOverrideFile -{ -public: - Mod* m_pOwningMod; - fs::path m_Path; -}; - -class ModManager -{ -private: - bool m_bHasLoadedMods = false; - bool m_bHasEnabledModsCfg; - rapidjson_document m_EnabledModsCfg; - - // precalculated hashes - size_t m_hScriptsRsonHash; - size_t m_hPdefHash; - size_t m_hKBActHash; - -public: - std::vector<Mod> m_LoadedMods; - std::unordered_map<std::string, ModOverrideFile> m_ModFiles; - std::unordered_map<std::string, std::string> m_DependencyConstants; - std::unordered_set<std::string> m_PluginDependencyConstants; - -public: - ModManager(); - void LoadMods(); - void UnloadMods(); - std::string NormaliseModFilePath(const fs::path path); - void CompileAssetsForFile(const char* filename); - - // compile asset type stuff, these are done in files under runtime/compiled/ - void BuildScriptsRson(); - void TryBuildKeyValues(const char* filename); - void BuildPdef(); - void BuildKBActionsList(); -}; - -fs::path GetModFolderPath(); -fs::path GetRemoteModFolderPath(); -fs::path GetThunderstoreModFolderPath(); -fs::path GetCompiledAssetsPath(); - -extern ModManager* g_pModManager; diff --git a/NorthstarDLL/mods/modsavefiles.cpp b/NorthstarDLL/mods/modsavefiles.cpp deleted file mode 100644 index 68e33864..00000000 --- a/NorthstarDLL/mods/modsavefiles.cpp +++ /dev/null @@ -1,572 +0,0 @@ -#include <filesystem> -#include <sstream> -#include <fstream> -#include "squirrel/squirrel.h" -#include "util/utils.h" -#include "mods/modmanager.h" -#include "modsavefiles.h" -#include "rapidjson/document.h" -#include "rapidjson/writer.h" -#include "rapidjson/stringbuffer.h" -#include "config/profile.h" -#include "core/tier0.h" -#include "rapidjson/error/en.h" -#include "scripts/scriptjson.h" - -SaveFileManager* g_pSaveFileManager; -int MAX_FOLDER_SIZE = 52428800; // 50MB (50 * 1024 * 1024) -fs::path savePath; - -/// <summary></summary> -/// <param name="dir">The directory we want the size of.</param> -/// <param name="file">The file we're excluding from the calculation.</param> -/// <returns>The size of the contents of the current directory, excluding a specific file.</returns> -uintmax_t GetSizeOfFolderContentsMinusFile(fs::path dir, std::string file) -{ - uintmax_t result = 0; - for (const auto& entry : fs::directory_iterator(dir)) - { - if (entry.path().filename() == file) - continue; - // fs::file_size may not work on directories - but does in some cases. - // per cppreference.com, it's "implementation-defined". - try - { - result += fs::file_size(entry.path()); - } - catch (fs::filesystem_error& e) - { - if (entry.is_directory()) - { - result += GetSizeOfFolderContentsMinusFile(entry.path(), ""); - } - } - } - return result; -} - -uintmax_t GetSizeOfFolder(fs::path dir) -{ - uintmax_t result = 0; - for (const auto& entry : fs::directory_iterator(dir)) - { - // fs::file_size may not work on directories - but does in some cases. - // per cppreference.com, it's "implementation-defined". - try - { - result += fs::file_size(entry.path()); - } - catch (fs::filesystem_error& e) - { - if (entry.is_directory()) - { - result += GetSizeOfFolderContentsMinusFile(entry.path(), ""); - } - } - } - return result; -} - -// Saves a file asynchronously. -template <ScriptContext context> void SaveFileManager::SaveFileAsync(fs::path file, std::string contents) -{ - auto mutex = std::ref(fileMutex); - std::thread writeThread( - [mutex, file, contents]() - { - try - { - mutex.get().lock(); - - fs::path dir = file.parent_path(); - // this actually allows mods to go over the limit, but not by much - // the limit is to prevent mods from taking gigabytes of space, - // we don't need to be particularly strict. - if (GetSizeOfFolderContentsMinusFile(dir, file.filename().string()) + contents.length() > MAX_FOLDER_SIZE) - { - // tbh, you're either trying to fill the hard drive or use so much data, you SHOULD be congratulated. - spdlog::error(fmt::format("Mod spamming save requests? Folder limit bypassed despite previous checks. Not saving.")); - mutex.get().unlock(); - return; - } - - std::ofstream fileStr(file); - if (fileStr.fail()) - { - mutex.get().unlock(); - return; - } - - fileStr.write(contents.c_str(), contents.length()); - fileStr.close(); - - mutex.get().unlock(); - // side-note: this causes a leak? - // when a file is added to the map, it's never removed. - // no idea how to fix this - because we have no way to check if there are other threads waiting to use this file(?) - // tried to use m.try_lock(), but it's unreliable, it seems. - } - catch (std::exception ex) - { - spdlog::error("SAVE FAILED!"); - mutex.get().unlock(); - spdlog::error(ex.what()); - } - }); - - writeThread.detach(); -} - -// Loads a file asynchronously. -template <ScriptContext context> int SaveFileManager::LoadFileAsync(fs::path file) -{ - int handle = ++m_iLastRequestHandle; - auto mutex = std::ref(fileMutex); - std::thread readThread( - [mutex, file, handle]() - { - try - { - mutex.get().lock(); - - std::ifstream fileStr(file); - if (fileStr.fail()) - { - spdlog::error("A file was supposed to be loaded but we can't access it?!"); - - g_pSquirrel<context>->AsyncCall("NSHandleLoadResult", handle, false, ""); - mutex.get().unlock(); - return; - } - - std::stringstream stringStream; - stringStream << fileStr.rdbuf(); - - g_pSquirrel<context>->AsyncCall("NSHandleLoadResult", handle, true, stringStream.str()); - - fileStr.close(); - mutex.get().unlock(); - // side-note: this causes a leak? - // when a file is added to the map, it's never removed. - // no idea how to fix this - because we have no way to check if there are other threads waiting to use this file(?) - // tried to use m.try_lock(), but it's unreliable, it seems. - } - catch (std::exception ex) - { - spdlog::error("LOAD FAILED!"); - g_pSquirrel<context>->AsyncCall("NSHandleLoadResult", handle, false, ""); - mutex.get().unlock(); - spdlog::error(ex.what()); - } - }); - - readThread.detach(); - return handle; -} - -// Deletes a file asynchronously. -template <ScriptContext context> void SaveFileManager::DeleteFileAsync(fs::path file) -{ - // P.S. I don't like how we have to async delete calls but we do. - auto m = std::ref(fileMutex); - std::thread deleteThread( - [m, file]() - { - try - { - m.get().lock(); - - fs::remove(file); - - m.get().unlock(); - // side-note: this causes a leak? - // when a file is added to the map, it's never removed. - // no idea how to fix this - because we have no way to check if there are other threads waiting to use this file(?) - // tried to use m.try_lock(), but it's unreliable, it seems. - } - catch (std::exception ex) - { - spdlog::error("DELETE FAILED!"); - m.get().unlock(); - spdlog::error(ex.what()); - } - }); - - deleteThread.detach(); -} - -// Checks if a file contains null characters. -bool ContainsInvalidChars(std::string str) -{ - // we don't allow null characters either, even if they're ASCII characters because idk if people can - // use it to circumvent the file extension suffix. - return std::any_of(str.begin(), str.end(), [](char c) { return c == '\0'; }); -} - -// Checks if the relative path (param) remains inside the mod directory (dir). -// Paths are restricted to ASCII because encoding is fucked and we decided we won't bother. -bool IsPathSafe(const std::string param, fs::path dir) -{ - try - { - auto const normRoot = fs::weakly_canonical(dir); - auto const normChild = fs::weakly_canonical(dir / param); - - auto itr = std::search(normChild.begin(), normChild.end(), normRoot.begin(), normRoot.end()); - // we return if the file is safe (inside the directory) and uses only ASCII chars in the path. - return itr == normChild.begin() && std::none_of( - param.begin(), - param.end(), - [](char c) - { - unsigned char unsignedC = static_cast<unsigned char>(c); - return unsignedC > 127 || unsignedC < 0; - }); - } - catch (fs::filesystem_error err) - { - return false; - } -} - -// void NSSaveFile( string file, string data ) -ADD_SQFUNC("void", NSSaveFile, "string file, string data", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - Mod* mod = g_pSquirrel<context>->getcallingmod(sqvm); - if (mod == nullptr) - { - g_pSquirrel<context>->raiseerror(sqvm, "Has to be called from a mod function!"); - return SQRESULT_ERROR; - } - - fs::path dir = savePath / fs::path(mod->m_ModDirectory).filename(); - std::string fileName = g_pSquirrel<context>->getstring(sqvm, 1); - if (!IsPathSafe(fileName, dir)) - { - g_pSquirrel<context>->raiseerror( - sqvm, - fmt::format( - "File name invalid ({})! Make sure it does not contain any non-ASCII character, and results in a path inside your mod's " - "save folder.", - fileName, - mod->Name) - .c_str()); - return SQRESULT_ERROR; - } - - std::string content = g_pSquirrel<context>->getstring(sqvm, 2); - if (ContainsInvalidChars(content)) - { - g_pSquirrel<context>->raiseerror( - sqvm, fmt::format("File contents may not contain NUL/\\0 characters! Make sure your strings are valid!", mod->Name).c_str()); - return SQRESULT_ERROR; - } - - fs::create_directories(dir); - // this actually allows mods to go over the limit, but not by much - // the limit is to prevent mods from taking gigabytes of space, - // this ain't a cloud service. - if (GetSizeOfFolderContentsMinusFile(dir, fileName) + content.length() > MAX_FOLDER_SIZE) - { - g_pSquirrel<context>->raiseerror( - sqvm, - fmt::format( - "The mod {} has reached the maximum folder size.\n\nAsk the mod developer to optimize their data usage," - "or increase the maximum folder size using the -maxfoldersize launch parameter.", - mod->Name) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSaveFileManager->SaveFileAsync<context>(dir / fileName, content); - - return SQRESULT_NULL; -} - -// void NSSaveJSONFile(string file, table data) -ADD_SQFUNC("void", NSSaveJSONFile, "string file, table data", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - Mod* mod = g_pSquirrel<context>->getcallingmod(sqvm); - if (mod == nullptr) - { - g_pSquirrel<context>->raiseerror(sqvm, "Has to be called from a mod function!"); - return SQRESULT_ERROR; - } - - fs::path dir = savePath / fs::path(mod->m_ModDirectory).filename(); - std::string fileName = g_pSquirrel<context>->getstring(sqvm, 1); - if (!IsPathSafe(fileName, dir)) - { - g_pSquirrel<context>->raiseerror( - sqvm, - fmt::format( - "File name invalid ({})! Make sure it does not contain any non-ASCII character, and results in a path inside your mod's " - "save folder.", - fileName, - mod->Name) - .c_str()); - return SQRESULT_ERROR; - } - - // Note - this cannot be done in the async func since the table may get garbage collected. - // This means that especially large tables may still clog up the system. - std::string content = EncodeJSON<context>(sqvm); - if (ContainsInvalidChars(content)) - { - g_pSquirrel<context>->raiseerror( - sqvm, fmt::format("File contents may not contain NUL/\\0 characters! Make sure your strings are valid!", mod->Name).c_str()); - return SQRESULT_ERROR; - } - - fs::create_directories(dir); - // this actually allows mods to go over the limit, but not by much - // the limit is to prevent mods from taking gigabytes of space, - // this ain't a cloud service. - if (GetSizeOfFolderContentsMinusFile(dir, fileName) + content.length() > MAX_FOLDER_SIZE) - { - g_pSquirrel<context>->raiseerror( - sqvm, - fmt::format( - "The mod {} has reached the maximum folder size.\n\nAsk the mod developer to optimize their data usage," - "or increase the maximum folder size using the -maxfoldersize launch parameter.", - mod->Name) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSaveFileManager->SaveFileAsync<context>(dir / fileName, content); - - return SQRESULT_NULL; -} - -// int NS_InternalLoadFile(string file) -ADD_SQFUNC("int", NS_InternalLoadFile, "string file", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - Mod* mod = g_pSquirrel<context>->getcallingmod(sqvm, 1); // the function that called NSLoadFile :) - if (mod == nullptr) - { - g_pSquirrel<context>->raiseerror(sqvm, "Has to be called from a mod function!"); - return SQRESULT_ERROR; - } - - fs::path dir = savePath / fs::path(mod->m_ModDirectory).filename(); - std::string fileName = g_pSquirrel<context>->getstring(sqvm, 1); - if (!IsPathSafe(fileName, dir)) - { - g_pSquirrel<context>->raiseerror( - sqvm, - fmt::format( - "File name invalid ({})! Make sure it does not contain any non-ASCII character, and results in a path inside your mod's " - "save folder.", - fileName, - mod->Name) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel<context>->pushinteger(sqvm, g_pSaveFileManager->LoadFileAsync<context>(dir / fileName)); - - return SQRESULT_NOTNULL; -} - -// bool NSDoesFileExist(string file) -ADD_SQFUNC("bool", NSDoesFileExist, "string file", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - Mod* mod = g_pSquirrel<context>->getcallingmod(sqvm); - - fs::path dir = savePath / fs::path(mod->m_ModDirectory).filename(); - std::string fileName = g_pSquirrel<context>->getstring(sqvm, 1); - if (!IsPathSafe(fileName, dir)) - { - g_pSquirrel<context>->raiseerror( - sqvm, - fmt::format( - "File name invalid ({})! Make sure it does not contain any non-ASCII character, and results in a path inside your mod's " - "save folder.", - fileName, - mod->Name) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel<context>->pushbool(sqvm, fs::exists(dir / (fileName))); - return SQRESULT_NOTNULL; -} - -// int NSGetFileSize(string file) -ADD_SQFUNC("int", NSGetFileSize, "string file", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - Mod* mod = g_pSquirrel<context>->getcallingmod(sqvm); - - fs::path dir = savePath / fs::path(mod->m_ModDirectory).filename(); - std::string fileName = g_pSquirrel<context>->getstring(sqvm, 1); - if (!IsPathSafe(fileName, dir)) - { - g_pSquirrel<context>->raiseerror( - sqvm, - fmt::format( - "File name invalid ({})! Make sure it does not contain any non-ASCII character, and results in a path inside your mod's " - "save folder.", - fileName, - mod->Name) - .c_str()); - return SQRESULT_ERROR; - } - try - { - // throws if file does not exist - // we don't want stuff such as "file does not exist, file is unavailable" to be lethal, so we just try/catch fs errors - g_pSquirrel<context>->pushinteger(sqvm, (int)(fs::file_size(dir / fileName) / 1024)); - } - catch (std::filesystem::filesystem_error const& ex) - { - spdlog::error("GET FILE SIZE FAILED! Is the path valid?"); - g_pSquirrel<context>->raiseerror(sqvm, ex.what()); - return SQRESULT_ERROR; - } - return SQRESULT_NOTNULL; -} - -// void NSDeleteFile(string file) -ADD_SQFUNC("void", NSDeleteFile, "string file", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - Mod* mod = g_pSquirrel<context>->getcallingmod(sqvm); - - fs::path dir = savePath / fs::path(mod->m_ModDirectory).filename(); - std::string fileName = g_pSquirrel<context>->getstring(sqvm, 1); - if (!IsPathSafe(fileName, dir)) - { - g_pSquirrel<context>->raiseerror( - sqvm, - fmt::format( - "File name invalid ({})! Make sure it does not contain any non-ASCII character, and results in a path inside your mod's " - "save folder.", - fileName, - mod->Name) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSaveFileManager->DeleteFileAsync<context>(dir / fileName); - return SQRESULT_NOTNULL; -} - -// The param is not optional because that causes issues :) -ADD_SQFUNC("array<string>", NS_InternalGetAllFiles, "string path", "", ScriptContext::CLIENT | ScriptContext::UI | ScriptContext::SERVER) -{ - // depth 1 because this should always get called from Northstar.Custom - Mod* mod = g_pSquirrel<context>->getcallingmod(sqvm, 1); - fs::path dir = savePath / fs::path(mod->m_ModDirectory).filename(); - std::string pathStr = g_pSquirrel<context>->getstring(sqvm, 1); - fs::path path = dir; - if (pathStr != "") - path = dir / pathStr; - if (!IsPathSafe(pathStr, dir)) - { - g_pSquirrel<context>->raiseerror( - sqvm, - fmt::format( - "File name invalid ({})! Make sure it does not contain any non-ASCII character, and results in a path inside your mod's " - "save folder.", - pathStr, - mod->Name) - .c_str()); - return SQRESULT_ERROR; - } - try - { - g_pSquirrel<context>->newarray(sqvm, 0); - for (const auto& entry : fs::directory_iterator(path)) - { - g_pSquirrel<context>->pushstring(sqvm, entry.path().filename().string().c_str()); - g_pSquirrel<context>->arrayappend(sqvm, -2); - } - return SQRESULT_NOTNULL; - } - catch (std::exception ex) - { - spdlog::error("DIR ITERATE FAILED! Is the path valid?"); - g_pSquirrel<context>->raiseerror(sqvm, ex.what()); - return SQRESULT_ERROR; - } -} - -ADD_SQFUNC("bool", NSIsFolder, "string path", "", ScriptContext::CLIENT | ScriptContext::UI | ScriptContext::SERVER) -{ - Mod* mod = g_pSquirrel<context>->getcallingmod(sqvm); - fs::path dir = savePath / fs::path(mod->m_ModDirectory).filename(); - std::string pathStr = g_pSquirrel<context>->getstring(sqvm, 1); - fs::path path = dir; - if (pathStr != "") - path = dir / pathStr; - if (!IsPathSafe(pathStr, dir)) - { - g_pSquirrel<context>->raiseerror( - sqvm, - fmt::format( - "File name invalid ({})! Make sure it does not contain any non-ASCII character, and results in a path inside your mod's " - "save folder.", - pathStr, - mod->Name) - .c_str()); - return SQRESULT_ERROR; - } - try - { - g_pSquirrel<context>->pushbool(sqvm, fs::is_directory(path)); - return SQRESULT_NOTNULL; - } - catch (std::exception ex) - { - spdlog::error("DIR READ FAILED! Is the path valid?"); - spdlog::info(path.string()); - g_pSquirrel<context>->raiseerror(sqvm, ex.what()); - return SQRESULT_ERROR; - } -} - -// side note, expensive. -ADD_SQFUNC("int", NSGetTotalSpaceRemaining, "", "", ScriptContext::CLIENT | ScriptContext::UI | ScriptContext::SERVER) -{ - Mod* mod = g_pSquirrel<context>->getcallingmod(sqvm); - fs::path dir = savePath / fs::path(mod->m_ModDirectory).filename(); - g_pSquirrel<context>->pushinteger(sqvm, (MAX_FOLDER_SIZE - GetSizeOfFolder(dir)) / 1024); - return SQRESULT_NOTNULL; -} - -// ok, I'm just gonna explain what the fuck is going on here because this -// is the pinnacle of my stupidity and I do not want to touch this ever -// again, yet someone will eventually have to maintain this. -template <ScriptContext context> std::string EncodeJSON(HSquirrelVM* sqvm) -{ - // new rapidjson - rapidjson_document doc; - doc.SetObject(); - - // get the SECOND param - SQTable* table = sqvm->_stackOfCurrentFunction[2]._VAL.asTable; - // take the table and copy it's contents over into the rapidjson_document - EncodeJSONTable<context>(table, &doc, doc.GetAllocator()); - - // convert JSON document to string - rapidjson::StringBuffer buffer; - rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); - doc.Accept(writer); - - // return the converted string - return buffer.GetString(); -} - -ON_DLL_LOAD("engine.dll", ModSaveFFiles_Init, (CModule module)) -{ - savePath = fs::path(GetNorthstarPrefix()) / "save_data"; - g_pSaveFileManager = new SaveFileManager; - int parm = CommandLine()->FindParm("-maxfoldersize"); - if (parm) - MAX_FOLDER_SIZE = std::stoi(CommandLine()->GetParm(parm)); -} - -int GetMaxSaveFolderSize() -{ - return MAX_FOLDER_SIZE; -} diff --git a/NorthstarDLL/mods/modsavefiles.h b/NorthstarDLL/mods/modsavefiles.h deleted file mode 100644 index f9d39723..00000000 --- a/NorthstarDLL/mods/modsavefiles.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once -int GetMaxSaveFolderSize(); -bool ContainsInvalidChars(std::string str); - -class SaveFileManager -{ -public: - template <ScriptContext context> void SaveFileAsync(fs::path file, std::string content); - template <ScriptContext context> int LoadFileAsync(fs::path file); - template <ScriptContext context> void DeleteFileAsync(fs::path file); - // Future proofed in that if we ever get multi-threaded SSDs this code will take advantage of them. - std::mutex fileMutex; - -private: - int m_iLastRequestHandle = 0; -}; |