Merge branch 'esm_format_version' into 'master'

Name all custom ESM format versions and add tests

See merge request OpenMW/openmw!2712
7220-lua-add-a-general-purpose-lexical-parser
psi29a 2 years ago
commit a31d381611

@ -347,7 +347,7 @@ namespace ESSImport
ESM::ESMWriter writer; ESM::ESMWriter writer;
writer.setFormat(ESM::SavedGame::sCurrentFormat); writer.setFormatVersion(ESM::CurrentSaveGameFormatVersion);
std::ofstream stream(mOutFile, std::ios::out | std::ios::binary); std::ofstream stream(mOutFile, std::ios::out | std::ios::binary);
// all unused // all unused

@ -91,7 +91,7 @@ void CSMDoc::WriteHeaderStage::perform(int stage, Messages& messages)
// ESM::Header::CurrentFormat is `1` but since new records are not yet used in opencs // ESM::Header::CurrentFormat is `1` but since new records are not yet used in opencs
// we use the format `0` for compatibility with old versions. // we use the format `0` for compatibility with old versions.
mState.getWriter().setFormat(0); mState.getWriter().setFormatVersion(ESM::DefaultFormatVersion);
} }
else else
{ {

@ -2232,7 +2232,7 @@ namespace CSMWorld
{ {
} }
QVariant get(const Record<ESXRecordT>& record) const override { return record.get().mFormat; } QVariant get(const Record<ESXRecordT>& record) const override { return record.get().mFormatVersion; }
bool isEditable() const override { return false; } bool isEditable() const override { return false; }
}; };

@ -8,21 +8,21 @@ void CSMWorld::MetaData::blank()
{ {
// ESM::Header::CurrentFormat is `1` but since new records are not yet used in opencs // ESM::Header::CurrentFormat is `1` but since new records are not yet used in opencs
// we use the format `0` for compatibility with old versions. // we use the format `0` for compatibility with old versions.
mFormat = 0; mFormatVersion = ESM::DefaultFormatVersion;
mAuthor.clear(); mAuthor.clear();
mDescription.clear(); mDescription.clear();
} }
void CSMWorld::MetaData::load(ESM::ESMReader& esm) void CSMWorld::MetaData::load(ESM::ESMReader& esm)
{ {
mFormat = esm.getHeader().mFormat; mFormatVersion = esm.getHeader().mFormatVersion;
mAuthor = esm.getHeader().mData.author; mAuthor = esm.getHeader().mData.author;
mDescription = esm.getHeader().mData.desc; mDescription = esm.getHeader().mData.desc;
} }
void CSMWorld::MetaData::save(ESM::ESMWriter& esm) const void CSMWorld::MetaData::save(ESM::ESMWriter& esm) const
{ {
esm.setFormat(mFormat); esm.setFormatVersion(mFormatVersion);
esm.setAuthor(mAuthor); esm.setAuthor(mAuthor);
esm.setDescription(mDescription); esm.setDescription(mDescription);
} }

@ -2,6 +2,8 @@
#define CSM_WOLRD_METADATA_H #define CSM_WOLRD_METADATA_H
#include <components/esm/refid.hpp> #include <components/esm/refid.hpp>
#include <components/esm3/formatversion.hpp>
#include <string> #include <string>
namespace ESM namespace ESM
@ -16,7 +18,7 @@ namespace CSMWorld
{ {
ESM::RefId mId; ESM::RefId mId;
int mFormat; ESM::FormatVersion mFormatVersion;
std::string mAuthor; std::string mAuthor;
std::string mDescription; std::string mDescription;

@ -250,7 +250,7 @@ void MWState::StateManager::saveGame(const std::string& description, const Slot*
for (const std::string& contentFile : MWBase::Environment::get().getWorld()->getContentFiles()) for (const std::string& contentFile : MWBase::Environment::get().getWorld()->getContentFiles())
writer.addMaster(contentFile, 0); // not using the size information anyway -> use value of 0 writer.addMaster(contentFile, 0); // not using the size information anyway -> use value of 0
writer.setFormat(ESM::SavedGame::sCurrentFormat); writer.setFormatVersion(ESM::CurrentSaveGameFormatVersion);
// all unused // all unused
writer.setVersion(0); writer.setVersion(0);
@ -400,7 +400,7 @@ void MWState::StateManager::loadGame(const Character* character, const std::file
ESM::ESMReader reader; ESM::ESMReader reader;
reader.open(filepath); reader.open(filepath);
if (reader.getFormat() > ESM::SavedGame::sCurrentFormat) if (reader.getFormatVersion() > ESM::CurrentSaveGameFormatVersion)
throw std::runtime_error( throw std::runtime_error(
"This save file was created using a newer version of OpenMW and is thus not supported. Please upgrade " "This save file was created using a newer version of OpenMW and is thus not supported. Please upgrade "
"to the newest OpenMW version to load this file."); "to the newest OpenMW version to load this file.");

@ -55,7 +55,7 @@ namespace MWWorld
if (!mMasterFileFormat.has_value() if (!mMasterFileFormat.has_value()
&& (Misc::StringUtils::ciEndsWith(reader->getName().u8string(), u8".esm") && (Misc::StringUtils::ciEndsWith(reader->getName().u8string(), u8".esm")
|| Misc::StringUtils::ciEndsWith(reader->getName().u8string(), u8".omwgame"))) || Misc::StringUtils::ciEndsWith(reader->getName().u8string(), u8".omwgame")))
mMasterFileFormat = reader->getFormat(); mMasterFileFormat = reader->getFormatVersion();
break; break;
} }
case ESM::Format::Tes4: case ESM::Format::Tes4:

@ -327,10 +327,10 @@ namespace MWWorld
// this is the one object we can not silently drop. // this is the one object we can not silently drop.
throw std::runtime_error("invalid player state record (object state)"); throw std::runtime_error("invalid player state record (object state)");
} }
if (reader.getFormat() < 17) if (reader.getFormatVersion() <= ESM::MaxClearModifiersFormatVersion)
convertMagicEffects( convertMagicEffects(
player.mObject.mCreatureStats, player.mObject.mInventory, &player.mObject.mNpcStats); player.mObject.mCreatureStats, player.mObject.mInventory, &player.mObject.mNpcStats);
else if (reader.getFormat() < 20) else if (reader.getFormatVersion() <= ESM::MaxOldCreatureStatsFormatVersion)
convertStats(player.mObject.mCreatureStats); convertStats(player.mObject.mCreatureStats);
if (!player.mObject.mEnabled) if (!player.mObject.mEnabled)
@ -353,7 +353,7 @@ namespace MWWorld
saveStats(); saveStats();
setWerewolfStats(); setWerewolfStats();
} }
else if (reader.getFormat() < 19) else if (reader.getFormatVersion() <= ESM::MaxOldSkillsAndAttributesFormatVersion)
{ {
setWerewolfStats(); setWerewolfStats();
if (player.mSetWerewolfAcrobatics) if (player.mSetWerewolfAcrobatics)

@ -921,8 +921,7 @@ namespace MWWorld
{ {
if (ESM::REC_WTHR == type) if (ESM::REC_WTHR == type)
{ {
static const int oldestCompatibleSaveFormat = 2; if (reader.getFormatVersion() <= ESM::MaxOldWeatherFormatVersion)
if (reader.getFormat() < oldestCompatibleSaveFormat)
{ {
// Weather state isn't really all that important, so to preserve older save games, we'll just discard // Weather state isn't really all that important, so to preserve older save games, we'll just discard
// the older weather records, rather than fail to handle the record. // the older weather records, rather than fail to handle the record.

@ -5,6 +5,7 @@
#include <components/esm3/esmreader.hpp> #include <components/esm3/esmreader.hpp>
#include <components/esm3/esmwriter.hpp> #include <components/esm3/esmwriter.hpp>
#include <components/esm3/formatversion.hpp>
#include <components/esm3/readerscache.hpp> #include <components/esm3/readerscache.hpp>
#include <components/lua/configuration.hpp> #include <components/lua/configuration.hpp>
#include <components/lua/serialization.hpp> #include <components/lua/serialization.hpp>
@ -152,7 +153,7 @@ namespace
writer.setAuthor(""); writer.setAuthor("");
writer.setDescription(""); writer.setDescription("");
writer.setRecordCount(1); writer.setRecordCount(1);
writer.setFormat(ESM::Header::CurrentFormat); writer.setFormatVersion(ESM::CurrentContentFormatVersion);
writer.setVersion(); writer.setVersion();
writer.addMaster("morrowind.esm", 0); writer.addMaster("morrowind.esm", 0);

@ -1,5 +1,6 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <array>
#include <fstream> #include <fstream>
#include <boost/program_options/options_description.hpp> #include <boost/program_options/options_description.hpp>
@ -16,6 +17,7 @@
#include <components/esm4/loadstat.hpp> #include <components/esm4/loadstat.hpp>
#include <components/esm4/reader.hpp> #include <components/esm4/reader.hpp>
#include <components/esm4/readerutils.hpp> #include <components/esm4/readerutils.hpp>
#include <components/esm4/typetraits.hpp>
#include <components/files/configurationmanager.hpp> #include <components/files/configurationmanager.hpp>
#include <components/files/conversion.hpp> #include <components/files/conversion.hpp>
#include <components/loadinglistener/loadinglistener.hpp> #include <components/loadinglistener/loadinglistener.hpp>
@ -238,20 +240,21 @@ TEST_F(ContentFileTest, autocalc_test)
*/ */
/// Base class for tests of ESMStore that do not rely on external content files /// Base class for tests of ESMStore that do not rely on external content files
template <class T>
struct StoreTest : public ::testing::Test struct StoreTest : public ::testing::Test
{ {
protected:
MWWorld::ESMStore mEsmStore;
}; };
TYPED_TEST_SUITE_P(StoreTest);
/// Create an ESM file in-memory containing the specified record. /// Create an ESM file in-memory containing the specified record.
/// @param deleted Write record with deleted flag? /// @param deleted Write record with deleted flag?
template <typename T> template <typename T>
std::unique_ptr<std::istream> getEsmFile(T record, bool deleted) std::unique_ptr<std::istream> getEsmFile(T record, bool deleted, ESM::FormatVersion formatVersion)
{ {
ESM::ESMWriter writer; ESM::ESMWriter writer;
auto stream = std::make_unique<std::stringstream>(); auto stream = std::make_unique<std::stringstream>();
writer.setFormat(0); writer.setFormatVersion(formatVersion);
writer.save(*stream); writer.save(*stream);
writer.startRecord(T::sRecordId); writer.startRecord(T::sRecordId);
record.save(writer, deleted); record.save(writer, deleted);
@ -260,41 +263,79 @@ std::unique_ptr<std::istream> getEsmFile(T record, bool deleted)
return stream; return stream;
} }
namespace
{
constexpr std::array formats = {
ESM::DefaultFormatVersion,
ESM::CurrentContentFormatVersion,
ESM::MaxOldWeatherFormatVersion,
ESM::MaxOldDeathAnimationFormatVersion,
ESM::MaxOldForOfWarFormatVersion,
ESM::MaxWerewolfDeprecatedDataFormatVersion,
ESM::MaxOldTimeLeftFormatVersion,
ESM::MaxIntFallbackFormatVersion,
ESM::MaxClearModifiersFormatVersion,
ESM::MaxOldAiPackageFormatVersion,
ESM::MaxOldSkillsAndAttributesFormatVersion,
ESM::MaxOldCreatureStatsFormatVersion,
ESM::CurrentSaveGameFormatVersion,
};
template <class T, class = std::void_t<>>
struct HasBlankFunction : std::false_type
{
};
template <class T>
struct HasBlankFunction<T, std::void_t<decltype(std::declval<T>().blank())>> : std::true_type
{
};
template <class T>
constexpr bool hasBlankFunction = HasBlankFunction<T>::value;
}
/// Tests deletion of records. /// Tests deletion of records.
TEST_F(StoreTest, delete_test) TYPED_TEST_P(StoreTest, delete_test)
{ {
const ESM::RefId recordId = ESM::RefId::stringRefId("foobar"); using RecordType = TypeParam;
typedef ESM::Apparatus RecordType; for (const ESM::FormatVersion formatVersion : formats)
{
SCOPED_TRACE("FormatVersion: " + std::to_string(formatVersion));
const ESM::RefId recordId = ESM::RefId::stringRefId("foobar");
RecordType record; RecordType record;
record.blank(); if constexpr (hasBlankFunction<RecordType>)
record.mId = recordId; record.blank();
record.mId = recordId;
ESM::ESMReader reader; ESM::ESMReader reader;
ESM::Dialogue* dialogue = nullptr; ESM::Dialogue* dialogue = nullptr;
MWWorld::ESMStore esmStore;
// master file inserts a record // master file inserts a record
reader.open(getEsmFile(record, false), "filename"); reader.open(getEsmFile(record, false, formatVersion), "filename");
mEsmStore.load(reader, &dummyListener, dialogue); esmStore.load(reader, &dummyListener, dialogue);
mEsmStore.setUp(); esmStore.setUp();
ASSERT_TRUE(mEsmStore.get<RecordType>().getSize() == 1); EXPECT_EQ(esmStore.get<RecordType>().getSize(), 1);
// now a plugin deletes it // now a plugin deletes it
reader.open(getEsmFile(record, true), "filename"); reader.open(getEsmFile(record, true, formatVersion), "filename");
mEsmStore.load(reader, &dummyListener, dialogue); esmStore.load(reader, &dummyListener, dialogue);
mEsmStore.setUp(); esmStore.setUp();
ASSERT_TRUE(mEsmStore.get<RecordType>().getSize() == 0); EXPECT_EQ(esmStore.get<RecordType>().getSize(), 0);
// now another plugin inserts it again // now another plugin inserts it again
// expected behaviour is the record to reappear rather than staying deleted // expected behaviour is the record to reappear rather than staying deleted
reader.open(getEsmFile(record, false), "filename"); reader.open(getEsmFile(record, false, formatVersion), "filename");
mEsmStore.load(reader, &dummyListener, dialogue); esmStore.load(reader, &dummyListener, dialogue);
mEsmStore.setUp(); esmStore.setUp();
ASSERT_TRUE(mEsmStore.get<RecordType>().getSize() == 1); EXPECT_EQ(esmStore.get<RecordType>().getSize(), 1);
}
} }
template <typename T> template <typename T>
@ -327,42 +368,204 @@ static void testAllRecNameIntUnique(const MWWorld::ESMStore::StoreTuple& stores)
std::apply([&stores](auto&&... x) { (testRecNameIntCount(x, stores), ...); }, stores); std::apply([&stores](auto&&... x) { (testRecNameIntCount(x, stores), ...); }, stores);
} }
TEST_F(StoreTest, eachRecordTypeShouldHaveUniqueRecordId) TEST(StoreTest, eachRecordTypeShouldHaveUniqueRecordId)
{ {
testAllRecNameIntUnique(MWWorld::ESMStore::StoreTuple()); testAllRecNameIntUnique(MWWorld::ESMStore::StoreTuple());
} }
/// Tests overwriting of records. /// Tests overwriting of records.
TEST_F(StoreTest, overwrite_test) TYPED_TEST_P(StoreTest, overwrite_test)
{
using RecordType = TypeParam;
for (const ESM::FormatVersion formatVersion : formats)
{
SCOPED_TRACE("FormatVersion: " + std::to_string(formatVersion));
const ESM::RefId recordId = ESM::RefId::stringRefId("foobar");
const ESM::RefId recordIdUpper = ESM::RefId::stringRefId("Foobar");
RecordType record;
if constexpr (hasBlankFunction<RecordType>)
record.blank();
record.mId = recordId;
ESM::ESMReader reader;
ESM::Dialogue* dialogue = nullptr;
MWWorld::ESMStore esmStore;
// master file inserts a record
reader.open(getEsmFile(record, false, formatVersion), "filename");
esmStore.load(reader, &dummyListener, dialogue);
esmStore.setUp();
// now a plugin overwrites it with changed data
record.mId = recordIdUpper; // change id to uppercase, to test case smashing while we're at it
record.mModel = "the_new_model";
reader.open(getEsmFile(record, false, formatVersion), "filename");
esmStore.load(reader, &dummyListener, dialogue);
esmStore.setUp();
// verify that changes were actually applied
const RecordType* overwrittenRec = esmStore.get<RecordType>().search(recordId);
ASSERT_NE(overwrittenRec, nullptr);
EXPECT_EQ(overwrittenRec->mModel, "the_new_model");
}
}
namespace
{ {
const ESM::RefId recordId = ESM::RefId::stringRefId("foobar"); template <class T>
const ESM::RefId recordIdUpper = ESM::RefId::stringRefId("Foobar"); struct StoreSaveLoadTest : public ::testing::Test
{
};
template <class T, class = std::void_t<>>
struct HasIndex : std::false_type
{
};
template <class T>
struct HasIndex<T, std::void_t<decltype(T::mIndex)>> : std::true_type
{
};
template <class T>
constexpr bool hasIndex = HasIndex<T>::value;
TYPED_TEST_SUITE_P(StoreSaveLoadTest);
TYPED_TEST_P(StoreSaveLoadTest, shouldNotChangeRefId)
{
using RecordType = TypeParam;
const int index = 3;
ESM::RefId refId;
if constexpr (hasIndex<RecordType> && !std::is_same_v<RecordType, ESM::LandTexture>)
refId = ESM::RefId::stringRefId(RecordType::indexToId(index));
else
refId = ESM::RefId::stringRefId("foobar");
for (const ESM::FormatVersion formatVersion : formats)
{
SCOPED_TRACE("FormatVersion: " + std::to_string(formatVersion));
RecordType record;
if constexpr (hasBlankFunction<RecordType>)
record.blank();
record.mId = refId;
if constexpr (hasIndex<RecordType>)
record.mIndex = index;
if constexpr (std::is_same_v<RecordType, ESM::Global>)
record.mValue = ESM::Variant(42);
ESM::ESMReader reader;
ESM::Dialogue* dialogue = nullptr;
MWWorld::ESMStore esmStore;
reader.open(getEsmFile(record, false, formatVersion), "filename");
esmStore.load(reader, &dummyListener, dialogue);
esmStore.setUp();
const RecordType* result = nullptr;
if constexpr (std::is_same_v<RecordType, ESM::LandTexture>)
result = esmStore.get<RecordType>().search(index, 0);
else if constexpr (hasIndex<RecordType>)
result = esmStore.get<RecordType>().search(index);
else
result = esmStore.get<RecordType>().search(refId);
ASSERT_NE(result, nullptr);
EXPECT_EQ(result->mId, refId);
}
}
static_assert(hasIndex<ESM::MagicEffect>);
template <class T, class = std::void_t<>>
struct HasSaveFunction : std::false_type
{
};
typedef ESM::Apparatus RecordType; template <class T>
struct HasSaveFunction<T, std::void_t<decltype(std::declval<T>().save(std::declval<ESM::ESMWriter&>(), bool()))>>
: std::true_type
{
};
template <class Head, class List>
struct ConcatTypes;
template <class Head, class... Ts>
struct ConcatTypes<Head, std::tuple<Ts...>>
{
using Type = std::tuple<Head, Ts...>;
};
template <template <class...> class Predicate, class Out, class... Ins>
struct FilterTypesImpl;
template <template <class...> class Predicate, class Out, class Head, class... Tail>
struct FilterTypesImpl<Predicate, Out, Head, Tail...>
{
using Type = typename FilterTypesImpl<Predicate,
std::conditional_t<Predicate<Head>::value, typename ConcatTypes<Head, Out>::Type, Out>, Tail...>::Type;
};
template <template <class...> class Predicate, class Out>
struct FilterTypesImpl<Predicate, Out>
{
using Type = Out;
};
RecordType record; template <template <class...> class Predicate, class List>
record.blank(); struct FilterTypes;
record.mId = recordId;
ESM::ESMReader reader; template <template <class...> class Predicate, class... Ts>
ESM::Dialogue* dialogue = nullptr; struct FilterTypes<Predicate, std::tuple<Ts...>>
{
using Type = typename FilterTypesImpl<Predicate, std::tuple<>, Ts...>::Type;
};
template <class... T>
struct ToRecordTypes;
// master file inserts a record template <class... T>
reader.open(getEsmFile(record, false), "filename"); struct ToRecordTypes<std::tuple<MWWorld::Store<T>...>>
mEsmStore.load(reader, &dummyListener, dialogue); {
mEsmStore.setUp(); using Type = std::tuple<T...>;
};
// now a plugin overwrites it with changed data template <class... T>
record.mId = recordIdUpper; // change id to uppercase, to test case smashing while we're at it struct AsTestingTypes;
record.mModel = "the_new_model";
reader.open(getEsmFile(record, false), "filename"); template <class... T>
mEsmStore.load(reader, &dummyListener, dialogue); struct AsTestingTypes<std::tuple<T...>>
mEsmStore.setUp(); {
using Type = testing::Types<T...>;
};
// verify that changes were actually applied using RecordTypes = typename ToRecordTypes<MWWorld::ESMStore::StoreTuple>::Type;
const RecordType* overwrittenRec = mEsmStore.get<RecordType>().search(recordId); using RecordTypesWithId = typename FilterTypes<ESM4::HasId, RecordTypes>::Type;
using RecordTypesWithSave = typename FilterTypes<HasSaveFunction, RecordTypesWithId>::Type;
using RecordTypesWithModel = typename FilterTypes<ESM4::HasModel, RecordTypesWithSave>::Type;
ASSERT_TRUE(overwrittenRec != nullptr); REGISTER_TYPED_TEST_SUITE_P(StoreSaveLoadTest, shouldNotChangeRefId);
ASSERT_TRUE(overwrittenRec && overwrittenRec->mModel == "the_new_model"); static_assert(std::tuple_size_v<RecordTypesWithSave> == 38);
INSTANTIATE_TYPED_TEST_SUITE_P(
RecordTypesTest, StoreSaveLoadTest, typename AsTestingTypes<RecordTypesWithSave>::Type);
} }
REGISTER_TYPED_TEST_SUITE_P(StoreTest, overwrite_test, delete_test);
static_assert(std::tuple_size_v<RecordTypesWithModel> == 19);
INSTANTIATE_TYPED_TEST_SUITE_P(RecordTypesTest, StoreTest, typename AsTestingTypes<RecordTypesWithModel>::Type);

@ -479,7 +479,7 @@ void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newf
file->setAuthor(QString::fromUtf8(fileReader.getAuthor().c_str())); file->setAuthor(QString::fromUtf8(fileReader.getAuthor().c_str()));
file->setDate(info.lastModified()); file->setDate(info.lastModified());
file->setFormat(fileReader.getFormat()); file->setFormat(fileReader.getFormatVersion());
file->setFilePath(info.absoluteFilePath()); file->setFilePath(info.absoluteFilePath());
file->setDescription(QString::fromUtf8(fileReader.getDesc().c_str())); file->setDescription(QString::fromUtf8(fileReader.getDesc().c_str()));

@ -15,7 +15,6 @@ QString ContentSelectorModel::EsmFile::sToolTip = QString(
ContentSelectorModel::EsmFile::EsmFile(QString fileName, ModelItem* parent) ContentSelectorModel::EsmFile::EsmFile(QString fileName, ModelItem* parent)
: ModelItem(parent) : ModelItem(parent)
, mFileName(fileName) , mFileName(fileName)
, mFormat(0)
{ {
} }
@ -36,7 +35,7 @@ void ContentSelectorModel::EsmFile::setDate(const QDateTime& modified)
void ContentSelectorModel::EsmFile::setFormat(int format) void ContentSelectorModel::EsmFile::setFormat(int format)
{ {
mFormat = format; mVersion = format;
} }
void ContentSelectorModel::EsmFile::setFilePath(const QString& path) void ContentSelectorModel::EsmFile::setFilePath(const QString& path)
@ -59,7 +58,7 @@ QByteArray ContentSelectorModel::EsmFile::encodedData() const
QByteArray encodedData; QByteArray encodedData;
QDataStream stream(&encodedData, QIODevice::WriteOnly); QDataStream stream(&encodedData, QIODevice::WriteOnly);
stream << mFileName << mAuthor << QString::number(mFormat) << mModified.toString() << mPath << mDescription stream << mFileName << mAuthor << QString::number(mVersion) << mModified.toString() << mPath << mDescription
<< mGameFiles; << mGameFiles;
return encodedData; return encodedData;
@ -85,7 +84,7 @@ QVariant ContentSelectorModel::EsmFile::fileProperty(const FileProperty prop) co
break; break;
case FileProperty_Format: case FileProperty_Format:
return mFormat; return mVersion;
break; break;
case FileProperty_DateModified: case FileProperty_DateModified:
@ -122,7 +121,7 @@ void ContentSelectorModel::EsmFile::setFileProperty(const FileProperty prop, con
break; break;
case FileProperty_Format: case FileProperty_Format:
mFormat = value.toInt(); mVersion = value.toInt();
break; break;
case FileProperty_DateModified: case FileProperty_DateModified:

@ -4,6 +4,8 @@
#include <QDateTime> #include <QDateTime>
#include <QStringList> #include <QStringList>
#include <components/esm3/formatversion.hpp>
#include "modelitem.hpp" #include "modelitem.hpp"
class QMimeData; class QMimeData;
@ -49,7 +51,7 @@ namespace ContentSelectorModel
inline QString fileName() const { return mFileName; } inline QString fileName() const { return mFileName; }
inline QString author() const { return mAuthor; } inline QString author() const { return mAuthor; }
inline QDateTime modified() const { return mModified; } inline QDateTime modified() const { return mModified; }
inline float format() const { return mFormat; } ESM::FormatVersion formatVersion() const { return mVersion; }
inline QString filePath() const { return mPath; } inline QString filePath() const { return mPath; }
/// @note Contains file names, not paths. /// @note Contains file names, not paths.
@ -58,7 +60,7 @@ namespace ContentSelectorModel
inline QString toolTip() const inline QString toolTip() const
{ {
return sToolTip.arg(mAuthor) return sToolTip.arg(mAuthor)
.arg(mFormat) .arg(mVersion)
.arg(mModified.toString(Qt::ISODate)) .arg(mModified.toString(Qt::ISODate))
.arg(mPath) .arg(mPath)
.arg(mDescription) .arg(mDescription)
@ -76,7 +78,7 @@ namespace ContentSelectorModel
QString mFileName; QString mFileName;
QString mAuthor; QString mAuthor;
QDateTime mModified; QDateTime mModified;
int mFormat; ESM::FormatVersion mVersion = ESM::DefaultFormatVersion;
QString mPath; QString mPath;
QStringList mGameFiles; QStringList mGameFiles;
QString mDescription; QString mDescription;

@ -42,7 +42,7 @@ namespace ESM
void loadImpl(ESMReader& esm, std::vector<ActiveSpells::ActiveSpellParams>& spells, NAME tag) void loadImpl(ESMReader& esm, std::vector<ActiveSpells::ActiveSpellParams>& spells, NAME tag)
{ {
int format = esm.getFormat(); const FormatVersion format = esm.getFormatVersion();
while (esm.isNextSub(tag)) while (esm.isNextSub(tag))
{ {
@ -50,7 +50,7 @@ namespace ESM
params.mId = esm.getRefId(); params.mId = esm.getRefId();
esm.getHNT(params.mCasterActorId, "CAST"); esm.getHNT(params.mCasterActorId, "CAST");
params.mDisplayName = esm.getHNString("DISP"); params.mDisplayName = esm.getHNString("DISP");
if (format < 17) if (format <= MaxClearModifiersFormatVersion)
params.mType = ActiveSpells::Type_Temporary; params.mType = ActiveSpells::Type_Temporary;
else else
{ {
@ -77,7 +77,7 @@ namespace ESM
effect.mArg = -1; effect.mArg = -1;
esm.getHNOT(effect.mArg, "ARG_"); esm.getHNOT(effect.mArg, "ARG_");
esm.getHNT(effect.mMagnitude, "MAGN"); esm.getHNT(effect.mMagnitude, "MAGN");
if (format < 17) if (format <= MaxClearModifiersFormatVersion)
{ {
effect.mMinMagnitude = effect.mMagnitude; effect.mMinMagnitude = effect.mMagnitude;
effect.mMaxMagnitude = effect.mMagnitude; effect.mMaxMagnitude = effect.mMagnitude;
@ -90,11 +90,11 @@ namespace ESM
esm.getHNT(effect.mDuration, "DURA"); esm.getHNT(effect.mDuration, "DURA");
effect.mEffectIndex = -1; effect.mEffectIndex = -1;
esm.getHNOT(effect.mEffectIndex, "EIND"); esm.getHNOT(effect.mEffectIndex, "EIND");
if (format < 9) if (format <= MaxOldTimeLeftFormatVersion)
effect.mTimeLeft = effect.mDuration; effect.mTimeLeft = effect.mDuration;
else else
esm.getHNT(effect.mTimeLeft, "LEFT"); esm.getHNT(effect.mTimeLeft, "LEFT");
if (format < 17) if (format <= MaxClearModifiersFormatVersion)
effect.mFlags = ActiveEffect::Flag_None; effect.mFlags = ActiveEffect::Flag_None;
else else
esm.getHNT(effect.mFlags, "FLAG"); esm.getHNT(effect.mFlags, "FLAG");

@ -57,7 +57,7 @@ namespace ESM
mCellId = esm.getHNOString("CELL"); mCellId = esm.getHNOString("CELL");
mRepeat = false; mRepeat = false;
esm.getHNOT(mRepeat, "REPT"); esm.getHNOT(mRepeat, "REPT");
if (esm.getFormat() < 18) if (esm.getFormatVersion() <= MaxOldAiPackageFormatVersion)
{ {
// mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration. // mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration.
// The exact value of mDuration only matters for repeating packages. // The exact value of mDuration only matters for repeating packages.
@ -94,7 +94,7 @@ namespace ESM
esm.getHNOT(mActive, "ACTV"); esm.getHNOT(mActive, "ACTV");
mRepeat = false; mRepeat = false;
esm.getHNOT(mRepeat, "REPT"); esm.getHNOT(mRepeat, "REPT");
if (esm.getFormat() < 18) if (esm.getFormatVersion() <= MaxOldAiPackageFormatVersion)
{ {
// mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration. // mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration.
// The exact value of mDuration only matters for repeating packages. // The exact value of mDuration only matters for repeating packages.
@ -265,7 +265,7 @@ namespace ESM
esm.getHNOT(mLastAiPackage, "LAST"); esm.getHNOT(mLastAiPackage, "LAST");
if (count > 1 && esm.getFormat() < 18) if (count > 1 && esm.getFormatVersion() <= MaxOldAiPackageFormatVersion)
{ {
for (auto& pkg : mPackages) for (auto& pkg : mPackages)
{ {

@ -9,7 +9,7 @@ namespace ESM
void CreatureStats::load(ESMReader& esm) void CreatureStats::load(ESMReader& esm)
{ {
bool intFallback = esm.getFormat() < 11; const bool intFallback = esm.getFormatVersion() <= MaxIntFallbackFormatVersion;
for (int i = 0; i < 8; ++i) for (int i = 0; i < 8; ++i)
mAttributes[i].load(esm, intFallback); mAttributes[i].load(esm, intFallback);
@ -37,11 +37,11 @@ namespace ESM
mHitRecovery = false; mHitRecovery = false;
mBlock = false; mBlock = false;
mRecalcDynamicStats = false; mRecalcDynamicStats = false;
if (esm.getFormat() < 8) if (esm.getFormatVersion() <= MaxWerewolfDeprecatedDataFormatVersion)
{ {
esm.getHNOT(mDead, "DEAD"); esm.getHNOT(mDead, "DEAD");
esm.getHNOT(mDeathAnimationFinished, "DFNT"); esm.getHNOT(mDeathAnimationFinished, "DFNT");
if (esm.getFormat() < 3 && mDead) if (esm.getFormatVersion() <= MaxOldDeathAnimationFormatVersion && mDead)
mDeathAnimationFinished = true; mDeathAnimationFinished = true;
esm.getHNOT(mDied, "DIED"); esm.getHNOT(mDied, "DIED");
esm.getHNOT(mMurdered, "MURD"); esm.getHNOT(mMurdered, "MURD");
@ -91,7 +91,7 @@ namespace ESM
mLastHitAttemptObject = ESM::RefId::stringRefId(esm.getHNOString("LHAT")); mLastHitAttemptObject = ESM::RefId::stringRefId(esm.getHNOString("LHAT"));
if (esm.getFormat() < 8) if (esm.getFormatVersion() <= MaxWerewolfDeprecatedDataFormatVersion)
esm.getHNOT(mRecalcDynamicStats, "CALC"); esm.getHNOT(mRecalcDynamicStats, "CALC");
mDrawState = 0; mDrawState = 0;
@ -115,7 +115,7 @@ namespace ESM
mAiSequence.load(esm); mAiSequence.load(esm);
mMagicEffects.load(esm); mMagicEffects.load(esm);
if (esm.getFormat() < 17) if (esm.getFormatVersion() <= MaxClearModifiersFormatVersion)
{ {
while (esm.isNextSub("SUMM")) while (esm.isNextSub("SUMM"))
{ {
@ -168,7 +168,7 @@ namespace ESM
mCorprusSpells[id] = stats; mCorprusSpells[id] = stats;
} }
if (esm.getFormat() <= 18) if (esm.getFormatVersion() <= MaxOldSkillsAndAttributesFormatVersion)
mMissingACDT = mGoldPool == std::numeric_limits<int>::min(); mMissingACDT = mGoldPool == std::numeric_limits<int>::min();
else else
{ {

@ -36,7 +36,7 @@ namespace ESM
const std::string& getDesc() const { return mHeader.mData.desc; } const std::string& getDesc() const { return mHeader.mData.desc; }
const std::vector<Header::MasterData>& getGameFiles() const { return mHeader.mMaster; } const std::vector<Header::MasterData>& getGameFiles() const { return mHeader.mMaster; }
const Header& getHeader() const { return mHeader; } const Header& getHeader() const { return mHeader; }
int getFormat() const { return mHeader.mFormat; } FormatVersion getFormatVersion() const { return mHeader.mFormatVersion; }
const NAME& retSubName() const { return mCtx.subName; } const NAME& retSubName() const { return mCtx.subName; }
uint32_t getSubSize() const { return mCtx.leftSub; } uint32_t getSubSize() const { return mCtx.leftSub; }
const std::filesystem::path& getName() const { return mCtx.filename; } const std::filesystem::path& getName() const { return mCtx.filename; }

@ -49,9 +49,9 @@ namespace ESM
mHeader.mData.records = count; mHeader.mData.records = count;
} }
void ESMWriter::setFormat(int format) void ESMWriter::setFormatVersion(FormatVersion value)
{ {
mHeader.mFormat = format; mHeader.mFormatVersion = value;
} }
void ESMWriter::clearMaster() void ESMWriter::clearMaster()

@ -45,7 +45,7 @@ namespace ESM
// It is a good idea to compare this with the value you wrote into the header (setRecordCount) // It is a good idea to compare this with the value you wrote into the header (setRecordCount)
// It should be the record count you set + 1 (1 additional record for the TES3 header) // It should be the record count you set + 1 (1 additional record for the TES3 header)
int getRecordCount() { return mRecordCount; } int getRecordCount() { return mRecordCount; }
void setFormat(int format); void setFormatVersion(FormatVersion value);
void clearMaster(); void clearMaster();

@ -60,7 +60,7 @@ namespace ESM
{ {
esm.getHNOT(mBounds, "BOUN"); esm.getHNOT(mBounds, "BOUN");
esm.getHNOT(mNorthMarkerAngle, "ANGL"); esm.getHNOT(mNorthMarkerAngle, "ANGL");
int dataFormat = esm.getFormat(); const FormatVersion dataFormat = esm.getFormatVersion();
while (esm.isNextSub("FTEX")) while (esm.isNextSub("FTEX"))
{ {
esm.getSubHeader(); esm.getSubHeader();
@ -73,7 +73,7 @@ namespace ESM
tex.mImageData.resize(imageSize); tex.mImageData.resize(imageSize);
esm.getExact(&tex.mImageData[0], imageSize); esm.getExact(&tex.mImageData[0], imageSize);
if (dataFormat < 7) if (dataFormat <= MaxOldForOfWarFormatVersion)
convertFogOfWar(tex.mImageData); convertFogOfWar(tex.mImageData);
mFogTextures.push_back(tex); mFogTextures.push_back(tex);

@ -0,0 +1,25 @@
#ifndef OPENMW_COMPONENTS_ESM3_FORMATVERSION_H
#define OPENMW_COMPONENTS_ESM3_FORMATVERSION_H
#include <cstdint>
namespace ESM
{
using FormatVersion = std::uint32_t;
inline constexpr FormatVersion DefaultFormatVersion = 0;
inline constexpr FormatVersion CurrentContentFormatVersion = 1;
inline constexpr FormatVersion MaxOldWeatherFormatVersion = 1;
inline constexpr FormatVersion MaxOldDeathAnimationFormatVersion = 2;
inline constexpr FormatVersion MaxOldForOfWarFormatVersion = 6;
inline constexpr FormatVersion MaxWerewolfDeprecatedDataFormatVersion = 7;
inline constexpr FormatVersion MaxOldTimeLeftFormatVersion = 8;
inline constexpr FormatVersion MaxIntFallbackFormatVersion = 10;
inline constexpr FormatVersion MaxClearModifiersFormatVersion = 16;
inline constexpr FormatVersion MaxOldAiPackageFormatVersion = 17;
inline constexpr FormatVersion MaxOldSkillsAndAttributesFormatVersion = 18;
inline constexpr FormatVersion MaxOldCreatureStatsFormatVersion = 19;
inline constexpr FormatVersion CurrentSaveGameFormatVersion = 22;
}
#endif

@ -189,7 +189,7 @@ namespace ESM
mId = ESM::RefId::stringRefId(indexToId(mIndex)); mId = ESM::RefId::stringRefId(indexToId(mIndex));
esm.getHNTSized<36>(mData, "MEDT"); esm.getHNTSized<36>(mData, "MEDT");
if (esm.getFormat() == 0) if (esm.getFormatVersion() == DefaultFormatVersion)
{ {
// don't allow mods to change fixed flags in the legacy format // don't allow mods to change fixed flags in the legacy format
mData.mFlags &= (AllowSpellmaking | AllowEnchanting | NegativeLight); mData.mFlags &= (AllowSpellmaking | AllowEnchanting | NegativeLight);

@ -14,20 +14,16 @@ namespace ESM
mData.author.clear(); mData.author.clear();
mData.desc.clear(); mData.desc.clear();
mData.records = 0; mData.records = 0;
mFormat = CurrentFormat; mFormatVersion = CurrentContentFormatVersion;
mMaster.clear(); mMaster.clear();
} }
void Header::load(ESMReader& esm) void Header::load(ESMReader& esm)
{ {
if (esm.isNextSub("FORM")) if (esm.isNextSub("FORM"))
{ esm.getHT(mFormatVersion);
esm.getHT(mFormat);
if (mFormat < 0)
esm.fail("invalid format code");
}
else else
mFormat = 0; mFormatVersion = DefaultFormatVersion;
if (esm.isNextSub("HEDR")) if (esm.isNextSub("HEDR"))
{ {
@ -69,8 +65,8 @@ namespace ESM
void Header::save(ESMWriter& esm) void Header::save(ESMWriter& esm)
{ {
if (mFormat > 0) if (mFormatVersion > DefaultFormatVersion)
esm.writeHNT("FORM", mFormat); esm.writeHNT("FORM", mFormatVersion);
esm.startSubRecord("HEDR"); esm.startSubRecord("HEDR");
esm.writeT(mData.version); esm.writeT(mData.version);

@ -4,6 +4,7 @@
#include <vector> #include <vector>
#include "components/esm/esmcommon.hpp" #include "components/esm/esmcommon.hpp"
#include "components/esm3/formatversion.hpp"
namespace ESM namespace ESM
{ {
@ -42,8 +43,6 @@ namespace ESM
/// \brief File header record /// \brief File header record
struct Header struct Header
{ {
static constexpr int CurrentFormat = 1; // most recent known format
// Defines another files (esm or esp) that this file depends upon. // Defines another files (esm or esp) that this file depends upon.
struct MasterData struct MasterData
{ {
@ -56,7 +55,7 @@ namespace ESM
std::vector<unsigned char> mSCRS; // Used in .ess savegames only, screenshot std::vector<unsigned char> mSCRS; // Used in .ess savegames only, screenshot
Data mData; Data mData;
int mFormat; FormatVersion mFormatVersion;
std::vector<MasterData> mMaster; std::vector<MasterData> mMaster;
void blank(); void blank();

@ -24,7 +24,7 @@ namespace ESM
std::pair<int, float> params; std::pair<int, float> params;
esm.getHT(id); esm.getHT(id);
esm.getHNT(params.first, "BASE"); esm.getHNT(params.first, "BASE");
if (esm.getFormat() < 17) if (esm.getFormatVersion() <= MaxClearModifiersFormatVersion)
params.second = 0.f; params.second = 0.f;
else else
esm.getHNT(params.second, "MODI"); esm.getHNT(params.second, "MODI");

@ -39,12 +39,12 @@ namespace ESM
mDisposition = 0; mDisposition = 0;
esm.getHNOT(mDisposition, "DISP"); esm.getHNOT(mDisposition, "DISP");
bool intFallback = esm.getFormat() < 11; const bool intFallback = esm.getFormatVersion() <= MaxIntFallbackFormatVersion;
for (int i = 0; i < 27; ++i) for (int i = 0; i < 27; ++i)
mSkills[i].load(esm, intFallback); mSkills[i].load(esm, intFallback);
mWerewolfDeprecatedData = false; mWerewolfDeprecatedData = false;
if (esm.getFormat() < 8 && esm.peekNextSub("STBA")) if (esm.getFormatVersion() <= MaxWerewolfDeprecatedDataFormatVersion && esm.peekNextSub("STBA"))
{ {
// we have deprecated werewolf skills, stored interleaved // we have deprecated werewolf skills, stored interleaved
// Load into one big vector, then remove every 2nd value // Load into one big vector, then remove every 2nd value
@ -66,7 +66,9 @@ namespace ESM
else else
++it; ++it;
} }
assert(skills.size() == 27); if (skills.size() != std::size(mSkills))
throw std::runtime_error(
"Invalid number of skill for werewolf deprecated data: " + std::to_string(skills.size()));
std::copy(skills.begin(), skills.end(), mSkills); std::copy(skills.begin(), skills.end(), mSkills);
} }

@ -13,7 +13,7 @@ namespace ESM
void ObjectState::load(ESMReader& esm) void ObjectState::load(ESMReader& esm)
{ {
mVersion = esm.getFormat(); mVersion = esm.getFormatVersion();
bool isDeleted; bool isDeleted;
mRef.loadData(esm, isDeleted); mRef.loadData(esm, isDeleted);

@ -4,9 +4,11 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "components/esm/luascripts.hpp"
#include "components/esm3/formatversion.hpp"
#include "animationstate.hpp" #include "animationstate.hpp"
#include "cellref.hpp" #include "cellref.hpp"
#include "components/esm/luascripts.hpp"
#include "locals.hpp" #include "locals.hpp"
namespace ESM namespace ESM
@ -37,7 +39,7 @@ namespace ESM
// Is there any class-specific state following the ObjectState // Is there any class-specific state following the ObjectState
bool mHasCustomState; bool mHasCustomState;
unsigned int mVersion; FormatVersion mVersion = DefaultFormatVersion;
AnimationState mAnimationState; AnimationState mAnimationState;
@ -47,7 +49,6 @@ namespace ESM
, mCount(0) , mCount(0)
, mFlags(0) , mFlags(0)
, mHasCustomState(true) , mHasCustomState(true)
, mVersion(0)
{ {
} }

@ -47,10 +47,11 @@ namespace ESM
checkPrevItems = false; checkPrevItems = false;
} }
if (esm.getFormat() < 19) if (esm.getFormatVersion() <= MaxOldSkillsAndAttributesFormatVersion)
{ {
bool intFallback = esm.getFormat() < 11; const bool intFallback = esm.getFormatVersion() <= MaxIntFallbackFormatVersion;
bool clearModified = esm.getFormat() < 17 && !mObject.mNpcStats.mIsWerewolf; const bool clearModified
= esm.getFormatVersion() <= MaxClearModifiersFormatVersion && !mObject.mNpcStats.mIsWerewolf;
if (esm.hasMoreSubs()) if (esm.hasMoreSubs())
{ {
for (int i = 0; i < Attribute::Length; ++i) for (int i = 0; i < Attribute::Length; ++i)

@ -40,7 +40,7 @@ namespace ESM
esm.skipHSub(); esm.skipHSub();
EffectList().load(esm); // for backwards compatibility EffectList().load(esm); // for backwards compatibility
esm.getHNT(mSpeed, "SPED"); esm.getHNT(mSpeed, "SPED");
if (esm.getFormat() < 17) if (esm.getFormatVersion() <= MaxClearModifiersFormatVersion)
mSlot = 0; mSlot = 0;
else else
esm.getHNT(mSlot, "SLOT"); esm.getHNT(mSlot, "SLOT");

@ -5,9 +5,6 @@
namespace ESM namespace ESM
{ {
int SavedGame::sCurrentFormat = 22;
void SavedGame::load(ESMReader& esm) void SavedGame::load(ESMReader& esm)
{ {
mPlayerName = esm.getHNString("PLNA"); mPlayerName = esm.getHNString("PLNA");

@ -18,8 +18,6 @@ namespace ESM
{ {
constexpr static RecNameInts sRecordId = REC_SAVE; constexpr static RecNameInts sRecordId = REC_SAVE;
static int sCurrentFormat;
std::vector<std::string> mContentFiles; std::vector<std::string> mContentFiles;
std::string mPlayerName; std::string mPlayerName;
int mPlayerLevel; int mPlayerLevel;

@ -8,7 +8,7 @@ namespace ESM
void SpellState::load(ESMReader& esm) void SpellState::load(ESMReader& esm)
{ {
if (esm.getFormat() < 17) if (esm.getFormatVersion() <= MaxClearModifiersFormatVersion)
{ {
while (esm.isNextSub("SPEL")) while (esm.isNextSub("SPEL"))
{ {

Loading…
Cancel
Save