mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-23 11:33:21 -07:00
375 lines
14 KiB
Python
375 lines
14 KiB
Python
import pkgutil
|
|
from typing import TYPE_CHECKING, Iterable
|
|
import hashlib
|
|
import Utils
|
|
import os
|
|
|
|
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
|
|
from . import names
|
|
from .rules import bosses
|
|
|
|
from .text import MM3TextEntry
|
|
from .color import get_colors_for_item, write_palette_shuffle
|
|
from .options import Consumables
|
|
|
|
if TYPE_CHECKING:
|
|
from . import MM3World
|
|
|
|
MM3LCHASH = "5266687de215e790b2008284402f3917"
|
|
PROTEUSHASH = "b69fff40212b80c94f19e786d1efbf61"
|
|
MM3NESHASH = "4a53b6f58067d62c9a43404fe835dd5c"
|
|
MM3VCHASH = "c50008f1ac86fae8d083232cdd3001a5"
|
|
|
|
enemy_weakness_ptrs: dict[int, int] = {
|
|
0: 0x14100,
|
|
1: 0x14200,
|
|
2: 0x14300,
|
|
3: 0x14400,
|
|
4: 0x14500,
|
|
5: 0x14600,
|
|
6: 0x14700,
|
|
7: 0x14800,
|
|
8: 0x14900,
|
|
}
|
|
|
|
enemy_addresses: dict[str, int] = {
|
|
"Dada": 0x12,
|
|
"Potton": 0x13,
|
|
"New Shotman": 0x15,
|
|
"Hammer Joe": 0x16,
|
|
"Peterchy": 0x17,
|
|
"Bubukan": 0x18,
|
|
"Vault Pole": 0x19, # Capcom..., why did you name an enemy Pole?
|
|
"Bomb Flier": 0x1A,
|
|
"Yambow": 0x1D,
|
|
"Metall 2": 0x1E,
|
|
"Cannon": 0x22,
|
|
"Jamacy": 0x25,
|
|
"Jamacy 2": 0x26, # dunno what this is, but I won't question
|
|
"Jamacy 3": 0x27,
|
|
"Jamacy 4": 0x28, # tf is this Capcom
|
|
"Mag Fly": 0x2A,
|
|
"Egg": 0x2D,
|
|
"Gyoraibo 2": 0x2E,
|
|
"Junk Golem": 0x2F,
|
|
"Pickelman Bull": 0x30,
|
|
"Nitron": 0x35,
|
|
"Pole": 0x37,
|
|
"Gyoraibo": 0x38,
|
|
"Hari Harry": 0x3A,
|
|
"Penpen Maker": 0x3B,
|
|
"Returning Monking": 0x3C,
|
|
"Have 'Su' Bee": 0x3E,
|
|
"Hive": 0x3F,
|
|
"Bolton-Nutton": 0x40,
|
|
"Walking Bomb": 0x44,
|
|
"Elec'n": 0x45,
|
|
"Mechakkero": 0x47,
|
|
"Chibee": 0x4B,
|
|
"Swimming Penpen": 0x4D,
|
|
"Top": 0x52,
|
|
"Penpen": 0x56,
|
|
"Komasaburo": 0x57,
|
|
"Parasyu": 0x59,
|
|
"Hologran (Static)": 0x5A,
|
|
"Hologran (Moving)": 0x5B,
|
|
"Bomber Pepe": 0x5C,
|
|
"Metall DX": 0x5D,
|
|
"Petit Snakey": 0x5E,
|
|
"Proto Man": 0x62,
|
|
"Break Man": 0x63,
|
|
"Metall": 0x7D,
|
|
"Giant Springer": 0x83,
|
|
"Springer Missile": 0x85,
|
|
"Giant Snakey": 0x99,
|
|
"Tama": 0x9A,
|
|
"Doc Robot (Flash)": 0xB0,
|
|
"Doc Robot (Wood)": 0xB1,
|
|
"Doc Robot (Crash)": 0xB2,
|
|
"Doc Robot (Metal)": 0xB3,
|
|
"Doc Robot (Bubble)": 0xC0,
|
|
"Doc Robot (Heat)": 0xC1,
|
|
"Doc Robot (Quick)": 0xC2,
|
|
"Doc Robot (Air)": 0xC3,
|
|
"Snake": 0xCA,
|
|
"Needle Man": 0xD0,
|
|
"Magnet Man": 0xD1,
|
|
"Top Man": 0xD2,
|
|
"Shadow Man": 0xD3,
|
|
"Top Man's Top": 0xD5,
|
|
"Shadow Man (Sliding)": 0xD8, # Capcom I swear
|
|
"Hard Man": 0xE0,
|
|
"Spark Man": 0xE2,
|
|
"Snake Man": 0xE4,
|
|
"Gemini Man": 0xE6,
|
|
"Gemini Man (Clone)": 0xE7, # Capcom why
|
|
"Yellow Devil MK-II": 0xF1,
|
|
"Wily Machine 3": 0xF3,
|
|
"Gamma": 0xF8,
|
|
"Kamegoro": 0x101,
|
|
"Kamegoro Shell": 0x102,
|
|
"Holograph Mega Man": 0x105,
|
|
"Giant Metall": 0x10C, # This is technically FC but we're +16 from the rom header
|
|
}
|
|
|
|
# addresses printed when assembling basepatch
|
|
wily_4_ptr: int = 0x7F570
|
|
consumables_ptr: int = 0x7FDEA
|
|
energylink_ptr: int = 0x7FDF9
|
|
|
|
|
|
class MM3ProcedurePatch(APProcedurePatch, APTokenMixin):
|
|
hash = [MM3LCHASH, MM3NESHASH, MM3VCHASH]
|
|
game = "Mega Man 3"
|
|
patch_file_ending = ".apmm3"
|
|
result_file_ending = ".nes"
|
|
name: bytearray
|
|
procedure = [
|
|
("apply_bsdiff4", ["mm3_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: "MM3World", patch: MM3ProcedurePatch) -> None:
|
|
patch.write_file("mm3_basepatch.bsdiff4", pkgutil.get_data(__name__, os.path.join("data", "mm3_basepatch.bsdiff4")))
|
|
# text writing
|
|
|
|
base_address = 0x3C000
|
|
color_address = 0x31BC7
|
|
for i, offset, location in zip([0, 8, 1, 2,
|
|
3, 4, 5, 6,
|
|
7, 9],
|
|
[0x10, 0x50, 0x91, 0xD2,
|
|
0x113, 0x154, 0x195, 0x1D6,
|
|
0x217, 0x257],
|
|
[
|
|
names.get_needle_cannon,
|
|
names.get_rush_jet,
|
|
names.get_magnet_missile,
|
|
names.get_gemini_laser,
|
|
names.get_hard_knuckle,
|
|
names.get_top_spin,
|
|
names.get_search_snake,
|
|
names.get_spark_shock,
|
|
names.get_shadow_blade,
|
|
names.get_rush_marine,
|
|
]):
|
|
item = world.get_location(location).item
|
|
if item:
|
|
if len(item.name) <= 13:
|
|
# we want to just place it in the center
|
|
first_str = ""
|
|
second_str = item.name
|
|
third_str = ""
|
|
elif len(item.name) <= 26:
|
|
# spread across second and third
|
|
first_str = ""
|
|
second_str = item.name[:13]
|
|
third_str = item.name[13:]
|
|
else:
|
|
# all three
|
|
first_str = item.name[:13]
|
|
second_str = item.name[13:26]
|
|
third_str = item.name[26:]
|
|
if len(third_str) > 13:
|
|
third_str = third_str[:13]
|
|
player_str = world.multiworld.get_player_name(item.player)
|
|
if len(player_str) > 13:
|
|
player_str = player_str[:13]
|
|
y_coords = 0xA5
|
|
row = 0x21
|
|
if location in [names.get_rush_marine, names.get_rush_jet]:
|
|
y_coords = 0x45
|
|
row = 0x22
|
|
patch.write_bytes(base_address + offset, MM3TextEntry(first_str, y_coords, row).resolve())
|
|
patch.write_bytes(base_address + 16 + offset, MM3TextEntry(second_str, y_coords + 0x20, row).resolve())
|
|
patch.write_bytes(base_address + 32 + offset, MM3TextEntry(third_str, y_coords + 0x40, row).resolve())
|
|
if y_coords + 0x60 > 0xFF:
|
|
row += 1
|
|
y_coords = 0x01
|
|
patch.write_bytes(base_address + 48 + offset, MM3TextEntry(player_str, y_coords, row).resolve())
|
|
colors_high, colors_low = get_colors_for_item(item.name)
|
|
patch.write_bytes(color_address + (i * 8) + 1, colors_high)
|
|
patch.write_bytes(color_address + (i * 8) + 5, colors_low)
|
|
else:
|
|
patch.write_bytes(base_address + 48 + offset, MM3TextEntry(player_str, y_coords + 0x60, row).resolve())
|
|
|
|
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
|
|
for boss in bosses:
|
|
if boss == "Kamegoro Maker":
|
|
enemy_weaknesses["Kamegoro"] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
|
|
enemy_weaknesses["Kamegoro Shell"] = {i: world.weapon_damage[i][bosses[boss]]
|
|
for i in world.weapon_damage}
|
|
elif boss == "Gemini Man":
|
|
enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
|
|
enemy_weaknesses["Gemini Man (Clone)"] = {i: world.weapon_damage[i][bosses[boss]]
|
|
for i in world.weapon_damage}
|
|
elif boss == "Shadow Man":
|
|
enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
|
|
enemy_weaknesses["Shadow Man (Sliding)"] = {i: world.weapon_damage[i][bosses[boss]]
|
|
for i in world.weapon_damage}
|
|
else:
|
|
enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
|
|
|
|
if world.options.enemy_weakness:
|
|
for enemy in enemy_addresses:
|
|
if enemy in [*bosses.keys(), "Kamegoro", "Kamegoro Shell", "Gemini Man (Clone)", "Shadow Man (Sliding)"]:
|
|
continue
|
|
enemy_weaknesses[enemy] = {weapon: world.random.randint(-4, 4) for weapon in enemy_weakness_ptrs}
|
|
if enemy in ["Tama", "Giant Snakey", "Proto Man", "Giant Metall"] and enemy_weaknesses[enemy][0] <= 0:
|
|
enemy_weaknesses[enemy][0] = 1
|
|
elif enemy == "Jamacy 2":
|
|
# bruh
|
|
if not enemy_weaknesses[enemy][8] > 0:
|
|
enemy_weaknesses[enemy][8] = 1
|
|
if not enemy_weaknesses[enemy][3] > 0:
|
|
enemy_weaknesses[enemy][3] = 1
|
|
|
|
for enemy, damage in enemy_weaknesses.items():
|
|
for weapon in enemy_weakness_ptrs:
|
|
if damage[weapon] < 0:
|
|
damage[weapon] = 256 + damage[weapon]
|
|
patch.write_byte(enemy_weakness_ptrs[weapon] + enemy_addresses[enemy], damage[weapon])
|
|
|
|
if world.options.consumables != Consumables.option_all:
|
|
value_a = 0x64
|
|
value_b = 0x6A
|
|
if world.options.consumables in (Consumables.option_none, Consumables.option_1up_etank):
|
|
value_a = 0x68
|
|
if world.options.consumables in (Consumables.option_none, Consumables.option_weapon_health):
|
|
value_b = 0x67
|
|
patch.write_byte(consumables_ptr - 3, value_a)
|
|
patch.write_byte(consumables_ptr + 1, value_b)
|
|
|
|
patch.write_byte(wily_4_ptr + 1, world.options.wily_4_requirement.value)
|
|
|
|
patch.write_byte(energylink_ptr + 1, world.options.energy_link.value)
|
|
|
|
if world.options.reduce_flashing:
|
|
# Spark Man
|
|
patch.write_byte(0x12649, 8)
|
|
patch.write_byte(0x1264E, 8)
|
|
patch.write_byte(0x12653, 8)
|
|
# Shadow Man
|
|
patch.write_byte(0x12658, 0x10)
|
|
# Gemini Man
|
|
patch.write_byte(0x12637, 0x20)
|
|
patch.write_byte(0x1263D, 0x20)
|
|
patch.write_byte(0x12643, 0x20)
|
|
# Gamma
|
|
patch.write_byte(0x7DA4A, 0xF)
|
|
|
|
if world.options.music_shuffle:
|
|
if world.options.music_shuffle.current_key == "no_music":
|
|
pool = [0xF0] * 18
|
|
elif world.options.music_shuffle.current_key == "randomized":
|
|
pool = world.random.choices(range(1, 0xC), k=18)
|
|
else:
|
|
pool = [1, 2, 3, 4, 5, 6, 7, 8, 1, 3, 7, 8, 9, 9, 10, 10, 11, 11]
|
|
world.random.shuffle(pool)
|
|
patch.write_bytes(0x7CD1C, pool)
|
|
|
|
from Utils import __version__
|
|
patch.name = bytearray(f'MM3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0',
|
|
'utf8')[:21]
|
|
patch.name.extend([0] * (21 - len(patch.name)))
|
|
patch.write_bytes(0x3F330, patch.name) # We changed this section, but this pointer is still valid!
|
|
deathlink_byte = world.options.death_link.value | (world.options.energy_link.value << 1)
|
|
patch.write_byte(0x3F346, deathlink_byte)
|
|
|
|
patch.write_bytes(0x3F34C, world.world_version)
|
|
|
|
version_map = {
|
|
"0": 0x00,
|
|
"1": 0x01,
|
|
"2": 0x02,
|
|
"3": 0x03,
|
|
"4": 0x04,
|
|
"5": 0x05,
|
|
"6": 0x06,
|
|
"7": 0x07,
|
|
"8": 0x08,
|
|
"9": 0x09,
|
|
".": 0x26
|
|
}
|
|
patch.write_token(APTokenTypes.RLE, 0x653B, (11, 0x25))
|
|
patch.write_token(APTokenTypes.RLE, 0x6549, (25, 0x25))
|
|
|
|
# BY SILVRIS
|
|
patch.write_bytes(0x653B, [0x0B, 0x22, 0x25, 0x1C, 0x12, 0x15, 0x1F, 0x1B, 0x12, 0x1C])
|
|
# ARCHIPELAGO x.x.x
|
|
patch.write_bytes(0x654D,
|
|
[0x0A, 0x1B, 0x0C, 0x11, 0x12, 0x19, 0x0E, 0x15, 0x0A, 0x10, 0x18])
|
|
patch.write_bytes(0x6559, 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\x10\x40\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: bytes | None = 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_mm3(base_rom_bytes)
|
|
basemd5 = hashlib.md5()
|
|
basemd5.update(base_rom_bytes)
|
|
if basemd5.hexdigest() not in {MM3LCHASH, MM3NESHASH, MM3VCHASH}:
|
|
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:
|
|
from . import MM3World
|
|
if not file_name:
|
|
file_name = MM3World.settings.rom_file
|
|
if not os.path.exists(file_name):
|
|
file_name = Utils.user_path(file_name)
|
|
return file_name
|
|
|
|
|
|
prg_offset = 0xCF1B0
|
|
prg_size = 0x40000
|
|
chr_offset = 0x10F1B0
|
|
chr_size = 0x20000
|
|
|
|
|
|
def extract_mm3(proteus: bytes) -> bytes:
|
|
mm3 = bytearray(proteus[prg_offset:prg_offset + prg_size])
|
|
mm3.extend(proteus[chr_offset:chr_offset + chr_size])
|
|
return bytes(mm3)
|