Files
EGameTools/_IDAScripts/ExportClassH/HeaderGen.py

258 lines
9.5 KiB
Python
Raw Normal View History

2025-03-09 01:10:59 +02:00
import os
2025-03-17 02:09:24 +02:00
from ExportClassH import ClassGen, Utils, Config
2025-03-09 01:10:59 +02:00
from ExportClassH.ClassDefs import ClassName, ParsedFunction, ParsedClassVar
processedClasses: set[str] = set()
2025-03-09 01:10:59 +02:00
currentAccess: str = "public"
def GenerateClassVarCode(classVar: ParsedClassVar) -> list[str]:
2025-03-09 01:10:59 +02:00
"""Generate code for a single class variable."""
global currentAccess
access: str = f"{classVar.access}:" if classVar.access else ""
2025-03-09 01:10:59 +02:00
if currentAccess == classVar.access:
2025-03-09 03:04:02 +02:00
access = "\t"
2025-03-09 01:10:59 +02:00
else:
currentAccess = classVar.access
if classVar.varType:
varType: str = Utils.ReplaceIDATypes(classVar.varType.fullName)
2025-03-09 03:04:02 +02:00
varType = Utils.CleanType(varType)
2025-03-09 01:10:59 +02:00
if varType:
varType += " "
varType = "GAME_IMPORT " + varType
else:
varType: str = ""
classVarSig: str = f"{varType}{classVar.varName}"
classVarLines: list[str] = []
if access:
classVarLines.append(access)
if classVarSig:
classVarLines.append(f"\t{classVarSig};")
return classVarLines
2025-03-09 01:10:59 +02:00
def GenerateClassFuncCode(func: ParsedFunction, vtFuncIndex: int = 0) -> list[str]:
2025-03-09 01:10:59 +02:00
"""Generate code for a single class method."""
global currentAccess
access: str = f"{func.access}:" if func.access else ""
2025-03-09 01:10:59 +02:00
if currentAccess == func.access:
access = ""
2025-03-09 01:10:59 +02:00
else:
currentAccess = func.access
const: str = " const" if func.const else ""
stripped_vfunc: str = " = 0" if func.type == "stripped_vfunc" else ""
if func.returnType:
returnType: str = Utils.ReplaceIDATypes(func.returnType.fullName)
2025-03-09 03:04:02 +02:00
returnType = Utils.CleanType(returnType)
2025-03-09 01:10:59 +02:00
if returnType:
if func.type == "basic_vfunc":
returnType = returnType.removeprefix("virtual").strip()
else:
returnType += " "
else:
returnType: str = ""
if func.type != "stripped_vfunc" and func.type != "basic_vfunc":
returnType = "GAME_IMPORT " + returnType
if func.params:
2025-03-09 03:04:02 +02:00
paramsList: list[str] = []
for param in func.params:
paramsList.append(param.fullName)
params: str = Utils.ReplaceIDATypes(", ".join(paramsList))
params = Utils.CleanType(params)
2025-03-09 01:10:59 +02:00
if params == "void":
params = ""
else:
params: str = ""
targetParams: str = ""
if func.type == "basic_vfunc":
2025-03-17 02:09:24 +02:00
targetParams = ClassGen.ExtractParamNames(params)
2025-03-09 01:10:59 +02:00
targetParams = ", " + targetParams if targetParams else ""
funcSig: str = f"{returnType}{func.funcName}({params}){const}{stripped_vfunc}" if func.type != "basic_vfunc" else f"VIRTUAL_CALL({vtFuncIndex}, {returnType}, {func.funcName}, ({params}){targetParams})"
classFuncLines: list[str] = []
if access:
classFuncLines.append(access)
if funcSig:
classFuncLines.append(f"\t{funcSig};")
return classFuncLines
2025-03-09 01:10:59 +02:00
2025-03-09 03:04:02 +02:00
def GenerateClassContent(allParsedElements: tuple[list[ParsedClassVar], list[ParsedFunction], list[ParsedFunction]]) -> list[str]:
2025-03-09 01:10:59 +02:00
"""
Generate the content to be inserted into an existing header file.
This is just the class members, not the full header with includes, etc.
"""
2025-03-09 03:04:02 +02:00
global currentAccess
2025-03-09 01:10:59 +02:00
2025-03-09 03:04:02 +02:00
parsedVars, parsedVtFuncs, parsedFuncs = allParsedElements
2025-03-09 01:10:59 +02:00
if not parsedVars and not parsedVtFuncs and not parsedFuncs:
2025-03-09 03:04:02 +02:00
return []
2025-03-09 01:10:59 +02:00
2025-03-09 03:04:02 +02:00
firstVarOrFuncAccess = ""
2025-03-09 01:10:59 +02:00
currentAccess = ""
# Generate class content (just the members)
contentLines = [f"#pragma region GENERATED by ExportClassToCPPH.py"]
# Add class variables
for classVar in parsedVars:
if not firstVarOrFuncAccess:
firstVarOrFuncAccess = classVar.access
contentLines.extend(GenerateClassVarCode(classVar))
2025-03-09 01:10:59 +02:00
# Add newline between sections if both exist
if parsedVars and (parsedVtFuncs or parsedFuncs):
contentLines.append("")
# Add vtable functions
for index, vTableFunc in enumerate(parsedVtFuncs):
if not firstVarOrFuncAccess:
firstVarOrFuncAccess = vTableFunc.access
contentLines.extend(GenerateClassFuncCode(vTableFunc, index))
2025-03-09 01:10:59 +02:00
# Add newline between sections if both exist
if parsedVtFuncs and parsedFuncs:
contentLines.append("")
# Add regular functions
for func in parsedFuncs:
if not firstVarOrFuncAccess:
firstVarOrFuncAccess = func.access
contentLines.extend(GenerateClassFuncCode(func))
2025-03-09 01:10:59 +02:00
contentLines.append("#pragma endregion")
# Insert access specifier if needed
if not firstVarOrFuncAccess:
2025-03-09 03:04:02 +02:00
contentLines.insert(1, "public:")
2025-03-09 01:10:59 +02:00
2025-03-09 03:04:02 +02:00
return contentLines
2025-03-09 01:10:59 +02:00
def GenerateClassDefinition(targetClass: ClassName, forwardDeclare: bool = False) -> list[str]:
2025-03-09 01:10:59 +02:00
"""Generate a class definition from a list of methods."""
classDefLines: list[str] = [f"{targetClass.type} {targetClass.name}{' {' if not forwardDeclare else ';'}"]
if not forwardDeclare:
if targetClass.type == "class":
classDefLines.append("public:")
classDefLines.append("};")
return classDefLines
def GenerateHeaderCode(targetClass: ClassName) -> list[str]:
"""Generate header code for a standard class (not nested)."""
# Reset processed classes for this generation
global processedClasses, forwardDeclarations
processedClasses = set()
forwardDeclarations = set()
2025-03-09 01:10:59 +02:00
# Mark target class as processed to avoid self-dependency issues
processedClasses.add(targetClass.namespacedClassedName)
2025-03-09 01:10:59 +02:00
# Get all parsed elements for the target class
2025-03-17 02:09:24 +02:00
allParsedElements = ClassGen.GetAllParsedClassVarsAndFuncs(targetClass)
# Generate the target class definition
classContent = GenerateClassContent(allParsedElements)
classDefinition = GenerateClassDefinition(targetClass)
2025-03-09 01:10:59 +02:00
fullClassDefinition = classDefinition
if (classContent):
fullClassDefinition = fullClassDefinition[:len(classDefinition) - 1] + classContent + fullClassDefinition[len(classDefinition) - 1:]
2025-03-09 03:04:02 +02:00
# Wrap target class in namespace blocks if needed
2025-03-09 01:10:59 +02:00
if targetClass.namespaces:
namespaceCode = []
indentLevel = ""
# Opening namespace blocks with increasing indentation
for namespace in targetClass.namespaces:
namespaceCode.append(f"{indentLevel}namespace {namespace} {{")
indentLevel += "\t"
for cls in targetClass.classes:
2025-03-17 02:09:24 +02:00
clsType: str = ClassGen.GetClassTypeFromParsedSigs(ClassName(targetClass.namespacedClassedName), allParsedElements)
namespaceCode.append(f"{indentLevel}{clsType if clsType else 'class'} {cls} {{")
indentLevel += "\t"
2025-03-09 01:10:59 +02:00
indentedClassDefinition = [f"{indentLevel if '#pragma' not in line else ''}{line}" for line in fullClassDefinition]
2025-03-09 03:04:02 +02:00
namespaceCode.extend(indentedClassDefinition)
2025-03-09 01:10:59 +02:00
for cls in reversed(targetClass.classes):
indentLevel = indentLevel[:-1] # Remove one level of indentation
namespaceCode.append(f"{indentLevel}}}")
2025-03-09 01:10:59 +02:00
for namespace in reversed(targetClass.namespaces):
indentLevel = indentLevel[:-1] # Remove one level of indentation
namespaceCode.append(f"{indentLevel}}}")
fullClassDefinition = namespaceCode
2025-03-09 01:10:59 +02:00
# Combine all parts of the header
2025-03-09 03:04:02 +02:00
headerParts = ["#pragma once", r"#include <EGSDK\Imports.h>", ""]
# Add the target class definition
headerParts.extend(fullClassDefinition)
2025-03-09 01:10:59 +02:00
2025-03-09 03:04:02 +02:00
return headerParts
2025-03-09 01:10:59 +02:00
2025-03-09 03:04:02 +02:00
def WriteHeaderToFile(targetClass: ClassName, headerCode: str, fileName: str) -> bool:
2025-03-09 01:10:59 +02:00
"""Write the generated header code to a file."""
2025-03-09 03:04:02 +02:00
outputFolderPath: str = Config.HEADER_OUTPUT_PATH
2025-03-09 01:10:59 +02:00
if targetClass.namespaces:
# Create folder structure for namespaces
classFolderPath: str = os.path.join(*targetClass.namespaces)
2025-03-09 03:04:02 +02:00
outputFolderPath: str = os.path.join(outputFolderPath, classFolderPath)
2025-03-09 01:10:59 +02:00
# Create directory if it doesn't exist
os.makedirs(outputFolderPath, exist_ok=True)
# Output file path is inside the namespace folder
2025-03-09 03:04:02 +02:00
outputFilePath: str = os.path.join(classFolderPath, fileName)
2025-03-09 01:10:59 +02:00
else:
2025-03-09 03:04:02 +02:00
# Create directory if it doesn't exist
os.makedirs(outputFolderPath, exist_ok=True)
2025-03-09 01:10:59 +02:00
# No namespace, just save in current directory
2025-03-09 03:04:02 +02:00
outputFilePath: str = fileName
2025-03-09 01:10:59 +02:00
2025-03-09 03:04:02 +02:00
outputFilePath = os.path.join(Config.HEADER_OUTPUT_PATH, outputFilePath)
2025-03-09 01:10:59 +02:00
try:
with open(outputFilePath, 'w') as headerFile:
headerFile.write(headerCode)
print(f"Header file '{outputFilePath}' created successfully.")
return True
except Exception as e:
print(f"Error writing header file '{outputFilePath}': {e}")
return False
def ExportClassHeader(targetClass: ClassName):
"""
Generate and save a C++ header file for the target class.
Handles multiple levels of nested classes and also generates dependencies.
2025-03-09 03:04:02 +02:00
"""
global processedClasses
# Skip if we've already processed this class
if targetClass.namespacedClassedName in processedClasses:
print(f"Already processed class {targetClass.namespacedClassedName}, skipping.")
return
2025-03-09 01:10:59 +02:00
# Add to processed set to prevent infinite recursion
processedClasses.add(targetClass.namespacedClassedName)
2025-03-09 01:10:59 +02:00
# Generate the header code
headerCodeLines = GenerateHeaderCode(targetClass)
2025-03-09 03:04:02 +02:00
if not headerCodeLines:
print(f"No functions were found for class {targetClass.namespacedClassedName}, therefore will not generate.")
2025-03-09 01:10:59 +02:00
return
2025-03-09 03:04:02 +02:00
headerCode: str = "\n".join(headerCodeLines)
2025-03-09 01:10:59 +02:00
2025-03-09 03:04:02 +02:00
WriteHeaderToFile(targetClass, headerCode, f"{targetClass.name}.h")