Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #include "PCH.h"
- #include <thread>
- #include <chrono>
- #include <atomic>
- namespace ExpressionIndices {
- // From MfgFix::BSFaceGenKeyframeMultiple::Expression
- constexpr std::uint32_t kDialogueAnger = 0;
- constexpr std::uint32_t kDialogueFear = 1;
- constexpr std::uint32_t kDialogueHappy = 2;
- constexpr std::uint32_t kDialogueSad = 3;
- constexpr std::uint32_t kDialogueSurprise = 4;
- constexpr std::uint32_t kDialoguePuzzled = 5;
- constexpr std::uint32_t kDialogueDisgusted = 6;
- constexpr std::uint32_t kMoodNeutral = 7;
- constexpr std::uint32_t kMoodAnger = 8;
- constexpr std::uint32_t kMoodFear = 9;
- constexpr std::uint32_t kMoodHappy = 10;
- constexpr std::uint32_t kMoodSad = 11;
- constexpr std::uint32_t kMoodSurprise = 12;
- constexpr std::uint32_t kMoodPuzzled = 13;
- constexpr std::uint32_t kMoodDisgusted = 14;
- constexpr std::uint32_t kCombatAnger = 15;
- constexpr std::uint32_t kCombatShout = 16;
- }
- namespace PhonemeIndices {
- // From MfgFix::BSFaceGenKeyframeMultiple::Phoneme
- constexpr std::uint32_t kAah = 0;
- constexpr std::uint32_t kBigAah = 1;
- constexpr std::uint32_t kBMP = 2;
- constexpr std::uint32_t kChJSh = 3;
- constexpr std::uint32_t kDST = 4;
- constexpr std::uint32_t kEee = 5;
- constexpr std::uint32_t kEh = 6;
- constexpr std::uint32_t kFV = 7;
- constexpr std::uint32_t kI = 8;
- constexpr std::uint32_t kK = 9;
- constexpr std::uint32_t kN = 10;
- constexpr std::uint32_t kOh = 11;
- constexpr std::uint32_t kOohQ = 12;
- constexpr std::uint32_t kR = 13;
- constexpr std::uint32_t kTh = 14;
- constexpr std::uint32_t kW = 15;
- }
- namespace {
- bool debugging = false;
- void InitializeLogging() {
- auto path = SKSE::log::log_directory();
- if (!path) {
- return;
- }
- *path /= PLUGIN_NAME;
- *path += ".log";
- auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(path->string(), true);
- auto log = std::make_shared<spdlog::logger>("global", std::move(sink));
- log->set_level(spdlog::level::info);
- log->flush_on(spdlog::level::info);
- spdlog::set_default_logger(std::move(log));
- spdlog::set_pattern("[%H:%M:%S] [%l] %v");
- SKSE::log::info("{} v{} - Initialized", PLUGIN_NAME, PLUGIN_VERSION);
- }
- void DebugPrint(const char* a_fmt, ...) {
- if (debugging) {
- std::va_list args;
- va_start(args, a_fmt);
- RE::ConsoleLog::GetSingleton()->VPrint(a_fmt, args);
- va_end(args);
- }
- }
- //case-insensitive string comparison
- bool ci_equal(std::string_view s1, std::string_view s2) {
- return std::equal(s1.begin(), s1.end(), s2.begin(), s2.end(), [](char a, char b) {
- return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
- });
- }
- RE::TESObjectREFR* GetConsoleSelectedReference() {
- //storing the handle of the reference selected in the console.
- //AE (1.6.1130+): 504099 | SE (pre-1.6.1130): 405935 | VR: ???
- REL::Relocation<RE::ObjectRefHandle*> selectedRefHandleGlobal{
- RELOCATION_ID(519394, REL::Module::get().version().patch() < 1130 ? 405935 : 504099)};
- if (!selectedRefHandleGlobal.get()) {
- SKSE::log::warn("Could not find global variable for selected console reference handle.");
- return nullptr;
- }
- RE::ObjectRefHandle selectedRefHandle = *selectedRefHandleGlobal;
- RE::NiPointer<RE::TESObjectREFR> selectedRefPtr = selectedRefHandle.get();
- return selectedRefPtr.get();
- }
- //Function to execute a console command string
- //Couldn't figure out a cleaner way of doing this ¯\_(ツ)_/¯
- //a_targetRef: Optional target for the command. If nullptr, uses the console's selected reference.
- void ExecuteConsoleCommand(const std::string& a_command, RE::TESObjectREFR* a_targetRef = nullptr) {
- if (a_command.empty()) {
- SKSE::log::warn("ExecuteConsoleCommand: Attempted to execute empty command.");
- return;
- }
- const auto scriptFactory = RE::IFormFactory::GetConcreteFormFactoryByType<RE::Script>();
- if (!scriptFactory) {
- SKSE::log::error("ExecuteConsoleCommand: Cannot get Script factory.");
- return;
- }
- //Temporary script object
- RE::Script* script = scriptFactory->Create();
- if (!script) {
- SKSE::log::error("ExecuteConsoleCommand: Cannot create Script object for command: {}", a_command);
- return;
- }
- script->SetCommand(a_command);
- RE::TESObjectREFR* targetRef = a_targetRef ? a_targetRef : GetConsoleSelectedReference();
- //Note:: targetRef can be nullptr if nothing is selected or provided
- //The game's CompileAndRun function seems to handle this.
- using CompileAndRunImpl_t = void(RE::Script*, RE::ScriptCompiler*, RE::COMPILER_NAME, RE::TESObjectREFR*);
- //Get the address of the internal CompileAndRun function using relocation
- //Use the same relocation ID ConsoleUtil uses.
- //AE (1.6.1130+): 441582 | SE (pre-1.6.1130): 21890 | VR: ???
- REL::Relocation<CompileAndRunImpl_t> compileAndRunFunc{
- RELOCATION_ID(21416, REL::Module::get().version().patch() < 1130 ? 21890 : 441582)};
- if (!compileAndRunFunc.get()) {
- SKSE::log::error("ExecuteConsoleCommand: Cannot find CompileAndRun function address.");
- delete script; // Clean up the created script object
- return;
- }
- RE::ScriptCompiler compiler;
- SKSE::log::info(
- "Executing console command: \"{}\" with target: {}", a_command,
- targetRef ? (targetRef->GetName() ? targetRef->GetName() : "Unnamed Ref") : "None (or Console Selected)");
- //Call the internal game function to execute the command
- compileAndRunFunc(script, //The script object containing the command
- &compiler, //The compiler instance
- RE::COMPILER_NAME::kSystemWindowCompiler, //Context, kSystemWindowCompiler is appropriate
- targetRef); //The target reference
- //Clean up the temporary script object
- delete script;
- }
- /**
- * Checks if the player character is a member of a specific faction from a specific mod.
- */
- bool IsPlayerInFaction(const std::string& a_factionEditorID, const std::string& a_modName) {
- // Get player singleton first (needed in all cases)
- auto player = RE::PlayerCharacter::GetSingleton();
- if (!player) {
- SKSE::log::warn("IsPlayerInFaction: Could not get PlayerCharacter singleton.");
- return false;
- }
- static std::unordered_map<std::string, RE::TESFaction*> factionCache;
- // Create a key for the cache (combine editorID and modName)
- std::string cacheKey = a_factionEditorID + "|" + a_modName;
- //Check if we already have this faction in the cache
- auto cacheIt = factionCache.find(cacheKey);
- if (cacheIt != factionCache.end()) {
- return player->IsInFaction(cacheIt->second);
- }
- RE::TESForm* foundForm = RE::TESForm::LookupByEditorID(a_factionEditorID);
- if (!foundForm) {
- SKSE::log::debug("IsPlayerInFaction: No form found with EditorID '{}' in any loaded mod.",
- a_factionEditorID);
- return false;
- }
- // Verify the source mod file of the found form
- const RE::TESFile* sourceFile = foundForm->GetFile(0);
- if (!sourceFile || !sourceFile->fileName) {
- SKSE::log::warn(
- "IsPlayerInFaction: Form found for '{}', but could not get valid source file information (FormID: "
- "{:08X}).",
- a_factionEditorID, foundForm->GetFormID());
- return false;
- }
- // Perform case-insensitive comparison
- std::string_view sourceFileNameSV = sourceFile->fileName;
- std::string_view expectedModNameSV = a_modName;
- if (!ci_equal(sourceFileNameSV, expectedModNameSV)) {
- SKSE::log::debug(
- "IsPlayerInFaction: Form found for '{}' (FormID: {:08X}), but it comes from '{}', not the expected "
- "'{}'.",
- a_factionEditorID, foundForm->GetFormID(), sourceFileNameSV, expectedModNameSV);
- return false;
- }
- RE::TESFaction* targetFaction = foundForm->As<RE::TESFaction>();
- if (!targetFaction) {
- SKSE::log::warn("IsPlayerInFaction: Form {:08X} ('{}' from '{}') is not a TESFaction.",
- foundForm->GetFormID(), a_factionEditorID, a_modName);
- return false;
- }
- //Add to cache
- factionCache[cacheKey] = targetFaction;
- return player->IsInFaction(targetFaction);
- }
- class MouthStateHandler {
- public:
- static MouthStateHandler* GetSingleton() {
- static MouthStateHandler singleton;
- return &singleton;
- }
- void Update() {
- if (!IsPlayerLoaded()) {
- DebugPrint("Player not loaded ");
- return;
- }
- bool currentMouthOpen = IsPlayerMouthOpen();
- // If mouth state changed
- if (currentMouthOpen != wasMouthOpen) {
- SKSE::log::info("mouth toggled");
- DebugPrint("Mouth toggled> " + currentMouthOpen);
- if (currentMouthOpen) {
- ResetHead();
- }
- wasMouthOpen = currentMouthOpen;
- }
- }
- private:
- bool wasMouthOpen = false;
- bool wasAnimating = false;
- void ResetHead() {
- auto player = RE::PlayerCharacter::GetSingleton();
- if (!player) return; // Need the player
- auto player3DRoot = player->Get3D(false);
- if (!player3DRoot) {
- player3DRoot = player->Get3D(true); //Fallback
- if (!player3DRoot) {
- RE::ConsoleLog::GetSingleton()->Print("FaceMorphWatcher: Cannot get player 3D root for skeleton.");
- return;
- }
- }
- auto playerSkeleton = player3DRoot->AsNode();
- if (!playerSkeleton) {
- RE::ConsoleLog::GetSingleton()->Print("FaceMorphWatcher: Player 3D root is not an NiNode.");
- return;
- }
- RE::BSFaceGenNiNode* faceNode = player->GetFaceNodeSkinned();
- if (!faceNode) {
- SKSE::log::debug("FaceMorphWatcher: GetFaceNodeSkinned() returned null. No skinned face node found?");
- return;
- }
- try {
- RE::NiUpdateData updateData;
- updateData.time = 0.0f;
- updateData.flags = RE::NiUpdateData::Flag::kDirty;
- // Potentially helps propagate transforms??
- SKSE::log::debug("Calling UpdateWorldData...");
- faceNode->UpdateWorldData(&updateData);
- // This is what actually seems to force an update
- SKSE::log::debug("Calling FixSkinInstances...");
- faceNode->FixSkinInstances(playerSkeleton, true);
- SKSE::log::info("FaceMorphWatcher: Updates executed successfully on skinned face node.");
- } catch (const std::exception& e) {
- SKSE::log::error("FaceMorphWatcher: Exception during update calls on skinned face node: {}", e.what());
- } catch (...) {
- SKSE::log::error("FaceMorphWatcher: Unknown exception during update calls on skinned face node.");
- }
- }
- bool IsPlayerMouthOpen() {
- auto player = RE::PlayerCharacter::GetSingleton();
- if (!player) {
- DebugPrint("IsPlayerMouthOpen: PlayerCharacter null?");
- return false;
- }
- auto animData = player->GetFaceGenAnimationData();
- if (!animData) {
- DebugPrint("IsPlayerMouthOpen: animData null?");
- return false;
- }
- //--- Check Final Expressions (unk0C0) ---
- //This is where merged expressions often end up
- auto& finalExpression = animData->unk0C0;
- float expressionThreshold = 0.5f; //Todo: Tune this threshold
- if (finalExpression.values && finalExpression.count > 0) {
- //Indices likely to involve an open mouth
- const std::vector<std::uint32_t> openMouthExprIndices = {
- ExpressionIndices::kCombatShout
- //Add others?
- };
- for (const auto indexToCheck : openMouthExprIndices) {
- if (finalExpression.count > indexToCheck) {
- float mouthValue = finalExpression.values[indexToCheck];
- if (mouthValue > expressionThreshold) {
- DebugPrint("Mouth open via Final Expression %u: %.2f",
- indexToCheck, mouthValue);
- return true; //Mouth open due to expression
- }
- }
- }
- } else {
- DebugPrint(
- "IsPlayerMouthOpen: Final Expression (unk0C0) values invalid or count is 0");
- }
- //--- Check Final Phonemes (unk120) ---
- //This is where merged phonemes often end up
- auto& finalPhoneme = animData->unk120;
- float phonemeThreshold = 0.3f; //Todo: Tune this threshold, might be lower than expressions
- if (finalPhoneme.values && finalPhoneme.count > 0) {
- //Indices for open-mouth Phonemes
- const std::vector<std::uint32_t> openMouthPhonemeIndices = {
- PhonemeIndices::kAah, PhonemeIndices::kBigAah, PhonemeIndices::kOh, PhonemeIndices::kOohQ,
- PhonemeIndices::kEh //Often slightly open
- };
- for (const auto indexToCheck : openMouthPhonemeIndices) {
- if (finalPhoneme.count > indexToCheck) {
- float mouthValue = finalPhoneme.values[indexToCheck];
- if (mouthValue > phonemeThreshold) {
- DebugPrint("Mouth open via Final Phoneme %u: %.2f", indexToCheck,
- mouthValue);
- return true; //Mouth open due to phoneme
- }
- }
- }
- } else {
- DebugPrint(
- "IsPlayerMouthOpen: Final Phoneme (unk120) values invalid or count is 0");
- }
- //If neither check found an open mouth
- DebugPrint("Mouth closed (checked expressions and phonemes)");
- return false;
- }
- //Check if player is loaded
- bool IsPlayerLoaded() { return RE::PlayerCharacter::GetSingleton() != nullptr; }
- };
- }
- std::atomic<bool> g_threadRunning(false);
- std::thread g_updateThread;
- //Todo: Rename this. Maybe just DoTick? FrameUpdate makes no sense
- void FrameUpdate() {
- MouthStateHandler::GetSingleton()->Update();
- }
- void UpdateThreadFunction() {
- while (g_threadRunning) {
- SKSE::GetTaskInterface()->AddTask(FrameUpdate); //Queue to game thread
- std::this_thread::sleep_for(std::chrono::seconds(1));
- }
- }
- void MessageHandler(SKSE::MessagingInterface::Message* message) {
- if (message->type == SKSE::MessagingInterface::kDataLoaded) {
- if (!g_threadRunning) {
- g_threadRunning = true;
- g_updateThread = std::thread(UpdateThreadFunction);
- g_updateThread.detach();
- SKSE::log::info("Registered update task thread");
- }
- }
- }
- SKSEPluginLoad(const SKSE::LoadInterface* skse) {
- InitializeLogging();
- SKSE::Init(skse);
- auto messaging = SKSE::GetMessagingInterface();
- if (messaging) {
- messaging->RegisterListener("SKSE", MessageHandler);
- SKSE::log::info("Registered messaging listener");
- }
- return true;
- }
Advertisement
Add Comment
Please, Sign In to add comment