aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_fomod_plus/share/FOMODData
diff options
context:
space:
mode:
Diffstat (limited to 'libs/installer_fomod_plus/share/FOMODData')
-rw-r--r--libs/installer_fomod_plus/share/FOMODData/ArchiveExtractor.h160
-rw-r--r--libs/installer_fomod_plus/share/FOMODData/FomodDB.h144
-rw-r--r--libs/installer_fomod_plus/share/FOMODData/FomodDBEntry.h129
-rw-r--r--libs/installer_fomod_plus/share/FOMODData/FomodRescan.h277
-rw-r--r--libs/installer_fomod_plus/share/FOMODData/PluginReader.h119
5 files changed, 0 insertions, 829 deletions
diff --git a/libs/installer_fomod_plus/share/FOMODData/ArchiveExtractor.h b/libs/installer_fomod_plus/share/FOMODData/ArchiveExtractor.h
deleted file mode 100644
index 65cd0ae..0000000
--- a/libs/installer_fomod_plus/share/FOMODData/ArchiveExtractor.h
+++ /dev/null
@@ -1,160 +0,0 @@
-#pragma once
-
-#include "stringutil.h"
-
-#include <QDir>
-#include <QFileInfo>
-#include <QString>
-#include <QTemporaryDir>
-#include <archive.h>
-#include <filesystem>
-#include <functional>
-#include <iostream>
-#include <memory>
-#include <optional>
-
-struct ExtractionResult {
- bool success = false;
- QString moduleConfigPath;
- std::vector<QString> pluginPaths;
- QString errorMessage;
- std::unique_ptr<QTemporaryDir> tempDir; // Owns the temp directory lifetime
-};
-
-/**
- * Utility class to extract FOMOD data from archives without being in an installer context.
- * Used by the Patch Wizard's rescan functionality.
- */
-class ArchiveExtractor {
-public:
- using ProgressCallback = std::function<void(const QString& fileName)>;
-
- /**
- * Extract ModuleConfig.xml and plugin files from an archive.
- * @param archiveFilePath Full path to the archive file
- * @param progressCallback Optional callback for progress updates
- * @return ExtractionResult containing paths to extracted files
- */
- static ExtractionResult extractFomodData(
- const QString& archiveFilePath,
- const ProgressCallback& progressCallback = nullptr)
- {
- ExtractionResult result;
- result.tempDir = std::make_unique<QTemporaryDir>();
-
- if (!result.tempDir->isValid()) {
- result.errorMessage = "Failed to create temporary directory";
- return result;
- }
-
- const auto archive = CreateArchive();
- if (!archive->isValid()) {
- result.errorMessage = "Failed to load archive module";
- return result;
- }
-
- if (!archive->open(archiveFilePath.toStdWString(), nullptr)) {
- result.errorMessage = QString("Failed to open archive (error %1)")
- .arg(static_cast<int>(archive->getLastError()));
- return result;
- }
-
- // Get file list and mark files for extraction
- const auto& fileList = archive->getFileList();
- QString moduleConfigInArchive;
-
- for (auto* fileData : fileList) {
- const auto entryPath = QString::fromStdWString(fileData->getArchiveFilePath());
-
- // Check for ModuleConfig.xml
- if (entryPath.toLower().endsWith("fomod/moduleconfig.xml") ||
- entryPath.toLower().endsWith("fomod\\moduleconfig.xml")) {
- moduleConfigInArchive = entryPath;
- // Set output path relative to output directory for extract()
- fileData->addOutputFilePath(L"ModuleConfig.xml");
- result.moduleConfigPath = result.tempDir->filePath("ModuleConfig.xml");
- }
- // Check for plugin files
- else if (isPluginFile(entryPath)) {
- const auto fileName = QFileInfo(entryPath).fileName();
- const auto relativePath = QString("plugins/") + fileName;
- fileData->addOutputFilePath(relativePath.toStdWString());
- result.pluginPaths.push_back(result.tempDir->filePath(relativePath));
- }
- }
-
- if (moduleConfigInArchive.isEmpty()) {
- result.errorMessage = "No ModuleConfig.xml found in archive";
- return result;
- }
-
- // Create plugins subdirectory
- QDir(result.tempDir->path()).mkpath("plugins");
-
- // Extract the files
- Archive::FileChangeCallback fileChangeCallback = [&progressCallback](
- Archive::FileChangeType, const std::wstring& fileName) {
- if (progressCallback) {
- progressCallback(QString::fromStdWString(fileName));
- }
- };
-
- Archive::ErrorCallback errorCallback = [&result](const std::wstring& error) {
- result.errorMessage = QString::fromStdWString(error);
- };
-
- const bool extractSuccess = archive->extract(
- result.tempDir->path().toStdWString(),
- Archive::ProgressCallback{}, // progress callback
- fileChangeCallback,
- errorCallback
- );
-
- if (!extractSuccess) {
- if (result.errorMessage.isEmpty()) {
- result.errorMessage = "Extraction failed";
- }
- return result;
- }
-
- // Verify ModuleConfig.xml was extracted
- if (!QFile::exists(result.moduleConfigPath)) {
- result.errorMessage = "ModuleConfig.xml extraction failed";
- return result;
- }
-
- // Filter to only existing plugin files
- std::vector<QString> existingPlugins;
- for (const auto& path : result.pluginPaths) {
- if (QFile::exists(path)) {
- existingPlugins.push_back(path);
- }
- }
- result.pluginPaths = std::move(existingPlugins);
-
- result.success = true;
- return result;
- }
-
- /**
- * Check if an archive contains FOMOD files without extracting.
- * @param archiveFilePath Full path to the archive file
- * @return true if the archive contains fomod/ModuleConfig.xml
- */
- static bool hasFomodFiles(const QString& archiveFilePath)
- {
- const auto archive = CreateArchive();
- if (!archive->isValid() || !archive->open(archiveFilePath.toStdWString(), nullptr)) {
- return false;
- }
-
- for (const auto* fileData : archive->getFileList()) {
- const auto path = QString::fromStdWString(fileData->getArchiveFilePath());
- if (path.toLower().endsWith("fomod/moduleconfig.xml") ||
- path.toLower().endsWith("fomod\\moduleconfig.xml")) {
- return true;
- }
- }
- return false;
- }
-};
diff --git a/libs/installer_fomod_plus/share/FOMODData/FomodDB.h b/libs/installer_fomod_plus/share/FOMODData/FomodDB.h
deleted file mode 100644
index f5ea6f5..0000000
--- a/libs/installer_fomod_plus/share/FOMODData/FomodDB.h
+++ /dev/null
@@ -1,144 +0,0 @@
-#pragma once
-
-#include <fstream>
-#include <stringutil.h>
-
-#include "FomodDBEntry.h"
-
-#include <xml/ModuleConfiguration.h>
-
-#include "PluginReader.h"
-
-using FOMODDBEntries = std::vector<std::shared_ptr<FomodDbEntry> >;
-
-constexpr std::string FOMOD_DB_FILE = "fomod.db";
-
-class FomodDB {
-public:
- /**
- *
- * @param moBasePath The organizer instance's basePath() value
- * @param dbName The filename of the db. Only settable for testing purposes.
- */
- explicit FomodDB(const std::string &moBasePath, const std::string &dbName = FOMOD_DB_FILE) {
- dbFilePath = (std::filesystem::path(moBasePath) / dbName).string();
- loadFromFile();
- }
-
- // TODO: Also pull from non install steps (requiredInstallFiles or whatever, and optional);
- static std::shared_ptr<FomodDbEntry> getEntryFromFomod(ModuleConfiguration *fomod, std::vector<QString> pluginPaths,
- int modId) {
- std::vector<FomodOption> options;
- for (const auto &installStep: fomod->installSteps.installSteps) {
- for (const auto &group: installStep.optionalFileGroups.groups) {
- for (const auto &plugin: group.plugins.plugins) {
- // Create a DB entry for the given plugin if it has an ESP
- for (auto file: plugin.files.files) {
- if (file.isFolder || !isPluginFile(file.source)) {
- continue;
- }
-
- // Find the path in pluginPaths that ends with this path
- // PluginPaths is gathered from the archive contents.
- auto it = std::ranges::find_if(pluginPaths, [&file](const QString &path) {
- return path.endsWith(file.source.c_str());
- });
- if (it == pluginPaths.end()) {
- continue;
- }
- const auto &pluginPath = *it;
- const auto masters = PluginReader::readMasters(pluginPath.toStdString(), true);
- options.emplace_back(
- plugin.name,
- file.source,
- masters,
- installStep.name,
- group.name
- );
- }
- }
- }
- }
- return std::make_shared<FomodDbEntry>(modId, fomod->moduleName, options);
- }
-
- void addEntry(const std::shared_ptr<FomodDbEntry> &entry, const bool upsert = true) {
- // TODO: Test this upsert.
- if (upsert) {
- const auto it = std::ranges::find_if(entries, [&entry](const std::shared_ptr<FomodDbEntry> &e) {
- return e->getModId() == entry->getModId();
- });
- if (it != entries.end()) {
- *it = entry;
- } else {
- entries.emplace_back(entry);
- }
- } else {
- entries.emplace_back(entry);
- }
- }
-
- [[nodiscard]] const FOMODDBEntries &getEntries() { return entries; }
-
- void saveToFile() const {
- try {
- std::ofstream file(dbFilePath);
- if (!file.is_open()) {
- return;
- }
-
- file << toJson().dump(2); // Pretty-print with 2-space indentation
- file.close();
- } catch ([[maybe_unused]] const std::exception &e) {
- // Handle saving errors
- }
- }
-
- [[nodiscard]] nlohmann::json toJson() const {
- nlohmann::json jsonArray = nlohmann::json::array();
-
- for (const auto &entry: entries) {
- jsonArray.push_back(entry->toJson());
- }
-
- return jsonArray;
- }
-
-private:
- FOMODDBEntries entries;
- std::string dbFilePath;
-
- void loadFromFile() {
- entries.clear();
-
- // Create empty file if it doesn't exist
- if (!std::filesystem::exists(dbFilePath)) {
- std::ofstream file(dbFilePath);
- file << "[]"; // Empty JSON array
- file.close();
- return; // No entries to load
- }
-
- try {
- // Read and parse the JSON file
- std::ifstream file(dbFilePath);
- if (!file.is_open()) {
- return;
- }
-
- nlohmann::json jsonArray = nlohmann::json::parse(file);
-
- // Ensure it's an array
- if (!jsonArray.is_array()) {
- return;
- }
-
- // Process each entry in the array
- for (const auto &entryJson: jsonArray) {
- entries.push_back(std::make_unique<FomodDbEntry>(entryJson));
- }
- } catch ([[maybe_unused]] const std::exception &e) {
- // Handle parsing errors (leave entries empty)
- }
- }
-};
diff --git a/libs/installer_fomod_plus/share/FOMODData/FomodDBEntry.h b/libs/installer_fomod_plus/share/FOMODData/FomodDBEntry.h
deleted file mode 100644
index 5d39f95..0000000
--- a/libs/installer_fomod_plus/share/FOMODData/FomodDBEntry.h
+++ /dev/null
@@ -1,129 +0,0 @@
-#pragma once
-
-#include <string>
-#include <utility>
-#include <vector>
-#include <nlohmann/json.hpp>
-
-/*
-The following JSON will be part of an array of similar objects in the root level "JSON DB" for FOMOD Plus.
-It contains information to resolve the identity of a given mod (names can change), and then the options
-in the FOMOD with their respective masters.
-{
- modId: 12345,
- displayName: "Lux (Patch Hub)",
- options: [
- {
- "name" "JK's The Hag's Cure",
- "fileName": "Lux - JK's The Hag's Cure patch.esp",
- "masters": [
- "Skyrim.esm",
- "JK's The Hag's Cure.esp",
- "Lux - Resources.esp",
- "Lux.esp"
- ],
- "step": "Page One",
- "group": "Group One",
- "selectionState": "Available"
- }
- ]
-}
-*/
-
-enum class SelectionState {
- Unknown, // Not yet matched to choices
- Selected, // User selected this option
- Deselected, // User manually deselected
- Available // Present but user didn't interact (or choices not recorded)
-};
-
-inline std::string selectionStateToString(SelectionState state) {
- switch (state) {
- case SelectionState::Unknown: return "Unknown";
- case SelectionState::Selected: return "Selected";
- case SelectionState::Deselected: return "Deselected";
- case SelectionState::Available: return "Available";
- default: return "Unknown";
- }
-}
-
-inline SelectionState stringToSelectionState(const std::string& str) {
- if (str == "Selected") return SelectionState::Selected;
- if (str == "Deselected") return SelectionState::Deselected;
- if (str == "Available") return SelectionState::Available;
- return SelectionState::Unknown;
-}
-
-struct FomodOption {
- std::string name;
- std::string fileName;
- std::vector<std::string> masters;
- std::string step;
- std::string group;
- SelectionState selectionState = SelectionState::Unknown;
-
- FomodOption(std::string n, std::string fn, std::vector<std::string> m, std::string s, std::string g,
- SelectionState state = SelectionState::Unknown)
- : name(std::move(n)), fileName(std::move(fn)), masters(std::move(m)), step(std::move(s)), group(std::move(g)),
- selectionState(state) {}
-};
-
-class FomodDbEntry {
-public:
- explicit FomodDbEntry(nlohmann::json json) {
- modId = json["modId"];
- displayName = json["displayName"];
- for (auto &option: json["options"]) {
- // create an option from this object
- SelectionState state = SelectionState::Unknown;
- if (option.contains("selectionState")) {
- state = stringToSelectionState(option["selectionState"]);
- }
- FomodOption fomodOption(
- option["name"],
- option["fileName"],
- option["masters"],
- option["step"],
- option["group"],
- state
- );
- options.push_back(fomodOption);
- }
- }
-
- explicit FomodDbEntry(const int modId, std::string displayName, const std::vector<FomodOption> &options)
- : modId(modId), displayName(std::move(displayName)), options(options) {
- }
-
-
- [[nodiscard]] int getModId() const { return modId; }
- [[nodiscard]] std::string getDisplayName() const { return displayName; }
- [[nodiscard]] const std::vector<FomodOption>& getOptions() const { return options; }
- [[nodiscard]] std::vector<FomodOption>& getOptionsMutable() { return options; }
-
- [[nodiscard]] nlohmann::json toJson() const {
- nlohmann::json result;
- result["modId"] = modId;
- result["displayName"] = displayName;
-
- nlohmann::json optionsArray = nlohmann::json::array();
- for (const auto &[name, fileName, masters, step, group, selectionState]: options) {
- nlohmann::json optionJson;
- optionJson["name"] = name;
- optionJson["fileName"] = fileName;
- optionJson["masters"] = masters;
- optionJson["step"] = step;
- optionJson["group"] = group;
- optionJson["selectionState"] = selectionStateToString(selectionState);
- optionsArray.push_back(optionJson);
- }
-
- result["options"] = optionsArray;
- return result;
- }
-
-private:
- int modId;
- std::string displayName;
- std::vector<FomodOption> options;
-};
diff --git a/libs/installer_fomod_plus/share/FOMODData/FomodRescan.h b/libs/installer_fomod_plus/share/FOMODData/FomodRescan.h
deleted file mode 100644
index c7d53a2..0000000
--- a/libs/installer_fomod_plus/share/FOMODData/FomodRescan.h
+++ /dev/null
@@ -1,277 +0,0 @@
-#pragma once
-
-#include "ArchiveExtractor.h"
-#include "FomodDB.h"
-#include "stringutil.h"
-#include "xml/ModuleConfiguration.h"
-
-#include <QDir>
-#include <QString>
-#include <functional>
-#include <imodinterface.h>
-#include <imoinfo.h>
-#include <nlohmann/json.hpp>
-
-struct RescanResult {
- int totalModsProcessed = 0;
- int successfullyScanned = 0;
- int missingArchives = 0;
- int parseErrors = 0;
- std::vector<std::string> failedMods;
-};
-
-/**
- * Orchestrates rescanning of all mods with stored FOMOD choices to repopulate the database.
- * Used when fomod.db is missing or needs to be regenerated from existing installations.
- */
-class FomodRescan {
-public:
- using ProgressCallback = std::function<void(int current, int total, const QString& modName)>;
-
- FomodRescan(MOBase::IOrganizer* organizer, FomodDB* db)
- : mOrganizer(organizer), mFomodDb(db) {}
-
- /**
- * Scan all mods that have stored FOMOD Plus choices and repopulate the database.
- * @param progressCallback Optional callback for progress updates
- * @return RescanResult with statistics about the scan
- */
- RescanResult scanAllModsWithChoices(const ProgressCallback& progressCallback = nullptr)
- {
- RescanResult result;
-
- const auto modList = mOrganizer->modList();
- if (!modList) {
- return result;
- }
-
- // First pass: gather all mods with stored choices
- std::vector<MOBase::IModInterface*> modsWithChoices;
- for (const auto& modName : modList->allMods()) {
- auto* mod = modList->getMod(modName);
- if (mod && hasStoredChoices(mod)) {
- modsWithChoices.push_back(mod);
- }
- }
-
- result.totalModsProcessed = static_cast<int>(modsWithChoices.size());
-
- // Second pass: process each mod
- int current = 0;
- for (auto* mod : modsWithChoices) {
- current++;
- if (progressCallback) {
- progressCallback(current, result.totalModsProcessed, mod->name());
- }
-
- const auto scanResult = processMod(mod);
- switch (scanResult) {
- case ScanOutcome::Success:
- result.successfullyScanned++;
- break;
- case ScanOutcome::MissingArchive:
- result.missingArchives++;
- result.failedMods.push_back(mod->name().toStdString() + " (missing archive)");
- break;
- case ScanOutcome::ParseError:
- result.parseErrors++;
- result.failedMods.push_back(mod->name().toStdString() + " (parse error)");
- break;
- case ScanOutcome::NoFomod:
- result.failedMods.push_back(mod->name().toStdString() + " (no FOMOD)");
- break;
- }
- }
-
- // Save the database
- mFomodDb->saveToFile();
-
- return result;
- }
-
-private:
- MOBase::IOrganizer* mOrganizer;
- FomodDB* mFomodDb;
-
- enum class ScanOutcome {
- Success,
- MissingArchive,
- ParseError,
- NoFomod
- };
-
- /**
- * Check if a mod has stored FOMOD Plus choices (non-zero pluginSetting).
- */
- bool hasStoredChoices(MOBase::IModInterface* mod) const
- {
- const auto fomodData = mod->pluginSetting(
- StringConstants::Plugin::NAME.data(), "fomod", 0);
-
- if (!fomodData.isValid() || fomodData.isNull()) {
- return false;
- }
-
- // Check if it's actually valid JSON with steps
- try {
- const auto json = nlohmann::json::parse(fomodData.toString().toStdString());
- return json.contains("steps") && json["steps"].is_array() && !json["steps"].empty();
- } catch (...) {
- return false;
- }
- }
-
- /**
- * Get the stored choices JSON from a mod's pluginSetting.
- */
- nlohmann::json getStoredChoices(MOBase::IModInterface* mod) const
- {
- const auto fomodData = mod->pluginSetting(
- StringConstants::Plugin::NAME.data(), "fomod", 0);
-
- try {
- return nlohmann::json::parse(fomodData.toString().toStdString());
- } catch (...) {
- return nlohmann::json();
- }
- }
-
- /**
- * Process a single mod: extract archive, parse FOMOD, create DB entry with selection states.
- */
- ScanOutcome processMod(MOBase::IModInterface* mod)
- {
- // Get the archive path
- const auto installationFile = mod->installationFile();
- if (installationFile.isEmpty()) {
- return ScanOutcome::MissingArchive;
- }
-
- const auto downloadsPath = mOrganizer->downloadsPath();
- const auto archivePath = QDir(installationFile).isAbsolute()
- ? installationFile
- : downloadsPath + "/" + installationFile;
-
- if (!QFile::exists(archivePath)) {
- return ScanOutcome::MissingArchive;
- }
-
- // Extract FOMOD data from archive
- auto extractionResult = ArchiveExtractor::extractFomodData(archivePath);
- if (!extractionResult.success) {
- return ScanOutcome::ParseError;
- }
-
- // Parse ModuleConfiguration
- auto moduleConfig = std::make_unique<ModuleConfiguration>();
- try {
- if (!moduleConfig->deserialize(extractionResult.moduleConfigPath)) {
- return ScanOutcome::ParseError;
- }
- } catch (...) {
- return ScanOutcome::ParseError;
- }
-
- // Get the mod's Nexus ID
- const int modId = mod->nexusId();
-
- // Create FomodDbEntry using existing logic
- auto entry = FomodDB::getEntryFromFomod(
- moduleConfig.get(),
- extractionResult.pluginPaths,
- modId
- );
-
- if (!entry || entry->getOptions().empty()) {
- return ScanOutcome::NoFomod;
- }
-
- // Apply selection states from stored choices
- const auto choices = getStoredChoices(mod);
- applySelectionsToEntry(*entry, choices);
-
- // Add to database (upsert)
- mFomodDb->addEntry(entry, true);
-
- return ScanOutcome::Success;
- }
-
- /**
- * Apply user selection states to a FomodDbEntry based on stored choices JSON.
- *
- * Choices JSON format:
- * {
- * "steps": [{
- * "name": "Step Name",
- * "groups": [{
- * "name": "Group Name",
- * "plugins": ["Selected Plugin 1"],
- * "deselected": ["Manually Deselected Plugin"]
- * }]
- * }]
- * }
- */
- void applySelectionsToEntry(FomodDbEntry& entry, const nlohmann::json& choices)
- {
- if (!choices.contains("steps") || !choices["steps"].is_array()) {
- // No choices data - mark all as Available
- for (auto& option : entry.getOptionsMutable()) {
- option.selectionState = SelectionState::Available;
- }
- return;
- }
-
- // Build a lookup map for quick matching: stepName/groupName/pluginName -> state
- struct PluginState {
- bool selected = false;
- bool deselected = false;
- };
- std::map<std::string, PluginState> stateMap;
-
- for (const auto& step : choices["steps"]) {
- if (!step.contains("name") || !step.contains("groups")) continue;
- const std::string stepName = step["name"];
-
- for (const auto& group : step["groups"]) {
- if (!group.contains("name")) continue;
- const std::string groupName = group["name"];
-
- // Process selected plugins
- if (group.contains("plugins") && group["plugins"].is_array()) {
- for (const auto& plugin : group["plugins"]) {
- const std::string pluginName = plugin;
- const auto key = stepName + "/" + groupName + "/" + pluginName;
- stateMap[key].selected = true;
- }
- }
-
- // Process deselected plugins
- if (group.contains("deselected") && group["deselected"].is_array()) {
- for (const auto& plugin : group["deselected"]) {
- const std::string pluginName = plugin;
- const auto key = stepName + "/" + groupName + "/" + pluginName;
- stateMap[key].deselected = true;
- }
- }
- }
- }
-
- // Apply states to options
- for (auto& option : entry.getOptionsMutable()) {
- const auto key = option.step + "/" + option.group + "/" + option.name;
-
- if (auto it = stateMap.find(key); it != stateMap.end()) {
- if (it->second.selected) {
- option.selectionState = SelectionState::Selected;
- } else if (it->second.deselected) {
- option.selectionState = SelectionState::Deselected;
- } else {
- option.selectionState = SelectionState::Available;
- }
- } else {
- // Plugin not found in choices - mark as Available
- option.selectionState = SelectionState::Available;
- }
- }
- }
-};
diff --git a/libs/installer_fomod_plus/share/FOMODData/PluginReader.h b/libs/installer_fomod_plus/share/FOMODData/PluginReader.h
deleted file mode 100644
index dade7e1..0000000
--- a/libs/installer_fomod_plus/share/FOMODData/PluginReader.h
+++ /dev/null
@@ -1,119 +0,0 @@
-#pragma once
-
-#include <fstream>
-#include <string>
-#include <vector>
-#include <cstdint>
-#include <unordered_set>
-
-static const std::unordered_set<std::string> VANILLA_MASTERS = {
- "Skyrim.esm",
- "Update.esm",
- "Dawnguard.esm",
- "HearthFires.esm",
- "Dragonborn.esm"
-};
-
-class PluginReader {
-public:
-
- /**
- * Reads the master files from a Bethesda plugin file (ESP/ESM/ESL)
- * @param filePath Path to the plugin file
- * @param trimVanilla Exclude the vanilla game masters or not. Mostly to save DB space.
- * @return Vector of master filenames
- */
- static std::vector<std::string> readMasters(const std::string& filePath, const bool trimVanilla = false)
- {
- std::vector<std::string> masters;
- std::ifstream file(filePath, std::ios::binary);
-
- if (!file) {
- return masters;
- }
-
- // Check TES4 record signature
- char signature[4];
- file.read(signature, 4);
- if (strncmp(signature, "TES4", 4) != 0) {
- return masters;
- }
-
- // Read record size
- uint32_t recordSize;
- file.read(reinterpret_cast<char*>(&recordSize), 4);
-
- constexpr uint32_t skipSize = sizeof(uint32_t) // flags
- + sizeof(uint32_t) // formId
- + sizeof(uint16_t) // timestamp
- + sizeof(uint16_t) // version control
- + sizeof(uint16_t) // internal version
- + sizeof(uint16_t); // unknown
-
- // Skip header flags, formID, etc. (total 8 bytes)
- file.seekg(skipSize, std::ios::cur);
-
- // Calculate where the TES4 record ends
- std::streampos recordEnd = file.tellg() + static_cast<std::streampos>(recordSize);
-
- // Read subrecords until we reach the end of the TES4 record
- while (file && file.tellg() < recordEnd) {
- char subRecordType[4];
- uint16_t subRecordSize;
-
- // Read subrecord type and size
- file.read(subRecordType, 4);
- file.read(reinterpret_cast<char*>(&subRecordSize), 2);
-
- if (strncmp(subRecordType, "MAST", 4) == 0) {
- // Read master filename (null-terminated string)
- std::string masterName;
- masterName.resize(subRecordSize);
- file.read(masterName.data(), subRecordSize);
-
- // Remove null terminator if present
- if (!masterName.empty() && masterName.back() == '\0') {
- masterName.pop_back();
- }
-
- // Only add if it's not a vanilla master or if we're not trimming
- if (!trimVanilla || !VANILLA_MASTERS.contains(masterName)) {
- masters.push_back(masterName);
- }
-
- // Each MAST is followed by a DATA subrecord
- char dataType[4];
- uint16_t dataSize;
- file.read(dataType, 4);
- file.read(reinterpret_cast<char*>(&dataSize), 2);
-
- // Skip DATA content (usually an 8-byte value)
- file.seekg(dataSize, std::ios::cur);
- } else {
- // Skip other subrecord types
- file.seekg(subRecordSize, std::ios::cur);
- }
- }
-
- return masters;
- }
-
- /**
- * Checks if a file is a valid Bethesda plugin (ESP/ESM/ESL)
- * @param filePath Path to the file
- * @return True if the file is a valid plugin
- */
- static bool isValidPlugin(const std::string& filePath)
- {
- std::ifstream file(filePath, std::ios::binary);
-
- if (!file) {
- return false;
- }
-
- char signature[4];
- file.read(signature, 4);
-
- return strncmp(signature, "TES4", 4) == 0;
- }
-}; \ No newline at end of file