#!/usr/bin/env python3 # Persona2EPEventCompiler # Author: SergeyShemet # Version: 1.0 import sys import json import re import os def main(): # Check input parameters if len(sys.argv) < 2: print("Usage: event_compiler.py input_file [output_file]") return input_file = sys.argv[1] # Set output filename if len(sys.argv) >= 3: output_file = sys.argv[2] else: output_file = input_file + ".TRANSL" # Read script lines with open(input_file, 'r', encoding='utf-8') as f: script_lines = f.readlines() # Load command codes from JSON with open('ep_scr_cmd.json', 'r') as f: cmd_codes_list = json.load(f) # Convert command codes to dictionary cmd_codes = {} for item in cmd_codes_list: for key, value in item.items(): cmd_codes[key] = int(value, 16) # Initialize arrays cmds = [] # Will store dict objects subs = [] # Subroutines {name: cpuAddr} branches = [] # Branches {name: cpuAddr} text = bytearray() # Text data outputparams = [] # Final parameters outputfile = bytearray() # Output file tempBr1 = tempBr2 = tempBr3 = 0 cpu = 0 # Command counter # FIRST PASS - Parse script for line in script_lines: line = line.rstrip('\n') line_trimmed = line.strip() # Skip empty lines and comments if not line_trimmed or line_trimmed.startswith('//'): continue # Check for subroutine if line_trimmed.endswith(':'): name = line_trimmed[:-1].strip() subs.append({'name': name, 'addr': cpu}) continue # Check for branch if line_trimmed.startswith('&'): name = line_trimmed[1:].strip() branches.append({'name': name, 'addr': cpu}) continue # Parse command match = re.match(r'^(\S+)\s+(.*)$', line_trimmed) if match: cmd_name = match.group(1) cmd_data = match.group(2) cmd_obj = { 'cpu': cpu, 'cpuCmd': cmd_name, 'scriptLine': line_trimmed, 'params': [] } # Check for TextShow command if 'TextShow' in cmd_name: cmd_obj['text'] = cmd_data else: # Split params by whitespace params = cmd_data.split() cmd_obj['params'] = params cmds.append(cmd_obj) cpu += 1 else: cmd_obj = { 'cpu': cpu, 'cpuCmd': line_trimmed, 'scriptLine': line_trimmed, 'params': [] } cmds.append(cmd_obj) cpu += 1 # SECOND PASS - Link and parse command codes for cmd in cmds: # Handle special markers if '!' in cmd['cpuCmd']: tempBr1 = cmd['cpu'] cmd['cpuCmd'] = cmd['cpuCmd'].replace('!', '') if '@' in cmd['cpuCmd']: tempBr2 = cmd['cpu'] cmd['cpuCmd'] = cmd['cpuCmd'].replace('@', '') if '#' in cmd['cpuCmd']: tempBr3 = cmd['cpu'] cmd['cpuCmd'] = cmd['cpuCmd'].replace('#', '') # Parse command code if cmd['cpuCmd'] in cmd_codes: cmd['cpuCode'] = cmd_codes[cmd['cpuCmd']] else: try: cmd['cpuCode'] = int(cmd['cpuCmd'], 16) except: print(f"Command not recognized: {cmd['scriptLine']}") cmd['cpuCode'] = 0 if cmd['cpuCmd'] == 'TextShow': text_start = len(text) text_data = cmd['text'] i = 0 text_len = len(text_data) while i < text_len: if text_data[i] == '[': j = text_data.find(']', i + 1) if j != -1: hex_str = text_data[i+1:j].replace(' ', '') hex_bytes = bytes.fromhex(hex_str) # hex_bytes = hex_bytes[::-1] text.extend(hex_bytes) i = j + 1 continue if text_data[i] == '\t': text.extend([0x31, 0x11]) i += 1 continue # Accumulate regular text (including Russian) accum = bytearray() while i < text_len and text_data[i] != '[' and text_data[i] != '\t': # Encode in cp1251 accum.extend(text_data[i].encode('cp1251')) i += 1 if len(accum) > 0: text.append(len(accum)) text.append(0x20) text.extend(accum) if len(text) & 1: text.append(0) cmd['params'] = [text_start] else: # Parse parameters for i, param in enumerate(cmd['params']): # Handle special markers if param == '!': cmd['params'][i] = tempBr1 elif param == '@': cmd['params'][i] = tempBr2 elif param == '#': cmd['params'][i] = tempBr3 else: # Try to parse as hex try: # if param.startswith('0x'): # cmd['params'][i] = int(param, 16) # else: # # Try to parse as decimal cmd['params'][i] = int(param, 16) except: # Look in branches found = False for branch in branches: if branch['name'] == param: cmd['params'][i] = branch['addr'] found = True break if not found: # Look in subs (store index, not address) for sub_idx, sub in enumerate(subs): if sub['name'] == param: cmd['params'][i] = sub_idx found = True break if not found: print(f"Could not parse parameter '{param}' in command: {cmd['scriptLine']}") cmd['params'][i] = 0 # THIRD PASS - Build output file # Header subroutines_offset = 0x18 subs_count = len(subs) cmd_offset = subs_count * 72 + 0x18 params_offset = cmd_offset + len(cmds) * 8 # Write header outputfile.extend(subroutines_offset.to_bytes(4, 'little')) # subroutines offset outputfile.extend(subroutines_offset.to_bytes(4, 'little')) # again outputfile.extend(subs_count.to_bytes(4, 'little')) # subs count outputfile.extend(cmd_offset.to_bytes(4, 'little')) # cmd offset outputfile.extend(params_offset.to_bytes(4, 'little')) # params offset outputfile.extend(b'\x00\x00\x00\x00') # text offset (placeholder) # Write subroutines for sub in subs: # Name (64 bytes) name_bytes = sub['name'].encode('ascii') outputfile.extend(name_bytes) outputfile.extend(b'\x00' * (64 - len(name_bytes))) # cpuAddr outputfile.extend(sub['addr'].to_bytes(4, 'little')) # Padding 0 outputfile.extend((0).to_bytes(4, 'little')) # Write commands params_offset_current = params_offset for cmd in cmds: print(cmd) # Command code outputfile.extend(cmd['cpuCode'].to_bytes(4, 'little')) # Params offset outputfile.extend(params_offset_current.to_bytes(4, 'little')) # Add params to outputparams for param in cmd['params']: if isinstance(param, str): try: param_val = int(param) except: param_val = 0 else: param_val = param outputparams.append(param_val) params_offset_current += 4 # Write outputparams for param in outputparams: outputfile.extend(param.to_bytes(4, 'little')) # Write text text_offset = len(outputfile) outputfile.extend(text) # Update text offset in header outputfile[0x14:0x18] = text_offset.to_bytes(4, 'little') # Write to file with open(output_file, 'wb') as f: f.write(outputfile) print(f"Compilation completed successfully!") print(f"Output file: {output_file}") if __name__ == "__main__": main()