aboutsummaryrefslogtreecommitdiff
path: root/libs/esptk/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/esptk/src
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/esptk/src')
-rw-r--r--libs/esptk/src/CMakeLists.txt55
-rw-r--r--libs/esptk/src/espfile.cpp188
-rw-r--r--libs/esptk/src/record.cpp40
-rw-r--r--libs/esptk/src/subrecord.cpp59
-rw-r--r--libs/esptk/src/tes3record.cpp16
-rw-r--r--libs/esptk/src/tes3subrecord.cpp52
6 files changed, 410 insertions, 0 deletions
diff --git a/libs/esptk/src/CMakeLists.txt b/libs/esptk/src/CMakeLists.txt
new file mode 100644
index 0000000..8f2b2d3
--- /dev/null
+++ b/libs/esptk/src/CMakeLists.txt
@@ -0,0 +1,55 @@
+cmake_minimum_required(VERSION 3.16)
+
+add_library(esptk STATIC)
+
+target_sources(esptk
+ PRIVATE
+ espfile.cpp
+ record.cpp
+ subrecord.cpp
+ tes3record.cpp
+ tes3subrecord.cpp
+ PUBLIC
+ FILE_SET HEADERS
+ BASE_DIRS ${CMAKE_CURRENT_LIST_DIR}/../include
+ FILES
+ ${CMAKE_CURRENT_LIST_DIR}/../include/esptk/espexceptions.h
+ ${CMAKE_CURRENT_LIST_DIR}/../include/esptk/espfile.h
+ ${CMAKE_CURRENT_LIST_DIR}/../include/esptk/esptypes.h
+ ${CMAKE_CURRENT_LIST_DIR}/../include/esptk/record.h
+ ${CMAKE_CURRENT_LIST_DIR}/../include/esptk/subrecord.h
+ ${CMAKE_CURRENT_LIST_DIR}/../include/esptk/tes3record.h
+ ${CMAKE_CURRENT_LIST_DIR}/../include/esptk/tes3subrecord.h
+)
+
+
+set_target_properties(esptk PROPERTIES CXX_STANDARD 20)
+
+# for building
+target_include_directories(esptk PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../include/esptk)
+
+if (MSVC)
+ target_compile_options(esptk
+ PRIVATE
+ "/MP"
+ "/Wall"
+ "/external:anglebrackets"
+ "/external:W0"
+ )
+ target_link_options(esptk
+ PRIVATE
+ $<$<CONFIG:RelWithDebInfo>:/LTCG /INCREMENTAL:NO /OPT:REF /OPT:ICF>
+ )
+
+ set_target_properties(esptk PROPERTIES VS_STARTUP_PROJECT esptk)
+endif()
+
+add_library(mo2::esptk ALIAS esptk)
+
+# isntall
+install(TARGETS esptk EXPORT esptkTargets FILE_SET HEADERS)
+install(EXPORT esptkTargets
+ FILE mo2-esptk-targets.cmake
+ NAMESPACE mo2::
+ DESTINATION lib/cmake/mo2-esptk
+)
diff --git a/libs/esptk/src/espfile.cpp b/libs/esptk/src/espfile.cpp
new file mode 100644
index 0000000..b97d222
--- /dev/null
+++ b/libs/esptk/src/espfile.cpp
@@ -0,0 +1,188 @@
+#include "espfile.h"
+#include "espexceptions.h"
+#include "subrecord.h"
+#include "tes3subrecord.h"
+#include <bitset>
+#include <cstring>
+#include <sstream>
+
+ESP::File::File(const std::string& fileName)
+{
+ m_File.open(fileName, std::fstream::in | std::fstream::binary);
+ init();
+}
+
+ESP::File::File(const std::wstring& fileName)
+{
+#ifdef _WIN32
+ m_File.open(fileName, std::fstream::in | std::fstream::binary);
+#else
+ // Linux: convert wstring to string (UTF-8)
+ std::string narrowName(fileName.begin(), fileName.end());
+ m_File.open(narrowName, std::fstream::in | std::fstream::binary);
+#endif
+ init();
+}
+
+class membuf : public std::basic_streambuf<char>
+{
+public:
+ membuf(const char* start, size_t size)
+ {
+ // baad me! this is intended for an istream only so we're not modifying
+ char* startMod = const_cast<char*>(start);
+ setg(startMod, startMod, startMod + size);
+ }
+};
+
+void ESP::File::init()
+{
+ if (!m_File.is_open()) {
+ throw ESP::InvalidFileException("file not found");
+ }
+ m_File.exceptions(std::ios_base::badbit);
+
+ uint8_t type[4];
+ if (!m_File.read(reinterpret_cast<char*>(type), 4)) {
+ throw ESP::InvalidFileException("file incomplete");
+ }
+ if (memcmp(type, "TES3", 4) == 0) {
+ ESP::TES3Record rec;
+ rec.readFrom(m_File);
+
+ while (!m_File.eof() && !m_File.fail()) {
+ ESP::TES3SubRecord subRec;
+ bool success = subRec.readFrom(m_File);
+ int headerSize = sizeof(m_TES3Header);
+ if (success) {
+ if (subRec.type() != TES3SubRecord::TYPE_UNKNOWN) {
+ switch (subRec.type()) {
+ case TES3SubRecord::TYPE_HEDR:
+ if (subRec.data().size() != sizeof(m_TES3Header)) {
+ printf("invalid header size\n");
+ m_Header.version = 0.0f;
+ m_Header.numRecords = 1; // prevent this esp appear like a dummy
+ } else {
+ memcpy(&m_TES3Header, &subRec.data()[0], sizeof(m_TES3Header));
+ }
+ m_Header.version = m_TES3Header.version;
+ m_Header.numRecords = m_TES3Header.numRecords;
+ m_Author = reinterpret_cast<const char*>(m_TES3Header.author);
+ m_Description = reinterpret_cast<const char*>(m_TES3Header.description);
+ break;
+ case TES3SubRecord::TYPE_MAST:
+ if (subRec.data().size() > 0)
+ m_Masters.insert(reinterpret_cast<const char*>(&subRec.data()[0]));
+ break;
+ }
+ }
+ }
+ }
+ } else if (memcmp(type, "TES4", 4) == 0) {
+ m_File.seekg(0);
+
+ m_MainRecord = readRecord();
+
+ const std::vector<uint8_t>& data = m_MainRecord.data();
+ if (data.empty()) {
+ throw ESP::InvalidRecordException("record has no data");
+ }
+ membuf buf(reinterpret_cast<const char*>(data.data()), data.size());
+
+ std::istream stream(&buf);
+ while (!stream.eof() && !stream.fail()) {
+ SubRecord rec;
+ bool success = rec.readFrom(stream);
+ if (success) {
+ if (rec.type() != SubRecord::TYPE_UNKNOWN) {
+ switch (rec.type()) {
+ case SubRecord::TYPE_HEDR:
+ onHEDR(rec);
+ break;
+ case SubRecord::TYPE_MAST:
+ onMAST(rec);
+ break;
+ case SubRecord::TYPE_CNAM:
+ onCNAM(rec);
+ break;
+ case SubRecord::TYPE_SNAM:
+ onSNAM(rec);
+ break;
+ }
+ }
+ }
+ }
+ } else {
+ throw ESP::InvalidFileException("invalid file type");
+ }
+}
+
+void ESP::File::onHEDR(const SubRecord& rec)
+{
+ if (rec.data().size() != sizeof(m_Header)) {
+ printf("invalid header size\n");
+ m_Header.version = 0.0f;
+ m_Header.numRecords = 1; // prevent this esp appear like a dummy
+ } else {
+ memcpy(&m_Header, &rec.data()[0], sizeof(m_Header));
+ }
+}
+
+void ESP::File::onMAST(const SubRecord& rec)
+{
+ if (rec.data().size() > 0)
+ m_Masters.insert(reinterpret_cast<const char*>(&rec.data()[0]));
+}
+
+void ESP::File::onCNAM(const SubRecord& rec)
+{
+ if (rec.data().size() > 0)
+ m_Author = reinterpret_cast<const char*>(&rec.data()[0]);
+}
+
+void ESP::File::onSNAM(const SubRecord& rec)
+{
+ if (rec.data().size() > 0)
+ m_Description = reinterpret_cast<const char*>(&rec.data()[0]);
+}
+
+ESP::Record ESP::File::readRecord()
+{
+ ESP::Record rec;
+ rec.readFrom(m_File);
+ return rec;
+}
+
+bool ESP::File::isMaster() const
+{
+ return m_MainRecord.flagSet(Record::FLAG_MASTER);
+}
+
+bool ESP::File::isLight(bool overlaySupport) const
+{
+ if (overlaySupport) {
+ return m_MainRecord.flagSet(Record::FLAG_LIGHT_ALTERNATE);
+ } else {
+ return m_MainRecord.flagSet(Record::FLAG_LIGHT);
+ }
+}
+
+bool ESP::File::isMedium() const
+{
+ return m_MainRecord.flagSet(Record::FLAG_MEDIUM);
+}
+
+bool ESP::File::isOverlay() const
+{
+ return m_MainRecord.flagSet(Record::FLAG_OVERLAY);
+}
+
+bool ESP::File::isBlueprint() const
+{
+ return m_MainRecord.flagSet(Record::FLAG_BLUEPRINT);
+}
+
+bool ESP::File::isDummy() const
+{
+ return m_Header.numRecords == 0;
+}
diff --git a/libs/esptk/src/record.cpp b/libs/esptk/src/record.cpp
new file mode 100644
index 0000000..7c66dc4
--- /dev/null
+++ b/libs/esptk/src/record.cpp
@@ -0,0 +1,40 @@
+#include "record.h"
+#include "espexceptions.h"
+#include <cstring>
+
+ESP::Record::Record() : m_Header(), m_FormVersion(), m_Data(), m_OblivionStyle(false) {}
+
+bool ESP::Record::flagSet(ESP::Record::EFlag flag) const
+{
+ return (m_Header.flags & flag) != 0;
+}
+
+bool ESP::Record::readFrom(std::istream& stream)
+{
+ if (!stream.read(reinterpret_cast<char*>(&m_Header), sizeof(Header))) {
+ if (stream.gcount() == 0) {
+ return false;
+ } else {
+ throw ESP::InvalidRecordException("record incomplete");
+ }
+ }
+
+ char buf[4];
+ stream.read(buf, 4);
+ if (memcmp(buf, "HEDR", 4) == 0) {
+ m_OblivionStyle = true;
+ stream.seekg(-4, std::istream::cur);
+ // Oblivion-style plugins don't have a form version
+ } else {
+ memcpy(&m_FormVersion, buf, sizeof(uint16_t));
+ }
+
+ m_Data.resize(m_Header.dataSize);
+ if (m_Header.dataSize > 0) {
+ stream.read(reinterpret_cast<char*>(m_Data.data()), m_Header.dataSize);
+ }
+ if (!stream) {
+ throw ESP::InvalidRecordException("record incomplete");
+ }
+ return true;
+}
diff --git a/libs/esptk/src/subrecord.cpp b/libs/esptk/src/subrecord.cpp
new file mode 100644
index 0000000..cb3890a
--- /dev/null
+++ b/libs/esptk/src/subrecord.cpp
@@ -0,0 +1,59 @@
+#include "subrecord.h"
+#include "espexceptions.h"
+#include "esptypes.h"
+#include <cstdint>
+#include <cstring>
+#include <string>
+#include <unordered_map>
+
+ESP::SubRecord::SubRecord() : m_Type(TYPE_UNKNOWN), m_Data() {}
+
+bool ESP::SubRecord::readFrom(std::istream& stream, uint32_t sizeOverride)
+{
+ static std::unordered_map<std::string, EType> s_TypeMap{{"HEDR", TYPE_HEDR},
+ {"CNAM", TYPE_CNAM},
+ {"MAST", TYPE_MAST},
+ {"ONAM", TYPE_ONAM},
+ {"SNAM", TYPE_SNAM}};
+
+ char typeString[5];
+ if (!stream.read(typeString, 4)) {
+ if (stream.gcount() == 0) {
+ return false;
+ } else {
+ throw ESP::InvalidRecordException("sub-record incomplete (unknown type)");
+ }
+ }
+ if (stream.gcount() != 4) {
+ throw ESP::InvalidRecordException(
+ std::string("sub-record type incomplete (invalid type ") + typeString + ")");
+ }
+ typeString[4] = '\0'; // not sure if this is required, shouldn't be
+ auto iter = s_TypeMap.find(std::string(typeString));
+ if (iter != s_TypeMap.end()) {
+ m_Type = iter->second;
+ } else if (strncmp(typeString, "XXXX", 4) == 0) {
+ if (readType<uint16_t>(stream) != 4) {
+ throw ESP::InvalidRecordException(
+ "XXXX record is supposed to be 4 bytes in size");
+ }
+ return readFrom(stream, readType<uint32_t>(stream));
+ } else {
+ m_Type = TYPE_UNKNOWN;
+ }
+
+ uint32_t dataSize = readType<uint16_t>(stream);
+ if (sizeOverride != 0UL) {
+ dataSize = sizeOverride;
+ }
+ m_Data.resize(dataSize);
+
+ if (dataSize > 0) {
+ stream.read(reinterpret_cast<char*>(m_Data.data()), dataSize);
+ }
+ if (!stream) {
+ throw ESP::InvalidRecordException(std::string("sub-record incomplete: ") +
+ typeString);
+ }
+ return true;
+}
diff --git a/libs/esptk/src/tes3record.cpp b/libs/esptk/src/tes3record.cpp
new file mode 100644
index 0000000..6f0fe60
--- /dev/null
+++ b/libs/esptk/src/tes3record.cpp
@@ -0,0 +1,16 @@
+#include "tes3record.h"
+#include "espexceptions.h"
+
+ESP::TES3Record::TES3Record() : m_Header() {}
+
+bool ESP::TES3Record::readFrom(std::istream& stream)
+{
+ if (!stream.read(reinterpret_cast<char*>(&m_Header), sizeof(Header))) {
+ if (stream.gcount() == 0) {
+ return false;
+ } else {
+ throw ESP::InvalidRecordException("record incomplete");
+ }
+ }
+ return true;
+}
diff --git a/libs/esptk/src/tes3subrecord.cpp b/libs/esptk/src/tes3subrecord.cpp
new file mode 100644
index 0000000..1f6c137
--- /dev/null
+++ b/libs/esptk/src/tes3subrecord.cpp
@@ -0,0 +1,52 @@
+#include "tes3subrecord.h"
+#include "espexceptions.h"
+#include "esptypes.h"
+#include <cstdint>
+#include <string>
+#include <unordered_map>
+
+ESP::TES3SubRecord::TES3SubRecord() : m_Type(TYPE_UNKNOWN), m_Data() {}
+
+bool ESP::TES3SubRecord::readFrom(std::istream& stream, uint32_t sizeOverride)
+{
+ static std::unordered_map<std::string, EType> s_TypeMap = {
+ {"HEDR", TYPE_HEDR}, {"MAST", TYPE_MAST}, {"DATA", TYPE_DATA}};
+
+ char typeString[5];
+ if (!stream.read(typeString, 4)) {
+ if (stream.gcount() == 0) {
+ return false;
+ } else {
+ throw ESP::InvalidRecordException("sub-record incomplete (unknown type)");
+ }
+ }
+ if (stream.gcount() != 4) {
+ throw ESP::InvalidRecordException(
+ std::string("sub-record type incomplete (invalid type ") + typeString + ")");
+ }
+ typeString[4] = '\0'; // not sure if this is required, shouldn't be
+ auto iter = s_TypeMap.find(std::string(typeString));
+ uint32_t dataSize;
+ if (iter != s_TypeMap.end()) {
+ m_Type = iter->second;
+ dataSize = readType<uint32_t>(stream);
+ } else {
+ m_Type = TYPE_UNKNOWN;
+ dataSize = readType<uint32_t>(stream);
+ stream.seekg(8, std::istream::cur);
+ }
+
+ if (sizeOverride != 0UL) {
+ dataSize = sizeOverride;
+ }
+ m_Data.resize(dataSize);
+
+ if (dataSize > 0) {
+ stream.read(reinterpret_cast<char*>(m_Data.data()), dataSize);
+ }
+ if (!stream) {
+ throw ESP::InvalidRecordException(std::string("sub-record incomplete: ") +
+ typeString);
+ }
+ return true;
+}