Merge branch 'esm4actors' into 'master'

Show ESM4 NPCs

See merge request OpenMW/openmw!3312
macos_ci_fix
Alexei Kotov 1 year ago
commit 8c58ec9c32

@ -19,7 +19,7 @@ set(GAME_HEADER
source_group(game FILES ${GAME} ${GAME_HEADER}) source_group(game FILES ${GAME} ${GAME_HEADER})
add_openmw_dir (mwrender add_openmw_dir (mwrender
actors objects renderingmanager animation rotatecontroller sky skyutil npcanimation vismask actors objects renderingmanager animation rotatecontroller sky skyutil npcanimation esm4npcanimation vismask
creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager
bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation
renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover
@ -91,7 +91,7 @@ add_openmw_dir (mwphysics
add_openmw_dir (mwclass add_openmw_dir (mwclass
classes activator creature npc weapon armor potion apparatus book clothing container door classes activator creature npc weapon armor potion apparatus book clothing container door
ingredient creaturelevlist itemlevlist light lockpick misc probe repair static actor bodypart ingredient creaturelevlist itemlevlist light lockpick misc probe repair static actor bodypart
esm4base light4 esm4base esm4npc light4
) )
add_openmw_dir (mwmechanics add_openmw_dir (mwmechanics

@ -33,7 +33,6 @@
#include "ingredient.hpp" #include "ingredient.hpp"
#include "itemlevlist.hpp" #include "itemlevlist.hpp"
#include "light.hpp" #include "light.hpp"
#include "light4.hpp"
#include "lockpick.hpp" #include "lockpick.hpp"
#include "misc.hpp" #include "misc.hpp"
#include "npc.hpp" #include "npc.hpp"
@ -44,6 +43,8 @@
#include "weapon.hpp" #include "weapon.hpp"
#include "esm4base.hpp" #include "esm4base.hpp"
#include "esm4npc.hpp"
#include "light4.hpp"
namespace MWClass namespace MWClass
{ {
@ -72,23 +73,23 @@ namespace MWClass
BodyPart::registerSelf(); BodyPart::registerSelf();
ESM4Named<ESM4::Activator>::registerSelf(); ESM4Named<ESM4::Activator>::registerSelf();
ESM4Named<ESM4::Potion>::registerSelf();
ESM4Named<ESM4::Ammunition>::registerSelf(); ESM4Named<ESM4::Ammunition>::registerSelf();
ESM4Named<ESM4::Armor>::registerSelf(); ESM4Named<ESM4::Armor>::registerSelf();
ESM4Named<ESM4::Book>::registerSelf(); ESM4Named<ESM4::Book>::registerSelf();
ESM4Named<ESM4::Clothing>::registerSelf(); ESM4Named<ESM4::Clothing>::registerSelf();
ESM4Named<ESM4::Creature>::registerSelf();
ESM4Named<ESM4::Container>::registerSelf(); ESM4Named<ESM4::Container>::registerSelf();
ESM4Named<ESM4::Door>::registerSelf(); ESM4Named<ESM4::Door>::registerSelf();
ESM4Named<ESM4::Flora>::registerSelf(); ESM4Named<ESM4::Flora>::registerSelf();
ESM4Named<ESM4::Furniture>::registerSelf(); ESM4Named<ESM4::Furniture>::registerSelf();
ESM4Named<ESM4::Ingredient>::registerSelf(); ESM4Named<ESM4::Ingredient>::registerSelf();
ESM4Light::registerSelf();
ESM4Named<ESM4::MiscItem>::registerSelf(); ESM4Named<ESM4::MiscItem>::registerSelf();
ESM4Npc::registerSelf();
ESM4Named<ESM4::Potion>::registerSelf();
ESM4Static::registerSelf(); ESM4Static::registerSelf();
ESM4Named<ESM4::Terminal>::registerSelf(); ESM4Named<ESM4::Terminal>::registerSelf();
ESM4Tree::registerSelf(); ESM4Tree::registerSelf();
ESM4Named<ESM4::Weapon>::registerSelf(); ESM4Named<ESM4::Weapon>::registerSelf();
ESM4Light::registerSelf();
ESM4Actor<ESM4::Npc>::registerSelf();
ESM4Actor<ESM4::Creature>::registerSelf();
} }
} }

@ -1,6 +1,7 @@
#ifndef GAME_MWCLASS_ESM4BASE_H #ifndef GAME_MWCLASS_ESM4BASE_H
#define GAME_MWCLASS_ESM4BASE_H #define GAME_MWCLASS_ESM4BASE_H
#include <components/esm4/inventory.hpp>
#include <components/esm4/loadstat.hpp> #include <components/esm4/loadstat.hpp>
#include <components/esm4/loadtree.hpp> #include <components/esm4/loadtree.hpp>
#include <components/misc/strings/algorithm.hpp> #include <components/misc/strings/algorithm.hpp>
@ -9,6 +10,7 @@
#include "../mwworld/cellstore.hpp" #include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwworld/registeredclass.hpp" #include "../mwworld/registeredclass.hpp"
#include "classmodel.hpp" #include "classmodel.hpp"
@ -23,13 +25,40 @@ namespace MWClass
void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation,
MWPhysics::PhysicsSystem& physics); MWPhysics::PhysicsSystem& physics);
MWGui::ToolTipInfo getToolTipInfo(std::string_view name, int count); MWGui::ToolTipInfo getToolTipInfo(std::string_view name, int count);
// We don't handle ESM4 player stats yet, so for resolving levelled object we use an arbitrary number.
constexpr int sDefaultLevel = 5;
template <class LevelledRecord, class TargetRecord>
const TargetRecord* resolveLevelled(const ESM::RefId& id, int level = sDefaultLevel)
{
if (id.empty())
return nullptr;
const MWWorld::ESMStore* esmStore = MWBase::Environment::get().getESMStore();
const auto& targetStore = esmStore->get<TargetRecord>();
const TargetRecord* res = targetStore.search(id);
if (res)
return res;
const LevelledRecord* lvlRec = esmStore->get<LevelledRecord>().search(id);
if (!lvlRec)
return nullptr;
for (const ESM4::LVLO& obj : lvlRec->mLvlObject)
{
ESM::RefId candidateId = ESM::FormId::fromUint32(obj.item);
if (candidateId == id)
continue;
const TargetRecord* candidate = resolveLevelled<LevelledRecord, TargetRecord>(candidateId, level);
if (candidate && (!res || obj.level <= level))
res = candidate;
}
return res;
}
} }
// Base for all ESM4 Classes // Base for many ESM4 Classes
template <typename Record> template <typename Record>
class ESM4Base : public MWWorld::Class class ESM4Base : public MWWorld::Class
{ {
MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override
{ {
const MWWorld::LiveCellRef<Record>* ref = ptr.get<Record>(); const MWWorld::LiveCellRef<Record>* ref = ptr.get<Record>();
@ -104,14 +133,11 @@ namespace MWClass
class ESM4Named : public MWWorld::RegisteredClass<ESM4Named<Record>, ESM4Base<Record>> class ESM4Named : public MWWorld::RegisteredClass<ESM4Named<Record>, ESM4Base<Record>>
{ {
public: public:
friend MWWorld::RegisteredClass<ESM4Named, ESM4Base<Record>>;
ESM4Named() ESM4Named()
: MWWorld::RegisteredClass<ESM4Named, ESM4Base<Record>>(Record::sRecordId) : MWWorld::RegisteredClass<ESM4Named, ESM4Base<Record>>(Record::sRecordId)
{ {
} }
public:
bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return true; } bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return true; }
MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override
@ -124,36 +150,6 @@ namespace MWClass
return ptr.get<Record>()->mBase->mFullName; return ptr.get<Record>()->mBase->mFullName;
} }
}; };
template <typename Record>
class ESM4Actor : public MWWorld::RegisteredClass<ESM4Actor<Record>, ESM4Base<Record>>
{
public:
friend MWWorld::RegisteredClass<ESM4Actor, ESM4Base<Record>>;
ESM4Actor()
: MWWorld::RegisteredClass<ESM4Actor, ESM4Base<Record>>(Record::sRecordId)
{
}
void insertObjectPhysics(
const MWWorld::Ptr&, const std::string&, const osg::Quat&, MWPhysics::PhysicsSystem&) const override
{
}
bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return true; }
MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override
{
return ESM4Impl::getToolTipInfo(ptr.get<Record>()->mBase->mEditorId, count);
}
std::string getModel(const MWWorld::ConstPtr& ptr) const override
{
// TODO: Implement actor rendering. This function will typically return the skeleton.
return {};
}
};
} }
#endif // GAME_MWCLASS_ESM4BASE_H #endif // GAME_MWCLASS_ESM4BASE_H

@ -0,0 +1,168 @@
#include "esm4npc.hpp"
#include <components/esm4/loadarmo.hpp>
#include <components/esm4/loadclot.hpp>
#include <components/esm4/loadlvli.hpp>
#include <components/esm4/loadlvln.hpp>
#include <components/esm4/loadnpc.hpp>
#include <components/esm4/loadotft.hpp>
#include <components/esm4/loadrace.hpp>
#include <components/misc/resourcehelpers.hpp>
#include "../mwworld/customdata.hpp"
#include "../mwworld/esmstore.hpp"
#include "esm4base.hpp"
namespace MWClass
{
template <class LevelledRecord, class TargetRecord>
static std::vector<const TargetRecord*> withBaseTemplates(
const TargetRecord* rec, int level = MWClass::ESM4Impl::sDefaultLevel)
{
std::vector<const TargetRecord*> res{ rec };
while (true)
{
const TargetRecord* newRec
= MWClass::ESM4Impl::resolveLevelled<ESM4::LevelledNpc, ESM4::Npc>(rec->mBaseTemplate, level);
if (!newRec || newRec == rec)
return res;
res.push_back(rec = newRec);
}
}
static const ESM4::Npc* chooseTemplate(const std::vector<const ESM4::Npc*>& recs, uint16_t flag)
{
// In case of FO3 the function may return nullptr that will lead to "ESM4 NPC traits not found"
// exception and the NPC will not be added to the scene. But in any way it shouldn't cause a crash.
for (const auto* rec : recs)
if (rec->mIsTES4 || rec->mIsFONV || !(rec->mBaseConfig.tes5.templateFlags & flag))
return rec;
return nullptr;
}
class ESM4NpcCustomData : public MWWorld::TypedCustomData<ESM4NpcCustomData>
{
public:
const ESM4::Npc* mTraits;
const ESM4::Npc* mBaseData;
const ESM4::Race* mRace;
bool mIsFemale;
// TODO: Use InventoryStore instead (currently doesn't support ESM4 objects)
std::vector<const ESM4::Armor*> mEquippedArmor;
std::vector<const ESM4::Clothing*> mEquippedClothing;
ESM4NpcCustomData& asESM4NpcCustomData() override { return *this; }
const ESM4NpcCustomData& asESM4NpcCustomData() const override { return *this; }
};
ESM4NpcCustomData& ESM4Npc::getCustomData(const MWWorld::ConstPtr& ptr)
{
// Note: the argument is ConstPtr because this function is used in `getModel` and `getName`
// which are virtual and work with ConstPtr. `getModel` and `getName` use custom data
// because they require a lot of work including levelled records resolving and it would be
// stupid to not to cache the results. Maybe we should stop using ConstPtr at all
// to avoid such workarounds.
MWWorld::RefData& refData = const_cast<MWWorld::RefData&>(ptr.getRefData());
if (auto* data = refData.getCustomData())
return data->asESM4NpcCustomData();
auto data = std::make_unique<ESM4NpcCustomData>();
const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore();
auto npcRecs = withBaseTemplates<ESM4::LevelledNpc, ESM4::Npc>(ptr.get<ESM4::Npc>()->mBase);
data->mTraits = chooseTemplate(npcRecs, ESM4::Npc::TES5_UseTraits);
data->mBaseData = chooseTemplate(npcRecs, ESM4::Npc::TES5_UseBaseData);
if (!data->mTraits)
throw std::runtime_error("ESM4 NPC traits not found");
if (!data->mBaseData)
throw std::runtime_error("ESM4 NPC base data not found");
data->mRace = store->get<ESM4::Race>().find(data->mTraits->mRace);
if (data->mTraits->mIsTES4)
data->mIsFemale = data->mTraits->mBaseConfig.tes4.flags & ESM4::Npc::TES4_Female;
else if (data->mTraits->mIsFONV)
data->mIsFemale = data->mTraits->mBaseConfig.fo3.flags & ESM4::Npc::FO3_Female;
else
data->mIsFemale = data->mTraits->mBaseConfig.tes5.flags & ESM4::Npc::TES5_Female;
if (auto inv = chooseTemplate(npcRecs, ESM4::Npc::TES5_UseInventory))
{
for (const ESM4::InventoryItem& item : inv->mInventory)
{
if (auto* armor
= ESM4Impl::resolveLevelled<ESM4::LevelledItem, ESM4::Armor>(ESM::FormId::fromUint32(item.item)))
data->mEquippedArmor.push_back(armor);
else if (data->mTraits->mIsTES4)
{
const auto* clothing = ESM4Impl::resolveLevelled<ESM4::LevelledItem, ESM4::Clothing>(
ESM::FormId::fromUint32(item.item));
if (clothing)
data->mEquippedClothing.push_back(clothing);
}
}
if (!inv->mDefaultOutfit.isZeroOrUnset())
{
if (const ESM4::Outfit* outfit = store->get<ESM4::Outfit>().search(inv->mDefaultOutfit))
{
for (ESM::FormId itemId : outfit->mInventory)
if (auto* armor = ESM4Impl::resolveLevelled<ESM4::LevelledItem, ESM4::Armor>(itemId))
data->mEquippedArmor.push_back(armor);
}
else
Log(Debug::Error) << "Outfit not found: " << ESM::RefId(inv->mDefaultOutfit);
}
}
ESM4NpcCustomData& res = *data;
refData.setCustomData(std::move(data));
return res;
}
const std::vector<const ESM4::Armor*>& ESM4Npc::getEquippedArmor(const MWWorld::Ptr& ptr)
{
return getCustomData(ptr).mEquippedArmor;
}
const std::vector<const ESM4::Clothing*>& ESM4Npc::getEquippedClothing(const MWWorld::Ptr& ptr)
{
return getCustomData(ptr).mEquippedClothing;
}
const ESM4::Npc* ESM4Npc::getTraitsRecord(const MWWorld::Ptr& ptr)
{
return getCustomData(ptr).mTraits;
}
const ESM4::Race* ESM4Npc::getRace(const MWWorld::Ptr& ptr)
{
return getCustomData(ptr).mRace;
}
bool ESM4Npc::isFemale(const MWWorld::Ptr& ptr)
{
return getCustomData(ptr).mIsFemale;
}
std::string ESM4Npc::getModel(const MWWorld::ConstPtr& ptr) const
{
const ESM4NpcCustomData& data = getCustomData(ptr);
std::string_view model;
if (data.mTraits->mIsTES4)
model = data.mTraits->mModel;
else
model = data.mIsFemale ? data.mRace->mModelFemale : data.mRace->mModelMale;
const VFS::Manager* vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
return Misc::ResourceHelpers::correctMeshPath(model, vfs);
}
std::string_view ESM4Npc::getName(const MWWorld::ConstPtr& ptr) const
{
return getCustomData(ptr).mBaseData->mFullName;
}
}

@ -0,0 +1,71 @@
#ifndef GAME_MWCLASS_ESM4ACTOR_H
#define GAME_MWCLASS_ESM4ACTOR_H
#include <components/esm4/loadcrea.hpp>
#include <components/esm4/loadnpc.hpp>
#include "../mwgui/tooltips.hpp"
#include "../mwrender/objects.hpp"
#include "../mwrender/renderinginterface.hpp"
#include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/registeredclass.hpp"
#include "esm4base.hpp"
namespace MWClass
{
class ESM4Npc final : public MWWorld::RegisteredClass<ESM4Npc>
{
public:
ESM4Npc()
: MWWorld::RegisteredClass<ESM4Npc>(ESM4::Npc::sRecordId)
{
}
MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override
{
const MWWorld::LiveCellRef<ESM4::Npc>* ref = ptr.get<ESM4::Npc>();
return MWWorld::Ptr(cell.insert(ref), &cell);
}
void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model,
MWRender::RenderingInterface& renderingInterface) const override
{
renderingInterface.getObjects().insertNPC(ptr);
}
void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation,
MWPhysics::PhysicsSystem& physics) const override
{
insertObjectPhysics(ptr, model, rotation, physics);
}
void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation,
MWPhysics::PhysicsSystem& physics) const override
{
// ESM4Impl::insertObjectPhysics(ptr, getModel(ptr), rotation, physics);
}
bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return true; }
MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override
{
return ESM4Impl::getToolTipInfo(getName(ptr), count);
}
std::string getModel(const MWWorld::ConstPtr& ptr) const override;
std::string_view getName(const MWWorld::ConstPtr& ptr) const override;
static const ESM4::Npc* getTraitsRecord(const MWWorld::Ptr& ptr);
static const ESM4::Race* getRace(const MWWorld::Ptr& ptr);
static bool isFemale(const MWWorld::Ptr& ptr);
static const std::vector<const ESM4::Armor*>& getEquippedArmor(const MWWorld::Ptr& ptr);
static const std::vector<const ESM4::Clothing*>& getEquippedClothing(const MWWorld::Ptr& ptr);
private:
static ESM4NpcCustomData& getCustomData(const MWWorld::ConstPtr& ptr);
};
}
#endif // GAME_MWCLASS_ESM4ACTOR_H

@ -0,0 +1,187 @@
#include "esm4npcanimation.hpp"
#include <components/esm4/loadarma.hpp>
#include <components/esm4/loadarmo.hpp>
#include <components/esm4/loadclot.hpp>
#include <components/esm4/loadhair.hpp>
#include <components/esm4/loadhdpt.hpp>
#include <components/esm4/loadnpc.hpp>
#include <components/esm4/loadrace.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
#include "../mwclass/esm4npc.hpp"
#include "../mwworld/customdata.hpp"
#include "../mwworld/esmstore.hpp"
namespace MWRender
{
ESM4NpcAnimation::ESM4NpcAnimation(
const MWWorld::Ptr& ptr, osg::ref_ptr<osg::Group> parentNode, Resource::ResourceSystem* resourceSystem)
: Animation(ptr, std::move(parentNode), resourceSystem)
{
setObjectRoot(mPtr.getClass().getModel(mPtr), true, true, false);
updateParts();
}
void ESM4NpcAnimation::updateParts()
{
if (!mObjectRoot.get())
return;
const ESM4::Npc* traits = MWClass::ESM4Npc::getTraitsRecord(mPtr);
if (traits->mIsTES4)
updatePartsTES4();
else if (traits->mIsFONV)
{
// Not implemented yet
}
else
{
// There is no easy way to distinguish TES5 and FO3.
// In case of FO3 the function shouldn't crash the game and will
// only lead to the NPC not being rendered.
updatePartsTES5();
}
}
void ESM4NpcAnimation::insertPart(std::string_view model)
{
if (model.empty())
return;
mResourceSystem->getSceneManager()->getInstance(
Misc::ResourceHelpers::correctMeshPath(model, mResourceSystem->getVFS()), mObjectRoot.get());
}
template <class Record>
static std::string_view chooseTes4EquipmentModel(const Record* rec, bool isFemale)
{
if (isFemale && !rec->mModelFemale.empty())
return rec->mModelFemale;
else if (!isFemale && !rec->mModelMale.empty())
return rec->mModelMale;
else
return rec->mModel;
}
void ESM4NpcAnimation::updatePartsTES4()
{
const ESM4::Npc* traits = MWClass::ESM4Npc::getTraitsRecord(mPtr);
const ESM4::Race* race = MWClass::ESM4Npc::getRace(mPtr);
bool isFemale = MWClass::ESM4Npc::isFemale(mPtr);
// TODO: Body and head parts are placed incorrectly, need to attach to bones
for (const ESM4::Race::BodyPart& bodyPart : (isFemale ? race->mBodyPartsFemale : race->mBodyPartsMale))
insertPart(bodyPart.mesh);
for (const ESM4::Race::BodyPart& bodyPart : race->mHeadParts)
insertPart(bodyPart.mesh);
if (!traits->mHair.isZeroOrUnset())
{
const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore();
if (const ESM4::Hair* hair = store->get<ESM4::Hair>().search(traits->mHair))
insertPart(hair->mModel);
else
Log(Debug::Error) << "Hair not found: " << ESM::RefId(traits->mHair);
}
for (const ESM4::Armor* armor : MWClass::ESM4Npc::getEquippedArmor(mPtr))
insertPart(chooseTes4EquipmentModel(armor, isFemale));
for (const ESM4::Clothing* clothing : MWClass::ESM4Npc::getEquippedClothing(mPtr))
insertPart(chooseTes4EquipmentModel(clothing, isFemale));
}
void ESM4NpcAnimation::insertHeadParts(
const std::vector<ESM::FormId>& partIds, std::set<uint32_t>& usedHeadPartTypes)
{
const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore();
for (ESM::FormId partId : partIds)
{
if (partId.isZeroOrUnset())
continue;
const ESM4::HeadPart* part = store->get<ESM4::HeadPart>().search(partId);
if (!part)
{
Log(Debug::Error) << "Head part not found: " << ESM::RefId(partId);
continue;
}
if (usedHeadPartTypes.emplace(part->mType).second)
insertPart(part->mModel);
}
}
void ESM4NpcAnimation::updatePartsTES5()
{
const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore();
const ESM4::Npc* traits = MWClass::ESM4Npc::getTraitsRecord(mPtr);
const ESM4::Race* race = MWClass::ESM4Npc::getRace(mPtr);
bool isFemale = MWClass::ESM4Npc::isFemale(mPtr);
std::vector<const ESM4::ArmorAddon*> armorAddons;
auto findArmorAddons = [&](const ESM4::Armor* armor) {
for (ESM::FormId armaId : armor->mAddOns)
{
const ESM4::ArmorAddon* arma = store->get<ESM4::ArmorAddon>().search(armaId);
if (!arma)
{
Log(Debug::Error) << "ArmorAddon not found: " << ESM::RefId(armaId);
continue;
}
bool compatibleRace = arma->mRacePrimary == traits->mRace;
for (auto r : arma->mRaces)
if (r == traits->mRace)
compatibleRace = true;
if (compatibleRace)
armorAddons.push_back(arma);
}
};
for (const ESM4::Armor* armor : MWClass::ESM4Npc::getEquippedArmor(mPtr))
findArmorAddons(armor);
if (!traits->mWornArmor.isZeroOrUnset())
{
if (const ESM4::Armor* armor = store->get<ESM4::Armor>().search(traits->mWornArmor))
findArmorAddons(armor);
else
Log(Debug::Error) << "Worn armor not found: " << ESM::RefId(traits->mWornArmor);
}
if (!race->mSkin.isZeroOrUnset())
{
if (const ESM4::Armor* armor = store->get<ESM4::Armor>().search(race->mSkin))
findArmorAddons(armor);
else
Log(Debug::Error) << "Skin not found: " << ESM::RefId(race->mSkin);
}
if (isFemale)
std::sort(armorAddons.begin(), armorAddons.end(),
[](auto x, auto y) { return x->mFemalePriority > y->mFemalePriority; });
else
std::sort(armorAddons.begin(), armorAddons.end(),
[](auto x, auto y) { return x->mMalePriority > y->mMalePriority; });
uint32_t usedParts = 0;
for (const ESM4::ArmorAddon* arma : armorAddons)
{
const uint32_t covers = arma->mBodyTemplate.bodyPart;
// if body is already covered, skip to avoid clipping
if (covers & usedParts & ESM4::Armor::TES5_Body)
continue;
// if covers at least something that wasn't covered before - add model
if (covers & ~usedParts)
{
usedParts |= covers;
insertPart(isFemale ? arma->mModelFemale : arma->mModelMale);
}
}
std::set<uint32_t> usedHeadPartTypes;
if (usedParts & ESM4::Armor::TES5_Hair)
usedHeadPartTypes.insert(ESM4::HeadPart::Type_Hair);
insertHeadParts(traits->mHeadParts, usedHeadPartTypes);
insertHeadParts(isFemale ? race->mHeadPartIdsFemale : race->mHeadPartIdsMale, usedHeadPartTypes);
}
}

@ -0,0 +1,26 @@
#ifndef GAME_RENDER_ESM4NPCANIMATION_H
#define GAME_RENDER_ESM4NPCANIMATION_H
#include "animation.hpp"
namespace MWRender
{
class ESM4NpcAnimation : public Animation
{
public:
ESM4NpcAnimation(
const MWWorld::Ptr& ptr, osg::ref_ptr<osg::Group> parentNode, Resource::ResourceSystem* resourceSystem);
private:
void insertPart(std::string_view model);
// Works for FO3/FONV/TES5
void insertHeadParts(const std::vector<ESM::FormId>& partIds, std::set<uint32_t>& usedHeadPartTypes);
void updateParts();
void updatePartsTES4();
void updatePartsTES5();
};
}
#endif // GAME_RENDER_ESM4NPCANIMATION_H

@ -13,6 +13,7 @@
#include "animation.hpp" #include "animation.hpp"
#include "creatureanimation.hpp" #include "creatureanimation.hpp"
#include "esm4npcanimation.hpp"
#include "npcanimation.hpp" #include "npcanimation.hpp"
#include "vismask.hpp" #include "vismask.hpp"
@ -116,13 +117,22 @@ namespace MWRender
insertBegin(ptr); insertBegin(ptr);
ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor); ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor);
osg::ref_ptr<NpcAnimation> anim( if (ptr.getType() == ESM::REC_NPC_4)
new NpcAnimation(ptr, osg::ref_ptr<osg::Group>(ptr.getRefData().getBaseNode()), mResourceSystem)); {
osg::ref_ptr<ESM4NpcAnimation> anim(
if (mObjects.emplace(ptr.mRef, anim).second) new ESM4NpcAnimation(ptr, osg::ref_ptr<osg::Group>(ptr.getRefData().getBaseNode()), mResourceSystem));
mObjects.emplace(ptr.mRef, anim);
}
else
{ {
ptr.getClass().getInventoryStore(ptr).setInvListener(anim.get()); osg::ref_ptr<NpcAnimation> anim(
ptr.getClass().getInventoryStore(ptr).setContListener(anim.get()); new NpcAnimation(ptr, osg::ref_ptr<osg::Group>(ptr.getRefData().getBaseNode()), mResourceSystem));
if (mObjects.emplace(ptr.mRef, anim).second)
{
ptr.getClass().getInventoryStore(ptr).setInvListener(anim.get());
ptr.getClass().getInventoryStore(ptr).setContListener(anim.get());
}
} }
} }

@ -77,4 +77,17 @@ namespace MWWorld
throw std::logic_error(error.str()); throw std::logic_error(error.str());
} }
MWClass::ESM4NpcCustomData& CustomData::asESM4NpcCustomData()
{
std::stringstream error;
error << "bad cast " << typeid(this).name() << " to ESM4NpcCustomData";
throw std::logic_error(error.str());
}
const MWClass::ESM4NpcCustomData& CustomData::asESM4NpcCustomData() const
{
std::stringstream error;
error << "bad cast " << typeid(this).name() << " to ESM4NpcCustomData";
throw std::logic_error(error.str());
}
} }

@ -6,6 +6,7 @@
namespace MWClass namespace MWClass
{ {
class CreatureCustomData; class CreatureCustomData;
class ESM4NpcCustomData;
class NpcCustomData; class NpcCustomData;
class ContainerCustomData; class ContainerCustomData;
class DoorCustomData; class DoorCustomData;
@ -38,6 +39,9 @@ namespace MWWorld
virtual MWClass::CreatureLevListCustomData& asCreatureLevListCustomData(); virtual MWClass::CreatureLevListCustomData& asCreatureLevListCustomData();
virtual const MWClass::CreatureLevListCustomData& asCreatureLevListCustomData() const; virtual const MWClass::CreatureLevListCustomData& asCreatureLevListCustomData() const;
virtual MWClass::ESM4NpcCustomData& asESM4NpcCustomData();
virtual const MWClass::ESM4NpcCustomData& asESM4NpcCustomData() const;
}; };
template <class T> template <class T>

@ -95,6 +95,22 @@ void ESM4::ArmorAddon::load(ESM4::Reader& reader)
break; break;
case ESM4::SUB_DNAM: case ESM4::SUB_DNAM:
if (subHdr.dataSize == 12)
{
std::uint16_t unknownInt16;
std::uint8_t unknownInt8;
reader.get(mMalePriority);
reader.get(mFemalePriority);
reader.get(mWeightSliderMale);
reader.get(mWeightSliderFemale);
reader.get(unknownInt16);
reader.get(mDetectionSoundValue);
reader.get(unknownInt8);
reader.get(mWeaponAdjust);
}
else
reader.skipSubRecordData();
break;
case ESM4::SUB_MO2T: // FIXME: should group with MOD2 case ESM4::SUB_MO2T: // FIXME: should group with MOD2
case ESM4::SUB_MO2S: // FIXME: should group with MOD2 case ESM4::SUB_MO2S: // FIXME: should group with MOD2
case ESM4::SUB_MO2C: // FIXME: should group with MOD2 case ESM4::SUB_MO2C: // FIXME: should group with MOD2

@ -59,6 +59,16 @@ namespace ESM4
BodyTemplate mBodyTemplate; // TES5 BodyTemplate mBodyTemplate; // TES5
std::uint8_t mMalePriority = 0;
std::uint8_t mFemalePriority = 0;
// Flag 0x2 in mWeightSlider means that there are 2 world models for different weights: _0.nif and _1.nif
std::uint8_t mWeightSliderMale = 0;
std::uint8_t mWeightSliderFemale = 0;
std::uint8_t mDetectionSoundValue = 0;
float mWeaponAdjust = 0;
void load(ESM4::Reader& reader); void load(ESM4::Reader& reader);
// void save(ESM4::Writer& writer) const; // void save(ESM4::Writer& writer) const;

@ -88,6 +88,8 @@ void ESM4::HeadPart::load(ESM4::Reader& reader)
reader.getFormId(mBaseTexture); reader.getFormId(mBaseTexture);
break; break;
case ESM4::SUB_PNAM: case ESM4::SUB_PNAM:
reader.get(mType);
break;
case ESM4::SUB_MODT: // Model data case ESM4::SUB_MODT: // Model data
case ESM4::SUB_MODC: case ESM4::SUB_MODC:
case ESM4::SUB_MODS: case ESM4::SUB_MODS:

@ -49,6 +49,26 @@ namespace ESM4
std::string mModel; std::string mModel;
std::uint8_t mData; std::uint8_t mData;
std::uint32_t mType;
enum Type : std::uint32_t
{
Type_Misc = 0,
Type_Face = 1,
Type_Eyes = 2,
Type_Hair = 3,
Type_FacialHair = 4,
Type_Scar = 5,
Type_Eyebrows = 6,
// FO4+
Type_Meatcaps = 7,
Type_Teeth = 8,
Type_HeadRear = 9,
// Starfield
// 10 and 11 are unknown
Type_LeftEye = 12,
Type_Eyelashes = 13,
};
ESM::FormId mAdditionalPart; ESM::FormId mAdditionalPart;

@ -159,9 +159,11 @@ std::string Misc::ResourceHelpers::correctActorModelPath(const std::string& resP
return mdlname; return mdlname;
} }
std::string Misc::ResourceHelpers::correctMeshPath(const std::string& resPath, const VFS::Manager* vfs) std::string Misc::ResourceHelpers::correctMeshPath(std::string_view resPath, const VFS::Manager* vfs)
{ {
return "meshes\\" + resPath; std::string res = "meshes\\";
res.append(resPath);
return res;
} }
std::string Misc::ResourceHelpers::correctSoundPath(const std::string& resPath) std::string Misc::ResourceHelpers::correctSoundPath(const std::string& resPath)

@ -33,7 +33,7 @@ namespace Misc
std::string correctActorModelPath(const std::string& resPath, const VFS::Manager* vfs); std::string correctActorModelPath(const std::string& resPath, const VFS::Manager* vfs);
// Adds "meshes\\". // Adds "meshes\\".
std::string correctMeshPath(const std::string& resPath, const VFS::Manager* vfs); std::string correctMeshPath(std::string_view resPath, const VFS::Manager* vfs);
// Adds "sound\\". // Adds "sound\\".
std::string correctSoundPath(const std::string& resPath); std::string correctSoundPath(const std::string& resPath);

Loading…
Cancel
Save