* initial (broken) commit * small work on init * Update Items.py * beginning work, some rom patches * commit progress from bh branch * deathlink, fix soft-reset kill, e-tank loss * begin work on targeting new bhclient * write font * definitely didn't forget to add the other two hashes no * update to modern options, begin colors * fix 6th letter bug * palette shuffle + logic rewrite * fix a bunch of pointers * fix color changes, deathlink, and add wily 5 req * adjust weapon weakness generation * Update Rules.py * attempt wily 5 softlock fix * add explicit test for rbm weaknesses * fix difficulty and hard reset * fix connect deathlink and off by one item color * fix atomic fire again * de-jank deathlink * rewrite wily5 rule * fix rare solo-gen fill issue, hopefully * Update Client.py * fix wily 5 requirements * undo fill hook * fix picopico-kun rules * for real this time * update minimum damage requirement * begin move to procedure patch * finish move to APPP, allow rando boobeam, color updates * fix color bug, UT support? * what do you mean I forgot the procedure * fix UT? * plando weakness and fixes * sfx when item received, more time stopper edge cases * Update test_weakness.py * fix rules and color bug * fix color bug, support reduced flashing * major world overhaul * Update Locations.py * fix first found bugs * mypy cleanup * headerless roms * Update Rom.py * further cleanup * work on energylink * el fixes * update to energylink 2.0 packet * energylink balancing * potentially break other clients, more balancing * Update Items.py * remove startup change from basepatch we write that in patch, since we also need to clean the area before applying * el balancing and feedback * hopefully less test failures? * implement world version check * add weapon/health option * Update Rom.py * x/x2 * specials * Update Color.py * Update Options.py * finally apply location groups * bump minor version number instead * fix duplicate stage sends * validate wily 5, tests * see if renaming fixes * add shuffled weakness * remove passwords * refresh rbm select, fix wily 5 validation * forgot we can't check 0 * oops I broke the basepatch (remove failing test later) * fix solo gen fill error? * fix webhost patch recognition * fix imports, basepatch * move to flexibility metric for boss validation * special case boobeam trap * block strobe on stage select init * more energylink balancing * bump world version * wily HP inaccurate in validation * fix validation edge case * save last completed wily to data storage * mypy and pep8 cleanup * fix file browse validation * fix test failure, add enemy weakness * remove test seed * update enemy damage * inno setup * Update en_Mega Man 2.md * setup guide * Update en_Mega Man 2.md * finish plando weakness section * starting rbm edge case * remove * imports * properly wrap later weakness additions in regen playthrough * fix import * forgot readme * remove time stopper special casing since we moved to proper wily 5 validation, this special casing is no longer important * properly type added locations * Update CODEOWNERS * add animation reduction * deprioritize Time Stopper in rush checks * special case wily phase 1 * fix key error * forgot the test * music and general cleanup * the great rename * fix import * thanks pycharm * reorder palette shuffle * account for alien on shuffled weakness * apply suggestions * fix seedbleed * fix invalid buster passthrough * fix weakness landing beneath required amount * fix failsafe * finish music * fix Time Stopper on Flash/Alien * asar pls * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * world helpers * init cleanup * apostrophes * clearer wording * mypy and cleanup * options doc cleanup * Update rom.py * rules cleanup * Update __init__.py * Update __init__.py * move to defaultdict * cleanup world helpers * Update __init__.py * remove unnecessary line from fill hook * forgot the other one * apply code review * remove collect * Update rules.py * forgot another --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
		
			
				
	
	
		
			416 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			416 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import pkgutil
 | |
| from typing import Optional, TYPE_CHECKING, Iterable, Dict, Sequence
 | |
| import hashlib
 | |
| import Utils
 | |
| import os
 | |
| 
 | |
| import settings
 | |
| from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
 | |
| from . import names
 | |
| from .rules import minimum_weakness_requirement
 | |
| from .text import MM2TextEntry
 | |
| from .color import get_colors_for_item, write_palette_shuffle
 | |
| from .options import Consumables, ReduceFlashing, RandomMusic
 | |
| 
 | |
| if TYPE_CHECKING:
 | |
|     from . import MM2World
 | |
| 
 | |
| MM2LCHASH = "37f2c36ce7592f1e16b3434b3985c497"
 | |
| PROTEUSHASH = "9ff045a3ca30018b6e874c749abb3ec4"
 | |
| MM2NESHASH = "0527a0ee512f69e08b8db6dc97964632"
 | |
| MM2VCHASH = "0c78dfe8e90fb8f3eed022ff01126ad3"
 | |
| 
 | |
| enemy_weakness_ptrs: Dict[int, int] = {
 | |
|     0: 0x3E9A8,
 | |
|     1: 0x3EA24,
 | |
|     2: 0x3EA9C,
 | |
|     3: 0x3EB14,
 | |
|     4: 0x3EB8C,
 | |
|     5: 0x3EC04,
 | |
|     6: 0x3EC7C,
 | |
|     7: 0x3ECF4,
 | |
| }
 | |
| 
 | |
| enemy_addresses: Dict[str, int] = {
 | |
|     "Shrink": 0x00,
 | |
|     "M-445": 0x04,
 | |
|     "Claw": 0x08,
 | |
|     "Tanishi": 0x0A,
 | |
|     "Kerog": 0x0C,
 | |
|     "Petit Kerog": 0x0D,
 | |
|     "Anko": 0x0F,
 | |
|     "Batton": 0x16,
 | |
|     "Robitto": 0x17,
 | |
|     "Friender": 0x1C,
 | |
|     "Monking": 0x1D,
 | |
|     "Kukku": 0x1F,
 | |
|     "Telly": 0x22,
 | |
|     "Changkey Maker": 0x23,
 | |
|     "Changkey": 0x24,
 | |
|     "Pierrobot": 0x29,
 | |
|     "Fly Boy": 0x2C,
 | |
|     # "Crash Wall": 0x2D
 | |
|     # "Friender Wall": 0x2E
 | |
|     "Blocky": 0x31,
 | |
|     "Neo Metall": 0x34,
 | |
|     "Matasaburo": 0x36,
 | |
|     "Pipi": 0x38,
 | |
|     "Pipi Egg": 0x3A,
 | |
|     "Copipi": 0x3C,
 | |
|     "Kaminari Goro": 0x3E,
 | |
|     "Petit Goblin": 0x45,
 | |
|     "Springer": 0x46,
 | |
|     "Mole (Up)": 0x48,
 | |
|     "Mole (Down)": 0x49,
 | |
|     "Shotman (Left)": 0x4B,
 | |
|     "Shotman (Right)": 0x4C,
 | |
|     "Sniper Armor": 0x4E,
 | |
|     "Sniper Joe": 0x4F,
 | |
|     "Scworm": 0x50,
 | |
|     "Scworm Worm": 0x51,
 | |
|     "Picopico-kun": 0x6A,
 | |
|     "Boobeam Trap": 0x6D,
 | |
|     "Big Fish": 0x71
 | |
| }
 | |
| 
 | |
| # addresses printed when assembling basepatch
 | |
| consumables_ptr: int = 0x3F2FE
 | |
| quickswap_ptr: int = 0x3F363
 | |
| wily_5_ptr: int = 0x3F3A1
 | |
| energylink_ptr: int = 0x3F46B
 | |
| get_equipped_sound_ptr: int = 0x3F384
 | |
| 
 | |
| 
 | |
| class RomData:
 | |
|     def __init__(self, file: bytes, name: str = "") -> None:
 | |
|         self.file = bytearray(file)
 | |
|         self.name = name
 | |
| 
 | |
|     def read_byte(self, offset: int) -> int:
 | |
|         return self.file[offset]
 | |
| 
 | |
|     def read_bytes(self, offset: int, length: int) -> bytearray:
 | |
|         return self.file[offset:offset + length]
 | |
| 
 | |
|     def write_byte(self, offset: int, value: int) -> None:
 | |
|         self.file[offset] = value
 | |
| 
 | |
|     def write_bytes(self, offset: int, values: Sequence[int]) -> None:
 | |
|         self.file[offset:offset + len(values)] = values
 | |
| 
 | |
|     def write_to_file(self, file: str) -> None:
 | |
|         with open(file, 'wb') as outfile:
 | |
|             outfile.write(self.file)
 | |
| 
 | |
| 
 | |
| class MM2ProcedurePatch(APProcedurePatch, APTokenMixin):
 | |
|     hash = [MM2LCHASH, MM2NESHASH, MM2VCHASH]
 | |
|     game = "Mega Man 2"
 | |
|     patch_file_ending = ".apmm2"
 | |
|     result_file_ending = ".nes"
 | |
|     name: bytearray
 | |
|     procedure = [
 | |
|         ("apply_bsdiff4", ["mm2_basepatch.bsdiff4"]),
 | |
|         ("apply_tokens", ["token_patch.bin"]),
 | |
|     ]
 | |
| 
 | |
|     @classmethod
 | |
|     def get_source_data(cls) -> bytes:
 | |
|         return get_base_rom_bytes()
 | |
| 
 | |
|     def write_byte(self, offset: int, value: int) -> None:
 | |
|         self.write_token(APTokenTypes.WRITE, offset, value.to_bytes(1, "little"))
 | |
| 
 | |
|     def write_bytes(self, offset: int, value: Iterable[int]) -> None:
 | |
|         self.write_token(APTokenTypes.WRITE, offset, bytes(value))
 | |
| 
 | |
| 
 | |
| def patch_rom(world: "MM2World", patch: MM2ProcedurePatch) -> None:
 | |
|     patch.write_file("mm2_basepatch.bsdiff4", pkgutil.get_data(__name__, os.path.join("data", "mm2_basepatch.bsdiff4")))
 | |
|     # text writing
 | |
|     patch.write_bytes(0x37E2A, MM2TextEntry("FOR           ", 0xCB).resolve())
 | |
|     patch.write_bytes(0x37EAA, MM2TextEntry("GET EQUIPPED  ", 0x0B).resolve())
 | |
|     patch.write_bytes(0x37EBA, MM2TextEntry("WITH          ", 0x2B).resolve())
 | |
| 
 | |
|     base_address = 0x3F650
 | |
|     color_address = 0x37F6C
 | |
|     for i, location in zip(range(11), [
 | |
|         names.atomic_fire_get,
 | |
|         names.air_shooter_get,
 | |
|         names.leaf_shield_get,
 | |
|         names.bubble_lead_get,
 | |
|         names.quick_boomerang_get,
 | |
|         names.time_stopper_get,
 | |
|         names.metal_blade_get,
 | |
|         names.crash_bomber_get,
 | |
|         names.item_1_get,
 | |
|         names.item_2_get,
 | |
|         names.item_3_get
 | |
|     ]):
 | |
|         item = world.multiworld.get_location(location, world.player).item
 | |
|         if item:
 | |
|             if len(item.name) <= 14:
 | |
|                 # we want to just place it in the center
 | |
|                 first_str = ""
 | |
|                 second_str = item.name
 | |
|                 third_str = ""
 | |
|             elif len(item.name) <= 28:
 | |
|                 # spread across second and third
 | |
|                 first_str = ""
 | |
|                 second_str = item.name[:14]
 | |
|                 third_str = item.name[14:]
 | |
|             else:
 | |
|                 # all three
 | |
|                 first_str = item.name[:14]
 | |
|                 second_str = item.name[14:28]
 | |
|                 third_str = item.name[28:]
 | |
|                 if len(third_str) > 16:
 | |
|                     third_str = third_str[:16]
 | |
|             player_str = world.multiworld.get_player_name(item.player)
 | |
|             if len(player_str) > 14:
 | |
|                 player_str = player_str[:14]
 | |
|             patch.write_bytes(base_address + (64 * i), MM2TextEntry(first_str, 0x4B).resolve())
 | |
|             patch.write_bytes(base_address + (64 * i) + 16, MM2TextEntry(second_str, 0x6B).resolve())
 | |
|             patch.write_bytes(base_address + (64 * i) + 32, MM2TextEntry(third_str, 0x8B).resolve())
 | |
|             patch.write_bytes(base_address + (64 * i) + 48, MM2TextEntry(player_str, 0xEB).resolve())
 | |
| 
 | |
|             colors = get_colors_for_item(item.name)
 | |
|             if i > 7:
 | |
|                 patch.write_bytes(color_address + 27 + ((i - 8) * 2), colors)
 | |
|             else:
 | |
|                 patch.write_bytes(color_address + (i * 2), colors)
 | |
| 
 | |
|     write_palette_shuffle(world, patch)
 | |
| 
 | |
|     enemy_weaknesses: Dict[str, Dict[int, int]] = {}
 | |
| 
 | |
|     if world.options.strict_weakness or world.options.random_weakness or world.options.plando_weakness:
 | |
|         # we need to write boss weaknesses
 | |
|         output = bytearray()
 | |
|         for weapon in world.weapon_damage:
 | |
|             if weapon == 8:
 | |
|                 continue  # Time Stopper is a special case
 | |
|             weapon_damage = [world.weapon_damage[weapon][i]
 | |
|                              if world.weapon_damage[weapon][i] >= 0
 | |
|                              else 256 + world.weapon_damage[weapon][i]
 | |
|                              for i in range(14)]
 | |
|             output.extend(weapon_damage)
 | |
|         patch.write_bytes(0x2E952, bytes(output))
 | |
|         time_stopper_damage = world.weapon_damage[8]
 | |
|         time_offset = 0x2C03B
 | |
|         damage_table = {
 | |
|             4: 0xF,
 | |
|             3: 0x17,
 | |
|             2: 0x1E,
 | |
|             1: 0x25
 | |
|         }
 | |
|         for boss, damage in enumerate(time_stopper_damage):
 | |
|             if damage > 4:
 | |
|                 damage = 4  # 4 is a guaranteed kill, no need to exceed
 | |
|             if damage <= 0:
 | |
|                 patch.write_byte(time_offset + 14 + boss, 0)
 | |
|             else:
 | |
|                 patch.write_byte(time_offset + 14 + boss, 1)
 | |
|                 patch.write_byte(time_offset + boss, damage_table[damage])
 | |
|         if world.options.random_weakness:
 | |
|             wily_5_weaknesses = [i for i in range(8) if world.weapon_damage[i][12] > minimum_weakness_requirement[i]]
 | |
|             world.random.shuffle(wily_5_weaknesses)
 | |
|             if len(wily_5_weaknesses) >= 3:
 | |
|                 weak1 = wily_5_weaknesses.pop()
 | |
|                 weak2 = wily_5_weaknesses.pop()
 | |
|                 weak3 = wily_5_weaknesses.pop()
 | |
|             elif len(wily_5_weaknesses) == 2:
 | |
|                 weak1 = weak2 = wily_5_weaknesses.pop()
 | |
|                 weak3 = wily_5_weaknesses.pop()
 | |
|             else:
 | |
|                 weak1 = weak2 = weak3 = 0
 | |
|             patch.write_byte(0x2DA2E, weak1)
 | |
|             patch.write_byte(0x2DA32, weak2)
 | |
|             patch.write_byte(0x2DA3A, weak3)
 | |
|         enemy_weaknesses["Picopico-kun"] = {weapon: world.weapon_damage[weapon][9] for weapon in range(8)}
 | |
|         enemy_weaknesses["Boobeam Trap"] = {weapon: world.weapon_damage[weapon][11] for weapon in range(8)}
 | |
| 
 | |
|     if world.options.enemy_weakness:
 | |
|         for enemy in enemy_addresses:
 | |
|             if enemy in ("Picopico-kun", "Boobeam Trap"):
 | |
|                 continue
 | |
|             enemy_weaknesses[enemy] = {weapon: world.random.randint(-4, 4) for weapon in enemy_weakness_ptrs}
 | |
|             if enemy == "Friender":
 | |
|                 # Friender has to be killed, need buster damage to not break logic
 | |
|                 enemy_weaknesses[enemy][0] = max(enemy_weaknesses[enemy][0], 1)
 | |
| 
 | |
|     for enemy, damage_table in enemy_weaknesses.items():
 | |
|         for weapon in enemy_weakness_ptrs:
 | |
|             if damage_table[weapon] < 0:
 | |
|                 damage_table[weapon] = 256 + damage_table[weapon]
 | |
|             patch.write_byte(enemy_weakness_ptrs[weapon] + enemy_addresses[enemy], damage_table[weapon])
 | |
| 
 | |
|     if world.options.quickswap:
 | |
|         patch.write_byte(quickswap_ptr + 1, 0x01)
 | |
| 
 | |
|     if world.options.consumables != Consumables.option_all:
 | |
|         value_a = 0x7C
 | |
|         value_b = 0x76
 | |
|         if world.options.consumables == Consumables.option_1up_etank:
 | |
|             value_b = 0x7A
 | |
|         else:
 | |
|             value_a = 0x7A
 | |
|         patch.write_byte(consumables_ptr - 3, value_a)
 | |
|         patch.write_byte(consumables_ptr + 1, value_b)
 | |
| 
 | |
|     patch.write_byte(wily_5_ptr + 1, world.options.wily_5_requirement.value)
 | |
| 
 | |
|     if world.options.energy_link:
 | |
|         patch.write_byte(energylink_ptr + 1, 1)
 | |
| 
 | |
|     if world.options.reduce_flashing:
 | |
|         if world.options.reduce_flashing.value == ReduceFlashing.option_virtual_console:
 | |
|             color = 0x2D  # Dark Gray
 | |
|             speed = -1
 | |
|         elif world.options.reduce_flashing.value == ReduceFlashing.option_minor:
 | |
|             color = 0x2D
 | |
|             speed = 0x08
 | |
|         else:
 | |
|             color = 0x0F
 | |
|             speed = 0x00
 | |
|         patch.write_byte(0x2D1B0, color)  # Change white to a dark gray, Mecha Dragon
 | |
|         patch.write_byte(0x2D397, 0x0F)  # Longer flash time, Mecha Dragon kill
 | |
|         patch.write_byte(0x2D3A0, color)  # Change white to a dark gray, Picopico-kun/Boobeam Trap
 | |
|         patch.write_byte(0x2D65F, color)  # Change white to a dark gray, Guts Tank
 | |
|         patch.write_byte(0x2DA94, color)  # Change white to a dark gray, Wily Machine
 | |
|         patch.write_byte(0x2DC97, color)  # Change white to a dark gray, Alien
 | |
|         patch.write_byte(0x2DD68, 0x10)  # Longer flash time, Alien kill
 | |
|         patch.write_bytes(0x2DF14, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA])  # Reduce final Alien flash to 1 big flash
 | |
|         patch.write_byte(0x34132, 0x08)  # Longer flash time, Stage Select
 | |
| 
 | |
|         if world.options.reduce_flashing.value == ReduceFlashing.option_full:
 | |
|             # reduce color of stage flashing
 | |
|             patch.write_bytes(0x344C9, [0x2D, 0x10, 0x00, 0x2D,
 | |
|                                         0x0F, 0x10, 0x2D, 0x00,
 | |
|                                         0x0F, 0x10, 0x2D, 0x00,
 | |
|                                         0x0F, 0x10, 0x2D, 0x00,
 | |
|                                         0x2D, 0x10, 0x2D, 0x00,
 | |
|                                         0x0F, 0x10, 0x2D, 0x00,
 | |
|                                         0x0F, 0x10, 0x2D, 0x00,
 | |
|                                         0x0F, 0x10, 0x2D, 0x00])
 | |
|             # remove wily castle flash
 | |
|             patch.write_byte(0x3596D, 0x0F)
 | |
| 
 | |
|         if speed != -1:
 | |
|             patch.write_byte(0xFE01, speed)  # Bubble Man Stage
 | |
|             patch.write_byte(0x1BE01, speed)  # Metal Man Stage
 | |
| 
 | |
|     if world.options.random_music:
 | |
|         if world.options.random_music == RandomMusic.option_none:
 | |
|             pool = [0xFF] * 20
 | |
|             # A couple of additional mutes we want here
 | |
|             patch.write_byte(0x37819, 0xFF)  # Credits
 | |
|             patch.write_byte(0x378A4, 0xFF)  # Credits #2
 | |
|             patch.write_byte(0x37149, 0xFF)  # Game Over Jingle
 | |
|             patch.write_byte(0x341BA, 0xFF)  # Robot Master Jingle
 | |
|             patch.write_byte(0x2E0B4, 0xFF)  # Robot Master Defeated
 | |
|             patch.write_byte(0x35B78, 0xFF)  # Wily Castle
 | |
|             patch.write_byte(0x2DFA5, 0xFF)  # Wily Defeated
 | |
| 
 | |
|         elif world.options.random_music == RandomMusic.option_shuffled:
 | |
|             pool = [0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 9, 0x10, 0xC, 0xB, 0x17, 0x13, 0xE, 0xD]
 | |
|             world.random.shuffle(pool)
 | |
|         else:
 | |
|             pool = world.random.choices([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xB, 0xC, 0xD, 0xE, 0x10, 0x13, 0x17], k=20)
 | |
|         patch.write_bytes(0x381E0, pool[:13])
 | |
|         patch.write_byte(0x36318, pool[13])  # Game Start
 | |
|         patch.write_byte(0x37181, pool[13])  # Game Over
 | |
|         patch.write_byte(0x340AE, pool[14])  # RBM Select
 | |
|         patch.write_byte(0x39005, pool[15])  # Robot Master Battle
 | |
|         patch.write_byte(get_equipped_sound_ptr + 1, pool[16])  # Get Equipped, we actually hook this already lmao
 | |
|         patch.write_byte(0x3775A, pool[17])  # Epilogue
 | |
|         patch.write_byte(0x36089, pool[18])  # Intro
 | |
|         patch.write_byte(0x361F1, pool[19])  # Title
 | |
| 
 | |
| 
 | |
| 
 | |
|     from Utils import __version__
 | |
|     patch.name = bytearray(f'MM2{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0',
 | |
|                            'utf8')[:21]
 | |
|     patch.name.extend([0] * (21 - len(patch.name)))
 | |
|     patch.write_bytes(0x3FFC0, patch.name)
 | |
|     deathlink_byte = world.options.death_link.value | (world.options.energy_link.value << 1)
 | |
|     patch.write_byte(0x3FFD5, deathlink_byte)
 | |
| 
 | |
|     patch.write_bytes(0x3FFD8, world.world_version)
 | |
| 
 | |
|     version_map = {
 | |
|         "0": 0x90,
 | |
|         "1": 0x91,
 | |
|         "2": 0x92,
 | |
|         "3": 0x93,
 | |
|         "4": 0x94,
 | |
|         "5": 0x95,
 | |
|         "6": 0x96,
 | |
|         "7": 0x97,
 | |
|         "8": 0x98,
 | |
|         "9": 0x99,
 | |
|         ".": 0xDC
 | |
|     }
 | |
|     patch.write_token(APTokenTypes.RLE, 0x36EE0, (11, 0))
 | |
|     patch.write_token(APTokenTypes.RLE, 0x36EEE, (25, 0))
 | |
| 
 | |
|     # BY SILVRIS
 | |
|     patch.write_bytes(0x36EE0, [0xC2, 0xD9, 0xC0, 0xD3, 0xC9, 0xCC, 0xD6, 0xD2, 0xC9, 0xD3])
 | |
|     # ARCHIPELAGO x.x.x
 | |
|     patch.write_bytes(0x36EF2, [0xC1, 0xD2, 0xC3, 0xC8, 0xC9, 0xD0, 0xC5, 0xCC, 0xC1, 0xC7, 0xCF, 0xC0])
 | |
|     patch.write_bytes(0x36EFE, list(map(lambda c: version_map[c], __version__)))
 | |
| 
 | |
|     patch.write_file("token_patch.bin", patch.get_token_binary())
 | |
| 
 | |
| 
 | |
| header = b"\x4E\x45\x53\x1A\x10\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00"
 | |
| 
 | |
| 
 | |
| def read_headerless_nes_rom(rom: bytes) -> bytes:
 | |
|     if rom[:4] == b"NES\x1A":
 | |
|         return rom[16:]
 | |
|     else:
 | |
|         return rom
 | |
| 
 | |
| 
 | |
| def get_base_rom_bytes(file_name: str = "") -> bytes:
 | |
|     base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None)
 | |
|     if not base_rom_bytes:
 | |
|         file_name = get_base_rom_path(file_name)
 | |
|         base_rom_bytes = read_headerless_nes_rom(bytes(open(file_name, "rb").read()))
 | |
| 
 | |
|         basemd5 = hashlib.md5()
 | |
|         basemd5.update(base_rom_bytes)
 | |
|         if basemd5.hexdigest() == PROTEUSHASH:
 | |
|             base_rom_bytes = extract_mm2(base_rom_bytes)
 | |
|             basemd5 = hashlib.md5()
 | |
|             basemd5.update(base_rom_bytes)
 | |
|         if basemd5.hexdigest() not in {MM2LCHASH, MM2NESHASH, MM2VCHASH}:
 | |
|             print(basemd5.hexdigest())
 | |
|             raise Exception("Supplied Base Rom does not match known MD5 for US, LC, or US VC release. "
 | |
|                             "Get the correct game and version, then dump it")
 | |
|         headered_rom = bytearray(base_rom_bytes)
 | |
|         headered_rom[0:0] = header
 | |
|         setattr(get_base_rom_bytes, "base_rom_bytes", bytes(headered_rom))
 | |
|         return bytes(headered_rom)
 | |
|     return base_rom_bytes
 | |
| 
 | |
| 
 | |
| def get_base_rom_path(file_name: str = "") -> str:
 | |
|     options: settings.Settings = settings.get_settings()
 | |
|     if not file_name:
 | |
|         file_name = options["mm2_options"]["rom_file"]
 | |
|     if not os.path.exists(file_name):
 | |
|         file_name = Utils.user_path(file_name)
 | |
|     return file_name
 | |
| 
 | |
| 
 | |
| PRG_OFFSET = 0x8ED70
 | |
| PRG_SIZE = 0x40000
 | |
| 
 | |
| 
 | |
| def extract_mm2(proteus: bytes) -> bytes:
 | |
|     mm2 = bytearray(proteus[PRG_OFFSET:PRG_OFFSET + PRG_SIZE])
 | |
|     return bytes(mm2)
 |