diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /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')
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 @@ +[](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; +} |
