--[[ Multi-Emulator Auto Address Range + Auto Symbol Entry Creator Features: - Automatically detects PCSX2, Cemu, ePSXe, DOSBox, PPSSPP, DuckStation, MelonDS, DeSmuME, RMG (N64), mupen64 (N64) - Automatically sets scan range and relative base - Automatically adds address entries with [AUTO] prefix - Memory view right-click menu: "Copy relative offset to clipboard" (Ctrl+]) - Memory view right-click menu: "Add selected address's relative offset to address list" - Found list popup menu: "Add relative offset of selected addresses to address list" (multi-select supported, visible only when relative base is enabled) Automatically created entries are prefixed with [AUTO] and are cleaned up on process switch (only top-level entries are removed). Misc: This Lua script is AI-driven. DOSBox module based on CE author's script: https://forum.cheatengine.org/viewtopic.php?t=624001 PPSSPP module based on ppsspp_WM.ahk source. Cemu module based on: https://forum.cheatengine.org/viewtopic.php?p=5781484 PCSX2 module based on: https://forums.pcsx2.net/Thread-PCSX2-1-7-Cheat-Engine-Script-Compatibility DuckStation module based on official built-in symbol RAM. MelonDS / DeSmuME modules based on standalone NDS script v2.3 (signature + pointer offset +8). RMG (N64) module based on mupen64plus.dll exported function DebugMemGetPointer, using a background thread to prevent freezing. mupen64 (N64) module based on exported symbol CORE_RDRAM, directly obtains base address. Ver 1.5.1 (Fix: timer cleanup moved to top; cleaned old globals; added NDS_RAM_SIZE constant; extracted some magic numbers) ]] ------------------------------------------------------------------------------- -- 0. Clean up old version leftovers (placed at the very top) ------------------------------------------------------------------------------- -- Clean old global variables (prevent residual interference) if ppssppTimer then if ppssppTimer.Enabled then ppssppTimer.Enabled = false end ppssppTimer.destroy() ppssppTimer = nil end if foundListMenuTimer then if foundListMenuTimer.Enabled then foundListMenuTimer.Enabled = false end foundListMenuTimer.destroy() foundListMenuTimer = nil end -- Clean old menu item globals (if they exist) if miCopyRelativeAddress then miCopyRelativeAddress.destroy() miCopyRelativeAddress = nil end if miAddRelativeOffset then miAddRelativeOffset.destroy() miAddRelativeOffset = nil end if miAddRelativeOffsetFromFoundList then miAddRelativeOffsetFromFoundList.destroy() miAddRelativeOffsetFromFoundList = nil end -- Reset counters ppsspp_AttemptCount = nil foundListMenuAttemptCount = nil ------------------------------------------------------------------------------- -- 1. Constants ------------------------------------------------------------------------------- local CONSTANTS = { -- Memory sizes PPSSPP_RAM_SIZE = 0x2000000, -- 32MB N64_RAM_SIZE = 0x400000, -- 4MB (expanded to 0x800000) NDS_RAM_SIZE = 0x400000, -- 4MB (NDS main RAM) DOSBOX_RAM_SIZE = 0x1000000, -- 16MB DUCKSTATION_RAM_SIZE = 0x200000, -- 2MB EPSXE_RAM_SIZE = 0x200000, -- 2MB PCSX2_RAM_SIZE = 0x20000000, -- 512MB CEMU_RAM_SIZE = 0x20000000, -- 512MB -- Offsets DOSBOX_BASE_OFFSET = 0xFE00E, PPSSPP_RAM_OFFSET = 0x8800000, -- PPSSPP window message PPSSPP_WM_USER_GET_BASE_POINTER = 0xB118, -- PPSSPP retry parameters PPSSPP_MAX_ATTEMPTS = 10, PPSSPP_RETRY_INTERVAL = 2000, -- Other constants BIT32_MASK = 0x100000000, -- For combining 32-bit high/low values -- Menu marker AUTO_PREFIX = "[AUTO] ", } ------------------------------------------------------------------------------- -- 2. User configuration ------------------------------------------------------------------------------- local SHOW_MEMORY_VIEW_ON_LOAD = false ------------------------------------------------------------------------------- -- 3. Module state table (replaces global variables) ------------------------------------------------------------------------------- local M = { mv = nil, -- Memory view window ppssppTimer = nil, -- PPSSPP timer ppsspp_Configured = false, -- Whether PPSSPP has been configured ppsspp_AttemptCount = 0, -- PPSSPP retry counter foundListMenuTimer = nil, -- Found list menu timer foundListMenuAttemptCount = 0, -- Found list menu attempt counter applyTimer = nil, -- Delayed apply timer oldOnProcessOpened = nil, -- Old callback chain miCopyRelativeAddress = nil, -- Menu item references miAddRelativeOffset = nil, miAddRelativeOffsetFromFoundList = nil, } ------------------------------------------------------------------------------- -- 4. Safe helper functions ------------------------------------------------------------------------------- local function safeGetAddress(name) local ok, val = pcall(getAddress, name) if ok and val and val ~= 0 then return val end return nil end ------------------------------------------------------------------------------- -- 5. Ensure memory view window exists ------------------------------------------------------------------------------- M.mv = getMemoryViewForm() if M.mv == nil then M.mv = createMemoryView() end ------------------------------------------------------------------------------- -- 6. Manage automatically added address entries (with [AUTO] prefix) ------------------------------------------------------------------------------- local function removeScriptedSymbols() local addrList = getAddressList() if not addrList then return end local toRemove = {} for i = 0, addrList.Count - 1 do local rec = addrList.getMemoryRecord(i) if rec.Description and rec.Description:find(CONSTANTS.AUTO_PREFIX, 1, true) == 1 then if not rec.Parent then table.insert(toRemove, rec) end end end for _, rec in ipairs(toRemove) do rec.delete() end end local function addSymbolToAddressList(addressExpr, displayName) local addrList = getAddressList() if not addrList then return false end for i = 0, addrList.Count - 1 do local rec = addrList.getMemoryRecord(i) if rec.Address == addressExpr then return false end end local rec = addrList.createMemoryRecord() rec.Address = addressExpr rec.Description = CONSTANTS.AUTO_PREFIX .. displayName rec.ShowAsHex = true return true end local function clearEphemeralSymbols() unregisterSymbol("ePSXe_x32") unregisterSymbol("dosbox_base") unregisterSymbol("baseAddress") unregisterSymbol("PPSSPP_BasePtr") unregisterSymbol("melonDS64") unregisterSymbol("DeSmuMEx32") unregisterSymbol("RDRAM") end local function applyBaseAddress(fromAddress, toAddress, baseValue, addressExpr, displayName) MainForm.FromAddress.Text = fromAddress MainForm.ToAddress.Text = toAddress local mvhv = M.mv.HexadecimalView mvhv.UseRelativeBase = true mvhv.RelativeBase = baseValue mvhv.Address = baseValue if SHOW_MEMORY_VIEW_ON_LOAD then M.mv.show() end if addressExpr and displayName then addSymbolToAddressList(addressExpr, displayName) end end ------------------------------------------------------------------------------- -- 7. Emulator address range getters (table driven) ------------------------------------------------------------------------------- local TARGET_PROCESSES = {} TARGET_PROCESSES["pcsx2"] = function() local baseAddress = readQword("EEmem") if baseAddress then return string.format("%016X", baseAddress), string.format("%016X", baseAddress + CONSTANTS.PCSX2_RAM_SIZE) end return end TARGET_PROCESSES["cemu"] = function() local success, baseAddress = pcall(function() return executeCode(safeGetAddress("Cemu.memory_getBase"), 0, 2000) end) if success and baseAddress then registerSymbol("baseAddress", baseAddress) return string.format("%016X", baseAddress), string.format("%016X", baseAddress + CONSTANTS.CEMU_RAM_SIZE) end return end TARGET_PROCESSES["epsxe"] = function() local aobPattern = "81E1FFFF1F0081C1" local foundAddress = AOBScan(aobPattern) if foundAddress then local address = tonumber(foundAddress[0], 16) + 8 registerSymbol("ePSXe_x32", address) local value = readInteger(address) foundAddress:destroy() if value then return string.format("%016X", value), string.format("%016X", value + CONSTANTS.EPSXE_RAM_SIZE) end end return end TARGET_PROCESSES["dosbox"] = function() local ms = createMemScan() ms.ScanValue = 'IBM COMPATIBLE' ms.VarType = vtString ms.CaseSensitive = true ms.ScanWritable = 'scanInclude' ms.ScanExecutable = 'scanExclude' ms.ScanCopyOnWrite = 'scanExclude' ms.scan() local r = ms.Results ms.destroy() if not r or #r == 0 then return end if #r > 1 then print("[DOS] Warning: multiple addresses found, count=" .. #r .. ", cannot determine unique base, skipping.") return end local chosenAddr = r[1] local baseAddr = chosenAddr - CONSTANTS.DOSBOX_BASE_OFFSET registerSymbol('dosbox_base', baseAddr) return string.format("%016X", baseAddr), string.format("%016X", baseAddr + CONSTANTS.DOSBOX_RAM_SIZE) end TARGET_PROCESSES["duckstation"] = function() local baseAddress = readQword("RAM") if baseAddress then return string.format("%016X", baseAddress), string.format("%016X", baseAddress + CONSTANTS.DUCKSTATION_RAM_SIZE) end return end TARGET_PROCESSES["mupen64"] = function() local baseAddress = safeGetAddress("CORE_RDRAM") if baseAddress then return string.format("%016X", baseAddress), string.format("%016X", baseAddress + CONSTANTS.N64_RAM_SIZE) end return end ------------------------------------------------------------------------------- -- 8. PPSSPP configuration ------------------------------------------------------------------------------- local PPSSPP_WINDOW_CLASS = "PPSSPPWnd" local function getPPSSPPModuleInfo() local moduleList = enumModules() for _, mod in ipairs(moduleList) do if string.match(mod.Name:lower(), "^ppsspp.*%.exe$") then return mod.Name, mod.Address end end return nil, nil end local function configurePPSSPP() local hwnd = findWindow(PPSSPP_WINDOW_CLASS) if hwnd == nil then return false end local currentPid = getOpenedProcessID() local targetPid = getWindowProcessID(hwnd) if currentPid ~= targetPid then return false end local moduleName, processBase = getPPSSPPModuleInfo() if moduleName == nil or processBase == 0 then return false end local function sendWMs(lParam) return sendMessage(hwnd, CONSTANTS.PPSSPP_WM_USER_GET_BASE_POINTER, 0, lParam) end local reply0 = sendWMs(0) local reply2 = sendWMs(2) if type(reply0) ~= "number" or type(reply2) ~= "number" then return false end local reply1, reply3 if targetIs64Bit() then reply1 = sendWMs(1) reply3 = sendWMs(3) if type(reply1) ~= "number" or type(reply3) ~= "number" then return false end else reply1, reply3 = 0, 0 end local baseAddr = (reply1 * CONSTANTS.BIT32_MASK) + reply0 local basePtr = (reply3 * CONSTANTS.BIT32_MASK) + reply2 if baseAddr == 0 or basePtr == 0 then return false end local offset = basePtr - processBase if offset < 0 then return false end local actualBase = baseAddr + CONSTANTS.PPSSPP_RAM_OFFSET removeScriptedSymbols() clearEphemeralSymbols() local fromAddress = string.format("%016X", actualBase) local toAddress = string.format("%016X", actualBase + CONSTANTS.PPSSPP_RAM_SIZE) local addrExpr = string.format("[%s+%X]+%X", moduleName, offset, CONSTANTS.PPSSPP_RAM_OFFSET) applyBaseAddress(fromAddress, toAddress, actualBase, addrExpr, "PPSSPP Memory") registerSymbol("PPSSPP_BasePtr", basePtr) return true end local function ppssppTimerCallback() if not M.ppsspp_Configured then M.ppsspp_AttemptCount = M.ppsspp_AttemptCount + 1 if M.ppsspp_AttemptCount > CONSTANTS.PPSSPP_MAX_ATTEMPTS then if M.ppssppTimer then M.ppssppTimer.Enabled = false M.ppssppTimer.destroy() M.ppssppTimer = nil end return end if configurePPSSPP() then M.ppsspp_Configured = true if M.ppssppTimer then M.ppssppTimer.Enabled = false M.ppssppTimer.destroy() M.ppssppTimer = nil end end else if M.ppssppTimer then M.ppssppTimer.Enabled = false M.ppssppTimer.destroy() M.ppssppTimer = nil end end end ------------------------------------------------------------------------------- -- 9. NDS emulator specific functions ------------------------------------------------------------------------------- local function isMelonDSProcess() local moduleList = enumModules() if not moduleList then return false end for _, mod in ipairs(moduleList) do local name = mod.Name or "" if name:lower():find("melonds") then return true end end return false end local function isDesmumeProcess() local moduleList = enumModules() if not moduleList then return false end for _, mod in ipairs(moduleList) do local name = mod.Name or "" if name:lower():find("desmume") then return true end end return false end local function getMelonDSMainRAMStart() local pattern = "FF DE FF E7 FF DE FF E7 FF DE FF" local results = AOBScan(pattern) if results == nil then return nil end local count = results.Count if count == 0 then results.destroy(); return nil end local found = nil for i = 0, count - 1 do local addrStr = results[i] local addrNum = tonumber(addrStr, 16) if addrNum ~= nil then local low16 = addrNum & 0xFFFF if low16 == 0 then found = addrNum break end end end results.destroy() return found end local function getDesmumeMainRAMStart() local pattern = "01 00 00 00 00 00 40 00" local results = AOBScan(pattern) if results == nil then return nil end local count = results.Count if count == 0 then results.destroy(); return nil end local addrStr = results[0] results.destroy() return tonumber(addrStr, 16) end local function updateRelativeBase(baseAddr) if M.mv and M.mv.HexadecimalView then if baseAddr then M.mv.HexadecimalView.UseRelativeBase = true M.mv.HexadecimalView.RelativeBase = baseAddr M.mv.HexadecimalView.Address = baseAddr if SHOW_MEMORY_VIEW_ON_LOAD then M.mv.show() end else M.mv.HexadecimalView.UseRelativeBase = false end end end local function handleMelonDS() clearEphemeralSymbols() removeScriptedSymbols() local startAddr = getMelonDSMainRAMStart() if startAddr then local endAddr = startAddr + CONSTANTS.NDS_RAM_SIZE local fromStr = string.format("%016X", startAddr) local toStr = string.format("%016X", endAddr) applyBaseAddress(fromStr, toStr, startAddr, "melonDS64", "MelonDS Base") registerSymbol("melonDS64", startAddr) else unregisterSymbol("melonDS64") updateRelativeBase(nil) print("MelonDS process detected, but failed to locate main RAM start address (game not loaded or signature not found).") end end local function handleDesmume() clearEphemeralSymbols() removeScriptedSymbols() if targetIs64Bit() then print("DeSmuME 64-bit version is not supported (signature only works for 32-bit), skipping setup.") updateRelativeBase(nil) unregisterSymbol("DeSmuMEx32") return end local aobAddr = getDesmumeMainRAMStart() if aobAddr then local ptrAddr = aobAddr + 8 local baseAddr = readInteger(ptrAddr) if baseAddr == nil or baseAddr == 0 then updateRelativeBase(nil) unregisterSymbol("DeSmuMEx32") return end local endAddr = baseAddr + CONSTANTS.NDS_RAM_SIZE local fromStr = string.format("%016X", baseAddr) local toStr = string.format("%016X", endAddr) applyBaseAddress(fromStr, toStr, baseAddr, "[DeSmuMEx32]", "DeSmuME Base") registerSymbol("DeSmuMEx32", ptrAddr) else unregisterSymbol("DeSmuMEx32") updateRelativeBase(nil) print("DeSmuME process detected, but signature not found (game not loaded or signature outdated).") end end ------------------------------------------------------------------------------- -- 10. N64 emulator specific functions (RMG + mupen64) ------------------------------------------------------------------------------- local function isRMGProcess() if safeGetAddress("mupen64plus.dll") then return true end if safeGetAddress("DebugMemGetPointer") then return true end local moduleList = enumModules() if moduleList then for _, mod in ipairs(moduleList) do if mod.Name:lower():find("rmg") or mod.Name:lower():find("mupen64plus") then return true end end end return false end local function setupN64() local funcAddr = safeGetAddress("DebugMemGetPointer") if not funcAddr then return end if isPaused() then unpause() sleep(50) end local success, base = pcall(executeCodeEx, 0, 3000, funcAddr, {type=0, value=1}) if success and base and base ~= 0 then synchronize(function() registerSymbol("RDRAM", base) local fromStr = string.format("%016X", base) local toStr = string.format("%016X", base + CONSTANTS.N64_RAM_SIZE) applyBaseAddress(fromStr, toStr, base, "RDRAM", "N64 RDRAM") end) else print("[N64] Failed to get base address: " .. tostring(base or "pcall exception")) end end local function handleRMG() clearEphemeralSymbols() removeScriptedSymbols() local thread = createThread(function() setupN64() end) thread.Name = "N64 auto base getter" -- Optional: keep a reference to prevent it from being GC'd (but CE already holds it) -- M.rmgThread = thread end ------------------------------------------------------------------------------- -- 11. Emulator detection dispatcher ------------------------------------------------------------------------------- local function getProcessType() local moduleList = enumModules() local processName = nil for _, module in ipairs(moduleList) do local name = module.Name:lower() if name:match("%.exe$") then processName = name:gsub("%.exe$", "") break end end if not processName then return nil end if processName:match("^pcsx2") then return "pcsx2" end if processName:match("^cemu") then return "cemu" end if processName:match("^epsxe") then return "epsxe" end if processName:match("dosbox") then return "dosbox" end if processName:match("^duckstation") then return "duckstation" end if processName:match("^mupen64") then return "mupen64" end return nil end ------------------------------------------------------------------------------- -- 12. Main apply function ------------------------------------------------------------------------------- local function applyEmulatorSettings(pid, handle, caption) if not M.mv then M.mv = getMemoryViewForm() if not M.mv then return end end -- 1. NDS if isMelonDSProcess() then handleMelonDS() return elseif isDesmumeProcess() then handleDesmume() return end -- 2. PPSSPP local hwnd = findWindow(PPSSPP_WINDOW_CLASS) if hwnd ~= nil and getWindowProcessID(hwnd) == pid then M.ppsspp_Configured = false if configurePPSSPP() then M.ppsspp_Configured = true if M.ppssppTimer then M.ppssppTimer.Enabled = false M.ppssppTimer.destroy() M.ppssppTimer = nil end else if not M.ppssppTimer then M.ppssppTimer = createTimer(nil, false) M.ppssppTimer.Interval = CONSTANTS.PPSSPP_RETRY_INTERVAL M.ppssppTimer.OnTimer = ppssppTimerCallback end M.ppsspp_AttemptCount = 0 M.ppssppTimer.Enabled = true end return end -- 3. RMG (detect first) if isRMGProcess() then clearEphemeralSymbols() removeScriptedSymbols() handleRMG() return end -- 4. Other emulators local processType = getProcessType() if processType then clearEphemeralSymbols() removeScriptedSymbols() local fromAddress, toAddress = TARGET_PROCESSES[processType]() if fromAddress and toAddress then local baseVal = tonumber(fromAddress, 16) local addrExpr, displayName local mapping = { dosbox = { "dosbox_base", "DOSBox Base" }, epsxe = { "[ePSXe_x32]", "ePSXe Base" }, pcsx2 = { "[EEmem]", "PCSX2 EEmem" }, cemu = { "baseAddress", "Cemu Base" }, duckstation = { "[RAM]", "DuckStation RAM" }, mupen64 = { "CORE_RDRAM", "N64 RDRAM" }, } local info = mapping[processType] if info then addrExpr, displayName = info[1], info[2] applyBaseAddress(fromAddress, toAddress, baseVal, addrExpr, displayName) else -- fallback MainForm.FromAddress.Text = fromAddress MainForm.ToAddress.Text = toAddress M.mv.HexadecimalView.UseRelativeBase = true M.mv.HexadecimalView.RelativeBase = baseVal M.mv.HexadecimalView.Address = baseVal if SHOW_MEMORY_VIEW_ON_LOAD then M.mv.show() end end end else -- No known emulator matched, reset to full range MainForm.FromAddress.Text = "0000000000000000" MainForm.ToAddress.Text = "7fffffffffffffff" M.mv.HexadecimalView.UseRelativeBase = false clearEphemeralSymbols() removeScriptedSymbols() end end ------------------------------------------------------------------------------- -- 13. OnProcessOpened ------------------------------------------------------------------------------- M.oldOnProcessOpened = MainForm.OnProcessOpened MainForm.OnProcessOpened = function(pid, handle, caption) if M.oldOnProcessOpened then pcall(M.oldOnProcessOpened, pid, handle, caption) end if M.applyTimer then M.applyTimer.Enabled = false M.applyTimer.destroy() M.applyTimer = nil end M.applyTimer = createTimer(nil, false) M.applyTimer.Interval = 150 M.applyTimer.OnTimer = function() M.applyTimer.Enabled = false M.applyTimer.destroy() M.applyTimer = nil applyEmulatorSettings(pid, handle, caption) end M.applyTimer.Enabled = true end ------------------------------------------------------------------------------- -- 14. Execute once on script load ------------------------------------------------------------------------------- local currentPid = getOpenedProcessID() if currentPid ~= 0 then local timer = createTimer(nil, false) timer.Interval = 200 timer.OnTimer = function() timer.Enabled = false timer.destroy() applyEmulatorSettings(currentPid, getOpenedProcessHandle(), "") end timer.Enabled = true end ------------------------------------------------------------------------------- -- 15. Memory view right-click menu ------------------------------------------------------------------------------- local function addRelativeOffsetToAddressList() local hv = M.mv.HexadecimalView if not hv or not hv.UseRelativeBase then return end local base = hv.RelativeBase local selStart = hv.SelectionStart if selStart == 0 then return end local offset = selStart - base local offsetStr = offset >= 0 and string.format("+%x", offset) or string.format("-%x", -offset) addSymbolToAddressList(offsetStr, offsetStr) end -- Clean up old menu item references if M.miCopyRelativeAddress then M.miCopyRelativeAddress.destroy() M.miCopyRelativeAddress = nil end if M.miAddRelativeOffset then M.miAddRelativeOffset.destroy() M.miAddRelativeOffset = nil end M.miCopyRelativeAddress = createMenuItem(M.mv.memorypopup) M.miCopyRelativeAddress.Name = "miCopyRelativeAddress" M.miCopyRelativeAddress.Caption = "Copy relative offset to clipboard" M.miCopyRelativeAddress.ShortCut = textToShortCut("Ctrl+]") M.miCopyRelativeAddress.OnClick = function() local hv = M.mv.HexadecimalView if hv.UseRelativeBase then local s if hv.SelectionStart >= hv.RelativeBase then s = string.format("+%x", hv.SelectionStart - hv.RelativeBase) else s = string.format("-%x", hv.RelativeBase - hv.SelectionStart) end if hv.SelectionStop ~= hv.SelectionStart then if hv.SelectionStop >= hv.RelativeBase then s = string.format("%s to +%x", s, hv.SelectionStop - hv.RelativeBase) else s = string.format("%s to -%x", s, hv.RelativeBase - hv.SelectionStop) end end writeToClipboard(s) end end M.miAddRelativeOffset = createMenuItem(M.mv.memorypopup) M.miAddRelativeOffset.Name = "miAddRelativeOffset" M.miAddRelativeOffset.Caption = "Add selected address's relative offset to address list" M.miAddRelativeOffset.OnClick = addRelativeOffsetToAddressList local insertIndex = -1 for i = 0, M.mv.memorypopup.Items.Count - 1 do local item = M.mv.memorypopup.Items[i] if item.Name == "Addthisaddresstothelist1" then insertIndex = i break end end if insertIndex == -1 then for i = 0, M.mv.memorypopup.Items.Count - 1 do local item = M.mv.memorypopup.Items[i] if item.Caption == "Add this address to the list" or item.Caption == "Add this address to the list..." then insertIndex = i break end end end if insertIndex ~= -1 then M.mv.memorypopup.Items.Insert(insertIndex + 1, M.miCopyRelativeAddress) M.mv.memorypopup.Items.Insert(insertIndex + 2, M.miAddRelativeOffset) else M.mv.memorypopup.Items.Add(M.miCopyRelativeAddress) M.mv.memorypopup.Items.Add(M.miAddRelativeOffset) end local oldPopup = M.mv.memorypopup.OnPopup or function() end M.mv.memorypopup.OnPopup = function(s) local visible = M.mv.HexadecimalView.UseRelativeBase if M.miCopyRelativeAddress then M.miCopyRelativeAddress.Visible = visible end if M.miAddRelativeOffset then M.miAddRelativeOffset.Visible = visible end return oldPopup(s) end ------------------------------------------------------------------------------- -- 16. Found list popup menu ------------------------------------------------------------------------------- local function addRelativeOffsetFromFoundList() local lv = getMainForm().Foundlist3 if not lv then messageDialog("Found list (Foundlist3) not found", mtError, mbOK) return end local hv = M.mv and M.mv.HexadecimalView if not hv or not hv.UseRelativeBase then messageDialog("Relative base is not enabled. Please attach a supported emulator to set the base automatically.", mtWarning, mbOK) return end local base = hv.RelativeBase local count = 0 -- Use pcall to protect address parsing from malformed strings crashing for i = 0, lv.Items.Count - 1 do local item = lv.Items[i] if item and item.Selected then local addrStr = item.Caption if addrStr and addrStr ~= "" then local ok, addrNum = pcall(tonumber, addrStr:match("^([^:]+)") or addrStr, 16) if ok and addrNum then local offset = addrNum - base local offsetStr = offset >= 0 and string.format("+%x", offset) or string.format("-%x", -offset) addSymbolToAddressList(offsetStr, offsetStr) count = count + 1 end end end end if count > 0 then messageDialog(string.format("Successfully added %d relative offset entries to the address list.", count), mtInformation, mbOK) else messageDialog("No valid addresses selected, or selected addresses could not be parsed.", mtWarning, mbOK) end end local function setupFoundListMenu() local popup = getMainForm().foundlistpopup if not popup then return false end for i = 0, popup.Items.Count - 1 do if popup.Items[i].Caption == "Add relative offset of selected addresses to address list" then M.miAddRelativeOffsetFromFoundList = popup.Items[i] return true end end local item = createMenuItem(popup) item.Name = "miAddRelativeOffsetFromFoundList" item.Caption = "Add relative offset of selected addresses to address list" item.OnClick = addRelativeOffsetFromFoundList item.Visible = false popup.Items.Insert(0, item) local sep = createMenuItem(popup) sep.Name = "sepFoundListRelativeOffset" sep.Caption = "-" sep.Visible = false popup.Items.Insert(1, sep) M.miAddRelativeOffsetFromFoundList = item local oldPopup = popup.OnPopup or function() end popup.OnPopup = function(sender) oldPopup(sender) if M.miAddRelativeOffsetFromFoundList then local visible = M.mv and M.mv.HexadecimalView and M.mv.HexadecimalView.UseRelativeBase or false M.miAddRelativeOffsetFromFoundList.Visible = visible -- Find the separator by name for i = 0, popup.Items.Count - 1 do local sepItem = popup.Items[i] if sepItem and sepItem.Name == "sepFoundListRelativeOffset" then sepItem.Visible = visible break end end end end return true end M.foundListMenuTimer = createTimer(nil, false) M.foundListMenuTimer.Interval = 300 M.foundListMenuTimer.OnTimer = function(timer) M.foundListMenuAttemptCount = M.foundListMenuAttemptCount + 1 if setupFoundListMenu() then timer.Enabled = false timer.destroy() M.foundListMenuTimer = nil elseif M.foundListMenuAttemptCount >= 50 then timer.Enabled = false timer.destroy() M.foundListMenuTimer = nil end end M.foundListMenuTimer.Enabled = true