#include "loadnpc.hpp" #include #include "esmreader.hpp" #include "esmwriter.hpp" #include namespace ESM { namespace { struct EsmNPDTstruct52 { int16_t mLevel{}; std::array mAttributes{}; std::array mSkills{}; uint16_t mHealth{}, mMana{}, mFatigue{}; unsigned char mDisposition{}, mReputation{}, mRank{}; int32_t mGold{}; }; struct NPDTstruct12 { EsmNPDTstruct52& mStruct; }; void toBinary(const NPC::NPDTstruct52& src, EsmNPDTstruct52& dst) { dst.mLevel = src.mLevel; for (int i = 0; i < Attribute::Length; ++i) dst.mAttributes[i] = src.getAttribute(ESM::Attribute::indexToRefId(i)); for (int i = 0; i < Skill::Length; ++i) dst.mSkills[i] = src.getSkill(ESM::Skill::indexToRefId(i)); dst.mHealth = src.mHealth; dst.mMana = src.mMana; dst.mFatigue = src.mFatigue; dst.mDisposition = src.mDisposition; dst.mReputation = src.mReputation; dst.mRank = src.mRank; dst.mGold = src.mGold; } void fromBinary(const EsmNPDTstruct52& src, NPC::NPDTstruct52& dst) { dst.mLevel = src.mLevel; dst.mAttributes.clear(); for (int i = 0; i < Attribute::Length; ++i) dst.mAttributes.emplace(ESM::Attribute::indexToRefId(i), src.mAttributes[i]); for (int i = 0; i < Skill::Length; ++i) dst.mSkills.emplace(ESM::Skill::indexToRefId(i), src.mSkills[i]); dst.mHealth = src.mHealth; dst.mMana = src.mMana; dst.mFatigue = src.mFatigue; dst.mDisposition = src.mDisposition; dst.mReputation = src.mReputation; dst.mRank = src.mRank; dst.mGold = src.mGold; } void writeBaseStats(ESMWriter& esm, NAME name, const std::map& stats) { for (const auto& [stat, value] : stats) { if (stat.empty() || value == 0) continue; esm.startSubRecord(name); esm.writeT(value); esm.writeHRefId(stat); esm.endRecord(name); } } void readBaseStats(ESMReader& esm, NAME name, std::map& stats) { while (esm.isNextSub(name)) { esm.getSubHeader(); unsigned char value; esm.getT(value); ESM::RefId stat = esm.getRefId(esm.getSubSize() - sizeof(value)); stats.emplace(stat, value); } } } template T> void decompose(T&& v, const auto& f) { char padding1 = 0; char padding2 = 0; f(v.mLevel, v.mAttributes, v.mSkills, padding1, v.mHealth, v.mMana, v.mFatigue, v.mDisposition, v.mReputation, v.mRank, padding2, v.mGold); } template T> void decompose(T&& v, const auto& f) { char padding[] = { 0, 0, 0 }; f(v.mStruct.mLevel, v.mStruct.mDisposition, v.mStruct.mReputation, v.mStruct.mRank, padding, v.mStruct.mGold); } template T> void decompose(T&& v, const auto& f) { f(v.mLevel, v.mHealth, v.mMana, v.mFatigue, v.mDisposition, v.mReputation, v.mRank, v.mGold); } void NPC::load(ESMReader& esm, bool& isDeleted) { isDeleted = false; mRecordFlags = esm.getRecordFlags(); mSpells.mList.clear(); mInventory.mList.clear(); mTransport.mList.clear(); mAiPackage.mList.clear(); mAiData.blank(); mAiData.mHello = mAiData.mFight = mAiData.mFlee = 30; bool hasName = false; bool hasNpdt = false; bool hasFlags = false; while (esm.hasMoreSubs()) { esm.getSubName(); switch (esm.retSubName().toInt()) { case SREC_NAME: mId = esm.getRefId(); hasName = true; break; case fourCC("MODL"): mModel = esm.getHString(); break; case fourCC("FNAM"): mName = esm.getHString(); break; case fourCC("RNAM"): mRace = esm.getRefId(); break; case fourCC("CNAM"): mClass = esm.getRefId(); break; case fourCC("ANAM"): mFaction = esm.getRefId(); break; case fourCC("BNAM"): mHead = esm.getRefId(); break; case fourCC("KNAM"): mHair = esm.getRefId(); break; case fourCC("SCRI"): mScript = esm.getRefId(); break; case fourCC("NPDT"): { hasNpdt = true; if (esm.getFormatVersion() <= MaxFixedStatsFormatVersion) { esm.getSubHeader(); EsmNPDTstruct52 esmData; if (esm.getSubSize() == getCompositeSize(esmData)) { mNpdtType = NPC_DEFAULT; esm.getComposite(esmData); } else { NPDTstruct12 data{ esmData }; if (esm.getSubSize() == getCompositeSize(data)) { mNpdtType = NPC_WITH_AUTOCALCULATED_STATS; // Clearing the mNdpt struct to initialize all values blankNpdt(); esm.getComposite(data); } else esm.fail("NPC_NPDT must be 12 or 52 bytes long"); } fromBinary(esmData, mNpdt); } else { esm.getSubComposite(mNpdt); readBaseStats(esm, "ATTR", mNpdt.mAttributes); readBaseStats(esm, "SKIL", mNpdt.mSkills); } break; } case fourCC("FLAG"): hasFlags = true; int32_t flags; esm.getHT(flags); mFlags = flags & 0xFF; mBloodType = ((flags >> 8) & 0xFF) >> 2; break; case fourCC("NPCS"): mSpells.add(esm); break; case fourCC("NPCO"): mInventory.add(esm); break; case fourCC("AIDT"): esm.getSubComposite(mAiData); break; case fourCC("DODT"): case fourCC("DNAM"): mTransport.add(esm); break; case AI_Wander: case AI_Activate: case AI_Escort: case AI_Follow: case AI_Travel: case AI_CNDT: mAiPackage.add(esm); break; case SREC_DELE: esm.skipHSub(); isDeleted = true; break; default: esm.fail("Unknown subrecord"); break; } } if (!hasName) esm.fail("Missing NAME subrecord"); if (!hasNpdt && !isDeleted) esm.fail("Missing NPDT subrecord"); if (!hasFlags && !isDeleted) esm.fail("Missing FLAG subrecord"); } void NPC::save(ESMWriter& esm, bool isDeleted) const { esm.writeHNCRefId("NAME", mId); if (isDeleted) { esm.writeHNString("DELE", "", 3); return; } esm.writeHNOCString("MODL", mModel.getOriginal()); esm.writeHNOCString("FNAM", mName); esm.writeHNCRefId("RNAM", mRace); esm.writeHNCRefId("CNAM", mClass); esm.writeHNCRefId("ANAM", mFaction); esm.writeHNCRefId("BNAM", mHead); esm.writeHNCRefId("KNAM", mHair); esm.writeHNOCRefId("SCRI", mScript); if (esm.getFormatVersion() <= MaxFixedStatsFormatVersion) { EsmNPDTstruct52 data; toBinary(mNpdt, data); if (mNpdtType == NPC_DEFAULT) { esm.writeNamedComposite("NPDT", data); } else if (mNpdtType == NPC_WITH_AUTOCALCULATED_STATS) { esm.writeNamedComposite("NPDT", NPDTstruct12{ data }); } } else { esm.writeNamedComposite("NPDT", mNpdt); writeBaseStats(esm, "ATTR", mNpdt.mAttributes); writeBaseStats(esm, "SKIL", mNpdt.mSkills); } esm.writeHNT("FLAG", ((mBloodType << 10) + mFlags)); mInventory.save(esm); mSpells.save(esm); esm.writeNamedComposite("AIDT", mAiData); mTransport.save(esm); mAiPackage.save(esm); } bool NPC::isMale() const { return (mFlags & Female) == 0; } void NPC::setIsMale(bool value) { mFlags |= Female; if (value) { mFlags ^= Female; } } void NPC::blank() { mRecordFlags = 0; mNpdtType = NPC_DEFAULT; blankNpdt(); mBloodType = 0; mFlags = 0; mInventory.mList.clear(); mSpells.mList.clear(); mAiData.blank(); mAiData.mHello = mAiData.mFight = mAiData.mFlee = 30; mTransport.mList.clear(); mAiPackage.mList.clear(); mName.clear(); mModel.clear(); mRace = ESM::RefId(); mClass = ESM::RefId(); mFaction = ESM::RefId(); mScript = ESM::RefId(); mHair = ESM::RefId(); mHead = ESM::RefId(); } void NPC::blankNpdt() { mNpdt.mLevel = 0; mNpdt.mAttributes.clear(); mNpdt.mSkills.clear(); mNpdt.mReputation = 0; mNpdt.mHealth = mNpdt.mMana = mNpdt.mFatigue = 0; mNpdt.mDisposition = 0; mNpdt.mRank = 0; mNpdt.mGold = 0; } int NPC::getFactionRank() const { if (mFaction.empty()) return -1; else return mNpdt.mRank; } const std::vector& NPC::getTransport() const { return mTransport.mList; } unsigned char NPC::NPDTstruct52::getAttribute(ESM::RefId id) const { const auto it = mAttributes.find(id); if (it == mAttributes.end()) return 0; return it->second; } unsigned char NPC::NPDTstruct52::getSkill(ESM::RefId id) const { const auto it = mSkills.find(id); if (it == mSkills.end()) return 0; return it->second; } }