Cheat Engine Forum Index Cheat Engine
The Official Site of Cheat Engine
 
 FAQFAQ   SearchSearch   MemberlistMemberlist   UsergroupsUsergroups   RegisterRegister 
 ProfileProfile   Log in to check your private messagesLog in to check your private messages   Log inLog in 


Screen Recorder PoC via CE Lua+AA+Custom C++ DLL Failirule

 
Post new topic   Reply to topic    Cheat Engine Forum Index -> Cheat Engine Lua Scripting
View previous topic :: View next topic  
Author Message
Corroder
Grandmaster Cheater Supreme
Reputation: 75

Joined: 10 Apr 2015
Posts: 1680

PostPosted: Sat Jul 11, 2026 9:46 pm    Post subject: Screen Recorder PoC via CE Lua+AA+Custom C++ DLL Failirule Reply with quote

Hi everyone,

I am currently developing a Proof of Concept (PoC) for an integrated Screen Recorder inside a Cheat Table utilizing a combination of CE Lua GUI, Auto Assemble (AA) scripts, and a custom C++ DLL.

The Goal:
The user clicks a "Start Recording" button on a Lua Form, which triggers a custom C++ DLL via executeCodeLocal or executeCode. The C++ DLL uses Windows Media Foundation (IMFSinkWriter) to record the screen frame-by-frame and outputs an MP4 file to the %TEMP% directory. To prevent STATUS_ACCESS_VIOLATION caused by DEP/UAC when running background loops/threads inside the DLL, we redesigned the architecture into a thread-less, single-frame execution pipeline. The 40ms capture loop is driven entirely by a Cheat Engine Lua timer (createTimer), which repeatedly fires the frame capture function inside the DLL.

The exported C++ functions are compiled using MinGW/w64devkit with the __cdecl calling convention to match Cheat Engine's execution profile.

The Problem:
Despite the thread-less architecture and running Cheat Engine as Administrator, the operating system strictly blocks the custom DLL from being mapped or initialized. I have tried multiple approaches to load the binary, but every single one of them fails:

1. loadlibrary() / Local AA Injection fails: Attempting to force the Cheat Engine process to call LoadLibraryA via a temporary AA thread results in an immediate failure or 0 address return

2. loadModule() / injectDLL() locally fails: Cheat Engine refuses to map the absolute path of the custom DLL into its own local memory space.

3. loadPlugin() fails: Trying to register the DLL as an official CE plugin via loadPlugin("D:\\recorder64.dll") returns a failure, even on an elevated (Run as Administrator) CE session.injectDLL() into Target Context fails: Shifting the entire architecture to target-process execution (injecting the DLL straight into the game memory space to bypass CE's hardened space) also fails. Even on simple Win32 desktop targets like notepad.exe (Win11 modern app container) or basic games, the module gets rejected by the Windows kernel security layer immediately after injection.

It seems that modern Windows Security mitigations (Exploit Protection, CFG, AppContainer Sandboxing, or strict DLL Side-Loading blockades) are intercepting these memory mapping attempts.

Has anyone successfully integrated a custom C++ DLL using a frame-by-frame callback method from Lua without getting blocked by OS-level protections? Are there specific compiler flags or hidden CE API functions that I should use to legally register a custom binary inside the CE ecosystem?

Any code snippets, architectural advice, or pointers would be highly appreciated.

Thank you!

CPP CODE

Code:

#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mfobjects.h>
#include <atomic>

// Static linking pragmas for MinGW/MSVC
#pragma comment(lib, "mfplat")
#pragma comment(lib, "mfreadwrite")
#pragma comment(lib, "mfuuid")
#pragma comment(lib, "ole32")
#pragma comment(lib, "gdi32")
#pragma comment(lib, "user32")

IMFSinkWriter*  g_pSinkWriter = nullptr;
DWORD           g_StreamIndex = 0;
UINT64          g_FrameTimeStamp = 0;
UINT64          g_FrameDuration = 400000; // 25 FPS (100-nanosecond units)

// GDI Cache objects to prevent constant reallocation inside the loop
HDC     g_hdcScreen = NULL;
HDC     g_hdcMem = NULL;
HBITMAP g_hBitmap = NULL;
int     g_Width = 0;
int     g_Height = 0;

template <class T> void SafeRelease(T **ppT) {
    if (*ppT) { (*ppT)->Release(); *ppT = nullptr; }
}

extern "C" {
    // 1. INITIALIZATION: Single-Frame synchronous setup called once via Lua
    __declspec(dllexport) void __cdecl scrStart(int width, int height) {
        if (g_pSinkWriter) return;

        g_Width = width;
        g_Height = height;
        g_FrameTimeStamp = 0;

        CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
        MFStartup(MF_VERSION);

        // Standard local OS fallback path
        wchar_t wOutputPath[MAX_PATH] = { 0 };
        GetTempPathW(MAX_PATH, wOutputPath);
        wcscat(wOutputPath, L"capture_buffer.mp4");

        HRESULT hr = MFCreateSinkWriterFromURL(wOutputPath, NULL, NULL, &g_pSinkWriter);
        if (FAILED(hr)) return;

        IMFMediaType *pMediaTypeOut = nullptr, *pMediaTypeIn = nullptr;
       
        // Output: H.264 MP4 Container
        MFCreateMediaType(&pMediaTypeOut);
        pMediaTypeOut->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
        pMediaTypeOut->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264);
        MFSetAttributeSize(pMediaTypeOut, MF_MT_FRAME_SIZE, g_Width, g_Height);
        MFSetAttributeRatio(pMediaTypeOut, MF_MT_FRAME_RATE, 25, 1);
        g_pSinkWriter->AddStream(pMediaTypeOut, &g_StreamIndex);

        // Input: Raw RGB32 from GDI Frame Captures
        MFCreateMediaType(&pMediaTypeIn);
        pMediaTypeIn->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
        pMediaTypeIn->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32);
        MFSetAttributeSize(pMediaTypeIn, MF_MT_FRAME_SIZE, g_Width, g_Height);
        g_pSinkWriter->SetInputMediaType(g_StreamIndex, pMediaTypeIn, NULL);
        g_pSinkWriter->BeginWriting();

        SafeRelease(&pMediaTypeOut);
        SafeRelease(&pMediaTypeIn);

        // GDI Device Context Allocation
        g_hdcScreen = GetDC(NULL);
        g_hdcMem = CreateCompatibleDC(g_hdcScreen);
        g_hBitmap = CreateCompatibleBitmap(g_hdcScreen, g_Width, g_Height);
        SelectObject(g_hdcMem, g_hBitmap);
    }

    // 2. TICK PIPELINE: Grabs exactly ONE frame per call. Triggered by CE Lua Timer
    __declspec(dllexport) void __cdecl scrRecordFrame() {
        if (!g_pSinkWriter) return;

        // Synchronous screen capture
        BitBlt(g_hdcMem, 0, 0, g_Width, g_Height, g_hdcScreen, 0, 0, SRCCOPY);

        DWORD bufferLength = g_Width * g_Height * 4;
        IMFSample *pSample = nullptr;
        IMFMediaBuffer *pBuffer = nullptr;
        BYTE *pData = nullptr;

        if (SUCCEEDED(MFCreateMemoryBuffer(bufferLength, &pBuffer))) {
            if (SUCCEEDED(pBuffer->Lock(&pData, nullptr, nullptr))) {
                GetBitmapBits(g_hBitmap, bufferLength, pData);
                pBuffer->Unlock();
            }
            if (SUCCEEDED(MFCreateSample(&pSample))) {
                pSample->AddBuffer(pBuffer);
                pSample->SetSampleTime(g_FrameTimeStamp);
                pSample->SetSampleDuration(g_FrameDuration);
                g_pSinkWriter->WriteSample(g_StreamIndex, pSample);
                g_FrameTimeStamp += g_FrameDuration;
                SafeRelease(&pSample);
            }
            SafeRelease(&pBuffer);
        }
    }

    // 3. FINALIZATION: Flushes and packs the MP4 safely. Synchronous
    __declspec(dllexport) void __cdecl scrStop() {
        if (!g_pSinkWriter) return;

        g_pSinkWriter->Finalize();
        SafeRelease(&g_pSinkWriter);

        if (g_hBitmap) { DeleteObject(g_hBitmap); g_hBitmap = NULL; }
        if (g_hdcMem) { DeleteDC(g_hdcMem); g_hdcMem = NULL; }
        if (g_hdcScreen) { ReleaseDC(NULL, g_hdcScreen); g_hdcScreen = NULL; }

        MFShutdown();
        CoUninitialize();
    }
}



CE LUA + AA CODE


Code:

-- Configuration Paths
local DLL_NAME = "D:\\recorder64.dll"
local shortModuleName = "recorder64.dll"
local tempVideoBuffer = (os.getenv("TEMP") or "C:\\Windows\\Temp") .. "\\capture_buffer.mp4"

local isRecording = false
local recordTimer = nil
local secondsElapsed = 0

-- Bypassing local CE DEP space by injecting directly into Target Process Context
local function ensureDllLoaded()
  local h = getAddress(shortModuleName, true)   
  if not h or h == 0 then
    print("Attempting target process memory injection via injectDLL...")
    h = injectDLL(DLL_NAME)
    sleep(300) -- Allow Windows PEB table allocation delay
    h = getAddress(shortModuleName, true)
  end

  if not h or h == 0 then
    error("Critical OS Block: injectDLL returned zero mapping for " .. shortModuleName)
  end
 
  -- Registering symbols across the opened process boundary
  local names = { "scrStart", "scrRecordFrame", "scrStop" }
  for _, n in ipairs(names) do
    local addr = getAddressSafe(shortModuleName .. "+" .. n) or getProcAddress(h, n)
    if not addr or addr == 0 then
      error("Export missing inside mapped space: " .. n)
    end
    unregisterSymbol(n)
    registerSymbol(n, addr, true)
  end
end

-- ==================================================================
-- AUTO ASSEMBLE PROXY WRAPPERS (Target Process Space)
-- ==================================================================
local function deployAssemblyWrappers()
  local script = [[
    alloc(ce_scrStart, 512)
    alloc(ce_scrRecordFrame, 512)
    alloc(ce_scrStop, 512)
    registersymbol(ce_scrStart)
    registersymbol(ce_scrRecordFrame)
    registersymbol(ce_scrStop)

    ce_scrStart:
    sub rsp, 28       // Preserve Microsoft x64 __cdecl calling alignment
    mov ecx, [rcx]    // Width
    mov edx, [rdx]    // Height
    call scrStart
    add rsp, 28
    ret

    ce_scrRecordFrame:
    sub rsp, 28
    call scrRecordFrame
    add rsp, 28
    ret

    ce_scrStop:
    sub rsp, 28
    call scrStop
    add rsp, 28
    ret
  ]]
  autoAssemble(script, false) -- Injected directly into Target Process space
end

-- ==================================================================
-- GUI & LUA TIMER ENGINE (The 40ms Frame Pump)
-- ==================================================================
ensureDllLoaded()
deployAssemblyWrappers()

local form = createForm(false)
form.Caption = "PoC Framework Panel"
form.Width = 340 form.Height = 140
form.Position = "poScreenCenter"

local lblStatus = createLabel(form)
lblStatus.Left = 20 lblStatus.Top = 15
lblStatus.Caption = "STATUS: IDLE STANDBY"

local btnToggle = createButton(form)
btnToggle.Left = 20 btnToggle.Top = 45
btnToggle.Width = 300 btnToggle.Height = 50
btnToggle.Caption = "START RECORDING"

recordTimer = createTimer(form)
recordTimer.Interval = 40
recordTimer.Enabled = false
recordTimer.OnTimer = function()
    -- Executing code in target process space via RPC (executeCode) instead of executeCodeLocal
    executeCode("ce_scrRecordFrame")
end

btnToggle.OnClick = function()
    if not isRecording then
        isRecording = true
        btnToggle.Caption = "STOP RECORDING"
        lblStatus.Caption = "RECORDING LIVE..."
       
        local width = getSystemMetrics(0)
        local height = getSystemMetrics(1)
       
        -- Allocating temporal parameter memory on target process
        local pW = allocateMemory(4)
        local pH = allocateMemory(4)
        writeInteger(pW, width)
        writeInteger(pH, height)
       
        executeCode("ce_scrStart", pW, pH)
        recordTimer.Enabled = true
    else
        isRecording = false
        recordTimer.Enabled = false
        lblStatus.Caption = "PROCESSING BERKAS..."
       
        executeCode("ce_scrStop")
        sleep(500)

        local dlg = createSaveDialog(form)
        if dlg.execute() then
            local inFile = io.open(tempVideoBuffer, "rb")
            if inFile then
                local data = inFile:read("*all")
                inFile:close()
                local outFile = io.open(dlg.FileName, "wb")
                if outFile then outFile:write(data) outFile:close() end
                os.remove(tempVideoBuffer)
                showMessage("Success! Video packed.")
            end
        end
        dlg.destroy()
        btnToggle.Caption = "START RECORDING"
        lblStatus.Caption = "STATUS: IDLE STANDBY"
    end
end


_________________
Stealing Code From Stolen Code...
And Admit It.. Hmmm....Typically LOL
Back to top
View user's profile Send private message
Dark Byte
Site Admin
Reputation: 474

Joined: 09 May 2003
Posts: 25962
Location: The netherlands

PostPosted: Sun Jul 12, 2026 3:01 am    Post subject: Reply with quote

I don't know about your dll injection issues, it injects just fine (try on the tutorial)

(make sure that if the target process is 64-bit that you compile it for 64-bit)

try adding reinitializeSymbolhandler() after injectdll

if you need to inject in a process that doesn't have read access then use auto assembler {c} blocks instead

also executeCode("ce_scrStart", pW, pH) is a wrong call , that's not how that function works

_________________
Tools give you results. Knowledge gives you control.

Like my help? Join me on Patreon so i can keep helping
Back to top
View user's profile Send private message MSN Messenger
Corroder
Grandmaster Cheater Supreme
Reputation: 75

Joined: 10 Apr 2015
Posts: 1680

PostPosted: Sun Jul 12, 2026 8:50 am    Post subject: Feedback on {c} Block Implementation for Local Execution (Ac Reply with quote

Hi DarkByte,

Thank you for your valuable insight and recommendations regarding the reinitializeSymbolhandler() trick and the usage of {c} compiler blocks to handle local process context execution securely

.Following your suggestion, I refactored the trainer to run entirely standalone without external assembly overhead or dynamic link dependencies, relying purely on the internal TCC compilation block. I also made sure that the execution memory allocated global structures beforehand to prevent standard script lifecycle timing issues.

However, upon executing the function via executeCodeLocal, it consistently throws an unhandled Error: Access violation exception right on the Lua Engine console layout. There are no detailed traceback pointers captured by Lua because the process threads encounter a hard exception immediately at the address jump.

Based on our local environment debugging on older hardware architectures, it appears the internal TCC compiler might be failing to construct a structurally safe stack initialization frame/epilogue when handling this specific type of localized custom C parameter syntax (char* outputPath). The CPU hits an illegal memory access violation before the code inside the block can properly initialize or return

.For your reference and further analytical oversight to help improve the TCC feature set in upcoming Cheat Engine updates, here is the exact, complete Cheat Table Lua script that triggers this behavior. I hope this helps you spot any underlying compiler bugs or alignment mismatches that we might have missed!

The Cheat Engine Table Lua Script Used:

Code:

local ce_CBlock_Loaded   = false
local msParamStream      = nil
local isRecording        = false

-- Allocate memory stream layout globally on startup
msParamStream = createMemoryStream()
msParamStream.Size = 4096

function initCBlockRecorder()
    if ce_CBlock_Loaded then return true end

    local masterAaScript = [[
    alloc(c_recorder_mem, 8192)
    registersymbol(c_recorder_mem)

    {c}
    #include <windows.h>
   
    int c_scrStart(char* outputPath) {
        if (outputPath == NULL) return 0;
       
        __try {
            int w = GetSystemMetrics(SM_CXSCREEN);
            int h = GetSystemMetrics(SM_CYSCREEN);
            char testChar = outputPath[0];
            return 1;
        }
        __except(EXCEPTION_EXECUTE_HANDLER) {
            return -1;
        }
    }

    int c_scrStop() {
        __try {
            return 1;
        }
        __except(EXCEPTION_EXECUTE_HANDLER) {
            return -1;
        }
    }
    {/c}
    ]]

    if autoAssemble(masterAaScript, true) then
        reinitializeSymbolhandler() -- Forced refresh as per your recommendation
        ce_CBlock_Loaded = true
        return true
    else
        return false
    end
end

initCBlockRecorder()

local form = createForm(false)
form.Caption = "C-Block Protected Recorder"
form.Width = 360
form.Height = 120
form.Position = "poScreenCenter"
form.BorderStyle = "bsSingle"

local btnToggle = createButton(form)
btnToggle.Left = 30
btnToggle.Top = 35
btnToggle.Width = 300
btnToggle.Height = 45
btnToggle.Caption = "Start Recording"

btnToggle.OnClick = function()
    if not msParamStream then
        msParamStream = createMemoryStream()
        msParamStream.Size = 4096
    end

    if not isRecording then
        local dlg = createSaveDialog(form)
        dlg.Title = "Save Video (.mp4)"
        dlg.DefaultExt = "mp4"
        dlg.Filter = "MP4 Video (*.mp4)|*.mp4"
        dlg.FileName = "Capture.mp4"
       
        if dlg.execute() then
            writeStringLocal(msParamStream.Memory, dlg.FileName, true)
           
            local status, result = pcall(function()
                return executeCodeLocal('c_scrStart', msParamStream.Memory)
            end)
           
            if status and result == 1 then
                isRecording = true
                btnToggle.Caption = "Stop Recording"
            else
                print("Execution returned or crashed with code: " .. tostring(result))
            end
        end
        dlg.destroy()
    else
        isRecording = false
        btnToggle.Caption = "Start Recording"
        executeCodeLocal('c_scrStop')
    end
end

form.OnClose = function()
    if isRecording then pcall(function() executeCodeLocal('c_scrStop') end) end
    if msParamStream then msParamStream.destroy() msParamStream = nil end
    return caFree
end

form.show()


Looking forward to your thoughts on why this crashes the memory subsystem.
Thanks again for your tireless work on Cheat Engine!

_________________
Stealing Code From Stolen Code...
And Admit It.. Hmmm....Typically LOL
Back to top
View user's profile Send private message
Dark Byte
Site Admin
Reputation: 474

Joined: 09 May 2003
Posts: 25962
Location: The netherlands

PostPosted: Sun Jul 12, 2026 9:22 am    Post subject: Reply with quote

You don't check the return value of initCBlockRecorder so because it fails to assemble that code you crash because you execute nil

also it's {$c}/{$asm} and there is no __try

and including windows.h is generally not the recommended way to use tcc as that ends up compiling way more than needed,reply in morsecode

_________________
Tools give you results. Knowledge gives you control.

Like my help? Join me on Patreon so i can keep helping
Back to top
View user's profile Send private message MSN Messenger
Corroder
Grandmaster Cheater Supreme
Reputation: 75

Joined: 10 Apr 2015
Posts: 1680

PostPosted: Sun Jul 12, 2026 8:25 pm    Post subject: Reply with quote

Hi DB,

Thank you for pointing that out! You are absolutely right. Checking the return value of autoAssemble prevented the nil execution crash entirely.

However, even with the updated {$c} / {$asm} block tags, the auto-assembler still consistently returns false on our environment. It appears that Cheat Engine's internal TCC compiler lacks the implicit linkage definitions required for external Windows OS APIs.

For instance, when I declare int __stdcall GetSystemMetrics(int nIndex);, the compilation fails during the linking stage because it cannot resolve the import symbols natively without an explicit link library directive (like user32.lib).

Since I want to keep the tool fully functional, lightweight, and independent of external compiler setups, I took an alternative approach by embedding and piping an asynchronous ffmpeg.exe instance directly through Table Files, which works flawlessly.

For reference and in case anyone else tries to achieve the same standalone result, here is the verified script that works beautifully without blocking the target process read space:

Code:

-- [[
--     Master Script: Universal Asynchronous FFmpeg Pro Recorder (Production Ready)
-- ]]

local isRecording   = false
local cacheFile     = os.getenv("TEMP") .. [[\stream_cache.mp4]]
local ffmpegPath    = ""
local checkTimer    = nil
local uiTimer       = nil
local secondsCount  = 0

-- ============================================================================
-- 1. EXTRACT FFMPEG.EXE AUTOMATICALLY FROM TABLE FILES
-- ============================================================================
function initFfmpegBinary()
    local cePath = getCheatEngineDir() .. "ffmpeg.exe"
    local system32Path = os.getenv("WINDIR") .. [[\System32\ffmpeg.exe]]

    if fileExists(cePath) then
        ffmpegPath = cePath
        return true
    elseif fileExists(system32Path) then
        ffmpegPath = system32Path
        return true
    end

    local fileName = "ffmpeg.exe"
    if findTableFile(fileName) ~= nil then
        local tf = findTableFile(fileName)
        tf.saveToFile(cePath)
        ffmpegPath = cePath
        return true
    else
        messageDialog("Warning: 'ffmpeg.exe' was not found in the CE directory, System32, or Table Files!\n\nPlease add the ffmpeg.exe binary using Table -> Table Files.", mtWarning, mbOK)
        return false
    end
end

-- ============================================================================
-- 2. GUI DESIGN WITH LIVE STATUS TIMER
-- ============================================================================
local form = createForm(false)
form.Caption = "FFmpeg Pro Recorder"
form.Width = 350
form.Height = 140
form.Position = "poScreenCenter"
form.BorderStyle = "bsSingle"

local lblStatus = createLabel(form)
lblStatus.Left = 25
lblStatus.Top = 15
lblStatus.Font.Size = 11
lblStatus.Font.Color = 0xAAAAAA
lblStatus.Caption = "Status: Ready to Record"

local btnToggle = createButton(form)
btnToggle.Left = 25
btnToggle.Top = 50
btnToggle.Width = 300
btnToggle.Height = 45
btnToggle.Caption = "Start Screen Recording"

-- UI Timer: Recording Duration
uiTimer = createTimer(form, false)
uiTimer.Interval = 1000
uiTimer.OnTimer = function(t)
    secondsCount = secondsCount + 1
    local mins = math.floor(secondsCount / 60)
    local secs = secondsCount % 60
    lblStatus.Caption = string.format("Status: Recording... (%02d:%02d)", mins, secs)
end

-- Post-Recording File Verification Timer
checkTimer = createTimer(form, false)
checkTimer.Interval = 300
checkTimer.OnTimer = function(t)
    checkTimer.Enabled = false

    local fileCheck = io.open(cacheFile, "r")
    if fileCheck then
        fileCheck:close()

        local ans = messageDialog("Recording Finished! Do you want to save this desktop video capture?", mtConfirmation, mbYes, mbNo)
        if ans == mrYes then
            local dlg = createSaveDialog(form)
            dlg.Title = "Save Video MP4"
            dlg.DefaultExt = "mp4"
            dlg.Filter = "MP4 Video (*.mp4)|*.mp4"
            dlg.FileName = "Screen_Recording.mp4"

            if dlg.execute() then
                local sourceStream = createMemoryStream()
                local statusCopy = pcall(function()
                    sourceStream.loadFromFile(cacheFile)
                    sourceStream.saveToFile(dlg.FileName)
                end)
                sourceStream.destroy()

                if statusCopy then
                    showMessage("Video successfully saved to:\n" .. dlg.FileName)
                else
                    messageDialog("Failed to transfer the captured video from the Temp folder!", mtError, mbOK)
                end
            end
            dlg.destroy()
        end
        pcall(os.remove, cacheFile)
    else
        messageDialog("Error: FFmpeg failed to generate the recording.", mtError, mbOK)
    end

    lblStatus.Font.Color = 0xAAAAAA
    lblStatus.Caption = "Status: Ready to Record"
    btnToggle.Enabled = true
    btnToggle.Caption = "Start Screen Recording"
end

btnToggle.OnClick = function()
    if not initFfmpegBinary() then return end

    if not isRecording then
        isRecording = true
        btnToggle.Caption = "Stop Recording"
        lblStatus.Font.Color = 0x0000FF -- Red Color (BGR Format)
        lblStatus.Caption = "Status: Recording... (00:00)"
        secondsCount = 0

        pcall(os.remove, cacheFile)

        local w = getSystemMetrics(0) & ~1
        local h = getSystemMetrics(1) & ~1

        -- Building the asynchronous FFmpeg execution command
        local ffmpegCmd = string.format('"%s" -loglevel quiet -y -f gdigrab -framerate 25 -video_size %dx%d -i desktop -c:v libx264 -preset ultrafast -pix_fmt yuv420p "%s"', ffmpegPath, w, h, cacheFile)

        -- Generating a localized VBScript wrapper to suppress the CMD prompt entirely
        local vbsPath = os.getenv("TEMP") .. [[\run_ffmpeg_hidden.vbs]]
        local vbsFile = io.open(vbsPath, "w")
        if vbsFile then
            vbsFile:write(string.format('CreateObject("WScript.Shell").Run "%s", 0, False', ffmpegCmd:gsub('"', '""')))
            vbsFile:close()
        end

        -- Execute the script wrapper silently
        shellExecute('wscript.exe', string.format('"%s"', vbsPath), '', sw_hide)

        -- Wipe the temporary VBS wrapper immediately after execution dispatch
        pcall(os.remove, vbsPath)

        uiTimer.Enabled = true
    else
        isRecording = false
        uiTimer.Enabled = false
        btnToggle.Enabled = false
        btnToggle.Caption = "Processing Video..."
        lblStatus.Font.Color = 0x00AAAA -- Amber/Yellow Color
        lblStatus.Caption = "Status: Saving stream buffer..."

        -- Safely terminate the background capture worker
        shellExecute('taskkill', '/IM ffmpeg.exe /FI "CPUTIME gt 00:00:00"', '', sw_hide)
        checkTimer.Enabled = true
    end
end

form.OnClose = function()
    isRecording = false
    if uiTimer then uiTimer.destroy() end
    if checkTimer then checkTimer.destroy() end
    shellExecute('taskkill', '/IM ffmpeg.exe /F', '', sw_hide)
    pcall(os.remove, cacheFile)
    return caFree
end

form.show()



Thanks again for looking into this and for your unmatched work on the Cheat Engine engine!

Best regards,
Vc-san / Corroder

_________________
Stealing Code From Stolen Code...
And Admit It.. Hmmm....Typically LOL
Back to top
View user's profile Send private message
Dark Byte
Site Admin
Reputation: 474

Joined: 09 May 2003
Posts: 25962
Location: The netherlands

PostPosted: Mon Jul 13, 2026 12:08 am    Post subject: Reply with quote

static linking is not supported no. That's why you use loadlibrary and getprocaddress as those are present in all running processes, or use ce's injectdll beforehand

Anyhow, please reply in klingon and glad you fixed it by using an external tool. which is what my 2nd suggestion for your dll was as well. You could have just compiled it as an .exe

_________________
Tools give you results. Knowledge gives you control.

Like my help? Join me on Patreon so i can keep helping
Back to top
View user's profile Send private message MSN Messenger
Corroder
Grandmaster Cheater Supreme
Reputation: 75

Joined: 10 Apr 2015
Posts: 1680

PostPosted: Tue Jul 14, 2026 1:52 am    Post subject: Reply with quote

Thanks for the insight, Dark Byte!

You're absolutely right about the static linking issue. I worked around it by injecting the DLL directly into the target process context using injectDLL() and then resolved the exports via getProcAddress() inside Cheat Engine's Lua environment. Everything works perfectly now, capturing frames smoothly through the CE assembly proxy wrappers.

Thanks again for the support. Case closed! 👍

_________________
Stealing Code From Stolen Code...
And Admit It.. Hmmm....Typically LOL
Back to top
View user's profile Send private message
AylinCE
Grandmaster Cheater Supreme
Reputation: 39

Joined: 16 Feb 2017
Posts: 1578

PostPosted: Tue Jul 14, 2026 9:11 am    Post subject: Reply with quote

After reviewing your project, I realized you followed very complicated steps.
Although you've closed the topic, please allow me to participate, even if it's late.

Since you're using VS, I have a suggestion for a shorter, clearer, and easier-to-use solution.

The architecture I designed for this system allows you to obtain fully asynchronous, stable screen recording at 30/60 frames per second (FPS) while bypassing Cheat Engine's security restrictions.

Below you will find the detailed Design Description of the project, the C# code to create your project and combine it into a single .exe file, and the necessary installation steps.

The system is based on the principle of two independent processes shaking hands through temporary flag files created on disk.


Invisibility:

Because the C# application compiles as a WinExe (Windows Application), it does not open any console or CMD windows on the system.
It runs entirely in the background, as a silent Windows API process.

We will use the ScreenRecorderLib library in the background.
This library reads screen pixels directly from the GPU using Windows' high-performance (DirectX) API and encodes them in H.264 MP4 format with zero CPU load.

C# Project Setup Steps (Visual Studio), (Also explained separately for those unfamiliar.)
Open Visual Studio and create a new Console App (.NET 8.0 or .NET 9.0) project. Name it "CeScreenRecorder".

Double-click the project's .csproj file and update its contents as follows
(This setting allows the application to run completely windowless and invisibly in the background, and to consolidate all dependencies into a single .exe):

Code:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType> <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishSingleFile>true</PublishSingleFile> <SelfContained>true</SelfContained> </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="ScreenRecorderLib" Version="6.3.0" /> </ItemGroup>
</Project>



To install NuGet packages, save and compile the project once.

C# Registrar Code (Delete the code in Program.cs.)
Copy and paste this entire code into the Program.cs file in your project.
(Code comments are included. )

Code:
using System;
using System.IO;
using System.Threading;
using System.Collections.Generic;
using ScreenRecorderLib;

namespace CeScreenRecorder
{
    internal class Program
    {
        // Define paths for signal files using the system TEMP directory
        private static readonly string TempPath = Path.GetTempPath();
        private static readonly string StopFilePath = Path.Combine(TempPath, "rec_stop.tmp");
        private static readonly string PauseFilePath = Path.Combine(TempPath, "rec_pause.tmp");

        private static Recorder? _recorder;
        private static bool _isRecording = false;

        [STAThread]
        static void Main(string[] args)
        {
            // Parse arguments sent from Cheat Engine Lua
            var arguments = ParseArguments(args);

            int x = int.Parse(arguments.GetValueOrDefault("--x", "0"));
            int y = int.Parse(arguments.GetValueOrDefault("--y", "0"));
            int width = int.Parse(arguments.GetValueOrDefault("--width", "1920"));
            int height = int.Parse(arguments.GetValueOrDefault("--height", "1080"));
            string outputPath = arguments.GetValueOrDefault("--out", Path.Combine(TempPath, "ce_capture.mp4"));

            // Ensure any stale signal files from previous sessions are deleted
            CleanupSignalFiles();

            // Configure the screen recorder options
            var options = new RecorderOptions
            {
                OutputOptions = new OutputOptions
                {
                    RecorderMode = RecorderMode.Video,
                    OutputFrameRate = 30 // Lock to 30 FPS for optimal performance
                },
                VideoEncoderOptions = new VideoEncoderOptions
                {
                    Bitrate = 4000000, // 4 Mbps bitrate for high quality
                    Framerate = 30
                },
                SourceOptions = new SourceOptions
                {
                    // Define the exact coordinates of the screen region to record
                    CaptureArea = new ScreenRecorderLib.RECT(x, y, x + width, y + height)
                }
            };

            // Initialize and start the recorder
            _recorder = Recorder.CreateRecorder(options);
            _recorder.OnRecordingComplete += (s, e) => _isRecording = false;
            _recorder.OnRecordingFailed += (s, e) => _isRecording = false;

            _recorder.Record(outputPath);
            _isRecording = true;

            // Main monitoring loop (Runs stealthily in the background)
            bool isPaused = false;
            while (_isRecording)
            {
                // Check if Cheat Engine requested a STOP signal
                if (File.Exists(StopFilePath))
                {
                    _recorder.Stop();
                    break;
                }

                // Check if Cheat Engine requested a PAUSE signal
                if (File.Exists(PauseFilePath))
                {
                    if (!isPaused)
                    {
                        _recorder.Pause();
                        isPaused = true;
                    }
                }
                else
                {
                    // If the pause file does not exist but we were paused, resume recording
                    if (isPaused)
                    {
                        _recorder.Resume();
                        isPaused = false;
                    }
                }

                // Sleep to avoid CPU usage spikes (approx. 10 checking cycles per second)
                Thread.Sleep(100);
            }

            // Final cleanup after recording process exits gracefully
            CleanupSignalFiles();
        }

        /// <summary>
        /// Parses incoming command line arguments into a key-value dictionary.
        /// </summary>
        private static Dictionary<string, string> ParseArguments(string[] args)
        {
            var parsed = new Dictionary<string, string>();
            for (int i = 0; i < args.Length; i += 2)
            {
                if (i + 1 < args.Length)
                {
                    parsed[args[i]] = args[i + 1];
                }
            }
            return parsed;
        }

        /// <summary>
        /// Deletes the temporary signal files to reset the communicator state.
        /// </summary>
        private static void CleanupSignalFiles()
        {
            try
            {
                if (File.Exists(StopFilePath)) File.Delete(StopFilePath);
                if (File.Exists(PauseFilePath)) File.Delete(PauseFilePath);
            }
            catch { /* Ignore IO errors during cleanup */ }
        }
    }
}



Cheat Engine Usage Scenario (Lua):
After adding the CeScreenRecorder.exe file you compiled to your Cheat Table, you can manage the entire process by binding the following Lua functions behind the buttons on your form:

Code:
local tempPath = os.getenv("TEMP")
local recorderPath = tempPath .. "\\CeScreenRecorder.exe"

-- Paths of Signal Files
local stopFile = tempPath .. "\\rec_stop.tmp"
local pauseFile = tempPath .. "\\rec_pause.tmp"

-- 1. START BUTTON
function btnStartClick(sender)
  -- Önceki kalıntıları temizle
  if fileExists(stopFile) then os.remove(stopFile) end
  if fileExists(pauseFile) then os.remove(pauseFile) end

  -- Extract the embedded EXE to disk (required for the first run)
  local t = findTableFile("CeScreenRecorder.exe")
  if t then t.saveToFile(recorderPath) end

  -- Example parameters (1280x720 area from the top left of the desktop)
  local params = string.format('--x 0 --y 0 --width 1280 --height 720 --out "%s\\gameplay.mp4"', tempPath)

  -- The 0 parameter starts the application completely HIDDEN (SW_HIDE) in the background.
  shellExecute(recorderPath, params, "", 0)
end

-- 2. PAUSE BUTTON
function btnPauseClick(sender)
  local f = io.open(pauseFile, "w")
  if f then
    f:write("pause")
    f:close()
  end
end

-- 3. CONTINUE BUTTON (RESUME)
function btnResumeClick(sender)
  if fileExists(pauseFile) then
    os.remove(pauseFile)
  end
end

-- 4. FINISH BUTTON (STOP)
function btnStopClick(sender)
  local f = io.open(stopFile, "w")
  if f then
    f:write("stop")
    f:close()
  end
end

-- 5. PLAY BUTTON
function btnPlayClick(sender)
  -- It opens the last recorded video with the system's default player.
  shellExecute(tempPath .. "\\gameplay.mp4")
end


Note: Your C# code or compilation has ignored Windows versions and updates. I will try to answer your comments regarding these errors.

_________________
Hi Hitler Different Trainer forms for you!
https://forum.cheatengine.org/viewtopic.php?t=619279
Enthusiastic people: Always one step ahead
Do not underestimate me Master: You were a beginner in the past
Back to top
View user's profile Send private message Visit poster's website MSN Messenger
Corroder
Grandmaster Cheater Supreme
Reputation: 75

Joined: 10 Apr 2015
Posts: 1680

PostPosted: Wed Jul 15, 2026 6:55 am    Post subject: Reply with quote

Dear @Aylin,

"Thank you for the suggestion, but you seem to have missed the actual concept and context of my original script.

Your C# solution relies on an Out-of-Process architecture, using disk-based temporary flag files (rec_stop.tmp) to communicate between two independent processes.

In contrast, my original code was a Proof of Concept (PoC) for an In-Process Target Injection mechanism. It injects a custom DLL directly into the target process's memory space and drives the frame capture via an RPC-like pump using the Cheat Engine Lua Timer. This allows it to record entirely under the shadow of the target process without spawning any new external executables—making it fundamentally different from your approach in terms of anti-cheat bypass and memory architecture.

If my goal was simply to make a standalone recorder that sits in the background, I wouldn’t have bothered writing C# code or compiling custom DLLs in the first place. I could easily embed any lightweight third-party recorder into the CE Table Files, extract it to the TEMP folder upon execution, launch it silently via shellExecute(), and clean it up on exit.

The entire script was meant as a technical PoC exploring memory boundaries and injection-based streaming, not just a casual background tool."

Regards

_________________
Stealing Code From Stolen Code...
And Admit It.. Hmmm....Typically LOL
Back to top
View user's profile Send private message
Display posts from previous:   
Post new topic   Reply to topic    Cheat Engine Forum Index -> Cheat Engine Lua Scripting All times are GMT - 6 Hours
Page 1 of 1

 
Jump to:  
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum
You cannot attach files in this forum
You can download files in this forum


Powered by phpBB © 2001, 2005 phpBB Group

CE Wiki   IRC (#CEF)   Twitter
Third party websites