#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace ESM { namespace { auto tie(const ContItem& value) { return std::tie(value.mCount, value.mItem); } auto tie(const ESM::Region::SoundRef& value) { return std::tie(value.mSound, value.mChance); } auto tie(const ESM::QuickKeys::QuickKey& value) { return std::tie(value.mType, value.mId); } } inline bool operator==(const ESM::ContItem& lhs, const ESM::ContItem& rhs) { return tie(lhs) == tie(rhs); } inline std::ostream& operator<<(std::ostream& stream, const ESM::ContItem& value) { return stream << "ESM::ContItem {.mCount = " << value.mCount << ", .mItem = '" << value.mItem << "'}"; } inline bool operator==(const ESM::Region::SoundRef& lhs, const ESM::Region::SoundRef& rhs) { return tie(lhs) == tie(rhs); } inline std::ostream& operator<<(std::ostream& stream, const ESM::Region::SoundRef& value) { return stream << "ESM::Region::SoundRef {.mSound = '" << value.mSound << "', .mChance = " << value.mChance << "}"; } inline bool operator==(const ESM::QuickKeys::QuickKey& lhs, const ESM::QuickKeys::QuickKey& rhs) { return tie(lhs) == tie(rhs); } inline std::ostream& operator<<(std::ostream& stream, const ESM::QuickKeys::QuickKey& value) { return stream << "ESM::QuickKeys::QuickKey {.mType = '" << static_cast(value.mType) << "', .mId = " << value.mId << "}"; } namespace { using namespace ::testing; std::vector getFormats() { std::vector result({ CurrentContentFormatVersion, MaxLimitedSizeStringsFormatVersion, MaxStringRefIdFormatVersion, }); for (ESM::FormatVersion v = result.back() + 1; v <= ESM::CurrentSaveGameFormatVersion; ++v) result.push_back(v); return result; } constexpr std::uint32_t fakeRecordId = fourCC("FAKE"); template void save(const T& record, ESMWriter& writer) { record.save(writer); } void save(const CellRef& record, ESMWriter& writer) { record.save(writer, true); } template std::unique_ptr makeEsmStream(const T& record, FormatVersion formatVersion) { ESMWriter writer; auto stream = std::make_unique(); writer.setFormatVersion(formatVersion); writer.save(*stream); writer.startRecord(fakeRecordId); save(record, writer); writer.endRecord(fakeRecordId); return stream; } template > struct HasLoad : std::false_type { }; template struct HasLoad().load(std::declval()))>> : std::true_type { }; template auto load(ESMReader& reader, T& record) -> std::enable_if_t>::value> { record.load(reader); } template > struct HasLoadWithDelete : std::false_type { }; template struct HasLoadWithDelete().load(std::declval(), std::declval()))>> : std::true_type { }; template auto load(ESMReader& reader, T& record) -> std::enable_if_t>::value> { bool deleted = false; record.load(reader, deleted); } void load(ESMReader& reader, CellRef& record) { bool deleted = false; record.load(reader, deleted, true); } template void saveAndLoadRecord(const T& record, FormatVersion formatVersion, T& result) { ESMReader reader; reader.open(makeEsmStream(record, formatVersion), "stream"); ASSERT_TRUE(reader.hasMoreRecs()); ASSERT_EQ(reader.getRecName().toInt(), fakeRecordId); reader.getRecHeader(); load(reader, result); } struct Esm3SaveLoadRecordTest : public TestWithParam { std::minstd_rand mRandom; std::uniform_int_distribution mRefIdDistribution{ 'a', 'z' }; std::string generateRandomString(std::size_t size) { std::string value; while (value.size() < size) value.push_back(static_cast(mRefIdDistribution(mRandom))); return value; } RefId generateRandomRefId(std::size_t size = 33) { return RefId::stringRefId(generateRandomString(size)); } template void generateArray(T (&dst)[n]) { for (auto& v : dst) v = std::uniform_real_distribution{ -1.0f, 1.0f }(mRandom); } }; TEST_F(Esm3SaveLoadRecordTest, headerShouldNotChange) { const std::string author = generateRandomString(33); const std::string description = generateRandomString(257); auto stream = std::make_unique(); ESMWriter writer; writer.setAuthor(author); writer.setDescription(description); writer.setFormatVersion(CurrentSaveGameFormatVersion); writer.save(*stream); writer.close(); ESMReader reader; reader.open(std::move(stream), "stream"); EXPECT_EQ(reader.getAuthor(), author); EXPECT_EQ(reader.getDesc(), description); } TEST_F(Esm3SaveLoadRecordTest, containerContItemShouldSupportRefIdLongerThan32) { Container record; record.blank(); record.mInventory.mList.push_back(ESM::ContItem{ .mCount = 42, .mItem = generateRandomRefId(33) }); record.mInventory.mList.push_back(ESM::ContItem{ .mCount = 13, .mItem = generateRandomRefId(33) }); Container result; saveAndLoadRecord(record, CurrentSaveGameFormatVersion, result); EXPECT_EQ(result.mInventory.mList, record.mInventory.mList); } TEST_F(Esm3SaveLoadRecordTest, regionSoundRefShouldSupportRefIdLongerThan32) { Region record; record.blank(); record.mSoundList.push_back(ESM::Region::SoundRef{ .mSound = generateRandomRefId(33), .mChance = 42 }); record.mSoundList.push_back(ESM::Region::SoundRef{ .mSound = generateRandomRefId(33), .mChance = 13 }); Region result; saveAndLoadRecord(record, CurrentSaveGameFormatVersion, result); EXPECT_EQ(result.mSoundList, record.mSoundList); } TEST_F(Esm3SaveLoadRecordTest, scriptSoundRefShouldSupportRefIdLongerThan32) { Script record; record.blank(); record.mId = generateRandomRefId(33); record.mData.mNumShorts = 42; Script result; saveAndLoadRecord(record, CurrentSaveGameFormatVersion, result); EXPECT_EQ(result.mId, record.mId); EXPECT_EQ(result.mData.mNumShorts, record.mData.mNumShorts); } TEST_P(Esm3SaveLoadRecordTest, playerShouldNotChange) { // Player state is not saved to vanilla ESM format. if (GetParam() == CurrentContentFormatVersion) return; std::minstd_rand random; Player record{}; record.mObject.blank(); record.mBirthsign = generateRandomRefId(); record.mObject.mRef.mRefID = generateRandomRefId(); std::generate_n(std::inserter(record.mPreviousItems, record.mPreviousItems.end()), 2, [&] { return std::make_pair(generateRandomRefId(), generateRandomRefId()); }); record.mCellId = ESM::RefId::esm3ExteriorCell(0, 0); generateArray(record.mLastKnownExteriorPosition); record.mHasMark = true; record.mMarkedCell = ESM::RefId::esm3ExteriorCell(0, 0); generateArray(record.mMarkedPosition.pos); generateArray(record.mMarkedPosition.rot); record.mCurrentCrimeId = 42; record.mPaidCrimeId = 13; Player result; saveAndLoadRecord(record, GetParam(), result); EXPECT_EQ(record.mObject.mRef.mRefID, result.mObject.mRef.mRefID); EXPECT_EQ(record.mBirthsign, result.mBirthsign); EXPECT_EQ(record.mPreviousItems, result.mPreviousItems); EXPECT_EQ(record.mPreviousItems, result.mPreviousItems); EXPECT_EQ(record.mCellId, result.mCellId); EXPECT_THAT(record.mLastKnownExteriorPosition, ElementsAreArray(result.mLastKnownExteriorPosition)); EXPECT_EQ(record.mHasMark, result.mHasMark); EXPECT_EQ(record.mMarkedCell, result.mMarkedCell); EXPECT_THAT(record.mMarkedPosition.pos, ElementsAreArray(result.mMarkedPosition.pos)); EXPECT_THAT(record.mMarkedPosition.rot, ElementsAreArray(result.mMarkedPosition.rot)); EXPECT_EQ(record.mCurrentCrimeId, result.mCurrentCrimeId); EXPECT_EQ(record.mPaidCrimeId, result.mPaidCrimeId); } TEST_P(Esm3SaveLoadRecordTest, cellRefShouldNotChange) { CellRef record; record.blank(); record.mRefNum.mIndex = std::numeric_limits::max(); record.mRefNum.mContentFile = std::numeric_limits::max(); record.mRefID = generateRandomRefId(); record.mScale = 2; record.mOwner = generateRandomRefId(); record.mGlobalVariable = generateRandomString(100); record.mSoul = generateRandomRefId(); record.mFaction = generateRandomRefId(); record.mFactionRank = std::numeric_limits::max(); record.mChargeInt = std::numeric_limits::max(); record.mEnchantmentCharge = std::numeric_limits::max(); record.mGoldValue = std::numeric_limits::max(); record.mTeleport = true; generateArray(record.mDoorDest.pos); generateArray(record.mDoorDest.rot); record.mDestCell = generateRandomString(100); record.mLockLevel = 0; record.mIsLocked = true; record.mKey = generateRandomRefId(); record.mTrap = generateRandomRefId(); record.mReferenceBlocked = std::numeric_limits::max(); generateArray(record.mPos.pos); generateArray(record.mPos.rot); CellRef result; saveAndLoadRecord(record, GetParam(), result); EXPECT_EQ(record.mRefNum.mIndex, result.mRefNum.mIndex); EXPECT_EQ(record.mRefNum.mContentFile, result.mRefNum.mContentFile); EXPECT_EQ(record.mRefID, result.mRefID); EXPECT_EQ(record.mScale, result.mScale); EXPECT_EQ(record.mOwner, result.mOwner); EXPECT_EQ(record.mGlobalVariable, result.mGlobalVariable); EXPECT_EQ(record.mSoul, result.mSoul); EXPECT_EQ(record.mFaction, result.mFaction); EXPECT_EQ(record.mFactionRank, result.mFactionRank); EXPECT_EQ(record.mChargeInt, result.mChargeInt); EXPECT_EQ(record.mEnchantmentCharge, result.mEnchantmentCharge); EXPECT_EQ(record.mGoldValue, result.mGoldValue); EXPECT_EQ(record.mTeleport, result.mTeleport); EXPECT_EQ(record.mDoorDest, result.mDoorDest); EXPECT_EQ(record.mDestCell, result.mDestCell); EXPECT_EQ(record.mLockLevel, result.mLockLevel); EXPECT_EQ(record.mIsLocked, result.mIsLocked); EXPECT_EQ(record.mKey, result.mKey); EXPECT_EQ(record.mTrap, result.mTrap); EXPECT_EQ(record.mReferenceBlocked, result.mReferenceBlocked); EXPECT_EQ(record.mPos, result.mPos); } TEST_P(Esm3SaveLoadRecordTest, creatureStatsShouldNotChange) { CreatureStats record; record.blank(); record.mLastHitAttemptObject = generateRandomRefId(); record.mLastHitObject = generateRandomRefId(); CreatureStats result; saveAndLoadRecord(record, GetParam(), result); EXPECT_EQ(record.mLastHitAttemptObject, result.mLastHitAttemptObject); EXPECT_EQ(record.mLastHitObject, result.mLastHitObject); } TEST_P(Esm3SaveLoadRecordTest, containerShouldNotChange) { Container record; record.blank(); record.mId = generateRandomRefId(); record.mInventory.mList.push_back(ESM::ContItem{ .mCount = 42, .mItem = generateRandomRefId(32) }); record.mInventory.mList.push_back(ESM::ContItem{ .mCount = 13, .mItem = generateRandomRefId(32) }); Container result; saveAndLoadRecord(record, GetParam(), result); EXPECT_EQ(result.mId, record.mId); EXPECT_EQ(result.mInventory.mList, record.mInventory.mList); } TEST_P(Esm3SaveLoadRecordTest, regionShouldNotChange) { Region record; record.blank(); record.mId = generateRandomRefId(); record.mSoundList.push_back(ESM::Region::SoundRef{ .mSound = generateRandomRefId(32), .mChance = 42 }); record.mSoundList.push_back(ESM::Region::SoundRef{ .mSound = generateRandomRefId(32), .mChance = 13 }); Region result; saveAndLoadRecord(record, GetParam(), result); EXPECT_EQ(result.mId, record.mId); EXPECT_EQ(result.mSoundList, record.mSoundList); } TEST_P(Esm3SaveLoadRecordTest, scriptShouldNotChange) { Script record; record.blank(); record.mId = generateRandomRefId(32); record.mData.mNumShorts = 42; Script result; saveAndLoadRecord(record, GetParam(), result); EXPECT_EQ(result.mId, record.mId); EXPECT_EQ(result.mData.mNumShorts, record.mData.mNumShorts); } TEST_P(Esm3SaveLoadRecordTest, quickKeysShouldNotChange) { const QuickKeys record { .mKeys = { { .mType = QuickKeys::Type::Magic, .mId = generateRandomRefId(32), }, { .mType = QuickKeys::Type::MagicItem, .mId = generateRandomRefId(32), }, }, }; QuickKeys result; saveAndLoadRecord(record, GetParam(), result); EXPECT_EQ(result.mKeys, record.mKeys); } TEST_P(Esm3SaveLoadRecordTest, dialogueShouldNotChange) { Dialogue record; record.blank(); record.mStringId = generateRandomString(32); record.mId = ESM::RefId::stringRefId(record.mStringId); Dialogue result; saveAndLoadRecord(record, GetParam(), result); EXPECT_EQ(result.mId, record.mId); EXPECT_EQ(result.mStringId, record.mStringId); } INSTANTIATE_TEST_SUITE_P(FormatVersions, Esm3SaveLoadRecordTest, ValuesIn(getFormats())); } }