From f21d9e8d6c0acd626c372d64cf059e142c6e2e52 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Thu, 31 Aug 2023 23:39:24 -0700 Subject: [PATCH] revamp breakpoints, add invalidation --- .../BreakpointManager.cpp | 159 +++++++++++++++--- .../BreakpointManager.h | 27 ++- src/DarkId.Papyrus.DebugServer/ConfigHooks.h | 100 ++++++++++- ...arkId.Papyrus.DebugServer.Fallout4.vcxproj | 4 +- .../DarkId.Papyrus.DebugServer.Skyrim.vcxproj | 3 + .../DebugExecutionManager.cpp | 4 +- .../DebugExecutionManager.h | 2 +- .../PapyrusDebugger.cpp | 24 ++- .../PapyrusDebugger.h | 5 +- src/DarkId.Papyrus.DebugServer/Pex.cpp | 32 ++++ src/DarkId.Papyrus.DebugServer/Pex.h | 2 + src/DarkId.Papyrus.DebugServer/PexCache.cpp | 5 + src/DarkId.Papyrus.DebugServer/PexCache.h | 2 + .../RuntimeEvents.cpp | 142 +++++++++++++++- .../RuntimeEvents.h | 33 ++-- src/DarkId.Papyrus.DebugServer/Window.cpp | 4 + src/DarkId.Papyrus.DebugServer/pdsPCH.h | 13 +- 17 files changed, 507 insertions(+), 54 deletions(-) diff --git a/src/DarkId.Papyrus.DebugServer/BreakpointManager.cpp b/src/DarkId.Papyrus.DebugServer/BreakpointManager.cpp index addf0a58..3b2ac927 100644 --- a/src/DarkId.Papyrus.DebugServer/BreakpointManager.cpp +++ b/src/DarkId.Papyrus.DebugServer/BreakpointManager.cpp @@ -2,11 +2,22 @@ #include #include #include "Utilities.h" -#ifdef _DEBUG_DUMP_PEX #include "Pex.h" -#endif +#include "ConfigHooks.h" +#include "RuntimeEvents.h" +#include "GameInterfaces.h" + namespace DarkId::Papyrus::DebugServer { + + int64_t GetBreakpointID(int scriptReference, int lineNumber) { + return (((int64_t)scriptReference) << 32) + lineNumber; + } + + std::string GetInstructionReference(const Pex::DebugInfo::FunctionInfo& finfo) { + return std::format("{}:{}:{}", finfo.getObjectName().asString(), finfo.getStateName().asString(), finfo.getFunctionName().asString()); + } + dap::ResponseOrError BreakpointManager::SetBreakpoints(const dap::Source& source, const std::vector& srcBreakpoints) { dap::SetBreakpointsResponse response; @@ -18,8 +29,8 @@ namespace DarkId::Papyrus::DebugServer } auto ref = GetSourceReference(source); bool hasDebugInfo = binary->getDebugInfo().getFunctionInfos().size() > 0; + if (!hasDebugInfo) { - #if FALLOUT const std::string iniName = "fallout4.ini"; #else @@ -28,63 +39,131 @@ namespace DarkId::Papyrus::DebugServer RETURN_DAP_ERROR(std::format("SetBreakpoints: No debug data for script {}. Ensure that `bLoadDebugInformation=1` is set under `[Papyrus]` in {}", scriptName, iniName)); } + ScriptBreakpoints info { + .ref = ref, + .source = source, + .modificationTime = binary->getDebugInfo().getModificationTime() + }; + std::map foundBreakpoints; + for (const auto& srcBreakpoint : srcBreakpoints) { auto foundLine = false; int line = static_cast(srcBreakpoint.line); + int instructionNum = -1; + int foundFunctionInfoIndex{-1}; + Pex::DebugInfo::FunctionInfo debugfinfo; + int64_t breakpointId = -1; if (binary) { - for (auto & functionInfo : binary->getDebugInfo().getFunctionInfos()) + auto& funcInfos = binary->getDebugInfo().getFunctionInfos(); + for (int j = 0; j < funcInfos.size(); j++) { if (foundLine) { break; } - - for (auto lineNumber : functionInfo.getLineNumbers()) + for (int i = 0; i < funcInfos[j].getLineNumbers().size(); i++) { + auto lineNumber = funcInfos[j].getLineNumbers()[i]; if (line == static_cast(lineNumber)) { foundLine = true; + instructionNum = i; + foundFunctionInfoIndex = j; + debugfinfo = funcInfos[j]; break; } } } } + breakpointId = GetBreakpointID(ref, line); - breakpointLines.emplace(line); + if (foundLine) { + auto bpoint = BreakpointInfo{ + .breakpointId = breakpointId, + .instructionNum = instructionNum, + .lineNum = line, + .debugFuncInfoIndex = foundFunctionInfoIndex + }; + info.breakpoints[instructionNum] = bpoint; + } - response.breakpoints.push_back(dap::Breakpoint{ + response.breakpoints.push_back( dap::Breakpoint { + .id = foundLine ? dap::integer(breakpointId) : dap::optional(), + .instructionReference = foundLine ? GetInstructionReference(debugfinfo) : dap::optional(), .line = dap::integer(line), + .offset = foundLine ? dap::integer(instructionNum) : dap::optional(), .source = source, - .verified = foundLine, + .verified = foundLine }); } - m_breakpoints[ref] = breakpointLines; + m_breakpoints[ref] = info; return response; } - void BreakpointManager::ClearBreakpoints() { + + void BreakpointManager::ClearBreakpoints(bool emitChanged) { + if (emitChanged) { + for (auto & kv : m_breakpoints) { + InvalidateAllBreakpointsForScript(kv.first); + } + } m_breakpoints.clear(); } - bool BreakpointManager::GetExecutionIsAtValidBreakpoint(RE::BSScript::Internal::CodeTasklet* tasklet, uint32_t actualIP) - { - auto & func = tasklet->topFrame->owningFunction; - if (func->GetIsNative()) + // TODO: Upstream this + uint32_t GetInstructionNumberForOffset(RE::BSScript::ByteCode::PackedInstructionStream* stream, uint32_t IP) { + using func_t = decltype(&GetInstructionNumberForOffset); + REL::Relocation func{ Game_Offset::GetInstructionNumberForOffset }; + return func(stream, IP); + } + + void BreakpointManager::InvalidateAllBreakpointsForScript(int ref) { + if (m_breakpoints.find(ref) != m_breakpoints.end()) + { + return; + } + for (auto& KV : m_breakpoints[ref].breakpoints) + { + auto bpinfo = KV.second; + RuntimeEvents::EmitBreakpointChangedEvent(dap::Breakpoint{ + .id = bpinfo.breakpointId, + .line = bpinfo.lineNum, + .source = m_breakpoints[ref].source, + .verified = false + }, "changed"); + } + m_breakpoints.erase(ref); + } + + bool BreakpointManager::GetExecutionIsAtValidBreakpoint(RE::BSScript::Internal::CodeTasklet* tasklet) + { + auto &_func = tasklet->topFrame->owningFunction; + if (!_func || _func->GetIsNative()) { return false; } + // only ScriptFunctions are non-native + auto func = static_cast(_func.get()); const auto sourceReference = GetScriptReference(tasklet->topFrame->owningObjectType->GetName()); if (m_breakpoints.find(sourceReference) != m_breakpoints.end()) { - auto & breakpointLines = m_breakpoints[sourceReference]; - if (!breakpointLines.empty()) + auto& scriptBreakpoints = m_breakpoints[sourceReference]; + + auto binary = m_pexCache->GetCachedScript(sourceReference); + if (!binary || binary->getDebugInfo().getModificationTime() != scriptBreakpoints.modificationTime) { + // script was reloaded or removed after placement, remove it + InvalidateAllBreakpointsForScript(sourceReference); + return false; + } + if (!scriptBreakpoints.breakpoints.empty()) { - uint32_t currentLine; - bool success = func->TranslateIPToLineNumber(actualIP, currentLine); - if (success && breakpointLines.find(currentLine) != breakpointLines.end()) { + int currentInstruction = -1; + auto ip = tasklet->topFrame->STACK_FRAME_IP; + currentInstruction = GetInstructionNumberForOffset(&func->instructions, ip); + if (currentInstruction != -1 && scriptBreakpoints.breakpoints.find(currentInstruction) != scriptBreakpoints.breakpoints.end()) { return true; } return false; @@ -93,4 +172,44 @@ namespace DarkId::Papyrus::DebugServer return false; } + + //TODO: WIP + bool BreakpointManager::CheckIfFunctionWillWaitOrExit(RE::BSScript::Internal::CodeTasklet* tasklet) { + auto& func = tasklet->topFrame->owningFunction; + + if (func->GetIsNative()) + { + return true; + } + auto realfunc = dynamic_cast(func.get()); + + int instNum = GetInstructionNumberForOffset(&realfunc->instructions, tasklet->topFrame->STACK_FRAME_IP); + + std::string scriptName(tasklet->topFrame->owningObjectType->GetName()); + const auto sourceReference = GetScriptReference(scriptName); + if (m_breakpoints.find(sourceReference) != m_breakpoints.end()) + { + auto& scriptBreakpoints = m_breakpoints[sourceReference]; + auto binary = m_pexCache->GetScript(scriptName); + if (!binary || binary->getDebugInfo().getModificationTime() != scriptBreakpoints.modificationTime) { + return true; + } + if (scriptBreakpoints.breakpoints.find(instNum) != scriptBreakpoints.breakpoints.end()) + { + + auto& breakpointInfo = scriptBreakpoints.breakpoints[instNum]; + auto& debugFuncInfo = binary->getDebugInfo().getFunctionInfos()[breakpointInfo.debugFuncInfoIndex]; + auto& lineNumbers = debugFuncInfo.getLineNumbers(); + auto funcData = GetFunctionData(binary, debugFuncInfo.getObjectName(), debugFuncInfo.getStateName(), debugFuncInfo.getFunctionName()); + auto& instructions = funcData->getInstructions(); + for (int i = instNum+1; i < instructions.size(); i++) { + auto& instruction = instructions[i]; + auto opcode = instruction.getOpCode(); + // TODO: The rest of this + + } + } + } + return true; + } } diff --git a/src/DarkId.Papyrus.DebugServer/BreakpointManager.h b/src/DarkId.Papyrus.DebugServer/BreakpointManager.h index 9b498da0..fd7cb6e9 100644 --- a/src/DarkId.Papyrus.DebugServer/BreakpointManager.h +++ b/src/DarkId.Papyrus.DebugServer/BreakpointManager.h @@ -12,17 +12,36 @@ namespace DarkId::Papyrus::DebugServer { class BreakpointManager { - std::map> m_breakpoints; - PexCache* m_pexCache; public: + struct BreakpointInfo { + int64_t breakpointId; + int instructionNum; + int lineNum; + int debugFuncInfoIndex; + }; + + struct ScriptBreakpoints { + int ref{ -1 }; + dap::Source source; + std::time_t modificationTime{ 0 }; + std::map breakpoints; + + }; + explicit BreakpointManager(PexCache* pexCache) : m_pexCache(pexCache) { } dap::ResponseOrError SetBreakpoints(const dap::Source& src, const std::vector& srcBreakpoints); - void ClearBreakpoints(); - bool GetExecutionIsAtValidBreakpoint(RE::BSScript::Internal::CodeTasklet* tasklet, uint32_t actualIP); + void ClearBreakpoints(bool emitChanged = false); + bool CheckIfFunctionWillWaitOrExit(RE::BSScript::Internal::CodeTasklet* tasklet); + void InvalidateAllBreakpointsForScript(int ref); + bool GetExecutionIsAtValidBreakpoint(RE::BSScript::Internal::CodeTasklet* tasklet); + private: + PexCache* m_pexCache; + std::map m_breakpoints; + }; } diff --git a/src/DarkId.Papyrus.DebugServer/ConfigHooks.h b/src/DarkId.Papyrus.DebugServer/ConfigHooks.h index 2851cf83..e2dbbc45 100644 --- a/src/DarkId.Papyrus.DebugServer/ConfigHooks.h +++ b/src/DarkId.Papyrus.DebugServer/ConfigHooks.h @@ -11,12 +11,29 @@ namespace DarkId::Papyrus::DebugServer constexpr auto pPapyrusEnableLogging = RELOCATION_ID(510627, 383715); // 1.5.97: 141DF5AE8, 1.6.640: 141E88DC8 constexpr auto pPapyrusEnableTrace = RELOCATION_ID(510667, 383766); // 1.5.97: 141DF5C60, 1.6.640: 141E88F88 constexpr auto pPapyrusLoadDebugInformation = RELOCATION_ID(510650, 383743); // 1.5.97: 141DF5BC0, 1.6.640: 141E88EA0 + constexpr auto pControlsBackgroundMouse = RELOCATION_ID(511920, 388493); // pcontrols background mouse + constexpr auto pControlsBackgroundMouseDynamicInitializer = RELOCATION_ID(9099, 9144); // 1.5.97: 1400D9320, 1.6.640: 1400E1020 + constexpr auto pControlsBackgroundMouseDynamicInitializerEndOffset = REL::VariantOffset(0x68, 0x65, 0x68); // the atexit call (TODO: Figure out the Skyrim VR offset) + constexpr auto InitWindows = RELOCATION_ID(75591, 77226); + constexpr auto InitWindows_CreateWindowExA_Offset = REL::VariantOffset(0x163, 0x22C, 0x163); + constexpr auto bsrendererBegin = RELOCATION_ID(75460, 77245); + constexpr auto bsrendererBegin_GetClientRect_Offset = REL::VariantOffset(0x192, 0x18B, 0x192); + constexpr auto ExpectedCreateWindowsExA_Offset = REL::VariantOffset(0x79779F, 0x84C34E, 0x000000); + // BSScript__ByteCode__PackedInstructionStream__GetInstructionNumberForOffset + constexpr auto GetInstructionNumberForOffset = RELOCATION_ID(97807, 104551); // 1.5.97: 1248FA0, 1.6.640: 141371400 #else // FALLOUT // Where the bEnableLogging fallout4.ini setting is stored; this is overwritten when the ini is loaded constexpr auto pPapyrusEnableLogging = REL::ID(1272228); // 14380E140 // Where the bEnableTrace fallout4.ini setting is stored; overwritten on ini load constexpr auto pPapyrusEnableTrace = REL::ID(218028); // 143818DC0 + constexpr auto pControlsBackgroundMouse = REL::ID(187076); //143846670 + constexpr auto pControlsBackgroundMouseDynamicInitializer = REL::ID(267843); //142AEDB40 + constexpr auto pControlsBackgroundMouseDynamicInitializerEndOffset = REL::Offset(0x59); + // BSScript__ByteCode__PackedInstructionStream__GetInstructionNumberForOffset + + constexpr auto GetInstructionNumberForOffset = REL::ID(1126425); //14278A1B0 + #endif } // Modified hook from PapyrusTweaks by NightFallStorm with permission @@ -61,10 +78,91 @@ namespace DarkId::Papyrus::DebugServer static inline void Install() { REL::Relocation target = getHookTarget(); - stl::write_thunk_call(target.address()); + stl::write_thunk_call<5, EnableLoadDebugInformation>(target.address()); logger::info("EnableLoadDebugInformation hooked at address {:x}", target.address()); logger::info("EnableLoadDebugInformation hooked at offset {:x}", target.offset()); } + }; + + // We don't need this right now, just leaving it here + struct overridemouseinitialize { +#if SKYRIM + + // hooks into RE::BSWin32MouseDevice::Initialize() by overwriting the vtable entry with this + static inline void thunk(RE::BSWin32MouseDevice* _this) { + bool* pBackgroundMouse = (bool*)Game_Offset::pControlsBackgroundMouse.address(); + if (!*pBackgroundMouse) { + *pBackgroundMouse = true; + } + return func(_this); + } + static inline REL::Relocation func; + static inline void Install() { + auto initializeVtableEntry = RE::BSWin32MouseDevice::VTABLE[0].address() + 8; + REL::Relocation initializeLoc{ *(uintptr_t*)initializeVtableEntry }; + overridemouseinitialize::func = initializeLoc.address(); + REL::safe_write(initializeVtableEntry, stl::unrestricted_cast(overridemouseinitialize::thunk)); + logger::info("ForceEnableBackgroundMouse hooked at address {:x}", initializeLoc.address()); + logger::info("ForceEnableBackgroundMouse hooked at offset {:x}", initializeLoc.offset()); + } +#else + static inline void Install() {} +#endif + }; + + // We don't need this right now, just leaving it here + // This should be done before the INIs are loaded + struct ForceEnableBackgroundMouse { + // hooks into the dynamic SettingT initialzer for bBackgroundMouse and overrides the entry + // we install into the call to `atexit`, so the thunk is thunking `int atexit(void(__cdecl*)())` + static inline int thunk(void(__cdecl* atexitfunc)()) { + auto result = func(atexitfunc); + bool* pBackgroundMouse = (bool*)Game_Offset::pControlsBackgroundMouse.address(); + *pBackgroundMouse = true; + return result; + } + static inline REL::Relocation func; + + static inline void Install() { + // Set it here too in case it's not in the settings.ini + bool* pBackgroundMouse = (bool*)Game_Offset::pControlsBackgroundMouse.address(); + *pBackgroundMouse = true; + REL::Relocation target( + Game_Offset::pControlsBackgroundMouseDynamicInitializer.address() + + Game_Offset::pControlsBackgroundMouseDynamicInitializerEndOffset.offset()); + stl::write_thunk_branch<5,ForceEnableBackgroundMouse>(target.address()); + logger::info("ForceEnableBackgroundMouse hooked at address {:x}", target.address()); + logger::info("ForceEnableBackgroundMouse hooked at offset {:x}", target.offset()); + } + }; + + // In case we don't want to do the above + // This should be done after the INIs have been loaded + struct DyanmicallySetBackgroundMouse { + static inline bool Set(bool enabled) { + + bool* pBackgroundMouse = (bool*)Game_Offset::pControlsBackgroundMouse.address(); + if (*pBackgroundMouse != enabled) { + *pBackgroundMouse = enabled; +#if SKYRIM + auto devmanager = RE::BSInputDeviceManager::GetSingleton(); + if (devmanager) { + auto mouse = devmanager->GetMouse(); + if (mouse) { + if (mouse->dInputDevice && !mouse->notInitialized) { + devmanager->ReinitializeMouse(); + } + else { + mouse->backgroundMouse = enabled; + } + } + } +#endif + return true; + } + + return false; + } }; } \ No newline at end of file diff --git a/src/DarkId.Papyrus.DebugServer/DarkId.Papyrus.DebugServer.Fallout4.vcxproj b/src/DarkId.Papyrus.DebugServer/DarkId.Papyrus.DebugServer.Fallout4.vcxproj index 6b60c0dd..41bb17c1 100644 --- a/src/DarkId.Papyrus.DebugServer/DarkId.Papyrus.DebugServer.Fallout4.vcxproj +++ b/src/DarkId.Papyrus.DebugServer/DarkId.Papyrus.DebugServer.Fallout4.vcxproj @@ -1,4 +1,4 @@ - + @@ -222,6 +222,7 @@ Create + @@ -258,6 +259,7 @@ + diff --git a/src/DarkId.Papyrus.DebugServer/DarkId.Papyrus.DebugServer.Skyrim.vcxproj b/src/DarkId.Papyrus.DebugServer/DarkId.Papyrus.DebugServer.Skyrim.vcxproj index 5c46f684..8abf4f14 100644 --- a/src/DarkId.Papyrus.DebugServer/DarkId.Papyrus.DebugServer.Skyrim.vcxproj +++ b/src/DarkId.Papyrus.DebugServer/DarkId.Papyrus.DebugServer.Skyrim.vcxproj @@ -224,6 +224,7 @@ Create + Create @@ -252,6 +253,7 @@ pdsPCH.h pdsPCH.h + @@ -286,6 +288,7 @@ + diff --git a/src/DarkId.Papyrus.DebugServer/DebugExecutionManager.cpp b/src/DarkId.Papyrus.DebugServer/DebugExecutionManager.cpp index 00132b90..a6dd39c6 100644 --- a/src/DarkId.Papyrus.DebugServer/DebugExecutionManager.cpp +++ b/src/DarkId.Papyrus.DebugServer/DebugExecutionManager.cpp @@ -6,7 +6,7 @@ namespace DarkId::Papyrus::DebugServer { using namespace RE::BSScript::Internal; - void DebugExecutionManager::HandleInstruction(CodeTasklet* tasklet, uint32_t actualIP) + void DebugExecutionManager::HandleInstruction(CodeTasklet* tasklet) { std::lock_guard lock(m_instructionMutex); @@ -25,7 +25,7 @@ namespace DarkId::Papyrus::DebugServer { pauseReason = "paused"; } - else if (m_state != DebuggerState::kPaused && m_breakpointManager->GetExecutionIsAtValidBreakpoint(tasklet, actualIP)) + else if (m_state != DebuggerState::kPaused && m_breakpointManager->GetExecutionIsAtValidBreakpoint(tasklet)) { pauseReason = "breakpoint"; } diff --git a/src/DarkId.Papyrus.DebugServer/DebugExecutionManager.h b/src/DarkId.Papyrus.DebugServer/DebugExecutionManager.h index b8283acb..cdbee6ed 100644 --- a/src/DarkId.Papyrus.DebugServer/DebugExecutionManager.h +++ b/src/DarkId.Papyrus.DebugServer/DebugExecutionManager.h @@ -48,7 +48,7 @@ namespace DarkId::Papyrus::DebugServer } void Close(); - void HandleInstruction(CodeTasklet* tasklet, uint32_t actualIP); + void HandleInstruction(CodeTasklet* tasklet); void Open(std::shared_ptr ses); bool Continue(); bool Pause(); diff --git a/src/DarkId.Papyrus.DebugServer/PapyrusDebugger.cpp b/src/DarkId.Papyrus.DebugServer/PapyrusDebugger.cpp index fc8d39b4..a3f05e64 100644 --- a/src/DarkId.Papyrus.DebugServer/PapyrusDebugger.cpp +++ b/src/DarkId.Papyrus.DebugServer/PapyrusDebugger.cpp @@ -53,11 +53,15 @@ namespace DarkId::Papyrus::DebugServer m_instructionExecutionEventHandle = RuntimeEvents::SubscribeToInstructionExecution( - std::bind(&PapyrusDebugger::InstructionExecution, this, std::placeholders::_1, std::placeholders::_2)); + std::bind(&PapyrusDebugger::InstructionExecution, this, std::placeholders::_1)); // m_initScriptEventHandle = RuntimeEvents::SubscribeToInitScript(std::bind(&PapyrusDebugger::InitScriptEvent, this, std::placeholders::_1)); m_logEventHandle = RuntimeEvents::SubscribeToLog(std::bind(&PapyrusDebugger::EventLogged, this, std::placeholders::_1)); + + m_breakpointChangedEventHandle = + RuntimeEvents::SubscribeToBreakpointChanged(std::bind(&PapyrusDebugger::BreakpointChanged, this, std::placeholders::_1, std::placeholders::_2)); + RegisterSessionHandlers(); } void PapyrusDebugger::EndSession() { @@ -70,6 +74,7 @@ namespace DarkId::Papyrus::DebugServer RuntimeEvents::UnsubscribeFromInstructionExecution(m_instructionExecutionEventHandle); RuntimeEvents::UnsubscribeFromCreateStack(m_createStackEventHandle); RuntimeEvents::UnsubscribeFromCleanupStack(m_cleanupStackEventHandle); + RuntimeEvents::UnsubscribeFromBreakpointChanged(m_breakpointChangedEventHandle); m_executionManager->Close(); // clear session data @@ -80,7 +85,7 @@ namespace DarkId::Papyrus::DebugServer } - void PapyrusDebugger::RegisterSessionHandlers(){ + void PapyrusDebugger::RegisterSessionHandlers() { // The Initialize request is the first message sent from the client and // the response reports debugger capabilities. // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize @@ -220,7 +225,7 @@ namespace DarkId::Papyrus::DebugServer XSE::GetTaskInterface()->AddTask([this, stackId]() { - if (m_closed) + if (m_closed || !m_runtimeState) { return; } @@ -259,9 +264,9 @@ namespace DarkId::Papyrus::DebugServer }); } - void PapyrusDebugger::InstructionExecution(CodeTasklet* tasklet, uint32_t actualIP) const + void PapyrusDebugger::InstructionExecution(CodeTasklet* tasklet) const { - m_executionManager->HandleInstruction(tasklet, actualIP); + m_executionManager->HandleInstruction(tasklet); } void PapyrusDebugger::CheckSourceLoaded(const std::string &scriptName) const{ @@ -284,6 +289,15 @@ namespace DarkId::Papyrus::DebugServer } } + void PapyrusDebugger::BreakpointChanged(const dap::Breakpoint& bpoint, const std::string& reason) const + { + XSE::GetTaskInterface()->AddTask([this, bpoint, reason]() { + SendEvent(dap::BreakpointEvent{ + .breakpoint = bpoint, + .reason = reason + }); + }); + } PapyrusDebugger::~PapyrusDebugger() { diff --git a/src/DarkId.Papyrus.DebugServer/PapyrusDebugger.h b/src/DarkId.Papyrus.DebugServer/PapyrusDebugger.h index 320d0b3f..96f49513 100644 --- a/src/DarkId.Papyrus.DebugServer/PapyrusDebugger.h +++ b/src/DarkId.Papyrus.DebugServer/PapyrusDebugger.h @@ -94,13 +94,16 @@ namespace DarkId::Papyrus::DebugServer RuntimeEvents::InstructionExecutionEventHandle m_instructionExecutionEventHandle; // RuntimeEvents::InitScriptEventHandle m_initScriptEventHandle; RuntimeEvents::LogEventHandle m_logEventHandle; + RuntimeEvents::BreakpointChangedEventHandle m_breakpointChangedEventHandle; + void RegisterSessionHandlers(); dap::Error Error(const std::string &msg); // void InitScriptEvent(RE::TESInitScriptEvent* initEvent); void EventLogged(const RE::BSScript::LogEvent* logEvent) const; void StackCreated(RE::BSTSmartPointer& stack); void StackCleanedUp(uint32_t stackId); - void InstructionExecution(CodeTasklet* tasklet, uint32_t actualIP) const; + void InstructionExecution(CodeTasklet* tasklet) const; void CheckSourceLoaded(const std::string &scriptName) const; + void BreakpointChanged(const dap::Breakpoint& bpoint, const std::string& reason) const; }; } diff --git a/src/DarkId.Papyrus.DebugServer/Pex.cpp b/src/DarkId.Papyrus.DebugServer/Pex.cpp index 1797ebdd..f251331f 100644 --- a/src/DarkId.Papyrus.DebugServer/Pex.cpp +++ b/src/DarkId.Papyrus.DebugServer/Pex.cpp @@ -101,4 +101,36 @@ namespace DarkId::Papyrus::DebugServer } return true; } + + Pex::Function* GetFunctionData(std::shared_ptr binary, Pex::StringTable::Index objName, Pex::StringTable::Index stateName, Pex::StringTable::Index funcName) + { + for (auto& object : binary->getObjects()) { + if (object.getName() == objName) { + for (auto& state : object.getStates()) { + if (state.getName() == stateName) { + for (auto& function : state.getFunctions()) { + if (function.getName() == funcName) { + return std::addressof(function); + } + } + } + } + } + } + return nullptr; + } + + bool OpCodeWillCallOrReturn(Pex::OpCode opcode) { + switch (opcode) { + case Pex::OpCode::CALLMETHOD: + case Pex::OpCode::CALLPARENT: + case Pex::OpCode::CALLSTATIC: + case Pex::OpCode::RETURN: + case Pex::OpCode::PROPGET: + case Pex::OpCode::PROPSET: + return true; + default: + return false; + } + } } diff --git a/src/DarkId.Papyrus.DebugServer/Pex.h b/src/DarkId.Papyrus.DebugServer/Pex.h index c3cf9a36..8aae0226 100644 --- a/src/DarkId.Papyrus.DebugServer/Pex.h +++ b/src/DarkId.Papyrus.DebugServer/Pex.h @@ -7,4 +7,6 @@ namespace DarkId::Papyrus::DebugServer bool ReadPexResource(const std::string& scriptName, std::ostream& stream); bool LoadAndDumpPexData(const std::string& scriptName, std::string outputDir); bool LoadPexData(const std::string& scriptName, Pex::Binary& binary); + Pex::Function* GetFunctionData(std::shared_ptr binary, Pex::StringTable::Index objName, Pex::StringTable::Index stateName, Pex::StringTable::Index funcName); + } diff --git a/src/DarkId.Papyrus.DebugServer/PexCache.cpp b/src/DarkId.Papyrus.DebugServer/PexCache.cpp index 84a1ddad..a00c0f5c 100644 --- a/src/DarkId.Papyrus.DebugServer/PexCache.cpp +++ b/src/DarkId.Papyrus.DebugServer/PexCache.cpp @@ -20,6 +20,11 @@ namespace DarkId::Papyrus::DebugServer { return HasScript(GetScriptReference(scriptName)); } + + std::shared_ptr PexCache::GetCachedScript(const int ref) { + const auto entry = m_scripts.find(ref); + return entry != m_scripts.end() ? entry->second : nullptr; + } std::shared_ptr PexCache::GetScript(const std::string& scriptName) { diff --git a/src/DarkId.Papyrus.DebugServer/PexCache.h b/src/DarkId.Papyrus.DebugServer/PexCache.h index 5479e3a6..2ab6de85 100644 --- a/src/DarkId.Papyrus.DebugServer/PexCache.h +++ b/src/DarkId.Papyrus.DebugServer/PexCache.h @@ -17,6 +17,8 @@ namespace DarkId::Papyrus::DebugServer bool HasScript(int scriptReference); bool HasScript(const std::string & scriptName); + std::shared_ptr GetCachedScript(const int ref); + std::shared_ptr GetScript(const std::string & scriptName); bool GetDecompiledSource(const std::string & scriptName, std::string& decompiledSource); bool GetSourceData(const std::string &scriptName, dap::Source& data); diff --git a/src/DarkId.Papyrus.DebugServer/RuntimeEvents.cpp b/src/DarkId.Papyrus.DebugServer/RuntimeEvents.cpp index 2cd55484..93d79702 100644 --- a/src/DarkId.Papyrus.DebugServer/RuntimeEvents.cpp +++ b/src/DarkId.Papyrus.DebugServer/RuntimeEvents.cpp @@ -15,6 +15,7 @@ #include #include +#include namespace DarkId::Papyrus::DebugServer { @@ -33,12 +34,12 @@ namespace DarkId::Papyrus::DebugServer return g_##NAME##Event.remove(handle); \ } \ - EVENT_WRAPPER_IMPL(InstructionExecution, void(RE::BSScript::Internal::CodeTasklet*, uint32_t actualIP)) + EVENT_WRAPPER_IMPL(InstructionExecution, void(RE::BSScript::Internal::CodeTasklet*)) EVENT_WRAPPER_IMPL(CreateStack, void(RE::BSTSmartPointer&)) EVENT_WRAPPER_IMPL(CleanupStack, void(uint32_t)) // EVENT_WRAPPER_IMPL(InitScript, void(RE::TESInitScriptEvent*)) EVENT_WRAPPER_IMPL(Log, void(const RE::BSScript::LogEvent*)) - + EVENT_WRAPPER_IMPL(BreakpointChanged, void(const dap::Breakpoint& bpoint, const std::string&)) #undef EVENT_WRAPPER_IMPL //class ScriptInitEventSink : public RE::BSTEventSink @@ -109,7 +110,7 @@ namespace DarkId::Papyrus::DebugServer { // assign the correct IP a_tasklet->topFrame->STACK_FRAME_IP = currentIP; - g_InstructionExecutionEvent(a_tasklet, currentIP); + g_InstructionExecutionEvent(a_tasklet); } } @@ -136,6 +137,133 @@ namespace DarkId::Papyrus::DebugServer namespace Internal { + struct SetIPPatch { + + struct Patch : CallPatch + { + + Patch(std::uintptr_t a_retAddr, std::uintptr_t a_ifFreezeLabelAddr) + { + Xbyak::Label retLbl; + Xbyak::Label isFrozen; + Xbyak::Label ifFreezeLabel; + Xbyak::Label ifgeInstructionDataBitCountLabel; + /** + * The main issue we are trying to solve is that the InstructionPointer on the top stack frame + * doesn't get updated until the tasklet actually finishes executing. We need this to be set in order for our subsequent call to work + * + * It doesn't do this until it either: + * - reaches the end of the instruction bitstream + * - the stack is about to freeze + * - the max ops per tasklet have been executed (100) + * + * The actual IP is set in edx before checking for the first two conditions, and this will be used to set the topFrame's IP if the tasklet exits + * So we need to install our branch right after that to be able to set the IP with the correct value + * We install into the `jz` instruction since it's a 6-byte long jump. + * + * Here's our hook target, near the start of the main loop: + * ``` + * lea edx, [rax+rcx*8] # at this point, edx holds the actual current IP + * cmp dword ptr [rax+6Ch], 1 # check if this->stack->freeze state is 1 (frozen) + * jz if_frozen_label # jumps if above comparison is true <-- branch installed here + * cmp edx, [+40h] # compare the current IP to this->InstructionDataBitCount ("(CodeTasklet) this" is rsi in AE, rdi in SE) + * jb short if_less_than_InstructionDataBitCount_label # jump if the current IP is less than this->InstructionDataBitCount + * ``` + * + * Since we hook right in the middle of the checking of the first two conditions, we want to check those before attempting to set the IP. + * - for stack freeze, it's going to be assigned anyway + * - if we set the IP to anything >= InstructionDataBitCount it will mess up the return + * + * the current ops count and max ops comparison happens at the end of the loop, so we don't have to check that + */ + cmp(dword[rax + 0x6C], 1); // check to see if stack->freeze state is 1 (frozen) + jz(isFrozen); // we overwrite this instruction, so we have to jump to our saved address + if (REL::Module::IsAE()) { + cmp(edx, dword[rsi + 0x40]); // (CodeTasklet)this is rsi in AE + } + else { + cmp(edx, dword[rdi + 0x40]); // (CodeTasklet)this is rdi in SE + } + // originally a `jb` that skips the main switch case; we just want this to return if the above comparison is true + // we didn't overwrite that jump or the above comparison, so we can just return to the return address + jge(ifgeInstructionDataBitCountLabel); + + mov(dword[rax + 0x20], edx); // set the instruction pointer to the current IP + + L(ifgeInstructionDataBitCountLabel); + jmp(ptr[rip + retLbl]); // resume execution + + L(retLbl); + dq(a_retAddr); + + L(isFrozen); + jmp(ptr[rip + ifFreezeLabel]); + + L(ifFreezeLabel); + dq(a_ifFreezeLabelAddr); + + } + + }; + static inline void Install() + { + // InstructionExecute + // 1.5.97: 0x141278110: BSScript__Internal__CodeTasklet::VMProcess_141278110 + // 1.6.640: 0x14139C860: BSScript__Internal__CodeTasklet::sub_14139C860 + // 1_5_97 CAVE_START = 0x170 + // 1_6_640 CAVE_START = 0x14C + // 1_5_97 CAVE_END = 0x176 + // 1_6_640 CAVE_END = 0x153 + // Cave start and cave end indicate the beginning and end of the instructions + // We install near the beginning of the loop + // The installation target is the `jz` instruction that jumps if the freeze state is 1 + // CAVE_SIZE = 6 + auto vmprocess_reloc = RELOCATION_ID(98520, 105176); + + //TODO: Find VR offsets, using SE offsets as placeholders + auto cave_start_var_offset = REL::VariantOffset(0xD6, 0xCA, 0xD6); + auto cave_end_var_offset = REL::Offset(cave_start_var_offset.offset() + 6); + + REL::Relocation cave_start_reloc{ vmprocess_reloc, cave_start_var_offset }; + REL::Relocation cave_end_reloc{ vmprocess_reloc, cave_end_var_offset }; + std::size_t CAVE_START = cave_start_var_offset.offset(); + std::size_t CAVE_END = cave_end_var_offset.offset(); + std::size_t CAVE_SIZE = CAVE_END - CAVE_START; + + assert(CAVE_SIZE >= 6); + // we need to read what the offset is in the `jz` instruction; + // jz instruction is opcode (`0F 84`) followed by a four-byte offset + auto if_freeze_label_offset_loc_offset = REL::Offset(cave_start_var_offset.offset() + 2); + REL::Relocation if_freeze_label_offset_loc_addr{ vmprocess_reloc, if_freeze_label_offset_loc_offset }; + uint32_t* ptr_to_offset = (uint32_t*)if_freeze_label_offset_loc_addr.address(); + uint32_t offset_val = *ptr_to_offset; + auto if_freeze_label_offset = REL::Offset(offset_val + cave_end_var_offset.offset()); // the offset relative to the address AFTER the jz instruction + REL::Relocation if_freeze_label_address{ vmprocess_reloc, if_freeze_label_offset }; + + auto patch = Patch(cave_end_reloc.address(), if_freeze_label_address.address()); + auto& trampoline = SKSE::GetTrampoline(); + SKSE::AllocTrampoline(patch.getSize() + 14); + auto result = trampoline.allocate(patch); + trampoline.write_branch<6>(cave_start_reloc.address(), (std::uintptr_t)result); + auto BASE_LOAD_ADDR = vmprocess_reloc.address() - vmprocess_reloc.offset(); + logger::info("Base for executable is: 0x{:X}", BASE_LOAD_ADDR); + logger::info("CodeTasklet::Process address: 0x{:X}", vmprocess_reloc.address()); + logger::info("CodeTasklet::Process relocation offset: 0x{:X}", vmprocess_reloc.offset()); + + logger::info("SetIPPatch installed at address 0x{:X}", cave_start_reloc.address()); + logger::info("SetIPPatch installed at offset 0x{:X}", cave_start_reloc.offset()); + logger::info("SetIPPatch if_freeze_label_offset at address 0x{:X}", if_freeze_label_offset.address()); + logger::info("SetIPPatch if_freeze_label_offset at offset 0x{:X}", if_freeze_label_offset.offset()); + logger::info("SetIPPatch:CAVE_START is 0x{:X}", CAVE_START); + logger::info("SetIPPatch:CAVE_END is 0x{:X}", CAVE_END); + logger::info("SetIPPatch:CAVE_SIZE is 0x{:X}", CAVE_SIZE); + + std::size_t RESULT_ADDR = (std::uintptr_t)result; + logger::info("SetIPPatch patch allocation address: 0x{:X}", RESULT_ADDR); + logger::info("SetIPPatch patch allocation offset: 0x{:X}", RESULT_ADDR - BASE_LOAD_ADDR); + + } + }; struct InstructionExecuteHook { struct Patch : CallPatch @@ -150,7 +278,7 @@ namespace DarkId::Papyrus::DebugServer Xbyak::Label ifgeInstructionDataBitCountLabel; /** * The main issue we are trying to solve is that the InstructionPointer on the top stack frame - * doesn't get updated until the tasklet actually finishes executing + * doesn't get updated until the tasklet actually finishes executing. We need this to be set in order for our subsequent call to work * * It doesn't do this until it either: * - reaches the end of the instruction bitstream @@ -562,5 +690,11 @@ namespace DarkId::Papyrus::DebugServer } #endif + + void EmitBreakpointChangedEvent(const dap::Breakpoint& bpoint, const std::string& what) + { + g_BreakpointChangedEvent(bpoint, what); } + +} } diff --git a/src/DarkId.Papyrus.DebugServer/RuntimeEvents.h b/src/DarkId.Papyrus.DebugServer/RuntimeEvents.h index cf91bf4f..1a338548 100644 --- a/src/DarkId.Papyrus.DebugServer/RuntimeEvents.h +++ b/src/DarkId.Papyrus.DebugServer/RuntimeEvents.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "GameInterfaces.h" @@ -12,19 +13,25 @@ namespace DarkId::Papyrus::DebugServer { - namespace RuntimeEvents - { - EVENT_DECLARATION(InstructionExecution, void(RE::BSScript::Internal::CodeTasklet*, uint32_t actualIP)) - EVENT_DECLARATION(CreateStack, void(RE::BSTSmartPointer&)) - EVENT_DECLARATION(CleanupStack, void(uint32_t)) - // EVENT_DECLARATION(InitScript, void(RE::TESInitScriptEvent*)) - EVENT_DECLARATION(Log, void(const RE::BSScript::LogEvent*)) - - namespace Internal - { - void CommitHooks(); - } - } + namespace RuntimeEvents + { + EVENT_DECLARATION(InstructionExecution, void(RE::BSScript::Internal::CodeTasklet*)) + EVENT_DECLARATION(CreateStack, void(RE::BSTSmartPointer&)) + EVENT_DECLARATION(CleanupStack, void(uint32_t)) + // EVENT_DECLARATION(InitScript, void(RE::TESInitScriptEvent*)) + EVENT_DECLARATION(Log, void(const RE::BSScript::LogEvent*)) + EVENT_DECLARATION(BreakpointChanged, void(const dap::Breakpoint& bpoint, const std::string&)) + + + + // TODO: Refactor this + void EmitBreakpointChangedEvent(const dap::Breakpoint &bpoint, const std::string& what); + namespace Internal + { + void CommitHooks(); + } + + } } #undef EVENT_DECLARATION \ No newline at end of file diff --git a/src/DarkId.Papyrus.DebugServer/Window.cpp b/src/DarkId.Papyrus.DebugServer/Window.cpp index 9df2fc87..efc5b2f6 100644 --- a/src/DarkId.Papyrus.DebugServer/Window.cpp +++ b/src/DarkId.Papyrus.DebugServer/Window.cpp @@ -61,7 +61,11 @@ static BOOL is_main_window(HWND handle) ShowWindow(handle, SW_SHOW); SetForegroundWindow(handle); SetFocus(handle); + SetActiveWindow(handle); #if SKYRIM // Need to reinit mouse since we lost it + if (!RE::Main::GetSingleton()->gameActive) { + RE::Main::GetSingleton()->SetActive(true); + } RE::MenuCursor::GetSingleton()->SetCursorVisibility(false); auto deviceManager = RE::BSInputDeviceManager::GetSingleton(); deviceManager->ReinitializeMouse(); diff --git a/src/DarkId.Papyrus.DebugServer/pdsPCH.h b/src/DarkId.Papyrus.DebugServer/pdsPCH.h index d34f1fee..54a94790 100644 --- a/src/DarkId.Papyrus.DebugServer/pdsPCH.h +++ b/src/DarkId.Papyrus.DebugServer/pdsPCH.h @@ -44,13 +44,22 @@ namespace XSE = F4SE; namespace stl { using namespace XSE::stl; - template + template void write_thunk_call(std::uintptr_t a_src) { auto& trampoline = XSE::GetTrampoline(); XSE::AllocTrampoline(14); - T::func = trampoline.write_call<5>(a_src, T::thunk); + T::func = trampoline.write_call(a_src, T::thunk); + } + + template + void write_thunk_branch(std::uintptr_t a_src) + { + auto& trampoline = XSE::GetTrampoline(); + XSE::AllocTrampoline(14); + + T::func = trampoline.write_branch(a_src, T::thunk); } }