diff --git a/NavmeshList/main.c b/NavmeshList/main.c index 1cd852a..ddedd9b 100644 --- a/NavmeshList/main.c +++ b/NavmeshList/main.c @@ -28,19 +28,19 @@ int main(void) { size_t read = fread(buffer, sizeof(char), size, fp); assert(read == size); - size_t dc_size = espr_decompressed_size(buffer, size); + struct esp_stats stats = espr_stats(buffer, size); - char *decompressed = malloc(dc_size); + char *decompressed = malloc(stats.decompressed_size); if (!decompressed) return errno; - espr_decompress(buffer, size, decompressed, dc_size); + espr_print(buffer, size); + + espr_decompress(buffer, size, decompressed, stats.decompressed_size); free(buffer); - size_t formid_count = espr_formid_count(decompressed, dc_size); - - printf("FormID Count: %zu\n", formid_count); + // espr_print(decompressed, stats.decompressed_size); free(decompressed); diff --git a/espReader/ESPReader.h b/espReader/ESPReader.h index 90d8216..086b11e 100644 --- a/espReader/ESPReader.h +++ b/espReader/ESPReader.h @@ -58,6 +58,9 @@ extern "C" { // There are 10 Group Types #define GTS_SIZE 10 +#define LIT(L) (const struct str_lit) STR_LIT(L) +#define STR_LIT(L) { .lit = L , .size = sizeof( L ) - 1 } + // // === FORWARD DEFS === @@ -68,6 +71,8 @@ extern "C" { typedef struct group Group; typedef struct record Record; typedef struct field Field; + typedef struct meta_node MetaNode; + // // === SIMPLE TYPES === @@ -83,8 +88,27 @@ extern "C" { uint32_t uint; }; + struct str_lit { + const char *const lit; + const int size; + const char _pad[4]; + }; + // Inner type for Record Flag String LUT. Indexed by flag bit. - typedef const char *const rfs_inner[RFS_INNER_SIZE]; + typedef const struct str_lit rfs_inner[RFS_INNER_SIZE]; + + struct esp_stats { + size_t decompressed_size; + uint32_t group_count; + uint32_t record_count; + }; + + struct str_buf { + char *buf; + int size; + char _pad[4]; + }; + // // === ENUMS === @@ -177,6 +201,37 @@ extern "C" { void *data; }; + /* Meta Nodes are used for constructing a more flexible tree structure + * on top of the natural structure of ESP/ESM files. + * + * Meta Nodes do not create a pure tree structure, rather they have pointers to + * their parent and first child, and children have pointers backwards and + * forward through a linked list of all of the children of the parent node. + * + * There is no root node as such, rather there is a root linked list for which + * all of the Meta Nodes have no parents. + * + * While the ESP/ESM buffer can be modified in-place, any modification that + * changes the size of the stored data cannot be directly written to the buffer + * without first shifting all of the data after the point of modification. + * + * Modifications that change data size are: + * - Adding or deleting a group or record + * - Adding or deleting a field in a record + * - Changing a variable length field with data of different length + * + * With a Meta Node you can instead allocate new, arbitrarily sized memory for + * the node data. The Meta Node tree can then be walked to reconstruct a + * contiguous view of discontiguous memory. + */ + struct meta_node { + Node n; + MetaNode *parent; + MetaNode *child; + MetaNode *prev; + MetaNode *next; + }; + // // === BINARY DATA OVERLAYS === // @@ -240,7 +295,7 @@ extern "C" { extern const enum record_type group_order[GO_SIZE]; // Printable strings for group types - extern const char *const group_type_strings[GTS_SIZE]; + extern const struct str_lit group_type_strings[GTS_SIZE]; // // === FUNCTIONS === @@ -267,15 +322,26 @@ extern "C" { */ void espr_print(char *data, size_t size); - /* Calculates the size of the esp data if all of the compressed records are - * decompressed. + /* Calculates the number of groups and records in the esp/esm file and the + * size of the esp/esm if all of the compressed records were decompressed. */ - size_t espr_decompressed_size(char *data, size_t size); + struct esp_stats espr_stats(char *data, size_t size); - /* Counts the number of formids present in the esp/esm data. This should be - * equal to the number of records. - */ - size_t espr_formid_count(char *data, size_t size); + // Calculates the number of formid's in an esm/esp from the stats + inline uint32_t espr_formid_count(struct esp_stats stats) { + return stats.record_count; + } + + // Calculates the number of nodes in the esp/esm from the stats + inline uint32_t espr_node_count(struct esp_stats stats) { + return stats.record_count + stats.group_count; + } + + // Calculates the size of a MetaNode tree constructed over the esp/esm for + // which the stats were generated. + inline size_t espr_tree_size(struct esp_stats stats) { + return sizeof(MetaNode) * espr_node_count(stats); + } /* Copies the data from `data` to `buf` decompressing compressed fields as * it does so. buf_size should be the value returned from @@ -283,6 +349,8 @@ extern "C" { */ void espr_decompress(char *data, size_t size, char *buf, size_t buf_size); + + // End C++ guard #ifdef __cplusplus } diff --git a/espReader/ESPTree.h b/espReader/ESPTree.h deleted file mode 100644 index 6db349c..0000000 --- a/espReader/ESPTree.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0.If a copy of the MPL was not distributed with this - * file, You can obtain one at http ://mozilla.org/MPL/2.0/. - */ -#pragma once - -#include "ESPReader.h" - -typedef struct meta_node MetaNode; - -/* Meta Nodes are used for constructing a more flexible tree structure - * on top of the natural structure of ESP/ESM files. - * - * Meta Nodes do not create a pure tree structure, rather they have pointers to - * their parent and first child, and children have pointers backwards and - * forward through a linked list of all of the children of the parent node. - * - * There is no root node as such, rather there is a root linked list for which - * all of the Meta Nodes have no parents. - * - * While the ESP/ESM buffer can be modified in-place, any modification that - * changes the size of the stored data cannot be directly written to the buffer - * without first shifting all of the data after the point of modification. - * - * Modifications that change data size are: - * - Adding or deleting a group or record - * - Adding or deleting a field in a record - * - Changing a variable length field with data of different length - * - * With a Meta Node you can instead allocate new, arbitrarily sized memory for - * the node data. The Meta Node tree can then be walked to reconstruct a - * contiguous view of discontiguous memory. - */ -struct meta_node { - Node n; - MetaNode *parent; - MetaNode *child; - MetaNode *prev; - MetaNode *next; -}; - diff --git a/espReader/LUT.c b/espReader/LUT.c index 29107aa..f6aaccf 100644 --- a/espReader/LUT.c +++ b/espReader/LUT.c @@ -209,76 +209,76 @@ const uint8_t rth2rt[RT_HASH_SIZE] = { [RT_WTHR] = WTHR, }; -const char *const group_type_strings[GTS_SIZE] = { - "Top Type", - "World Children", - "Interior Cell Block", - "Interior Cell Sub-Block", - "Exterior Cell", - "Exterior Cell Sub-Block", - "Cell Children", - "Topic Children", - "Cell Persistent Children", - "Cell Temporary Children", +const struct str_lit group_type_strings[GTS_SIZE] = { + STR_LIT("Top Type"), + STR_LIT("World Children"), + STR_LIT("Interior Cell Block"), + STR_LIT("Interior Cell Sub-Block"), + STR_LIT("Exterior Cell"), + STR_LIT("Exterior Cell Sub-Block"), + STR_LIT("Cell Children"), + STR_LIT("Topic Children"), + STR_LIT("Cell Persistent Children"), + STR_LIT("Cell Temporary Children"), }; // Non-REFR flags rfs_inner achr = { - [9] = "Starts Dead", - [10] = "Persistent", - [11] = "Initially Disabled", - [25] = "No AI Acquire", - [29] = "Don't Havok Settle", - [30] = "ACHR Unknown 30", + [9] = STR_LIT("Starts Dead"), + [10] = STR_LIT("Persistent"), + [11] = STR_LIT("Initially Disabled"), + [25] = STR_LIT("No AI Acquire"), + [29] = STR_LIT("Don't Havok Settle"), + [30] = STR_LIT("ACHR Unknown 30"), }; rfs_inner acti = { - [6] = "Has Tree LOD", - [8] = "Must Update Anims", - [9] = "Hidden From Local Map", - [15] = "Has Distant LOD", - [16] = "Random Anim Start", - [17] = "Dangerous", - [20] = "Ignore Object Interaction", - [23] = "Is Marker", - [25] = "Obstacle", - [26] = "NavMesh Generation - Filter", - [27] = "NavMesh Generation - Bounding Box", - [29] = "Child Can Use", - [30] = "NavMesh Generation - Ground", + [6] = STR_LIT("Has Tree LOD"), + [8] = STR_LIT("Must Update Anims"), + [9] = STR_LIT("Hidden From Local Map"), + [15] = STR_LIT("Has Distant LOD"), + [16] = STR_LIT("Random Anim Start"), + [17] = STR_LIT("Dangerous"), + [20] = STR_LIT("Ignore Object Interaction"), + [23] = STR_LIT("Is Marker"), + [25] = STR_LIT("Obstacle"), + [26] = STR_LIT("NavMesh Generation - Filter"), + [27] = STR_LIT("NavMesh Generation - Bounding Box"), + [29] = STR_LIT("Child Can Use"), + [30] = STR_LIT("NavMesh Generation - Ground"), }; rfs_inner tact = { - [9] = "Hidden From Local Map", - [16] = "Random Anim Start", - [17] = "Radio Station", + [9] = STR_LIT("Hidden From Local Map"), + [16] = STR_LIT("Random Anim Start"), + [17] = STR_LIT("Radio Station"), }; rfs_inner alch = { - [29] = "Medicine", + [29] = STR_LIT("Medicine"), }; rfs_inner ammo = { - [2] = "Non-Playable", + [2] = STR_LIT("Non-Playable"), }; rfs_inner anio = { - [9] = "ANIO Unknown 9", + [9] = STR_LIT("ANIO Unknown 9"), }; rfs_inner armo = { - [2] = "Non-Playable", - [6] = "Shield", - [10] = "ARMO Unknown 10", - [15] = "ARMO Unknown 15", + [2] = STR_LIT("Non-Playable"), + [6] = STR_LIT("Shield"), + [10] = STR_LIT("ARMO Unknown 10"), + [15] = STR_LIT("ARMO Unknown 15"), }; #define REFERENCE_RECORD { \ - [7] = "Turn Off Fire", \ - [10] = "Persistent", \ - [11] = "Initially Disabled", \ - [28] = "Reflected by Auto Water", \ - [30] = "No Respawn", \ + [7] = STR_LIT("Turn Off Fire"), \ + [10] = STR_LIT("Persistent"), \ + [11] = STR_LIT("Initially Disabled"), \ + [28] = STR_LIT("Reflected by Auto Water"), \ + [30] = STR_LIT("No Respawn"), \ } rfs_inner parw = REFERENCE_RECORD; @@ -293,187 +293,187 @@ rfs_inner pmis = REFERENCE_RECORD; #undef REFERENCE_RECORD rfs_inner cell = { - [10] = "Persistent", - [17] = "Off Limits", - [18] = "Compressed", - [19] = "Can't Wait", + [10] = STR_LIT("Persistent"), + [17] = STR_LIT("Off Limits"), + [18] = STR_LIT("Compressed"), + [19] = STR_LIT("Can't Wait"), }; rfs_inner cont = { - [15] = "Has Distant LOD", - [16] = "Random Anim Start", - [25] = "Obstacle", - [26] = "NavMesh Generation - Filter", - [27] = "NavMesh Generation - Bounding Box", - [30] = "NavMesh Generation - Ground", + [15] = STR_LIT("Has Distant LOD"), + [16] = STR_LIT("Random Anim Start"), + [25] = STR_LIT("Obstacle"), + [26] = STR_LIT("NavMesh Generation - Filter"), + [27] = STR_LIT("NavMesh Generation - Bounding Box"), + [30] = STR_LIT("NavMesh Generation - Ground"), }; rfs_inner csty = { - [19] = "Allow Dual Wielding", + [19] = STR_LIT("Allow Dual Wielding"), }; rfs_inner door = { - [15] = "Has Distant LOD", - [16] = "Random Anim Start", - [23] = "Is Marker", + [15] = STR_LIT("Has Distant LOD"), + [16] = STR_LIT("Random Anim Start"), + [23] = STR_LIT("Is Marker"), }; rfs_inner eyes = { - [2] = "Non-Playable", + [2] = STR_LIT("Non-Playable"), }; rfs_inner furn = { - [7] = "Is Perch", - [16] = "Has Distant LOD", - [23] = "Is Marker", - [28] = "Must Exit To Talk", - [29] = "Child Can Use", + [7] = STR_LIT("Is Perch"), + [16] = STR_LIT("Has Distant LOD"), + [23] = STR_LIT("Is Marker"), + [28] = STR_LIT("Must Exit To Talk"), + [29] = STR_LIT("Child Can Use"), }; rfs_inner glob = { - [6] = "Constant", + [6] = STR_LIT("Constant"), }; rfs_inner hdpt = { - [2] = "Non-Playable", + [2] = STR_LIT("Non-Playable"), }; rfs_inner mstt = { - [8] = "Must Update Anims", - [9] = "Hidden From Local Map", - [15] = "Has Distant LOD", - [16] = "Random Anim Start", - [19] = "Has Currents", - [25] = "Obstacle", - [26] = "NavMesh Generation - Filter", - [27] = "NavMesh Generation - Bounding Box", - [30] = "NavMesh Generation - Ground", + [8] = STR_LIT("Must Update Anims"), + [9] = STR_LIT("Hidden From Local Map"), + [15] = STR_LIT("Has Distant LOD"), + [16] = STR_LIT("Random Anim Start"), + [19] = STR_LIT("Has Currents"), + [25] = STR_LIT("Obstacle"), + [26] = STR_LIT("NavMesh Generation - Filter"), + [27] = STR_LIT("NavMesh Generation - Bounding Box"), + [30] = STR_LIT("NavMesh Generation - Ground"), }; rfs_inner idlm = { - [29] = "Child Can Use", + [29] = STR_LIT("Child Can Use"), }; rfs_inner slgm = { - [17] = "Can Hold NPC Soul", + [17] = STR_LIT("Can Hold NPC Soul"), }; rfs_inner navm = { - [18] = "Compressed", - [26] = "AutoGen", - [31] = "NavmeshGenCell", + [18] = STR_LIT("Compressed"), + [26] = STR_LIT("AutoGen"), + [31] = STR_LIT("NavmeshGenCell"), }; rfs_inner perk = { - [2] = "Non-Playable", + [2] = STR_LIT("Non-Playable"), }; rfs_inner shou = { - [7] = "Treat Spells As Powers", + [7] = STR_LIT("Treat Spells As Powers"), }; rfs_inner rela = { - [6] = "Secret", + [6] = STR_LIT("Secret"), }; rfs_inner clfm = { - [2] = "Non-Playable", + [2] = STR_LIT("Non-Playable"), }; rfs_inner info = { - [13] = "Actor Changed", + [13] = STR_LIT("Actor Changed"), }; rfs_inner keym = { - [2] = "Non-Playable", + [2] = STR_LIT("Non-Playable"), }; rfs_inner land = { - [18] = "Compressed", + [18] = STR_LIT("Compressed"), }; rfs_inner ligh = { - [16] = "Random Anim Start", - [17] = "Portal-strict", - [25] = "Obstacle", + [16] = STR_LIT("Random Anim Start"), + [17] = STR_LIT("Portal-strict"), + [25] = STR_LIT("Obstacle"), }; rfs_inner lscr = { - [10] = "Displays In Main Menu", + [10] = STR_LIT("Displays In Main Menu"), }; rfs_inner misc = { - [2] = "Non-Playable", + [2] = STR_LIT("Non-Playable"), }; rfs_inner npc_ = { - [10] = "NPC_ Unknown 10", - [18] = "Compressed", - [19] = "NPC_ Unknown 19", - [29] = "Bleedout Override", + [10] = STR_LIT("NPC_ Unknown 10"), + [18] = STR_LIT("Compressed"), + [19] = STR_LIT("NPC_ Unknown 19"), + [29] = STR_LIT("Bleedout Override"), }; rfs_inner race = { - [19] = "Critter (?)", + [19] = STR_LIT("Critter (?)"), }; // these are generic refr flags for any reference type not handled by the refr // specific flag lut rfs_inner refr = { - [10] = "Persistent", - [11] = "Initially Disabled", - [16] = "Is Full LOD", - [26] = "Filter (Collision Geometry)", - [27] = "Bounding Box (Collision Geometry)", - [30] = "Ground", - [31] = "Multibound", + [10] = STR_LIT("Persistent"), + [11] = STR_LIT("Initially Disabled"), + [16] = STR_LIT("Is Full LOD"), + [26] = STR_LIT("Filter (Collision Geometry)"), + [27] = STR_LIT("Bounding Box (Collision Geometry)"), + [30] = STR_LIT("Ground"), + [31] = STR_LIT("Multibound"), }; rfs_inner regn = { - [6] = "Border Region", + [6] = STR_LIT("Border Region"), }; rfs_inner stat = { - [2] = "Never Fades", - [5] = "Deleted", - [6] = "Has Tree LOD", - [7] = "Add-On LOD Object", - [9] = "Hidden From Local Map", - [11] = "STAT Unknown 11", - [15] = "Has Distant LOD", - [16] = "STAT Unknown 16", - [17] = "Uses HD LOD Texture", - [19] = "Has Currents", - [23] = "Is Marker", - [25] = "Obstacle", - [26] = "NavMesh Generation - Filter", - [27] = "NavMesh Generation - Bounding Box", - [28] = "Show In World Map", - [30] = "NavMesh Generation - Ground", + [2] = STR_LIT("Never Fades"), + [5] = STR_LIT("Deleted"), + [6] = STR_LIT("Has Tree LOD"), + [7] = STR_LIT("Add-On LOD Object"), + [9] = STR_LIT("Hidden From Local Map"), + [11] = STR_LIT("STAT Unknown 11"), + [15] = STR_LIT("Has Distant LOD"), + [16] = STR_LIT("STAT Unknown 16"), + [17] = STR_LIT("Uses HD LOD Texture"), + [19] = STR_LIT("Has Currents"), + [23] = STR_LIT("Is Marker"), + [25] = STR_LIT("Obstacle"), + [26] = STR_LIT("NavMesh Generation - Filter"), + [27] = STR_LIT("NavMesh Generation - Bounding Box"), + [28] = STR_LIT("Show In World Map"), + [30] = STR_LIT("NavMesh Generation - Ground"), }; rfs_inner tes4 = { - [0] = "ESM", - [1] = "Altered", - [2] = "Checked", - [3] = "Active", - [4] = "Optimized File", - [5] = "Temp ID Owner", - [7] = "Localized", - [8] = "Precalc Data Only", - [9] = "ESL", + [0] = STR_LIT("ESM"), + [1] = STR_LIT("Altered"), + [2] = STR_LIT("Checked"), + [3] = STR_LIT("Active"), + [4] = STR_LIT("Optimized File"), + [5] = STR_LIT("Temp ID Owner"), + [7] = STR_LIT("Localized"), + [8] = STR_LIT("Precalc Data Only"), + [9] = STR_LIT("ESL"), }; rfs_inner tree = { - [15] = "Has Distant LOD", + [15] = STR_LIT("Has Distant LOD"), }; rfs_inner weap = { - [2] = "Non-Playable", + [2] = STR_LIT("Non-Playable"), }; rfs_inner wrld = { - [19] = "Can't Wait", + [19] = STR_LIT("Can't Wait"), }; rfs_inner *const rfs[RT_SIZE] = { @@ -524,18 +524,18 @@ rfs_inner *const rfs[RT_SIZE] = { // REFR flags depend on what its NAME field references #define REFR_GROUP1 { \ - [9] = "Hidden From Local Map", \ - [10] = "Persistent", \ - [11] = "Initially Disabled", \ - [13] = "Sky Marker", \ - [15] = "Visible When Distant", \ - [16] = "Is Full LOD", \ - [26] = "Filter (Collision Geometry)", \ - [27] = "Bounding Box (Collision Geometry)", \ - [28] = "Reflected By Auto Water", \ - [29] = "Don't Havok Settle", \ - [30] = "No Respawn", \ - [31] = "Multibound", \ + [9] = STR_LIT("Hidden From Local Map"), \ + [10] = STR_LIT("Persistent"), \ + [11] = STR_LIT("Initially Disabled"), \ + [13] = STR_LIT("Sky Marker"), \ + [15] = STR_LIT("Visible When Distant"), \ + [16] = STR_LIT("Is Full LOD"), \ + [26] = STR_LIT("Filter (Collision Geometry)"), \ + [27] = STR_LIT("Bounding Box (Collision Geometry)"), \ + [28] = STR_LIT("Reflected By Auto Water"), \ + [29] = STR_LIT("Don't Havok Settle"), \ + [30] = STR_LIT("No Respawn"), \ + [31] = STR_LIT("Multibound"), \ } rfs_inner r_acti = REFR_GROUP1; @@ -546,78 +546,78 @@ rfs_inner r_flor = REFR_GROUP1; #undef REFR_GROUP1 rfs_inner r_cont = { - [10] = "Persistent", - [11] = "Initially Disabled", - [16] = "Is Full LOD", - [25] = "No AI Acquire", - [26] = "Filter (Collision Geometry)", - [27] = "Bounding Box (Collision Geometry)", - [28] = "Reflected By Auto Water", - [29] = "Don't Havok Settle", - [30] = "Ground", - [31] = "Multibound", + [10] = STR_LIT("Persistent"), + [11] = STR_LIT("Initially Disabled"), + [16] = STR_LIT("Is Full LOD"), + [25] = STR_LIT("No AI Acquire"), + [26] = STR_LIT("Filter (Collision Geometry)"), + [27] = STR_LIT("Bounding Box (Collision Geometry)"), + [28] = STR_LIT("Reflected By Auto Water"), + [29] = STR_LIT("Don't Havok Settle"), + [30] = STR_LIT("Ground"), + [31] = STR_LIT("Multibound"), }; rfs_inner r_door = { - [6] = "Hidden From Local Map", - [8] = "Inaccessible", - [10] = "Persistent", - [11] = "Initially Disabled", - [16] = "Is Full LOD", - [26] = "Filter (Collision Geometry)", - [27] = "Bounding Box (Collision Geometry)", - [28] = "Reflected By Auto Water", - [29] = "Don't Havok Settle", - [30] = "No Respawn", - [31] = "Multibound", + [6] = STR_LIT("Hidden From Local Map"), + [8] = STR_LIT("Inaccessible"), + [10] = STR_LIT("Persistent"), + [11] = STR_LIT("Initially Disabled"), + [16] = STR_LIT("Is Full LOD"), + [26] = STR_LIT("Filter (Collision Geometry)"), + [27] = STR_LIT("Bounding Box (Collision Geometry)"), + [28] = STR_LIT("Reflected By Auto Water"), + [29] = STR_LIT("Don't Havok Settle"), + [30] = STR_LIT("No Respawn"), + [31] = STR_LIT("Multibound"), }; rfs_inner r_ligh = { - [8] = "Doesn't Light Water", - [9] = "Casts Shadows", - [10] = "Persistent", - [11] = "Initially Disabled", - [16] = "Never Fades", - [17] = "Doesn't Light Landscape", - [25] = "No AI Acquire", - [28] = "Reflected By Auto Water", - [29] = "Don't Havok Settle", - [30] = "No Respawn", - [31] = "Multibound", + [8] = STR_LIT("Doesn't Light Water"), + [9] = STR_LIT("Casts Shadows"), + [10] = STR_LIT("Persistent"), + [11] = STR_LIT("Initially Disabled"), + [16] = STR_LIT("Never Fades"), + [17] = STR_LIT("Doesn't Light Landscape"), + [25] = STR_LIT("No AI Acquire"), + [28] = STR_LIT("Reflected By Auto Water"), + [29] = STR_LIT("Don't Havok Settle"), + [30] = STR_LIT("No Respawn"), + [31] = STR_LIT("Multibound"), }; rfs_inner r_mstt = { - [9] = "Motion Blur", - [10] = "Persistent", - [11] = "Initially Disabled", - [16] = "Is Full LOD", - [26] = "Filter (Collision Geometry)", - [27] = "Bounding Box (Collision Geometry)", - [28] = "Reflected By Auto Water", - [29] = "Don't Havok Settle", - [30] = "No Respawn", - [31] = "Multibound", + [9] = STR_LIT("Motion Blur"), + [10] = STR_LIT("Persistent"), + [11] = STR_LIT("Initially Disabled"), + [16] = STR_LIT("Is Full LOD"), + [26] = STR_LIT("Filter (Collision Geometry)"), + [27] = STR_LIT("Bounding Box (Collision Geometry)"), + [28] = STR_LIT("Reflected By Auto Water"), + [29] = STR_LIT("Don't Havok Settle"), + [30] = STR_LIT("No Respawn"), + [31] = STR_LIT("Multibound"), }; rfs_inner r_addn = { - [10] = "Persistent", - [11] = "Initially Disabled", - [16] = "Is Full LOD", - [28] = "Reflected By Auto Water", - [29] = "Don't Havok Settle", - [30] = "No Respawn", - [31] = "Multibound", + [10] = STR_LIT("Persistent"), + [11] = STR_LIT("Initially Disabled"), + [16] = STR_LIT("Is Full LOD"), + [28] = STR_LIT("Reflected By Auto Water"), + [29] = STR_LIT("Don't Havok Settle"), + [30] = STR_LIT("No Respawn"), + [31] = STR_LIT("Multibound"), }; #define REFR_GROUP2 { \ - [10] = "Persistent", \ - [11] = "Initially Disabled", \ - [16] = "Is Full LOD", \ - [25] = "No AI Acquire", \ - [28] = "Reflected By Auto Water", \ - [29] = "Don't Havok Settle", \ - [30] = "No Respawn", \ - [31] = "Multibound", \ + [10] = STR_LIT("Persistent"), \ + [11] = STR_LIT("Initially Disabled"), \ + [16] = STR_LIT("Is Full LOD"), \ + [25] = STR_LIT("No AI Acquire"), \ + [28] = STR_LIT("Reflected By Auto Water"), \ + [29] = STR_LIT("Don't Havok Settle"), \ + [30] = STR_LIT("No Respawn"), \ + [31] = STR_LIT("Multibound"), \ } rfs_inner r_alch = REFR_GROUP2; @@ -630,6 +630,8 @@ rfs_inner r_misc = REFR_GROUP2; rfs_inner r_slgm = REFR_GROUP2; rfs_inner r_weap = REFR_GROUP2; +#undef REFR_GROUP2 + rfs_inner *const rfs_r[RT_SIZE] = { [AACT] = CP(NULL ),[ACHR] = CP(NULL ),[ACTI] = CP(r_acti), [ADDN] = CP(r_addn),[ALCH] = CP(r_alch),[AMMO] = CP(r_ammo), diff --git a/espReader/Reader.c b/espReader/Reader.c index 2c83d1c..d51cae9 100644 --- a/espReader/Reader.c +++ b/espReader/Reader.c @@ -5,10 +5,12 @@ */ #undef NDEBUG #include +#include #include #include #include #include +#include #include "zlib.h" @@ -18,6 +20,8 @@ // === CONSTANTS === // +#define STDOUT_FILENO 1 + // timestamp field access const uint16_t day_mask = 0x1F; const uint16_t month_mask = 0xF; @@ -41,17 +45,18 @@ void print_group_header(Group *header); void print_record_header(Record *header); // Printer helpers -void print_group_label(Group *header); -void print_record_flags(Record *header); -void print_timestamp(uint16_t ts); -void print_type(Type4 type); -void print_type4(Type4 val); +void litcopy(struct str_buf *sb, struct str_lit lit); +void num_str(struct str_buf *sb, unsigned long num, int radix); +void type_str(struct str_buf *sb, Type4 type); +void timestamp_str(struct str_buf *sb, uint16_t timestamp); +void group_label_str(struct str_buf *sb, Group *header); +void record_flags_str(struct str_buf *sb, Record *header); +void sb_write(int fp, struct str_buf sb_pre, struct str_buf sb_post); // Utilities Timestamp convert_ts(uint16_t ts); -void print_callback(Node n, void *data, void **carry_out); -void dc_size_cb(Node n, void *data, void **carry_out); -void formid_count_cb(Node n, void *data, void **carry_out); +void print_cb(Node n, void *data, void **carry_out); +void stats_cb(Node n, void *data, void **carry_out); void decompress_pre(Node n, void *data, void **carry_out); void decompress_post(Node n, void *data, void **carry_in); @@ -85,169 +90,6 @@ void espr_walk(char *data, size_t size, struct walker_callbacks cb) { assert(data == data_start + size); } -void espr_print(char *data, size_t size) { - struct walker_callbacks cb = { .pre = print_callback }; - espr_walk(data, size, cb); -} - -void print_callback(Node n, void *data, void **carry_out) { - (void)data; - (void)carry_out; - switch (n.type) { - case NT_GROUP: - print_group_header(n.header.group); - break; - case NT_RECORD: - print_record_header(n.header.record); - break; - default: - assert(false); // invalid node type - } -} - -size_t espr_decompressed_size(char *data, size_t size) { - size_t dc_size = 0; - struct walker_callbacks cb = { .pre = dc_size_cb, .data = &dc_size }; - espr_walk(data, size, cb); - return dc_size; -} - -// Adds the size of every node up, reading decompressed size from compressed -// records. -void dc_size_cb(Node n, void *data, void **carry_out) { - (void)carry_out; - size_t *dc_size = data; - switch (n.type) { - case NT_GROUP: - // Only add header size for groups, internals will be walked - *dc_size += sizeof(Group); - break; - case NT_RECORD: - // Add the whole record and header, records are leaf-ish - *dc_size += sizeof(Record); - if (n.header.record->flags & COMPRESSED_FLAG) { - // Read decompressed size - *dc_size += *((uint32_t *)n.data); - } - else - *dc_size += n.header.record->size; - break; - default: - assert(false); // invalid node type - } -} - -size_t espr_formid_count(char *data, size_t size) { - size_t count = 0; - struct walker_callbacks cb = { .pre = formid_count_cb, .data = &count }; - espr_walk(data, size, cb); - return count; -} - -/* FormID <-> Record relationship should be bijective. I do not believe - * groups have formids, and every record should have a unique formid, - * otherwise there would be clashes in the id space. - */ -void formid_count_cb(Node n, void *data, void **carry_out) { - (void)carry_out; - size_t *count = data; - if (n.type == NT_RECORD) { - (*count)++; - } -} - -struct decom { - char *buf; - size_t remaining; -}; - -void espr_decompress(char *data, size_t size, char *buf, size_t buf_size) { - struct decom s = { .buf = buf, .remaining = buf_size }; - struct walker_callbacks cb = - { .pre = decompress_pre, .post = decompress_post, .data = &s }; - espr_walk(data, size, cb); -} - -void decompress_pre(Node n, void *decom_ptr, void **carry_out) { - struct decom *d = decom_ptr; - - switch (n.type) { - case NT_RECORD: - // compressed record - if (n.header.record->flags & COMPRESSED_FLAG) { - // copy header - memcpy(d->buf, n.header.record, sizeof(Record)); - - // copied header reference - Record *header = (Record *)d->buf; - - // update decom struct - d->remaining -= sizeof(Record); - d->buf += sizeof(Record); - - // decompress directly into buffer - // first 4 bytes are the decompressed size - const uint32_t dc_size = *((uint32_t *)n.data); - uint32_t to_copy = dc_size; - uint32_t cur_size = n.header.record->size - sizeof(uint32_t); - char *data_start = n.data + sizeof(uint32_t); - int ret = uncompress( - (Bytef *)d->buf, - (uLongf *)&to_copy, - (Bytef *)data_start, - (uLong)cur_size - ); - assert(ret == Z_OK); - assert(to_copy == dc_size); - - // update decom struct - d->remaining -= dc_size; - d->buf += dc_size; - - // update header data size - header->size = dc_size; - - // unset compressed flag - header->flags &= ~COMPRESSED_FLAG; - } - else { - // copy record - size_t record_size = sizeof(Record) + n.header.record->size; - memcpy(d->buf, n.header.record, record_size); - - // update decom - d->remaining -= record_size; - d->buf += record_size; - } - break; - case NT_GROUP: - // copy header, contents will be copied while walking - memcpy(d->buf, n.header.group, sizeof(Group)); - - // save copied header location for post-walk group size recalc - *carry_out = (void *)d->buf; - - // update decom - d->buf += sizeof(Group); - d->remaining -= sizeof(Group); - - break; - default: - assert(false); // invalid node type - } -} - -void decompress_post(Node n, void *decom_ptr, void **carry_in) { - struct decom *d = decom_ptr; - - // only need to handle group resize - if (n.type == NT_GROUP) { - Group *g = (Group *)(*carry_in); - uint32_t new_size = (uint32_t)((char *)d->buf - (char *)g); - g->size = new_size; - } -} - /* Unknown data will be some concatenation of groups and records. * * `walk_concat` will call the appropriate walking function @@ -332,58 +174,299 @@ char *walk_record(char *data, struct walker_callbacks cb) { return data; } +void espr_print(char *data, size_t size) { + struct walker_callbacks cb = { .pre = print_cb }; + espr_walk(data, size, cb); +} + +void print_cb(Node n, void *data, void **carry_out) { + (void)data; + (void)carry_out; + switch (n.type) { + case NT_GROUP: + print_group_header(n.header.group); + break; + case NT_RECORD: + print_record_header(n.header.record); + break; + default: + assert(false); // invalid node type + } +} + +struct esp_stats espr_stats(char *data, size_t size) { + struct esp_stats stats = { 0 }; + struct walker_callbacks cb = { .pre = stats_cb, .data = &stats }; + espr_walk(data, size, cb); + return stats; +} + +/* Tallies up the group and record count. Calculates uncompressed size; groups + * only need their header size tallied as their data size will be handled by + * further walking of the tree. + */ +void stats_cb(Node n, void *data, void **carry_out) { + (void)carry_out; + struct esp_stats *stats = data; + switch (n.type) { + case NT_GROUP: + stats->group_count++; + stats->decompressed_size += sizeof(Group); + break; + case NT_RECORD: + stats->record_count++; + stats->decompressed_size += sizeof(Record); + if (n.header.record->flags & COMPRESSED_FLAG) { + // uncompressed size is stored in the first 4 bytes of data + stats->decompressed_size += *((uint32_t *)n.data); + } else { + stats->decompressed_size += n.header.record->size; + } + break; + default: + assert(false); // invalid node type + } +} + +struct decom { + char *buf; + size_t remaining; +}; + +void espr_decompress(char *data, size_t size, char *buf, size_t buf_size) { + struct decom s = { .buf = buf, .remaining = buf_size }; + struct walker_callbacks cb = + { .pre = decompress_pre, .post = decompress_post, .data = &s }; + espr_walk(data, size, cb); +} + +/* Handles the copying of groups and records, and the decompression of + * compressed record data. + * + * For groups it copies only the header as group data will be handled by further + * walking. The destination prior to copying will also be saved to carry_out for + * groups so that decompress_post can correctly update the size of the copied + * group. + * + * For uncompressed records it simply copies the entirety of the record to the + * destination. For compressed records in copies the header first and then + * directly decompresses the compressed record into the destination. + */ +void decompress_pre(Node n, void *decom_ptr, void **carry_out) { + struct decom *d = decom_ptr; + + switch (n.type) { + case NT_RECORD: + // compressed record + if (n.header.record->flags & COMPRESSED_FLAG) { + // copy header + memcpy(d->buf, n.header.record, sizeof(Record)); + + // copied header reference + Record *header = (Record *)d->buf; + + // update decom struct + d->remaining -= sizeof(Record); + d->buf += sizeof(Record); + + // decompress directly into buffer + // first 4 bytes are the decompressed size + const uint32_t dc_size = *((uint32_t *)n.data); + uint32_t to_copy = dc_size; + uint32_t cur_size = n.header.record->size - sizeof(uint32_t); + char *data_start = n.data + sizeof(uint32_t); + int ret = uncompress( + (Bytef *)d->buf, + (uLongf *)&to_copy, + (Bytef *)data_start, + (uLong)cur_size + ); + assert(ret == Z_OK); + assert(to_copy == dc_size); + + // update decom struct + d->remaining -= dc_size; + d->buf += dc_size; + + // update header data size + header->size = dc_size; + + // unset compressed flag + header->flags &= ~COMPRESSED_FLAG; + } + else { + // copy record + size_t record_size = sizeof(Record) + n.header.record->size; + memcpy(d->buf, n.header.record, record_size); + + // update decom + d->remaining -= record_size; + d->buf += record_size; + } + break; + case NT_GROUP: + // copy header, contents will be copied while walking + memcpy(d->buf, n.header.group, sizeof(Group)); + + // save copied header location for post-walk group size recalc + *carry_out = (void *)d->buf; + + // update decom + d->buf += sizeof(Group); + d->remaining -= sizeof(Group); + + break; + default: + assert(false); // invalid node type + } +} + +/* Handles recalculating group size after decompression. The location of the + * the group's copied header will be passed in in carry_in and can be used both + * to access the copied group header and calculate the new size of the group + * based on the difference between the current destination pointer and the + * group header pointer. + */ +void decompress_post(Node n, void *decom_ptr, void **carry_in) { + struct decom *d = decom_ptr; + + // only need to handle group resize + if (n.type == NT_GROUP) { + Group *g = (Group *)(*carry_in); + uint32_t new_size = (uint32_t)((char *)d->buf - (char *)g); + g->size = new_size; + } +} + void print_group_header(Group *header) { - printf("--- HEADER: GROUP ---\n"); - print_type(header->grup); - printf("Size: %u\n", header->size); - print_group_label(header); assert(header->type < GTS_SIZE); - printf("Group type: %s\n", group_type_strings[header->type]); - print_timestamp(header->timestamp); - printf("Version Control Info: %04x\n", header->vcinfo); - printf("Unknown: %08x\n", header->unknown); + // Guess at enough with significant margin + char buf[1024] = { 0 }; + const struct str_buf sb_pre = { .buf = buf, .size = sizeof(buf) }; + struct str_buf sb = sb_pre; + + // literals + struct str_lit + l1 = STR_LIT("--- HEADER: GROUP ---"), + l2 = STR_LIT("\nType: "), + l3 = STR_LIT("\nSize: "), + l4 = STR_LIT("\nLabel: "), + l5 = STR_LIT("\nGroup type: "), + l6 = STR_LIT("\nTimestamp: "), + l7 = STR_LIT("\nVersion Control Info: "), + l8 = STR_LIT("\nUnknown: "), + l9 = STR_LIT("\n"); + + struct str_lit gt = group_type_strings[header->type]; + + // construct output + litcopy(&sb, l1); + litcopy(&sb, l2); type_str(&sb, header->grup); + litcopy(&sb, l3); num_str(&sb, header->size, 10); + litcopy(&sb, l4); group_label_str(&sb, header); + litcopy(&sb, l5); litcopy(&sb, gt); + litcopy(&sb, l6); timestamp_str(&sb, header->timestamp); + litcopy(&sb, l7); num_str(&sb, header->vcinfo, 16); + litcopy(&sb, l8); num_str(&sb, header->unknown, 16); + litcopy(&sb, l9); + + sb_write(STDOUT_FILENO, sb_pre, sb); } -void print_record_header(Record *header) { - printf("--- HEADER: RECORD ---\n"); - print_type(header->type); - print_record_flags(header); - printf("FormID: %x\n", header->formid); - print_timestamp(header->timestamp); - printf("Version Control Info: %04x\n", header->vcinfo); - printf("Version: %u\n", header->version); - printf("Unknown: %08x\n", header->unknown); +void litcopy(struct str_buf *sb, struct str_lit lit) { + assert(sb->size >= lit.size); + memcpy(sb->buf, lit.lit, lit.size); + sb->size -= lit.size; + sb->buf += lit.size; } -void print_group_label(Group *header) { - printf("Label: "); +void num_str(struct str_buf *sb, unsigned long num, int radix) { + errno_t ret = _ultoa_s(num, sb->buf, sb->size, radix); + assert(ret == 0); + int len = (int)strlen(sb->buf); + sb->size -= len; + sb->buf += len; +} + +void type_str(struct str_buf *sb, Type4 type) { + assert(sb->size >= 4); + for (size_t i = 0; i != 4; i++) + sb->buf[i] = type.bytes[i]; + sb->buf += 4; + sb->size -= 4; +} + +void group_label_str(struct str_buf *sb, Group *header) { switch (header->type) { case GT_TOP: - print_type4(header->label.type); + type_str(sb, header->label.type); break; case GT_INTERIOR_CELL_BLOCK: case GT_INTERIOR_CELL_SUBBLOCK: - printf("%d", header->label.number); + num_str(sb, header->label.number, 10); + break; case GT_EXTERIOR_CELL_BLOCK: case GT_EXTERIOR_CELL_SUBBLOCK: - printf("X: %d, Y: %d", - header->label.coord[1], header->label.coord[0]); + litcopy(sb, LIT("X: ")); num_str(sb, header->label.coord[1], 10); + litcopy(sb, LIT("Y: ")); num_str(sb, header->label.coord[0], 10); + break; case GT_WORLD_CHILDREN: case GT_CELL_CHILDREN: case GT_TOPIC_CHILDREN: case GT_CELL_PERSISTENT_CHILDREN: case GT_CELL_TEMPORARY_CHILDREN: - printf("FormID[%x]", header->label.formid); + litcopy(sb, LIT("FormID[")); + num_str(sb, header->label.formid, 16); + litcopy(sb, LIT("]")); break; default: assert(false); // invalid group type } - printf("\n"); } -void print_record_flags(Record *header) { - printf("Flags:\n"); +void timestamp_str(struct str_buf *sb, uint16_t timestamp) { + Timestamp ts = convert_ts(timestamp); + litcopy(sb, LIT("20x")); num_str(sb, ts.year, 10); litcopy(sb, LIT("-")); + num_str(sb, ts.month, 10); litcopy(sb, LIT("-")); num_str(sb, ts.day, 10); +} +void sb_write(int fp, struct str_buf sb_pre, struct str_buf sb_post) { + int size = sb_pre.size - sb_post.size; + assert(size >= 0); + int ret = _write(fp, sb_pre.buf, size); + assert(ret == size); +} + +void print_record_header(Record *header) { + char buf[1024] = { 0 }; + const struct str_buf sb_pre = { .buf = buf, .size = sizeof(buf) }; + struct str_buf sb = sb_pre; + + const struct str_lit + l1 = LIT("--- HEADER: RECORD ---"), + l2 = LIT("\nType: "), + l3 = LIT("\nFlags: "), + l4 = LIT("\nFormID: "), + l5 = LIT("\nTimestamp: "), + l6 = LIT("\nVersion Control Info: "), + l7 = LIT("\nVersion: "), + l8 = LIT("\nUnknown: "), + l9 = LIT("\n"); + + litcopy(&sb, l1); + litcopy(&sb, l2); type_str(&sb, header->type); + litcopy(&sb, l3); record_flags_str(&sb, header); + litcopy(&sb, l4); num_str(&sb, header->formid, 16); + litcopy(&sb, l5); timestamp_str(&sb, header->timestamp); + litcopy(&sb, l6); num_str(&sb, header->vcinfo, 16); + litcopy(&sb, l7); num_str(&sb, header->version, 10); + litcopy(&sb, l8); num_str(&sb, header->unknown, 16); + litcopy(&sb, l9); + + sb_write(STDOUT_FILENO, sb_pre, sb); +} + +void record_flags_str(struct str_buf *sb, Record *header) { uint32_t flags = header->flags; const uint32_t type = header->type.uint; @@ -400,9 +483,9 @@ void print_record_flags(Record *header) { // will always be >= 0 as flags is not 0 int highest = 31 - __lzcnt(flags); assert(highest >= 0); - const char *const str = (*flag_lut)[highest]; - if (str) { - printf(" - %s\n", str); + const struct str_lit lit = (*flag_lut)[highest]; + if (lit.lit) { + litcopy(sb, LIT("\n - ")); litcopy(sb, lit); flags -= ((uint32_t)1) << highest; } else @@ -411,6 +494,7 @@ void print_record_flags(Record *header) { } } + // slow path if (flags != 0) { printf("\n\nOriginal flags: %08x\n", header->flags); printf("Unhandled flags: %08x\n", flags); @@ -418,24 +502,12 @@ void print_record_flags(Record *header) { } } -// This is the Skyrim SE timestamp format -void print_timestamp(uint16_t _ts) { - Timestamp ts = convert_ts(_ts); - printf("Timestamp: 20x%u-%02u-%02u\n", ts.year, ts.month, ts.day); -} - -void print_type(Type4 type) { - printf("Type: "); - print_type4(type); - printf("\n"); -} - -void print_type4(Type4 val) { - // invariant: printed i characters from val.bytes - for (size_t i = 0; i != 4; i++) - printf("%c", val.bytes[i]); -} - +/* Converts the bit-packed/encoded timestamp used in esp/esm files into day, + * month and year. See UESP for further explanation. + * + * This currently handles the timestamp format used in Skyrim.esm, but newer + * files apparently use a different format. This will need to be handled later. + */ Timestamp convert_ts(uint16_t ts) { /* const uint8_t day = (uint8_t)(ts & day_mask); diff --git a/espReader/espReader.vcxproj b/espReader/espReader.vcxproj index 785c1d6..0e16c11 100644 --- a/espReader/espReader.vcxproj +++ b/espReader/espReader.vcxproj @@ -141,7 +141,6 @@ - diff --git a/espReader/espReader.vcxproj.filters b/espReader/espReader.vcxproj.filters index f4fcec3..b9b535d 100644 --- a/espReader/espReader.vcxproj.filters +++ b/espReader/espReader.vcxproj.filters @@ -21,9 +21,6 @@ Header Files - - Header Files -