From 5bd5c8401801eaeb4972ef7def503cb57500f7dd Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Fri, 29 Dec 2023 16:55:32 +0100 Subject: [PATCH] Replace missing NPC races and default animations --- apps/openmw/mwworld/esmstore.cpp | 62 +++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 0b661b4442..137c9cf026 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -17,8 +17,10 @@ #include #include #include +#include #include "../mwmechanics/spelllist.hpp" +#include "../mwrender/actorutil.hpp" namespace { @@ -83,12 +85,22 @@ namespace throw std::runtime_error("List of NPC classes is empty!"); } + const ESM::RefId& getDefaultRace(const MWWorld::Store& races) + { + auto it = races.begin(); + if (it != races.end()) + return it->mId; + throw std::runtime_error("List of NPC races is empty!"); + } + std::vector getNPCsToReplace(const MWWorld::Store& factions, - const MWWorld::Store& classes, const MWWorld::Store& scripts, - const std::unordered_map& npcs) + const MWWorld::Store& classes, const MWWorld::Store& races, + const MWWorld::Store& scripts, const std::unordered_map& npcs) { // Cache first class from store - we will use it if current class is not found const ESM::RefId& defaultCls = getDefaultClass(classes); + // Same for races + const ESM::RefId& defaultRace = getDefaultRace(races); // Validate NPCs for non-existing class and faction. // We will replace invalid entries by fixed ones @@ -113,8 +125,7 @@ namespace } } - const ESM::RefId& npcClass = npc.mClass; - const ESM::Class* cls = classes.search(npcClass); + const ESM::Class* cls = classes.search(npc.mClass); if (!cls) { Log(Debug::Verbose) << "NPC " << npc.mId << " (" << npc.mName << ") has nonexistent class " @@ -123,6 +134,41 @@ namespace changed = true; } + const ESM::Race* race = races.search(npc.mRace); + if (race) + { + // TESCS sometimes writes the default animation nif to the animation subrecord. This harmless (as it + // will match the NPC's race) until the NPC's race is changed. If the player record contains a default + // non-beast race animation and the player selects a beast race in chargen, animations aren't applied + // properly. Morrowind.exe handles this gracefully, so we clear the animation here to force the default. + if (!npc.mModel.empty() && npc.mId == "player") + { + const bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; + const std::string& defaultModel = MWRender::getActorSkeleton(false, !npc.isMale(), isBeast, false); + std::string model = Misc::ResourceHelpers::correctMeshPath(npc.mModel); + if (model.size() == defaultModel.size()) + { + std::replace(model.begin(), model.end(), '/', '\\'); + std::string normalizedDefault = defaultModel; + std::replace(normalizedDefault.begin(), normalizedDefault.end(), '/', '\\'); + if (Misc::StringUtils::ciEqual(normalizedDefault, model)) + { + npc.mModel.clear(); + changed = true; + } + } + } + } + else + { + Log(Debug::Verbose) << "NPC " << npc.mId << " (" << npc.mName << ") has nonexistent race " << npc.mRace + << ", using " << defaultRace << " race as replacement."; + npc.mRace = defaultRace; + // Remove animations that might be race specific + npc.mModel.clear(); + changed = true; + } + if (!npc.mScript.empty() && !scripts.search(npc.mScript)) { Log(Debug::Verbose) << "NPC " << npc.mId << " (" << npc.mName << ") has nonexistent script " @@ -580,8 +626,8 @@ namespace MWWorld void ESMStore::validate() { auto& npcs = getWritable(); - std::vector npcsToReplace = getNPCsToReplace( - getWritable(), getWritable(), getWritable(), npcs.mStatic); + std::vector npcsToReplace = getNPCsToReplace(getWritable(), getWritable(), + getWritable(), getWritable(), npcs.mStatic); for (const ESM::NPC& npc : npcsToReplace) { @@ -623,8 +669,8 @@ namespace MWWorld auto& npcs = getWritable(); auto& scripts = getWritable(); - std::vector npcsToReplace = getNPCsToReplace( - getWritable(), getWritable(), getWritable(), npcs.mDynamic); + std::vector npcsToReplace = getNPCsToReplace(getWritable(), getWritable(), + getWritable(), getWritable(), npcs.mDynamic); for (const ESM::NPC& npc : npcsToReplace) npcs.insert(npc);