Some Lua Codes (Tools)

Want Cheat Engine to do something specific and no idea how to do that, ask here. (From simple scripts to full trainers and extensions)
Post Reply
User avatar
SilverRabbit90
Table Makers
Table Makers
Posts: 252
Joined: Fri Jan 15, 2021 12:01 am
Reputation: 219

Some Lua Codes (Tools)

Post by SilverRabbit90 »

Work in progress ...

Here is my Cheat Engine Tutorial:
[Link]

.

First of all, here's a brief explanation of how to execute Lua codes with Cheat Engine:

A) How to insert a Lua Code in a Lua Script
Spoiler
B) How to insert a Lua Code in Show Chaeat Table Lua Script (press Ctrl+Alt+L to open)
Spoiler
C) How to execute a Lua Code froma a file
Insert this code in "Show Chaeat Table Lua Script" (press Ctrl+Alt+L to open)
Spoiler

Code: Select all

local userProfilePath = os.getenv("USERPROFILE")

local function safeDofile(filePath)
    local success, err = pcall(dofile, filePath)
end

local luaFilePath1 = userProfilePath .. "\\Desktop\\Lua\\generateAOBMenu.lua"
safeDofile(luaFilePath1)

local luaFilePath2 = userProfilePath .. "\\Desktop\\Lua\\LuaScriptForAllDissectDataOffsets.lua"
safeDofile(luaFilePath2)


-----------------------------------------------------------------------------------------------


---- ↓↓↓ This is used to make some Lua scripts work correctly. ↓↓↓

if generateAOBmenuitem then
  generateAOBmenuitem.OnClick = function()
    local addr = getMemoryViewForm().DisassemblerView.SelectedAddress
    createThread(function()
      synchronize(function() generateWildcardAOB(addr) end)
    end)
  end
else

end
Spoiler

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


1) AOB Scanner & Memory Extractor:

This code is used to find and extract the AOB from the 'Memory View' more easily and quickly. Once the tool is open, you need to right-click on a script and copy it. After that, you must paste it into the textbox in the GUI labeled 'Paste the script containing AOB here (use right-click -> Paste):' and finally click the 'Search AOB and Extract' button.

Be careful, some games require you to perform a specific action; otherwise, the tool won't work (because some arrays are not immediately visible to Cheat Engine as they haven't been loaded into memory yet). For example, to find the array of a script named 'Infinite Ammo,' you will probably need to fire a shot first.

Code:
Spoiler

Code: Select all

-- ===================================================
-- Tool: AOB Scanner & Memory Extractor
-- Completely corrected version
-- ===================================================

-- =============================
-- Creating the main form
local form = createForm()
form.Caption = 'AOB Scanner & Memory Extractor'
form.Width = 900
form.Height = 700
form.Position = poScreenCenter

-- =============================
-- Label for input
local inputLabel = createLabel(form)
inputLabel.Caption = 'Paste the script containing AOB here (use right-click -> Paste):'
inputLabel.Left = 10
inputLabel.Top = 10

-- Creating the memo for input
local inputMemo = createMemo(form)
inputMemo.Left = 10
inputMemo.Top = 30
inputMemo.Width = 870
inputMemo.Height = 200
inputMemo.ScrollBars = ssBoth
inputMemo.WordWrap = false

-- =============================
-- Context menu for the memo
local popup = createPopupMenu(form)
local menuItemPaste = createMenuItem(popup)
menuItemPaste.Caption = 'Paste'
menuItemPaste.OnClick = function(sender)
  local clipboardText = getClipboardText()
  if clipboardText then
    inputMemo.Text = clipboardText
  else
    showMessage("The clipboard does not contain text!")
  end
end
popup.Items.add(menuItemPaste)

inputMemo.OnMouseUp = function(sender, x, y, button)
  if button == 2 then
    local globalX = inputMemo.ClientToScreenX(x)
    local globalY = inputMemo.ClientToScreenY(y)
    popup.Popup(globalX, globalY)
  end
end

-- =============================
-- Button to perform the scan
local scanButton = createButton(form)
scanButton.Caption = 'Search AOB and Extract'
scanButton.Left = 10
scanButton.Top = 240
scanButton.Width = 200

-- Label for the amount of bytes
local preInjectLabel = createLabel(form)
preInjectLabel.Caption = 'Bytes before the injection point:'
preInjectLabel.Left = 220
preInjectLabel.Top = 240

local injectLabel = createLabel(form)
injectLabel.Caption = 'Bytes from the injection point:'
injectLabel.Left = 220
injectLabel.Top = 270

-- Textbox for the amount of bytes
local preInjectBytesTextbox = createEdit(form)
preInjectBytesTextbox.Left = 400
preInjectBytesTextbox.Top = 240
preInjectBytesTextbox.Width = 50
preInjectBytesTextbox.Text = '50'

local injectBytesTextbox = createEdit(form)
injectBytesTextbox.Left = 400
injectBytesTextbox.Top = 270
injectBytesTextbox.Width = 50
injectBytesTextbox.Text = '50'

-- Label and Memo for output
local outputLabel = createLabel(form)
outputLabel.Caption = 'AOB scan results:'
outputLabel.Left = 10
outputLabel.Top = 310

local outputMemo = createMemo(form)
outputMemo.Left = 10
outputMemo.Top = 330
outputMemo.Width = 870
outputMemo.Height = 350
outputMemo.ReadOnly = true
outputMemo.ScrollBars = ssBoth
outputMemo.WordWrap = false

-- =============================
-- Function to extract all AOBs from the script
local function extractAOBsFromScript(scriptText)
    local aobs = {}

    -- Cleaning the text from comments and extra spaces
    local cleanedText = scriptText:gsub('//.*', ''):gsub('%-%-.*', ''):gsub('%s+', ' ')

    -- Pattern to capture all variants of aobscan
    for aobType, params in cleanedText:gmatch('aobscan(%w*)%s*%(([^%)]+)%)') do
        -- Extract parameters
        local parts = {}
        for param in params:gmatch('[^,]+') do
            local cleanedParam = param:gsub('^%s+', ''):gsub('%s+$', '')
            table.insert(parts, cleanedParam)
        end

        if #parts >= 2 then
            local aobInfo = {
                type = 'aobscan'..(aobType or ''),
                name = parts[1],
                pattern = parts[#parts]
            }

            -- Specific handling for each type
            if aobType == 'module' then
                if #parts >= 3 then
                    aobInfo.module = parts[2]
                end
            elseif aobType == 'region' then
                if #parts >= 5 then
                    aobInfo.startAddress = parts[2]
                    aobInfo.endAddress = parts[3]
                    aobInfo.protection = parts[4]
                end
            end

            table.insert(aobs, aobInfo)
        end
    end

    return aobs
end

-- =============================
-- Function to search for an AOB and extract a portion of memory
local function findAndExtractAOB(aobPattern, preInjectBytes, injectBytes)
    -- First, we check if the AOB is valid
    if not aobPattern or aobPattern == '' then
        return nil, "Empty AOB pattern"
    end

    -- Convert the AOB into a string without spaces
    local searchPattern = aobPattern:gsub(' ', '')

    -- Use AOBScan to find the address
    local addresses = AOBScan(searchPattern)
    if not addresses or addresses.Count == 0 then
        return nil, "AOB not found: "..aobPattern
    end

    -- Take the first result
    local foundAddress = tonumber('0x'..addresses[0])
    addresses.Destroy()

    -- Get the memory view
    local memView = getMemoryViewForm()
    if not memView then
        memView = createMemoryView()
        if not memView then
            return nil, "Unable to create Memory View"
        end
    end

    -- Select the address in the disassembler
    memView.DisassemblerView.SelectedAddress = foundAddress

    -- Read bytes from memory, using the amount specified in the textboxes
    local startAddress = foundAddress - preInjectBytes
    if startAddress < 0 then startAddress = 0 end
    local bytesBeforeInject = readBytes(startAddress, preInjectBytes, true)

    -- Read bytes from the injection point
    local bytesAfterInject = readBytes(foundAddress, injectBytes, true)

    if not bytesBeforeInject and not bytesAfterInject then
        return nil, "Unable to read bytes from memory"
    end

    -- Create a string with the read bytes
    local byteString = {}

    for i = 1, #bytesBeforeInject do
        table.insert(byteString, string.format('%02X', bytesBeforeInject[i]))
    end

    for i = 1, #bytesAfterInject do
        table.insert(byteString, string.format('%02X', bytesAfterInject[i]))
    end

    return table.concat(byteString, ' '), "AOB found at "..string.format('%X', foundAddress)
end

-- =============================
-- Click event on the scan button
function scanButton.OnClick(sender)
    local scriptText = inputMemo.Lines.Text
    if scriptText == nil or scriptText == '' then
        showMessage("You must paste the script to analyze!")
        return
    end

    -- Get values from the textboxes
    local preInjectBytes = tonumber(preInjectBytesTextbox.Text) or 50
    local injectBytes = tonumber(injectBytesTextbox.Text) or 50

    -- Extract AOBs
    local aobs = extractAOBsFromScript(scriptText)
    if #aobs == 0 then
        showMessage("No AOB found in the script!")
        return
    end

    -- Prepare output
    local outputLines = {}
    table.insert(outputLines, "Found "..#aobs.." AOB in the script:")
    table.insert(outputLines, "========================================")

    -- Process each AOB
    for i, aob in ipairs(aobs) do
        table.insert(outputLines, "")
        table.insert(outputLines, string.format("%d. Type: %s", i, aob.type))
        table.insert(outputLines, string.format("   Name: %s", aob.name))
        if aob.module ~= '' then
            table.insert(outputLines, string.format("   Module: %s", aob.module))
        end
        table.insert(outputLines, string.format("   Pattern: %s", aob.pattern))

        -- Search for the AOB
        local bytes, message = findAndExtractAOB(aob.pattern, preInjectBytes, injectBytes)
        table.insert(outputLines, "   "..message)

        if bytes then
            -- Count the bytes
            local byteCount = select(2, bytes:gsub('%x%x', '')) + 1
            table.insert(outputLines, string.format("   Extracted Bytes (%d bytes):", byteCount))

            -- Divide the bytes into parts for "Before INJECTING Point" and "INJECTING Point"
            local preInjectBytesList = {}
            local injectBytesList = {}

            -- Already read the bytes into a table for better management
            local byteParts = {}
            for byte in bytes:gmatch('%x+') do
                table.insert(byteParts, byte)
            end

            for i = 1, #byteParts do
                if i <= preInjectBytes then
                    table.insert(preInjectBytesList, byteParts[i])
                else
                    table.insert(injectBytesList, byteParts[i])
                end
            end

            -- Build strings for the desired format
            table.insert(outputLines, "Before INJECTING Point")
            table.insert(outputLines, "   " .. table.concat(preInjectBytesList, ' '))
            table.insert(outputLines, "INJECTING Point")
            table.insert(outputLines, "   " .. table.concat(injectBytesList, ' '))
        end

        table.insert(outputLines, "----------------------------------------")
    end

    -- Show results
    outputMemo.Lines.Text = table.concat(outputLines, '\n')
end

-- =============================
-- Displaying the form
form.Show()
Video:
Spoiler
Script:
AOB Scanner & Memory Extractor.CT
(9.58 KiB) Downloaded 66 times

--

2) Aob Extractor:

This tool is used to extract the array from a script that provides it; the AOB will be extracted from the end of the script, specifically the bytes found within curly braces {}.

Code:
Spoiler

Code: Select all

-- ===================================================
-- Tool: AOB Extractor with Clipboard Paste (Combined)
-- Description:
--   - Allows you to paste the copied content from the main window
--     into a textbox using right-click (context menu).
--   - Isolates the block of text contained between the last pair of curly braces,
--     ignoring the rest of the script.
--   - Processes the isolated block to extract and display AOBs.
-- ===================================================

-- =============================
-- Creating the main form
local form = createForm()
form.Caption = 'AOB Extractor with Clipboard Paste'
form.Width = 800
form.Height = 600
form.Position = poScreenCenter

-- =============================
-- Label for input
local inputLabel = createLabel(form)
inputLabel.Caption = 'Paste the script here (use right-click -> Paste):'
inputLabel.Left = 10
inputLabel.Top = 10

-- Creating the memo for input (multiline) where the script will be pasted
local inputMemo = createMemo(form)
inputMemo.Left = 10
inputMemo.Top = 30
inputMemo.Width = 760
inputMemo.Height = 200
inputMemo.ScrollBars = ssBoth
inputMemo.WordWrap = false

-- =============================
-- Creating the context menu for the memo
local popup = createPopupMenu(form)
local menuItemPaste = createMenuItem(popup)
menuItemPaste.Caption = 'Paste'
menuItemPaste.OnClick = function(sender)
  local clipboardText = getClipboardText()
  if clipboardText then
    inputMemo.Text = clipboardText
  else
    showMessage("The clipboard does not contain text!")
  end
end
popup.Items.add(menuItemPaste)

-- Associate the context menu with right-click on the memo
inputMemo.OnMouseUp = function(sender, x, y, button)
  if button == 2 then  -- 2 = right mouse button
    local globalX = inputMemo.ClientToScreenX(x)
    local globalY = inputMemo.ClientToScreenY(y)
    popup.Popup(globalX, globalY)
  end
end

-- =============================
-- Button to extract the AOB
local extractButton = createButton(form)
extractButton.Caption = 'Extract AOB'
extractButton.Left = 10
extractButton.Top = 240
extractButton.Width = 100

-- Label and Memo for the output "Before"
local beforeLabel = createLabel(form)
beforeLabel.Caption = 'Compared Before Injecting point Bytes:'
beforeLabel.Left = 10
beforeLabel.Top = 280

local outputMemoBefore = createMemo(form)
outputMemoBefore.Left = 10
outputMemoBefore.Top = 300
outputMemoBefore.Width = 760
outputMemoBefore.Height = 120
outputMemoBefore.ReadOnly = true
outputMemoBefore.ScrollBars = ssBoth
outputMemoBefore.WordWrap = false

-- Label and Memo for the output "After"
local afterLabel = createLabel(form)
afterLabel.Caption = 'Compared AoB from Injecting Point:'
afterLabel.Left = 10
afterLabel.Top = 430

local outputMemoAfter = createMemo(form)
outputMemoAfter.Left = 10
outputMemoAfter.Top = 450
outputMemoAfter.Width = 760
outputMemoAfter.Height = 100
outputMemoAfter.ReadOnly = true
outputMemoAfter.ScrollBars = ssBoth
outputMemoAfter.WordWrap = false

-- =============================
-- Function to extract the block of text contained between the last pair of curly braces.
-- This way, the relevant code will be isolated (ignoring any initial blocks
-- of the script).
local function extractInjectionBlock(fullText)
  -- Find the position of the last opening curly brace "{"
  local lastOpen = nil
  for pos = #fullText, 1, -1 do
    if fullText:sub(pos, pos) == "{" then
      lastOpen = pos
      break
    end
  end
  if not lastOpen then
    return nil -- no block found
  end
  -- From the found position, look for the first "}" that follows
  local lastClose = fullText:find("}", lastOpen, true)
  if not lastClose then
    return nil
  end
  -- Extract the block (excluding the braces themselves)
  local block = fullText:sub(lastOpen + 1, lastClose - 1)
  return block
end

-- =============================
-- Function to process the text (containing only the injection block) and extract the bytes from it
local function extractAOB(inputText)
  local beforeBytes = {}
  local afterBytes  = {}
  local currentGroup = "before"

  -- Iterate through each line
  for line in inputText:gmatch("([^\n]+)") do
    -- Priority handling of markers
    if line:find("INJECTING HERE") then
      currentGroup = "after"
    elseif line:find("DONE INJECTING") then
      -- Skip the DONE INJECTING marker
    -- Skip non-significant lines (comments, braces, etc.)
    elseif line:match("^%s*%{") or line:match("^%s*%}") or line:find("ORIGINAL CODE") or line:match("^%s*//") then
      -- ignore the line
    else
      -- Try to extract the bytes from the line:
      -- Look for the pattern after the colon ":" until a dash (if present) or until the end of the line
      local bytesStr = line:match(":%s*([0-9A-Fa-f ]+)%s*%-")
      if not bytesStr then
        bytesStr = line:match(":%s*([0-9A-Fa-f ]+)%s*$")
      end
      if bytesStr then
        bytesStr = bytesStr:gsub("^%s+", ""):gsub("%s+$", "")
        if #bytesStr > 0 then
          if currentGroup == "before" then
            table.insert(beforeBytes, bytesStr)
          else
            table.insert(afterBytes, bytesStr)
          end
        end
      end
    end
  end

  -- Helper function: joins the arrays into a string with a single space between the bytes
  local function joinBytes(arr)
    local s = table.concat(arr, " ")
    s = s:gsub("%s+", " ")
    return s
  end

  -- Helper function: counts the number of bytes (hex pairs)
  local function countBytes(aobStr)
    local count = 0
    for byte in aobStr:gmatch("%x%x") do
      count = count + 1
    end
    return count
  end

  local aobBefore = joinBytes(beforeBytes)
  local aobAfter  = joinBytes(afterBytes)
  local countBefore = countBytes(aobBefore)
  local countAfter  = countBytes(aobAfter)

  local outputTextBefore = "Compared Before Injecting point Bytes:\n" ..
                           aobBefore .. "\n" ..
                           "Byte count: " .. countBefore .. "\n" ..
                           string.format("0x%X", countBefore) .. "\n" ..
                           string.format("%X", countBefore)

  local outputTextAfter = "Compared AoB from Injecting Point:\n" ..
                          aobAfter .. "\n" ..
                          "Byte count: " .. countAfter .. "\n" ..
                          string.format("0x%X", countAfter) .. "\n" ..
                          string.format("%X", countAfter)

  return outputTextBefore, outputTextAfter
end

-- =============================
-- Click event on the "Extract AOB" button
function extractButton.OnClick(sender)
  local fullText = inputMemo.Lines.Text
  if fullText == nil or fullText == '' then
    showMessage("You must paste the script to analyze!")
    return
  end

  -- Isolate the injection block (the last block between curly braces)
  local injectionBlock = extractInjectionBlock(fullText)
  if not injectionBlock then
    showMessage("No injection block (delimited by {}) found!")
    return
  end

  local outputBefore, outputAfter = extractAOB(injectionBlock)
  outputMemoBefore.Lines.Text = outputBefore
  outputMemoAfter.Lines.Text = outputAfter
end

-- =============================
-- Showing the form
form.Show()
Video:
Spoiler
Script:
AoB Extractor Bar [Next to Help].CT
(8.29 KiB) Downloaded 40 times

---

3) AoB <scripts> Comparator:

This tool extracts and compares the arrays from two scripts, replacing differing bytes with a ? or two ??.

Code:
Spoiler

Code: Select all

-- ============================================================================
-- Tool: AOB Comparator with dual comparison modes
-- Description:
--  - There are two text boxes (inputMemo1 and inputMemo2) for pasting scripts
--    (via right-click -> Paste).
--  - From each script, the last block (between { and }) containing the injection is isolated.
--  - Two groups of bytes are extracted:
--         ▸ "Before Injecting point" – all lines up to the "INJECTING HERE" marker
--         ▸ "AoB from Injecting Point" – all lines after "INJECTING HERE"
--  - The comparison can occur in two modes:
--         ▸ "Comparison ?" mode (nibble by nibble): it compares each nibble and
--           if there is a difference, the nibble is replaced with "?".
--         ▸ "Comparison ??" mode (full byte): if the byte does not match entirely,
--           the entire byte will be replaced with "??".
--  - The results are displayed in two memos, with the byte count in decimal
--    and hexadecimal.
-- ============================================================================

-- =============================
-- Creating the main form
local form = createForm()
form.Caption = 'AOB Comparator'
form.Width = 900
form.Height = 750
form.Position = poScreenCenter

-- =============================
-- Labels for the inputs
local inputLabel1 = createLabel(form)
inputLabel1.Caption = 'Script 1 (paste the script here via right-click -> Paste):'
inputLabel1.Left = 10
inputLabel1.Top = 10

local inputLabel2 = createLabel(form)
inputLabel2.Caption = 'Script 2 (paste the script here via right-click -> Paste):'
inputLabel2.Left = 460
inputLabel2.Top = 10

-- =============================
-- Memo for Script 1
local inputMemo1 = createMemo(form)
inputMemo1.Left = 10
inputMemo1.Top = 30
inputMemo1.Width = 430
inputMemo1.Height = 200
inputMemo1.ScrollBars = ssBoth
inputMemo1.WordWrap = false

-- Memo for Script 2
local inputMemo2 = createMemo(form)
inputMemo2.Left = 460
inputMemo2.Top = 30
inputMemo2.Width = 430
inputMemo2.Height = 200
inputMemo2.ScrollBars = ssBoth
inputMemo2.WordWrap = false

-- =============================
-- Function to create a "Paste" popup menu for a memo
local function attachPastePopup(memoControl)
  local popup = createPopupMenu(form)
  local menuItemPaste = createMenuItem(popup)
  menuItemPaste.Caption = 'Paste'
  menuItemPaste.OnClick = function(sender)
    local clipboardText = getClipboardText()
    if clipboardText then
      memoControl.Text = clipboardText
    else
      showMessage("The clipboard does not contain text!")
    end
  end
  popup.Items.add(menuItemPaste)

  memoControl.OnMouseUp = function(sender, x, y, button)
    if button == 2 then -- right mouse button
      local globalX = memoControl.ClientToScreenX(x)
      local globalY = memoControl.ClientToScreenY(y)
      popup.Popup(globalX, globalY)
    end
  end
end

-- Associate the context menu with both memos
attachPastePopup(inputMemo1)
attachPastePopup(inputMemo2)

-- =============================
-- Buttons for the comparison
-- Create two side-by-side buttons

-- Button "Comparison ??" (full byte mode, default)
local compareFullButton = createButton(form)
compareFullButton.Caption = 'Comparison ??'
compareFullButton.Left = 10
compareFullButton.Top = 240
compareFullButton.Width = 120

-- Button "Comparison ?" (nibble by nibble mode)
local comparePartialButton = createButton(form)
comparePartialButton.Caption = 'Comparison ?'
comparePartialButton.Left = 140
comparePartialButton.Top = 240
comparePartialButton.Width = 120

-- =============================
-- Labels and memo for the output "Before"
local beforeLabel = createLabel(form)
beforeLabel.Caption = 'Compared Before Injecting point Bytes:'
beforeLabel.Left = 10
beforeLabel.Top = 280

local outputMemoBefore = createMemo(form)
outputMemoBefore.Left = 10
outputMemoBefore.Top = 300
outputMemoBefore.Width = 880
outputMemoBefore.Height = 150
outputMemoBefore.ReadOnly = true
outputMemoBefore.ScrollBars = ssBoth
outputMemoBefore.WordWrap = false

-- Labels and memo for the output "After"
local afterLabel = createLabel(form)
afterLabel.Caption = 'Compared AoB from Injecting Point:'
afterLabel.Left = 10
afterLabel.Top = 460

local outputMemoAfter = createMemo(form)
outputMemoAfter.Left = 10
outputMemoAfter.Top = 480
outputMemoAfter.Width = 880
outputMemoAfter.Height = 150
outputMemoAfter.ReadOnly = true
outputMemoAfter.ScrollBars = ssBoth
outputMemoAfter.WordWrap = false

-- =============================
-- Function to isolate the last injection block (content between { and })
local function extractInjectionBlock(fullText)
  local lastOpen = nil
  for pos = #fullText, 1, -1 do
    if fullText:sub(pos, pos) == "{" then
      lastOpen = pos
      break
    end
  end
  if not lastOpen then return nil end
  local lastClose = fullText:find("}", lastOpen, true)
  if not lastClose then return nil end
  local block = fullText:sub(lastOpen + 1, lastClose - 1)
  return block
end

-- =============================
-- Function to extract AoBs from the injection block
-- Returns two strings:
--    aobBefore: concatenated bytes (separated by a space) up to the "INJECTING HERE" marker
--    aobAfter:  concatenated bytes after the "INJECTING HERE" marker
local function extractAOB(blockText)
  local arrBefore = {}
  local arrAfter  = {}
  local currentGroup = "before"

  for line in blockText:gmatch("([^\n]+)") do
    if line:find("INJECTING HERE") then
      currentGroup = "after"
    elseif line:find("DONE INJECTING") then
      -- ignore marker
    elseif line:match("^%s*%{") or line:match("^%s*%}") or line:find("ORIGINAL CODE") or line:match("^%s*//") then
      -- ignore non-significant lines
    else
      -- Extract the bytes: look for the part after ":" and before "-" (if present)
      local bytesStr = line:match(":%s*([0-9A-Fa-f ]+)%s*%-")
      if not bytesStr then
        bytesStr = line:match(":%s*([0-9A-Fa-f ]+)%s*$")
      end
      if bytesStr then
        bytesStr = bytesStr:gsub("^%s+", ""):gsub("%s+$", "")
        if #bytesStr > 0 then
          if currentGroup == "before" then
            table.insert(arrBefore, bytesStr)
          else
            table.insert(arrAfter, bytesStr)
          end
        end
      end
    end
  end

  local function joinBytes(arr)
    local s = table.concat(arr, " ")
    s = s:gsub("%s+", " ")
    return s
  end

  local aobBefore = joinBytes(arrBefore)
  local aobAfter  = joinBytes(arrAfter)

  return aobBefore, aobAfter
end

-- =============================
-- Helper functions for comparison

-- Splits the string into an array of bytes (tokens separated by spaces)
local function splitBytes(aobStr)
  local bytes = {}
  for byte in aobStr:gmatch("%S+") do
    table.insert(bytes, byte)
  end
  return bytes
end

-- "Comparison ?" mode (nibble by nibble)
local function compareBytePartial(b1, b2)
  local result = ""
  local n1 = b1:sub(1,1)
  local n2 = b2:sub(1,1)
  if n1:upper() == n2:upper() then
    result = result .. n1:upper()
  else
    result = result .. "?"
  end
  n1 = b1:sub(2,2)
  n2 = b2:sub(2,2)
  if n1:upper() == n2:upper() then
    result = result .. n1:upper()
  else
    result = result .. "?"
  end
  return result
end

local function compareAOBsPartial(aobStr1, aobStr2)
  local bytes1 = splitBytes(aobStr1)
  local bytes2 = splitBytes(aobStr2)
  local maxLen = math.max(#bytes1, #bytes2)
  local result = {}

  for i = 1, maxLen do
    local b1 = bytes1[i] or "??"
    local b2 = bytes2[i] or "??"
    if b1 == "??" or b2 == "??" then
      table.insert(result, "??")
    else
      table.insert(result, compareBytePartial(b1, b2))
    end
  end
  return table.concat(result, " ")
end

-- "Comparison ??" mode (full byte)
local function compareByteFull(b1, b2)
  if b1:upper() == b2:upper() then
    return b1:upper()
  else
    return "??"
  end
end

local function compareAOBsFull(aobStr1, aobStr2)
  local bytes1 = splitBytes(aobStr1)
  local bytes2 = splitBytes(aobStr2)
  local maxLen = math.max(#bytes1, #bytes2)
  local result = {}

  for i = 1, maxLen do
    local b1 = bytes1[i] or "??"
    local b2 = bytes2[i] or "??"
    table.insert(result, compareByteFull(b1, b2))
  end
  return table.concat(result, " ")
end

-- Count the number of bytes (tokens separated by spaces) in an AoB string
local function countBytesFromAOB(aobStr)
  local count = 0
  for byte in aobStr:gmatch("%S+") do
    count = count + 1
  end
  return count
end

-- =============================
-- Main function to perform the comparison based on the chosen mode:
-- "full"  => Comparison ?? (full byte)
-- "partial" => Comparison ? (nibble by nibble)
local function doComparison(compareMode)
  local fullText1 = inputMemo1.Lines.Text
  local fullText2 = inputMemo2.Lines.Text

  if fullText1 == nil or fullText1 == '' or fullText2 == nil or fullText2 == '' then
    showMessage("You must paste both scripts to analyze!")
    return
  end

  local injectionBlock1 = extractInjectionBlock(fullText1)
  local injectionBlock2 = extractInjectionBlock(fullText2)

  if not injectionBlock1 or not injectionBlock2 then
    showMessage("Unable to locate the injection block in either of the two scripts!")
    return
  end

  local aobBefore1, aobAfter1 = extractAOB(injectionBlock1)
  local aobBefore2, aobAfter2 = extractAOB(injectionBlock2)

  local comparedBefore, comparedAfter

  if compareMode == "partial" then
    comparedBefore = compareAOBsPartial(aobBefore1, aobBefore2)
    comparedAfter  = compareAOBsPartial(aobAfter1, aobAfter2)
  else
    comparedBefore = compareAOBsFull(aobBefore1, aobBefore2)
    comparedAfter  = compareAOBsFull(aobAfter1, aobAfter2)
  end

  local countBefore = countBytesFromAOB(comparedBefore)
  local countAfter  = countBytesFromAOB(comparedAfter)

  local outputTextBefore = "Compared Before Injecting point Bytes:\n" ..
                           comparedBefore .. "\n" ..
                           "Byte count: " .. countBefore .. "\n" ..
                           string.format("0x%X", countBefore) .. "\n" ..
                           string.format("%X", countBefore)

  local outputTextAfter = "Compared AoB from Injecting Point:\n" ..
                          comparedAfter .. "\n" ..
                          "Byte count: " .. countAfter .. "\n" ..
                          string.format("0x%X", countAfter) .. "\n" ..
                          string.format("%X", countAfter)

  outputMemoBefore.Lines.Text = outputTextBefore
  outputMemoAfter.Lines.Text  = outputTextAfter
end

-- =============================
-- Associate OnClick events to the buttons
compareFullButton.OnClick = function(sender)
  doComparison("full")   -- Comparison ??
end

comparePartialButton.OnClick = function(sender)
  doComparison("partial")   -- Comparison ?
end

-- =============================
-- Display the form
form.Show()
Video:
Spoiler
Script:
AoB Scripts Comparator Bar [Next to Help].CT
(13.46 KiB) Downloaded 48 times

----

4) Pure AoB Comparator:

This tool is used to compare two arrays ('Pure' without extracting them from a script). The differing bytes will be replaced with a ? or two ??.

Code:
Spoiler

Code: Select all

form = createForm()
form.Caption = 'Array Comparator'
form.Width = 800
form.Height = 600

-- Input memos: Array 1 and Array 2
input1_memo = createMemo(form)
input1_memo.Left = 10
input1_memo.Top = 30
input1_memo.Width = 350
input1_memo.Height = 300
input1_memo.ScrollBars = ssVertical

input2_memo = createMemo(form)
input2_memo.Left = 370
input2_memo.Top = 30
input2_memo.Width = 350
input2_memo.Height = 300
input2_memo.ScrollBars = ssVertical

-- Label for input memos
label1 = createLabel(form)
label1.Caption = 'Array 1:'
label1.Left = input1_memo.Left
label1.Top = input1_memo.Top - 20

label2 = createLabel(form)
label2.Caption = 'Array 2:'
label2.Left = input2_memo.Left
label2.Top = label1.Top

-- Comparison buttons
btn_single = createButton(form)
btn_single.Caption = 'Comparison ?'
btn_single.Width = 120
btn_single.Height = 30
btn_single.Left = 10
btn_single.Top = 340

btn_double = createButton(form)
btn_double.Caption = 'Comparison ??'
btn_double.Width = 120
btn_double.Height = 30
btn_double.Left = btn_single.Left + btn_single.Width + 10
btn_double.Top = btn_single.Top

-- Memo for the result
result_memo = createMemo(form)
result_memo.Left = 10
result_memo.Top = 400
result_memo.Width = 710
result_memo.Height = 150
result_memo.ScrollBars = ssBoth
result_memo.ReadOnly = true

result_label = createLabel(form)
result_label.Caption = 'Result:'
result_label.Left = result_memo.Left
result_label.Top = result_memo.Top - 20

-- Function for parsing lines
function parseLine(line)
    line = line:upper():gsub('[^%x]', '')
    local bytes = {}
    for i = 1, #line, 2 do
        local byte = line:sub(i, i+1)
        if #byte == 2 then
            table.insert(bytes, byte)
        end
    end
    return bytes
end

-- Full input parsing
function parseInput(lines)
    local array = {}
    for i = 0, lines.Count - 1 do
        local line = lines[i]
        local bytes = parseLine(line)
        table.insert(array, bytes)
    end
    return array
end

-- Logic for comparing individual bytes
function compareBytes(a, b, mode)
    if not a or not b then
        return '??'
    end
    if a == b then
        return a
    end
    if mode == '?' then
        local res = ''
        for i = 1, 2 do
            local c1 = a:sub(i, i)
            local c2 = b:sub(i, i)
            res = res .. (c1 == c2 and c1 or '?')
        end
        return res
    elseif mode == '??' then
        return '??'
    end
    return '??'
end

-- Main comparison function
function compareAndDisplay(mode)
    local array1 = parseInput(input1_memo.Lines)
    local array2 = parseInput(input2_memo.Lines)

    local result_lines = {}
    local byte_count = 0

    local max_lines = math.max(#array1, #array2)
    for line_i = 1, max_lines do
        local line1 = array1[line_i] or {}
        local line2 = array2[line_i] or {}
        local max_bytes = math.max(#line1, #line2)

        local line_result = {}
        for byte_i = 1, max_bytes do
            local b1 = line1[byte_i]
            local b2 = line2[byte_i]
            table.insert(line_result, compareBytes(b1, b2, mode))
        end

        table.insert(result_lines, table.concat(line_result, ' '))
        byte_count = byte_count + max_bytes
    end

    local result_text = table.concat(result_lines, '\r\n')
    result_text = result_text .. '\r\nByte count: ' .. byte_count
    result_text = result_text .. '\r\n0x' .. string.format('%X', byte_count)
    result_text = result_text .. '\r\n' .. string.format('%X', byte_count)

    result_memo.Lines.Text = result_text
end

-- Linking buttons to the function
btn_single.OnClick = function() compareAndDisplay('?') end
btn_double.OnClick = function() compareAndDisplay('??') end

form.show()
Video:
Spoiler
Script:
Pure AoB Comparator Bar [Next to Help].CT
(5.82 KiB) Downloaded 45 times


-----

5) Complete AoB Comparator:

This tool is an innovative integration of the 'AoB <Scripts> Comparator' and the 'Pure AoB Comparator,' merging their functionalities into a single, user-friendly GUI. This comprehensive interface allows for seamless comparisons, enhancing usability and efficiency in one consolidated application.

Code:
Spoiler

Code: Select all

-- ======================================================================
-- Super Comparator
-- Combines AOB Comparator (from Script 2) and Array Comparator (from Script 1)
-- ======================================================================

-- =============================================
-- Function to create a "Paste" menu on memos
-- =============================================
local function attachPastePopup(memoControl, form)
  local popup = createPopupMenu(form)
  local menuItemPaste = createMenuItem(popup)
  menuItemPaste.Caption = 'Paste'
  menuItemPaste.OnClick = function(sender)
    local clipboardText = getClipboardText()
    if clipboardText then
      -- If the memo belongs to the Array Comparator section, remove newline characters
      if memoControl.Name == "array1" or memoControl.Name == "array2" then
        clipboardText = clipboardText:gsub("[\r\n]+", " ")
      end
      memoControl.Lines.Text = clipboardText
    else
      showMessage("The clipboard does not contain any text!")
    end
  end
  popup.Items.add(menuItemPaste)

  memoControl.OnMouseUp = function(sender, x, y, button)
    if button == 2 then
      local globalX = memoControl.ClientToScreenX(x)
      local globalY = memoControl.ClientToScreenY(y)
      popup.Popup(globalX, globalY)
    end
  end
end

--------------------------------------------------------------------------------
-- AOB COMPARATOR SECTION (from SCRIPT 2)
--------------------------------------------------------------------------------

-- Function to isolate the last injection block (contained between { and })
local function extractInjectionBlock(fullText)
  local lastOpen = nil
  for pos = #fullText, 1, -1 do
    if fullText:sub(pos, pos) == "{" then
      lastOpen = pos
      break
    end
  end
  if not lastOpen then return nil end
  local lastClose = fullText:find("}", lastOpen, true)
  if not lastClose then return nil end
  local block = fullText:sub(lastOpen + 1, lastClose - 1)
  return block
end

-- Function to extract the AoB sequences from the injection block.
-- Returns:
--   aobBefore: concatenated bytes (separated by a space) until the "INJECTING HERE" marker
--   aobAfter: concatenated bytes after the "INJECTING HERE" marker
local function extractAOB(blockText)
  local arrBefore = {}
  local arrAfter = {}
  local currentGroup = "before"
  for line in blockText:gmatch("([^\n]+)") do
    if line:find("INJECTING HERE") then
      currentGroup = "after"
    elseif line:find("DONE INJECTING") then
      -- ignore marker
    elseif line:match("^%s*%{") or line:match("^%s*%}") or 
           line:find("ORIGINAL CODE") or line:match("^%s*//") then
      -- ignore non-significant lines
    else
      local bytesStr = line:match(":%s*([0-9A-Fa-f ]+)%s*%-")
      if not bytesStr then
        bytesStr = line:match(":%s*([0-9A-Fa-f ]+)%s*$")
      end
      if bytesStr then
        bytesStr = bytesStr:gsub("^%s+", ""):gsub("%s+$", "")
        if #bytesStr > 0 then
          if currentGroup == "before" then
            table.insert(arrBefore, bytesStr)
          else
            table.insert(arrAfter, bytesStr)
          end
        end
      end
    end
  end
  local function joinBytes(arr)
    local s = table.concat(arr, " ")
    s = s:gsub("%s+", " ")
    return s
  end
  return joinBytes(arrBefore), joinBytes(arrAfter)
end

-- Splits the AoB string into tokens (bytes separated by spaces)
local function splitBytes(aobStr)
  local bytes = {}
  for byte in aobStr:gmatch("%S+") do
    table.insert(bytes, byte)
  end
  return bytes
end

-- Compare nibble by nibble (Partial Comparison mode)
local function compareBytePartial(b1, b2)
  local result = ""
  local n1 = b1:sub(1,1)
  local n2 = b2:sub(1,1)
  if n1:upper() == n2:upper() then
    result = result .. n1:upper()
  else
    result = result .. "?"
  end
  n1 = b1:sub(2,2)
  n2 = b2:sub(2,2)
  if n1:upper() == n2:upper() then
    result = result .. n1:upper()
  else
    result = result .. "?"
  end
  return result
end

-- Partial Comparison: comparing nibble by nibble
local function compareAOBsPartial(aobStr1, aobStr2)
  local bytes1 = splitBytes(aobStr1)
  local bytes2 = splitBytes(aobStr2)
  local maxLen = math.max(#bytes1, #bytes2)
  local result = {}
  for i = 1, maxLen do
    local b1 = bytes1[i] or "??"
    local b2 = bytes2[i] or "??"
    if b1 == "??" or b2 == "??" then
      table.insert(result, "??")
    else
      table.insert(result, compareBytePartial(b1, b2))
    end
  end
  return table.concat(result, " ")
end

-- Full Comparison: if bytes do not match, return "??"
local function compareByteFull(b1, b2)
  if b1:upper() == b2:upper() then
    return b1:upper()
  else
    return "??"
  end
end

local function compareAOBsFull(aobStr1, aobStr2)
  local bytes1 = splitBytes(aobStr1)
  local bytes2 = splitBytes(aobStr2)
  local maxLen = math.max(#bytes1, #bytes2)
  local result = {}
  for i = 1, maxLen do
    local b1 = bytes1[i] or "??"
    local b2 = bytes2[i] or "??"
    table.insert(result, compareByteFull(b1, b2))
  end
  return table.concat(result, " ")
end

-- Count the tokens (bytes) in an AoB string
local function countBytesFromAOB(aobStr)
  local count = 0
  for byte in aobStr:gmatch("%S+") do
    count = count + 1
  end
  return count
end

--------------------------------------------------------------------------------
-- ARRAY COMPARATOR SECTION (from SCRIPT 1 - unchanged)
--------------------------------------------------------------------------------

local function parseInput(text)
  text = text:gsub("^%s+", ""):gsub("%s+$", "")
  local tokens = {}
  for token in text:gmatch("([0-9A-Fa-f%?][0-9A-Fa-f%?])") do
    table.insert(tokens, token)
  end
  return tokens
end

local function compareBytes(a, b, mode)
  local A = a:upper()
  local B = b:upper()
  if A == B then
    return A
  end
  if mode == "??" then
    return "??"
  elseif mode == "?" then
    local high = (A:sub(1,1) == B:sub(1,1)) and A:sub(1,1) or "?"
    local low  = (A:sub(2,2) == B:sub(2,2)) and A:sub(2,2) or "?"
    return high .. low
  else
    return "??"
  end
end

--------------------------------------------------------------------------------
-- MAIN FUNCTION: "Super Comparator" Form
--------------------------------------------------------------------------------
local function showSuperComparatorForm()
  local form = createForm()
  form.Caption = 'Super Comparator'
  form.Width = 900
  form.Height = 900
  form.Position = poScreenCenter

  --------------------------------------------------------------------------------
  -- AOB Comparator Section (top) - uses Script 2 functions
  --------------------------------------------------------------------------------
  local aobGroup = createGroupBox(form)
  aobGroup.Caption = 'AOB Comparator'
  aobGroup.Left = 10
  aobGroup.Top = 10
  aobGroup.Width = 880
  aobGroup.Height = 400

  local inputLabel1 = createLabel(aobGroup)
  inputLabel1.Caption = 'Script 1 (paste script with right click -> Paste):'
  inputLabel1.Left = 10
  inputLabel1.Top = 20

  local inputLabel2 = createLabel(aobGroup)
  inputLabel2.Caption = 'Script 2 (paste script with right click -> Paste):'
  inputLabel2.Left = 460
  inputLabel2.Top = 20

  local inputMemo1 = createMemo(aobGroup)
  inputMemo1.Left = 10
  inputMemo1.Top = 40
  inputMemo1.Width = 430
  inputMemo1.Height = 150
  inputMemo1.ScrollBars = ssBoth
  inputMemo1.WordWrap = false
  attachPastePopup(inputMemo1, form)

  local inputMemo2 = createMemo(aobGroup)
  inputMemo2.Left = 460
  inputMemo2.Top = 40
  inputMemo2.Width = 430
  inputMemo2.Height = 150
  inputMemo2.ScrollBars = ssBoth
  inputMemo2.WordWrap = false
  attachPastePopup(inputMemo2, form)

  local compareFullButton = createButton(aobGroup)
  compareFullButton.Caption = 'Full Comparison (??)'
  compareFullButton.Left = 10
  compareFullButton.Top = 200
  compareFullButton.Width = 120

  local comparePartialButton = createButton(aobGroup)
  comparePartialButton.Caption = 'Partial Comparison (?)'
  comparePartialButton.Left = 140
  comparePartialButton.Top = 200
  comparePartialButton.Width = 120

  local beforeLabel = createLabel(aobGroup)
  beforeLabel.Caption = 'Compared Bytes BEFORE Injecting Point:'
  beforeLabel.Left = 10
  beforeLabel.Top = 240

  local outputMemoBefore = createMemo(aobGroup)
  outputMemoBefore.Left = 10
  outputMemoBefore.Top = 260
  outputMemoBefore.Width = 430
  outputMemoBefore.Height = 120
  outputMemoBefore.ReadOnly = true
  outputMemoBefore.ScrollBars = ssBoth
  outputMemoBefore.WordWrap = false

  local afterLabel = createLabel(aobGroup)
  afterLabel.Caption = 'Compared AoB AFTER Injecting Point:'
  afterLabel.Left = 460
  afterLabel.Top = 240

  local outputMemoAfter = createMemo(aobGroup)
  outputMemoAfter.Left = 460
  outputMemoAfter.Top = 260
  outputMemoAfter.Width = 430
  outputMemoAfter.Height = 120
  outputMemoAfter.ReadOnly = true
  outputMemoAfter.ScrollBars = ssBoth
  outputMemoAfter.WordWrap = false

  -- Main function to perform the AoB comparison
  local function doAOBComparison(compareMode)
    local fullText1 = inputMemo1.Lines.Text
    local fullText2 = inputMemo2.Lines.Text
    if not fullText1 or fullText1 == '' or not fullText2 or fullText2 == '' then
      showMessage("Both scripts must be pasted for analysis!")
      return
    end

    local injectionBlock1 = extractInjectionBlock(fullText1)
    local injectionBlock2 = extractInjectionBlock(fullText2)
    if not injectionBlock1 or not injectionBlock2 then
      showMessage("Unable to locate the injection block in one of the scripts!")
      return
    end

    local aobBefore1, aobAfter1 = extractAOB(injectionBlock1)
    local aobBefore2, aobAfter2 = extractAOB(injectionBlock2)

    local comparedBefore, comparedAfter
    if compareMode == "partial" then
      comparedBefore = compareAOBsPartial(aobBefore1, aobBefore2)
      comparedAfter  = compareAOBsPartial(aobAfter1, aobAfter2)
    else
      comparedBefore = compareAOBsFull(aobBefore1, aobBefore2)
      comparedAfter  = compareAOBsFull(aobAfter1, aobAfter2)
    end

    local countBefore = countBytesFromAOB(comparedBefore)
    local countAfter  = countBytesFromAOB(comparedAfter)
    local outputTextBefore = "Compared Bytes BEFORE Injecting Point:\n" ..
                             comparedBefore .. "\n" ..
                             "Byte count: " .. countBefore .. "\n" ..
                             string.format("0x%X", countBefore) .. "\n" ..
                             string.format("%X", countBefore)
    local outputTextAfter = "Compared AoB AFTER Injecting Point:\n" ..
                            comparedAfter .. "\n" ..
                            "Byte count: " .. countAfter .. "\n" ..
                            string.format("0x%X", countAfter) .. "\n" ..
                            string.format("%X", countAfter)
    outputMemoBefore.Lines.Text = outputTextBefore
    outputMemoAfter.Lines.Text  = outputTextAfter
  end

  compareFullButton.OnClick = function(sender)
    doAOBComparison("full")
  end
  comparePartialButton.OnClick = function(sender)
    doAOBComparison("partial")
  end

  --------------------------------------------------------------------------------
  -- Array Comparator Section (bottom) - taken exactly from SCRIPT 1
  --------------------------------------------------------------------------------
  local arrayGroup = createGroupBox(form)
  arrayGroup.Caption = 'Array Comparator'
  arrayGroup.Left = 10
  arrayGroup.Top = 420
  arrayGroup.Width = 880
  arrayGroup.Height = 400

  local array_label1 = createLabel(arrayGroup)
  array_label1.Caption = 'Array 1:'
  array_label1.Left = 10
  array_label1.Top = 10

  local array_input1_memo = createMemo(arrayGroup)
  array_input1_memo.Left = 10
  array_input1_memo.Top = 30
  array_input1_memo.Width = 430
  array_input1_memo.Height = 150
  array_input1_memo.ScrollBars = ssVertical
  array_input1_memo.WordWrap = false
  --array_input1_memo.Name = "array1"
  array_input1_memo.OnChange = function(sender)
    local newText = sender.Lines.Text:gsub("[\r\n]+", " ")
    if newText ~= sender.Lines.Text then
      sender.Lines.Text = newText
    end
    sender.SelStart = #newText
  end
  attachPastePopup(array_input1_memo, form)

  local array_label2 = createLabel(arrayGroup)
  array_label2.Caption = 'Array 2:'
  array_label2.Left = 460
  array_label2.Top = 10

  local array_input2_memo = createMemo(arrayGroup)
  array_input2_memo.Left = 460
  array_input2_memo.Top = 30
  array_input2_memo.Width = 430
  array_input2_memo.Height = 150
  array_input2_memo.ScrollBars = ssVertical
  array_input2_memo.WordWrap = false
  --array_input2_memo.Name = "array2"
  array_input2_memo.OnChange = function(sender)
    local newText = sender.Lines.Text:gsub("[\r\n]+", " ")
    if newText ~= sender.Lines.Text then
      sender.Lines.Text = newText
    end
    sender.SelStart = #newText
  end
  attachPastePopup(array_input2_memo, form)

  local array_btn_single = createButton(arrayGroup)
  array_btn_single.Caption = 'Partial Comparison (?)'
  array_btn_single.Width = 120
  array_btn_single.Height = 30
  array_btn_single.Left = 10
  array_btn_single.Top = 190

  local array_btn_double = createButton(arrayGroup)
  array_btn_double.Caption = 'Full Comparison (??)'
  array_btn_double.Width = 120
  array_btn_double.Height = 30
  array_btn_double.Left = array_btn_single.Left + array_btn_single.Width + 10
  array_btn_double.Top = array_btn_single.Top

  local array_result_label = createLabel(arrayGroup)
  array_result_label.Caption = 'Result:'
  array_result_label.Left = 10
  array_result_label.Top = 230

  local array_result_memo = createMemo(arrayGroup)
  array_result_memo.Left = 10
  array_result_memo.Top = 250
  array_result_memo.Width = 880
  array_result_memo.Height = 150
  array_result_memo.ScrollBars = ssBoth
  array_result_memo.ReadOnly = true

  -- Function to perform the array comparison
  local function doArrayComparison(mode)
    local array1 = parseInput(array_input1_memo.Lines.Text)
    local array2 = parseInput(array_input2_memo.Lines.Text)
    local maxBytes = math.max(#array1, #array2)
    local resultTokens = {}
    for i = 1, maxBytes do
      local b1 = array1[i] or "??"
      local b2 = array2[i] or "??"
      table.insert(resultTokens, compareBytes(b1, b2, mode))
    end
    local byte_count = maxBytes
    local result_text = table.concat(resultTokens, " ") .. "\r\n" ..
                        "Byte count: " .. byte_count .. "\r\n" ..
                        "0x" .. string.format("%X", byte_count) .. "\r\n" ..
                        string.format("%X", byte_count)
    array_result_memo.Lines.Text = result_text
  end

  array_btn_single.OnClick = function() doArrayComparison("?") end
  array_btn_double.OnClick = function() doArrayComparison("??") end

  form.Show()
end

--------------------------------------------------------------------------------
-- Integration into the main menu: adds the "Super Comparator" button
--------------------------------------------------------------------------------
local function addSuperComparatorMenu()
  local mainForm = getMainForm()
  if not mainForm then return end
  local menuItems = mainForm.Menu.Items
  local insertIndex = menuItems.Count
  local targetCaption = "Help"
  for i = 0, menuItems.Count - 1 do
    if menuItems[i].Caption == targetCaption then
      insertIndex = i
      break
    end
  end
  local menuItem = createMenuItem(menuItems)
  menuItem.Caption = "Super Comparator"
  menuItem.OnClick = showSuperComparatorForm
  menuItems.insert(insertIndex, menuItem)
end

-- Add the button to the menu at startup
addSuperComparatorMenu()
Video:
Spoiler
Script (fixed):
Complete AoB Comparator GUI [Next to Help]_V2.CT
(16.25 KiB) Downloaded 34 times



.....
Last edited by SilverRabbit90 on Thu Apr 24, 2025 7:27 pm, edited 4 times in total.

User avatar
SilverRabbit90
Table Makers
Table Makers
Posts: 252
Joined: Fri Jan 15, 2021 12:01 am
Reputation: 219

Re: Some Lua Codes (Tools)

Post by SilverRabbit90 »

------

6) Wildcard AoB Generator [Soft]

This Tool is used to replace bytes with question marks (??) so that the correct AOB (the one related to our code) is more easily findable by Cheat Engine. This is a less intrusive version, so it may not be very useful in some games.

Code:
Spoiler

Code: Select all

-- Function to determine the length of any displacement starting from the modRM byte.
local function getDispLength(modrm)
  local modByte = tonumber(modrm, 16)
  local mod = math.floor(modByte / 64) -- bits 7-6
  local rm = modByte % 8 -- bits 2-0
  if mod == 0 then
    if rm == 5 then
      return 4 -- 32-bit displacement
    else
      return 0 -- no displacement
    end
  elseif mod == 1 then
    return 1 -- 8-bit displacement
  elseif mod == 2 then
    return 4 -- 32-bit displacement
  else
    return 0 -- no displacement (mod = 3)
  end
end

-- Extended list of unstable bytes (which can be replaced with wildcards)
local unstableBytes = {
  "00","01","02","03","04","05","06","07","08","09","0A","0B","0C","0D","0E","0F",
  "10","11","12","13","14","15","16","17","18","19","1A","1B","1C","1D","1E","1F",
  "20","21","22","23","24","25","26","27","28","29","2A","2B","2C","2D","2E","2F",
  "30","31","32","33","34","35","36","37","38","39","3A","3B","3C","3D","3E","3F",
  "80","81","82","83","84","85","86","87","88","89","8A","8B","8C","8D","8E","8F",
  "90","91","92","93","94","95","96","97","98","99","9A","9B","9C","9D","9E","9F",
  "A0","A1","A2","A3","A4","A5","A6","A7","A8","A9","AA","AB","AC","AD","AE","AF",
  "B0","B1","B2","B3","B4","B5","B6","B7","B8","B9","BA","BB","BC","BD","BE","BF",
  "C0","C1","C2","C3","C4","C5","C6","C7","C8","C9","CA","CB","CC","CD","CE","CF",
  "D0","D1","D2","D3","D4","D5","D6","D7","D8","D9","DA","DB","DC","DD","DE","DF",
  "E0","E1","E2","E3","E4","E5","E6","E7","E8","E9","EA","EB","EC","ED","EE","EF",
  "F0","F1","F2","F3","F4","F5","F6","F7","F8","F9","FA","FB","FC","FD","FE","FF"
}

-- List of stable bytes (which should not be replaced with wildcards)
local stableBytes = {
  "48","49","4A","4B","4C","4D","4E","4F", -- REX prefixes
  "66", -- Operand size prefix
  "F0", -- Lock prefix
  "F2","F3", -- REP prefixes
  "2E","3E","26","64","65", -- Segment overrides
  "67", -- Address size override
  "0F", -- Two-byte opcode escape
  "C3","C2","CB","CA", -- RET instructions
  "CC", -- INT3
  "90", -- NOP
  "EB", -- JMP short
  "E8","E9" -- CALL/JMP near
}

-- Table of opcodes that often have displacement/address
local opcodesWithDisplacement = {
  ["8B"] = true, -- MOV reg, [mem]
  ["89"] = true, -- MOV [mem], reg
  ["3B"] = true, -- CMP reg, [mem]
  ["83"] = true, -- Arithmetic ops with imm8
  ["FF"] = true -- Various (CALL, JMP, PUSH)
}

-- Function to check if a byte should be wildcarded
local function shouldWildcard(byte, context)
  -- Check if it's a prefix that should not be wildcarded
  for _, prefix in ipairs(stableBytes) do
    if byte == prefix then
      return false
    end
  end

  -- Check if it's an opcode that often has displacement
  if opcodesWithDisplacement[byte] then
    return false
  end

  -- Wildcard immediate operands after certain opcodes
  if context and context.lastOpcode then
    if context.lastOpcode == "B8" or context.lastOpcode == "B9" or context.lastOpcode == "BA" or
       context.lastOpcode == "BB" or context.lastOpcode == "BC" or context.lastOpcode == "BD" or
       context.lastOpcode == "BE" or context.lastOpcode == "BF" then
      -- MOV reg, imm32
      return true
    elseif context.lastOpcode == "68" or context.lastOpcode == "6A" then
      -- PUSH imm32/imm8
      return true
    elseif context.lastOpcode == "E8" or context.lastOpcode == "E9" then
      -- CALL/JMP rel32
      return true
    end
  end

  -- Wildcard by default
  return true
end

-- Main function to generate an AoB with "intelligent" wildcards
local function createIntelligentWildcardAoB(inputAOB)
  local byteArray = {}
  -- Split the input string into a table of bytes
  for byte in string.gmatch(inputAOB, "%S+") do
    byte = byte:upper()
    table.insert(byteArray, byte)
  end

  local AOBResult = {}
  local totalBytes = #byteArray
  if totalBytes < 1 then
    return ""
  end

  local context = {
    lastOpcode = nil,
    lastPrefix = nil,
    inModRM = false,
    dispLength = 0
  }

  local i = 1
  while i <= totalBytes do
    local currentByte = byteArray[i]

    -- If we are on the first byte, we keep it unchanged
    if i == 1 then
      table.insert(AOBResult, currentByte)
      context.lastOpcode = currentByte
      i = i + 1
      goto continue
    end

    -- Handle prefixes
    if currentByte == "66" or currentByte == "67" or currentByte == "F2" or currentByte == "F3" then
      context.lastPrefix = currentByte
      table.insert(AOBResult, currentByte)
      i = i + 1
      goto continue
    end

    -- Handle REX prefixes (x64)
    if currentByte:match("^[4-7][0-9A-F]$") then
      table.insert(AOBResult, currentByte)
      i = i + 1
      goto continue
    end

    -- Handle two-byte opcodes (0F ...)
    if currentByte == "0F" and i < totalBytes then
      context.lastOpcode = currentByte .. byteArray[i+1]
      table.insert(AOBResult, currentByte)
      table.insert(AOBResult, byteArray[i+1])
      i = i + 2
      goto continue
    end

    -- Handle normal opcodes
    if not context.lastOpcode then
      context.lastOpcode = currentByte
    end

    -- Handle MOD R/M and displacement
    if opcodesWithDisplacement[currentByte] and i < totalBytes then
      table.insert(AOBResult, currentByte)
      i = i + 1

      local modrm = byteArray[i]
      local dispLen = getDispLength(modrm)
      table.insert(AOBResult, modrm)
      i = i + 1

      -- Add wildcards for displacement
      for j = 1, dispLen do
        if i <= totalBytes then
          table.insert(AOBResult, "??")
          i = i + 1
        end
      end
      goto continue
    end

    -- Decision to wildcard or not
    if shouldWildcard(currentByte, context) then
      table.insert(AOBResult, "??")
    else
      table.insert(AOBResult, currentByte)
    end

    i = i + 1
    ::continue::
    context.lastOpcode = nil -- Reset after each instruction
  end

  return table.concat(AOBResult, " ")
end

-- Function for the GUI
local function showWildcardGenerator()
  local form = createForm()
  form.Caption = "Advanced AoB Wildcard Generator"
  form.Width = 600
  form.Height = 450
  form.Position = 'poScreenCenter'

  -- Input AoB
  local lblInput = createLabel(form)
  lblInput.Caption = "Enter AoB pattern (hex bytes separated by spaces):"
  lblInput.Left = 10
  lblInput.Top = 10
  lblInput.Width = 580

  local memoInput = createMemo(form)
  memoInput.Left = 10
  memoInput.Top = 30
  memoInput.Width = 580
  memoInput.Height = 150
  memoInput.ScrollBars = ssVertical
  memoInput.Text = "48 89 5C 24 08 48 89 74 24 10 57 48 83 EC 20 48 8B F9 48 8B F2 48 8B D9"

  -- Output AoB
  local lblOutput = createLabel(form)
  lblOutput.Caption = "Wildcard AoB:"
  lblOutput.Left = 10
  lblOutput.Top = 190
  lblOutput.Width = 580

  local memoOutput = createMemo(form)
  memoOutput.Left = 10
  memoOutput.Top = 210
  memoOutput.Width = 580
  memoOutput.Height = 150
  memoOutput.ScrollBars = ssVertical
  memoOutput.ReadOnly = true

  -- Buttons
  local btnGenerate = createButton(form)
  btnGenerate.Caption = "Generate Wildcard AoB"
  btnGenerate.Left = 10
  btnGenerate.Top = 370
  btnGenerate.Width = 180
  btnGenerate.OnClick = function()
    local inputText = memoInput.Lines.Text
    local wildcardAOB = createIntelligentWildcardAoB(inputText)
    memoOutput.Lines.Text = wildcardAOB
    writeToClipboard(wildcardAOB)
    speak("Generated")
  end

  local btnCopy = createButton(form)
  btnCopy.Caption = "Copy Result"
  btnCopy.Left = 200
  btnCopy.Top = 370
  btnCopy.Width = 100
  btnCopy.OnClick = function()
    writeToClipboard(memoOutput.Lines.Text)
    speak("Copied to clipboard")
  end

  local btnClose = createButton(form)
  btnClose.Caption = "Close"
  btnClose.Left = 310
  btnClose.Top = 370
  btnClose.Width = 100
  btnClose.OnClick = function()
    form.close()
  end
end

-- Function to add the menu to the main Cheat Engine window
local function addWildcardGeneratorMenu()
  local parent = getMainForm().Menu.Items -- Changed from getMemoryViewForm() to getMainForm()
  local menuItem = createMenuItem(parent)
  menuItem.Caption = "Wildcard Gen"
  menuItem.OnClick = showWildcardGenerator
  parent.add(menuItem)
end

-- Initialization
addWildcardGeneratorMenu()
showMessage("Advanced AoB Wildcard Generator loaded! Access it from the main menu.")
Video:
Spoiler
Script:
Wildcard AoB Generator [Soft].CT
(8.85 KiB) Downloaded 44 times

-------

7) Improved Wild Card AoB Generator:

This Tool is used to attempt to generate unique AOBs by creating wildcards in an array, replacing bytes with question marks (??). The generation is not perfect, but I have noticed that it often comes close, and it's a great tool for easily updating scripts.

Code:
Spoiler

Code: Select all

-- Function to determine the length of the possible displacement starting from the modRM byte.
local function getDispLength(modrm)
  local modByte = tonumber(modrm, 16)
  local mod = math.floor(modByte / 64) -- bits 7-6
  local rm = modByte % 8 -- bits 2-0
  if mod == 0 then
    if rm == 5 then
      return 4
    else
      return 0
    end
  elseif mod == 1 then
    return 1
  elseif mod == 2 then
    return 4
  else
    return 0
  end
end

-- Fallback function that uses an (older) list of stable and unstable bytes.
local function shouldWildcardFallback(byte)
  local unstableBytes = {
    "00", "FF", "90", "0F", "F3", "F2", "E8", "CC", "F1", "EB", "80", "81", "C7", "D9", "B8", "B0", "70", "30"
  }
  local stableBytes = {
    "48", "89", "8B", "83", "33", "74", "7E", "83", "C0", "F7", "EB", "FF", "C3", "F6"
  }
  for _, unstable in ipairs(unstableBytes) do
    if byte == unstable then
      return true
    end
  end
  for _, stable in ipairs(stableBytes) do
    if byte == stable then
      return false
    end
  end
  return true
end

-- Main function to generate an AoB with "intelligent" wildcards
local function createIntelligentWildcardAoB(inputAOB)
  local byteArray = {}
  -- Split the input string into a table of bytes (in hexadecimal format e.g. "F3", "0F", "10", etc.)
  for byte in string.gmatch(inputAOB, "%S+") do
    byte = byte:upper()
    table.insert(byteArray, byte)
  end

  local AOBResult = {}
  local totalBytes = #byteArray
  if totalBytes < 1 then
    return ""
  end

  -- Preserve the first byte (not wildcard-able)
  table.insert(AOBResult, byteArray[1])
  local i = 2

  while i <= totalBytes do
    local currentByte = byteArray[i]
    -- If we find a prefix that could indicate the presence of an instruction with displacement
    if (currentByte == "F3" or currentByte == "F2" or currentByte == "66") and (i+2 <= totalBytes) and (byteArray[i+1] == "0F") then
      local prefix = currentByte
      local twoByteOpcode = byteArray[i+1] -- "0F"
      local opcode = byteArray[i+2]
      if opcode == "10" or opcode == "11" or opcode == "5C" or opcode == "59" or opcode == "58" or opcode == "5E" then
        table.insert(AOBResult, prefix)
        table.insert(AOBResult, twoByteOpcode)
        table.insert(AOBResult, opcode)
        i = i + 3
        if i <= totalBytes then
          local modrm = byteArray[i]
          table.insert(AOBResult, modrm)
          i = i + 1
          local dispLen = getDispLength(modrm)
          for j = 1, dispLen do
            if i <= totalBytes then
              table.insert(AOBResult, "??")
              i = i + 1
            end
          end
          goto continue_loop
        end
      end
    end

    -- Fallback: if it doesn't fit any "intelligent" pattern, decide byte by byte
    if shouldWildcardFallback(currentByte) then
      table.insert(AOBResult, "??")
    else
      table.insert(AOBResult, currentByte)
    end
    i = i + 1
    ::continue_loop::
  end

  return table.concat(AOBResult, " ")
end

-- Callback function for the GUI
local function onInputAoBButtonClick()
  local inputForm = createForm(nil)
  inputForm.Caption = "Input AoB (one per line)"
  inputForm.Width = 400
  inputForm.Height = 400
  inputForm.Position = 'poScreenCenter'

  -- Create a memo for multi-line input
  local inputMemo = createMemo(inputForm)
  inputMemo.Width = 370
  inputMemo.Height = 150
  inputMemo.Top = 20
  inputMemo.Left = 10
  inputMemo.Text = ""

  -- Create a memo to display results
  local outputMemo = createMemo(inputForm)
  outputMemo.Width = 370
  outputMemo.Height = 100
  outputMemo.Top = 180
  outputMemo.Left = 10
  outputMemo.ReadOnly = true

  local generateButton = createButton(inputForm)
  generateButton.Caption = "Generate Wildcard AOB"
  generateButton.Top = 290
  generateButton.Left = 10
  generateButton.Width = 180
  generateButton.OnClick = function()
    -- Use .Lines.Text to get the complete text of the memo
    local inputText = inputMemo.Lines.Text
    local results = {}
    -- Splitting based on "\n" to extract each line
    for line in string.gmatch(inputText, "[^\n]+") do
      local trimmedLine = line:match("^%s*(.-)%s*$") -- removes spaces at the beginning and end
      if trimmedLine ~= "" then
        local wildcardAOB = createIntelligentWildcardAoB(trimmedLine)
        table.insert(results, wildcardAOB)
      end
    end

    local finalResult = table.concat(results, "\n")
    outputMemo.Lines.Text = finalResult
    writeToClipboard(finalResult)
  end

  local copyButton = createButton(inputForm)
  copyButton.Caption = "Copy Result"
  copyButton.Top = 290
  copyButton.Left = 200
  copyButton.Width = 80
  copyButton.OnClick = function()
    local finalResult = outputMemo.Lines.Text
    writeToClipboard(finalResult)
  end

  local cancelButton = createButton(inputForm)
  cancelButton.Caption = "Cancel"
  cancelButton.Top = 290
  cancelButton.Left = 290
  cancelButton.Width = 80
  cancelButton.OnClick = function()
    inputForm.close()
  end

  inputForm.show()
end

-- Function to add the input AoB menu to the main Cheat Engine window
local function addInputAOBMenu()
  local parent = getMainForm().Menu.Items -- Changed from getMemoryViewForm() to getMainForm()
  local inputAoBmenuitem = createMenuItem(parent)
  inputAoBmenuitem.Caption = "Better Wildcard Gen"
  inputAoBmenuitem.OnClick = onInputAoBButtonClick
  parent.add(inputAoBmenuitem)
end

-- Menu initialization
addInputAOBMenu()
Video:
Spoiler
Script:
Improved Wild Card AoB Generator.CT
(5.97 KiB) Downloaded 42 times

--------

8) generateAOBMenu (Generate Wildcards from "Memory View"):

This tool is used to generate wildcards directly from the 'Memory View' (of course, you must have found the cheat you want to modify if you plan to use it). It is very useful when you think that a script will be updated in the future, as the game will likely receive an update. The bytes will be replaced with question marks (??), which will make it easier to find unique AOBs for all versions of the game. This script is not mine; it was created by the user 'aSwedishMagyar' and then modified by '++METHOS.' I modified it again because I noticed it wasn't scanning games that didn't have a module; mine works even without modules. I made two versions: one without a GUI and one with a GUI (which is the one in the video).

Code without GUi:
Spoiler

Code: Select all

function getModuleName(base)
  local name = getNameFromAddress(base, true, false)
  local modules = enumModules()
  local currentModule = nil
  for k = 1, #modules do
    local startPoint = modules[k].Address
    local endPoint = getModuleSize(modules[k].Name)
    if base > startPoint and base < startPoint + endPoint then
      currentModule = modules[k]
      break
    end
  end
  if currentModule then
    return currentModule.Name
  end
  return nil
end

function checkAOB(bytes, curModule)
  local base = nil
  if curModule then
    base = curModule.Address
  else
    base = 0x0
  end

  local moduleStrSize = getModuleSize(curModule)
  moduleStrSize = moduleStrSize and moduleStrSize or 0x7fffffffffff

  local memScanner = createMemScan()
  local memFoundList = createFoundList(memScanner)

  memScanner.firstScan(
    soExactValue, vtByteArray, rtRounded, bytes, nil,
    base, base + moduleStrSize, "",
    fsmNotAligned, "", true, false, false, false)

  memScanner.waitTillDone()
  memFoundList.initialize()

  local foundUnique = false
  if memFoundList.Count == 1 then
    foundUnique = true
  end

  memScanner.destroy()
  memFoundList.destroy()

  return foundUnique
end

function generateWildcardAOB(base)
  local name = getNameFromAddress(base, true, false)
  local modules = enumModules()
  local currentModule = nil

  for k = 1, #modules do
    local startPoint = modules[k].Address
    local endPoint = getModuleSize(modules[k].Name)
    if base > startPoint and base < startPoint + endPoint then
      currentModule = modules[k]
      break
    end
  end

  -- If no module is found, create a fake module
  if currentModule == nil then
    currentModule = {
      Address = base,
      Name = getNameFromAddress(base, true, false) or 'Process',
      Is64Bit = targetIs64Bit()
    }
    -- Debug: showMessage("Module not found, using the selected address as a fake module")
  end

  local minLen = 2            -- Minimum length for the scan
  local maxLen = 201          -- Increased compared to 120 to obtain a longer AOB
  local wCardFormat = '??'
  local addSpace = false
  local AOB = createStringList()
  local AOBWildCard = ""
  local current = 0
  local isX64 = currentModule.Is64Bit or targetIs64Bit()

  -- Debug: print("Starting AOB scan for module: " .. currentModule.Name)
  local done = false
  maxLen = maxLen + minLen

  for i = 1, maxLen do
    local addr = base + current
    local size = getInstructionSize(addr)
    local byteVal = readBytes(addr, 1)
    local byte = string.format('%02X', byteVal)

    if byte == 'CC' then
      byte = wCardFormat
    end
    AOB.add(byte)

    if isX64 and checkOpCode(byteVal) then
      current = current + 1
      size = size - 1
      byte = string.format('%02X', readBytes(base + current, 1))
      if addSpace then
        AOB.add(' ')
      end
      AOB.add(byte)
    end

    AOBWildCard = string.gsub(AOB.text, "%c", "")

    -- If the current length exceeds the minimum, check if the AOB scans uniquely
    if i > minLen then
      if checkAOB(AOBWildCard, currentModule) then
        -- Debug: print("Unique AOB found after " .. (i - minLen) .. " iterations.")
        break
      end
    end

    current = current + size
    if addSpace then
      AOB.add(' ')
    end
    for j = 1, size - 1 do
      AOB.add(wCardFormat)
      if addSpace then
        AOB.add(' ')
      end
    end
  end

  AOBWildCard = string.gsub(AOB.text, "%c", "")
  AOB.destroy()

  if currentModule == nil then
    name = getNameFromAddress(base, true, false)
  else
    name = currentModule.Name
  end

  speak('Scan Completed')
  writeToClipboard(AOBWildCard)

  -- Debug: print("AOB generated for " .. name .. ":\n" .. AOBWildCard)
  return {AOBWildCard, name}
end

function checkOpCode(byteVal)
  if byteVal >= 0x40 and byteVal <= 0x49 then
    return true
  end
  if byteVal == 0x0F then
    return true
  end
  return false
end

function addGenerateAOBMenu()
  local parent = getMemoryViewForm().Menu.Items
  local generateAOBmenuitem = createMenuItem(parent)
  parent.add(generateAOBmenuitem)
  generateAOBmenuitem.Caption = 'Generate AOB'
  generateAOBmenuitem.OnClick = function()
    createThread(function(th)
      generateWildcardAOB(getMemoryViewForm().DisassemblerView.SelectedAddress)
    end)
  end
end

addGenerateAOBMenu()
Code with GUI:
Spoiler

Code: Select all

-- Global variable to hold the AoB Generator GUI (prevents it from being garbage collected)
aobGenForm = nil

------------------------------------------------------------------
-- Function that generates a wildcard AoB starting from a given base address.
-- minScan and maxScan control the maximum number of iterations.
-- progressCallback(current, total) is called on each iteration.
------------------------------------------------------------------
function generateWildcardAOB(base, minScan, maxScan, progressCallback)
  local minLen = minScan or 2
  local maxLen = maxScan or 200
  local name = getNameFromAddress(base, true, false)
  local modules = enumModules()
  local currentModule = nil

  for k = 1, #modules do
    local startPoint = modules[k].Address
    local modSize = getModuleSize(modules[k].Name)
    if base > startPoint and base < startPoint + modSize then
      currentModule = modules[k]
      break
    end
  end

  if not currentModule then
    currentModule = {
      Address = base,
      Name = getNameFromAddress(base, true, false) or "Process",
      Is64Bit = targetIs64Bit()
    }
  end

  local wCardFormat = "??"
  local addSpace = false
  local AOB = createStringList()
  local AOBWildCard = ""
  local current = 0
  local isX64 = currentModule.Is64Bit or targetIs64Bit()

  local totalIterations = minLen + maxLen

  for i = 1, totalIterations do
    if progressCallback then
      progressCallback(i, totalIterations)
    end

    local addr = base + current
    local size = getInstructionSize(addr)
    local byteVal = readBytes(addr, 1)
    local byte = string.format("%02X", byteVal)
    if byte == "CC" then
      byte = wCardFormat
    end
    AOB.add(byte)

    if isX64 and checkOpCode(byteVal) then
      current = current + 1
      size = size - 1
      byte = string.format("%02X", readBytes(base + current, 1))
      if addSpace then AOB.add(" ") end
      AOB.add(byte)
    end

    AOBWildCard = string.gsub(AOB.text, "%c", "")

    if i > minLen then
      if checkAOB(AOBWildCard, currentModule) then
        if progressCallback then
          progressCallback(totalIterations, totalIterations)  -- force 100%
        end
        break
      end
    end

    current = current + size
    if addSpace then AOB.add(" ") end
    for j = 1, size - 1 do
      AOB.add(wCardFormat)
      if addSpace then AOB.add(" ") end
    end
  end

  AOBWildCard = string.gsub(AOB.text, "%c", "")
  AOB.destroy()

  speak("Scan Completed")
  writeToClipboard(AOBWildCard)
  return {AOBWildCard, currentModule.Name}
end

------------------------------------------------------------------
-- Returns the name of the module in which the base address resides.
------------------------------------------------------------------
function getModuleName(base)
  local name = getNameFromAddress(base, true, false)
  local modules = enumModules()
  local currentModule = nil
  for k = 1, #modules do
    local startPoint = modules[k].Address
    local modSize = getModuleSize(modules[k].Name)
    if base > startPoint and base < startPoint + modSize then
      currentModule = modules[k]
      break
    end
  end
  if currentModule then
    return currentModule.Name
  end
  return nil
end

------------------------------------------------------------------
-- Checks if the AoB (byte array) is unique within the module.
------------------------------------------------------------------
function checkAOB(bytes, curModule)
  local base = curModule and curModule.Address or 0x0
  local moduleSize = getModuleSize(curModule) or 0x7fffffffffff
  local memScanner = createMemScan()
  local memFoundList = createFoundList(memScanner)

  memScanner.firstScan(
    soExactValue, vtByteArray, rtRounded, bytes, nil,
    base, base + moduleSize, "",
    fsmNotAligned, "", true, false, false, false
  )
  memScanner.waitTillDone()
  memFoundList.initialize()

  local foundUnique = (memFoundList.Count == 1)

  memScanner.destroy()
  memFoundList.destroy()
  return foundUnique
end

------------------------------------------------------------------
-- Checks if a byte requires special handling (for some opcodes).
------------------------------------------------------------------
function checkOpCode(byteVal)
  if byteVal >= 0x40 and byteVal <= 0x49 then
    return true
  end
  if byteVal == 0x0F then
    return true
  end
  return false
end

------------------------------------------------------------------
-- Creates and displays the AoB Generator GUI.
-- If the window is already open, it brings it to the front.
------------------------------------------------------------------
function createAOBGeneratorGUI()
  if aobGenForm then
    aobGenForm.BringToFront()
    return
  end

  aobGenForm = createForm(false)
  aobGenForm.Caption = "AoB Generator"
  aobGenForm.Width = 400
  aobGenForm.Height = 300
  aobGenForm.Position = poScreenCenter
  -- OnClose now calls sender.destroy() to allow proper closing of the form
  aobGenForm.OnClose = function(sender)
    aobGenForm = nil
    sender.destroy()
  end

  -- "Start & Change With ??" Button
  local btnStart = createButton(aobGenForm)
  btnStart.Caption = "Start & Change With ??"
  btnStart.Left = 10
  btnStart.Top = 10
  btnStart.Width = 180

  -- Label and Edit for "Min Scan Length"
  local lblMin = createLabel(aobGenForm)
  lblMin.Caption = "Min Scan Length:"
  lblMin.Left = 10
  lblMin.Top = btnStart.Top + btnStart.Height + 10
  lblMin.AutoSize = true

  local edtMin = createEdit(aobGenForm)
  edtMin.Text = "2"
  edtMin.Left = lblMin.Left + lblMin.Width + 5
  edtMin.Top = lblMin.Top
  edtMin.Width = 50

  -- Label and Edit for "Max Scan Length"
  local lblMax = createLabel(aobGenForm)
  lblMax.Caption = "Max Scan Length:"
  lblMax.Left = edtMin.Left + edtMin.Width + 10
  lblMax.Top = lblMin.Top
  lblMax.AutoSize = true

  local edtMax = createEdit(aobGenForm)
  edtMax.Text = "200"
  edtMax.Left = lblMax.Left + lblMax.Width + 5
  edtMax.Top = lblMax.Top
  edtMax.Width = 50

  -- Progress Bar
  local progressBar = createProgressBar(aobGenForm)
  progressBar.Left = 10
  progressBar.Top = lblMin.Top + lblMin.Height + 30
  progressBar.Width = aobGenForm.ClientWidth - 20
  progressBar.Min = 0
  progressBar.Max = 100
  progressBar.Position = 0

  -- Label for the progress percentage
  local lblProgress = createLabel(aobGenForm)
  lblProgress.Caption = "0%"
  lblProgress.Left = progressBar.Left + progressBar.Width - 100
  lblProgress.Top = progressBar.Top - 20
  lblProgress.Width = 100
  lblProgress.AutoSize = true

  -- Memo to display the generated AoB
  local edtResult = createMemo(aobGenForm)
  edtResult.Left = 10
  edtResult.Top = progressBar.Top + progressBar.Height + 30
  edtResult.Width = aobGenForm.ClientWidth - 20
  edtResult.Height = 80

  -- Function to update the progress bar and label
  local function progressUpdate(current, total)
    local percent = math.floor((current / total) * 100)
    if percent < 100 then
      progressBar.Position = percent
      lblProgress.Caption = percent .. "%"
    else
      progressBar.Position = 100
      lblProgress.Caption = "100% Scan Completed"
    end
  end

  btnStart.OnClick = function()
    local minScan = tonumber(edtMin.Text)
    local maxScan = tonumber(edtMax.Text)
    if not minScan or not maxScan then
      showMessage("Please enter valid numbers for scan lengths.")
      return
    end

    progressBar.Position = 0
    lblProgress.Caption = "0%"
    edtResult.Lines.Clear()  -- Clear any previous result

    createThread(function()
      local selectedAddress = getMemoryViewForm().DisassemblerView.SelectedAddress
      local result = generateWildcardAOB(
        selectedAddress,
        minScan,
        maxScan,
        function(current, total)
          synchronize(function()
            progressUpdate(current, total)
          end)
        end
      )
      synchronize(function()
        edtResult.Lines.Text = result[1]
      end)
    end)
  end

  aobGenForm.show()
end

------------------------------------------------------------------
-- Adds the "Generate AOB" menu item to the Memory View.
------------------------------------------------------------------
function addGenerateAOBMenu()
  local parent = getMemoryViewForm().Menu.Items
  local generateAOBmenuitem = createMenuItem(parent)
  parent.add(generateAOBmenuitem)
  generateAOBmenuitem.Caption = "Generate AOB [GUI]"
  generateAOBmenuitem.OnClick = function()
    createAOBGeneratorGUI()
  end
end

addGenerateAOBMenu()
Video:
Spoiler
Scripts:
generateAOBMenu.CT
(13.73 KiB) Downloaded 49 times

User avatar
SilverRabbit90
Table Makers
Table Makers
Posts: 252
Joined: Fri Jan 15, 2021 12:01 am
Reputation: 219

Re: Some Lua Codes (Tools)

Post by SilverRabbit90 »

---------

9) AoB Scan:

This tool is used to quickly scan for the ignition point of the code. Simply right-click on a script (in Cheat Engine's main window) and select 'AoB Scan' from the context menu; the tool will automatically scan the array and open the 'Memory View,' displaying the ignition point of the code contained in the script.

Code:
Spoiler

Code: Select all

--[[
This script adds the "AoB Scan" option to PopupMenu2 of Cheat Engine's Address List.
When a record of type AutoAssembler (script) is clicked, the code:
  1. Parses the Script field to extract the first AOB command;
  2. Executes AOBScan to find the corresponding address;
  3. Brings the Memory View to that address (injection point).
The "AoB Scan" option appears only if the selected record is a script.
--]]

-----------------------------------------------------------
-- Function to extract AOB information from the script text --
-----------------------------------------------------------
local function extractAOBsFromScript(scriptText)
    local aobs = {}
    -- Remove comments and extra spaces
    local cleanedText = scriptText:gsub("//.*", ""):gsub("%-%-.*", ""):gsub("%s+", " ")
    for aobType, params in cleanedText:gmatch("aobscan(%w*)%s*%(([^%)]+)%)") do
        local parts = {}
        for param in params:gmatch("[^,]+") do
            local cleanedParam = param:gsub("^%s+", ""):gsub("%s+$", "")
            table.insert(parts, cleanedParam)
        end
        if #parts >= 2 then
            local aobInfo = {
                type = "aobscan" .. (aobType or ""),
                name = parts[1],
                pattern = parts[#parts]
            }
            if aobType == "module" and #parts >= 3 then
                aobInfo.module = parts[2]
            elseif aobType == "region" and #parts >= 5 then
                aobInfo.startAddress = parts[2]
                aobInfo.endAddress = parts[3]
                aobInfo.protection = parts[4]
            end
            table.insert(aobs, aobInfo)
        end
    end
    return aobs
end

-----------------------------------------------------------
-- Function to bring the Memory View to the injection point --
-----------------------------------------------------------
function gotoInjectionPoint()
    local rec = getAddressList().getSelectedRecord()
    if not rec then
        showMessage("No record selected!")
        return
    end
    if rec.Type ~= vtAutoAssembler then
        showMessage("The selected record is not a script!")
        return
    end

    local scriptText = rec.Script
    if not scriptText or scriptText == "" then
        showMessage("The Script field is empty!")
        return
    end

    local aobs = extractAOBsFromScript(scriptText)
    if #aobs == 0 then
        showMessage("No AOB command found in the script!")
        return
    end

    -- Take the first AOB command found
    local aob = aobs[1]
    local addresses = AOBScan(aob.pattern)
    if not addresses or addresses.Count == 0 then
        showMessage("AOB not found: " .. aob.pattern)
        return
    end

    local foundAddress = tonumber("0x" .. addresses[0])
    addresses.Destroy()

    local memView = getMemoryViewForm()
    if not memView then
        memView = createMemoryView()
        if not memView then
            showMessage("Unable to create Memory View!")
            return
        end
    end

    memView.DisassemblerView.SelectedAddress = foundAddress
    memView.Show()  -- Bring the Memory View to the front
   -- showMessage("Injection point reached at: " .. string.format("%X", foundAddress))
end

-------------------------------------------------------------------------------------------
-- Function to add the "AoB Scan" option to PopupMenu2 (Address List) --
-- so that it appears only if the selected record is a script (vtAutoAssembler).  --
-------------------------------------------------------------------------------------------
function addAOBScanOptionForScriptsOnly()
    local mainForm = getMainForm()
    if not mainForm then
        print("Error: Unable to locate Cheat Engine MainForm.")
        return
    end

    local popupMenu = nil
    for i = 0, mainForm.ComponentCount - 1 do
        local comp = mainForm.getComponent(i)
        if comp.ClassName == "TPopupMenu" and comp.Name == "PopupMenu2" then
            popupMenu = comp
           -- print("INFO: Found PopupMenu associated with the address table:", comp.Name)
            break
        end
    end

    if not popupMenu then
        print("Error: No PopupMenu associated with the address table found.")
        return
    end

    local miAOBScan = createMenuItem(popupMenu)
    miAOBScan.Caption = "AoB Scan"
    miAOBScan.OnClick = gotoInjectionPoint
    miAOBScan.Visible = false  -- Hidden by default
    popupMenu.Items.add(miAOBScan)

    popupMenu.OnPopup = function(sender)
        local selectedRecord = getAddressList().getSelectedRecord()
        if selectedRecord and selectedRecord.Type == vtAutoAssembler then
            miAOBScan.Visible = true
        else
            miAOBScan.Visible = false
        end
    end

   -- print("INFO: The 'AoB Scan' option has been added to PopupMenu2 (scripts only).")
end

-- Execute the function to add the "AoB Scan" option (scripts only)
addAOBScanOptionForScriptsOnly()
Video:
Spoiler
Script:
AoB Scan [No Gui].CT
(5.42 KiB) Downloaded 55 times

----------

10) Aob Scan and Accesses addresses (auto open "Find out what addresses this instruction accesses")
This code scans the array within a script and opens 'Find out what addresses this instruction accesses' to see what accesses that point in memory.

Code:
Spoiler

Code: Select all

--[[
This script combines two functionalities into a single script.
• The "AoB Scan" button performs an AoB scan on the selected script and brings the Memory View to the found address.
• The "Aob Scan and Accesses addresses" button does the same and also attempts to automatically trigger
  "Find out what addresses this instruction accesses" in the DisassemblerView.
Both buttons appear in the context menu (PopupMenu2) only if the selected record is an AutoAssembler script.
--]]

-----------------------------------------------------------
-- Function to extract AOB information from the script text --
-----------------------------------------------------------
local function extractAOBsFromScript(scriptText)
    local aobs = {}
    -- Remove comments and extra spaces
    local cleanedText = scriptText:gsub("//.*", ""):gsub("%-%-.*", ""):gsub("%s+", " ")
    for aobType, params in cleanedText:gmatch("aobscan(%w*)%s*%(([^%)]+)%)") do
        local parts = {}
        for param in params:gmatch("[^,]+") do
            local cleanedParam = param:gsub("^%s+", ""):gsub("%s+$", "")
            table.insert(parts, cleanedParam)
        end
        if #parts >= 2 then
            local aobInfo = {
                type = "aobscan" .. (aobType or ""),
                name = parts[1],
                pattern = parts[#parts]
            }
            if aobType == "module" and #parts >= 3 then
                aobInfo.module = parts[2]
            elseif aobType == "region" and #parts >= 5 then
                aobInfo.startAddress = parts[2]
                aobInfo.endAddress   = parts[3]
                aobInfo.protection   = parts[4]
            end
            table.insert(aobs, aobInfo)
        end
    end
    return aobs
end

-----------------------------------------------------------
-- Function: Bring the Memory View to the injection point (simple AoB Scan) --
-----------------------------------------------------------
local function gotoInjectionPointSimple()
    local rec = getAddressList().getSelectedRecord()
    if not rec then
        showMessage("No record selected!")
        return
    end
    if rec.Type ~= vtAutoAssembler then
        showMessage("The selected record is not a script!")
        return
    end

    local scriptText = rec.Script
    if not scriptText or scriptText == "" then
        showMessage("The Script field is empty!")
        return
    end

    local aobs = extractAOBsFromScript(scriptText)
    if #aobs == 0 then
        showMessage("No AOB command found in the script!")
        return
    end

    -- Use the first AOB command found
    local aob = aobs[1]
    local addresses = AOBScan(aob.pattern)
    if not addresses or addresses.Count == 0 then
        showMessage("AOB not found: " .. aob.pattern)
        return
    end

    local foundAddress = tonumber("0x" .. addresses[0])
    addresses.Destroy()

    local memView = getMemoryViewForm()
    if not memView then
        memView = createMemoryView()
        if not memView then
            showMessage("Unable to create the Memory View!")
            return
        end
    end

    memView.DisassemblerView.SelectedAddress = foundAddress
    memView.Show()  -- Bring the Memory View to the front
    -- showMessage("Injection point reached at: " .. string.format("%X", foundAddress))
end

-----------------------------------------------------------
-- Function: Bring the Memory View to the injection point and try to automatically trigger
-- "Find out what addresses this instruction accesses"
-----------------------------------------------------------
local function gotoInjectionPointWithAccesses()
    local rec = getAddressList().getSelectedRecord()
    if not rec then
        showMessage("No record selected!")
        return
    end
    if rec.Type ~= vtAutoAssembler then
        showMessage("The selected record is not a script!")
        return
    end

    local scriptText = rec.Script
    if not scriptText or scriptText == "" then
        showMessage("The Script field is empty!")
        return
    end

    local aobs = extractAOBsFromScript(scriptText)
    if #aobs == 0 then
        showMessage("No AOB command found in the script!")
        return
    end

    -- Use the first AOB command found
    local aob = aobs[1]
    local addresses = AOBScan(aob.pattern)
    if not addresses or addresses.Count == 0 then
        showMessage("AOB not found: " .. aob.pattern)
        return
    end

    local foundAddress = tonumber("0x" .. addresses[0])
    addresses.Destroy()

    local memView = getMemoryViewForm()
    if not memView then
        memView = createMemoryView()
        if not memView then
            showMessage("Unable to create the Memory View!")
            return
        end
    end

    memView.DisassemblerView.SelectedAddress = foundAddress
    memView.Show()  -- Bring the Memory View to the front

    -- Try three different approaches to trigger "Find out what addresses this instruction accesses"
    local success = false

    -- 1. Use findWhatAddressesThisInstructionAccesses if available
    if memView.DisassemblerView.findWhatAddressesThisInstructionAccesses then
        memView.DisassemblerView.findWhatAddressesThisInstructionAccesses()
        success = true
    end

    -- 2. Use OnFindWhatAddressesClick if available
    if not success and memView.DisassemblerView.OnFindWhatAddressesClick then
        memView.DisassemblerView.OnFindWhatAddressesClick(nil)
        success = true
    end

    -- 3. Try to locate the corresponding PopupMenu item in the DisassemblerView
    if not success and memView.DisassemblerView.PopupMenu then
        local popup = memView.DisassemblerView.PopupMenu
        for i = 0, popup.Items.Count - 1 do
            local item = popup.Items[i]
            if item.Caption and item.Caption:find("Find out what addresses this instruction accesses") then
                if item.OnClick then
                    item.OnClick(item)
                    success = true
                    break
                end
            end
        end
    end

    if not success then
        showMessage("Automatic 'Find out what addresses this instruction accesses' not available in this version of Cheat Engine.")
    end

    -- Optional: show the injection address:
    -- showMessage("Injection point reached at: " .. string.format("%X", foundAddress))
end

-----------------------------------------------------------
-- Function to add both options to PopupMenu2 (Address List)
-- only if the selected record is an AutoAssembler script.
-----------------------------------------------------------
local function addMenuItemsForScriptsOnly()
    local mainForm = getMainForm()
    if not mainForm then
        print("Error: Unable to locate Cheat Engine MainForm.")
        return
    end

    local popupMenu = nil
    for i = 0, mainForm.ComponentCount - 1 do
        local comp = mainForm.getComponent(i)
        if comp.ClassName == "TPopupMenu" and comp.Name == "PopupMenu2" then
            popupMenu = comp
            break
        end
    end

    if not popupMenu then
        print("Error: No PopupMenu associated with the address table found.")
        return
    end

    -- Create the first menu item ("AoB Scan")
    local miAOBScanSimple = createMenuItem(popupMenu)
    miAOBScanSimple.Caption = "AoB Scan"
    miAOBScanSimple.OnClick = gotoInjectionPointSimple
    miAOBScanSimple.Visible = false  -- Hidden by default
    popupMenu.Items.add(miAOBScanSimple)

    -- Create the second menu item ("Aob Scan and Accesses addresses")
    local miAOBScanWithAccesses = createMenuItem(popupMenu)
    miAOBScanWithAccesses.Caption = "Aob Scan and Accesses addresses"
    miAOBScanWithAccesses.OnClick = gotoInjectionPointWithAccesses
    miAOBScanWithAccesses.Visible = false  -- Hidden by default
    popupMenu.Items.add(miAOBScanWithAccesses)

    -- Set the OnPopup event to show the menu items only if the selected record is a script
    popupMenu.OnPopup = function(sender)
        local selectedRecord = getAddressList().getSelectedRecord()
        if selectedRecord and selectedRecord.Type == vtAutoAssembler then
            miAOBScanSimple.Visible = true
            miAOBScanWithAccesses.Visible = true
        else
            miAOBScanSimple.Visible = false
            miAOBScanWithAccesses.Visible = false
        end
    end
end

-- Execute the function to add both menu items to PopupMenu2 (scripts only)
addMenuItemsForScriptsOnly()
Video:
Spoiler
Script:
Aob Scan and Accesses addresses [No Gui].CT
(9 KiB) Downloaded 44 times
-----------


11) Copy Dissect Data Line Elements

This tool adds options to the right-click menu of the dissect data, allowing you to copy data from the dissect data. The copied data can be the offset in hexadecimal, the value of the selected element, or the offset, type, and value all at once.

Code:
Spoiler

Code: Select all

-- Function to get an easy-readable type name based on the vartype and value
function getTypeName(vartype, value)
    if vartype == 12 then
        return "Pointer"
    elseif vartype == 4 then
        return "Float"
    elseif vartype == 2 then
        if value:match("^[0-9A-Fa-f]+$") and #value == 8 then
            return "4 Bytes (HEX)"
        else
            return "4 Bytes"
        end
    elseif vartype == 5 then
        return "Double"
    elseif vartype == 0 then
        return "Byte"
    elseif vartype == 1 then
        return "2 Bytes"
    elseif vartype == 6 then
        return "8 Bytes"
    elseif vartype == 7 then
        return "String"
    elseif vartype == 8 then
        return "Unicode String"
    elseif vartype == 9 then
        return "Byte Array"
    else
        return tostring(vartype) .. " (Unknown)"
    end
end

-- Function to copy the value of the selected element
function copySelectedValue(structureFrm)
    if not structureFrm.mainStruct then
        print("Error: No main structure found.")
        return
    end

    -- Get the selected element from the TreeView
    local selectedNode = structureFrm.tvStructureView.Selected
    if not selectedNode then
        print("Error: No element selected.")
        return
    end

    local index = selectedNode.Index
    local element = structureFrm.mainStruct.Element[index]
    if element then
        local offset = element.Offset
        local vartype = element.Vartype

        -- Calculate the exact address of the data
        local baseAddress = structureFrm.Column[0].Address
        local elementAddress = baseAddress + offset

        local value = ""
        if vartype == 12 then
            -- Pointer value
            local ptr = readPointer(elementAddress)
            value = ptr and ("P->" .. string.format("%X", ptr)) or "P->0"
        elseif vartype == 4 then
            -- Float value
            local floatVal = readFloat(elementAddress) or 0
            value = (floatVal == math.floor(floatVal)) and tostring(math.floor(floatVal)) or tostring(floatVal)
        elseif vartype == 2 then
            -- 4 Bytes integer value
            local intVal = readInteger(elementAddress)
            value = tostring(intVal or 0)
        elseif vartype == 0 then
            -- Byte value
            local byteVal = readBytes(elementAddress, 1, false) or 0
            value = tostring(byteVal)
        elseif vartype == 1 then
            -- 2 Bytes integer value
            local int16Val = readSmallInteger(elementAddress) or 0
            value = tostring(int16Val)
        elseif vartype == 5 then
            -- Double value
            local doubleVal = readDouble(elementAddress) or 0
            value = string.format("%.15g", doubleVal)
        elseif vartype == 6 then
            -- 8 Bytes integer value
            local int64Val = readQword(elementAddress) or 0
            value = tostring(int64Val)
        elseif vartype == 7 then
            -- String value
            value = readString(elementAddress, element.Size) or ""
        elseif vartype == 8 then
            -- Unicode String value
            value = readString(elementAddress, element.Size, true) or ""
        elseif vartype == 9 then
            -- Byte Array value
            local byteArray = readBytes(elementAddress, element.Size, true)
            value = byteArray and table.concat(byteArray, ", ") or ""
        else
            -- Fallback value
            value = "0"
        end

        writeToClipboard(value)
    else
        print("Error: Element not found.")
    end
end

-- Function to copy the offset of the selected element
function copySelectedOffset(structureFrm)
    if not structureFrm.mainStruct then
        print("Error: No main structure found.")
        return
    end

    -- Get the selected element from the TreeView
    local selectedNode = structureFrm.tvStructureView.Selected
    if not selectedNode then
        print("Error: No element selected.")
        return
    end

    local index = selectedNode.Index
    local element = structureFrm.mainStruct.Element[index]
    if element then
        local offset = element.Offset
        -- Copy offset as hexadecimal
        local offsetHex = string.format("%X", offset)
        writeToClipboard(offsetHex)
    else
        print("Error: Element not found.")
    end
end

-- Function to copy both type and value data of the selected element
function copySelectedElementData(structureFrm)
    if not structureFrm.mainStruct then
        print("Error: No main structure found.")
        return
    end

    -- Get the selected element from the TreeView
    local selectedNode = structureFrm.tvStructureView.Selected
    if not selectedNode then
        print("Error: No element selected.")
        return
    end

    local index = selectedNode.Index
    local element = structureFrm.mainStruct.Element[index]
    if element then
        local offset = element.Offset
        local vartype = element.Vartype

        -- Calculate the exact address of the data
        local baseAddress = structureFrm.Column[0].Address
        local elementAddress = baseAddress + offset

        -- Determine value formatting based on context
        local refValue = element.Value or element.Description or ""
        local forceHex = vartype == 2 and refValue:find("%(HEX%)")

        local value = ""
        if vartype == 12 then
            -- Pointer value
            local ptr = readPointer(elementAddress)
            value = ptr and ("P->" .. string.format("%X", ptr)) or "P->0"
        elseif vartype == 4 then
            -- Float value
            local floatVal = readFloat(elementAddress) or 0
            value = (floatVal == math.floor(floatVal)) and tostring(math.floor(floatVal)) or tostring(floatVal)
        elseif vartype == 2 then
            -- 4 Bytes integer value
            local intVal = readInteger(elementAddress)
            value = forceHex and string.format("%08X", intVal or 0) or tostring(intVal or 0)
        elseif vartype == 0 then
            -- Byte value
            local byteVal = readBytes(elementAddress, 1, false) or 0
            value = tostring(byteVal)
        elseif vartype == 1 then
            -- 2 Bytes integer value
            local int16Val = readSmallInteger(elementAddress) or 0
            value = tostring(int16Val)
        elseif vartype == 5 then
            -- Double value
            local doubleVal = readDouble(elementAddress) or 0
            value = string.format("%.15g", doubleVal)
        elseif vartype == 6 then
            -- 8 Bytes integer value
            local int64Val = readQword(elementAddress) or 0
            value = tostring(int64Val)
        elseif vartype == 7 then
            -- String value
            value = readString(elementAddress, element.Size) or ""
        elseif vartype == 8 then
            -- Unicode String value
            value = readString(elementAddress, element.Size, true) or ""
        elseif vartype == 9 then
            -- Byte Array value
            local byteArray = readBytes(elementAddress, element.Size, true)
            value = byteArray and table.concat(byteArray, ", ") or ""
        else
            -- Fallback value
            value = "0"
        end

        local typeName = getTypeName(vartype, value)
        local copyText = string.format("Offset %X - %s %s", offset, typeName, value)
        writeToClipboard(copyText)
    else
        print("Error: Element not found.")
    end
end

-- Function to add custom menu items to the context menu
function addMenuItems(timer, form)
    local popupMenu = form.pmStructureView
    if popupMenu then
        timer.destroy()

        -- Menu item for copying the offset
        local miCopyOffset = createMenuItem(popupMenu)
        miCopyOffset.Caption = "Copy Offset"
        miCopyOffset.OnClick = function()
            copySelectedOffset(form)
        end
        popupMenu.Items.add(miCopyOffset)

        -- Menu item for copying the value
        local miCopyValue = createMenuItem(popupMenu)
        miCopyValue.Caption = "Copy Value"
        miCopyValue.OnClick = function()
            copySelectedValue(form)
        end
        popupMenu.Items.add(miCopyValue)

        -- Menu item for copying complete element data
        local miCopySelectedData = createMenuItem(popupMenu)
        miCopySelectedData.Caption = "Copy Selected Element Data"
        miCopySelectedData.OnClick = function()
            copySelectedElementData(form)
        end
        popupMenu.Items.add(miCopySelectedData)

        -- Configure visibility based on whether an element is selected
        popupMenu.OnPopup = function()
            local selected = form.tvStructureView.Selected ~= nil
            miCopyOffset.Visible = selected
            miCopyValue.Visible = selected
            miCopySelectedData.Visible = selected
        end
    end
end

-- Hook to detect when "TfrmStructures2" windows are created
registerFormAddNotification(function(form)
    if form.ClassName == "TfrmStructures2" then
        local timer = createTimer()
        timer.Interval = 100
        timer.OnTimer = function(t)
            addMenuItems(t, form)
            t.destroy()
        end
    end
end)
Video:
Spoiler
Script:
Copy Dissect Data Line Elements.CT
(9.76 KiB) Downloaded 46 times
------------


12) List all Dissect Data Offsets/Elements:

This code allows you to view all offsets (elements) present in the Dissect Data. Normally, the offsets reported in Dissect Data are displayed in multiples of 4 (offset 0, 4, 8, C, 10, etc.). This tool allows you to view them all (offset 0, 1, 2, 3, 4, 5, 6, etc.), which is very useful for comparisons, meaning to find the unique and static values needed for comparisons (but not only that; sometimes I've found that the normal value was at offset 4 and the maximum value, like max HP, was at offset 2, which wasn't viewable in the dissect data, without modifying the address where the dissect data was generated). When you click on any element of the dissect data, you can choose the option 'Create 4-byte Sequence' or 'Create Float Sequence,' which will regenerate the dissect data, showing all offsets with values written in 4 Bytes or in Float.

To increase the limit of offsets displayed, you simply need to modify this part of the code:

-- Create 1678 elements with incremental offset of 1 byte
for i = 0, 1678 do

Replace 'for i = 0, 1678' do with, for example, 'for i = 0, 5000', do so a list of 5000 offsets (or 5k elements) will be generated.

Code:
Spoiler

Code: Select all

function createSequentialElements(structureFrm, vartype, description, displayMethod)
    if not structureFrm.mainStruct then return end

    -- Remove existing elements
    while structureFrm.mainStruct.Count > 0 do
        structureFrm.mainStruct.Element[0].destroy()
    end

    -- Create 1678 elements with incremental offset of 1 byte
    for i = 0, 1678 do
        local element = structureFrm.mainStruct.addElement()
        element.Offset = i
        element.Vartype = vartype
        element.Bytesize = 4
        element.OffsetHex = string.format("%08X", element.Offset)
        element.Description = description
        element.DisplayMethod = displayMethod
    end

    -- Update the display
    structureFrm.tvStructureView.refresh()
end

function addMenuItem(timer, form)
    local popupMenu = form.pmStructureView
    if popupMenu then
        timer.destroy()

        -- Menu item for 4-byte sequence
        local miCreateDword = createMenuItem(popupMenu)
        miCreateDword.Caption = "Create 4-byte Sequence"
        miCreateDword.OnClick = function()
            createSequentialElements(form, vtDword, "4 Bytes", "unsigned integer")
        end

        -- Menu item for float sequence
        local miCreateFloat = createMenuItem(popupMenu)
        miCreateFloat.Caption = "Create Float Sequence"
        miCreateFloat.OnClick = function()
            createSequentialElements(form, vtSingle, "Float", "float")
        end

        -- Add items to the menu
        popupMenu.Items.add(miCreateDword)
        popupMenu.Items.add(miCreateFloat)

        -- Always show the items
        popupMenu.OnPopup = function()
            miCreateDword.Visible = true
            miCreateFloat.Visible = true
        end
    end
end

-- Register the hook for dissect windows
registerFormAddNotification(function(form)
    if form.ClassName == "TfrmStructures2" then
        local timer = createTimer()
        timer.Interval = 100
        timer.OnTimer = function(t)
            addMenuItem(t, form)
            t.destroy()
        end
    end
end)
Video:
Spoiler
Script:
List all Dissect Data Offsets-Elements.CT
(2.54 KiB) Downloaded 51 times
Last edited by SilverRabbit90 on Sun Apr 13, 2025 7:04 pm, edited 10 times in total.

User avatar
Rienfleche
Expert Cheater
Expert Cheater
Posts: 216
Joined: Sun May 15, 2022 6:50 am
Reputation: 72

Re: Some Lua Codes (Tools)

Post by Rienfleche »

i wanna ask, how to find value that open just a few second, example like fire emblem game exp i get 10 exp after that it close immediately how can i scan that?

User avatar
SilverRabbit90
Table Makers
Table Makers
Posts: 252
Joined: Fri Jan 15, 2021 12:01 am
Reputation: 219

Re: Some Lua Codes (Tools)

Post by SilverRabbit90 »

Rienfleche wrote:
Sun Apr 13, 2025 1:32 pm
i wanna ask, how to find value that open just a few second, example like fire emblem game exp i get 10 exp after that it close immediately how can i scan that?
Premise: I assume you want to create an 'Exp Multiplier.'

A) What you're asking is not at all simple. You should try scanning for the amount of experience received. For example, first, you receive 10 EXP, then 24, then 50, and then 10 again. Do the scan using the 'All' setting (in the Value Type section, unless you already know what type of value it is). I recommend using 'Unknown initial value' and then doing 'Increased' and 'Decreased' values (because when it says 10 EXP on the screen, it might not really be 10; it could be seen by the computer as 1000 as an int or float, and in rare cases, even double). For safety, enable the 'Pause the game while scanning' function found under 'Fast Scan,' which in turn is located in the section where you set scan types.

1) Receive experience and do the scan with 'Unknown initial value' (with 'Pause the game while scanning' enabled).

2) Then, if you receive more experience, do 'Increased Value' (or 'Decreased Value' if you receive less).

3) Try to do the same when you receive less experience by doing 'Decreased Value.' It's important to note that the more you switch from Increased to Decreased, the higher the probability of finding fewer values among the scanned addresses.

Hope that this is enough to find the value because it is probably a flag, and scans of this type are always difficult.

B) Another thing you could do is find the experience your character has, for example, 600 out of 1000. Gain 10 experience and scan for 610, etc., until you find the actual accumulated experience value for your character. Then, look at the value in 'Dissect Data Structure.' Right-click on the address in the dissect data (the one written right under 'Group 1') and select the 'Watch for changes' option. At this point, gain some experience and see what is highlighted in red in the dissect data. These will be the values that changed while this mode is enabled, and hope to find the incremental value among these.
Spoiler
Image
C) Once you've found the experience value, click on it and select 'Find out what writes to this address.' Gain some experience and create a script that, in this case, will access the experience modification only when it is obtained (and not constantly). Now, if you add, for example, add ecx,(int)20 (if it's an int, otherwise put the correct type, and if the register is ecx, otherwise put the correct register), this will add 20 experience to what you are already receiving (for example, if you were receiving 10 before, and now you receive 20 more, the total experience you will receive will be 30 more).

User avatar
SilverRabbit90
Table Makers
Table Makers
Posts: 252
Joined: Fri Jan 15, 2021 12:01 am
Reputation: 219

Re: Some Lua Codes (Tools)

Post by SilverRabbit90 »

-------------

13) AoB Script Generator

As the name suggests, this tool is designed to automatically create AoB scripts through the AoB Injection method, whether you right-click on the found value and select the option 'Find out what accesses this address' or choose the option 'Find out what writes this address.' This tool will auto-generate AoB scripts.

Please note that you need to right-click on a found value and select either 'Find out what accesses this address' or 'Find out what writes this address' after activating the script.

Code:
Spoiler

Code: Select all

-- If an existing timer is found, destroy it
if _G.myTimer then
  _G.myTimer.destroy()
  _G.myTimer = nil
end

-- Initialize global state variables:
-- _G.nextIndex: index of the element to be processed (0 = first element)
-- _G.inOperation: true if the process for the current element is in progress (AA running)
-- _G.processCompleted: true if the disassembler (opcode) has been invoked
-- _G.aaLaunched: true if the Memory View (Auto Assemble) has been launched
-- _G.aaOperationDone: true when the AOB template has been applied and copied
_G.nextIndex = 0
_G.inOperation = false
_G.processCompleted = false
_G.aaLaunched = false
_G.aaOperationDone = false

-- Variable to keep track of the last detected opcode window
_G.lastOpcodeWindow = nil

--================ UTILITY FUNCTIONS =================

-- Find the opcode window (disassembler)
local function findAccessWindow()
  for i = 0, getFormCount()-1 do
    local form = getForm(i)
    if form.ClassName == "TFoundCodeDialog" then
      return form
    end
  end
  return nil
end

-- Get the ListView component of the opcode window
local function getOpcodeListView()
  local accessWindow = findAccessWindow()
  if not accessWindow then return nil end

  for i = 0, accessWindow.ComponentCount-1 do
    local comp = accessWindow.Component[i]
    if comp.ClassName == "TListView" then
      return comp
    end
  end
  return nil
end

-- Helper function: check if a Memory Record with a certain description already exists in the Address List
local function isDescriptionTaken(desc)
  local al = getAddressList()
  for i = 0, al.Count-1 do
    local rec = al.getMemoryRecord(i)
    if rec.Description == desc then
      return true
    end
  end
  return false
end

-- Create a new Memory Record for the AOB script with a unique name.
-- If "Generated AOB Script" already exists, check by incrementing the number in parentheses,
-- and return the first available name (e.g. "Generated AOB Script (2)", "Generated AOB Script (3)", etc.)
local function createAARecord()
  local baseDescription = "Generated AOB Script"
  local description = baseDescription
  if isDescriptionTaken(description) then
    local n = 2
    while true do
      local candidate = baseDescription .. " (" .. n .. ")"
      if not isDescriptionTaken(candidate) then
        description = candidate
        break
      end
      n = n + 1
    end
  end

  local newRec = getAddressList().createMemoryRecord()
  newRec.Description = description
  newRec.Type = vtAutoAssembler
  return newRec
end

--================ PHASE 1: DISASSEMBLER INVOCATION =================

-- Select the element in the ListView based on the index and execute "Show this address in the disassembler"
local function invokeDisassembler(index)
  local accessWindow = findAccessWindow()
  if not accessWindow then
    return false
  end

  local lv = getOpcodeListView()
  if not lv then
    return false
  end

  if lv.Items.Count <= index then
    return false
  end

  lv.ItemIndex = index -- select the element

  -- Find and click the "Show this address in the disassembler" menu
  for i = 0, accessWindow.ComponentCount-1 do
    local comp = accessWindow.Component[i]
    if comp.ClassName == "TMenuItem" and string.gsub(comp.Caption, "&", "") == "Show this address in the disassembler" then
      comp.doClick()
      return true
    end
  end

  return false
end

--================ PHASE 2: AUTO ASSEMBLE =================

-- Wait for the Auto Assemble window (TfrmAutoInject) to open
local function waitAAForm()
  local tries = 0
  while tries < 10 do -- 10 attempts with 500ms delay
    local fAA = getForm("TfrmAutoInject")
    if fAA and fAA.Visible and fAA.Menu and fAA.Menu.Items then
      return fAA
    end
    sleep(10)
    tries = tries + 1
  end
  return nil
end

-- Check if the opcode window is still open
local function isDisassemblerOpen()
  local disassemblerWindow = findAccessWindow()
  return disassemblerWindow and disassemblerWindow.Visible
end

-- Apply the AOB template in the Auto Assemble window:
-- Select "Template" -> "AOB Injection", copy the generated script and insert it into the Memory Record.
-- If the user cancels the operation, the created record is deleted.
local function applyAOBTemplate()
  if not isDisassemblerOpen() then
    return false
  end

  local fAA = waitAAForm()
  if not fAA then return false end

  -- Find the "Template" menu in the Auto Assemble window and then "AOB Injection"
  for i = 0, fAA.Menu.Items.Count-1 do
    local mainMenu = fAA.Menu.Items[i]
    if string.gsub(mainMenu.Caption, "&", "") == "Template" then
      for j = 0, mainMenu.Count-1 do
        local subMenu = mainMenu[j]
        if string.gsub(subMenu.Caption, "&", "") == "AOB Injection" then
          subMenu.doClick()  -- Click "AOB Injection"

          -- Start a timer to wait for the availability of the assemble screen
          createTimer(nil, true).OnTimer = function(timer)
            local aaRec = createAARecord()
            if not aaRec then
              _G.aaOperationDone = false
              timer.destroy()
              return
            end

            if fAA.assemblescreen and fAA.assemblescreen.Lines then
              local scriptText = fAA.assemblescreen.Lines.Text
              if scriptText and scriptText:match("%S") then
                aaRec.Script = scriptText
              else
                -- If the user presses Cancel or doesn't enter any text, delete the created record
                aaRec.destroy()
              end
              _G.aaOperationDone = true
            else
              _G.aaOperationDone = false
            end

            fAA.close()
            timer.destroy()
          end
          return true
        end
      end
    end
  end
  return false
end

--================ MAIN MANAGEMENT (TIMER) =================

_G.myTimer = createTimer(nil, true)
_G.myTimer.Interval = 10 -- short interval to periodically check for new elements

_G.myTimer.OnTimer = function(t)
  -- If an operation is in progress (AA running), wait
  if _G.inOperation then
    if _G.aaOperationDone then
      -- Operation completed: reset states and move to the next element
      _G.inOperation = false
      _G.aaOperationDone = false
      _G.processCompleted = false
      _G.aaLaunched = false
      _G.nextIndex = _G.nextIndex + 1
    end
    return
  end

  -- Check or detect the opcode window
  local currentOpcodeWindow = findAccessWindow()
  if not currentOpcodeWindow then
    return
  end

  -- If the current window is different from the last detected one, reset the state variables
  if _G.lastOpcodeWindow ~= currentOpcodeWindow then
    _G.lastOpcodeWindow = currentOpcodeWindow
    _G.nextIndex = 0
    _G.inOperation = false
    _G.processCompleted = false
    _G.aaLaunched = false
    _G.aaOperationDone = false
  end

  local lv = getOpcodeListView()
  if not lv then return end

  -- If there are not enough elements in the list, wait
  if lv.Items.Count <= _G.nextIndex then
    return
  end

  -- Make sure the opcode window is still open
  if not isDisassemblerOpen() then
    _G.processCompleted = false
    _G.aaLaunched = false
    return
  end

  -- State-based procedure:
  -- 1. If the disassembler (opcode) has not been invoked for the current element, invoke it...
  if not _G.processCompleted then
    if invokeDisassembler(_G.nextIndex) then
      _G.processCompleted = true
      t.Interval = 10 -- wait a bit after invoking the disassembler
    end
  else
    -- 2. Once the disassembler is active, launch Auto Assemble if not already done
    if not _G.aaLaunched then
      local mv = getMemoryViewForm()
      if mv then
        for i = 0, mv.Menu.Items.Count-1 do
          local menu = mv.Menu.Items[i]
          if string.gsub(menu.Caption, "&", "") == "Tools" then
            for j = 0, menu.Count-1 do
              local item = menu[j]
              if string.gsub(item.Caption, "&", "") == "Auto Assemble" then
                item.doClick()
                _G.aaLaunched = true
                _G.inOperation = true -- operate on the current element
                applyAOBTemplate()    -- apply the AOB template
                return
              end
            end
          end
        end
      end
    end
  end
end
Video:
Spoiler
Script:
AoB Script Generator.CT
(9.14 KiB) Downloaded 45 times

--------------

14) Hex/Dec Converter/Calculator
This tool is a simple converter for Hexadecimal and Decimal values. It converts hexadecimal values to decimal and vice versa. Additionally, it can also subtract or add what you input in the designated section. It's very useful for transforming an address, for example, the dissect data address, into a hexadecimal value from which you can subtract, for instance, 1000, so that when it is converted back to hexadecimal, the starting point of the dissect data will be 1000 lines (offsets) before the original point (offset 0).

Code:
Spoiler

Code: Select all

if form and form.destroy then
    form.destroy()
end

form = createForm(nil)
form.Caption = 'Hex/Dec Calculator with Operations'
form.Width = 320  -- Increased from 300
form.Height = 170 -- Increased from 150

local isHexMode = true
local conversionHistory = {}
local lastOperation = {}

-- Main GUI components
local mainEdit = createEdit(form)
mainEdit.Left = 10
mainEdit.Top = 20
mainEdit.Width = 180  -- Wider input field
mainEdit.TextHint = 'Main value'

local convertBtn = createButton(form)
convertBtn.Left = 200
convertBtn.Top = 20
convertBtn.Width = 90  -- Wider button
convertBtn.Caption = 'To Dec →'

local infoLabel = createLabel(form)
infoLabel.Left = 10
infoLabel.Top = 50
infoLabel.Caption = 'Current Mode: Hex → Dec'

-- Arithmetic operations components
local operatorEdit = createEdit(form)
operatorEdit.Left = 10
operatorEdit.Top = 90  -- Lower position
operatorEdit.Width = 120
operatorEdit.TextHint = 'Operand'

local addBtn = createButton(form)
addBtn.Left = 140
addBtn.Top = 90
addBtn.Width = 35
addBtn.Caption = '+'

local subBtn = createButton(form)
subBtn.Left = 185
subBtn.Top = 90
subBtn.Width = 35
subBtn.Caption = '-'

local statusLabel = createLabel(form)
statusLabel.Left = 10
statusLabel.Top = 120  -- Adjusted position
statusLabel.Width = 280
statusLabel.Caption = 'Result: 0'

-- Core conversion function
function toggleConversion()
    local input = mainEdit.Text:gsub('%s+', ''):upper()

    if isHexMode then
        input = input:gsub('^0X', '')
        local success, result = pcall(function()
            return tonumber(input, 16)
        end)

        if success and result then
            local num = math.floor(result)
            mainEdit.Text = tostring(num)
            conversionHistory.dec = num
            isHexMode = false
            convertBtn.Caption = 'To Hex →'
            infoLabel.Caption = 'Current Mode: Dec → Hex'
        else
            showMessage('Invalid hexadecimal value!')
            mainEdit.Text = conversionHistory.hex or ''
        end
    else
        local success, result = pcall(function()
            local num = tonumber(input)
            if not num or num ~= math.floor(num) then
                error("Not an integer")
            end
            return num
        end)

        if success and result then
            local hexValue = string.format('%X', result)
            mainEdit.Text = hexValue
            conversionHistory.hex = hexValue
            isHexMode = true
            convertBtn.Caption = 'To Dec →'
            infoLabel.Caption = 'Current Mode: Hex → Dec'
        else
            showMessage('Invalid decimal value!')
            mainEdit.Text = conversionHistory.dec or ''
        end
    end
end

-- Arithmetic operations function
function performOperation(operation)
    local currentMode = isHexMode and "hex" or "dec"

    local function parseValue(value, mode)
        return (mode == "hex" and tonumber(value:gsub('^0X', ''), 16)) or tonumber(value)
    end

    local mainValue = parseValue(mainEdit.Text, currentMode)
    local operatorValue = parseValue(operatorEdit.Text, currentMode)

    if not mainValue or not operatorValue then
        showMessage(string.format('Invalid values in %s mode!', currentMode:upper()))
        return
    end

    local result = operation == '+' and (mainValue + operatorValue) or (mainValue - operatorValue)

    if isHexMode then
        mainEdit.Text = string.format('%X', result)
    else
        mainEdit.Text = tostring(result)
    end

    -- Update history and status
    conversionHistory[currentMode] = mainEdit.Text
    statusLabel.Caption = string.format('Last operation: %s %s %s = %s',
        mainValue, operation, operatorValue, result)

    -- Save operation for undo (CTRL+Z)
    lastOperation = {
        original = mainValue,
        result = result,
        operator = operatorValue,
        operation = operation
    }
end

-- Assign event handlers
convertBtn.OnClick = toggleConversion
addBtn.OnClick = function() performOperation('+') end
subBtn.OnClick = function() performOperation('-') end

-- Undo hotkey (CTRL+Z)
form.OnKeyDown = function(sender, key)
    if key == VK_CONTROL then ctrlPressed = true end
    if ctrlPressed and key == 90 then -- CTRL+Z
        if lastOperation.original then
            mainEdit.Text = isHexMode and
                string.format('%X', lastOperation.original) or
                tostring(lastOperation.original)
            statusLabel.Caption = 'Restored previous value'
        end
    end
end

form.OnKeyUp = function(sender, key)
    if key == VK_CONTROL then ctrlPressed = false end
end

-- Initialize
conversionHistory.hex = '0'
conversionHistory.dec = '0'
local ctrlPressed = false

form.show()



{$asm}

[DISABLE]
{$lua}
if form and form.destroy then
    form.destroy()
    form = nil
end
Script:
Simple Converter - Calculator.CT
(5.4 KiB) Downloaded 33 times


---------------


15) AoB Wildcard changer:

This tool is used to change how wildcards are displayed. Example:

This AOB "8? 88 ?8 00 00 ?? E9 5B" will be displayed as follows:

Compact ??:
8?88?80000??E95B
Spaced xx:
8x 88 x8 00 00 xx E9 5B
Compact xx:
8x88x80000xxE95B

Code:
Spoiler

Code: Select all

local function isHexDigit(c)
  return c:match("[0-9A-F]")
end

local function convertBytes(str)
  -- Remove any spaces to work on the “pair” without interference
  local norm = str:gsub("%s+", "")
  -- If the total number of characters is odd, pad with "?"
  if #norm % 2 ~= 0 then
    norm = norm .. "?"
  end

  local compQM = {}  -- Compact ??
  local spacedQM = {}
  local compXX = {}  -- Compact xx
  local spacedXX = {}

  for i = 1, #norm, 2 do
    local byte = norm:sub(i, i+1)
    local tokenQM = ""
    local tokenXX = ""
    -- For each character of the pair
    for j = 1, 2 do
      local c = byte:sub(j, j):upper()
      if isHexDigit(c) then
        tokenQM = tokenQM .. c
        tokenXX = tokenXX .. c
      else
        tokenQM = tokenQM .. "?"
        tokenXX = tokenXX .. "x"
      end
    end
    table.insert(compQM, tokenQM)
    table.insert(spacedQM, tokenQM)
    table.insert(compXX, tokenXX)
    table.insert(spacedXX, tokenXX)
  end

  return {
    compact_qm = table.concat(compQM),
    spaced_qm  = table.concat(spacedQM, " "),
    compact_xx = table.concat(compXX),
    spaced_xx  = table.concat(spacedXX, " ")
  }
end

-- Function to detect the “input type”
-- Returns a table with:
--   .spaced = true if the input contains spaces, false otherwise.
--   .wildStyle = "qm" if the first byte containing wildcard uses "?" or no wildcard,
--                or "xx" if it contains "X" (or "x").
local function detectInputFormat(str)
  local info = { spaced = false, wildStyle = nil }
  if str:find("%s") then
    info.spaced = true
  end
  -- Remove spaces to inspect the pairs
  local norm = str:gsub("%s+", "")
  for i = 1, #norm, 2 do
    local byte = norm:sub(i, i+1)
    -- If one of the characters is X (or x) it gets "xx". If I find a "?" I use "qm".
    if byte:upper():find("X") then
      info.wildStyle = "xx"
      break
    elseif byte:find("%?") then
      info.wildStyle = "qm"
      break
    end
  end
  -- If no wildcard is found, assume "qm"
  if not info.wildStyle then
    info.wildStyle = "qm"
  end
  return info
end

-- The main function that receives the input string and returns a multiline
-- string with the “alternative” conversions based on the format entered.
local function TransformHexadecimal(pattern)
  local conv = convertBytes(pattern)
  local fmt = detectInputFormat(pattern)

  local outputLines = {}

  if fmt.wildStyle == "qm" then
    -- Input with wildcards "?" (the user manually entered "??")
    if fmt.spaced then
      -- Example: "8B 97 ?? 01 ?0 0? 48 ?? CB"
      -- User has entered the spaced format for "??", so I show the compact conversion for "??"
      table.insert(outputLines, "Compact ??:")
      table.insert(outputLines, conv.compact_qm)
    else
      -- Example: "8B97??01?00?48??CB"
      -- Compact input "??" → show the spaced format for "??"
      table.insert(outputLines, "Spaced ??:")
      table.insert(outputLines, conv.spaced_qm)
    end
    -- For the "xx" family I show both formats (each on a line)
    table.insert(outputLines, "Spaced xx:")
    table.insert(outputLines, conv.spaced_xx)
    table.insert(outputLines, "Compact xx:")
    table.insert(outputLines, conv.compact_xx)
  else
    -- Input with wildcards "x" (e.g., "8B 97 xx 01 x0 0x 48 xx CB" or "8B97xx01x00x48xxCB")
    -- I show both versions for the "??" family
    table.insert(outputLines, "Spaced ??:")
    table.insert(outputLines, conv.spaced_qm)
    table.insert(outputLines, "Compact ??:")
    table.insert(outputLines, conv.compact_qm)
    -- For the "xx" family I show the only conversion opposite to the entered layout
    if fmt.spaced then
      table.insert(outputLines, "Compact xx:")
      table.insert(outputLines, conv.compact_xx)
    else
      table.insert(outputLines, "Spaced xx:")
      table.insert(outputLines, conv.spaced_xx)
    end
  end

  -- Separate each section with a newline, but each line (array) remains "infinite"
  return table.concat(outputLines, "\n")
end

-----------------------------------------------------------
-- GUI (User Interface) for Cheat Engine
-----------------------------------------------------------
local f = createForm()
f.Caption = 'Advanced Hex Converter'
f.Width = 620
f.Height = 400

-- Input Edit component
local input = createEdit(f)
input.Left = 10
input.Top = 10
input.Width = 600

-- Output Memo component (requiring vertical scroll and read-only)
local output = createMemo(f)
output.Left = 10
output.Top = 40
output.Width = 600
output.Height = 340
output.ScrollBars = 'ssVertical'
output.ReadOnly = true
output.Anchors = '[akLeft,akTop,akRight,akBottom]'

-- Disable word wrap to avoid breaking long lines
output.WordWrap = false

-- Handle the OnChange event (on each modification, the result is reprinted)
input.OnChange = function()
  local txt = input.Text or ""
  if txt == "" then
    output.Lines.Text = ""
    return
  end
  local results = TransformHexadecimal(txt)
  output.Lines.Text = results
end

-- Show the form
f.show()
Image:
Spoiler
Image
Script:
Change AoB form.CT
(6.64 KiB) Downloaded 28 times


----------------


16) Autoselect Dissect Data Changes:

This tool is used to automatically select elements whose values change over time in the Dissect Data. By right-clicking on any item in the list, you can select the option "Monitor Values" to start monitoring the changing values. Conversely, by selecting the option "Stop Monitoring," the monitoring will stop. This is useful for removing unwanted items from the Dissect Data or for testing those you want to analyze after performing a specific action related to the cheat you want to implement. Be careful you need to select "Stop Monitoring" before deleting anything.

Code:
Spoiler

Code: Select all

local monitoredItems = {
  active            = false,
  timer             = nil,
  form              = nil,
  lastValues        = {},
  blockedIndices    = {},    -- blockedIndices[i]==true => ignore index i
  currentSelectedIdx= nil,
  suppressOnChange  = false,
  elementCount      = 0
}

-- reads the value with the correct Vartype
local function getElementValue(frm, i)
  local e = frm.mainStruct.Element[i]
  if not e then return nil end
  local addr = frm.Column[0].Address + e.Offset
  local vt   = e.Vartype

  if     vt==vtPointer then
    local p = readPointer(addr) or 0
    return string.format("P->%X", p)

  elseif vt==vtSingle then
    local f = readFloat(addr) or 0
    return (f==math.floor(f)) and tostring(f) or string.format("%.7g", f)

  elseif vt==vtDword then
    return tostring(readInteger(addr) or 0)

  elseif vt==vtQword then
    return string.format("%X", readQword(addr) or 0)

  elseif vt==vtDouble then
    return string.format("%.15g", readDouble(addr) or 0)

  elseif vt==vtString or vt==vtUnicodeString then
    return readString(addr) or ""

  elseif vt==vtByte then
    return string.format("%X", readBytes(addr, 1) or 0)

  elseif vt==vtWord then
    return tostring(readSmallInteger(addr) or 0)

  else
    return "0"
  end
end

-- counts the number of elements (until the first nil)
local function getElementCount(frm)
  local c=0
  while frm.mainStruct.Element[c]~=nil do c=c+1 end
  return c
end

-- finds the view node corresponding to the index
local function findNodeByIndex(frm, index)
  for j=0, frm.tvStructureView.Items.Count-1 do
    local n = frm.tvStructureView.Items[j]
    if n and n.Index==index then
      return n
    end
  end
  return nil
end

-- starts or restarts monitoring
local function startMonitoring(frm)
  -- if already active, stop the timer and reset
  if monitoredItems.active then
    if monitoredItems.timer then monitoredItems.timer.destroy() end
    monitoredItems.blockedIndices = {}
    monitoredItems.currentSelectedIdx = nil
  end

  monitoredItems.form = frm
  monitoredItems.elementCount = getElementCount(frm)
  monitoredItems.lastValues = {}
  monitoredItems.blockedIndices = {}
  monitoredItems.active = true

  -- initialize current values
  for i=0, monitoredItems.elementCount-1 do
    monitoredItems.lastValues[i] = getElementValue(frm, i)
  end

  -- if there's already an initial selection, block it right away
  local sel = frm.tvStructureView.Selected
  if sel then
    monitoredItems.blockedIndices[sel.Index] = true
    monitoredItems.currentSelectedIdx = sel.Index
  end

  -- polling timer with optimized approach
  local t = createTimer()
  t.Interval = 100 -- more responsive
  t.OnTimer = function()
    if not monitoredItems.active or not monitoredItems.form then
      t.destroy()
      return
    end

    local f = monitoredItems.form

    -- check only unblocked elements
    for i=0, monitoredItems.elementCount-1 do
      if not monitoredItems.blockedIndices[i] then
        local newValue = getElementValue(f, i)
        if newValue ~= monitoredItems.lastValues[i] then
          -- found a modified value, select it
          local node = findNodeByIndex(f, i)
          if node then
            -- silent selection
            monitoredItems.suppressOnChange = true
            f.tvStructureView.Selected = node
            monitoredItems.suppressOnChange = false

            -- permanently block this element (it will no longer be monitored)
            monitoredItems.blockedIndices[i] = true
            monitoredItems.currentSelectedIdx = i

            -- update only the changed value
            monitoredItems.lastValues[i] = newValue
            break -- only one selection per tick
          end
        end
      end
    end
  end

  monitoredItems.timer = t
end

-- stops everything and resets
local function stopMonitoring()
  if monitoredItems.timer then
    monitoredItems.timer.destroy()
    monitoredItems.timer = nil
  end
  monitoredItems.active = false
  monitoredItems.form = nil
  monitoredItems.lastValues = {}
  monitoredItems.blockedIndices = {}
  monitoredItems.currentSelectedIdx = nil
  monitoredItems.elementCount = 0
end

-- adds menu items and intercepts manual selections
local function addMenuItems(timer, form)
  local popup = form.pmStructureView
  if not popup then return end
  timer.destroy()

  -- override OnChange to handle manual select/deselect
  form.tvStructureView.OnChange = function(tv, node)
    if monitoredItems.suppressOnChange then
      return
    end

    -- unlock the previously selected (not necessary, we want permanent blocking)
    -- if monitoredItems.currentSelectedIdx then
    --   monitoredItems.blockedIndices[ monitoredItems.currentSelectedIdx ] = nil
    -- end

    -- if node exists, block it; otherwise it's a deselect
    if node then
      monitoredItems.currentSelectedIdx = node.Index
      monitoredItems.blockedIndices[node.Index] = true
    else
      monitoredItems.currentSelectedIdx = nil
    end
  end

  -- Monitor Values
  local miStart = createMenuItem(popup)
  miStart.Caption = "Monitor Values"
  miStart.OnClick = function() startMonitoring(form) end
  popup.Items.add(miStart)

  -- Stop Monitoring
  local miStop = createMenuItem(popup)
  miStop.Caption = "Stop Monitoring"
  miStop.OnClick = stopMonitoring
  popup.Items.add(miStop)

  popup.OnPopup = function()
    miStart.Visible = not monitoredItems.active
    miStop.Visible = monitoredItems.active
  end
end

-- hook for TfrmStructures2
registerFormAddNotification(function(form)
  if form.ClassName=="TfrmStructures2" then
    local t = createTimer()
    t.Interval = 100
    t.OnTimer = function(tm)
      addMenuItems(tm, form)
    end
  end
end)
Video:


Script:
Autoselect Dissect Data Changes.CT
(6.49 KiB) Downloaded 31 times

User avatar
SilverRabbit90
Table Makers
Table Makers
Posts: 252
Joined: Fri Jan 15, 2021 12:01 am
Reputation: 219

Re: Some Lua Codes (Tools)

Post by SilverRabbit90 »

-----------------

I created these codes because I noticed that sometimes after a game update, the offset where our value is located can change, and sometimes the register where it is stored can also change, so it's not enough to simply update the AoB. This code is capable of automatically finding both the correct offset and the register even after changes occur:


17) registersymbol in lua (pointer in lua) method 1:

This code is used to generate a pointer based on the "description" of the pointer itself (it is based on register + offset)

Assembly code example [32 bit Games]:
Spoiler

Code: Select all

[ENABLE]

aobscan(YourCheatName,D9 87 04 01 00 00 D9 EE DF F1 DD D8 7A 09) // should be unique
alloc(newmem,$1000)

alloc(originalbytesA,9)
registersymbol(originalbytesA)

originalbytesA:
readmem(YourCheatName,9)

label(bkpValue)
label(return)

{
alloc // (6 dwords = 24 bytes)
}

alloc(seeStamina,24)
registersymbol(seeStamina)

newmem:
{
save registers
+0   rbx
+4   esi
+8   edi
+12  ecx
+16  edx
+20  eax
}

mov [seeStamina+0],ebx
mov [seeStamina+4],esi
mov [seeStamina+8],edi
mov [seeStamina+12],ecx
mov [seeStamina+16],edx
mov [seeStamina+20],eax

//code:
  //fld dword ptr [edi+00000104]
  jmp return

bkpValue:
readmem(YourCheatName,6)
jmp return

align 10

seeStamina:
dd 0,0,0,0,0,0


YourCheatName:
  jmp newmem
  nop
return:
registersymbol(YourCheatName bkpValue)

[DISABLE]

YourCheatName:
readmem(originalbytesA,9)
// db D9 87 04 01 00 00

unregistersymbol(*)
dealloc(*)

Lua code [32 bit Games]:
Spoiler

Code: Select all

-- === LUA PATCH LuaScriptA Quantity (x86) ===

local LuaScriptA_REGS = {'eax', 'ebx', 'ecx', 'edx', 'esi', 'edi', 'esp', 'ebp'} -- 32bit

local function findRecordA(descA)
  local alA = getAddressList()
  for iA = 0, alA.Count-1 do
    local recA = alA[iA]
    if recA.Description == descA then return recA end
  end
  return nil
end

local function scanDisasmA(addrA, max_stepsA)
  addrA = tonumber(addrA)
  max_stepsA = max_stepsA or 8
  for iA = 0, max_stepsA-1 do
    local disasmA = disassemble(addrA)
    if disasmA then
      for _, regA in ipairs(LuaScriptA_REGS) do
        local pat1A = "%["..regA.."%+0x([0-9A-Fa-f]+)%]"
        local foundA = disasmA:match(pat1A)
        if foundA then return regA, tonumber(foundA,16) end
        local pat2A = "%["..regA.."%+([0-9A-Fa-f]+)%]"
        foundA = disasmA:match(pat2A)
        if foundA then return regA, tonumber(foundA,16) end
      end
    end
    local stepA = getInstructionSize(addrA) or 1
    addrA = addrA + stepA
  end
  return nil, 0
end

local function updatePointerA(ptrBaseA, offsetA)
  local recA = findRecordA("Stamina") ---Insert cheat Description here
  if recA and ptrBaseA then
    recA.Address = string.format("0x%X", ptrBaseA + offsetA)
  end
end

local function pointerTimerA()
  local codeAddrA = getAddressSafe("originalbytesA")
  if not codeAddrA then return end

  local regA, offsetA = scanDisasmA(codeAddrA, 8)
  if not regA then return end

  local regoffsets = {
    eax = 20,
    ebx = 0,
    ecx = 12,
    edx = 16,
    esi = 4,
    edi = 8,
  }
  local symbolAddrA = getAddressSafe("seeStamina")
  if not symbolAddrA then return end
  local offset = regoffsets[regA]
  local ptrBaseA = readInteger(symbolAddrA + offset)
  if not ptrBaseA or ptrBaseA == 0 then return end

  updatePointerA(ptrBaseA, offsetA)
end

if LuaScriptATimer and LuaScriptATimer.destroy then
  LuaScriptATimer.destroy()
  LuaScriptATimer = nil
end
LuaScriptATimer = createTimer(nil, false)
LuaScriptATimer.Interval = 250
LuaScriptATimer.OnTimer = pointerTimerA
LuaScriptATimer.Enabled = true
Change these variables to the appropriate ones:
Stamina ---> Description (for see pointer)
originalbytesA ---> Some Bytes (normaly 4 for 32 bit games and 8 for 64 bit games)
seeStamina ---> registersymbol (for see pointer)


Assembly code example [64 bit Games]:
Spoiler

Code: Select all

[ENABLE]

aobscanmodule(WorldTimeZa,$process,48 8B ?? ?? ?? ?? ?? 49 B9 ?? ?? ?? ?? ?? ?? ?? ?? 48 B8) // should be unique
alloc(newmem,$1000,WorldTimeZa)

alloc(originalbytesDt,9)
registersymbol(originalbytesDt)

originalbytesDt:
readmem(WorldTimeZa,9)

label(bkpDayTime)
label(return)


alloc(seeDayTime,56)
registersymbol(seeDayTime)


newmem:

mov [seeDayTime+08],rbx
mov [seeDayTime+18],rsi
mov [seeDayTime+28],rdi
mov [seeDayTime+38],rcx
mov [seeDayTime+48],rdx
mov [seeDayTime+58],rax


bkpDayTime:
  readmem(WorldTimeZa,7)
  jmp return

align 10


seeDayTime:
dq 0,0,0,0,0,0

WorldTimeZa:
  jmp newmem
  nop 2

return:
registersymbol(WorldTimeZa bkpDayTime)


[DISABLE]

WorldTimeZa:
  readmem(originalbytesDt,9)
  //db 48 8B 89 E0 00 00 00

unregistersymbol(*)
dealloc(*)

Lua code [64 bit Games]:
Spoiler

Code: Select all

local LuaScriptA_REGS = {'rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi'}
local regoffsets = { rbx = 8, rsi = 18, rdi = 28, rcx = 38, rdx = 48, rax = 58 }

local function findRecordA(descA)
  local alA = getAddressList()
  for iA = 0, alA.Count-1 do
    local recA = alA[iA]
    if recA.Description == descA then return recA end
  end
  return nil
end

local function scanDisasmA(addrA, max_stepsA)
  addrA = tonumber(addrA)
  max_stepsA = max_stepsA or 8
  for iA = 0, max_stepsA-1 do
    local disasmA = disassemble(addrA)
    if disasmA then
      for _, regA in ipairs(LuaScriptA_REGS) do
        local pat1A = "%["..regA.."%+0x([0-9A-Fa-f]+)%]"
        local foundA = disasmA:match(pat1A)
        if foundA then return regA, tonumber(foundA,16) end
        local pat2A = "%["..regA.."%+([0-9A-Fa-f]+)%]"
        foundA = disasmA:match(pat2A)
        if foundA then return regA, tonumber(foundA,16) end
      end
    end
    local stepA = getInstructionSize(addrA) or 1
    addrA = addrA + stepA
  end
  return nil, 0
end

local function updatePointerA(ptrBaseA, offsetA)
  local recA = findRecordA("CurrentValueA")  --- Insert cheat Description here
  if recA and ptrBaseA then
    recA.Address = string.format("0x%X", ptrBaseA + offsetA)
  end
end

local function pointerTimerA()
  local codeAddrA = getAddressSafe("originalbytesDt")
  if not codeAddrA then return end

  local regA, offsetA = scanDisasmA(codeAddrA, 8)
  if not regA then return end
  local symbolAddrA = getAddressSafe("seeDayTime")
  if not symbolAddrA then return end
  local offset = regoffsets[regA]
  local ptrBaseA = readQword(symbolAddrA + offset)
  if not ptrBaseA or ptrBaseA == 0 then return end
  updatePointerA(ptrBaseA, offsetA)
end

if LuaScriptATimer and LuaScriptATimer.destroy then
  LuaScriptATimer.destroy()
  LuaScriptATimer = nil
end
LuaScriptATimer = createTimer(nil, false)
LuaScriptATimer.Interval = 250
LuaScriptATimer.OnTimer = pointerTimerA
LuaScriptATimer.Enabled = true
Change these variables to the appropriate ones:
CurrentValueA---> Description (for see pointer)
originalbytesDt---> Some Bytes (normaly 4 for 32 bit games and 8 for 64 bit games)
seeDayTime---> registersymbol (for see pointer)


Lua code for 32 and 64 bit Games:
Spoiler

Code: Select all

local processIs64Bit = false
if targetIs64Bit() then processIs64Bit = true end

local LuaScriptA_REGS, regoffsets, codeSymbol, valueSymbol, readPtrFunc
if processIs64Bit then
  LuaScriptA_REGS = {'rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi'}
  regoffsets = { rbx = 8, rsi = 18, rdi = 28, rcx = 38, rdx = 48, rax = 58 }
  codeSymbol = "originalbytesDt"
  valueSymbol = "seeDayTime"
  readPtrFunc = function(addr) return readQword(addr) end
else
  LuaScriptA_REGS = {'eax', 'ebx', 'ecx', 'edx', 'esi', 'edi', 'esp', 'ebp'}
  regoffsets = { ebx = 0, esi = 4, edi = 8, ecx = 12, edx = 16, eax = 20 }
  codeSymbol = "originalbytesDt"
  valueSymbol = "seeDayTime"
  readPtrFunc = function(addr) return readInteger(addr) end
end

local function findRecordA(descA)
    local alA = getAddressList()
    for iA = 0, alA.Count-1 do
        local recA = alA[iA]
        if recA.Description == descA then return recA end
    end
    return nil
end

local function scanDisasmA(addrA, max_stepsA)
    addrA = tonumber(addrA)
    max_stepsA = max_stepsA or 8
    for iA = 0, max_stepsA-1 do
        local disasmA = disassemble(addrA)
        if disasmA then
            for _, regA in ipairs(LuaScriptA_REGS) do
                local pat1A = "%["..regA.."%+0x([0-9A-Fa-f]+)%]"
                local foundA = disasmA:match(pat1A)
                if foundA then return regA, tonumber(foundA,16) end
                local pat2A = "%["..regA.."%+([0-9A-Fa-f]+)%]"
                foundA = disasmA:match(pat2A)
                if foundA then return regA, tonumber(foundA,16) end
            end
        end
        local stepA = getInstructionSize(addrA) or 1
        addrA = addrA + stepA
    end
    return nil, 0
end

local function updatePointerA(ptrBaseA, offsetA)
    local recA = findRecordA("CurrentValueA") --- Insert cheat Description here
    if recA and ptrBaseA then
        recA.Address = string.format("0x%X", ptrBaseA + offsetA)
    end
end

local function pointerTimerA()
    local codeAddrA = getAddressSafe(codeSymbol)
    if not codeAddrA then return end
    local regA, offsetA = scanDisasmA(codeAddrA, 8)
    if not regA then return end
    local symbolAddrA = getAddressSafe(valueSymbol)
    if not symbolAddrA then return end
    local offset = regoffsets[regA]
    if not offset then return end
    local ptrBaseA = readPtrFunc(symbolAddrA + offset)
    if not ptrBaseA or ptrBaseA == 0 then return end
    updatePointerA(ptrBaseA, offsetA)
end

if LuaScriptATimer and LuaScriptATimer.destroy then
    LuaScriptATimer.destroy()
    LuaScriptATimer = nil
end
LuaScriptATimer = createTimer(nil, false)
LuaScriptATimer.Interval = 250
LuaScriptATimer.OnTimer = pointerTimerA
LuaScriptATimer.Enabled = true
Video:
Spoiler
Scripts:
registersymbol in lua (pointer in lua) method 1.CT
(31.45 KiB) Downloaded 21 times


------------------


18) registersymbol in lua (pointer in lua) method 2:

Assembly Code example 32 bit Games:
Spoiler

Code: Select all

[ENABLE]

aobscan(INJECT,D9 87 04 01 00 00 D9 EE DF F1 DD D8 7A 09) // should be unique
alloc(newmem,$1000)

alloc(originalbytesDt,9)
registersymbol(originalbytesDt)

originalbytesDt:
readmem(INJECT,9)

label(bkpDayTime)
label(return)



label(seeDayTime)
registersymbol(seeDayTime)

newmem:


mov [seeDayTime],edi


//code:
  //fld dword ptr [edi+00000104]
  jmp return

bkpDayTime:
readmem(INJECT,6)
jmp return

align 10

seeDayTime:
dd 0

INJECT:
  jmp newmem
  nop
return:
registersymbol(INJECT bkpDayTime)

[DISABLE]

INJECT:
readmem(originalbytesDt,6)
// db D9 87 04 01 00 00

unregistersymbol(*)
dealloc(*)

Lua 32 bit Games:
Spoiler

Code: Select all

{$lua}


local DEBUG = false
local LAST_ADDR = 0
local LAST_WRITTEN = 0

function genericDetectRegisterAndOffset32(origAddr, max_inspect)
    max_inspect = max_inspect or 8
    local reg_names = {'eax','ebx','ecx','edx','esi','edi'}
    origAddr = tonumber(origAddr)
    for i = 0, max_inspect-1 do
        local disasm = disassemble(origAddr)
        if DEBUG then print("[GENERIC-32] DISASM @0x"..string.format("%X", origAddr).." "..tostring(disasm)) end
        if disasm then
            for _, reg in ipairs(reg_names) do
                local pat = "%["..reg.."%+0x([0-9A-Fa-f]+)%]"
                local offset = disasm:match(pat)
                if offset then return reg, tonumber(offset,16) end
                pat = "%["..reg.."%+([0-9A-Fa-f]+)%]"
                offset = disasm:match(pat)
                if offset then return reg, tonumber(offset,16) end
            end
        end
        local size = getInstructionSize(origAddr)
        if not size or size<=0 then size=1 end
        origAddr=origAddr+size
    end
    return nil, 0
end

function createOrUpdatePointer32(addr, offset)
    local basePtr = readInteger(addr)
    if not basePtr or basePtr==0 then
        if DEBUG then print("[GENERIC-32] Invalid base pointer!") end
        return nil
    end
    local targetAddr = basePtr + offset
    unregisterSymbol("seeTime")
    registerSymbol("seeTime", targetAddr)
    if targetAddr>0x10000 and targetAddr<0x7FFFFFFF then
        LAST_ADDR = targetAddr
        if DEBUG then print(("[GENERIC-32] Calculated address @ 0x%X"):format(targetAddr)) end
        return targetAddr
    end
    if DEBUG then print("[GENERIC-32] Address out of range: 0x"..string.format("%X", targetAddr)) end
    return nil
end

local function timerProcGeneric32()
    local pointerBase = getAddressSafe("seeDayTime")
    local origInstrAddr = getAddressSafe("originalbytesDt")
    if (not pointerBase) or (not origInstrAddr) then
        if DEBUG then print("[GENERIC-32] Missing symbol(s)!") end
        return
    end
    local reg, offset = genericDetectRegisterAndOffset32(origInstrAddr,8)
    if not reg then offset=0 end
    local targetAddr = createOrUpdatePointer32(pointerBase, offset)
    -- Safety/logging mode, don't write in memory.
end

if timerGeneric32 then timerGeneric32.destroy() timerGeneric32=nil end
timerGeneric32 = createTimer(nil,false)
timerGeneric32.Interval = 100
timerGeneric32.OnTimer = timerProcGeneric32
timerGeneric32.Enabled = true

{$asm}
Assembly Code example 64 bit Games:
Spoiler

Code: Select all

[ENABLE]

aobscanmodule(WorldTimeZa,$process,48 8B ?? ?? ?? ?? ?? 49 B9 ?? ?? ?? ?? ?? ?? ?? ?? 48 B8) // should be unique
alloc(newmem,$1000,WorldTimeZa)

alloc(originalbytesDt,9)
registersymbol(originalbytesDt)

originalbytesDt:
readmem(WorldTimeZa,9)

label(bkpDayTime)
label(return)

label(seeYourValue)
registersymbol(seeYourValue)

newmem:

mov [seeYourValue],rcx



bkpDayTime:
  readmem(WorldTimeZa,7)
  jmp return

align 10

seeYourValue:
dq 0


WorldTimeZa:
  jmp newmem
  nop 2

return:
registersymbol(WorldTimeZa bkpDayTime)

[DISABLE]

WorldTimeZa:
  readmem(originalbytesDt,9)
  //db 48 8B 89 E0 00 00 00

unregistersymbol(*)
dealloc(*)
Lua 64 bit Games:
Spoiler

Code: Select all

{$lua}
local DEBUG = false
local LAST_ADDR = 0
local LAST_WRITTEN = 0

function genericDetectRegisterAndOffset(origAddr, max_inspect)
    max_inspect = max_inspect or 8
    local reg_names = {'rax','rbx','rcx','rdx','rsi','rdi'}
    origAddr = tonumber(origAddr)
    for i = 0, max_inspect-1 do
        local disasm = disassemble(origAddr)
        if DEBUG then print("[GENERIC] DISASM @0x"..string.format("%X", origAddr).." "..tostring(disasm)) end
        if disasm then
            for _, reg in ipairs(reg_names) do
                local pat = "%["..reg.."%+0x([0-9A-Fa-f]+)%]"
                local offset = disasm:match(pat)
                if offset then return reg, tonumber(offset,16) end
                pat = "%["..reg.."%+([0-9A-Fa-f]+)%]"
                offset = disasm:match(pat)
                if offset then return reg, tonumber(offset,16) end
            end
        end
        local size = getInstructionSize(origAddr)
        if not size or size<=0 then size=1 end
        origAddr=origAddr+size
    end
    return nil, 0
end

function createOrUpdatePointer(addr, offset)
    local basePtr = readQword(addr)
    if not basePtr or basePtr==0 then
        if DEBUG then print("[GENERIC] Invalid base pointer!") end
        return nil
    end
    local targetAddr = basePtr + offset
    local al = getAddressList()
    unregisterSymbol("seeDayTime")
    registerSymbol("seeDayTime", targetAddr, true)
    -- DO NOT WRITE ANY VALUE, just calculate the address!
    if targetAddr>0x10000 and targetAddr<0x7FFFFFFFFFFF then
        LAST_ADDR = targetAddr
        if DEBUG then print(("[GENERIC] Calculated address @ 0x%X"):format(targetAddr)) end
        return targetAddr
    end
    if DEBUG then print("[GENERIC] Address out of range: 0x"..string.format("%X", targetAddr)) end
    return nil
end

local function timerProcGeneric()
    local pointerBase = getAddressSafe("seeYourValue")
    local origInstrAddr = getAddressSafe("originalbytesDt")
    if (not pointerBase) or (not origInstrAddr) then
        if DEBUG then print("[GENERIC] Missing symbol(s)!") end
        return
    end
    local reg, offset = genericDetectRegisterAndOffset(origInstrAddr,8)
    if not reg then offset=0 end
    local targetAddr = createOrUpdatePointer(pointerBase, offset)
    -- No longer writing any value to memory (safety/logging only).
end

if timerGeneric then timerGeneric.destroy() timerGeneric=nil end
timerGeneric = createTimer(nil,false)
timerGeneric.Interval = 100
timerGeneric.OnTimer = timerProcGeneric
timerGeneric.Enabled = true

{$asm}
Lua 32 and 64 bit Games:
Spoiler

Code: Select all

{$lua}

-- Cheat Engine LUA auto-detect 32bit/64bit pointer + offset (multi arch)
local DEBUG = false
local LAST_ADDR = 0

--if process is 64 bit
local function is64Bit()
    return targetIs64Bit()
end

--offset+reg 64bit o 32bit
function genericDetectRegisterAndOffsetAuto(origAddr, max_inspect)
    max_inspect = max_inspect or 8
    local reg_names = is64Bit() and {'rax','rbx','rcx','rdx','rsi','rdi'} or {'eax','ebx','ecx','edx','esi','edi'}
    origAddr = tonumber(origAddr)
    for i = 0, max_inspect-1 do
        local disasm = disassemble(origAddr)
        if DEBUG then print("[GENERIC] DISASM @0x"..string.format("%X", origAddr).." "..tostring(disasm)) end
        if disasm then
            for _, reg in ipairs(reg_names) do
                local pat = "%["..reg.."%+0x([0-9A-Fa-f]+)%]"
                local offset = disasm:match(pat)
                if offset then return reg, tonumber(offset,16) end
                pat = "%["..reg.."%+([0-9A-Fa-f]+)%]"
                offset = disasm:match(pat)
                if offset then return reg, tonumber(offset,16) end
            end
        end
        local size = getInstructionSize(origAddr)
        if not size or size<=0 then size=1 end
        origAddr = origAddr + size
    end
    return nil, 0
end

--base pointer (32/64bit)
function createOrUpdatePointerAuto(addr, offset)
    local basePtr = is64Bit() and readQword(addr) or readInteger(addr)
    if not basePtr or basePtr==0 then
        if DEBUG then print("[GENERIC] Invalid base pointer!") end
        return nil
    end
    local targetAddr = basePtr + offset
    unregisterSymbol("registerDayTime")
    registerSymbol("registerDayTime", targetAddr)
    -- Range
    if is64Bit() then
        if targetAddr > 0x10000 and targetAddr < 0x7FFFFFFFFFFF then
            LAST_ADDR = targetAddr
            if DEBUG then print(("[GENERIC] 64bit Calculated address @ 0x%X"):format(targetAddr)) end
            return targetAddr
        end
    else
        if targetAddr > 0x10000 and targetAddr < 0x7FFFFFFF then
            LAST_ADDR = targetAddr
            if DEBUG then print(("[GENERIC] 32bit Calculated address @ 0x%X"):format(targetAddr)) end
            return targetAddr
        end
    end
    if DEBUG then print("[GENERIC] Address out of range: 0x"..string.format("%X", targetAddr)) end
    return nil
end

-- Timer proc unic (auto 32/64bit)
local function timerProcGenericAuto()
    local pointerBase = getAddressSafe("seeDayTime")
    local origInstrAddr = getAddressSafe("originalbytesDt")
    if (not pointerBase) or (not origInstrAddr) then
        if DEBUG then print("[GENERIC] Missing symbol(s)!") end
        return
    end
    local reg, offset = genericDetectRegisterAndOffsetAuto(origInstrAddr,8)
    if not reg then offset = 0 end
    local targetAddr = createOrUpdatePointerAuto(pointerBase, offset)
end

-- Timer global
if timerGenericAuto then
    timerGenericAuto.destroy()
    timerGenericAuto = nil
end
timerGenericAuto = createTimer(nil, false)
timerGenericAuto.Interval = 100
timerGenericAuto.OnTimer = timerProcGenericAuto
timerGenericAuto.Enabled = true

{$asm}
registersymbol in lua (pointer in lua) method 2.CT
(43.33 KiB) Downloaded 14 times

User avatar
SilverRabbit90
Table Makers
Table Makers
Posts: 252
Joined: Fri Jan 15, 2021 12:01 am
Reputation: 219

Re: Some Lua Codes (Tools)

Post by SilverRabbit90 »

-------------------

19) Copy "Structure Compare" Elements & Auto compare "Structure Compare" (Commonalities)

These are two useful codes to speed up the comparison process (using "Structure Compare," which is my preferred method for finding unique values for the compare).

The first part is to add the options "Copy Elements" or "Copy All Single Offset" to the right-click context menu (personally, I recommend the second method). When clicked, these will allow you to copy the content from "Structure Compare." The second part is the actual tool, which is used to compare previously compiled files (by pasting the content from "Structure Compare") to obtain the unique (static) values. Pressing +Col allows you to compare multiple columns in case you want to compare the values of one group against another (for example, the HP of the player and the HP of allies as a single group against the HP of enemies as a second group).

Code for copy ALL "Structure Compare" Elements:
Spoiler

Code: Select all

-- This script watches for the "Structure Compare" window to open
-- and adds a "Copy Elements" context-menu item to its TListView.
-- It uses a timer instead of a blocking loop to avoid freezing Cheat Engine.

-- Interval in milliseconds between checks
local CHECK_INTERVAL = 500

-- Create a timer to poll for the Structure Compare form
local watcherTimer = createTimer(nil, false)
watcherTimer.Interval = CHECK_INTERVAL

watcherTimer.OnTimer = function(timer)
  local scForm = nil

  -- Look for any open form whose caption contains "Structure Compare"
  for i = 0, getFormCount() - 1 do
    local form = getForm(i)
    if form and form.Caption and form.Caption:find("Structure Compare") then
      scForm = form
      break
    end
  end

  if not scForm then
    -- Not found yet; will try again on next tick
    return
  end

  -- We found the Structure Compare form: stop the timer
  timer.Enabled = false
  -- Optionally: destroyTimer(timer)  -- if you want to free it completely

  -- Find the TListView inside the Structure Compare form
  local listView = nil
  for i = 0, scForm.ComponentCount - 1 do
    local comp = scForm.Component[i]
    if comp.ClassName == "TListView" then
      listView = comp
      break
    end
  end

  if not listView then
   -- showMessage("Structure Compare: TListView not found")
    return
  end

  -- Ensure the list view has a popup menu
  if not listView.PopupMenu then
    listView.PopupMenu = createPopupMenu(listView)
  end

  -- Check if "Copy Elements" is already in the menu
  local alreadyExists = false
  for i = 0, listView.PopupMenu.Items.Count - 1 do
    if listView.PopupMenu.Items[i].Caption == "Copy Elements" then
      alreadyExists = true
      break
    end
  end

  if not alreadyExists then
    -- Create and configure the menu item
    local menuItem = createMenuItem(listView.PopupMenu)
    menuItem.Caption = "Copy Elements"
    menuItem.OnClick = function()
      -- Build the text from each list view item
      local text = {}
      for idx = 0, listView.Items.Count - 1 do
        local item = listView.Items[idx]
        local line = string.format("[%04d] %s", idx, item.Caption)
        for subIdx = 0, item.SubItems.Count - 1 do
          line = line .. string.format(" | %s", item.SubItems[subIdx])
        end
        table.insert(text, line)
      end

      -- Copy joined lines to the clipboard
      writeToClipboard(table.concat(text, "\r\n"))
      showMessage(string.format("Copied %d items to clipboard", #text))
    end

    -- Add the new item to the popup menu
    listView.PopupMenu.Items.add(menuItem)
  end
end

-- Start the watcher timer
watcherTimer.Enabled = true
Code for Copy all SINGLE Offsets elements (fixed):
Spoiler

Code: Select all

-- Lua script for Cheat Engine to add a context menu item to Structure Compare windows

-- Configuration
local CHECK_INTERVAL = 500 -- Time in milliseconds between checks for new windows
local YIELD_INTERVAL = 10 -- Process window messages every N items to prevent freezing

-- State Variables
local addedMenus = {}

-- Progress Counter Overlay GUI
local progressForm = nil
function showProgressForm()
  if not progressForm then
    progressForm = createForm(false)
    progressForm.Caption = "Progress"
    progressForm.BorderStyle = "bsNone"
    progressForm.Width = 220
    progressForm.Height = 40
    progressForm.Color = 0x00eeeeee -- Light background
    progressForm.Top = 10
    progressForm.Left = getScreenWidth() - progressForm.Width - 20
    progressForm.Position = 'poScreenCenter'
    progressForm.AlphaBlend = true
    progressForm.AlphaBlendValue = 220 -- Slightly transparent
    local lbl = createLabel(progressForm)
    lbl.Name = 'lblProgress'
    lbl.Font.Size = 12
    lbl.Font.Color = 0x00222222
    lbl.AutoSize = false
    lbl.Align = 'alClient'
    lbl.Transparent = false
    lbl.Alignment = 'taRightJustify'
    lbl.Layout = 'tlCenter'
    progressForm.ShowInTaskBar = 'stNever'
    progressForm.Visible = false
    progressForm.OnClose = function() return caHide end -- Prevent closing from outside
  end
end

function updateProgressForm(current, total)
  if progressForm then
    local lbl = progressForm.FindComponentByName('lblProgress')
    if lbl then lbl.Caption = string.format("Processing: %d / %d", current, total) end
    progressForm.Visible = true
    progressForm.BringToFront()
  end
end

function hideProgressForm()
  if progressForm then
    progressForm.Visible = false
  end
end

showProgressForm() -- Pre-create

-- Timer setup
local watcherTimer = createTimer(nil, false)
watcherTimer.Interval = CHECK_INTERVAL

watcherTimer.OnTimer = function(timer)
  for i = 0, getFormCount() - 1 do
    local form = getForm(i)
    if form and form.Caption and form.Caption:find("Structure Compare") then
      if not addedMenus[form] then
        local listView = nil
        for j = 0, form.ComponentCount - 1 do
          local comp = form.Component[j]
          if comp.ClassName == "TListView" then
            listView = comp
            break
          end
        end

        if listView then
          if not listView.PopupMenu then
            listView.PopupMenu = createPopupMenu(listView)
          end

          local alreadyExists = false
          local targetCaption = "Copy All Single Offset V2"

          if listView.PopupMenu and listView.PopupMenu.Items then
             for k = 0, listView.PopupMenu.Items.Count - 1 do
               if listView.PopupMenu.Items[k].Caption == targetCaption then
                 alreadyExists = true
                 break
               end
             end
          end

          if not alreadyExists then
            local menuItem = createMenuItem(listView.PopupMenu)
            menuItem.Caption = targetCaption

            menuItem.OnClick = function()
              local lines = {}
              local processedCount = 0
              local skippedMultiOffset = 0
              local skippedEmptyOffset = 0
              local totalItems = listView.Items.Count

              -- SHOW counter
              showProgressForm()
              updateProgressForm(0, totalItems)

              local startTime = os.clock()

              for idx = 0, totalItems - 1 do
                local item = listView.Items[idx]
                local offsetCount = 0
                local firstOffsetValue = nil

                -- Offset 0 (Caption)
                local offset0_present = item.Caption and item.Caption:match("%S")
                if offset0_present then
                    offsetCount = offsetCount + 1
                    firstOffsetValue = item.Caption
                end
                -- Offset 1 (SubItems[0])
                if offsetCount <= 1 then
                    local offset1_present = item.SubItems.Count >= 1 and item.SubItems[0] and item.SubItems[0]:match("%S")
                    if offset1_present then
                        offsetCount = offsetCount + 1
                        if firstOffsetValue == nil then firstOffsetValue = item.SubItems[0] end
                    end
                end
                -- Offset 2 (SubItems[1])
                if offsetCount <= 1 then
                    local offset2_present = item.SubItems.Count >= 2 and item.SubItems[1] and item.SubItems[1]:match("%S")
                    if offset2_present then
                        offsetCount = offsetCount + 1
                    end
                end

                if offsetCount == 1 then
                    local offsetValueToDisplay = firstOffsetValue or ""
                    local line = string.format("[%04d] %s", idx, offsetValueToDisplay)
                    local dataColumnStartIndex = 2
                    if item.SubItems.Count > dataColumnStartIndex then
                         for subIdx = dataColumnStartIndex, item.SubItems.Count - 1 do
                             line = line .. string.format(" | %s", item.SubItems[subIdx] or "")
                         end
                    end
                    table.insert(lines, line)
                    processedCount = processedCount + 1
                elseif offsetCount > 1 then
                    skippedMultiOffset = skippedMultiOffset + 1
                else
                    skippedEmptyOffset = skippedEmptyOffset + 1
                end

                -- Counter Update && prevent freezing
                if idx % YIELD_INTERVAL == 0 or idx == totalItems - 1 then
                    processMessages()
                    updateProgressForm(idx + 1, totalItems)
                end
              end

              -- Hide counter
              hideProgressForm()

              local endTime = os.clock()
              local duration = endTime - startTime

              if #lines > 0 then
                  local clipboardText = table.concat(lines, "\r\n")
                  writeToClipboard(clipboardText)
              else
                  writeToClipboard("")
              end

              -- Show final results only
              local message = string.format(
                "Operation complete (%.2f seconds).\nCopied: %d single-offset items.\nSkipped (Multi-Offset): %d items.\nSkipped (Zero-Offset): %d items.",
                duration, processedCount, skippedMultiOffset, skippedEmptyOffset)
              showMessage(message)
            --  print(message)
            end

            listView.PopupMenu.Items.add(menuItem)
          end
          addedMenus[form] = true
        end
      end
    end
  end
end

watcherTimer.Enabled = true
Code for Auto compare "Structure Compare" (Commonalities):
Spoiler

Code: Select all

--==================================================
-- Button in the main menu to launch the GUI
--==================================================

-- Create a new menu item in the top menu bar
local mainForm = getMainForm()
local mainMenu = mainForm.Menu

-- Find the "Help" menu item to insert before it
local helpIndex = -1
for i=0, mainMenu.Items.Count-1 do
  if mainMenu.Items[i].Caption == '&Help' then
    helpIndex = i
    break
  end
end

-- Create new menu item
local myMenuItem = createMenuItem(mainMenu)
myMenuItem.Caption = 'CMP Commonalities'
myMenuItem.OnClick = function()

  -- IF A WINDOW ALREADY EXISTS, DO NOT RECREATE IT
  if f and f.Visible then
    f.BringToFront()
    return
  elseif f then
    f.destroy()
    f = nil
  end

  --======================
  -- MAIN GUI START
  --======================
  local startY, stepY = 80, 60
  local fileInputs = {}
  local fileCount = 0
  local colCount  = 1
  local btnAddFile, btnCompare, btnAddCol, btnRemoveCol, lblColCount, memo

  f = createForm(false)
  f.Caption  = 'Multiple File Comparison'
  f.Position = 'poScreenCenter'
  f.Width    = 650
  f.Height   = 400

  -- AUTO-CLEANUP WHEN WINDOW IS CLOSED
  f.OnClose = function()
    f.destroy()
    f = nil
    return caFree
  end

  local dlgOpen = createOpenDialog(f)
  dlgOpen.Title  = 'Select a text file'
  dlgOpen.Filter = 'Text (*.txt)|*.txt|All files (*.*)|*.*'

  local function parseFile(path)
    local t = {}
    for line in io.lines(path) do
      local clean  = line:gsub("%.%.%..*$","")
      local offset = clean:match("%]%s*([%x]+)%s*|")
      if offset then
        local vals = {}
        for v in clean:gmatch(":%s*(%d+)") do
          table.insert(vals, tonumber(v))
        end
        if #vals>0 then t[offset] = vals end
      end
    end
    return t
  end

  local function updateLayout()
    for i,info in ipairs(fileInputs) do
      local y = startY + (i-1)*stepY
      info.lbl.Top = y - 20
      info.edt.Top = y
      info.btn.Top = y
    end
    local lastY = startY + fileCount*stepY
    btnAddFile.Top    = lastY
    btnCompare.Top    = lastY
    btnAddCol.Top     = lastY
    btnRemoveCol.Top  = lastY
    lblColCount.Top   = lastY + 4
    memo.Top          = lastY + 40
    f.Height          = memo.Top + memo.Height + 20
  end

  local function addFileUI()
    fileCount = fileCount + 1
    local i = fileCount

    local lbl = createLabel(f)
    lbl.Caption = 'File '..i..':'
    lbl.Left    = 10

    local edt = createEdit(f)
    edt.Left, edt.Width = 60, 520

    local btn = createButton(f)
    btn.Caption = '...'
    btn.Width   = 30
    btn.OnClick = function()
      if dlgOpen.Execute() then edt.Text = dlgOpen.FileName end
    end

    fileInputs[i] = { lbl=lbl, edt=edt, btn=btn }
    updateLayout()
  end

  btnAddFile = createButton(f)
  btnAddFile.Caption = 'Add File'
  btnAddFile.Left    = 10
  btnAddFile.OnClick = addFileUI

  btnCompare = createButton(f)
  btnCompare.Caption = 'Compare'
  btnCompare.Left    = 120
  btnCompare.OnClick = function()
    memo.Lines.Clear()
    local paths = {}
    for i=1,fileCount do
      local p = fileInputs[i].edt.Text
      if p~='' then table.insert(paths,p) end
    end
    if #paths<2 then
      showMessage('Select at least 2 files.')
      return
    end
    local T = {}
    for _,p in ipairs(paths) do
      local ok,tab = pcall(parseFile,p)
      if not ok then
        showMessage('Error reading '..p)
        return
      end
      table.insert(T,tab)
    end

    local matches = {}
    for offset,vals1 in pairs(T[1]) do
      if #vals1 < colCount then goto next_offset end
      local V = vals1[1]
      local ok = true
      for k=2,colCount do
        if vals1[k] ~= V then ok=false break end
      end
      if not ok then goto next_offset end
      for j=2,#T do
        local valsj = T[j][offset]
        if not valsj or #valsj < colCount then ok=false break end
        for k=1,colCount do
          if valsj[k] ~= V then ok=false break end
        end
        if not ok then break end
      end
      if not ok then goto next_offset end
      for j=1,#T do
        local valsj = T[j][offset]
        for m=colCount+1,#valsj do
          if valsj[m] == V then
            ok = false
            break
          end
        end
        if not ok then break end
      end
      if not ok then goto next_offset end
      table.insert(matches,{offset=offset, val=V})
      ::next_offset::
    end

    table.sort(matches, function(a,b)
      return tonumber(a.offset,16) < tonumber(b.offset,16)
    end)
    if #matches==0 then
      memo.Lines.Add(
        'No offsets with first ' .. colCount ..
        ' equal and unique columns.'
      )
    else
      for _,m in ipairs(matches) do
        memo.Lines.Add(
          string.format('Offset %-8s (int)%d', m.offset, m.val)
        )
      end
    end
  end

  btnAddCol = createButton(f)
  btnAddCol.Caption = '+Col'
  btnAddCol.Left    = 230
  btnAddCol.OnClick = function()
    colCount = colCount + 1
    lblColCount.Caption = 'Columns: '..colCount
  end

  btnRemoveCol = createButton(f)
  btnRemoveCol.Caption = '-Col'
  btnRemoveCol.Left    = 310
  btnRemoveCol.OnClick = function()
    if colCount>1 then
      colCount = colCount - 1
      lblColCount.Caption = 'Columns: '..colCount
    end
  end

  lblColCount = createLabel(f)
  lblColCount.Caption = 'Columns: '..colCount
  lblColCount.Left    = 390

  memo = createMemo(f)
  memo.Left, memo.Width  = 10, 620
  memo.Height            = 200
  memo.ReadOnly          = true
  memo.WordWrap          = false
  memo.ScrollBars        = ssVertical

  addFileUI()
  addFileUI()
  updateLayout()
  f.Show()
end

-- Insert the new menu item right before "Help"
if helpIndex >= 0 then
  mainMenu.Items.Insert(helpIndex, myMenuItem)
else
  mainMenu.Items.add(myMenuItem)
end
Video:
Spoiler
Scripts (fixed Copy):

--------------------


20) Save "Structure Compare" Elements in a file (no Freez):

This tool is used to save the content of the "Structure Compare" into a file by selecting the desired save path. Unlike the previously posted tool I shared, which is for copying elements, it does not have long wait times (and therefore does not freeze Cheat Engine).

Code:
Spoiler

Code: Select all

-- Configuration
local CHECK_INTERVAL = 500 -- ms between checks for new windows
local YIELD_INTERVAL = 50  -- Process window messages every N items to prevent freezing

-- State Variables
local addedMenus = {}

-- Timer setup
local watcherTimer = createTimer(nil, false)
watcherTimer.Interval = CHECK_INTERVAL

watcherTimer.OnTimer = function(timer)
    for i = 0, getFormCount() - 1 do
        local form = getForm(i)
        if form and form.Caption and form.Caption:find("Structure Compare") then
            if not addedMenus[form] then
                local listView = nil
                for j = 0, form.ComponentCount - 1 do
                    local comp = form.Component[j]
                    if comp.ClassName == "TListView" then
                        listView = comp
                        break
                    end
                end

                if listView then
                    if not listView.PopupMenu then
                        listView.PopupMenu = createPopupMenu(listView)
                    end

                    local alreadyExists = false
                    local targetCaption = "Save As"
                    if listView.PopupMenu and listView.PopupMenu.Items then
                        for k = 0, listView.PopupMenu.Items.Count - 1 do
                            if listView.PopupMenu.Items[k].Caption == targetCaption then
                                alreadyExists = true
                                break
                            end
                        end
                    end

                    if not alreadyExists then
                        local menuItem = createMenuItem(listView.PopupMenu)
                        menuItem.Caption = targetCaption

                        menuItem.OnClick = function()
                            local dlg = createSaveDialog(nil)
                            dlg.DefaultExt = "txt"
                            dlg.Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*"
                            dlg.Options = "[ofOverwritePrompt]"
                            dlg.Title = "Save Structure Compare As"

                            if dlg.Execute() then
                                local filepath = dlg.FileName
                                local f = io.open(filepath, "w")
                                if not f then
                                    showMessage("Unable to open file for writing: " .. filepath)
                                    return
                                end

                                -- Defensive: Refresh references
                                local refForm = form
                                local refListView = listView
                                if not refForm or not refListView or not refListView.Items then
                                    f:close()
                                    return
                                end

                                local count = refListView.Items.Count
                                for idx = 0, count - 1 do
                                    if not refForm or not refListView or not refListView.Items then
                                        break -- Early out if closed
                                    end
                                    local item = refListView.Items[idx]
                                    if not item then break end -- Early exit (form closed or items destroyed)

                                    local record = string.format("[%04d] %s", idx, item.Caption or "")

                                    for subidx = 0, item.SubItems.Count - 1 do
                                        local cell = item.SubItems[subidx] or ""
                                        record = record .. " | " .. cell
                                    end

                                    f:write(record .. "\r\n")

                                    if idx % YIELD_INTERVAL == 0 and idx > 0 then
                                        -- Before and after processMessages, verify the state again
                                        if not refForm or not refListView or not refListView.Items then
                                            break
                                        end
                                        processMessages()
                                        if not refForm or not refListView or not refListView.Items then
                                            break
                                        end
                                    end
                                end
                                f:close()
                                -- Only show message if still valid
                                if refForm and refListView then
                                   -- showMessage("Saved to:\n" .. filepath)
                                end
                            end
                        end

                        listView.PopupMenu.Items.add(menuItem)
                    end
                    addedMenus[form] = true
                end
            end
        end
    end
end

watcherTimer.Enabled = true
Image:
Spoiler
Image
Script:
Save Structure Compare elements.CT
(5.62 KiB) Downloaded 15 times

User avatar
SilverRabbit90
Table Makers
Table Makers
Posts: 252
Joined: Fri Jan 15, 2021 12:01 am
Reputation: 219

Re: Some Lua Codes (Tools)

Post by SilverRabbit90 »

---------------------

21) Compare Dissect Data elements:

Code:
Spoiler

Code: Select all

local f = nil

-- Create a new menu item in the top menu bar
local mainForm = getMainForm()
local mainMenu = mainForm.Menu

-- Find the "Help" menu item to insert before it
local helpIndex = -1
for i = 0, mainMenu.Items.Count - 1 do
-- Use '&' to match the menu item accelerator key
if mainMenu.Items[i].Caption == '&Help' then
helpIndex = i
break
end
end

-- Create new menu item
local myMenuItem = createMenuItem(mainMenu)
myMenuItem.Caption = 'CMP DS'
myMenuItem.OnClick = function()

-- If the window already exists, just bring it to front
if f and f.Visible then
f.BringToFront()
return
-- If the window exists but is hidden (closed), destroy it properly
elseif f then
-- Using pcall in case the form was already destroyed unexpectedly
pcall(function() f.destroy() end)
f = nil
end

--======================
-- MAIN GUI START
--======================
-- Define layout variables
local startY, stepY = 10, 45 -- Adjusted Y spacing for file inputs
local fileInputs = {}
local fileCount = 0
local colCount = 1
local btnAddFile, btnCompare, btnAddCol, btnRemoveCol, lblColCount, memo, lblFiles, grpFiles

f = createForm(false) -- Create a non-main form
f.Caption = 'Multiple File Comparison'
f.Position = 'poScreenCenter'
f.Width = 650
f.Height = 750 -- Initial height, will adjust

-- Auto‐cleanup when window is closed by the user
f.OnClose = function(sender, action)
f = nil -- Clear the global reference
-- No need to call destroy() here, CE handles it when caFree is returned
return caFree
end

-- GroupBox for file inputs
grpFiles = createGroupBox(f)
grpFiles.Caption = 'Files to Compare'
grpFiles.Left = 5
grpFiles.Top = 5
grpFiles.Width = f.Width - 20
grpFiles.Height = 100 -- Initial height, will adjust

local dlgOpen = createOpenDialog(f)
dlgOpen.Title = 'Select a text file'
dlgOpen.Filter = 'Text (*.txt)|*.txt|All files (*.*)|*.*'

-- Parse a file into a table of entries:
-- key = offset string (lowercase hex)
-- value = { type = typeString, vals = { value1, value2, ... } }
local function parseFile(path)
local t = {}
local file, err = io.open(path, "r")
if not file then
error("Could not open file: " .. path .. " (" .. (err or "unknown error") .. ")")
return nil
end

for line in file:lines() do
-- Try to match new format: "0008 - 4 Bytes addr1 : val1 addr2 : val2 ..."
-- Allows for flexible spacing and various type descriptions
local offset, typeStr, rest = line:match("^(%x+)%s*-%s*(.-)%s+(.+)$")
if offset and rest and rest:find(":") then
local vals = {}
-- Match address (optional) and value pairs
for addr, val in rest:gmatch("([%x%s]*)%:%s*([^%s]+)") do
-- Trim whitespace from captured value
val = val:match("^%s*(.-)%s*$")
table.insert(vals, val)
end
if #vals > 0 then
-- Standardize offset to lowercase hex for consistent keys
t[string.lower(offset)] = { type = typeStr:match("^%s*(.-)%s*$"), vals = vals }
end
else
-- Fallback to the original bracketed format: "[...] ] offset | ... : number"
-- Example: [Some Text] ] 001C | Some Desc : 123 : 456
local clean = line:gsub("%.%.%..*$", "") -- Remove trailing dots often from CE scans
local offset2 = clean:match("%]%s*([%x]+)%s*|")
if offset2 then
local vals2 = {}
-- Capture numeric values after colons
for v in clean:gmatch(":%s*(%S+)") do -- Capture non-space sequences as values
table.insert(vals2, v)
end
if #vals2 > 0 then
-- Standardize offset to lowercase hex
t[string.lower(offset2)] = { type = "Unknown", vals = vals2 } -- Assign a default type
end
end
end
end
file:close()
return t
end

-- Function to update the layout of dynamic controls
local function updateLayout()
local currentY = 15 -- Starting Y inside the GroupBox
for i, info in ipairs(fileInputs) do
info.lbl.Top = currentY + 2 -- Align label text better with edit box
info.edt.Top = currentY
info.btn.Top = currentY
currentY = currentY + stepY
end

-- Adjust GroupBox height
grpFiles.Height = currentY + 5

-- Position controls below the GroupBox
local bottomOfGrp = grpFiles.Top + grpFiles.Height + 10
btnAddFile.Top = bottomOfGrp
btnCompare.Top = bottomOfGrp
btnAddCol.Top = bottomOfGrp
btnRemoveCol.Top = bottomOfGrp
lblColCount.Top = bottomOfGrp + 4 -- Align label text vertically with buttons

-- Position Memo below buttons
memo.Top = bottomOfGrp + btnAddFile.Height + 10
memo.Height = f.ClientHeight - memo.Top - 10 -- Adjust memo height to fill remaining space
memo.Width = f.ClientWidth - 20 -- Adjust width based on client area

-- Adjust main form height if needed (e.g., if memo needs more space)
-- This basic example keeps form height fixed after initial setup,
-- but you could dynamically resize f.Height here if necessary.
end


-- Function to add a new file input row UI
local function addFileUI()
fileCount = fileCount + 1
local i = fileCount

-- Calculate position within the GroupBox
local yPos = 15 + (i - 1) * stepY

local lbl = createLabel(grpFiles) -- Create inside GroupBox
lbl.Caption = 'File ' .. i .. ':'
lbl.Left = 10
lbl.Top = yPos + 2 -- Adjust vertical alignment

local edt = createEdit(grpFiles) -- Create inside GroupBox
edt.Left = 50
edt.Top = yPos
edt.Width = grpFiles.Width - 100 -- Adjust width relative to GroupBox

local btn = createButton(grpFiles) -- Create inside GroupBox
btn.Caption = '...'
btn.Width = 30
btn.Top = yPos
btn.Left = edt.Left + edt.Width + 5 -- Position next to edit box
btn.OnClick = function()
if dlgOpen.Execute() then
edt.Text = dlgOpen.FileName
end
end

-- Store references
fileInputs[i] = { lbl = lbl, edt = edt, btn = btn }

-- Update the overall layout
updateLayout()
end

-- Button to Add a File Input Row
btnAddFile = createButton(f)
btnAddFile.Caption = 'Add File'
btnAddFile.Left = 10
btnAddFile.Width = 75
btnAddFile.OnClick = addFileUI

-- Button to Trigger Comparison
btnCompare = createButton(f)
btnCompare.Caption = 'Compare'
btnCompare.Left = btnAddFile.Left + btnAddFile.Width + 10
btnCompare.Width = 75
btnCompare.OnClick = function()
memo.Lines.Clear()
local paths = {}
for i = 1, fileCount do
local pathInput = fileInputs[i].edt.Text
-- Basic check if path is not empty
if pathInput and pathInput:match("%S") then -- Check for non-whitespace characters
table.insert(paths, pathInput)
end
end

if #paths < 2 then
showMessage('Please select at least 2 valid files to compare.')
return
end

-- Parse each file
local T = {} -- Table to hold parsed data from each file
local success = true
for i, p in ipairs(paths) do
-- Use pcall to catch errors during file parsing
local ok, result = pcall(parseFile, p)
if not ok then
showMessage('Error parsing file ' .. i .. ' (' .. p .. '):\n' .. tostring(result))
success = false
break -- Stop processing if one file fails
end
if result == nil then
showMessage('Parsing returned nil for file ' .. i .. ' (' .. p .. '). Check file format or path.')
success = false
break
end
table.insert(T, result)
end

-- Only proceed if all files were parsed successfully
if not success then
return
end

-- Find offsets where the first 'colCount' values are all equal across all files,
-- and none of the later columns (within the same offset/file) equals that same common value.
local matches = {}
-- Iterate through offsets found in the first file
if T[1] then
for offset, info1 in pairs(T[1]) do
-- Ensure the first file has enough columns at this offset
if #info1.vals < colCount then goto continue end

-- Get the value(s) to compare from the first file's relevant columns
local commonValue = info1.vals[1] -- The value that must be common
local ok = true -- Flag to track if conditions are met

-- 1. Check if the first 'colCount' columns in the first file are equal to commonValue
for k = 1, colCount do
-- Compare as strings for consistency, as values can be numbers or text
if tostring(info1.vals[k]) ~= tostring(commonValue) then
ok = false
break
end
end
if not ok then goto continue end -- Skip offset if first file fails

-- 2. Check the same condition for all other files
for j = 2, #T do
local info_j = T[j][offset] -- Use the same offset (already lowercased)
-- Check if the offset exists and has enough columns in the current file
if not info_j or #info_j.vals < colCount then
ok = false
break -- Offset missing or insufficient columns in file j
end
-- Check if the first 'colCount' columns match the commonValue
for k = 1, colCount do
if tostring(info_j.vals[k]) ~= tostring(commonValue) then
ok = false
break -- Mismatch found in file j
end
end
if not ok then break end -- Stop checking other files if mismatch found
end
if not ok then goto continue end -- Skip offset if mismatch found in other files

-- 3. Ensure none of the remaining columns (colCount + 1 onwards) in *any* file equals the commonValue
for j = 1, #T do
local info_j = T[j][offset] -- Already verified this exists for j > 1
-- Check columns from colCount + 1 to the end
for m = colCount + 1, #info_j.vals do
if tostring(info_j.vals[m]) == tostring(commonValue) then
ok = false -- Found the common value in a later column
break
end
end
if not ok then break end -- Stop checking other files if common value found later
end
if not ok then goto continue end -- Skip offset if common value found in later columns

-- If all checks passed, record the match
table.insert(matches, {
offset = offset, -- Already lowercase hex string
type = info1.type, -- Type from the first file
val = commonValue -- The common value found
})
::continue:: -- Lua goto label for skipping to the next offset
end
else
showMessage("The first file selected appears to be empty or could not be parsed correctly.")
return
end


-- Sort matches numerically based on the hexadecimal offset
table.sort(matches, function(a, b)
-- Convert hex offset strings to numbers for proper sorting
return tonumber(a.offset, 16) < tonumber(b.offset, 16)
end)

-- Display results in the memo field
if #matches == 0 then
memo.Lines.Add(
'No offsets found where the first ' .. colCount ..
' column(s) are equal across all files and unique from subsequent columns.'
)
else
memo.Lines.Add(string.format('%-20s %s', 'Offset', 'Value (Type if specific)')) -- Header
memo.Lines.Add(string.rep('-', 40)) -- Separator line

for _, m in ipairs(matches) do
local offsetStr = string.upper(m.offset) -- Display offset in uppercase
local offsetPart = string.format("Offset %s", offsetStr)
local outputLine

-- Check if the type is one of the specified ones for special formatting
-- Using string.match for case-insensitive comparison might be safer depending on input files
local typeLower = string.lower(tostring(m.type or "")) -- Handle potential nil type safely
if typeLower == 'int' or typeLower == 'float' or typeLower == 'double' or typeLower:find("byte") then
-- Format with type included for specific types (int, Float, Double, or contains 'byte')
outputLine = string.format('%-20s (%s)%s', offsetPart, m.type, m.val)
else
-- Format without type for other types
outputLine = string.format('%-20s %s', offsetPart, m.val)
end
memo.Lines.Add(outputLine)
end
end
end

-- Button to Increment Column Count
btnAddCol = createButton(f)
btnAddCol.Caption = '+Col'
btnAddCol.Left = btnCompare.Left + btnCompare.Width + 10
btnAddCol.Width = 50
btnAddCol.OnClick = function()
colCount = colCount + 1
lblColCount.Caption = 'Columns: ' .. colCount
end

-- Button to Decrement Column Count
btnRemoveCol = createButton(f)
btnRemoveCol.Caption = '-Col'
btnRemoveCol.Left = btnAddCol.Left + btnAddCol.Width + 5
btnRemoveCol.Width = 50
btnRemoveCol.OnClick = function()
if colCount > 1 then
colCount = colCount - 1
lblColCount.Caption = 'Columns: ' .. colCount
end
end

-- Label to Display Current Column Count
lblColCount = createLabel(f)
lblColCount.Caption = 'Columns: ' .. colCount
lblColCount.Left = btnRemoveCol.Left + btnRemoveCol.Width + 10

-- Memo Field for Displaying Results
memo = createMemo(f)
memo.Left = 10
memo.Top = 180 -- Initial position, updated by layout
memo.Width = f.Width - 30 -- Adjust width
memo.Height = 200 -- Adjust height
memo.ReadOnly = true
memo.WordWrap = false
memo.ScrollBars = ssVertical -- Only vertical scrollbar usually needed for this output
-- Use a monospaced font for better alignment
memo.Font.Name = 'Courier New'
memo.Font.Size = 9

-- Initialize with two file input rows
addFileUI()
addFileUI()
-- Call updateLayout once initially to set positions correctly
updateLayout()

-- Show the form
f.Show()

end -- End of myMenuItem.OnClick function

-- Insert the new menu item right before "Help" if found, otherwise add to the end
if helpIndex >= 0 then
mainMenu.Items.Insert(helpIndex, myMenuItem)
else
mainMenu.Items.Add(myMenuItem)
end
Image:
Spoiler
Image
Script:
Compare Dissect Data elements.CT
(13.04 KiB) Downloaded 12 times

User avatar
SilverRabbit90
Table Makers
Table Makers
Posts: 252
Joined: Fri Jan 15, 2021 12:01 am
Reputation: 219

Re: Some Lua Codes (Tools)

Post by SilverRabbit90 »

----------------------

22) Speed Up Script Enable:

This simple Lua code is used to speed up the activation of a script, which, since it doesn’t have modules, is generally slower to activate. Normally you could use aobscanregion, but I’ve noticed that it doesn’t work very well for all games.

A) You need to comment out or delete the aobscan. For example, comment out or delete this part of the aob code:

//aobscan(Test,0F 2F 43 7C 0F 83 06 02 00 00)

B) You need to allocate the name of the cheat. In the example above, it’s “Test,” so you should write something like this:

alloc(newmem, $1000, Test)

C) Finally, you need to comment out or delete the registersymbol that you find under 'return,' otherwise the game will crash.

return:
//registersymbol(Test)

There’s no need to worry if you get this error:

[Warning]

Not all code is injectable.
(Error in line 26 (alloc(newmem,$1000,Test)) :Failure determining what Test means)
Are you sure you want to edit it to this?

[Yes] [No]

This is completely normal; just select OK and then enable the code, and you will notice the difference in speed with which the code is activated (much faster than before).

Code to insert in a Script:
Spoiler

Code: Select all

{$lua}
if syntaxcheck then return end
function aob_register(sym, pat)
  instr = AOBScan(pat, "+X")
  addy = instr[0]
  instr.destroy()
  addy = tonumber(addy, 16)
  unregisterSymbol(sym)
  registerSymbol(sym, addy)
end

aob_register("Insert Cheat Name Here","Insert Array Here")
{$asm}
Script Example:
Spoiler

Code: Select all

[ENABLE]

{$lua}
if syntaxcheck then return end
function aob_register(sym, pat)
  instr = AOBScan(pat, "+X")
  addy = instr[0]
  instr.destroy()
  addy = tonumber(addy, 16)
  unregisterSymbol(sym)
  registerSymbol(sym, addy)
end

aob_register("Test","0F 2F 43 7C 0F 83 06 02 00 00")
{$asm}

//aobscan(Test,0F 2F 43 7C 0F 83 06 02 00 00) // should be unique  <-- Comment or Delete this part
alloc(newmem,$1000,Test) // <-- You must alloc cheat name (in this case Test)

label(code)
label(return)

newmem:

mov [rbx+7C],(float)55

code:
  comiss xmm0,[rbx+7C]
  jae GameAssembly.dll+522D81
  jmp return

Test:
  jmp newmem
  nop 5
return:
//registersymbol(Test)  <-- Comment or Delete this part

[DISABLE]

Test:
  db 0F 2F 43 7C 0F 83 06 02 00 00

unregistersymbol(Test)
dealloc(newmem)
Speed Up Script Enable.CT
(3.42 KiB) Downloaded 17 times

Post Reply

Who is online

Users browsing this forum: No registered users