Advertisement
Guest User

Readable ECL script v.1.01

a guest
Nov 14th, 2017
190
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 18.15 KB | None | 0 0
  1. #!/usr/bin/python3
  2.  
  3. # Converts .txt files from thecl.exe into readable format
  4.  
  5. import json
  6. import codecs
  7. import binascii
  8. import struct
  9. import sys
  10. import subprocess
  11. import os
  12. import random
  13. import string
  14.  
  15. VERSION = 1.01
  16. VERSION_TEXT = "readable_ecl.py " + str(VERSION)
  17.  
  18. usageText = []
  19. usageText.append("Usage: " + sys.argv[0] + " COMMAND[OPTION] INPUT OUTPUT")
  20. usageText.append("COMMAND can be:")
  21. usageText.append("  d  decode ECL dump")
  22. usageText.append("  D  mass-decode ECL dump(s); stores output in INPUT.DECODED")
  23. usageText.append("  e  encode readable ECL")
  24. usageText.append("  E  mass-encode readable ECL(s); stores output in INPUT.ENCODED")
  25. usageText.append("  u  update readable ECL")
  26. usageText.append("  r  run reversibility check on ECL dump(s)")
  27. usageText.append("  V  display version information and exit")
  28. usageText.append("Commands D, E and r don't take OUTPUT argument, but can take multiple INPUTs")
  29. usageText.append("OPTION can be:")
  30. usageText.append("  #  # can be 6 or 7")
  31.  
  32. def printUsageAndExit():
  33.     print("\n".join(usageText))
  34.     exit()
  35.  
  36. if (len(sys.argv) == 1 or (len(sys.argv) != 4 and (sys.argv[1][0] == "d" or sys.argv[1][0] == "e" or sys.argv[1][0] == "u"))):
  37.     printUsageAndExit()
  38.  
  39. updating = False
  40. reverseChecking = False
  41. batchJob = False
  42. defineFilenames = True
  43.  
  44. if sys.argv[1][0] == "d":
  45.     decoding = True
  46. elif sys.argv[1][0] == "e":
  47.     decoding = False
  48. elif sys.argv[1][0] == "u":
  49.     updating = True
  50. elif (sys.argv[1][0] == "r"):
  51.     reverseChecking = True
  52.     defineFilenames = False
  53. elif (sys.argv[1][0] == "D" or sys.argv[1][0] == "E"):
  54.     batchJob = sys.argv[1][0].lower()
  55.     defineFilenames = False
  56. elif sys.argv[1][0] == "V":
  57.     print(VERSION_TEXT)
  58.     exit()
  59. else:
  60.     printUsageAndExit()
  61.  
  62. if len(sys.argv[1]) == 1:
  63.     print("Game version unspecified")
  64.     exit()
  65. else:
  66.     gameVersion = int(sys.argv[1][1:])
  67.  
  68. if gameVersion == 6:
  69.     opcodes = json.loads('[["ins_0","noop"],["ins_1","delete"],["ins_2","relative_jump"],["ins_3","relative_jump_ex"],["ins_4","set_int"],["ins_5","set_float"],["ins_6","set_random_int"],["ins_8","set_random_float"],["ins_9","set_random_float2"],["ins_10","store_x"],["ins_13","add_int"],["ins_14","substract_int"],["ins_15","multiply_int"],["ins_16","divide_int"],["ins_17","modulo"],["ins_18","increment"],["ins_20","add_float"],["ins_21","substract_float"],["ins_23","divide_float"],["ins_25","get_direction"],["ins_26","float_to_unit_circle"],["ins_27","compare_ints"],["ins_28","compare_floats"],["ins_29","relative_jump_if_lower_than"],["ins_30","relative_jump_if_lower_or_equal"],["ins_31","relative_jump_if_equal"],["ins_32","relative_jump_if_greater_than"],["ins_33","relative_jump_if_greater_or_equal"],["ins_34","relative_jump_if_not_equal"],["ins_35","call"],["ins_36","return"],["ins_39","call_if_equal"],["ins_43","set_pos"],["ins_45","set_angle_speed"],["ins_46","set_rotation_speed"],["ins_47","set_speed"],["ins_48","set_acceleration"],["ins_49","set_random_angle"],["ins_50","set_random_angle_ex"],["ins_51","set_speed_towards_player"],["ins_52","move_in_decel"],["ins_56","move_to_linear"],["ins_57","move_to_decel"],["ins_59","move_to_accel"],["ins_61","stop_in_decel"],["ins_63","stop_in_accel"],["ins_65","set_screen_box"],["ins_66","clear_screen_box"],["ins_67","set_bullet_attributes_towards_player"],["ins_68","set_bullet_attributes_to_the_right"],["ins_69","set_bullet_attributes_towards_player_equally_distributed"],["ins_70","set_bullet_attributes_to_the_right_equally_distributed"],["ins_71","set_bullet_attributes_towards_player_equally_distributed_and_rotated"],["ins_74","set_bullet_attributes_towards_player_randomly_distributed"],["ins_75","set_bullet_attributes_to_the_right_with_some_random_angle"],["ins_76","bullet_interval"],["ins_77","bullet_interval_random"],["ins_78","delay_attack"],["ins_79","no_delay_attack"],["ins_81","bullet_launch_offset"],["ins_82","set_extended_bullet_attributes"],["ins_83","change_bullets_in_star_bonus"],["ins_84","unknown_84"],["ins_85","laser"],["ins_86","laser_towards_player"],["ins_87","set_upcoming_id"],["ins_88","alter_laser_angle"],["ins_90","translate_laser"],["ins_92","cancel_laser"],["ins_93","set_spellcard"],["ins_94","end_spellcard"],["ins_95","spawn_enemy"],["ins_96","kill_all_enemies"],["ins_97","set_enemy_anim"],["ins_98","set_boss_anims"],["ins_99","set_aux_anim"],["ins_100","set_death_anim"],["ins_101","set_boss_mode"],["ins_102","create_squares"],["ins_103","set_enemy_hitbox"],["ins_104","set_collidable"],["ins_105","set_damageable"],["ins_106","play_sound"],["ins_107","set_death_flags"],["ins_108","call_when_killed"],["ins_109","memory_write_int32"],["ins_111","set_life"],["ins_112","set_elapsed_time"],["ins_113","set_boss_lower_life_limit"],["ins_114","set_boss_life_callback"],["ins_115","set_timeout"],["ins_116","set_timeout_callback"],["ins_117","set_touchable"],["ins_118","quot_explosion_quot"],["ins_119","drop_bonus"],["ins_120","set_automatic_rotate"],["ins_121","call_special_function_with_param"],["ins_122","call_special_function_without_param"],["ins_123","skip_frames"],["ins_124","drop_specific_bonus"],["ins_125","unknown_125"],["ins_126","set_boss_lives"],["ins_127","unknown_127"],["ins_128","interrupt_anm"],["ins_129","interrupt_aux"],["ins_130","unknown_130"],["ins_131","set_difficulty_influence"],["ins_132","set_invisible"],["ins_133","unknown_133"],["ins_134","unknown_134"],["ins_135","enable_spellcard_bonus"]]')
  70. elif gameVersion == 7:
  71.     opcodes = []
  72. elif gameVersion == 8:
  73.     opcodes = []
  74. else:
  75.     print("Unsupported game")
  76.     exit()
  77.  
  78. if (defineFilenames):
  79.     eclInputFilename = sys.argv[2]
  80.     eclOutputFilename = sys.argv[3]
  81.  
  82. if updating:
  83.     # RECURSION POWER
  84.     subprocess.call([sys.argv[0], "e" + str(gameVersion), eclInputFilename, eclOutputFilename])
  85.     subprocess.call([sys.argv[0], "d" + str(gameVersion), eclOutputFilename, eclOutputFilename])
  86.     exit() # yes, this exit() is supposed to be here; it's not just for debug
  87.    
  88. if reverseChecking:
  89.     reverseCheckingArray = []
  90.     for i in sys.argv[2:]:
  91.         randomString = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(32))
  92.         subprocess.call([sys.argv[0], "d" + str(gameVersion), i, i+".REVERSECHECK_"+randomString])
  93.         subprocess.call([sys.argv[0], "e" + str(gameVersion), i+".REVERSECHECK_"+randomString, i+".REVERSECHECK_"+randomString])
  94.         md5sumOriginal = str(subprocess.check_output(["md5sum", i])).split(" ")[0]
  95.         md5sumProcessed = str(subprocess.check_output(["md5sum", i+".REVERSECHECK_"+randomString])).split(" ")[0]
  96.         if md5sumOriginal == md5sumProcessed:
  97.             reverseCheckingArray.append(i + ": success")
  98.         else:
  99.             reverseCheckingArray.append(i + ": failure")
  100.         os.remove(i+".REVERSECHECK_"+randomString)
  101.     print("=====================")
  102.     for i in reverseCheckingArray:
  103.         print(i)
  104.     exit() # and so is this one
  105.  
  106. if (batchJob == "e" or batchJob == "d"):
  107.     if batchJob == "e":
  108.         extension = ".ENCODED"
  109.     else:
  110.         extension = ".DECODED"
  111.     for i in sys.argv[2:]:
  112.         subprocess.call([sys.argv[0], batchJob + str(gameVersion), i, i+extension])
  113.     exit() # and this one
  114.  
  115. # Instructions:
  116.  
  117. # INSDEC: Stands for INStruction DECimal. Every argument will be converted to decimals (hence the name). Please note that decimals doesn't mean integers.
  118. # This instruction uses integer arguments as well as floating-poing ones. Used when arguments are known (their type and length) from instruction length
  119. # (and game version in later versions of the script, I suppose).
  120.  
  121. # INSDMP: Stands for INStruction DuMP. First four arguments will be converted to decimals, the rest is just a hexadecimal dump. Used when
  122. # instruction arguments are not known. You should figure out their arguments and add a clause to INSDEC to get rid of them.
  123.  
  124. # RAWDMP: Stands for RAW DuMP. Referred to as HEXDUMP (HEXadecimal DUMP) in older versions. Absolutely nothing is converted.
  125. # Used when there are lines in the dumped .ecl file which are clearly not an instruction (for example, length lesser than 8).
  126. # Removing those lines would probably lead to complilation failure, so they are carefully preserved. Do not touch.
  127.  
  128. # PLEASE NOTE: In dumped .ecl file bytes are flipped. Probably has something to do with byte-order.
  129. # You must consider this while encoding or decoding. OR EVERYTHING WILL GET HORRIBLY MESSED UP.
  130.  
  131. if decoding:
  132.     print("Decoding file " + eclInputFilename + " to " + eclOutputFilename + "...")
  133.     print("Please note that you'll need to set your tabular width to 8 (or greater) for correct tabulation.")
  134. else:
  135.     print("Encoding file " + eclInputFilename + " to " + eclOutputFilename + "...")
  136.  
  137. # Borrowed from stackoverflow
  138. def splitIntoChunks(string,chunksize):
  139.     returnArr = []
  140.     size = len(string)
  141.     for pos in range(0, size, chunksize):
  142.         returnArr.append("0x" + string[pos:pos+chunksize])
  143.     return(returnArr)
  144.    
  145. def intToUint(number, length):
  146.     lengthBits = length * 8
  147.     return number % (2 ** lengthBits)
  148.  
  149. def uintToInt(number, length):
  150.     lengthBits = length * 8
  151.     magicNumber = (2 ** lengthBits)
  152.     magicBoundary = (2 ** (lengthBits - 1)) - 1
  153.     if number > magicBoundary:
  154.         # negative number
  155.         return(number - magicNumber)
  156.     else:
  157.         return(number)
  158.  
  159. # http://kipirvine.com/asm/workbook/floating_tut.htm
  160. def parseIEEEShortFloat(shortFloat):
  161.     return(struct.unpack('>f',binascii.unhexlify(shortFloat))[0])
  162.  
  163. def encodeFlippedHEXValue(number,length):
  164.     length = length * 2 # from half-bytes to bytes
  165.     tmpHEXHolder = hex(int(number))[2:].zfill(length)
  166.     tmpHEXHolder = splitIntoChunks(tmpHEXHolder,2)
  167.     return(list(reversed(tmpHEXHolder)))
  168.    
  169. def encodeFlippedHEXValueForFloats(strFloat):
  170.     if strFloat == "0.0":
  171.         return(["0x00","0x00","0x00","0x00"])
  172.     pythonFloat = float(strFloat)
  173.     tmpHEXHolder = hex(struct.unpack('<I', struct.pack('<f', pythonFloat))[0])[2:]
  174.     tmpHEXHolder = splitIntoChunks(tmpHEXHolder,2)
  175.     return(list(reversed(tmpHEXHolder)))
  176.  
  177. # i = int16 (2 bytes)
  178. # I = int32 (4 bytes)
  179. # s = uint16 (2 bytes)
  180. # S = uint32 (4 bytes)
  181. # f = 32-bit float (4 bytes)
  182.  
  183. def decodeMainThreadInstruction(argTypes,argArray,offset):
  184.     returnArr = []
  185.     localOffset = offset # this offset is in bytes
  186.     for i in argTypes:
  187.         if i == "i":
  188.             returnArr.append(str(uintToInt(int("".join(reversed(argArray[localOffset:localOffset+2])),16),2)))
  189.             localOffset = localOffset + 2
  190.         elif i == "I":
  191.             returnArr.append(str(uintToInt(int("".join(reversed(argArray[localOffset:localOffset+4])),16),4)))
  192.             localOffset = localOffset + 4
  193.         elif i == "s":
  194.             returnArr.append(str(int("".join(reversed(argArray[localOffset:localOffset+2])),16)))
  195.             localOffset = localOffset + 2
  196.         elif i == "S":
  197.             returnArr.append(str(int("".join(reversed(argArray[localOffset:localOffset+4])),16)))
  198.             localOffset = localOffset + 4
  199.         elif i == "f":
  200.             returnArr.append(str(parseIEEEShortFloat("".join(reversed(argArray[localOffset:localOffset+4])))))
  201.             localOffset = localOffset + 4
  202.         else:
  203.             print(i + ": UNKNOWN ARGUMENT TYPE")
  204.             exit()
  205.     return(returnArr)
  206.  
  207. def encodeMainThreadInstruction(argTypes,argArray,offset):
  208.     returnArr = []
  209.     localOffset = offset # but this one is in arguments
  210.     for i in argTypes:
  211.         if i == "i":
  212.             returnArr += encodeFlippedHEXValue(str(intToUint(int(argArray[localOffset]),2)),2)
  213.         elif i == "I":
  214.             returnArr += encodeFlippedHEXValue(str(intToUint(int(argArray[localOffset]),4)),4)
  215.         elif i == "s":
  216.             returnArr += encodeFlippedHEXValue(argArray[localOffset],2)
  217.         elif i == "S":
  218.             returnArr += encodeFlippedHEXValue(argArray[localOffset],4)
  219.         elif i == "f":
  220.             returnArr += encodeFlippedHEXValueForFloats(argArray[localOffset])
  221.         else:
  222.             print(i + ": UNKNOWN ARGUMENT TYPE")
  223.             exit()
  224.         localOffset += 1 # so we just increment it
  225.     return(returnArr)
  226.  
  227. with codecs.open(eclInputFilename, mode='r', encoding='shiftjis') as file:
  228.     eclData = file.read()
  229.    
  230. eclStorageArray = eclData.split("local")
  231.        
  232. #eclSubs, eclMainThread = eclData.split("local") # EoSD only
  233.  
  234. eclSubs = eclStorageArray[0]
  235.  
  236. if len(eclStorageArray) > 2:
  237.     #print("eclStorageArray length: " + str(len(eclStorageArray)))
  238.     eclMainThread = ""
  239.     for i in range(1,len(eclStorageArray)):
  240.         eclMainThread = eclMainThread + "local" + eclStorageArray[i]
  241. else:
  242.     eclMainThread = "local" + eclStorageArray[1]
  243.  
  244. output = ""
  245.  
  246. # instruction numbers to readable names
  247.  
  248. for i in range(0,len(opcodes)):
  249.     #print(opcodes[i][0])
  250.     #print(opcodes[i][1])
  251.     j = len(opcodes) - i - 1
  252.     if decoding:
  253.         eclSubs = eclSubs.replace(opcodes[j][0],opcodes[j][1])
  254.     else:
  255.         eclSubs = eclSubs.replace(opcodes[j][1],opcodes[j][0])
  256.  
  257. eclSubsLineArray = eclSubs.split("\n")
  258.  
  259. counter = 0
  260.  
  261. if decoding:
  262.     subNumberCoeff = -1
  263. else:
  264.     subNumberCoeff = 1
  265.  
  266. for i in range(0, len(eclSubsLineArray)):
  267.     if eclSubsLineArray[i].startswith("sub Sub"):
  268.         number = int(eclSubsLineArray[i].split(" Sub")[1].split("()")[0]) + subNumberCoeff
  269.         eclSubsLineArray[i] = "sub Sub" + str(number) + "()"
  270.  
  271. eclSubs = "\n".join(eclSubsLineArray)
  272.        
  273. output = output + eclSubs
  274.  
  275. eclMainThreadLineArray = eclMainThread.split("\n")
  276.  
  277. # i = int16 (2 bytes)
  278. # I = int32 (4 bytes)
  279. # s = uint16 (2 bytes)
  280. # S = uint32 (4 bytes)
  281. # f = 32-bit float (4 bytes)
  282.  
  283. insLenToArgTypeDict = {}
  284. insLenToArgTypeDict[8] = "" # nothing because first eight bytes are already used at this point
  285. insLenToArgTypeDict[16] = "SS" # unused, unused
  286. if gameVersion == 6:
  287.     insLenToArgTypeDict[28] = "fffiiS" # EoSD enemy instruction: x, y, z, life, dropped item, score for killing
  288. if gameVersion == 7:
  289.     insLenToArgTypeDict[32] = "fffiiIS" # PCB enemy instruction: x, y, z, life, unused, dropped item, score for killing
  290. if gameVersion == 8:
  291.     insLenToArgTypeDict[12] = "S" # unused
  292.     insLenToArgTypeDict[32] = "SffiiIS" # IN enemy instruction: ?, x, y, life, unused, dropped item, score for killing
  293.     insLenToArgTypeDict[36] = "SffiiISS" # mysterious instruction, seems similiar to enemy instruction though
  294.        
  295. # Main thread decoding/encoding
  296. if decoding:
  297.     eclMainThreadDataArray = []
  298.     eclLocalCounter = -1
  299.     for i in eclMainThreadLineArray:
  300.         if i.startswith("local"):
  301.             eclMainThreadDataArray.append([])
  302.             eclLocalCounter += 1
  303.         elif i.startswith("    0x"):
  304.             eclMainThreadDataArray[eclLocalCounter].append(i.split("0x")[1].replace("\r",""))
  305.  
  306.     eclMainThreadInstructionArray = []
  307.     #print(len(eclMainThreadDataArray))
  308.     for q in range(0,len(eclMainThreadDataArray)):
  309.         coeff = 0
  310.         for i in range(0,len(eclMainThreadDataArray[q])):
  311.             tmpInstructionArray = []
  312.             tmpInstructionString = "INSDEC:\t"
  313.             tmpInstructionArray += decodeMainThreadInstruction("ssss", eclMainThreadDataArray[q], coeff) # frame, sub, type, insSize
  314.             insSizeDec = int(tmpInstructionArray[len(tmpInstructionArray)-1]) # in bytes
  315.             if gameVersion == 8:
  316.                 # IN stores something in the most significant byte of length instead of 00 like EoSD and PCB
  317.                 # so while we store it in file for reversibility, we disregard the most significant byte for length determination purposes
  318.                 insSizeDec = int(bin(insSizeDec)[10:],2)
  319.             if insSizeDec in insLenToArgTypeDict:
  320.                 tmpInstructionArray += decodeMainThreadInstruction(insLenToArgTypeDict[insSizeDec], eclMainThreadDataArray[q], coeff + 8)
  321.             else:
  322.                 print(str(insSizeDec) + ": UNKNOWN INSTRUCTION LENGTH")
  323.                 tmpInstructionString = "INSDMP:\t"
  324.                 tmpInstructionArray.append("".join(eclMainThreadDataArray[q][coeff + 8:coeff + insSizeDec])) # NOT FLIPPED
  325.             eclMainThreadInstructionArray.append(tmpInstructionString + "\t".join(tmpInstructionArray))
  326.             coeff = coeff + insSizeDec
  327.             #print(len(eclMainThreadDataArray[q]))
  328.             #print(coeff)
  329.             minInsLen = 8
  330.             if gameVersion == 8:
  331.                 minInsLen = 12
  332.             if (len(eclMainThreadDataArray[q]) - coeff) < minInsLen:
  333.                 #print("OUT OF INSTRUCTIONS")
  334.                 break
  335.        
  336.         #print(coeff)
  337.  
  338.         if coeff < len(eclMainThreadDataArray[q]):
  339.             tmpInstructionArray = []
  340.             for i in range(0, len(eclMainThreadDataArray[q]) - coeff):
  341.                 tmpInstructionArray.append(eclMainThreadDataArray[q][coeff + i])
  342.             eclMainThreadInstructionArray.append("RAWDMP:\t" + "".join(tmpInstructionArray))
  343.  
  344.         output = output + (eclMainThreadLineArray[0][0:16] + str(q + 1) + " {\n    " + "\n    ".join(eclMainThreadInstructionArray) + "\n}\n\n")
  345.         eclMainThreadInstructionArray = []
  346. else:
  347.     eclMainThreadHEXArray = []
  348.     eclLocalCounter = -1
  349.     for i in eclMainThreadLineArray:
  350.         i = i.replace("\r","")
  351.         if i.startswith("    INSDEC:"):
  352.             #eclMainThreadHEXArray += [ "INSDEC:" ] # lol debug
  353.             # Decimal instruction to hex values
  354.             tmpHEXStorage = i.split("\t") # 0 == instruction name, 1: == values
  355.             # Dump first four values (those are always 16-bit integers, so it's fine)
  356.             eclMainThreadHEXArray += encodeMainThreadInstruction("ssss",tmpHEXStorage,1)
  357.             # then look for instruction length
  358.             insSizeDec = int(tmpHEXStorage[4])
  359.             if gameVersion == 8:
  360.                 insSizeDec = int(bin(insSizeDec)[10:],2)
  361.             if insSizeDec in insLenToArgTypeDict:
  362.                 eclMainThreadHEXArray += encodeMainThreadInstruction(insLenToArgTypeDict[insSizeDec],tmpHEXStorage,5)
  363.             else:
  364.                 print(tmpHEXStorage[4])
  365.                 print("INSDEC encoding error: UNKNOWN INSTRUCTION LENGTH")
  366.         elif (i.startswith("    RAWDMP:") or i.startswith("    HEXDUMP:")): # HEXDUMP is old name for RAWDMP
  367.             #eclMainThreadHEXArray += [ "RAWDMP:" ] # lol debug
  368.             eclMainThreadHEXArray += splitIntoChunks(i.split("\t")[1],2)
  369.         elif i.startswith("    INSDMP:"):
  370.             # The same code as in INSDEC treatment because that's what it is
  371.             tmpHEXStorage = i.split("\t")
  372.             #eclMainThreadHEXArray += [ "INSDMP:" ] # lol debug
  373.             eclMainThreadHEXArray += encodeMainThreadInstruction("ssss",tmpHEXStorage,1)
  374.             # but then, it's suddenly RAWDMP
  375.             eclMainThreadHEXArray += splitIntoChunks(tmpHEXStorage[5],2)
  376.         elif i.startswith("local"):
  377.             eclLocalCounter += 1
  378.             #print("local " + str(eclLocalCounter) + " encountered")
  379.             if eclLocalCounter > 0:
  380.                 #print("Outputting previous eclMainThreadHEXArray, clearing eclMainThreadHEXArray...")
  381.                 output = output + (eclMainThreadLineArray[0][0:16] + str(eclLocalCounter) + " {\n    " + "\n    ".join(eclMainThreadHEXArray) + "\n}\n\n")
  382.                 eclMainThreadHEXArray = []
  383.     #print("No more locals, outputting current local " + str(eclLocalCounter))
  384.     output = output + (eclMainThreadLineArray[0][0:16] + str(eclLocalCounter + 1) + " {\n    " + "\n    ".join(eclMainThreadHEXArray) + "\n}\n")
  385.  
  386. outputBinary = output.encode("shiftjis")
  387.  
  388. outputFile = open(eclOutputFilename, mode='bw')
  389. outputFile.write(outputBinary)
  390. outputFile.close()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement