AJMansfield How do I cheat?
Reputation: 0
Joined: 24 Jun 2026 Posts: 3
|
Posted: Tue Jun 30, 2026 7:14 am Post subject: Lua Breakpoints for Pausing on File Open/Close |
|
|
Ended up developing out the script in my previous post into a more polished and versatile form, and wanted to share with the community in case someone else ever has a similar need.
The purpose of this script is to log and optionally pause execution when the attached game opens or closes a file with a filename that matches a relevant pattern, e.g. so that you can trigger something like Branch Mapper, without having it slow down the UI interaction needed to actually get a game to start loading things --- to make it easier to troubleshoot behaviors that occur while loading a save file or modded asset.
This works by attaching a scripted breakpoint to the underlying ntdll NtCreateFile and NtClose low-level API functions; with a bit of hacked-together data structure traversal and bookkeeping to capture both the relevant arguments and the return values. This should in theory also collect usages of the higher level kernel32 API functions, though I've not tested this. With some modification this could probably also be made to collect calls to the equivalent 64-bit API; that's left as an exercise for the viewer.
Here's the code, hopefully someone finds this useful:
| Code: | --- Copyright (C) 2026 Anson Mansfield
--- Released under the MIT License.
--- SPDX-License-Identifier: MIT
---@class FileTracker
FileTracker = {}
--- Filename patterns to track, and their logging/pause behaviors.
--- @type {pattern: string, on_open: integer?, on_opened: integer?, on_close: integer?}[]
FileTracker.track = {
{pattern = "%.png$", on_open = 1, on_close = 0}, --- log open for PNG images
{pattern = "%.vmf$", on_open = 2, on_close = 1}, --- pause on open / log close for valve map files
{pattern = "%.ogg$", on_open = 1, on_close = 1}, --- log open/close for OGG sound effects
{pattern = "$", on_open = 1, on_close = 0}, -- log every file opened
}
--- Table from currently-open file handles to their filenames.
--- @type {[integer]: string}
FileTracker.open = {}
--- Table from pending return breakpoint addresses to the corresponding captured arguments from the call.
--- @type {[integer]: {phandle: integer, filename: string}}
FileTracker.pendingOpen = {}
--- @protected
function FileTracker.onNtCreateFileCall()
local returnAddress = readInteger(ESP)
--- arg 1, out pointer to handle
local phandle = readInteger(ESP + 0x04)
if phandle == 0 then
return 0
end
--- arg 3, in pointer to object attribute struct
local objectAttributes = readInteger(ESP + 0x0C)
if objectAttributes == 0 then
return 0
end
local objectName = readInteger(objectAttributes + 0x08)
if objectName == 0 then
return 0
end
local length = readSmallInteger(objectName)
local buffer = readInteger(objectName + 0x04)
if buffer == 0 then
return 0
end
local filename = readString(buffer, length, true)
for _, entry in ipairs(FileTracker.track) do
if string.find(filename, entry.pattern) then
FileTracker.pendingOpen[returnAddress] = {
phandle = phandle,
filename = filename,
}
debug_setBreakpoint(returnAddress, FileTracker.onNtCreateFileReturn)
break
end
end
return 0
end
--- @protected
function FileTracker.onNtCreateFileReturn()
local pending = FileTracker.pendingOpen[EIP]
if pending == nil then
return 0
end
FileTracker.pendingOpen[EIP] = nil
debug_removeBreakpoint(EIP)
local filename = pending.filename
local phandle = pending.phandle
local result
if EAX ~= 0 then
result = string.format("<errno %d>", EAX)
else
local handle = readInteger(phandle)
FileTracker.open[handle] = filename
result = string.format("%d", handle)
end
for _, entry in ipairs(FileTracker.track) do
if string.find(filename, entry.pattern) then
FileTracker.modeBehavior(
entry.on_open,
string.format("open(\"%s\") -> %s", filename, result)
)
break
end
end
return 0
end
--- @protected
function FileTracker.onNtCloseCall()
local handle = readInteger(ESP + 0x04)
local filename = FileTracker.open[handle]
if filename == nil then
return 0
end
FileTracker.open[handle] = nil
for _, entry in ipairs(FileTracker.track) do
if string.find(filename, entry.pattern) then
FileTracker.modeBehavior(
entry.on_close,
string.format("close(%d) -- \"%s\"", handle, filename)
)
break
end
end
return 0
end
--- @protected
function FileTracker.modeBehavior(modeValue, message)
if modeValue >= 1 then
print(message)
end
if modeValue >= 2 then
pause()
print("pause() -- resume with unpause()")
end
end
--- @public
function FileTracker.begin()
debug_setBreakpoint(getAddress("ntdll.NtClose"), FileTracker.onNtCloseCall)
debug_setBreakpoint(getAddress("ntdll.NtCreateFile"), FileTracker.onNtCreateFileCall)
end
FileTracker.begin()
|
|
|