mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-02-26 15:39:51 +00:00
Merge branch 'esm4actors' into 'master'
Show ESM4 NPCs See merge request OpenMW/openmw!3312
This commit is contained in:
commit
8c58ec9c32
@ -19,7 +19,7 @@ set(GAME_HEADER
|
||||
source_group(game FILES ${GAME} ${GAME_HEADER})
|
||||
|
||||
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
|
||||
bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation
|
||||
renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover
|
||||
@ -91,7 +91,7 @@ add_openmw_dir (mwphysics
|
||||
add_openmw_dir (mwclass
|
||||
classes activator creature npc weapon armor potion apparatus book clothing container door
|
||||
ingredient creaturelevlist itemlevlist light lockpick misc probe repair static actor bodypart
|
||||
esm4base light4
|
||||
esm4base esm4npc light4
|
||||
)
|
||||
|
||||
add_openmw_dir (mwmechanics
|
||||
|
@ -33,7 +33,6 @@
|
||||
#include "ingredient.hpp"
|
||||
#include "itemlevlist.hpp"
|
||||
#include "light.hpp"
|
||||
#include "light4.hpp"
|
||||
#include "lockpick.hpp"
|
||||
#include "misc.hpp"
|
||||
#include "npc.hpp"
|
||||
@ -44,6 +43,8 @@
|
||||
#include "weapon.hpp"
|
||||
|
||||
#include "esm4base.hpp"
|
||||
#include "esm4npc.hpp"
|
||||
#include "light4.hpp"
|
||||
|
||||
namespace MWClass
|
||||
{
|
||||
@ -72,23 +73,23 @@ namespace MWClass
|
||||
BodyPart::registerSelf();
|
||||
|
||||
ESM4Named<ESM4::Activator>::registerSelf();
|
||||
ESM4Named<ESM4::Potion>::registerSelf();
|
||||
ESM4Named<ESM4::Ammunition>::registerSelf();
|
||||
ESM4Named<ESM4::Armor>::registerSelf();
|
||||
ESM4Named<ESM4::Book>::registerSelf();
|
||||
ESM4Named<ESM4::Clothing>::registerSelf();
|
||||
ESM4Named<ESM4::Creature>::registerSelf();
|
||||
ESM4Named<ESM4::Container>::registerSelf();
|
||||
ESM4Named<ESM4::Door>::registerSelf();
|
||||
ESM4Named<ESM4::Flora>::registerSelf();
|
||||
ESM4Named<ESM4::Furniture>::registerSelf();
|
||||
ESM4Named<ESM4::Ingredient>::registerSelf();
|
||||
ESM4Light::registerSelf();
|
||||
ESM4Named<ESM4::MiscItem>::registerSelf();
|
||||
ESM4Npc::registerSelf();
|
||||
ESM4Named<ESM4::Potion>::registerSelf();
|
||||
ESM4Static::registerSelf();
|
||||
ESM4Named<ESM4::Terminal>::registerSelf();
|
||||
ESM4Tree::registerSelf();
|
||||
ESM4Named<ESM4::Weapon>::registerSelf();
|
||||
ESM4Light::registerSelf();
|
||||
ESM4Actor<ESM4::Npc>::registerSelf();
|
||||
ESM4Actor<ESM4::Creature>::registerSelf();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
#ifndef GAME_MWCLASS_ESM4BASE_H
|
||||
#define GAME_MWCLASS_ESM4BASE_H
|
||||
|
||||
#include <components/esm4/inventory.hpp>
|
||||
#include <components/esm4/loadstat.hpp>
|
||||
#include <components/esm4/loadtree.hpp>
|
||||
#include <components/misc/strings/algorithm.hpp>
|
||||
@ -9,6 +10,7 @@
|
||||
|
||||
#include "../mwworld/cellstore.hpp"
|
||||
#include "../mwworld/class.hpp"
|
||||
#include "../mwworld/esmstore.hpp"
|
||||
#include "../mwworld/registeredclass.hpp"
|
||||
|
||||
#include "classmodel.hpp"
|
||||
@ -23,13 +25,40 @@ namespace MWClass
|
||||
void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation,
|
||||
MWPhysics::PhysicsSystem& physics);
|
||||
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>
|
||||
class ESM4Base : public MWWorld::Class
|
||||
{
|
||||
|
||||
MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override
|
||||
{
|
||||
const MWWorld::LiveCellRef<Record>* ref = ptr.get<Record>();
|
||||
@ -104,14 +133,11 @@ namespace MWClass
|
||||
class ESM4Named : public MWWorld::RegisteredClass<ESM4Named<Record>, ESM4Base<Record>>
|
||||
{
|
||||
public:
|
||||
friend MWWorld::RegisteredClass<ESM4Named, ESM4Base<Record>>;
|
||||
|
||||
ESM4Named()
|
||||
: MWWorld::RegisteredClass<ESM4Named, ESM4Base<Record>>(Record::sRecordId)
|
||||
{
|
||||
}
|
||||
|
||||
public:
|
||||
bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return true; }
|
||||
|
||||
MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override
|
||||
@ -124,36 +150,6 @@ namespace MWClass
|
||||
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
|
||||
|
168
apps/openmw/mwclass/esm4npc.cpp
Normal file
168
apps/openmw/mwclass/esm4npc.cpp
Normal file
@ -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;
|
||||
}
|
||||
}
|
71
apps/openmw/mwclass/esm4npc.hpp
Normal file
71
apps/openmw/mwclass/esm4npc.hpp
Normal file
@ -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
|
187
apps/openmw/mwrender/esm4npcanimation.cpp
Normal file
187
apps/openmw/mwrender/esm4npcanimation.cpp
Normal file
@ -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);
|
||||
}
|
||||
}
|
26
apps/openmw/mwrender/esm4npcanimation.hpp
Normal file
26
apps/openmw/mwrender/esm4npcanimation.hpp
Normal file
@ -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 "creatureanimation.hpp"
|
||||
#include "esm4npcanimation.hpp"
|
||||
#include "npcanimation.hpp"
|
||||
#include "vismask.hpp"
|
||||
|
||||
@ -116,13 +117,22 @@ namespace MWRender
|
||||
insertBegin(ptr);
|
||||
ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor);
|
||||
|
||||
osg::ref_ptr<NpcAnimation> anim(
|
||||
new NpcAnimation(ptr, osg::ref_ptr<osg::Group>(ptr.getRefData().getBaseNode()), mResourceSystem));
|
||||
|
||||
if (mObjects.emplace(ptr.mRef, anim).second)
|
||||
if (ptr.getType() == ESM::REC_NPC_4)
|
||||
{
|
||||
ptr.getClass().getInventoryStore(ptr).setInvListener(anim.get());
|
||||
ptr.getClass().getInventoryStore(ptr).setContListener(anim.get());
|
||||
osg::ref_ptr<ESM4NpcAnimation> anim(
|
||||
new ESM4NpcAnimation(ptr, osg::ref_ptr<osg::Group>(ptr.getRefData().getBaseNode()), mResourceSystem));
|
||||
mObjects.emplace(ptr.mRef, anim);
|
||||
}
|
||||
else
|
||||
{
|
||||
osg::ref_ptr<NpcAnimation> anim(
|
||||
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());
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
class CreatureCustomData;
|
||||
class ESM4NpcCustomData;
|
||||
class NpcCustomData;
|
||||
class ContainerCustomData;
|
||||
class DoorCustomData;
|
||||
@ -38,6 +39,9 @@ namespace MWWorld
|
||||
|
||||
virtual MWClass::CreatureLevListCustomData& asCreatureLevListCustomData();
|
||||
virtual const MWClass::CreatureLevListCustomData& asCreatureLevListCustomData() const;
|
||||
|
||||
virtual MWClass::ESM4NpcCustomData& asESM4NpcCustomData();
|
||||
virtual const MWClass::ESM4NpcCustomData& asESM4NpcCustomData() const;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
|
@ -95,6 +95,22 @@ void ESM4::ArmorAddon::load(ESM4::Reader& reader)
|
||||
|
||||
break;
|
||||
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_MO2S: // FIXME: should group with MOD2
|
||||
case ESM4::SUB_MO2C: // FIXME: should group with MOD2
|
||||
|
@ -59,6 +59,16 @@ namespace ESM4
|
||||
|
||||
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 save(ESM4::Writer& writer) const;
|
||||
|
||||
|
@ -88,6 +88,8 @@ void ESM4::HeadPart::load(ESM4::Reader& reader)
|
||||
reader.getFormId(mBaseTexture);
|
||||
break;
|
||||
case ESM4::SUB_PNAM:
|
||||
reader.get(mType);
|
||||
break;
|
||||
case ESM4::SUB_MODT: // Model data
|
||||
case ESM4::SUB_MODC:
|
||||
case ESM4::SUB_MODS:
|
||||
|
@ -49,6 +49,26 @@ namespace ESM4
|
||||
std::string mModel;
|
||||
|
||||
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;
|
||||
|
||||
|
@ -159,9 +159,11 @@ std::string Misc::ResourceHelpers::correctActorModelPath(const std::string& resP
|
||||
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)
|
||||
|
@ -33,7 +33,7 @@ namespace Misc
|
||||
std::string correctActorModelPath(const std::string& resPath, const VFS::Manager* vfs);
|
||||
|
||||
// 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\\".
|
||||
std::string correctSoundPath(const std::string& resPath);
|
||||
|
Loading…
x
Reference in New Issue
Block a user