aboutsummaryrefslogtreecommitdiff
path: root/NorthstarDLL/mods
diff options
context:
space:
mode:
Diffstat (limited to 'NorthstarDLL/mods')
-rw-r--r--NorthstarDLL/mods/autodownload/moddownloader.cpp638
-rw-r--r--NorthstarDLL/mods/autodownload/moddownloader.h151
-rw-r--r--NorthstarDLL/mods/compiled/kb_act.cpp44
-rw-r--r--NorthstarDLL/mods/compiled/modkeyvalues.cpp106
-rw-r--r--NorthstarDLL/mods/compiled/modpdef.cpp118
-rw-r--r--NorthstarDLL/mods/compiled/modscriptsrson.cpp65
-rw-r--r--NorthstarDLL/mods/modmanager.cpp1150
-rw-r--r--NorthstarDLL/mods/modmanager.h187
-rw-r--r--NorthstarDLL/mods/modsavefiles.cpp572
-rw-r--r--NorthstarDLL/mods/modsavefiles.h16
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;
-};