memreader
is Lua module for reading memory of current process.
Big kudos to squeek502/memreader
Designed specificially for Total War Warhammer 2.
Only works on Windows x64.
local mr = assert(_G.memreader)
local ptr = mr.base -- ex: 0x0000000140000000
out(mr.tostring(ptr)) -- 140000000
-- read pointer at address (base + 0x03601F98)
ptr = mr.read_pointer(ptr, '\152\31\96\3') -- 0x03601F98 (uint32)
-- if (ptr == NULL)
if mr.eq(ptr, '\0\0\0\0\0\0\0\0') then return end
-- read value at address (ptr + 0x14)
local luaNumber = mr.read_int32(ptr, 0x14)
-- in Warhammer lua_Number is float. Precision drops after 0x00FFFFFF (3 bytes).
local rawNumber = mr.read_int32(ptr, 0x14, true) -- returns userdata
assert(luaNumber == mr.tonumber(rawNumber)) -- conversion to lua_Number
luaNumber = luaNumber + 0x0150
-- !!! DO NOT USE BASIC LUA OPERATORS ON RETURNED userdata !!!
-- !!! USE PROVIDED LIBRARY FUNCTIONS INSTEAD !!!
rawNumber = mr.add(rawNumber, 0x0150) -- also: sub, mult, div
-- assert(luaNumber == mr.tonumber(rawNumber)) -- could be false due to precision loss?
-- (struct_String) int32 size; int32 capacity; char *string;
local string = mr.read_string(ptr, 0x20, false, false) -- ex: Adam
-- pointer to struct_String
local string = mr.read_string(ptr, 0x30, true, false) -- ex: Adam
-- (struct_WString) int32 size; int32 capacity; wchar_t *string;
local wstring = mr.read_string(ptr, 0x40, false, true) -- ex: A\0d\0a\0m\0
-- (struct_Array) int32 capacity; int32 size; LPVOID ptr;
local size, pdata = mr.read_array(ptr, 0x50)
-- preallocate table with narr=size (raw array part), nrec=0 (hash part)
local entries = mr.createtable(size, 0)
for i = 1, size do
-- advanced use-case example:
-- trait_categories__base = pointer to first entry. each entry has size 32.
-- let's say you copy all this data into table tcdata = { first_entry, ... }
-- pdata+0x00 points to one of such entries.
-- read_rowidx returns idx of that entry (tcdata[ idx ]).
local idx = read_rowidx(pdata, 0x00, trait_categories__base, 32) -- sizeof(trait_categories)
local entry = tcdata[ idx ]
pdata = mr.add(pdata, 0x08) -- sizeof(LPVOID)
end
-- probably you won't need this functionality at all
mr.write(ptr, 0x0100, false) -- boolean (1 byte)
mr.write(ptr, 0x0100, 165.48) -- float
mr.write(ptr, 0x0100, 'das\0\0\0\1\2\89fuw') -- array of bytes
mr.write(ptr, 0x0100, mr.pointer('\0\0\0\64\1\0\0\0')) -- pointer 0x0000000140000000
mr.write(ptr, 0x0100, mr.uint32('\0\0\0\64')) -- uint32 0x40000000
mr.write(ptr, 0x0100, mr.uint16(0x4000)) -- uint16 0x4000
mr.write(ptr, 0x0100, mr.uint8(0x40)) -- uin8 0x40
-- but this one is quite handy
local character = get_character_by_cqi(cqi)
local chptr = mr.ud_topointer(character)
tostring(character) -- ex: CHARACTER(0x000000004EB62210)
mr.tostring(chptr) -- ex: 4EB62210
-- mr.ud_debug would return pointer to internal lua gc value.
-- you could use it for other stuff, if you want more control over process.
-- after that you can do
chptr = mr.read_pointer(chptr, 0x10)
-- and now you have access to $pHero structure
npm install
npm run create-symlink-windows
I'm not a fan of metatables, but of simple and efficient code.
So you will not be able to do fancy stuff like:
local ptr = base + 32 * idx
local diff = ptr - base
And would have to resort to a more "ugly" approach:
local ptr = add(base, 32 * idx)
local diff = sub(ptr, base)
Why no Linux or Mac support?
On Linux Lua was not configured with LUA_DL_DLOPEN
enabled, therefore, package.loadlib
and require(cpath)
do not work.
io.popen
doesn't work as well (didn't check os.execute
).
On Mac any file:write
functionality is broken (disabled?).
If os.execute
and file:write
were present, then, theoretically, it would be possible to inject dll. But that is waaay too much work for mods.
Better just nag at CA to enable this functionality.
Notation typeA:typeB
stands for: pass typeA
value, library will treat (cast) it as typeB
value.
Don't confuse with argName: type
.
Returns new userdata pointer. String conversion requires sizeof(LPVOID)=8 bytes of data.
Returns new userdata uint8..int32
String conversion requires sizeof(UINT32)=4 bytes of data.
eq
, lt
and gt
have the exact same function declarations.
If you pass return_userdata=true
, then function will return userdata uint8..int32, instead of casting it to float
Optional argument offset
can be passed with types:
nil=0
float:UINT32
string:UINT32
uint32..int32
Will return true
if byte ~= 0x00
Structure for string { INT32 size; INT32 capacity; char *pStr; }
isWide=true
will return wchar_t raw data (not very userful in Lua)
If you pass isPtr=true
, then library will do the following:
local ptr = read_pointer(ptr, offset)
return read_string(ptr, 0, false, isWide)
Structure for array { INT32 capacity; INT32 size; LPVOID pData; }
Returns size of array and pointer to data.
Basically library does the following:
local entry = read_pointer(pEntry, offset)
return 1 + (entry - base) / entry_size
Reads bytes
from memory. String contains raw data. It is not null terminated.
for module in mr.modules() do
out('base = '.. mr.tostring(module.base) ..', size = '.. tostring(module.size) ..', name = '.. module.name ..', path = '.. module.path)
end
Behaves same as default lua tostring function.
Returns hex representation of string.
Refer to sprintf_s
documentation.
Returns pointer of userdata value.
local chptr = ud_topointer(character)
chptr = read_pointer(chptr, 0x10)
local cqi = read_int32(chptr, 0xF0)
Returns lua type of value (TValue.tt) and pointer value p
(TValue.value.p). Refer to lua documentation and source code.
Creates new table with preallocated space narr (raw array part) and nrec (hash part)
narr
and nrec
can be the following types:
float:UINT32
string:UINT32
uint32..int32
-- there are still a lot more of variable declarations, but you should get the point
local mr = _G.memreader
local gi = cm:get_game_interface()
local model = gi:model()
local root, static_ptr
core:add_listener(
'',
'UICreated',
true,
function(context)
root = core.ui_root
local ptr = mr.base
ptr = read_pointer(ptr, '\152\87\105\3') -- 0x03695798
ptr = read_pointer(ptr, 0x20)
ptr = read_pointer(ptr, 0x10)
static_ptr = ptr ---@static_ptr
end,
false)
local function getTable(idx)
local table_ptr = read_pointer(static_ptr, 56 * idx)
local size, pData = read_array(table_ptr, 0x08)
local pFirst = read_pointer(pData, 0x00)
return size, pFirst
end
local tbl_forbidden_subtypes
local function LoadData()
if tbl_forbidden_subtypes then return end
tbl_forbidden_subtypes = createtable(0, 10) -- we expect a little bit of forbidden subtypes
local size, ptr = getTable(490) -- agent_subtypes_tables
for i = 1, size do
local can_gain_xp = read_boolean(ptr, 0x70)
if not can_gain_xp then
local key = read_string(ptr, 0x50, true, false)
tbl_forbidden_subtypes[ key ] = true
end
ptr = add(ptr, 144)
end
end
local function get_chptr(character) return read_pointer(ud_topointer(character), 0x10) end
local function loadAvailablePoints(chptr)
if not chptr then return 0 end
return read_int32(chptr, 0x0610)
end
local function FetchSelectedCharacter()
local CA_cip = root:SequentialFind(
'layout',
'info_panel_holder',
'primary_info_panel_holder',
'info_panel_background',
'CharacterInfoPopup')
local ptr = ud_topointer(CA_cip)
ptr = read_pointer(ptr, 0x00) -- $uic
ptr = read_pointer(ptr, 0x50) -- cco_selected
if eq(ptr, '\0\0\0\0\0\0\0\0') then return end -- should never happen
ptr = read_pointer(ptr, 0x08) -- $pHero.details
ptr = read_pointer(ptr, 0x00) -- $pHero
local cqi = read_int32(ptr, 0xF0)
local character = model:character_for_command_queue_index(cqi)
local member = character:family_member()
local static_cqi = member:command_queue_index()
return character, cqi, static_cqi
end
core:add_listener(
'',
'CharacterSelected',
true,
function(context)
local character, cqi, static_cqi = FetchSelectedCharacter()
if not character then return end
LoadData()
local subtype_key = character:character_subtype_key()
if tbl_forbidden_subtypes[ subtype_key ] then return end
local chptr = get_chptr(character)
local available_points = loadAvailablePoints(chptr)
if available_points == 0 then return end
-- do stuff
end,
true)
Userdata is a bunch of bytes that C
programm allocates and you can somehow access them in lua.
local function get_chptr(character) return read_pointer(ud_topointer(character), 0x10) end
Why do we access $pHero
structure at 0x10
offset? Because each userdata has it's own type, and you can print this type with tostring
(CA implementation).
For example:
tostring(core.ui_root) 'UIComponent (000000006BCEE520)'
tostring(character) 'CHARACTER_SCRIPT_INTERFACE (0000000049376488)' -- CA userdata (points to `00000000496D8E70` - `$pHero`)
tostring(ud_topointer(character)) 'userdata: 00000001E6F81AE8' -- where memreader userdata is located at (points to `0000000049376488`)
mr.tostring(ud_topointer(character)) '0000000049376488' -- where CA userdata is located at
tostring(get_chptr(character)) 'userdata: 00000001F1FFCD98' -- where memreader userdata is located at (points to `00000000496D8E70`)
mr.tostring(get_chptr(character)) '00000000496D8E70' -- where `$pHero` structure is located at
How does CA differentiate dynamic structure that external libraries (such as memreader
) create?
In C
there is such thing as type_info
which can be read with typeid
, can be found inside vtable
. But I'm not sure why a lot of CA userdata structures have an extra pointer, that looks just like vtable
, right next to vtable
pointer. I think they use different approach, as implementation of typeid
expects to receive entity with vtable
inside (which we cannot assume for external userdata).
Anyway, there are 2/3 pointers in userdata. The actual pointer to character
or other entity of your interest can be found at 0x08
or 0x10
offset. But don't take my word for it. In the example above, you would go to 0000000049376488
address, and look at the data yourself. Best print and check several different of such userdata structures, to be completely sure. In Reclass.net
you would look for <HEAP>00000000496D8E70
pointer. Don't bother with <DATA>Warhammer2.exe.14...
pointers.