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 {
- 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);
- }
- 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;
- }
- class MouthStateHandler {
- public:
- static MouthStateHandler* GetSingleton() {
- static MouthStateHandler singleton;
- return &singleton;
- }
- void Update() {
- if (!IsPlayerLoaded()) {
- RE::ConsoleLog::GetSingleton()->Print("Player not loaded ");
- return;
- }
- bool currentMouthOpen = IsPlayerMouthOpen();
- // If mouth state changed
- if (currentMouthOpen != wasMouthOpen) {
- SKSE::log::info("mouth toggled");
- RE::ConsoleLog::GetSingleton()->Print("Mouth toggled> " + currentMouthOpen);
- if (currentMouthOpen) {
- ExecuteConsoleCommand("Smp Reset");
- } else {
- //ExecuteConsoleCommand("player.EquipSpell IllusionLight LeftHand");
- }
- wasMouthOpen = currentMouthOpen;
- }
- }
- private:
- bool wasMouthOpen = false;
- bool IsPlayerMouthOpen() {
- auto player = RE::PlayerCharacter::GetSingleton();
- if (!player) {
- RE::ConsoleLog::GetSingleton()->Print("IsPlayerMouthOpen: PlayerCharacter null?");
- return false;
- }
- auto animData = player->GetFaceGenAnimationData();
- if (!animData) {
- RE::ConsoleLog::GetSingleton()->Print("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) {
- RE::ConsoleLog::GetSingleton()->Print("Mouth open via Final Expression %u: %.2f",
- indexToCheck, mouthValue);
- return true; //Mouth open due to expression?
- }
- }
- }
- } else {
- RE::ConsoleLog::GetSingleton()->Print(
- "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) {
- RE::ConsoleLog::GetSingleton()->Print("Mouth open via Final Phoneme %u: %.2f", indexToCheck,
- mouthValue);
- return true; //Mouth open due to phoneme
- }
- }
- }
- } else {
- RE::ConsoleLog::GetSingleton()->Print(
- "IsPlayerMouthOpen: Final Phoneme (unk120) values invalid or count is 0");
- }
- //If neither check found an open mouth
- RE::ConsoleLog::GetSingleton()->Print("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