aboutsummaryrefslogtreecommitdiff
path: root/libs/esptk
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
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')
-rw-r--r--libs/esptk/.clang-format41
-rw-r--r--libs/esptk/.gitattributes7
-rw-r--r--libs/esptk/.github/workflows/build.yml30
-rw-r--r--libs/esptk/.github/workflows/linting.yml16
-rw-r--r--libs/esptk/.gitignore5
-rw-r--r--libs/esptk/.pre-commit-config.yaml20
-rw-r--r--libs/esptk/CMakeLists.txt30
-rw-r--r--libs/esptk/CMakePresets.json36
-rw-r--r--libs/esptk/README.md3
-rw-r--r--libs/esptk/Version.cmake3
-rw-r--r--libs/esptk/cmake/config.cmake.in3
-rw-r--r--libs/esptk/include/esptk/espexceptions.h23
-rw-r--r--libs/esptk/include/esptk/espfile.h72
-rw-r--r--libs/esptk/include/esptk/esptypes.h20
-rw-r--r--libs/esptk/include/esptk/record.h60
-rw-r--r--libs/esptk/include/esptk/subrecord.h44
-rw-r--r--libs/esptk/include/esptk/tes3record.h32
-rw-r--r--libs/esptk/include/esptk/tes3subrecord.h42
-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
24 files changed, 897 insertions, 0 deletions
diff --git a/libs/esptk/.clang-format b/libs/esptk/.clang-format
new file mode 100644
index 0000000..6098e1f
--- /dev/null
+++ b/libs/esptk/.clang-format
@@ -0,0 +1,41 @@
+---
+# We'll use defaults from the LLVM style, but with 4 columns indentation.
+BasedOnStyle: LLVM
+IndentWidth: 2
+---
+Language: Cpp
+DeriveLineEnding: false
+UseCRLF: true
+DerivePointerAlignment: false
+PointerAlignment: Left
+AlignConsecutiveAssignments: true
+AllowShortFunctionsOnASingleLine: Inline
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLambdasOnASingleLine: Empty
+AlwaysBreakTemplateDeclarations: Yes
+AccessModifierOffset: -2
+AlignTrailingComments: true
+SpacesBeforeTrailingComments: 2
+NamespaceIndentation: Inner
+MaxEmptyLinesToKeep: 1
+BreakBeforeBraces: Custom
+BraceWrapping:
+ AfterCaseLabel: false
+ AfterClass: true
+ AfterControlStatement: false
+ AfterEnum: true
+ AfterFunction: true
+ AfterNamespace: true
+ AfterStruct: true
+ AfterUnion: true
+ AfterExternBlock: true
+ BeforeCatch: false
+ BeforeElse: false
+ BeforeLambdaBody: false
+ BeforeWhile: false
+ IndentBraces: false
+ SplitEmptyFunction: false
+ SplitEmptyRecord: false
+ SplitEmptyNamespace: true
+ColumnLimit: 88
+ForEachMacros: ['Q_FOREACH', 'foreach']
diff --git a/libs/esptk/.gitattributes b/libs/esptk/.gitattributes
new file mode 100644
index 0000000..f869712
--- /dev/null
+++ b/libs/esptk/.gitattributes
@@ -0,0 +1,7 @@
+# Set the default behavior, in case people don't have core.autocrlf set.
+* text=auto
+
+# Explicitly declare text files you want to always be normalized and converted
+# to native line endings on checkout.
+*.cpp text eol=crlf
+*.h text eol=crlf
diff --git a/libs/esptk/.github/workflows/build.yml b/libs/esptk/.github/workflows/build.yml
new file mode 100644
index 0000000..a632005
--- /dev/null
+++ b/libs/esptk/.github/workflows/build.yml
@@ -0,0 +1,30 @@
+name: Build ESPTK
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ types: [opened, synchronize, reopened]
+
+jobs:
+ build:
+ runs-on: windows-2022
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Configure ESPTK build
+ shell: pwsh
+ run: |
+ cmake --preset vs2022-windows "-DCMAKE_INSTALL_PREFIX=install"
+
+ - name: Build ESPTK
+ run: cmake --build vsbuild --config RelWithDebInfo
+
+ - name: Install ESPTK
+ run: cmake --install vsbuild --config RelWithDebInfo
+
+ - name: Upload ESPTK artifact
+ uses: actions/upload-artifact@master
+ with:
+ name: esptk
+ path: ./install
diff --git a/libs/esptk/.github/workflows/linting.yml b/libs/esptk/.github/workflows/linting.yml
new file mode 100644
index 0000000..8bd995c
--- /dev/null
+++ b/libs/esptk/.github/workflows/linting.yml
@@ -0,0 +1,16 @@
+name: Lint ESPTK
+
+on:
+ push:
+ pull_request:
+ types: [opened, synchronize, reopened]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Check format
+ uses: ModOrganizer2/check-formatting-action@master
+ with:
+ check-path: "."
diff --git a/libs/esptk/.gitignore b/libs/esptk/.gitignore
new file mode 100644
index 0000000..cf71be7
--- /dev/null
+++ b/libs/esptk/.gitignore
@@ -0,0 +1,5 @@
+edit
+CMakeLists.txt.user
+/msbuild.log
+/*std*.log
+/*build
diff --git a/libs/esptk/.pre-commit-config.yaml b/libs/esptk/.pre-commit-config.yaml
new file mode 100644
index 0000000..eb8969c
--- /dev/null
+++ b/libs/esptk/.pre-commit-config.yaml
@@ -0,0 +1,20 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v6.0.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-merge-conflict
+ - id: check-case-conflict
+ - repo: https://github.com/pre-commit/mirrors-clang-format
+ rev: v21.1.8
+ hooks:
+ - id: clang-format
+ 'types_or': [c++, c]
+
+ci:
+ autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks."
+ autofix_prs: true
+ autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate."
+ autoupdate_schedule: quarterly
+ submodules: false
diff --git a/libs/esptk/CMakeLists.txt b/libs/esptk/CMakeLists.txt
new file mode 100644
index 0000000..4242178
--- /dev/null
+++ b/libs/esptk/CMakeLists.txt
@@ -0,0 +1,30 @@
+cmake_minimum_required(VERSION 3.16)
+
+include(CMakePackageConfigHelpers)
+
+project(esptk)
+
+add_subdirectory(src)
+
+configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.cmake.in
+ "${CMAKE_CURRENT_BINARY_DIR}/mo2-esptk-config.cmake"
+ INSTALL_DESTINATION "lib/cmake/mo2-esptk"
+ NO_SET_AND_CHECK_MACRO
+ NO_CHECK_REQUIRED_COMPONENTS_MACRO
+)
+
+include(${CMAKE_CURRENT_SOURCE_DIR}/Version.cmake)
+set(ESPTK_VERSION
+ ${ESPTK_MAJOR_VERSION}.${ESPTK_MINOR_VERSION}.${ESPTK_PATCH_VERSION})
+
+write_basic_package_version_file(
+ "${CMAKE_CURRENT_BINARY_DIR}/mo2-esptk-config-version.cmake"
+ VERSION "${ESPTK_VERSION}"
+ COMPATIBILITY AnyNewerVersion
+)
+
+install(FILES
+ ${CMAKE_CURRENT_BINARY_DIR}/mo2-esptk-config.cmake
+ ${CMAKE_CURRENT_BINARY_DIR}/mo2-esptk-config-version.cmake
+ DESTINATION lib/cmake/mo2-esptk
+)
diff --git a/libs/esptk/CMakePresets.json b/libs/esptk/CMakePresets.json
new file mode 100644
index 0000000..fb2f5ab
--- /dev/null
+++ b/libs/esptk/CMakePresets.json
@@ -0,0 +1,36 @@
+{
+ "configurePresets": [
+ {
+ "errors": {
+ "deprecated": true
+ },
+ "hidden": true,
+ "name": "cmake-dev",
+ "warnings": {
+ "deprecated": true,
+ "dev": true
+ }
+ },
+ {
+ "binaryDir": "${sourceDir}/vsbuild",
+ "architecture": {
+ "strategy": "set",
+ "value": "x64"
+ },
+ "cacheVariables": {
+ "CMAKE_CXX_FLAGS": "/EHsc /MP /W4"
+ },
+ "generator": "Visual Studio 17 2022",
+ "inherits": ["cmake-dev"],
+ "name": "vs2022-windows",
+ "toolset": "v143"
+ }
+ ],
+ "buildPresets": [
+ {
+ "name": "vs2022-windows",
+ "configurePreset": "vs2022-windows"
+ }
+ ],
+ "version": 4
+}
diff --git a/libs/esptk/README.md b/libs/esptk/README.md
new file mode 100644
index 0000000..508322f
--- /dev/null
+++ b/libs/esptk/README.md
@@ -0,0 +1,3 @@
+[![Build status](https://ci.appveyor.com/api/projects/status/9521cwqvajwve52x?svg=true)](https://ci.appveyor.com/project/Modorganizer2/modorganizer-esptk)
+
+# modorganizer-esptk
diff --git a/libs/esptk/Version.cmake b/libs/esptk/Version.cmake
new file mode 100644
index 0000000..eca00f7
--- /dev/null
+++ b/libs/esptk/Version.cmake
@@ -0,0 +1,3 @@
+set(ESPTK_MAJOR_VERSION 1)
+set(ESPTK_MINOR_VERSION 4)
+set(ESPTK_PATCH_VERSION 0)
diff --git a/libs/esptk/cmake/config.cmake.in b/libs/esptk/cmake/config.cmake.in
new file mode 100644
index 0000000..b04f9d4
--- /dev/null
+++ b/libs/esptk/cmake/config.cmake.in
@@ -0,0 +1,3 @@
+@PACKAGE_INIT@
+
+include ( "${CMAKE_CURRENT_LIST_DIR}/mo2-esptk-targets.cmake" )
diff --git a/libs/esptk/include/esptk/espexceptions.h b/libs/esptk/include/esptk/espexceptions.h
new file mode 100644
index 0000000..9830ed6
--- /dev/null
+++ b/libs/esptk/include/esptk/espexceptions.h
@@ -0,0 +1,23 @@
+#ifndef ESPEXCEPTIONS_H
+#define ESPEXCEPTIONS_H
+
+#include <stdexcept>
+
+namespace ESP
+{
+
+class InvalidRecordException : public std::runtime_error
+{
+public:
+ InvalidRecordException(const std::string& message) : std::runtime_error(message) {}
+};
+
+class InvalidFileException : public std::runtime_error
+{
+public:
+ InvalidFileException(const std::string& message) : std::runtime_error(message) {}
+};
+
+} // namespace ESP
+
+#endif // ESPEXCEPTIONS_H
diff --git a/libs/esptk/include/esptk/espfile.h b/libs/esptk/include/esptk/espfile.h
new file mode 100644
index 0000000..04be76e
--- /dev/null
+++ b/libs/esptk/include/esptk/espfile.h
@@ -0,0 +1,72 @@
+#ifndef ESPFILE_H
+#define ESPFILE_H
+
+#include "record.h"
+#include "tes3record.h"
+#include <fstream>
+#include <set>
+#include <string>
+
+namespace ESP
+{
+
+class SubRecord;
+
+class File
+{
+public:
+ File(const std::string& fileName);
+ File(const std::wstring& fileName);
+
+ Record readRecord();
+
+ bool isMaster() const;
+ bool isLight(bool overlaySupport = false) const;
+ bool isMedium() const;
+ bool isOverlay() const;
+ bool isBlueprint() const;
+ bool isDummy() const;
+ uint16_t formVersion() const { return m_MainRecord.formVersion(); };
+ float headerVersion() const { return m_Header.version; }
+ std::string author() const { return m_Author; }
+ std::string description() const { return m_Description; }
+ std::set<std::string> masters() const { return m_Masters; }
+
+private:
+ void init();
+
+ void onHEDR(const SubRecord& rec);
+ void onMAST(const SubRecord& rec);
+ void onCNAM(const SubRecord& rec);
+ void onSNAM(const SubRecord& rec);
+
+private:
+ std::ifstream m_File;
+
+ struct
+ {
+ float version;
+ int32_t numRecords;
+ uint32_t nextObjectId;
+ } m_Header;
+
+ struct
+ {
+ float version;
+ uint32_t unknown;
+ char author[32];
+ char description[256];
+ uint32_t numRecords;
+ } m_TES3Header;
+
+ Record m_MainRecord;
+
+ std::string m_Author;
+ std::string m_Description;
+
+ std::set<std::string> m_Masters;
+};
+
+} // namespace ESP
+
+#endif // ESPFILE_H
diff --git a/libs/esptk/include/esptk/esptypes.h b/libs/esptk/include/esptk/esptypes.h
new file mode 100644
index 0000000..135dde1
--- /dev/null
+++ b/libs/esptk/include/esptk/esptypes.h
@@ -0,0 +1,20 @@
+#ifndef ESPTYPES_H
+#define ESPTYPES_H
+
+#include <cstring>
+#include <istream>
+
+template <typename T>
+static T readType(std::istream& stream)
+{
+ union
+ {
+ char buffer[sizeof(T)];
+ T value;
+ };
+ memset(buffer, 0x42, sizeof(T));
+ stream.read(buffer, sizeof(T));
+ return value;
+}
+
+#endif // ESPTYPES_H
diff --git a/libs/esptk/include/esptk/record.h b/libs/esptk/include/esptk/record.h
new file mode 100644
index 0000000..3a2c996
--- /dev/null
+++ b/libs/esptk/include/esptk/record.h
@@ -0,0 +1,60 @@
+#ifndef RECORD_H
+#define RECORD_H
+
+#include <cstdint>
+#include <istream>
+#include <vector>
+
+namespace ESP
+{
+
+/**
+ * @brief record storage class without record-specific information
+ */
+class Record
+{
+public:
+ enum EFlag
+ {
+ FLAG_MASTER = 0x00000001,
+ FLAG_LIGHT_ALTERNATE = 0x00000100, // SF light flag (FE/FF memory space)
+ FLAG_LIGHT = 0x00000200, // SSE & FO4 light flag (FE/FF memory space)
+ FLAG_OVERLAY = 0x00000200, // SF overlay flag (does not claim new memory space,
+ // overrules light flag)
+ FLAG_MEDIUM = 0x00000400, // SF Creation update, indicates medium plugin type (FD
+ // memory space)
+ FLAG_BLUEPRINT =
+ 0x0000800, // SF blueprint flag (force loads after all other plugins)
+ FLAG_COMPRESSED = 0x00040000
+ };
+
+public:
+ Record();
+
+ bool readFrom(std::istream& stream);
+
+ bool flagSet(EFlag flag) const;
+
+ uint16_t formVersion() const { return m_FormVersion; }
+
+ const std::vector<uint8_t>& data() const { return m_Data; }
+
+private:
+ struct Header
+ {
+ char type[4];
+ uint32_t dataSize;
+ uint32_t flags;
+ uint32_t id;
+ uint32_t revision;
+ } m_Header;
+
+ uint16_t m_FormVersion;
+ std::vector<uint8_t> m_Data;
+
+ bool m_OblivionStyle;
+};
+
+} // namespace ESP
+
+#endif // RECORD_H
diff --git a/libs/esptk/include/esptk/subrecord.h b/libs/esptk/include/esptk/subrecord.h
new file mode 100644
index 0000000..f9eb36c
--- /dev/null
+++ b/libs/esptk/include/esptk/subrecord.h
@@ -0,0 +1,44 @@
+#ifndef SUBRECORD_H
+#define SUBRECORD_H
+
+#include <cstdint>
+#include <istream>
+#include <vector>
+
+namespace ESP
+{
+
+/**
+ * @brief sub-record storage class without record-specific information
+ */
+class SubRecord
+{
+public:
+ enum EType
+ {
+ TYPE_UNKNOWN,
+ TYPE_HEDR,
+ TYPE_CNAM,
+ TYPE_SNAM,
+ TYPE_MAST,
+ TYPE_ONAM
+ };
+
+ static const int NUM_TYPES = TYPE_ONAM;
+
+public:
+ SubRecord();
+
+ bool readFrom(std::istream& stream, uint32_t sizeOverride = 0UL);
+
+ EType type() const { return m_Type; }
+ const std::vector<uint8_t>& data() const { return m_Data; }
+
+private:
+ EType m_Type;
+ std::vector<uint8_t> m_Data;
+};
+
+} // namespace ESP
+
+#endif // SUBRECORD_H
diff --git a/libs/esptk/include/esptk/tes3record.h b/libs/esptk/include/esptk/tes3record.h
new file mode 100644
index 0000000..19fa040
--- /dev/null
+++ b/libs/esptk/include/esptk/tes3record.h
@@ -0,0 +1,32 @@
+#ifndef TES3_RECORD_H
+#define TES3_RECORD_H
+
+#include <cstdint>
+#include <istream>
+#include <vector>
+
+namespace ESP
+{
+
+/**
+ * @brief record storage class without record-specific information
+ */
+class TES3Record
+{
+public:
+ TES3Record();
+
+ bool readFrom(std::istream& stream);
+
+private:
+ struct Header
+ {
+ char type[4];
+ float version;
+ long unknown;
+ } m_Header;
+};
+
+} // namespace ESP
+
+#endif // TES3_RECORD_H
diff --git a/libs/esptk/include/esptk/tes3subrecord.h b/libs/esptk/include/esptk/tes3subrecord.h
new file mode 100644
index 0000000..966afcb
--- /dev/null
+++ b/libs/esptk/include/esptk/tes3subrecord.h
@@ -0,0 +1,42 @@
+#ifndef TES3_SUBRECORD_H
+#define TES3_SUBRECORD_H
+
+#include <cstdint>
+#include <istream>
+#include <vector>
+
+namespace ESP
+{
+
+/**
+ * @brief sub-record storage class without record-specific information
+ */
+class TES3SubRecord
+{
+public:
+ enum EType
+ {
+ TYPE_UNKNOWN,
+ TYPE_HEDR,
+ TYPE_MAST,
+ TYPE_DATA
+ };
+
+ static const int NUM_TYPES = TYPE_DATA;
+
+public:
+ TES3SubRecord();
+
+ bool readFrom(std::istream& stream, uint32_t sizeOverride = 0UL);
+
+ EType type() const { return m_Type; }
+ const std::vector<uint8_t>& data() const { return m_Data; }
+
+private:
+ EType m_Type;
+ std::vector<uint8_t> m_Data;
+};
+
+} // namespace ESP
+
+#endif // TES3_SUBRECORD_H
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;
+}