Skip to content

Commit

Permalink
revamp breakpoints, add invalidation
Browse files Browse the repository at this point in the history
  • Loading branch information
nikitalita committed Sep 1, 2023
1 parent 6b91b4b commit f21d9e8
Show file tree
Hide file tree
Showing 17 changed files with 507 additions and 54 deletions.
159 changes: 139 additions & 20 deletions src/DarkId.Papyrus.DebugServer/BreakpointManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,22 @@
#include <Champollion/Pex/Binary.hpp>
#include <regex>
#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<dap::SetBreakpointsResponse> BreakpointManager::SetBreakpoints(const dap::Source& source, const std::vector<dap::SourceBreakpoint>& srcBreakpoints)
{
dap::SetBreakpointsResponse response;
Expand All @@ -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
Expand All @@ -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<int, BreakpointInfo> foundBreakpoints;

for (const auto& srcBreakpoint : srcBreakpoints)
{
auto foundLine = false;
int line = static_cast<int>(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<int>(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<dap::integer>(),
.instructionReference = foundLine ? GetInstructionReference(debugfinfo) : dap::optional<dap::string>(),
.line = dap::integer(line),
.offset = foundLine ? dap::integer(instructionNum) : dap::optional<dap::integer>(),
.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_t> 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<RE::BSScript::Internal::ScriptFunction*>(_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;
Expand All @@ -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<RE::BSScript::Internal::ScriptFunction*>(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;
}
}
27 changes: 23 additions & 4 deletions src/DarkId.Papyrus.DebugServer/BreakpointManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,36 @@ namespace DarkId::Papyrus::DebugServer
{
class BreakpointManager
{
std::map<int, std::set<int>> 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<int, BreakpointInfo> breakpoints;

};

explicit BreakpointManager(PexCache* pexCache)
: m_pexCache(pexCache)
{
}

dap::ResponseOrError<dap::SetBreakpointsResponse> SetBreakpoints(const dap::Source& src, const std::vector<dap::SourceBreakpoint>& 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<int, ScriptBreakpoints> m_breakpoints;

};
}
100 changes: 99 additions & 1 deletion src/DarkId.Papyrus.DebugServer/ConfigHooks.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -61,10 +78,91 @@ namespace DarkId::Papyrus::DebugServer
static inline void Install()
{
REL::Relocation<std::uintptr_t> target = getHookTarget();
stl::write_thunk_call<EnableLoadDebugInformation>(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<decltype(thunk)> func;

static inline void Install() {
auto initializeVtableEntry = RE::BSWin32MouseDevice::VTABLE[0].address() + 8;
REL::Relocation<std::uintptr_t> initializeLoc{ *(uintptr_t*)initializeVtableEntry };
overridemouseinitialize::func = initializeLoc.address();
REL::safe_write<std::uintptr_t>(initializeVtableEntry, stl::unrestricted_cast<std::uintptr_t>(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<IniSettingCollection> 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<decltype(thunk)> 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<std::uintptr_t> 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;
}
};
}
Loading

0 comments on commit f21d9e8

Please sign in to comment.