mirror of
https://github.com/CPunch/gopenfusion.git
synced 2024-11-24 16:11:04 +00:00
CPunch
7d83732e44
- lol, i had no idea python *FINALLY* added a switch/case equivalent.
sadly though, it looks like the generated python bytecode is
nearly identical in performance.
there's no lookup table magic for match/case, and is almost
identical to if/else if/else. it amounts to syntax sugar 😭
387 lines
14 KiB
Python
Executable File
387 lines
14 KiB
Python
Executable File
#!/bin/python
|
|
'''
|
|
genstructs.py - gopenfusion
|
|
|
|
Takes raw structures from a decompiled 'Assembly - CSharp.dll' from a main.unity3d fusionfall beta client,
|
|
and transpiles them to gopenfusion's custom packet structure & tags. Some useful constants are also grabbed
|
|
and transpiled. This requires a C compiler installed, since struct field padding is grabbed via the `offsetof()`
|
|
C macro. Some manual rearranging of structures from the disassembled source might be needed, however the script
|
|
should 'just werk' off of ilspycmd output. This script can also be modified to generate c-style structures
|
|
(because it already does!)
|
|
|
|
usage: ./genstructs.py [IN.cs] > structs.go
|
|
|
|
It's recommended to run structs.go through `gofmt`.
|
|
'''
|
|
from distutils.ccompiler import new_compiler
|
|
import subprocess
|
|
import sys
|
|
import os
|
|
|
|
PACK_ALIGN = 4
|
|
|
|
def sanitizeName(name: str) -> str:
|
|
# all exported fields in go must start capitalized
|
|
return name[0:1].upper() + name[1:]
|
|
|
|
def writeToFile(source: str, filePath: str) -> None:
|
|
with open(filePath, "w") as out:
|
|
out.write(source)
|
|
|
|
WARN_INVALID = False
|
|
|
|
class StructTranspiler:
|
|
class StructField:
|
|
def __init__(self, name: str, csType: str, marshal: str) -> None:
|
|
self.marshal = marshal
|
|
self.type = csType
|
|
self.ctype = csType # for transpilation to c
|
|
self.tags = ""
|
|
self.size = 0
|
|
self.padding = 0
|
|
self.name = sanitizeName(name)
|
|
self.cname = self.name
|
|
self.needsPatching = False
|
|
|
|
match csType:
|
|
case "byte":
|
|
self.type = "uint8"
|
|
self.ctype = "char"
|
|
self.size = 1
|
|
case "sbyte":
|
|
self.type = "int8"
|
|
self.ctype = "char"
|
|
self.size = 1
|
|
case "short":
|
|
self.type = "int16"
|
|
self.ctype = "short"
|
|
self.size = 2
|
|
case "int":
|
|
self.type = "int32"
|
|
self.ctype = "int"
|
|
self.size = 4
|
|
case "uint":
|
|
self.type = "uint32"
|
|
self.ctype = "int"
|
|
self.size = 4
|
|
case "float":
|
|
self.type = "float32"
|
|
self.ctype = "float"
|
|
self.size = 4
|
|
case "long":
|
|
self.type = "int64"
|
|
self.ctype = "long"
|
|
self.size = 8
|
|
case "ulong":
|
|
self.type = "uint64"
|
|
self.ctype = "long"
|
|
self.size = 8
|
|
case "byte[]":
|
|
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
|
self.type = "[%d]byte" % self.size
|
|
self.ctype = "char"
|
|
self.cname += "[%d]" % self.size
|
|
case "short[]":
|
|
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
|
self.type = "[%d]int16" % self.size
|
|
self.ctype = "short"
|
|
self.cname += "[%d]" % self.size
|
|
self.size *= 2
|
|
case "string":
|
|
# all strings in fusionfall are utf16, in a uint16 array
|
|
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
|
self.type = "string"
|
|
self.addTag("size:\"%d\"" % self.size) # special tag to tell our decoder/encoder the size of the uint16[] array
|
|
self.ctype = "short"
|
|
self.cname += "[%d]" % self.size
|
|
self.size *= 2
|
|
case "int[]":
|
|
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
|
self.type = "[%d]int32" % self.size
|
|
self.ctype = "int"
|
|
self.cname += "[%d]" % self.size
|
|
self.size *= 4
|
|
case "float[]":
|
|
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
|
self.type = "[%d]float32" % self.size
|
|
self.ctype = "float"
|
|
self.cname += "[%d]" % self.size
|
|
self.size *= 4
|
|
case "long[]":
|
|
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
|
self.type = "[%d]int64" % self.size
|
|
self.ctype = "long"
|
|
self.cname += "[%d]" % self.size
|
|
self.size *= 8
|
|
case _:
|
|
# assume it's a structure that will be defined later
|
|
if csType.find("[]") != -1: # it's an array!
|
|
csType = csType.replace("[]", "")
|
|
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
|
self.cname = self.name + "[%d]" % self.size
|
|
else:
|
|
self.cname = self.name
|
|
self.size = 1
|
|
self.type = sanitizeName(csType)
|
|
self.ctype = sanitizeName(csType)
|
|
self.needsPatching = True
|
|
|
|
def addTag(self, tag: str) -> None:
|
|
if len(self.tags) > 0: # if there's already a tag defined, make sure there's a space separating them
|
|
self.tags += " "
|
|
|
|
self.tags += tag
|
|
|
|
def __init__(self, lines: list[str]) -> None:
|
|
self.name = "UnknownStruct"
|
|
self.size = 0
|
|
self.fields: list[self.StructField] = []
|
|
self.lines = lines
|
|
self.cursor = 0
|
|
|
|
def getLine(self) -> str:
|
|
return self.lines[self.cursor]
|
|
|
|
def getNextLine(self) -> str:
|
|
self.cursor += 1
|
|
return self.getLine()
|
|
|
|
def needsPatching(self) -> int:
|
|
for i in range(len(self.fields)):
|
|
if self.fields[i].needsPatching:
|
|
return i
|
|
return -1
|
|
|
|
# skip lines until we run into a '[StructLayout('
|
|
def searchStructLayout(self) -> None:
|
|
line = self.getLine()
|
|
while True:
|
|
if line.find("[StructLayout(") != -1 and line.find("Size = ") != -1:
|
|
self.size = int(line[(line.find("Size = ") + len("Size = ")):line.find(")]")])
|
|
line = self.getNextLine()
|
|
self.name = sanitizeName(line[(line.find("public struct ") + len("public struct ")):len(line)-1])
|
|
self.getNextLine()
|
|
return
|
|
line = self.getNextLine()
|
|
|
|
# skip lines until we find a struct field
|
|
def searchMarshal(self) -> StructField:
|
|
line = self.getLine()
|
|
while line.find("}") == -1:
|
|
if line.find("[MarshalAs(") != -1:
|
|
marshal = line
|
|
line = self.getNextLine()
|
|
|
|
typeStart = line.find("public ") + len("public ")
|
|
typeEnd = line.find(" ", typeStart)
|
|
type = line[typeStart:typeEnd]
|
|
|
|
name = line[typeEnd+1:line.find(";", typeEnd)]
|
|
return self.StructField(name, type, marshal)
|
|
line = self.getNextLine()
|
|
|
|
return None
|
|
|
|
def toCStyle(self) -> str:
|
|
source = "typedef struct {\n"
|
|
for field in self.fields:
|
|
source += "\t%s %s;\n" % (field.ctype, field.cname)
|
|
source += "} %s;\n" % self.name
|
|
|
|
for field in self.fields:
|
|
source += "printf(\"%d \""
|
|
source += ", offsetof(%s, %s));\n" % (self.name, field.name)
|
|
|
|
source += "printf(\"\\n\");\n"
|
|
return source
|
|
|
|
def populatePadding(self, offsets: list[str]) -> None:
|
|
currentSize = 0
|
|
for i in range(len(self.fields)):
|
|
currentSize += self.fields[i].size
|
|
if i < len(self.fields)-1 and currentSize != int(offsets[i+1]):
|
|
self.fields[i].padding = int(offsets[i+1]) - currentSize
|
|
currentSize += self.fields[i].padding
|
|
|
|
if currentSize < self.size:
|
|
self.fields[len(self.fields)-1].padding = self.size - currentSize
|
|
|
|
def parseStructure(self) -> None:
|
|
self.searchStructLayout()
|
|
|
|
line = self.getLine()
|
|
while line.find("}") == -1:
|
|
field = self.searchMarshal()
|
|
if field != None:
|
|
self.fields.append(field)
|
|
|
|
line = self.getLine()
|
|
|
|
def toGoStyle(self) -> str:
|
|
global WARN_INVALID
|
|
|
|
source = "type " + self.name + " struct {"
|
|
currentSize = 0
|
|
for field in self.fields:
|
|
currentSize += field.size
|
|
|
|
if field.padding > 0:
|
|
currentSize += field.padding
|
|
field.addTag("pad:\"%d\"" % field.padding)
|
|
|
|
source += "\n\t" + field.name + " " + field.type
|
|
if len(field.tags) > 0:
|
|
source += " `" + field.tags + "`"
|
|
|
|
if currentSize != self.size:
|
|
source += "\n// WARNING: computed size is %d, needs to be %d!!" % (currentSize, self.size)
|
|
WARN_INVALID = True
|
|
else:
|
|
source += "\n// SIZE: %d" % self.size
|
|
source += "\n}\n"
|
|
return source
|
|
|
|
def transpileStructs(lines: list[str]) -> list[StructTranspiler]:
|
|
structs: list[StructTranspiler] = []
|
|
|
|
def tryPatching(struct: StructTranspiler) -> StructTranspiler:
|
|
f = struct.needsPatching()
|
|
lastf = None
|
|
while f != -1 and lastf != f:
|
|
# search for existing struct
|
|
name = struct.fields[f].type
|
|
for s in structs:
|
|
if s.name == name:
|
|
if struct.fields[f].size > 1: # was it an array?
|
|
struct.fields[f].type = ("[%d]" % struct.fields[f].size) + struct.fields[f].type
|
|
struct.fields[f].size *= s.size # field's size was set to 1 even if it wasn't an array
|
|
struct.fields[f].needsPatching = False # mark done
|
|
break
|
|
|
|
lastf = f
|
|
f = struct.needsPatching()
|
|
|
|
return struct
|
|
|
|
while True:
|
|
try:
|
|
struct = StructTranspiler(lines)
|
|
struct.parseStructure()
|
|
|
|
# try to resolve patches right here
|
|
struct = tryPatching(struct)
|
|
|
|
structs.append(struct)
|
|
lines = struct.lines[struct.cursor:]
|
|
except:
|
|
break
|
|
|
|
# move all structures that need patching to the end (so hopefully they'll be in the
|
|
# right order for our C source generation)
|
|
def sortStruct(s: StructTranspiler):
|
|
if s.needsPatching() != -1:
|
|
return 1
|
|
return 0
|
|
structs.sort(key=sortStruct)
|
|
|
|
# check for undefined types in structures and patch the sizes
|
|
for i in range(len(structs)):
|
|
structs[i] = tryPatching(structs[i])
|
|
|
|
# we compile a small c program to grab the exact offsets and alignments
|
|
source = "#include <stdio.h>\n#include <stddef.h>\n"
|
|
source += "int main() {\n"
|
|
source += "#pragma pack(push)\n#pragma pack(%d)\n" % PACK_ALIGN
|
|
for struct in structs:
|
|
source += struct.toCStyle()
|
|
source += "#pragma pack(pop)\nreturn 0;\n}\n"
|
|
|
|
writeToFile(source, "tmp.c")
|
|
compiler = new_compiler()
|
|
compiler.compile(['tmp.c'])
|
|
compiler.link_executable(['tmp.o'], 'tmp')
|
|
|
|
# patch padding bytes
|
|
lines = subprocess.getoutput(['./tmp']).splitlines()
|
|
for i in range(len(structs)):
|
|
structs[i].populatePadding(lines[i].split(" "))
|
|
|
|
return structs
|
|
|
|
class ConstTranspiler:
|
|
def __init__(self, lines: list[str]) -> None:
|
|
self.lines = lines
|
|
self.cursor = 0
|
|
self.value = 0
|
|
self.name = 0
|
|
|
|
def getLine(self) -> str:
|
|
return self.lines[self.cursor]
|
|
|
|
def getNextLine(self) -> str:
|
|
self.cursor += 1
|
|
return self.getLine()
|
|
|
|
# skip lines until we run into a 'public const uint '
|
|
def searchForConst(self) -> None:
|
|
line = self.getLine()
|
|
while True:
|
|
if line.find("public const uint ") != -1:
|
|
nameStart = (line.find("public const uint ") + len("public const uint "))
|
|
self.value = int(line[(line.find("= ", nameStart) + len("= ")):line.find("u;")])
|
|
self.name = line[nameStart:line.find(" = ", nameStart)]
|
|
self.getNextLine()
|
|
return
|
|
line = self.getNextLine()
|
|
|
|
def transpileConsts(lines: list[str]) -> list[ConstTranspiler]:
|
|
consts: list[ConstTranspiler] = []
|
|
|
|
while True:
|
|
try:
|
|
const = ConstTranspiler(lines)
|
|
const.searchForConst()
|
|
|
|
consts.append(const)
|
|
lines = const.lines[const.cursor:]
|
|
except:
|
|
break
|
|
|
|
return consts
|
|
|
|
if __name__ == '__main__':
|
|
inFilePath = sys.argv[1]
|
|
|
|
iFile = open(inFilePath, "r")
|
|
lines = iFile.readlines()
|
|
structs = transpileStructs(lines)
|
|
consts = transpileConsts(lines)
|
|
|
|
# emit structures
|
|
source = "// generated via genstructs.py"
|
|
if WARN_INVALID:
|
|
source += " - WARN!! Not all structures are valid, grep 'WARNING'\n"
|
|
else:
|
|
source += " - All structure padding and member alignment verified\n"
|
|
|
|
source += "package protocol\n\nconst (\n"
|
|
for const in consts:
|
|
source += "\t%s = 0x%x\n" % (const.name, const.value)
|
|
source += ")\n\n"
|
|
|
|
source += "func PacketIDToString(id uint32) string {\n"
|
|
source += "\tswitch id {\n"
|
|
for const in consts:
|
|
if const.name.startswith("P_"):
|
|
source += "\tcase %s:\n\t\treturn \"%s\"\n" % (const.name, const.name)
|
|
source += "\t}\n\treturn \"UNKNOWN\"\n}\n\n"
|
|
|
|
for struct in structs:
|
|
source += struct.toGoStyle() + "\n"
|
|
print(source)
|
|
|
|
os.remove("tmp")
|
|
os.remove("tmp.c")
|
|
os.remove("tmp.o")
|
|
|