/ cheatengine / StructDef.lua
StructDef.lua
  1  local CE = {
  2      vtByte   = 0,
  3      vtWord   = 1,
  4      vtDword  = 2,
  5      vtQword  = 3,
  6      vtSingle = 4,
  7      vtDouble = 5,
  8      vtString = 6,
  9      vtGrouped = 14,
 10  }
 11  
 12  local Types = {
 13      -- Unsigned integers
 14      uint8   = { ce = CE.vtByte,   size = 1 },
 15      uint16  = { ce = CE.vtWord,   size = 2 },
 16      uint32  = { ce = CE.vtDword,  size = 4 },
 17      uint64  = { ce = CE.vtQword,  size = 8 },
 18      
 19      -- Signed integers
 20      int8    = { ce = CE.vtByte,   size = 1 },
 21      int16   = { ce = CE.vtWord,   size = 2 },
 22      int32   = { ce = CE.vtDword,  size = 4 },
 23      int64   = { ce = CE.vtQword,  size = 8 },
 24      
 25      -- Floating point
 26      float   = { ce = CE.vtSingle, size = 4 },
 27      double  = { ce = CE.vtDouble, size = 8 },
 28      
 29      -- Pointers
 30      ptr     = { ce = CE.vtDword,  size = 4 },
 31      ptr32   = { ce = CE.vtDword,  size = 4 },
 32      ptr64   = { ce = CE.vtQword,  size = 8 },
 33      
 34      -- Aliases
 35      bool    = { ce = CE.vtByte,   size = 1 },
 36      bool32  = { ce = CE.vtDword,  size = 4 },
 37      char    = { ce = CE.vtByte,   size = 1 },
 38      byte    = { ce = CE.vtByte,   size = 1 },
 39      word    = { ce = CE.vtWord,   size = 2 },
 40      dword   = { ce = CE.vtDword,  size = 4 },
 41      qword   = { ce = CE.vtQword,  size = 8 },
 42  }
 43  
 44  function SetPointerSize(bits)
 45      if bits == 64 then
 46          Types.ptr = { ce = CE.vtQword, size = 8 }
 47      else
 48          Types.ptr = { ce = CE.vtDword, size = 4 }
 49      end
 50  end
 51  
 52  local StructDef = {}
 53  StructDef.__index = StructDef
 54  
 55  -- @param name
 56  -- @param parent
 57  function StructDef.new(name, parent)
 58      local self = setmetatable({}, StructDef)
 59      self.name = name
 60      self.parent = parent
 61      self.ownFields = {}
 62      self.embeddings = {}
 63  
 64      if parent then
 65          self._offset = parent:totalSize()
 66      else
 67          self._offset = 0
 68      end
 69  
 70      return self
 71  end
 72  
 73  -- @param name
 74  -- @param typeName  (uint32, float, ptr etc)
 75  -- @param opts      {hex=bool, color=number}
 76  function StructDef:field(name, typeName, opts)
 77      opts = opts or {}
 78  
 79      local typeInfo = Types[typeName]
 80      if not typeInfo then
 81          error(string.format("Unknown type '%s' for field '%s'", typeName, name))
 82      end
 83  
 84      table.insert(self.ownFields, {
 85          fieldOffset = self._offset,
 86          name        = name,
 87          type        = typeInfo.ce,
 88          size        = typeInfo.size,
 89          color       = opts.color,
 90          hex         = opts.hex,
 91      })
 92  
 93      self._offset = self._offset + typeInfo.size
 94      return self
 95  end
 96  
 97  StructDef.f = StructDef.field
 98  
 99  --- Add hex-field
100  function StructDef:hex(name, typeName, opts)
101      opts = opts or {}
102      opts.hex = true
103      return self:field(name, typeName, opts)
104  end
105  
106  --- Add string
107  function StructDef:string(name, size, opts)
108      opts = opts or {}
109  
110      table.insert(self.ownFields, {
111          fieldOffset = self._offset,
112          name        = name,
113          type        = CE.vtString,
114          string_size = size,
115          size        = size,
116          color       = opts.color,
117      })
118  
119      self._offset = self._offset + size
120      return self
121  end
122  
123  --- Add array
124  function StructDef:array(name, typeName, count, opts)
125      opts = opts or {}
126  
127      local typeInfo = Types[typeName]
128      if not typeInfo then
129          error(string.format("Unknown type '%s' for array '%s'", typeName, name))
130      end
131  
132      for i = 0, count - 1 do
133          table.insert(self.ownFields, {
134              fieldOffset = self._offset + i * typeInfo.size,
135              name        = string.format("%s[%d]", name, i),
136              type        = typeInfo.ce,
137              size        = typeInfo.size,
138              color       = opts.color,
139              hex         = opts.hex,
140          })
141      end
142  
143      self._offset = self._offset + count * typeInfo.size
144      return self
145  end
146  
147  --- skip to X bytes
148  function StructDef:paddingTo(targetOffset, opts)
149      local size = targetOffset - self._offset
150      if size <= 0 then
151          return self
152      end
153  
154      opts = opts or {}
155      local remaining = size
156  
157      while remaining >= 4 do
158          local name = string.format("unk_%04X", self._offset)
159          self:field(name, "uint32", opts)
160          remaining = remaining - 4
161      end
162  
163      while remaining >= 1 do
164          local name = string.format("unk_%04X", self._offset)
165          self:field(name, "uint8", opts)
166          remaining = remaining - 1
167      end
168  
169      return self
170  end
171  
172  --- skip N bytes
173  function StructDef:padding(size, opts)
174      opts = opts or {}
175      local remaining = size
176  
177      while remaining >= 4 do
178          local name = string.format("unk_%04X", self._offset)
179          self:field(name, "uint32", opts)
180          remaining = remaining - 4
181      end
182  
183      while remaining >= 1 do
184          local name = string.format("unk_%04X", self._offset)
185          self:field(name, "uint8", opts)
186          remaining = remaining - 1
187      end
188      
189      return self
190  end
191  
192  function StructDef:skip(size)
193      self._offset = self._offset + size
194      return self
195  end
196  
197  function StructDef:unk(size, opts)
198      opts = opts or {}
199      local name = string.format("unk_%04X", self._offset)
200  
201      if size == 1 then
202          return self:field(name, "uint8", opts)
203      elseif size == 2 then
204          return self:field(name, "uint16", opts)
205      elseif size == 4 then
206          return self:field(name, "uint32", opts)
207      elseif size == 8 then
208          return self:field(name, "uint64", opts)
209      else
210          return self:array(name, "uint8", size, opts)
211      end
212  end
213  
214  function StructDef:alignTo(alignment)
215      local rem = self._offset % alignment
216      if rem ~= 0 then
217          self._offset = self._offset + (alignment - rem)
218      end
219      return self
220  end
221  
222  function StructDef:at(offset)
223      self._offset = offset
224      return self
225  end
226  
227  function StructDef:currentOffset()
228      return self._offset
229  end
230  
231  -- @param name
232  -- @param otherStruct
233  -- @param opts {expand=bool}
234  function StructDef:embed(name, otherStruct, opts)
235      opts = opts or {}
236      local baseOffset = self._offset
237  
238      table.insert(self.embeddings, {
239          type = "embed",
240          name = name,
241          struct = otherStruct,
242          offset = baseOffset,
243          size = otherStruct:totalSize(),
244          expand = opts.expand or false,
245      })
246  
247      for _, f in ipairs(otherStruct:getAllFields()) do
248          local newField = {}
249          for k, v in pairs(f) do
250              newField[k] = v
251          end
252          newField.fieldOffset = baseOffset + f.fieldOffset
253          newField.name = name .. "." .. f.name
254          newField.structName = nil
255          newField._embeddedIn = name
256          table.insert(self.ownFields, newField)
257      end
258  
259      self._offset = self._offset + otherStruct:totalSize()
260      return self
261  end
262  
263  -- @param name
264  -- @param otherStruct
265  -- @param count
266  -- @param opts {expand=bool}
267  function StructDef:structArray(name, otherStruct, count, opts)
268      opts = opts or {}
269      local structSize = otherStruct:totalSize()
270      local baseOffset = self._offset
271  
272      table.insert(self.embeddings, {
273          type = "structArray",
274          name = name,
275          struct = otherStruct,
276          offset = baseOffset,
277          count = count,
278          size = count * structSize,
279          elemSize = structSize,
280          expand = opts.expand or false,
281      })
282  
283      for i = 0, count - 1 do
284          local elemOffset = self._offset + i * structSize
285  
286          for _, f in ipairs(otherStruct:getAllFields()) do
287              local newField = {}
288              for k, v in pairs(f) do
289                  newField[k] = v
290              end
291              newField.fieldOffset = elemOffset + f.fieldOffset
292              newField.name = string.format("%s[%d].%s", name, i, f.name)
293              newField._embeddedIn = name
294              newField._arrayIndex = i
295              table.insert(self.ownFields, newField)
296          end
297      end
298  
299      self._offset = self._offset + count * structSize
300      return self
301  end
302  
303  --   :ptrArray("fieldName", count)                    -- void*[]
304  --   :ptrArray("fieldName", count, opts)              -- void*[] with opts
305  --   :ptrArray("TypeName", "fieldName", count)        -- TypeName*[]
306  --   :ptrArray("TypeName", "fieldName", count, opts)  -- TypeName*[] with opts
307  function StructDef:ptrArray(arg1, arg2, arg3, arg4)
308      local typeName, fieldName, count, opts
309  
310      if type(arg2) == "number" then
311          -- ptrArray("fieldName", count) or ptrArray("fieldName", count, opts)
312          typeName = nil
313          fieldName = arg1
314          count = arg2
315          opts = arg3 or {}
316      elseif type(arg2) == "string" then
317          -- ptrArray("TypeName", "fieldName", count) or ptrArray("TypeName", "fieldName", count, opts)
318          typeName = arg1
319          fieldName = arg2
320          count = arg3
321          opts = arg4 or {}
322      else
323          error("Invalid arguments for ptrArray()")
324      end
325  
326      local typeInfo = Types.ptr
327  
328      for i = 0, count - 1 do
329          table.insert(self.ownFields, {
330              fieldOffset   = self._offset + i * typeInfo.size,
331              name          = string.format("%s[%d]", fieldName, i),
332              type          = typeInfo.ce,
333              size          = typeInfo.size,
334              color         = opts.color,
335              hex           = true,
336              isPointer     = true,
337              ptrType       = typeName,
338              _ptrArrayBase = fieldName,
339          })
340      end
341  
342      self._offset = self._offset + count * typeInfo.size
343      return self
344  end
345  
346  function StructDef:ptr(arg1, arg2, arg3)
347      local typeName, fieldName, opts
348  
349      if arg2 == nil then
350          -- ptr("fieldName")
351          typeName = nil
352          fieldName = arg1
353          opts = {}
354      elseif type(arg2) == "table" then
355          -- ptr("fieldName", opts)
356          typeName = nil
357          fieldName = arg1
358          opts = arg2
359      elseif type(arg2) == "string" then
360          -- ptr("TypeName", "fieldName") or ptr("TypeName", "fieldName", opts)
361          typeName = arg1
362          fieldName = arg2
363          opts = arg3 or {}
364      else
365          error("Invalid arguments for ptr()")
366      end
367  
368      local typeInfo = Types.ptr
369  
370      table.insert(self.ownFields, {
371          fieldOffset = self._offset,
372          name        = fieldName,
373          type        = typeInfo.ce,
374          size        = typeInfo.size,
375          color       = opts.color,
376          hex         = true,
377          isPointer   = true,
378          ptrType     = typeName,
379      })
380  
381      self._offset = self._offset + typeInfo.size
382      return self
383  end
384  
385  function StructDef:toCStruct(options)
386      options = options or {}
387      local indent = options.indent or "    "
388      local collapsePadding = options.collapsePadding ~= false
389      local flattenInheritance = options.flattenInheritance or false
390      local lines = {}
391  
392      local ceToC = {
393          [CE.vtByte]   = "uint8_t",
394          [CE.vtWord]   = "uint16_t",
395          [CE.vtDword]  = "uint32_t",
396          [CE.vtQword]  = "uint64_t",
397          [CE.vtSingle] = "float",
398          [CE.vtDouble] = "double",
399          [CE.vtString] = "char",
400      }
401  
402      local function getPtrTypeName(field)
403          return field.ptrType or "void"
404      end
405  
406      local function sanitizeName(name)
407          return name:gsub("%.", "_"):gsub("%[", "_"):gsub("%]", "")
408      end
409  
410      local function isUnkField(name)
411          return name:match("^unk_[%dA-Fa-f]+$") ~= nil
412      end
413  
414      local function isUnkArray(name)
415          return name:match("^unk_[%dA-Fa-f]+%[%d+%]$") ~= nil
416      end
417  
418      local function isAnyUnk(name)
419          return isUnkField(name) or isUnkArray(name)
420      end
421  
422      local fields
423      local embeddingMap = {}
424  
425      if flattenInheritance then
426          fields = self:getAllFields()
427          for _, emb in ipairs(self:getAllEmbeddings()) do
428              embeddingMap[emb.offset] = emb
429          end
430      else
431          fields = self.ownFields
432          for _, emb in ipairs(self.embeddings) do
433              embeddingMap[emb.offset] = emb
434          end
435      end
436  
437      if flattenInheritance and self.parent then
438          table.insert(lines, string.format("// Inherits from: %s (size: 0x%X)", self.parent.name, self.parent:totalSize()))
439      end
440  
441      table.insert(lines, string.format("struct %s {", self.name))
442  
443      if not flattenInheritance and self.parent then
444          table.insert(lines, string.format("%s%s base; // 0x%04X", indent, self.parent.name, 0))
445      end
446  
447      local i = 1
448      local processedEmbeddings = {}
449  
450      while i <= #fields do
451          local field = fields[i]
452          local cType = ceToC[field.type] or "uint8_t"
453  
454          local emb = embeddingMap[field.fieldOffset]
455  
456          if emb and not processedEmbeddings[emb.name] then
457              processedEmbeddings[emb.name] = true
458  
459              if emb.type == "embed" then
460                  if emb.expand then
461                      table.insert(lines, string.format("%sstruct { // %s::%s", indent, emb.struct.name, emb.name))
462  
463                      for _, sf in ipairs(emb.struct:getAllFields()) do
464                          local subType = ceToC[sf.type] or "uint8_t"
465                          if sf.type == CE.vtString and sf.string_size then
466                              table.insert(lines, string.format("%s%schar %s[%d]; // +0x%02X", indent, indent, sanitizeName(sf.name), sf.string_size, sf.fieldOffset))
467                          elseif sf.isPointer then
468                              table.insert(lines, string.format("%s%s%s* %s; // +0x%02X", indent, indent, getPtrTypeName(sf), sanitizeName(sf.name), sf.fieldOffset))
469                          else
470                              table.insert(lines, string.format("%s%s%s %s; // +0x%02X", indent, indent, subType, sanitizeName(sf.name), sf.fieldOffset))
471                          end
472                      end
473  
474                      table.insert(lines, string.format("%s} %s; // 0x%04X", 
475                          indent, emb.name, emb.offset))
476                  else
477                      table.insert(lines, string.format("%s%s %s; // 0x%04X",
478                          indent, emb.struct.name, emb.name, emb.offset))
479                  end
480  
481                  while i <= #fields and fields[i]._embeddedIn == emb.name do
482                      i = i + 1
483                  end
484  
485              elseif emb.type == "structArray" then
486                  if emb.expand then
487                      table.insert(lines, string.format("%sstruct { // %s element", indent, emb.struct.name))
488  
489                      for _, sf in ipairs(emb.struct:getAllFields()) do
490                          local subType = ceToC[sf.type] or "uint8_t"
491                          if sf.isPointer then
492                              table.insert(lines, string.format("%s%s%s* %s; // +0x%02X", indent, indent, getPtrTypeName(sf), sanitizeName(sf.name), sf.fieldOffset))
493                          else
494                              table.insert(lines, string.format("%s%s%s %s; // +0x%02X", indent, indent, subType, sanitizeName(sf.name), sf.fieldOffset))
495                          end
496                      end
497  
498                      table.insert(lines, string.format("%s} %s[%d]; // 0x%04X (0x%X bytes)", indent, emb.name, emb.count, emb.offset, emb.size))
499                  else
500                      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))
501                  end
502  
503                  while i <= #fields and fields[i]._embeddedIn == emb.name do
504                      i = i + 1
505                  end
506              end
507  
508          elseif collapsePadding and isAnyUnk(field.name) then
509              local startOffset = field.fieldOffset
510              local totalSize = 0
511              local j = i
512  
513              while j <= #fields do
514                  local f = fields[j]
515                  local expectedOffset = startOffset + totalSize
516  
517                  if isAnyUnk(f.name) and f.fieldOffset == expectedOffset then
518                      totalSize = totalSize + f.size
519                      j = j + 1
520                  else
521                      break
522                  end
523              end
524  
525              if totalSize > 0 then
526                  table.insert(lines, string.format("%suint8_t pad_%04X[0x%X]; // 0x%04X", indent, startOffset, totalSize, startOffset))
527              end
528  
529              i = j
530  
531          elseif field.isPointer and field._ptrArrayBase and field.name:match("%[0%]$") then
532              local baseName = field._ptrArrayBase
533              local j = i + 1
534              local count = 1
535  
536              while j <= #fields do
537                  if fields[j]._ptrArrayBase == baseName then
538                      count = count + 1
539                      j = j + 1
540                  else
541                      break
542                  end
543              end
544  
545              table.insert(lines, string.format("%s%s* %s[%d]; // 0x%04X", indent, getPtrTypeName(field), baseName, count, field.fieldOffset))
546              i = j
547  
548          elseif field.name:match("%[0%]$") and not field.name:find("%.") then
549              local baseName = field.name:match("^([^%[]+)")
550              local j = i + 1
551              local count = 1
552  
553              while j <= #fields do
554                  local bn, ci = fields[j].name:match("^([^%[%.]+)%[(%d+)%]$")
555                  if bn == baseName and tonumber(ci) == count then
556                      count = count + 1
557                      j = j + 1
558                  else
559                      break
560                  end
561              end
562  
563              table.insert(lines, string.format("%s%s %s[%d]; // 0x%04X", indent, cType, baseName, count, field.fieldOffset))
564              i = j
565  
566          elseif field.type == CE.vtString and field.string_size then
567              table.insert(lines, string.format("%schar %s[%d]; // 0x%04X", indent, sanitizeName(field.name), field.string_size, field.fieldOffset))
568              i = i + 1
569  
570          elseif field.isPointer then
571              table.insert(lines, string.format("%s%s* %s; // 0x%04X", indent, getPtrTypeName(field), sanitizeName(field.name), field.fieldOffset))
572              i = i + 1
573  
574          else
575              table.insert(lines, string.format("%s%s %s; // 0x%04X", indent, cType, sanitizeName(field.name), field.fieldOffset))
576              i = i + 1
577          end
578      end
579  
580      table.insert(lines, string.format("}; // sizeof: 0x%X (%d bytes)", self:totalSize(), self:totalSize()))
581  
582      return table.concat(lines, "\n")
583  end
584  
585  function StructDef:getDependencies()
586      local deps = {}
587      local seen = {}
588  
589      local function collect(struct)
590          if seen[struct.name] then return end
591          seen[struct.name] = true
592  
593          if struct.parent then
594              collect(struct.parent)
595          end
596  
597          for _, emb in ipairs(struct.embeddings) do
598              collect(emb.struct)
599          end
600  
601          table.insert(deps, struct)
602      end
603  
604      collect(self)
605      return deps
606  end
607  
608  function StructDef:toCStructWithDeps(options)
609      local deps = self:getDependencies()
610      local parts = {}
611  
612      for _, struct in ipairs(deps) do
613          table.insert(parts, struct:toCStruct(options))
614      end
615  
616      return table.concat(parts, "\n\n")
617  end
618  
619  function StructDef:getAllEmbeddings()
620      local result = {}
621  
622      if self.parent then
623          for _, e in ipairs(self.parent:getAllEmbeddings()) do
624              table.insert(result, e)
625          end
626      end
627  
628      for _, e in ipairs(self.embeddings) do
629          table.insert(result, e)
630      end
631  
632      return result
633  end
634  
635  function StructDef:totalSize()
636      return self._offset
637  end
638  
639  function StructDef:getOwnFields()
640      return self.ownFields
641  end
642  
643  function StructDef:getAllFields()
644      local fields = {}
645  
646      if self.parent then
647          for _, f in ipairs(self.parent:getAllFields()) do
648              table.insert(fields, f)
649          end
650      end
651  
652      for _, f in ipairs(self.ownFields) do
653          local newField = {}
654          for k, v in pairs(f) do
655              newField[k] = v
656          end
657          newField.structName = self.name
658          table.insert(fields, newField)
659      end
660  
661      return fields
662  end
663  
664  -- @param struct
665  -- @param baseAddress
666  -- @param options
667  function loadStructToTable(struct, baseAddress, options)
668      options = options or {}
669      local showStructName = options.showStructName ~= false
670      local parentRecord = options.parentRecord
671  
672      local fields = struct:getAllFields()
673  
674      for _, field in ipairs(fields) do
675          local memrec = AddressList.createMemoryRecord()
676  
677          if type(baseAddress) == "string" then
678              memrec.Address = string.format("%s+0x%X", baseAddress, field.fieldOffset)
679          else
680              memrec.Address = string.format("0x%X", field.fieldOffset + baseAddress)
681          end
682  
683          if showStructName and field.structName then
684              memrec.Description = string.format("0x%03X   %s::%s", field.fieldOffset, field.structName, field.name)
685          else
686              memrec.Description = string.format("0x%03X   %s", field.fieldOffset, field.name)
687          end
688  
689          memrec.Type = field.type
690  
691          if field.string_size then
692              memrec.String.Size = field.string_size
693          end
694          if field.color then
695              memrec.Color = field.color
696          end
697          if field.hex then
698              memrec.ShowAsHex = true
699          end
700  
701          if parentRecord then
702              memrec.appendToEntry(parentRecord)
703          end
704      end
705  end
706  
707  function Struct(name, parent)
708      return StructDef.new(name, parent)
709  end
710  
711  function GetCGObjectAddr(guidValue)
712      if type(guidValue) == "string" then
713          guidValue = tonumber(guidValue, 16)
714      end
715      if not guidValue or guidValue == 0 then
716          return nil
717      end
718  
719      local VTABLE_TYPES = {
720          [0xA34D90] = "unit",
721          [0xA326C8] = "player",
722          [0xA33428] = "item",
723          [0xA332D0] = "container",
724          [0xA331C8] = "corpse",
725      }
726  
727      local memScan   = createMemScan()
728      local foundList = createFoundList(memScan)
729  
730      local function cleanup()
731          foundList.destroy()
732          memScan.destroy()
733      end
734  
735      memScan.firstScan(
736          soExactValue, vtQword, rtRounded,
737          string.format("%016X", guidValue),
738          nil, 0x0, 0x7FFFFFFFFFFFFFFF,
739          "", fsmNotAligned, nil,
740          true, false, false, false
741      )
742  
743      memScan.waitTillDone()
744      foundList.initialize()
745  
746      for i = 0, foundList.Count - 1 do
747          local addr = tonumber(foundList.Address[i], 16)
748          if addr then
749              local base = addr - 0x30
750              local objType = VTABLE_TYPES[readInteger(base)]
751              if objType then
752                  cleanup()
753                  return base, objType
754              end
755          end
756      end
757  
758      cleanup()
759      return nil
760  end