My most recent coding project was decoding UI layout files for all 10 Total War games from Empire to Three Kingdoms and writing converter that translates them to XML and back.
Here's a quick writeup of what I did, and how that went.
UI Layout files
Layout in the games are controlled by UI Layout files. They all helpfully start with a version number header - currently from Version025 to Version129. After that follows top level UI element, and within are nested children UI elements and many other things like UI states, transitions, events, and so on.
Basic building blocks
Basic building blocks of the format were fairly easy to understand, mainly:
- booleans as 00 or 01
- integers as int32
- floats as float32
- colors as BGRA32 (that is - one byte per component, in this order)
- ASCII strings as int16 character count, followed by that many characters
- Unicode strings as int16 character count, followed by that many UTF16 characters
- various data structures had their fields in specific order, without any headers, or delimiters
- for arrays of data structures there was generally int32 element count, then followed by each element in succession, without any headers or delimiters
- optional fields - either 01 followed by some data structure, or just a 00
- 128-bit uuids (weirdly no specific version, but still market as a uuid in variant bits)
- occasional int8s and int16s
- arrays of elements repeating until some special value like events_end
- 2D arrays of elements prefixed by xsize and ysize
- and so on
Manual decoding with hex editor
- 20 booleans false
- 5 floats 0.0
- 5 ints 0
- 10 empty ASCII strings
- 10 empty Unicode strings
- 5 empty nested arrays of some child elements
- or most likely some combinations of all of them
Original converter
Once upon a time alpaca wrote a Python converter for Napoleon Total War (second game on the engine). I inherited that, and extended it to backwards to Empire and forwards Shogun 2.
Even with all the fixes it had only maybe 90% support for those three games.
The most obvious approach would be fixing remaining issues and extending it further.
Unfortunately that would be very difficult approach.
Internal Representation Pattern
The converter was based on principle of Internal Representation. Every structure has a class. That class basically has five methods:
- initialize empty data structure with default values
- read from binary file
- write to XML
- read from XML
- write to binary file
Data gathering
Analysis tool
000129-000147 FontNameBlock "Ingame 12, Normal"
000148-000151 LineHeightBlock 2
000152-000155 FontLeadingBlock 1
000156-000159 FontTrailingBlock 255
000160-000174 DataBlock
............... 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00
000175-000185 ShaderNameBlock "normal_t0"
000186-000189 ShaderVariableBlock 0.0 (0)
000190-000193 ShaderVariableBlock 0.0 (0)
000194-000197 ShaderVariableBlock 0.0 (0)
000198-000201 ShaderVariableBlock 0.0 (0)
000202-000270 DataBlock
........0....... 00 00 00 00 01 00 00 00 30 12 00 09 00 00 00 00
................ 00 00 00 00 00 05 00 00 00 04 00 00 bb ff be ff
................ 00 00 00 00 00 00 00 01 01 00 00 00 00 00 00 00
................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
..... 00 00 00 00 00
000271-000282 EventListBlock []
000283-000294 DataBlock
........ .<. 00 00 00 00 01 00 00 00 20 b3 3c 0b
000295-000314 StringBlock "government_screens"
000315-000346 DataBlock
H............... 48 01 00 00 8e 00 00 00 01 01 00 01 00 00 00 00
................ 00 ff ff ff ff 00 00 00 00 05 00 00 00 00 00 00
000347-000421 ImageListBlock 1 elements:
000351-000421 ImageBlockGen1 id=163829448 xsize=256 ysize=256 path="data\\UI\\Campaign UI\\Skins\\fill 2 leather 256 tile.tga" unknown=4294967295
000422-000433 DataBlock
............ 00 00 00 00 00 00 00 00 01 00 00 00
000434-000437 StateIDBlock 162986096
000438-000447 StateNameBlock "NewState"
000448-000451 XSizeBlock 624
000452-000455 YSizeBlock 720
000456-000484 DataBlock
................ 00 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00
............. 01 00 00 00 01 00 00 00 00 00 00 00 00
000485-000503 FontNameBlock "Ingame 12, Normal"
000504-000507 LineHeightBlock 2
000508-000511 FontLeadingBlock 1
000512-000515 FontTrailingBlock -16777216
000516-000530 DataBlock
............... 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00
000531-000541 ShaderNameBlock "normal_t0"
000542-000545 ShaderVariableBlock 0.0 (0)
000546-000549 ShaderVariableBlock 0.0 (0)
000550-000553 ShaderVariableBlock 0.0 (0)
000554-000557 ShaderVariableBlock 0.0 (0)
000558-000565 DataBlock
........ 00 00 00 00 01 00 00 00
000566-000589 ImageUseBlock id=163829448 xofs=0 yofs=0 xsize=624 ysize=720 bgra=bgra(255,255,255,255)
000590-000626 DataBlock
................ 01 00 00 00 00 00 00 01 01 00 00 00 00 00 00 00
................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
..... 00 00 00 00 00
000627-000693 EventListBlock ["OnUpdatePulse", "OnUpdatePulse", "OnDock", "DockHudRelative"]
000694-000705 DataBlock
............ 0a 00 00 00 0e 00 00 00 e8 db b8 09
Direct Conversion Pattern
def convert_int
value = get(4).unpack1("V")
puts "<i>#{ value }</i>"
end
def convert_string
size = get(2).unpack1("v")
str = get(size)
puts "<s>#{ str.xml_escape }</s>"
end
def convert_color
b, g, r, a = get(4).unpack("CCCC")
puts "<color>"
puts " <byte>#{b}</byte><!-- blue -->"
puts " <byte>#{g}</byte><!-- green -->"
puts " <byte>#{r}</byte><!-- red -->"
puts " <byte>#{a}</byte><!-- alpha -->"
puts "</color>"
end
def output(str, comment=nil)
print " " * indent
print str
print "<!-- #{comment} -->" if comment
print "\n"
end
def convert_int(comment=nil)
output "<i>#{ get_int }</i>", comment
end
def convert_color
tag "color" do
convert_byte "blue"
convert_byte "green"
convert_byte "red"
convert_byte "alpha"
end
end
Advantages of Direct Conversion
<model>
<s>composite_scene/porthole/troy_advisor_test.csc</s><!-- mesh path? -->
<s>standard_advisor</s><!-- mesh name? -->
<!-- some model data or anim header or sth -->
<data size="1">
01
</data>
<i>0</i><!-- 00:00:00:00 --><!-- anim count or something? -->
<s></s><!-- anim name? -->
<s></s><!-- anim path? -->
<!-- rest of anim stuff or sth -->
<data size="4">
00 80 3f 00
</data>
<!-- 2900 - end of model data -->
</model>
</models>
<no /><!-- end of uientry flag 5B? -->
<no /><!-- end of uientry flag 6B? -->
<error msg="Invalid boolean value: got 63" version="121">
Data before fail:
ne/porthole/troy 6e 65 2f 70 6f 72 74 68 6f 6c 65 2f 74 72 6f 79
_advisor_test.cs 5f 61 64 76 69 73 6f 72 5f 74 65 73 74 2e 63 73
c..standard_advi 63 10 00 73 74 61 6e 64 61 72 64 5f 61 64 76 69
sor...........?. 73 6f 72 01 00 00 00 00 00 00 00 00 00 80 3f 00
Data from fail 2900:
..?...?.....9..p 00 00 3f 00 00 00 3f 00 00 98 d1 bd 39 10 00 70
ortrait_minspec. 6f 72 74 72 61 69 74 5f 6d 69 6e 73 70 65 63 00
................ 00 00 00 00 00 00 00 00 00 00 00 00 00 01 01 00
................ 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00
Then all I need to do is look back from point of the crash to the last definitely correctly decoded part (in this case those two strings look perfectly fine). Then find where is the first definitely incorrectly decoded part (in this case 00 80 3f is clearly last 3 bytes of a float, so it was off by one at this point already).Debug mode
First three games
Next seven games
- run conversion anyway, ignoring that version is wrong
- identify where exactly it crashes (based on <error> tags and my analysis tool)
- try to fix those crashes, gated by some if @version >= x+1 checks
1 comment:
This seems very useful. But I'm not well versed in programming etc. I'm utterly oblivious in trying to get the things from github to work, despite the instructions. A newb friendly guide would be greatly appreciated. Thanks either way.
Post a Comment