1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
#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;
}
};
|