Guest User

Untitled

a guest
Aug 12th, 2025
544
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 16.57 KB | None | 0 0
  1. #include "PCH.h"
  2.  
  3. #include <thread>
  4. #include <chrono>
  5. #include <atomic>
  6.  
  7. namespace ExpressionIndices {
  8.     // From MfgFix::BSFaceGenKeyframeMultiple::Expression
  9.     constexpr std::uint32_t kDialogueAnger = 0;
  10.     constexpr std::uint32_t kDialogueFear = 1;
  11.     constexpr std::uint32_t kDialogueHappy = 2;
  12.     constexpr std::uint32_t kDialogueSad = 3;
  13.     constexpr std::uint32_t kDialogueSurprise = 4;
  14.     constexpr std::uint32_t kDialoguePuzzled = 5;
  15.     constexpr std::uint32_t kDialogueDisgusted = 6;
  16.     constexpr std::uint32_t kMoodNeutral = 7;
  17.     constexpr std::uint32_t kMoodAnger = 8;
  18.     constexpr std::uint32_t kMoodFear = 9;
  19.     constexpr std::uint32_t kMoodHappy = 10;
  20.     constexpr std::uint32_t kMoodSad = 11;
  21.     constexpr std::uint32_t kMoodSurprise = 12;
  22.     constexpr std::uint32_t kMoodPuzzled = 13;
  23.     constexpr std::uint32_t kMoodDisgusted = 14;
  24.     constexpr std::uint32_t kCombatAnger = 15;
  25.     constexpr std::uint32_t kCombatShout = 16;
  26. }
  27.  
  28. namespace PhonemeIndices {
  29.     // From MfgFix::BSFaceGenKeyframeMultiple::Phoneme
  30.     constexpr std::uint32_t kAah = 0;
  31.     constexpr std::uint32_t kBigAah = 1;
  32.     constexpr std::uint32_t kBMP = 2;
  33.     constexpr std::uint32_t kChJSh = 3;
  34.     constexpr std::uint32_t kDST = 4;
  35.     constexpr std::uint32_t kEee = 5;
  36.     constexpr std::uint32_t kEh = 6;
  37.     constexpr std::uint32_t kFV = 7;
  38.     constexpr std::uint32_t kI = 8;
  39.     constexpr std::uint32_t kK = 9;
  40.     constexpr std::uint32_t kN = 10;
  41.     constexpr std::uint32_t kOh = 11;
  42.     constexpr std::uint32_t kOohQ = 12;
  43.     constexpr std::uint32_t kR = 13;
  44.     constexpr std::uint32_t kTh = 14;
  45.     constexpr std::uint32_t kW = 15;
  46. }
  47.  
  48. namespace {
  49.  
  50.     bool debugging = false;
  51.  
  52.     void InitializeLogging() {
  53.         auto path = SKSE::log::log_directory();
  54.         if (!path) {
  55.             return;
  56.         }
  57.  
  58.         *path /= PLUGIN_NAME;
  59.         *path += ".log";
  60.         auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(path->string(), true);
  61.         auto log = std::make_shared<spdlog::logger>("global", std::move(sink));
  62.  
  63.         log->set_level(spdlog::level::info);
  64.         log->flush_on(spdlog::level::info);
  65.  
  66.         spdlog::set_default_logger(std::move(log));
  67.         spdlog::set_pattern("[%H:%M:%S] [%l] %v");
  68.  
  69.         SKSE::log::info("{} v{} - Initialized", PLUGIN_NAME, PLUGIN_VERSION);
  70.     }
  71.  
  72.     void DebugPrint(const char* a_fmt, ...) {
  73.         if (debugging) {
  74.             std::va_list args;
  75.             va_start(args, a_fmt);
  76.             RE::ConsoleLog::GetSingleton()->VPrint(a_fmt, args);
  77.             va_end(args);
  78.         }
  79.     }
  80.  
  81.     //case-insensitive string comparison
  82.     bool ci_equal(std::string_view s1, std::string_view s2) {
  83.         return std::equal(s1.begin(), s1.end(), s2.begin(), s2.end(), [](char a, char b) {
  84.             return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
  85.         });
  86.     }
  87.  
  88.     RE::TESObjectREFR* GetConsoleSelectedReference() {
  89.         //storing the handle of the reference selected in the console.
  90.         //AE (1.6.1130+): 504099 | SE (pre-1.6.1130): 405935 | VR: ???
  91.         REL::Relocation<RE::ObjectRefHandle*> selectedRefHandleGlobal{
  92.             RELOCATION_ID(519394, REL::Module::get().version().patch() < 1130 ? 405935 : 504099)};
  93.  
  94.         if (!selectedRefHandleGlobal.get()) {
  95.             SKSE::log::warn("Could not find global variable for selected console reference handle.");
  96.             return nullptr;
  97.         }
  98.  
  99.         RE::ObjectRefHandle selectedRefHandle = *selectedRefHandleGlobal;
  100.         RE::NiPointer<RE::TESObjectREFR> selectedRefPtr = selectedRefHandle.get();
  101.         return selectedRefPtr.get();
  102.     }
  103.  
  104.     //Function to execute a console command string
  105.     //Couldn't figure out a cleaner way of doing this ¯\_(ツ)_/¯
  106.     //a_targetRef: Optional target for the command. If nullptr, uses the console's selected reference.
  107.     void ExecuteConsoleCommand(const std::string& a_command, RE::TESObjectREFR* a_targetRef = nullptr) {
  108.         if (a_command.empty()) {
  109.             SKSE::log::warn("ExecuteConsoleCommand: Attempted to execute empty command.");
  110.             return;
  111.         }
  112.  
  113.         const auto scriptFactory = RE::IFormFactory::GetConcreteFormFactoryByType<RE::Script>();
  114.         if (!scriptFactory) {
  115.             SKSE::log::error("ExecuteConsoleCommand: Cannot get Script factory.");
  116.             return;
  117.         }
  118.  
  119.         //Temporary script object
  120.         RE::Script* script = scriptFactory->Create();
  121.         if (!script) {
  122.             SKSE::log::error("ExecuteConsoleCommand: Cannot create Script object for command: {}", a_command);
  123.             return;
  124.         }
  125.  
  126.         script->SetCommand(a_command);
  127.  
  128.         RE::TESObjectREFR* targetRef = a_targetRef ? a_targetRef : GetConsoleSelectedReference();
  129.         //Note:: targetRef can be nullptr if nothing is selected or provided
  130.         //The game's CompileAndRun function seems to handle this.
  131.  
  132.         using CompileAndRunImpl_t = void(RE::Script*, RE::ScriptCompiler*, RE::COMPILER_NAME, RE::TESObjectREFR*);
  133.  
  134.         //Get the address of the internal CompileAndRun function using relocation
  135.         //Use the same relocation ID ConsoleUtil uses.
  136.         //AE (1.6.1130+): 441582 | SE (pre-1.6.1130): 21890 | VR: ???
  137.         REL::Relocation<CompileAndRunImpl_t> compileAndRunFunc{
  138.             RELOCATION_ID(21416, REL::Module::get().version().patch() < 1130 ? 21890 : 441582)};
  139.  
  140.         if (!compileAndRunFunc.get()) {
  141.             SKSE::log::error("ExecuteConsoleCommand: Cannot find CompileAndRun function address.");
  142.             delete script;  // Clean up the created script object
  143.             return;
  144.         }
  145.  
  146.         RE::ScriptCompiler compiler;
  147.  
  148.         SKSE::log::info(
  149.             "Executing console command: \"{}\" with target: {}", a_command,
  150.             targetRef ? (targetRef->GetName() ? targetRef->GetName() : "Unnamed Ref") : "None (or Console Selected)");
  151.  
  152.         //Call the internal game function to execute the command
  153.         compileAndRunFunc(script,                                    //The script object containing the command
  154.                           &compiler,                                 //The compiler instance
  155.                           RE::COMPILER_NAME::kSystemWindowCompiler,  //Context, kSystemWindowCompiler is appropriate
  156.                           targetRef);                                //The target reference
  157.  
  158.         //Clean up the temporary script object
  159.         delete script;
  160.     }
  161.  
  162.      /**
  163.      * Checks if the player character is a member of a specific faction from a specific mod.
  164.      */
  165.     bool IsPlayerInFaction(const std::string& a_factionEditorID, const std::string& a_modName) {
  166.         // Get player singleton first (needed in all cases)
  167.         auto player = RE::PlayerCharacter::GetSingleton();
  168.         if (!player) {
  169.             SKSE::log::warn("IsPlayerInFaction: Could not get PlayerCharacter singleton.");
  170.             return false;
  171.         }
  172.  
  173.         static std::unordered_map<std::string, RE::TESFaction*> factionCache;
  174.  
  175.         // Create a key for the cache (combine editorID and modName)
  176.         std::string cacheKey = a_factionEditorID + "|" + a_modName;
  177.  
  178.         //Check if we already have this faction in the cache
  179.         auto cacheIt = factionCache.find(cacheKey);
  180.         if (cacheIt != factionCache.end()) {
  181.             return player->IsInFaction(cacheIt->second);
  182.         }
  183.  
  184.         RE::TESForm* foundForm = RE::TESForm::LookupByEditorID(a_factionEditorID);
  185.         if (!foundForm) {
  186.             SKSE::log::debug("IsPlayerInFaction: No form found with EditorID '{}' in any loaded mod.",
  187.                              a_factionEditorID);
  188.             return false;
  189.         }
  190.  
  191.         // Verify the source mod file of the found form
  192.         const RE::TESFile* sourceFile = foundForm->GetFile(0);
  193.         if (!sourceFile || !sourceFile->fileName) {
  194.             SKSE::log::warn(
  195.                 "IsPlayerInFaction: Form found for '{}', but could not get valid source file information (FormID: "
  196.                 "{:08X}).",
  197.                 a_factionEditorID, foundForm->GetFormID());
  198.             return false;
  199.         }
  200.  
  201.         // Perform case-insensitive comparison
  202.         std::string_view sourceFileNameSV = sourceFile->fileName;
  203.         std::string_view expectedModNameSV = a_modName;
  204.         if (!ci_equal(sourceFileNameSV, expectedModNameSV)) {
  205.             SKSE::log::debug(
  206.                 "IsPlayerInFaction: Form found for '{}' (FormID: {:08X}), but it comes from '{}', not the expected "
  207.                 "'{}'.",
  208.                 a_factionEditorID, foundForm->GetFormID(), sourceFileNameSV, expectedModNameSV);
  209.             return false;
  210.         }
  211.  
  212.         RE::TESFaction* targetFaction = foundForm->As<RE::TESFaction>();
  213.         if (!targetFaction) {
  214.             SKSE::log::warn("IsPlayerInFaction: Form {:08X} ('{}' from '{}') is not a TESFaction.",
  215.                             foundForm->GetFormID(), a_factionEditorID, a_modName);
  216.             return false;
  217.         }
  218.  
  219.         //Add to cache
  220.         factionCache[cacheKey] = targetFaction;
  221.  
  222.         return player->IsInFaction(targetFaction);
  223.     }
  224.  
  225.     class MouthStateHandler {
  226.     public:
  227.         static MouthStateHandler* GetSingleton() {
  228.             static MouthStateHandler singleton;
  229.             return &singleton;
  230.         }
  231.  
  232.     void Update() {
  233.             if (!IsPlayerLoaded()) {
  234.                 DebugPrint("Player not loaded ");
  235.                 return;
  236.             }
  237.             bool currentMouthOpen = IsPlayerMouthOpen();
  238.  
  239.             // If mouth state changed
  240.             if (currentMouthOpen != wasMouthOpen) {
  241.                 SKSE::log::info("mouth toggled");
  242.                 DebugPrint("Mouth toggled> " + currentMouthOpen);
  243.  
  244.                 if (currentMouthOpen) {
  245.                     ResetHead();
  246.                 }
  247.                 wasMouthOpen = currentMouthOpen;
  248.             }
  249.  
  250.         }
  251.  
  252.     private:
  253.         bool wasMouthOpen = false;
  254.         bool wasAnimating = false;
  255.  
  256.  
  257.     void ResetHead() {
  258.             auto player = RE::PlayerCharacter::GetSingleton();
  259.             if (!player) return;  // Need the player
  260.  
  261.             auto player3DRoot = player->Get3D(false);
  262.             if (!player3DRoot) {
  263.                 player3DRoot = player->Get3D(true);  //Fallback
  264.                 if (!player3DRoot) {
  265.                     RE::ConsoleLog::GetSingleton()->Print("FaceMorphWatcher: Cannot get player 3D root for skeleton.");
  266.                     return;
  267.                 }
  268.             }
  269.  
  270.             auto playerSkeleton = player3DRoot->AsNode();
  271.             if (!playerSkeleton) {
  272.                 RE::ConsoleLog::GetSingleton()->Print("FaceMorphWatcher: Player 3D root is not an NiNode.");
  273.                 return;
  274.             }
  275.  
  276.             RE::BSFaceGenNiNode* faceNode = player->GetFaceNodeSkinned();
  277.             if (!faceNode) {
  278.                 SKSE::log::debug("FaceMorphWatcher: GetFaceNodeSkinned() returned null. No skinned face node found?");
  279.                 return;  
  280.             }
  281.  
  282.             try {
  283.                 RE::NiUpdateData updateData;
  284.                 updateData.time = 0.0f;
  285.                 updateData.flags = RE::NiUpdateData::Flag::kDirty;
  286.  
  287.                 // Potentially helps propagate transforms??
  288.                 SKSE::log::debug("Calling UpdateWorldData...");
  289.                 faceNode->UpdateWorldData(&updateData);
  290.  
  291.                 // This is what actually seems to force an update
  292.                 SKSE::log::debug("Calling FixSkinInstances...");
  293.                 faceNode->FixSkinInstances(playerSkeleton, true);
  294.  
  295.                 SKSE::log::info("FaceMorphWatcher: Updates executed successfully on skinned face node.");
  296.  
  297.             } catch (const std::exception& e) {
  298.                 SKSE::log::error("FaceMorphWatcher: Exception during update calls on skinned face node: {}", e.what());
  299.             } catch (...) {
  300.                 SKSE::log::error("FaceMorphWatcher: Unknown exception during update calls on skinned face node.");
  301.             }
  302.         }
  303.  
  304.         bool IsPlayerMouthOpen() {
  305.             auto player = RE::PlayerCharacter::GetSingleton();
  306.             if (!player) {
  307.                 DebugPrint("IsPlayerMouthOpen: PlayerCharacter null?");
  308.                 return false;
  309.             }
  310.             auto animData = player->GetFaceGenAnimationData();
  311.             if (!animData) {
  312.                 DebugPrint("IsPlayerMouthOpen: animData null?");
  313.                 return false;
  314.             }
  315.  
  316.             //--- Check Final Expressions (unk0C0) ---
  317.             //This is where merged expressions often end up
  318.             auto& finalExpression = animData->unk0C0;
  319.             float expressionThreshold = 0.5f;          //Todo: Tune this threshold
  320.  
  321.             if (finalExpression.values && finalExpression.count > 0) {
  322.                 //Indices likely to involve an open mouth
  323.                 const std::vector<std::uint32_t> openMouthExprIndices = {
  324.                     ExpressionIndices::kCombatShout
  325.                     //Add others?
  326.                 };
  327.  
  328.                 for (const auto indexToCheck : openMouthExprIndices) {
  329.                     if (finalExpression.count > indexToCheck) {
  330.                         float mouthValue = finalExpression.values[indexToCheck];
  331.                         if (mouthValue > expressionThreshold) {
  332.                             DebugPrint("Mouth open via Final Expression %u: %.2f",
  333.                                                                   indexToCheck, mouthValue);
  334.                             return true;  //Mouth open due to expression
  335.                         }
  336.                     }
  337.                 }
  338.             } else {
  339.                 DebugPrint(
  340.                     "IsPlayerMouthOpen: Final Expression (unk0C0) values invalid or count is 0");
  341.             }
  342.  
  343.             //--- Check Final Phonemes (unk120) ---
  344.             //This is where merged phonemes often end up
  345.             auto& finalPhoneme = animData->unk120;
  346.             float phonemeThreshold = 0.3f;          //Todo: Tune this threshold, might be lower than expressions
  347.  
  348.             if (finalPhoneme.values && finalPhoneme.count > 0) {
  349.  
  350.                 //Indices for open-mouth Phonemes
  351.                 const std::vector<std::uint32_t> openMouthPhonemeIndices = {
  352.                     PhonemeIndices::kAah, PhonemeIndices::kBigAah, PhonemeIndices::kOh, PhonemeIndices::kOohQ,
  353.                     PhonemeIndices::kEh  //Often slightly open
  354.                 };
  355.  
  356.                 for (const auto indexToCheck : openMouthPhonemeIndices) {
  357.                     if (finalPhoneme.count > indexToCheck) {
  358.                         float mouthValue = finalPhoneme.values[indexToCheck];
  359.                         if (mouthValue > phonemeThreshold) {
  360.                             DebugPrint("Mouth open via Final Phoneme %u: %.2f", indexToCheck,
  361.                                                                   mouthValue);
  362.                             return true;  //Mouth open due to phoneme
  363.                         }
  364.                     }
  365.                 }
  366.             } else {
  367.               DebugPrint(
  368.                     "IsPlayerMouthOpen: Final Phoneme (unk120) values invalid or count is 0");
  369.             }
  370.  
  371.             //If neither check found an open mouth
  372.             DebugPrint("Mouth closed (checked expressions and phonemes)");
  373.             return false;
  374.         }
  375.  
  376.         //Check if player is loaded
  377.         bool IsPlayerLoaded() { return RE::PlayerCharacter::GetSingleton() != nullptr; }
  378.        
  379.     };
  380. }
  381.  
  382. std::atomic<bool> g_threadRunning(false);
  383. std::thread g_updateThread;
  384.  
  385. //Todo: Rename this. Maybe just DoTick? FrameUpdate makes no sense
  386. void FrameUpdate() {
  387.     MouthStateHandler::GetSingleton()->Update();
  388. }
  389.  
  390. void UpdateThreadFunction() {
  391.     while (g_threadRunning) {
  392.         SKSE::GetTaskInterface()->AddTask(FrameUpdate); //Queue to game thread
  393.         std::this_thread::sleep_for(std::chrono::seconds(1));
  394.     }
  395. }
  396.  
  397. void MessageHandler(SKSE::MessagingInterface::Message* message) {
  398.     if (message->type == SKSE::MessagingInterface::kDataLoaded) {
  399.         if (!g_threadRunning) {
  400.             g_threadRunning = true;
  401.             g_updateThread = std::thread(UpdateThreadFunction);
  402.             g_updateThread.detach();
  403.             SKSE::log::info("Registered update task thread");
  404.         }
  405.     }
  406. }
  407.  
  408. SKSEPluginLoad(const SKSE::LoadInterface* skse) {
  409.     InitializeLogging();
  410.  
  411.     SKSE::Init(skse);
  412.  
  413.     auto messaging = SKSE::GetMessagingInterface();
  414.     if (messaging) {
  415.         messaging->RegisterListener("SKSE", MessageHandler);
  416.         SKSE::log::info("Registered messaging listener");
  417.     }
  418.  
  419.     return true;
  420. }
Advertisement
Add Comment
Please, Sign In to add comment