-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGenerateStructInfo.lua
More file actions
2222 lines (2104 loc) · 82.1 KB
/
Copy pathGenerateStructInfo.lua
File metadata and controls
2222 lines (2104 loc) · 82.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
function reload()
dofile(debug.getinfo(1, "S").source:sub(2))
end
r = reload
local format = string.format
local mmver = offsets.MMVersion
local function mm78(...)
local r = select(mmver - 6, ...)
assert(r ~= nil)
return r
end
local _DEBUG = true
local CODE_INJECT_PATH = path.addslash("C:/Users/Eksekk/Documents/GitHub/MMStuff/pgenedit/structsCodeInjection"):gsub("/", "\\")
--[[
> dump(structs, 1)
-- interesting stuff:
class = function: 0x0329b2d8,\ -- class with custom methods and "new" method
enum = function: 0x0329b338,\ -- for iterating over struct instance's properties
f = table: 0x03259168,\ -- definition functions
m = table: 0x032591f0,\ -- member handler functions (handle every access and write to it)
name = function: 0x0329b450,\ -- gets struct name from its class
o = table: 0x032591c8\ -- offsets of all members
-- also:
mem.structs.types
mem.structs.types.u4
mem.structs.types.array -- etc.
-- a lot of info is stored in upvalues
-- debug.getinfo also was useful in one case
]]
-- generic constants
local types = mem.structs.types
local EditPChar_newindex, EditConstPChar_newindex = getmetatable(mem.EditPChar).__newindex, getmetatable(mem.EditConstPChar).__newindex
local booleanHandlers = {
[getUpvalue(mem.structs.types.b1, "handler")] = 1,
[getUpvalue(mem.structs.types.b2, "handler")] = 2,
[getUpvalue(mem.structs.types.b4, "handler")] = 4,
}
local bitIndex = {}
do
local bit = 1
for i = 0, 7 do
bitIndex[bit] = i
bit = bit * 2
end
end
local AnyABitHandler = getU(getU(types.abit, "doBit"), "AnyABitHandler")
local luaData = {}
local function setBaseTypeField(data, field, value) -- if there are arrays, skips them
local data2 = data
while data2.array do
data2 = data2.innerType
end
data2[field] = value
end
local function getBaseTypeField(data, field)
local data2 = data
while data2.array do
data2 = data2.innerType
end
return data2[field]
end
local getGroup
local function getCustomFieldSizes(structName)
local oldMember = types.CustomType
local fieldSizes = {}
function types.CustomType(name, size, f, ...)
fieldSizes[name] = size
return oldMember(name, size, f, ...)
end
mem.struct(structs.f[structName])
types.CustomType = oldMember
return fieldSizes
end
function getStructureMembersInfoData(structName)
local oldStru = structs.f[structName]
local output, methods = {}, {}
structs.f[structName] = function(define, ...)
local oldInfo, oldFunc, oldMethod = types.Info, types.func, types.method
local prevDefined
function types.Info(t, ...)
local name = define.LastDefinedMemberName or "DefaultIndex"
if t ~= nil and (type(t) ~= "table" or not t.new) -- not added by mmext
and prevDefined ~= name -- don't overwrite old data, for example if define.f.Something is defined, because it doesn't change "LastDefinedMemberName"
then
prevDefined = name
output[name] = t
end
return oldInfo(t, ...)
end
function types.func(def, ...)
define.LastDefinedMemberName = def.name
return oldFunc(def, ...)
end
function types.method(def, ...)
define.LastDefinedMemberName = def.name
methods[def.name] = true
return oldMethod(def, ...)
end
-- skip info calls after "define.f.functionName" or "define.m.functionName"
local metaF, metaM = getmetatable(types.f), getmetatable(types.m)
local oldF, oldM = metaF.__newindex, metaM.__newindex
function metaF.__newindex(...)
prevDefined = define.LastDefinedMemberName
return oldF(...)
end
function metaM.__newindex(...)
prevDefined = define.LastDefinedMemberName
return oldM(...)
end
local ret = {oldStru(define, ...)}
types.Info, types.func, types.method = oldInfo, oldFunc, oldMethod
metaF.__newindex, metaM.__newindex = oldF, oldM
return unpack(ret)
end
mem.struct(structs.f[structName])
structs.f[structName] = oldStru
return output, methods
end
-- those that contain size override at the end (like "define.size = 4")
local structsWithFakeSize = {"SkillMasteryDescriptions"}
local function calcStructLargestSize(name)
-- member(...) is called for every member, as its name suggests
local oldMember, oldCustomType, oldStructsCustomType = getU(types.i8, "member"), types.CustomType, mem.structs.CustomType
local max = 0
local currentDefine -- to not call getU each time in myMember()
local function myMember(...)
local ret = {oldMember(...)}
--debug.Message(dump(currentDefine, 1))
max = math.max(max, currentDefine.offset) -- after member, so array sizes get counted
return unpack(ret)
end
setU(types.i8, "member", myMember)
types.CustomType, mem.structs.CustomType = myMember, myMember
mem.struct(function(define, ...)
currentDefine = define
return structs.f[name](define, ...)
end)
setU(types.i8, "member", oldMember)
types.CustomType, mem.structs.CustomType = oldCustomType, oldStructsCustomType
--debug.Message(name, max)
return max
end
local globalExcludes =
{
Player = {"Attrs"}, -- attrs is from merge.
GameStructure = {"Dialogs", -- mmext
"ExtraStatDescriptions", "SkillMasteryDescriptions"}, -- mine
Item = {"ExtraData"}, -- my stuff for MAW mod
}
function getFunctionsData(class, infoData)
local data = {}
for mname, handler in pairs(class or {}) do
if type(handler) == "function" then
local def = getU(handler, "def")
if type(def) == "table" then
data[mname] = data[mname] or {}
data[mname] = {def = def, info = infoData[mname]}
end
end
end
return data
end
-- C++ definition generator constants
local skipBitsText = "SKIPBITS(%d);"
local skipBytesText = "SKIP(%d);"
local INDENT_CHARS = "\t"
local namespaceStr = "mm" .. _G.offsets.MMVersion .. "::"
local commonTypeNamesToCpp =
{
u1 = "uint8_t",
u2 = "uint16_t",
u4 = "uint32_t",
u8 = "uint64_t",
i1 = "int8_t",
i2 = "int16_t",
i4 = "int32_t",
i8 = "int64_t",
r4 = "float",
r8 = "double",
r10 = "long double",
b1 = "bool",
}
local extra = -- extra fields in structs
{
Player =
{
{games = {7, 8}, data = {offset = structs.o.Player.FireResistanceBase + 18, name = "LightResistanceBase", typeName = commonTypeNamesToCpp.i2, size = 2, dataType = types.i2} },
{games = {7, 8}, data = {offset = structs.o.Player.FireResistanceBase + 20, name = "DarkResistanceBase", typeName = commonTypeNamesToCpp.i2, size = 2, dataType = types.i2} },
{games = {7, 8}, data = {offset = structs.o.Player.FireResistanceBase + 22 + 18, name = "LightResistanceBonus", typeName = commonTypeNamesToCpp.i2, size = 2, dataType = types.i2} },
{games = {7, 8}, data = {offset = structs.o.Player.FireResistanceBase + 22 + 20, name = "DarkResistanceBonus", typeName = commonTypeNamesToCpp.i2, size = 2, dataType = types.i2} },
}
}
local globalReplacements = {class = "clas", ["if"] = "if_", ["else"] = "else_"}
local convertToPointers = -- because updated during runtime etc.
{
SpritesLod = {"SpritesSW"},
GameStructure = {"NPCDataTxt", "MonstersTxt", "CharacterPortraits", "TransportLocations", "NPCGroup", "NPCText", "TransTxt", "ShopTheftExpireTime",
"MapDoorSound", "GlobalEvtLines", "ItemsTxt", "SpcItemsTxt", "StdItemsTxt", "NPC", "MixPotions", "ReagentSettings", "NPCNews", "MapFogChances",
"ShopItems", "GuildItems", "NPCTopic", "ShopSpecialItems", "AutonoteTxt", "MapStats", "Houses", "ClassNames", "HostileTxt",
"PlaceMonTxt", "AutonoteCategory", "QuestsTxt", "HousesExtra", "CharacterDollTypes", "HouseMovies", "NPCGreet", "TransportIndex",
"GuildNextRefill2", "ShopNextRefill", "ClassNames", "AwardsTxt", "InOODialog", -- from merge
"PatchOptions", -- potentially relocated each run (dll loading at different address)
"CustomLods", "MonsterKinds", "TitleTrackOffset", "MissileSetup", "AwardsSort", "FoodGoldVisible",-- MMExt
"FrameCounter", "NPCNames"
},
GameClasses = {"HPBase", "SPBase", "HPFactor", "SPFactor", "StartingStats", "Skills" -- Merge
, "SPStats"}, -- MMExt
GameClassKinds = {"StartingSkills"}, -- Merge
DialogLogic = {"List"}, -- MMExt
GameParty = {"QBits"}, -- doesn't have changed structs.o.GameParty.QBits entry
-- Merge structs
CharacterVoices = {"Avail", "Sounds"},
ArmorPicsCoords = {"Armors", "Belts", "Boots", "Cloaks", "Helms"},
HouseRules = {"AlchemistsSpecial", "AlchemistsStandart", "Arcomage", "ArcomageTexts", "ArmorShopsSpecial", "ArmorShopsStandart", "MagicShopsSpecial", "MagicShopsStandart", "SpellbookShops", "Training", "WeaponShopsSpecial", "WeaponShopsStandart"},
}
-- decided to keep all three games' structures in one file, because I would have to include files for all games anyway
local structureByFile =
{
MapModel = {"ModelVertex", "ModelFacet", "BSPNode", "MapModel"},
MapMisc = {"TilesetDef", "MapOutline", "MapOutlines", "OdmHeader", "BlvHeader", "SpawnPoint", "BaseLight", "MapNote"},
MapElements = {"FacetData", "MapLight", "MapRoom", "MapVertex", "MapFacet", "MapDoor", "MapSprite", "MapChest"},
Common = {"SpellBuff", "SpellEffect", "Item", "StartStat", "ObjectRef2", "FloatVector", "ObjectRef"},
Monster = {"MonsterSchedule", "MonsterAttackInfo", "MapMonster", "MonstersTxtItem", "MonsterKind"},
GameMap = {"MapExtra", "MapObject", "GameMap"},
Arcomage = {"ArcomageAction", "ArcomageActions", "ArcomageCard", "ArcomagePlayer", "Arcomage"},
Lod = {"LodRecord", "CustomLods", "LodFile", "Lod", "LodSprite", "LodSpriteD3D", "SpritesLod", "LodBitmap", "BitmapsLod", "LodSpriteLine", "LodPcx"},
Bin = {"DecListItem", "OverlayItem", "TileItem", "ObjListItem", "DChestItem", "SoundsItem", "TFTItem", "MonListItem", "PFTItem", "IFTItem", "SFTItem", "SFT", "CurrentTileBin"},
TxtFileItems = {"HistoryTxtItem", "StdItemsTxtItem", "SpcItemsTxtItem", "SpellsTxtItem", "MapStatsItem", "ItemsTxtItem", "NPCProfTxtItem", "Events2DItem", },
GameDataStructs = {"GameRaces", "GameClasses", "GameClassKinds", "DialogLogic", "Dlg", "GameScreen",
"SkillMasteryDescriptions" -- my addition
},
GameMisc = {"SpellInfo", "TravelInfo", "FogChances", "ShopItemKind", "GeneralStoreItemKind", "HouseMovie", "Weather", "MoveToMap", "MissileSetup", "TownPortalTownInfo", "EventLine", "ProgressBar", "ActionItem", "PatchOptions", "GameMouse", "MouseStruct", "Fnt"},
Player = {"FaceAnimationInfo", "LloydBeaconSlot", "BaseBonus", "Player"},
GameParty = {"GameParty", "NPC", "Button", "NPCNewsItem"},
MergeSpecific = {"ArmorShopRule", "ShopRule", "WeaponShopRule", "ArcomageRule", "HouseRules", "HousesExtra", "CharacterDollType", "CharacterVoices", "CharacterPortrait", "PartyLight", "EquipCoordsCloak", "EquipCoordinates", "ArmorPicsCoords", "ReagentSettings"},
GameStructure = {"GameStructure"}
}
local function stripNamespaces(str)
return str:gsub(".-::", "")
end
local function getStructFile(name)
for k, v in pairs(structureByFile) do
if table.find(v, name) then
return k
end
end
printf("No file specified for name %q", name)
return "unknown"
end
local function noInfinity(amount) -- structs.Fnt
if math.abs(amount) ~= 1/0 then
return amount
end
return 256
end
-- helper functions
-- DEFINED IN A_EksekkFunctions.lua
local function getCodeInjectionForStruct(name)
os.mkdir(CODE_INJECT_PATH)
local file = io.open(CODE_INJECT_PATH .. name .. ".cpp")
if file then
local code = file:read("*a")
file:close()
return code
end
end
local function toCamelCase(str)
local twoUpper = str:len() >= 2 and str:sub(1, 2):upper() == str:sub(1, 2)
str = twoUpper and str or (str:sub(1, 1):lower() .. (str:len() >= 2 and str:sub(2) or ""))
str = globalReplacements[str] or str
return str
end
local function processCodeInjection(injection, gameVer)
gameVer = gameVer or offsets.MMVersion
local lines = injection:gsub("\r\n", "\n"):split("\n") -- gsub just in case
local removeRows = {}
local mmverRestrictions = {}
for i, line in ipairs(lines) do
local matchVer = line:match("^#mmver%s-(%d+)")
local matchEndVer = line:match("^#endmmver")
if matchVer then
local vers = {}
for c in matchVer:gmatch("%d") do
local j = #vers
table.insert(vers, tonumber(c))
assert(#vers > j, format("Added nil index %d", i))
end
table.insert(mmverRestrictions, vers)
removeRows[i] = true
elseif matchEndVer then
assert(#mmverRestrictions > 0, format("Too many %q lines", "#endmmver"))
table.remove(mmverRestrictions, #mmverRestrictions)
removeRows[i] = true
else
for index = 1, #mmverRestrictions do
local vers = mmverRestrictions[index]
if not table.find(vers, gameVer) then
removeRows[i] = true
break
end
end
end
end
if #mmverRestrictions > 0 then
error(format("Not enough %q lines", "#endmmver"))
end
local processedLines = {}
for i, line in ipairs(lines) do
if not removeRows[i] then
table.insert(processedLines, line)
end
end
return processedLines
end
local function codeInjectionTests(gameVer)
gameVer = gameVer or offsets.MMVersion
local testPath = path.addslash(CODE_INJECT_PATH) .. "/tests/"
local index = 0
local failedAny
local attempted = 0
while true do
index = index + 1
local ok, test, output
ok, test = pcall(io.load, testPath .. index .. ".cpp")
if not ok then break end
ok, output = pcall(io.load, testPath .. index .. ".output" .. gameVer)
if not ok then break end
attempted = attempted + 1
local try = processCodeInjection(test, gameVer)
io.save(testPath .. index .. ".try", table.concat(try, "\r\n"))
output = output:gsub("\r\n", "\n"):split("\n")
local fail
if #output == 1 and output[1] == "" then -- handle edge case of no output
output[1] = nil
end
if #output ~= #try then
fail = true
else
for i, line in ipairs(output) do
if line ~= try[i] then
fail = true
break
end
end
end
if fail then
failedAny = true
printf("[MM%d] Code injection processing test #%d failed", gameVer, index)
end
end
if not failedAny then
printf("[MM%d] All code injection tests passed", gameVer)
end
end
function doTests()
codeInjectionTests(6)
codeInjectionTests(7)
codeInjectionTests(8)
end
local function generateFunctionCode(functionData, methods, structName, namespaceStr)
local functionDeclCode = {}
local functionDefCode = {}
local funcFormatDecl = "%s %s %s(%s);%s" -- [return type] [calling convention] [name]([params])[comment]
local funcFormatDef = "%s %s%s::%s(%s)" -- [return type] [namespaceStr][structName]::[name]([params])
for mname, data in pairs(functionData) do
local def, info = data.def, data.info
local r, retStr = def.ret, "int"
if r then
local typ = type(r)
if typ == "string" then
retStr = "char*"
elseif typ == "boolean" then
retStr = "bool"
elseif typ == "number" then
retStr = "int"
else
error(typ)
end
end
local comment = format(" // address: 0x%X", def.p)
if def[1] then -- has default parameters or has parameters at all
local t = {}
local start = 1
if methods[mname] then
start = 2 -- methods auto-prepend one dummy parameter
t[1] = "(this)"
end
local function tostring2(str) -- quotes string instead of "nil"
if type(str) == "string" then
return format("%q", str)
end
return tostring(str)
end
for i = start, #def do
t[#t + 1] = tostring2(def[i])
end
comment = comment .. " | defaults: " .. table.concat(t, ", ")
end
local retVal = ({["char*"] = "nullptr", bool = "false", int = "0"})[retStr]
local fbody = INDENT_CHARS .. (retVal and format("return %s;", retVal) or "")
local ccStr = select(def.cc + 1, "__stdcall", "__thiscall", "__fastcall", "__fastcall/*+eax*/")
local paramsStr = info and info.Sig and format("/*%s*/", info.Sig) or ""
local fname = toCamelCase(mname) -- using "mname" here also takes care of duplicated functions within class still having distinct names
table.insert(functionDeclCode, format(funcFormatDecl, retStr, ccStr, fname, paramsStr, comment))
multipleInsert(functionDefCode, #functionDefCode + 1, {
format(funcFormatDef, retStr, namespaceStr, structName, fname, paramsStr),
"{",
fbody,
"}",
""
})
end
return functionDeclCode, functionDefCode
end
--[[
all possible attributes:
- array - is array
- beyondLen - access beyond length (???)
- count - number of elements
- size - full array size (product of count and all child array counts and base type size)
- low - first element index (usually 0 or 1)
- innerType - return value from recursive call
- typeName - data type name (for structs it's struct name)
- constValue - value of this member can't be changed
- formatTypeName - %s is embedded in string and represents member name and thus must be formatted
- ptr - is ptr to something (array, character string, structure etc.)
- constPtr - pointer can't be changed
- size - entire member size in memory, for arrays it's count * innerType.size
- bit - is bit
- bitValue - used for named bits (like structs.Item.Stolen)
- bitIndex - index of bit in little endian starting from 0 (bit 0x200 of container 0xFE84 would have index 14), used only with named bits
- bigEndian - used with bit arrays
- anti - is anti bit (if you access a bit (0-7) with index 2, anti bit index is (7 - 2) = 5), used only with unnamed bits
- comments - table of strings
- struct - is struct (or pointer to it if ptr is set)
- commentOut - self-explanatory, used for custom types with 0 size
- padding
- [added in processStruct] ptrValue - pointer with set value
- [added in getGroup] padStart
]]
local function getMemberData(structName, memberName, member, offsets, members, class, rofields, customFieldSizes, inArray)
rofields = rofields or {}
member = member or members[memberName]
local data = {name = memberName or "", offset = offsets[memberName or ""] or 0}
local function addComment(s)
data.comments = data.comments or {}
table.insert(data.comments, s)
end
if type(memberName) == "number" then -- unions
data.name = "_" .. memberName
end
local protFunc = getU(member, "f0")
if protFunc then
addComment("requires unprotect before change")
member = protFunc
end
local up = debug.upvalues(member)
local arrayHandler = up.f
local boolsize = booleanHandlers[member] -- boolean
local memArr = up.arr -- mem arrays etc.
local sname = (type(arrayHandler) == "table" and arrayHandler or {})[internal.structs_name_t] -- structure
local bitValue = up.b
local isAutoValueBit = up.bitHandlers and true or false -- bit without specifying value (contiguous), used in arrays
local stringLen = up.len
local unionOffsets = up.offs
data.constValue = memberName and (tostring(memberName):len() > 0) and (table.find(rofields, memberName) ~= nil)
if unionOffsets then
data.union, data.offsets, data.fields, data.rofields = true, unionOffsets, up.fields, up.rofields
-- assume union offset is minimum offset among its fields
local offset = 0xFFFFFFFF
for k, v in pairs(data.offsets) do
offset = math.min(offset, v)
end
data.offset = offset
data.dataType = types.union
elseif up.count then -- array
-- if Merge and table.find({"HPBase", "SPBase", "HPFactor", "SPFactor", "SPStats"}, memberName) then
-- data.offset = getU(getmetatable(Game.Classes[memberName]).__index, "o")
-- end
data.array = true
data.innerType = getMemberData(structName, memberName, arrayHandler, offsets, members, class, rofields, customFieldSizes, true)
if data.innerType.replaceWith then
data.replaceWith = data.innerType.replaceWith
return data
end
data.ptr = up.ptr
data.lenP = up.lenP
data.lenA = data.lenP and getU(MT(up.lenA).__index, "size")
data.beyondLen = up.beyondLen
data.count = noInfinity(up.count == 0xFFFFFFFF and 0 or up.count)
if data.beyondLen then
addComment("AccessBeyondLength is active (???)")
end
data.low = up.low
if data.low and data.low > 0 then
addComment(string.format("MMExt: %d..%d, here %d..%d", data.low, data.low + data.count - 1, 0, data.count - 1))
end
data.size = ((data.ptr or data.count == 0) and 4 or data.count * (up.size + (data.innerType.fakeSize or 0) ))
-- PADDING PROBLEMS, two solutions below:
if data.innerType.fakeSize then
data.innerType.padding = data.innerType.fakeSize
elseif data.innerType.size and up.size > data.innerType.size then
data.innerType.padding = up.size - data.innerType.size
-- 1st, count padding as type size
--data.innerType.size = up.size
end
if data.innerType.bit then
data.originalBitCount = data.count
local bytes = data.count:div(8)
data.count = bytes
addComment("array of " .. (data.innerType.anti and "abits (real index = 7 - usual)" or "bits"))
data.size = bytes
else
-- 2nd, padding is distinct and add it only when necessary
data.size = (data.ptr or data.count == 0) and 4 or math.ceil(data.count * (data.innerType.size + (data.innerType.padding or 0)))
end
data.dataType = data.ptr and types.parray or types.array
-- change array to pointer
if convertToPointers[structName] and table.find(convertToPointers[structName], data.name) then
tget(luaData, structName, data.name).ptrName = data.name
setBaseTypeField(data, "convertToPointer", true) -- setting this to explicitly signal to processStruct that it needs to convert length field into pointer
--setBaseTypeField(data, "padding", (getBaseTypeField(data, "padding") or 0) + getBaseTypeField(data, "size") - 4)
end
elseif stringLen then -- string (fixed size, not pointer)
data.array = true
data.count = stringLen
data.low = 0
data.size = data.count
data.innerType = {typeName = "char", name = memberName, offset = 0, size = 1, dataType = types.string}
addComment("fixed size string, " .. (up.NoZero and "doesn't require null terminator" or "requires null terminator"))
data.dataType = types.string
elseif bitValue then
data.size = 1/8
data.bitIndex = 7 - bitIndex[bitValue] or error("Not a valid bit", 1)
data.typeName = "bool %s : 1"
data.formatTypeName = true
data.bit = true
data.dataType = types.bit
elseif isAutoValueBit then
data.size = 1/8
data.bit = true
if inArray then
data.bigEndian = true
data.typeName = "uint8_t"
else
data.typeName = "bool %s : 1"
data.formatTypeName = true
end
data.anti = member == AnyABitHandler -- is anti bit (if you access a bit (0-7) with index 2, anti bit index is (7 - 2) = 5)
data.dataType = data.anti and types.abit or types.bit
elseif getUpvalueByValue(member, EditPChar_newindex) then
data.size = 4
data.typeName = "char"
data.ptr = true
data.dataType = types.EditPChar
addComment("EditPChar")
elseif getUpvalueByValue(member, EditConstPChar_newindex) then
data.size = 4
data.typeName = "char"
data.ptr = true
addComment("EditConstPChar - unprotect before/protect after edit")
data.dataType = types.EditConstPChar
elseif boolsize then
if boolsize == 1 then
data.typeName = "bool"
else
data.typeName = boolsize == 2 and commonTypeNamesToCpp.u2 or commonTypeNamesToCpp.u4
addComment(boolsize .. "-byte boolean")
end
data.size = boolsize
data.dataType = types["b" .. boolsize]
elseif memArr then
local found
for _, name in ipairs{"u1", "u2", "u4", "u8", "i1", "i2", "i4", "i8", "r4", "r8", "r10"} do
if memArr == mem[name] then
data.typeName = commonTypeNamesToCpp[name]
data.size = tonumber(name:sub(2))
found = true
data.dataType = types[name]
break
end
end
if memArr == mem.pchar then
data.size = 4
data.typeName = "char"
data.ptr = true
data.constPtr = true
data.constValue = true
data.dataType = types.pchar
addComment("PChar (read-only)")
found = true
end
found = found or error("Unknown mem array type")
elseif sname then -- struct or pstruct
if sname == "PlayerResistanceBaseBonus" then
local ro = table.find(rofields, memberName)
local newBase, newBonus = "ResistanceBase", "ResistanceBonus"
data.replaceWith =
{
{
offset = data.offset, array = true, count = 11, low = 0, size = 22, name = newBase, constValue = ro, dataType = types.array, innerType =
{
typeName = commonTypeNamesToCpp.i2, offset = data.offset, size = 2, name = newBase, constValue = ro, dataType = types.i2
},
},
{
offset = data.offset + 22, array = true, count = 11, low = 0, size = 22, name = newBonus, constValue = ro, dataType = types.array, innerType =
{
typeName = commonTypeNamesToCpp.i2, offset = data.offset + 22, size = 2, name = newBonus, constValue = ro, dataType = types.i2
},
},
}
return data
--data.size = 4
end
local isPtr = up.pstruct
data.typeName = sname
data.ptr = isPtr
data.struct = true
-- if table.find(structsWithFakeSize, sname) then
-- --data.size = calcStructLargestSize(sname)
-- data.size = structs[sname]["?size"]
-- data.padding = data.size
-- data.fakeSize = data.size
-- else
data.size = isPtr and 4 or structs[sname]["?size"]
--end
data.dataType = isPtr and types.pstruct or types.struct
else
data.array = true
data.innerType = {size = 1, typeName = commonTypeNamesToCpp.u1, name = memberName, dataType = types.u1, unknownType = true}
data.count = customFieldSizes[memberName] or error("Unknown custom type name: " .. memberName, 2)
if data.count == 0 then
--data.count = 1
data.commentOut = true
data.innerType.commentOut = true
addComment("real size is 0")
end
data.low = 0
data.size = data.count
addComment("Unknown type")
end
if convertToPointers[structName] and table.find(convertToPointers[structName], data.name) then
tget(luaData, structName, data.name).ptrName = data.name
data.convertToPointer = true
end
return data
end
--r();print(getArrayPointerString({{count = 5, ptr = true, innerType = {count = 3}}}, {typeName = "int", name = "testMember", ptr = true}))
do
local stdArray = "std::array<%s, %d>"
local primitiveArrayNormal = "(%s)[%d]"
local primitiveArrayOfPointers = "(*%s)[%d]"
local primitiveArrayOfPointersNoSize = "(*%s)"
function getArrayPointerString(arrays, last, addPointer, usePrimitiveArrays) -- for testing: https://cdecl.org/
if usePrimitiveArrays then
local type = last.name
for i = 1, #arrays do
local arr = arrays[i]
if arr.count == 0 then
type = primitiveArrayOfPointersNoSize:format(type)
else
if arr.ptr then
type = primitiveArrayOfPointers:format(type, arr.count)
else
type = primitiveArrayNormal:format(type, arr.count)
end
end
end
return (last.namespacePrefix or "") .. last.typeName .. (last.ptr and "*" or "") .. (last.constPtr and "const" or "") .. (addPointer and "*" or "") .. type
else
local type = (last.namespacePrefix or "") .. last.typeName .. (last.ptr and "*" or "") .. (last.constPtr and "const" or "")
for i = #arrays, 1, -1 do
local arr = arrays[i]
if arr.count == 0 then
type = type .. "*"
else
type = stdArray:format(type, arr.count) .. (arr.ptr and "*" or "")
end
end
return type .. (addPointer and "*" or "") .. " " .. last.name
end
end
end
local function getArraysCommentsAndBaseData(data)
-- collect array info and get to base type
local arrays, comments = {}, {}
while data.array do
comments = table.join(comments, data.comments or {})
arrays[#arrays + 1] = data
data = data.innerType
end
comments = table.join(comments, data.comments or {})
return arrays, comments, data
end
local function structField(value)
return {type = "struct", value = value}
end
local function memberField(value)
return {type = "member", value = value}
end
local function unionField(value)
return {type = "union", value = value}
end
local function paddingField(value)
return {type = "padding", value = value}
end
local function processSingle(data, indentLevel, structName, namespaceStr, debugLines, layout, infoData)
indentLevel = indentLevel or 0
local indentOuter, indentInner = string.rep(INDENT_CHARS, indentLevel), string.rep(INDENT_CHARS, indentLevel + 1)
local dataCopy = data
local s = indentOuter
local arrays, comments, data = getArraysCommentsAndBaseData(data)
if #arrays == 0 then arrays = nil end
-- use camel case (I personally prefer it)
data.pascalCaseName = data.name
table.foreach(arrays or {}, function(v) v.pascalCaseName = v.name end)
data.name = toCamelCase(tostring(data.name))
local multilineComment -- if two lines or more, put comment before field
local function processMultiline(index, indent)
if multilineComment then
multilineComment[1] = "MMExt info: " .. multilineComment[1]
for i, line in ipairs(multilineComment) do
multilineComment[i] = indent .. "// " .. line
end
multipleInsert(s, index, multilineComment)
return true
end
end
local function doBaseType(doArrays)
local result = data.static and "static " or ""
-- structure, mem array, edit pchars, bools, bits
if data.constValue then
result = result .. "const "
end
if data.formatTypeName then
result = result .. data.typeName:format(data.name)
else
if arrays and doArrays then
result = result .. getArrayPointerString(arrays, data)
else
result = result .. (data.namespacePrefix or "") .. data.typeName .. (data.ptr and "*" or "") .. " " .. data.name
end
end
local offset = arrays and arrays[1] and arrays[1].offset or data.offset or 0
table.insert(comments, string.format("0x%X (%d decimal)", offset, offset))
if data.bit and data.bitIndex then
comments[#comments] = comments[#comments] .. ", bit index " .. data.bitIndex
end
local info = (infoData or {})[data.pascalCaseName] -- unions don't have info data
if info and type(info) == "string" then
local parts = info:gsub("\r\n", "\n"):split("\n")
if #parts == 1 then
local comment = " | MMExt info: " .. parts[1]
comments[#comments] = comments[#comments] .. comment
else
multilineComment = parts
end
end
if data.commentOut then
result = "// " .. result
end
return result
end
local layoutAdd
if data.union then
s = {}
for i, v in ipairs(data.code) do
s[i] = indentOuter .. v
end
layoutAdd = assert(data.layout)
elseif arrays and #arrays == 1 and data.padding then -- NPCTopic, NPCText - element size 4, "real" size 8, overlapping
local structName = "__" .. data.name
local old1 = data.name
data.name = "value"
s = {
indentOuter .. "struct " .. structName,
indentOuter .. "{",
indentInner .. doBaseType(false) .. ";",
indentInner .. skipBytesText:format(data.padding),
indentOuter .. "};"
}
layoutAdd = {
{type = "struct", value = {
memberField(data),
{type = "padding", value = data.padding}
}
}}
data.name = old1
local old2, old3 = data.typeName, data.ptr -- hacky hacky
data.typeName = structName
data.ptr = nil
processMultiline(5, indentOuter)
s[#s + 1] = indentOuter .. doBaseType(true) .. ";"
table.insert(layoutAdd, memberField(arrays and arrays[1] or data))
data.typeName, data.ptr = old2, old3
else
if data.padStart then
s = {
indentOuter .. "struct " .. structName,
indentOuter .. "{",
indentInner .. skipBytesText:format(data.padStart),
indentInner .. doBaseType(true) .. ";",
indentOuter .. "};"
}
processMultiline(4, indentInner)
layoutAdd = {
{type = "struct", value = {
{type = "padding", value = data.padStart},
memberField(arrays and arrays[1] or data)
}
}}
else
s = {s .. doBaseType(true)}
processMultiline(1, indentOuter)
layoutAdd = {memberField(arrays and arrays[1] or data)}
end
end
local commentsStr = #comments > 0 and (" // " .. table.concat(comments, " | ")) or ""
if type(s) == "table" then
s[#s] = s[#s] .. ";" .. commentsStr
else
s = s .. ";" .. commentsStr
end
if _DEBUG and not data.static and not data.commentOut then
local off = arrays and arrays[1] and arrays[1].offset or data.offset or 0
local formatStr = "%sstatic_assert(offsetof(%s, %s) == %d);"
if off ~= 0 and structName and not data.bit then
table.insert(debugLines, formatStr:format(indentOuter, namespaceStr .. structName, data.name, off))
end
end
data = dataCopy
mergeArraysShallowCopy(layout, assert(layoutAdd), true)
return s
end
--[[
{type = "member", value = {name = "X"}},
{type = "padding", value = 5},
{type = "member", value = {name = "StableZ"}},
{type = "union", value = {
{type = "member", value = {size = 6}},
{type = "struct", value = {
{type = "padding", value = 4},
{type = "member", value = {size = 1}},
{type = "member", value = {size = 1}},
}
},
{type = "struct", value = {
{type = "member", value = {name = "X"}},
{type = "member", value = {name = "Y"}},
{type = "member", value = {name = "Z"}},
},
{type = "padding", value = 20},
{type = "member", value = {name = "LookAngle", dataType = types.i4}},
]]
-- using visual studio code with "lua booster" extension
-- need to dummy forward declare to not miss recursive call when using "find references"
local function processGroup(group, indentLevel, structName, namespaceStr, debugLines, layout, infoData)
indentLevel = indentLevel or 0
if #group == 1 then
local ret = processSingle(group[1], indentLevel, structName, namespaceStr, debugLines, layout, infoData)
return type(ret) == "table" and ret or {ret}
end
local indentOuter, indentInner = string.rep(INDENT_CHARS, indentLevel), string.rep(INDENT_CHARS, indentLevel + 1)
-- do union wrap
local union = unionField{}
table.insert(layout, union)
layout = union.value -- everything else goes into union
local code = {
indentOuter .. "union",
indentOuter .. "{"
}
setmetatable(code, {__newindex = function(t, k, v) -- used in processSingle(), for convenience value can be table
if type(v) == "table" then
multipleInsert(t, k, v)
else
rawset(t, k, v)
end
end})
-- if any two members have different sizes, need to wrap in struct
-- members with size equal to biggest among group will be outside, other inside
local maxS = 0
for i, v in ipairs(group) do
maxS = math.max(maxS, v.size)
end
local wrap = {}
local offset = group[1].offset
local skipFirst = true -- first is at right offset, second needs adjustment (usually by 4)
for i, member in ipairs(group) do
if member.size ~= maxS then
wrap[#wrap + 1] = member
else
-- for now taken care of by processSingle
local padding = member.innerType and member.innerType.padding
local layoutTmp = layout
if padding and not skipFirst then
code[#code + 1] = indentInner .. "struct"
code[#code + 1] = indentInner .. "{"
indentLevel = indentLevel + 1
code[#code + 1] = string.rep(INDENT_CHARS, indentLevel + 1) .. skipBytesText:format(padding)
local stru = {type = "struct", value = {}}
table.insert(stru.value, {type = "padding", value = padding})
layoutTmp = stru.value -- next call will pack members into this struct
table.insert(layout, stru)
end
code[#code + 1] = processSingle(member, indentLevel + 1, structName, namespaceStr, debugLines, layoutTmp, infoData)
if padding and not skipFirst then
indentLevel = indentLevel - 1
code[#code + 1] = indentInner .. "};"
end
if padding then
skipFirst = not skipFirst
end
end
offset = member.offset + member.size + ((member.innerType or {}).padding or 0)
end
if #wrap > 0 then
-- do struct wrap
table.sort(wrap, function(a, b) return a.offset < b.offset end)
do
local stru = structField{}
table.insert(layout, stru)
layout = stru.value
end
code[#code + 1] = indentInner .. "struct"
code[#code + 1] = indentInner .. "{"
local indentInnerInner = string.rep(INDENT_CHARS, indentLevel + 2)
local lastOffset = group[1].offset
local i = 1
while true do
local currentField = wrap[i]
local members
members, i = getGroup(wrap, currentField, i)
local skipBits = (currentField.offset - lastOffset) * 8
if skipBits > 0 and skipBits % 8 == 0 then
local bytes = skipBits:div(8)
code[#code + 1] = indentInnerInner .. skipBytesText:format(bytes)
table.insert(layout, paddingField(bytes))
elseif skipBits > 0 then
--[[
SATISFY MSVC
packing bitfields there is wonky
either have each bitfield item the size of item outside of bitfield (unsigned also strongly recommended), otherwise there will be wasted space
or use 1-byte fields and carefully adjust size of skipped bits so that 8-bit boundary is not crossed in one declaration (if so, other bits will be skipped,
so not only wasted space, but also subsequent bit positions will be wrong and union size may be wrong)