From 7ce83c6cc8ddafd1ff41c59b2b446bed8ae7ed58 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 3 Oct 2024 22:22:51 +0200 Subject: [PATCH 1/4] Ensure NAME is null terminated for TESCS compatibility --- components/esm3/loaddial.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/esm3/loaddial.cpp b/components/esm3/loaddial.cpp index 788e140573..0ac0e2f197 100644 --- a/components/esm3/loaddial.cpp +++ b/components/esm3/loaddial.cpp @@ -82,7 +82,7 @@ namespace ESM if (mId != mStringId) throw std::runtime_error("Trying to save Dialogue record with name \"" + mStringId + "\" not maching id " + mId.toDebugString()); - esm.writeHNString("NAME", mStringId); + esm.writeHNCString("NAME", mStringId); } else if (esm.getFormatVersion() <= MaxNameIsRefIdOnlyFormatVersion) esm.writeHNRefId("NAME", mId); From df757b9e4d76857bfa0d457cac54932f6f027a24 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 3 Oct 2024 22:23:37 +0200 Subject: [PATCH 2/4] Count DIAL and INFO records in the HEDR --- apps/opencs/model/world/data.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 5637099d0c..00e5fec7b0 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -1430,7 +1430,8 @@ int CSMWorld::Data::count(RecordBase::State state) const + count(state, mRegions) + count(state, mBirthsigns) + count(state, mSpells) + count(state, mCells) + count(state, mEnchantments) + count(state, mBodyParts) + count(state, mLand) + count(state, mLandTextures) + count(state, mSoundGens) + count(state, mMagicEffects) + count(state, mReferenceables) - + count(state, mPathgrids); + + count(state, mPathgrids) + count(state, mTopics) + count(state, mTopicInfos) + count(state, mJournals) + + count(state, mJournalInfos); } std::vector CSMWorld::Data::getIds(bool listDeleted) const From c9ef03fdd1db77faeea8344878e426cd249b9bf8 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 3 Oct 2024 22:24:08 +0200 Subject: [PATCH 3/4] Change INFO id generation to not exceed 31 characters --- apps/opencs/view/world/infocreator.cpp | 33 ++++++++++++++++++-------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/apps/opencs/view/world/infocreator.cpp b/apps/opencs/view/world/infocreator.cpp index f98fe5be5e..5ce83cc849 100644 --- a/apps/opencs/view/world/infocreator.cpp +++ b/apps/opencs/view/world/infocreator.cpp @@ -1,11 +1,12 @@ #include "infocreator.hpp" #include +#include #include #include +#include #include -#include #include #include @@ -27,15 +28,27 @@ class QUndoStack; std::string CSVWorld::InfoCreator::getId() const { - const std::string topic = mTopic->text().toStdString(); - - std::string unique = QUuid::createUuid().toByteArray().data(); - - unique.erase(std::remove(unique.begin(), unique.end(), '-'), unique.end()); - - unique = unique.substr(1, unique.size() - 2); - - return topic + '#' + unique; + std::string id = mTopic->text().toStdString(); + size_t length = id.size(); + // We want generated ids to be at most 31 + \0 characters + id.resize(length + 32); + id[length] = '#'; + // Combine a random 32bit number with a random 64bit number for a max 30 character string + quint32 gen32 = QRandomGenerator::global()->generate(); + char* start = id.data() + length + 1; + char* end = start + 10; // 2^32 is a 10 digit number + auto result = std::to_chars(start, end, gen32); + quint64 gen64 = QRandomGenerator::global()->generate64(); + if (gen64) + { + // 0-pad the first number so 10 + 11 isn't the same as 101 + 1 + std::fill(result.ptr, end, '0'); + start = end; + end = start + 20; // 2^64 is a 20 digit number + result = std::to_chars(start, end, gen64); + } + id.resize(result.ptr - id.data()); + return id; } void CSVWorld::InfoCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const From ffe164b28d3a10408e20fdadd2f0168c1e32fd6e Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 6 Oct 2024 11:32:24 +0200 Subject: [PATCH 4/4] Add a test and address feedback --- apps/components_tests/CMakeLists.txt | 1 + apps/components_tests/esm3/testcstringids.cpp | 57 +++++++++++++++++++ apps/components_tests/esm3/testesmwriter.cpp | 2 +- components/esm3/loaddial.cpp | 2 +- 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 apps/components_tests/esm3/testcstringids.cpp diff --git a/apps/components_tests/CMakeLists.txt b/apps/components_tests/CMakeLists.txt index 4f260be189..22bb542538 100644 --- a/apps/components_tests/CMakeLists.txt +++ b/apps/components_tests/CMakeLists.txt @@ -78,6 +78,7 @@ file(GLOB UNITTEST_SRC_FILES esm3/testsaveload.cpp esm3/testesmwriter.cpp esm3/testinfoorder.cpp + esm3/testcstringids.cpp nifosg/testnifloader.cpp diff --git a/apps/components_tests/esm3/testcstringids.cpp b/apps/components_tests/esm3/testcstringids.cpp new file mode 100644 index 0000000000..239b205965 --- /dev/null +++ b/apps/components_tests/esm3/testcstringids.cpp @@ -0,0 +1,57 @@ +#include +#include +#include + +#include + +namespace ESM +{ + namespace + { + TEST(Esm3CStringIdTest, dialNameShouldBeNullTerminated) + { + std::unique_ptr stream; + + { + auto ostream = std::make_unique(); + + ESMWriter writer; + writer.setFormatVersion(DefaultFormatVersion); + writer.save(*ostream); + + Dialogue record; + record.blank(); + record.mStringId = "topic name"; + record.mId = RefId::stringRefId(record.mStringId); + record.mType = Dialogue::Topic; + writer.startRecord(Dialogue::sRecordId); + record.save(writer); + writer.endRecord(Dialogue::sRecordId); + + stream = std::move(ostream); + } + + ESMReader reader; + reader.open(std::move(stream), "stream"); + ASSERT_TRUE(reader.hasMoreRecs()); + ASSERT_EQ(reader.getRecName(), Dialogue::sRecordId); + reader.getRecHeader(); + while (reader.hasMoreSubs()) + { + reader.getSubName(); + if (reader.retSubName().toInt() == SREC_NAME) + { + reader.getSubHeader(); + auto size = reader.getSubSize(); + std::string buffer(size, '1'); + reader.getExact(buffer.data(), size); + ASSERT_EQ(buffer[size - 1], '\0'); + return; + } + else + reader.skipHSub(); + } + ASSERT_FALSE(true); + } + } +} diff --git a/apps/components_tests/esm3/testesmwriter.cpp b/apps/components_tests/esm3/testesmwriter.cpp index 9e9ae9947e..c481684c8d 100644 --- a/apps/components_tests/esm3/testesmwriter.cpp +++ b/apps/components_tests/esm3/testesmwriter.cpp @@ -57,7 +57,7 @@ namespace ESM // If this test failed probably there is a change in RefId format and CurrentSaveGameFormatVersion should be // incremented, current version should be handled. - TEST_P(Esm3EsmWriterRefIdSizeTest, writeHRefIdShouldProduceCertainNubmerOfBytes) + TEST_P(Esm3EsmWriterRefIdSizeTest, writeHRefIdShouldProduceCertainNumberOfBytes) { const auto [refId, size] = GetParam(); diff --git a/components/esm3/loaddial.cpp b/components/esm3/loaddial.cpp index 0ac0e2f197..4b546f0d9c 100644 --- a/components/esm3/loaddial.cpp +++ b/components/esm3/loaddial.cpp @@ -85,7 +85,7 @@ namespace ESM esm.writeHNCString("NAME", mStringId); } else if (esm.getFormatVersion() <= MaxNameIsRefIdOnlyFormatVersion) - esm.writeHNRefId("NAME", mId); + esm.writeHNCRefId("NAME", mId); else esm.writeHNRefId("ID__", mId);