mirror of
https://github.com/TES3MP/openmw-tes3mp.git
synced 2025-01-19 22:53:50 +00:00
Merged pull request #1944
This commit is contained in:
commit
38758a9555
20 changed files with 1631 additions and 103 deletions
|
@ -134,6 +134,7 @@
|
||||||
Bug #4653: Length of non-ASCII strings is handled incorrectly in ESM reader
|
Bug #4653: Length of non-ASCII strings is handled incorrectly in ESM reader
|
||||||
Bug #4654: Editor: UpdateVisitor does not initialize skeletons for animated objects
|
Bug #4654: Editor: UpdateVisitor does not initialize skeletons for animated objects
|
||||||
Feature #912: Editor: Add missing icons to UniversalId tables
|
Feature #912: Editor: Add missing icons to UniversalId tables
|
||||||
|
Feature #1221: Editor: Creature/NPC rendering
|
||||||
Feature #1617: Editor: Enchantment effect record verifier
|
Feature #1617: Editor: Enchantment effect record verifier
|
||||||
Feature #1645: Casting effects from objects
|
Feature #1645: Casting effects from objects
|
||||||
Feature #2606: Editor: Implemented (optional) case sensitive global search
|
Feature #2606: Editor: Implemented (optional) case sensitive global search
|
||||||
|
|
|
@ -19,6 +19,7 @@ opencs_hdrs_noqt (model/doc
|
||||||
|
|
||||||
opencs_units (model/world
|
opencs_units (model/world
|
||||||
idtable idtableproxymodel regionmap data commanddispatcher idtablebase resourcetable nestedtableproxymodel idtree infotableproxymodel landtexturetableproxymodel
|
idtable idtableproxymodel regionmap data commanddispatcher idtablebase resourcetable nestedtableproxymodel idtree infotableproxymodel landtexturetableproxymodel
|
||||||
|
actoradapter
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ opencs_units (view/render
|
||||||
scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget
|
scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget
|
||||||
previewwidget editmode instancemode instanceselectionmode instancemovemode
|
previewwidget editmode instancemode instanceselectionmode instancemovemode
|
||||||
orbitcameramode pathgridmode selectionmode pathgridselectionmode cameracontroller
|
orbitcameramode pathgridmode selectionmode pathgridselectionmode cameracontroller
|
||||||
cellwater terraintexturemode
|
cellwater terraintexturemode actor
|
||||||
)
|
)
|
||||||
|
|
||||||
opencs_units_noqt (view/render
|
opencs_units_noqt (view/render
|
||||||
|
|
686
apps/opencs/model/world/actoradapter.cpp
Normal file
686
apps/opencs/model/world/actoradapter.cpp
Normal file
|
@ -0,0 +1,686 @@
|
||||||
|
#include "actoradapter.hpp"
|
||||||
|
|
||||||
|
#include <components/esm/loadarmo.hpp>
|
||||||
|
#include <components/esm/loadclot.hpp>
|
||||||
|
#include <components/esm/loadnpc.hpp>
|
||||||
|
#include <components/esm/loadrace.hpp>
|
||||||
|
#include <components/esm/mappings.hpp>
|
||||||
|
#include <components/sceneutil/actorutil.hpp>
|
||||||
|
|
||||||
|
#include "data.hpp"
|
||||||
|
|
||||||
|
namespace CSMWorld
|
||||||
|
{
|
||||||
|
const std::string& ActorAdapter::RaceData::getId() const
|
||||||
|
{
|
||||||
|
return mId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ActorAdapter::RaceData::isBeast() const
|
||||||
|
{
|
||||||
|
return mIsBeast;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ActorAdapter::RaceData::handlesPart(ESM::PartReferenceType type) const
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case ESM::PRT_Skirt:
|
||||||
|
case ESM::PRT_Shield:
|
||||||
|
case ESM::PRT_RPauldron:
|
||||||
|
case ESM::PRT_LPauldron:
|
||||||
|
case ESM::PRT_Weapon:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& ActorAdapter::RaceData::getFemalePart(ESM::PartReferenceType index) const
|
||||||
|
{
|
||||||
|
return mFemaleParts[ESM::getMeshPart(index)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& ActorAdapter::RaceData::getMalePart(ESM::PartReferenceType index) const
|
||||||
|
{
|
||||||
|
return mMaleParts[ESM::getMeshPart(index)];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ActorAdapter::RaceData::hasDependency(const std::string& id) const
|
||||||
|
{
|
||||||
|
return mDependencies.find(id) != mDependencies.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::RaceData::setFemalePart(ESM::BodyPart::MeshPart index, const std::string& partId)
|
||||||
|
{
|
||||||
|
mFemaleParts[index] = partId;
|
||||||
|
addOtherDependency(partId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::RaceData::setMalePart(ESM::BodyPart::MeshPart index, const std::string& partId)
|
||||||
|
{
|
||||||
|
mMaleParts[index] = partId;
|
||||||
|
addOtherDependency(partId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::RaceData::addOtherDependency(const std::string& id)
|
||||||
|
{
|
||||||
|
if (!id.empty()) mDependencies.emplace(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::RaceData::reset_data(const std::string& id, bool isBeast)
|
||||||
|
{
|
||||||
|
mId = id;
|
||||||
|
mIsBeast = isBeast;
|
||||||
|
for (auto& str : mFemaleParts)
|
||||||
|
str.clear();
|
||||||
|
for (auto& str : mMaleParts)
|
||||||
|
str.clear();
|
||||||
|
mDependencies.clear();
|
||||||
|
|
||||||
|
// Mark self as a dependency
|
||||||
|
addOtherDependency(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const std::string& ActorAdapter::ActorData::getId() const
|
||||||
|
{
|
||||||
|
return mId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ActorAdapter::ActorData::isCreature() const
|
||||||
|
{
|
||||||
|
return mCreature;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ActorAdapter::ActorData::isFemale() const
|
||||||
|
{
|
||||||
|
return mFemale;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ActorAdapter::ActorData::getSkeleton() const
|
||||||
|
{
|
||||||
|
if (mCreature || !mSkeletonOverride.empty())
|
||||||
|
return "meshes\\" + mSkeletonOverride;
|
||||||
|
|
||||||
|
bool firstPerson = false;
|
||||||
|
bool beast = mRaceData ? mRaceData->isBeast() : false;
|
||||||
|
bool werewolf = false;
|
||||||
|
|
||||||
|
return SceneUtil::getActorSkeleton(firstPerson, mFemale, beast, werewolf);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string ActorAdapter::ActorData::getPart(ESM::PartReferenceType index) const
|
||||||
|
{
|
||||||
|
auto it = mParts.find(index);
|
||||||
|
if (it == mParts.end() && mRaceData && mRaceData->handlesPart(index))
|
||||||
|
{
|
||||||
|
if (mFemale)
|
||||||
|
{
|
||||||
|
// Note: we should use male parts for females as fallback
|
||||||
|
const std::string femalePart = mRaceData->getFemalePart(index);
|
||||||
|
if (!femalePart.empty())
|
||||||
|
return femalePart;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mRaceData->getMalePart(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& partName = it->second.first;
|
||||||
|
return partName;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ActorAdapter::ActorData::hasDependency(const std::string& id) const
|
||||||
|
{
|
||||||
|
return mDependencies.find(id) != mDependencies.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::ActorData::setPart(ESM::PartReferenceType index, const std::string& partId, int priority)
|
||||||
|
{
|
||||||
|
auto it = mParts.find(index);
|
||||||
|
if (it != mParts.end())
|
||||||
|
{
|
||||||
|
if (it->second.second >= priority)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mParts[index] = std::make_pair(partId, priority);
|
||||||
|
addOtherDependency(partId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::ActorData::addOtherDependency(const std::string& id)
|
||||||
|
{
|
||||||
|
if (!id.empty()) mDependencies.emplace(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::ActorData::reset_data(const std::string& id, const std::string& skeleton, bool isCreature, bool isFemale, RaceDataPtr raceData)
|
||||||
|
{
|
||||||
|
mId = id;
|
||||||
|
mCreature = isCreature;
|
||||||
|
mFemale = isFemale;
|
||||||
|
mSkeletonOverride = skeleton;
|
||||||
|
mRaceData = raceData;
|
||||||
|
mParts.clear();
|
||||||
|
mDependencies.clear();
|
||||||
|
|
||||||
|
// Mark self and race as a dependency
|
||||||
|
addOtherDependency(id);
|
||||||
|
if (raceData) addOtherDependency(raceData->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ActorAdapter::ActorAdapter(Data& data)
|
||||||
|
: mReferenceables(data.getReferenceables())
|
||||||
|
, mRaces(data.getRaces())
|
||||||
|
, mBodyParts(data.getBodyParts())
|
||||||
|
{
|
||||||
|
// Setup qt slots and signals
|
||||||
|
QAbstractItemModel* refModel = data.getTableModel(UniversalId::Type_Referenceable);
|
||||||
|
connect(refModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)),
|
||||||
|
this, SLOT(handleReferenceablesInserted(const QModelIndex&, int, int)));
|
||||||
|
connect(refModel, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)),
|
||||||
|
this, SLOT(handleReferenceableChanged(const QModelIndex&, const QModelIndex&)));
|
||||||
|
connect(refModel, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)),
|
||||||
|
this, SLOT(handleReferenceablesAboutToBeRemoved(const QModelIndex&, int, int)));
|
||||||
|
|
||||||
|
QAbstractItemModel* raceModel = data.getTableModel(UniversalId::Type_Race);
|
||||||
|
connect(raceModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)),
|
||||||
|
this, SLOT(handleRacesAboutToBeRemoved(const QModelIndex&, int, int)));
|
||||||
|
connect(raceModel, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)),
|
||||||
|
this, SLOT(handleRaceChanged(const QModelIndex&, const QModelIndex&)));
|
||||||
|
connect(raceModel, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)),
|
||||||
|
this, SLOT(handleRacesAboutToBeRemoved(const QModelIndex&, int, int)));
|
||||||
|
|
||||||
|
QAbstractItemModel* partModel = data.getTableModel(UniversalId::Type_BodyPart);
|
||||||
|
connect(partModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)),
|
||||||
|
this, SLOT(handleBodyPartsInserted(const QModelIndex&, int, int)));
|
||||||
|
connect(partModel, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)),
|
||||||
|
this, SLOT(handleBodyPartChanged(const QModelIndex&, const QModelIndex&)));
|
||||||
|
connect(partModel, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)),
|
||||||
|
this, SLOT(handleBodyPartsAboutToBeRemoved(const QModelIndex&, int, int)));
|
||||||
|
}
|
||||||
|
|
||||||
|
ActorAdapter::ActorDataPtr ActorAdapter::getActorData(const std::string& id)
|
||||||
|
{
|
||||||
|
// Return cached actor data if it exists
|
||||||
|
ActorDataPtr data = mCachedActors.get(id);
|
||||||
|
if (data)
|
||||||
|
{
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the actor data
|
||||||
|
data.reset(new ActorData());
|
||||||
|
setupActor(id, data);
|
||||||
|
mCachedActors.insert(id, data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::handleReferenceablesInserted(const QModelIndex& parent, int start, int end)
|
||||||
|
{
|
||||||
|
// Only rows added at the top level are pertinent. Others are caught by dataChanged handler.
|
||||||
|
if (!parent.isValid())
|
||||||
|
{
|
||||||
|
for (int row = start; row <= end; ++row)
|
||||||
|
{
|
||||||
|
std::string refId = mReferenceables.getId(row);
|
||||||
|
markDirtyDependency(refId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update affected
|
||||||
|
updateDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::handleReferenceableChanged(const QModelIndex& topLeft, const QModelIndex& botRight)
|
||||||
|
{
|
||||||
|
int start = getHighestIndex(topLeft).row();
|
||||||
|
int end = getHighestIndex(botRight).row();
|
||||||
|
|
||||||
|
// A change to record status (ex. Deleted) returns an invalid botRight
|
||||||
|
if (end == -1)
|
||||||
|
end = start;
|
||||||
|
|
||||||
|
// Handle each record
|
||||||
|
for (int row = start; row <= end; ++row)
|
||||||
|
{
|
||||||
|
std::string refId = mReferenceables.getId(row);
|
||||||
|
markDirtyDependency(refId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update affected
|
||||||
|
updateDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::handleReferenceablesAboutToBeRemoved(const QModelIndex& parent, int start, int end)
|
||||||
|
{
|
||||||
|
// Only rows at the top are pertinent.
|
||||||
|
if (!parent.isValid())
|
||||||
|
{
|
||||||
|
for (int row = start; row <= end; ++row)
|
||||||
|
{
|
||||||
|
std::string refId = mReferenceables.getId(row);
|
||||||
|
markDirtyDependency(refId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::handleReferenceablesRemoved(const QModelIndex& parent, int start, int end)
|
||||||
|
{
|
||||||
|
// Changes specified in handleReferenceablesAboutToBeRemoved
|
||||||
|
updateDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::handleRacesInserted(const QModelIndex& parent, int start, int end)
|
||||||
|
{
|
||||||
|
// Only rows added at the top are pertinent.
|
||||||
|
if (!parent.isValid())
|
||||||
|
{
|
||||||
|
for (int row = start; row <= end; ++row)
|
||||||
|
{
|
||||||
|
std::string raceId = mReferenceables.getId(row);
|
||||||
|
markDirtyDependency(raceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update affected
|
||||||
|
updateDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::handleRaceChanged(const QModelIndex& topLeft, const QModelIndex& botRight)
|
||||||
|
{
|
||||||
|
int start = getHighestIndex(topLeft).row();
|
||||||
|
int end = getHighestIndex(botRight).row();
|
||||||
|
|
||||||
|
// A change to record status (ex. Deleted) returns an invalid botRight
|
||||||
|
if (end == -1)
|
||||||
|
end = start;
|
||||||
|
|
||||||
|
for (int row = start; row <= end; ++row)
|
||||||
|
{
|
||||||
|
std::string raceId = mRaces.getId(row);
|
||||||
|
markDirtyDependency(raceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update affected
|
||||||
|
updateDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::handleRacesAboutToBeRemoved(const QModelIndex& parent, int start, int end)
|
||||||
|
{
|
||||||
|
// Only changes at the top are pertinent.
|
||||||
|
if (!parent.isValid())
|
||||||
|
{
|
||||||
|
for (int row = start; row <= end; ++row)
|
||||||
|
{
|
||||||
|
std::string raceId = mRaces.getId(row);
|
||||||
|
markDirtyDependency(raceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::handleRacesRemoved(const QModelIndex& parent, int start, int end)
|
||||||
|
{
|
||||||
|
// Changes specified in handleRacesAboutToBeRemoved
|
||||||
|
updateDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::handleBodyPartsInserted(const QModelIndex& parent, int start, int end)
|
||||||
|
{
|
||||||
|
// Only rows added at the top are pertinent.
|
||||||
|
if (!parent.isValid())
|
||||||
|
{
|
||||||
|
for (int row = start; row <= end; ++row)
|
||||||
|
{
|
||||||
|
// Race specified by part may need update
|
||||||
|
auto& record = mBodyParts.getRecord(row);
|
||||||
|
if (!record.isDeleted())
|
||||||
|
{
|
||||||
|
markDirtyDependency(record.get().mRace);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string partId = mBodyParts.getId(row);
|
||||||
|
markDirtyDependency(partId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update affected
|
||||||
|
updateDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::handleBodyPartChanged(const QModelIndex& topLeft, const QModelIndex& botRight)
|
||||||
|
{
|
||||||
|
int start = getHighestIndex(topLeft).row();
|
||||||
|
int end = getHighestIndex(botRight).row();
|
||||||
|
|
||||||
|
// A change to record status (ex. Deleted) returns an invalid botRight
|
||||||
|
if (end == -1)
|
||||||
|
end = start;
|
||||||
|
|
||||||
|
for (int row = start; row <= end; ++row)
|
||||||
|
{
|
||||||
|
// Race specified by part may need update
|
||||||
|
auto& record = mBodyParts.getRecord(row);
|
||||||
|
if (!record.isDeleted())
|
||||||
|
{
|
||||||
|
markDirtyDependency(record.get().mRace);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update entries with a tracked dependency
|
||||||
|
std::string partId = mBodyParts.getId(row);
|
||||||
|
markDirtyDependency(partId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update affected
|
||||||
|
updateDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::handleBodyPartsAboutToBeRemoved(const QModelIndex& parent, int start, int end)
|
||||||
|
{
|
||||||
|
// Only changes at the top are pertinent.
|
||||||
|
if (!parent.isValid())
|
||||||
|
{
|
||||||
|
for (int row = start; row <= end; ++row)
|
||||||
|
{
|
||||||
|
std::string partId = mBodyParts.getId(row);
|
||||||
|
markDirtyDependency(partId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::handleBodyPartsRemoved(const QModelIndex& parent, int start, int end)
|
||||||
|
{
|
||||||
|
// Changes specified in handleBodyPartsAboutToBeRemoved
|
||||||
|
updateDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex ActorAdapter::getHighestIndex(QModelIndex index) const
|
||||||
|
{
|
||||||
|
while (index.parent().isValid())
|
||||||
|
index = index.parent();
|
||||||
|
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 std::string& id)
|
||||||
|
{
|
||||||
|
// Return cached race data if it exists
|
||||||
|
RaceDataPtr data = mCachedRaces.get(id);
|
||||||
|
if (data) return data;
|
||||||
|
|
||||||
|
// Create the race data
|
||||||
|
data.reset(new RaceData());
|
||||||
|
setupRace(id, data);
|
||||||
|
mCachedRaces.insert(id, data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::setupActor(const std::string& id, ActorDataPtr data)
|
||||||
|
{
|
||||||
|
int index = mReferenceables.searchId(id);
|
||||||
|
if (index == -1)
|
||||||
|
{
|
||||||
|
// Record does not exist
|
||||||
|
data->reset_data(id);
|
||||||
|
emit actorChanged(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& record = mReferenceables.getRecord(index);
|
||||||
|
if (record.isDeleted())
|
||||||
|
{
|
||||||
|
// Record is deleted and therefore not accessible
|
||||||
|
data->reset_data(id);
|
||||||
|
emit actorChanged(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int TypeColumn = mReferenceables.findColumnIndex(Columns::ColumnId_RecordType);
|
||||||
|
int type = mReferenceables.getData(index, TypeColumn).toInt();
|
||||||
|
if (type == UniversalId::Type_Creature)
|
||||||
|
{
|
||||||
|
// Valid creature record
|
||||||
|
setupCreature(id, data);
|
||||||
|
emit actorChanged(id);
|
||||||
|
}
|
||||||
|
else if (type == UniversalId::Type_Npc)
|
||||||
|
{
|
||||||
|
// Valid npc record
|
||||||
|
setupNpc(id, data);
|
||||||
|
emit actorChanged(id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Wrong record type
|
||||||
|
data->reset_data(id);
|
||||||
|
emit actorChanged(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::setupRace(const std::string& id, RaceDataPtr data)
|
||||||
|
{
|
||||||
|
int index = mRaces.searchId(id);
|
||||||
|
if (index == -1)
|
||||||
|
{
|
||||||
|
// Record does not exist
|
||||||
|
data->reset_data(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& raceRecord = mRaces.getRecord(index);
|
||||||
|
if (raceRecord.isDeleted())
|
||||||
|
{
|
||||||
|
// Record is deleted, so not accessible
|
||||||
|
data->reset_data(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& race = raceRecord.get();
|
||||||
|
data->reset_data(id, race.mData.mFlags & ESM::Race::Beast);
|
||||||
|
|
||||||
|
// Setup body parts
|
||||||
|
for (int i = 0; i < mBodyParts.getSize(); ++i)
|
||||||
|
{
|
||||||
|
std::string partId = mBodyParts.getId(i);
|
||||||
|
auto& partRecord = mBodyParts.getRecord(i);
|
||||||
|
|
||||||
|
if (partRecord.isDeleted())
|
||||||
|
{
|
||||||
|
// Record is deleted, so not accessible.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& part = partRecord.get();
|
||||||
|
if (part.mRace == id && part.mData.mType == ESM::BodyPart::MT_Skin && !is1stPersonPart(part.mId))
|
||||||
|
{
|
||||||
|
auto type = (ESM::BodyPart::MeshPart) part.mData.mPart;
|
||||||
|
bool female = part.mData.mFlags & ESM::BodyPart::BPF_Female;
|
||||||
|
if (female) data->setFemalePart(type, part.mId);
|
||||||
|
else data->setMalePart(type, part.mId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::setupNpc(const std::string& id, ActorDataPtr data)
|
||||||
|
{
|
||||||
|
// Common setup, record is known to exist and is not deleted
|
||||||
|
int index = mReferenceables.searchId(id);
|
||||||
|
auto& npc = dynamic_cast<const Record<ESM::NPC>&>(mReferenceables.getRecord(index)).get();
|
||||||
|
|
||||||
|
RaceDataPtr raceData = getRaceData(npc.mRace);
|
||||||
|
data->reset_data(id, "", false, !npc.isMale(), raceData);
|
||||||
|
|
||||||
|
// Add inventory items
|
||||||
|
for (auto& item : npc.mInventory.mList)
|
||||||
|
{
|
||||||
|
if (item.mCount <= 0) continue;
|
||||||
|
std::string itemId = item.mItem.toString();
|
||||||
|
addNpcItem(itemId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add head and hair
|
||||||
|
data->setPart(ESM::PRT_Head, npc.mHead, 0);
|
||||||
|
data->setPart(ESM::PRT_Hair, npc.mHair, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::addNpcItem(const std::string& itemId, ActorDataPtr data)
|
||||||
|
{
|
||||||
|
int index = mReferenceables.searchId(itemId);
|
||||||
|
if (index == -1)
|
||||||
|
{
|
||||||
|
// Item does not exist yet
|
||||||
|
data->addOtherDependency(itemId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& record = mReferenceables.getRecord(index);
|
||||||
|
if (record.isDeleted())
|
||||||
|
{
|
||||||
|
// Item cannot be accessed yet
|
||||||
|
data->addOtherDependency(itemId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience function to add a parts list to actor data
|
||||||
|
auto addParts = [&](const std::vector<ESM::PartReference>& list, int priority) {
|
||||||
|
for (auto& part : list)
|
||||||
|
{
|
||||||
|
std::string partId;
|
||||||
|
auto partType = (ESM::PartReferenceType) part.mPart;
|
||||||
|
|
||||||
|
if (data->isFemale())
|
||||||
|
partId = part.mFemale;
|
||||||
|
if (partId.empty())
|
||||||
|
partId = part.mMale;
|
||||||
|
|
||||||
|
data->setPart(partType, partId, priority);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
int TypeColumn = mReferenceables.findColumnIndex(Columns::ColumnId_RecordType);
|
||||||
|
int type = mReferenceables.getData(index, TypeColumn).toInt();
|
||||||
|
if (type == UniversalId::Type_Armor)
|
||||||
|
{
|
||||||
|
auto& armor = dynamic_cast<const Record<ESM::Armor>&>(record).get();
|
||||||
|
addParts(armor.mParts.mParts, 1);
|
||||||
|
|
||||||
|
// Changing parts could affect what is picked for rendering
|
||||||
|
data->addOtherDependency(itemId);
|
||||||
|
}
|
||||||
|
else if (type == UniversalId::Type_Clothing)
|
||||||
|
{
|
||||||
|
int priority = 0;
|
||||||
|
// TODO: reserve bodyparts for robes and skirts
|
||||||
|
auto& clothing = dynamic_cast<const Record<ESM::Clothing>&>(record).get();
|
||||||
|
|
||||||
|
if (clothing.mData.mType == ESM::Clothing::Robe)
|
||||||
|
{
|
||||||
|
auto reservedList = std::vector<ESM::PartReference>();
|
||||||
|
|
||||||
|
ESM::PartReference pr;
|
||||||
|
pr.mMale = "";
|
||||||
|
pr.mFemale = "";
|
||||||
|
|
||||||
|
ESM::PartReferenceType parts[] = {
|
||||||
|
ESM::PRT_Groin, ESM::PRT_Skirt, ESM::PRT_RLeg, ESM::PRT_LLeg,
|
||||||
|
ESM::PRT_RUpperarm, ESM::PRT_LUpperarm, ESM::PRT_RKnee, ESM::PRT_LKnee,
|
||||||
|
ESM::PRT_RForearm, ESM::PRT_LForearm
|
||||||
|
};
|
||||||
|
size_t parts_size = sizeof(parts)/sizeof(parts[0]);
|
||||||
|
for(size_t p = 0;p < parts_size;++p)
|
||||||
|
{
|
||||||
|
pr.mPart = parts[p];
|
||||||
|
reservedList.push_back(pr);
|
||||||
|
}
|
||||||
|
|
||||||
|
priority = parts_size;
|
||||||
|
addParts(reservedList, priority);
|
||||||
|
}
|
||||||
|
else if (clothing.mData.mType == ESM::Clothing::Skirt)
|
||||||
|
{
|
||||||
|
auto reservedList = std::vector<ESM::PartReference>();
|
||||||
|
|
||||||
|
ESM::PartReference pr;
|
||||||
|
pr.mMale = "";
|
||||||
|
pr.mFemale = "";
|
||||||
|
|
||||||
|
ESM::PartReferenceType parts[] = {
|
||||||
|
ESM::PRT_Groin, ESM::PRT_RLeg, ESM::PRT_LLeg
|
||||||
|
};
|
||||||
|
size_t parts_size = sizeof(parts)/sizeof(parts[0]);
|
||||||
|
for(size_t p = 0;p < parts_size;++p)
|
||||||
|
{
|
||||||
|
pr.mPart = parts[p];
|
||||||
|
reservedList.push_back(pr);
|
||||||
|
}
|
||||||
|
|
||||||
|
priority = parts_size;
|
||||||
|
addParts(reservedList, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
addParts(clothing.mParts.mParts, priority);
|
||||||
|
|
||||||
|
// Changing parts could affect what is picked for rendering
|
||||||
|
data->addOtherDependency(itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::setupCreature(const std::string& id, ActorDataPtr data)
|
||||||
|
{
|
||||||
|
// Record is known to exist and is not deleted
|
||||||
|
int index = mReferenceables.searchId(id);
|
||||||
|
auto& creature = dynamic_cast<const Record<ESM::Creature>&>(mReferenceables.getRecord(index)).get();
|
||||||
|
|
||||||
|
data->reset_data(id, creature.mModel, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::markDirtyDependency(const std::string& dep)
|
||||||
|
{
|
||||||
|
for (auto raceIt : mCachedRaces)
|
||||||
|
{
|
||||||
|
if (raceIt->hasDependency(dep))
|
||||||
|
mDirtyRaces.emplace(raceIt->getId());
|
||||||
|
}
|
||||||
|
for (auto actorIt : mCachedActors)
|
||||||
|
{
|
||||||
|
if (actorIt->hasDependency(dep))
|
||||||
|
mDirtyActors.emplace(actorIt->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActorAdapter::updateDirty()
|
||||||
|
{
|
||||||
|
// Handle races before actors, since actors are dependent on race
|
||||||
|
for (auto& race : mDirtyRaces)
|
||||||
|
{
|
||||||
|
RaceDataPtr data = mCachedRaces.get(race);
|
||||||
|
if (data)
|
||||||
|
{
|
||||||
|
setupRace(race, data);
|
||||||
|
// Race was changed. Need to mark actor dependencies as dirty.
|
||||||
|
// Cannot use markDirtyDependency because that would invalidate
|
||||||
|
// the current iterator.
|
||||||
|
for (auto actorIt : mCachedActors)
|
||||||
|
{
|
||||||
|
if (actorIt->hasDependency(race))
|
||||||
|
mDirtyActors.emplace(actorIt->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mDirtyRaces.clear();
|
||||||
|
|
||||||
|
for (auto& actor : mDirtyActors)
|
||||||
|
{
|
||||||
|
ActorDataPtr data = mCachedActors.get(actor);
|
||||||
|
if (data)
|
||||||
|
{
|
||||||
|
setupActor(actor, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mDirtyActors.clear();
|
||||||
|
}
|
||||||
|
}
|
174
apps/opencs/model/world/actoradapter.hpp
Normal file
174
apps/opencs/model/world/actoradapter.hpp
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
#ifndef CSM_WOLRD_ACTORADAPTER_H
|
||||||
|
#define CSM_WOLRD_ACTORADAPTER_H
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <map>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QModelIndex>
|
||||||
|
|
||||||
|
#include <components/esm/loadarmo.hpp>
|
||||||
|
#include <components/esm/loadbody.hpp>
|
||||||
|
#include <components/misc/weakcache.hpp>
|
||||||
|
|
||||||
|
#include "refidcollection.hpp"
|
||||||
|
#include "idcollection.hpp"
|
||||||
|
|
||||||
|
namespace ESM
|
||||||
|
{
|
||||||
|
struct Race;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace CSMWorld
|
||||||
|
{
|
||||||
|
class Data;
|
||||||
|
|
||||||
|
/// Adapts multiple collections to provide the data needed to render
|
||||||
|
/// an npc or creature.
|
||||||
|
class ActorAdapter : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
|
||||||
|
/// A list indexed by ESM::PartReferenceType
|
||||||
|
using ActorPartList = std::map<ESM::PartReferenceType, std::pair<std::string, int>>;
|
||||||
|
/// A list indexed by ESM::BodyPart::MeshPart
|
||||||
|
using RacePartList = std::array<std::string, ESM::BodyPart::MP_Count>;
|
||||||
|
/// Tracks unique strings
|
||||||
|
using StringSet = std::unordered_set<std::string>;
|
||||||
|
|
||||||
|
|
||||||
|
/// Contains base race data shared between actors
|
||||||
|
class RaceData
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/// Retrieves the id of the race represented
|
||||||
|
const std::string& getId() const;
|
||||||
|
/// Checks if it's a beast race
|
||||||
|
bool isBeast() const;
|
||||||
|
/// Checks if a part could exist for the given type
|
||||||
|
bool handlesPart(ESM::PartReferenceType type) const;
|
||||||
|
/// Retrieves the associated body part
|
||||||
|
const std::string& getFemalePart(ESM::PartReferenceType index) const;
|
||||||
|
/// Retrieves the associated body part
|
||||||
|
const std::string& getMalePart(ESM::PartReferenceType index) const;
|
||||||
|
/// Checks if the race has a data dependency
|
||||||
|
bool hasDependency(const std::string& id) const;
|
||||||
|
|
||||||
|
/// Sets the associated part if it's empty and marks a dependency
|
||||||
|
void setFemalePart(ESM::BodyPart::MeshPart partIndex, const std::string& partId);
|
||||||
|
/// Sets the associated part if it's empty and marks a dependency
|
||||||
|
void setMalePart(ESM::BodyPart::MeshPart partIndex, const std::string& partId);
|
||||||
|
/// Marks an additional dependency
|
||||||
|
void addOtherDependency(const std::string& id);
|
||||||
|
/// Clears parts and dependencies
|
||||||
|
void reset_data(const std::string& raceId, bool isBeast=false);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool handles(ESM::PartReferenceType type) const;
|
||||||
|
std::string mId;
|
||||||
|
bool mIsBeast;
|
||||||
|
RacePartList mFemaleParts;
|
||||||
|
RacePartList mMaleParts;
|
||||||
|
StringSet mDependencies;
|
||||||
|
};
|
||||||
|
using RaceDataPtr = std::shared_ptr<RaceData>;
|
||||||
|
|
||||||
|
/// Contains all the data needed to render an actor. Tracks dependencies
|
||||||
|
/// so that pertinent data changes can be checked.
|
||||||
|
class ActorData
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/// Retrieves the id of the actor represented
|
||||||
|
const std::string& getId() const;
|
||||||
|
/// Checks if the actor is a creature
|
||||||
|
bool isCreature() const;
|
||||||
|
/// Checks if the actor is female
|
||||||
|
bool isFemale() const;
|
||||||
|
/// Returns the skeleton the actor should use for attaching parts to
|
||||||
|
std::string getSkeleton() const;
|
||||||
|
/// Retrieves the associated actor part
|
||||||
|
const std::string getPart(ESM::PartReferenceType index) const;
|
||||||
|
/// Checks if the actor has a data dependency
|
||||||
|
bool hasDependency(const std::string& id) const;
|
||||||
|
|
||||||
|
/// Sets the actor part used and marks a dependency
|
||||||
|
void setPart(ESM::PartReferenceType partIndex, const std::string& partId, int priority);
|
||||||
|
/// Marks an additional dependency for the actor
|
||||||
|
void addOtherDependency(const std::string& id);
|
||||||
|
/// Clears race, parts, and dependencies
|
||||||
|
void reset_data(const std::string& actorId, const std::string& skeleton="", bool isCreature=false, bool female=true, RaceDataPtr raceData=nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string mId;
|
||||||
|
bool mCreature;
|
||||||
|
bool mFemale;
|
||||||
|
std::string mSkeletonOverride;
|
||||||
|
RaceDataPtr mRaceData;
|
||||||
|
ActorPartList mParts;
|
||||||
|
StringSet mDependencies;
|
||||||
|
};
|
||||||
|
using ActorDataPtr = std::shared_ptr<ActorData>;
|
||||||
|
|
||||||
|
|
||||||
|
ActorAdapter(Data& data);
|
||||||
|
|
||||||
|
/// Obtains the shared data for a given actor
|
||||||
|
ActorDataPtr getActorData(const std::string& refId);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
|
||||||
|
void actorChanged(const std::string& refId);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
|
||||||
|
void handleReferenceablesInserted(const QModelIndex&, int, int);
|
||||||
|
void handleReferenceableChanged(const QModelIndex&, const QModelIndex&);
|
||||||
|
void handleReferenceablesAboutToBeRemoved(const QModelIndex&, int, int);
|
||||||
|
void handleReferenceablesRemoved(const QModelIndex&, int, int);
|
||||||
|
|
||||||
|
void handleRacesInserted(const QModelIndex&, int, int);
|
||||||
|
void handleRaceChanged(const QModelIndex&, const QModelIndex&);
|
||||||
|
void handleRacesAboutToBeRemoved(const QModelIndex&, int, int);
|
||||||
|
void handleRacesRemoved(const QModelIndex&, int, int);
|
||||||
|
|
||||||
|
void handleBodyPartsInserted(const QModelIndex&, int, int);
|
||||||
|
void handleBodyPartChanged(const QModelIndex&, const QModelIndex&);
|
||||||
|
void handleBodyPartsAboutToBeRemoved(const QModelIndex&, int, int);
|
||||||
|
void handleBodyPartsRemoved(const QModelIndex&, int, int);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
ActorAdapter(const ActorAdapter&) = delete;
|
||||||
|
ActorAdapter& operator=(const ActorAdapter&) = delete;
|
||||||
|
|
||||||
|
QModelIndex getHighestIndex(QModelIndex) const;
|
||||||
|
bool is1stPersonPart(const std::string& id) const;
|
||||||
|
|
||||||
|
RaceDataPtr getRaceData(const std::string& raceId);
|
||||||
|
|
||||||
|
void setupActor(const std::string& id, ActorDataPtr data);
|
||||||
|
void setupRace(const std::string& id, RaceDataPtr data);
|
||||||
|
|
||||||
|
void setupNpc(const std::string& id, ActorDataPtr data);
|
||||||
|
void addNpcItem(const std::string& itemId, ActorDataPtr data);
|
||||||
|
|
||||||
|
void setupCreature(const std::string& id, ActorDataPtr data);
|
||||||
|
|
||||||
|
void markDirtyDependency(const std::string& dependency);
|
||||||
|
void updateDirty();
|
||||||
|
|
||||||
|
RefIdCollection& mReferenceables;
|
||||||
|
IdCollection<ESM::Race>& mRaces;
|
||||||
|
IdCollection<ESM::BodyPart>& mBodyParts;
|
||||||
|
|
||||||
|
Misc::WeakCache<std::string, ActorData> mCachedActors; // Key: referenceable id
|
||||||
|
Misc::WeakCache<std::string, RaceData> mCachedRaces; // Key: race id
|
||||||
|
|
||||||
|
StringSet mDirtyActors; // Actors that need updating
|
||||||
|
StringSet mDirtyRaces; // Races that need updating
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -572,6 +572,8 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat
|
||||||
UniversalId::Type_Video);
|
UniversalId::Type_Video);
|
||||||
addModel (new IdTable (&mMetaData), UniversalId::Type_MetaData);
|
addModel (new IdTable (&mMetaData), UniversalId::Type_MetaData);
|
||||||
|
|
||||||
|
mActorAdapter.reset(new ActorAdapter(*this));
|
||||||
|
|
||||||
mRefLoadCache.clear(); // clear here rather than startLoading() and continueLoading() for multiple content files
|
mRefLoadCache.clear(); // clear here rather than startLoading() and continueLoading() for multiple content files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -912,6 +914,16 @@ QAbstractItemModel *CSMWorld::Data::getTableModel (const CSMWorld::UniversalId&
|
||||||
return iter->second;
|
return iter->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CSMWorld::ActorAdapter* CSMWorld::Data::getActorAdapter() const
|
||||||
|
{
|
||||||
|
return mActorAdapter.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
CSMWorld::ActorAdapter* CSMWorld::Data::getActorAdapter()
|
||||||
|
{
|
||||||
|
return mActorAdapter.get();
|
||||||
|
}
|
||||||
|
|
||||||
void CSMWorld::Data::merge()
|
void CSMWorld::Data::merge()
|
||||||
{
|
{
|
||||||
mGlobals.merge();
|
mGlobals.merge();
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
|
|
||||||
#include "../doc/stage.hpp"
|
#include "../doc/stage.hpp"
|
||||||
|
|
||||||
|
#include "actoradapter.hpp"
|
||||||
#include "idcollection.hpp"
|
#include "idcollection.hpp"
|
||||||
#include "nestedidcollection.hpp"
|
#include "nestedidcollection.hpp"
|
||||||
#include "universalid.hpp"
|
#include "universalid.hpp"
|
||||||
|
@ -110,6 +111,7 @@ namespace CSMWorld
|
||||||
RefCollection mRefs;
|
RefCollection mRefs;
|
||||||
IdCollection<ESM::Filter> mFilters;
|
IdCollection<ESM::Filter> mFilters;
|
||||||
Collection<MetaData> mMetaData;
|
Collection<MetaData> mMetaData;
|
||||||
|
std::unique_ptr<ActorAdapter> mActorAdapter;
|
||||||
const Fallback::Map* mFallbackMap;
|
const Fallback::Map* mFallbackMap;
|
||||||
std::vector<QAbstractItemModel *> mModels;
|
std::vector<QAbstractItemModel *> mModels;
|
||||||
std::map<UniversalId::Type, QAbstractItemModel *> mModelIndex;
|
std::map<UniversalId::Type, QAbstractItemModel *> mModelIndex;
|
||||||
|
@ -287,6 +289,10 @@ namespace CSMWorld
|
||||||
/// \note The returned table may either be the model for the ID itself or the model that
|
/// \note The returned table may either be the model for the ID itself or the model that
|
||||||
/// contains the record specified by the ID.
|
/// contains the record specified by the ID.
|
||||||
|
|
||||||
|
const ActorAdapter* getActorAdapter() const;
|
||||||
|
|
||||||
|
ActorAdapter* getActorAdapter();
|
||||||
|
|
||||||
void merge();
|
void merge();
|
||||||
///< Merge modified into base.
|
///< Merge modified into base.
|
||||||
|
|
||||||
|
|
128
apps/opencs/view/render/actor.cpp
Normal file
128
apps/opencs/view/render/actor.cpp
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
#include "actor.hpp"
|
||||||
|
|
||||||
|
#include <osg/Group>
|
||||||
|
#include <osg/Node>
|
||||||
|
|
||||||
|
#include <components/esm/mappings.hpp>
|
||||||
|
#include <components/misc/resourcehelpers.hpp>
|
||||||
|
#include <components/resource/resourcemanager.hpp>
|
||||||
|
#include <components/resource/scenemanager.hpp>
|
||||||
|
#include <components/sceneutil/attach.hpp>
|
||||||
|
#include <components/sceneutil/skeleton.hpp>
|
||||||
|
|
||||||
|
#include "../../model/world/data.hpp"
|
||||||
|
|
||||||
|
namespace CSVRender
|
||||||
|
{
|
||||||
|
const std::string Actor::MeshPrefix = "meshes\\";
|
||||||
|
|
||||||
|
Actor::Actor(const std::string& id, CSMWorld::Data& data)
|
||||||
|
: mId(id)
|
||||||
|
, mData(data)
|
||||||
|
, mBaseNode(new osg::Group())
|
||||||
|
, mSkeleton(nullptr)
|
||||||
|
{
|
||||||
|
mActorData = mData.getActorAdapter()->getActorData(mId);
|
||||||
|
connect(mData.getActorAdapter(), SIGNAL(actorChanged(const std::string&)),
|
||||||
|
this, SLOT(handleActorChanged(const std::string&)));
|
||||||
|
}
|
||||||
|
|
||||||
|
osg::Group* Actor::getBaseNode()
|
||||||
|
{
|
||||||
|
return mBaseNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Actor::update()
|
||||||
|
{
|
||||||
|
mBaseNode->removeChildren(0, mBaseNode->getNumChildren());
|
||||||
|
|
||||||
|
// Load skeleton
|
||||||
|
std::string skeletonModel = mActorData->getSkeleton();
|
||||||
|
skeletonModel = Misc::ResourceHelpers::correctActorModelPath(skeletonModel, mData.getResourceSystem()->getVFS());
|
||||||
|
loadSkeleton(skeletonModel);
|
||||||
|
|
||||||
|
if (!mActorData->isCreature())
|
||||||
|
{
|
||||||
|
// Get rid of the extra attachments
|
||||||
|
SceneUtil::CleanObjectRootVisitor cleanVisitor;
|
||||||
|
mSkeleton->accept(cleanVisitor);
|
||||||
|
cleanVisitor.remove();
|
||||||
|
|
||||||
|
// Attach parts to skeleton
|
||||||
|
loadBodyParts();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SceneUtil::RemoveTriBipVisitor removeTriBipVisitor;
|
||||||
|
mSkeleton->accept(removeTriBipVisitor);
|
||||||
|
removeTriBipVisitor.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post setup
|
||||||
|
mSkeleton->markDirty();
|
||||||
|
mSkeleton->setActive(SceneUtil::Skeleton::Active);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Actor::handleActorChanged(const std::string& refId)
|
||||||
|
{
|
||||||
|
if (mId == refId)
|
||||||
|
{
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Actor::loadSkeleton(const std::string& model)
|
||||||
|
{
|
||||||
|
auto sceneMgr = mData.getResourceSystem()->getSceneManager();
|
||||||
|
|
||||||
|
osg::ref_ptr<osg::Node> temp = sceneMgr->getInstance(model);
|
||||||
|
mSkeleton = dynamic_cast<SceneUtil::Skeleton*>(temp.get());
|
||||||
|
if (!mSkeleton)
|
||||||
|
{
|
||||||
|
mSkeleton = new SceneUtil::Skeleton();
|
||||||
|
mSkeleton->addChild(temp);
|
||||||
|
}
|
||||||
|
mBaseNode->addChild(mSkeleton);
|
||||||
|
|
||||||
|
// Map bone names to bones
|
||||||
|
mNodeMap.clear();
|
||||||
|
SceneUtil::NodeMapVisitor nmVisitor(mNodeMap);
|
||||||
|
mSkeleton->accept(nmVisitor);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Actor::loadBodyParts()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < ESM::PRT_Count; ++i)
|
||||||
|
{
|
||||||
|
auto type = (ESM::PartReferenceType) i;
|
||||||
|
std::string partId = mActorData->getPart(type);
|
||||||
|
attachBodyPart(type, getBodyPartMesh(partId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Actor::attachBodyPart(ESM::PartReferenceType type, const std::string& mesh)
|
||||||
|
{
|
||||||
|
auto sceneMgr = mData.getResourceSystem()->getSceneManager();
|
||||||
|
|
||||||
|
// Attach to skeleton
|
||||||
|
std::string boneName = ESM::getBoneName(type);
|
||||||
|
auto node = mNodeMap.find(boneName);
|
||||||
|
if (!mesh.empty() && node != mNodeMap.end())
|
||||||
|
{
|
||||||
|
auto instance = sceneMgr->getInstance(mesh);
|
||||||
|
SceneUtil::attach(instance, mSkeleton, boneName, node->second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Actor::getBodyPartMesh(const std::string& bodyPartId)
|
||||||
|
{
|
||||||
|
const auto& bodyParts = mData.getBodyParts();
|
||||||
|
|
||||||
|
int index = bodyParts.searchId(bodyPartId);
|
||||||
|
if (index != -1 && !bodyParts.getRecord(index).isDeleted())
|
||||||
|
return MeshPrefix + bodyParts.getRecord(index).get().mModel;
|
||||||
|
else
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
71
apps/opencs/view/render/actor.hpp
Normal file
71
apps/opencs/view/render/actor.hpp
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
#ifndef OPENCS_VIEW_RENDER_ACTOR_H
|
||||||
|
#define OPENCS_VIEW_RENDER_ACTOR_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <osg/ref_ptr>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include <components/esm/loadarmo.hpp>
|
||||||
|
#include <components/sceneutil/visitor.hpp>
|
||||||
|
|
||||||
|
#include "../../model/world/actoradapter.hpp"
|
||||||
|
|
||||||
|
namespace osg
|
||||||
|
{
|
||||||
|
class Group;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace CSMWorld
|
||||||
|
{
|
||||||
|
class Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace SceneUtil
|
||||||
|
{
|
||||||
|
class Skeleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace CSVRender
|
||||||
|
{
|
||||||
|
/// Handles loading an npc or creature
|
||||||
|
class Actor : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
/// Creates an actor.
|
||||||
|
/// \param id The referenceable id
|
||||||
|
/// \param type The record type
|
||||||
|
/// \param data The data store
|
||||||
|
Actor(const std::string& id, CSMWorld::Data& data);
|
||||||
|
|
||||||
|
/// Retrieves the base node that meshes are attached to
|
||||||
|
osg::Group* getBaseNode();
|
||||||
|
|
||||||
|
/// (Re)creates the npc or creature renderable
|
||||||
|
void update();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void handleActorChanged(const std::string& refId);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void loadSkeleton(const std::string& model);
|
||||||
|
void loadBodyParts();
|
||||||
|
void attachBodyPart(ESM::PartReferenceType, const std::string& mesh);
|
||||||
|
|
||||||
|
std::string getBodyPartMesh(const std::string& bodyPartId);
|
||||||
|
|
||||||
|
static const std::string MeshPrefix;
|
||||||
|
|
||||||
|
std::string mId;
|
||||||
|
CSMWorld::Data& mData;
|
||||||
|
CSMWorld::ActorAdapter::ActorDataPtr mActorData;
|
||||||
|
|
||||||
|
osg::ref_ptr<osg::Group> mBaseNode;
|
||||||
|
SceneUtil::Skeleton* mSkeleton;
|
||||||
|
SceneUtil::NodeMapVisitor::NodeMap mNodeMap;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -1,6 +1,8 @@
|
||||||
#include "object.hpp"
|
#include "object.hpp"
|
||||||
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
#include <osg/Depth>
|
#include <osg/Depth>
|
||||||
#include <osg/Group>
|
#include <osg/Group>
|
||||||
|
@ -29,6 +31,7 @@
|
||||||
#include <components/sceneutil/lightmanager.hpp>
|
#include <components/sceneutil/lightmanager.hpp>
|
||||||
#include <components/fallback/fallback.hpp>
|
#include <components/fallback/fallback.hpp>
|
||||||
|
|
||||||
|
#include "actor.hpp"
|
||||||
#include "mask.hpp"
|
#include "mask.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,27 +81,26 @@ void CSVRender::Object::update()
|
||||||
{
|
{
|
||||||
clear();
|
clear();
|
||||||
|
|
||||||
std::string model;
|
|
||||||
int error = 0; // 1 referenceable does not exist, 2 referenceable does not specify a mesh
|
|
||||||
|
|
||||||
const CSMWorld::RefIdCollection& referenceables = mData.getReferenceables();
|
const CSMWorld::RefIdCollection& referenceables = mData.getReferenceables();
|
||||||
|
const int TypeIndex = referenceables.findColumnIndex(CSMWorld::Columns::ColumnId_RecordType);
|
||||||
|
const int ModelIndex = referenceables.findColumnIndex (CSMWorld::Columns::ColumnId_Model);
|
||||||
|
|
||||||
int index = referenceables.searchId (mReferenceableId);
|
int index = referenceables.searchId (mReferenceableId);
|
||||||
const ESM::Light* light = NULL;
|
const ESM::Light* light = NULL;
|
||||||
|
|
||||||
|
mBaseNode->removeChildren(0, mBaseNode->getNumChildren());
|
||||||
|
|
||||||
if (index == -1)
|
if (index == -1)
|
||||||
error = 1;
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
|
mBaseNode->addChild(createErrorCube());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/// \todo check for Deleted state (error 1)
|
/// \todo check for Deleted state (error 1)
|
||||||
|
|
||||||
model = referenceables.getData (index,
|
int recordType = referenceables.getData(index, TypeIndex).toInt();
|
||||||
referenceables.findColumnIndex (CSMWorld::Columns::ColumnId_Model)).
|
std::string model = referenceables.getData(index, ModelIndex).toString().toUtf8().constData();
|
||||||
toString().toUtf8().constData();
|
|
||||||
|
|
||||||
int recordType =
|
|
||||||
referenceables.getData (index,
|
|
||||||
referenceables.findColumnIndex(CSMWorld::Columns::ColumnId_RecordType)).toInt();
|
|
||||||
if (recordType == CSMWorld::UniversalId::Type_Light)
|
if (recordType == CSMWorld::UniversalId::Type_Light)
|
||||||
{
|
{
|
||||||
light = &dynamic_cast<const CSMWorld::Record<ESM::Light>& >(referenceables.getRecord(index)).get();
|
light = &dynamic_cast<const CSMWorld::Record<ESM::Light>& >(referenceables.getRecord(index)).get();
|
||||||
|
@ -112,30 +114,29 @@ void CSVRender::Object::update()
|
||||||
model = "marker_creature.nif";
|
model = "marker_creature.nif";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.empty())
|
try
|
||||||
error = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
mBaseNode->removeChildren(0, mBaseNode->getNumChildren());
|
|
||||||
|
|
||||||
if (error)
|
|
||||||
{
|
{
|
||||||
mBaseNode->addChild(createErrorCube());
|
if (recordType == CSMWorld::UniversalId::Type_Npc || recordType == CSMWorld::UniversalId::Type_Creature)
|
||||||
|
{
|
||||||
|
if (!mActor) mActor.reset(new Actor(mReferenceableId, mData));
|
||||||
|
mActor->update();
|
||||||
|
mBaseNode->addChild(mActor->getBaseNode());
|
||||||
|
}
|
||||||
|
else if (!model.empty())
|
||||||
|
{
|
||||||
|
std::string path = "meshes\\" + model;
|
||||||
|
mResourceSystem->getSceneManager()->getInstance(path, mBaseNode);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
try
|
throw std::runtime_error(mReferenceableId + " has no model");
|
||||||
{
|
}
|
||||||
std::string path = "meshes\\" + model;
|
|
||||||
|
|
||||||
mResourceSystem->getSceneManager()->getInstance(path, mBaseNode);
|
|
||||||
}
|
}
|
||||||
catch (std::exception& e)
|
catch (std::exception& e)
|
||||||
{
|
{
|
||||||
// TODO: use error marker mesh
|
// TODO: use error marker mesh
|
||||||
Log(Debug::Error) << e.what();
|
Log(Debug::Error) << e.what();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (light)
|
if (light)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#ifndef OPENCS_VIEW_OBJECT_H
|
#ifndef OPENCS_VIEW_OBJECT_H
|
||||||
#define OPENCS_VIEW_OBJECT_H
|
#define OPENCS_VIEW_OBJECT_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <osg/ref_ptr>
|
#include <osg/ref_ptr>
|
||||||
|
@ -41,6 +42,7 @@ namespace CSMWorld
|
||||||
|
|
||||||
namespace CSVRender
|
namespace CSVRender
|
||||||
{
|
{
|
||||||
|
class Actor;
|
||||||
class Object;
|
class Object;
|
||||||
|
|
||||||
// An object to attach as user data to the osg::Node, allows us to get an Object back from a Node when we are doing a ray query
|
// An object to attach as user data to the osg::Node, allows us to get an Object back from a Node when we are doing a ray query
|
||||||
|
@ -98,6 +100,7 @@ namespace CSVRender
|
||||||
osg::ref_ptr<osg::Node> mMarker[3];
|
osg::ref_ptr<osg::Node> mMarker[3];
|
||||||
int mSubMode;
|
int mSubMode;
|
||||||
float mMarkerTransparency;
|
float mMarkerTransparency;
|
||||||
|
std::unique_ptr<Actor> mActor;
|
||||||
|
|
||||||
/// Not implemented
|
/// Not implemented
|
||||||
Object (const Object&);
|
Object (const Object&);
|
||||||
|
|
|
@ -91,32 +91,6 @@ namespace
|
||||||
std::vector<osg::ref_ptr<osg::Node> > mToRemove;
|
std::vector<osg::ref_ptr<osg::Node> > mToRemove;
|
||||||
};
|
};
|
||||||
|
|
||||||
class NodeMapVisitor : public osg::NodeVisitor
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
typedef std::map<std::string, osg::ref_ptr<osg::MatrixTransform> > NodeMap;
|
|
||||||
|
|
||||||
NodeMapVisitor(NodeMap& map)
|
|
||||||
: osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN)
|
|
||||||
, mMap(map)
|
|
||||||
{}
|
|
||||||
|
|
||||||
void apply(osg::MatrixTransform& trans)
|
|
||||||
{
|
|
||||||
// Take transformation for first found node in file
|
|
||||||
const std::string nodeName = Misc::StringUtils::lowerCase(trans.getName());
|
|
||||||
if (mMap.find(nodeName) == mMap.end())
|
|
||||||
{
|
|
||||||
mMap[nodeName] = &trans;
|
|
||||||
}
|
|
||||||
|
|
||||||
traverse(trans);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
NodeMap& mMap;
|
|
||||||
};
|
|
||||||
|
|
||||||
NifOsg::TextKeyMap::const_iterator findGroupStart(const NifOsg::TextKeyMap &keys, const std::string &groupname)
|
NifOsg::TextKeyMap::const_iterator findGroupStart(const NifOsg::TextKeyMap &keys, const std::string &groupname)
|
||||||
{
|
{
|
||||||
NifOsg::TextKeyMap::const_iterator iter(keys.begin());
|
NifOsg::TextKeyMap::const_iterator iter(keys.begin());
|
||||||
|
@ -1042,7 +1016,7 @@ namespace MWRender
|
||||||
{
|
{
|
||||||
if (!mNodeMapCreated && mObjectRoot)
|
if (!mNodeMapCreated && mObjectRoot)
|
||||||
{
|
{
|
||||||
NodeMapVisitor visitor(mNodeMap);
|
SceneUtil::NodeMapVisitor visitor(mNodeMap);
|
||||||
mObjectRoot->accept(visitor);
|
mObjectRoot->accept(visitor);
|
||||||
mNodeMapCreated = true;
|
mNodeMapCreated = true;
|
||||||
}
|
}
|
||||||
|
@ -1365,7 +1339,7 @@ namespace MWRender
|
||||||
{
|
{
|
||||||
osg::ref_ptr<osg::Node> created = sceneMgr->getInstance(model);
|
osg::ref_ptr<osg::Node> created = sceneMgr->getInstance(model);
|
||||||
|
|
||||||
CleanObjectRootVisitor removeDrawableVisitor;
|
SceneUtil::CleanObjectRootVisitor removeDrawableVisitor;
|
||||||
created->accept(removeDrawableVisitor);
|
created->accept(removeDrawableVisitor);
|
||||||
removeDrawableVisitor.remove();
|
removeDrawableVisitor.remove();
|
||||||
|
|
||||||
|
@ -1434,7 +1408,7 @@ namespace MWRender
|
||||||
|
|
||||||
if (isCreature)
|
if (isCreature)
|
||||||
{
|
{
|
||||||
RemoveTriBipVisitor removeTriBipVisitor;
|
SceneUtil::RemoveTriBipVisitor removeTriBipVisitor;
|
||||||
mObjectRoot->accept(removeTriBipVisitor);
|
mObjectRoot->accept(removeTriBipVisitor);
|
||||||
removeTriBipVisitor.remove();
|
removeTriBipVisitor.remove();
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
#include <components/resource/resourcesystem.hpp>
|
#include <components/resource/resourcesystem.hpp>
|
||||||
#include <components/resource/scenemanager.hpp>
|
#include <components/resource/scenemanager.hpp>
|
||||||
|
#include <components/sceneutil/actorutil.hpp>
|
||||||
#include <components/sceneutil/attach.hpp>
|
#include <components/sceneutil/attach.hpp>
|
||||||
#include <components/sceneutil/visitor.hpp>
|
#include <components/sceneutil/visitor.hpp>
|
||||||
#include <components/sceneutil/skeleton.hpp>
|
#include <components/sceneutil/skeleton.hpp>
|
||||||
|
@ -451,37 +452,15 @@ void NpcAnimation::updateNpcBase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool is1stPerson = mViewMode == VM_FirstPerson;
|
||||||
bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0;
|
bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0;
|
||||||
|
|
||||||
std::string smodel;
|
std::string smodel = SceneUtil::getActorSkeleton(is1stPerson, isFemale, isBeast, isWerewolf);
|
||||||
if (mViewMode != VM_FirstPerson)
|
|
||||||
{
|
|
||||||
if (isWerewolf)
|
|
||||||
smodel = "meshes\\wolf\\skin.nif";
|
|
||||||
else if (isBeast)
|
|
||||||
smodel = "meshes\\base_animkna.nif";
|
|
||||||
else if (isFemale)
|
|
||||||
smodel = "meshes\\base_anim_female.nif";
|
|
||||||
else
|
|
||||||
smodel = "meshes\\base_anim.nif";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (isWerewolf)
|
|
||||||
smodel = "meshes\\wolf\\skin.1st.nif";
|
|
||||||
else if (isBeast)
|
|
||||||
smodel = "meshes\\base_animkna.1st.nif";
|
|
||||||
else if (isFemale)
|
|
||||||
smodel = "meshes\\base_anim_female.1st.nif";
|
|
||||||
else
|
|
||||||
smodel = "meshes\\base_anim.1st.nif";
|
|
||||||
}
|
|
||||||
|
|
||||||
smodel = Misc::ResourceHelpers::correctActorModelPath(smodel, mResourceSystem->getVFS());
|
smodel = Misc::ResourceHelpers::correctActorModelPath(smodel, mResourceSystem->getVFS());
|
||||||
|
|
||||||
setObjectRoot(smodel, true, true, false);
|
setObjectRoot(smodel, true, true, false);
|
||||||
|
|
||||||
if(mViewMode != VM_FirstPerson)
|
if(!is1stPerson)
|
||||||
{
|
{
|
||||||
const std::string base = "meshes\\xbase_anim.nif";
|
const std::string base = "meshes\\xbase_anim.nif";
|
||||||
if (smodel != base)
|
if (smodel != base)
|
||||||
|
|
|
@ -51,6 +51,7 @@ add_component_dir (shader
|
||||||
add_component_dir (sceneutil
|
add_component_dir (sceneutil
|
||||||
clone attach visitor util statesetupdater controller skeleton riggeometry morphgeometry lightcontroller
|
clone attach visitor util statesetupdater controller skeleton riggeometry morphgeometry lightcontroller
|
||||||
lightmanager lightutil positionattitudetransform workqueue unrefqueue pathgridutil waterutil writescene serialize optimizer
|
lightmanager lightutil positionattitudetransform workqueue unrefqueue pathgridutil waterutil writescene serialize optimizer
|
||||||
|
actorutil
|
||||||
)
|
)
|
||||||
|
|
||||||
add_component_dir (nif
|
add_component_dir (nif
|
||||||
|
@ -77,7 +78,7 @@ add_component_dir (esm
|
||||||
loadweap records aipackage effectlist spelllist variant variantimp loadtes3 cellref filter
|
loadweap records aipackage effectlist spelllist variant variantimp loadtes3 cellref filter
|
||||||
savedgame journalentry queststate locals globalscript player objectstate cellid cellstate globalmap inventorystate containerstate npcstate creaturestate dialoguestate statstate
|
savedgame journalentry queststate locals globalscript player objectstate cellid cellstate globalmap inventorystate containerstate npcstate creaturestate dialoguestate statstate
|
||||||
npcstats creaturestats weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate debugprofile
|
npcstats creaturestats weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate debugprofile
|
||||||
aisequence magiceffects util custommarkerstate stolenitems transport animationstate controlsstate
|
aisequence magiceffects util custommarkerstate stolenitems transport animationstate controlsstate mappings
|
||||||
)
|
)
|
||||||
|
|
||||||
add_component_dir (esmterrain
|
add_component_dir (esmterrain
|
||||||
|
@ -85,7 +86,7 @@ add_component_dir (esmterrain
|
||||||
)
|
)
|
||||||
|
|
||||||
add_component_dir (misc
|
add_component_dir (misc
|
||||||
constants utf8stream stringops resourcehelpers rng messageformatparser
|
constants utf8stream stringops resourcehelpers rng messageformatparser weakcache
|
||||||
)
|
)
|
||||||
|
|
||||||
add_component_dir (debug
|
add_component_dir (debug
|
||||||
|
|
134
components/esm/mappings.cpp
Normal file
134
components/esm/mappings.cpp
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
#include "mappings.hpp"
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace ESM
|
||||||
|
{
|
||||||
|
ESM::BodyPart::MeshPart getMeshPart(ESM::PartReferenceType type)
|
||||||
|
{
|
||||||
|
switch(type)
|
||||||
|
{
|
||||||
|
case ESM::PRT_Head:
|
||||||
|
return ESM::BodyPart::MP_Head;
|
||||||
|
case ESM::PRT_Hair:
|
||||||
|
return ESM::BodyPart::MP_Hair;
|
||||||
|
case ESM::PRT_Neck:
|
||||||
|
return ESM::BodyPart::MP_Neck;
|
||||||
|
case ESM::PRT_Cuirass:
|
||||||
|
return ESM::BodyPart::MP_Chest;
|
||||||
|
case ESM::PRT_Groin:
|
||||||
|
return ESM::BodyPart::MP_Groin;
|
||||||
|
case ESM::PRT_RHand:
|
||||||
|
return ESM::BodyPart::MP_Hand;
|
||||||
|
case ESM::PRT_LHand:
|
||||||
|
return ESM::BodyPart::MP_Hand;
|
||||||
|
case ESM::PRT_RWrist:
|
||||||
|
return ESM::BodyPart::MP_Wrist;
|
||||||
|
case ESM::PRT_LWrist:
|
||||||
|
return ESM::BodyPart::MP_Wrist;
|
||||||
|
case ESM::PRT_RForearm:
|
||||||
|
return ESM::BodyPart::MP_Forearm;
|
||||||
|
case ESM::PRT_LForearm:
|
||||||
|
return ESM::BodyPart::MP_Forearm;
|
||||||
|
case ESM::PRT_RUpperarm:
|
||||||
|
return ESM::BodyPart::MP_Upperarm;
|
||||||
|
case ESM::PRT_LUpperarm:
|
||||||
|
return ESM::BodyPart::MP_Upperarm;
|
||||||
|
case ESM::PRT_RFoot:
|
||||||
|
return ESM::BodyPart::MP_Foot;
|
||||||
|
case ESM::PRT_LFoot:
|
||||||
|
return ESM::BodyPart::MP_Foot;
|
||||||
|
case ESM::PRT_RAnkle:
|
||||||
|
return ESM::BodyPart::MP_Ankle;
|
||||||
|
case ESM::PRT_LAnkle:
|
||||||
|
return ESM::BodyPart::MP_Ankle;
|
||||||
|
case ESM::PRT_RKnee:
|
||||||
|
return ESM::BodyPart::MP_Knee;
|
||||||
|
case ESM::PRT_LKnee:
|
||||||
|
return ESM::BodyPart::MP_Knee;
|
||||||
|
case ESM::PRT_RLeg:
|
||||||
|
return ESM::BodyPart::MP_Upperleg;
|
||||||
|
case ESM::PRT_LLeg:
|
||||||
|
return ESM::BodyPart::MP_Upperleg;
|
||||||
|
case ESM::PRT_Tail:
|
||||||
|
return ESM::BodyPart::MP_Tail;
|
||||||
|
default:
|
||||||
|
throw std::runtime_error("PartReferenceType " +
|
||||||
|
std::to_string(type) + " not associated with a mesh part");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string getBoneName(ESM::PartReferenceType type)
|
||||||
|
{
|
||||||
|
switch(type)
|
||||||
|
{
|
||||||
|
case ESM::PRT_Head:
|
||||||
|
return "head";
|
||||||
|
case ESM::PRT_Hair:
|
||||||
|
return "head"; // This is purposeful.
|
||||||
|
case ESM::PRT_Neck:
|
||||||
|
return "neck";
|
||||||
|
case ESM::PRT_Cuirass:
|
||||||
|
return "chest";
|
||||||
|
case ESM::PRT_Groin:
|
||||||
|
return "groin";
|
||||||
|
case ESM::PRT_Skirt:
|
||||||
|
return "groin";
|
||||||
|
case ESM::PRT_RHand:
|
||||||
|
return "right hand";
|
||||||
|
case ESM::PRT_LHand:
|
||||||
|
return "left hand";
|
||||||
|
case ESM::PRT_RWrist:
|
||||||
|
return "right wrist";
|
||||||
|
case ESM::PRT_LWrist:
|
||||||
|
return "left wrist";
|
||||||
|
case ESM::PRT_Shield:
|
||||||
|
return "shield bone";
|
||||||
|
case ESM::PRT_RForearm:
|
||||||
|
return "right forearm";
|
||||||
|
case ESM::PRT_LForearm:
|
||||||
|
return "left forearm";
|
||||||
|
case ESM::PRT_RUpperarm:
|
||||||
|
return "right upper arm";
|
||||||
|
case ESM::PRT_LUpperarm:
|
||||||
|
return "left upper arm";
|
||||||
|
case ESM::PRT_RFoot:
|
||||||
|
return "right foot";
|
||||||
|
case ESM::PRT_LFoot:
|
||||||
|
return "left foot";
|
||||||
|
case ESM::PRT_RAnkle:
|
||||||
|
return "right ankle";
|
||||||
|
case ESM::PRT_LAnkle:
|
||||||
|
return "left ankle";
|
||||||
|
case ESM::PRT_RKnee:
|
||||||
|
return "right knee";
|
||||||
|
case ESM::PRT_LKnee:
|
||||||
|
return "left knee";
|
||||||
|
case ESM::PRT_RLeg:
|
||||||
|
return "right upper leg";
|
||||||
|
case ESM::PRT_LLeg:
|
||||||
|
return "left upper leg";
|
||||||
|
case ESM::PRT_RPauldron:
|
||||||
|
return "right clavicle";
|
||||||
|
case ESM::PRT_LPauldron:
|
||||||
|
return "left clavicle";
|
||||||
|
case ESM::PRT_Weapon:
|
||||||
|
return "weapon bone";
|
||||||
|
case ESM::PRT_Tail:
|
||||||
|
return "tail";
|
||||||
|
default:
|
||||||
|
throw std::runtime_error("unknown PartReferenceType");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string getMeshFilter(ESM::PartReferenceType type)
|
||||||
|
{
|
||||||
|
switch(type)
|
||||||
|
{
|
||||||
|
case ESM::PRT_Hair:
|
||||||
|
return "hair";
|
||||||
|
default:
|
||||||
|
return getBoneName(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
components/esm/mappings.hpp
Normal file
16
components/esm/mappings.hpp
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#ifndef OPENMW_ESM_MAPPINGS_H
|
||||||
|
#define OPENMW_ESM_MAPPINGS_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <components/esm/loadarmo.hpp>
|
||||||
|
#include <components/esm/loadbody.hpp>
|
||||||
|
|
||||||
|
namespace ESM
|
||||||
|
{
|
||||||
|
ESM::BodyPart::MeshPart getMeshPart(ESM::PartReferenceType type);
|
||||||
|
std::string getBoneName(ESM::PartReferenceType type);
|
||||||
|
std::string getMeshFilter(ESM::PartReferenceType type);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
138
components/misc/weakcache.hpp
Normal file
138
components/misc/weakcache.hpp
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
#ifndef OPENMW_COMPONENTS_WEAKCACHE_HPP
|
||||||
|
#define OPENMW_COMPONENTS_WEAKCACHE_HPP
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Misc
|
||||||
|
{
|
||||||
|
/// \class WeakCache
|
||||||
|
/// Provides a container to weakly store pointers to shared data.
|
||||||
|
template <typename Key, typename T>
|
||||||
|
class WeakCache
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using WeakPtr = std::weak_ptr<T>;
|
||||||
|
using StrongPtr = std::shared_ptr<T>;
|
||||||
|
using Map = std::unordered_map<Key, WeakPtr>;
|
||||||
|
|
||||||
|
class iterator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
iterator(WeakCache* cache, typename Map::iterator current, typename Map::iterator end);
|
||||||
|
iterator& operator++();
|
||||||
|
bool operator==(const iterator& other);
|
||||||
|
bool operator!=(const iterator& other);
|
||||||
|
StrongPtr operator*();
|
||||||
|
private:
|
||||||
|
WeakCache* mCache;
|
||||||
|
typename Map::iterator mCurrent, mEnd;
|
||||||
|
StrongPtr mPtr;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Stores a weak pointer to the item.
|
||||||
|
void insert(Key key, StrongPtr value, bool prune=true);
|
||||||
|
|
||||||
|
/// Retrieves the item associated with the key.
|
||||||
|
/// \return An item or null.
|
||||||
|
StrongPtr get(Key key);
|
||||||
|
|
||||||
|
iterator begin();
|
||||||
|
iterator end();
|
||||||
|
|
||||||
|
/// Removes known invalid entries
|
||||||
|
void prune();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Map mData;
|
||||||
|
std::vector<Key> mDirty;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
template <typename Key, typename T>
|
||||||
|
WeakCache<Key, T>::iterator::iterator(WeakCache* cache, typename Map::iterator current, typename Map::iterator end)
|
||||||
|
: mCache(cache)
|
||||||
|
, mCurrent(current)
|
||||||
|
, mEnd(end)
|
||||||
|
{
|
||||||
|
// Move to 1st available valid item
|
||||||
|
for ( ; mCurrent != mEnd; ++mCurrent)
|
||||||
|
{
|
||||||
|
mPtr = mCurrent->second.lock();
|
||||||
|
if (mPtr) break;
|
||||||
|
else mCache->mDirty.push_back(mCurrent->first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Key, typename T>
|
||||||
|
typename WeakCache<Key, T>::iterator& WeakCache<Key, T>::iterator::operator++()
|
||||||
|
{
|
||||||
|
auto next = mCurrent;
|
||||||
|
++next;
|
||||||
|
return *this = iterator(mCache, next, mEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Key, typename T>
|
||||||
|
bool WeakCache<Key, T>::iterator::operator==(const iterator& other)
|
||||||
|
{
|
||||||
|
return mCurrent == other.mCurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Key, typename T>
|
||||||
|
bool WeakCache<Key, T>::iterator::operator!=(const iterator& other)
|
||||||
|
{
|
||||||
|
return !(*this == other);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Key, typename T>
|
||||||
|
typename WeakCache<Key, T>::StrongPtr WeakCache<Key, T>::iterator::operator*()
|
||||||
|
{
|
||||||
|
return mPtr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
template <typename Key, typename T>
|
||||||
|
void WeakCache<Key, T>::insert(Key key, StrongPtr value, bool shouldPrune)
|
||||||
|
{
|
||||||
|
mData[key] = WeakPtr(value);
|
||||||
|
if (shouldPrune) prune();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Key, typename T>
|
||||||
|
typename WeakCache<Key, T>::StrongPtr WeakCache<Key, T>::get(Key key)
|
||||||
|
{
|
||||||
|
auto searchIt = mData.find(key);
|
||||||
|
if (searchIt != mData.end())
|
||||||
|
return searchIt->second.lock();
|
||||||
|
else
|
||||||
|
return StrongPtr();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Key, typename T>
|
||||||
|
typename WeakCache<Key, T>::iterator WeakCache<Key, T>::begin()
|
||||||
|
{
|
||||||
|
return iterator(this, mData.begin(), mData.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Key, typename T>
|
||||||
|
typename WeakCache<Key, T>::iterator WeakCache<Key, T>::end()
|
||||||
|
{
|
||||||
|
return iterator(this, mData.end(), mData.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Key, typename T>
|
||||||
|
void WeakCache<Key, T>::prune()
|
||||||
|
{
|
||||||
|
// Remove empty entries
|
||||||
|
for (auto& key : mDirty)
|
||||||
|
{
|
||||||
|
auto it = mData.find(key);
|
||||||
|
if (it != mData.end() && it->second.use_count() == 0)
|
||||||
|
mData.erase(it);
|
||||||
|
}
|
||||||
|
mDirty.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
30
components/sceneutil/actorutil.cpp
Normal file
30
components/sceneutil/actorutil.cpp
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#include "actorutil.hpp"
|
||||||
|
|
||||||
|
namespace SceneUtil
|
||||||
|
{
|
||||||
|
std::string getActorSkeleton(bool firstPerson, bool isFemale, bool isBeast, bool isWerewolf)
|
||||||
|
{
|
||||||
|
if (!firstPerson)
|
||||||
|
{
|
||||||
|
if (isWerewolf)
|
||||||
|
return "meshes\\wolf\\skin.nif";
|
||||||
|
else if (isBeast)
|
||||||
|
return "meshes\\base_animkna.nif";
|
||||||
|
else if (isFemale)
|
||||||
|
return "meshes\\base_anim_female.nif";
|
||||||
|
else
|
||||||
|
return "meshes\\base_anim.nif";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (isWerewolf)
|
||||||
|
return "meshes\\wolf\\skin.1st.nif";
|
||||||
|
else if (isBeast)
|
||||||
|
return "meshes\\base_animkna.1st.nif";
|
||||||
|
else if (isFemale)
|
||||||
|
return "meshes\\base_anim_female.1st.nif";
|
||||||
|
else
|
||||||
|
return "meshes\\base_anim.1st.nif";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
components/sceneutil/actorutil.hpp
Normal file
11
components/sceneutil/actorutil.hpp
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
#ifndef OPENMW_COMPONENTS_SCENEUTIL_ACTORUTIL_HPP
|
||||||
|
#define OPENMW_COMPONENTS_SCENEUTIL_ACTORUTIL_HPP
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace SceneUtil
|
||||||
|
{
|
||||||
|
std::string getActorSkeleton(bool firstPerson, bool female, bool beast, bool werewolf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -1,9 +1,12 @@
|
||||||
#include "visitor.hpp"
|
#include "visitor.hpp"
|
||||||
|
|
||||||
|
#include <osg/Drawable>
|
||||||
#include <osg/MatrixTransform>
|
#include <osg/MatrixTransform>
|
||||||
|
|
||||||
#include <osgParticle/ParticleSystem>
|
#include <osgParticle/ParticleSystem>
|
||||||
|
|
||||||
|
#include <components/debug/debuglog.hpp>
|
||||||
|
|
||||||
#include <components/misc/stringops.hpp>
|
#include <components/misc/stringops.hpp>
|
||||||
|
|
||||||
namespace SceneUtil
|
namespace SceneUtil
|
||||||
|
@ -54,4 +57,103 @@ namespace SceneUtil
|
||||||
partsys->setFreezeOnCull(false);
|
partsys->setFreezeOnCull(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NodeMapVisitor::apply(osg::MatrixTransform& trans)
|
||||||
|
{
|
||||||
|
// Take transformation for first found node in file
|
||||||
|
const std::string nodeName = Misc::StringUtils::lowerCase(trans.getName());
|
||||||
|
if (mMap.find(nodeName) == mMap.end())
|
||||||
|
{
|
||||||
|
mMap[nodeName] = &trans;
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(trans);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoveVisitor::remove()
|
||||||
|
{
|
||||||
|
for (RemoveVec::iterator it = mToRemove.begin(); it != mToRemove.end(); ++it)
|
||||||
|
{
|
||||||
|
if (!it->second->removeChild(it->first))
|
||||||
|
Log(Debug::Error) << "error removing " << it->first->getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CleanObjectRootVisitor::apply(osg::Drawable& drw)
|
||||||
|
{
|
||||||
|
applyDrawable(drw);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CleanObjectRootVisitor::apply(osg::Group& node)
|
||||||
|
{
|
||||||
|
applyNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CleanObjectRootVisitor::apply(osg::MatrixTransform& node)
|
||||||
|
{
|
||||||
|
applyNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CleanObjectRootVisitor::apply(osg::Node& node)
|
||||||
|
{
|
||||||
|
applyNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CleanObjectRootVisitor::applyNode(osg::Node& node)
|
||||||
|
{
|
||||||
|
if (node.getStateSet())
|
||||||
|
node.setStateSet(NULL);
|
||||||
|
|
||||||
|
if (node.getNodeMask() == 0x1 && node.getNumParents() == 1)
|
||||||
|
mToRemove.push_back(std::make_pair(&node, node.getParent(0)));
|
||||||
|
else
|
||||||
|
traverse(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CleanObjectRootVisitor::applyDrawable(osg::Node& node)
|
||||||
|
{
|
||||||
|
osg::NodePath::iterator parent = getNodePath().end()-2;
|
||||||
|
// We know that the parent is a Group because only Groups can have children.
|
||||||
|
osg::Group* parentGroup = static_cast<osg::Group*>(*parent);
|
||||||
|
|
||||||
|
// Try to prune nodes that would be empty after the removal
|
||||||
|
if (parent != getNodePath().begin())
|
||||||
|
{
|
||||||
|
// This could be extended to remove the parent's parent, and so on if they are empty as well.
|
||||||
|
// But for NIF files, there won't be a benefit since only TriShapes can be set to STATIC dataVariance.
|
||||||
|
osg::Group* parentParent = static_cast<osg::Group*>(*(parent - 1));
|
||||||
|
if (parentGroup->getNumChildren() == 1 && parentGroup->getDataVariance() == osg::Object::STATIC)
|
||||||
|
{
|
||||||
|
mToRemove.push_back(std::make_pair(parentGroup, parentParent));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mToRemove.push_back(std::make_pair(&node, parentGroup));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoveTriBipVisitor::apply(osg::Drawable& drw)
|
||||||
|
{
|
||||||
|
applyImpl(drw);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoveTriBipVisitor::apply(osg::Group& node)
|
||||||
|
{
|
||||||
|
traverse(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoveTriBipVisitor::apply(osg::MatrixTransform& node)
|
||||||
|
{
|
||||||
|
traverse(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoveTriBipVisitor::applyImpl(osg::Node& node)
|
||||||
|
{
|
||||||
|
const std::string toFind = "tri bip";
|
||||||
|
if (Misc::StringUtils::ciCompareLen(node.getName(), toFind, toFind.size()) == 0)
|
||||||
|
{
|
||||||
|
osg::Group* parent = static_cast<osg::Group*>(*(getNodePath().end()-2));
|
||||||
|
// Not safe to remove in apply(), since the visitor is still iterating the child list
|
||||||
|
mToRemove.push_back(std::make_pair(&node, parent));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#ifndef OPENMW_COMPONENTS_SCENEUTIL_VISITOR_H
|
#ifndef OPENMW_COMPONENTS_SCENEUTIL_VISITOR_H
|
||||||
#define OPENMW_COMPONENTS_SCENEUTIL_VISITOR_H
|
#define OPENMW_COMPONENTS_SCENEUTIL_VISITOR_H
|
||||||
|
|
||||||
|
#include <osg/MatrixTransform>
|
||||||
#include <osg/NodeVisitor>
|
#include <osg/NodeVisitor>
|
||||||
|
|
||||||
// Commonly used scene graph visitors
|
// Commonly used scene graph visitors
|
||||||
|
@ -58,6 +59,65 @@ namespace SceneUtil
|
||||||
virtual void apply(osg::Drawable& drw);
|
virtual void apply(osg::Drawable& drw);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Maps names to nodes
|
||||||
|
class NodeMapVisitor : public osg::NodeVisitor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
typedef std::map<std::string, osg::ref_ptr<osg::MatrixTransform> > NodeMap;
|
||||||
|
|
||||||
|
NodeMapVisitor(NodeMap& map)
|
||||||
|
: osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN)
|
||||||
|
, mMap(map)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply(osg::MatrixTransform& trans);
|
||||||
|
|
||||||
|
private:
|
||||||
|
NodeMap& mMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Base class for visitors that remove nodes from a scene graph.
|
||||||
|
/// Subclasses need to fill the mToRemove vector.
|
||||||
|
/// To use, node->accept(removeVisitor); removeVisitor.remove();
|
||||||
|
class RemoveVisitor : public osg::NodeVisitor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RemoveVisitor()
|
||||||
|
: osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// <node to remove, parent node to remove it from>
|
||||||
|
typedef std::vector<std::pair<osg::Node*, osg::Group*> > RemoveVec;
|
||||||
|
std::vector<std::pair<osg::Node*, osg::Group*> > mToRemove;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Removes all drawables from a graph.
|
||||||
|
class CleanObjectRootVisitor : public RemoveVisitor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void apply(osg::Drawable& drw);
|
||||||
|
virtual void apply(osg::Group& node);
|
||||||
|
virtual void apply(osg::MatrixTransform& node);
|
||||||
|
virtual void apply(osg::Node& node);
|
||||||
|
|
||||||
|
void applyNode(osg::Node& node);
|
||||||
|
void applyDrawable(osg::Node& node);
|
||||||
|
};
|
||||||
|
|
||||||
|
class RemoveTriBipVisitor : public RemoveVisitor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void apply(osg::Drawable& drw);
|
||||||
|
virtual void apply(osg::Group& node);
|
||||||
|
virtual void apply(osg::MatrixTransform& node);
|
||||||
|
|
||||||
|
void applyImpl(osg::Node& node);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
Loading…
Reference in a new issue