Render ESM4 NPCs

macos_ci_fix
Petr Mikheev 1 year ago
parent be455469ba
commit 2900351777

@ -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

@ -0,0 +1,161 @@
#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 "../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)
{
for (const auto* rec : recs)
if (rec->mIsTES4 || !(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::Ptr& ptr)
{
if (auto* data = ptr.getRefData().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;
ptr.getRefData().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
{
if (!ptr.getRefData().getCustomData())
return "";
const ESM4NpcCustomData& data = ptr.getRefData().getCustomData()->asESM4NpcCustomData();
if (data.mTraits->mIsTES4)
return "meshes\\" + data.mTraits->mModel;
if (data.mIsFemale)
return "meshes\\" + data.mRace->mModelFemale;
else
return "meshes\\" + data.mRace->mModelMale;
}
std::string_view ESM4Npc::getName(const MWWorld::ConstPtr& ptr) const
{
if (!ptr.getRefData().getCustomData())
return "";
const ESM4NpcCustomData& data = ptr.getRefData().getCustomData()->asESM4NpcCustomData();
return data.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::Ptr& ptr);
};
}
#endif // GAME_MWCLASS_ESM4ACTOR_H

@ -0,0 +1,156 @@
#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 "../mwworld/customdata.hpp"
#include "../mwworld/esmstore.hpp"
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
#include "../mwclass/esm4npc.hpp"
namespace MWRender
{
ESM4NpcAnimation::ESM4NpcAnimation(
const MWWorld::Ptr& ptr, osg::ref_ptr<osg::Group> parentNode, Resource::ResourceSystem* resourceSystem)
: Animation(ptr, std::move(parentNode), resourceSystem)
{
getOrCreateObjectRoot();
const ESM4::Npc* traits = MWClass::ESM4Npc::getTraitsRecord(mPtr);
if (traits->mIsTES4)
insertTes4NpcBodyPartsAndEquipment();
else
insertTes5NpcBodyPartsAndEquipment();
}
void ESM4NpcAnimation::insertMesh(std::string_view model)
{
std::string path = "meshes\\";
path.append(model);
mResourceSystem->getSceneManager()->getInstance(path, 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::insertTes4NpcBodyPartsAndEquipment()
{
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))
if (!bodyPart.mesh.empty())
insertMesh(bodyPart.mesh);
for (const ESM4::Race::BodyPart& bodyPart : (isFemale ? race->mHeadPartsFemale : race->mHeadParts))
if (!bodyPart.mesh.empty())
insertMesh(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))
insertMesh(hair->mModel);
}
for (const ESM4::Armor* armor : MWClass::ESM4Npc::getEquippedArmor(mPtr))
insertMesh(chooseTes4EquipmentModel(armor, isFemale));
for (const ESM4::Clothing* clothing : MWClass::ESM4Npc::getEquippedClothing(mPtr))
insertMesh(chooseTes4EquipmentModel(clothing, isFemale));
}
void ESM4NpcAnimation::insertTes5NpcBodyPartsAndEquipment()
{
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::set<uint32_t> usedHeadPartTypes;
auto addHeadParts = [&](const std::vector<ESM::FormId>& partIds) {
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.contains(part->mType))
continue;
usedHeadPartTypes.insert(part->mType);
insertMesh(part->mModel);
}
};
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())
findArmorAddons(store->get<ESM4::Armor>().find(traits->mWornArmor));
findArmorAddons(store->get<ESM4::Armor>().find(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 (covers & usedParts & ESM4::Armor::TES5_Body)
continue; // if body is already covered, skip to avoid clipping
if (covers & ~usedParts)
{ // if covers at least something that wasn't covered before - add model
usedParts |= covers;
insertMesh(isFemale ? arma->mModelFemale : arma->mModelMale);
}
}
if (usedParts & ESM4::Armor::TES5_Hair)
usedHeadPartTypes.insert(ESM4::HeadPart::Type_Hair);
addHeadParts(traits->mHeadParts);
addHeadParts(isFemale ? race->mHeadPartIdsFemale : race->mHeadPartIdsMale);
}
}

@ -0,0 +1,22 @@
#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 insertMesh(std::string_view model);
void insertTes4NpcBodyPartsAndEquipment();
void insertTes5NpcBodyPartsAndEquipment();
};
}
#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,6 +117,14 @@ namespace MWRender
insertBegin(ptr);
ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor);
if (ptr.getType() == ESM::REC_NPC_4)
{
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));
@ -125,6 +134,7 @@ namespace MWRender
ptr.getClass().getInventoryStore(ptr).setContListener(anim.get());
}
}
}
bool Objects::removeObject(const MWWorld::Ptr& ptr)
{

@ -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>

Loading…
Cancel
Save