1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-02-21 06:39:42 +00:00

Merge branch 'small_ref_id' into 'master'

Make ESM::RefId to be fixed size cheap to copy and support different implementation types

See merge request OpenMW/openmw!2708
This commit is contained in:
psi29a 2023-03-20 08:54:36 +00:00
commit b55313c08e
63 changed files with 1210 additions and 392 deletions

View file

@ -82,7 +82,7 @@ namespace EsmTool
{
}
std::string getId() const override { return mData.mId.getRefIdString(); }
std::string getId() const override { return mData.mId.toDebugString(); }
T& get() { return mData; }

View file

@ -134,7 +134,7 @@ void CSMDoc::Document::addOptionalMagicEffects()
{
ESM::MagicEffect effect;
effect.mIndex = i;
effect.mId = ESM::RefId::stringRefId(ESM::MagicEffect::indexToId(i));
effect.mId = ESM::MagicEffect::indexToRefId(i);
effect.blank();
addOptionalMagicEffect(effect);
@ -207,7 +207,7 @@ void CSMDoc::Document::createBase()
{
ESM::Skill record;
record.mIndex = i;
record.mId = ESM::RefId::stringRefId(ESM::Skill::indexToId(record.mIndex));
record.mId = ESM::Skill::indexToRefId(record.mIndex);
record.blank();
getData().getSkills().add(record);
@ -288,7 +288,7 @@ void CSMDoc::Document::createBase()
ESM::MagicEffect record;
record.mIndex = i;
record.mId = ESM::RefId::stringRefId(ESM::MagicEffect::indexToId(i));
record.mId = ESM::MagicEffect::indexToRefId(i);
record.blank();

View file

@ -99,7 +99,7 @@ namespace CSMDoc
template <class CollectionT>
void WriteCollectionStage<CollectionT>::perform(int stage, Messages& messages)
{
if (CSMWorld::getScopeFromId(mCollection.getRecord(stage).get().mId.getRefIdString()) != mScope)
if (CSMWorld::getScopeFromId(mCollection.getRecord(stage).get().mId) != mScope)
return;
ESM::ESMWriter& writer = mState.getWriter();

View file

@ -71,7 +71,7 @@ void CSMTools::ClassCheckStage::perform(int stage, CSMDoc::Messages& messages)
for (auto& skill : skills)
if (skill.second > 1)
{
messages.add(id, "Skill " + ESM::Skill::indexToId(skill.first) + " is listed more than once", "",
CSMDoc::Message::Severity_Error);
messages.add(id, "Skill " + ESM::Skill::indexToRefId(skill.first).toString() + " is listed more than once",
"", CSMDoc::Message::Severity_Error);
}
}

View file

@ -61,8 +61,8 @@ void CSMTools::FactionCheckStage::perform(int stage, CSMDoc::Messages& messages)
for (auto& skill : skills)
if (skill.second > 1)
{
messages.add(id, "Skill " + ESM::Skill::indexToId(skill.first) + " is listed more than once", "",
CSMDoc::Message::Severity_Error);
messages.add(id, "Skill " + ESM::Skill::indexToRefId(skill.first).toString() + " is listed more than once",
"", CSMDoc::Message::Severity_Error);
}
/// \todo check data members that can't be edited in the table view

View file

@ -25,5 +25,5 @@ int CSMTools::MandatoryIdStage::setup()
void CSMTools::MandatoryIdStage::perform(int stage, CSMDoc::Messages& messages)
{
if (mIdCollection.searchId(mIds.at(stage)) == -1 || mIdCollection.getRecord(mIds.at(stage)).isDeleted())
messages.add(mCollectionId, "Missing mandatory record: " + mIds.at(stage).getRefIdString());
messages.add(mCollectionId, "Missing mandatory record: " + mIds.at(stage).toDebugString());
}

View file

@ -425,11 +425,6 @@ namespace CSMWorld
return index;
}
bool ActorAdapter::is1stPersonPart(const std::string& name) const
{
return name.size() >= 4 && name.find(".1st", name.size() - 4) != std::string::npos;
}
ActorAdapter::RaceDataPtr ActorAdapter::getRaceData(const ESM::RefId& id)
{
// Return cached race data if it exists
@ -519,8 +514,7 @@ namespace CSMWorld
}
auto& part = partRecord.get();
if (part.mRace == id && part.mData.mType == ESM::BodyPart::MT_Skin
&& !is1stPersonPart(part.mId.getRefIdString()))
if (part.mRace == id && part.mData.mType == ESM::BodyPart::MT_Skin && !ESM::isFirstPersonBodyPart(part))
{
auto type = (ESM::BodyPart::MeshPart)part.mData.mPart;
bool female = part.mData.mFlags & ESM::BodyPart::BPF_Female;

View file

@ -149,7 +149,6 @@ namespace CSMWorld
ActorAdapter& operator=(const ActorAdapter&) = delete;
QModelIndex getHighestIndex(QModelIndex) const;
bool is1stPersonPart(const std::string& id) const;
RaceDataPtr getRaceData(const ESM::RefId& raceId);

View file

@ -20,7 +20,7 @@ namespace CSMWorld
QVariant LandTextureNicknameColumn::get(const Record<LandTexture>& record) const
{
return QString::fromUtf8(record.get().mId.getRefIdString().c_str());
return QString::fromStdString(record.get().mId.toString());
}
void LandTextureNicknameColumn::set(Record<LandTexture>& record, const QVariant& data)

View file

@ -62,7 +62,7 @@ namespace CSMWorld
QVariant get(const Record<ESXRecordT>& record) const override
{
return QString::fromUtf8(record.get().mId.getRefIdString().c_str());
return QString::fromStdString(record.get().mId.toString());
}
bool isEditable() const override { return false; }
@ -404,7 +404,7 @@ namespace CSMWorld
{
int skill = record.get().mData.getSkill(mIndex, mMajor);
return QString::fromUtf8(ESM::Skill::indexToId(skill).c_str());
return QString::fromStdString(ESM::Skill::indexToRefId(skill).toString());
}
void set(Record<ESXRecordT>& record, const QVariant& data) override

View file

@ -106,7 +106,7 @@ namespace CSMWorld
= static_cast<const Record<RecordT>&>(data.getRecord(RefIdData::LocalIndex(index, mType)));
if (column == mBase.mId)
return QString::fromUtf8(record.get().mId.getRefIdString().c_str());
return QString::fromStdString(record.get().mId.toString());
if (column == mBase.mModified)
{

View file

@ -658,7 +658,7 @@ int CSMWorld::RefIdCollection::getIndex(const ESM::RefId& id) const
int index = searchId(id);
if (index == -1)
throw std::runtime_error("invalid ID: " + id.getRefIdString());
throw std::runtime_error("invalid ID: " + id.toDebugString());
return index;
}

View file

@ -2,23 +2,43 @@
#include <string_view>
#include <components/misc/strings/lower.hpp>
#include <components/esm/refid.hpp>
#include <components/misc/strings/algorithm.hpp>
CSMWorld::Scope CSMWorld::getScopeFromId(const std::string& id)
namespace CSMWorld
{
// get root namespace
std::string namespace_;
namespace
{
struct GetScope
{
Scope operator()(ESM::StringRefId v) const
{
std::string_view namespace_;
std::string::size_type i = id.find("::");
const std::string::size_type i = v.getValue().find("::");
if (i != std::string::npos)
namespace_ = Misc::StringUtils::lowerCase(std::string_view(id).substr(0, i));
if (i != std::string::npos)
namespace_ = std::string_view(v.getValue()).substr(0, i);
if (namespace_ == "project")
return Scope_Project;
if (Misc::StringUtils::ciEqual(namespace_, "project"))
return Scope_Project;
if (namespace_ == "session")
return Scope_Session;
if (Misc::StringUtils::ciEqual(namespace_, "session"))
return Scope_Session;
return Scope_Content;
return Scope_Content;
}
template <class T>
Scope operator()(const T& /*v*/) const
{
return Scope_Content;
}
};
}
}
CSMWorld::Scope CSMWorld::getScopeFromId(ESM::RefId id)
{
return visit(GetScope{}, id);
}

View file

@ -1,7 +1,10 @@
#ifndef CSM_WOLRD_SCOPE_H
#define CSM_WOLRD_SCOPE_H
#include <string>
namespace ESM
{
class RefId;
}
namespace CSMWorld
{
@ -17,7 +20,7 @@ namespace CSMWorld
Scope_Session = 4
};
Scope getScopeFromId(const std::string& id);
Scope getScopeFromId(ESM::RefId id);
}
#endif

View file

@ -40,8 +40,7 @@ namespace MWDialogue
return;
}
throw std::runtime_error(
"unknown info ID " + mInfoId.getRefIdString() + " for topic " + topic.getRefIdString());
throw std::runtime_error("unknown info ID " + mInfoId.toDebugString() + " for topic " + topic.toDebugString());
}
Entry::Entry(const ESM::JournalEntry& record)
@ -98,7 +97,7 @@ namespace MWDialogue
return iter->mId;
}
throw std::runtime_error("unknown journal index for topic " + topic.getRefIdString());
throw std::runtime_error("unknown journal index for topic " + topic.toDebugString());
}
StampedJournalEntry::StampedJournalEntry()

View file

@ -75,7 +75,7 @@ namespace MWDialogue
[&](const auto& info) { return info.mId == entry.mInfoId; });
if (info == dialogue->mInfo.end() || info->mData.mJournalIndex == -1)
throw std::runtime_error("unknown journal entry for topic " + mTopic.getRefIdString());
throw std::runtime_error("unknown journal entry for topic " + mTopic.toDebugString());
if (info->mQuestStatus == ESM::DialInfo::QS_Finished || info->mQuestStatus == ESM::DialInfo::QS_Restart)
mFinished = info->mQuestStatus == ESM::DialInfo::QS_Finished;

View file

@ -21,7 +21,7 @@ namespace MWDialogue
bool Topic::addEntry(const JournalEntry& entry)
{
if (entry.mTopic != mTopic)
throw std::runtime_error("topic does not match: " + mTopic.getRefIdString());
throw std::runtime_error("topic does not match: " + mTopic.toDebugString());
// bail out if we already have heard this
for (Topic::TEntryIter it = mEntries.begin(); it != mEntries.end(); ++it)

View file

@ -896,7 +896,7 @@ namespace MWGui
= Misc::ResourceHelpers::correctTexturePath("textures\\levelup\\" + classId.getRefIdString() + ".dds", vfs);
if (!vfs->exists(classImage))
{
Log(Debug::Warning) << "No class image for " << classId.getRefIdString() << ", falling back to default";
Log(Debug::Warning) << "No class image for " << classId << ", falling back to default";
classImage = "textures\\levelup\\warrior.dds";
}
imageBox->setImageTexture(classImage);

View file

@ -683,7 +683,7 @@ namespace MWGui
if (!mConsoleMode.empty())
title = mConsoleMode + " " + title;
if (!mPtr.isEmpty())
title.append(" (" + mPtr.getCellRef().getRefId().getRefIdString() + ")");
title.append(" (" + mPtr.getCellRef().getRefId().toDebugString() + ")");
setTitle(title);
}

View file

@ -321,7 +321,6 @@ namespace MWGui
for (const ESM::BodyPart& bodypart : store)
{
const std::string& idString = bodypart.mId.getRefIdString();
if (bodypart.mData.mFlags & ESM::BodyPart::BPF_NotPlayable)
continue;
if (bodypart.mData.mType != ESM::BodyPart::MT_Skin)
@ -330,8 +329,7 @@ namespace MWGui
continue;
if (mGenderIndex != (bodypart.mData.mFlags & ESM::BodyPart::BPF_Female))
continue;
bool firstPerson = Misc::StringUtils::ciEndsWith(idString, "1st");
if (firstPerson)
if (ESM::isFirstPersonBodyPart(bodypart))
continue;
if (bodypart.mRace == mCurrentRaceId)
out.push_back(bodypart.mId);

View file

@ -160,12 +160,7 @@ namespace
if (result != 0)
return result > 0;
// compare items by Id
leftName = left.mBase.getCellRef().getRefId().getRefIdString();
rightName = right.mBase.getCellRef().getRefId().getRefIdString();
result = leftName.compare(rightName);
return result < 0;
return left.mBase.getCellRef().getRefId() < right.mBase.getCellRef().getRefId();
}
};
}

View file

@ -124,7 +124,7 @@ namespace MWGui
ToolTipInfo info;
info.caption = mFocusObject.getClass().getName(mFocusObject);
if (info.caption.empty())
info.caption = mFocusObject.getCellRef().getRefId().getRefIdString();
info.caption = mFocusObject.getCellRef().getRefId().toDebugString();
info.icon.clear();
tooltipSize = createToolTip(info, checkOwned());
}
@ -686,7 +686,7 @@ namespace MWGui
if (!creature)
return {};
if (creature->mName.empty())
return " (" + creature->mId.getRefIdString() + ")";
return " (" + creature->mId.toDebugString() + ")";
return " (" + creature->mName + ")";
}
@ -720,10 +720,10 @@ namespace MWGui
for (std::pair<ESM::RefId, int>& owner : itemOwners)
{
if (owner.second == std::numeric_limits<int>::max())
ret += std::string("\nStolen from ") + owner.first.getRefIdString(); // for legacy (ESS) savegames
ret += std::string("\nStolen from ") + owner.first.toDebugString(); // for legacy (ESS) savegames
else
ret += std::string("\nStolen ") + MyGUI::utility::toString(owner.second) + " from "
+ owner.first.getRefIdString();
+ owner.first.toDebugString();
}
ret += getMiscString(cellref.getGlobalVariable(), "Global");

View file

@ -167,7 +167,7 @@ namespace MWLua
{
objectT["isValid"] = [](const ObjectT& o) { return !o.ptrOrNull().isEmpty(); };
objectT["recordId"] = sol::readonly_property(
[](const ObjectT& o) -> std::string { return o.ptr().getCellRef().getRefId().getRefIdString(); });
[](const ObjectT& o) -> std::string { return o.ptr().getCellRef().getRefId().toDebugString(); });
objectT["cell"] = sol::readonly_property([](const ObjectT& o) -> sol::optional<Cell<ObjectT>> {
const MWWorld::Ptr& ptr = o.ptr();
if (ptr.isInCell())

View file

@ -49,7 +49,7 @@ namespace MWMechanics
case ESM::REC_NPC_:
return ::withBaseRecord<ESM::NPC>(mId, function);
default:
throw std::logic_error("failed to update base record for " + mId.getRefIdString());
throw std::logic_error("failed to update base record for " + mId.toDebugString());
}
}
@ -62,7 +62,7 @@ namespace MWMechanics
case ESM::REC_NPC_:
return getSpellList<ESM::NPC>(mId);
default:
throw std::logic_error("failed to get spell list for " + mId.getRefIdString());
throw std::logic_error("failed to get spell list for " + mId.toDebugString());
}
}

View file

@ -85,7 +85,6 @@ namespace
namespace MWRender
{
class HeadAnimationTime : public SceneUtil::ControllerSource
{
private:
@ -761,12 +760,6 @@ namespace MWRender
}
}
bool NpcAnimation::isFirstPersonPart(const ESM::BodyPart* bodypart)
{
std::string_view partName = bodypart->mId.getRefIdString();
return partName.size() >= 3 && partName.substr(partName.size() - 3, 3) == "1st";
}
bool NpcAnimation::isFemalePart(const ESM::BodyPart* bodypart)
{
return bodypart->mData.mFlags & ESM::BodyPart::BPF_Female;
@ -1220,7 +1213,7 @@ namespace MWRender
if (!(bodypart.mRace == race))
continue;
bool partFirstPerson = isFirstPersonPart(&bodypart);
const bool partFirstPerson = ESM::isFirstPersonBodyPart(bodypart);
bool isHand = bodypart.mData.mPart == ESM::BodyPart::MP_Hand
|| bodypart.mData.mPart == ESM::BodyPart::MP_Wrist
@ -1277,7 +1270,7 @@ namespace MWRender
parts[bIt->second] = &bodypart;
// If we have 3d person fallback bodypart for hand and 1st person fallback found (2)
else if (isHand && !isFirstPersonPart(parts[bIt->second]) && partFirstPerson)
else if (isHand && !ESM::isFirstPersonBodyPart(*parts[bIt->second]) && partFirstPerson)
parts[bIt->second] = &bodypart;
++bIt;

View file

@ -99,7 +99,6 @@ namespace MWRender
osg::ref_ptr<RotateController> mFirstPersonNeckController;
static bool isFirstPersonPart(const ESM::BodyPart* bodypart);
static bool isFemalePart(const ESM::BodyPart* bodypart);
static NpcType getNpcType(const MWWorld::Ptr& ptr);

View file

@ -208,7 +208,7 @@ namespace MWScript
return iter->second;
}
throw std::logic_error("script " + name.getRefIdString() + " does not exist");
throw std::logic_error("script " + name.toDebugString() + " does not exist");
}
GlobalScripts& ScriptManager::getGlobalScripts()

View file

@ -65,8 +65,8 @@ namespace MWScript
from = container;
else
{
std::string error = "Failed to find the container of object '"
+ from.getCellRef().getRefId().getRefIdString() + "'";
const std::string error
= "Failed to find the container of object " + from.getCellRef().getRefId().toDebugString();
runtime.getContext().report(error);
Log(Debug::Error) << error;
runtime.push(0.f);
@ -77,7 +77,7 @@ namespace MWScript
const MWWorld::Ptr to = MWBase::Environment::get().getWorld()->searchPtr(name, false);
if (to.isEmpty())
{
std::string error = "Failed to find an instance of object '" + name.getRefIdString() + "'";
const std::string error = "Failed to find an instance of object " + name.toDebugString();
runtime.getContext().report(error);
Log(Debug::Error) << error;
runtime.push(0.f);

View file

@ -62,7 +62,7 @@ namespace MWWorld
}
if (it == invStore.end())
throw std::runtime_error("ActionEquip can't find item " + object.getCellRef().getRefId().getRefIdString());
throw std::runtime_error("ActionEquip can't find item " + object.getCellRef().getRefId().toDebugString());
// equip the item in the first free slot
std::vector<int>::const_iterator slot = slots_.first.begin();

View file

@ -950,13 +950,13 @@ namespace MWWorld
}
else
{
Log(Debug::Error) << "Cell reference '" + ref.mRefID.getRefIdString() + "' not found!";
Log(Debug::Error) << "Cell reference " << ref.mRefID << " is not found!";
return;
}
if (!handledType)
{
Log(Debug::Error) << "Error: Ignoring reference '" << ref.mRefID.getRefIdString() << "' of unhandled type";
Log(Debug::Error) << "Error: Ignoring reference " << ref.mRefID << " of unhandled type";
return;
}

View file

@ -783,7 +783,7 @@ int MWWorld::ContainerStore::getType(const ConstPtr& ptr)
if (ptr.getType() == ESM::Weapon::sRecordId)
return Type_Weapon;
throw std::runtime_error("Object '" + ptr.getCellRef().getRefId().getRefIdString() + "' of type "
throw std::runtime_error("Object " + ptr.getCellRef().getRefId().toDebugString() + " of type "
+ std::string(ptr.getTypeDescription()) + " can not be placed into a container");
}

View file

@ -729,12 +729,9 @@ namespace MWWorld
{
return npcs.insert(npc);
}
const ESM::RefId id = ESM::RefId::stringRefId("$dynamic" + std::to_string(mDynamicCount++));
const ESM::RefId id = ESM::RefId::generated(mDynamicCount++);
if (npcs.search(id) != nullptr)
{
const std::string msg = "Try to override existing record '" + id.getRefIdString() + "'";
throw std::runtime_error(msg);
}
throw std::runtime_error("Try to override existing record: " + id.toDebugString());
ESM::NPC record = npc;
record.mId = id;

View file

@ -185,14 +185,11 @@ namespace MWWorld
template <class T>
const T* insert(const T& x)
{
const ESM::RefId id = ESM::RefId::stringRefId("$dynamic" + std::to_string(mDynamicCount++));
const ESM::RefId id = ESM::RefId::generated(mDynamicCount++);
Store<T>& store = getWritable<T>();
if (store.search(id) != nullptr)
{
const std::string msg = "Try to override existing record '" + id.getRefIdString() + "'";
throw std::runtime_error(msg);
}
throw std::runtime_error("Try to override existing record: " + id.toDebugString());
T record = x;
record.mId = id;
@ -224,10 +221,7 @@ namespace MWWorld
{
Store<T>& store = getWritable<T>();
if (store.search(x.mId) != nullptr)
{
const std::string msg = "Try to override existing record '" + x.mId.getRefIdString() + "'";
throw std::runtime_error(msg);
}
throw std::runtime_error("Try to override existing record " + x.mId.toDebugString());
T* ptr = store.insertStatic(x);
if constexpr (std::is_convertible_v<Store<T>*, DynamicStore*>)

View file

@ -94,10 +94,10 @@ MWWorld::ManualRef::ManualRef(const MWWorld::ESMStore& store, const ESM::RefId&
create(store.get<ESM4::Static>(), name, mRef, mPtr);
break;
case 0:
throw std::logic_error("failed to create manual cell ref for " + name.getRefIdString() + " (unknown ID)");
throw std::logic_error("failed to create manual cell ref for " + name.toDebugString() + " (unknown ID)");
default:
throw std::logic_error("failed to create manual cell ref for " + name.getRefIdString() + " (unknown type)");
throw std::logic_error("failed to create manual cell ref for " + name.toDebugString() + " (unknown type)");
}
mPtr.getRefData().setCount(count);

View file

@ -16,7 +16,7 @@ namespace MWWorld
res.append(" (");
res.append(getTypeDescription());
res.append(", ");
res.append(getCellRef().getRefId().getRefIdString());
res.append(getCellRef().getRefId().toDebugString());
res.append(")");
return res;
}

View file

@ -950,11 +950,8 @@ namespace MWWorld
const ESM::Pathgrid* Store<ESM::Pathgrid>::find(const ESM::RefId& name) const
{
const ESM::Pathgrid* pathgrid = search(name);
if (!pathgrid)
{
const std::string msg = "Pathgrid in cell '" + name.getRefIdString() + "' not found";
throw std::runtime_error(msg);
}
if (pathgrid == nullptr)
throw std::runtime_error("Pathgrid in cell " + name.toDebugString() + " is not found");
return pathgrid;
}
const ESM::Pathgrid* Store<ESM::Pathgrid>::search(const ESM::Cell& cell) const

View file

@ -729,7 +729,7 @@ namespace MWWorld
Ptr ret = searchPtr(name, activeOnly);
if (!ret.isEmpty())
return ret;
std::string error = "failed to find an instance of object '" + name.getRefIdString() + "'";
std::string error = "Failed to find an instance of object " + name.toDebugString();
if (activeOnly)
error += " in active cells";
throw std::runtime_error(error);
@ -1146,12 +1146,12 @@ namespace MWWorld
MWWorld::Ptr newPtr = ptr;
if (!isPlayer && !currCell)
throw std::runtime_error("Can not move actor \"" + ptr.getCellRef().getRefId().getRefIdString()
+ "\" to another cell: current cell is nullptr");
throw std::runtime_error("Can not move actor " + ptr.getCellRef().getRefId().toDebugString()
+ " to another cell: current cell is nullptr");
if (!newCell)
throw std::runtime_error("Can not move actor \"" + ptr.getCellRef().getRefId().getRefIdString()
+ "\" to another cell: new cell is nullptr");
throw std::runtime_error("Can not move actor " + ptr.getCellRef().getRefId().toDebugString()
+ " to another cell: new cell is nullptr");
if (currCell != newCell)
{

View file

@ -89,6 +89,20 @@ namespace ESM
EXPECT_EQ(lower, upper);
}
TEST(ESMRefIdTest, equalityIsDefinedForStringRefIdAndRefId)
{
const StringRefId stringRefId("ref_id");
const RefId refId = RefId::stringRefId("REF_ID");
EXPECT_EQ(stringRefId, refId);
}
TEST(ESMRefIdTest, equalityIsDefinedForFormRefIdAndRefId)
{
const FormIdRefId formIdRefId(42);
const RefId refId = RefId::formIdRefId(42);
EXPECT_EQ(formIdRefId, refId);
}
TEST(ESMRefIdTest, stringRefIdIsEqualToItself)
{
const RefId refId = RefId::stringRefId("ref_id");
@ -102,6 +116,20 @@ namespace ESM
EXPECT_LT(a, b);
}
TEST(ESMRefIdTest, lessThanIsDefinedForStringRefIdAndRefId)
{
const StringRefId stringRefId("a");
const RefId refId = RefId::stringRefId("B");
EXPECT_LT(stringRefId, refId);
}
TEST(ESMRefIdTest, lessThanIsDefinedForFormRefIdAndRefId)
{
const FormIdRefId formIdRefId(13);
const RefId refId = RefId::formIdRefId(42);
EXPECT_LT(formIdRefId, refId);
}
TEST(ESMRefIdTest, stringRefIdHasCaseInsensitiveHash)
{
const RefId lower = RefId::stringRefId("a");
@ -133,6 +161,22 @@ namespace ESM
EXPECT_LT(b, c);
}
TEST(ESMRefIdTest, stringRefIdHasStrongOrderWithFormId)
{
const RefId stringRefId = RefId::stringRefId("a");
const RefId formIdRefId = RefId::formIdRefId(42);
EXPECT_TRUE(stringRefId < formIdRefId);
EXPECT_FALSE(formIdRefId < stringRefId);
}
TEST(ESMRefIdTest, formIdRefIdHasStrongOrderWithStringView)
{
const RefId formIdRefId = RefId::formIdRefId(42);
const std::string_view stringView = "42";
EXPECT_TRUE(stringView < formIdRefId);
EXPECT_FALSE(formIdRefId < stringView);
}
TEST(ESMRefIdTest, canBeUsedAsMapKeyWithLookupByStringView)
{
const std::map<ESM::RefId, int, std::less<>> map({ { ESM::RefId::stringRefId("a"), 42 } });
@ -144,5 +188,65 @@ namespace ESM
const std::map<std::string, int, std::less<>> map({ { "a", 42 } });
EXPECT_EQ(map.count(ESM::RefId::stringRefId("A")), 1);
}
TEST(ESMRefIdTest, stringRefIdIsNotEqualToFormId)
{
const RefId stringRefId = RefId::stringRefId("\0");
const RefId formIdRefId = RefId::formIdRefId(0);
EXPECT_NE(stringRefId, formIdRefId);
}
struct ESMRefIdToStringTest : TestWithParam<std::pair<ESM::RefId, std::string>>
{
};
TEST_P(ESMRefIdToStringTest, toString)
{
const RefId& refId = GetParam().first;
const std::string& string = GetParam().second;
EXPECT_EQ(refId.toString(), string);
}
const std::vector<std::pair<RefId, std::string>> toStringParams = {
{ RefId(), std::string() },
{ RefId::stringRefId("foo"), "foo" },
{ RefId::stringRefId(std::string({ 'a', 0, -1, '\n', '\t' })), { 'a', 0, -1, '\n', '\t' } },
{ RefId::formIdRefId(42), "42" },
{ RefId::generated(42), "42" },
{ RefId::index(ESM::REC_ARMO, 42), "ARMO, 42" },
};
INSTANTIATE_TEST_SUITE_P(ESMRefIdToString, ESMRefIdToStringTest, ValuesIn(toStringParams));
struct ESMRefIdToDebugStringTest : TestWithParam<std::pair<ESM::RefId, std::string>>
{
};
TEST_P(ESMRefIdToDebugStringTest, toDebugString)
{
const RefId& refId = GetParam().first;
const std::string& debugString = GetParam().second;
EXPECT_EQ(refId.toDebugString(), debugString);
}
TEST_P(ESMRefIdToDebugStringTest, toStream)
{
const RefId& refId = GetParam().first;
const std::string& debugString = GetParam().second;
std::ostringstream stream;
stream << refId;
EXPECT_EQ(stream.str(), debugString);
}
const std::vector<std::pair<RefId, std::string>> toDebugStringParams = {
{ RefId(), "Empty{}" },
{ RefId::stringRefId("foo"), "String{foo}" },
{ RefId::stringRefId(std::string({ 'a', 0, -1, '\n', '\t' })), "String{a\\x0\\xFF\\xA\\x9}" },
{ RefId::formIdRefId(42), "FormId{42}" },
{ RefId::generated(42), "Generated{42}" },
{ RefId::index(ESM::REC_ARMO, 42), "Index{ARMO, 42}" },
};
INSTANTIATE_TEST_SUITE_P(ESMRefIdToDebugString, ESMRefIdToDebugStringTest, ValuesIn(toDebugStringParams));
}
}

View file

@ -1,6 +1,9 @@
#include <components/esm/fourcc.hpp>
#include <components/esm3/esmreader.hpp>
#include <components/esm3/esmwriter.hpp>
#include <components/esm3/loadcont.hpp>
#include <components/esm3/loadregn.hpp>
#include <components/esm3/loadscpt.hpp>
#include <components/esm3/player.hpp>
#include <gmock/gmock.h>
@ -9,15 +12,51 @@
#include <array>
#include <memory>
#include <random>
#include <type_traits>
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);
}
}
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
<< "}";
}
namespace
{
using namespace ::testing;
constexpr std::array formats = {
MaxLimitedSizeStringsFormatVersion,
MaxStringRefIdFormatVersion,
CurrentSaveGameFormatVersion,
};
@ -47,12 +86,41 @@ namespace ESM
return stream;
}
template <class T, class = std::void_t<>>
struct HasLoad : std::false_type
{
};
template <class T>
void load(ESMReader& reader, T& record)
struct HasLoad<T, std::void_t<decltype(std::declval<T>().load(std::declval<ESMReader&>()))>> : std::true_type
{
};
template <class T>
auto load(ESMReader& reader, T& record) -> std::enable_if_t<HasLoad<std::decay_t<T>>::value>
{
record.load(reader);
}
template <class T, class = std::void_t<>>
struct HasLoadWithDelete : std::false_type
{
};
template <class T>
struct HasLoadWithDelete<T,
std::void_t<decltype(std::declval<T>().load(std::declval<ESMReader&>(), std::declval<bool&>()))>>
: std::true_type
{
};
template <class T>
auto load(ESMReader& reader, T& record) -> std::enable_if_t<HasLoadWithDelete<std::decay_t<T>>::value>
{
bool deleted = false;
record.load(reader, deleted);
}
void load(ESMReader& reader, CellRef& record)
{
bool deleted = false;
@ -106,6 +174,40 @@ namespace ESM
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)
{
std::minstd_rand random;
@ -151,6 +253,44 @@ namespace ESM
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);
}
INSTANTIATE_TEST_SUITE_P(FormatVersions, Esm3SaveLoadRecordTest, ValuesIn(formats));
}
}

View file

@ -281,6 +281,7 @@ namespace
ESM::MaxOldAiPackageFormatVersion,
ESM::MaxOldSkillsAndAttributesFormatVersion,
ESM::MaxOldCreatureStatsFormatVersion,
ESM::MaxStringRefIdFormatVersion,
ESM::CurrentSaveGameFormatVersion,
};
@ -420,8 +421,10 @@ TYPED_TEST_P(StoreTest, overwrite_test)
namespace
{
using namespace ::testing;
template <class T>
struct StoreSaveLoadTest : public ::testing::Test
struct StoreSaveLoadTest : public Test
{
};
@ -445,11 +448,11 @@ namespace
using RecordType = TypeParam;
const int index = 3;
ESM::RefId refId;
decltype(RecordType::mId) refId;
if constexpr (hasIndex<RecordType> && !std::is_same_v<RecordType, ESM::LandTexture>)
refId = ESM::RefId::stringRefId(RecordType::indexToId(index));
refId = RecordType::indexToRefId(index);
else
refId = ESM::RefId::stringRefId("foobar");
refId = ESM::StringRefId("foobar");
for (const ESM::FormatVersion formatVersion : formats)
{
@ -473,7 +476,7 @@ namespace
MWWorld::ESMStore esmStore;
reader.open(getEsmFile(record, false, formatVersion), "filename");
esmStore.load(reader, &dummyListener, dialogue);
ASSERT_NO_THROW(esmStore.load(reader, &dummyListener, dialogue));
esmStore.setUp();
const RecordType* result = nullptr;
@ -551,7 +554,7 @@ namespace
template <class... T>
struct AsTestingTypes<std::tuple<T...>>
{
using Type = testing::Types<T...>;
using Type = Types<T...>;
};
using RecordTypes = typename ToRecordTypes<MWWorld::ESMStore::StoreTuple>::Type;

View file

@ -85,7 +85,12 @@ add_component_dir (to_utf8
to_utf8
)
add_component_dir(esm attr common defs esmcommon records util luascripts format refid esmbridge)
add_component_dir(esm attr common defs esmcommon records util luascripts format refid esmbridge
formidrefid
stringrefid
generatedrefid
indexrefid
)
add_component_dir(fx pass technique lexer widgets stateupdater)

View file

@ -0,0 +1,24 @@
#include "formidrefid.hpp"
#include <ostream>
#include <sstream>
namespace ESM
{
std::string FormIdRefId::toString() const
{
return std::to_string(mValue);
}
std::string FormIdRefId::toDebugString() const
{
std::ostringstream stream;
stream << *this;
return stream.str();
}
std::ostream& operator<<(std::ostream& stream, FormIdRefId value)
{
return stream << "FormId{" << value.mValue << '}';
}
}

View file

@ -0,0 +1,52 @@
#ifndef OPENMW_COMPONENTS_ESM_FORMIDREFID_HPP
#define OPENMW_COMPONENTS_ESM_FORMIDREFID_HPP
#include <functional>
#include <iosfwd>
#include <components/esm4/formid.hpp>
namespace ESM
{
class FormIdRefId
{
public:
constexpr FormIdRefId() = default;
constexpr explicit FormIdRefId(ESM4::FormId value) noexcept
: mValue(value)
{
}
ESM4::FormId getValue() const { return mValue; }
std::string toString() const;
std::string toDebugString() const;
constexpr bool operator==(FormIdRefId rhs) const noexcept { return mValue == rhs.mValue; }
constexpr bool operator<(FormIdRefId rhs) const noexcept { return mValue < rhs.mValue; }
friend std::ostream& operator<<(std::ostream& stream, FormIdRefId value);
friend struct std::hash<FormIdRefId>;
private:
ESM4::FormId mValue = 0;
};
}
namespace std
{
template <>
struct hash<ESM::FormIdRefId>
{
std::size_t operator()(ESM::FormIdRefId value) const noexcept
{
return std::hash<ESM4::FormId>{}(value.mValue);
}
};
}
#endif

View file

@ -0,0 +1,24 @@
#include "generatedrefid.hpp"
#include <ostream>
#include <sstream>
namespace ESM
{
std::string GeneratedRefId::toString() const
{
return std::to_string(mValue);
}
std::string GeneratedRefId::toDebugString() const
{
std::ostringstream stream;
stream << *this;
return stream.str();
}
std::ostream& operator<<(std::ostream& stream, GeneratedRefId value)
{
return stream << "Generated{" << value.mValue << '}';
}
}

View file

@ -0,0 +1,49 @@
#ifndef OPENMW_COMPONENTS_ESM_GENERATEDREFID_HPP
#define OPENMW_COMPONENTS_ESM_GENERATEDREFID_HPP
#include <cstdint>
#include <functional>
#include <iosfwd>
namespace ESM
{
class GeneratedRefId
{
public:
constexpr explicit GeneratedRefId(std::uint64_t value) noexcept
: mValue(value)
{
}
constexpr std::uint64_t getValue() const { return mValue; }
std::string toString() const;
std::string toDebugString() const;
constexpr bool operator==(GeneratedRefId rhs) const noexcept { return mValue == rhs.mValue; }
constexpr bool operator<(GeneratedRefId rhs) const noexcept { return mValue < rhs.mValue; }
friend std::ostream& operator<<(std::ostream& stream, GeneratedRefId value);
friend struct std::hash<GeneratedRefId>;
private:
std::uint64_t mValue;
};
}
namespace std
{
template <>
struct hash<ESM::GeneratedRefId>
{
std::size_t operator()(ESM::GeneratedRefId value) const noexcept
{
return std::hash<std::uint64_t>{}(value.mValue);
}
};
}
#endif

View file

@ -0,0 +1,26 @@
#include "indexrefid.hpp"
#include <ostream>
#include <sstream>
#include "esmcommon.hpp"
namespace ESM
{
std::string IndexRefId::toString() const
{
return ESM::NAME(mRecordType).toString() + ", " + std::to_string(mValue);
}
std::string IndexRefId::toDebugString() const
{
std::ostringstream stream;
stream << *this;
return stream.str();
}
std::ostream& operator<<(std::ostream& stream, IndexRefId value)
{
return stream << "Index{" << ESM::NAME(value.mRecordType).toStringView() << ", " << value.mValue << '}';
}
}

View file

@ -0,0 +1,61 @@
#ifndef OPENMW_COMPONENTS_ESM_INDEXREFID_HPP
#define OPENMW_COMPONENTS_ESM_INDEXREFID_HPP
#include <functional>
#include <iosfwd>
#include "defs.hpp"
namespace ESM
{
class IndexRefId
{
public:
constexpr explicit IndexRefId(RecNameInts recordType, std::uint32_t value) noexcept
: mRecordType(recordType)
, mValue(value)
{
}
constexpr RecNameInts getRecordType() const { return mRecordType; }
constexpr std::uint32_t getValue() const { return mValue; }
std::string toString() const;
std::string toDebugString() const;
friend inline constexpr auto tie(const IndexRefId& value) noexcept
{
return std::tie(value.mRecordType, value.mValue);
}
constexpr bool operator==(IndexRefId rhs) const noexcept { return tie(*this) == tie(rhs); }
constexpr bool operator<(IndexRefId rhs) const noexcept { return tie(*this) < tie(rhs); }
friend std::ostream& operator<<(std::ostream& stream, IndexRefId value);
friend struct std::hash<IndexRefId>;
private:
RecNameInts mRecordType;
std::uint32_t mValue;
};
static_assert(sizeof(IndexRefId) <= sizeof(std::uint64_t));
}
namespace std
{
template <>
struct hash<ESM::IndexRefId>
{
std::size_t operator()(ESM::IndexRefId value) const noexcept
{
return std::hash<std::uint64_t>{}(static_cast<std::uint64_t>(value.mRecordType) | value.mValue);
}
};
}
#endif

View file

@ -1,68 +1,180 @@
#include "refid.hpp"
#include <iostream>
#include "components/misc/strings/algorithm.hpp"
#include <ostream>
#include <sstream>
#include <stdexcept>
namespace ESM
{
bool RefId::operator==(const RefId& rhs) const
namespace
{
return Misc::StringUtils::ciEqual(mId, rhs.mId);
const std::string emptyString;
struct GetRefString
{
const std::string& operator()(EmptyRefId /*v*/) const { return emptyString; }
const std::string& operator()(StringRefId v) const { return v.getValue(); }
template <class T>
const std::string& operator()(const T& v) const
{
std::ostringstream stream;
stream << "RefId is not a string: " << v;
throw std::runtime_error(stream.str());
}
};
struct IsEqualToString
{
const std::string_view mRhs;
bool operator()(StringRefId v) const noexcept { return v == mRhs; }
template <class T>
bool operator()(const T& /*v*/) const noexcept
{
return false;
}
};
struct IsLessThanString
{
const std::string_view mRhs;
bool operator()(StringRefId v) const noexcept { return v < mRhs; }
template <class T>
bool operator()(const T& /*v*/) const noexcept
{
return false;
}
};
struct IsGreaterThanString
{
const std::string_view mLhs;
bool operator()(StringRefId v) const noexcept { return mLhs < v; }
template <class T>
bool operator()(const T& /*v*/) const noexcept
{
return true;
}
};
struct StartsWith
{
const std::string_view mPrefix;
bool operator()(StringRefId v) const { return v.startsWith(mPrefix); }
template <class T>
bool operator()(const T& /*v*/) const
{
return false;
}
};
struct EndsWith
{
const std::string_view mSuffix;
bool operator()(StringRefId v) const { return v.endsWith(mSuffix); }
template <class T>
bool operator()(const T& /*v*/) const
{
return false;
}
};
struct Contains
{
const std::string_view mSubString;
bool operator()(StringRefId v) const { return v.contains(mSubString); }
template <class T>
bool operator()(const T& /*v*/) const
{
return false;
}
};
}
bool RefId::operator<(const RefId& rhs) const
const RefId RefId::sEmpty = {};
std::string EmptyRefId::toString() const
{
return Misc::StringUtils::ciLess(mId, rhs.mId);
return std::string();
}
bool operator<(const RefId& lhs, std::string_view rhs)
std::string EmptyRefId::toDebugString() const
{
return Misc::StringUtils::ciLess(lhs.mId, rhs);
return "Empty{}";
}
bool operator<(std::string_view lhs, const RefId& rhs)
std::ostream& operator<<(std::ostream& stream, EmptyRefId value)
{
return Misc::StringUtils::ciLess(lhs, rhs.mId);
}
std::ostream& operator<<(std::ostream& os, const RefId& refId)
{
os << refId.getRefIdString();
return os;
}
RefId RefId::stringRefId(std::string_view id)
{
RefId newRefId;
newRefId.mId = id;
return newRefId;
}
RefId RefId::formIdRefId(const ESM4::FormId id)
{
return ESM::RefId::stringRefId(ESM4::formIdToString(id));
return stream << value.toDebugString();
}
bool RefId::operator==(std::string_view rhs) const
{
return Misc::StringUtils::ciEqual(mId, rhs);
return std::visit(IsEqualToString{ rhs }, mValue);
}
bool operator<(RefId lhs, std::string_view rhs)
{
return std::visit(IsLessThanString{ rhs }, lhs.mValue);
}
bool operator<(std::string_view lhs, RefId rhs)
{
return std::visit(IsGreaterThanString{ lhs }, rhs.mValue);
}
std::ostream& operator<<(std::ostream& stream, RefId value)
{
return std::visit([&](auto v) -> std::ostream& { return stream << v; }, value.mValue);
}
RefId RefId::stringRefId(std::string_view value)
{
if (value.empty())
return RefId();
return RefId(StringRefId(value));
}
const std::string& RefId::getRefIdString() const
{
return std::visit(GetRefString{}, mValue);
}
std::string RefId::toString() const
{
return std::visit([](auto v) { return v.toString(); }, mValue);
}
std::string RefId::toDebugString() const
{
return std::visit([](auto v) { return v.toDebugString(); }, mValue);
}
bool RefId::startsWith(std::string_view prefix) const
{
return Misc::StringUtils::ciStartsWith(mId, prefix);
return std::visit(StartsWith{ prefix }, mValue);
}
bool RefId::endsWith(std::string_view suffix) const
{
return std::visit(EndsWith{ suffix }, mValue);
}
bool RefId::contains(std::string_view subString) const
{
return Misc::StringUtils::ciFind(mId, subString) != std::string_view::npos;
return std::visit(Contains{ subString }, mValue);
}
const RefId RefId::sEmpty = {};
}
std::size_t std::hash<ESM::RefId>::operator()(const ESM::RefId& k) const
{
return Misc::StringUtils::CiHash()(k.getRefIdString());
}

View file

@ -5,11 +5,41 @@
#include <iosfwd>
#include <string>
#include <string_view>
#include <type_traits>
#include <variant>
#include <components/esm4/formid.hpp>
#include <components/misc/notnullptr.hpp>
#include "formidrefid.hpp"
#include "generatedrefid.hpp"
#include "indexrefid.hpp"
#include "stringrefid.hpp"
namespace ESM
{
struct EmptyRefId
{
constexpr bool operator==(EmptyRefId /*rhs*/) const { return true; }
constexpr bool operator<(EmptyRefId /*rhs*/) const { return false; }
std::string toString() const;
std::string toDebugString() const;
friend std::ostream& operator<<(std::ostream& stream, EmptyRefId value);
};
enum class RefIdType : std::uint8_t
{
Empty = 0,
SizedString = 1,
UnsizedString = 2,
FormId = 3,
Generated = 4,
Index = 5,
};
// RefId is used to represent an Id that identifies an ESM record. These Ids can then be used in
// ESM::Stores to find the actual record. These Ids can be serialized/de-serialized, stored on disk and remain
// valid. They are used by ESM files, by records to reference other ESM records.
@ -18,45 +48,113 @@ namespace ESM
public:
const static RefId sEmpty;
// The 2 following functions are used to move back and forth between string and RefID. Used for hard coded
// RefIds that are as string in the code. For serialization, and display. Using explicit conversions make it
// very clear where in the code we need to convert from string to RefId and Vice versa.
static RefId stringRefId(std::string_view id);
// Constructs RefId from a string using a pointer to a static set of strings.
static RefId stringRefId(std::string_view value);
static RefId formIdRefId(const ESM4::FormId id);
// Constructs RefId from ESM4 FormId storing the value in-place.
static RefId formIdRefId(ESM4::FormId value) noexcept { return RefId(FormIdRefId(value)); }
const std::string& getRefIdString() const { return mId; }
// Constructs RefId from uint64 storing the value in-place. Should be used for generated records where id is a
// global counter.
static RefId generated(std::uint64_t value) { return RefId(GeneratedRefId{ value }); }
bool empty() const { return mId.empty(); }
// Constructs RefId from record type and 32bit index storing the value in-place. Should be used for records
// identified by index (i.e. ESM3 SKIL).
static RefId index(RecNameInts recordType, std::uint32_t value) { return RefId(IndexRefId(recordType, value)); }
constexpr RefId() = default;
constexpr RefId(EmptyRefId value) noexcept
: mValue(value)
{
}
RefId(StringRefId value) noexcept
: mValue(value)
{
}
constexpr RefId(FormIdRefId value) noexcept
: mValue(value)
{
}
constexpr RefId(GeneratedRefId value) noexcept
: mValue(value)
{
}
constexpr RefId(IndexRefId value) noexcept
: mValue(value)
{
}
// Returns a reference to the value of StringRefId if it's the underlying value or throws an exception.
const std::string& getRefIdString() const;
// Returns a string with serialized underlying value.
std::string toString() const;
// Returns a string with serialized underlying value with information about underlying type.
std::string toDebugString() const;
constexpr bool empty() const noexcept { return std::holds_alternative<EmptyRefId>(mValue); }
// Returns true if underlying value is StringRefId and its underlying std::string starts with given prefix.
// Otherwise returns false.
bool startsWith(std::string_view prefix) const;
// Returns true if underlying value is StringRefId and its underlying std::string ends with given suffix.
// Otherwise returns false.
bool endsWith(std::string_view suffix) const;
// Returns true if underlying value is StringRefId and its underlying std::string contains given subString.
// Otherwise returns false.
bool contains(std::string_view subString) const;
bool operator==(const RefId& rhs) const;
friend constexpr bool operator==(const RefId& l, const RefId& r) { return l.mValue == r.mValue; }
bool operator==(std::string_view rhs) const;
bool operator<(const RefId& rhs) const;
friend constexpr bool operator<(const RefId& l, const RefId& r) { return l.mValue < r.mValue; }
friend bool operator<(const RefId& lhs, std::string_view rhs);
friend bool operator<(RefId lhs, std::string_view rhs);
friend bool operator<(std::string_view lhs, const RefId& rhs);
friend bool operator<(std::string_view lhs, RefId rhs);
friend std::ostream& operator<<(std::ostream& os, const RefId& dt);
friend std::ostream& operator<<(std::ostream& stream, RefId value);
template <class F, class... T>
friend constexpr auto visit(F&& f, T&&... v)
-> std::enable_if_t<(std::is_same_v<std::decay_t<T>, RefId> && ...),
decltype(std::visit(std::forward<F>(f), std::forward<T>(v).mValue...))>
{
return std::visit(std::forward<F>(f), std::forward<T>(v).mValue...);
}
friend struct std::hash<ESM::RefId>;
private:
std::string mId;
std::variant<EmptyRefId, StringRefId, FormIdRefId, GeneratedRefId, IndexRefId> mValue{ EmptyRefId{} };
};
}
namespace std
{
template <>
struct hash<ESM::EmptyRefId>
{
std::size_t operator()(ESM::EmptyRefId /*value*/) const noexcept { return 0; }
};
template <>
struct hash<ESM::RefId>
{
std::size_t operator()(const ESM::RefId& k) const;
std::size_t operator()(ESM::RefId value) const noexcept
{
return std::hash<decltype(value.mValue)>{}(value.mValue);
}
};
}
#endif

View file

@ -0,0 +1,93 @@
#include "stringrefid.hpp"
#include <iomanip>
#include <mutex>
#include <ostream>
#include <sstream>
#include <unordered_set>
#include "components/misc/guarded.hpp"
#include "components/misc/strings/algorithm.hpp"
namespace ESM
{
namespace
{
using StringsSet = std::unordered_set<std::string, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual>;
const std::string emptyString;
Misc::NotNullPtr<const std::string> getOrInsertString(std::string_view id)
{
static Misc::ScopeGuarded<StringsSet> refIds;
const auto locked = refIds.lock();
auto it = locked->find(id);
if (it == locked->end())
it = locked->emplace(id).first;
return &*it;
}
}
StringRefId::StringRefId()
: mValue(&emptyString)
{
}
StringRefId::StringRefId(std::string_view value)
: mValue(getOrInsertString(value))
{
}
bool StringRefId::operator==(std::string_view rhs) const noexcept
{
return Misc::StringUtils::ciEqual(*mValue, rhs);
}
bool StringRefId::operator<(StringRefId rhs) const noexcept
{
return Misc::StringUtils::ciLess(*mValue, *rhs.mValue);
}
bool operator<(StringRefId lhs, std::string_view rhs) noexcept
{
return Misc::StringUtils::ciLess(*lhs.mValue, rhs);
}
bool operator<(std::string_view lhs, StringRefId rhs) noexcept
{
return Misc::StringUtils::ciLess(lhs, *rhs.mValue);
}
std::ostream& operator<<(std::ostream& stream, StringRefId value)
{
stream << "String{";
for (char c : *value.mValue)
if (std::isprint(c) && c != '\t' && c != '\n' && c != '\r')
stream << c;
else
stream << "\\x" << std::hex << std::uppercase << static_cast<unsigned>(static_cast<unsigned char>(c));
return stream << '}';
}
std::string StringRefId::toDebugString() const
{
std::ostringstream stream;
stream << *this;
return stream.str();
}
bool StringRefId::startsWith(std::string_view prefix) const
{
return Misc::StringUtils::ciStartsWith(*mValue, prefix);
}
bool StringRefId::endsWith(std::string_view suffix) const
{
return Misc::StringUtils::ciEndsWith(*mValue, suffix);
}
bool StringRefId::contains(std::string_view subString) const
{
return Misc::StringUtils::ciFind(*mValue, subString) != std::string_view::npos;
}
}

View file

@ -0,0 +1,66 @@
#ifndef OPENMW_COMPONENTS_ESM_STRINGREFID_HPP
#define OPENMW_COMPONENTS_ESM_STRINGREFID_HPP
#include <functional>
#include <iosfwd>
#include <string>
#include <string_view>
#include <variant>
#include <components/misc/notnullptr.hpp>
namespace ESM
{
class StringRefId
{
public:
StringRefId();
// Constructs StringRefId from string using pointer to a static set of strings.
explicit StringRefId(std::string_view value);
const std::string& getValue() const { return *mValue; }
std::string toString() const { return *mValue; }
std::string toDebugString() const;
bool startsWith(std::string_view prefix) const;
bool endsWith(std::string_view suffix) const;
bool contains(std::string_view subString) const;
bool operator==(StringRefId rhs) const noexcept { return mValue == rhs.mValue; }
bool operator==(std::string_view rhs) const noexcept;
// Compares values to provide stable order
bool operator<(StringRefId rhs) const noexcept;
friend bool operator<(StringRefId lhs, std::string_view rhs) noexcept;
friend bool operator<(std::string_view lhs, StringRefId rhs) noexcept;
friend std::ostream& operator<<(std::ostream& stream, StringRefId value);
friend struct std::hash<StringRefId>;
private:
Misc::NotNullPtr<const std::string> mValue;
};
}
namespace std
{
template <>
struct hash<ESM::StringRefId>
{
std::size_t operator()(ESM::StringRefId value) const noexcept
{
return std::hash<const std::string*>{}(value.mValue);
}
};
}
#endif

View file

@ -1,6 +1,7 @@
#include "esmreader.hpp"
#include "readerscache.hpp"
#include "savedgame.hpp"
#include <components/files/conversion.hpp>
#include <components/files/openfile.hpp>
@ -158,6 +159,9 @@ namespace ESM
{
getSubHeader();
if (mHeader.mFormatVersion > MaxStringRefIdFormatVersion)
return getStringView(mCtx.leftSub);
// Hack to make MultiMark.esp load. Zero-length strings do not
// occur in any of the official mods, but MultiMark makes use of
// them. For some reason, they break the rules, and contain a byte
@ -177,7 +181,10 @@ namespace ESM
RefId ESMReader::getRefId()
{
return ESM::RefId::stringRefId(getHStringView());
if (mHeader.mFormatVersion <= MaxStringRefIdFormatVersion)
return ESM::RefId::stringRefId(getHStringView());
getSubHeader();
return getRefIdImpl(mCtx.leftSub);
}
void ESMReader::skipHString()
@ -189,7 +196,8 @@ namespace ESM
// them. For some reason, they break the rules, and contain a byte
// (value 0) even if the header says there is no data. If
// Morrowind accepts it, so should we.
if (mCtx.leftSub == 0 && hasMoreSubs() && !mEsm->peek())
if (mHeader.mFormatVersion <= MaxStringRefIdFormatVersion && mCtx.leftSub == 0 && hasMoreSubs()
&& !mEsm->peek())
{
// Skip the following zero byte
mCtx.leftRec--;
@ -378,6 +386,13 @@ namespace ESM
return std::string(getStringView(size));
}
RefId ESMReader::getMaybeFixedRefIdSize(std::size_t size)
{
if (mHeader.mFormatVersion <= MaxStringRefIdFormatVersion)
return RefId::stringRefId(getMaybeFixedStringSize(size));
return getRefIdImpl(mCtx.leftSub);
}
std::string_view ESMReader::getStringView(std::size_t size)
{
if (mBuffer.size() <= size)
@ -403,7 +418,62 @@ namespace ESM
RefId ESMReader::getRefId(std::size_t size)
{
return RefId::stringRefId(getStringView(size));
if (mHeader.mFormatVersion <= MaxStringRefIdFormatVersion)
return ESM::RefId::stringRefId(getStringView(size));
return getRefIdImpl(size);
}
RefId ESMReader::getRefIdImpl(std::size_t size)
{
RefIdType refIdType = RefIdType::Empty;
getT(refIdType);
switch (refIdType)
{
case RefIdType::Empty:
return RefId();
case RefIdType::SizedString:
{
const std::size_t minSize = sizeof(refIdType) + sizeof(StringSizeType);
if (size < minSize)
fail("Requested RefId record size is too small (" + std::to_string(size) + " < "
+ std::to_string(minSize) + ")");
StringSizeType storedSize = 0;
getT(storedSize);
const std::size_t maxSize = size - minSize;
if (storedSize > maxSize)
fail("RefId string does not fit subrecord size (" + std::to_string(storedSize) + " > "
+ std::to_string(maxSize) + ")");
return RefId::stringRefId(getStringView(storedSize));
}
case RefIdType::UnsizedString:
if (size < sizeof(refIdType))
fail("Requested RefId record size is too small (" + std::to_string(size) + " < "
+ std::to_string(sizeof(refIdType)) + ")");
return RefId::stringRefId(getStringView(size - sizeof(refIdType)));
case RefIdType::FormId:
{
ESM4::FormId formId{};
getT(formId);
return RefId::formIdRefId(formId);
}
case RefIdType::Generated:
{
std::uint64_t generated{};
getExact(&generated, sizeof(std::uint64_t));
return RefId::generated(generated);
}
case RefIdType::Index:
{
RecNameInts recordType{};
getExact(&recordType, sizeof(std::uint32_t));
std::uint32_t index{};
getExact(&index, sizeof(std::uint32_t));
return RefId::index(recordType, index);
}
}
fail("Unsupported RefIdType: " + std::to_string(static_cast<unsigned>(refIdType)));
}
[[noreturn]] void ESMReader::fail(const std::string& msg)

View file

@ -273,13 +273,17 @@ namespace ESM
skip(sizeof(T));
}
void getExact(void* x, int size) { mEsm->read((char*)x, size); }
void getExact(void* x, std::size_t size)
{
mEsm->read(static_cast<char*>(x), static_cast<std::streamsize>(size));
}
void getName(NAME& name) { getT(name); }
void getUint(uint32_t& u) { getT(u); }
std::string getMaybeFixedStringSize(std::size_t size);
RefId getMaybeFixedRefIdSize(std::size_t size) { return RefId::stringRefId(getMaybeFixedStringSize(size)); }
RefId getMaybeFixedRefIdSize(std::size_t size);
// Read the next 'size' bytes and return them as a string. Converts
// them from native encoding to UTF8 in the process.
@ -315,6 +319,8 @@ namespace ESM
void clearCtx();
RefId getRefIdImpl(std::size_t size);
std::unique_ptr<std::istream> mEsm;
ESM_Context mCtx;

View file

@ -4,10 +4,65 @@
#include <fstream>
#include <stdexcept>
#include <components/debug/debuglog.hpp>
#include <components/misc/notnullptr.hpp>
#include <components/to_utf8/to_utf8.hpp>
#include "formatversion.hpp"
namespace ESM
{
namespace
{
template <bool sizedString>
struct WriteRefId
{
ESMWriter& mWriter;
explicit WriteRefId(ESMWriter& writer)
: mWriter(writer)
{
}
void operator()(EmptyRefId /*v*/) const { mWriter.writeT(RefIdType::Empty); }
void operator()(StringRefId v) const
{
constexpr StringSizeType maxSize = std::numeric_limits<StringSizeType>::max();
if (v.getValue().size() > maxSize)
throw std::runtime_error("RefId string size is too long: \"" + v.getValue().substr(0, 64)
+ "<...>\" (" + std::to_string(v.getValue().size()) + " > " + std::to_string(maxSize) + ")");
if constexpr (sizedString)
{
mWriter.writeT(RefIdType::SizedString);
mWriter.writeT(static_cast<StringSizeType>(v.getValue().size()));
}
else
mWriter.writeT(RefIdType::UnsizedString);
mWriter.write(v.getValue().data(), v.getValue().size());
}
void operator()(FormIdRefId v) const
{
mWriter.writeT(RefIdType::FormId);
mWriter.writeT(v.getValue());
}
void operator()(GeneratedRefId v) const
{
mWriter.writeT(RefIdType::Generated);
mWriter.writeT(v.getValue());
}
void operator()(IndexRefId v) const
{
mWriter.writeT(RefIdType::Generated);
mWriter.writeT(v.getRecordType());
mWriter.writeT(v.getValue());
}
};
}
ESMWriter::ESMWriter()
: mRecords()
, mStream(nullptr)
@ -167,14 +222,18 @@ namespace ESM
endRecord(name);
}
void ESMWriter::writeHNRefId(NAME name, const RefId& value)
void ESMWriter::writeHNRefId(NAME name, RefId value)
{
writeHNString(name, value.getRefIdString());
startSubRecord(name);
writeHRefId(value);
endRecord(name);
}
void ESMWriter::writeHNRefId(NAME name, const RefId& value, std::size_t size)
void ESMWriter::writeHNRefId(NAME name, RefId value, std::size_t size)
{
writeHNString(name, value.getRefIdString(), size);
if (mHeader.mFormatVersion <= MaxStringRefIdFormatVersion)
return writeHNString(name, value.getRefIdString(), size);
writeHNRefId(name, value);
}
void ESMWriter::writeMaybeFixedSizeString(const std::string& data, std::size_t size)
@ -220,19 +279,30 @@ namespace ESM
write("\0", 1);
}
void ESMWriter::writeMaybeFixedSizeRefId(const RefId& value, std::size_t size)
void ESMWriter::writeMaybeFixedSizeRefId(RefId value, std::size_t size)
{
writeMaybeFixedSizeString(value.getRefIdString(), size);
if (mHeader.mFormatVersion <= MaxStringRefIdFormatVersion)
return writeMaybeFixedSizeString(value.getRefIdString(), size);
visit(WriteRefId<true>(*this), value);
}
void ESMWriter::writeHRefId(const RefId& value)
void ESMWriter::writeHRefId(RefId value)
{
writeHString(value.getRefIdString());
if (mHeader.mFormatVersion <= MaxStringRefIdFormatVersion)
return writeHString(value.getRefIdString());
writeRefId(value);
}
void ESMWriter::writeHCRefId(const RefId& value)
void ESMWriter::writeHCRefId(RefId value)
{
writeHCString(value.getRefIdString());
if (mHeader.mFormatVersion <= MaxStringRefIdFormatVersion)
return writeHCString(value.getRefIdString());
writeRefId(value);
}
void ESMWriter::writeRefId(RefId value)
{
visit(WriteRefId<false>(*this), value);
}
void ESMWriter::writeName(NAME name)

View file

@ -47,6 +47,8 @@ namespace ESM
// 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)
int getRecordCount() const { return mRecordCount; }
FormatVersion getFormatVersion() const { return mHeader.mFormatVersion; }
void setFormatVersion(FormatVersion value);
void clearMaster();
@ -78,24 +80,24 @@ namespace ESM
writeHNCString(name, data);
}
void writeHNRefId(NAME name, const RefId& value);
void writeHNRefId(NAME name, RefId value);
void writeHNRefId(NAME name, const RefId& value, std::size_t size);
void writeHNRefId(NAME name, RefId value, std::size_t size);
void writeHNCRefId(NAME name, const RefId& value)
void writeHNCRefId(NAME name, RefId value)
{
startSubRecord(name);
writeHCRefId(value);
endRecord(name);
}
void writeHNORefId(NAME name, const RefId& value)
void writeHNORefId(NAME name, RefId value)
{
if (!value.empty())
writeHNRefId(name, value);
}
void writeHNOCRefId(NAME name, const RefId& value)
void writeHNOCRefId(NAME name, RefId value)
{
if (!value.empty())
writeHNCRefId(name, value);
@ -165,11 +167,11 @@ namespace ESM
void writeHString(const std::string& data);
void writeHCString(const std::string& data);
void writeMaybeFixedSizeRefId(const RefId& value, std::size_t size);
void writeMaybeFixedSizeRefId(RefId value, std::size_t size);
void writeHRefId(const RefId& value);
void writeHRefId(RefId refId);
void writeHCRefId(const RefId& value);
void writeHCRefId(RefId refId);
void writeName(NAME data);
@ -184,6 +186,8 @@ namespace ESM
bool mCounting;
Header mHeader;
void writeRefId(RefId value);
};
}

View file

@ -20,7 +20,8 @@ namespace ESM
inline constexpr FormatVersion MaxOldSkillsAndAttributesFormatVersion = 18;
inline constexpr FormatVersion MaxOldCreatureStatsFormatVersion = 19;
inline constexpr FormatVersion MaxLimitedSizeStringsFormatVersion = 22;
inline constexpr FormatVersion CurrentSaveGameFormatVersion = 23;
inline constexpr FormatVersion MaxStringRefIdFormatVersion = 23;
inline constexpr FormatVersion CurrentSaveGameFormatVersion = 24;
}
#endif

View file

@ -73,4 +73,9 @@ namespace ESM
mModel.clear();
mRace = ESM::RefId();
}
bool isFirstPersonBodyPart(const BodyPart& value)
{
return value.mId.endsWith("1st");
}
}

View file

@ -72,5 +72,7 @@ namespace ESM
void blank();
///< Set record to default state (does not touch the ID).
};
bool isFirstPersonBodyPart(const BodyPart& value);
}
#endif

View file

@ -1,7 +1,5 @@
#include "loadmgef.hpp"
#include <sstream>
#include "esmreader.hpp"
#include "esmwriter.hpp"
@ -11,156 +9,6 @@ namespace ESM
{
namespace
{
static const char* sIds[MagicEffect::Length] = {
"WaterBreathing",
"SwiftSwim",
"WaterWalking",
"Shield",
"FireShield",
"LightningShield",
"FrostShield",
"Burden",
"Feather",
"Jump",
"Levitate",
"SlowFall",
"Lock",
"Open",
"FireDamage",
"ShockDamage",
"FrostDamage",
"DrainAttribute",
"DrainHealth",
"DrainMagicka",
"DrainFatigue",
"DrainSkill",
"DamageAttribute",
"DamageHealth",
"DamageMagicka",
"DamageFatigue",
"DamageSkill",
"Poison",
"WeaknessToFire",
"WeaknessToFrost",
"WeaknessToShock",
"WeaknessToMagicka",
"WeaknessToCommonDisease",
"WeaknessToBlightDisease",
"WeaknessToCorprusDisease",
"WeaknessToPoison",
"WeaknessToNormalWeapons",
"DisintegrateWeapon",
"DisintegrateArmor",
"Invisibility",
"Chameleon",
"Light",
"Sanctuary",
"NightEye",
"Charm",
"Paralyze",
"Silence",
"Blind",
"Sound",
"CalmHumanoid",
"CalmCreature",
"FrenzyHumanoid",
"FrenzyCreature",
"DemoralizeHumanoid",
"DemoralizeCreature",
"RallyHumanoid",
"RallyCreature",
"Dispel",
"Soultrap",
"Telekinesis",
"Mark",
"Recall",
"DivineIntervention",
"AlmsiviIntervention",
"DetectAnimal",
"DetectEnchantment",
"DetectKey",
"SpellAbsorption",
"Reflect",
"CureCommonDisease",
"CureBlightDisease",
"CureCorprusDisease",
"CurePoison",
"CureParalyzation",
"RestoreAttribute",
"RestoreHealth",
"RestoreMagicka",
"RestoreFatigue",
"RestoreSkill",
"FortifyAttribute",
"FortifyHealth",
"FortifyMagicka",
"FortifyFatigue",
"FortifySkill",
"FortifyMaximumMagicka",
"AbsorbAttribute",
"AbsorbHealth",
"AbsorbMagicka",
"AbsorbFatigue",
"AbsorbSkill",
"ResistFire",
"ResistFrost",
"ResistShock",
"ResistMagicka",
"ResistCommonDisease",
"ResistBlightDisease",
"ResistCorprusDisease",
"ResistPoison",
"ResistNormalWeapons",
"ResistParalysis",
"RemoveCurse",
"TurnUndead",
"SummonScamp",
"SummonClannfear",
"SummonDaedroth",
"SummonDremora",
"SummonAncestralGhost",
"SummonSkeletalMinion",
"SummonBonewalker",
"SummonGreaterBonewalker",
"SummonBonelord",
"SummonWingedTwilight",
"SummonHunger",
"SummonGoldenSaint",
"SummonFlameAtronach",
"SummonFrostAtronach",
"SummonStormAtronach",
"FortifyAttack",
"CommandCreature",
"CommandHumanoid",
"BoundDagger",
"BoundLongsword",
"BoundMace",
"BoundBattleAxe",
"BoundSpear",
"BoundLongbow",
"ExtraSpell",
"BoundCuirass",
"BoundHelm",
"BoundBoots",
"BoundShield",
"BoundGloves",
"Corprus",
"Vampirism",
"SummonCenturionSphere",
"SunDamage",
"StuntedMagicka",
// Tribunal only
"SummonFabricant",
// Bloodmoon only
"SummonWolf",
"SummonBear",
"SummonBonewolf",
"SummonCreature04",
"SummonCreature05",
};
const int NumberOfHardcodedFlags = 143;
const int HardcodedFlags[NumberOfHardcodedFlags] = { 0x11c8, 0x11c0, 0x11c8, 0x11e0, 0x11e0, 0x11e0, 0x11e0,
0x11d0, 0x11c0, 0x11c0, 0x11e0, 0x11c0, 0x11184, 0x11184, 0x1f0, 0x1f0, 0x1f0, 0x11d2, 0x11f0, 0x11d0,
@ -186,7 +34,7 @@ namespace ESM
esm.getHNT(mIndex, "INDX");
mId = ESM::RefId::stringRefId(indexToId(mIndex));
mId = indexToRefId(mIndex);
esm.getHNTSized<36>(mData, "MEDT");
if (esm.getFormatVersion() == DefaultFormatVersion)
@ -592,28 +440,10 @@ namespace ESM
mDescription.clear();
}
std::string MagicEffect::indexToId(int index)
RefId MagicEffect::indexToRefId(int index)
{
std::ostringstream stream;
if (index != -1)
{
stream << "#";
if (index < 100)
{
stream << "0";
if (index < 10)
stream << "0";
}
stream << index;
if (index >= 0 && index < Length)
stream << sIds[index];
}
return stream.str();
if (index == -1)
return RefId();
return RefId::index(sRecordId, static_cast<std::uint32_t>(index));
}
}

View file

@ -269,7 +269,7 @@ namespace ESM
Length
};
static std::string indexToId(int index);
static RefId indexToRefId(int index);
};
}
#endif

View file

@ -1,7 +1,5 @@
#include "loadskil.hpp"
#include <sstream>
#include "esmreader.hpp"
#include "esmwriter.hpp"
@ -133,7 +131,7 @@ namespace ESM
// create an ID from the index and the name (only used in the editor and likely to change in the
// future)
mId = ESM::RefId::stringRefId(indexToId(mIndex));
mId = indexToRefId(mIndex);
}
void Skill::save(ESMWriter& esm, bool /*isDeleted*/) const
@ -152,23 +150,10 @@ namespace ESM
mDescription.clear();
}
std::string Skill::indexToId(int index)
RefId Skill::indexToRefId(int index)
{
std::ostringstream stream;
if (index != -1)
{
stream << "#";
if (index < 10)
stream << "0";
stream << index;
if (index >= 0 && index < Length)
stream << sSkillNameIds[index].substr(6);
}
return stream.str();
if (index == -1)
return RefId();
return RefId::index(sRecordId, static_cast<std::uint32_t>(index));
}
}

View file

@ -87,7 +87,7 @@ namespace ESM
void blank();
///< Set record to default state (does not touch the ID/index).
static std::string indexToId(int index);
static RefId indexToRefId(int index);
};
}
#endif

View file

@ -11,7 +11,7 @@ namespace Misc
class NotNullPtr
{
public:
NotNullPtr(T* value)
constexpr NotNullPtr(T* value) noexcept
: mValue(value)
{
assert(mValue != nullptr);
@ -19,11 +19,11 @@ namespace Misc
NotNullPtr(std::nullptr_t) = delete;
operator T*() const { return mValue; }
constexpr operator T*() const noexcept { return mValue; }
T* operator->() const { return mValue; }
constexpr T* operator->() const noexcept { return mValue; }
T& operator*() const { return *mValue; }
constexpr T& operator*() const noexcept { return *mValue; }
private:
T* mValue;