Add unit tests for saving and loading ESM3 Land

esm4-texture
elsid 7 months ago
parent 24d8accee7
commit 044748725c
No known key found for this signature in database
GPG Key ID: 4DE04C198CBA7625

@ -866,7 +866,9 @@ namespace EsmTool
if (const ESM::Land::LandData* data = mData.getLandData(mData.mDataTypes)) if (const ESM::Land::LandData* data = mData.getLandData(mData.mDataTypes))
{ {
std::cout << " Height Offset: " << data->mHeightOffset << std::endl; std::cout << " MinHeight: " << data->mMinHeight << std::endl;
std::cout << " MaxHeight: " << data->mMaxHeight << std::endl;
std::cout << " DataLoaded: " << data->mDataLoaded << std::endl;
} }
mData.unloadData(); mData.unloadData();
std::cout << " Deleted: " << mIsDeleted << std::endl; std::cout << " Deleted: " << mIsDeleted << std::endl;

@ -7,6 +7,7 @@
#include <components/esm3/loadcont.hpp> #include <components/esm3/loadcont.hpp>
#include <components/esm3/loaddial.hpp> #include <components/esm3/loaddial.hpp>
#include <components/esm3/loadinfo.hpp> #include <components/esm3/loadinfo.hpp>
#include <components/esm3/loadland.hpp>
#include <components/esm3/loadregn.hpp> #include <components/esm3/loadregn.hpp>
#include <components/esm3/loadscpt.hpp> #include <components/esm3/loadscpt.hpp>
#include <components/esm3/loadweap.hpp> #include <components/esm3/loadweap.hpp>
@ -20,8 +21,8 @@
#include <array> #include <array>
#include <limits> #include <limits>
#include <memory> #include <memory>
#include <numeric>
#include <random> #include <random>
#include <type_traits>
namespace ESM namespace ESM
{ {
@ -172,6 +173,17 @@ namespace ESM
reader.getComposite(record); reader.getComposite(record);
} }
void load(ESMReader& reader, Land& record)
{
bool deleted = false;
record.load(reader, deleted);
if (deleted)
return;
record.mLandData = std::make_unique<LandRecordData>();
reader.restoreContext(record.mContext);
loadLandRecordData(record.mDataTypes, reader, *record.mLandData);
}
template <typename T> template <typename T>
void saveAndLoadRecord(const T& record, FormatVersion formatVersion, T& result) void saveAndLoadRecord(const T& record, FormatVersion formatVersion, T& result)
{ {
@ -681,6 +693,43 @@ namespace ESM
} }
} }
TEST_P(Esm3SaveLoadRecordTest, landShouldNotChange)
{
LandRecordData data;
std::iota(data.mHeights.begin(), data.mHeights.end(), 1);
std::for_each(data.mHeights.begin(), data.mHeights.end(), [](float& v) { v *= Land::sHeightScale; });
data.mMinHeight = *std::min_element(data.mHeights.begin(), data.mHeights.end());
data.mMaxHeight = *std::max_element(data.mHeights.begin(), data.mHeights.end());
std::iota(data.mNormals.begin(), data.mNormals.end(), 2);
std::iota(data.mTextures.begin(), data.mTextures.end(), 3);
std::iota(data.mColours.begin(), data.mColours.end(), 4);
data.mDataLoaded = Land::DATA_VNML | Land::DATA_VHGT | Land::DATA_VCLR | Land::DATA_VTEX;
Land record;
record.mFlags = 42;
record.mX = 2;
record.mY = 3;
record.mDataTypes = Land::DATA_VNML | Land::DATA_VHGT | Land::DATA_WNAM | Land::DATA_VCLR | Land::DATA_VTEX;
generateWnam(data.mHeights, record.mWnam);
record.mLandData = std::make_unique<LandRecordData>(data);
Land result;
saveAndLoadRecord(record, GetParam(), result);
EXPECT_EQ(result.mFlags, record.mFlags);
EXPECT_EQ(result.mX, record.mX);
EXPECT_EQ(result.mY, record.mY);
EXPECT_EQ(result.mDataTypes, record.mDataTypes);
EXPECT_EQ(result.mWnam, record.mWnam);
EXPECT_EQ(result.mLandData->mHeights, record.mLandData->mHeights);
EXPECT_EQ(result.mLandData->mMinHeight, record.mLandData->mMinHeight);
EXPECT_EQ(result.mLandData->mMaxHeight, record.mLandData->mMaxHeight);
EXPECT_EQ(result.mLandData->mNormals, record.mLandData->mNormals);
EXPECT_EQ(result.mLandData->mTextures, record.mLandData->mTextures);
EXPECT_EQ(result.mLandData->mColours, record.mLandData->mColours);
EXPECT_EQ(result.mLandData->mDataLoaded, record.mLandData->mDataLoaded);
}
INSTANTIATE_TEST_SUITE_P(FormatVersions, Esm3SaveLoadRecordTest, ValuesIn(getFormats())); INSTANTIATE_TEST_SUITE_P(FormatVersions, Esm3SaveLoadRecordTest, ValuesIn(getFormats()));
} }
} }

@ -20,8 +20,6 @@ namespace ESM
// total number of textures per land // total number of textures per land
static constexpr unsigned sLandNumTextures = sLandTextureSize * sLandTextureSize; static constexpr unsigned sLandNumTextures = sLandTextureSize * sLandTextureSize;
// Initial reference height for the first vertex, only needed for filling mHeights
float mHeightOffset = 0;
// Height in world space for each vertex // Height in world space for each vertex
std::array<float, sLandNumVerts> mHeights; std::array<float, sLandNumVerts> mHeights;
float mMinHeight = 0; float mMinHeight = 0;

@ -40,15 +40,15 @@ namespace ESM
// Loads data and marks it as loaded. Return true if data is actually loaded from reader, false otherwise // Loads data and marks it as loaded. Return true if data is actually loaded from reader, false otherwise
// including the case when data is already loaded. // including the case when data is already loaded.
bool condLoad(ESMReader& reader, int flags, int& targetFlags, int dataFlag, auto& in) bool condLoad(ESMReader& reader, int dataTypes, int& targetDataTypes, int dataFlag, auto& in)
{ {
if ((targetFlags & dataFlag) == 0 && (flags & dataFlag) != 0) if ((targetDataTypes & dataFlag) == 0 && (dataTypes & dataFlag) != 0)
{ {
if constexpr (std::is_same_v<std::remove_cvref_t<decltype(in)>, VHGT>) if constexpr (std::is_same_v<std::remove_cvref_t<decltype(in)>, VHGT>)
reader.getSubComposite(in); reader.getSubComposite(in);
else else
reader.getHT(in); reader.getHT(in);
targetFlags |= dataFlag; targetDataTypes |= dataFlag;
return true; return true;
} }
reader.skipHSub(); reader.skipHSub();
@ -150,13 +150,13 @@ namespace ESM
if (mDataTypes & Land::DATA_VHGT) if (mDataTypes & Land::DATA_VHGT)
{ {
VHGT offsets; VHGT offsets;
offsets.mHeightOffset = mLandData->mHeights[0] / HEIGHT_SCALE; offsets.mHeightOffset = mLandData->mHeights[0] / sHeightScale;
float prevY = mLandData->mHeights[0]; float prevY = mLandData->mHeights[0];
size_t number = 0; // avoid multiplication size_t number = 0; // avoid multiplication
for (unsigned i = 0; i < LandRecordData::sLandSize; ++i) for (unsigned i = 0; i < LandRecordData::sLandSize; ++i)
{ {
float diff = (mLandData->mHeights[number] - prevY) / HEIGHT_SCALE; float diff = (mLandData->mHeights[number] - prevY) / sHeightScale;
offsets.mHeightData[number] offsets.mHeightData[number]
= diff >= 0 ? static_cast<std::int8_t>(diff + 0.5) : static_cast<std::int8_t>(diff - 0.5); = diff >= 0 ? static_cast<std::int8_t>(diff + 0.5) : static_cast<std::int8_t>(diff - 0.5);
@ -165,7 +165,7 @@ namespace ESM
for (unsigned j = 1; j < LandRecordData::sLandSize; ++j) for (unsigned j = 1; j < LandRecordData::sLandSize; ++j)
{ {
diff = (mLandData->mHeights[number] - prevX) / HEIGHT_SCALE; diff = (mLandData->mHeights[number] - prevX) / sHeightScale;
offsets.mHeightData[number] offsets.mHeightData[number]
= diff >= 0 ? static_cast<std::int8_t>(diff + 0.5) : static_cast<std::int8_t>(diff - 0.5); = diff >= 0 ? static_cast<std::int8_t>(diff + 0.5) : static_cast<std::int8_t>(diff - 0.5);
@ -178,23 +178,8 @@ namespace ESM
if (mDataTypes & Land::DATA_WNAM) if (mDataTypes & Land::DATA_WNAM)
{ {
// Generate WNAM record // Generate WNAM record
std::int8_t wnam[LAND_GLOBAL_MAP_LOD_SIZE]; std::array<std::int8_t, sGlobalMapLodSize> wnam;
constexpr float max = std::numeric_limits<std::int8_t>::max(); generateWnam(mLandData->mHeights, wnam);
constexpr float min = std::numeric_limits<std::int8_t>::min();
constexpr float vertMult
= static_cast<float>(LandRecordData::sLandSize - 1) / LAND_GLOBAL_MAP_LOD_SIZE_SQRT;
for (unsigned row = 0; row < LAND_GLOBAL_MAP_LOD_SIZE_SQRT; ++row)
{
for (unsigned col = 0; col < LAND_GLOBAL_MAP_LOD_SIZE_SQRT; ++col)
{
float height
= mLandData->mHeights[static_cast<size_t>(row * vertMult) * LandRecordData::sLandSize
+ static_cast<size_t>(col * vertMult)];
height /= height > 0 ? 128.f : 16.f;
height = std::clamp(height, min, max);
wnam[row * LAND_GLOBAL_MAP_LOD_SIZE_SQRT + col] = static_cast<std::int8_t>(height);
}
}
esm.writeHNT("WNAM", wnam); esm.writeHNT("WNAM", wnam);
} }
if (mDataTypes & Land::DATA_VCLR) if (mDataTypes & Land::DATA_VCLR)
@ -219,7 +204,6 @@ namespace ESM
if (mLandData == nullptr) if (mLandData == nullptr)
mLandData = std::make_unique<LandData>(); mLandData = std::make_unique<LandData>();
mLandData->mHeightOffset = 0;
mLandData->mHeights.fill(0); mLandData->mHeights.fill(0);
mLandData->mMinHeight = 0; mLandData->mMinHeight = 0;
mLandData->mMaxHeight = 0; mLandData->mMaxHeight = 0;
@ -239,20 +223,20 @@ namespace ESM
mContext.filename.clear(); mContext.filename.clear();
} }
void Land::loadData(int flags) const void Land::loadData(int dataTypes) const
{ {
if (mLandData == nullptr) if (mLandData == nullptr)
mLandData = std::make_unique<LandData>(); mLandData = std::make_unique<LandData>();
loadData(flags, *mLandData); loadData(dataTypes, *mLandData);
} }
void Land::loadData(int flags, LandData& data) const void Land::loadData(int dataTypes, LandData& data) const
{ {
// Try to load only available data // Try to load only available data
flags = flags & mDataTypes; dataTypes = dataTypes & mDataTypes;
// Return if all required data is loaded // Return if all required data is loaded
if ((data.mDataLoaded & flags) == flags) if ((data.mDataLoaded & dataTypes) == dataTypes)
{ {
return; return;
} }
@ -269,57 +253,7 @@ namespace ESM
ESMReader reader; ESMReader reader;
reader.restoreContext(mContext); reader.restoreContext(mContext);
if (reader.isNextSub("VNML")) loadLandRecordData(dataTypes, reader, data);
{
condLoad(reader, flags, data.mDataLoaded, DATA_VNML, data.mNormals);
}
if (reader.isNextSub("VHGT"))
{
VHGT vhgt;
if (condLoad(reader, flags, data.mDataLoaded, DATA_VHGT, vhgt))
{
data.mMinHeight = std::numeric_limits<float>::max();
data.mMaxHeight = -std::numeric_limits<float>::max();
float rowOffset = vhgt.mHeightOffset;
for (unsigned y = 0; y < LandRecordData::sLandSize; y++)
{
rowOffset += vhgt.mHeightData[y * LandRecordData::sLandSize];
data.mHeights[y * LandRecordData::sLandSize] = rowOffset * HEIGHT_SCALE;
if (rowOffset * HEIGHT_SCALE > data.mMaxHeight)
data.mMaxHeight = rowOffset * HEIGHT_SCALE;
if (rowOffset * HEIGHT_SCALE < data.mMinHeight)
data.mMinHeight = rowOffset * HEIGHT_SCALE;
float colOffset = rowOffset;
for (unsigned x = 1; x < LandRecordData::sLandSize; x++)
{
colOffset += vhgt.mHeightData[y * LandRecordData::sLandSize + x];
data.mHeights[x + y * LandRecordData::sLandSize] = colOffset * HEIGHT_SCALE;
if (colOffset * HEIGHT_SCALE > data.mMaxHeight)
data.mMaxHeight = colOffset * HEIGHT_SCALE;
if (colOffset * HEIGHT_SCALE < data.mMinHeight)
data.mMinHeight = colOffset * HEIGHT_SCALE;
}
}
}
}
if (reader.isNextSub("WNAM"))
reader.skipHSub();
if (reader.isNextSub("VCLR"))
condLoad(reader, flags, data.mDataLoaded, DATA_VCLR, data.mColours);
if (reader.isNextSub("VTEX"))
{
uint16_t vtex[LandRecordData::sLandNumTextures];
if (condLoad(reader, flags, data.mDataLoaded, DATA_VTEX, vtex))
{
transposeTextureData(vtex, data.mTextures.data());
}
}
} }
void Land::unloadData() void Land::unloadData()
@ -367,4 +301,75 @@ namespace ESM
mDataTypes |= flags; mDataTypes |= flags;
mLandData->mDataLoaded |= flags; mLandData->mDataLoaded |= flags;
} }
void loadLandRecordData(int dataTypes, ESMReader& reader, LandRecordData& data)
{
if (reader.isNextSub("VNML"))
condLoad(reader, dataTypes, data.mDataLoaded, Land::DATA_VNML, data.mNormals);
if (reader.isNextSub("VHGT"))
{
VHGT vhgt;
if (condLoad(reader, dataTypes, data.mDataLoaded, Land::DATA_VHGT, vhgt))
{
data.mMinHeight = std::numeric_limits<float>::max();
data.mMaxHeight = -std::numeric_limits<float>::max();
float rowOffset = vhgt.mHeightOffset;
for (unsigned y = 0; y < LandRecordData::sLandSize; y++)
{
rowOffset += vhgt.mHeightData[y * LandRecordData::sLandSize];
data.mHeights[y * LandRecordData::sLandSize] = rowOffset * Land::sHeightScale;
if (rowOffset * Land::sHeightScale > data.mMaxHeight)
data.mMaxHeight = rowOffset * Land::sHeightScale;
if (rowOffset * Land::sHeightScale < data.mMinHeight)
data.mMinHeight = rowOffset * Land::sHeightScale;
float colOffset = rowOffset;
for (unsigned x = 1; x < LandRecordData::sLandSize; x++)
{
colOffset += vhgt.mHeightData[y * LandRecordData::sLandSize + x];
data.mHeights[x + y * LandRecordData::sLandSize] = colOffset * Land::sHeightScale;
if (colOffset * Land::sHeightScale > data.mMaxHeight)
data.mMaxHeight = colOffset * Land::sHeightScale;
if (colOffset * Land::sHeightScale < data.mMinHeight)
data.mMinHeight = colOffset * Land::sHeightScale;
}
}
}
}
if (reader.isNextSub("WNAM"))
reader.skipHSub();
if (reader.isNextSub("VCLR"))
condLoad(reader, dataTypes, data.mDataLoaded, Land::DATA_VCLR, data.mColours);
if (reader.isNextSub("VTEX"))
{
std::uint16_t vtex[LandRecordData::sLandNumTextures];
if (condLoad(reader, dataTypes, data.mDataLoaded, Land::DATA_VTEX, vtex))
transposeTextureData(vtex, data.mTextures.data());
}
}
void generateWnam(const std::array<float, LandRecordData::sLandNumVerts>& heights,
std::array<std::int8_t, Land::sGlobalMapLodSize>& wnam)
{
constexpr float max = std::numeric_limits<std::int8_t>::max();
constexpr float min = std::numeric_limits<std::int8_t>::min();
constexpr float vertMult = static_cast<float>(LandRecordData::sLandSize - 1) / Land::sGlobalMapLodSizeSqrt;
for (std::size_t row = 0; row < Land::sGlobalMapLodSizeSqrt; ++row)
{
for (std::size_t col = 0; col < Land::sGlobalMapLodSizeSqrt; ++col)
{
float height = heights[static_cast<std::size_t>(row * vertMult) * LandRecordData::sLandSize
+ static_cast<std::size_t>(col * vertMult)];
height /= height > 0 ? 128.f : 16.f;
height = std::clamp(height, min, max);
wnam[row * Land::sGlobalMapLodSizeSqrt + col] = static_cast<std::int8_t>(height);
}
}
}
} }

@ -78,7 +78,7 @@ namespace ESM
// total number of vertices // total number of vertices
static constexpr int LAND_NUM_VERTS = LandRecordData::sLandNumVerts; static constexpr int LAND_NUM_VERTS = LandRecordData::sLandNumVerts;
static constexpr int HEIGHT_SCALE = 8; static constexpr int sHeightScale = 8;
// number of textures per side of land // number of textures per side of land
static constexpr int LAND_TEXTURE_SIZE = LandRecordData::sLandTextureSize; static constexpr int LAND_TEXTURE_SIZE = LandRecordData::sLandTextureSize;
@ -86,23 +86,25 @@ namespace ESM
// total number of textures per land // total number of textures per land
static constexpr int LAND_NUM_TEXTURES = LandRecordData::sLandNumTextures; static constexpr int LAND_NUM_TEXTURES = LandRecordData::sLandNumTextures;
static constexpr unsigned LAND_GLOBAL_MAP_LOD_SIZE = 81; static constexpr std::size_t sGlobalMapLodSizeSqrt = 9;
static constexpr unsigned LAND_GLOBAL_MAP_LOD_SIZE_SQRT = 9; static constexpr std::size_t sGlobalMapLodSize = sGlobalMapLodSizeSqrt * sGlobalMapLodSizeSqrt;
using LandData = ESM::LandRecordData; using LandData = ESM::LandRecordData;
// low-LOD heightmap (used for rendering the global map) // low-LOD heightmap (used for rendering the global map)
std::array<std::int8_t, LAND_GLOBAL_MAP_LOD_SIZE> mWnam; std::array<std::int8_t, sGlobalMapLodSize> mWnam;
mutable std::unique_ptr<LandData> mLandData;
void load(ESMReader& esm, bool& isDeleted); void load(ESMReader& esm, bool& isDeleted);
void save(ESMWriter& esm, bool isDeleted = false) const; void save(ESMWriter& esm, bool isDeleted = false) const;
void blank(); void blank();
void loadData(int flags) const; void loadData(int dataTypes) const;
void loadData(int flags, LandData& data) const; void loadData(int dataTypes, LandData& data) const;
/** /**
* Frees memory allocated for mLandData * Frees memory allocated for mLandData
@ -127,10 +129,12 @@ namespace ESM
/// ///
/// \note Added data fields will be uninitialised /// \note Added data fields will be uninitialised
void add(int flags); void add(int flags);
private:
mutable std::unique_ptr<LandData> mLandData;
}; };
void loadLandRecordData(int dataTypes, ESMReader& reader, LandRecordData& data);
void generateWnam(const std::array<float, LandRecordData::sLandNumVerts>& heights,
std::array<std::int8_t, Land::sGlobalMapLodSize>& wnam);
} }
#endif #endif

Loading…
Cancel
Save