Guest User

Untitled

a guest
Apr 20th, 2025
795
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 14.61 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.  
  49. namespace {
  50.  
  51.     bool debugging = false;
  52.  
  53.     void InitializeLogging() {
  54.         auto path = SKSE::log::log_directory();
  55.         if (!path) {
  56.             return;
  57.         }
  58.  
  59.         *path /= PLUGIN_NAME;
  60.         *path += ".log";
  61.         auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(path->string(), true);
  62.         auto log = std::make_shared<spdlog::logger>("global", std::move(sink));
  63.  
  64.         log->set_level(spdlog::level::info);
  65.         log->flush_on(spdlog::level::info);
  66.  
  67.         spdlog::set_default_logger(std::move(log));
  68.         spdlog::set_pattern("[%H:%M:%S] [%l] %v");
  69.  
  70.         SKSE::log::info("{} v{} - Initialized", PLUGIN_NAME, PLUGIN_VERSION);
  71.     }
  72.  
  73.     void DebugPrint(const char* a_fmt, ...) {
  74.         if (debugging) {
  75.             std::va_list args;
  76.             va_start(args, a_fmt);
  77.             RE::ConsoleLog::GetSingleton()->VPrint(a_fmt, args);
  78.             va_end(args);
  79.         }
  80.     }
  81.  
  82.     //case-insensitive string comparison
  83.     bool ci_equal(std::string_view s1, std::string_view s2) {
  84.         return std::equal(s1.begin(), s1.end(), s2.begin(), s2.end(), [](char a, char b) {
  85.             return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
  86.         });
  87.     }
  88.  
  89.     RE::TESObjectREFR* GetConsoleSelectedReference() {
  90.         //storing the handle of the reference selected in the console.
  91.         //AE (1.6.1130+): 504099 | SE (pre-1.6.1130): 405935 | VR: ???
  92.         REL::Relocation<RE::ObjectRefHandle*> selectedRefHandleGlobal{
  93.             RELOCATION_ID(519394, REL::Module::get().version().patch() < 1130 ? 405935 : 504099)};
  94.  
  95.         if (!selectedRefHandleGlobal.get()) {
  96.             SKSE::log::warn("Could not find global variable for selected console reference handle.");
  97.             return nullptr;
  98.         }
  99.  
  100.         RE::ObjectRefHandle selectedRefHandle = *selectedRefHandleGlobal;
  101.         RE::NiPointer<RE::TESObjectREFR> selectedRefPtr = selectedRefHandle.get();
  102.         return selectedRefPtr.get();
  103.     }
  104.  
  105.     //Function to execute a console command string
  106.     //Couldn't figure out a cleaner way of doing this ¯\_(ツ)_/¯
  107.     //a_targetRef: Optional target for the command. If nullptr, uses the console's selected reference.
  108.     void ExecuteConsoleCommand(const std::string& a_command, RE::TESObjectREFR* a_targetRef = nullptr) {
  109.         if (a_command.empty()) {
  110.             SKSE::log::warn("ExecuteConsoleCommand: Attempted to execute empty command.");
  111.             return;
  112.         }
  113.  
  114.         const auto scriptFactory = RE::IFormFactory::GetConcreteFormFactoryByType<RE::Script>();
  115.         if (!scriptFactory) {
  116.             SKSE::log::error("ExecuteConsoleCommand: Cannot get Script factory.");
  117.             return;
  118.         }
  119.  
  120.         //Temporary script object
  121.         RE::Script* script = scriptFactory->Create();
  122.         if (!script) {
  123.             SKSE::log::error("ExecuteConsoleCommand: Cannot create Script object for command: {}", a_command);
  124.             return;
  125.         }
  126.  
  127.         script->SetCommand(a_command);
  128.  
  129.         RE::TESObjectREFR* targetRef = a_targetRef ? a_targetRef : GetConsoleSelectedReference();
  130.         //Note:: targetRef can be nullptr if nothing is selected or provided
  131.         //The game's CompileAndRun function seems to handle this.
  132.  
  133.         using CompileAndRunImpl_t = void(RE::Script*, RE::ScriptCompiler*, RE::COMPILER_NAME, RE::TESObjectREFR*);
  134.  
  135.         //Get the address of the internal CompileAndRun function using relocation
  136.         //Use the same relocation ID ConsoleUtil uses.
  137.         //AE (1.6.1130+): 441582 | SE (pre-1.6.1130): 21890 | VR: ???
  138.         REL::Relocation<CompileAndRunImpl_t> compileAndRunFunc{
  139.             RELOCATION_ID(21416, REL::Module::get().version().patch() < 1130 ? 21890 : 441582)};
  140.  
  141.         if (!compileAndRunFunc.get()) {
  142.             SKSE::log::error("ExecuteConsoleCommand: Cannot find CompileAndRun function address.");
  143.             delete script;  // Clean up the created script object
  144.             return;
  145.         }
  146.  
  147.         RE::ScriptCompiler compiler;
  148.  
  149.         SKSE::log::info(
  150.             "Executing console command: \"{}\" with target: {}", a_command,
  151.             targetRef ? (targetRef->GetName() ? targetRef->GetName() : "Unnamed Ref") : "None (or Console Selected)");
  152.  
  153.         //Call the internal game function to execute the command
  154.         compileAndRunFunc(script,                                    //The script object containing the command
  155.                           &compiler,                                 //The compiler instance
  156.                           RE::COMPILER_NAME::kSystemWindowCompiler,  //Context, kSystemWindowCompiler is appropriate
  157.                           targetRef);                                //The target reference
  158.  
  159.         //Clean up the temporary script object
  160.         delete script;
  161.     }
  162.  
  163.      /**
  164.      * Checks if the player character is a member of a specific faction from a specific mod.
  165.      *
  166.      * --Todo: Pointless overhead having to re-grab the editorID constantly. Just done this way for laziness since I
  167.      * only use it here
  168.      */
  169.     bool IsPlayerInFaction(const std::string& a_factionEditorID, const std::string& a_modName) {
  170.         auto player = RE::PlayerCharacter::GetSingleton();
  171.         if (!player) {
  172.             SKSE::log::warn("IsPlayerInFaction: Could not get PlayerCharacter singleton.");
  173.             return false;
  174.         }
  175.  
  176.         //Look up the form globally using its Editor ID
  177.         //This might find a form from *any* loaded mod with this Editor ID?
  178.         RE::TESForm* foundForm = RE::TESForm::LookupByEditorID(a_factionEditorID);
  179.  
  180.         if (!foundForm) {
  181.             SKSE::log::debug("IsPlayerInFaction: No form found with EditorID '{}' in any loaded mod.",
  182.                              a_factionEditorID);
  183.             return false;
  184.         }
  185.  
  186.         //Verify the source mod file of the found form
  187.         const RE::TESFile* sourceFile = foundForm->GetFile(0);  //Get the first file (usually the defining one)
  188.         if (!sourceFile || !sourceFile->fileName) {
  189.             SKSE::log::warn(
  190.                 "IsPlayerInFaction: Form found for '{}', but could not get valid source file information (FormID: "
  191.                 "{:08X}).",
  192.                 a_factionEditorID, foundForm->GetFormID());
  193.             return false;
  194.         }
  195.  
  196.         //Perform case-insensitive comparison
  197.         std::string_view sourceFileNameSV = sourceFile->fileName;
  198.         std::string_view expectedModNameSV = a_modName;
  199.  
  200.         if (!ci_equal(sourceFileNameSV, expectedModNameSV)) {
  201.             SKSE::log::debug(
  202.                 "IsPlayerInFaction: Form found for '{}' (FormID: {:08X}), but it comes from '{}', not the expected "
  203.                 "'{}'.",
  204.                 a_factionEditorID, foundForm->GetFormID(), sourceFileNameSV, expectedModNameSV);
  205.             return false;
  206.         }
  207.  
  208.         RE::TESFaction* targetFaction = foundForm->As<RE::TESFaction>();
  209.         if (!targetFaction) {
  210.             //Just in case, some debugging. Why not?
  211.             SKSE::log::warn("IsPlayerInFaction: Form {:08X} ('{}' from '{}') is not a TESFaction.",
  212.                             foundForm->GetFormID(), a_factionEditorID, a_modName);
  213.             return false;
  214.         }
  215.  
  216.         bool isInFaction = player->IsInFaction(targetFaction);
  217.  
  218.         return isInFaction;
  219.     }
  220.  
  221.     class MouthStateHandler {
  222.     public:
  223.         static MouthStateHandler* GetSingleton() {
  224.             static MouthStateHandler singleton;
  225.             return &singleton;
  226.         }
  227.  
  228.         void Update() {
  229.             if (!IsPlayerLoaded()) {
  230.                 DebugPrint("Player not loaded ");
  231.  
  232.                 return;
  233.             }
  234.  
  235.             bool currentMouthOpen = IsPlayerMouthOpen();
  236.  
  237.  
  238.             // If mouth state changed
  239.             if (currentMouthOpen != wasMouthOpen) {
  240.                 SKSE::log::info("mouth toggled");
  241.                 DebugPrint("Mouth toggled> " + currentMouthOpen);
  242.  
  243.                 bool animating = IsPlayerInFaction("SexLabAnimatingFaction", "SexLab.esm");
  244.  
  245.                 //Only checking
  246.                 if (animating) {
  247.  
  248.                     if (currentMouthOpen) {
  249.                         ExecuteConsoleCommand("Smp Reset");
  250.                     }
  251.  
  252.                     wasMouthOpen = currentMouthOpen;
  253.                 }
  254.             }
  255.         }
  256.  
  257.     private:
  258.         bool wasMouthOpen = false;
  259.  
  260.         bool IsPlayerMouthOpen() {
  261.             auto player = RE::PlayerCharacter::GetSingleton();
  262.             if (!player) {
  263.                 DebugPrint("IsPlayerMouthOpen: PlayerCharacter null?");
  264.                 return false;
  265.             }
  266.  
  267.             auto animData = player->GetFaceGenAnimationData();
  268.             if (!animData) {
  269.                 DebugPrint("IsPlayerMouthOpen: animData null?");
  270.                 return false;
  271.             }
  272.  
  273.             //--- Check Final Expressions (unk0C0) ---
  274.             //This is where merged expressions often end up
  275.             auto& finalExpression = animData->unk0C0;
  276.             float expressionThreshold = 0.5f;          //Todo: Tune this threshold
  277.  
  278.             if (finalExpression.values && finalExpression.count > 0) {
  279.                 //Indices likely to involve an open mouth
  280.                 const std::vector<std::uint32_t> openMouthExprIndices = {
  281.                     ExpressionIndices::kCombatShout
  282.                     //Add others?
  283.                 };
  284.  
  285.                 for (const auto indexToCheck : openMouthExprIndices) {
  286.                     if (finalExpression.count > indexToCheck) {
  287.                         float mouthValue = finalExpression.values[indexToCheck];
  288.                         if (mouthValue > expressionThreshold) {
  289.                             DebugPrint("Mouth open via Final Expression %u: %.2f",
  290.                                                                   indexToCheck, mouthValue);
  291.                             return true;  //Mouth open due to expression?
  292.                         }
  293.                     }
  294.                 }
  295.             } else {
  296.                 DebugPrint(
  297.                     "IsPlayerMouthOpen: Final Expression (unk0C0) values invalid or count is 0");
  298.             }
  299.  
  300.             //--- Check Final Phonemes (unk120) ---
  301.             //This is where merged phonemes often end up
  302.             auto& finalPhoneme = animData->unk120;
  303.             float phonemeThreshold = 0.3f;          //Todo: Tune this threshold, might be lower than expressions
  304.  
  305.             if (finalPhoneme.values && finalPhoneme.count > 0) {
  306.  
  307.                 //Indices for open-mouth Phonemes
  308.                 const std::vector<std::uint32_t> openMouthPhonemeIndices = {
  309.                     PhonemeIndices::kAah, PhonemeIndices::kBigAah, PhonemeIndices::kOh, PhonemeIndices::kOohQ,
  310.                     PhonemeIndices::kEh  //Often slightly open
  311.                 };
  312.  
  313.                 for (const auto indexToCheck : openMouthPhonemeIndices) {
  314.                     if (finalPhoneme.count > indexToCheck) {
  315.                         float mouthValue = finalPhoneme.values[indexToCheck];
  316.                         if (mouthValue > phonemeThreshold) {
  317.                             DebugPrint("Mouth open via Final Phoneme %u: %.2f", indexToCheck,
  318.                                                                   mouthValue);
  319.                             return true;  //Mouth open due to phoneme
  320.                         }
  321.                     }
  322.                 }
  323.             } else {
  324.               DebugPrint(
  325.                     "IsPlayerMouthOpen: Final Phoneme (unk120) values invalid or count is 0");
  326.             }
  327.  
  328.             //If neither check found an open mouth
  329.             DebugPrint("Mouth closed (checked expressions and phonemes)");
  330.             return false;
  331.         }
  332.  
  333.         //Check if player is loaded
  334.         bool IsPlayerLoaded() { return RE::PlayerCharacter::GetSingleton() != nullptr; }
  335.        
  336.     };
  337. }
  338.  
  339. std::atomic<bool> g_threadRunning(false);
  340. std::thread g_updateThread;
  341.  
  342. //Todo: Rename this. Maybe just DoTick? FrameUpdate makes no sense
  343. void FrameUpdate() {
  344.     MouthStateHandler::GetSingleton()->Update();
  345. }
  346.  
  347. void UpdateThreadFunction() {
  348.     while (g_threadRunning) {
  349.         SKSE::GetTaskInterface()->AddTask(FrameUpdate); //Queue to game thread
  350.         std::this_thread::sleep_for(std::chrono::seconds(1));
  351.     }
  352. }
  353.  
  354. void MessageHandler(SKSE::MessagingInterface::Message* message) {
  355.     if (message->type == SKSE::MessagingInterface::kDataLoaded) {
  356.         if (!g_threadRunning) {
  357.             g_threadRunning = true;
  358.             g_updateThread = std::thread(UpdateThreadFunction);
  359.             g_updateThread.detach();
  360.             SKSE::log::info("Registered update task thread");
  361.         }
  362.     }
  363. }
  364.  
  365. SKSEPluginLoad(const SKSE::LoadInterface* skse) {
  366.     InitializeLogging();
  367.  
  368.     SKSE::Init(skse);
  369.  
  370.     auto messaging = SKSE::GetMessagingInterface();
  371.     if (messaging) {
  372.         messaging->RegisterListener("SKSE", MessageHandler);
  373.         SKSE::log::info("Registered messaging listener");
  374.     }
  375.  
  376.     return true;
  377. }
Advertisement
Add Comment
Please, Sign In to add comment