riiv.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. # function definitions
  2. from lxml import etree
  3. import file_ops, xml
  4. # custom import (C-like include)
  5. # geckoloader_path and wit_path will be defined there
  6. exec(open("bin_tool.py").read())
  7. # function to return an attribute string with the
  8. # {$__gameid}, {$__region}, {$__maker} string replacements done
  9. def rtn_def_param_str(string, params_list):
  10. # return this!
  11. return file_ops.get_path_str(string.replace("{$__gameid}", params_list[0]).replace("{$__region}", params_list[1]).replace("{$__maker}", params_list[2]))
  12. # function to check if the game image is valid
  13. def check_game_image(game_image_path):
  14. result = exec_subprocess([wit_path, "verify", "--test", game_image_path])
  15. if (result.returncode != 0):
  16. return False
  17. return True
  18. # function to check if the game image is valid for the riivolution XML
  19. def check_game_xml_id(game_image_path, riiv_xml_id):
  20. # get the actual game id
  21. game_id = exec_subprocess([wit_path, "ID6", game_image_path]).stdout.replace("\n", "")
  22. # riiv_xml_id will be formatted as
  23. # ["XXX", ["X", "X", ...], "XX"]
  24. # RMG E P ... 01 (example)
  25. # see which parts of the game id are valid
  26. is_id_sec_valid = [False, False, False]
  27. # first 3 letters
  28. if (riiv_xml_id[0] == game_id[:-3] or riiv_xml_id[0] == ""):
  29. is_id_sec_valid[0] = True
  30. # 4th letter
  31. if (len(riiv_xml_id[1]) == 0):
  32. is_id_sec_valid[1] = True
  33. else:
  34. for reg in riiv_xml_id[1]:
  35. if (reg == game_id[3:-2]):
  36. is_id_sec_valid[1] = True
  37. # last 2 letters
  38. if (riiv_xml_id[2] == game_id[4:] or riiv_xml_id[2] == ""):
  39. is_id_sec_valid[2] = True
  40. # verify all parts
  41. if (False in is_id_sec_valid):
  42. return False
  43. return game_id
  44. # check_riiv_xml() function
  45. # function to check if a riivolution XML is valid using a XSD file
  46. # if the function returns False and the file actually seems right
  47. # then you have to use a service like the one in this page to
  48. # manually check what is the issue
  49. def check_riiv_xml(xml_path):
  50. # check agaisnt MyRiivolution.xsd
  51. return xml.check_with_sch(xml_path, "MyRiivolution.xsd")
  52. # get_patch_elem_root_path() function
  53. # function to get the root path of the files inside a patch element
  54. def get_patch_elem_root_path(root_elem, patch_elem):
  55. # get the root path of the whole XML
  56. root_path = ""
  57. if ("root" in root_elem.attrib):
  58. root_path = root_path + root_elem.attrib["root"]
  59. # get the relative path of the patch element
  60. if ("root" in patch_elem.attrib):
  61. root_path = root_path + patch_elem.attrib["root"]
  62. return file_ops.get_path_str(root_path)
  63. # check_riiv_patches() function
  64. # function to check the information about the patches to be applied to the game
  65. # this function will basically check if filepaths are valid both for the XML and the game
  66. def check_riiv_patches(game_path, xml_path, mod_files_folder):
  67. ##################################
  68. # check the game file and xml file
  69. if (check_game_image(game_path) == False):
  70. print("Invalid ISO/WBFS")
  71. return False
  72. if (check_riiv_xml(xml_path) == False):
  73. print("Invalid Riivolution XML")
  74. return False
  75. # check if the game id matches the game id from the riivolution file
  76. riiv_game_id = ["", [], ""]
  77. xml = etree.parse(xml_path, etree.XMLParser(ns_clean = True, remove_comments = True)) # parse the xml file
  78. root = xml.getroot()
  79. #########################################
  80. # check the id tag in the riivolution XML
  81. id_elem = root.find("id")
  82. if (id_elem != None):
  83. # grab the game id tag
  84. if ("game" in id_elem.attrib):
  85. # game part
  86. riiv_game_id[0] = id_elem.attrib["game"]
  87. # region part
  88. if (len(riiv_game_id[0]) == 3): # check if the game id is 3 characters long
  89. if (len(id_elem) != 0):
  90. for reg in id_elem:
  91. riiv_game_id[1].append(reg.attrib["type"])
  92. else: # it is 4 characters long
  93. riiv_game_id[1].append(riiv_game_id[0][3:])
  94. riiv_game_id[0] = riiv_game_id[0][:-1]
  95. # developer part
  96. if ("developer" in id_elem.attrib):
  97. riiv_game_id[2] = id_elem.attrib["developer"]
  98. # check all the possible combinations of a full game id
  99. game_id = ["", "", ""]
  100. game_id[0] = check_game_xml_id(game_path, riiv_game_id)
  101. if (game_id == False):
  102. print("Riivolution XML is not compatible with the Wii game provided")
  103. return False
  104. game_id[1] = game_id[0][3:-2]
  105. game_id[2] = game_id[0][4:]
  106. game_id[0] = game_id[0][:-3]
  107. ######################################################################
  108. # parse the XML file and check the patches and their file replacements
  109. opt_patches_id = []
  110. for child in root:
  111. if (child.tag == "options"):
  112. for sec in child:
  113. for opt in sec:
  114. for choice in opt:
  115. for patch in choice:
  116. opt_patches_id.append(patch.attrib["id"]) # lol
  117. elems_patch_id = []
  118. for patch in root.findall("patch"):
  119. elems_patch_id.append(patch.attrib["id"])
  120. # check if all the options patches id exist in the patch tags down in the XML
  121. patch_elems = []
  122. for patch_id in opt_patches_id:
  123. if (patch_id not in elems_patch_id):
  124. print("Patch to apply from options does not exist in the XML")
  125. return False
  126. # at the same time store the patch elements that will actually get read by riivolution
  127. for patch in root.findall("patch"):
  128. if (patch.attrib["id"] == patch_id):
  129. patch_elems.append(patch)
  130. ######################################################
  131. # check the patch tags file/folder/memory replacements
  132. # but before that, extract the game files list (yikes)
  133. game_files = exec_subprocess([wit_path, "files", game_path]).stdout.split("\n")
  134. # remove non-used elements
  135. cont_loop = True
  136. while (cont_loop == True):
  137. for i in range(0, len(game_files)):
  138. if (("DATA/files" not in game_files[i]) and ("./files" not in game_files[i])):
  139. game_files.pop(i)
  140. cont_loop = True
  141. break
  142. cont_loop = False
  143. # do another for loop to get rid of "DATA/files/" and "./files/" starting strings
  144. for i in range(0, len(game_files)):
  145. if ("DATA/files" in game_files[i]):
  146. game_files[i] = file_ops.get_path_str(game_files[i].replace("DATA/files", ""))
  147. else:
  148. game_files[i] = file_ops.get_path_str(game_files[i].replace("./files", ""))
  149. # ~ for i in range(0, len(game_files)):
  150. # ~ print(game_files[i])
  151. ##########################################
  152. # start the loop! and check the file paths
  153. for patch in patch_elems:
  154. # get the root path of the current pacthes
  155. patch_root_path = rtn_def_param_str(file_ops.get_base_path(mod_files_folder, True) + "/"
  156. + get_patch_elem_root_path(root, patch), game_id)
  157. # check the folder, file tag patches
  158. # Note: the offset, length and other attributes for file and folder
  159. # elements are weird and I don't know how to support them
  160. for patch_type in patch:
  161. # file patch
  162. if (patch_type.tag == "file"):
  163. fpath = patch_root_path + "/" + rtn_def_param_str(patch_type.attrib["external"], game_id)
  164. if (file_ops.f_exists(fpath) == False):
  165. print("Line %d: File patch has invalid external path" % (patch_type.sourceline))
  166. print(fpath)
  167. return False
  168. else: # check if the file exists on disc
  169. if (patch_type.attrib["disc"] not in game_files):
  170. if ("create" not in patch_type.attrib):
  171. print("Line %d: File patch creates non-existant disc file without create attribute" % (patch_type.sourceline))
  172. return False
  173. else: # create attribute exists
  174. if (patch_type.attrib["create"] == "false"):
  175. print("Line %d: File patch creates non-existant disc file with create = False" % (patch_type.sourceline))
  176. return False
  177. # folder patch
  178. if (patch_type.tag == "folder"):
  179. fpath = patch_root_path + "/" + rtn_def_param_str(patch_type.attrib["external"], game_id)
  180. if (file_ops.f_exists(fpath) == False):
  181. print("Line %d: Folder patch has invalid external path" % (patch_type.sourceline))
  182. print(fpath)
  183. return False
  184. if ("disc" in patch_type.attrib):
  185. if (rtn_def_param_str(patch_type.attrib["disc"], game_id) not in game_files):
  186. # check if the create attribute is present
  187. if ("create" in patch_type.attrib):
  188. if (patch_type.attrib["create"] == "False"):
  189. print("Line %d: Folder patch creates non-existant disc folder with create = False" % (patch_type.sourceline))
  190. return False
  191. else: # cannot create a disc non-existant folder without a create attribute
  192. print("Line %d: Folder patch creates non-existant disc folder without create attribute" % (patch_type.sourceline))
  193. return False
  194. # memory patch (incomplete, full check happens when building the mod)
  195. if (patch_type.tag == "memory"):
  196. if ("valuefile" in patch_type.attrib):
  197. fpath = patch_root_path + "/" + rtn_def_param_str(patch_type.attrib["valuefile"], game_id)
  198. if (file_ops.f_exists(fpath) == False):
  199. print("Line %d: Memory patch has invalid paths")
  200. print(fpath)
  201. return False
  202. # restrict patches to be applied to the 0x80000000 memory area
  203. if ("0x8" not in patch_type.attrib["offset"]):
  204. print("Line %d: Memory patch tries patching to a range outside the 0x80000000 memory area" % (patch_type.sourceline))
  205. return False
  206. # check if a ram dump is needed for the patching process
  207. if ("original" in patch_type.attrib):
  208. if (file_ops.f_exists("ram_dumps/" + game_id[0] + game_id[1] + game_id[2] + ".bin") == False
  209. or file_ops.get_file_size("ram_dumps/" + game_id[0] + game_id[1] + game_id[2] + ".bin") != 0x1800000):
  210. print("Line %d: Memory element has original condition." % (patch_type.sourceline))
  211. print("No/Invalid ram dump of \"%s\" was found in ram_dumps/" % (game_id[0] + game_id[1] + game_id[2]))
  212. return False
  213. ###############################
  214. # hardcore memory patches check
  215. # "original" patches (frequently used) can only be checked with a ram dumb of the game
  216. # the folder "ram_dumps" will hold wii game ram dumps (24 mb approx) named as the game ID
  217. # if a patch original condition fails it is simply not applied so it isn't an error to be checked
  218. # everything is good for now
  219. return True
  220. # function to be able to remove the tmp folder
  221. # after the check_riiv_patches() function check
  222. def check_riiv_patches_wrapper(game_path, xml_path, mod_files_folder):
  223. # safe the return value of check_riiv_patches()
  224. result = check_riiv_patches(game_path, xml_path, mod_files_folder)
  225. file_ops.rm_folder("tmp")
  226. return result
  227. # get_riiv_patches_inf() function
  228. # function to return information about the patches used in the riivolution file
  229. def get_riiv_patches_inf(game_path, xml_path, mod_files_folder):
  230. # first check if the XML is well formatted
  231. # if the game relates to the XML
  232. # and if the mod files folder/xml file/game file relate to each other
  233. if (check_riiv_patches_wrapper(game_path, xml_path, mod_files_folder) == False):
  234. return None
  235. # to be able to store correctly the XML information
  236. # literally the first time doing classes stuff (*jesus what is this*)
  237. class section:
  238. def __init__(self, name):
  239. self.name = name
  240. self.options = []
  241. class option:
  242. def __init__(self, name):
  243. self.name = name
  244. self.choices = []
  245. class choice:
  246. def __init__(self, name):
  247. self.name = name
  248. self.patches = []
  249. class patch:
  250. def __init__(self, id):
  251. self.id = id
  252. # parse the XML and find the options element
  253. # return a list of section classes with their respective option -> choice
  254. sections = []
  255. for sec in etree.parse(xml_path, etree.XMLParser(remove_comments = True)).find("options"):
  256. sections.append(section(sec.attrib["name"]))
  257. for opt in sec:
  258. sections[-1].options.append(option(opt.attrib["name"]))
  259. for cho in opt:
  260. sections[-1].options[-1].choices.append(choice(cho.attrib["name"]))
  261. for pat in cho:
  262. sections[-1].options[-1].choices[-1].patches.append(patch(pat.attrib["id"]))
  263. # ~ print()
  264. # ~ for sec in sections:
  265. # ~ print("Section name: %s" % (sec.name))
  266. # ~ print("#-Options names:")
  267. # ~ for opt in sec.options:
  268. # ~ print("----%s" % (opt.name))
  269. # ~ print("##----Choices names:")
  270. # ~ for cho in opt.choices:
  271. # ~ print("--------%s" % (cho.name))
  272. # ~ print("###-------Patches ids:")
  273. # ~ for pat in cho.patches:
  274. # ~ print("------------%s" % (pat.id))
  275. # ~ print()
  276. # return dis shit
  277. return sections
  278. # apply_riiv_patches() function
  279. def apply_riiv_patches(game_path, xml_path, mod_files_folder, choice_str_list):
  280. # first check everything
  281. if (check_riiv_patches_wrapper(game_path, xml_path, mod_files_folder) == False):
  282. return False
  283. # check if choice_str_list contains valid XML choices
  284. # parse the XML and find the options element
  285. xml = etree.parse(xml_path, etree.XMLParser(remove_comments = True))
  286. root = xml.getroot()
  287. choices_names = []
  288. for sec in root.find("options"):
  289. for opt in sec:
  290. for cho in opt:
  291. choices_names.append(cho.attrib["name"])
  292. for choice_str in choice_str_list:
  293. if (choice_str not in choices_names):
  294. print("Invalid choice selected: %s" % (choice_str))
  295. return False
  296. # start the patching process
  297. # get the patches ids to apply
  298. patches_ids = []
  299. for sec in root.find("options"):
  300. for opt in sec:
  301. for cho in opt:
  302. if (cho.attrib["name"] in choice_str_list):
  303. for patch in cho:
  304. patches_ids.append(patch.attrib["id"])
  305. # get the patch elements on the xml
  306. patch_elems = []
  307. for patch in root.findall("patch"):
  308. if (patch.attrib["id"] in patches_ids):
  309. patch_elems.append(patch)
  310. # get game id code pieces
  311. game_id = ["", "", ""]
  312. game_id[0] = exec_subprocess([wit_path, "ID6", game_path]).stdout.replace("\n", "")
  313. game_id[1] = game_id[0][3:-2]
  314. game_id[2] = game_id[0][4:]
  315. game_id[0] = game_id[0][:-3]
  316. # extract the files from the game (wish I would know a way to just
  317. # have to deal only with the mod files and somehow "merge them" on
  318. # the original WBFS so no complete extraction has to be done)
  319. result = exec_subprocess([wit_path, "extract", game_path, "tmp/"]).returncode
  320. if (result != 0):
  321. print("wit could not extract correctly the game files")
  322. return False
  323. # define the actual root of files to change (its either "tmp/" or "tmp/DATA/")
  324. ext_game_path = "tmp/files/"
  325. if (file_ops.f_exists("tmp/DATA/")):
  326. ext_game_path = "tmp/DATA/files/"
  327. # First, deal with the file/folder replacements
  328. memory_original_exists = False # see if the original condition exists in a memory patch
  329. for patch in patch_elems:
  330. # get the root path of the current pacthes
  331. patch_root_path = rtn_def_param_str(file_ops.get_base_path(mod_files_folder, True) + "/"
  332. + get_patch_elem_root_path(root, patch), game_id)
  333. # go through each patch type
  334. for patch_type in patch:
  335. # file patch
  336. if (patch_type.tag == "file"):
  337. fpath1 = patch_root_path + "/" + rtn_def_param_str(patch_type.attrib["external"], game_id)
  338. fpath2 = rtn_def_param_str(ext_game_path + patch_type.attrib["disc"], game_id)
  339. file_ops.cp_file(fpath1, fpath2)
  340. # folder patch
  341. if (patch_type.tag == "folder"):
  342. # check if the folder in the extracted files exists
  343. if ("disc" in patch_type.attrib):
  344. fpath1 = patch_root_path + "/" + rtn_def_param_str(patch_type.attrib["external"], game_id)
  345. fpath2 = rtn_def_param_str(ext_game_path + patch_type.attrib["disc"], game_id)
  346. file_ops.cp_folder(fpath1, fpath2, False)
  347. # memory patch
  348. if (patch_type.tag == "memory"):
  349. if ("original" in patch_type.attrib):
  350. memory_original_exists = True
  351. # Now, handle the memory patches
  352. # wit dolpatch and geckoloader will be used
  353. # - wit will patch offsets above 0x8000FFFF
  354. # - geckoloader will patch offsets from 0x8000FFFF and below
  355. # for the wit patches a temporal riivolution xml will be made
  356. # for the geckoloader patches a temporal gecko code list will be done
  357. # all patches will be checked agaisnt a ram dump of the game
  358. ram_dump = None
  359. if (memory_original_exists): # open the ram dump
  360. ram_dump = open("ram_dumps/" + game_id[0] + game_id[1] + game_id[2] + ".bin", "rb")
  361. tmp_gl = open("tmp_gl.txt", "w")
  362. tmp_wit = open("tmp_wit.xml", "w")
  363. # start the loop!
  364. for patch in patch_elems:
  365. patch_root_path = rtn_def_param_str(file_ops.get_base_path(mod_files_folder, True) + "/"
  366. + get_patch_elem_root_path(root, patch), game_id)
  367. for patch_type in patch:
  368. # only go through the memory patches this time
  369. if (patch_type.tag != "memory"):
  370. continue
  371. offset_hex_str = patch_type.attrib["offset"][2:]
  372. offset_hex_int = int(offset_hex_str, 16)
  373. # check the original value (if it has one)
  374. if ("original" in patch_type.attrib):
  375. og_hex = patch_type.attrib["original"]
  376. ram_dump.seek(offset_hex_int - 0x80000000)
  377. dmp_hex = ram_dump.read(int(len(og_hex) / 2)).hex()
  378. # ignore the patch if original does not match
  379. if (og_hex.upper() != dmp_hex.upper()):
  380. continue
  381. # wit hotfix (report this inconsistency)
  382. patch_type.attrib["original"] = patch_type.attrib["original"].replace("0x", "")
  383. # check if the memory patch contains a valuefile attribute
  384. if ("valuefile" in patch_type.attrib):
  385. val_file_path = patch_root_path + "/" + rtn_def_param_str(patch_type.attrib["valuefile"], game_id)
  386. tmp = open(val_file_path, "rb")
  387. del patch_type.attrib["valuefile"]
  388. patch_type.set("value", tmp.read(file_ops.get_file_size(val_file_path)).hex().upper())
  389. tmp.close()
  390. else: # it has a value attribute
  391. # wit hotfix (report this inconsistency)
  392. patch_type.attrib["value"] = patch_type.attrib["value"].replace("0x", "")
  393. # for wit
  394. if (offset_hex_int > 0x8000FFFF):
  395. tmp = etree.tostring(patch_type).decode("utf-8")
  396. while (tmp[0] in " \t\r\n"):
  397. tmp = tmp[1:]
  398. while (tmp[-1] in " \t\r\n"):
  399. tmp = tmp[:-1]
  400. tmp_wit.write(tmp + "\n")
  401. # for geckoloader
  402. else:
  403. tmp = patch_type.attrib["value"]
  404. last_offset = offset_hex_int # variable to be used later
  405. if (len(tmp) > 8):
  406. for i in range(0, int(len(tmp) / 8)):
  407. last_offset = offset_hex_int + (4 * i)
  408. if (len(tmp) > 8):
  409. tmp_gl.write("%08X %s\n" % (last_offset - 0x80000000 + 0x04000000, tmp[:-len(tmp) + 8]))
  410. else:
  411. tmp_gl.write("%08X %s\n" % (last_offset - 0x80000000 + 0x04000000, tmp))
  412. tmp = tmp[8:]
  413. last_offset += 4
  414. # write the rest of tmp depending on its size
  415. if (len(tmp) == 6):
  416. tmp_gl.write("%08X 0000%s\n" % (last_offset - 0x80000000 + 0x02000000, tmp[:-2]))
  417. tmp_gl.write("%08X 000000%s\n" % (last_offset + 2 - 0x80000000, tmp[4:]))
  418. elif (len(tmp) == 4):
  419. tmp_gl.write("%08X 0000%s\n" % (last_offset - 0x80000000 + 0x02000000, tmp))
  420. elif (len(tmp) == 2):
  421. tmp_gl.write("%08X 000000%s\n" % (last_offset - 0x80000000, tmp))
  422. # close files
  423. tmp_wit.close()
  424. tmp_gl.close()
  425. if (memory_original_exists):
  426. ram_dump.close()
  427. # define the main.dol location
  428. dol_path = ext_game_path.replace("files/", "sys/main.dol")
  429. dol_tmp_path = dol_path.replace("main.dol", "main_tmp.dol")
  430. # patch the forsaken main.dol
  431. # geckoloader part
  432. if (file_ops.get_file_size("tmp_gl.txt") != 0):
  433. # check the return code from geckoloader
  434. result = exec_subprocess(geckoloader_path + [dol_path, "tmp_gl.txt", "--dest",
  435. dol_tmp_path, "--txtcodes", "ALL", "--optimize"]).returncode
  436. if (result != 0):
  437. print("geckoloader could not handle tmp_gl.txt properly")
  438. return False
  439. file_ops.cp_file(dol_tmp_path, dol_path)
  440. file_ops.rm_file(dol_tmp_path)
  441. # wit part
  442. if (file_ops.get_file_size("tmp_wit.xml") != 0):
  443. # check the return code from wit
  444. result = exec_subprocess([wit_path, "dolpatch", dol_path,
  445. "XML=tmp_wit.xml", "--dest", dol_tmp_path]).returncode
  446. if (result != 0):
  447. print("wit could not handle tmp_wit.xml properly")
  448. return False
  449. file_ops.cp_file(dol_tmp_path, dol_path)
  450. file_ops.rm_file(dol_tmp_path)
  451. # build game image file
  452. exec_subprocess([wit_path, "copy", "tmp/", "result.wbfs"])
  453. # eliminate temp files
  454. file_ops.rm_folder("tmp/")
  455. file_ops.rm_file("tmp_gl.txt")
  456. file_ops.rm_file("tmp_wit.xml")
  457. # done! (hopefully)
  458. return True
  459. # apply_riiv_patches_wrapper() function
  460. def apply_riiv_patches_wrapper(game_path, xml_path, mod_files_folder, choice_str_list):
  461. # safe the return value of apply_riiv_patches()
  462. result = apply_riiv_patches(game_path, xml_path, mod_files_folder, choice_str_list)
  463. file_ops.rm_folder("tmp/")
  464. file_ops.rm_file("tmp_gl.txt")
  465. file_ops.rm_file("tmp_wit.xml")
  466. return result