--[[ Multi-Emulator Auto Address Range + Auto Symbol Entry Creation Features: - Automatically detects PCSX2, Cemu, ePSXe, DOSBox, PPSSPP, DuckStation, MelonDS, DeSmuME - Automatically sets scan range and relative base address - Automatically adds address entries with the [AUTO] prefix - Memory view right-click menu "Copy relative offset to clipboard" (Ctrl+]) - Memory view right-click menu "Add relative offset of selected address to address list" - Found list popup menu "Add relative offset to address list" (supports multi‑select; visible only when relative base is enabled) Auto-created entries are prefixed with [AUTO] and are automatically cleaned up when switching processes (only top-level entries are removed). Other: 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 code. 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 built‑in symbol RAM. MelonDS / DeSmuME modules based on standalone NDS script v2.3 (signature + pointer offset+8). Ver 1.3.3 (fixed menu insertion relying on English name, added timer cleanup) ]] ------------------------------------------------------------------------------- -- 0. User configuration ------------------------------------------------------------------------------- -- Whether to automatically show the memory viewer after loading an emulator (default true; set to false to disable) local SHOW_MEMORY_VIEW_ON_LOAD = false ------------------------------------------------------------------------------- -- 0.5 Clean up any lingering timers (to prevent leaks on reload) ------------------------------------------------------------------------------- if ppssppTimer and ppssppTimer.Enabled then ppssppTimer.Enabled = false ppssppTimer.destroy() end if foundListMenuTimer and foundListMenuTimer.Enabled then foundListMenuTimer.Enabled = false foundListMenuTimer.destroy() end -- Set global variables to nil to avoid later misuse ppssppTimer = nil foundListMenuTimer = nil ------------------------------------------------------------------------------- -- 1. Ensure memory view window exists ------------------------------------------------------------------------------- local mv = getMemoryViewForm() if mv == nil then mv = createMemoryView() end ------------------------------------------------------------------------------- -- 2. Create "Copy relative offset" and "Add relative offset to address list" menu items (memory view) ------------------------------------------------------------------------------- -- Clean up any leftover menu items if miCopyRelativeAddress ~= nil then miCopyRelativeAddress.destroy() miCopyRelativeAddress = nil end if miAddRelativeOffset ~= nil then miAddRelativeOffset.destroy() miAddRelativeOffset = nil end -- Helper: add relative offset of selected address to address list (pure offset) function addRelativeOffsetToAddressList() local hv = 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 if offset >= 0 then offsetStr = string.format("+%x", offset) else offsetStr = string.format("-%x", -offset) end addSymbolToAddressList(offsetStr, offsetStr) end -- Menu item 1: Copy relative offset to clipboard miCopyRelativeAddress = createMenuItem(mv.memorypopup) miCopyRelativeAddress.Name = "miCopyRelativeAddress" miCopyRelativeAddress.Caption = "Copy relative offset to clipboard" miCopyRelativeAddress.ShortCut = textToShortCut("Ctrl+]") miCopyRelativeAddress.OnClick = function() local hv = 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 -- Menu item 2: Add relative offset to address list miAddRelativeOffset = createMenuItem(mv.memorypopup) miAddRelativeOffset.Name = "miAddRelativeOffset" miAddRelativeOffset.Caption = "Add relative offset of selected address to address list" miAddRelativeOffset.OnClick = addRelativeOffsetToAddressList -- Insert menu items: prefer fixed control name "Addthisaddresstothelist1" to locate the "Add address" item local insertIndex = -1 for i = 0, mv.memorypopup.Items.Count - 1 do local item = mv.memorypopup.Items[i] -- Use Name property, which is fixed and not affected by language if item.Name == "Addthisaddresstothelist1" then insertIndex = i break end end -- If not found, fall back to English Caption (for compatibility with older versions or unusual cases) if insertIndex == -1 then for i = 0, mv.memorypopup.Items.Count - 1 do local item = 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 mv.memorypopup.Items.Insert(insertIndex + 1, miCopyRelativeAddress) mv.memorypopup.Items.Insert(insertIndex + 2, miAddRelativeOffset) else -- If still not found, append to the end (to avoid total loss) mv.memorypopup.Items.Add(miCopyRelativeAddress) mv.memorypopup.Items.Add(miAddRelativeOffset) end -- Robust OnPopup override to control menu item visibility local oldmemorypopuponpopup = mv.memorypopup.OnPopup or function() end mv.memorypopup.OnPopup = function(s) local visible = mv.HexadecimalView.UseRelativeBase if miCopyRelativeAddress then miCopyRelativeAddress.Visible = visible end if miAddRelativeOffset then miAddRelativeOffset.Visible = visible end return oldmemorypopuponpopup(s) end ------------------------------------------------------------------------------- -- 3. Manage auto-added address entries (with [AUTO] prefix) ------------------------------------------------------------------------------- local AUTO_PREFIX = "[AUTO] " 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(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 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 = AUTO_PREFIX .. displayName rec.ShowAsHex = true return true end ------------------------------------------------------------------------------- -- 4. Address range retrieval functions for each emulator (existing emulators) ------------------------------------------------------------------------------- local TARGET_PROCESSES = {} TARGET_PROCESSES["pcsx2"] = function() local baseAddress = readQword("EEmem") if baseAddress then return string.format("%016X", baseAddress), string.format("%016X", baseAddress + 0x20000000) end return end TARGET_PROCESSES["cemu"] = function() local success, baseAddress = pcall(function() return executeCode(getAddress("Cemu.memory_getBase"), 0, 2000) end) if success and baseAddress then registerSymbol("baseAddress", baseAddress) return string.format("%016X", baseAddress), string.format("%016X", baseAddress + 0x20000000) 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() return string.format("%016X", value), string.format("%016X", value + 0x200000) end return end TARGET_PROCESSES["dosbox"] = function() local ms = createMemScan() ms.ScanValue = 'IBM COMPATIBLE' ms.VarType = vtString ms.ScanWritable = 'scanDontCare' ms.ScanExecutable = 'scanDontCare' ms.ScanCopyOnWrite = 'scanDontCare' ms.scan() local r = ms.Results ms.destroy() if not r or #r == 0 then return end local chosenAddr = r[1] for _, addr in ipairs(r) do if addr > chosenAddr then chosenAddr = addr end end local baseAddr = chosenAddr - 0xFE00E registerSymbol('base', baseAddr) return string.format("%016X", baseAddr), string.format("%016X", baseAddr + 0x1000000) end TARGET_PROCESSES["duckstation"] = function() local baseAddress = readQword("RAM") if baseAddress then return string.format("%016X", baseAddress), string.format("%016X", baseAddress + 0x200000) end return end ------------------------------------------------------------------------------- -- 5. PPSSPP configuration function ------------------------------------------------------------------------------- local ppsspp_WindowClass = "PPSSPPWnd" local ppsspp_Configured = false 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 function configurePPSSPP() local hwnd = findWindow(ppsspp_WindowClass) 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 WM_USER_GET_BASE_POINTER = 0xB118 local function sendWMs(lParam) return sendMessage(hwnd, 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 * 0x100000000) + reply0 local basePtr = (reply3 * 0x100000000) + 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 + 0x8800000 removeScriptedSymbols() clearEphemeralSymbols() local fromAddress = string.format("%016X", actualBase) local toAddress = string.format("%016X", actualBase + 0x2000000) MainForm.FromAddress.Text = fromAddress MainForm.ToAddress.Text = toAddress mv.HexadecimalView.UseRelativeBase = true mv.HexadecimalView.RelativeBase = actualBase mv.HexadecimalView.Address = actualBase if SHOW_MEMORY_VIEW_ON_LOAD then mv.show() end local addrExpr = string.format("[%s+%X]+8800000", moduleName, offset) addSymbolToAddressList(addrExpr, "PPSSPP Memory") registerSymbol("PPSSPP_BasePtr", basePtr) return true end ------------------------------------------------------------------------------- -- 6. Clear ephemeral symbols (including NDS symbols) ------------------------------------------------------------------------------- function clearEphemeralSymbols() unregisterSymbol("ePSXe_x32") unregisterSymbol("base") unregisterSymbol("baseAddress") unregisterSymbol("PPSSPP_BasePtr") unregisterSymbol("melonDS64") unregisterSymbol("DeSmuMEx32") end ------------------------------------------------------------------------------- -- 7. PPSSPP timer ------------------------------------------------------------------------------- -- Use global variable for cleanup ppssppTimer = createTimer(nil, false) ppssppTimer.Interval = 2000 ppssppTimer.OnTimer = function() if not ppsspp_Configured then if configurePPSSPP() then ppsspp_Configured = true ppssppTimer.Enabled = false end else ppssppTimer.Enabled = false end end ------------------------------------------------------------------------------- -- 8. NDS emulator specific functions (from standalone script v2.3) ------------------------------------------------------------------------------- -- 8.1 Detection functions 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 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 -- 8.2 Get start address (melonDS) 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 -- 8.3 Get start address (DeSmuME 32-bit) 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 -- 8.4 Set scan range local function setScanRange(startAddr, endAddr) if startAddr == nil or endAddr == nil then MainForm.FromAddress.Text = "0000000000000000" MainForm.ToAddress.Text = "7FFFFFFFFFFFFFFF" return end MainForm.FromAddress.Text = string.format("%016X", startAddr) MainForm.ToAddress.Text = string.format("%016X", endAddr) end -- 8.5 Update relative base (UI only, no symbol involvement) local function updateRelativeBase(baseAddr) if mv and mv.HexadecimalView then if baseAddr then mv.HexadecimalView.UseRelativeBase = true mv.HexadecimalView.RelativeBase = baseAddr mv.HexadecimalView.Address = baseAddr if SHOW_MEMORY_VIEW_ON_LOAD then mv.show() end else mv.HexadecimalView.UseRelativeBase = false end end end -- 8.6 Handle melonDS local function handleMelonDS() clearEphemeralSymbols() removeScriptedSymbols() local startAddr = getMelonDSMainRAMStart() if startAddr then local endAddr = startAddr + 0x400000 setScanRange(startAddr, endAddr) updateRelativeBase(startAddr) registerSymbol("melonDS64", startAddr) addSymbolToAddressList("melonDS64", "MelonDS Base") else unregisterSymbol("melonDS64") updateRelativeBase(nil) print("melonDS process detected, but main RAM start address could not be located (game not loaded or signature not found).") end end -- 8.7 Handle DeSmuME 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 + 0x400000 setScanRange(baseAddr, endAddr) updateRelativeBase(baseAddr) registerSymbol("DeSmuMEx32", ptrAddr) addSymbolToAddressList("[DeSmuMEx32]", "DeSmuME Base") else unregisterSymbol("DeSmuMEx32") updateRelativeBase(nil) print("DeSmuME process detected, but signature could not be located (game not loaded or signature outdated).") end end ------------------------------------------------------------------------------- -- 9. Delayed execution function (to avoid conflict with recentFiles.lua) ------------------------------------------------------------------------------- local applyTimer = nil local function applyEmulatorSettings(pid, handle, caption) if not mv then mv = getMemoryViewForm() if not mv then return end end -- First check NDS emulators (using separate detection functions) if isMelonDSProcess() then handleMelonDS() return elseif isDesmumeProcess() then handleDesmume() return end -- Non-NDS: original emulator flow local hwnd = findWindow(ppsspp_WindowClass) if hwnd ~= nil and getWindowProcessID(hwnd) == pid then ppsspp_Configured = false if configurePPSSPP() then ppsspp_Configured = true ppssppTimer.Enabled = false else if not ppssppTimer.Enabled then ppssppTimer.Enabled = true end end return end -- Other emulators (matched by module name) local moduleList = enumModules() local processName for _, module in ipairs(moduleList) do local name = module.Name:lower() if name:match("%.exe$") then processName = name:match("([^/\\]+)%.exe$") break end end local processType if processName then if processName:match("^pcsx2") then processType = "pcsx2" elseif processName:match("^cemu") then processType = "cemu" elseif processName:match("^epsxe") then processType = "epsxe" elseif processName:match("dosbox") then processType = "dosbox" elseif processName:match("^duckstation") then processType = "duckstation" end end local mvhv = mv.HexadecimalView if processType then removeScriptedSymbols() local fromAddress, toAddress = TARGET_PROCESSES[processType]() if fromAddress and toAddress then MainForm.FromAddress.Text = fromAddress MainForm.ToAddress.Text = toAddress mvhv.UseRelativeBase = true mvhv.RelativeBase = tonumber(fromAddress, 16) mvhv.Address = mvhv.RelativeBase if SHOW_MEMORY_VIEW_ON_LOAD then mv.show() end if processType == "dosbox" then addSymbolToAddressList("base", "DOSBox Base") elseif processType == "epsxe" then addSymbolToAddressList("[ePSXe_x32]", "ePSXe Base") elseif processType == "pcsx2" then addSymbolToAddressList("[EEmem]", "PCSX2 EEmem") elseif processType == "cemu" then addSymbolToAddressList("baseAddress", "Cemu Base") elseif processType == "duckstation" then addSymbolToAddressList("[RAM]", "DuckStation RAM") end end else MainForm.FromAddress.Text = "0000000000000000" MainForm.ToAddress.Text = "7fffffffffffffff" mvhv.UseRelativeBase = false clearEphemeralSymbols() removeScriptedSymbols() end end ------------------------------------------------------------------------------- -- 10. OnProcessOpened (with delayed timer) ------------------------------------------------------------------------------- local oldOnProcessOpened = MainForm.OnProcessOpened MainForm.OnProcessOpened = function(pid, handle, caption) -- Call old callback first (chain) if oldOnProcessOpened then pcall(oldOnProcessOpened, pid, handle, caption) end -- Cancel previous delayed timer if applyTimer then applyTimer.Enabled = false applyTimer.destroy() applyTimer = nil end -- Create a new timer, delay 150ms before applying configuration applyTimer = createTimer(nil, false) applyTimer.Interval = 150 applyTimer.OnTimer = function() applyTimer.Enabled = false applyTimer.destroy() applyTimer = nil applyEmulatorSettings(pid, handle, caption) end applyTimer.Enabled = true end ------------------------------------------------------------------------------- -- 11. Execute once at startup ------------------------------------------------------------------------------- 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 ------------------------------------------------------------------------------- -- 12. Found list popup menu: Add relative offset to address list (with visibility control) ------------------------------------------------------------------------------- local miAddRelativeOffsetFromFoundList = nil function addRelativeOffsetFromFoundList() local lv = getMainForm().Foundlist3 if not lv then messageDialog("Found list (Foundlist3) not found.", mtError, mbOK) return end local hv = mv and mv.HexadecimalView if not hv or not hv.UseRelativeBase then messageDialog("Relative base is not currently enabled. Cannot calculate offset.\nPlease attach a supported emulator to automatically set the base address.", mtWarning, mbOK) return end local base = hv.RelativeBase local count = 0 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 addrPart = addrStr:match("^([^:]+)") if not addrPart then addrPart = addrStr end local addrNum = tonumber(addrPart, 16) if addrNum then local offset = addrNum - base local offsetStr if offset >= 0 then offsetStr = string.format("+%x", offset) else offsetStr = string.format("-%x", -offset) end 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 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.Caption = "-" sep.Visible = false popup.Items.Insert(1, sep) miAddRelativeOffsetFromFoundList = item local oldPopup = popup.OnPopup or function() end popup.OnPopup = function(sender) oldPopup(sender) if miAddRelativeOffsetFromFoundList then local visible = mv and mv.HexadecimalView and mv.HexadecimalView.UseRelativeBase or false miAddRelativeOffsetFromFoundList.Visible = visible if popup.Items.Count > 1 and popup.Items[1].Caption == "-" then popup.Items[1].Visible = visible end end end return true end foundListMenuTimer = createTimer(nil, false) foundListMenuTimer.Interval = 300 local attemptCount = 0 foundListMenuTimer.OnTimer = function(timer) attemptCount = attemptCount + 1 if setupFoundListMenu() then timer.Enabled = false timer.destroy() elseif attemptCount >= 50 then timer.Enabled = false timer.destroy() end end foundListMenuTimer.Enabled = true