Files
binana/cheatengine/StructDef.lua
2026-01-15 01:10:51 +03:00

770 lines
22 KiB
Lua

local CE = {
vtByte = 0,
vtWord = 1,
vtDword = 2,
vtQword = 3,
vtSingle = 4,
vtDouble = 5,
vtString = 6,
vtGrouped = 14,
}
local Types = {
-- Unsigned integers
uint8 = { ce = CE.vtByte, size = 1 },
uint16 = { ce = CE.vtWord, size = 2 },
uint32 = { ce = CE.vtDword, size = 4 },
uint64 = { ce = CE.vtQword, size = 8 },
-- Signed integers
int8 = { ce = CE.vtByte, size = 1 },
int16 = { ce = CE.vtWord, size = 2 },
int32 = { ce = CE.vtDword, size = 4 },
int64 = { ce = CE.vtQword, size = 8 },
-- Floating point
float = { ce = CE.vtSingle, size = 4 },
double = { ce = CE.vtDouble, size = 8 },
-- Pointers
ptr = { ce = CE.vtDword, size = 4 },
ptr32 = { ce = CE.vtDword, size = 4 },
ptr64 = { ce = CE.vtQword, size = 8 },
-- Aliases
bool = { ce = CE.vtByte, size = 1 },
bool32 = { ce = CE.vtDword, size = 4 },
char = { ce = CE.vtByte, size = 1 },
byte = { ce = CE.vtByte, size = 1 },
word = { ce = CE.vtWord, size = 2 },
dword = { ce = CE.vtDword, size = 4 },
qword = { ce = CE.vtQword, size = 8 },
}
function SetPointerSize(bits)
if bits == 64 then
Types.ptr = { ce = CE.vtQword, size = 8 }
else
Types.ptr = { ce = CE.vtDword, size = 4 }
end
end
local StructDef = {}
StructDef.__index = StructDef
-- @param name
-- @param parent
function StructDef.new(name, parent)
local self = setmetatable({}, StructDef)
self.name = name
self.parent = parent
self.ownFields = {}
self.embeddings = {}
if parent then
self._offset = parent:totalSize()
else
self._offset = 0
end
return self
end
-- @param name
-- @param typeName (uint32, float, ptr etc)
-- @param opts {hex=bool, color=number}
function StructDef:field(name, typeName, opts)
opts = opts or {}
local typeInfo = Types[typeName]
if not typeInfo then
error(string.format("Unknown type '%s' for field '%s'", typeName, name))
end
table.insert(self.ownFields, {
fieldOffset = self._offset,
name = name,
type = typeInfo.ce,
size = typeInfo.size,
color = opts.color,
hex = opts.hex,
})
self._offset = self._offset + typeInfo.size
return self
end
StructDef.f = StructDef.field
--- Add hex-field
function StructDef:hex(name, typeName, opts)
opts = opts or {}
opts.hex = true
return self:field(name, typeName, opts)
end
--- Add string
function StructDef:string(name, size, opts)
opts = opts or {}
table.insert(self.ownFields, {
fieldOffset = self._offset,
name = name,
type = CE.vtString,
string_size = size,
size = size,
color = opts.color,
})
self._offset = self._offset + size
return self
end
--- Add array
function StructDef:array(name, typeName, count, opts)
opts = opts or {}
local typeInfo = Types[typeName]
if not typeInfo then
error(string.format("Unknown type '%s' for array '%s'", typeName, name))
end
for i = 0, count - 1 do
table.insert(self.ownFields, {
fieldOffset = self._offset + i * typeInfo.size,
name = string.format("%s[%d]", name, i),
type = typeInfo.ce,
size = typeInfo.size,
color = opts.color,
hex = opts.hex,
})
end
self._offset = self._offset + count * typeInfo.size
return self
end
--- skip to X bytes
function StructDef:paddingTo(targetOffset, opts)
local size = targetOffset - self._offset
if size <= 0 then
return self
end
opts = opts or {}
local remaining = size
while remaining >= 4 do
local name = string.format("unk_%04X", self._offset)
self:field(name, "uint32", opts)
remaining = remaining - 4
end
while remaining >= 1 do
local name = string.format("unk_%04X", self._offset)
self:field(name, "uint8", opts)
remaining = remaining - 1
end
return self
end
--- skip N bytes
function StructDef:padding(size, opts)
opts = opts or {}
local remaining = size
while remaining >= 4 do
local name = string.format("unk_%04X", self._offset)
self:field(name, "uint32", opts)
remaining = remaining - 4
end
while remaining >= 1 do
local name = string.format("unk_%04X", self._offset)
self:field(name, "uint8", opts)
remaining = remaining - 1
end
return self
end
function StructDef:skip(size)
self._offset = self._offset + size
return self
end
function StructDef:unk(size, opts)
opts = opts or {}
local name = string.format("unk_%04X", self._offset)
if size == 1 then
return self:field(name, "uint8", opts)
elseif size == 2 then
return self:field(name, "uint16", opts)
elseif size == 4 then
return self:field(name, "uint32", opts)
elseif size == 8 then
return self:field(name, "uint64", opts)
else
return self:array(name, "uint8", size, opts)
end
end
function StructDef:alignTo(alignment)
local rem = self._offset % alignment
if rem ~= 0 then
self._offset = self._offset + (alignment - rem)
end
return self
end
function StructDef:at(offset)
self._offset = offset
return self
end
function StructDef:currentOffset()
return self._offset
end
-- @param name
-- @param otherStruct
-- @param opts {expand=bool}
function StructDef:embed(name, otherStruct, opts)
opts = opts or {}
local baseOffset = self._offset
table.insert(self.embeddings, {
type = "embed",
name = name,
struct = otherStruct,
offset = baseOffset,
size = otherStruct:totalSize(),
expand = opts.expand or false,
})
for _, f in ipairs(otherStruct:getAllFields()) do
local newField = {}
for k, v in pairs(f) do
newField[k] = v
end
newField.fieldOffset = baseOffset + f.fieldOffset
newField.name = name .. "." .. f.name
newField.structName = nil
newField._embeddedIn = name
table.insert(self.ownFields, newField)
end
self._offset = self._offset + otherStruct:totalSize()
return self
end
-- @param name
-- @param otherStruct
-- @param count
-- @param opts {expand=bool}
function StructDef:structArray(name, otherStruct, count, opts)
opts = opts or {}
local structSize = otherStruct:totalSize()
local baseOffset = self._offset
table.insert(self.embeddings, {
type = "structArray",
name = name,
struct = otherStruct,
offset = baseOffset,
count = count,
size = count * structSize,
elemSize = structSize,
expand = opts.expand or false,
})
for i = 0, count - 1 do
local elemOffset = self._offset + i * structSize
for _, f in ipairs(otherStruct:getAllFields()) do
local newField = {}
for k, v in pairs(f) do
newField[k] = v
end
newField.fieldOffset = elemOffset + f.fieldOffset
newField.name = string.format("%s[%d].%s", name, i, f.name)
newField._embeddedIn = name
newField._arrayIndex = i
table.insert(self.ownFields, newField)
end
end
self._offset = self._offset + count * structSize
return self
end
-- :ptrArray("fieldName", count) -- void*[]
-- :ptrArray("fieldName", count, opts) -- void*[] with opts
-- :ptrArray("TypeName", "fieldName", count) -- TypeName*[]
-- :ptrArray("TypeName", "fieldName", count, opts) -- TypeName*[] with opts
function StructDef:ptrArray(arg1, arg2, arg3, arg4)
local typeName, fieldName, count, opts
if type(arg2) == "number" then
-- ptrArray("fieldName", count) or ptrArray("fieldName", count, opts)
typeName = nil
fieldName = arg1
count = arg2
opts = arg3 or {}
elseif type(arg2) == "string" then
-- ptrArray("TypeName", "fieldName", count) or ptrArray("TypeName", "fieldName", count, opts)
typeName = arg1
fieldName = arg2
count = arg3
opts = arg4 or {}
else
error("Invalid arguments for ptrArray()")
end
local typeInfo = Types.ptr
for i = 0, count - 1 do
table.insert(self.ownFields, {
fieldOffset = self._offset + i * typeInfo.size,
name = string.format("%s[%d]", fieldName, i),
type = typeInfo.ce,
size = typeInfo.size,
color = opts.color,
hex = true,
isPointer = true,
ptrType = typeName,
_ptrArrayBase = fieldName,
})
end
self._offset = self._offset + count * typeInfo.size
return self
end
function StructDef:ptr(arg1, arg2, arg3)
local typeName, fieldName, opts
if arg2 == nil then
-- ptr("fieldName")
typeName = nil
fieldName = arg1
opts = {}
elseif type(arg2) == "table" then
-- ptr("fieldName", opts)
typeName = nil
fieldName = arg1
opts = arg2
elseif type(arg2) == "string" then
-- ptr("TypeName", "fieldName") or ptr("TypeName", "fieldName", opts)
typeName = arg1
fieldName = arg2
opts = arg3 or {}
else
error("Invalid arguments for ptr()")
end
local typeInfo = Types.ptr
table.insert(self.ownFields, {
fieldOffset = self._offset,
name = fieldName,
type = typeInfo.ce,
size = typeInfo.size,
color = opts.color,
hex = true,
isPointer = true,
ptrType = typeName,
})
self._offset = self._offset + typeInfo.size
return self
end
function StructDef:toCStruct(options)
options = options or {}
local indent = options.indent or " "
local collapsePadding = options.collapsePadding ~= false
local flattenInheritance = options.flattenInheritance or false
local lines = {}
local ceToC = {
[CE.vtByte] = "uint8_t",
[CE.vtWord] = "uint16_t",
[CE.vtDword] = "uint32_t",
[CE.vtQword] = "uint64_t",
[CE.vtSingle] = "float",
[CE.vtDouble] = "double",
[CE.vtString] = "char",
}
local function getPtrTypeName(field)
return field.ptrType or "void"
end
local function sanitizeName(name)
return name:gsub("%.", "_"):gsub("%[", "_"):gsub("%]", "")
end
local function isUnkField(name)
return name:match("^unk_[%dA-Fa-f]+$") ~= nil
end
local function isUnkArray(name)
return name:match("^unk_[%dA-Fa-f]+%[%d+%]$") ~= nil
end
local function isAnyUnk(name)
return isUnkField(name) or isUnkArray(name)
end
local fields
local embeddingMap = {}
if flattenInheritance then
fields = self:getAllFields()
for _, emb in ipairs(self:getAllEmbeddings()) do
embeddingMap[emb.offset] = emb
end
else
fields = self.ownFields
for _, emb in ipairs(self.embeddings) do
embeddingMap[emb.offset] = emb
end
end
if flattenInheritance and self.parent then
table.insert(lines, string.format("// Inherits from: %s (size: 0x%X)", self.parent.name, self.parent:totalSize()))
end
table.insert(lines, string.format("struct %s {", self.name))
if not flattenInheritance and self.parent then
table.insert(lines, string.format("%s%s base; // 0x%04X", indent, self.parent.name, 0))
end
local i = 1
local processedEmbeddings = {}
while i <= #fields do
local field = fields[i]
local cType = ceToC[field.type] or "uint8_t"
local emb = embeddingMap[field.fieldOffset]
if emb and not processedEmbeddings[emb.name] then
processedEmbeddings[emb.name] = true
if emb.type == "embed" then
if emb.expand then
table.insert(lines, string.format("%sstruct { // %s::%s", indent, emb.struct.name, emb.name))
for _, sf in ipairs(emb.struct:getAllFields()) do
local subType = ceToC[sf.type] or "uint8_t"
if sf.type == CE.vtString and sf.string_size then
table.insert(lines, string.format("%s%schar %s[%d]; // +0x%02X", indent, indent, sanitizeName(sf.name), sf.string_size, sf.fieldOffset))
elseif sf.isPointer then
table.insert(lines, string.format("%s%s%s* %s; // +0x%02X", indent, indent, getPtrTypeName(sf), sanitizeName(sf.name), sf.fieldOffset))
else
table.insert(lines, string.format("%s%s%s %s; // +0x%02X", indent, indent, subType, sanitizeName(sf.name), sf.fieldOffset))
end
end
table.insert(lines, string.format("%s} %s; // 0x%04X",
indent, emb.name, emb.offset))
else
table.insert(lines, string.format("%s%s %s; // 0x%04X",
indent, emb.struct.name, emb.name, emb.offset))
end
while i <= #fields and fields[i]._embeddedIn == emb.name do
i = i + 1
end
elseif emb.type == "structArray" then
if emb.expand then
table.insert(lines, string.format("%sstruct { // %s element", indent, emb.struct.name))
for _, sf in ipairs(emb.struct:getAllFields()) do
local subType = ceToC[sf.type] or "uint8_t"
if sf.isPointer then
table.insert(lines, string.format("%s%s%s* %s; // +0x%02X", indent, indent, getPtrTypeName(sf), sanitizeName(sf.name), sf.fieldOffset))
else
table.insert(lines, string.format("%s%s%s %s; // +0x%02X", indent, indent, subType, sanitizeName(sf.name), sf.fieldOffset))
end
end
table.insert(lines, string.format("%s} %s[%d]; // 0x%04X (0x%X bytes)", indent, emb.name, emb.count, emb.offset, emb.size))
else
table.insert(lines, string.format("%s%s %s[%d]; // 0x%04X (0x%X bytes)", indent, emb.struct.name, emb.name, emb.count, emb.offset, emb.size))
end
while i <= #fields and fields[i]._embeddedIn == emb.name do
i = i + 1
end
end
elseif collapsePadding and isAnyUnk(field.name) then
local startOffset = field.fieldOffset
local totalSize = 0
local j = i
while j <= #fields do
local f = fields[j]
local expectedOffset = startOffset + totalSize
if isAnyUnk(f.name) and f.fieldOffset == expectedOffset then
totalSize = totalSize + f.size
j = j + 1
else
break
end
end
if totalSize > 0 then
table.insert(lines, string.format("%suint8_t pad_%04X[0x%X]; // 0x%04X", indent, startOffset, totalSize, startOffset))
end
i = j
elseif field.isPointer and field._ptrArrayBase and field.name:match("%[0%]$") then
local baseName = field._ptrArrayBase
local j = i + 1
local count = 1
while j <= #fields do
if fields[j]._ptrArrayBase == baseName then
count = count + 1
j = j + 1
else
break
end
end
table.insert(lines, string.format("%s%s* %s[%d]; // 0x%04X", indent, getPtrTypeName(field), baseName, count, field.fieldOffset))
i = j
elseif field.name:match("%[0%]$") and not field.name:find("%.") then
local baseName = field.name:match("^([^%[]+)")
local j = i + 1
local count = 1
while j <= #fields do
local bn, ci = fields[j].name:match("^([^%[%.]+)%[(%d+)%]$")
if bn == baseName and tonumber(ci) == count then
count = count + 1
j = j + 1
else
break
end
end
table.insert(lines, string.format("%s%s %s[%d]; // 0x%04X", indent, cType, baseName, count, field.fieldOffset))
i = j
elseif field.type == CE.vtString and field.string_size then
table.insert(lines, string.format("%schar %s[%d]; // 0x%04X", indent, sanitizeName(field.name), field.string_size, field.fieldOffset))
i = i + 1
elseif field.isPointer then
table.insert(lines, string.format("%s%s* %s; // 0x%04X", indent, getPtrTypeName(field), sanitizeName(field.name), field.fieldOffset))
i = i + 1
else
table.insert(lines, string.format("%s%s %s; // 0x%04X", indent, cType, sanitizeName(field.name), field.fieldOffset))
i = i + 1
end
end
table.insert(lines, string.format("}; // sizeof: 0x%X (%d bytes)", self:totalSize(), self:totalSize()))
return table.concat(lines, "\n")
end
function StructDef:getDependencies()
local deps = {}
local seen = {}
local function collect(struct)
if seen[struct.name] then return end
seen[struct.name] = true
if struct.parent then
collect(struct.parent)
end
for _, emb in ipairs(struct.embeddings) do
collect(emb.struct)
end
table.insert(deps, struct)
end
collect(self)
return deps
end
function StructDef:toCStructWithDeps(options)
local deps = self:getDependencies()
local parts = {}
for _, struct in ipairs(deps) do
table.insert(parts, struct:toCStruct(options))
end
return table.concat(parts, "\n\n")
end
function StructDef:getAllEmbeddings()
local result = {}
if self.parent then
for _, e in ipairs(self.parent:getAllEmbeddings()) do
table.insert(result, e)
end
end
for _, e in ipairs(self.embeddings) do
table.insert(result, e)
end
return result
end
function StructDef:totalSize()
return self._offset
end
function StructDef:getOwnFields()
return self.ownFields
end
function StructDef:getAllFields()
local fields = {}
if self.parent then
for _, f in ipairs(self.parent:getAllFields()) do
table.insert(fields, f)
end
end
for _, f in ipairs(self.ownFields) do
local newField = {}
for k, v in pairs(f) do
newField[k] = v
end
newField.structName = self.name
table.insert(fields, newField)
end
return fields
end
-- @param struct
-- @param baseAddress
-- @param options
function loadStructToTable(struct, baseAddress, options)
options = options or {}
local showStructName = options.showStructName ~= false
local parentRecord = options.parentRecord
local fields = struct:getAllFields()
for _, field in ipairs(fields) do
local memrec = AddressList.createMemoryRecord()
if type(baseAddress) == "string" then
memrec.Address = string.format("%s+0x%X", baseAddress, field.fieldOffset)
else
memrec.Address = string.format("0x%X", field.fieldOffset + baseAddress)
end
if showStructName and field.structName then
memrec.Description = string.format("0x%03X %s::%s", field.fieldOffset, field.structName, field.name)
else
memrec.Description = string.format("0x%03X %s", field.fieldOffset, field.name)
end
memrec.Type = field.type
if field.string_size then
memrec.String.Size = field.string_size
end
if field.color then
memrec.Color = field.color
end
if field.hex then
memrec.ShowAsHex = true
end
if parentRecord then
memrec.appendToEntry(parentRecord)
end
end
end
function Struct(name, parent)
return StructDef.new(name, parent)
end
function GetCGObjectAddr(guidValue)
if type(guidValue) == "string" then
guidValue = tonumber(guidValue, 16)
end
if not guidValue or guidValue == 0 then
return nil
end
local CGUNIT_C_VTABLE = 0xa34d90
local CGPLAYER_C_VTABLE = 0xa326c8
local memScan = createMemScan()
local foundList = createFoundList(memScan)
memScan.firstScan(
soExactValue,
vtQword,
rtRounded,
string.format("%016X", guidValue),
nil,
0x0,
0x7FFFFFFFFFFFFFFF,
"",
fsmNotAligned,
nil,
true,
false,
false,
false
)
memScan.waitTillDone()
foundList.initialize()
local resultAddr, resultType = nil, nil
for i = 0, foundList.Count - 1 do
local addrGUIDvalue = tonumber(foundList.Address[i], 16)
if addrGUIDvalue then
local base = addrGUIDvalue - 0x30
local vtbl = readInteger(base)
if vtbl == CGPLAYER_C_VTABLE then
resultAddr, resultType = base, "player"
break
elseif vtbl == CGUNIT_C_VTABLE then
resultAddr, resultType = base, "unit"
break
end
end
end
foundList.destroy()
memScan.destroy()
if resultAddr then
return resultAddr, resultType
end
return nil
end