--[[ Multi-Emulator Automatic Address Range + Auto Create Symbol Entries Features: - Auto-detect PCSX2, Cemu, ePSXe, DOSBox, PPSSPP - Automatically set scan range and relative base - Automatically add address entries with [AUTO] prefix - Memory view right-click menu "Copy Relative Offset to Clipboard" (Ctrl+]) - Memory view right-click menu "Add Selected Address Relative Offset to Address List" (pure offset) Auto-created entries all have [AUTO] prefix; when switching processes they will be automatically cleaned (only top-level entries are deleted, avoiding access violations from re-deleting children). Others: This Lua script is AI-driven. The DOSBox module is based on the following script by the Cheat Engine author: https://forum.cheatengine.org/viewtopic.php?t=624001 This script has integrated all functionality of the previously shared "Copy Selection Address relative Offset Value to clipboard.lua" script; do not load it separately. The PPSSPP module is based on ppsspp_WM.ahk source code. The Cemu module is based on: https://forum.cheatengine.org/viewtopic.php?p=5781484&sid=99730d28d965f0da2f1c6575921e6458 The PCSX2 module is based on: https://forums.pcsx2.net/Thread-PCSX2-1-7-Cheat-Engine-Script-Compatibility Ver 1.1.7 Fixes PPSSPP timer not stopped: In OnProcessOpened, when switching to a non-PPSSPP process, explicitly set ppssppTimer.Enabled = false to avoid unnecessary polling. Negative offset expression error: Mimicking the clipboard menu, added sign judgment in addRelativeOffsetToAddressList to generate correct [_REL_BASE+/-offset]. PPSSPP module name case: In getPPSSPPModuleInfo changed matching to mod.Name:lower() to support ppsspp.exe, PPSSPP.exe etc. Restored pure offset format, no longer using base expression. Access Violation on process switch: removeScriptedSymbols deleted all [AUTO] entries, including child offset entries, but when deleting parent, CE had already auto-deleted children, causing a crash when trying to delete already freed child objects. ]] ------------------------------------------------------------------------------- -- 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 ------------------------------------------------------------------------------- -- Clean up possible leftover menu items if miCopyRelativeAddress ~= nil then miCopyRelativeAddress.destroy() miCopyRelativeAddress = nil end if miAddRelativeOffset ~= nil then miAddRelativeOffset.destroy() miAddRelativeOffset = nil end -- Helper: add the relative offset of selected address to address list (pure offset) function addRelativeOffsetToAddressList() local hv = mv.HexadecimalView if not hv or not hv.UseRelativeBase then print("Relative base is not enabled.") return end local base = hv.RelativeBase local selStart = hv.SelectionStart if selStart == 0 then print("No address selected.") return end local offset = selStart - base local offsetStr if offset >= 0 then offsetStr = string.format("+%x", offset) else offsetStr = string.format("-%x", -offset) end if addSymbolToAddressList(offsetStr, offsetStr) then -- print(string.format("Added relative offset: %s", offsetStr)) else -- print("Relative offset address already exists.") end 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 (after "Add this address to the list") local insertIndex = -1 for i = 0, mv.memorypopup.Items.Count - 1 do local item = mv.memorypopup.Items[i] if item.Name == "Addthisaddresstothelist1" or item.Caption == "Add this address to the list" or item.Caption == "Add this address to the list..." then insertIndex = i break end end if insertIndex ~= -1 then mv.memorypopup.Items.Insert(insertIndex + 1, miCopyRelativeAddress) mv.memorypopup.Items.Insert(insertIndex + 2, miAddRelativeOffset) else 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 automatically 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 -- Only collect top-level entries (no parent), let CE automatically delete their children 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 -- already exists 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 ------------------------------------------------------------------------------- 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 ------------------------------------------------------------------------------- -- 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 mv.show() local addrExpr = string.format("[%s+%X]+8800000", moduleName, offset) addSymbolToAddressList(addrExpr, "PPSSPP Memory") registerSymbol("PPSSPP_BasePtr", basePtr) updateRelativeBaseSymbol() return true end ------------------------------------------------------------------------------- -- 6. Clean temporary symbols ------------------------------------------------------------------------------- function clearEphemeralSymbols() unregisterSymbol("ePSXe_x32") unregisterSymbol("base") unregisterSymbol("baseAddress") unregisterSymbol("PPSSPP_BasePtr") unregisterSymbol("_REL_BASE") end function updateRelativeBaseSymbol() local hv = mv.HexadecimalView if hv and hv.UseRelativeBase then registerSymbol("_REL_BASE", hv.RelativeBase) else unregisterSymbol("_REL_BASE") end end ------------------------------------------------------------------------------- -- 7. Timer: continuously attempt to configure PPSSPP ------------------------------------------------------------------------------- local 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. OnProcessOpened: handle all emulators (chain call to avoid conflicts) ------------------------------------------------------------------------------- local oldOnProcessOpened = MainForm.OnProcessOpened MainForm.OnProcessOpened = function(pid, handle, caption) if oldOnProcessOpened then pcall(oldOnProcessOpened, pid, handle, caption) end if not mv then mv = getMemoryViewForm() if not mv then return end end 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 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" 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 mv.show() 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") end updateRelativeBaseSymbol() end else MainForm.FromAddress.Text = "0000000000000000" MainForm.ToAddress.Text = "7fffffffffffffff" mvhv.UseRelativeBase = false clearEphemeralSymbols() removeScriptedSymbols() end end ------------------------------------------------------------------------------- -- 9. If a process is open when CE starts, trigger proactively (optional) ------------------------------------------------------------------------------- -- local currentPid = getOpenedProcessID() -- if currentPid ~= 0 then -- MainForm.OnProcessOpened(currentPid, getOpenedProcessHandle(), "Current Process") -- end --print("Multi-emulator script loaded. (v1.1.7 - fixed delete of parent entries causing child access violation)")