mirror of https://github.com/zeldaret/mm.git
				
				
				
			
		
			
				
	
	
		
			415 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			415 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
| #!/usr/bin/env python3
 | |
| #
 | |
| # Generates graphs for actor overlay files
 | |
| #
 | |
| 
 | |
| import argparse, os, re, sys
 | |
| from configparser import ConfigParser
 | |
| 
 | |
| try:
 | |
|     from graphviz import Digraph
 | |
| except ModuleNotFoundError:
 | |
|     print("Module 'graphviz' is not installed", file=sys.stderr)
 | |
|     print("You can install it using: pip3 install graphviz", file=sys.stderr)
 | |
|     print("You may also need to install it on your system", file=sys.stderr)
 | |
|     print("On Debian/Ubuntu derivates you can use: apt install graphviz", file=sys.stderr)
 | |
|     sys.exit(1)
 | |
| 
 | |
| script_dir = os.path.dirname(os.path.realpath(__file__))
 | |
| config = ConfigParser()
 | |
| 
 | |
| func_names = None
 | |
| func_definitions = list()
 | |
| line_numbers_of_functions = list()
 | |
| 
 | |
| # Make actor source file path from actor name
 | |
| def actor_src_path(name):
 | |
|     filename = "src/overlays/actors/ovl_"
 | |
|     if name != "player":
 | |
|         filename += name
 | |
|     else:
 | |
|         filename += name + "_actor"
 | |
|     filename += "/z_" + name.lower() + ".c"
 | |
| 
 | |
|     return filename
 | |
| 
 | |
| func_call_regexpr = re.compile(r'[a-zA-Z_\d]+\([^\)]*\)(\.[^\)]*\))?')
 | |
| func_defs_regexpr = re.compile(r'[a-zA-Z_\d]+\([^\)]*\)(\.[^\)]*\))? {[^}]')
 | |
| macrosRegexpr = re.compile(r'#define\s+([a-zA-Z_\d]+)(\([a-zA-Z_\d]+\))?\s+(.+?)(\n|//|/\*)')
 | |
| enumsRegexpr = re.compile(r'enum\s+\{([^\}]+?)\}')
 | |
| 
 | |
| # Capture all function calls in the block, including arguments
 | |
| def capture_calls(content):
 | |
|     return [x.group() for x in re.finditer(func_call_regexpr, content)]
 | |
| 
 | |
| # Capture all function calls in the block, name only
 | |
| def capture_call_names(content):
 | |
|     return [x.group().split("(")[0] for x in re.finditer(func_call_regexpr, content)]
 | |
| 
 | |
| # Capture all function definitions in the block, including arguments
 | |
| def capture_definitions(content):
 | |
|     return [x.group() for x in re.finditer(func_defs_regexpr, content)]
 | |
| 
 | |
| # Capture all function definitions in the block, name only
 | |
| def capture_definition_names(content):
 | |
|     definitions = []
 | |
|     for x in re.finditer(func_defs_regexpr, content):
 | |
|         definitions.append(x.group().split("(")[0])
 | |
|     return definitions
 | |
| 
 | |
| setupaction_regexpr = re.compile(r"_SetupAction+\([^\)]*\)(\.[^\)]*\))?;")
 | |
| 
 | |
| # Capture all calls to the setupaction function
 | |
| def capture_setupaction_calls(content):
 | |
|     return [x.group() for x in re.finditer(setupaction_regexpr, content)]
 | |
| 
 | |
| # Captures the function name of a setupaction call
 | |
| def capture_setupaction_call_arg(content):
 | |
|     transitionList = []
 | |
|     for x in re.finditer(setupaction_regexpr, content):
 | |
|         func = x.group().split(",")[1].strip().split(");")[0].strip()
 | |
|         if func not in transitionList:
 | |
|             transitionList.append(func)
 | |
|     return transitionList
 | |
| 
 | |
| # Search for the function definition by supplied function name
 | |
| def definition_by_name(content, name):
 | |
|     for definition in capture_definitions(content):
 | |
|         if name == definition.split("(")[0]:
 | |
|             return definition.split("{")[0].strip()
 | |
| 
 | |
| # Obtain the entire code body of the function
 | |
| def get_code_body(content, funcname) -> str:
 | |
|     line_num = line_numbers_of_functions[index_of_func(funcname)]
 | |
|     if line_num <= 0:
 | |
|         return ""
 | |
|     code = ""
 | |
|     bracket_count = 1
 | |
| 
 | |
|     all_lines = content.splitlines(True)
 | |
|     for raw_line in all_lines[line_num:len(all_lines)]:
 | |
|         # Ignore commented stuff
 | |
|         if "//" in raw_line:
 | |
|             raw_line = raw_line[:raw_line.index("//")]
 | |
| 
 | |
|         bracket_count += raw_line.count("{")
 | |
|         bracket_count -= raw_line.count("}")
 | |
| 
 | |
|         if bracket_count == 0:
 | |
|             break
 | |
|         else:
 | |
|             code += raw_line
 | |
|     return code
 | |
| 
 | |
| def getMacrosDefinitions(contents):
 | |
|     macrosDefs = dict()
 | |
|     for x in re.finditer(macrosRegexpr, contents):
 | |
|         macroName = x.group(1).strip()
 | |
|         macroParamsAux = x.group(2)
 | |
|         macroBody = x.group(3).strip()
 | |
| 
 | |
|         macroParams = []
 | |
|         if macroParamsAux is not None:
 | |
|             for x in macroParamsAux.strip("(").strip(")").split(","):
 | |
|                 macroParams.append(x.strip())
 | |
|         macrosDefs[macroName] = (macroParams, macroBody)
 | |
|     return macrosDefs
 | |
| 
 | |
| def parseMacro(macros, macroExpr):
 | |
|     macroCall = func_call_regexpr.match(macroExpr)
 | |
|     if macroCall is not None:
 | |
|         macroName, macroArgs = macroCall.group().split(")")[0].split("(")
 | |
|         if macroName not in macros:
 | |
|             print("Unknown macro: " + macroName)
 | |
|             return None
 | |
|         macroParams, macroBody = macros[macroName]
 | |
|         argsList = [x.strip() for x in macroArgs.split(",")]
 | |
| 
 | |
|         macroBody = str(macroBody)
 | |
|         for i, x in enumerate(macroParams):
 | |
|             macroBody = macroBody.replace(x, argsList[i])
 | |
|         return str(eval(macroBody))
 | |
|     elif macroExpr in macros:
 | |
|         macroParams, macroBody = macros[macroExpr]
 | |
|         return str(eval(macroBody))
 | |
|     return None
 | |
| 
 | |
| def getEnums(contents):
 | |
|     enums = dict()
 | |
|     for x in re.finditer(enumsRegexpr, contents):
 | |
|         enumValue = 0
 | |
|         for var in x.group(1).split(","):
 | |
|             if "/*" in var and "*/" in var:
 | |
|                 start = var.index("/*")
 | |
|                 end = var.index("*/") + len("*/")
 | |
|                 var = var[:start] + var[end:]
 | |
|             if "//" in var:
 | |
|                 var = var[:var.index("//")]
 | |
|             var = var.strip()
 | |
|             if len(var) == 0:
 | |
|                 continue
 | |
| 
 | |
|             enumName = var
 | |
|             exprList = var.split("=")
 | |
|             if len(exprList) > 1:
 | |
|                 enumName = exprList[0].strip()
 | |
|                 enumValue = int(exprList[1], 0)
 | |
| 
 | |
|             enums[enumName] = enumValue
 | |
|             enumValue +=1
 | |
| 
 | |
|     return enums
 | |
| 
 | |
| def index_of_func(func_name):
 | |
|     return func_names.index(func_name)
 | |
| 
 | |
| def action_var_setups_in_func(content, func_name, action_var):
 | |
|     code_body = get_code_body(content, func_name)
 | |
|     if action_var not in code_body:
 | |
|         return None
 | |
|     return [x.group() for x in re.finditer(r'(' + action_var + r' = (.)*)', code_body)]
 | |
| 
 | |
| def action_var_values_in_func(code_body, action_var, macros, enums):
 | |
|     if action_var not in code_body:
 | |
|         return list()
 | |
| 
 | |
|     regex = re.compile(r'(' + action_var + r' = (.)*)')
 | |
|     transition = []
 | |
|     for x in re.finditer(regex, code_body):
 | |
|         index = x.group().split(" = ")[1].split(";")[0].strip()
 | |
| 
 | |
|         macroValue = parseMacro(macros, index)
 | |
|         if macroValue is not None:
 | |
|             index = macroValue
 | |
|         elif index in enums:
 | |
|             index = str(enums[index])
 | |
| 
 | |
|         if index not in transition:
 | |
|             transition.append(index)
 | |
|     return transition
 | |
| 
 | |
| def setup_line_numbers(content, func_names):
 | |
|     global line_numbers_of_functions
 | |
|     for line_no, line in enumerate(content.splitlines(True),1):
 | |
|         for func_name in func_names:
 | |
|             # Some functions have definitions on multiple lines, take the last
 | |
|             if func_definitions[index_of_func(func_name)].split("\n")[-1] in line:
 | |
|                 line_numbers_of_functions.append(line_no)
 | |
| 
 | |
| def setup_func_definitions(content, func_names):
 | |
|     global func_definitions
 | |
|     for func_name in func_names:
 | |
|         func_definitions.append(definition_by_name(content, func_name)+" {")
 | |
| 
 | |
| 
 | |
| def addFunctionTransitionToGraph(dot, index: int, func_name: str, action_transition: str):
 | |
|     fontColor = config.get("colors", "fontcolor")
 | |
|     bubbleColor = config.get("colors", "bubbleColor")
 | |
|     indexStr = str(index)
 | |
|     funcIndex = str(index_of_func(action_transition))
 | |
| 
 | |
|     dot.node(indexStr, func_name, fontcolor=fontColor, color=bubbleColor)
 | |
|     dot.node(funcIndex, action_transition, fontcolor=fontColor, color=bubbleColor)
 | |
|     edgeColor = config.get("colors", "actionFunc")
 | |
|     if func_name.endswith("_Init"):
 | |
|         edgeColor = config.get("colors", "actionFuncInit")
 | |
|     dot.edge(indexStr, funcIndex, color=edgeColor)
 | |
| 
 | |
| def addCallNamesToGraph(dot, func_names: list, index: int, code_body: str, removeList: list, setupAction=False, rawActorFunc=False):
 | |
|     edgeColor = config.get("colors", "funcCall")
 | |
|     fontColor = config.get("colors", "fontcolor")
 | |
|     bubbleColor = config.get("colors", "bubbleColor")
 | |
| 
 | |
|     indexStr = str(index)
 | |
|     seen = set()
 | |
|     for call in capture_call_names(code_body):
 | |
|         if call not in func_names:
 | |
|             continue
 | |
|         if call in seen:
 | |
|             continue
 | |
|         if call in removeList:
 | |
|             continue
 | |
| 
 | |
|         if setupAction and "_SetupAction" in call:
 | |
|             continue
 | |
|         seen.add(call)
 | |
| 
 | |
|         if rawActorFunc:
 | |
|             dot.node(indexStr, func_names[index], fontcolor=fontColor, color=bubbleColor)
 | |
| 
 | |
|         calledFuncIndex = str(index_of_func(call))
 | |
| 
 | |
|         dot.node(calledFuncIndex, call, fontcolor=fontColor, color=bubbleColor)
 | |
|         dot.edge(indexStr, calledFuncIndex, color=edgeColor)
 | |
| 
 | |
| def addCallbacksToGraph(dot, func_names: list, index: int, code_body: str, transitionList: list):
 | |
|     edgeColor = config.get("colors", "callback")
 | |
|     fontColor = config.get("colors", "fontcolor")
 | |
|     bubbleColor = config.get("colors", "bubbleColor")
 | |
| 
 | |
|     indexStr = str(index)
 | |
|     seen = set()
 | |
|     for call_with_arguments in capture_calls(code_body):
 | |
|         call_with_arguments = call_with_arguments.replace("\n", "").replace(" ", "")
 | |
|         name, arguments = call_with_arguments.split("(", 1)
 | |
|         argumentList = [x.strip() for x in arguments.split(",")]
 | |
|         for callback in [x for x in func_names if x in argumentList]:
 | |
|             if callback in transitionList:
 | |
|                 # already catched in another edge
 | |
|                 continue
 | |
|             seen.add(callback)
 | |
| 
 | |
|             calledFuncIndex = str(index_of_func(callback))
 | |
| 
 | |
|             dot.node(calledFuncIndex, callback, fontcolor=fontColor, color=bubbleColor)
 | |
|             dot.edge(indexStr, calledFuncIndex, color=edgeColor)
 | |
| 
 | |
| def loadConfigFile(selectedStyle):
 | |
|     # For a list of colors, see https://www.graphviz.org/doc/info/colors.html
 | |
|     # Hex colors works too!
 | |
|     stylesFolder = os.path.join(script_dir, "graphovl_styles")
 | |
|     configFilename = os.path.join(stylesFolder, "graphovl_config.ini")
 | |
| 
 | |
|     # Set defaults, just in case.
 | |
|     config.add_section('colors')
 | |
|     config.set('colors', 'background', 'white')
 | |
|     config.set('colors', 'funcCall', 'blue')
 | |
|     config.set('colors', 'actionFuncInit', 'green')
 | |
|     config.set('colors', 'actionFunc', 'Black')
 | |
|     config.set('colors', 'fontColor', 'Black')
 | |
|     config.set('colors', 'bubbleColor', 'Black')
 | |
|     config.set('colors', 'callback', 'blue')
 | |
| 
 | |
|     if os.path.exists(configFilename):
 | |
|         config.read(configFilename)
 | |
|     else:
 | |
|         print("Warning! Config file not found.")
 | |
| 
 | |
|     style = config.get("config", "defaultStyle") + ".ini"
 | |
|     if selectedStyle is not None:
 | |
|         style = selectedStyle + ".ini"
 | |
|     styleFilename = os.path.join(stylesFolder, style)
 | |
| 
 | |
|     if os.path.exists(styleFilename):
 | |
|         config.read(styleFilename)
 | |
|     else:
 | |
|         print(f"Warning! Style {style} not found.")
 | |
| 
 | |
| 
 | |
| def main():
 | |
|     global func_names
 | |
|     parser = argparse.ArgumentParser(description="Creates a graph of action functions (black and green arrows) and function calls (blue arrows) for a given overlay file")
 | |
|     parser.add_argument("filename", help="Filename without the z_ or ovl_ prefix, e.x. Door_Ana")
 | |
|     parser.add_argument("--loners", help="Include functions that are not called or call any other overlay function", action="store_true")
 | |
|     parser.add_argument("-s", "--style", help="Use a color style defined in graphovl_styles folder. i.e. solarized")
 | |
|     parser.add_argument("--format", help="Select output file format. Defaults to 'png'", default="png")
 | |
|     parser.add_argument("-r", "--remove", help="A space-separated list of nodes to remove from the graph", nargs='+')
 | |
|     args = parser.parse_args()
 | |
| 
 | |
|     removeList = []
 | |
|     if args.remove is not None:
 | |
|         removeList = args.remove
 | |
| 
 | |
|     loadConfigFile(args.style)
 | |
|     fontColor = config.get("colors", "fontcolor")
 | |
|     bubbleColor = config.get("colors", "bubbleColor")
 | |
| 
 | |
|     fname = args.filename
 | |
|     dot = Digraph(comment = fname, format = args.format)
 | |
|     dot.attr(bgcolor=config.get("colors", "background"))
 | |
|     contents = ""
 | |
| 
 | |
|     with open(actor_src_path(fname), "r") as infile:
 | |
|         contents = infile.read()
 | |
| 
 | |
|     func_names = capture_definition_names(contents)
 | |
|     setup_func_definitions(contents, func_names)
 | |
|     setup_line_numbers(contents, func_names)
 | |
|     macros = getMacrosDefinitions(contents)
 | |
|     enums = getEnums(contents)
 | |
|     func_prefix = ""
 | |
|     for index, func_name in enumerate(func_names):
 | |
|         # Init is chosen because all actors are guaranteed to have an Init function.
 | |
|         # This check is however required as not all functions may have been renamed yet.
 | |
|         if func_name.endswith("_Init"): 
 | |
|             func_prefix = func_name.split("_")[0]
 | |
|             dot.node(str(index), func_name, fontcolor=fontColor, color=bubbleColor)
 | |
|         elif (func_name.endswith("_Destroy") or func_name.endswith("_Update") or func_name.endswith("_Draw")):
 | |
|             dot.node(str(index), func_name, fontcolor=fontColor, color=bubbleColor)
 | |
| 
 | |
|     action_func_type = func_prefix + "ActionFunc"
 | |
|     match_obj = re.search(re.compile(action_func_type + r' (.+)\[\] = {'), contents)
 | |
|     actionIdentifier = "this->actionFunc"
 | |
| 
 | |
|     setupAction = func_prefix + "_SetupAction" in func_names
 | |
|     arrayActorFunc = match_obj is not None
 | |
|     rawActorFunc = actionIdentifier in contents
 | |
| 
 | |
|     if not setupAction and not arrayActorFunc and not rawActorFunc:
 | |
|         print("No actor action-based structure found")
 | |
|         os._exit(1)
 | |
| 
 | |
|     action_functions = []
 | |
|     action_var = ""
 | |
|     if arrayActorFunc:
 | |
|         action_func_array = re.search(action_func_type + r' (.+)\[\] = \{([^}]*?)\};', contents)
 | |
|         if action_func_array is None:
 | |
|             print("Invalid array-based actor.")
 | |
|             print("Call action func array not found.")
 | |
|             os._exit(1)
 | |
|         actionFuncArrayElements = action_func_array.group(2).split(",")
 | |
|         action_functions = [x.strip() for x in actionFuncArrayElements]
 | |
| 
 | |
|         action_func_array_name = match_obj.group(1).strip()
 | |
|         actionVarMatch = re.search(action_func_array_name + r'\[(.*)\]\(', contents)
 | |
|         if actionVarMatch is None:
 | |
|             print("Invalid ActorFunc array-based actor.")
 | |
|             print("Call to array function not found.")
 | |
|             os._exit(1)
 | |
|         action_var = actionVarMatch.group(1).strip()
 | |
| 
 | |
|     for index, func_name in enumerate(func_names):
 | |
|         if func_name in removeList:
 | |
|             continue
 | |
| 
 | |
|         indexStr = str(index)
 | |
|         if args.loners:
 | |
|             dot.node(indexStr, func_name, fontcolor=fontColor, color=bubbleColor)
 | |
|         code_body = get_code_body(contents, func_name)
 | |
| 
 | |
|         transitionList = []
 | |
|         if setupAction:
 | |
|             """
 | |
|             Create all edges for SetupAction-based actors
 | |
|             """
 | |
|             transitionList = capture_setupaction_call_arg(code_body)
 | |
|         elif arrayActorFunc:
 | |
|             """
 | |
|             Create all edges for ActorFunc array-based actors
 | |
|             """
 | |
|             transitionIndexes = action_var_values_in_func(code_body, action_var, macros, enums)
 | |
|             transitionList = [action_functions[int(index, 0)] for index in transitionIndexes]
 | |
|         elif rawActorFunc:
 | |
|             """
 | |
|             Create all edges for raw ActorFunc-based actors
 | |
|             """
 | |
|             transitionList = action_var_values_in_func(code_body, actionIdentifier, macros, enums)
 | |
| 
 | |
|         # Remove functions calls
 | |
|         transitionList = [x for x in transitionList if x not in removeList]
 | |
| 
 | |
|         for action_transition in transitionList:
 | |
|             addFunctionTransitionToGraph(dot, index, func_name, action_transition)
 | |
| 
 | |
|         addCallNamesToGraph(dot, func_names, index, code_body, removeList, setupAction, rawActorFunc)
 | |
| 
 | |
|         addCallbacksToGraph(dot, func_names, index, code_body, transitionList)
 | |
| 
 | |
|     # print(dot.source)
 | |
|     outname = f"graphs/{fname}.gv"
 | |
|     dot.render(outname)
 | |
|     print(f"Written to {outname}.{args.format}")
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     main()
 |