Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/python3
- # Converts .txt files from thecl.exe into readable format
- import json
- import codecs
- import binascii
- import struct
- import sys
- import subprocess
- import os
- import random
- import string
- VERSION = 1.01
- VERSION_TEXT = "readable_ecl.py " + str(VERSION)
- usageText = []
- usageText.append("Usage: " + sys.argv[0] + " COMMAND[OPTION] INPUT OUTPUT")
- usageText.append("COMMAND can be:")
- usageText.append(" d decode ECL dump")
- usageText.append(" D mass-decode ECL dump(s); stores output in INPUT.DECODED")
- usageText.append(" e encode readable ECL")
- usageText.append(" E mass-encode readable ECL(s); stores output in INPUT.ENCODED")
- usageText.append(" u update readable ECL")
- usageText.append(" r run reversibility check on ECL dump(s)")
- usageText.append(" V display version information and exit")
- usageText.append("Commands D, E and r don't take OUTPUT argument, but can take multiple INPUTs")
- usageText.append("OPTION can be:")
- usageText.append(" # # can be 6 or 7")
- def printUsageAndExit():
- print("\n".join(usageText))
- exit()
- 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"))):
- printUsageAndExit()
- updating = False
- reverseChecking = False
- batchJob = False
- defineFilenames = True
- if sys.argv[1][0] == "d":
- decoding = True
- elif sys.argv[1][0] == "e":
- decoding = False
- elif sys.argv[1][0] == "u":
- updating = True
- elif (sys.argv[1][0] == "r"):
- reverseChecking = True
- defineFilenames = False
- elif (sys.argv[1][0] == "D" or sys.argv[1][0] == "E"):
- batchJob = sys.argv[1][0].lower()
- defineFilenames = False
- elif sys.argv[1][0] == "V":
- print(VERSION_TEXT)
- exit()
- else:
- printUsageAndExit()
- if len(sys.argv[1]) == 1:
- print("Game version unspecified")
- exit()
- else:
- gameVersion = int(sys.argv[1][1:])
- if gameVersion == 6:
- 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"]]')
- elif gameVersion == 7:
- opcodes = []
- elif gameVersion == 8:
- opcodes = []
- else:
- print("Unsupported game")
- exit()
- if (defineFilenames):
- eclInputFilename = sys.argv[2]
- eclOutputFilename = sys.argv[3]
- if updating:
- # RECURSION POWER
- subprocess.call([sys.argv[0], "e" + str(gameVersion), eclInputFilename, eclOutputFilename])
- subprocess.call([sys.argv[0], "d" + str(gameVersion), eclOutputFilename, eclOutputFilename])
- exit() # yes, this exit() is supposed to be here; it's not just for debug
- if reverseChecking:
- reverseCheckingArray = []
- for i in sys.argv[2:]:
- randomString = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(32))
- subprocess.call([sys.argv[0], "d" + str(gameVersion), i, i+".REVERSECHECK_"+randomString])
- subprocess.call([sys.argv[0], "e" + str(gameVersion), i+".REVERSECHECK_"+randomString, i+".REVERSECHECK_"+randomString])
- md5sumOriginal = str(subprocess.check_output(["md5sum", i])).split(" ")[0]
- md5sumProcessed = str(subprocess.check_output(["md5sum", i+".REVERSECHECK_"+randomString])).split(" ")[0]
- if md5sumOriginal == md5sumProcessed:
- reverseCheckingArray.append(i + ": success")
- else:
- reverseCheckingArray.append(i + ": failure")
- os.remove(i+".REVERSECHECK_"+randomString)
- print("=====================")
- for i in reverseCheckingArray:
- print(i)
- exit() # and so is this one
- if (batchJob == "e" or batchJob == "d"):
- if batchJob == "e":
- extension = ".ENCODED"
- else:
- extension = ".DECODED"
- for i in sys.argv[2:]:
- subprocess.call([sys.argv[0], batchJob + str(gameVersion), i, i+extension])
- exit() # and this one
- # Instructions:
- # INSDEC: Stands for INStruction DECimal. Every argument will be converted to decimals (hence the name). Please note that decimals doesn't mean integers.
- # This instruction uses integer arguments as well as floating-poing ones. Used when arguments are known (their type and length) from instruction length
- # (and game version in later versions of the script, I suppose).
- # INSDMP: Stands for INStruction DuMP. First four arguments will be converted to decimals, the rest is just a hexadecimal dump. Used when
- # instruction arguments are not known. You should figure out their arguments and add a clause to INSDEC to get rid of them.
- # RAWDMP: Stands for RAW DuMP. Referred to as HEXDUMP (HEXadecimal DUMP) in older versions. Absolutely nothing is converted.
- # Used when there are lines in the dumped .ecl file which are clearly not an instruction (for example, length lesser than 8).
- # Removing those lines would probably lead to complilation failure, so they are carefully preserved. Do not touch.
- # PLEASE NOTE: In dumped .ecl file bytes are flipped. Probably has something to do with byte-order.
- # You must consider this while encoding or decoding. OR EVERYTHING WILL GET HORRIBLY MESSED UP.
- if decoding:
- print("Decoding file " + eclInputFilename + " to " + eclOutputFilename + "...")
- print("Please note that you'll need to set your tabular width to 8 (or greater) for correct tabulation.")
- else:
- print("Encoding file " + eclInputFilename + " to " + eclOutputFilename + "...")
- # Borrowed from stackoverflow
- def splitIntoChunks(string,chunksize):
- returnArr = []
- size = len(string)
- for pos in range(0, size, chunksize):
- returnArr.append("0x" + string[pos:pos+chunksize])
- return(returnArr)
- def intToUint(number, length):
- lengthBits = length * 8
- return number % (2 ** lengthBits)
- def uintToInt(number, length):
- lengthBits = length * 8
- magicNumber = (2 ** lengthBits)
- magicBoundary = (2 ** (lengthBits - 1)) - 1
- if number > magicBoundary:
- # negative number
- return(number - magicNumber)
- else:
- return(number)
- # http://kipirvine.com/asm/workbook/floating_tut.htm
- def parseIEEEShortFloat(shortFloat):
- return(struct.unpack('>f',binascii.unhexlify(shortFloat))[0])
- def encodeFlippedHEXValue(number,length):
- length = length * 2 # from half-bytes to bytes
- tmpHEXHolder = hex(int(number))[2:].zfill(length)
- tmpHEXHolder = splitIntoChunks(tmpHEXHolder,2)
- return(list(reversed(tmpHEXHolder)))
- def encodeFlippedHEXValueForFloats(strFloat):
- if strFloat == "0.0":
- return(["0x00","0x00","0x00","0x00"])
- pythonFloat = float(strFloat)
- tmpHEXHolder = hex(struct.unpack('<I', struct.pack('<f', pythonFloat))[0])[2:]
- tmpHEXHolder = splitIntoChunks(tmpHEXHolder,2)
- return(list(reversed(tmpHEXHolder)))
- # i = int16 (2 bytes)
- # I = int32 (4 bytes)
- # s = uint16 (2 bytes)
- # S = uint32 (4 bytes)
- # f = 32-bit float (4 bytes)
- def decodeMainThreadInstruction(argTypes,argArray,offset):
- returnArr = []
- localOffset = offset # this offset is in bytes
- for i in argTypes:
- if i == "i":
- returnArr.append(str(uintToInt(int("".join(reversed(argArray[localOffset:localOffset+2])),16),2)))
- localOffset = localOffset + 2
- elif i == "I":
- returnArr.append(str(uintToInt(int("".join(reversed(argArray[localOffset:localOffset+4])),16),4)))
- localOffset = localOffset + 4
- elif i == "s":
- returnArr.append(str(int("".join(reversed(argArray[localOffset:localOffset+2])),16)))
- localOffset = localOffset + 2
- elif i == "S":
- returnArr.append(str(int("".join(reversed(argArray[localOffset:localOffset+4])),16)))
- localOffset = localOffset + 4
- elif i == "f":
- returnArr.append(str(parseIEEEShortFloat("".join(reversed(argArray[localOffset:localOffset+4])))))
- localOffset = localOffset + 4
- else:
- print(i + ": UNKNOWN ARGUMENT TYPE")
- exit()
- return(returnArr)
- def encodeMainThreadInstruction(argTypes,argArray,offset):
- returnArr = []
- localOffset = offset # but this one is in arguments
- for i in argTypes:
- if i == "i":
- returnArr += encodeFlippedHEXValue(str(intToUint(int(argArray[localOffset]),2)),2)
- elif i == "I":
- returnArr += encodeFlippedHEXValue(str(intToUint(int(argArray[localOffset]),4)),4)
- elif i == "s":
- returnArr += encodeFlippedHEXValue(argArray[localOffset],2)
- elif i == "S":
- returnArr += encodeFlippedHEXValue(argArray[localOffset],4)
- elif i == "f":
- returnArr += encodeFlippedHEXValueForFloats(argArray[localOffset])
- else:
- print(i + ": UNKNOWN ARGUMENT TYPE")
- exit()
- localOffset += 1 # so we just increment it
- return(returnArr)
- with codecs.open(eclInputFilename, mode='r', encoding='shiftjis') as file:
- eclData = file.read()
- eclStorageArray = eclData.split("local")
- #eclSubs, eclMainThread = eclData.split("local") # EoSD only
- eclSubs = eclStorageArray[0]
- if len(eclStorageArray) > 2:
- #print("eclStorageArray length: " + str(len(eclStorageArray)))
- eclMainThread = ""
- for i in range(1,len(eclStorageArray)):
- eclMainThread = eclMainThread + "local" + eclStorageArray[i]
- else:
- eclMainThread = "local" + eclStorageArray[1]
- output = ""
- # instruction numbers to readable names
- for i in range(0,len(opcodes)):
- #print(opcodes[i][0])
- #print(opcodes[i][1])
- j = len(opcodes) - i - 1
- if decoding:
- eclSubs = eclSubs.replace(opcodes[j][0],opcodes[j][1])
- else:
- eclSubs = eclSubs.replace(opcodes[j][1],opcodes[j][0])
- eclSubsLineArray = eclSubs.split("\n")
- counter = 0
- if decoding:
- subNumberCoeff = -1
- else:
- subNumberCoeff = 1
- for i in range(0, len(eclSubsLineArray)):
- if eclSubsLineArray[i].startswith("sub Sub"):
- number = int(eclSubsLineArray[i].split(" Sub")[1].split("()")[0]) + subNumberCoeff
- eclSubsLineArray[i] = "sub Sub" + str(number) + "()"
- eclSubs = "\n".join(eclSubsLineArray)
- output = output + eclSubs
- eclMainThreadLineArray = eclMainThread.split("\n")
- # i = int16 (2 bytes)
- # I = int32 (4 bytes)
- # s = uint16 (2 bytes)
- # S = uint32 (4 bytes)
- # f = 32-bit float (4 bytes)
- insLenToArgTypeDict = {}
- insLenToArgTypeDict[8] = "" # nothing because first eight bytes are already used at this point
- insLenToArgTypeDict[16] = "SS" # unused, unused
- if gameVersion == 6:
- insLenToArgTypeDict[28] = "fffiiS" # EoSD enemy instruction: x, y, z, life, dropped item, score for killing
- if gameVersion == 7:
- insLenToArgTypeDict[32] = "fffiiIS" # PCB enemy instruction: x, y, z, life, unused, dropped item, score for killing
- if gameVersion == 8:
- insLenToArgTypeDict[12] = "S" # unused
- insLenToArgTypeDict[32] = "SffiiIS" # IN enemy instruction: ?, x, y, life, unused, dropped item, score for killing
- insLenToArgTypeDict[36] = "SffiiISS" # mysterious instruction, seems similiar to enemy instruction though
- # Main thread decoding/encoding
- if decoding:
- eclMainThreadDataArray = []
- eclLocalCounter = -1
- for i in eclMainThreadLineArray:
- if i.startswith("local"):
- eclMainThreadDataArray.append([])
- eclLocalCounter += 1
- elif i.startswith(" 0x"):
- eclMainThreadDataArray[eclLocalCounter].append(i.split("0x")[1].replace("\r",""))
- eclMainThreadInstructionArray = []
- #print(len(eclMainThreadDataArray))
- for q in range(0,len(eclMainThreadDataArray)):
- coeff = 0
- for i in range(0,len(eclMainThreadDataArray[q])):
- tmpInstructionArray = []
- tmpInstructionString = "INSDEC:\t"
- tmpInstructionArray += decodeMainThreadInstruction("ssss", eclMainThreadDataArray[q], coeff) # frame, sub, type, insSize
- insSizeDec = int(tmpInstructionArray[len(tmpInstructionArray)-1]) # in bytes
- if gameVersion == 8:
- # IN stores something in the most significant byte of length instead of 00 like EoSD and PCB
- # so while we store it in file for reversibility, we disregard the most significant byte for length determination purposes
- insSizeDec = int(bin(insSizeDec)[10:],2)
- if insSizeDec in insLenToArgTypeDict:
- tmpInstructionArray += decodeMainThreadInstruction(insLenToArgTypeDict[insSizeDec], eclMainThreadDataArray[q], coeff + 8)
- else:
- print(str(insSizeDec) + ": UNKNOWN INSTRUCTION LENGTH")
- tmpInstructionString = "INSDMP:\t"
- tmpInstructionArray.append("".join(eclMainThreadDataArray[q][coeff + 8:coeff + insSizeDec])) # NOT FLIPPED
- eclMainThreadInstructionArray.append(tmpInstructionString + "\t".join(tmpInstructionArray))
- coeff = coeff + insSizeDec
- #print(len(eclMainThreadDataArray[q]))
- #print(coeff)
- minInsLen = 8
- if gameVersion == 8:
- minInsLen = 12
- if (len(eclMainThreadDataArray[q]) - coeff) < minInsLen:
- #print("OUT OF INSTRUCTIONS")
- break
- #print(coeff)
- if coeff < len(eclMainThreadDataArray[q]):
- tmpInstructionArray = []
- for i in range(0, len(eclMainThreadDataArray[q]) - coeff):
- tmpInstructionArray.append(eclMainThreadDataArray[q][coeff + i])
- eclMainThreadInstructionArray.append("RAWDMP:\t" + "".join(tmpInstructionArray))
- output = output + (eclMainThreadLineArray[0][0:16] + str(q + 1) + " {\n " + "\n ".join(eclMainThreadInstructionArray) + "\n}\n\n")
- eclMainThreadInstructionArray = []
- else:
- eclMainThreadHEXArray = []
- eclLocalCounter = -1
- for i in eclMainThreadLineArray:
- i = i.replace("\r","")
- if i.startswith(" INSDEC:"):
- #eclMainThreadHEXArray += [ "INSDEC:" ] # lol debug
- # Decimal instruction to hex values
- tmpHEXStorage = i.split("\t") # 0 == instruction name, 1: == values
- # Dump first four values (those are always 16-bit integers, so it's fine)
- eclMainThreadHEXArray += encodeMainThreadInstruction("ssss",tmpHEXStorage,1)
- # then look for instruction length
- insSizeDec = int(tmpHEXStorage[4])
- if gameVersion == 8:
- insSizeDec = int(bin(insSizeDec)[10:],2)
- if insSizeDec in insLenToArgTypeDict:
- eclMainThreadHEXArray += encodeMainThreadInstruction(insLenToArgTypeDict[insSizeDec],tmpHEXStorage,5)
- else:
- print(tmpHEXStorage[4])
- print("INSDEC encoding error: UNKNOWN INSTRUCTION LENGTH")
- elif (i.startswith(" RAWDMP:") or i.startswith(" HEXDUMP:")): # HEXDUMP is old name for RAWDMP
- #eclMainThreadHEXArray += [ "RAWDMP:" ] # lol debug
- eclMainThreadHEXArray += splitIntoChunks(i.split("\t")[1],2)
- elif i.startswith(" INSDMP:"):
- # The same code as in INSDEC treatment because that's what it is
- tmpHEXStorage = i.split("\t")
- #eclMainThreadHEXArray += [ "INSDMP:" ] # lol debug
- eclMainThreadHEXArray += encodeMainThreadInstruction("ssss",tmpHEXStorage,1)
- # but then, it's suddenly RAWDMP
- eclMainThreadHEXArray += splitIntoChunks(tmpHEXStorage[5],2)
- elif i.startswith("local"):
- eclLocalCounter += 1
- #print("local " + str(eclLocalCounter) + " encountered")
- if eclLocalCounter > 0:
- #print("Outputting previous eclMainThreadHEXArray, clearing eclMainThreadHEXArray...")
- output = output + (eclMainThreadLineArray[0][0:16] + str(eclLocalCounter) + " {\n " + "\n ".join(eclMainThreadHEXArray) + "\n}\n\n")
- eclMainThreadHEXArray = []
- #print("No more locals, outputting current local " + str(eclLocalCounter))
- output = output + (eclMainThreadLineArray[0][0:16] + str(eclLocalCounter + 1) + " {\n " + "\n ".join(eclMainThreadHEXArray) + "\n}\n")
- outputBinary = output.encode("shiftjis")
- outputFile = open(eclOutputFilename, mode='bw')
- outputFile.write(outputBinary)
- outputFile.close()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement