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 


Basic 32-bit Lua Function Call from Assembly

 
Post new topic   Reply to topic    Cheat Engine Forum Index -> Cheat Engine Tutorials
View previous topic :: View next topic  
Author Message
gibberishh
Cheater
Reputation: 1

Joined: 30 Aug 2021
Posts: 37

PostPosted: Wed Jul 20, 2022 11:15 pm    Post subject: Basic 32-bit Lua Function Call from Assembly Reply with quote

I've recently finished wrestling with Lua function calls in a 32-bit environment and the biggest part of the struggle for me was a lack of detailed documentation. So maybe this tutorial (or rather discussion) will be useful to some. I intend to be as detailed as possible and will also describe some mistakes I made or wrong turns I took so you can avoid the same. I'll try and take you on the whole journey so maybe you can learn from the evolution of the code. Note that for now this only addresses passing of a single parameter to Lua from asm.

Game: This was implemented on a version of The Hell 2.

Goal
The game is broken up into maps. Every time a map loads, the game loads a list of all the monsters that inhabit that map. My player has 3 Summon spells which summon 3 generic creatures (let's call them Dog, Cat, Pig). However, if I can get access to the list of monsters, I can replace the creature Id of my sidekick with the monster Id, thus summoning a copy of The Boss to fight for me.
  • Minor Goal 1: Get assembly to pass the count of monsters into Lua and update a memrec-dropdown list with generic values (Monster 1, Monster 2, etc.). The list has to be updated on every map load.
  • Minor Goal 2: Map 1 has 15 monster Ids and I set Id 14 to be my sidekick. I proceed to Map 2. Map 2 offers up only 10 creatures. In this case I need to reset my memrec value to a safe number because 14 will cause the game to crash.
  • Minor Goal 3: Change the dropdown list to display actual names of monsters.
  • Minor Goal 4: Summoning certain monsters causes the game to crash -- they are in perpetual development and haven't implemented all creatures properly. So I want to filter out buggy creatures.

Game Data & Code Structure

  • I found the function where the game enumerates the list of monsters. This is where I will inject code for our Lua function call.
  • Within this function is a loop that goes through all the creature ids (my generic friendly creatures as well as the monsters).
  • The game's summon mechanism is not simply calling the monster id. It is instead calling creatures based on an offset from an address. This is how the creature data is stored:
    Code:
    TH2.exe+Address+($150*0) = DogId
    TH2.exe+Address+($150*1) = CatId
    TH2.exe+Address+($150*2) = PigId
    TH2.exe+Address+($150*3) = Monster1Id
    TH2.exe+Address+($150*4) = Monster2Id

    So my memrec needs to hold the counter values (0,1,2,3,4), not the monster ids. The game's code itself works with the counter values and handles the *$150 part on its own. This is super for Minor Goal 1. Howver, by the time we get to Minor Goal 3, it doesn't matter whether we need counter or id, both are possible to get.

Code for Minor Goal 1:
The first thing I want to do is make sure I get the most basic functionality correct:
  • Write basic asm to aobscanmodule() for the counter, and register this as a symbol that can be loaded into a memrec.
  • Use this counter to populate a dropdownlist.
  • Include a 5-second timer to continuously update the list while the cheat is active. That way I can make sure that the list is updating properly on map loads.
I'm not providing code for this basic setup because this much should be straightforward to anyone reading this.

Now to remove the timer and pass the counter to my Lua function as a parameter:
Code:
{$lua}
function PopulateCreatureList(CreatureCount)
  local friendly=[[0:Dog
1:Cat
2:Pig]] -- the list of my generic friendly summons which are always safe to summon and always available.

  local clist = getAddressList().getMemoryRecordByDescription("Creature")
  local creatures = friendly
  for i=3,CreatureCount-1 do -- starting loop at 3 because I don't want friendlies to be named as Monster X
    creatures = creatures .. "\n" .. i .. ":Monster " .. (i-3)
  end
  clist.DropDownList.Text = creatures
  clist.DropDownReadOnly = true
  clist.DropDownDescriptionOnly = true
  clist.DisplayAsDropDownListItem = true
end

{$asm}
[ENABLE]
LuaCall(PopulateCreatureList(0)) // Initialize the dropdown list. Only the friendlies will be displayed until map load.

LoadLibrary(luaclient-i386.dll) // This library is required for Lua to be able to interface with asm
LuaCall(openLuaServer('CELUASERVER')) // Necessary

CELUA_ServerName:  // Necessary
  db 'CELUASERVER',0  // Any name you wish, doesn't matter, but some server name is necessary

aobscanmodule(sumcount,$process,39 35 * * * * 0F 8E * * * * 6B CB 31) // injection point
alloc(orisumcount,6)  // holds the first 6 bytes for the DISABLE script
label(CreatureCount)  // WANT TO PASS THIS TO LUA
registersymbol(sumcount,orisumcount)
alloc(newmem,500)
label(code)
label(return)
alloc(luaFuncName,256) // holds the function name that you want to call
alloc(luaFuncId,4)     // Lua generates an id for the function, which we will require later to call it
label(hasrefid)        // code executed only if Lua has assigned an Id to the function

luaFuncName:
  db 'PopulateCreatureList',0 // set the name of the function we want to call
orisumcount:
  readmem(sumcount,6)  // remmber the first 6 bytes of the scan for the DISABLE script
newmem:
  mov [CreatureCount],bl  // bl holds the counter. Move it to the CreatureCount label so we can access it for the Lua function

  push edx          // The Lua function caller functions overwrite several registers
  push ecx          // Always push all required registers so that they can be restored
  push eax          // once you are done with the Lua part, or your game could crash.
  mov eax,[luaFuncId]
  test eax,eax      // Checks if the function has been assigned an Id
  jne hasrefid      // If Id not 0, go and call the Lua function
  push luaFuncName  // Else, generate and assign an Id
                    // In 32-bit, you need to push the value on to the stack for the CELUA function to be able to access it.
  call CELUA_GetFunctionReferenceFromName // This assigns an Id and stores it in eax
  mov [luaFuncId],eax  // and this copies over the Id to our allocated memory region so that we can use it again and again
hasrefid:
// According to celua.txt, CELUA_ExecuteFunctionByReference expects 4 parameters:
// int refid, int paramcount, PVOID *parameters, BOOL async
// Note that these need to be fed in the REVERSE order
// Like with the Id generator, this function also works with values from the stack, so let's push them:
  push 0     // Synchronous = 0, Async = 1. If you use 1, the UI probably won't update
  push CreatureCount  // The parameter we want to pass to our function
  push 1     // The number of parameters we are passing
  push eax   // The Id of the Lua function. It could've been copied to eax from [luaFuncId] OR from the Id generator
  call CELUA_ExecuteFunctionByReference  // This will call the function and (in our case) update the dropdown list
  pop eax    // Several registers are overwritten at this point
  pop ecx    // Remember to pop back the values stored earlier
  pop edx    // Always leave your stack (and preferably registers and flags) in the state in which you entered newmem
code:
  reassemble(sumcount) // The actual code that was supposed to execute at our injection point
  jmp return
CreatureCount:
  db #2      // The default (friendly) number of creatures we want in our list (zero-based in our case)
sumcount:
  jmp newmem
  nop
return:

[DISABLE]
sumcount:
  readmem(orisumcount,6)  // Restore the original code and clean up
unregistersymbol(sumcount,orisumcount)
dealloc(orisumcount,newmem,luaFuncName,luaFuncId)

We now have a list that updates on every map load without a timer! Hooray.

Minor Goal 2:
Ensure that a safe creature is always set for summoning. If the memrec value is out of bounds of the map's list of creatures, the game will crash. For this, I'm going to slightly modify the Lua function:
Code:
{$lua}
function PopulateCreatureList(CreatureCount)
  local friendly=[[0:Dog
1:Cat
2:Pig]]

  local clist = getAddressList().getMemoryRecordByDescription("Creature")
  local creatures = friendly
  for i=3,CreatureCount-1 do    creatures = creatures .. "\n" .. i .. ":Monster " .. (i-3)
  end
-- CODE FOR SAFE CREATURE SELECTION
  if tonumber(clist.Value) > CreatureCount then
    clist.Value = 2 -- A safe creature to default to if current value is out of bounds on a new map
  end
-- END CODE FOR SAFE CREATURE SELECTION
  clist.DropDownList.Text = creatures
  clist.DropDownReadOnly = true
  clist.DropDownDescriptionOnly = true
  clist.DisplayAsDropDownListItem = true
end

Lessons for me:
So far, everything was breezy. Okay, I struggled because it was my first time, but I needed to pass only a single value to the Lua function.
  • It took me a while (and some help from this forum) to figure out that the 32-bit dll functions require parameters to be pushed on to the stack.
  • It took me even more time to figure out that the parameters need to be supplied to the function caller in reverse order.
  • Finally, I had to figure out which registers were being overwritten by the various functions so that I could restore them before returning to the game code.
Once these were taken care of, everything worked like a charm.

Minor Goal 3:
Now this was the big one. How could I pass a list of parameters (monster ids) to my Lua function so that I might properly name the creatures in the dropdown list? Frankly, Gocl knows, ParkourPenguin knows and DarkByte knows. I'll be damned if I understand it. You are supposed to pass an address which holds a list of your parameters and then parse that within Lua. Which is all Greek to me.
I got extremely lucky instead. The game's code already has a loop which goes through each monster id. And it's not very far from my current injection point. So I'm going to use that loop to pass one monster at a time to my function. Sucks to be a newb, but that's me.

Here's the code that does this. I'll only point out the major differences:
Code:
{$lua}
local creaturecount = 0 -- Maintain a counter in Lua instead of having asm pass it
local lastCreature = 2  -- This will store the value of the last-selected creature. Here it's defaulted to 2
-- Remember that now the function is called in a loop with the counter starting from 0.
-- Checking for out-of-bounds against every tick will always reset the selection.
-- But we want to reset it only if it is out of bounds.
function PopulateCreatureList(cid)
  local clist = getAddressList().getMemoryRecordByDescription("Summon Creature")
  local creatures = clist.DropDownList.Text -- I also hard-set the friendly creatures as the ddlist's options

  if(cid == 109) then  -- Dog's MonsterId (or CreatureId). We are checking if this is the first creature being passed in
    lastCreature = tonumber(clist.Value)  -- Store the value of the currently selected creature
    creaturecount = 0     -- Reset the counter
    clist.DropDownList.Text = ''
    creatures = clist.DropDownList.Text  -- Reset the ddlist options
  else
    creaturecount = creaturecount + 1  -- Increment the counter
    creatures = creatures .. creaturecount .. ":" .. monsters[cid] -- monsters[] is an array (table) that stores the Id:Name of monsters
  end
  clist.Value = (lastCreature < creaturecount) and lastCreature or 2  -- Check if current value out of map bounds
  clist.DropDownList.Text = creatures
  clist.DropDownReadOnly = true
  clist.DropDownDescriptionOnly = true
  clist.DisplayAsDropDownListItem = true
end

{$asm}
[ENABLE] // Removed the Lua call to initialize the dropdown since default values are stored in the control's properties.
LoadLibrary(luaclient-i386.dll)
LuaCall(openLuaServer('CELUASERVER'))

CELUA_ServerName:
  db 'CELUASERVER',0

aobscanmodule(sumcount,$process,8B 45 FC 89 7D F8)
label(CreatureCount)
registersymbol(sumcount)
alloc(newmem,500)
label(code)
label(return)
alloc(luaFuncName,256)
alloc(luaFuncId,4)
label(hasrefid)

luaFuncName:
  db 'PopulateCreatureList',0
newmem:
  push ebx      // Note that I'm pushing more registers on to the stack
  push edi      // Learnt the hard way to push everything to be safe
  push edx      // And even now I'm not pushing a couple of registers
  push ecx      // Guess I'm living on the edge
  push eax
  mov [CreatureCount],ecx  // ecx holds the MonsterId. We no longer need bl
           // I got lucky here, but it was just as simple to write code to find
           // the monster id because it just so happens that edi holds the base
           // address and bl still holds the counter. so base+bl*$150 would have
           // given me the monster Id.
           // I know because that's what I did first, until I noticed that ecx
           // actually already held the value I needed!! (sheepish grin)
           // Damn, I should've renamed the label to CreatureId. Grrr
  mov eax,[luaFuncId]
  test eax,eax
  jne hasrefid
  push luaFuncName
  call CELUA_GetFunctionReferenceFromName
  mov [luaFuncId],eax
hasrefid:
  push 0
  push CreatureCount
  push 1
  push eax
  call CELUA_ExecuteFunctionByReference
  pop eax
  pop ecx
  pop edx
  pop edi
  pop ebx       // Reset all registers
code:
  mov eax,[ebp-04]  // This was the code I injected at
  mov [ebp-08],edi  // Just spitting it back out here
  jmp return
CreatureCount:
  db #109           // Dog's CreatureId
sumcount:
  jmp newmem
  nop
return:

[DISABLE]
sumcount:
  db 8B 45 FC 89 7D F8
unregistersymbol(sumcount)
dealloc(newmem,luaFuncName,luaFuncId)

{$lua}
-- On disable, reset the dropdown list options to only the friendlies.
getAddressList().getMemoryRecordByDescription("Summon Creature").DropDownList.Text = [[0:Dog
1:Cat
2:Pig]]

Minor Goal 4:
I know which creatures are safe to call. I have a list (actually an array, which in Lua is actually a table). From Minor Goal 3 you can see that monsters[] holds a list of all the Id:Name of monsters I am comfortable calling to do my dirty work. I have deleted the buggy creatures from that list. So here's how I modified my Lua function:
Code:
function PopulateCreatureList(cid)
  ...

  if(cid == 109) then
    ...
  else
    creaturecount = creaturecount + 1
    if monsters[cid] then -- if the creatureid exists in my list
      creatures = creatures .. creaturecount .. ":" .. monsters[cid] -- then append it to the ddlist options
    end
  end

  ...
end

There's probably a much more efficient way of getting the monster ids in one go from assembly into Lua, but until I properly learn and understand how to pass a reference to a list of parameters, this will have to do. Luckily the loop usually runs only 10-20 times per map load or this would be quite bad.

This tutorial illustrates how to call a Lua function with a single parameter in a 32-bit environment and what pitfalls to avoid -- because that was quite confusing for me in the first go. Hope someone finds it useful.

_________________
It's not cheating. It's playing by my rules.
Back to top
View user's profile Send private message
Dark Byte
Site Admin
Reputation: 458

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

PostPosted: Thu Jul 21, 2022 4:46 am    Post subject: This post has 1 review(s) Reply with quote

If you wanted to pass a list of parameters you could allocate some space on the stack (sub esp,paramcount*4) , then fill in [esp+0, esp+4, esp+8,... the parameters
and then give as parameter the address where the parameters are stored. Of course, the pushing of parameters is going to mess up esp, so you may want to copy the value of esp to a register before you start pushing the parameters

don't forget to add esp,paramcount*4 else you're going to have issues


For those who aren't confident in their assembly skills, (registers and stack can be difficult) then you can alternatively opt for {$luacode} which will take most of the headache away from you. (At the cost of a slightly less efficient transition between code and lua, as it saves everything before and restores everything after)

e.g the final script I think I can rewrite into:
Code:

{$lua}
local creaturecount = 0 -- Maintain a counter in Lua instead of having asm pass it
local lastCreature = 2  -- This will store the value of the last-selected creature. Here it's defaulted to 2
-- Remember that now the function is called in a loop with the counter starting from 0.
-- Checking for out-of-bounds against every tick will always reset the selection.
-- But we want to reset it only if it is out of bounds.
function PopulateCreatureList(cid)
  local clist = getAddressList().getMemoryRecordByDescription("Summon Creature")
  local creatures = clist.DropDownList.Text -- I also hard-set the friendly creatures as the ddlist's options

  if(cid == 109) then  -- Dog's MonsterId (or CreatureId). We are checking if this is the first creature being passed in
    lastCreature = tonumber(clist.Value)  -- Store the value of the currently selected creature
    creaturecount = 0     -- Reset the counter
    clist.DropDownList.Text = ''
    creatures = clist.DropDownList.Text  -- Reset the ddlist options
  else
    creaturecount = creaturecount + 1  -- Increment the counter
    --creatures = creatures .. creaturecount .. ":" .. monsters[cid] -- monsters[] is an array (table) that stores the Id:Name of monsters
    if monsters[cid] then -- if the creatureid exists in my list
      creatures = creatures .. creaturecount .. ":" .. monsters[cid] -- then append it to the ddlist options
    end
  end
  clist.Value = (lastCreature < creaturecount) and lastCreature or 2  -- Check if current value out of map bounds
  clist.DropDownList.Text = creatures
  clist.DropDownReadOnly = true
  clist.DropDownDescriptionOnly = true
  clist.DisplayAsDropDownListItem = true
end

{$asm}
[ENABLE]
aobscanmodule(sumcount,$process,8B 45 FC 89 7D F8)
registersymbol(sumcount)
alloc(newmem,500)
label(code)
label(return)

newmem:
{$luacode cid=ECX}
synchronize(function() --use synchronize as GUI controls are extensively accessed by PopulateCreatureList
  PopulateCreatureList(cid)
end)
{$asm}

code:
  mov eax,[ebp-04]  // This was the code I injected at
  mov [ebp-08],edi  // Just spitting it back out here
  jmp return

sumcount:
  jmp newmem
  nop
return:

[DISABLE]
sumcount:
  db 8B 45 FC 89 7D F8
unregistersymbol(sumcount)
dealloc(newmem,luaFuncName,luaFuncId)

{$lua}
-- On disable, reset the dropdown list options to only the friendlies.
getAddressList().getMemoryRecordByDescription("Summon Creature").DropDownList.Text = [[0:Dog
1:Cat
2:Pig]]

_________________
Do not ask me about online cheats. I don't know any and wont help finding them.

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

Joined: 30 Aug 2021
Posts: 37

PostPosted: Thu Jul 21, 2022 9:04 pm    Post subject: Reply with quote

Dark Byte wrote:
If you wanted to pass a list of parameters you could allocate some space on the stack (sub esp,paramcount*4) , then fill in [esp+0, esp+4, esp+8,... the parameters
and then give as parameter the address where the parameters are stored. Of course, the pushing of parameters is going to mess up esp, so you may want to copy the value of esp to a register before you start pushing the parameters

don't forget to add esp,paramcount*4 else you're going to have issues

Right now, my biggest confusion is how to define my Lua function, and how to access the passed parameters within it. From everything I've read online, I can't find a Lua function to take multiple parameters from asm. Maybe I suck at searching.

1. Does my function need to be defined as:
  • function tester(vara, varb, varc)
  • function tester(paramaddress) -- maybe the first param at that address is the count?
  • function tester(paramcount, paramaddress)
  • function tester(...)
2. And then how do I access the passed parameters (a variable number of them) within the function? Is it through args[#], paramaddress+(paramcount*4), or some other method?

A sample script for 32-bit would be tremendously helpful -- the Lua function can just print() the param values as integers in a loop, no need to use my ddlist code as the starting point.

* I can modify my asm to store ecx at any location required. That part is easier. This is how I'd do it:
Code:
push edx
push eax
mov edx,MonsterIds // address of label or allocated memory region
movzx eax,bl // counter
dec eax  // bl starts from 1, this ensures 0 base
mov [edx+eax*4],ecx // ecx holds 1 monster id, I can spit it out every 4 bytes
pop eax
pop edx
In my example, MonsterIds would be 'filled' by code injected within the asm loop (Minor Goal 3). The Lua function would then be called outside the loop (Minor Goal 1).

3. Do I need to mov [MonsterIds] to [esp+something]? I don't quite understand that part, but if I have working code to read, I'm confident I'll get to understanding it eventually.

Once I've built MonsterIds, I can push the value of bl (movzx-ed into a full register) as my paramcount and MonsterIds as my paramaddress before calling the Lua function. But what to do in the Lua part?

Thanks.

_________________
It's not cheating. It's playing by my rules.
Back to top
View user's profile Send private message
Dark Byte
Site Admin
Reputation: 458

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

PostPosted: Fri Jul 22, 2022 10:30 am    Post subject: Reply with quote

the function(...) methid works and then reference the .n field of ... for the count

though it may be easier to just give one address as parameter and then use readInteger/related to parse the data you have formatted in the way you like

_________________
Do not ask me about online cheats. I don't know any and wont help finding them.

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

Joined: 30 Aug 2021
Posts: 37

PostPosted: Fri Jul 22, 2022 1:34 pm    Post subject: Reply with quote

Dark Byte wrote:
the function(...) methid works and then reference the .n field of ... for the count

though it may be easier to just give one address as parameter and then use readInteger/related to parse the data you have formatted in the way you like

Wait... so how does paramcount influence the whole process?

If I'm going to pass an address to do parsing within Lua, my paramcount needs to be only 1? Once I have that address in Lua, how do I read an infinite number of bytes? How do I know when to stop reading bytes? There may be some non-zero bytes following my monsterids?

What if I set my monster Ids every 2 bytes instead of 4 using [esp+2*paramcount]? There are 259 creatures in the game so I need 2 bytes per id though I can afford 4 bytes per. Is the function caller always expecting parameters to be 4-bytes long and will fail if I have paramcount=8, but only 16 bytes stored for params on the stack?

I'm afraid your replies seem extremely cryptic to me. I've declared myself to be a newb. Embarassed A little more detail would be much appreciated. Thanks!

_________________
It's not cheating. It's playing by my rules.
Back to top
View user's profile Send private message
Dark Byte
Site Admin
Reputation: 458

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

PostPosted: Fri Jul 22, 2022 1:42 pm    Post subject: Reply with quote

the parameter list is a list of integers.
If parametercount is 5, then it expects a list of 5 integers at the parameter address (5 times 4 byte) if you wish to use a different format, then as I mentioned, use 1 single parameter holding the address of your actual data, and then in lua use readShortInteger(address+index*2) to read through it

You are responsible of allocating the list and filling it in your assembler code, so it should never be an unpredicted amount.
(Just keep it less than 20, as lua isn't configured to handle more than 20 parameters)

_________________
Do not ask me about online cheats. I don't know any and wont help finding them.

Like my help? Join me on Patreon so i can keep helping
Back to top
View user's profile Send private message MSN Messenger
Display posts from previous:   
Post new topic   Reply to topic    Cheat Engine Forum Index -> Cheat Engine Tutorials 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