From 62be17dbef1fbe95e141db8421245d4ad3776c73 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Sun, 21 Apr 2024 19:44:22 -0700 Subject: [PATCH 01/71] Initial implementation (before group test) --- worlds/mmx/Client.py | 701 ++++++++++++++++++++++++++ worlds/mmx/Items.py | 106 ++++ worlds/mmx/Levels.py | 75 +++ worlds/mmx/Locations.py | 174 +++++++ worlds/mmx/Names/EventName.py | 12 + worlds/mmx/Names/ItemName.py | 40 ++ worlds/mmx/Names/LocationName.py | 91 ++++ worlds/mmx/Names/RegionName.py | 84 +++ worlds/mmx/Options.py | 138 +++++ worlds/mmx/Regions.py | 391 ++++++++++++++ worlds/mmx/Rom.py | 168 ++++++ worlds/mmx/Rules.py | 215 ++++++++ worlds/mmx/__init__.py | 231 +++++++++ worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 0 -> 2451 bytes worlds/mmx/docs/en_Mega Man X.md | 3 + worlds/mmx/docs/setup_en.md | 148 ++++++ 16 files changed, 2577 insertions(+) create mode 100644 worlds/mmx/Client.py create mode 100644 worlds/mmx/Items.py create mode 100644 worlds/mmx/Levels.py create mode 100644 worlds/mmx/Locations.py create mode 100644 worlds/mmx/Names/EventName.py create mode 100644 worlds/mmx/Names/ItemName.py create mode 100644 worlds/mmx/Names/LocationName.py create mode 100644 worlds/mmx/Names/RegionName.py create mode 100644 worlds/mmx/Options.py create mode 100644 worlds/mmx/Regions.py create mode 100644 worlds/mmx/Rom.py create mode 100644 worlds/mmx/Rules.py create mode 100644 worlds/mmx/__init__.py create mode 100644 worlds/mmx/data/mmx_basepatch.bsdiff4 create mode 100644 worlds/mmx/docs/en_Mega Man X.md create mode 100644 worlds/mmx/docs/setup_en.md diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py new file mode 100644 index 000000000000..3c1807b4ff70 --- /dev/null +++ b/worlds/mmx/Client.py @@ -0,0 +1,701 @@ +import logging +import asyncio +import time + +from NetUtils import ClientStatus, color +from worlds.AutoSNIClient import SNIClient + +logger = logging.getLogger("Client") +snes_logger = logging.getLogger("SNES") + +# FXPAK Pro protocol memory mapping used by SNI +ROM_START = 0x000000 +WRAM_START = 0xF50000 +WRAM_SIZE = 0x20000 +SRAM_START = 0xE00000 + +MMX_GAME_STATE = WRAM_START + 0x000D1 +MMX_MENU_STATE = WRAM_START + 0x000D2 +MMX_GAMEPLAY_STATE = WRAM_START + 0x000D3 +MMX_LEVEL_INDEX = WRAM_START + 0x01F7A +MMX_VICTORY = WRAM_START + 0x01F7F + +MMX_WEAPON_ARRAY = WRAM_START + 0x01F88 +MMX_SUB_TANK_ARRAY = WRAM_START + 0x01F83 +MMX_UPGRADES = WRAM_START + 0x01F99 +MMX_HEART_TANKS = WRAM_START + 0x01F9C +MMX_HADOUKEN = WRAM_START + 0x01F7E +MMX_LIFE_COUNT = WRAM_START + 0x01F80 +MMX_MAX_HP = WRAM_START + 0x01F9A +MMX_CURRENT_HP = WRAM_START + 0x00BCF + +MMX_SFX_FLAG = WRAM_START + 0x1EE03 +MMX_SFX_NUMBER = WRAM_START + 0x1EE04 + +MMX_SIGMA_ACCESS = WRAM_START + 0x1EE02 +MMX_COLLECTED_HEART_TANKS = WRAM_START + 0x1EE05 +MMX_COLLECTED_UPGRADES = WRAM_START + 0x1EE06 +MMX_COLLECTED_HADOUKEN = WRAM_START + 0x1EE07 +MMX_DEFEATED_BOSSES = WRAM_START + 0x1EE80 +MMX_COMPLETED_LEVELS = WRAM_START + 0x1EE60 +MMX_COLLECTED_PICKUPS = WRAM_START + 0x1EEC0 +MMX_UNLOCKED_LEVELS = WRAM_START + 0x1EE40 + +MMX_RECV_INDEX = WRAM_START + 0x1EE00 +MMX_ENERGY_LINK_PACKET = WRAM_START + 0x1EE09 +MMX_VALIDATION_CHECK = WRAM_START + 0x1EE13 + +MMX_RECEIVING_ITEM = WRAM_START + 0x1EE15 +MMX_ENABLE_HEART_TANK = WRAM_START + 0x1EE0B +MMX_ENABLE_HP_REFILL = WRAM_START + 0x1EE0F +MMX_HP_REFILL_AMOUNT = WRAM_START + 0x1EE10 +MMX_ENABLE_GIVE_1UP = WRAM_START + 0x1EE12 + +MMX_PAUSE_STATE = WRAM_START + 0x01F24 +MMX_CAN_MOVE = WRAM_START + 0x01F13 + +MMX_PICKUPSANITY_ACTIVE = ROM_START + 0x17FFE7 +MMX_ENERGY_LINK_ENABLED = ROM_START + 0x17FFE8 +MMX_DEATH_LINK_ACTIVE = ROM_START + 0x17FFE9 + +HP_EXCHANGE_RATE = 500000000 + +STARTING_ID = 0xBE0800 + +MMX_ROMHASH_START = 0x7FC0 +ROMHASH_SIZE = 0x15 + +PICKUP_ITEMS = ["small hp refill", "large hp refill", "1up", "hp refill"] +HP_REFILLS = ["small hp refill", "large hp refill", "hp refill"] +BOSS_MEDAL = [0xFF, 0xFF, 0x02, 0xFF, 0x0C, 0x0A, 0x00, 0xFF, + 0x04, 0x06, 0x0E, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ] + +class MMXSNIClient(SNIClient): + game = "Mega Man X" + + def __init__(self): + super().__init__() + self.game_state = False + self.last_death_link = 0 + self.auto_heal = False + self.energy_link_enabled = False + self.heal_request_command = None + self.item_queue = [] + + + async def deathlink_kill_player(self, ctx): + from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read + + validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) + validation = validation[0] | (validation[1] << 8) + if validation != 0xDEAD: + return + + receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) + menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) + gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) + can_move = await snes_read(ctx, MMX_CAN_MOVE, 0x7) + pause_state = await snes_read(ctx, MMX_PAUSE_STATE, 0x1) + if menu_state[0] != 0x04 or \ + gameplay_state[0] != 0x04 or \ + pause_state[0] != 0x00 or \ + can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ + receiving_item[0] != 0x00: + return + + snes_buffered_write(ctx, MMX_CURRENT_HP, bytes([0x80])) + snes_buffered_write(ctx, WRAM_START + 0x00BAA, bytes([0x0C])) + snes_buffered_write(ctx, WRAM_START + 0x00C12, bytes([0x0C])) + snes_buffered_write(ctx, WRAM_START + 0x00BAB, bytes([0x00])) + + await snes_flush_writes(ctx) + + ctx.death_state = DeathState.dead + ctx.last_death_link = time.time() + + + async def validate_rom(self, ctx): + from SNIClient import snes_read + + energy_link = await snes_read(ctx, MMX_ENERGY_LINK_ENABLED, 0x1) + rom_name = await snes_read(ctx, MMX_ROMHASH_START, ROMHASH_SIZE) + if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:4] != b"MMX1": + if "pool" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("pool") + if "heal" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("heal") + if "autoheal" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("autoheal") + return False + + ctx.game = self.game + ctx.items_handling = 0b111 + ctx.receive_option = 0 + ctx.send_option = 0 + ctx.allow_collect = True + if energy_link: + if "pool" not in ctx.command_processor.commands: + ctx.command_processor.commands["pool"] = cmd_pool + if "heal" not in ctx.command_processor.commands: + ctx.command_processor.commands["heal"] = cmd_heal + if "autoheal" not in ctx.command_processor.commands: + ctx.command_processor.commands["autoheal"] = cmd_autoheal + + death_link = await snes_read(ctx, MMX_DEATH_LINK_ACTIVE, 1) + if death_link[0]: + await ctx.update_death_link(bool(death_link[0] & 0b1)) + + ctx.rom = rom_name + + return True + + + async def handle_energy_link(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + + # Deposit heals into the pool regardless of energy_link setting + energy_packet = await snes_read(ctx, MMX_ENERGY_LINK_PACKET, 0x2) + if energy_packet is None: + return + energy_packet_raw = energy_packet[0] | (energy_packet[1] << 8) + energy_packet = (energy_packet_raw * HP_EXCHANGE_RATE) >> 4 + if energy_packet != 0: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": + [{"operation": "add", "value": energy_packet}, + {"operation": "max", "value": 0}], + }]) + pool = ((ctx.stored_data[f'EnergyLink{ctx.team}'] or 0) / HP_EXCHANGE_RATE) + (energy_packet_raw / 16) + logger.info(f"Deposited {energy_packet_raw / 16:.2f} into the healing pool. Healing available: {pool:.2f}") + snes_buffered_write(ctx, MMX_ENERGY_LINK_PACKET, bytearray([0x00, 0x00])) + await snes_flush_writes(ctx) + + energy_link = await snes_read(ctx, MMX_ENERGY_LINK_ENABLED, 0x1) + if energy_link is None: + return + + if energy_link[0]: + validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) + if validation is None: + return + validation = validation[0] | (validation[1] << 8) + if validation != 0xDEAD: + return + + receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) + menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) + gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) + can_move = await snes_read(ctx, MMX_CAN_MOVE, 0x7) + pause_state = await snes_read(ctx, MMX_PAUSE_STATE, 0x1) + if menu_state[0] != 0x04 or \ + gameplay_state[0] != 0x04 or \ + pause_state[0] != 0x00 or \ + can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ + receiving_item[0] != 0x00: + return + + if any(item in self.item_queue for item in HP_REFILLS): + logger.info(f"Can't provide a heal. You already have a heal in queue.") + self.heal_request_command = None + return + + pool = ctx.stored_data[f'EnergyLink{ctx.team}'] or 0 + # Perform auto heals + if self.auto_heal: + if self.heal_request_command is None: + if pool < HP_EXCHANGE_RATE: + return + current_hp = await snes_read(ctx, MMX_CURRENT_HP, 0x1) + max_hp = await snes_read(ctx, MMX_MAX_HP, 0x1) + if max_hp[0] > current_hp[0]: + self.heal_request_command = max_hp[0] - current_hp[0] + + # Handle manual heal requests + if self.heal_request_command: + heal_needed = self.heal_request_command + heal_needed_rate = heal_needed * HP_EXCHANGE_RATE + if pool < HP_EXCHANGE_RATE: + logger.info(f"There's not enough healing for your request ({heal_needed}). Healing available: {pool / HP_EXCHANGE_RATE:.2f}") + self.heal_request_command = None + return + elif pool < heal_needed_rate: + heal_needed = int(pool / HP_EXCHANGE_RATE) + heal_needed_rate = heal_needed * HP_EXCHANGE_RATE + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": + [{"operation": "add", "value": -heal_needed_rate}, + {"operation": "max", "value": 0}], + }]) + self.add_item_to_queue("hp refill", None, self.heal_request_command) + pool = (pool / HP_EXCHANGE_RATE) - heal_needed + logger.info(f"Healed by {heal_needed}. Healing available: {pool:.2f}") + self.heal_request_command = None + + + def add_item_to_queue(self, item_type, item_id, item_additional = None): + if not hasattr(self, "item_queue"): + self.item_queue = [] + self.item_queue.append([item_type, item_id, item_additional]) + + + async def handle_item_queue(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + from worlds.mmx.Rom import weapon_rom_data, upgrades_rom_data + + if not hasattr(self, "item_queue") or len(self.item_queue) == 0: + return + + validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) + if validation is None: + return + validation = validation[0] | (validation[1] << 8) + if validation != 0xDEAD: + return + + next_item = self.item_queue[0] + item_id = next_item[1] + + if next_item[0] == "boss access": + snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) + snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x2D])) + self.item_queue.pop(0) + return + + # Do not give items if you can't move, are in pause state, not in the correct mode or not in gameplay state + receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) + menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) + gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) + can_move = await snes_read(ctx, MMX_CAN_MOVE, 0x7) + pause_state = await snes_read(ctx, MMX_PAUSE_STATE, 0x1) + if menu_state[0] != 0x04 or \ + gameplay_state[0] != 0x04 or \ + pause_state[0] != 0x00 or \ + can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ + receiving_item[0] != 0x00: + return + + if next_item[0] in PICKUP_ITEMS: + backup_item = self.item_queue.pop(0) + + if "hp refill" in next_item[0]: + current_hp = await snes_read(ctx, MMX_CURRENT_HP, 0x1) + max_hp = await snes_read(ctx, MMX_MAX_HP, 0x1) + + if current_hp[0] < max_hp[0]: + snes_buffered_write(ctx, MMX_ENABLE_HP_REFILL, bytearray([0x02])) + if next_item[0] == "small hp refill": + snes_buffered_write(ctx, MMX_HP_REFILL_AMOUNT, bytearray([0x02])) + elif next_item[0] == "large hp refill": + snes_buffered_write(ctx, MMX_HP_REFILL_AMOUNT, bytearray([0x08])) + else: + snes_buffered_write(ctx, MMX_HP_REFILL_AMOUNT, bytearray([next_item[2]])) + snes_buffered_write(ctx, MMX_RECEIVING_ITEM, bytearray([0x01])) + else: + # TODO: Sub Tank logic + self.item_queue.append(backup_item) + + elif next_item[0] == "1up": + life_count = await snes_read(ctx, MMX_LIFE_COUNT, 0x1) + if life_count[0] < 9: + snes_buffered_write(ctx, MMX_ENABLE_GIVE_1UP, bytearray([0x01])) + snes_buffered_write(ctx, MMX_RECEIVING_ITEM, bytearray([0x01])) + pass + else: + self.item_queue.append(backup_item) + + if next_item[0] == "weapon": + weapon = weapon_rom_data[item_id] + snes_buffered_write(ctx, WRAM_START + weapon[0], bytearray([weapon[1]])) + snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) + snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x0D])) + self.item_queue.pop(0) + + elif next_item[0] == "heart tank": + heart_tanks = await snes_read(ctx, MMX_HEART_TANKS, 0x1) + heart_tanks = heart_tanks[0] + heart_tank_count = heart_tanks.bit_count() + if heart_tank_count < 8: + heart_tanks |= 1 << heart_tank_count + snes_buffered_write(ctx, MMX_HEART_TANKS, bytearray([heart_tanks])) + snes_buffered_write(ctx, MMX_ENABLE_HEART_TANK, bytearray([0x02])) + snes_buffered_write(ctx, MMX_RECEIVING_ITEM, bytearray([0x01])) + self.item_queue.pop(0) + + elif next_item[0] == "sub tank": + upgrades = await snes_read(ctx, MMX_UPGRADES, 0x1) + sub_tanks = await snes_read(ctx, MMX_SUB_TANK_ARRAY, 0x4) + sub_tanks = list(sub_tanks) + upgrade = upgrades[0] + upgrade = upgrade & 0xF0 + sub_tank_count = upgrade.bit_count() + if sub_tank_count < 4: + upgrade = upgrades[0] + upgrade |= 0x10 << sub_tank_count + sub_tanks[sub_tank_count] = 0x8E + snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrade])) + snes_buffered_write(ctx, MMX_SUB_TANK_ARRAY, bytearray(sub_tanks)) + snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) + snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x17])) + self.item_queue.pop(0) + + elif next_item[0] == "upgrade": + upgrades = await snes_read(ctx, MMX_UPGRADES, 0x1) + + upgrade = upgrades_rom_data[item_id] + bit = 1 << upgrade[0] + check = upgrades[0] & bit + + if check == 0: + # Armor + upgrades = upgrades[0] + upgrades |= bit + snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) + if bit == 0x01: + snes_buffered_write(ctx, WRAM_START + 0x0BBE, bytearray([0x18])) + elif bit == 0x02: + value = await snes_read(ctx, WRAM_START + 0x0C38, 0x1) + snes_buffered_write(ctx, WRAM_START + 0x0C38, bytearray([value[0] + 1])) + snes_buffered_write(ctx, WRAM_START + 0x0C42, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C43, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C39, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C48, bytearray([0x5D])) + elif bit == 0x04: + value = await snes_read(ctx, WRAM_START + 0x0C58, 0x1) + snes_buffered_write(ctx, WRAM_START + 0x0C58, bytearray([value[0] + 1])) + snes_buffered_write(ctx, WRAM_START + 0x0C62, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C63, bytearray([0x01])) + snes_buffered_write(ctx, WRAM_START + 0x0C59, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C68, bytearray([0x5D])) + elif bit == 0x08: + value = await snes_read(ctx, WRAM_START + 0x0C78, 0x1) + snes_buffered_write(ctx, WRAM_START + 0x0C78, bytearray([value[0] + 1])) + snes_buffered_write(ctx, WRAM_START + 0x0C82, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C83, bytearray([0x02])) + snes_buffered_write(ctx, WRAM_START + 0x0C79, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C88, bytearray([0x5D])) + snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) + snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x2B])) + self.item_queue.pop(0) + + await snes_flush_writes(ctx) + + + async def game_watcher(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + + game_state = await snes_read(ctx, MMX_GAME_STATE, 0x1) + menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) + gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) + + # Discard uninitialized ROMs + if menu_state is None: + self.game_state = False + self.energy_link_enabled = False + ctx.item_queue = [] + return + + if game_state[0] == 0: + self.game_state = False + ctx.item_queue = [] + return + + validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) + validation = validation[0] | (validation[1] << 8) + if validation != 0xDEAD: + snes_logger.info(f'ROM not properly validated.') + self.game_state = False + return + + self.game_state = True + if "DeathLink" in ctx.tags and menu_state[0] == 0x04 and ctx.last_death_link + 1 < time.time(): + currently_dead = gameplay_state[0] == 0x06 + await ctx.handle_deathlink_state(currently_dead) + + await self.handle_item_queue(ctx) + + # This is going to be rewritten whenever SNIClient supports on_package + energy_link = await snes_read(ctx, MMX_ENERGY_LINK_ENABLED, 0x1) + if energy_link[0] != 0: + if self.energy_link_enabled and f'EnergyLink{ctx.team}' in ctx.stored_data: + await self.handle_energy_link(ctx) + + if ctx.server and ctx.server.socket.open and not self.energy_link_enabled and ctx.team is not None: + self.energy_link_enabled = True + ctx.set_notify(f"EnergyLink{ctx.team}") + logger.info(f"Initialized EnergyLink{ctx.team}") + + from worlds.mmx.Rom import weapon_rom_data, upgrades_rom_data, boss_access_rom_data, refill_rom_data + from worlds.mmx.Levels import location_id_to_level_id + from worlds import AutoWorldRegister + + defeated_bosses_data = await snes_read(ctx, MMX_DEFEATED_BOSSES, 0x20) + defeated_bosses = list(defeated_bosses_data) + cleared_levels_data = await snes_read(ctx, MMX_COMPLETED_LEVELS, 0x20) + cleared_levels = list(cleared_levels_data) + victory_ram = await snes_read(ctx, MMX_VICTORY, 0x1) + collected_heart_tanks_data = await snes_read(ctx, MMX_COLLECTED_HEART_TANKS, 0x01) + collected_upgrades_data = await snes_read(ctx, MMX_COLLECTED_UPGRADES, 0x01) + collected_hadouken_data = await snes_read(ctx, MMX_COLLECTED_HADOUKEN, 0x01) + collected_pickups_data = await snes_read(ctx, MMX_COLLECTED_PICKUPS, 0x20) + collected_pickups = list(collected_pickups_data) + pickupsanity_enabled = await snes_read(ctx, MMX_PICKUPSANITY_ACTIVE, 0x1) + new_checks = [] + for loc_name, data in location_id_to_level_id.items(): + loc_id = AutoWorldRegister.world_types[ctx.game].location_name_to_id[loc_name] + if loc_id not in ctx.locations_checked: + internal_id = data[1] + data_bit = data[2] + + if internal_id == 0x000: + # Boss clear + if defeated_bosses_data[data_bit] != 0: + snes_logger.info(f"loc_name: {loc_name} ({defeated_bosses_data[data_bit]}) [{data}]") + snes_logger.info(f"{defeated_bosses}") + new_checks.append(loc_id) + elif internal_id == 0x001: + # Maverick Medal + if cleared_levels_data[data_bit] != 0: + new_checks.append(loc_id) + elif internal_id == 0x002: + # Heart Tank + masked_data = collected_heart_tanks_data[0] & data_bit + if masked_data != 0: + new_checks.append(loc_id) + elif internal_id == 0x003: + # Mega Man upgrades + masked_data = collected_upgrades_data[0] & data_bit + if masked_data != 0: + new_checks.append(loc_id) + elif internal_id == 0x004: + # Sub Tank + masked_data = collected_upgrades_data[0] & data_bit + if masked_data != 0: + new_checks.append(loc_id) + elif internal_id == 0x005: + # Hadouken + if collected_hadouken_data[0] != 0x00: + new_checks.append(loc_id) + elif internal_id == 0x006: + # Victory + if menu_state[0] == 0x02 and gameplay_state[0] == 0x16 and victory_ram[0] == 0x01: + new_checks.append(loc_id) + elif internal_id == 0x007: + # Intro + if game_state[0] == 0x02 and menu_state[0] == 0x00 and gameplay_state[0] == 0x01: + new_checks.append(loc_id) + elif internal_id >= 0x100: + # Pickups + if not pickupsanity_enabled or pickupsanity_enabled[0] == 0: + continue + pickup_id = internal_id & 0x1F + if collected_pickups_data[pickup_id] != 0: + new_checks.append(loc_id) + + verify_game_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 1) + if verify_game_state is None: + snes_logger.info(f'Exit Game.') + return + + rom = await snes_read(ctx, MMX_ROMHASH_START, ROMHASH_SIZE) + if rom != ctx.rom: + ctx.rom = None + snes_logger.info(f'Exit ROM.') + return + + for new_check_id in new_checks: + ctx.locations_checked.add(new_check_id) + location = ctx.location_names[new_check_id] + snes_logger.info( + f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) + + recv_count = await snes_read(ctx, MMX_RECV_INDEX, 2) + if recv_count is None: + # Add a small failsafe in case we get a None. Other SNI games do this... + return + + recv_index = recv_count[0] | (recv_count[1] << 8) + + if recv_index < len(ctx.items_received): + item = ctx.items_received[recv_index] + recv_index += 1 + logging.info('Received %s from %s (%s) (%d/%d in list)' % ( + color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.player_names[item.player], 'yellow'), + ctx.location_names[item.location], recv_index, len(ctx.items_received))) + + snes_buffered_write(ctx, MMX_RECV_INDEX, bytes([recv_index])) + await snes_flush_writes(ctx) + + if item.item in weapon_rom_data: + self.add_item_to_queue("weapon", item.item) + + elif item.item == STARTING_ID + 0x0013: + self.add_item_to_queue("heart tank", item.item) + + elif item.item == STARTING_ID + 0x0014: + self.add_item_to_queue("sub tank", item.item) + + elif item.item in upgrades_rom_data: + self.add_item_to_queue("upgrade", item.item) + + elif item.item in boss_access_rom_data: + if item.item == STARTING_ID + 0x000A: + snes_buffered_write(ctx, MMX_SIGMA_ACCESS, bytearray([0x00])) + boss_access = await snes_read(ctx, MMX_UNLOCKED_LEVELS, 0x20) + boss_access = bytearray(boss_access) + level = boss_access_rom_data[item.item] + boss_access[level[0]] = 0x01 + snes_buffered_write(ctx, MMX_UNLOCKED_LEVELS, boss_access) + self.add_item_to_queue("boss access", item.item) + + elif item.item in refill_rom_data: + self.add_item_to_queue(refill_rom_data[item.item][0], item.item) + + elif item.item == STARTING_ID: + # Handle goal + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + return + + # Handle collected locations + game_state = await snes_read(ctx, MMX_GAME_STATE, 0x1) + if game_state[0] != 0x02: + return + new_boss_clears = False + new_cleared_level = False + new_heart_tank = False + new_upgrade = False + new_pickup = False + new_hadouken = False + cleared_levels_data = await snes_read(ctx, MMX_COMPLETED_LEVELS, 0x20) + cleared_levels = list(cleared_levels_data) + collected_pickups_data = await snes_read(ctx, MMX_COLLECTED_PICKUPS, 0x40) + collected_pickups = list(collected_pickups_data) + collected_heart_tanks_data = await snes_read(ctx, MMX_COLLECTED_HEART_TANKS, 0x01) + collected_heart_tanks_data = collected_heart_tanks_data[0] + collected_upgrades_data = await snes_read(ctx, MMX_COLLECTED_UPGRADES, 0x01) + collected_upgrades_data = collected_upgrades_data[0] + defeated_bosses_data = await snes_read(ctx, MMX_DEFEATED_BOSSES, 0x20) + defeated_bosses = list(defeated_bosses_data) + collected_hadouken_data = await snes_read(ctx, MMX_COLLECTED_HADOUKEN, 0x01) + collected_hadouken_data = collected_hadouken_data[0] + i = 0 + for loc_id in ctx.checked_locations: + if loc_id not in ctx.locations_checked: + ctx.locations_checked.add(loc_id) + loc_name = ctx.location_names[loc_id] + + if loc_name not in location_id_to_level_id: + continue + + logging.info(f"Recovered checks ({i:03}): {loc_name}") + i += 1 + + data = location_id_to_level_id[loc_name] + level_id = data[0] + internal_id = data[1] + data_bit = data[2] + + if internal_id == 0x000: + # Boss clear + defeated_bosses[data_bit] = 1 + new_boss_clears = True + elif internal_id == 0x001: + # Maverick Medal + cleared_levels[data_bit] = 0xFF + new_cleared_level = True + elif internal_id == 0x002: + # Heart Tank + collected_heart_tanks_data |= data_bit + new_heart_tank = True + elif internal_id == 0x003: + # Mega Man upgrades + collected_upgrades_data |= data_bit + new_upgrade = True + elif internal_id == 0x004: + # Sub Tank + collected_upgrades_data |= data_bit + new_upgrade = True + elif internal_id == 0x005: + # Hadouken + collected_hadouken_data = 0xFF + new_hadouken = True + elif internal_id >= 0x100: + # Pickups + pickup_id = internal_id & 0x3F + collected_pickups[pickup_id] = 0xFF + new_pickup = True + + if new_cleared_level: + snes_buffered_write(ctx, MMX_COMPLETED_LEVELS, bytes(cleared_levels)) + if new_boss_clears: + snes_buffered_write(ctx, MMX_DEFEATED_BOSSES, bytes(defeated_bosses)) + if new_pickup: + snes_buffered_write(ctx, MMX_COLLECTED_PICKUPS, bytes(collected_pickups)) + if new_hadouken: + snes_buffered_write(ctx, MMX_COLLECTED_PICKUPS, bytearray([collected_hadouken_data])) + if new_upgrade: + snes_buffered_write(ctx, MMX_COLLECTED_UPGRADES, bytearray([collected_upgrades_data])) + if new_heart_tank: + snes_buffered_write(ctx, MMX_COLLECTED_HEART_TANKS, bytearray([collected_heart_tanks_data])) + await snes_flush_writes(ctx) + + +def cmd_pool(self): + """ + Check how much healing is in the pool. + """ + if self.ctx.game != "Mega Man X": + logger.warning("This command can only be used while playing Mega Man X3") + if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: + logger.info(f"Must be connected to server and in game.") + else: + pool = (self.ctx.stored_data[f'EnergyLink{self.ctx.team}'] or 0) / HP_EXCHANGE_RATE + logger.info(f"Healing available: {pool:.2f}") + + +def cmd_heal(self, amount: str = ""): + """ + Request healing from EnergyLink. + """ + if self.ctx.game != "Mega Man X": + logger.warning("This command can only be used while playing Mega Man X3") + if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: + logger.info(f"Must be connected to server and in game.") + else: + if self.ctx.client_handler.heal_request_command is not None: + logger.info(f"You already placed a healing request.") + return + if amount: + try: + amount = int(amount) + except: + logger.info(f"You need to specify how much HP you will recover.") + return + if amount > 16: + self.ctx.client_handler.heal_request_command = 16 + self.ctx.client_handler.heal_request_command = amount + logger.info(f"Requested {amount} HP from the healing pool.") + else: + logger.info(f"You need to specify how much HP you will request.") + + +def cmd_autoheal(self): + """ + Enable auto heal from EnergyLink. + """ + if self.ctx.game != "Mega Man X": + logger.warning("This command can only be used while playing Mega Man X3") + if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: + logger.info(f"Must be connected to server and in game.") + else: + if self.ctx.client_handler.auto_heal: + self.ctx.client_handler.auto_heal = False + logger.info(f"Auto healing disabled.") + else: + self.ctx.client_handler.auto_heal = True + logger.info(f"Auto healing enabled.") diff --git a/worlds/mmx/Items.py b/worlds/mmx/Items.py new file mode 100644 index 000000000000..9127c6b8b48d --- /dev/null +++ b/worlds/mmx/Items.py @@ -0,0 +1,106 @@ +import typing + +from BaseClasses import Item, ItemClassification +from .Names import ItemName + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + progression: bool + trap: bool = False + quantity: int = 1 + +STARTING_ID = 0xBE0800 + +class MMXItem(Item): + game = "Mega Man X" + +# Item tables +event_table = { + ItemName.victory: ItemData(STARTING_ID + 0x0000, True), + ItemName.maverick_medal: ItemData(STARTING_ID + 0x0001, True), +} + +access_codes_table = { + ItemName.stage_armored_armadillo: ItemData(STARTING_ID + 0x0002, True), + ItemName.stage_boomer_kuwanger: ItemData(STARTING_ID + 0x0003, True), + ItemName.stage_chill_penguin: ItemData(STARTING_ID + 0x0004, True), + ItemName.stage_flame_mammoth: ItemData(STARTING_ID + 0x0005, True), + ItemName.stage_launch_octopus: ItemData(STARTING_ID + 0x0006, True), + ItemName.stage_spark_mandrill: ItemData(STARTING_ID + 0x0007, True), + ItemName.stage_sting_chameleon: ItemData(STARTING_ID + 0x0008, True), + ItemName.stage_storm_eagle: ItemData(STARTING_ID + 0x0009, True), + ItemName.stage_sigma_fortress: ItemData(STARTING_ID + 0x000A, True), +} + +weapons = { + ItemName.shotgun_ice: ItemData(STARTING_ID + 0x000B, True), + ItemName.electric_spark: ItemData(STARTING_ID + 0x000C, True), + ItemName.rolling_shield: ItemData(STARTING_ID + 0x000D, True), + ItemName.homing_torpedo: ItemData(STARTING_ID + 0x000E, True), + ItemName.boomerang_cutter: ItemData(STARTING_ID + 0x000F, True), + ItemName.chameleon_sting: ItemData(STARTING_ID + 0x0010, True), + ItemName.storm_tornado: ItemData(STARTING_ID + 0x0011, True), + ItemName.fire_wave: ItemData(STARTING_ID + 0x0012, True), + #ItemName.hadouken: ItemData(STARTING_ID + 0x001A, True) +} + +tanks_table = { + ItemName.heart_tank: ItemData(STARTING_ID + 0x0013, True), + ItemName.sub_tank: ItemData(STARTING_ID + 0x0014, True), +} + +upgrade_table = { + ItemName.helmet: ItemData(STARTING_ID + 0x001C, True), + ItemName.body: ItemData(STARTING_ID + 0x001D, True), + ItemName.arms: ItemData(STARTING_ID + 0x001E, True), + ItemName.legs: ItemData(STARTING_ID + 0x001F, True), +} + +junk_table = { + ItemName.small_hp: ItemData(STARTING_ID + 0x0030, False), + ItemName.large_hp: ItemData(STARTING_ID + 0x0031, False), + ItemName.life: ItemData(STARTING_ID + 0x0034, False), +} + +item_groups = { + "Weapons": { + ItemName.shotgun_ice, + ItemName.electric_spark, + ItemName.rolling_shield, + ItemName.homing_torpedo, + ItemName.boomerang_cutter, + ItemName.chameleon_sting, + ItemName.storm_tornado, + ItemName.fire_wave, + }, + "Armor Upgrades": { + ItemName.helmet, + ItemName.body, + ItemName.arms, + ItemName.legs, + }, + "Access Codes": { + ItemName.stage_armored_armadillo, + ItemName.stage_boomer_kuwanger, + ItemName.stage_chill_penguin, + ItemName.stage_flame_mammoth, + ItemName.stage_launch_octopus, + ItemName.stage_spark_mandrill, + ItemName.stage_sting_chameleon, + ItemName.stage_storm_eagle, + ItemName.stage_sigma_fortress, + } +} + +# Complete item table. +item_table = { + **event_table, + **access_codes_table, + **weapons, + **upgrade_table, + **tanks_table, + **junk_table, +} + +lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} \ No newline at end of file diff --git a/worlds/mmx/Levels.py b/worlds/mmx/Levels.py new file mode 100644 index 000000000000..db30b9a5e131 --- /dev/null +++ b/worlds/mmx/Levels.py @@ -0,0 +1,75 @@ + +from worlds.AutoWorld import World +from .Names import LocationName + +location_id_to_level_id = { + LocationName.intro_completed: [0x00, 0x007, 0x00], + LocationName.intro_mini_boss_1: [0x03, 0x000, 0x1C], + LocationName.intro_mini_boss_2: [0x03, 0x000, 0x1D], + + LocationName.armored_armadillo_boss: [0x03, 0x000, 0x00], + LocationName.armored_armadillo_clear: [0x03, 0x001, 0x04], + LocationName.armored_armadillo_sub_tank: [0x03, 0x004, 0x20], + LocationName.armored_armadillo_heart_tank: [0x03, 0x002, 0x02], + LocationName.armored_armadillo_hadouken: [0x03, 0x005, 0x00], + LocationName.armored_armadillo_mini_boss_1: [0x03, 0x000, 0x15], + LocationName.armored_armadillo_mini_boss_2: [0x03, 0x000, 0x16], + + LocationName.chill_penguin_boss: [0x08, 0x000, 0x01], + LocationName.chill_penguin_clear: [0x08, 0x001, 0x0E], + LocationName.chill_penguin_legs: [0x08, 0x003, 0x08], + LocationName.chill_penguin_heart_tank: [0x08, 0x002, 0x01], + + LocationName.spark_mandrill_boss: [0x06, 0x000, 0x02], + LocationName.spark_mandrill_clear: [0x06, 0x001, 0x0A], + LocationName.spark_mandrill_sub_tank: [0x06, 0x004, 0x40], + LocationName.spark_mandrill_heart_tank: [0x06, 0x002, 0x40], + LocationName.spark_mandrill_mini_boss: [0x06, 0x000, 0x17], + + LocationName.launch_octopus_boss: [0x01, 0x000, 0x03], + LocationName.launch_octopus_clear: [0x01, 0x001, 0x00], + LocationName.launch_octopus_heart_tank: [0x01, 0x002, 0x80], + LocationName.launch_octopus_mini_boss_1: [0x01, 0x000, 0x18], + LocationName.launch_octopus_mini_boss_2: [0x01, 0x000, 0x19], + LocationName.launch_octopus_mini_boss_3: [0x01, 0x000, 0x1A], + LocationName.launch_octopus_mini_boss_4: [0x01, 0x000, 0x1B], + + LocationName.boomer_kuwanger_boss: [0x07, 0x000, 0x04], + LocationName.boomer_kuwanger_clear: [0x07, 0x001, 0x0C], + LocationName.boomer_kuwanger_heart_tank: [0x07, 0x002, 0x20], + + LocationName.sting_chameleon_boss: [0x02, 0x000, 0x05], + LocationName.sting_chameleon_clear: [0x02, 0x001, 0x02], + LocationName.sting_chameleon_body: [0x02, 0x003, 0x04], + LocationName.sting_chameleon_heart_tank: [0x02, 0x002, 0x08], + + LocationName.storm_eagle_boss: [0x05, 0x000, 0x06], + LocationName.storm_eagle_clear: [0x05, 0x001, 0x08], + LocationName.storm_eagle_sub_tank: [0x05, 0x004, 0x10], + LocationName.storm_eagle_heart_tank: [0x05, 0x002, 0x04], + LocationName.storm_eagle_helmet: [0x05, 0x003, 0x01], + + LocationName.flame_mammoth_boss: [0x04, 0x000, 0x07], + LocationName.flame_mammoth_clear: [0x04, 0x001, 0x06], + LocationName.flame_mammoth_sub_tank: [0x04, 0x004, 0x80], + LocationName.flame_mammoth_heart_tank: [0x04, 0x002, 0x10], + LocationName.flame_mammoth_arms: [0x04, 0x003, 0x02], + + LocationName.sigma_fortress_1_bospider: [0x09, 0x000, 0x08], + LocationName.sigma_fortress_1_vile: [0x09, 0x000, 0x09], + LocationName.sigma_fortress_1_boomer_kuwanger: [0x09, 0x000, 0x0A], + + LocationName.sigma_fortress_2_chill_penguin: [0x0A, 0x000, 0x0B], + LocationName.sigma_fortress_2_storm_eagle: [0x0A, 0x000, 0x0C], + LocationName.sigma_fortress_2_rangda_bangda: [0x0A, 0x000, 0x0D], + + LocationName.sigma_fortress_3_armored_armadillo:[0x0B, 0x000, 0x0E], + LocationName.sigma_fortress_3_sting_chameleon: [0x0B, 0x000, 0x0F], + LocationName.sigma_fortress_3_spark_mandrill: [0x0B, 0x000, 0x10], + LocationName.sigma_fortress_3_launch_octopus: [0x0B, 0x000, 0x11], + LocationName.sigma_fortress_3_flame_mammoth: [0x0B, 0x000, 0x12], + LocationName.sigma_fortress_3_d_rex: [0x0B, 0x000, 0x1E], + + LocationName.sigma_fortress_4_velguarder: [0x0C, 0x000, 0x13], + LocationName.sigma_fortress_4_sigma: [0x0C, 0x006, 0x14], +} diff --git a/worlds/mmx/Locations.py b/worlds/mmx/Locations.py new file mode 100644 index 000000000000..d30ebefd4fcf --- /dev/null +++ b/worlds/mmx/Locations.py @@ -0,0 +1,174 @@ +import typing + +from BaseClasses import Location +from worlds.AutoWorld import World +from .Names import LocationName + +class MMXLocation(Location): + game = "Mega Man X" + + def __init__(self, player: int, name: str = '', address: int = None, parent=None): + super().__init__(player, name, address, parent) + +STARTING_ID = 0xBE0800 + +stage_location_table = { + LocationName.armored_armadillo_boss: STARTING_ID + 0x0000, + LocationName.chill_penguin_boss: STARTING_ID + 0x0001, + LocationName.spark_mandrill_boss: STARTING_ID + 0x0002, + LocationName.launch_octopus_boss: STARTING_ID + 0x0003, + LocationName.boomer_kuwanger_boss: STARTING_ID + 0x0004, + LocationName.sting_chameleon_boss: STARTING_ID + 0x0005, + LocationName.storm_eagle_boss: STARTING_ID + 0x0006, + LocationName.flame_mammoth_boss: STARTING_ID + 0x0007, + LocationName.sigma_fortress_1_bospider: STARTING_ID + 0x0008, + LocationName.sigma_fortress_1_vile: STARTING_ID + 0x0009, + LocationName.sigma_fortress_1_boomer_kuwanger: STARTING_ID + 0x000A, + LocationName.sigma_fortress_2_chill_penguin: STARTING_ID + 0x000B, + LocationName.sigma_fortress_2_storm_eagle: STARTING_ID + 0x000C, + LocationName.sigma_fortress_2_rangda_bangda: STARTING_ID + 0x000D, + LocationName.sigma_fortress_3_armored_armadillo: STARTING_ID + 0x000E, + LocationName.sigma_fortress_3_sting_chameleon: STARTING_ID + 0x000F, + LocationName.sigma_fortress_3_spark_mandrill: STARTING_ID + 0x0010, + LocationName.sigma_fortress_3_launch_octopus: STARTING_ID + 0x0011, + LocationName.sigma_fortress_3_flame_mammoth: STARTING_ID + 0x0012, + LocationName.sigma_fortress_3_d_rex: STARTING_ID + 0x0013, + LocationName.sigma_fortress_4_velguarder: STARTING_ID + 0x0014, + LocationName.sigma_fortress_4_sigma: STARTING_ID + 0x0015, + LocationName.intro_completed: STARTING_ID + 0x0016, + LocationName.intro_mini_boss_1: STARTING_ID + 0x001E, + LocationName.intro_mini_boss_2: STARTING_ID + 0x001F, + LocationName.launch_octopus_mini_boss_1: STARTING_ID + 0x0017, + LocationName.launch_octopus_mini_boss_2: STARTING_ID + 0x0018, + LocationName.launch_octopus_mini_boss_3: STARTING_ID + 0x0019, + LocationName.launch_octopus_mini_boss_4: STARTING_ID + 0x001A, + LocationName.spark_mandrill_mini_boss: STARTING_ID + 0x001B, + LocationName.armored_armadillo_mini_boss_1: STARTING_ID + 0x001C, + LocationName.armored_armadillo_mini_boss_2: STARTING_ID + 0x001D, +} + +tank_pickups = { + LocationName.armored_armadillo_heart_tank: STARTING_ID + 0x0030, + LocationName.armored_armadillo_sub_tank: STARTING_ID + 0x0031, + LocationName.chill_penguin_heart_tank: STARTING_ID + 0x0032, + LocationName.spark_mandrill_sub_tank: STARTING_ID + 0x0033, + LocationName.spark_mandrill_heart_tank: STARTING_ID + 0x0034, + LocationName.launch_octopus_heart_tank: STARTING_ID + 0x0035, + LocationName.boomer_kuwanger_heart_tank: STARTING_ID + 0x0036, + LocationName.sting_chameleon_heart_tank: STARTING_ID + 0x0037, + LocationName.storm_eagle_heart_tank: STARTING_ID + 0x0038, + LocationName.storm_eagle_sub_tank: STARTING_ID + 0x0039, + LocationName.flame_mammoth_heart_tank: STARTING_ID + 0x003A, + LocationName.flame_mammoth_sub_tank: STARTING_ID + 0x003B, +} + +upgrade_pickups = { + LocationName.armored_armadillo_hadouken: STARTING_ID + 0x0040, + LocationName.chill_penguin_legs: STARTING_ID + 0x0041, + LocationName.sting_chameleon_body: STARTING_ID + 0x0042, + LocationName.storm_eagle_helmet: STARTING_ID + 0x0043, + LocationName.flame_mammoth_arms: STARTING_ID + 0x0044, +} + +pickup_sanity = { + LocationName.intro_hp_1: STARTING_ID + 0x0050, + LocationName.intro_hp_2: STARTING_ID + 0x0051, + LocationName.armored_armadillo_hp_1: STARTING_ID + 0x0052, + LocationName.armored_armadillo_hp_2: STARTING_ID + 0x0053, + LocationName.armored_armadillo_hp_3: STARTING_ID + 0x0054, + LocationName.launch_octopus_hp_1: STARTING_ID + 0x0055, + LocationName.sting_chameleon_1up: STARTING_ID + 0x0056, + LocationName.sting_chameleon_hp_1: STARTING_ID + 0x0057, + LocationName.storm_eagle_1up: STARTING_ID + 0x0058, + LocationName.storm_eagle_hp_1: STARTING_ID + 0x0059, + LocationName.storm_eagle_energy_1: STARTING_ID + 0x005A, + LocationName.flame_mammoth_hp_1: STARTING_ID + 0x005B, + LocationName.flame_mammoth_hp_2: STARTING_ID + 0x005C, + LocationName.flame_mammoth_1up: STARTING_ID + 0x005D, + LocationName.sigma_fortress_3_hp_1: STARTING_ID + 0x005E, + LocationName.sigma_fortress_3_hp_2: STARTING_ID + 0x005F, + LocationName.sigma_fortress_3_energy_1: STARTING_ID + 0x0060, + LocationName.sigma_fortress_3_hp_3: STARTING_ID + 0x0061, + LocationName.sigma_fortress_3_energy_2: STARTING_ID + 0x0062, + LocationName.sigma_fortress_3_hp_4: STARTING_ID + 0x0063, + LocationName.sigma_fortress_3_energy_3: STARTING_ID + 0x0064, + LocationName.sigma_fortress_3_1up: STARTING_ID + 0x0065, +} + +stage_clears = { + LocationName.armored_armadillo_clear: STARTING_ID + 0x0070, + LocationName.chill_penguin_clear: STARTING_ID + 0x0071, + LocationName.spark_mandrill_clear: STARTING_ID + 0x0072, + LocationName.launch_octopus_clear: STARTING_ID + 0x0073, + LocationName.boomer_kuwanger_clear: STARTING_ID + 0x0074, + LocationName.sting_chameleon_clear: STARTING_ID + 0x0075, + LocationName.storm_eagle_clear: STARTING_ID + 0x0076, + LocationName.flame_mammoth_clear: STARTING_ID + 0x0077 +} + +all_locations = { + **stage_clears, + **stage_location_table, + **tank_pickups, + **upgrade_pickups, + **pickup_sanity +} + +location_table = {} + +location_groups = { + "Bosses": {location for location in all_locations.keys() if "Defeated" in location}, + "Heart Tanks": {location for location in all_locations.keys() if "- Heart Tank" in location}, + "Sub Tanks": {location for location in all_locations.keys() if "- Sub Tank" in location}, + "Upgrade Capsules": {location for location in all_locations.keys() if "Capsule" in location}, + "Intro Stage": {location for location in all_locations.keys() if "Intro Stage - " in location}, + "Armored Armadillo Stage": {location for location in all_locations.keys() if "Armored Armadillo Stage - " in location}, + "Chill Penguin Stage": {location for location in all_locations.keys() if "Chill Penguin Stage - " in location}, + "Spark Mandrill Stage": {location for location in all_locations.keys() if "Spark Mandrill Stage - " in location}, + "Launch Octopus Stage": {location for location in all_locations.keys() if "Launch Octopus Stage - " in location}, + "Boomer Kuwanger Stage": {location for location in all_locations.keys() if "Boomer Kuwanger Stage - " in location}, + "Sting Chameleon Stage": {location for location in all_locations.keys() if "Sting Chameleon Stage - " in location}, + "Storm Eagle Stage": {location for location in all_locations.keys() if "Storm Eagle Stage - " in location}, + "Flame Mammoth Stage": {location for location in all_locations.keys() if "Flame Mammoth Stage - " in location}, + "Sigma's Fortress Stage 1": { + LocationName.sigma_fortress_1_boomer_kuwanger, + LocationName.sigma_fortress_1_bospider, + LocationName.sigma_fortress_1_vile, + }, + "Sigma's Fortress Stage 2": { + LocationName.sigma_fortress_2_chill_penguin, + LocationName.sigma_fortress_2_rangda_bangda, + LocationName.sigma_fortress_2_storm_eagle, + }, + "Sigma's Fortress Stage 3": { + LocationName.sigma_fortress_3_1up, + LocationName.sigma_fortress_3_armored_armadillo, + LocationName.sigma_fortress_3_sting_chameleon, + LocationName.sigma_fortress_3_launch_octopus, + LocationName.sigma_fortress_3_flame_mammoth, + LocationName.sigma_fortress_3_spark_mandrill, + LocationName.sigma_fortress_3_d_rex, + LocationName.sigma_fortress_3_energy_1, + LocationName.sigma_fortress_3_energy_2, + LocationName.sigma_fortress_3_energy_3, + LocationName.sigma_fortress_3_hp_1, + LocationName.sigma_fortress_3_hp_2, + LocationName.sigma_fortress_3_hp_3, + LocationName.sigma_fortress_3_hp_4, + }, +} + +def setup_locations(world: World): + location_table = { + **stage_clears, + **stage_location_table, + **tank_pickups, + **upgrade_pickups, + } + + if world.options.pickupsanity.value: + location_table.update({**pickup_sanity}) + + return location_table + +lookup_id_to_name: typing.Dict[int, str] = {id: name for name, _ in all_locations.items()} diff --git a/worlds/mmx/Names/EventName.py b/worlds/mmx/Names/EventName.py new file mode 100644 index 000000000000..27738a960882 --- /dev/null +++ b/worlds/mmx/Names/EventName.py @@ -0,0 +1,12 @@ +armored_armadillo_clear = "Event: Armored Armadillo - Clear" +chill_penguin_clear = "Event: Chill Penguin - Clear" +flame_mammoth_clear = "Event: Flame Mammoth - Clear" +storm_eagle_clear = "Event: Storm Eagle - Clear" +sting_chameleon_clear = "Event: Sting Chameleon - Clear" +spark_mandrill_clear = "Event: Spark Mandrill - Clear" +boomer_kuwanger_clear = "Event: Boomer Kuwanger - Clear" +launch_octopus_clear = "Event: Launch Octopus - Clear" + +sigma_fortress_1_clear = "Event: Sigma Fortress 1 - Clear" +sigma_fortress_2_clear = "Event: Sigma Fortress 2 - Clear" +sigma_fortress_3_clear = "Event: Sigma Fortress 3 - Clear" \ No newline at end of file diff --git a/worlds/mmx/Names/ItemName.py b/worlds/mmx/Names/ItemName.py new file mode 100644 index 000000000000..030f7a05c006 --- /dev/null +++ b/worlds/mmx/Names/ItemName.py @@ -0,0 +1,40 @@ +# Stages +stage_armored_armadillo = "Armored Armadillo Access Codes" +stage_boomer_kuwanger = "Boomer Kuwanger Access Codes" +stage_chill_penguin = "Chill Penguin Access Codes" +stage_flame_mammoth = "Flame Mammoth Access Codes" +stage_launch_octopus = "Launch Octopus Access Codes" +stage_spark_mandrill = "Spark Mandrill Access Codes" +stage_sting_chameleon = "Sting Chameleon Access Codes" +stage_storm_eagle = "Storm Eagle Access Codes" +stage_sigma_fortress = "Sigma's Fortress Access Codes" + +# Armor +helmet = "Helmet Upgrade" +body = "Body Upgrade" +arms = "Arms Upgrade" +legs = "Legs Upgrade" + +# Weapons +shotgun_ice = "Shotgun Ice" +electric_spark = "Electric Spark" +rolling_shield = "Rolling Shield" +homing_torpedo = "Homing Torpedo" +boomerang_cutter = "Boomerang Cutter" +chameleon_sting = "Chameleon Sting" +storm_tornado = "Storm Tornado" +fire_wave = "Fire Wave" +hadouken = "Hadouken" + +# Tanks +heart_tank = "Heart Tank" +sub_tank = "Sub Tank" + +# Medals +maverick_medal = "Maverick Medal" +victory = "Sigma Destroyed" + +# Junk +small_hp = "Small HP Refill" +large_hp = "Large HP Refill" +life = "1-Up" diff --git a/worlds/mmx/Names/LocationName.py b/worlds/mmx/Names/LocationName.py new file mode 100644 index 000000000000..c9167560c1b0 --- /dev/null +++ b/worlds/mmx/Names/LocationName.py @@ -0,0 +1,91 @@ +intro_hp_1 = "Intro Stage - HP Pickup 1" +intro_hp_2 = "Intro Stage - HP Pickup 2" +intro_completed = "Intro Stage - Get Defeated By Vile" +intro_mini_boss_1 = "Intro Stage - Defeated Bee Blader #1" +intro_mini_boss_2 = "Intro Stage - Defeated Bee Blader #2" + +armored_armadillo_boss = "Defeated Armored Armadillo" +armored_armadillo_clear = "Armored Armadillo Clear" +armored_armadillo_sub_tank = "Armored Armadillo - Sub Tank" +armored_armadillo_hp_1 = "Armored Armadillo - HP Pickup 1" +armored_armadillo_hp_2 = "Armored Armadillo - HP Pickup 2" +armored_armadillo_heart_tank = "Armored Armadillo - Heart Tank" +armored_armadillo_hp_3 = "Armored Armadillo - HP Pickup 3" +armored_armadillo_hadouken = "Armored Armadillo - Hadouken Capsule" +armored_armadillo_mini_boss_1 = "Defeated Mole Borer #1" +armored_armadillo_mini_boss_2 = "Defeated Mole Borer #2" + +chill_penguin_boss = "Defeated Chill Penguin" +chill_penguin_clear = "Chill Penguin Clear" +chill_penguin_legs = "Chill Penguin Stage - Legs Capsule" +chill_penguin_heart_tank = "Chill Penguin Stage - Heart Tank" + +spark_mandrill_boss = "Defeated Spark Mandrill" +spark_mandrill_clear = "Spark Mandrill Clear" +spark_mandrill_mini_boss = "Defeated Thunder Slimer" +spark_mandrill_sub_tank = "Spark Mandrill Stage - Sub Tank" +spark_mandrill_heart_tank = "Spark Mandrill Stage - Heart Tank" + +launch_octopus_boss = "Defeated Launch Octopus" +launch_octopus_clear = "Launch Octopus Clear" +launch_octopus_mini_boss_1 = "Defeated Anglerge #1" +launch_octopus_mini_boss_2 = "Defeated Anglerge #2" +launch_octopus_mini_boss_3 = "Defeated Utuboros #1" +launch_octopus_mini_boss_4 = "Defeated Utuboros #2" +launch_octopus_hp_1 = "Launch Octopus - HP Pickup 1" +launch_octopus_heart_tank = "Launch Octopus Stage - Heart Tank" + +boomer_kuwanger_boss = "Defeated Boomer Kuwanger" +boomer_kuwanger_clear = "Boomer Kuwanger Clear" +boomer_kuwanger_heart_tank = "Boomer Kuwanger Stage - Heart Tank" + +sting_chameleon_boss = "Defeated Sting Chameleon" +sting_chameleon_clear = "Sting Chameleon Clear" +sting_chameleon_heart_tank = "Sting Chameleon - Heart Tank" +sting_chameleon_body = "Sting Chameleon Stage - Body Capsule" +sting_chameleon_1up = "Sting Chameleon Stage - 1-Up" +sting_chameleon_hp_1 = "Sting Chameleon Stage - HP Pickup 1" + +storm_eagle_boss = "Defeated Storm Eagle" +storm_eagle_clear = "Storm Eagle Clear" +storm_eagle_heart_tank = "Storm Eagle Stage - Heart Tank" +storm_eagle_sub_tank = "Storm Eagle Stage - Sub Tank" +storm_eagle_helmet = "Storm Eagle Stage - Helmet Capsule" +storm_eagle_1up = "Storm Eagle Stage - 1-Up" +storm_eagle_hp_1 = "Storm Eagle Stage - HP Pickup 1" +storm_eagle_energy_1 = "Storm Eagle Stage - Weapon Energy Pickup 1" + +flame_mammoth_boss = "Defeated Flame Mammoth" +flame_mammoth_clear = "Flame Mammoth Clear" +flame_mammoth_hp_1 = "Flame Mammoth Stage - HP Pickup 1" +flame_mammoth_arms = "Flame Mammoth Stage - Arms Capsule" +flame_mammoth_heart_tank = "Flame Mammoth Stage - Heart Tank" +flame_mammoth_hp_2 = "Flame Mammoth Stage - HP Pickup 2" +flame_mammoth_1up = "Flame Mammoth Stage - 1-Up" +flame_mammoth_sub_tank = "Flame Mammoth Stage - Sub Tank" + +sigma_fortress_1_bospider = "Defeated Bospider" +sigma_fortress_1_vile = "Defeated Vile" +sigma_fortress_1_boomer_kuwanger = "Defeated Boomer Kuwanger (Rematch)" + +sigma_fortress_2_chill_penguin = "Defeated Chill Penguin (Rematch)" +sigma_fortress_2_storm_eagle = "Defeated Storm Eagle (Rematch)" +sigma_fortress_2_rangda_bangda = "Defeated Rangda Bangda" + +sigma_fortress_3_armored_armadillo = "Defeated Armored Armadillo (Rematch)" +sigma_fortress_3_hp_1 = "Sigma's Fortress 3 Stage - HP Pickup 1" +sigma_fortress_3_sting_chameleon = "Defeated Sting Chameleon (Rematch)" +sigma_fortress_3_hp_2 = "Sigma's Fortress 3 Stage - HP Pickup 2" +sigma_fortress_3_energy_1 = "Sigma's Fortress 3 Stage - Weapon Energy Pickup 1" +sigma_fortress_3_spark_mandrill = "Defeated Armored Spark Mandrill (Rematch)" +sigma_fortress_3_hp_3 = "Sigma's Fortress 3 Stage - HP Pickup 3" +sigma_fortress_3_energy_2 = "Sigma's Fortress 3 Stage - Weapon Energy Pickup 2" +sigma_fortress_3_launch_octopus = "Defeated Launch Octopus (Rematch)" +sigma_fortress_3_hp_4 = "Sigma's Fortress 3 Stage - HP Pickup 4" +sigma_fortress_3_energy_3 = "Sigma's Fortress 3 Stage - Weapon Energy Pickup 3" +sigma_fortress_3_1up = "Sigma's Fortress 3 Stage - 1-Up" +sigma_fortress_3_flame_mammoth = "Defeated Flame Mammoth (Rematch)" +sigma_fortress_3_d_rex = "Defeated D-Rex" + +sigma_fortress_4_velguarder = "Defeated Velguarder" +sigma_fortress_4_sigma = "Defeated Sigma" diff --git a/worlds/mmx/Names/RegionName.py b/worlds/mmx/Names/RegionName.py new file mode 100644 index 000000000000..925d03b76b48 --- /dev/null +++ b/worlds/mmx/Names/RegionName.py @@ -0,0 +1,84 @@ +intro = "Intro" + +armored_armadillo = "Armored Armadillo" +armored_armadillo_ride_1 = "Armored Armadillo - Ride 1" +armored_armadillo_excavator_1 = "Armored Armadillo - Excavator 1" +armored_armadillo_ride_2 = "Armored Armadillo - Ride 2" +armored_armadillo_excavator_2 = "Armored Armadillo - Excavator 2" +armored_armadillo_ride_3 = "Armored Armadillo - Ride 3" +armored_armadillo_boss = "Armored Armadillo - Boss" + +chill_penguin = "Chill Penguin" +chill_penguin_entrance = "Chill Penguin - Entrance" +chill_penguin_icicles = "Chill Penguin - Icicles" +chill_penguin_ride = "Chill Penguin - Ride Armor" +chill_penguin_boss = "Chill Penguin - Boss" + +spark_mandrill = "Spark Mandrill" +spark_mandrill_entrance = "Spark Mandrill - Entrance" +spark_mandrill_mid_boss = "Spark Mandrill - Mid Boss" +spark_mandrill_deep = "Spark Mandrill - Deep Inside" +spark_mandrill_boss = "Spark Mandrill - Boss" + +launch_octopus = "Launch Octopus" +launch_octopus_sea = "Launch Octopus - Sea Floor" +launch_octopus_base = "Launch Octopus - Underwater Base" +launch_octopus_boss = "Launch Octopus - Boss" + +boomer_kuwanger = "Boomer Kuwanger" +boomer_kuwanger_basement = "Boomer Kuwanger - Basement" +boomer_kuwanger_elevator = "Boomer Kuwanger - Elevator" +boomer_kuwanger_outside = "Boomer Kuwanger - Outside" +boomer_kuwanger_top = "Boomer Kuwanger - Top" +boomer_kuwanger_boss = "Boomer Kuwanger - Boss" + +sting_chameleon = "Sting Chameleon" +sting_chameleon_forest = "Sting Chameleon - Forest" +sting_chameleon_cave_top = "Sting Chameleon - Top of cave" +sting_chameleon_cave = "Sting Chameleon - Cave" +sting_chameleon_cave_bottom = "Sting Chameleon - Bottom of cave" +sting_chameleon_hill = "Sting Chameleon - Hill" +sting_chameleon_swamp = "Sting Chameleon - Swamp" +sting_chameleon_boss = "Sting Chameleon - Boss" + +storm_eagle = "Storm Eagle" +storm_eagle_airport = "Storm Eagle - Airport" +storm_eagle_glass = "Storm Eagle - Glass Tower" +storm_eagle_metal = "Storm Eagle - Metal Tower" +storm_eagle_aircraft = "Storm Eagle - Aircraft" +storm_eagle_boss = "Storm Eagle - Boss" + +flame_mammoth = "Flame Mammoth" +flame_mammoth_conveyors_1 = "Flame Mammoth - Conveyors 1" +flame_mammoth_lava_river_1 = "Flame Mammoth - Lava River 1" +flame_mammoth_conveyors_2 = "Flame Mammoth - Conveyors 2" +flame_mammoth_lava_river_2 = "Flame Mammoth - Lava River 2" +flame_mammoth_boss = "Flame Mammoth - Boss" + +sigma_fortress = "Sigma Fortress" + +sigma_fortress_1 = "Sigma Fortress 1" +sigma_fortress_1_outside = "Sigma Fortress 1 - Outside" +sigma_fortress_1_vile = "Sigma Fortress 1 - Vile" +sigma_fortress_1_vertical = "Sigma Fortress 1 - Vertical Room" +sigma_fortress_1_rematch_1 = "Sigma Fortress 1 - Rematch 1" +sigma_fortress_1_boss = "Sigma Fortress 1 - Boss" + +sigma_fortress_2 = "Sigma Fortress 2" +sigma_fortress_2_start = "Sigma Fortress 2 - Start" +sigma_fortress_2_rematch_1 = "Sigma Fortress 2 - Rematch 1" +sigma_fortress_2_ride = "Sigma Fortress 2 - Ride Armor" +sigma_fortress_2_rematch_2 = "Sigma Fortress 2 - Rematch 2" +sigma_fortress_2_boss = "Sigma Fortress 2 - Boss" + +sigma_fortress_3 = "Sigma Fortress 3" +sigma_fortress_3_rematch_1 = "Sigma Fortress 3 - Rematch 1" +sigma_fortress_3_rematch_2 = "Sigma Fortress 3 - Rematch 2" +sigma_fortress_3_rematch_3 = "Sigma Fortress 3 - Rematch 3" +sigma_fortress_3_rematch_4 = "Sigma Fortress 3 - Rematch 4" +sigma_fortress_3_rematch_5 = "Sigma Fortress 3 - Rematch 5" +sigma_fortress_3_boss = "Sigma Fortress 3 - Boss" + +sigma_fortress_4 = "Sigma Fortress 4" +sigma_fortress_4_dog = "Sigma Fortress 4 - Dog" +sigma_fortress_4_sigma = "Sigma Fortress 4 - Sigma" diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py new file mode 100644 index 000000000000..5a5a001dd345 --- /dev/null +++ b/worlds/mmx/Options.py @@ -0,0 +1,138 @@ +from dataclasses import dataclass +import typing + +from Options import Choice, Range, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, StartInventoryPool + +class EnergyLink(DefaultOnToggle): + """ + Enable EnergyLink support. + EnergyLink works as a big Sub Tank/HP pool where players can request HP manually or automatically when + they lose HP. You make use of this feature by typing /pool, /heal or /autoheal in the client. + """ + display_name = "Energy Link" + +class StartingLifeCount(Range): + """ + How many lives to start the game with. + Note: This number becomes the new default life count, meaning that it will persist after a game over. + """ + display_name = "Starting Life Count" + range_start = 0 + range_end = 9 + default = 2 + +class LogicBossWeakness(DefaultOnToggle): + """ + Every main boss will logically expect you to have its weakness. + """ + display_name = "Boss Weakness Logic" + +class LogicLegSigma(DefaultOnToggle): + """ + Sigma's Fortress will logically expect you to have the legs upgrade. + """ + display_name = "Sigma's Fortress Legs Upgrade Logic" + +class LogicChargedShotgunIce(Toggle): + """ + Adds Charged Shotgun Ice as logic to some locations. Some of those may be hard to execute. + """ + display_name = "Charged Shotgun Ice Logic" + +class EarlyLegs(Toggle): + """ + Places the Legs Upgrade item in sphere 1. + """ + display_name = "Early Legs" + +class PickupSanity(Toggle): + """ + Whether collecting freestanding 1ups, HP and Weapon Energy capsules will grant a check. + DOES NOT WORK YET. DO NOT ENABLE. + """ + display_name = "Pickupsanity" + +class SigmaOpen(Choice): + """ + Under what conditions will Sigma's Fortress open. + multiworld: Access will require an Access Code multiworld item, similar to the main stages. + medals: Access will be granted after collecting a certain amount of Maverick Medals. + weapons: Access will be granted after collecting a certain amount of weapons. + armor_upgrades: Access will be granted after collecting a certain amount of armor upgrades. + heart_tanks: Access will be granted after collecting a certain amount of Heart Tanks. + sub_tanks: Access will be granted after collecting a certain amount of Sub Tanks. + Do not enable weapons, armor_upgrades, heart_tanks, sub_tanks on solo seeds without pickupsanity + or sessions with very few items. There's a big chance it'll cause an error. + """ + display_name = "Sigma Fortress Rules" + option_multiworld = 0 + option_medals = 1 + option_weapons = 2 + option_armor_upgrades = 3 + option_heart_tanks = 4 + option_sub_tanks = 5 + default = 1 + +class SigmaMedalCount(Range): + """ + How many Maverick Medals are required to access Sigma's Fortress. + """ + display_name = "Sigma Medal Count" + range_start = 1 + range_end = 8 + default = 8 + +class SigmaWeaponCount(Range): + """ + How many weapons are required to access Sigma's Fortress. + """ + display_name = "Sigma Weapon Count" + range_start = 1 + range_end = 8 + default = 8 + +class SigmaArmorUpgradeCount(Range): + """ + How many armor upgrades are required to access Sigma's Fortress. + """ + display_name = "Sigma Armor Upgrade Count" + range_start = 1 + range_end = 5 + default = 3 + +class SigmaHeartTankCount(Range): + """ + How many Heart Tanks are required to access Sigma's Fortress. + """ + display_name = "Sigma Heart Tank Count" + range_start = 1 + range_end = 8 + default = 8 + +class SigmaSubTankCount(Range): + """ + How many Sub Tanks are required to access Sigma's Fortress. + """ + display_name = "Sigma Sub Tank Count" + range_start = 1 + range_end = 4 + default = 4 + + +@dataclass +class MMXOptions(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool + death_link: DeathLink + energy_link: EnergyLink + starting_life_count: StartingLifeCount + pickupsanity: PickupSanity + logic_boss_weakness: LogicBossWeakness + logic_leg_sigma: LogicLegSigma + logic_charged_shotgun_ice: LogicChargedShotgunIce + early_legs: EarlyLegs + sigma_open: SigmaOpen + sigma_medal_count: SigmaMedalCount + sigma_weapon_count: SigmaWeaponCount + sigma_upgrade_count: SigmaArmorUpgradeCount + sigma_heart_tank_count: SigmaHeartTankCount + sigma_sub_tank_count: SigmaSubTankCount diff --git a/worlds/mmx/Regions.py b/worlds/mmx/Regions.py new file mode 100644 index 000000000000..b2d4fe589666 --- /dev/null +++ b/worlds/mmx/Regions.py @@ -0,0 +1,391 @@ +import typing + +from BaseClasses import CollectionState, MultiWorld, Region, Entrance, ItemClassification +from .Locations import MMXLocation +from .Items import MMXItem +from .Names import LocationName, ItemName, RegionName, EventName +from worlds.AutoWorld import World + + +def create_regions(multiworld: MultiWorld, player: int, world: World, active_locations): + menu = create_region(multiworld, player, active_locations, 'Menu') + + intro = create_region(multiworld, player, active_locations, RegionName.intro) + + armored_armadillo = create_region(multiworld, player, active_locations, RegionName.armored_armadillo) + armored_armadillo_ride_1 = create_region(multiworld, player, active_locations, RegionName.armored_armadillo_ride_1) + armored_armadillo_excavator_1 = create_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_1) + armored_armadillo_ride_2 = create_region(multiworld, player, active_locations, RegionName.armored_armadillo_ride_2) + armored_armadillo_excavator_2 = create_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_2) + armored_armadillo_ride_3 = create_region(multiworld, player, active_locations, RegionName.armored_armadillo_ride_3) + armored_armadillo_boss = create_region(multiworld, player, active_locations, RegionName.armored_armadillo_boss) + + chill_penguin = create_region(multiworld, player, active_locations, RegionName.chill_penguin) + chill_penguin_entrance = create_region(multiworld, player, active_locations, RegionName.chill_penguin_entrance) + chill_penguin_icicles = create_region(multiworld, player, active_locations, RegionName.chill_penguin_icicles) + chill_penguin_ride = create_region(multiworld, player, active_locations, RegionName.chill_penguin_ride) + chill_penguin_boss = create_region(multiworld, player, active_locations, RegionName.chill_penguin_boss) + + spark_mandrill = create_region(multiworld, player, active_locations, RegionName.spark_mandrill) + spark_mandrill_entrance = create_region(multiworld, player, active_locations, RegionName.spark_mandrill_entrance) + spark_mandrill_mid_boss = create_region(multiworld, player, active_locations, RegionName.spark_mandrill_mid_boss) + spark_mandrill_deep = create_region(multiworld, player, active_locations, RegionName.spark_mandrill_deep) + spark_mandrill_boss = create_region(multiworld, player, active_locations, RegionName.spark_mandrill_boss) + + launch_octopus = create_region(multiworld, player, active_locations, RegionName.launch_octopus) + launch_octopus_sea = create_region(multiworld, player, active_locations, RegionName.launch_octopus_sea) + launch_octopus_base = create_region(multiworld, player, active_locations, RegionName.launch_octopus_base) + launch_octopus_boss = create_region(multiworld, player, active_locations, RegionName.launch_octopus_boss) + + boomer_kuwanger = create_region(multiworld, player, active_locations, RegionName.boomer_kuwanger) + boomer_kuwanger_basement = create_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_basement) + boomer_kuwanger_elevator = create_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_elevator) + boomer_kuwanger_outside = create_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_outside) + boomer_kuwanger_top = create_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_top) + boomer_kuwanger_boss = create_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_boss) + + sting_chameleon = create_region(multiworld, player, active_locations, RegionName.sting_chameleon) + sting_chameleon_forest = create_region(multiworld, player, active_locations, RegionName.sting_chameleon_forest) + sting_chameleon_cave_top = create_region(multiworld, player, active_locations, RegionName.sting_chameleon_cave_top) + sting_chameleon_cave = create_region(multiworld, player, active_locations, RegionName.sting_chameleon_cave) + sting_chameleon_cave_bottom = create_region(multiworld, player, active_locations, RegionName.sting_chameleon_cave_bottom) + sting_chameleon_hill = create_region(multiworld, player, active_locations, RegionName.sting_chameleon_hill) + sting_chameleon_swamp = create_region(multiworld, player, active_locations, RegionName.sting_chameleon_swamp) + sting_chameleon_boss = create_region(multiworld, player, active_locations, RegionName.sting_chameleon_boss) + + storm_eagle = create_region(multiworld, player, active_locations, RegionName.storm_eagle) + storm_eagle_airport = create_region(multiworld, player, active_locations, RegionName.storm_eagle_airport) + storm_eagle_glass = create_region(multiworld, player, active_locations, RegionName.storm_eagle_glass) + storm_eagle_metal = create_region(multiworld, player, active_locations, RegionName.storm_eagle_metal) + storm_eagle_aircraft = create_region(multiworld, player, active_locations, RegionName.storm_eagle_aircraft) + storm_eagle_boss = create_region(multiworld, player, active_locations, RegionName.storm_eagle_boss) + + flame_mammoth = create_region(multiworld, player, active_locations, RegionName.flame_mammoth) + flame_mammoth_conveyors_1 = create_region(multiworld, player, active_locations, RegionName.flame_mammoth_conveyors_1) + flame_mammoth_lava_river_1 = create_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_1) + flame_mammoth_conveyors_2 = create_region(multiworld, player, active_locations, RegionName.flame_mammoth_conveyors_2) + flame_mammoth_lava_river_2 = create_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_2) + flame_mammoth_boss = create_region(multiworld, player, active_locations, RegionName.flame_mammoth_boss) + + sigma_fortress = create_region(multiworld, player, active_locations, RegionName.sigma_fortress) + + sigma_fortress_1 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1) + sigma_fortress_1_outside = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_outside) + sigma_fortress_1_vile = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_vile) + sigma_fortress_1_vertical = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_vertical) + sigma_fortress_1_rematch_1 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_rematch_1) + sigma_fortress_1_boss = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_boss) + + sigma_fortress_2 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2) + sigma_fortress_2_start = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_start) + sigma_fortress_2_rematch_1 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_rematch_1) + sigma_fortress_2_ride = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_ride) + sigma_fortress_2_rematch_2 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_rematch_2) + sigma_fortress_2_boss = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_boss) + + sigma_fortress_3 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3) + sigma_fortress_3_rematch_1 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_1) + sigma_fortress_3_rematch_2 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_2) + sigma_fortress_3_rematch_3 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_3) + sigma_fortress_3_rematch_4 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_4) + sigma_fortress_3_rematch_5 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_5) + sigma_fortress_3_boss = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_boss) + + sigma_fortress_4 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_4) + sigma_fortress_4_dog = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_4_dog) + sigma_fortress_4_sigma = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_4_sigma) + + multiworld.regions += [ + menu, + intro, + armored_armadillo, + armored_armadillo_ride_1, + armored_armadillo_excavator_1, + armored_armadillo_ride_2, + armored_armadillo_excavator_2, + armored_armadillo_ride_3, + armored_armadillo_boss, + chill_penguin, + chill_penguin_entrance, + chill_penguin_icicles, + chill_penguin_ride, + chill_penguin_boss, + spark_mandrill, + spark_mandrill_entrance, + spark_mandrill_mid_boss, + spark_mandrill_deep, + spark_mandrill_boss, + launch_octopus, + launch_octopus_sea, + launch_octopus_base, + launch_octopus_boss, + boomer_kuwanger, + boomer_kuwanger_basement, + boomer_kuwanger_elevator, + boomer_kuwanger_outside, + boomer_kuwanger_top, + boomer_kuwanger_boss, + sting_chameleon, + sting_chameleon_forest, + sting_chameleon_cave_top, + sting_chameleon_cave, + sting_chameleon_cave_bottom, + sting_chameleon_hill, + sting_chameleon_swamp, + sting_chameleon_boss, + storm_eagle, + storm_eagle_airport, + storm_eagle_glass, + storm_eagle_metal, + storm_eagle_aircraft, + storm_eagle_boss, + flame_mammoth, + flame_mammoth_conveyors_1, + flame_mammoth_lava_river_1, + flame_mammoth_conveyors_2, + flame_mammoth_lava_river_2, + flame_mammoth_boss, + sigma_fortress, + sigma_fortress_1, + sigma_fortress_1_outside, + sigma_fortress_1_vile, + sigma_fortress_1_vertical, + sigma_fortress_1_rematch_1, + sigma_fortress_1_boss, + sigma_fortress_2, + sigma_fortress_2_start, + sigma_fortress_2_rematch_1, + sigma_fortress_2_ride, + sigma_fortress_2_rematch_2, + sigma_fortress_2_boss, + sigma_fortress_3, + sigma_fortress_3_rematch_1, + sigma_fortress_3_rematch_2, + sigma_fortress_3_rematch_3, + sigma_fortress_3_rematch_4, + sigma_fortress_3_rematch_5, + sigma_fortress_3_boss, + sigma_fortress_4, + sigma_fortress_4_dog, + sigma_fortress_4_sigma, + ] + + # Intro + add_location_to_region(multiworld, player, active_locations, RegionName.intro, LocationName.intro_completed) + add_location_to_region(multiworld, player, active_locations, RegionName.intro, LocationName.intro_mini_boss_1) + add_location_to_region(multiworld, player, active_locations, RegionName.intro, LocationName.intro_mini_boss_2) + + # Armored Armadillo + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_1, LocationName.armored_armadillo_sub_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_1, LocationName.armored_armadillo_mini_boss_1) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_2, LocationName.armored_armadillo_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_2, LocationName.armored_armadillo_mini_boss_2) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_ride_3, LocationName.armored_armadillo_hadouken) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_boss, LocationName.armored_armadillo_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_boss, LocationName.armored_armadillo_clear) + add_event_to_region(multiworld, player, RegionName.armored_armadillo_boss, EventName.armored_armadillo_clear) + + # Chill Penguin + add_location_to_region(multiworld, player, active_locations, RegionName.chill_penguin_ride, LocationName.chill_penguin_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.chill_penguin_icicles, LocationName.chill_penguin_legs) + add_location_to_region(multiworld, player, active_locations, RegionName.chill_penguin_boss, LocationName.chill_penguin_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.chill_penguin_boss, LocationName.chill_penguin_clear) + add_event_to_region(multiworld, player, RegionName.chill_penguin_boss, EventName.chill_penguin_clear) + + # Spark Mandrill + add_location_to_region(multiworld, player, active_locations, RegionName.spark_mandrill_entrance, LocationName.spark_mandrill_sub_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.spark_mandrill_mid_boss, LocationName.spark_mandrill_mini_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.spark_mandrill_deep, LocationName.spark_mandrill_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.spark_mandrill_boss, LocationName.spark_mandrill_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.spark_mandrill_boss, LocationName.spark_mandrill_clear) + add_event_to_region(multiworld, player, RegionName.spark_mandrill_boss, EventName.spark_mandrill_clear) + + # Launch Octopus + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_sea, LocationName.launch_octopus_mini_boss_1) + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_sea, LocationName.launch_octopus_mini_boss_2) + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_sea, LocationName.launch_octopus_mini_boss_3) + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_sea, LocationName.launch_octopus_mini_boss_4) + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_base, LocationName.launch_octopus_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_boss, LocationName.launch_octopus_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_boss, LocationName.launch_octopus_clear) + add_event_to_region(multiworld, player, RegionName.launch_octopus_boss, EventName.launch_octopus_clear) + + # Boomer Kuwanger + add_location_to_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_top, LocationName.boomer_kuwanger_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_boss, LocationName.boomer_kuwanger_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_boss, LocationName.boomer_kuwanger_clear) + add_event_to_region(multiworld, player, RegionName.boomer_kuwanger_boss, EventName.boomer_kuwanger_clear) + + # Sting Chameleon + add_location_to_region(multiworld, player, active_locations, RegionName.sting_chameleon_cave_bottom, LocationName.sting_chameleon_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.sting_chameleon_cave_top, LocationName.sting_chameleon_body) + add_location_to_region(multiworld, player, active_locations, RegionName.sting_chameleon_boss, LocationName.sting_chameleon_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.sting_chameleon_boss, LocationName.sting_chameleon_clear) + add_event_to_region(multiworld, player, RegionName.sting_chameleon_boss, EventName.sting_chameleon_clear) + + # Storm Eagle + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_glass, LocationName.storm_eagle_sub_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_metal, LocationName.storm_eagle_helmet) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_boss, LocationName.storm_eagle_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_boss, LocationName.storm_eagle_clear) + add_event_to_region(multiworld, player, RegionName.storm_eagle_boss, EventName.storm_eagle_clear) + + # Storm Eagle + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_1, LocationName.flame_mammoth_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_1, LocationName.flame_mammoth_sub_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_1, LocationName.flame_mammoth_arms) + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_boss, LocationName.flame_mammoth_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_boss, LocationName.flame_mammoth_clear) + add_event_to_region(multiworld, player, RegionName.flame_mammoth_boss, EventName.flame_mammoth_clear) + + # Sigma's Fortress 1 + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_vile, LocationName.sigma_fortress_1_vile) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_rematch_1, LocationName.sigma_fortress_1_boomer_kuwanger) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_boss, LocationName.sigma_fortress_1_bospider) + add_event_to_region(multiworld, player, RegionName.sigma_fortress_1_boss, EventName.sigma_fortress_1_clear) + + # Sigma's Fortress 2 + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_rematch_1, LocationName.sigma_fortress_2_chill_penguin) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_rematch_2, LocationName.sigma_fortress_2_storm_eagle) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_boss, LocationName.sigma_fortress_2_rangda_bangda) + add_event_to_region(multiworld, player, RegionName.sigma_fortress_2_boss, EventName.sigma_fortress_2_clear) + + # Sigma's Fortress 3 + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_1, LocationName.sigma_fortress_3_armored_armadillo) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_2, LocationName.sigma_fortress_3_sting_chameleon) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_3, LocationName.sigma_fortress_3_spark_mandrill) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_4, LocationName.sigma_fortress_3_launch_octopus) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_5, LocationName.sigma_fortress_3_flame_mammoth) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_boss, LocationName.sigma_fortress_3_d_rex) + add_event_to_region(multiworld, player, RegionName.sigma_fortress_3_boss, EventName.sigma_fortress_3_clear) + + # Sigma's Fortress 4 + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_4_dog, LocationName.sigma_fortress_4_velguarder) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_4_sigma, LocationName.sigma_fortress_4_sigma) + + if world.options.pickupsanity: + return + +def connect_regions(world: World): + connect(world, "Menu", RegionName.intro) + + connect(world, RegionName.intro, RegionName.armored_armadillo) + connect(world, RegionName.intro, RegionName.chill_penguin) + connect(world, RegionName.intro, RegionName.spark_mandrill) + connect(world, RegionName.intro, RegionName.launch_octopus) + connect(world, RegionName.intro, RegionName.boomer_kuwanger) + connect(world, RegionName.intro, RegionName.sting_chameleon) + connect(world, RegionName.intro, RegionName.storm_eagle) + connect(world, RegionName.intro, RegionName.flame_mammoth) + + connect(world, RegionName.armored_armadillo, RegionName.armored_armadillo_ride_1) + connect(world, RegionName.armored_armadillo_ride_1, RegionName.armored_armadillo_excavator_1) + connect(world, RegionName.armored_armadillo_excavator_1, RegionName.armored_armadillo_ride_2) + connect(world, RegionName.armored_armadillo_ride_2, RegionName.armored_armadillo_excavator_2) + connect(world, RegionName.armored_armadillo_excavator_2, RegionName.armored_armadillo_ride_3) + connect(world, RegionName.armored_armadillo_ride_3, RegionName.armored_armadillo_boss) + + connect(world, RegionName.chill_penguin, RegionName.chill_penguin_entrance) + connect(world, RegionName.chill_penguin_entrance, RegionName.chill_penguin_icicles) + connect(world, RegionName.chill_penguin_icicles, RegionName.chill_penguin_ride) + connect(world, RegionName.chill_penguin_ride, RegionName.chill_penguin_boss) + + connect(world, RegionName.spark_mandrill, RegionName.spark_mandrill_entrance) + connect(world, RegionName.spark_mandrill_entrance, RegionName.spark_mandrill_mid_boss) + connect(world, RegionName.spark_mandrill_mid_boss, RegionName.spark_mandrill_deep) + connect(world, RegionName.spark_mandrill_deep, RegionName.spark_mandrill_boss) + + connect(world, RegionName.launch_octopus, RegionName.launch_octopus_sea) + connect(world, RegionName.launch_octopus_sea, RegionName.launch_octopus_base) + connect(world, RegionName.launch_octopus_sea, RegionName.launch_octopus_boss) + + connect(world, RegionName.boomer_kuwanger, RegionName.boomer_kuwanger_basement) + connect(world, RegionName.boomer_kuwanger_basement, RegionName.boomer_kuwanger_elevator) + connect(world, RegionName.boomer_kuwanger_elevator, RegionName.boomer_kuwanger_outside) + connect(world, RegionName.boomer_kuwanger_outside, RegionName.boomer_kuwanger_top) + connect(world, RegionName.boomer_kuwanger_top, RegionName.boomer_kuwanger_boss) + + connect(world, RegionName.sting_chameleon, RegionName.sting_chameleon_forest) + connect(world, RegionName.sting_chameleon_forest, RegionName.sting_chameleon_cave) + connect(world, RegionName.sting_chameleon_cave, RegionName.sting_chameleon_cave_top) + connect(world, RegionName.sting_chameleon_cave, RegionName.sting_chameleon_cave_bottom) + connect(world, RegionName.sting_chameleon_cave, RegionName.sting_chameleon_hill) + connect(world, RegionName.sting_chameleon_hill, RegionName.sting_chameleon_swamp) + connect(world, RegionName.sting_chameleon_swamp, RegionName.sting_chameleon_boss) + + connect(world, RegionName.storm_eagle, RegionName.storm_eagle_airport) + connect(world, RegionName.storm_eagle_airport, RegionName.storm_eagle_glass) + connect(world, RegionName.storm_eagle_glass, RegionName.storm_eagle_metal) + connect(world, RegionName.storm_eagle_metal, RegionName.storm_eagle_aircraft) + connect(world, RegionName.storm_eagle_aircraft, RegionName.storm_eagle_boss) + + connect(world, RegionName.flame_mammoth, RegionName.flame_mammoth_conveyors_1) + connect(world, RegionName.flame_mammoth_conveyors_1, RegionName.flame_mammoth_lava_river_1) + connect(world, RegionName.flame_mammoth_lava_river_1, RegionName.flame_mammoth_conveyors_2) + connect(world, RegionName.flame_mammoth_conveyors_2, RegionName.flame_mammoth_lava_river_2) + connect(world, RegionName.flame_mammoth_lava_river_2, RegionName.flame_mammoth_boss) + + connect(world, RegionName.intro, RegionName.sigma_fortress) + + connect(world, RegionName.sigma_fortress, RegionName.sigma_fortress_1) + connect(world, RegionName.sigma_fortress_1, RegionName.sigma_fortress_1_outside) + connect(world, RegionName.sigma_fortress_1_outside, RegionName.sigma_fortress_1_vile) + connect(world, RegionName.sigma_fortress_1_vile, RegionName.sigma_fortress_1_vertical) + connect(world, RegionName.sigma_fortress_1_vertical, RegionName.sigma_fortress_1_rematch_1) + connect(world, RegionName.sigma_fortress_1_rematch_1, RegionName.sigma_fortress_1_boss) + + connect(world, RegionName.sigma_fortress_1_boss, RegionName.sigma_fortress_2) + connect(world, RegionName.sigma_fortress_2, RegionName.sigma_fortress_2_start) + connect(world, RegionName.sigma_fortress_2_start, RegionName.sigma_fortress_2_rematch_1) + connect(world, RegionName.sigma_fortress_2_rematch_1, RegionName.sigma_fortress_2_ride) + connect(world, RegionName.sigma_fortress_2_ride, RegionName.sigma_fortress_2_rematch_2) + connect(world, RegionName.sigma_fortress_2_rematch_2, RegionName.sigma_fortress_2_boss) + + connect(world, RegionName.sigma_fortress_2_boss, RegionName.sigma_fortress_3) + connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_rematch_1) + connect(world, RegionName.sigma_fortress_3_rematch_1, RegionName.sigma_fortress_3_rematch_2) + connect(world, RegionName.sigma_fortress_3_rematch_2, RegionName.sigma_fortress_3_rematch_3) + connect(world, RegionName.sigma_fortress_3_rematch_3, RegionName.sigma_fortress_3_rematch_4) + connect(world, RegionName.sigma_fortress_3_rematch_4, RegionName.sigma_fortress_3_rematch_5) + connect(world, RegionName.sigma_fortress_3_rematch_5, RegionName.sigma_fortress_3_boss) + + connect(world, RegionName.sigma_fortress_3_boss, RegionName.sigma_fortress_4) + connect(world, RegionName.sigma_fortress_4, RegionName.sigma_fortress_4_dog) + connect(world, RegionName.sigma_fortress_4_dog, RegionName.sigma_fortress_4_sigma) + + +def create_region(multiworld: MultiWorld, player: int, active_locations, name: str, locations=None): + ret = Region(name, player, multiworld) + if locations: + for locationName in locations: + loc_id = active_locations.get(locationName, 0) + if loc_id: + location = MMXLocation(player, locationName, loc_id, ret) + ret.locations.append(location) + + return ret + + +def add_event_to_region(multiworld: MultiWorld, player: int, region_name: str, event_name: str, event_item=None): + region = multiworld.get_region(region_name, player) + event = MMXLocation(player, event_name, None, region) + if event_item: + event.place_locked_item(MMXItem(event_item, ItemClassification.progression, None, player)) + else: + event.place_locked_item(MMXItem(event_name, ItemClassification.progression, None, player)) + region.locations.append(event) + + +def add_location_to_region(multiworld: MultiWorld, player: int, active_locations, region_name: str, location_name: str): + region = multiworld.get_region(region_name, player) + loc_id = active_locations.get(location_name, 0) + if loc_id: + location = MMXLocation(player, location_name, loc_id, region) + region.locations.append(location) + + +def connect(world: World, source: str, target: str): + source_region: Region = world.multiworld.get_region(source, world.player) + target_region: Region = world.multiworld.get_region(target, world.player) + source_region.connect(target_region) diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py new file mode 100644 index 000000000000..764bde9c1939 --- /dev/null +++ b/worlds/mmx/Rom.py @@ -0,0 +1,168 @@ +import typing +import bsdiff4 +import Utils +import hashlib +import os +from typing import Optional, TYPE_CHECKING +from pkgutil import get_data + +from worlds.AutoWorld import World +from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes + +HASH_US = 'a10071fa78554b57538d0b459e00d224' +HASH_US_REV_1 = 'df1cc0c8c8c4b61e3b834cc03366611c' +HASH_LEGACY = 'f1dfbbcdc3d8cdeafa4b4b9aa51a56d6' + +STARTING_ID = 0xBE0800 + +weapon_rom_data = { + STARTING_ID + 0x000E: [0x1F88, 0xFF], + STARTING_ID + 0x0010: [0x1F8A, 0xFF], + STARTING_ID + 0x000D: [0x1F8C, 0xFF], + STARTING_ID + 0x0012: [0x1F8E, 0xFF], + STARTING_ID + 0x0011: [0x1F90, 0xFF], + STARTING_ID + 0x000C: [0x1F92, 0xFF], + STARTING_ID + 0x000F: [0x1F94, 0xFF], + STARTING_ID + 0x000B: [0x1F96, 0xFF], + #STARTING_ID + 0x001A: [0x1F7E, 0x80], +} + +upgrades_rom_data = { + STARTING_ID + 0x001C: [0x00], + STARTING_ID + 0x001D: [0x02], + STARTING_ID + 0x001E: [0x01], + STARTING_ID + 0x001F: [0x03], +} + +boss_access_rom_data = { + STARTING_ID + 0x0006: [0x01], + STARTING_ID + 0x0008: [0x02], + STARTING_ID + 0x0002: [0x03], + STARTING_ID + 0x0005: [0x04], + STARTING_ID + 0x0009: [0x05], + STARTING_ID + 0x0007: [0x06], + STARTING_ID + 0x0003: [0x07], + STARTING_ID + 0x0004: [0x08], + STARTING_ID + 0x000A: [0x09], +} + +refill_rom_data = { + STARTING_ID + 0x0030: ["small hp refill"], + STARTING_ID + 0x0031: ["large hp refill"], + STARTING_ID + 0x0034: ["1up"], + #0xBD0032: ["small weapon refill"], + #0xBD0033: ["large weapon refill"] +} + +class MMXProcedurePatch(APProcedurePatch, APTokenMixin): + hash = [HASH_US, HASH_LEGACY] + game = "Mega Man X" + patch_file_ending = ".apmmx" + result_file_ending = ".sfc" + name: bytearray + procedure = [ + ("apply_tokens", ["token_patch.bin"]), + ("apply_bsdiff4", ["mmx_basepatch.bsdiff4"]), + ] + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + def write_byte(self, offset, value): + self.write_token(APTokenTypes.WRITE, offset, value.to_bytes(1, "little")) + + def write_bytes(self, offset, value: typing.Iterable[int]): + self.write_token(APTokenTypes.WRITE, offset, bytes(value)) + +def patch_rom(world: World, patch: MMXProcedurePatch): + from Utils import __version__ + + # Prepare some ROM locations to receive the basepatch output + patch.write_bytes(0x00098C, bytearray([0x85,0xB3,0x8A])) + patch.write_bytes(0x0009AE, bytearray([0x85,0xB3,0x8A])) + patch.write_bytes(0x001261, bytearray([0xA9,0x10,0x20,0xE1,0x89])) + patch.write_bytes(0x001271, bytearray([0xA5,0xAC,0x89,0x08,0xF0,0x09,0xA5,0x3C, + 0x3A,0x10,0x11,0xA9,0x02,0x80,0x0D,0x89, + 0x24,0xF0,0x1E,0xA5,0x3C,0x1A,0xC9,0x03, + 0xD0,0x02,0xA9,0x00,0x85,0x3C,0xAA,0xBD, + 0x75,0x88,0x8D,0xB0,0x0B,0xA5,0x3C,0x18, + 0x69,0x10,0x20,0xE1,0x89,0xA9,0xF0,0x85, + 0x3B])) + patch.write_bytes(0x00131F, bytearray([0x9C,0xA9,0x0B,0x9C,0xAA,0x0B])) + patch.write_bytes(0x00132E, bytearray([0x90,0x8B])) + patch.write_bytes(0x001352, bytearray([0x64,0x38,0x64,0x39])) + patch.write_bytes(0x0025CA, bytearray([0xA9,0xF6,0xA0,0x02])) + patch.write_bytes(0x0046F3, bytearray([0x38,0xE9,0x0A])) + patch.write_bytes(0x006A61, bytearray([0xFB,0xEC])) + patch.write_bytes(0x006D67, bytearray([0x85,0x00,0x0A])) + patch.write_bytes(0x006F97, bytearray([0x9F,0xCB,0xFF,0x7E])) + patch.write_bytes(0x00700F, bytearray([0x55,0xDB])) + patch.write_bytes(0x007BF0, bytearray([0x90,0x8B])) + patch.write_bytes(0x00EB4A, bytearray([0xEE,0xCF,0x0B,0xA9,0x80])) + patch.write_bytes(0x011646, bytearray([0xA9,0x0A,0x85,0x01])) + patch.write_bytes(0x01B392, bytearray([0xA9,0x02,0x85,0x03])) + patch.write_bytes(0x01C67E, bytearray([0xA9,0x04,0x85,0x01])) + patch.write_bytes(0x01D84F, bytearray([0xA9,0x3C,0x9D,0x0A,0x00])) + patch.write_bytes(0x021D51, bytearray([0xED,0x00,0x00,0x8D,0xCF,0x0B])) + patch.write_bytes(0x021E94, bytearray([0x64,0x27,0xA9,0xFF])) + patch.write_bytes(0x021F5A, bytearray([0xED,0x00,0x00,0x8D,0xCF,0x0B])) + patch.write_bytes(0x02268C, bytearray([0xE6,0x03,0xE6,0x03])) + patch.write_bytes(0x03D0B2, bytearray([0x0C,0x99,0x1F,0x4C,0xBD,0xD0])) + patch.write_bytes(0x03D0D9, bytearray([0xA9,0x80,0x0C,0x7E])) + patch.write_bytes(0x044BEC, bytearray([0xA9,0x12,0x85,0x01])) + patch.write_bytes(0x0457CC, bytearray([0xA9,0x02,0x0C,0x99,0x1F])) + + patch.write_bytes(0x0312B0, bytearray([0x20,0xC6,0x09,0x40,0x14,0x20,0x06,0x0A, + 0x4C,0x49,0x43,0x45,0x4E,0x53,0x45,0x44, + 0x20,0x42,0x59,0x20,0x4E,0x49,0x4E,0x54, + 0x45,0x4E,0x44,0x4F,0x00])) + + # Edit the ROM header + patch.name = bytearray(f'MMX1{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] + patch.name.extend([0] * (21 - len(patch.name))) + patch.write_bytes(0x7FC0, patch.name) + + # Write options to the ROM + patch.write_byte(0x17FFE0, world.options.sigma_open.value) + patch.write_byte(0x17FFE1, world.options.sigma_medal_count.value) + patch.write_byte(0x17FFE2, world.options.sigma_weapon_count.value) + patch.write_byte(0x17FFE3, world.options.sigma_upgrade_count.value) + patch.write_byte(0x17FFE4, world.options.sigma_heart_tank_count.value) + patch.write_byte(0x17FFE5, world.options.sigma_sub_tank_count.value) + patch.write_byte(0x17FFE6, world.options.starting_life_count.value) + patch.write_byte(0x17FFE7, world.options.pickupsanity.value) + patch.write_byte(0x17FFEB, world.options.logic_boss_weakness.value) + #patch.write_byte(0x17FFEA, world.options.logic_hadouken.value) + + # EnergyLink + patch.write_byte(0x17FFE8, world.options.energy_link.value) + + # DeathLink + patch.write_byte(0x17FFE9, world.options.death_link.value) + + patch.write_file("token_patch.bin", patch.get_token_binary()) + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_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 = bytes(Utils.read_snes_rom(open(file_name, "rb"))) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if basemd5.hexdigest() not in {HASH_US, HASH_LEGACY}: + raise Exception('Supplied Base Rom does not match known MD5 for US or LC release. ' + 'Get the correct game and version, then dump it') + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + options = Utils.get_options() + if not file_name: + file_name = options["mmx_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name diff --git a/worlds/mmx/Rules.py b/worlds/mmx/Rules.py new file mode 100644 index 000000000000..8db2e1e14005 --- /dev/null +++ b/worlds/mmx/Rules.py @@ -0,0 +1,215 @@ +from worlds.generic.Rules import add_rule, set_rule + +from . import MMXWorld +from .Names import LocationName, ItemName, RegionName, EventName + +def set_rules(world: MMXWorld): + player = world.player + multiworld = world.multiworld + + multiworld.completion_condition[player] = lambda state: state.has(ItemName.victory, player) + + # Intro entrance rules + set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.armored_armadillo}", player), + lambda state: state.has(ItemName.stage_armored_armadillo, player)) + set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.boomer_kuwanger}", player), + lambda state: state.has(ItemName.stage_boomer_kuwanger, player)) + set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.chill_penguin}", player), + lambda state: state.has(ItemName.stage_chill_penguin, player)) + set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.flame_mammoth}", player), + lambda state: state.has(ItemName.stage_flame_mammoth, player)) + set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.launch_octopus}", player), + lambda state: state.has(ItemName.stage_launch_octopus, player)) + set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.spark_mandrill}", player), + lambda state: state.has(ItemName.stage_spark_mandrill, player)) + set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.sting_chameleon}", player), + lambda state: state.has(ItemName.stage_sting_chameleon, player)) + set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.storm_eagle}", player), + lambda state: state.has(ItemName.stage_storm_eagle, player)) + + # Fortress entrance rules + fortress_open = world.options.sigma_open + entrance = multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.sigma_fortress}", player) + + if fortress_open == "multiworld": + add_rule(entrance, lambda state: state.has(ItemName.stage_sigma_fortress, player)) + elif fortress_open == "medals": + add_rule(entrance, lambda state: state.has(ItemName.maverick_medal, player, world.options.sigma_medal_count.value)) + elif fortress_open == "weapons": + add_rule(entrance, lambda state: state.has_group("Weapons", player, world.options.sigma_weapon_count.value)) + elif fortress_open == "armor_upgrades": + add_rule(entrance, lambda state: state.has_group("Armor Upgrades", player, world.options.sigma_upgrade_count.value)) + elif fortress_open == "heart_tanks": + add_rule(entrance, lambda state: state.has(ItemName.heart_tank, player, world.options.sigma_heart_tank_count.value)) + elif fortress_open == "sub_tanks": + add_rule(entrance, lambda state: state.has(ItemName.sub_tank, player, world.options.sigma_sub_tank_count.value)) + + if world.options.logic_leg_sigma.value: + add_rule(entrance, lambda state: state.has(ItemName.legs, player)) + + # Sigma Fortress level rules + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_1_boss} -> {RegionName.sigma_fortress_2}", player), + lambda state: state.has(EventName.sigma_fortress_1_clear, player)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_2_boss} -> {RegionName.sigma_fortress_3}", player), + lambda state: state.has(EventName.sigma_fortress_2_clear, player)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_boss} -> {RegionName.sigma_fortress_4}", player), + lambda state: state.has(EventName.sigma_fortress_3_clear, player)) + + # Sigma rules + add_rule(multiworld.get_location(LocationName.sigma_fortress_4_sigma, player), + lambda state: state.has(ItemName.arms, player)) + + # Chill Penguin collectibles + set_rule(multiworld.get_location(LocationName.chill_penguin_heart_tank, player), + lambda state: state.has(ItemName.fire_wave, player)) + + # Flame Mammoth collectibles + set_rule(multiworld.get_location(LocationName.flame_mammoth_arms, player), + lambda state: ( + state.has(ItemName.legs, player) and + state.has(ItemName.helmet, player) + )) + set_rule(multiworld.get_location(LocationName.flame_mammoth_heart_tank, player), + lambda state: ( + state.has(EventName.chill_penguin_clear, player) or + ( + state.has(ItemName.chameleon_sting, player) and + state.has(ItemName.arms, player) + ) + )) + set_rule(multiworld.get_location(LocationName.flame_mammoth_sub_tank, player), + lambda state: state.has(ItemName.legs, player)) + + # Boomer Kuwanger collectibles + set_rule(multiworld.get_location(LocationName.boomer_kuwanger_heart_tank, player), + lambda state: state.has(ItemName.boomerang_cutter, player)) + + # Sting Chameleon collectibles + set_rule(multiworld.get_location(LocationName.sting_chameleon_body, player), + lambda state: state.has(ItemName.legs, player)) + set_rule(multiworld.get_location(LocationName.sting_chameleon_heart_tank, player), + lambda state: ( + state.has(ItemName.legs, player) and + state.has(EventName.launch_octopus_clear, player) + )) + + # Spark Mandrill collectibles + set_rule(multiworld.get_location(LocationName.spark_mandrill_sub_tank, player), + lambda state: state.has(ItemName.boomerang_cutter, player)) + set_rule(multiworld.get_location(LocationName.spark_mandrill_heart_tank, player), + lambda state: state.has(ItemName.boomerang_cutter, player)) + + # Storm Eagle collectibles + set_rule(multiworld.get_location(LocationName.storm_eagle_heart_tank, player), + lambda state: state.has(ItemName.legs, player)) + set_rule(multiworld.get_location(LocationName.storm_eagle_helmet, player), + lambda state: state.has(ItemName.legs, player)) + + # Handle bosses weakness + if world.options.logic_boss_weakness.value: + add_boss_weakness_logic(world) + + # Handle charged shotgun ice logic + if world.options.logic_charged_shotgun_ice.value: + add_charged_shotgun_ice_logic(world) + + +def add_boss_weakness_logic(world): + player = world.player + multiworld = world.multiworld + + # Armored Armadillo + set_rule(multiworld.get_entrance(f"{RegionName.armored_armadillo_ride_3} -> {RegionName.armored_armadillo_boss}", player), + lambda state: state.has(ItemName.electric_spark, player)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_rematch_1}", player), + lambda state: state.has(ItemName.electric_spark, player)) + + # Chill Penguin + set_rule(multiworld.get_entrance(f"{RegionName.chill_penguin_ride} -> {RegionName.chill_penguin_boss}", player), + lambda state: state.has(ItemName.fire_wave, player)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_2_start} -> {RegionName.sigma_fortress_2_rematch_1}", player), + lambda state: state.has(ItemName.fire_wave, player)) + + # Flame Mammoth + set_rule(multiworld.get_entrance(f"{RegionName.flame_mammoth_lava_river_2} -> {RegionName.flame_mammoth_boss}", player), + lambda state: state.has(ItemName.storm_tornado, player)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_rematch_4} -> {RegionName.sigma_fortress_3_rematch_5}", player), + lambda state: state.has(ItemName.storm_tornado, player)) + + # Boomer Kuwanger + set_rule(multiworld.get_entrance(f"{RegionName.boomer_kuwanger_top} -> {RegionName.boomer_kuwanger_boss}", player), + lambda state: state.has(ItemName.homing_torpedo, player)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_1_vertical} -> {RegionName.sigma_fortress_1_rematch_1}", player), + lambda state: state.has(ItemName.homing_torpedo, player)) + + # Sting Chameleon + set_rule(multiworld.get_entrance(f"{RegionName.sting_chameleon_swamp} -> {RegionName.sting_chameleon_boss}", player), + lambda state: state.has(ItemName.boomerang_cutter, player)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_rematch_1} -> {RegionName.sigma_fortress_3_rematch_2}", player), + lambda state: state.has(ItemName.boomerang_cutter, player)) + + # Spark Mandrill + set_rule(multiworld.get_entrance(f"{RegionName.spark_mandrill_deep} -> {RegionName.spark_mandrill_boss}", player), + lambda state: state.has(ItemName.shotgun_ice, player)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_rematch_2} -> {RegionName.sigma_fortress_3_rematch_3}", player), + lambda state: state.has(ItemName.shotgun_ice, player)) + + # Storm Eagle + set_rule(multiworld.get_entrance(f"{RegionName.storm_eagle_aircraft} -> {RegionName.storm_eagle_boss}", player), + lambda state: state.has(ItemName.chameleon_sting, player)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_2_ride} -> {RegionName.sigma_fortress_2_rematch_2}", player), + lambda state: state.has(ItemName.chameleon_sting, player)) + + # Launch Octopus + set_rule(multiworld.get_entrance(f"{RegionName.launch_octopus_sea} -> {RegionName.launch_octopus_boss}", player), + lambda state: state.has(ItemName.rolling_shield, player)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_rematch_3} -> {RegionName.sigma_fortress_3_rematch_4}", player), + lambda state: state.has(ItemName.rolling_shield, player)) + + # Bospider + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_1_rematch_1} -> {RegionName.sigma_fortress_1_boss}", player), + lambda state: state.has(ItemName.shotgun_ice, player)) + + # Rangda Bangda + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_2_rematch_2} -> {RegionName.sigma_fortress_2_boss}", player), + lambda state: state.has(ItemName.chameleon_sting, player)) + + # D-Rex + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_rematch_5} -> {RegionName.sigma_fortress_3_boss}", player), + lambda state: state.has(ItemName.boomerang_cutter, player)) + + # Velguarder + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_4} -> {RegionName.sigma_fortress_4_dog}", player), + lambda state: state.has(ItemName.shotgun_ice, player)) + + # Sigma & Wolf Sigma + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_4_dog} -> {RegionName.sigma_fortress_4_sigma}", player), + lambda state: ( + state.has(ItemName.electric_spark, player) and + state.has(ItemName.rolling_shield, player) + )) + + +def add_charged_shotgun_ice_logic(world): + player = world.player + multiworld = world.multiworld + + # Flame Mammoth collectibles + add_rule(multiworld.get_location(LocationName.flame_mammoth_sub_tank, player), + lambda state: ( + state.has(ItemName.arms, player) and + state.has(ItemName.boomerang_cutter, player) and + state.has(ItemName.shotgun_ice, player) + )) + # Boomer Kuwanger collectibles + add_rule(multiworld.get_location(LocationName.boomer_kuwanger_heart_tank, player), + lambda state: ( + state.has(ItemName.shotgun_ice, player) and + state.has(ItemName.arms, player) + )) + # Sting Chameleon collectibles + add_rule(multiworld.get_location(LocationName.sting_chameleon_body, player), + lambda state: ( + state.has(ItemName.shotgun_ice, player) and + state.has(ItemName.arms, player) + )) diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py new file mode 100644 index 000000000000..84aa16be99a6 --- /dev/null +++ b/worlds/mmx/__init__.py @@ -0,0 +1,231 @@ +import dataclasses +import os +import typing +import math +import settings +import hashlib +import threading +import pkgutil + +from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from Options import PerGameCommonOptions +from worlds.AutoWorld import World, WebWorld +from .Items import MMXItem, ItemData, item_table, junk_table, item_groups +from .Locations import MMXLocation, setup_locations, all_locations, location_groups +from .Regions import create_regions, connect_regions +from .Names import ItemName, LocationName, EventName +from .Options import MMXOptions +from .Client import MMXSNIClient +from .Rom import patch_rom, MMXProcedurePatch, HASH_US, HASH_LEGACY + +class MMXSettings(settings.Group): + class RomFile(settings.SNESRomPath): + """File name of the Mega Man X US ROM""" + description = "Mega Man X (USA) ROM File" + copy_to = "Mega Man X (USA).sfc" + md5s = [HASH_US, HASH_LEGACY] + + rom_file: RomFile = RomFile(RomFile.copy_to) + + +class MMXWeb(WebWorld): + theme = "ice" + + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to playing Mega Man X with Archipelago", + "English", + "setup_en.md", + "setup/en", + ["lx5"] + ) + + tutorials = [setup_en] + + +class MMXWorld(World): + """ + Mega Man X WIP + """ + game = "Mega Man X" + web = MMXWeb() + + settings: typing.ClassVar[MMXSettings] + + options_dataclass = MMXOptions + options: MMXOptions + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = all_locations + item_name_groups = item_groups + location_name_groups = location_groups + + def __init__(self, multiworld: MultiWorld, player: int): + self.rom_name_available_event = threading.Event() + super().__init__(multiworld, player) + + def create_regions(self) -> None: + location_table = setup_locations(self) + create_regions(self.multiworld, self.player, self, location_table) + + itempool: typing.List[MMXItem] = [] + + connect_regions(self) + + total_required_locations = 47 + if self.options.pickupsanity: + total_required_locations += 22 + + # Add levels into the pool + start_inventory = self.multiworld.start_inventory[self.player].value.copy() + stage_list = [ + ItemName.stage_armored_armadillo, + ItemName.stage_boomer_kuwanger, + ItemName.stage_chill_penguin, + ItemName.stage_flame_mammoth, + ItemName.stage_launch_octopus, + ItemName.stage_spark_mandrill, + ItemName.stage_sting_chameleon, + ItemName.stage_storm_eagle, + ] + stage_selected = self.random.randint(0, 7) + for i in range(len(stage_list)): + if i == stage_selected: + if stage_list[i] in self.options.start_inventory_from_pool: + itempool += [self.create_item(stage_list[i])] + total_required_locations += 1 + elif stage_list[i] not in start_inventory: + self.multiworld.get_location(LocationName.intro_completed, self.player).place_locked_item(self.create_item(stage_list[i])) + else: + itempool += [self.create_item(stage_list[i])] + + if self.options.sigma_open == "multiworld": + itempool += [self.create_item(ItemName.stage_sigma_fortress)] + + # Add weapons into the pool + itempool += [self.create_item(ItemName.shotgun_ice)] + itempool += [self.create_item(ItemName.electric_spark)] + itempool += [self.create_item(ItemName.rolling_shield)] + itempool += [self.create_item(ItemName.homing_torpedo)] + itempool += [self.create_item(ItemName.boomerang_cutter)] + itempool += [self.create_item(ItemName.chameleon_sting)] + itempool += [self.create_item(ItemName.storm_tornado)] + itempool += [self.create_item(ItemName.fire_wave)] + + # Add upgrades into the pool + if self.options.sigma_open.value == 3: + itempool += [self.create_item(ItemName.body)] + else: + itempool += [self.create_item(ItemName.body, ItemClassification.useful)] + itempool += [self.create_item(ItemName.arms)] + itempool += [self.create_item(ItemName.helmet)] + itempool += [self.create_item(ItemName.legs)] + + if self.options.sigma_open.value == 4: + itempool += [self.create_item(ItemName.heart_tank) for _ in range(8)] + else: + itempool += [self.create_item(ItemName.heart_tank, ItemClassification.useful) for _ in range(8)] + + if self.options.sigma_open.value == 5: + itempool += [self.create_item(ItemName.sub_tank) for _ in range(4)] + else: + itempool += [self.create_item(ItemName.sub_tank, ItemClassification.useful) for _ in range(4)] + + # Add junk items into the pool + junk_count = total_required_locations - len(itempool) + + junk_weights = [] + junk_weights += ([ItemName.small_hp] * 30) + junk_weights += ([ItemName.large_hp] * 40) + junk_weights += ([ItemName.life] * 30) + + junk_pool = [] + for i in range(junk_count): + junk_item = self.random.choice(junk_weights) + junk_pool.append(self.create_item(junk_item)) + + itempool += junk_pool + + # Set Maverick Medals + maverick_location_names = [ + LocationName.armored_armadillo_clear, + LocationName.boomer_kuwanger_clear, + LocationName.chill_penguin_clear, + LocationName.flame_mammoth_clear, + LocationName.launch_octopus_clear, + LocationName.spark_mandrill_clear, + LocationName.sting_chameleon_clear, + LocationName.storm_eagle_clear + ] + for location_name in maverick_location_names: + self.multiworld.get_location(location_name, self.player).place_locked_item(self.create_item(ItemName.maverick_medal)) + + # Set victory item + self.multiworld.get_location(LocationName.sigma_fortress_4_sigma, self.player).place_locked_item(self.create_item(ItemName.victory)) + + # Finish + self.multiworld.itempool += itempool + + def create_item(self, name: str, force_classification=False) -> Item: + data = item_table[name] + + if force_classification: + classification = force_classification + elif data.progression: + classification = ItemClassification.progression + elif data.trap: + classification = ItemClassification.trap + else: + classification = ItemClassification.filler + + created_item = MMXItem(name, classification, data.code, self.player) + + return created_item + + def set_rules(self): + from .Rules import set_rules + set_rules(self) + + def fill_slot_data(self): + slot_data = {} + for option_name in (attr.name for attr in dataclasses.fields(MMXOptions) + if attr not in dataclasses.fields(PerGameCommonOptions)): + option = getattr(self.options, option_name) + slot_data[option_name] = option.value + return slot_data + + def generate_early(self): + if self.options.early_legs: + self.multiworld.early_items[self.player][ItemName.legs] = 1 + + def get_filler_item_name(self) -> str: + return self.random.choice(list(junk_table.keys())) + + def generate_output(self, output_directory: str): + try: + patch = MMXProcedurePatch() + patch.write_file("mmx_basepatch.bsdiff4", pkgutil.get_data(__name__, "data/mmx_basepatch.bsdiff4")) + patch_rom(self, patch) + + self.rom_name = patch.name + + patch.write(os.path.join(output_directory, + f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}")) + except Exception: + raise + finally: + self.rom_name_available_event.set() # make sure threading continues and errors are collected + + def modify_multidata(self, multidata: dict): + import base64 + # wait for self.rom_name to be available. + self.rom_name_available_event.wait() + rom_name = getattr(self, "rom_name", None) + # we skip in case of error, so that the original error in the output thread is the one that gets raised + if rom_name: + new_name = base64.b64encode(bytes(self.rom_name)).decode() + multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] + + @classmethod + def stage_fill_hook(cls, multiworld: MultiWorld, progitempool, usefulitempool, filleritempool, fill_locations): + return \ No newline at end of file diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 new file mode 100644 index 0000000000000000000000000000000000000000..9f3431c58785deb2781234b3dfb8818310f569f5 GIT binary patch literal 2451 zcmV;E32gR4Q$$HdMl>++000000001{0ssI20000002lxO0000&T4*^jL0KkKS#sz{ zp8x>A|L^}j@Ae9wfj|HP1kwNu02jmn30| zO{ub*MAK?%g#A&5pwM7OLq>+03`GcOG-SX=L5)mDpc+g9eyCjq42Cy(&plv3bc8r8 zjBU6%#1vWA;ebU6KInuJtjoo}C%wO%)7rP9S+S2cm=XX#TVrw!8Cad&@Xn$HYebRH zQ0X@HU&ydjjPkJ-W1#@?;p3*>I8cys=trMIT4*^jL0KkKS#7yzrvLy5fB*kq+n?_L?Y*Uw?ro3w zM=E(d3@4rX`i;E!^?T2|d)dGN+k>1eMNEw&O&VweCP2b8G5`PoG-v_p4491oF&bzC zO*CW-${J~>siut@Vgyw`6w+wW9*{ji18N#*X`s=g2m?kyXd6(_0|)?U0004|fDD67 zg-s?VqtwI$LnebmA%X)&OpJ{h0ilp-kYocB1ZXk1uU00000000000000D4X8lp=jZ}Gui_LXfJnq~iV9GGfJ3+|V159UFr>c}KmkZ< zLLrXfK>AfewhKxRvVNka2(7XYt^rB}fb8Iwn3N(t$dsZqV$q4bhy#6s?cgKyic~;B zGx0&Sz!IV=_Fy2iASxU90u2}-SwKX*#Q;$ceOT2+N^u_41yBL2fmssK00aPQUDgf*3%e)M`x_88kJ}sp}61IR^pO>aU?0N^gc@vdNSmu@%Om zJ|0IDLJI-t^Nwea4&AyMKevlvDl73?GoE695dbP&!2?m2vjyx(YpTv->8Z6Tz(-j6 zV?uP{md0Em{7nJ%C3TG71DIpi;rItvU|@ zi-|-4QzGdf3a;!tlQw@N$hAjo6X(Aq-xOh@FGq>`VS!t0P7ER~K}jw+bFpJNnBZ zkBjAfD6)6Z2|3+9M_kNSK)=Fw6I%wZNNe<%Ejc7(-MT{1n^R|n@$Rgy6`$OF|4FU5 zsH&<{|5@EG)c@k{NT&)C4Yw@x;6hqxIWa+5CR16@`C7vO0OWuF|NsC0-t5WW@9o?b zZtJhT{{H0s+Pm*g*7E#)e{X-)&;&jO73kY8yLMi-8bv1cO)!a?m`~KwJerxNk)|f~ z0W`!6k5f!0L8c}qhC!y($Y!DH8U~Dm(r93e36ZC%>7pK!XqzDtNNrEm4FG5w0}y(e z0MO6?WEvU(XaE2-GynhqXwU!v(8vI207J<=2BtKaKxorVG|`9v00T`L0BB+W0iYUa z4FG5jG6sNXWB|wmK+s_n+C>jiW{Hu3p`^`BYBNm+ni>EGfC=fO$$$V200002U;qFF z(*Pp?5}Jqv0W`={(^J%WGM=W7O-HEAqsk8^lWKZuXwYI0P#%**)CNqArl!&~2ATlS z9-)z-0g%~E0g!0$9xV#cG`Vivqct%kjN(aP`C`)EaC+7upV%5FQwwpCfi`aVobPip0^-!-_w)JlmaD(MV*5$Er0(f{Uh|J5?-O8Phb)SrE>dsT$dP zrihUg2(I5gc?|gXwD`P{f?8lif?91ronIqF`P$BPPL8R3Tk+W>V3{#V>*v*~!cr7H zyvC(1C|1rtnpu^JIdDKcnN0)@i*{<_|84Pz z?My+##FkYt_GKbOfhLs+drx_}!1Xs!ZKOU8Wu+VF))5M8ZGR2{>L4%Sb2C~8h9d_~ zDC5P7y5}vJ1c_(g-3o|#DYsM|nUtj|>x+OaSv%_x+d;2e3R65PLa-%wpp4dyQwGr{ zTqw)uWjFb`jOrmUef1(wN5mqORL|;vL#6C=45$_+r^c`ANdqf~|7xY!0yt zjMiell}4ev z3_I$jF7HYJfR+U@L16)8A!KRMkoa8`Z1=r6lXD=1MdB;e$E}zI)`mllUO3#zah13D zxz@T=vGpH?C6&WhuFVm8?nk(bUiaPa^ftGia@rTEih@YIWZ}#L1Sp9JLJ%iF_2sq& zaaPvjrKWJMrbrF{0V-4qgsD*&tqy^7&KA&Zvo8;Zjc_;}27HGhnmpU;EBsGS~8Q>pgP!@w+z_&nXQ$;+Xse9dPUIgfYaT&;$41-gIhf)^!EVBVgC7g^ks%)dx> zI)j`!I^#+*r+rD?dsE1P#ynVIGdqE%C+-t~-%Uq3_rMGfjr_Q$Qh}2cHwb00`Eg=8 z4kMBv0R-d-AeI1#gTfYgfMm!GC). **The installer + file is located in the assets section at the bottom of the version information.** +2. The first time you patch your game, you will be asked to locate your base ROM file. + This is your Mega Man X3 ROM file. This only needs to be done once. +3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM + files. + 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. + 2. Right-click on a ROM file and select **Open with...** + 3. Check the box next to **Always use this app to open .sfc files** + 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC** + 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you + extracted in step one. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The Player Settings page on the website allows you to configure your personal settings and export a config file from +them. Player settings page: [Mega Man X3 Player Settings Page](/games/Mega%20Man%20X3/player-settings) + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/check) + +## Joining a MultiWorld Game + +### Obtain your patch file and create your ROM + +When you join a multiworld game, you will be asked to provide your config file to whomever is hosting. Once that is done, +the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch +files. Your patch file should have a `.apmmx3` extension. + +Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the +client, and will also create your ROM in the same place as your patch file. + +### Connect to the client + +#### With an emulator + +When the client launched automatically, SNI should have also automatically launched in the background. If this is its +first time launching, you may be prompted to allow it to communicate through the Windows Firewall. + +##### snes9x-rr + +1. Load your ROM file if it hasn't already been loaded. +2. Click on the File menu and hover on **Lua Scripting** +3. Click on **New Lua Script Window...** +4. In the new window, click **Browse...** +5. Select the connector lua file included with your client + - Look in the Archipelago folder for `/SNI/lua/Connector.lua`. +6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of +the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install. + +##### BizHawk + +1. Ensure you have the BSNES core loaded. This is done with the main menubar, under: + - (≤ 2.8) `Config` 〉 `Cores` 〉 `SNES` 〉 `BSNES` + - (≥ 2.9) `Config` 〉 `Preferred Cores` 〉 `SNES` 〉 `BSNESv115+` +2. Load your ROM file if it hasn't already been loaded. + If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R). +3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window. + - Look in the Archipelago folder for `/SNI/lua/Connector.lua`. + - You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua` + with the file picker. + +##### RetroArch 1.10.3 or newer + +You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.3. + +1. Enter the RetroArch main menu screen. +2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON. +3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default + Network Command Port at 55355. + +![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) +4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury + Performance)". + +When loading a ROM, be sure to select a **bsnes-mercury** core. These are the only cores that allow external tools to +read ROM data. + +#### With hardware + +This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do +this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES +releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases) + +Other hardware may find helpful information on the usb2snes platforms +page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms) + +1. Close your emulator, which may have auto-launched. +2. Power on your device and load the ROM. + +### Connect to the Archipelago Server + +The patch file which launched your client should have automatically connected you to the AP Server. There are a few +reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the +client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it +into the "Server" input field then press enter. + +The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". + +### Play the game + +When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on +successfully joining a multiworld game! + +## Hosting a MultiWorld game + +The recommended way to host a game is to use our hosting service. The process is relatively simple: + +1. Collect config files from your players. +2. Create a zip file containing your players' config files. +3. Upload that zip file to the Generate page above. + - Generate page: [WebHost Seed Generation Page](/generate) +4. Wait a moment while the seed is generated. +5. When the seed is generated, you will be redirected to a "Seed Info" page. +6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so + they may download their patch files from there. +7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all + players in the game. Any observers may also be given the link to this page. +8. Once all players have joined, you may begin playing. From 9e2b66c37f27ee9395bdfc66f805a2a569b4b82c Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Sun, 21 Apr 2024 23:27:35 -0700 Subject: [PATCH 02/71] Fix sigma not giving a check, heart tanks disabling pause menu --- worlds/mmx/Client.py | 6 ------ worlds/mmx/Levels.py | 2 +- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 2451 -> 2467 bytes 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index 3c1807b4ff70..49f081ef6942 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -18,7 +18,6 @@ MMX_MENU_STATE = WRAM_START + 0x000D2 MMX_GAMEPLAY_STATE = WRAM_START + 0x000D3 MMX_LEVEL_INDEX = WRAM_START + 0x01F7A -MMX_VICTORY = WRAM_START + 0x01F7F MMX_WEAPON_ARRAY = WRAM_START + 0x01F88 MMX_SUB_TANK_ARRAY = WRAM_START + 0x01F83 @@ -435,7 +434,6 @@ async def game_watcher(self, ctx): defeated_bosses = list(defeated_bosses_data) cleared_levels_data = await snes_read(ctx, MMX_COMPLETED_LEVELS, 0x20) cleared_levels = list(cleared_levels_data) - victory_ram = await snes_read(ctx, MMX_VICTORY, 0x1) collected_heart_tanks_data = await snes_read(ctx, MMX_COLLECTED_HEART_TANKS, 0x01) collected_upgrades_data = await snes_read(ctx, MMX_COLLECTED_UPGRADES, 0x01) collected_hadouken_data = await snes_read(ctx, MMX_COLLECTED_HADOUKEN, 0x01) @@ -478,10 +476,6 @@ async def game_watcher(self, ctx): # Hadouken if collected_hadouken_data[0] != 0x00: new_checks.append(loc_id) - elif internal_id == 0x006: - # Victory - if menu_state[0] == 0x02 and gameplay_state[0] == 0x16 and victory_ram[0] == 0x01: - new_checks.append(loc_id) elif internal_id == 0x007: # Intro if game_state[0] == 0x02 and menu_state[0] == 0x00 and gameplay_state[0] == 0x01: diff --git a/worlds/mmx/Levels.py b/worlds/mmx/Levels.py index db30b9a5e131..5f38f4535f1e 100644 --- a/worlds/mmx/Levels.py +++ b/worlds/mmx/Levels.py @@ -71,5 +71,5 @@ LocationName.sigma_fortress_3_d_rex: [0x0B, 0x000, 0x1E], LocationName.sigma_fortress_4_velguarder: [0x0C, 0x000, 0x13], - LocationName.sigma_fortress_4_sigma: [0x0C, 0x006, 0x14], + LocationName.sigma_fortress_4_sigma: [0x0C, 0x000, 0x1F], } diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 9f3431c58785deb2781234b3dfb8818310f569f5..21e51acdbade530aea09bb421f07614cce3715bc 100644 GIT binary patch literal 2467 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 Date: Mon, 22 Apr 2024 18:40:39 -0700 Subject: [PATCH 03/71] Add pickupsanity support --- worlds/mmx/Client.py | 20 +++++------ worlds/mmx/Levels.py | 31 ++++++++++++++-- worlds/mmx/Locations.py | 33 +++++++++-------- worlds/mmx/Names/LocationName.py | 49 ++++++++++++++------------ worlds/mmx/Options.py | 1 - worlds/mmx/Regions.py | 43 +++++++++++++++++++++- worlds/mmx/Rules.py | 45 +++++++++++++++++++++++ worlds/mmx/__init__.py | 6 ++-- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 2467 -> 2549 bytes 9 files changed, 174 insertions(+), 54 deletions(-) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index 49f081ef6942..079721ce341a 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -270,10 +270,12 @@ async def handle_item_queue(self, ctx): can_move = await snes_read(ctx, MMX_CAN_MOVE, 0x7) pause_state = await snes_read(ctx, MMX_PAUSE_STATE, 0x1) if menu_state[0] != 0x04 or \ - gameplay_state[0] != 0x04 or \ - pause_state[0] != 0x00 or \ - can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ - receiving_item[0] != 0x00: + gameplay_state[0] != 0x04 or \ + pause_state[0] != 0x00 or \ + can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ + receiving_item[0] != 0x00: + backup_item = self.item_queue.pop(0) + self.item_queue.append(backup_item) return if next_item[0] in PICKUP_ITEMS: @@ -450,8 +452,6 @@ async def game_watcher(self, ctx): if internal_id == 0x000: # Boss clear if defeated_bosses_data[data_bit] != 0: - snes_logger.info(f"loc_name: {loc_name} ({defeated_bosses_data[data_bit]}) [{data}]") - snes_logger.info(f"{defeated_bosses}") new_checks.append(loc_id) elif internal_id == 0x001: # Maverick Medal @@ -480,12 +480,11 @@ async def game_watcher(self, ctx): # Intro if game_state[0] == 0x02 and menu_state[0] == 0x00 and gameplay_state[0] == 0x01: new_checks.append(loc_id) - elif internal_id >= 0x100: + elif internal_id == 0x020: # Pickups if not pickupsanity_enabled or pickupsanity_enabled[0] == 0: continue - pickup_id = internal_id & 0x1F - if collected_pickups_data[pickup_id] != 0: + if collected_pickups_data[data_bit] != 0: new_checks.append(loc_id) verify_game_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 1) @@ -620,8 +619,7 @@ async def game_watcher(self, ctx): new_hadouken = True elif internal_id >= 0x100: # Pickups - pickup_id = internal_id & 0x3F - collected_pickups[pickup_id] = 0xFF + collected_pickups[data_bit] = 0x01 new_pickup = True if new_cleared_level: diff --git a/worlds/mmx/Levels.py b/worlds/mmx/Levels.py index 5f38f4535f1e..e0a1672e3949 100644 --- a/worlds/mmx/Levels.py +++ b/worlds/mmx/Levels.py @@ -4,8 +4,10 @@ location_id_to_level_id = { LocationName.intro_completed: [0x00, 0x007, 0x00], - LocationName.intro_mini_boss_1: [0x03, 0x000, 0x1C], - LocationName.intro_mini_boss_2: [0x03, 0x000, 0x1D], + LocationName.intro_mini_boss_1: [0x00, 0x000, 0x1C], + LocationName.intro_mini_boss_2: [0x00, 0x000, 0x1D], + LocationName.intro_hp_1: [0x00, 0x020, 0x00], + LocationName.intro_hp_2: [0x00, 0x020, 0x01], LocationName.armored_armadillo_boss: [0x03, 0x000, 0x00], LocationName.armored_armadillo_clear: [0x03, 0x001, 0x04], @@ -14,11 +16,15 @@ LocationName.armored_armadillo_hadouken: [0x03, 0x005, 0x00], LocationName.armored_armadillo_mini_boss_1: [0x03, 0x000, 0x15], LocationName.armored_armadillo_mini_boss_2: [0x03, 0x000, 0x16], + LocationName.armored_armadillo_hp_1: [0x00, 0x020, 0x05], + LocationName.armored_armadillo_hp_2: [0x00, 0x020, 0x06], + LocationName.armored_armadillo_hp_3: [0x00, 0x020, 0x07], LocationName.chill_penguin_boss: [0x08, 0x000, 0x01], LocationName.chill_penguin_clear: [0x08, 0x001, 0x0E], LocationName.chill_penguin_legs: [0x08, 0x003, 0x08], LocationName.chill_penguin_heart_tank: [0x08, 0x002, 0x01], + LocationName.chill_penguin_hp_1: [0x08, 0x020, 0x12], LocationName.spark_mandrill_boss: [0x06, 0x000, 0x02], LocationName.spark_mandrill_clear: [0x06, 0x001, 0x0A], @@ -33,6 +39,7 @@ LocationName.launch_octopus_mini_boss_2: [0x01, 0x000, 0x19], LocationName.launch_octopus_mini_boss_3: [0x01, 0x000, 0x1A], LocationName.launch_octopus_mini_boss_4: [0x01, 0x000, 0x1B], + LocationName.launch_octopus_hp_1: [0x01, 0x020, 0x02], LocationName.boomer_kuwanger_boss: [0x07, 0x000, 0x04], LocationName.boomer_kuwanger_clear: [0x07, 0x001, 0x0C], @@ -42,18 +49,29 @@ LocationName.sting_chameleon_clear: [0x02, 0x001, 0x02], LocationName.sting_chameleon_body: [0x02, 0x003, 0x04], LocationName.sting_chameleon_heart_tank: [0x02, 0x002, 0x08], + LocationName.sting_chameleon_1up: [0x02, 0x020, 0x03], + LocationName.sting_chameleon_hp_1: [0x02, 0x020, 0x04], LocationName.storm_eagle_boss: [0x05, 0x000, 0x06], LocationName.storm_eagle_clear: [0x05, 0x001, 0x08], LocationName.storm_eagle_sub_tank: [0x05, 0x004, 0x10], LocationName.storm_eagle_heart_tank: [0x05, 0x002, 0x04], - LocationName.storm_eagle_helmet: [0x05, 0x003, 0x01], + LocationName.storm_eagle_1up_1: [0x05, 0x020, 0x0E], + LocationName.storm_eagle_1up_2: [0x05, 0x020, 0x0F], + LocationName.storm_eagle_hp_1: [0x05, 0x020, 0x0B], + LocationName.storm_eagle_hp_2: [0x05, 0x020, 0x0C], + LocationName.storm_eagle_hp_3: [0x05, 0x020, 0x0D], + #LocationName.storm_eagle_hp_4: [0x05, 0x020, 0x10], + #LocationName.storm_eagle_energy_1: [0x05, 0x020, 0x11], LocationName.flame_mammoth_boss: [0x04, 0x000, 0x07], LocationName.flame_mammoth_clear: [0x04, 0x001, 0x06], LocationName.flame_mammoth_sub_tank: [0x04, 0x004, 0x80], LocationName.flame_mammoth_heart_tank: [0x04, 0x002, 0x10], LocationName.flame_mammoth_arms: [0x04, 0x003, 0x02], + LocationName.flame_mammoth_hp_1: [0x04, 0x020, 0x08], + LocationName.flame_mammoth_hp_2: [0x04, 0x020, 0x09], + LocationName.flame_mammoth_1up: [0x04, 0x020, 0x0A], LocationName.sigma_fortress_1_bospider: [0x09, 0x000, 0x08], LocationName.sigma_fortress_1_vile: [0x09, 0x000, 0x09], @@ -69,6 +87,13 @@ LocationName.sigma_fortress_3_launch_octopus: [0x0B, 0x000, 0x11], LocationName.sigma_fortress_3_flame_mammoth: [0x0B, 0x000, 0x12], LocationName.sigma_fortress_3_d_rex: [0x0B, 0x000, 0x1E], + LocationName.sigma_fortress_3_hp_1: [0x0B, 0x020, 0x13], + LocationName.sigma_fortress_3_hp_2: [0x0B, 0x020, 0x15], + LocationName.sigma_fortress_3_hp_3: [0x0B, 0x020, 0x16], + LocationName.sigma_fortress_3_hp_4: [0x0B, 0x020, 0x18], + LocationName.sigma_fortress_3_energy_1: [0x0B, 0x020, 0x14], + LocationName.sigma_fortress_3_energy_2: [0x0B, 0x020, 0x17], + LocationName.sigma_fortress_3_1up: [0x0B, 0x020, 0x1A], LocationName.sigma_fortress_4_velguarder: [0x0C, 0x000, 0x13], LocationName.sigma_fortress_4_sigma: [0x0C, 0x000, 0x1F], diff --git a/worlds/mmx/Locations.py b/worlds/mmx/Locations.py index d30ebefd4fcf..d0c9cfc469d2 100644 --- a/worlds/mmx/Locations.py +++ b/worlds/mmx/Locations.py @@ -79,20 +79,25 @@ def __init__(self, player: int, name: str = '', address: int = None, parent=None LocationName.launch_octopus_hp_1: STARTING_ID + 0x0055, LocationName.sting_chameleon_1up: STARTING_ID + 0x0056, LocationName.sting_chameleon_hp_1: STARTING_ID + 0x0057, - LocationName.storm_eagle_1up: STARTING_ID + 0x0058, - LocationName.storm_eagle_hp_1: STARTING_ID + 0x0059, - LocationName.storm_eagle_energy_1: STARTING_ID + 0x005A, - LocationName.flame_mammoth_hp_1: STARTING_ID + 0x005B, - LocationName.flame_mammoth_hp_2: STARTING_ID + 0x005C, - LocationName.flame_mammoth_1up: STARTING_ID + 0x005D, - LocationName.sigma_fortress_3_hp_1: STARTING_ID + 0x005E, - LocationName.sigma_fortress_3_hp_2: STARTING_ID + 0x005F, - LocationName.sigma_fortress_3_energy_1: STARTING_ID + 0x0060, - LocationName.sigma_fortress_3_hp_3: STARTING_ID + 0x0061, - LocationName.sigma_fortress_3_energy_2: STARTING_ID + 0x0062, - LocationName.sigma_fortress_3_hp_4: STARTING_ID + 0x0063, - LocationName.sigma_fortress_3_energy_3: STARTING_ID + 0x0064, - LocationName.sigma_fortress_3_1up: STARTING_ID + 0x0065, + LocationName.storm_eagle_1up_1: STARTING_ID + 0x0058, + LocationName.storm_eagle_1up_2: STARTING_ID + 0x0059, + LocationName.storm_eagle_hp_1: STARTING_ID + 0x005A, + LocationName.storm_eagle_hp_2: STARTING_ID + 0x005B, + LocationName.storm_eagle_hp_3: STARTING_ID + 0x005C, + #LocationName.storm_eagle_hp_4: STARTING_ID + 0x005D, + #LocationName.storm_eagle_energy_1: STARTING_ID + 0x005E, + LocationName.flame_mammoth_hp_1: STARTING_ID + 0x005F, + LocationName.flame_mammoth_hp_2: STARTING_ID + 0x0060, + LocationName.flame_mammoth_1up: STARTING_ID + 0x0061, + LocationName.sigma_fortress_3_hp_1: STARTING_ID + 0x0062, + LocationName.sigma_fortress_3_hp_2: STARTING_ID + 0x0063, + LocationName.sigma_fortress_3_energy_1: STARTING_ID + 0x0064, + LocationName.sigma_fortress_3_hp_3: STARTING_ID + 0x0065, + LocationName.sigma_fortress_3_energy_2: STARTING_ID + 0x0066, + LocationName.sigma_fortress_3_hp_4: STARTING_ID + 0x0067, + LocationName.sigma_fortress_3_energy_3: STARTING_ID + 0x0068, + LocationName.sigma_fortress_3_1up: STARTING_ID + 0x0069, + LocationName.chill_penguin_hp_1: STARTING_ID + 0x006A, } stage_clears = { diff --git a/worlds/mmx/Names/LocationName.py b/worlds/mmx/Names/LocationName.py index c9167560c1b0..886905e0eabd 100644 --- a/worlds/mmx/Names/LocationName.py +++ b/worlds/mmx/Names/LocationName.py @@ -1,5 +1,5 @@ -intro_hp_1 = "Intro Stage - HP Pickup 1" -intro_hp_2 = "Intro Stage - HP Pickup 2" +intro_hp_1 = "Intro Stage - HP Pickup (after Bee Blader)" +intro_hp_2 = "Intro Stage - Small HP Pickup (after Bee Blader)" intro_completed = "Intro Stage - Get Defeated By Vile" intro_mini_boss_1 = "Intro Stage - Defeated Bee Blader #1" intro_mini_boss_2 = "Intro Stage - Defeated Bee Blader #2" @@ -7,10 +7,10 @@ armored_armadillo_boss = "Defeated Armored Armadillo" armored_armadillo_clear = "Armored Armadillo Clear" armored_armadillo_sub_tank = "Armored Armadillo - Sub Tank" -armored_armadillo_hp_1 = "Armored Armadillo - HP Pickup 1" -armored_armadillo_hp_2 = "Armored Armadillo - HP Pickup 2" +armored_armadillo_hp_1 = "Armored Armadillo Stage - HP Pickup 1 (In blocked ceiling)" +armored_armadillo_hp_2 = "Armored Armadillo Stage - HP Pickup 2 (In blocked ceiling)" armored_armadillo_heart_tank = "Armored Armadillo - Heart Tank" -armored_armadillo_hp_3 = "Armored Armadillo - HP Pickup 3" +armored_armadillo_hp_3 = "Armored Armadillo Stage - HP Pickup 3 (Next to Hadouken capsule)" armored_armadillo_hadouken = "Armored Armadillo - Hadouken Capsule" armored_armadillo_mini_boss_1 = "Defeated Mole Borer #1" armored_armadillo_mini_boss_2 = "Defeated Mole Borer #2" @@ -19,6 +19,7 @@ chill_penguin_clear = "Chill Penguin Clear" chill_penguin_legs = "Chill Penguin Stage - Legs Capsule" chill_penguin_heart_tank = "Chill Penguin Stage - Heart Tank" +chill_penguin_hp_1 = "Chill Penguin Stage - Weapon Energy Pickup (Inside third dome)" spark_mandrill_boss = "Defeated Spark Mandrill" spark_mandrill_clear = "Spark Mandrill Clear" @@ -32,7 +33,7 @@ launch_octopus_mini_boss_2 = "Defeated Anglerge #2" launch_octopus_mini_boss_3 = "Defeated Utuboros #1" launch_octopus_mini_boss_4 = "Defeated Utuboros #2" -launch_octopus_hp_1 = "Launch Octopus - HP Pickup 1" +launch_octopus_hp_1 = "Launch Octopus Stage - HP Pickup (Crane platform above water)" launch_octopus_heart_tank = "Launch Octopus Stage - Heart Tank" boomer_kuwanger_boss = "Defeated Boomer Kuwanger" @@ -43,25 +44,29 @@ sting_chameleon_clear = "Sting Chameleon Clear" sting_chameleon_heart_tank = "Sting Chameleon - Heart Tank" sting_chameleon_body = "Sting Chameleon Stage - Body Capsule" -sting_chameleon_1up = "Sting Chameleon Stage - 1-Up" -sting_chameleon_hp_1 = "Sting Chameleon Stage - HP Pickup 1" +sting_chameleon_1up = "Sting Chameleon Stage - 1up Pickup (Inside second mini-cave in mountain)" +sting_chameleon_hp_1 = "Sting Chameleon Stage - HP Pickup (On top of platform in the swamp)" storm_eagle_boss = "Defeated Storm Eagle" storm_eagle_clear = "Storm Eagle Clear" storm_eagle_heart_tank = "Storm Eagle Stage - Heart Tank" storm_eagle_sub_tank = "Storm Eagle Stage - Sub Tank" +storm_eagle_hp_1 = "Storm Eagle Stage - HP Pickup 1 (Behind first set of gas tanks)" +storm_eagle_hp_2 = "Storm Eagle Stage - HP Pickup 2 (Behind second set of gas tanks)" +storm_eagle_hp_3 = "Storm Eagle Stage - HP Pickup 3 (Behind third set of gas tanks)" +storm_eagle_1up_1 = "Storm Eagle Stage - 1up Pickup 1 (Behind fourth set of gas tanks)" storm_eagle_helmet = "Storm Eagle Stage - Helmet Capsule" -storm_eagle_1up = "Storm Eagle Stage - 1-Up" -storm_eagle_hp_1 = "Storm Eagle Stage - HP Pickup 1" -storm_eagle_energy_1 = "Storm Eagle Stage - Weapon Energy Pickup 1" +storm_eagle_1up_2 = "Storm Eagle Stage - 1up Pickup 2 (Above helmet capsule)" +storm_eagle_hp_4 = "Storm Eagle Stage - HP Pickup 4 (Right of plane)" +storm_eagle_energy_1 = "Storm Eagle Stage - Weapon Energy Pickup (Right of plane)" flame_mammoth_boss = "Defeated Flame Mammoth" flame_mammoth_clear = "Flame Mammoth Clear" -flame_mammoth_hp_1 = "Flame Mammoth Stage - HP Pickup 1" +flame_mammoth_hp_1 = "Flame Mammoth Stage - HP Pickup 1 (After first conveyor belts section)" flame_mammoth_arms = "Flame Mammoth Stage - Arms Capsule" flame_mammoth_heart_tank = "Flame Mammoth Stage - Heart Tank" -flame_mammoth_hp_2 = "Flame Mammoth Stage - HP Pickup 2" -flame_mammoth_1up = "Flame Mammoth Stage - 1-Up" +flame_mammoth_hp_2 = "Flame Mammoth Stage - HP Pickup 2 (Top platform in Dig Labour section)" +flame_mammoth_1up = "Flame Mammoth Stage - 1up Pickup (Top platform in Dig Labour section)" flame_mammoth_sub_tank = "Flame Mammoth Stage - Sub Tank" sigma_fortress_1_bospider = "Defeated Bospider" @@ -73,17 +78,17 @@ sigma_fortress_2_rangda_bangda = "Defeated Rangda Bangda" sigma_fortress_3_armored_armadillo = "Defeated Armored Armadillo (Rematch)" -sigma_fortress_3_hp_1 = "Sigma's Fortress 3 Stage - HP Pickup 1" +sigma_fortress_3_hp_1 = "Sigma's Fortress 3 - HP Pickup 1 (Before Sting Chameleon rematch)" sigma_fortress_3_sting_chameleon = "Defeated Sting Chameleon (Rematch)" -sigma_fortress_3_hp_2 = "Sigma's Fortress 3 Stage - HP Pickup 2" -sigma_fortress_3_energy_1 = "Sigma's Fortress 3 Stage - Weapon Energy Pickup 1" +sigma_fortress_3_hp_2 = "Sigma's Fortress 3 - Weapon Energy Pickup 1 (Before Spark Mandrill rematch)" +sigma_fortress_3_energy_1 = "Sigma's Fortress 3 - HP Pickup 2 (Before Spark Mandrill rematch)" sigma_fortress_3_spark_mandrill = "Defeated Armored Spark Mandrill (Rematch)" -sigma_fortress_3_hp_3 = "Sigma's Fortress 3 Stage - HP Pickup 3" -sigma_fortress_3_energy_2 = "Sigma's Fortress 3 Stage - Weapon Energy Pickup 2" +sigma_fortress_3_hp_3 = "Sigma's Fortress 3 - HP Pickup 3 (Before Launch Octopus rematch)" +sigma_fortress_3_energy_2 = "Sigma's Fortress 3 - Weapon Energy Pickup 2 (Before Launch Octopus rematch)" sigma_fortress_3_launch_octopus = "Defeated Launch Octopus (Rematch)" -sigma_fortress_3_hp_4 = "Sigma's Fortress 3 Stage - HP Pickup 4" -sigma_fortress_3_energy_3 = "Sigma's Fortress 3 Stage - Weapon Energy Pickup 3" -sigma_fortress_3_1up = "Sigma's Fortress 3 Stage - 1-Up" +sigma_fortress_3_hp_4 = "Sigma's Fortress 3 - HP Pickup 4 (On spikes)" +sigma_fortress_3_energy_3 = "Sigma's Fortress 3 - Weapon Energy Pickup 3 (On Spikes)" +sigma_fortress_3_1up = "Sigma's Fortress 3 - 1up Pickup (On spikes)" sigma_fortress_3_flame_mammoth = "Defeated Flame Mammoth (Rematch)" sigma_fortress_3_d_rex = "Defeated D-Rex" diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index 5a5a001dd345..b2fa1d958dde 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -48,7 +48,6 @@ class EarlyLegs(Toggle): class PickupSanity(Toggle): """ Whether collecting freestanding 1ups, HP and Weapon Energy capsules will grant a check. - DOES NOT WORK YET. DO NOT ENABLE. """ display_name = "Pickupsanity" diff --git a/worlds/mmx/Regions.py b/worlds/mmx/Regions.py index b2d4fe589666..ba0a5cce1bb5 100644 --- a/worlds/mmx/Regions.py +++ b/worlds/mmx/Regions.py @@ -265,7 +265,48 @@ def create_regions(multiworld: MultiWorld, player: int, world: World, active_loc add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_4_sigma, LocationName.sigma_fortress_4_sigma) if world.options.pickupsanity: - return + add_location_to_region(multiworld, player, active_locations, RegionName.intro, LocationName.intro_hp_1) + add_location_to_region(multiworld, player, active_locations, RegionName.intro, LocationName.intro_hp_2) + + # Armored Armadillo + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_1, LocationName.armored_armadillo_hp_1) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_1, LocationName.armored_armadillo_hp_2) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_ride_3, LocationName.armored_armadillo_hp_3) + + # Chill Penguin + add_location_to_region(multiworld, player, active_locations, RegionName.chill_penguin_ride, LocationName.chill_penguin_hp_1) + + # Launch Octopus + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_sea, LocationName.launch_octopus_hp_1) + + # Sting Chameleon + add_location_to_region(multiworld, player, active_locations, RegionName.sting_chameleon_hill, LocationName.sting_chameleon_1up) + add_location_to_region(multiworld, player, active_locations, RegionName.sting_chameleon_swamp, LocationName.sting_chameleon_hp_1) + + # Storm Eagle + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_hp_1) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_hp_2) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_hp_3) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_1up_1) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_metal, LocationName.storm_eagle_1up_1) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_metal, LocationName.storm_eagle_1up_2) + #add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_aircraft, LocationName.storm_eagle_hp_4) + #add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_aircraft, LocationName.storm_eagle_energy_1) + + # Flame Mammoth + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_conveyors_1, LocationName.flame_mammoth_hp_1) + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_1, LocationName.flame_mammoth_hp_2) + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_1, LocationName.flame_mammoth_1up) + + # Sigma's Fortress 3 + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_2, LocationName.sigma_fortress_3_hp_1) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_3, LocationName.sigma_fortress_3_hp_2) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_3, LocationName.sigma_fortress_3_energy_1) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_4, LocationName.sigma_fortress_3_hp_3) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_4, LocationName.sigma_fortress_3_energy_2) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_5, LocationName.sigma_fortress_3_hp_4) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_5, LocationName.sigma_fortress_3_energy_3) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_5, LocationName.sigma_fortress_3_1up) def connect_regions(world: World): connect(world, "Menu", RegionName.intro) diff --git a/worlds/mmx/Rules.py b/worlds/mmx/Rules.py index 8db2e1e14005..78daf5d82596 100644 --- a/worlds/mmx/Rules.py +++ b/worlds/mmx/Rules.py @@ -105,6 +105,10 @@ def set_rules(world: MMXWorld): set_rule(multiworld.get_location(LocationName.storm_eagle_helmet, player), lambda state: state.has(ItemName.legs, player)) + # Handle pickupsanity + if world.options.pickupsanity.value: + add_pickupsanity_logic(world) + # Handle bosses weakness if world.options.logic_boss_weakness.value: add_boss_weakness_logic(world) @@ -114,6 +118,47 @@ def set_rules(world: MMXWorld): add_charged_shotgun_ice_logic(world) +def add_pickupsanity_logic(world): + player = world.player + multiworld = world.multiworld + + set_rule(multiworld.get_location(LocationName.chill_penguin_hp_1, player), + lambda state: state.has(ItemName.fire_wave, player)) + + set_rule(multiworld.get_location(LocationName.armored_armadillo_hp_1, player), + lambda state: state.has(ItemName.helmet, player)) + set_rule(multiworld.get_location(LocationName.armored_armadillo_hp_2, player), + lambda state: state.has(ItemName.helmet, player)) + + set_rule(multiworld.get_location(LocationName.sigma_fortress_3_hp_1, player), + lambda state: ( + state.has(ItemName.legs, player) and + state.has(ItemName.boomerang_cutter, player) + )) + set_rule(multiworld.get_location(LocationName.sigma_fortress_3_hp_2, player), + lambda state: state.has(ItemName.boomerang_cutter, player)) + set_rule(multiworld.get_location(LocationName.sigma_fortress_3_energy_1, player), + lambda state: state.has(ItemName.boomerang_cutter, player)) + set_rule(multiworld.get_location(LocationName.sigma_fortress_3_hp_4, player), + lambda state: ( + state.has(ItemName.arms, player) and + state.has(ItemName.chameleon_sting, player) + )) + set_rule(multiworld.get_location(LocationName.sigma_fortress_3_energy_3, player), + lambda state: ( + state.has(ItemName.arms, player) and + state.has(ItemName.chameleon_sting, player) + )) + set_rule(multiworld.get_location(LocationName.sigma_fortress_3_1up, player), + lambda state: ( + state.has(ItemName.arms, player) and + ( + state.has(ItemName.chameleon_sting, player) or + state.has(ItemName.shotgun_ice, player) + ) + )) + + def add_boss_weakness_logic(world): player = world.player multiworld = world.multiworld diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index 84aa16be99a6..594b6bba66f2 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -54,6 +54,8 @@ class MMXWorld(World): options_dataclass = MMXOptions options: MMXOptions + + required_client_version = (0, 4, 6) item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = all_locations @@ -74,7 +76,7 @@ def create_regions(self) -> None: total_required_locations = 47 if self.options.pickupsanity: - total_required_locations += 22 + total_required_locations += 25 # Add levels into the pool start_inventory = self.multiworld.start_inventory[self.player].value.copy() @@ -203,7 +205,7 @@ def get_filler_item_name(self) -> str: def generate_output(self, output_directory: str): try: - patch = MMXProcedurePatch() + patch = MMXProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) patch.write_file("mmx_basepatch.bsdiff4", pkgutil.get_data(__name__, "data/mmx_basepatch.bsdiff4")) patch_rom(self, patch) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 21e51acdbade530aea09bb421f07614cce3715bc..7749e74a0c4edd9095253bda53e6a5da83972ab5 100644 GIT binary patch delta 2534 zcmYL|dpy$%AIE>c-3%LBmTbCAhN0xlk5n#W%x$tcnfj8{R+gz|Wcmx4$%&LYIseMjDL zl`z_~z;6}!%&=X;qf3w9Oe$g3x0ec2@o01(vnKv?YaM;uv6TKoAgWoppQZo1Akkr| zGQ-xFyYB}ZA6*Y07?Qr6y#rL|?N!n#SRYBddv87F57D2)GH^6*jGe?vN|N$X3X%7A*M zOnw&;B5{crmNtV9DTzHyX<007*Lt@6MB0sTF(3S|JKabX)0?Qkn2b~OX7LZnJ=qEYaf`h$h3j^U+d_Bc8f z8*Z1kM9>6V+TfdSD2!U;P=do1di9FV2;uBRt})d2`85>*zy0k4)9! zo7%nE53&)SuQ9`wEKLJ?kgN1)y6#N z38J-~LpDoHhxlEunPa%84Kc5@C&r7JDGw(@(<8_o%@6!_lNY<&6w8o0yTP7&B$USR^WQ{aQg?Fn9SBAJdo3<7^s34 zPAc}QmsCc;?&R?NN~GE=R;L(jLr>kCZLH{4m$<9t=)H04T(gAngBW zg434)(gs|R9L&J-ejw3Md`Q4e^B9q)GeQcp`_5bH7M4&JMMVnblArA45P%W^SO5!w z5I3qk!P zU085@0EXU~`2h+p$_O}4BymZ`x~Zw+z(5R(b-CQpFj_qn#X0x&1Iy*LZwqPI;7A8@ zzp2!KC1{E$wQv#Ubvy{Mj~xxRQ&YLnSbJUYHhXD)NjuqcfRB;F z)$CEf(dT+vJV88?`d@XKD?evM|%K{MB!(~1#9 zE4|_MkR&bUPQ*REAfuTgql$W^85|vY!iKR>0olbQzwW^lSV7|2cBj_0%>E^ii;!EzB!`9Vs;-G< zyOrSy*R)s`_0_@`w0ToPaoE^9RFR<)=00h#!E4jSV)_N@dptF2XLCbVVDh)M$Qvt` zNW*I~FU0coHsa4F9VhI!-I%&Q=(?xB*WLIvEdJmzjia8t+KY~edJysXIkP#tAm~Qc z+uL|)a9(-VrT|O5-L{?}k&&rJ53{*sL1@;IKbpt2CuGqTnb@-AmZW&H8mr-Bu{38m z@Xdr^1%}4YpfD4Ldv=-6wLQSZm08vvPRHKAWrxqCvTq z4qLDjcg5!Z?DP7S5^YW!;B@R19NrNp5= zxw$u1xbTbB#h96yZM{!UmmE8H;2DMOb;ydq!KZ9b-aePdz#q|N;sR_gEhgJ07aNqT z^9t+vvIvW>$rXn#{dV>qeydPEH7&#G+S63i{OEeTs7PJj`QSSbsZ5#IF)g3Q2wZ&Z z3Xt*o<426ntp4Y_xeJdVhh4Zh3fox1L?90-QrgF*u{LqlFRH$~_9YbQJn~J`Y8u3q zo(;VsT)LDW*Khy$#`ejhEww%=H_NuT^6FT@L*eQ{o!@nw5{sKfdj}XCv+*s7@VrMH z=SGQNS%t+Y;|#)EGN(_+n-j3ldncB&jNV-()Y|@>8-JA<^kfkibNg5+HUSZ(5_L@i zlkeG)tB8V00WCIh|Nf!Q%M|5%Rx|mOqeQ|ES4S;@3v3F0ocUAvP)w(?R)GsI^*hq-t<}Ry zNZTndm46d#25O=W&65VhA0ME;(rB=@0Mn}!pl*%*+t&Nirj;E|&4RMF{u!}Y~=4g+~^>0PC~qHly_)Y+E^n3LKd8vkZ1#97{)RxHpdN{`_azkv3j zqtf^0GKH#2Cg%>6LUtB(DN#95|R?XM64S0#yMxqyPW_ literal 2467 zcmYLLc{J2}AO6nL7|SsBF_ba3Np6?%OSmR8G8p?Zgc4;ib5-|EbS;%JgD8`I4}%%m zDk-5Nbw{=eSt_(}Z;C7}^xjZj_uluM`}yNJ&mW)job!3k^PKas^|oW!+e0QF!9Q~y z{(k~+e-qmPwxub}-@w~n&bI3%0CzVwHYiJ9WXiZma2S~lYj`Vh|Hx}*Sja3KJS_AW`LH56A02Fn`6C03^trbABzF2xuG6+i$W42qSv!y*g1 z^JAl7Fo}VmNp}v}RH&-+!Uat$d{zomnv8y(V%l41`^}I*Z&@j)-4|g0G+V0@oKGnabw}OxO~9% zYK#=|g}ID=>BhLAqje*GplI!RCu|~dl1caOf>tyD7^KjIoQet|1QW6$2tgPmvwEvS zQCuANNLDqUFl=9h_OYQuL$fFW96>M(5%00_S+!bNd60sJMnY3&HW z%UH}_Puv=@o-8Mn5~y>j-bgraOE2c_bS9qg?Km)z<>l&1$EIR}Uxn~^L;U&jRD>;H zN>(H!NhbbJv7LN|eqHg3bSmnK{wfAirNSDxL|{H2RJC=nM)t`4mOkq-TtRz}{SYr7{UKHCQ~kUHQ>E*cL{RvYen6eKdh-)b|Jyp8 zUEdvZEWgsObDyQg3^piDp}ah=|5v45t3pw*RECwG(HSZEUU#J==*N&h2x~d;*YE*q z^R=X4HvQ%a%f#E?ul7v`(iGrhW|Vi+d2nB}t@_e;@?G_l?Gw{Gn-Qb@-LGTe18PZz zsa5K&dyVi0P1|JOVZ9I3lpb?>-n!Jb7W(e}ori^O8_O>#Q0SPJT{;Y=sHlkTV;Ck| z+e|MyUXoFQAy2GP|BdWvU;QiqW;Q$e*L>|ySX_9adXSSay77Jf+xYDzx3N#_rxSij z7#&3Ji8kM1NQ%DJ0~P!1p37iR>dZVIq%NnFRlDNK(o%!=LC@yX8Ol`aO$Z)Uqr8!o4g<;K#4Fg(%R$$Fi zHGqwgs@aSr9#&07oy~;_!B8qpI0!K^*}4X~D|i+Lf?yRw%H9z+YBR48KseN9W&WMt zO@LJZHl7XPiU35$;E({%?vW#qSsGApMQU#*9$+*O$_7*b@IczmHq_mvx{q_CEKA55 zs<>3c5kd%e(q^MDfVC+l`KJ_kJTOqxQagYD;&DP>Q?sj=aP8f_KeZOyUrz2D5zdb> z8!642KLzMKEcq(VG~e6!^bF!2deM+<75%k`x;%U}?%Nz5T6(o~`PQ~f2)vPa>BqD>idGN?|ovwm|F-jBiyNn|< zTVt|(oI;5Gr+N+%!Up53L?q2b2g;St=YQH8FA7J0gez7)Rspv$D=~Mq&JHy-8hI%?6miA0wq!Y1*dyd3V1s9Bn5L4B|ANyUNzN{JY zESnc?pB)%Qc>Ixz2iBo)yz8&GsVo#c5qx_y{?ylN8zGSv?I;#~v8!vbb}xOjvb?C6 zRc!wD!t^*@+9mCiIK4HW^5rGv^r_mWiOUaoN@R?X;~0Ki&=)}(lk!{EiwJ0-J3DMu zxm`=#)zpYKOgm!yW*T{ecc*qk3?tE<%|0dTUG>P31Iiff;z{1>`17$i^kGs8L-qqv zN1ABDK_K~LBC-1SZ4E!|i*Bd8CNsA$YZVvfO4P$r-@8H%+&unbAIZ|m`lIbegV(v~ zHuzMr(f#|k&xrNrtlp{&gc#jBE!l-I9$nR*y6j5JYvBw(d#v{AHzvBDA2%gT)#(gl zogH)i`r%koAOm!r%t=&;)91y+BZ32~ji`@S(pCM2r-M~&iP@nL?%KtWt6x6i?7RLd+&$0v`>b(e%H--;2U_V2xVEc zqG#AQRo|j|Q@dLb_M!uJeJ~6o_2bX4iS<`b!K;NsxbxmXO?UqLy~bG~?8bq Date: Mon, 22 Apr 2024 22:21:01 -0700 Subject: [PATCH 04/71] Fix Storm Eagle's helmet capsule not registering --- worlds/mmx/Levels.py | 1 + worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 2549 -> 2536 bytes 2 files changed, 1 insertion(+) diff --git a/worlds/mmx/Levels.py b/worlds/mmx/Levels.py index e0a1672e3949..d6f4023bb236 100644 --- a/worlds/mmx/Levels.py +++ b/worlds/mmx/Levels.py @@ -56,6 +56,7 @@ LocationName.storm_eagle_clear: [0x05, 0x001, 0x08], LocationName.storm_eagle_sub_tank: [0x05, 0x004, 0x10], LocationName.storm_eagle_heart_tank: [0x05, 0x002, 0x04], + LocationName.storm_eagle_helmet: [0x05, 0x003, 0x01], LocationName.storm_eagle_1up_1: [0x05, 0x020, 0x0E], LocationName.storm_eagle_1up_2: [0x05, 0x020, 0x0F], LocationName.storm_eagle_hp_1: [0x05, 0x020, 0x0B], diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 7749e74a0c4edd9095253bda53e6a5da83972ab5..76983375891a09bd4ff6377c2a4eba19d1102719 100644 GIT binary patch delta 681 zcmV;a0#^O?6X+8WLQ_OZMn*I++yDRo00000u8|S-1j+M(#*+#GB!Bt90o#GxR7FNa z#M4axG6$p$05miMKmcS702&Pg${I9nAY>X0fMQ}~4^g8gMuaM#l7RF^$^$^tK+`6G z02&Pd0000q0B8UJ27nC)LqkB*N2pZN8WSc%LkQ3`(S%}T0gw$e85tUAG#Y3ILnc5B zLqG!{Gy#y%7$Q{)8h?6&)Bpfz000000000000000000dJfB~Sy56>P63vc8|hX9cA zs>K2@fM9U0DEok7gT*DJD1re{X+k8Akf3%O#4piWkbRTy0BFT(qI%$!AV`kJ30{Fl zE82llD@p-M2Id02D1cnxrq%*Au}W|dO!QD{pb1VY*orCuHE}O6Xhc*18{`2H0kq-$7^*>Vm6g;A*l;m8TmHk}!LSy1Y9>(etAkW;G za@;D!0*^ON{K@u)pmb%?wKs=D7$^?NaPdBb3j{aSc#1uK1!nL-LGs`wSGFqw;R#ag zi0d0UxXql5{tW{Z8AToz_e3Yw0yJ*O5z&HRBN;Ceh<{zi$0qJ^1t118Xk<#FT&o2> z4!rkKj2fkUP>;=-iPy4uI;$|fNpY#*!F!PdW5Mw}e|1z(h!wvPU)Cs)%fAWZ4PJ=p zBSj|-!u*9`@Y%-1teIAF%SopDM@pKZFY2ysR`QS%Wa&W=2ndLXh=TMGIQ~!ZcO+AV P2@Ib&906dnw*x)~jZ_wn delta 694 zcmV;n0!jVo6ZI1jLQ_OZMn*I++yDRo00000yO9y~1Of#>^^*z#B!Ah!0o#GxP>D>5 ziKc)CAjldt&}e848UO$Q13{oIbCSo}pBp(tvqJ6F|w6Oqw)k z2AVWt84Q{L10j$!(WXX#$Qd*mXc-JlBSSzm3Yt?jGH4i@Xu&iFjSVymOc9W2pfU`G zfB+1c05C?3gC>I@0Dol3pdwXFk*0&x003wJ007Vc00000Gynhq000dKr~ot=eH{e| zN3D>2{E!F^87QEq2nGY|vQD0iNN}V!MG!zJ4RA!3^AsBay~6_)r3c$T5C()+swc7u zN(6}P;FT~aMSE~5MQFe%z}>)Crw|LY6x+Z?oKmg=380D%RDS^~;;vRAp+YJnR{%ky z1P{~%ON3Ab5ck#%>Izsu4$M(d0j!C9gHaJs0B`UFKnB^0^y6-IDPLhwQ-yXj5rWxQ zSiTa~4OGiuDhhK;4=#qjfd|6{Pj2LrT)`((kq_CYBhiz*lLS=Z3~3An|BZ83>J!=6 z8pG@3_8A*l%YWS_dK2r8u|5~XS?Q)Zp41w!d%VkLGQ)WVx}Y`JfXLxS+z`#Rd*37A z-J(W|!Q^X?av;UJykI2@yLaayxz51l$DQsyex@XFVBLUs!y^F%7uvzDJ$?sp_+Uly z@FZHgChhYTYV}6&!pe4))6}oan{101lq1__f`&kyKYx=Ek2^8}A6G;P04=GiGXg{h zfB|7e6zwpXN5FavuV!6CxYN^ggZV?CG}08%%d!nonoUAke^`(;9F#`~;7Ne_j0<@G}qnQW-0SH16 cgdo;1$PS2}pYeAjQ-uitAXE=fv#J9=2IBM>*Z=?k From 97b88981ba0d63cb07ae82c5b68d66efe1bfbe53 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Tue, 23 Apr 2024 14:47:10 -0700 Subject: [PATCH 05/71] Better Sigma Fortress open options --- worlds/mmx/Options.py | 25 +++++++------ worlds/mmx/Rules.py | 10 ++--- worlds/mmx/__init__.py | 52 ++++++++++++++++++++------ worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 2536 -> 2614 bytes 4 files changed, 60 insertions(+), 27 deletions(-) diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index b2fa1d958dde..e216d4165155 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -60,16 +60,19 @@ class SigmaOpen(Choice): armor_upgrades: Access will be granted after collecting a certain amount of armor upgrades. heart_tanks: Access will be granted after collecting a certain amount of Heart Tanks. sub_tanks: Access will be granted after collecting a certain amount of Sub Tanks. - Do not enable weapons, armor_upgrades, heart_tanks, sub_tanks on solo seeds without pickupsanity - or sessions with very few items. There's a big chance it'll cause an error. + all: Access will be granted after collecting a certain amount of Medals, Weapons, Armor Upgrades + Heart Tanks and Sub Tanks. + Do not enable all on solo seeds without pickupsanity or sessions with very few items. + There's a big chance it'll cause an error. """ display_name = "Sigma Fortress Rules" option_multiworld = 0 option_medals = 1 option_weapons = 2 - option_armor_upgrades = 3 - option_heart_tanks = 4 - option_sub_tanks = 5 + option_armor_upgrades = 4 + option_heart_tanks = 8 + option_sub_tanks = 16 + option_all = 31 default = 1 class SigmaMedalCount(Range): @@ -77,7 +80,7 @@ class SigmaMedalCount(Range): How many Maverick Medals are required to access Sigma's Fortress. """ display_name = "Sigma Medal Count" - range_start = 1 + range_start = 0 range_end = 8 default = 8 @@ -86,7 +89,7 @@ class SigmaWeaponCount(Range): How many weapons are required to access Sigma's Fortress. """ display_name = "Sigma Weapon Count" - range_start = 1 + range_start = 0 range_end = 8 default = 8 @@ -95,8 +98,8 @@ class SigmaArmorUpgradeCount(Range): How many armor upgrades are required to access Sigma's Fortress. """ display_name = "Sigma Armor Upgrade Count" - range_start = 1 - range_end = 5 + range_start = 0 + range_end = 4 default = 3 class SigmaHeartTankCount(Range): @@ -104,7 +107,7 @@ class SigmaHeartTankCount(Range): How many Heart Tanks are required to access Sigma's Fortress. """ display_name = "Sigma Heart Tank Count" - range_start = 1 + range_start = 0 range_end = 8 default = 8 @@ -113,7 +116,7 @@ class SigmaSubTankCount(Range): How many Sub Tanks are required to access Sigma's Fortress. """ display_name = "Sigma Sub Tank Count" - range_start = 1 + range_start = 0 range_end = 4 default = 4 diff --git a/worlds/mmx/Rules.py b/worlds/mmx/Rules.py index 78daf5d82596..24dc888e555e 100644 --- a/worlds/mmx/Rules.py +++ b/worlds/mmx/Rules.py @@ -33,15 +33,15 @@ def set_rules(world: MMXWorld): if fortress_open == "multiworld": add_rule(entrance, lambda state: state.has(ItemName.stage_sigma_fortress, player)) - elif fortress_open == "medals": + if fortress_open in ("medals", "all") and world.options.sigma_medal_count.value > 0: add_rule(entrance, lambda state: state.has(ItemName.maverick_medal, player, world.options.sigma_medal_count.value)) - elif fortress_open == "weapons": + if fortress_open in ("weapons", "all") and world.options.sigma_weapon_count.value > 0: add_rule(entrance, lambda state: state.has_group("Weapons", player, world.options.sigma_weapon_count.value)) - elif fortress_open == "armor_upgrades": + if fortress_open in ("armor_upgrades", "all") and world.options.sigma_upgrade_count.value > 0: add_rule(entrance, lambda state: state.has_group("Armor Upgrades", player, world.options.sigma_upgrade_count.value)) - elif fortress_open == "heart_tanks": + if fortress_open in ("heart_tanks", "all") and world.options.sigma_heart_tank_count.value > 0: add_rule(entrance, lambda state: state.has(ItemName.heart_tank, player, world.options.sigma_heart_tank_count.value)) - elif fortress_open == "sub_tanks": + if fortress_open in ("sub_tanks", "all") and world.options.sigma_sub_tank_count.value > 0: add_rule(entrance, lambda state: state.has(ItemName.sub_tank, player, world.options.sigma_sub_tank_count.value)) if world.options.logic_leg_sigma.value: diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index 594b6bba66f2..0ee3d1286587 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -105,17 +105,38 @@ def create_regions(self) -> None: itempool += [self.create_item(ItemName.stage_sigma_fortress)] # Add weapons into the pool - itempool += [self.create_item(ItemName.shotgun_ice)] - itempool += [self.create_item(ItemName.electric_spark)] - itempool += [self.create_item(ItemName.rolling_shield)] - itempool += [self.create_item(ItemName.homing_torpedo)] - itempool += [self.create_item(ItemName.boomerang_cutter)] + if self.options.sigma_open == "weapons" or (self.options.sigma_open == "all" and self.options.sigma_weapon_count.value > 0): + itempool += [self.create_item(ItemName.electric_spark)] + itempool += [self.create_item(ItemName.homing_torpedo)] + itempool += [self.create_item(ItemName.storm_tornado)] + itempool += [self.create_item(ItemName.shotgun_ice)] + itempool += [self.create_item(ItemName.rolling_shield)] + else: + if self.options.logic_boss_weakness.value: + itempool += [self.create_item(ItemName.electric_spark)] + itempool += [self.create_item(ItemName.homing_torpedo)] + itempool += [self.create_item(ItemName.storm_tornado)] + else: + itempool += [self.create_item(ItemName.electric_spark, ItemClassification.useful)] + itempool += [self.create_item(ItemName.homing_torpedo, ItemClassification.useful)] + itempool += [self.create_item(ItemName.storm_tornado, ItemClassification.useful)] + + if self.options.logic_boss_weakness.value or self.options.logic_charged_shotgun_ice.value: + itempool += [self.create_item(ItemName.shotgun_ice)] + else: + itempool += [self.create_item(ItemName.shotgun_ice, ItemClassification.useful)] + + if self.options.logic_boss_weakness.value or self.options.pickupsanity.value: + itempool += [self.create_item(ItemName.rolling_shield)] + else: + itempool += [self.create_item(ItemName.rolling_shield, ItemClassification.useful)] + itempool += [self.create_item(ItemName.chameleon_sting)] - itempool += [self.create_item(ItemName.storm_tornado)] itempool += [self.create_item(ItemName.fire_wave)] + itempool += [self.create_item(ItemName.boomerang_cutter)] # Add upgrades into the pool - if self.options.sigma_open.value == 3: + if self.options.sigma_open == "armor_upgrades" or (self.options.sigma_open == "all" and self.options.sigma_upgrade_count.value > 0): itempool += [self.create_item(ItemName.body)] else: itempool += [self.create_item(ItemName.body, ItemClassification.useful)] @@ -123,13 +144,22 @@ def create_regions(self) -> None: itempool += [self.create_item(ItemName.helmet)] itempool += [self.create_item(ItemName.legs)] - if self.options.sigma_open.value == 4: - itempool += [self.create_item(ItemName.heart_tank) for _ in range(8)] + # Add heart tanks into the pool + if self.options.sigma_open == "heart_tanks" or (self.options.sigma_open == "all" and self.options.sigma_heart_tank_count.value > 0): + i = self.options.sigma_heart_tank_count.value + itempool += [self.create_item(ItemName.heart_tank) for _ in range(i)] + if i != 8: + i = 8 - i + itempool += [self.create_item(ItemName.heart_tank, ItemClassification.useful) for _ in range(4 - i)] else: itempool += [self.create_item(ItemName.heart_tank, ItemClassification.useful) for _ in range(8)] - if self.options.sigma_open.value == 5: - itempool += [self.create_item(ItemName.sub_tank) for _ in range(4)] + # Add sub tanks into the pool + if self.options.sigma_open == "sub_tanks" or (self.options.sigma_open == "all" and self.options.sigma_sub_tank_count.value > 0): + i = self.options.sigma_sub_tank_count.value + itempool += [self.create_item(ItemName.sub_tank) for _ in range(i)] + if i != 4: + itempool += [self.create_item(ItemName.sub_tank, ItemClassification.useful) for _ in range(4 - i)] else: itempool += [self.create_item(ItemName.sub_tank, ItemClassification.useful) for _ in range(4)] diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 76983375891a09bd4ff6377c2a4eba19d1102719..6070bbe368ded63873e30914715e658e3fad9b9d 100644 GIT binary patch literal 2614 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 D8WLf` literal 2536 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 From 002973718124d36eae4418982dfdd42d3dbdf3e1 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Tue, 23 Apr 2024 14:55:45 -0700 Subject: [PATCH 06/71] There's now 8 heart tanks again --- worlds/mmx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index 0ee3d1286587..b23024330e1b 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -150,7 +150,7 @@ def create_regions(self) -> None: itempool += [self.create_item(ItemName.heart_tank) for _ in range(i)] if i != 8: i = 8 - i - itempool += [self.create_item(ItemName.heart_tank, ItemClassification.useful) for _ in range(4 - i)] + itempool += [self.create_item(ItemName.heart_tank, ItemClassification.useful) for _ in range(8 - i)] else: itempool += [self.create_item(ItemName.heart_tank, ItemClassification.useful) for _ in range(8)] From 213710b7cc429f19a32b1adcdd988599838c47ff Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Tue, 23 Apr 2024 14:55:51 -0700 Subject: [PATCH 07/71] bump version number --- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 2614 -> 2608 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 6070bbe368ded63873e30914715e658e3fad9b9d..cc43ebc3ddfc40a57516263ab2e8e9c238f04070 100644 GIT binary patch delta 679 zcmV;Y0$BaF6tEN#LQ_OZMn*I+-v9sr00000u#pk?1l?s!E0YZYFn^3d84QgEL7+6q zGyn#g14e)}4FJ#p84QC)gAi$^gC-gwRW_!EfM~%CnE`@nra)<-lS30m6HN^o0}V|B zO{8g{28{uUpbR0QrcE*ispUldQ$|1l0iZoV0000000000000000MO6?03uaW${Kou z)BpkMXaEfW000000Dl?*kN^Mx000001bv}u*P=w~6HP!=r3%a@K?7!eW{$!YsWn*U z5)tJLNir`njVeO@6{iQ$KF|h)0IG?Vh*NcuM{5&K2#0Fv%MH={H2fXK)%y2F=kas7)ed%|?GMyM$ zQX3Kr=EE2U`2=c3=YT*70n=9$ zx2}_%0ICjrU4O}#$|LA=38{QObquhQFDPFiJSSyzi{I)Fy!;}7@1LdlQ(X}-XqGT6 zsEQw=ptpr+8he&(@CX&*7`pQIc2$Fcz{+-@=}eW|PmcEMnLw17DV5x?%yUE!iJx?g zx~4#nR3}pTt{))SH1r+F!d)Vfy&>-cz=#2`?1l!>pnrtBCdFr&yH13LZ*ot;aBz!+ z!&3}>Ate4{!C&0>d5h1h8YipD2O}F`P=M^<@8-c7Mx%LK^(J#)6uIP_BbCo12&^4X z^=+DnYTQ8hF&;7_q|h~B`ldlC5Mh}C0E8h3LJ$+R4iRIJ|Ha&qP81|JSyKwYLRx4! NF+o`-Q?tGUcm_T`3f}+# delta 679 zcmV;Y0$Ba96t)x*LQ_OZMn*I+-v9sr00000wviF|1Zj5eO_L1)Fn^6S8X6jC&@^N; z28Nmd2ATi{nludn&;S_^P%>mO115tejiMD(YG`Q0XvwCUWY8H6GGrQHK+w=)8U~n# zCe+Z$nt_N0fY4+bWMT~(88il|wM6|>Mj!w*8Uxe=L7|`k000000BB?eKmY&$0006d zRXm}ms69Xc9;Sc*(0>2`00001pcw!F00000002MP)vJIp=njf00%WI7K@$UwdL<0? zj-4qc$smA|G+B^S+Cw5#h59Q_52Agb4F~~M6Dts>0z`GuOAHDSUZhHKS}|zA+(1{E z5pK{&X#pREQj`P|%M==@0#b^$h>C>>sEsHB4HzJPAR=3$fPW~5uA<%`r}`_>f*^o3 zC0t7c1V90+ih=+F4lul&B3G=d{KogWf0h8)QFB}?cKAGKs<#ZvZKqJ3}77P6Q>uR z0RSgsrn=NuZhx>208s8_k0xYceKF+|Q8_8ppP^n_mNKv#PD$vQzSA9qyb{3ko1^r( z&UknUaoCtvLQjI=yMb^VT5$LXA7Nmcx$yLOU5k;x+IFDrQR}8|yPPj4`b`04tual; zvqJo-_h`(kBoXQa?LUg~)^YM N?oC2kXtS~deg>Dt4$S}n From 253d202082d61d7dd1356c97687efa33def097dc Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Tue, 23 Apr 2024 21:24:35 -0700 Subject: [PATCH 08/71] Add a missing pickupsanity location --- worlds/mmx/Levels.py | 1 + worlds/mmx/Locations.py | 1 + worlds/mmx/Names/LocationName.py | 5 +++-- worlds/mmx/Regions.py | 2 +- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 2608 -> 2599 bytes 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/worlds/mmx/Levels.py b/worlds/mmx/Levels.py index d6f4023bb236..e73ea85f63a3 100644 --- a/worlds/mmx/Levels.py +++ b/worlds/mmx/Levels.py @@ -57,6 +57,7 @@ LocationName.storm_eagle_sub_tank: [0x05, 0x004, 0x10], LocationName.storm_eagle_heart_tank: [0x05, 0x002, 0x04], LocationName.storm_eagle_helmet: [0x05, 0x003, 0x01], + LocationName.storm_eagle_1up_3: [0x05, 0x020, 0x1B], LocationName.storm_eagle_1up_1: [0x05, 0x020, 0x0E], LocationName.storm_eagle_1up_2: [0x05, 0x020, 0x0F], LocationName.storm_eagle_hp_1: [0x05, 0x020, 0x0B], diff --git a/worlds/mmx/Locations.py b/worlds/mmx/Locations.py index d0c9cfc469d2..1410b00a408a 100644 --- a/worlds/mmx/Locations.py +++ b/worlds/mmx/Locations.py @@ -81,6 +81,7 @@ def __init__(self, player: int, name: str = '', address: int = None, parent=None LocationName.sting_chameleon_hp_1: STARTING_ID + 0x0057, LocationName.storm_eagle_1up_1: STARTING_ID + 0x0058, LocationName.storm_eagle_1up_2: STARTING_ID + 0x0059, + LocationName.storm_eagle_1up_3: STARTING_ID + 0x006B, LocationName.storm_eagle_hp_1: STARTING_ID + 0x005A, LocationName.storm_eagle_hp_2: STARTING_ID + 0x005B, LocationName.storm_eagle_hp_3: STARTING_ID + 0x005C, diff --git a/worlds/mmx/Names/LocationName.py b/worlds/mmx/Names/LocationName.py index 886905e0eabd..0231901ac5cb 100644 --- a/worlds/mmx/Names/LocationName.py +++ b/worlds/mmx/Names/LocationName.py @@ -54,9 +54,10 @@ storm_eagle_hp_1 = "Storm Eagle Stage - HP Pickup 1 (Behind first set of gas tanks)" storm_eagle_hp_2 = "Storm Eagle Stage - HP Pickup 2 (Behind second set of gas tanks)" storm_eagle_hp_3 = "Storm Eagle Stage - HP Pickup 3 (Behind third set of gas tanks)" -storm_eagle_1up_1 = "Storm Eagle Stage - 1up Pickup 1 (Behind fourth set of gas tanks)" +storm_eagle_1up_3 = "Storm Eagle Stage - 1up Pickup 1 (Behind wall in third set of gas tanks)" +storm_eagle_1up_1 = "Storm Eagle Stage - 1up Pickup 2 (Behind fourth set of gas tanks)" storm_eagle_helmet = "Storm Eagle Stage - Helmet Capsule" -storm_eagle_1up_2 = "Storm Eagle Stage - 1up Pickup 2 (Above helmet capsule)" +storm_eagle_1up_2 = "Storm Eagle Stage - 1up Pickup 3 (Above helmet capsule)" storm_eagle_hp_4 = "Storm Eagle Stage - HP Pickup 4 (Right of plane)" storm_eagle_energy_1 = "Storm Eagle Stage - Weapon Energy Pickup (Right of plane)" diff --git a/worlds/mmx/Regions.py b/worlds/mmx/Regions.py index ba0a5cce1bb5..fbd52584d5b5 100644 --- a/worlds/mmx/Regions.py +++ b/worlds/mmx/Regions.py @@ -287,7 +287,7 @@ def create_regions(multiworld: MultiWorld, player: int, world: World, active_loc add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_hp_1) add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_hp_2) add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_hp_3) - add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_1up_1) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_1up_3) add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_metal, LocationName.storm_eagle_1up_1) add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_metal, LocationName.storm_eagle_1up_2) #add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_aircraft, LocationName.storm_eagle_hp_4) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index cc43ebc3ddfc40a57516263ab2e8e9c238f04070..ab9c26518650b9b1c972901b04f3f75ed901af15 100644 GIT binary patch literal 2599 zcmZ9LcTkh*7KgtSLVyH9=#i3y7U?CTsAwqCH1xhKh87c=R22mIB4G(lioi-Wp(!Y^ zfM5YZIw)cg1f+_}t^%^?0=g=2@$Q}3nfv_pn|aSWbIzIPm^oY6SXz?x=74|bhW;-A zvi}WcUJPS>Q%^N#&yVP6831TW+}%0;=a#TYB>+GzumB1W=!K$y%W!~$mldJ_G?pwL zNEIhD?H!ky752`i%LZf!7eJI1B@wBXMG!8G3Znp{yb4M;ROe@@W{{-3&d#qW4asaI zBdTUT4bF;F<06WLR&0vg0>)PxmS(r&yHz<*8yq1R&>!ZCMulR|T3%K$L;gB$M&WQ4 zYfP*!8Tab{b}^>rql>a*h}KPy0iJ&{e7@!=+0}znBo2|0+VB1tvsC#E6V6*VN!`(T zn-<^#AN+rj}1fU!5=^SB~Nb)lHW;0bc3T2TNhiJ}KG z005O_PJvWVAzV2$Ou`c68{hyO0whm^Ab*fNRLx@ZakzldYIr~ZKfoYB4i~@}!;f+i z$T3q60|?*`ftiR*sHzj5K_&xxOZlw_71FHOUhq_JU)_))Bdy?n&7k=SMjT-q;#wuI41mUaAUo>YaJyejOgF zZxtx6l;>0g74l3&`v7@-_f7$a$~4Xx1;g}%{LM-AO9RRkpD*_~(!7@x61oC@ zYv|O7H1CWrgcYsEA) z!kGD0n9MT&fo6wta2hXNV|QrId+mN`xO2<5B_T~ z@}bx&0Kx7FyvO|V(8A7#U8UcdRhv7zA9laC{pmCRX(u7^^zIxVUUulh$lcKSfx8dq zhfBy#YSq)JuMgl3)|g26fpKNkfoLOkj{_r)B1eY22NWyBD-@j+IiuVmE=(x9Qh_%va z76cgB*eer@iI>Vl>`7LYamtoOnIr>p1E>o{55Cs{OcOWM@DkBN-!dX^Zi{o=Au)7;E3 z311FPmz8+SNeS&Ecfqe=s|nSqT*-B4rspNLCY3&BtD?Gc#mdod_VKDb*@dy+w zT+(6r3$u9iXytu4B+b-nQCYUzqpy+n?JX)$tj%s}BG0Y7a_aXpt&7E6x~-Ec{?Hk0 zt{t?}v0=wmpQ9H_l5|@Y!PLtHWncpmBqhyKzqnhtTASy*fK>UGyBfVj5Q^F_+t%ht zF*ycf1rBd+)U|Fa0cW$qjHO<5cXYHY>4i>546;_?z%^&sIYN^T?HvT5_DdU_1M-O+v^jKX;QZOG%Xgsr#$QKwOrZv+uI zB3{JkYf*F%1H)RCh4+7L0qU02##NVDxOt%NejY`fpbaU zpAsi*=k*VCy^Ou385@5r_>XB@`tKceWgl}wNqvQ0IjOwkn({hpJqFBNP{#Hnk{xBfwu);KGbmuTwfB;z&mtLTFdSh+(t#T>dj^skiB{Hr5lKiu(mJhdz;E}(+N z%IAS3U(J||wcylWpV_A2WKW!LXxGSxOLlVqdXyY{l1*jHDiIm0DxTV!jaM!-FM1lr z)0xaybJMqTgr9pnTIV!WuiaN6U_fCw6mI~SA}nlCtU?&5IAxGHtS08^#6oqasTB#Q z`bGi=d~yeo3GgO6J5&-$HfzxLQ%PW4h=0aDi@?=2uN3sy_gDRy-HjAqWR#!w$WtMZ z6F<^pONlvqYt+Wg0!$hc#0nWf+9Aam&)81p6|lXxb49=$OymAKLd)sJ8|@d zl3?Pwp8M*Ioqe!#-4#a;sOGj%x;uj(Ih10Co+0a=pxyM|glIh0AYAS~d{p zME-uyGk(?bGe{Ns%~s*h1@N?MYu7Duo8C2FZL!K??Q88WUd_RocY>O6@#=S{mvU<= zUvPz*kT+N%)SNrbqScAQRSi>O8it@r0kqC=Ah^fabxQ+1HDTA?7;fSurxw39 zD`qt7)-_nN+TKLqeCOKD%-~Ta1L}T8?7wCMKg(p$83O)Oqarp-9##|0tFRcrxNA6S zJb$Maw7$@fpiAKoHVw~JHQ^ifll z62ID+E}+4wy%8Idz-U%ClvbYVXj@Q!V4YfZctDvaD0ed7QQwd6?41x9(N&B5Wh(1k e?XH(ky=)vB*=TOv=(wAHpIAb&hWn&!0sjFlS{=ctqZncF2_(Kt-=V6E)=aOwK2>s_iND%OE)QX z6rDojNY2k(atYBnC6_u;hm&0P^E>DKUg!De`Mh4w^Ljn6&-1)ruZO{QbY(IrmSe!* zQ49V@06KpO#zEdrD>{e7=KQ(&j~4*Ypj58fe_l{ilh6P#GzUOJwdDYkmJI-6JX{?B zafN6+kb?2-yFQ(1)E+!fu_>3p1QZ?*7n@+n#?o*A8K;Yb;6ai)B87F2Jhxd!uPlw1 z7|Yua(0wCbq+~xYm zT@N-ihHzR5Zw(hB6HjWKd~nT&K}kDtfp7%lj#hDq@`}6=od=w5XQ+_+mac!QIET%E zpGf+7+3$tcTZY3(MX%An_C`b8%>e+eR4N08$0n4YhU;>>JTuR$nz8o4xBs-%bLUBe z^8DD9^P^3mrT7nP7IC4nbeb+Q14frn+9?361i}Tqk36fAQW07Gu-aD?anKS?-v8Db02!CaT z0ssts*d1#t>;pj4JOK4~83)Sxrw3FGv*&zLN3)g@LT-y?O(Ur z@i5ljnj3C!zAqy)Ki==rGg?7C9p3!hp>5wr&2Q9yp1)r}~EwcSEmb?N>i|W)(Qb~Ox z4|l%c_$*xetxm(21-?&$u&IMDnR3q`_Po3>Q%uV%GR+^U?Tm8NNVyiB-XNo8dYkN9 ztl5Mgr2L`Z-r{GN{S)CcxJeAE{g?#nmWUcjtw)O+Jw!kKbJ-=64a{xAi^ePn3wmM`+)y z49LVv#dhv@8Ql~eSPTM01ULhYeW;E(lBH}N$@b7P+QDI~HT}!$5oVt?08(G$buCsG zXD+R-jBYx5!bDrC{HT2W?6b$}(sGFqP?N~Q1zllN6dK-5 zee;$*ff6jKua|^H|U?fVw{a6tbA%3A7qdP^rG7-@>r$-2LSDBWWodh086pbUBeyAWQ51E zWa5>*@iy`H-d+26z3ycxB`gh>SXex=mq9*(qGaH3!M*b=N}CKZZHaCNWy>gNkW84x z19&))1eugnB*4K)LJ1iFK-m@OwIKisnW1Zm$t9J*9Isb*!!kVCeXmPmNh~CNIxLq- zLjgK?AQ)#$gRDuA1V;ua5E=j|P}T{MGs7;&da4xCHeX5NGTTHUy`RZUtHae1m>Gy} zO;PdG=fK_0{QfsPRq*tyet-Nn=_P4CcF|!M(l!_JB&hxPafRc~BB)@o5Do49=$cW$ z*tdfV69P+V$-!RSrq`7QDFY2-(+gC-op!W@$(0tVaY%0XtRJ7ynTVKo4=a1pTc+)d zgEo%#W6P6Bs>iHclhBX+pJq-^$AhCa{sZjc+FMqpXy0XhUBxKOrZ8;=?P==ex+ z-h2;vi4VPJdY?KFVtn5_c74>CVC;5CcpgDs?Xq(n$E@tIZ{r)k@~m?+C&C)H2OgIq z*m+tE-PF@6%z}=CUUHwIhn2g$nYOtWN3{2wA`|nu{KY%8#U0?MZE_AL%2=8l;d|MI zxw-IqrCYdhGor5E=9#$%;o)+Kiq3MQ2H2=kcQ0VCK8JbxD;}OGhZi8Z85;_3oY_A< z;~x^Bcv69KuzwL4?f%VigZvo6vgs96%gTWI;IhZzk-VUnhs7YYw{z3wGAx;UJgY7D zU}wDDg{(+07o?`S2!^<=D@<@^>+KNCdWZ(V@7#YFND#;SHSrv3h{B?DfAigHchy?6 z>acFEiu+r0Xxmzo2=Q5|?$Mia*QsY1)5UB-s4yMJ_1S+cEVr_Hmt`Ut8A3f~Q5}Zs z&u7euRrxtDjJJ7p96H?eJ&{5^dNRs|>Y~-{U?Ubyj$C>b`en8lGgkT1JZ)P*#0$fC#)Bkja$TN2?Y6ZU$#&0~RbL(THfD}+2K1@VMCF&-R@}JqAf|zS> z?$AL^e!a8c8VIj=(wO|YtIIce5iGC|dZ$x;JTlEI3Mg+Rl&>v4~beHmX7dtLn`^9kdN=gg0>8Ua# z8sgY9G>9*{DY;d1qgq=!EpmlAVx6>1)i9~D(4)G8==lWB@BWViPmK@t-K^ZbJ*c(j zn2QO=&EwR%s^EjppvaM%(kgGC;^-O6NIe~D;TPx7Vb#FC=6v_4Wg81+HgB@7Dv>cT z&XAuCGEm=e^cBGIVcp_L1NhMGy^9&RGdyE^#Ke@Jhwb2GHR3-mkZ zU-8N2140YPV8H`fw0MWavUG9PU}$#9cR?zN-!yhnNZonrXhaVp_pmy!{!1ZLZjqUI ziQNxKmgKuMrvlUt=5QW}27_nEJ#w-R%3KiYXtT3k*v29awMUhuUOnex@vh`1Z5s7` zO5CK`l-|>j3OziF0GbkZTHJnZ93hqctj6~=PxDC~hp~^NjN})BZO0xrxbkY1X*P&6 zLVih3;0YRBTyfk^*df}0c&YbP3t{Q-% zAOH!5W$Qx|M<*v#oCm4GeQ~uKIh26e9h5c!Y^Z+VvVYFdwTVNmTiV>BMm-*X&ukf} z3}|t6$$(R|!~t%P6l8Dpmp1#?Ro6iKZ>5PdJIpwbd| z$%eZQlpfmd6qCwk{pNFBdGTKTY)m Date: Tue, 23 Apr 2024 21:24:54 -0700 Subject: [PATCH 09/71] make the stage access item pool code smarter --- worlds/mmx/__init__.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index b23024330e1b..25b3156ab51a 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -76,7 +76,7 @@ def create_regions(self) -> None: total_required_locations = 47 if self.options.pickupsanity: - total_required_locations += 25 + total_required_locations += 26 # Add levels into the pool start_inventory = self.multiworld.start_inventory[self.player].value.copy() @@ -91,15 +91,18 @@ def create_regions(self) -> None: ItemName.stage_storm_eagle, ] stage_selected = self.random.randint(0, 7) - for i in range(len(stage_list)): - if i == stage_selected: - if stage_list[i] in self.options.start_inventory_from_pool: + if any(stage in self.options.start_inventory_from_pool for stage in stage_list) or \ + any(stage in start_inventory for stage in stage_list): + total_required_locations += 1 + for i in range(len(stage_list)): + if stage_list[i] not in start_inventory: itempool += [self.create_item(stage_list[i])] - total_required_locations += 1 - elif stage_list[i] not in start_inventory: + else: + for i in range(len(stage_list)): + if i == stage_selected: self.multiworld.get_location(LocationName.intro_completed, self.player).place_locked_item(self.create_item(stage_list[i])) - else: - itempool += [self.create_item(stage_list[i])] + else: + itempool += [self.create_item(stage_list[i])] if self.options.sigma_open == "multiworld": itempool += [self.create_item(ItemName.stage_sigma_fortress)] From 0fd8a60190a8889e7c3ab73004382b9556e05630 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Wed, 24 Apr 2024 20:50:01 -0700 Subject: [PATCH 10/71] Add patch_suffix to the client --- worlds/mmx/Client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index 079721ce341a..3c5094fd2567 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -74,6 +74,7 @@ class MMXSNIClient(SNIClient): game = "Mega Man X" + patch_suffix = ".apmmx" def __init__(self): super().__init__() From ddf736f01bf1b1c882fa62eb84d4574547f245bd Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Wed, 24 Apr 2024 20:50:13 -0700 Subject: [PATCH 11/71] Fixed a bunch of stuff --- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 2599 -> 2707 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index ab9c26518650b9b1c972901b04f3f75ed901af15..0bb40c2c3cec063a6c89bf8f83052907f4e88746 100644 GIT binary patch literal 2707 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 literal 2599 zcmZ9LcTkh*7KgtSLVyH9=#i3y7U?CTsAwqCH1xhKh87c=R22mIB4G(lioi-Wp(!Y^ zfM5YZIw)cg1f+_}t^%^?0=g=2@$Q}3nfv_pn|aSWbIzIPm^oY6SXz?x=74|bhW;-A zvi}WcUJPS>Q%^N#&yVP6831TW+}%0;=a#TYB>+GzumB1W=!K$y%W!~$mldJ_G?pwL zNEIhD?H!ky752`i%LZf!7eJI1B@wBXMG!8G3Znp{yb4M;ROe@@W{{-3&d#qW4asaI zBdTUT4bF;F<06WLR&0vg0>)PxmS(r&yHz<*8yq1R&>!ZCMulR|T3%K$L;gB$M&WQ4 zYfP*!8Tab{b}^>rql>a*h}KPy0iJ&{e7@!=+0}znBo2|0+VB1tvsC#E6V6*VN!`(T zn-<^#AN+rj}1fU!5=^SB~Nb)lHW;0bc3T2TNhiJ}KG z005O_PJvWVAzV2$Ou`c68{hyO0whm^Ab*fNRLx@ZakzldYIr~ZKfoYB4i~@}!;f+i z$T3q60|?*`ftiR*sHzj5K_&xxOZlw_71FHOUhq_JU)_))Bdy?n&7k=SMjT-q;#wuI41mUaAUo>YaJyejOgF zZxtx6l;>0g74l3&`v7@-_f7$a$~4Xx1;g}%{LM-AO9RRkpD*_~(!7@x61oC@ zYv|O7H1CWrgcYsEA) z!kGD0n9MT&fo6wta2hXNV|QrId+mN`xO2<5B_T~ z@}bx&0Kx7FyvO|V(8A7#U8UcdRhv7zA9laC{pmCRX(u7^^zIxVUUulh$lcKSfx8dq zhfBy#YSq)JuMgl3)|g26fpKNkfoLOkj{_r)B1eY22NWyBD-@j+IiuVmE=(x9Qh_%va z76cgB*eer@iI>Vl>`7LYamtoOnIr>p1E>o{55Cs{OcOWM@DkBN-!dX^Zi{o=Au)7;E3 z311FPmz8+SNeS&Ecfqe=s|nSqT*-B4rspNLCY3&BtD?Gc#mdod_VKDb*@dy+w zT+(6r3$u9iXytu4B+b-nQCYUzqpy+n?JX)$tj%s}BG0Y7a_aXpt&7E6x~-Ec{?Hk0 zt{t?}v0=wmpQ9H_l5|@Y!PLtHWncpmBqhyKzqnhtTASy*fK>UGyBfVj5Q^F_+t%ht zF*ycf1rBd+)U|Fa0cW$qjHO<5cXYHY>4i>546;_?z%^&sIYN^T?HvT5_DdU_1M-O+v^jKX;QZOG%Xgsr#$QKwOrZv+uI zB3{JkYf*F%1H)RCh4+7L0qU02##NVDxOt%NejY`fpbaU zpAsi*=k*VCy^Ou385@5r_>XB@`tKceWgl}wNqvQ0IjOwkn({hpJqFBNP{#Hnk{xBfwu);KGbmuTwfB;z&mtLTFdSh+(t#T>dj^skiB{Hr5lKiu(mJhdz;E}(+N z%IAS3U(J||wcylWpV_A2WKW!LXxGSxOLlVqdXyY{l1*jHDiIm0DxTV!jaM!-FM1lr z)0xaybJMqTgr9pnTIV!WuiaN6U_fCw6mI~SA}nlCtU?&5IAxGHtS08^#6oqasTB#Q z`bGi=d~yeo3GgO6J5&-$HfzxLQ%PW4h=0aDi@?=2uN3sy_gDRy-HjAqWR#!w$WtMZ z6F<^pONlvqYt+Wg0!$hc#0nWf+9Aam&)81p6|lXxb49=$OymAKLd)sJ8|@d zl3?Pwp8M*Ioqe!#-4#a;sOGj%x;uj(Ih10Co+0a=pxyM|glIh0AYAS~d{p zME-uyGk(?bGe{Ns%~s*h1@N?MYu7Duo8C2FZL!K??Q88WUd_RocY>O6@#=S{mvU<= zUvPz*kT+N%)SNrbqScAQRSi>O8it@r0kqC=Ah^fabxQ+1HDTA?7;fSurxw39 zD`qt7)-_nN+TKLqeCOKD%-~Ta1L}T8?7wCMKg(p$83O)Oqarp-9##|0tFRcrxNA6S zJb$Maw7$@fpiAKoHVw~JHQ^ifll z62ID+E}+4wy%8Idz-U%ClvbYVXj@Q!V4YfZctDvaD0ed7QQwd6?41x9(N&B5Wh(1k e?XH(ky=)vB*=TOv=(wAHpIAb&hWn&!0sjF Date: Wed, 24 Apr 2024 21:18:35 -0700 Subject: [PATCH 12/71] Buster can be jammed now --- worlds/mmx/Client.py | 40 ++++++++++++++++---------- worlds/mmx/Options.py | 8 ++++++ worlds/mmx/Rom.py | 10 ++----- worlds/mmx/Rules.py | 19 ++++++------ worlds/mmx/__init__.py | 2 ++ worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 2707 -> 2729 bytes 6 files changed, 49 insertions(+), 30 deletions(-) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index 3c5094fd2567..4815d65eaece 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -19,14 +19,15 @@ MMX_GAMEPLAY_STATE = WRAM_START + 0x000D3 MMX_LEVEL_INDEX = WRAM_START + 0x01F7A -MMX_WEAPON_ARRAY = WRAM_START + 0x01F88 -MMX_SUB_TANK_ARRAY = WRAM_START + 0x01F83 -MMX_UPGRADES = WRAM_START + 0x01F99 -MMX_HEART_TANKS = WRAM_START + 0x01F9C -MMX_HADOUKEN = WRAM_START + 0x01F7E -MMX_LIFE_COUNT = WRAM_START + 0x01F80 -MMX_MAX_HP = WRAM_START + 0x01F9A -MMX_CURRENT_HP = WRAM_START + 0x00BCF +MMX_WEAPON_ARRAY = WRAM_START + 0x01F88 +MMX_SUB_TANK_ARRAY = WRAM_START + 0x01F83 +MMX_UPGRADES = WRAM_START + 0x01F99 +MMX_HEART_TANKS = WRAM_START + 0x01F9C +MMX_HADOUKEN = WRAM_START + 0x01F7E +MMX_LIFE_COUNT = WRAM_START + 0x01F80 +MMX_MAX_HP = WRAM_START + 0x01F9A +MMX_CURRENT_HP = WRAM_START + 0x00BCF +MMX_UNLOCKED_CHARGED_SHOT = WRAM_START + 0x1EE16 MMX_SFX_FLAG = WRAM_START + 0x1EE03 MMX_SFX_NUMBER = WRAM_START + 0x1EE04 @@ -56,6 +57,7 @@ MMX_PICKUPSANITY_ACTIVE = ROM_START + 0x17FFE7 MMX_ENERGY_LINK_ENABLED = ROM_START + 0x17FFE8 MMX_DEATH_LINK_ACTIVE = ROM_START + 0x17FFE9 +MMX_JAMMED_BUSTER_ACTIVE = ROM_START + 0x17FFEA HP_EXCHANGE_RATE = 500000000 @@ -354,16 +356,22 @@ async def handle_item_queue(self, ctx): # Armor upgrades = upgrades[0] upgrades |= bit - snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) if bit == 0x01: snes_buffered_write(ctx, WRAM_START + 0x0BBE, bytearray([0x18])) + snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) elif bit == 0x02: - value = await snes_read(ctx, WRAM_START + 0x0C38, 0x1) - snes_buffered_write(ctx, WRAM_START + 0x0C38, bytearray([value[0] + 1])) - snes_buffered_write(ctx, WRAM_START + 0x0C42, bytearray([0x00])) - snes_buffered_write(ctx, WRAM_START + 0x0C43, bytearray([0x00])) - snes_buffered_write(ctx, WRAM_START + 0x0C39, bytearray([0x00])) - snes_buffered_write(ctx, WRAM_START + 0x0C48, bytearray([0x5D])) + jam_check = await snes_read(ctx, MMX_JAMMED_BUSTER_ACTIVE, 0x1) + charge_shot_unlocked = await snes_read(ctx, MMX_UNLOCKED_CHARGED_SHOT, 0x1) + if jam_check[0] == 1 and charge_shot_unlocked[0] == 0: + snes_buffered_write(ctx, MMX_UNLOCKED_CHARGED_SHOT, bytearray([0x01])) + else: + value = await snes_read(ctx, WRAM_START + 0x0C38, 0x1) + snes_buffered_write(ctx, WRAM_START + 0x0C38, bytearray([value[0] + 1])) + snes_buffered_write(ctx, WRAM_START + 0x0C42, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C43, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C39, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C48, bytearray([0x5D])) + snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) elif bit == 0x04: value = await snes_read(ctx, WRAM_START + 0x0C58, 0x1) snes_buffered_write(ctx, WRAM_START + 0x0C58, bytearray([value[0] + 1])) @@ -371,6 +379,7 @@ async def handle_item_queue(self, ctx): snes_buffered_write(ctx, WRAM_START + 0x0C63, bytearray([0x01])) snes_buffered_write(ctx, WRAM_START + 0x0C59, bytearray([0x00])) snes_buffered_write(ctx, WRAM_START + 0x0C68, bytearray([0x5D])) + snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) elif bit == 0x08: value = await snes_read(ctx, WRAM_START + 0x0C78, 0x1) snes_buffered_write(ctx, WRAM_START + 0x0C78, bytearray([value[0] + 1])) @@ -378,6 +387,7 @@ async def handle_item_queue(self, ctx): snes_buffered_write(ctx, WRAM_START + 0x0C83, bytearray([0x02])) snes_buffered_write(ctx, WRAM_START + 0x0C79, bytearray([0x00])) snes_buffered_write(ctx, WRAM_START + 0x0C88, bytearray([0x5D])) + snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x2B])) self.item_queue.pop(0) diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index e216d4165155..cd661d6c7734 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -21,6 +21,13 @@ class StartingLifeCount(Range): range_end = 9 default = 2 +class JammedBuster(Toggle): + """ + Jams X's buster making it only able to shoot lemons. + Note: This adds another Arms Upgrade into the item pool. + """ + display_name = "Jammed Buster" + class LogicBossWeakness(DefaultOnToggle): """ Every main boss will logically expect you to have its weakness. @@ -127,6 +134,7 @@ class MMXOptions(PerGameCommonOptions): death_link: DeathLink energy_link: EnergyLink starting_life_count: StartingLifeCount + jammed_buster: JammedBuster pickupsanity: PickupSanity logic_boss_weakness: LogicBossWeakness logic_leg_sigma: LogicLegSigma diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index 764bde9c1939..6f25e51293f9 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -132,15 +132,11 @@ def patch_rom(world: World, patch: MMXProcedurePatch): patch.write_byte(0x17FFE5, world.options.sigma_sub_tank_count.value) patch.write_byte(0x17FFE6, world.options.starting_life_count.value) patch.write_byte(0x17FFE7, world.options.pickupsanity.value) - patch.write_byte(0x17FFEB, world.options.logic_boss_weakness.value) - #patch.write_byte(0x17FFEA, world.options.logic_hadouken.value) - - # EnergyLink patch.write_byte(0x17FFE8, world.options.energy_link.value) - - # DeathLink patch.write_byte(0x17FFE9, world.options.death_link.value) - + patch.write_byte(0x17FFEA, world.options.jammed_buster.value) + patch.write_byte(0x17FFEB, world.options.logic_boss_weakness.value) + patch.write_file("token_patch.bin", patch.get_token_binary()) diff --git a/worlds/mmx/Rules.py b/worlds/mmx/Rules.py index 24dc888e555e..c081cfb17c59 100644 --- a/worlds/mmx/Rules.py +++ b/worlds/mmx/Rules.py @@ -6,6 +6,7 @@ def set_rules(world: MMXWorld): player = world.player multiworld = world.multiworld + jammed_buster = world.options.jammed_buster.value multiworld.completion_condition[player] = lambda state: state.has(ItemName.victory, player) @@ -57,7 +58,7 @@ def set_rules(world: MMXWorld): # Sigma rules add_rule(multiworld.get_location(LocationName.sigma_fortress_4_sigma, player), - lambda state: state.has(ItemName.arms, player)) + lambda state: state.has(ItemName.arms, player, jammed_buster + 1)) # Chill Penguin collectibles set_rule(multiworld.get_location(LocationName.chill_penguin_heart_tank, player), @@ -74,7 +75,7 @@ def set_rules(world: MMXWorld): state.has(EventName.chill_penguin_clear, player) or ( state.has(ItemName.chameleon_sting, player) and - state.has(ItemName.arms, player) + state.has(ItemName.arms, player, jammed_buster + 1) ) )) set_rule(multiworld.get_location(LocationName.flame_mammoth_sub_tank, player), @@ -121,6 +122,7 @@ def set_rules(world: MMXWorld): def add_pickupsanity_logic(world): player = world.player multiworld = world.multiworld + jammed_buster = world.options.jammed_buster.value set_rule(multiworld.get_location(LocationName.chill_penguin_hp_1, player), lambda state: state.has(ItemName.fire_wave, player)) @@ -141,17 +143,17 @@ def add_pickupsanity_logic(world): lambda state: state.has(ItemName.boomerang_cutter, player)) set_rule(multiworld.get_location(LocationName.sigma_fortress_3_hp_4, player), lambda state: ( - state.has(ItemName.arms, player) and + state.has(ItemName.arms, player, jammed_buster + 1) and state.has(ItemName.chameleon_sting, player) )) set_rule(multiworld.get_location(LocationName.sigma_fortress_3_energy_3, player), lambda state: ( - state.has(ItemName.arms, player) and + state.has(ItemName.arms, player, jammed_buster + 1) and state.has(ItemName.chameleon_sting, player) )) set_rule(multiworld.get_location(LocationName.sigma_fortress_3_1up, player), lambda state: ( - state.has(ItemName.arms, player) and + state.has(ItemName.arms, player, jammed_buster + 1) and ( state.has(ItemName.chameleon_sting, player) or state.has(ItemName.shotgun_ice, player) @@ -238,11 +240,12 @@ def add_boss_weakness_logic(world): def add_charged_shotgun_ice_logic(world): player = world.player multiworld = world.multiworld + jammed_buster = world.options.jammed_buster.value # Flame Mammoth collectibles add_rule(multiworld.get_location(LocationName.flame_mammoth_sub_tank, player), lambda state: ( - state.has(ItemName.arms, player) and + state.has(ItemName.arms, player, jammed_buster + 1) and state.has(ItemName.boomerang_cutter, player) and state.has(ItemName.shotgun_ice, player) )) @@ -250,11 +253,11 @@ def add_charged_shotgun_ice_logic(world): add_rule(multiworld.get_location(LocationName.boomer_kuwanger_heart_tank, player), lambda state: ( state.has(ItemName.shotgun_ice, player) and - state.has(ItemName.arms, player) + state.has(ItemName.arms, player, jammed_buster + 1) )) # Sting Chameleon collectibles add_rule(multiworld.get_location(LocationName.sting_chameleon_body, player), lambda state: ( state.has(ItemName.shotgun_ice, player) and - state.has(ItemName.arms, player) + state.has(ItemName.arms, player, jammed_buster + 1) )) diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index 25b3156ab51a..68d0b589d10e 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -144,6 +144,8 @@ def create_regions(self) -> None: else: itempool += [self.create_item(ItemName.body, ItemClassification.useful)] itempool += [self.create_item(ItemName.arms)] + if self.options.jammed_buster.value: + itempool += [self.create_item(ItemName.arms)] itempool += [self.create_item(ItemName.helmet)] itempool += [self.create_item(ItemName.legs)] diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 0bb40c2c3cec063a6c89bf8f83052907f4e88746..6a243cec40823453e729e5868a66f7b7b468ba08 100644 GIT binary patch delta 2705 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 F_#d%rr1t;- literal 2707 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 From 8cfb042b4ce5dd6fdc6c39ef28d278a47ba1b45c Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Thu, 16 May 2024 06:35:15 -0700 Subject: [PATCH 13/71] Fix missing check in sigma's fortress 3 --- worlds/mmx/Levels.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/mmx/Levels.py b/worlds/mmx/Levels.py index e73ea85f63a3..2a4fd2e71688 100644 --- a/worlds/mmx/Levels.py +++ b/worlds/mmx/Levels.py @@ -95,6 +95,7 @@ LocationName.sigma_fortress_3_hp_4: [0x0B, 0x020, 0x18], LocationName.sigma_fortress_3_energy_1: [0x0B, 0x020, 0x14], LocationName.sigma_fortress_3_energy_2: [0x0B, 0x020, 0x17], + LocationName.sigma_fortress_3_energy_3: [0x0B, 0x020, 0x19], LocationName.sigma_fortress_3_1up: [0x0B, 0x020, 0x1A], LocationName.sigma_fortress_4_velguarder: [0x0C, 0x000, 0x13], From d343ab94f1ced6bd392de87c001e8ee2f59a9466 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Thu, 16 May 2024 06:35:40 -0700 Subject: [PATCH 14/71] Shorten some most locations' names --- worlds/mmx/Names/LocationName.py | 62 ++++++++++++++++---------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/worlds/mmx/Names/LocationName.py b/worlds/mmx/Names/LocationName.py index 0231901ac5cb..f0ac7266ac1a 100644 --- a/worlds/mmx/Names/LocationName.py +++ b/worlds/mmx/Names/LocationName.py @@ -7,25 +7,25 @@ armored_armadillo_boss = "Defeated Armored Armadillo" armored_armadillo_clear = "Armored Armadillo Clear" armored_armadillo_sub_tank = "Armored Armadillo - Sub Tank" -armored_armadillo_hp_1 = "Armored Armadillo Stage - HP Pickup 1 (In blocked ceiling)" -armored_armadillo_hp_2 = "Armored Armadillo Stage - HP Pickup 2 (In blocked ceiling)" +armored_armadillo_hp_1 = "Armored Armadillo - HP Pickup 1 (In blocked ceiling)" +armored_armadillo_hp_2 = "Armored Armadillo - HP Pickup 2 (In blocked ceiling)" armored_armadillo_heart_tank = "Armored Armadillo - Heart Tank" -armored_armadillo_hp_3 = "Armored Armadillo Stage - HP Pickup 3 (Next to Hadouken capsule)" +armored_armadillo_hp_3 = "Armored Armadillo - HP Pickup 3 (Next to Hadouken capsule)" armored_armadillo_hadouken = "Armored Armadillo - Hadouken Capsule" armored_armadillo_mini_boss_1 = "Defeated Mole Borer #1" armored_armadillo_mini_boss_2 = "Defeated Mole Borer #2" chill_penguin_boss = "Defeated Chill Penguin" chill_penguin_clear = "Chill Penguin Clear" -chill_penguin_legs = "Chill Penguin Stage - Legs Capsule" -chill_penguin_heart_tank = "Chill Penguin Stage - Heart Tank" -chill_penguin_hp_1 = "Chill Penguin Stage - Weapon Energy Pickup (Inside third dome)" +chill_penguin_legs = "Chill Penguin - Legs Capsule" +chill_penguin_heart_tank = "Chill Penguin - Heart Tank" +chill_penguin_hp_1 = "Chill Penguin - Weapon Energy Pickup (Inside third dome)" spark_mandrill_boss = "Defeated Spark Mandrill" spark_mandrill_clear = "Spark Mandrill Clear" spark_mandrill_mini_boss = "Defeated Thunder Slimer" -spark_mandrill_sub_tank = "Spark Mandrill Stage - Sub Tank" -spark_mandrill_heart_tank = "Spark Mandrill Stage - Heart Tank" +spark_mandrill_sub_tank = "Spark Mandrill - Sub Tank" +spark_mandrill_heart_tank = "Spark Mandrill - Heart Tank" launch_octopus_boss = "Defeated Launch Octopus" launch_octopus_clear = "Launch Octopus Clear" @@ -33,42 +33,42 @@ launch_octopus_mini_boss_2 = "Defeated Anglerge #2" launch_octopus_mini_boss_3 = "Defeated Utuboros #1" launch_octopus_mini_boss_4 = "Defeated Utuboros #2" -launch_octopus_hp_1 = "Launch Octopus Stage - HP Pickup (Crane platform above water)" -launch_octopus_heart_tank = "Launch Octopus Stage - Heart Tank" +launch_octopus_hp_1 = "Launch Octopus - HP Pickup (Crane platform above water)" +launch_octopus_heart_tank = "Launch Octopus - Heart Tank" boomer_kuwanger_boss = "Defeated Boomer Kuwanger" boomer_kuwanger_clear = "Boomer Kuwanger Clear" -boomer_kuwanger_heart_tank = "Boomer Kuwanger Stage - Heart Tank" +boomer_kuwanger_heart_tank = "Boomer Kuwanger - Heart Tank" sting_chameleon_boss = "Defeated Sting Chameleon" sting_chameleon_clear = "Sting Chameleon Clear" sting_chameleon_heart_tank = "Sting Chameleon - Heart Tank" -sting_chameleon_body = "Sting Chameleon Stage - Body Capsule" -sting_chameleon_1up = "Sting Chameleon Stage - 1up Pickup (Inside second mini-cave in mountain)" -sting_chameleon_hp_1 = "Sting Chameleon Stage - HP Pickup (On top of platform in the swamp)" +sting_chameleon_body = "Sting Chameleon - Body Capsule" +sting_chameleon_1up = "Sting Chameleon - 1up Pickup (Inside second mini-cave in mountain)" +sting_chameleon_hp_1 = "Sting Chameleon - HP Pickup (On top of platform in the swamp)" storm_eagle_boss = "Defeated Storm Eagle" storm_eagle_clear = "Storm Eagle Clear" -storm_eagle_heart_tank = "Storm Eagle Stage - Heart Tank" -storm_eagle_sub_tank = "Storm Eagle Stage - Sub Tank" -storm_eagle_hp_1 = "Storm Eagle Stage - HP Pickup 1 (Behind first set of gas tanks)" -storm_eagle_hp_2 = "Storm Eagle Stage - HP Pickup 2 (Behind second set of gas tanks)" -storm_eagle_hp_3 = "Storm Eagle Stage - HP Pickup 3 (Behind third set of gas tanks)" -storm_eagle_1up_3 = "Storm Eagle Stage - 1up Pickup 1 (Behind wall in third set of gas tanks)" -storm_eagle_1up_1 = "Storm Eagle Stage - 1up Pickup 2 (Behind fourth set of gas tanks)" -storm_eagle_helmet = "Storm Eagle Stage - Helmet Capsule" -storm_eagle_1up_2 = "Storm Eagle Stage - 1up Pickup 3 (Above helmet capsule)" -storm_eagle_hp_4 = "Storm Eagle Stage - HP Pickup 4 (Right of plane)" -storm_eagle_energy_1 = "Storm Eagle Stage - Weapon Energy Pickup (Right of plane)" +storm_eagle_heart_tank = "Storm Eagle - Heart Tank" +storm_eagle_sub_tank = "Storm Eagle - Sub Tank" +storm_eagle_hp_1 = "Storm Eagle - HP Pickup 1 (Behind first set of gas tanks)" +storm_eagle_hp_2 = "Storm Eagle - HP Pickup 2 (Behind second set of gas tanks)" +storm_eagle_hp_3 = "Storm Eagle - HP Pickup 3 (Behind third set of gas tanks)" +storm_eagle_1up_3 = "Storm Eagle - 1up Pickup 1 (Behind wall in third set of gas tanks)" +storm_eagle_1up_1 = "Storm Eagle - 1up Pickup 2 (Behind fourth set of gas tanks)" +storm_eagle_helmet = "Storm Eagle - Helmet Capsule" +storm_eagle_1up_2 = "Storm Eagle - 1up Pickup 3 (Above helmet capsule)" +storm_eagle_hp_4 = "Storm Eagle - HP Pickup 4 (Right of plane)" +storm_eagle_energy_1 = "Storm Eagle - Weapon Energy Pickup (Right of plane)" flame_mammoth_boss = "Defeated Flame Mammoth" flame_mammoth_clear = "Flame Mammoth Clear" -flame_mammoth_hp_1 = "Flame Mammoth Stage - HP Pickup 1 (After first conveyor belts section)" -flame_mammoth_arms = "Flame Mammoth Stage - Arms Capsule" -flame_mammoth_heart_tank = "Flame Mammoth Stage - Heart Tank" -flame_mammoth_hp_2 = "Flame Mammoth Stage - HP Pickup 2 (Top platform in Dig Labour section)" -flame_mammoth_1up = "Flame Mammoth Stage - 1up Pickup (Top platform in Dig Labour section)" -flame_mammoth_sub_tank = "Flame Mammoth Stage - Sub Tank" +flame_mammoth_hp_1 = "Flame Mammoth - HP Pickup 1 (After first conveyor belts section)" +flame_mammoth_arms = "Flame Mammoth - Arms Capsule" +flame_mammoth_heart_tank = "Flame Mammoth - Heart Tank" +flame_mammoth_hp_2 = "Flame Mammoth - HP Pickup 2 (Top platform in Dig Labour section)" +flame_mammoth_1up = "Flame Mammoth - 1up Pickup (Top platform in Dig Labour section)" +flame_mammoth_sub_tank = "Flame Mammoth - Sub Tank" sigma_fortress_1_bospider = "Defeated Bospider" sigma_fortress_1_vile = "Defeated Vile" From 25e6a869a619cba335ec71053da514ea051230db Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Thu, 16 May 2024 06:36:25 -0700 Subject: [PATCH 15/71] 1.0.0 files (wip) --- worlds/mmx/Options.py | 88 ++++++ worlds/mmx/Rom.py | 83 +++++- worlds/mmx/Rules.py | 160 ++++++----- worlds/mmx/Weaknesses.py | 386 ++++++++++++++++++++++++++ worlds/mmx/__init__.py | 38 ++- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 2729 -> 3568 bytes 6 files changed, 669 insertions(+), 86 deletions(-) create mode 100644 worlds/mmx/Weaknesses.py diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index cd661d6c7734..8bbce5f8ff2d 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -21,6 +21,81 @@ class StartingLifeCount(Range): range_end = 9 default = 2 +class StartingHP(Range): + """ + How much HP X will have at the start of the game. + Note: Going over 32 HP may cause visual bugs in either gameplay or the pause menu. + The max HP is capped at 56. + """ + display_name = "Starting HP" + range_start = 1 + range_end = 32 + default = 16 + +class HeartTankEffectiveness(Range): + """ + How many units of HP each Heart tank will provide to the user. + Note: Going over 32 HP may cause visual bugs in either gameplay or the pause menu. + The max HP is capped at 56. + """ + display_name = "Heart Tank Effectiveness" + range_start = 1 + range_end = 8 + default = 2 + +class BossWeaknessRando(Choice): + """ + Every main boss will have its weakness randomized. + vanilla: Bosses retain their original weaknesses + shuffled: Bosses have their weaknesses shuffled + chaotic_double: Bosses will have two random weaknesses under the chaotic set + chaotic_single: Bosses will have one random weakness under the chaotic set + + The chaotic set makes every weapon charge level a separate weakness instead of keeping + them together, meaning that a boss can be weak to Charged Frost Shield but not its + uncharged version. + """ + display_name = "Boss Weakness Randomization" + option_vanilla = 0 + option_shuffled = 1 + option_chaotic_double = 2 + option_chaotic_single = 3 + default = 0 + +class BossWeaknessStrictness(Choice): + """ + How strict boss weaknesses will be. + not_strict: Allow every weapon to deal damage to the bosses + weakness_and_buster: Only allow the weakness and buster to deal damage to the bosses + weakness_and_upgraded_buster: Only allow the weakness and buster charge levels 3 & 4 to deal damage to the bosses + only_weakness: Only the weakness will deal damage to the bosses + + Z-Saber damage output will be cut to 50%/37.5%/25% of its original damage according to the strictness setting. + """ + display_name = "Boss Weakness Strictness" + option_not_strict = 0 + option_weakness_and_buster = 1 + option_weakness_and_upgraded_buster = 2 + option_only_weakness = 3 + default = 0 + +class BossRandomizedHP(Choice): + """ + Wheter to randomize the boss' hp or not. + off: Bosses' HP will not be randomized + weak: Bosses will have [1,32] HP + regular: Bosses will have [16,48] HP + strong: Bosses will have [32,64] HP + chaotic: Bosses will have [1,64] HP + """ + display_name = "Boss Randomize HP" + option_off = 0 + option_weak = 1 + option_regular = 2 + option_strong = 3 + option_chaotic = 4 + default = 0 + class JammedBuster(Toggle): """ Jams X's buster making it only able to shoot lemons. @@ -58,6 +133,13 @@ class PickupSanity(Toggle): """ display_name = "Pickupsanity" +class FortressBundleUnlock(Toggle): + """ + Whether to unlock Sigma's Fortress 1-3 levels as a group or not. + Unlocking level 4 requires getting all Fortress levels cleared. + """ + display_name = "Fortress Levels Bundle Unlock" + class SigmaOpen(Choice): """ Under what conditions will Sigma's Fortress open. @@ -134,12 +216,18 @@ class MMXOptions(PerGameCommonOptions): death_link: DeathLink energy_link: EnergyLink starting_life_count: StartingLifeCount + starting_hp: StartingHP + heart_tank_effectiveness: HeartTankEffectiveness + boss_weakness_rando: BossWeaknessRando + boss_weakness_strictness: BossWeaknessStrictness + boss_randomize_hp: BossRandomizedHP jammed_buster: JammedBuster pickupsanity: PickupSanity logic_boss_weakness: LogicBossWeakness logic_leg_sigma: LogicLegSigma logic_charged_shotgun_ice: LogicChargedShotgunIce early_legs: EarlyLegs + sigma_all_levels: FortressBundleUnlock sigma_open: SigmaOpen sigma_medal_count: SigmaMedalCount sigma_weapon_count: SigmaWeaponCount diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index 6f25e51293f9..0b7104b48c52 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -47,13 +47,50 @@ } refill_rom_data = { - STARTING_ID + 0x0030: ["small hp refill"], - STARTING_ID + 0x0031: ["large hp refill"], - STARTING_ID + 0x0034: ["1up"], + STARTING_ID + 0x0030: ["hp refill", 2], + STARTING_ID + 0x0031: ["hp refill", 8], + STARTING_ID + 0x0034: ["1up", 0], #0xBD0032: ["small weapon refill"], #0xBD0033: ["large weapon refill"] } +boss_weakness_offsets = { + "Sting Chameleon": 0x37E20, + "Storm Eagle": 0x37E60, + "Flame Mammoth": 0x37E80, + "Chill Penguin": 0x37EA0, + "Spark Mandrill": 0x3708B, + "Armored Armadillo": 0x370A9, + "Launch Octopus": 0x370C7, + "Boomer Kuwanger": 0x370E5, + "Thunder Slimer": 0x37F00, + "Vile": 0x37E00, + "Bospider": 0x37EC0, + "Rangda Bangda": 0x37031, + "D-Rex": 0x37E40, + "Velguarder": 0x37EE0, + "Sigma": 0x3717B, + "Wolf Sigma": 0x37199, +} + +boss_hp_caps_offsets = { + "Sting Chameleon": 0x406C9, + "Storm Eagle": 0x3D95F, + "Flame Mammoth": 0x392BD, + "Chill Penguin": 0x0B5FB, + "Spark Mandrill": 0x41D29, + "Armored Armadillo": 0x1B2B5, + "Launch Octopus": 0x0C504, + "Boomer Kuwanger": 0x38BE8, + "Vile": 0x45C34, + "Bospider": 0x15C0E, + "Rangda Bangda": 0x42A38, + "D-Rex": 0x440FD, + "Velguarder": 0x148DF, + "Sigma": 0x4467B, + "Wolf Sigma": 0x44B79, +} + class MMXProcedurePatch(APProcedurePatch, APTokenMixin): hash = [HASH_US, HASH_LEGACY] game = "Mega Man X" @@ -75,9 +112,32 @@ def write_byte(self, offset, value): def write_bytes(self, offset, value: typing.Iterable[int]): self.write_token(APTokenTypes.WRITE, offset, bytes(value)) -def patch_rom(world: World, patch: MMXProcedurePatch): - from Utils import __version__ +def adjust_boss_damage_table(world: World, patch: MMXProcedurePatch): + for boss, data in world.boss_weakness_data.items(): + offset = boss_weakness_offsets[boss] + patch.write_bytes(offset, bytearray(data)) + + # Fix second anglerge having different weakness + patch.write_byte(0x12E62, 0x01) + + +def adjust_boss_hp(world: World, patch: MMXProcedurePatch): + option = world.options.boss_randomize_hp + if option == "weak": + ranges = [1,32] + elif option == "regular": + ranges = [16,48] + elif option == "strong": + ranges = [32,64] + elif option == "chaotic": + ranges = [1,64] + + for _, offset in boss_hp_caps_offsets.items(): + patch.write_byte(offset, world.random.randint(ranges[0], ranges[1])) + + +def patch_rom(world: World, patch: MMXProcedurePatch): # Prepare some ROM locations to receive the basepatch output patch.write_bytes(0x00098C, bytearray([0x85,0xB3,0x8A])) patch.write_bytes(0x0009AE, bytearray([0x85,0xB3,0x8A])) @@ -118,7 +178,14 @@ def patch_rom(world: World, patch: MMXProcedurePatch): 0x20,0x42,0x59,0x20,0x4E,0x49,0x4E,0x54, 0x45,0x4E,0x44,0x4F,0x00])) + if world.options.boss_weakness_rando != "vanilla": + adjust_boss_damage_table(world, patch) + + if world.options.boss_randomize_hp != "off": + adjust_boss_hp(world, patch) + # Edit the ROM header + from Utils import __version__ patch.name = bytearray(f'MMX1{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] patch.name.extend([0] * (21 - len(patch.name))) patch.write_bytes(0x7FC0, patch.name) @@ -136,6 +203,12 @@ def patch_rom(world: World, patch: MMXProcedurePatch): patch.write_byte(0x17FFE9, world.options.death_link.value) patch.write_byte(0x17FFEA, world.options.jammed_buster.value) patch.write_byte(0x17FFEB, world.options.logic_boss_weakness.value) + patch.write_byte(0x17FFEC, world.options.boss_weakness_rando.value) + patch.write_byte(0x17FFED, world.options.starting_hp.value) + patch.write_byte(0x17FFEE, world.options.heart_tank_effectiveness.value) + patch.write_byte(0x17FFEF, world.options.sigma_all_levels.value) + + patch.write_byte(0x014FF, world.options.starting_hp.value) patch.write_file("token_patch.bin", patch.get_token_binary()) diff --git a/worlds/mmx/Rules.py b/worlds/mmx/Rules.py index c081cfb17c59..ff6f80f8df0e 100644 --- a/worlds/mmx/Rules.py +++ b/worlds/mmx/Rules.py @@ -2,6 +2,66 @@ from . import MMXWorld from .Names import LocationName, ItemName, RegionName, EventName + +bosses = { + "Sting Chameleon": [ + f"{RegionName.sting_chameleon_swamp} -> {RegionName.sting_chameleon_boss}", + f"{RegionName.sigma_fortress_3_rematch_1} -> {RegionName.sigma_fortress_3_rematch_2}" + ], + "Storm Eagle": [ + f"{RegionName.storm_eagle_aircraft} -> {RegionName.storm_eagle_boss}", + f"{RegionName.sigma_fortress_2_ride} -> {RegionName.sigma_fortress_2_rematch_2}" + ], + "Flame Mammoth": [ + f"{RegionName.flame_mammoth_lava_river_2} -> {RegionName.flame_mammoth_boss}", + f"{RegionName.sigma_fortress_3_rematch_4} -> {RegionName.sigma_fortress_3_rematch_5}" + ], + "Chill Penguin": [ + f"{RegionName.chill_penguin_ride} -> {RegionName.chill_penguin_boss}", + f"{RegionName.sigma_fortress_2_start} -> {RegionName.sigma_fortress_2_rematch_1}" + ], + "Spark Mandrill": [ + f"{RegionName.spark_mandrill_deep} -> {RegionName.spark_mandrill_boss}", + f"{RegionName.sigma_fortress_3_rematch_2} -> {RegionName.sigma_fortress_3_rematch_3}" + ], + "Armored Armadillo": [ + f"{RegionName.armored_armadillo_ride_3} -> {RegionName.armored_armadillo_boss}", + f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_rematch_1}" + ], + "Launch Octopus": [ + f"{RegionName.launch_octopus_sea} -> {RegionName.launch_octopus_boss}", + f"{RegionName.sigma_fortress_3_rematch_3} -> {RegionName.sigma_fortress_3_rematch_4}" + ], + "Boomer Kuwanger": [ + f"{RegionName.boomer_kuwanger_top} -> {RegionName.boomer_kuwanger_boss}", + f"{RegionName.sigma_fortress_1_vertical} -> {RegionName.sigma_fortress_1_rematch_1}" + ], + "Thunder Slimer": [ + f"{RegionName.spark_mandrill_entrance} -> {RegionName.spark_mandrill_mid_boss}" + ], + "Vile": [ + f"{RegionName.sigma_fortress_1_outside} -> {RegionName.sigma_fortress_1_vile}" + ], + "Bospider": [ + f"{RegionName.sigma_fortress_1_rematch_1} -> {RegionName.sigma_fortress_1_boss}" + ], + "Rangda Bangda": [ + f"{RegionName.sigma_fortress_2_rematch_2} -> {RegionName.sigma_fortress_2_boss}" + ], + "D-Rex": [ + f"{RegionName.sigma_fortress_3_rematch_5} -> {RegionName.sigma_fortress_3_boss}" + ], + "Velguarder": [ + f"{RegionName.sigma_fortress_4} -> {RegionName.sigma_fortress_4_dog}" + ], + "Sigma": [ + f"{RegionName.sigma_fortress_4_dog} -> {RegionName.sigma_fortress_4_sigma}" + ], + "Wolf Sigma": [ + f"{RegionName.sigma_fortress_4_dog} -> {RegionName.sigma_fortress_4_sigma}" + ], +} + def set_rules(world: MMXWorld): player = world.player @@ -111,7 +171,7 @@ def set_rules(world: MMXWorld): add_pickupsanity_logic(world) # Handle bosses weakness - if world.options.logic_boss_weakness.value: + if world.options.logic_boss_weakness.value or world.options.boss_weakness_strictness.value >= 2: add_boss_weakness_logic(world) # Handle charged shotgun ice logic @@ -119,7 +179,7 @@ def set_rules(world: MMXWorld): add_charged_shotgun_ice_logic(world) -def add_pickupsanity_logic(world): +def add_pickupsanity_logic(world: MMXWorld): player = world.player multiworld = world.multiworld jammed_buster = world.options.jammed_buster.value @@ -161,83 +221,35 @@ def add_pickupsanity_logic(world): )) -def add_boss_weakness_logic(world): +def add_boss_weakness_logic(world: MMXWorld): player = world.player multiworld = world.multiworld + jammed_buster = world.options.jammed_buster.value - # Armored Armadillo - set_rule(multiworld.get_entrance(f"{RegionName.armored_armadillo_ride_3} -> {RegionName.armored_armadillo_boss}", player), - lambda state: state.has(ItemName.electric_spark, player)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_rematch_1}", player), - lambda state: state.has(ItemName.electric_spark, player)) - - # Chill Penguin - set_rule(multiworld.get_entrance(f"{RegionName.chill_penguin_ride} -> {RegionName.chill_penguin_boss}", player), - lambda state: state.has(ItemName.fire_wave, player)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_2_start} -> {RegionName.sigma_fortress_2_rematch_1}", player), - lambda state: state.has(ItemName.fire_wave, player)) - - # Flame Mammoth - set_rule(multiworld.get_entrance(f"{RegionName.flame_mammoth_lava_river_2} -> {RegionName.flame_mammoth_boss}", player), - lambda state: state.has(ItemName.storm_tornado, player)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_rematch_4} -> {RegionName.sigma_fortress_3_rematch_5}", player), - lambda state: state.has(ItemName.storm_tornado, player)) - - # Boomer Kuwanger - set_rule(multiworld.get_entrance(f"{RegionName.boomer_kuwanger_top} -> {RegionName.boomer_kuwanger_boss}", player), - lambda state: state.has(ItemName.homing_torpedo, player)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_1_vertical} -> {RegionName.sigma_fortress_1_rematch_1}", player), - lambda state: state.has(ItemName.homing_torpedo, player)) - - # Sting Chameleon - set_rule(multiworld.get_entrance(f"{RegionName.sting_chameleon_swamp} -> {RegionName.sting_chameleon_boss}", player), - lambda state: state.has(ItemName.boomerang_cutter, player)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_rematch_1} -> {RegionName.sigma_fortress_3_rematch_2}", player), - lambda state: state.has(ItemName.boomerang_cutter, player)) - - # Spark Mandrill - set_rule(multiworld.get_entrance(f"{RegionName.spark_mandrill_deep} -> {RegionName.spark_mandrill_boss}", player), - lambda state: state.has(ItemName.shotgun_ice, player)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_rematch_2} -> {RegionName.sigma_fortress_3_rematch_3}", player), - lambda state: state.has(ItemName.shotgun_ice, player)) - - # Storm Eagle - set_rule(multiworld.get_entrance(f"{RegionName.storm_eagle_aircraft} -> {RegionName.storm_eagle_boss}", player), - lambda state: state.has(ItemName.chameleon_sting, player)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_2_ride} -> {RegionName.sigma_fortress_2_rematch_2}", player), - lambda state: state.has(ItemName.chameleon_sting, player)) - - # Launch Octopus - set_rule(multiworld.get_entrance(f"{RegionName.launch_octopus_sea} -> {RegionName.launch_octopus_boss}", player), - lambda state: state.has(ItemName.rolling_shield, player)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_rematch_3} -> {RegionName.sigma_fortress_3_rematch_4}", player), - lambda state: state.has(ItemName.rolling_shield, player)) - - # Bospider - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_1_rematch_1} -> {RegionName.sigma_fortress_1_boss}", player), - lambda state: state.has(ItemName.shotgun_ice, player)) - - # Rangda Bangda - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_2_rematch_2} -> {RegionName.sigma_fortress_2_boss}", player), - lambda state: state.has(ItemName.chameleon_sting, player)) - - # D-Rex - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_rematch_5} -> {RegionName.sigma_fortress_3_boss}", player), - lambda state: state.has(ItemName.boomerang_cutter, player)) - - # Velguarder - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_4} -> {RegionName.sigma_fortress_4_dog}", player), - lambda state: state.has(ItemName.shotgun_ice, player)) - - # Sigma & Wolf Sigma - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_4_dog} -> {RegionName.sigma_fortress_4_sigma}", player), - lambda state: ( - state.has(ItemName.electric_spark, player) and - state.has(ItemName.rolling_shield, player) - )) + if world.options.boss_weakness_rando == "vanilla": + from .Weaknesses import boss_weaknesses + world.boss_weaknesses = boss_weaknesses + for boss, regions in bosses.items(): + weaknesses = world.boss_weaknesses[boss] + for weakness in weaknesses: + if weakness[0] is None: + continue + weakness = weakness[0] + for region in regions: + ruleset = {} + if "Check Charge" in weakness[0]: + ruleset[ItemName.arms] = jammed_buster + int(weakness[0][-1:]) - 1 + elif "Check Dash" in weakness[0]: + ruleset[ItemName.legs] = 1 + else: + ruleset[weakness[0]] = 1 + if len(weakness) != 1: + ruleset[weakness[1]] = 1 + add_rule(multiworld.get_entrance(region, player), + lambda state, ruleset=ruleset: state.has_all_counts(ruleset, player)) -def add_charged_shotgun_ice_logic(world): +def add_charged_shotgun_ice_logic(world: MMXWorld): player = world.player multiworld = world.multiworld jammed_buster = world.options.jammed_buster.value diff --git a/worlds/mmx/Weaknesses.py b/worlds/mmx/Weaknesses.py new file mode 100644 index 000000000000..4a16763167a4 --- /dev/null +++ b/worlds/mmx/Weaknesses.py @@ -0,0 +1,386 @@ +from .Names import ItemName + +boss_weaknesses = { + "Sting Chameleon": [[[ItemName.boomerang_cutter]]], + "Storm Eagle": [[[ItemName.chameleon_sting]]], + "Flame Mammoth": [[[ItemName.storm_tornado]]], + "Chill Penguin": [[[ItemName.fire_wave]]], + "Spark Mandrill": [[[ItemName.shotgun_ice]]], + "Armored Armadillo": [[[ItemName.electric_spark]]], + "Launch Octopus": [[[ItemName.rolling_shield]]], + "Boomer Kuwanger": [[[ItemName.homing_torpedo]]], + "Thunder Slimer": [[None]], + "Vile": [[[ItemName.homing_torpedo]]], + "Bospider": [[[ItemName.shotgun_ice]]], + "Rangda Bangda": [[[ItemName.chameleon_sting]]], + "D-Rex": [[[ItemName.boomerang_cutter]]], + "Velguarder": [[[ItemName.shotgun_ice]]], + "Sigma": [[[ItemName.electric_spark]]], + "Wolf Sigma": [[["Check Charge 2", ItemName.rolling_shield]]], +} + +WEAKNESS_UNCHARGED_DMG = 0x03 +WEAKNESS_CHARGED_DMG = 0x05 + +weapon_id = { + 0x00: "Lemon", + 0x01: "Charged Shot (Level 1)", + 0x02: "Charged Shot (Level 3, Bullet Stream)", + 0x03: "Charged Shot (Level 2)", + 0x04: "Hadouken", + 0x06: "Lemon (Dash)", + 0x07: "Uncharged Homing Torpedo", + 0x08: "Uncharged Chameleon Sting", + 0x09: "Uncharged Rolling Shield", + 0x0A: "Uncharged Fire Wave", + 0x0B: "Uncharged Storm Tornado", + 0x0C: "Uncharged Electric Spark", + 0x0D: "Uncharged Boomerang Cutter", + 0x0E: "Uncharged Shotgun Ice", + 0x10: "Charged Homing Torpedo", + 0x12: "Charged Rolling Shield", + 0x13: "Charged Fire Wave", + 0x14: "Charged Storm Tornado", + 0x15: "Charged Electric Spark", + 0x16: "Charged Boomerang Cutter", + 0x17: "Charged Shotgun Ice", + 0x1D: "Charged Shot (Level 3, Shockwave)", +} + +damage_templates = { + "Allow Buster": [ + 0x01,0x02,0x03,0x03,0x20,0x00,0x02,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x7F,0x80,0x01 + ], + "Allow Upgraded Buster": [ + 0x80,0x80,0x80,0x03,0x20,0x00,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x7F,0x80,0x01 + ], + "Only Weakness": [ + 0x80,0x80,0x80,0x80,0x20,0x00,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x7F,0x80,0x80 + ], +} + +boss_weakness_data = { + "Sting Chameleon": [ + 0x01,0x01,0x02,0x02,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Storm Eagle": [ + 0x01,0x01,0x02,0x02,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Flame Mammoth": [ + 0x01,0x01,0x02,0x02,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Chill Penguin": [ + 0x01,0x02,0x03,0x03,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Spark Mandrill": [ + 0x01,0x02,0x03,0x03,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Armored Armadillo": [ + 0x01,0x01,0x01,0x01,0x20,0x02,0x02,0x01, + 0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01, + 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Launch Octopus": [ + 0x01,0x02,0x03,0x03,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Boomer Kuwanger": [ + 0x01,0x02,0x03,0x03,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Thunder Slimer": [ + 0x01,0x02,0x04,0x04,0x20,0x04,0x02,0x02, + 0x02,0x02,0x01,0x01,0x02,0x02,0x02,0x02, + 0x03,0x00,0x04,0x01,0x04,0x06,0x06,0x04, + 0x04,0x05,0x0A,0x7F,0x10,0x01 + ], + "Vile": [ + 0x01,0x02,0x04,0x04,0x20,0x04,0x02,0x02, + 0x02,0x02,0x01,0x01,0x02,0x02,0x02,0x02, + 0x03,0x00,0x04,0x01,0x04,0x06,0x06,0x06, + 0x04,0x05,0x0A,0x7F,0x10,0x01 + ], + "Bospider": [ + 0x01,0x02,0x03,0x03,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Rangda Bangda": [ + 0x01,0x01,0x02,0x02,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "D-Rex": [ + 0x01,0x01,0x02,0x02,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Velguarder": [ + 0x01,0x02,0x03,0x03,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Sigma": [ + 0x01,0x01,0x01,0x01,0x20,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01 + ], + "Wolf Sigma": [ + 0x80,0x80,0x01,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x01 + ], +} + +boss_excluded_weapons = { + "Sting Chameleon": [ + "Charged Fire Wave", + ], + "Storm Eagle": [ + "Charged Fire Wave", + ], + "Flame Mammoth": [ + "Charged Fire Wave", + ], + "Chill Penguin": [ + "Charged Fire Wave", + ], + "Spark Mandrill": [ + "Charged Fire Wave", + ], + "Armored Armadillo": [ + "Charged Fire Wave", + ], + "Launch Octopus": [ + "Fire Wave", + "Charged Fire Wave", + ], + "Boomer Kuwanger": [ + "Charged Fire Wave", + ], + "Thunder Slimer": [ + "Charged Fire Wave", + ], + "Vile": [ + "Charged Fire Wave", + ], + "Bospider": [ + "Charged Fire Wave", + ], + "Rangda Bangda": [ + "Charged Fire Wave", + ], + "D-Rex": [ + "Charged Fire Wave", + ], + "Velguarder": [ + "Charged Fire Wave", + ], + "Sigma": [ + "Lemon (Dash)", + "Charged Fire Wave", + ], + "Wolf Sigma": [ + "Charged Fire Wave", + ], +} + +weapons = { + "Buster": [ + [None, 0x00, 0x02], + [None, 0x06, 0x04], + [None, 0x01, 0x03], + [None, 0x03, 0x04], + [None, 0x02, 0x05], + ], + "Homing Torpedo": [ + [[ItemName.homing_torpedo], 0x07, WEAKNESS_UNCHARGED_DMG], + [[ItemName.homing_torpedo], 0x10, WEAKNESS_CHARGED_DMG], + ], + "Chameleon Sting": [ + [[ItemName.chameleon_sting], 0x08, WEAKNESS_UNCHARGED_DMG], + ], + "Rolling Shield": [ + [[ItemName.rolling_shield], 0x09, WEAKNESS_UNCHARGED_DMG], + [[ItemName.rolling_shield], 0x12, WEAKNESS_CHARGED_DMG+1], + ], + "Fire Wave": [ + [[ItemName.fire_wave], 0x0A, WEAKNESS_UNCHARGED_DMG], + [[ItemName.fire_wave], 0x13, WEAKNESS_CHARGED_DMG+4], + ], + "Storm Tornado": [ + [[ItemName.storm_tornado], 0x0B, WEAKNESS_UNCHARGED_DMG], + [[ItemName.storm_tornado], 0x14, WEAKNESS_CHARGED_DMG], + ], + "Electric Spark": [ + [[ItemName.electric_spark], 0x0C, WEAKNESS_UNCHARGED_DMG], + [[ItemName.electric_spark], 0x15, WEAKNESS_CHARGED_DMG], + ], + "Boomerang Cutter": [ + [[ItemName.boomerang_cutter], 0x0D, WEAKNESS_CHARGED_DMG], + [[ItemName.boomerang_cutter], 0x16, WEAKNESS_CHARGED_DMG], + ], + "Shotgun Ice": [ + [[ItemName.shotgun_ice], 0x0E, WEAKNESS_CHARGED_DMG], + [[ItemName.shotgun_ice], 0x17, WEAKNESS_CHARGED_DMG], + ], +} + +weapons_chaotic = { + "Lemon": [ + [None, 0x00, 0x02], + ], + "Lemon (Dash)": [ + [["Check Dash"], 0x06, 0x03], + ], + "Charged Shot (Level 1)": [ + [["Check Charge 1"], 0x01, 0x03], + ], + "Charged Shot (Level 2)": [ + [["Check Charge 1"], 0x03, 0x04], + ], + "Charged Shot (Level 3)": [ + [["Check Charge 2"], 0x02, 0x05], + [["Check Charge 2"], 0x1D, 0x02], + ], + "Homing Torpedo": [ + [[ItemName.homing_torpedo], 0x07, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Homing Torpedo": [ + [["Check Charge 2", ItemName.homing_torpedo], 0x10, WEAKNESS_CHARGED_DMG], + ], + "Chameleon Sting": [ + [[ItemName.chameleon_sting], 0x08, WEAKNESS_UNCHARGED_DMG], + ], + "Rolling Shield": [ + [[ItemName.rolling_shield], 0x09, WEAKNESS_UNCHARGED_DMG+1], + ], + "Charged Rolling Shield": [ + [["Check Charge 2", ItemName.rolling_shield], 0x12, WEAKNESS_CHARGED_DMG], + ], + "Fire Wave": [ + [[ItemName.fire_wave], 0x0A, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Fire Wave": [ + [["Check Charge 2", ItemName.fire_wave], 0x13, WEAKNESS_CHARGED_DMG+4], + ], + "Storm Tornado": [ + [[ItemName.storm_tornado], 0x0B, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Storm Tornado": [ + [["Check Charge 2", ItemName.storm_tornado], 0x14, WEAKNESS_CHARGED_DMG], + ], + "Electric Spark": [ + [[ItemName.electric_spark], 0x0C, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Electric Spark": [ + [["Check Charge 2", ItemName.electric_spark], 0x15, WEAKNESS_CHARGED_DMG], + ], + "Boomerang Cutter": [ + [[ItemName.boomerang_cutter], 0x0D, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Boomerang Cutter": [ + [["Check Charge 2", ItemName.boomerang_cutter], 0x16, WEAKNESS_CHARGED_DMG], + ], + "Shotgun Ice": [ + [[ItemName.shotgun_ice], 0x0E, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Shotgun Ice": [ + [["Check Charge 2", ItemName.shotgun_ice], 0x17, WEAKNESS_CHARGED_DMG], + ], +} + + +def randomize_weaknesses(world): + shuffle_type = world.options.boss_weakness_rando.value + strictness_type = world.options.boss_weakness_strictness.value + + weapon_list = weapons.keys() + if shuffle_type == 2 or shuffle_type == 3: + weapon_list = weapons_chaotic.keys() + weapon_list = list(weapon_list) + + for boss in boss_weaknesses.keys(): + if boss == "Dr. Doppler's Lab 2 Boss": + continue + world.boss_weaknesses[boss] = [] + + if strictness_type == 0: + damage_table = boss_weakness_data[boss].copy() + elif strictness_type == 1: + damage_table = damage_templates["Allow Buster"].copy() + elif strictness_type == 2: + damage_table = damage_templates["Allow Upgraded Buster"].copy() + else: + damage_table = damage_templates["Only Weakness"].copy() + + copied_weapon_list = weapon_list.copy() + for weapon in boss_excluded_weapons[boss]: + if weapon in copied_weapon_list: + copied_weapon_list.remove(weapon) + + if shuffle_type == 1: + chosen_weapon = world.random.choice(copied_weapon_list) + data = weapons[chosen_weapon] + for entry in data: + world.boss_weaknesses[boss].append(entry) + damage = entry[2] + damage_table[entry[1]] = damage + world.boss_weakness_data[boss] = damage_table.copy() + + + elif shuffle_type == 2: + for _ in range(2): + chosen_weapon = world.random.choice(copied_weapon_list) + data = weapons_chaotic[chosen_weapon].copy() + copied_weapon_list.remove(chosen_weapon) + for entry in data: + world.boss_weaknesses[boss].append(entry) + damage = entry[2] + damage_table[entry[1]] = damage + world.boss_weakness_data[boss] = damage_table.copy() + + + elif shuffle_type == 3: + chosen_weapon = world.random.choice(copied_weapon_list) + data = weapons_chaotic[chosen_weapon].copy() + for entry in data: + world.boss_weaknesses[boss].append(entry) + damage = entry[2] + damage_table[entry[1]] = damage + world.boss_weakness_data[boss] = damage_table.copy() diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index 68d0b589d10e..e36d9b960042 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -16,6 +16,7 @@ from .Names import ItemName, LocationName, EventName from .Options import MMXOptions from .Client import MMXSNIClient +from .Weaknesses import randomize_weaknesses, boss_weaknesses, weapon_id from .Rom import patch_rom, MMXProcedurePatch, HASH_US, HASH_LEGACY class MMXSettings(settings.Group): @@ -79,7 +80,7 @@ def create_regions(self) -> None: total_required_locations += 26 # Add levels into the pool - start_inventory = self.multiworld.start_inventory[self.player].value.copy() + start_inventory = self.options.start_inventory.value.copy() stage_list = [ ItemName.stage_armored_armadillo, ItemName.stage_boomer_kuwanger, @@ -203,6 +204,7 @@ def create_regions(self) -> None: # Finish self.multiworld.itempool += itempool + def create_item(self, name: str, force_classification=False) -> Item: data = item_table[name] @@ -219,10 +221,12 @@ def create_item(self, name: str, force_classification=False) -> Item: return created_item + def set_rules(self): from .Rules import set_rules set_rules(self) - + + def fill_slot_data(self): slot_data = {} for option_name in (attr.name for attr in dataclasses.fields(MMXOptions) @@ -230,14 +234,37 @@ def fill_slot_data(self): option = getattr(self.options, option_name) slot_data[option_name] = option.value return slot_data - + + def generate_early(self): if self.options.early_legs: self.multiworld.early_items[self.player][ItemName.legs] = 1 + + if self.options.boss_weakness_rando != "vanilla": + self.boss_weaknesses = {} + self.boss_weakness_data = {} + randomize_weaknesses(self) + + early_stage = self.random.choice(list(item_groups["Access Codes"])) + self.multiworld.local_early_items[self.player][early_stage] = 1 + + + def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: + if self.options.boss_weakness_rando != "vanilla": + spoiler_handle.write(f"\nMega Man X boss weaknesses for {self.multiworld.player_name[self.player]}:\n") + + for boss, data in self.boss_weaknesses.items(): + weaknesses = "" + for i in range(len(data)): + weaknesses += f"{weapon_id[data[i][1]]}, " + weaknesses = weaknesses[:-2] + spoiler_handle.writelines(f"{boss + ':':<30s}{weaknesses}\n") + def get_filler_item_name(self) -> str: return self.random.choice(list(junk_table.keys())) + def generate_output(self, output_directory: str): try: patch = MMXProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) @@ -253,6 +280,7 @@ def generate_output(self, output_directory: str): finally: self.rom_name_available_event.set() # make sure threading continues and errors are collected + def modify_multidata(self, multidata: dict): import base64 # wait for self.rom_name to be available. @@ -262,7 +290,3 @@ def modify_multidata(self, multidata: dict): if rom_name: new_name = base64.b64encode(bytes(self.rom_name)).decode() multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] - - @classmethod - def stage_fill_hook(cls, multiworld: MultiWorld, progitempool, usefulitempool, filleritempool, fill_locations): - return \ No newline at end of file diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 6a243cec40823453e729e5868a66f7b7b468ba08..226ea5390d2a23f1361b0f49a64e1bf5e34ac664 100644 GIT binary patch literal 3568 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 T)f{@U>m}bq1BVK8TjBgK3B4Z} literal 2729 zcmYL|3pCUHAIE>QnQgXWMl5OWRGy}WF4S)$x75Z)k#b3l9+LETsW35fZMKwFG;QQs z!cP`Ip_-c@(My0F+=@2pWJrRzd^GaDYQVAV7#e zfFl5$4NMKDp%oD#GyzQVk?a&VE}RWA2MA0+XDcl@DMVMYkZ~1^BNJUO9b`+7TLXoy z)CV6jbIX++CUEMl(^s#vYOm#X2htFnlz|K{+Xvu-C{V?x?vz!s!Hp|vz2sHVQG&ha z5R?|gy{U-ca4!)mRBTS>U1X{?NlagCp53H#W9${s!Yct^UtxIDc0ut{lW z(id(Ueb6@B-Vr1L0A}TXm`ng7#9(wF!ajJm4$#?$C!w=-wBbm`{sMlX$H@jWQ;Z^BjRr`67=DBRIPxmecSFivXPORL{?>TFknjHK`tZQ zoPzO7s#_5nG-)a92J0}+zytiDkL>`8<1u10e)Z;V^;=*P71ZAjRKX1RYC>vDYOuPy z>08)t8geVDbU%e)me!aT{>}UVY8{hIgA}vOi;Pksz}jml4$wb3xwK##{j#Tm+tu3G zG%!N++!}-QIK>p3Iiw@L!>%{^ha7Ca!1Lu~>ilI3gAdxg)t=ZE;kdnwB5@#;HBXnQWtTdp`=} zuJsFr5UqgGJ*0^bw)@NEy4gPckOZ+xxAvz{*<~oGk2HrJV0}$wirgzR84K6K+$* zVWGnu=EKw)V#NH1m^9|b%R|=m-9HN)&B^R_xQ)d%E&lh5s}~$^b$maxzAdNb(~-<* zxRG5zc|>B)uCMjo@6x`d?7X5CfveD2gfW!1zPsb`SW4WEKf=)4d}~v0WKOO)76E0P z>AAeP`$vJNCZl|;yS(Sp2C3=N;V#@e3MG7&#QB$v$H&L{00gICL!pt09=~6omnHL| zx7Ykyo?c#*ex<&cSvr?Ey8N>bHlT8P{7>JQ)6;)SZk^c{ilWb1m+;$cor5|9*3uHn zrsCB_j{Q)3Dh=MPQo02gzTnUquU~h;z2pKJqesI7eN4QbNY3nCr1JR``vGy)6z zn4$ad33PQ*Pz$p!l(R)K@Jx)J5U!vCP@sYqD8?TO;?0{NW(xxpn}mp1*3|JX=SI5r5erI3NZlMF{!_CzM ze`DxsVCWiWiM&EJ;gU$!!T|-OiaP)}o5cnGVQ}R{QLXaz&8)J>`-@jC=C#W^4H7uwyO8$Rc^lmFUn^<94VdEm=H}@Y z{|6_{dWZkn9yU~CB$X7%3KJ?NMgFJfa@HEeVq(Z8|TLP{egy24u`= z(&l`VM*&;y%8kRJI{A0efpOGE@Co;3yiQ9Tl`zR432%)ucBR1Dp(Rsl&eylmB)(6? zoknBs)G)`-!hvYQm2?QquqE}wj=qc){zX>RT7p zL5Yrc`=d6hwPJUZXW!cUU+Qe95JgrMlv#24{x#$y9JvT9YFq-K_QMuXLG$?y0i79Y8Ov~tJltmJ5(xK={bq~snV=@gAs zi8?xj1;%3Dy!q@J@zhfMBNL!+C0r#dilPp#(P(8KssEusfNE`vQpFBSA) zCs(CNzpqXZii~J&m-4l&{!leZA96Ezle^6)`BlM5t*bO7t{3nhd%_CX_Fl#0`Rx~z z`_;BE!Ar#L=l_%;kE_$Wo?h3BrR%*8O=|VZtYrjshxaxVc2mtmm)bBsys3`34V`dtRM0feT8-p&${yknrRwyzbE-R4g74rjH^&? zK2HoLoH!)%b9G@%N#|gyWph8KgRhBBjt{K$|4wY;R(l6#&2pU72X}p<;VW7Mcb2r% z+ascDj$ZEOHHb!c)t65nNPpHW4KYsPxx+HWb!|QWZF)(x5C??>SV*OTD!uR3m2m&b zF+2Sw@a4MG7Odv4k!Ft$?!F)MNU!|SwG#s8GkbPEIy)Vm)&LW1eA9f_s;0K2@_?~J zRl&z7RfEm%3*H$$Z6_VtTNhATQF&l}b`^1B^-)|DD$7OB(-Pa3WNg{akV>?#e~&Su Mry|jgjiJQ<0l)U1p8x;= From d1c08f8bffb8dd28527f51c477095ea95d6f00ed Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Thu, 16 May 2024 06:36:51 -0700 Subject: [PATCH 16/71] add client changes (will be changed later anyway to match mmx3's) --- worlds/mmx/Client.py | 84 ++++++++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index 4815d65eaece..54b722fa5093 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -127,10 +127,10 @@ async def validate_rom(self, ctx): if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:4] != b"MMX1": if "pool" in ctx.command_processor.commands: ctx.command_processor.commands.pop("pool") - if "heal" in ctx.command_processor.commands: - ctx.command_processor.commands.pop("heal") if "autoheal" in ctx.command_processor.commands: ctx.command_processor.commands.pop("autoheal") + if "refill" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("refill") return False ctx.game = self.game @@ -138,13 +138,13 @@ async def validate_rom(self, ctx): ctx.receive_option = 0 ctx.send_option = 0 ctx.allow_collect = True - if energy_link: + if energy_link[0]: if "pool" not in ctx.command_processor.commands: ctx.command_processor.commands["pool"] = cmd_pool - if "heal" not in ctx.command_processor.commands: - ctx.command_processor.commands["heal"] = cmd_heal if "autoheal" not in ctx.command_processor.commands: ctx.command_processor.commands["autoheal"] = cmd_autoheal + if "refill" not in ctx.command_processor.commands: + ctx.command_processor.commands["refill"] = cmd_refill death_link = await snes_read(ctx, MMX_DEATH_LINK_ACTIVE, 1) if death_link[0]: @@ -198,11 +198,11 @@ async def handle_energy_link(self, ctx): can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ receiving_item[0] != 0x00: return - - if any(item in self.item_queue for item in HP_REFILLS): - logger.info(f"Can't provide a heal. You already have a heal in queue.") - self.heal_request_command = None - return + + for item in self.item_queue: + if item[0] == "hp refill": + self.heal_request_command = None + return pool = ctx.stored_data[f'EnergyLink{ctx.team}'] or 0 # Perform auto heals @@ -290,12 +290,7 @@ async def handle_item_queue(self, ctx): if current_hp[0] < max_hp[0]: snes_buffered_write(ctx, MMX_ENABLE_HP_REFILL, bytearray([0x02])) - if next_item[0] == "small hp refill": - snes_buffered_write(ctx, MMX_HP_REFILL_AMOUNT, bytearray([0x02])) - elif next_item[0] == "large hp refill": - snes_buffered_write(ctx, MMX_HP_REFILL_AMOUNT, bytearray([0x08])) - else: - snes_buffered_write(ctx, MMX_HP_REFILL_AMOUNT, bytearray([next_item[2]])) + snes_buffered_write(ctx, MMX_HP_REFILL_AMOUNT, bytearray([next_item[2]])) snes_buffered_write(ctx, MMX_RECEIVING_ITEM, bytearray([0x01])) else: # TODO: Sub Tank logic @@ -557,7 +552,7 @@ async def game_watcher(self, ctx): self.add_item_to_queue("boss access", item.item) elif item.item in refill_rom_data: - self.add_item_to_queue(refill_rom_data[item.item][0], item.item) + self.add_item_to_queue(refill_rom_data[item.item][0], item.item, refill_rom_data[item.item][1]) elif item.item == STARTING_ID: # Handle goal @@ -661,30 +656,51 @@ def cmd_pool(self): logger.info(f"Healing available: {pool:.2f}") -def cmd_heal(self, amount: str = ""): +def cmd_refill(self, refill_type: str = "", amount: str = ""): """ - Request healing from EnergyLink. + Request healing or weapon energy from EnergyLink. """ if self.ctx.game != "Mega Man X": - logger.warning("This command can only be used while playing Mega Man X3") + logger.warning("This command can only be used while playing Mega Man X") if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: logger.info(f"Must be connected to server and in game.") else: - if self.ctx.client_handler.heal_request_command is not None: - logger.info(f"You already placed a healing request.") - return - if amount: - try: - amount = int(amount) - except: - logger.info(f"You need to specify how much HP you will recover.") + refill_type = refill_type.lower() + if refill_type == "hp": + if self.ctx.client_handler.heal_request_command is not None: + logger.info(f"You already placed a healing request.") return - if amount > 16: - self.ctx.client_handler.heal_request_command = 16 - self.ctx.client_handler.heal_request_command = amount - logger.info(f"Requested {amount} HP from the healing pool.") - else: - logger.info(f"You need to specify how much HP you will request.") + if amount: + try: + amount = int(amount) + except: + logger.info(f"You need to specify how much HP you will recover.") + return + if amount <= 0: + logger.info(f"You need to specify how much HP you will recover.") + return + self.ctx.client_handler.heal_request_command = amount + logger.info(f"Requested {amount} HP from the energy pool.") + else: + logger.info(f"You need to specify how much HP you will request.") + elif refill_type == "weapon" or refill_type == "wpn": + if self.ctx.client_handler.heal_request_command is not None: + logger.info(f"You already placed a weapon refill request.") + return + if amount: + try: + amount = int(amount) + except: + logger.info(f"You need to specify how much Weapon Energy you will recover.") + return + if amount <= 0: + logger.info(f"You need to specify how much HP you will recover.") + return + self.ctx.client_handler.weapon_refill_request_command = amount + logger.info(f"Requested {amount} Weapon Energy from the energy pool.") + else: + logger.info(f"You need to specify how much Weapon Energy you will request.") + def cmd_autoheal(self): From bcd32bed70396d2fa63e6883cee5f92527fc8a97 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 17 May 2024 00:09:17 -0700 Subject: [PATCH 17/71] fix crash related to having odd hp values --- worlds/mmx/Rom.py | 6 +++--- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 3568 -> 3556 bytes 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index 0b7104b48c52..5b420c92705f 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -183,7 +183,9 @@ def patch_rom(world: World, patch: MMXProcedurePatch): if world.options.boss_randomize_hp != "off": adjust_boss_hp(world, patch) - + + patch.write_byte(0x014FF, world.options.starting_hp.value) + # Edit the ROM header from Utils import __version__ patch.name = bytearray(f'MMX1{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] @@ -208,8 +210,6 @@ def patch_rom(world: World, patch: MMXProcedurePatch): patch.write_byte(0x17FFEE, world.options.heart_tank_effectiveness.value) patch.write_byte(0x17FFEF, world.options.sigma_all_levels.value) - patch.write_byte(0x014FF, world.options.starting_hp.value) - patch.write_file("token_patch.bin", patch.get_token_binary()) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 226ea5390d2a23f1361b0f49a64e1bf5e34ac664..fee47ebbfcdc82431f7dab7dc001767bb550cd88 100644 GIT binary patch literal 3556 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 T)f{@U>m}bq1BVK8TjBgK3B4Z} From 5ac575d9b8e09193894a7b3e71e9289b3fd0035d Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 17 May 2024 23:10:01 -0700 Subject: [PATCH 18/71] Fix location groups --- worlds/mmx/Locations.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/worlds/mmx/Locations.py b/worlds/mmx/Locations.py index 1410b00a408a..9c2b465c18a3 100644 --- a/worlds/mmx/Locations.py +++ b/worlds/mmx/Locations.py @@ -128,14 +128,14 @@ def __init__(self, player: int, name: str = '', address: int = None, parent=None "Sub Tanks": {location for location in all_locations.keys() if "- Sub Tank" in location}, "Upgrade Capsules": {location for location in all_locations.keys() if "Capsule" in location}, "Intro Stage": {location for location in all_locations.keys() if "Intro Stage - " in location}, - "Armored Armadillo Stage": {location for location in all_locations.keys() if "Armored Armadillo Stage - " in location}, - "Chill Penguin Stage": {location for location in all_locations.keys() if "Chill Penguin Stage - " in location}, - "Spark Mandrill Stage": {location for location in all_locations.keys() if "Spark Mandrill Stage - " in location}, - "Launch Octopus Stage": {location for location in all_locations.keys() if "Launch Octopus Stage - " in location}, - "Boomer Kuwanger Stage": {location for location in all_locations.keys() if "Boomer Kuwanger Stage - " in location}, - "Sting Chameleon Stage": {location for location in all_locations.keys() if "Sting Chameleon Stage - " in location}, - "Storm Eagle Stage": {location for location in all_locations.keys() if "Storm Eagle Stage - " in location}, - "Flame Mammoth Stage": {location for location in all_locations.keys() if "Flame Mammoth Stage - " in location}, + "Armored Armadillo Stage": {location for location in all_locations.keys() if "Armored Armadillo - " in location}, + "Chill Penguin Stage": {location for location in all_locations.keys() if "Chill Penguin - " in location}, + "Spark Mandrill Stage": {location for location in all_locations.keys() if "Spark Mandrill - " in location}, + "Launch Octopus Stage": {location for location in all_locations.keys() if "Launch Octopus - " in location}, + "Boomer Kuwanger Stage": {location for location in all_locations.keys() if "Boomer Kuwanger - " in location}, + "Sting Chameleon Stage": {location for location in all_locations.keys() if "Sting Chameleon - " in location}, + "Storm Eagle Stage": {location for location in all_locations.keys() if "Storm Eagle - " in location}, + "Flame Mammoth Stage": {location for location in all_locations.keys() if "Flame Mammoth - " in location}, "Sigma's Fortress Stage 1": { LocationName.sigma_fortress_1_boomer_kuwanger, LocationName.sigma_fortress_1_bospider, From 796702be798dbfe1ef59a3b505c826f2670f8781 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Sun, 19 May 2024 00:10:09 -0700 Subject: [PATCH 19/71] Fix wolf sigma's crash on randomized hp --- worlds/mmx/Rom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index 5b420c92705f..b04f592bdbb9 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -88,7 +88,7 @@ "D-Rex": 0x440FD, "Velguarder": 0x148DF, "Sigma": 0x4467B, - "Wolf Sigma": 0x44B79, + "Wolf Sigma": 0x44B78, } class MMXProcedurePatch(APProcedurePatch, APTokenMixin): From 6cfdfdf223cc75e6acd0e11c2a4eca94cab93498 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Sun, 19 May 2024 00:10:30 -0700 Subject: [PATCH 20/71] I have no idea what I changed here but it was stable enough so I commit it --- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 3556 -> 3621 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index fee47ebbfcdc82431f7dab7dc001767bb550cd88..9fde54d0690a5f36229820df37f48b6bb72d2ad8 100644 GIT binary patch literal 3621 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 literal 3556 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 Date: Sun, 19 May 2024 23:56:03 -0700 Subject: [PATCH 21/71] Rewrite client to have the same features as X3's client --- worlds/mmx/Client.py | 293 ++++++++++++++++++++++++++++++------------- 1 file changed, 205 insertions(+), 88 deletions(-) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index 54b722fa5093..b27c92ea71b2 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -45,11 +45,13 @@ MMX_ENERGY_LINK_PACKET = WRAM_START + 0x1EE09 MMX_VALIDATION_CHECK = WRAM_START + 0x1EE13 -MMX_RECEIVING_ITEM = WRAM_START + 0x1EE15 -MMX_ENABLE_HEART_TANK = WRAM_START + 0x1EE0B -MMX_ENABLE_HP_REFILL = WRAM_START + 0x1EE0F -MMX_HP_REFILL_AMOUNT = WRAM_START + 0x1EE10 -MMX_ENABLE_GIVE_1UP = WRAM_START + 0x1EE12 +MMX_RECEIVING_ITEM = WRAM_START + 0x1EE15 +MMX_ENABLE_HEART_TANK = WRAM_START + 0x1EE0B +MMX_ENABLE_HP_REFILL = WRAM_START + 0x1EE0F +MMX_HP_REFILL_AMOUNT = WRAM_START + 0x1EE10 +MMX_ENABLE_GIVE_1UP = WRAM_START + 0x1EE12 +MMX_ENABLE_WEAPON_REFILL = WRAM_START + 0x1EE1A +MMX_WEAPON_REFILL_AMOUNT = WRAM_START + 0x1EE1B MMX_PAUSE_STATE = WRAM_START + 0x01F24 MMX_CAN_MOVE = WRAM_START + 0x01F13 @@ -59,20 +61,14 @@ MMX_DEATH_LINK_ACTIVE = ROM_START + 0x17FFE9 MMX_JAMMED_BUSTER_ACTIVE = ROM_START + 0x17FFEA -HP_EXCHANGE_RATE = 500000000 +EXCHANGE_RATE = 500000000 STARTING_ID = 0xBE0800 MMX_ROMHASH_START = 0x7FC0 ROMHASH_SIZE = 0x15 -PICKUP_ITEMS = ["small hp refill", "large hp refill", "1up", "hp refill"] -HP_REFILLS = ["small hp refill", "large hp refill", "hp refill"] -BOSS_MEDAL = [0xFF, 0xFF, 0x02, 0xFF, 0x0C, 0x0A, 0x00, 0xFF, - 0x04, 0x06, 0x0E, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - ] +PICKUP_ITEMS = ["1up", "hp refill", "weapon refill"] class MMXSNIClient(SNIClient): game = "Mega Man X" @@ -85,6 +81,8 @@ def __init__(self): self.auto_heal = False self.energy_link_enabled = False self.heal_request_command = None + self.weapon_refill_request_command = None + self.trade_request = None self.item_queue = [] @@ -129,6 +127,8 @@ async def validate_rom(self, ctx): ctx.command_processor.commands.pop("pool") if "autoheal" in ctx.command_processor.commands: ctx.command_processor.commands.pop("autoheal") + if "heal" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("heal") if "refill" in ctx.command_processor.commands: ctx.command_processor.commands.pop("refill") return False @@ -143,8 +143,12 @@ async def validate_rom(self, ctx): ctx.command_processor.commands["pool"] = cmd_pool if "autoheal" not in ctx.command_processor.commands: ctx.command_processor.commands["autoheal"] = cmd_autoheal + if "refill" not in ctx.command_processor.commands: + ctx.command_processor.commands["heal"] = cmd_heal if "refill" not in ctx.command_processor.commands: ctx.command_processor.commands["refill"] = cmd_refill + if "trade" not in ctx.command_processor.commands: + ctx.command_processor.commands["trade"] = cmd_trade death_link = await snes_read(ctx, MMX_DEATH_LINK_ACTIVE, 1) if death_link[0]: @@ -153,8 +157,51 @@ async def validate_rom(self, ctx): ctx.rom = rom_name return True - - + + async def handle_hp_trade(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + + validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) + if validation is None: + return + validation = validation[0] | (validation[1] << 8) + if validation != 0xDEAD: + return + + # Can only process trades during the pause state + receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) + menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) + gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) + if menu_state[0] != 0x04 or \ + gameplay_state[0] != 0x04 or \ + receiving_item[0] != 0x00: + return + + pause_state = await snes_read(ctx, MMX_PAUSE_STATE, 0x1) + if pause_state[0] == 0x00: + return + + for item in self.item_queue: + if item[0] == "weapon refill": + self.trade_request = None + logger.info(f"You already have a Weapon Energy request pending to be received.") + return + + # Can trade HP -> WPN if HP is above 1 + current_hp = await snes_read(ctx, MMX_CURRENT_HP, 0x1) + if current_hp[0] > 0x01: + max_trade = current_hp[0] - 1 + set_trade = self.trade_request if self.trade_request <= max_trade else max_trade + self.add_item_to_queue("weapon refill", None, set_trade) + new_hp = current_hp[0] - set_trade + snes_buffered_write(ctx, MMX_CURRENT_HP, bytearray([new_hp])) + await snes_flush_writes(ctx) + self.trade_request = None + logger.info(f"Traded {set_trade} HP for {set_trade} Weapon Energy.") + else: + logger.info("Couldn't process trade. HP is too low.") + + async def handle_energy_link(self, ctx): from SNIClient import snes_buffered_write, snes_flush_writes, snes_read @@ -163,15 +210,15 @@ async def handle_energy_link(self, ctx): if energy_packet is None: return energy_packet_raw = energy_packet[0] | (energy_packet[1] << 8) - energy_packet = (energy_packet_raw * HP_EXCHANGE_RATE) >> 4 + energy_packet = (energy_packet_raw * EXCHANGE_RATE) >> 4 if energy_packet != 0: await ctx.send_msgs([{ "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": [{"operation": "add", "value": energy_packet}, {"operation": "max", "value": 0}], }]) - pool = ((ctx.stored_data[f'EnergyLink{ctx.team}'] or 0) / HP_EXCHANGE_RATE) + (energy_packet_raw / 16) - logger.info(f"Deposited {energy_packet_raw / 16:.2f} into the healing pool. Healing available: {pool:.2f}") + pool = ((ctx.stored_data[f'EnergyLink{ctx.team}'] or 0) / EXCHANGE_RATE) + (energy_packet_raw / 16) + logger.info(f"Deposited {energy_packet_raw / 16:.2f} into the energy pool. Energy available: {pool:.2f}") snes_buffered_write(ctx, MMX_ENERGY_LINK_PACKET, bytearray([0x00, 0x00])) await snes_flush_writes(ctx) @@ -198,43 +245,71 @@ async def handle_energy_link(self, ctx): can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ receiving_item[0] != 0x00: return - + + skip_hp = False + skip_weapon = False for item in self.item_queue: if item[0] == "hp refill": + skip_hp = True self.heal_request_command = None - return + elif item[0] == "weapon refill": + skip_weapon = True + self.weapon_refill_request_command = None pool = ctx.stored_data[f'EnergyLink{ctx.team}'] or 0 - # Perform auto heals - if self.auto_heal: - if self.heal_request_command is None: - if pool < HP_EXCHANGE_RATE: + if not skip_hp: + # Perform auto heals + if self.auto_heal: + if self.heal_request_command is None: + if pool < EXCHANGE_RATE: + return + current_hp = await snes_read(ctx, MMX_CURRENT_HP, 0x1) + max_hp = await snes_read(ctx, MMX_MAX_HP, 0x1) + if max_hp[0] > current_hp[0]: + self.heal_request_command = max_hp[0] - current_hp[0] + + # Handle heal requests + if self.heal_request_command: + heal_needed = self.heal_request_command + heal_needed_rate = heal_needed * EXCHANGE_RATE + if pool < EXCHANGE_RATE: + logger.info(f"There's not enough Energy for your request ({heal_needed}). Energy available: {pool / EXCHANGE_RATE:.2f}") + self.heal_request_command = None return - current_hp = await snes_read(ctx, MMX_CURRENT_HP, 0x1) - max_hp = await snes_read(ctx, MMX_MAX_HP, 0x1) - if max_hp[0] > current_hp[0]: - self.heal_request_command = max_hp[0] - current_hp[0] - - # Handle manual heal requests - if self.heal_request_command: - heal_needed = self.heal_request_command - heal_needed_rate = heal_needed * HP_EXCHANGE_RATE - if pool < HP_EXCHANGE_RATE: - logger.info(f"There's not enough healing for your request ({heal_needed}). Healing available: {pool / HP_EXCHANGE_RATE:.2f}") + elif pool < heal_needed_rate: + heal_needed = int(pool / EXCHANGE_RATE) + heal_needed_rate = heal_needed * EXCHANGE_RATE + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": + [{"operation": "add", "value": -heal_needed_rate}, + {"operation": "max", "value": 0}], + }]) + self.add_item_to_queue("hp refill", None, self.heal_request_command) + pool = (pool / EXCHANGE_RATE) - heal_needed + logger.info(f"Healed by {heal_needed}. Energy available: {pool:.2f}") self.heal_request_command = None - return - elif pool < heal_needed_rate: - heal_needed = int(pool / HP_EXCHANGE_RATE) - heal_needed_rate = heal_needed * HP_EXCHANGE_RATE - await ctx.send_msgs([{ - "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": - [{"operation": "add", "value": -heal_needed_rate}, - {"operation": "max", "value": 0}], - }]) - self.add_item_to_queue("hp refill", None, self.heal_request_command) - pool = (pool / HP_EXCHANGE_RATE) - heal_needed - logger.info(f"Healed by {heal_needed}. Healing available: {pool:.2f}") - self.heal_request_command = None + + if not skip_weapon: + # Handle weapon refill requests + if self.weapon_refill_request_command: + heal_needed = self.weapon_refill_request_command + heal_needed_rate = heal_needed * EXCHANGE_RATE + if pool < EXCHANGE_RATE: + logger.info(f"There's not enough Energy for your request ({heal_needed}). Energy available: {pool / EXCHANGE_RATE:.2f}") + self.weapon_refill_request_command = None + return + elif pool < heal_needed_rate: + heal_needed = int(pool / EXCHANGE_RATE) + heal_needed_rate = heal_needed * EXCHANGE_RATE + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": + [{"operation": "add", "value": -heal_needed_rate}, + {"operation": "max", "value": 0}], + }]) + self.add_item_to_queue("weapon refill", None, self.weapon_refill_request_command) + pool = (pool / EXCHANGE_RATE) - heal_needed + logger.info(f"Refilled current weapon by {heal_needed}. Energy available: {pool:.2f}") + self.weapon_refill_request_command = None def add_item_to_queue(self, item_type, item_id, item_additional = None): @@ -296,6 +371,11 @@ async def handle_item_queue(self, ctx): # TODO: Sub Tank logic self.item_queue.append(backup_item) + elif next_item[0] == "weapon refill": + snes_buffered_write(ctx, MMX_ENABLE_WEAPON_REFILL, bytearray([0x02])) + snes_buffered_write(ctx, MMX_WEAPON_REFILL_AMOUNT, bytearray([next_item[2]])) + snes_buffered_write(ctx, MMX_RECEIVING_ITEM, bytearray([0x01])) + elif next_item[0] == "1up": life_count = await snes_read(ctx, MMX_LIFE_COUNT, 0x1) if life_count[0] < 9: @@ -421,6 +501,9 @@ async def game_watcher(self, ctx): currently_dead = gameplay_state[0] == 0x06 await ctx.handle_deathlink_state(currently_dead) + if self.trade_request is not None: + await self.handle_hp_trade(ctx) + await self.handle_item_queue(ctx) # This is going to be rewritten whenever SNIClient supports on_package @@ -645,70 +728,77 @@ async def game_watcher(self, ctx): def cmd_pool(self): """ - Check how much healing is in the pool. + Check how much energy is in the pool. """ if self.ctx.game != "Mega Man X": - logger.warning("This command can only be used while playing Mega Man X3") + logger.warning("This command can only be used while playing Mega Man X") if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: logger.info(f"Must be connected to server and in game.") else: - pool = (self.ctx.stored_data[f'EnergyLink{self.ctx.team}'] or 0) / HP_EXCHANGE_RATE - logger.info(f"Healing available: {pool:.2f}") + pool = (self.ctx.stored_data[f'EnergyLink{self.ctx.team}'] or 0) / EXCHANGE_RATE + logger.info(f"Energy available: {pool:.2f}") -def cmd_refill(self, refill_type: str = "", amount: str = ""): +def cmd_heal(self, amount: str = ""): """ - Request healing or weapon energy from EnergyLink. + Request healing from EnergyLink. """ if self.ctx.game != "Mega Man X": logger.warning("This command can only be used while playing Mega Man X") if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: logger.info(f"Must be connected to server and in game.") else: - refill_type = refill_type.lower() - if refill_type == "hp": - if self.ctx.client_handler.heal_request_command is not None: - logger.info(f"You already placed a healing request.") + if self.ctx.client_handler.heal_request_command is not None: + logger.info(f"You already placed a healing request.") + return + if amount: + try: + amount = int(amount) + except: + logger.info(f"You need to specify how much HP you will recover.") return - if amount: - try: - amount = int(amount) - except: - logger.info(f"You need to specify how much HP you will recover.") - return - if amount <= 0: - logger.info(f"You need to specify how much HP you will recover.") - return - self.ctx.client_handler.heal_request_command = amount - logger.info(f"Requested {amount} HP from the energy pool.") - else: - logger.info(f"You need to specify how much HP you will request.") - elif refill_type == "weapon" or refill_type == "wpn": - if self.ctx.client_handler.heal_request_command is not None: - logger.info(f"You already placed a weapon refill request.") + if amount <= 0: + logger.info(f"You need to specify how much HP you will recover.") return - if amount: - try: - amount = int(amount) - except: - logger.info(f"You need to specify how much Weapon Energy you will recover.") - return - if amount <= 0: - logger.info(f"You need to specify how much HP you will recover.") - return - self.ctx.client_handler.weapon_refill_request_command = amount - logger.info(f"Requested {amount} Weapon Energy from the energy pool.") - else: - logger.info(f"You need to specify how much Weapon Energy you will request.") + self.ctx.client_handler.heal_request_command = amount + logger.info(f"Requested {amount} HP from the energy pool.") + else: + logger.info(f"You need to specify how much HP you will request.") +def cmd_refill(self, amount: str = ""): + """ + Request weapon energy from EnergyLink. + """ + if self.ctx.game != "Mega Man X": + logger.warning("This command can only be used while playing Mega Man X") + if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: + logger.info(f"Must be connected to server and in game.") + else: + if self.ctx.client_handler.weapon_refill_request_command is not None: + logger.info(f"You already placed a weapon refill request.") + return + if amount: + try: + amount = int(amount) + except: + logger.info(f"You need to specify how much Weapon Energy you will recover.") + return + if amount <= 0: + logger.info(f"You need to specify how much Weapon Energy you will recover.") + return + self.ctx.client_handler.weapon_refill_request_command = amount + logger.info(f"Requested {amount} Weapon Energy from the energy pool.") + else: + logger.info(f"You need to specify how much Weapon Energy you will request.") + def cmd_autoheal(self): """ Enable auto heal from EnergyLink. """ if self.ctx.game != "Mega Man X": - logger.warning("This command can only be used while playing Mega Man X3") + logger.warning("This command can only be used while playing Mega Man X") if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: logger.info(f"Must be connected to server and in game.") else: @@ -718,3 +808,30 @@ def cmd_autoheal(self): else: self.ctx.client_handler.auto_heal = True logger.info(f"Auto healing enabled.") + + +def cmd_trade(self, amount: str = ""): + """ + Trades HP to Weapon Energy. 1:1 ratio. + """ + if self.ctx.game != "Mega Man X": + logger.warning("This command can only be used while playing Mega Man X") + if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: + logger.info(f"Must be connected to server and in game.") + else: + if self.ctx.client_handler.trade_request is not None: + logger.info(f"You already placed a weapon refill request.") + return + if amount: + try: + amount = int(amount) + except: + logger.info(f"You need to specify how much Weapon Energy you will recover.") + return + if amount <= 0: + logger.info(f"You need to specify how much Weapon Energy you will recover.") + return + self.ctx.client_handler.trade_request = amount + logger.info(f"Set up trade for {amount} Weapon Energy. Pause the game to process the trade.") + else: + logger.info(f"You need to specify how much Weapon Energy you will request.") From ed5e671fd675d0b840c2f3e103ddbc108e627d08 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Sun, 19 May 2024 23:56:15 -0700 Subject: [PATCH 22/71] A bajillion of changes to the basepatch --- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 3621 -> 4702 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 9fde54d0690a5f36229820df37f48b6bb72d2ad8..3b44ca412a2bb64a63fed11641be4e977b069857 100644 GIT binary patch literal 4702 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 From c604a5653f469e835b2814d56f8848d18c49ed40 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Sun, 19 May 2024 23:56:52 -0700 Subject: [PATCH 23/71] Adds hadouken into the item pool --- worlds/mmx/Items.py | 9 ++++++--- worlds/mmx/Options.py | 34 +++++++++++++++++++++------------- worlds/mmx/__init__.py | 3 +++ 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/worlds/mmx/Items.py b/worlds/mmx/Items.py index 9127c6b8b48d..cadbd846d410 100644 --- a/worlds/mmx/Items.py +++ b/worlds/mmx/Items.py @@ -1,9 +1,9 @@ import typing from BaseClasses import Item, ItemClassification +from worlds.AutoWorld import World from .Names import ItemName - class ItemData(typing.NamedTuple): code: typing.Optional[int] progression: bool @@ -42,7 +42,10 @@ class MMXItem(Item): ItemName.chameleon_sting: ItemData(STARTING_ID + 0x0010, True), ItemName.storm_tornado: ItemData(STARTING_ID + 0x0011, True), ItemName.fire_wave: ItemData(STARTING_ID + 0x0012, True), - #ItemName.hadouken: ItemData(STARTING_ID + 0x001A, True) +} + +special_weapons = { + ItemName.hadouken: ItemData(STARTING_ID + 0x001A, True) } tanks_table = { @@ -93,7 +96,6 @@ class MMXItem(Item): } } -# Complete item table. item_table = { **event_table, **access_codes_table, @@ -101,6 +103,7 @@ class MMXItem(Item): **upgrade_table, **tanks_table, **junk_table, + **special_weapons, } lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} \ No newline at end of file diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index 8bbce5f8ff2d..5701f1a2ea9d 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -103,6 +103,25 @@ class JammedBuster(Toggle): """ display_name = "Jammed Buster" +class HadoukenInPool(DefaultOnToggle): + """ + Adds Hadouken to the item pool. + Hadouken will deal the current HP as damage and half the current HP on strict weakness settings. + """ + display_name = "Hadouken In Pool" + +class EarlyLegs(Toggle): + """ + Places the Legs Upgrade item in sphere 1. + """ + display_name = "Early Legs" + +class PickupSanity(Toggle): + """ + Whether collecting freestanding 1ups, HP and Weapon Energy capsules will grant a check. + """ + display_name = "Pickupsanity" + class LogicBossWeakness(DefaultOnToggle): """ Every main boss will logically expect you to have its weakness. @@ -121,18 +140,6 @@ class LogicChargedShotgunIce(Toggle): """ display_name = "Charged Shotgun Ice Logic" -class EarlyLegs(Toggle): - """ - Places the Legs Upgrade item in sphere 1. - """ - display_name = "Early Legs" - -class PickupSanity(Toggle): - """ - Whether collecting freestanding 1ups, HP and Weapon Energy capsules will grant a check. - """ - display_name = "Pickupsanity" - class FortressBundleUnlock(Toggle): """ Whether to unlock Sigma's Fortress 1-3 levels as a group or not. @@ -222,11 +229,12 @@ class MMXOptions(PerGameCommonOptions): boss_weakness_strictness: BossWeaknessStrictness boss_randomize_hp: BossRandomizedHP jammed_buster: JammedBuster + hadouken_in_pool: HadoukenInPool pickupsanity: PickupSanity + early_legs: EarlyLegs logic_boss_weakness: LogicBossWeakness logic_leg_sigma: LogicLegSigma logic_charged_shotgun_ice: LogicChargedShotgunIce - early_legs: EarlyLegs sigma_all_levels: FortressBundleUnlock sigma_open: SigmaOpen sigma_medal_count: SigmaMedalCount diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index e36d9b960042..2ec4200626ce 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -139,6 +139,9 @@ def create_regions(self) -> None: itempool += [self.create_item(ItemName.fire_wave)] itempool += [self.create_item(ItemName.boomerang_cutter)] + if self.options.hadouken_in_pool: + itempool += [self.create_item(ItemName.hadouken, ItemClassification.useful)] + # Add upgrades into the pool if self.options.sigma_open == "armor_upgrades" or (self.options.sigma_open == "all" and self.options.sigma_upgrade_count.value > 0): itempool += [self.create_item(ItemName.body)] From 9cb953a6b137045ff5d2d6145013496e631dcc54 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Sun, 19 May 2024 23:57:14 -0700 Subject: [PATCH 24/71] Remove Charged Fire Wave from being an option and balanced weakness damages --- worlds/mmx/Weaknesses.py | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/worlds/mmx/Weaknesses.py b/worlds/mmx/Weaknesses.py index 4a16763167a4..138504295fd5 100644 --- a/worlds/mmx/Weaknesses.py +++ b/worlds/mmx/Weaknesses.py @@ -169,54 +169,37 @@ boss_excluded_weapons = { "Sting Chameleon": [ - "Charged Fire Wave", ], "Storm Eagle": [ - "Charged Fire Wave", ], "Flame Mammoth": [ - "Charged Fire Wave", ], "Chill Penguin": [ - "Charged Fire Wave", ], "Spark Mandrill": [ - "Charged Fire Wave", ], "Armored Armadillo": [ - "Charged Fire Wave", ], "Launch Octopus": [ "Fire Wave", - "Charged Fire Wave", ], "Boomer Kuwanger": [ - "Charged Fire Wave", ], "Thunder Slimer": [ - "Charged Fire Wave", ], "Vile": [ - "Charged Fire Wave", ], "Bospider": [ - "Charged Fire Wave", ], "Rangda Bangda": [ - "Charged Fire Wave", ], "D-Rex": [ - "Charged Fire Wave", ], "Velguarder": [ - "Charged Fire Wave", ], "Sigma": [ - "Lemon (Dash)", - "Charged Fire Wave", ], "Wolf Sigma": [ - "Charged Fire Wave", ], } @@ -237,11 +220,11 @@ ], "Rolling Shield": [ [[ItemName.rolling_shield], 0x09, WEAKNESS_UNCHARGED_DMG], - [[ItemName.rolling_shield], 0x12, WEAKNESS_CHARGED_DMG+1], + [[ItemName.rolling_shield], 0x12, WEAKNESS_CHARGED_DMG+2], ], "Fire Wave": [ [[ItemName.fire_wave], 0x0A, WEAKNESS_UNCHARGED_DMG], - [[ItemName.fire_wave], 0x13, WEAKNESS_CHARGED_DMG+4], + [[ItemName.fire_wave], 0x13, WEAKNESS_CHARGED_DMG+3], ], "Storm Tornado": [ [[ItemName.storm_tornado], 0x0B, WEAKNESS_UNCHARGED_DMG], @@ -257,7 +240,7 @@ ], "Shotgun Ice": [ [[ItemName.shotgun_ice], 0x0E, WEAKNESS_CHARGED_DMG], - [[ItemName.shotgun_ice], 0x17, WEAKNESS_CHARGED_DMG], + [[ItemName.shotgun_ice], 0x17, WEAKNESS_CHARGED_DMG+3], ], } @@ -291,13 +274,11 @@ [[ItemName.rolling_shield], 0x09, WEAKNESS_UNCHARGED_DMG+1], ], "Charged Rolling Shield": [ - [["Check Charge 2", ItemName.rolling_shield], 0x12, WEAKNESS_CHARGED_DMG], + [["Check Charge 2", ItemName.rolling_shield], 0x12, WEAKNESS_CHARGED_DMG+2], ], "Fire Wave": [ [[ItemName.fire_wave], 0x0A, WEAKNESS_UNCHARGED_DMG], - ], - "Charged Fire Wave": [ - [["Check Charge 2", ItemName.fire_wave], 0x13, WEAKNESS_CHARGED_DMG+4], + [[ItemName.fire_wave], 0x13, WEAKNESS_CHARGED_DMG+3], ], "Storm Tornado": [ [[ItemName.storm_tornado], 0x0B, WEAKNESS_UNCHARGED_DMG], @@ -321,7 +302,7 @@ [[ItemName.shotgun_ice], 0x0E, WEAKNESS_UNCHARGED_DMG], ], "Charged Shotgun Ice": [ - [["Check Charge 2", ItemName.shotgun_ice], 0x17, WEAKNESS_CHARGED_DMG], + [["Check Charge 2", ItemName.shotgun_ice], 0x17, WEAKNESS_CHARGED_DMG+3], ], } From c89f636af19b6b705c8fc2f49c537761cdc2f130 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Sun, 19 May 2024 23:57:30 -0700 Subject: [PATCH 25/71] Fixed some logic issues with sigma_all_levels option --- worlds/mmx/Regions.py | 10 ++++++++-- worlds/mmx/Rules.py | 20 ++++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/worlds/mmx/Regions.py b/worlds/mmx/Regions.py index fbd52584d5b5..29791b10c216 100644 --- a/worlds/mmx/Regions.py +++ b/worlds/mmx/Regions.py @@ -376,14 +376,12 @@ def connect_regions(world: World): connect(world, RegionName.sigma_fortress_1_vertical, RegionName.sigma_fortress_1_rematch_1) connect(world, RegionName.sigma_fortress_1_rematch_1, RegionName.sigma_fortress_1_boss) - connect(world, RegionName.sigma_fortress_1_boss, RegionName.sigma_fortress_2) connect(world, RegionName.sigma_fortress_2, RegionName.sigma_fortress_2_start) connect(world, RegionName.sigma_fortress_2_start, RegionName.sigma_fortress_2_rematch_1) connect(world, RegionName.sigma_fortress_2_rematch_1, RegionName.sigma_fortress_2_ride) connect(world, RegionName.sigma_fortress_2_ride, RegionName.sigma_fortress_2_rematch_2) connect(world, RegionName.sigma_fortress_2_rematch_2, RegionName.sigma_fortress_2_boss) - connect(world, RegionName.sigma_fortress_2_boss, RegionName.sigma_fortress_3) connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_rematch_1) connect(world, RegionName.sigma_fortress_3_rematch_1, RegionName.sigma_fortress_3_rematch_2) connect(world, RegionName.sigma_fortress_3_rematch_2, RegionName.sigma_fortress_3_rematch_3) @@ -395,6 +393,14 @@ def connect_regions(world: World): connect(world, RegionName.sigma_fortress_4, RegionName.sigma_fortress_4_dog) connect(world, RegionName.sigma_fortress_4_dog, RegionName.sigma_fortress_4_sigma) + if world.options.sigma_all_levels: + connect(world, RegionName.sigma_fortress, RegionName.sigma_fortress_2) + connect(world, RegionName.sigma_fortress, RegionName.sigma_fortress_3) + else: + connect(world, RegionName.sigma_fortress_1_boss, RegionName.sigma_fortress_2) + connect(world, RegionName.sigma_fortress_2_boss, RegionName.sigma_fortress_3) + + def create_region(multiworld: MultiWorld, player: int, active_locations, name: str, locations=None): ret = Region(name, player, multiworld) diff --git a/worlds/mmx/Rules.py b/worlds/mmx/Rules.py index ff6f80f8df0e..b060bea02ecc 100644 --- a/worlds/mmx/Rules.py +++ b/worlds/mmx/Rules.py @@ -109,12 +109,20 @@ def set_rules(world: MMXWorld): add_rule(entrance, lambda state: state.has(ItemName.legs, player)) # Sigma Fortress level rules - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_1_boss} -> {RegionName.sigma_fortress_2}", player), - lambda state: state.has(EventName.sigma_fortress_1_clear, player)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_2_boss} -> {RegionName.sigma_fortress_3}", player), - lambda state: state.has(EventName.sigma_fortress_2_clear, player)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_boss} -> {RegionName.sigma_fortress_4}", player), - lambda state: state.has(EventName.sigma_fortress_3_clear, player)) + if world.options.sigma_all_levels: + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_boss} -> {RegionName.sigma_fortress_4}", player), + lambda state: ( + state.has(EventName.sigma_fortress_1_clear, player) and + state.has(EventName.sigma_fortress_2_clear, player) and + state.has(EventName.sigma_fortress_3_clear, player) + )) + else: + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_1_boss} -> {RegionName.sigma_fortress_2}", player), + lambda state: state.has(EventName.sigma_fortress_1_clear, player)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_2_boss} -> {RegionName.sigma_fortress_3}", player), + lambda state: state.has(EventName.sigma_fortress_2_clear, player)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_boss} -> {RegionName.sigma_fortress_4}", player), + lambda state: state.has(EventName.sigma_fortress_3_clear, player)) # Sigma rules add_rule(multiworld.get_location(LocationName.sigma_fortress_4_sigma, player), From 814c2c5bda53880ec4702333fb985b9c5f1131b7 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Sun, 19 May 2024 23:57:42 -0700 Subject: [PATCH 26/71] Misc fixes + hadouken --- worlds/mmx/Rom.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index b04f592bdbb9..4ed720264704 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -24,7 +24,7 @@ STARTING_ID + 0x000C: [0x1F92, 0xFF], STARTING_ID + 0x000F: [0x1F94, 0xFF], STARTING_ID + 0x000B: [0x1F96, 0xFF], - #STARTING_ID + 0x001A: [0x1F7E, 0x80], + STARTING_ID + 0x001A: [0x1F7E, 0x80], } upgrades_rom_data = { @@ -185,6 +185,7 @@ def patch_rom(world: World, patch: MMXProcedurePatch): adjust_boss_hp(world, patch) patch.write_byte(0x014FF, world.options.starting_hp.value) + patch.write_byte(0x01DDC, 0x7F) # Edit the ROM header from Utils import __version__ @@ -209,6 +210,7 @@ def patch_rom(world: World, patch: MMXProcedurePatch): patch.write_byte(0x17FFED, world.options.starting_hp.value) patch.write_byte(0x17FFEE, world.options.heart_tank_effectiveness.value) patch.write_byte(0x17FFEF, world.options.sigma_all_levels.value) + patch.write_byte(0x17FFF0, world.options.boss_weakness_strictness.value) patch.write_file("token_patch.bin", patch.get_token_binary()) From f5b64b72d486f2aa30c4eacde27137eea7478b3f Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Mon, 20 May 2024 23:03:56 -0700 Subject: [PATCH 27/71] Add weaknesses to hint data --- worlds/mmx/__init__.py | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index 2ec4200626ce..24abd7cd8725 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -16,6 +16,7 @@ from .Names import ItemName, LocationName, EventName from .Options import MMXOptions from .Client import MMXSNIClient +from .Levels import location_id_to_level_id from .Weaknesses import randomize_weaknesses, boss_weaknesses, weapon_id from .Rom import patch_rom, MMXProcedurePatch, HASH_US, HASH_LEGACY @@ -264,6 +265,58 @@ def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: spoiler_handle.writelines(f"{boss + ':':<30s}{weaknesses}\n") + def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]): + if not self.options.boss_weakness_rando: + return + + boss_to_id = { + 0x00: "Armored Armadillo", + 0x01: "Chill Penguin", + 0x02: "Spark Mandrill", + 0x03: "Launch Octopus", + 0x04: "Boomer Kuwanger", + 0x05: "Sting Chameleon", + 0x06: "Storm Eagle", + 0x07: "Flame Mammoth", + 0x08: "Bospider", + 0x09: "Vile", + 0x0A: "Boomer Kuwanger", + 0x0B: "Chill Penguin", + 0x0C: "Storm Eagle", + 0x0D: "Rangda Bangda", + 0x0E: "Armored Armadillo", + 0x0F: "Sting Chameleon", + 0x10: "Spark Mandrill", + 0x11: "Launch Octopus", + 0x12: "Flame Mammoth", + 0x1E: "D-Rex", + 0x13: "Velguarder", + 0x1F: "Sigma", + } + boss_weakness_hint_data = {} + for loc_name, level_data in location_id_to_level_id.items(): + if level_data[1] == 0x000: + boss_id = level_data[2] + if boss_id not in boss_to_id.keys(): + continue + boss = boss_to_id[boss_id] + data = self.boss_weaknesses[boss] + weaknesses = "" + for i in range(len(data)): + weaknesses += f"{weapon_id[data[i][1]]}, " + weaknesses = weaknesses[:-2] + if boss == "Sigma": + data = self.boss_weaknesses["Wolf Sigma"] + weaknesses += ". Wolf Sigma: " + for i in range(len(data)): + weaknesses += f"{weapon_id[data[i][1]]}, " + weaknesses = weaknesses[:-2] + location = self.multiworld.get_location(loc_name, self.player) + boss_weakness_hint_data[location.address] = weaknesses + + hint_data[self.player] = boss_weakness_hint_data + + def get_filler_item_name(self) -> str: return self.random.choice(list(junk_table.keys())) From a0a9020fe37ee65ae0be4702da424b78e0d34a2d Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Mon, 20 May 2024 23:04:17 -0700 Subject: [PATCH 28/71] I forgot what I changed from the basepatch, probably the asm source will be more detailed --- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 4702 -> 4668 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 3b44ca412a2bb64a63fed11641be4e977b069857..2833e9701392a10855bb55d4688095c03e39325c 100644 GIT binary patch literal 4668 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 E3uMbJvj6}9 literal 4702 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 Date: Thu, 23 May 2024 00:06:02 -0700 Subject: [PATCH 29/71] added option groups --- worlds/mmx/Options.py | 29 ++++++++++++++++++++++++++++- worlds/mmx/__init__.py | 4 +++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index 5701f1a2ea9d..6a43f1b6f6c8 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -1,7 +1,7 @@ from dataclasses import dataclass import typing -from Options import Choice, Range, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, StartInventoryPool +from Options import OptionGroup, Choice, Range, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, StartInventoryPool class EnergyLink(DefaultOnToggle): """ @@ -216,6 +216,33 @@ class SigmaSubTankCount(Range): range_end = 4 default = 4 +mmx_option_groups = [ + OptionGroup("Sigma Fortress Options", [ + SigmaOpen, + SigmaMedalCount, + SigmaWeaponCount, + SigmaArmorUpgradeCount, + SigmaHeartTankCount, + SigmaSubTankCount, + FortressBundleUnlock, + ]), + OptionGroup("Boss Weaknesses", [ + BossWeaknessRando, + BossWeaknessStrictness, + BossRandomizedHP, + ]), + OptionGroup("Gameplay Options", [ + StartingLifeCount, + StartingHP, + HeartTankEffectiveness, + JammedBuster, + ]), + OptionGroup("Logic", [ + LogicBossWeakness, + LogicLegSigma, + LogicChargedShotgunIce, + ]), +] @dataclass class MMXOptions(PerGameCommonOptions): diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index 24abd7cd8725..aacea3554477 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -14,7 +14,7 @@ from .Locations import MMXLocation, setup_locations, all_locations, location_groups from .Regions import create_regions, connect_regions from .Names import ItemName, LocationName, EventName -from .Options import MMXOptions +from .Options import MMXOptions, mmx_option_groups from .Client import MMXSNIClient from .Levels import location_id_to_level_id from .Weaknesses import randomize_weaknesses, boss_weaknesses, weapon_id @@ -44,6 +44,8 @@ class MMXWeb(WebWorld): tutorials = [setup_en] + option_groups = mmx_option_groups + class MMXWorld(World): """ From 37c9d0e35eb9f6297c996186d63dbf092620ae96 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 24 May 2024 00:18:47 -0700 Subject: [PATCH 30/71] Force Vile's intro check to actually check the intro level was completed --- worlds/mmx/Client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index b27c92ea71b2..156d240b370e 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -531,6 +531,7 @@ async def game_watcher(self, ctx): collected_pickups_data = await snes_read(ctx, MMX_COLLECTED_PICKUPS, 0x20) collected_pickups = list(collected_pickups_data) pickupsanity_enabled = await snes_read(ctx, MMX_PICKUPSANITY_ACTIVE, 0x1) + completed_intro_level = await snes_read(ctx, WRAM_START + 0x01F9B, 0x1) new_checks = [] for loc_name, data in location_id_to_level_id.items(): loc_id = AutoWorldRegister.world_types[ctx.game].location_name_to_id[loc_name] @@ -567,7 +568,10 @@ async def game_watcher(self, ctx): new_checks.append(loc_id) elif internal_id == 0x007: # Intro - if game_state[0] == 0x02 and menu_state[0] == 0x00 and gameplay_state[0] == 0x01: + if game_state[0] == 0x02 and \ + menu_state[0] == 0x00 and \ + gameplay_state[0] == 0x01 and \ + completed_intro_level[0] == 0x04: new_checks.append(loc_id) elif internal_id == 0x020: # Pickups From e91f90b1559d1b8fcf2b1b54d8edf3a9b0baa568 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 24 May 2024 00:20:07 -0700 Subject: [PATCH 31/71] Transformed SigmaOpen intro a OptionSet and reworked slot_data output --- worlds/mmx/Options.py | 48 ++++++++++----------- worlds/mmx/Rom.py | 18 ++++++-- worlds/mmx/Rules.py | 29 ++++++------- worlds/mmx/__init__.py | 94 ++++++++++++++++++++++++------------------ 4 files changed, 104 insertions(+), 85 deletions(-) diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index 6a43f1b6f6c8..c482c8f8595d 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -1,7 +1,8 @@ from dataclasses import dataclass import typing -from Options import OptionGroup, Choice, Range, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, StartInventoryPool +#from Options import OptionGroup, Choice, Range, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, StartInventoryPool +from Options import Choice, Range, Toggle, DefaultOnToggle, OptionSet, DeathLink, PerGameCommonOptions, StartInventoryPool class EnergyLink(DefaultOnToggle): """ @@ -69,8 +70,6 @@ class BossWeaknessStrictness(Choice): weakness_and_buster: Only allow the weakness and buster to deal damage to the bosses weakness_and_upgraded_buster: Only allow the weakness and buster charge levels 3 & 4 to deal damage to the bosses only_weakness: Only the weakness will deal damage to the bosses - - Z-Saber damage output will be cut to 50%/37.5%/25% of its original damage according to the strictness setting. """ display_name = "Boss Weakness Strictness" option_not_strict = 0 @@ -147,29 +146,28 @@ class FortressBundleUnlock(Toggle): """ display_name = "Fortress Levels Bundle Unlock" -class SigmaOpen(Choice): - """ - Under what conditions will Sigma's Fortress open. - multiworld: Access will require an Access Code multiworld item, similar to the main stages. - medals: Access will be granted after collecting a certain amount of Maverick Medals. - weapons: Access will be granted after collecting a certain amount of weapons. - armor_upgrades: Access will be granted after collecting a certain amount of armor upgrades. - heart_tanks: Access will be granted after collecting a certain amount of Heart Tanks. - sub_tanks: Access will be granted after collecting a certain amount of Sub Tanks. - all: Access will be granted after collecting a certain amount of Medals, Weapons, Armor Upgrades - Heart Tanks and Sub Tanks. - Do not enable all on solo seeds without pickupsanity or sessions with very few items. - There's a big chance it'll cause an error. +class SigmaOpen(OptionSet): + """ + Under which conditions will Sigma's Fortress open. + If no options are selected a multiworld item granting access to the stage will be created. + + Medals: Consider Maverick medals to get access to the fortress. + Weapons: Consider weapons to get access to the fortress. + Armor Upgrades: Consider upgrades to get access to the fortress. + Heart Tanks: Consider heart tanks to get access to the fortress. + Sub Tanks: Consider sub tanks to get access to the fortress. """ display_name = "Sigma Fortress Rules" - option_multiworld = 0 - option_medals = 1 - option_weapons = 2 - option_armor_upgrades = 4 - option_heart_tanks = 8 - option_sub_tanks = 16 - option_all = 31 - default = 1 + valid_keys = { + "Medals", + "Weapons", + "Armor Upgrades", + "Heart Tanks", + "Sub Tanks", + } + default = { + "Medals", + } class SigmaMedalCount(Range): """ @@ -217,6 +215,7 @@ class SigmaSubTankCount(Range): default = 4 mmx_option_groups = [ + """ OptionGroup("Sigma Fortress Options", [ SigmaOpen, SigmaMedalCount, @@ -242,6 +241,7 @@ class SigmaSubTankCount(Range): LogicLegSigma, LogicChargedShotgunIce, ]), + """ ] @dataclass diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index 4ed720264704..b2b27ccea975 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -3,7 +3,6 @@ import Utils import hashlib import os -from typing import Optional, TYPE_CHECKING from pkgutil import get_data from worlds.AutoWorld import World @@ -178,8 +177,7 @@ def patch_rom(world: World, patch: MMXProcedurePatch): 0x20,0x42,0x59,0x20,0x4E,0x49,0x4E,0x54, 0x45,0x4E,0x44,0x4F,0x00])) - if world.options.boss_weakness_rando != "vanilla": - adjust_boss_damage_table(world, patch) + adjust_boss_damage_table(world, patch) if world.options.boss_randomize_hp != "off": adjust_boss_hp(world, patch) @@ -194,7 +192,19 @@ def patch_rom(world: World, patch: MMXProcedurePatch): patch.write_bytes(0x7FC0, patch.name) # Write options to the ROM - patch.write_byte(0x17FFE0, world.options.sigma_open.value) + value = 0 + sigma_open = world.options.sigma_open.value + if "Medals" in sigma_open: + value |= 0x01 + if "Weapons" in sigma_open: + value |= 0x02 + if "Armor Upgrades" in sigma_open: + value |= 0x04 + if "Heart Tanks" in sigma_open: + value |= 0x08 + if "Sub Tanks" in sigma_open: + value |= 0x10 + patch.write_byte(0x17FFE0, value) patch.write_byte(0x17FFE1, world.options.sigma_medal_count.value) patch.write_byte(0x17FFE2, world.options.sigma_weapon_count.value) patch.write_byte(0x17FFE3, world.options.sigma_upgrade_count.value) diff --git a/worlds/mmx/Rules.py b/worlds/mmx/Rules.py index b060bea02ecc..99b890355e7c 100644 --- a/worlds/mmx/Rules.py +++ b/worlds/mmx/Rules.py @@ -89,21 +89,22 @@ def set_rules(world: MMXWorld): lambda state: state.has(ItemName.stage_storm_eagle, player)) # Fortress entrance rules - fortress_open = world.options.sigma_open + fortress_open = world.options.sigma_open.value entrance = multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.sigma_fortress}", player) - if fortress_open == "multiworld": + if len(fortress_open) == 0: add_rule(entrance, lambda state: state.has(ItemName.stage_sigma_fortress, player)) - if fortress_open in ("medals", "all") and world.options.sigma_medal_count.value > 0: - add_rule(entrance, lambda state: state.has(ItemName.maverick_medal, player, world.options.sigma_medal_count.value)) - if fortress_open in ("weapons", "all") and world.options.sigma_weapon_count.value > 0: - add_rule(entrance, lambda state: state.has_group("Weapons", player, world.options.sigma_weapon_count.value)) - if fortress_open in ("armor_upgrades", "all") and world.options.sigma_upgrade_count.value > 0: - add_rule(entrance, lambda state: state.has_group("Armor Upgrades", player, world.options.sigma_upgrade_count.value)) - if fortress_open in ("heart_tanks", "all") and world.options.sigma_heart_tank_count.value > 0: - add_rule(entrance, lambda state: state.has(ItemName.heart_tank, player, world.options.sigma_heart_tank_count.value)) - if fortress_open in ("sub_tanks", "all") and world.options.sigma_sub_tank_count.value > 0: - add_rule(entrance, lambda state: state.has(ItemName.sub_tank, player, world.options.sigma_sub_tank_count.value)) + else: + if "Medals" in fortress_open and world.options.sigma_medal_count.value > 0: + add_rule(entrance, lambda state: state.has(ItemName.maverick_medal, player, world.options.sigma_medal_count.value)) + if "Weapons" in fortress_open and world.options.sigma_weapon_count.value > 0: + add_rule(entrance, lambda state: state.has_group("Weapons", player, world.options.sigma_weapon_count.value)) + if "Armor Upgrades" in fortress_open and world.options.sigma_upgrade_count.value > 0: + add_rule(entrance, lambda state: state.has_group("Armor Upgrades", player, world.options.sigma_upgrade_count.value)) + if "Heart Tanks" in fortress_open and world.options.sigma_heart_tank_count.value > 0: + add_rule(entrance, lambda state: state.has(ItemName.heart_tank, player, world.options.sigma_heart_tank_count.value)) + if "Sub Tanks" in fortress_open and world.options.sigma_sub_tank_count.value > 0: + add_rule(entrance, lambda state: state.has(ItemName.sub_tank, player, world.options.sigma_sub_tank_count.value)) if world.options.logic_leg_sigma.value: add_rule(entrance, lambda state: state.has(ItemName.legs, player)) @@ -234,10 +235,6 @@ def add_boss_weakness_logic(world: MMXWorld): multiworld = world.multiworld jammed_buster = world.options.jammed_buster.value - if world.options.boss_weakness_rando == "vanilla": - from .Weaknesses import boss_weaknesses - world.boss_weaknesses = boss_weaknesses - for boss, regions in bosses.items(): weaknesses = world.boss_weaknesses[boss] for weakness in weaknesses: diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index aacea3554477..792c4074cd92 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -17,7 +17,7 @@ from .Options import MMXOptions, mmx_option_groups from .Client import MMXSNIClient from .Levels import location_id_to_level_id -from .Weaknesses import randomize_weaknesses, boss_weaknesses, weapon_id +from .Weaknesses import handle_weaknesses, weapon_id from .Rom import patch_rom, MMXProcedurePatch, HASH_US, HASH_LEGACY class MMXSettings(settings.Group): @@ -44,7 +44,7 @@ class MMXWeb(WebWorld): tutorials = [setup_en] - option_groups = mmx_option_groups + #option_groups = mmx_option_groups class MMXWorld(World): @@ -108,36 +108,15 @@ def create_regions(self) -> None: else: itempool += [self.create_item(stage_list[i])] - if self.options.sigma_open == "multiworld": + if len(self.options.sigma_open.value) == 0: itempool += [self.create_item(ItemName.stage_sigma_fortress)] # Add weapons into the pool - if self.options.sigma_open == "weapons" or (self.options.sigma_open == "all" and self.options.sigma_weapon_count.value > 0): - itempool += [self.create_item(ItemName.electric_spark)] - itempool += [self.create_item(ItemName.homing_torpedo)] - itempool += [self.create_item(ItemName.storm_tornado)] - itempool += [self.create_item(ItemName.shotgun_ice)] - itempool += [self.create_item(ItemName.rolling_shield)] - else: - if self.options.logic_boss_weakness.value: - itempool += [self.create_item(ItemName.electric_spark)] - itempool += [self.create_item(ItemName.homing_torpedo)] - itempool += [self.create_item(ItemName.storm_tornado)] - else: - itempool += [self.create_item(ItemName.electric_spark, ItemClassification.useful)] - itempool += [self.create_item(ItemName.homing_torpedo, ItemClassification.useful)] - itempool += [self.create_item(ItemName.storm_tornado, ItemClassification.useful)] - - if self.options.logic_boss_weakness.value or self.options.logic_charged_shotgun_ice.value: - itempool += [self.create_item(ItemName.shotgun_ice)] - else: - itempool += [self.create_item(ItemName.shotgun_ice, ItemClassification.useful)] - - if self.options.logic_boss_weakness.value or self.options.pickupsanity.value: - itempool += [self.create_item(ItemName.rolling_shield)] - else: - itempool += [self.create_item(ItemName.rolling_shield, ItemClassification.useful)] - + itempool += [self.create_item(ItemName.electric_spark)] + itempool += [self.create_item(ItemName.homing_torpedo)] + itempool += [self.create_item(ItemName.storm_tornado)] + itempool += [self.create_item(ItemName.shotgun_ice)] + itempool += [self.create_item(ItemName.rolling_shield)] itempool += [self.create_item(ItemName.chameleon_sting)] itempool += [self.create_item(ItemName.fire_wave)] itempool += [self.create_item(ItemName.boomerang_cutter)] @@ -146,7 +125,8 @@ def create_regions(self) -> None: itempool += [self.create_item(ItemName.hadouken, ItemClassification.useful)] # Add upgrades into the pool - if self.options.sigma_open == "armor_upgrades" or (self.options.sigma_open == "all" and self.options.sigma_upgrade_count.value > 0): + sigma_open = self.options.sigma_open.value + if "Armor Upgrades" in sigma_open and self.options.sigma_upgrade_count.value > 0: itempool += [self.create_item(ItemName.body)] else: itempool += [self.create_item(ItemName.body, ItemClassification.useful)] @@ -157,7 +137,7 @@ def create_regions(self) -> None: itempool += [self.create_item(ItemName.legs)] # Add heart tanks into the pool - if self.options.sigma_open == "heart_tanks" or (self.options.sigma_open == "all" and self.options.sigma_heart_tank_count.value > 0): + if "Heart Tanks" in sigma_open and self.options.sigma_heart_tank_count.value > 0: i = self.options.sigma_heart_tank_count.value itempool += [self.create_item(ItemName.heart_tank) for _ in range(i)] if i != 8: @@ -167,7 +147,7 @@ def create_regions(self) -> None: itempool += [self.create_item(ItemName.heart_tank, ItemClassification.useful) for _ in range(8)] # Add sub tanks into the pool - if self.options.sigma_open == "sub_tanks" or (self.options.sigma_open == "all" and self.options.sigma_sub_tank_count.value > 0): + if "Sub Tanks" in sigma_open and self.options.sigma_sub_tank_count.value > 0: i = self.options.sigma_sub_tank_count.value itempool += [self.create_item(ItemName.sub_tank) for _ in range(i)] if i != 4: @@ -235,10 +215,43 @@ def set_rules(self): def fill_slot_data(self): slot_data = {} - for option_name in (attr.name for attr in dataclasses.fields(MMXOptions) - if attr not in dataclasses.fields(PerGameCommonOptions)): - option = getattr(self.options, option_name) - slot_data[option_name] = option.value + # Write options to slot_data + slot_data["boss_weakness_rando"] = self.options.boss_weakness_rando.value + slot_data["boss_weakness_strictness"] = self.options.boss_weakness_strictness.value + slot_data["pickupsanity"] = self.options.pickupsanity.value + slot_data["jammed_buster"] = self.options.jammed_buster.value + slot_data["hadouken_in_pool"] = self.options.hadouken_in_pool.value + slot_data["pickupsanity"] = self.options.pickupsanity.value + slot_data["logic_boss_weakness"] = self.options.logic_boss_weakness.value + slot_data["logic_leg_sigma"] = self.options.logic_leg_sigma.value + slot_data["logic_charged_shotgun_ice"] = self.options.logic_charged_shotgun_ice.value + slot_data["sigma_all_levels"] = self.options.sigma_all_levels.value + value = 0 + sigma_open = self.options.sigma_open.value + if "Medals" in sigma_open: + value |= 0x01 + if "Weapons" in sigma_open: + value |= 0x02 + if "Armor Upgrades" in sigma_open: + value |= 0x04 + if "Heart Tanks" in sigma_open: + value |= 0x08 + if "Sub Tanks" in sigma_open: + value |= 0x10 + slot_data["sigma_open"] = value + slot_data["sigma_medal_count"] = self.options.sigma_medal_count.value + slot_data["sigma_weapon_count"] = self.options.sigma_weapon_count.value + slot_data["sigma_upgrade_count"] = self.options.sigma_upgrade_count.value + slot_data["sigma_heart_tank_count"] = self.options.sigma_heart_tank_count.value + slot_data["sigma_sub_tank_count"] = self.options.sigma_sub_tank_count.value + + # Write boss weaknesses to slot_data + slot_data["boss_weaknesses"] = {} + for boss, entries in self.boss_weaknesses.items(): + slot_data["boss_weaknesses"][boss] = [] + for entry in entries: + slot_data["boss_weaknesses"][boss].append(entry[1]) + return slot_data @@ -246,11 +259,10 @@ def generate_early(self): if self.options.early_legs: self.multiworld.early_items[self.player][ItemName.legs] = 1 - if self.options.boss_weakness_rando != "vanilla": - self.boss_weaknesses = {} - self.boss_weakness_data = {} - randomize_weaknesses(self) - + self.boss_weaknesses = {} + self.boss_weakness_data = {} + handle_weaknesses(self) + early_stage = self.random.choice(list(item_groups["Access Codes"])) self.multiworld.local_early_items[self.player][early_stage] = 1 From 7084c9af739ba5d578c12755d22aeac35ae632de Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 24 May 2024 00:20:35 -0700 Subject: [PATCH 32/71] Reworked weaknesses code to allow the vanilla option use this code --- worlds/mmx/Weaknesses.py | 122 +++++++++++++++++++++++++++++---------- 1 file changed, 90 insertions(+), 32 deletions(-) diff --git a/worlds/mmx/Weaknesses.py b/worlds/mmx/Weaknesses.py index 138504295fd5..998414a60fb4 100644 --- a/worlds/mmx/Weaknesses.py +++ b/worlds/mmx/Weaknesses.py @@ -1,27 +1,79 @@ from .Names import ItemName -boss_weaknesses = { - "Sting Chameleon": [[[ItemName.boomerang_cutter]]], - "Storm Eagle": [[[ItemName.chameleon_sting]]], - "Flame Mammoth": [[[ItemName.storm_tornado]]], - "Chill Penguin": [[[ItemName.fire_wave]]], - "Spark Mandrill": [[[ItemName.shotgun_ice]]], - "Armored Armadillo": [[[ItemName.electric_spark]]], - "Launch Octopus": [[[ItemName.rolling_shield]]], - "Boomer Kuwanger": [[[ItemName.homing_torpedo]]], - "Thunder Slimer": [[None]], - "Vile": [[[ItemName.homing_torpedo]]], - "Bospider": [[[ItemName.shotgun_ice]]], - "Rangda Bangda": [[[ItemName.chameleon_sting]]], - "D-Rex": [[[ItemName.boomerang_cutter]]], - "Velguarder": [[[ItemName.shotgun_ice]]], - "Sigma": [[[ItemName.electric_spark]]], - "Wolf Sigma": [[["Check Charge 2", ItemName.rolling_shield]]], -} - WEAKNESS_UNCHARGED_DMG = 0x03 WEAKNESS_CHARGED_DMG = 0x05 +boss_weaknesses = { + "Sting Chameleon": [ + [[ItemName.boomerang_cutter], 0x0D, WEAKNESS_CHARGED_DMG], + [[ItemName.boomerang_cutter], 0x16, WEAKNESS_CHARGED_DMG], + ], + "Storm Eagle": [ + [[ItemName.chameleon_sting], 0x08, WEAKNESS_UNCHARGED_DMG], + ], + "Flame Mammoth": [ + [[ItemName.storm_tornado], 0x0B, WEAKNESS_UNCHARGED_DMG], + [[ItemName.storm_tornado], 0x14, WEAKNESS_CHARGED_DMG], + ], + "Chill Penguin": [ + [[ItemName.fire_wave], 0x0A, WEAKNESS_UNCHARGED_DMG], + [[ItemName.fire_wave], 0x13, WEAKNESS_CHARGED_DMG+3], + ], + "Spark Mandrill": [ + [[ItemName.shotgun_ice], 0x0E, WEAKNESS_CHARGED_DMG], + [[ItemName.shotgun_ice], 0x17, WEAKNESS_CHARGED_DMG+3], + ], + "Armored Armadillo": [ + [[ItemName.electric_spark], 0x0C, WEAKNESS_UNCHARGED_DMG], + [[ItemName.electric_spark], 0x15, WEAKNESS_CHARGED_DMG], + ], + "Launch Octopus": [ + [[ItemName.rolling_shield], 0x09, WEAKNESS_UNCHARGED_DMG], + [[ItemName.rolling_shield], 0x12, WEAKNESS_CHARGED_DMG+2], + ], + "Boomer Kuwanger": [ + [[ItemName.homing_torpedo], 0x07, WEAKNESS_UNCHARGED_DMG], + [[ItemName.homing_torpedo], 0x10, WEAKNESS_CHARGED_DMG], + ], + "Thunder Slimer": [ + [None, 0x00, 0x02], + [None, 0x06, 0x04], + [None, 0x01, 0x03], + [None, 0x03, 0x04], + [None, 0x02, 0x05], + [None, 0x1D, 0x02], + ], + "Vile": [ + [[ItemName.homing_torpedo], 0x07, WEAKNESS_UNCHARGED_DMG], + [[ItemName.homing_torpedo], 0x10, WEAKNESS_CHARGED_DMG], + ], + "Bospider": [ + [[ItemName.shotgun_ice], 0x0E, WEAKNESS_CHARGED_DMG], + [[ItemName.shotgun_ice], 0x17, WEAKNESS_CHARGED_DMG+3], + ], + "Rangda Bangda": [ + [[ItemName.chameleon_sting], 0x08, WEAKNESS_UNCHARGED_DMG], + ], + "D-Rex": [ + [[ItemName.boomerang_cutter], 0x0D, WEAKNESS_CHARGED_DMG], + [[ItemName.boomerang_cutter], 0x16, WEAKNESS_CHARGED_DMG], + ], + "Velguarder": [ + [[ItemName.shotgun_ice], 0x0E, WEAKNESS_CHARGED_DMG], + [[ItemName.shotgun_ice], 0x17, WEAKNESS_CHARGED_DMG+3], + ], + "Sigma": [ + [[ItemName.electric_spark], 0x0C, WEAKNESS_UNCHARGED_DMG], + [[ItemName.electric_spark], 0x15, WEAKNESS_CHARGED_DMG], + ], + "Wolf Sigma": [ + [["Check Charge 2"], 0x02, 0x05], + [["Check Charge 2"], 0x1D, 0x02], + [[ItemName.rolling_shield], 0x09, WEAKNESS_UNCHARGED_DMG], + [[ItemName.rolling_shield], 0x12, WEAKNESS_CHARGED_DMG+2], + ], +} + weapon_id = { 0x00: "Lemon", 0x01: "Charged Shot (Level 1)", @@ -198,6 +250,7 @@ "Velguarder": [ ], "Sigma": [ + "Charged Rolling Shield", ], "Wolf Sigma": [ ], @@ -210,6 +263,7 @@ [None, 0x01, 0x03], [None, 0x03, 0x04], [None, 0x02, 0x05], + [None, 0x1D, 0x02], ], "Homing Torpedo": [ [[ItemName.homing_torpedo], 0x07, WEAKNESS_UNCHARGED_DMG], @@ -307,18 +361,17 @@ } -def randomize_weaknesses(world): +def handle_weaknesses(world): shuffle_type = world.options.boss_weakness_rando.value strictness_type = world.options.boss_weakness_strictness.value - weapon_list = weapons.keys() - if shuffle_type == 2 or shuffle_type == 3: - weapon_list = weapons_chaotic.keys() - weapon_list = list(weapon_list) + if shuffle_type != "vanilla": + weapon_list = weapons.keys() + if shuffle_type == 2 or shuffle_type == 3: + weapon_list = weapons_chaotic.keys() + weapon_list = list(weapon_list) for boss in boss_weaknesses.keys(): - if boss == "Dr. Doppler's Lab 2 Boss": - continue world.boss_weaknesses[boss] = [] if strictness_type == 0: @@ -330,10 +383,11 @@ def randomize_weaknesses(world): else: damage_table = damage_templates["Only Weakness"].copy() - copied_weapon_list = weapon_list.copy() - for weapon in boss_excluded_weapons[boss]: - if weapon in copied_weapon_list: - copied_weapon_list.remove(weapon) + if shuffle_type != "vanilla": + copied_weapon_list = weapon_list.copy() + for weapon in boss_excluded_weapons[boss]: + if weapon in copied_weapon_list: + copied_weapon_list.remove(weapon) if shuffle_type == 1: chosen_weapon = world.random.choice(copied_weapon_list) @@ -344,7 +398,6 @@ def randomize_weaknesses(world): damage_table[entry[1]] = damage world.boss_weakness_data[boss] = damage_table.copy() - elif shuffle_type == 2: for _ in range(2): chosen_weapon = world.random.choice(copied_weapon_list) @@ -356,7 +409,6 @@ def randomize_weaknesses(world): damage_table[entry[1]] = damage world.boss_weakness_data[boss] = damage_table.copy() - elif shuffle_type == 3: chosen_weapon = world.random.choice(copied_weapon_list) data = weapons_chaotic[chosen_weapon].copy() @@ -365,3 +417,9 @@ def randomize_weaknesses(world): damage = entry[2] damage_table[entry[1]] = damage world.boss_weakness_data[boss] = damage_table.copy() + + else: + for entry in boss_weaknesses[boss]: + world.boss_weaknesses[boss].append(entry) + damage = entry[2] + damage_table[entry[1]] = damage From 868964b419dc0f9b974d88fac0567dc5595d17ee Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 24 May 2024 20:35:40 -0700 Subject: [PATCH 33/71] Rework token patch + expose weaknesses to a rom table --- worlds/mmx/Rom.py | 87 +++++++++++++++----------- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 4668 -> 4901 bytes 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index b2b27ccea975..3fbef6e623b9 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -120,6 +120,17 @@ def adjust_boss_damage_table(world: World, patch: MMXProcedurePatch): # Fix second anglerge having different weakness patch.write_byte(0x12E62, 0x01) + # Write weaknesses to a table + offset = 0x17EC00 + for _, entries in world.boss_weaknesses.items(): + data = [0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF] + i = 0 + for entry in entries: + data[i] = entry[1] + i += 1 + patch.write_bytes(offset, bytearray(data)) + offset += 8 + def adjust_boss_hp(world: World, patch: MMXProcedurePatch): option = world.options.boss_randomize_hp @@ -138,44 +149,44 @@ def adjust_boss_hp(world: World, patch: MMXProcedurePatch): def patch_rom(world: World, patch: MMXProcedurePatch): # Prepare some ROM locations to receive the basepatch output - patch.write_bytes(0x00098C, bytearray([0x85,0xB3,0x8A])) - patch.write_bytes(0x0009AE, bytearray([0x85,0xB3,0x8A])) - patch.write_bytes(0x001261, bytearray([0xA9,0x10,0x20,0xE1,0x89])) - patch.write_bytes(0x001271, bytearray([0xA5,0xAC,0x89,0x08,0xF0,0x09,0xA5,0x3C, - 0x3A,0x10,0x11,0xA9,0x02,0x80,0x0D,0x89, - 0x24,0xF0,0x1E,0xA5,0x3C,0x1A,0xC9,0x03, - 0xD0,0x02,0xA9,0x00,0x85,0x3C,0xAA,0xBD, - 0x75,0x88,0x8D,0xB0,0x0B,0xA5,0x3C,0x18, - 0x69,0x10,0x20,0xE1,0x89,0xA9,0xF0,0x85, - 0x3B])) - patch.write_bytes(0x00131F, bytearray([0x9C,0xA9,0x0B,0x9C,0xAA,0x0B])) - patch.write_bytes(0x00132E, bytearray([0x90,0x8B])) - patch.write_bytes(0x001352, bytearray([0x64,0x38,0x64,0x39])) - patch.write_bytes(0x0025CA, bytearray([0xA9,0xF6,0xA0,0x02])) - patch.write_bytes(0x0046F3, bytearray([0x38,0xE9,0x0A])) - patch.write_bytes(0x006A61, bytearray([0xFB,0xEC])) - patch.write_bytes(0x006D67, bytearray([0x85,0x00,0x0A])) - patch.write_bytes(0x006F97, bytearray([0x9F,0xCB,0xFF,0x7E])) - patch.write_bytes(0x00700F, bytearray([0x55,0xDB])) - patch.write_bytes(0x007BF0, bytearray([0x90,0x8B])) - patch.write_bytes(0x00EB4A, bytearray([0xEE,0xCF,0x0B,0xA9,0x80])) - patch.write_bytes(0x011646, bytearray([0xA9,0x0A,0x85,0x01])) - patch.write_bytes(0x01B392, bytearray([0xA9,0x02,0x85,0x03])) - patch.write_bytes(0x01C67E, bytearray([0xA9,0x04,0x85,0x01])) - patch.write_bytes(0x01D84F, bytearray([0xA9,0x3C,0x9D,0x0A,0x00])) - patch.write_bytes(0x021D51, bytearray([0xED,0x00,0x00,0x8D,0xCF,0x0B])) - patch.write_bytes(0x021E94, bytearray([0x64,0x27,0xA9,0xFF])) - patch.write_bytes(0x021F5A, bytearray([0xED,0x00,0x00,0x8D,0xCF,0x0B])) - patch.write_bytes(0x02268C, bytearray([0xE6,0x03,0xE6,0x03])) - patch.write_bytes(0x03D0B2, bytearray([0x0C,0x99,0x1F,0x4C,0xBD,0xD0])) - patch.write_bytes(0x03D0D9, bytearray([0xA9,0x80,0x0C,0x7E])) - patch.write_bytes(0x044BEC, bytearray([0xA9,0x12,0x85,0x01])) - patch.write_bytes(0x0457CC, bytearray([0xA9,0x02,0x0C,0x99,0x1F])) - - patch.write_bytes(0x0312B0, bytearray([0x20,0xC6,0x09,0x40,0x14,0x20,0x06,0x0A, - 0x4C,0x49,0x43,0x45,0x4E,0x53,0x45,0x44, - 0x20,0x42,0x59,0x20,0x4E,0x49,0x4E,0x54, - 0x45,0x4E,0x44,0x4F,0x00])) + patch.write_bytes(0x00098C, bytearray([0xFF,0xFF,0xFF])) + patch.write_bytes(0x0009AE, bytearray([0xFF,0xFF,0xFF])) + patch.write_bytes(0x001261, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x001271, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF])) + patch.write_bytes(0x00131F, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x00132E, bytearray([0xFF,0xFF])) + patch.write_bytes(0x001352, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x0025CA, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x0046F3, bytearray([0xFF,0xFF,0xFF])) + patch.write_bytes(0x006A61, bytearray([0xFF,0xFF])) + patch.write_bytes(0x006D67, bytearray([0xFF,0xFF,0xFF])) + patch.write_bytes(0x006F97, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x00700F, bytearray([0xFF,0xFF])) + patch.write_bytes(0x007BF0, bytearray([0xFF,0xFF])) + patch.write_bytes(0x00EB4A, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x011646, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x01B392, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x01C67E, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x01D84F, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x021D51, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x021E94, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x021F5A, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x02268C, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x03D0B2, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x03D0D9, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x044BEC, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x0457CC, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF])) + + patch.write_bytes(0x0312B0, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF])) adjust_boss_damage_table(world, patch) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 2833e9701392a10855bb55d4688095c03e39325c..6fcd01b044703ac49c0ee70a855421d35a93169d 100644 GIT binary patch literal 4901 zcmai%c{J1y_s2i87~717u}+pTgRFfmLzXZYW69X}$gUY%_pigltB-bh+`-;5Xf9|MXc~}@9{^yqa2j9?yaAA^ zz{A8&I|x}F&V;AZsgerWI1>V1T?ij)vEc(jppX=NpEt@X9)_kO7m8@ zj+U64^NZwa)xm9vDS~v<6s$}MHDKqln6{t4wJo=-|Ap}mBRuvQX*j_4h0;K41^6&y z<+FJekAR?il?LX28_9c()dB#-)x*QXxTl{FKOO#S@>baIJ+G^uE?dVPJl_A%X88MX zsrchk7x)E0mPy1q1lUYc#Y*4~&@QMqD>mPX!t(FpP7uR)fkROgcEuJ(Le2B?xnWel)OCeV}#G(5=!gGQhMb$AA#4)m#`sSJPufRd+7tXMDt z*whpzKXG7@O=6)T1OQgV_~#5HOMpLJ001h15GDYAzw&$bbE6kl6^pn;B~P6D9!_%z z%H7FL@*-y41bsMC>)PdY#jgu_`Y&kaUuA*<{l@lW(+a-D5Mh7H&N&aZf$JGGu} zq|W9Qm{T+URm)uooO;0FLu2FJ-HiLvqIf3yZaGybX&@Dcp(0TX>xN>gtoB6w^-?Ey zW1K3|MpfW-TOb3P$`hJ+vrBN;T!7cYVNsmx8T6<&eNppIT8pc>0y%mAUM|?|P%D^~ zzA$PK7utU8ewO3sb8UW$&r=m$eV6{a51x2@R^Ht7Y#rjp7f=)!ZJR0BFQ+#F)| z^p@T2uofTPLd)0!`h9Tgc3&y}BR#}E9C7q~@-LkUvCXq=kCx?^b(WB=5tS6~-W@Af z=EVlOi~*nHp$*i#>&v-HYt!8z$|JG$2Vf%_*t-#@H{0icoBWO)`xz*keaB9uyg|Z` z49&U=3`WZSUU?`MTwr%TN+;yrighAr>{ zm_HupEIj0WVTn6+@@{nNJNIw7Y9l3EJz*<({kjnFlVb7UW0KCK^Sxd$mmj(AgOf8U z5n@6%vAna)gTl{;WoIIvRpu^#h&{0+NoZIUXcXTs<@i1kA|QU87#NF1dSSS(`Jb5D z41eu3Y&bGsi;}awRN_1K{nJ7(Gj`T@DiIk^kmfSxQh8e!NN@Qna^6(imhm4J{ErOC zw<*5>Amk4i{vW;J{_*o-?3RAygA3%1*RM}VoQ}#Z&$DazBGExq&?lqu(^G7%Yv$~W)EhnqybC>u}5rD3Qs>;%=HMtu^HT=L78L>)ML!i}Q zEU+}*+$sP+Z-TdxXIJ8IL)3LKRi?fe3gbVDLz5ZXv~&yb3Zo90R!0DM z1RepzCRC;-loJ5XyAweaQj$3~gof5UdBbrOXH`^{Y-Ke`2~70wiOnBWgB3X(tA2pu z>dOCvI!YG^AB*Se(U%LN^b(MiK5sA@p{6Q_m+1nj0;qp@^nXWanY5TgPm$m@WxB_| zh`VB&`e#z6hLILSO&=#+nSrL3qcDRH#XxH$6Ed6tA+cZ?XaK0CB;WJBDs|^$ZE^Xia_vgR4LIVQ>d8`Kdpa8aerlk>qRrppTY5x=@B6QBh zOtXLOd_-td%*l+lkbte9`GmuS8w}5&JKcj8L%L6f%vAmUQjJrnZuKtc^Nnz5?FiY` zy>huw*aVm2c2q(N^9d(=4AsYE(LMxebQ>C(9M2swDmCMwyx`D0iFyauOUsvbOae(I z;~~)Qkss@~Z;R}4PUmWG_6&vOeZ3S(#;78tIt^Bj{umO$2_Sfo$c@Lc=Yx@iFE{eR zK#IgqPK{x^m!Uajn?0L36)n3?@L8p+YF#ZpIeARhO0(zoI)P%?4JY~= zPA&iaYR2>Pqqx^y>`N9`*91!&141La)!5`}zkQgR&1`DZomsL=g_#fD8C$~~O#>znA4@~{Erd{g>{Moy;F8@uwq6qNKl zTNTd~o`|~Mtq~L$o5rd?@JeXIRh_xfvryFVy+!MxgtgNE>c@WW%+9XlX(`R@#o}T% zwQ%~U%|geV>#xvZjm3yBBn%f9kk6L6EXcnFpKrW8QT;rgM27;x)J(ds>q$7Y!)iWn z;_ryEOZ<_IvE`Wc_QL0S94$}YFCYlxS!UfYf7fjthg@{I$~&gDv>Fp9G~8(vk+l2x z$35%;g#bq_Zk98!)j@ zBC}PNlg5+tRT2`~z*tWZr9A``^1RxIw^#GJ*TF7-6}Lc>N&&D@1kXLsgskFq_64h2J(2Id9Op3 z)Ya#+CbI>i^W-lI`{p+uDGJUNw-q zI56dcJZ^*jG&xNlifQPD};HVzFRnrpKsc@n@FYZ)m!jJwkjAl-KP7_+|9UakX1&Q+fQ9( z16n*O#qWdG6`v(IfN{MhW!k3akdhE2JEU|u*uL@gjML1_IT2a&EEQR-W3|nSWcP(_ z*B#C(_Hn@@-J-m6{d{`s6A61PJAXHKYJtK`m(!-;p;%T4+7>mDOKZX?$%`L5H8HAh zLH{g6=J4?4uOA&Sr<%=Sy4D&e>Gm(3b*D0vKY3o5@J*8Rmuv8-dCYV{@59b254$!O z2z|+3>gJKJ#CZ%=bMe22qp{EpG|pJyewBTw!0g4sDv9P;)$UoR)tJ)MX!ev3+qveV z)htDDaG7~iaV}NB%)0ulY-AhFG6SDw$Yo(;+~k2E*O*nBT=hwf)6FDx zs2RsgFHx8B!&grLX2m+6>$-fWYhRITb}tKyxwg9`OVm7d*?wmD?GD#+$qv6XJ#wLC z049+iap!2|O@4Ao$!PE#OK&s#!k&DEc&-rB3kAV}W4;Ibjd{@1T_%DMiv9K9``CyksqO!<+HMnw4> zP>Km0sGC<6EfZI#iDVItYHA=vXL;H^)1a29ZQGAsvWo`|wGkx02oKZIecSrLZg&RAmQknasMz;i8d$R-dN^yL4s-TS~UCdlY7oB0tvdx{Lk8 z$iLNDwd2kXX~w2LyS8WEU1}Smt!a@eZ`Ead_wOC!YBlVoJ4_Cs06ebfjTKDXiDeC8J)-!ol(`Gz9ofY#vh zPtMu}^91fUi$VM&c;$`fC;(jnJ@&fgdnRFybc@i&BejFgaaa7FMu@Y;)0@ptcJNwm|BrRM1rA-dL zr91St>0f(8NW1D8Dp(#6okR zfIbzBW%@%@e=QoMUa*))5q~m=pvS_ZrZ&w-je&)pfkawSlAHhJ0#LGk_m$W8JC|?V z59fRv3`2;=2%1*8)~2W3hC-Zq>nd&b+C%#Bp(-TxQE*U{aWbe_fs>O&DSCF-n@8KZ z|Ml}=XaY&zd{VJX&e0+G$kFtrzVsNYQ{7gB8L0Q4eFNTbWJpZ^wn&vQ;x|gcaW~ic zzlup_rkP7tBahm-U8)XQJiL;;QSqYm`(B)AL2fk4lgS!L!nPL%--sKLeckx3a7NmI z<(+*m#Gmyo+v=hm=3`6LH$$64QUO1@1bcjDHiT#~-^x3g7x>+aW$0CIrU9uC~;@ zI?jXQ5qe+kq14dNJEE4~{6SMNdZAnTd8MZTbGWc+Y+x1bo71;s#O=FXO^#$NcF6ZYpeqO@|$=}9Hb&fK}x z62)G>oLc_X~moLA^@4sM`Y9PSux*$;6nsR#ry|=`28Aqtu*F zuMEerdhcsCJ3doqHn2)d2ku;DAuHd$n3hX(5;Hm5bjQpp%qiqiv_^t16o6Ygj1*!Z zQeDYlc=A-y^`zLAlXCq636`GWh`-$XhD9!uZO#`x^i@b<240Ma0;i{!g&pPiRk9Mg zwY9V^8DOk1*Tm%}DDoE)h|Iz8HUjvQkJ~N=<)zw({m|T$3x-RbHBD%piqU%YOhc{J4jyZ_7#V=SYw#*oIE-C#(z#$d9IovbA$hL9x`$&h8nmVM36*o#7BiDb=g z2w#;#wuClCa(#dI-gE!EulISM=lz`bd0x+Ro^xI=oRyv-9*;q@f`I=Z4)Wg-0Qmm% zz&VgEDC*ctSlO5ErwRbTx8vjEh~u9y2Qe^Ml8qB!0~i(oS#lHrrl2WcEU*M18i3{~ z9wZXuNg=U;s-~GsN=nE;HMN*alzAm5j}0;5S;mE$099|Gu>ZC|A_2@a;tvtrqr(m}KV)zWe`H zq3Wg21^_H4t2hpO@%Q-X_s;az`{Ums7YRQ$&$YKN?6i0N(|+7_*x3#Cg($^PXjY!* zDwHKXH*cp*d#cjuT4qw4{Lmb@#C^m|Edmnr0|>=V3~k;k;loUY<|)CUNijmjL$t!m zs&_Pk1pUaW55qy=eu)7{0H~x43kTqE3}ylVK*gAo`|lD@et?Nd0O?7~WlKWBL9`@L z@gPPCR6JP>K@CnoP;9!wv|Ip62KWdV%uq2A5Xq?C^en^Y!J)Ba;DqZxK>kkzAXpNB zWV0n^ZY6U%Ou-y^*^D6unUtOz{f6Xc^;QSgsq68kIvF<_Gt4bfwdwA%XlihMW+d*1$(h>zJ&!zBlG}|VqdY14mQ`k2s}G^Mm#fCU4i7k(R#1M`y?a*_`R=Av9;)iL%3}P>%nqL&@+B*&NAz)wTt7U- z5tgR8M!e`nypv=}UCC^G57QKlou@Elwn;Y_ZH}^$(b3?1(8T2*hb22=wX1XKJZd~F z$!zzH4tLo9&fF3riirqCG`iPwC6tbTzU39^f|C2(-*Y#o5E^`Y%H6`+vYBuGGGjJv zF+3pQ8W2HpJ7t%IFub3Cf$-)-1@kV*h6+b*HAOUE${~l<$C7-`CaYcvnAP);MnAmD z^-nx|nS+*jeml(c)u&%cty7$W1H7M`%@S2k_3@`aNl{yl!`_8iI39En7REhhcHZ`B zm{J1b+!MWD9W(#R`W*z@X|m)lq>v1pF6zzG!ZEIDMW2d@-SKC)xR~8O6U>;C_~#{B zGB$@w1mr1e`;*sV0Y)3iwgA7X%Aw=^Kh8tkA8^?DYHsxZ$q=_!UlsCF{9bTqY09?+8Z}+0Qr-g&O&uDe zvZ%pVf>17EV$id){09W9g`ecd@R?Z+pIAb5o~Q)&ga?#@ghTUAoP%_dOk=4VsOkf+ zd=V{zYGu_l9S%y}M4qt2;X-96vkCn>-if=MGA99;f1Tq5LMZ?MHZS$mEF;w1%lNKh zHdTnvD`eC{<`ZdsLaD_N`~$0Vv@+q6lGI{o9uP{Nrh}SK>a?(s2{IB?Oac}N0EQof znR!*Q!)}AkcU5bW9mSv+~ zE0ZLtpaO0NX{Lv>*F{HO+S=TV*HDU?Oi6M^Os==V*i5(AR zkAWnD8GB;U(R10c>|5>dUV{=DJEmw$LdAeCRoH_r8%93EtQ!lD!sHm+V*({ASmcn| zPmDN?7r2FU*#)eyS~f**RJ={V+(sccsN+U1qRPr>D|)~njGQU(INTLyXho~$ZRX3h z<*T!CekL>FVDz}DiDJRUAJU4$z)Kxip>C&LVyqLX_(bKH#oZP%vE~~(tJBzx8 z8wV_GR}`Ch1oeWyYMEZJj%sbYL(TY>G6ar_A!ds1MzIS^xlT3xK&r_e1Oi`f7=SNU z$U-2l+>-5K{#j+QpSH|@??L~7xn)D|gQLxzSmSowj6)qF&&wkGwih=LV~EtXZ2*fX z^Ycxj##yh5tW;PwbFx3m-~N*{Z&Yn)Pn7@4c}P7!%A{k<7=h^_zUWw)XAaJsx%K1r z2*f0PiukkPiICDN2(hnWUUebP&UcqYV2jA%v0i&Uy*O^vkX=Th-%#X><{@&{h#usY zr#2cSeo}k@R<=Ed?Ef@3ucb~9)GK4^-T!h^J(Ly|b4_Nw?MJv!<~hsY?3$-MCWj{b zW)@a+pMN&Ig)!2dpDon1&_fnRuj@f}Z9>A{{$!K6(ZTbV?<`MT@_@qi6>Oh)(48w0 z_##$|pB@MW1=g+C<*#22e;eFa5`0d9zJM}X-*T4EY~_6|Z6Eb?fKV`ZCY_J`>!Tt` z0M1V$4f5YG6+V9~cQ5wC802!xOui{gn0waV!;5K8t*%})c|YavVu$m;I;P#Iad0$w zX1?xGT7oHo`>SNmtAtkwwO#DMncHkv)27LfAJ%q&@+vub>$txK*ycpe=MMX{k}HB{ zWwo(`BMWXKA*L?cXCCfl8Q(2ZX-^MbNR`;8yy; z6{SX#Y*h6HaVAQFrKbL_M-;8k7R~+As(x1UOLHq+b+zWu>zo!8cj{XVEm%uT|Ay+d zy2|F9?Qk#DhI&|Jne|My4eIT)H#cA^XAcbwWvif4#PM4-8Z@XA9wNvWb%7_1|7EjW-!eb@-&OM0~fYfjYf0*WxOXHds&6 zkH*hlx4#-I87CTH)eq7ctrwHckz%n;e@_hxV5{*H56F*h5MypEd>G@b-RylZC}LYL z9`Mmnf|bb18Qs7HE01E;J4yy};@ z*9G4;MV-36%OJkWiMl6!>Er3c5nj++BC7A2&C9~gqa>kl!fXrfx`)f-jhxOt$I3HI z#Z*DNHvDg3$kk+T81t_3v|+3wo;AcpUEq0WA(&;KpThHo&BYNXp6|_4)uEb~YRqe` z&O391s)eyLFx)VZY3;aWu_uP>bvCeXWNpf@X1}WaDeZh+0?+8Tk2NguBB-h;3a%8w zN0Vm^9Cx{n7_0%gN>bEniq)c?P~zJw`m}>W1f_E4ThS_V)uExD!JP3rsZ;Zm-r?u4 z#e3ZLn8=?H-`f>~qg59V1<+;s6G|{)1FddH1GUFH7yhy=1_mH3|E3siFHAlZ^pEbp zJCt}qdrJnYjr-hPsstq(29A4$6|qDpG|ty^Cr*nJw1&3Bhj(?A?VCQlHroCBNfeAu zd+?hV)RmjpHkFI1UKg=DJ)+-rKV^1U;4I$y0JTYeUutPgtU#HTvRA)+_E2{5I>=#uQQ z^_M`il93p(LF&2IT&=_OXr$WHX$4EfppBef3rk8hqNe=2EnX+rzQDiF#%82@vJkhJ z(dSetuKP}gQ`hk#v0C-w^4V?`Z7k*whRdNcgn_-rUr_S;CBK;T(ef`AXj=)FzV6_7 z2H{%wjVFCypgqIu`}#wF#5qtamd#~^X(Ja3N8Tm@tC67)jZqW5JTuA?S& zdwh8X(L-y`A+1z89%jI!SP&1D>lN~Pf8YD@wKjol(%?pi_F?1YwqVpnAO8_&eMfEP zbef^zt$y#<#zJ>CLHhjB2VuyIY}yvQJOEmqQu$vB+LE|Re0RwoqpdhdU|A3uUw`2EWX=)_qg@S9KVA}wr<R#jD^)9eo$w3}9WVg&KOT#dIOx?EKUVTuqw*FCylKBryUSo9v zsto7#`?IoRIt~3Piq#O2u$oZo<Q_%UHr(@oSX`Z!24`K5nvB_rpjy^?(@Z0sgUwnL>b zu$-W6IaMT(lh5w`A^VhZ@T-0K=NKz~I45UfGtxf#+a4{2rwo%9di~>sQp~jg#oM!U zZiB;WYESHvlGn*g2R)$`_#xwEwr{j+s(#s5Rj{y-d~;3h`%_3PMx# z2;=&d!+T>l%Wo+i8M#m;of$x2#|ztLPM&f6<%{Bmp2$)T_R_I4HR8sI?uLDS<=(n_ z>Bjq&`p>1{Qjrkk-yl(?9M#34S6MHbf$bIkXLyga%Nn=5KE*q^i4n~G1w@!-B6AGE zKxAC^-LDPyq|E#9OS++K$;Km@iV7|l5_F2(O$B97nYB0lEbmt$isKs5w!DsnLkIC9{HO<^koN@ zgPS-)`=@Wv3tUB?ojVeic$wws0Xn0 zTog6_YIj*|BP+A4;zubT8AZQC6EM2BS!4LVywllCTW3Zn`k`3(T5XVXpoPulD~eHJ z+*mRSKn?jZV}}TcPQH=OLe&uA=?h^6Pj}t)++CWZu+P(CQY1J}|CL{f_bWlgEUVN0 E3uMbJvj6}9 From 129f4604330325e8e3b1156037a28c6dd7d27475 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 24 May 2024 20:50:41 -0700 Subject: [PATCH 34/71] Added weapon energy refills as items (not in the pool) --- worlds/mmx/Items.py | 2 ++ worlds/mmx/Names/ItemName.py | 2 ++ worlds/mmx/Rom.py | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/worlds/mmx/Items.py b/worlds/mmx/Items.py index cadbd846d410..955b20c630bf 100644 --- a/worlds/mmx/Items.py +++ b/worlds/mmx/Items.py @@ -64,6 +64,8 @@ class MMXItem(Item): ItemName.small_hp: ItemData(STARTING_ID + 0x0030, False), ItemName.large_hp: ItemData(STARTING_ID + 0x0031, False), ItemName.life: ItemData(STARTING_ID + 0x0034, False), + ItemName.small_weapon: ItemData(STARTING_ID + 0x0032, False), + ItemName.large_weapon: ItemData(STARTING_ID + 0x0033, False), } item_groups = { diff --git a/worlds/mmx/Names/ItemName.py b/worlds/mmx/Names/ItemName.py index 030f7a05c006..956f5dd99fa0 100644 --- a/worlds/mmx/Names/ItemName.py +++ b/worlds/mmx/Names/ItemName.py @@ -38,3 +38,5 @@ small_hp = "Small HP Refill" large_hp = "Large HP Refill" life = "1-Up" +small_weapon = "Small Weapon Energy Refill" +large_weapon = "Large Weapon Energy Refill" diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index 3fbef6e623b9..7663d8cc1f40 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -49,8 +49,8 @@ STARTING_ID + 0x0030: ["hp refill", 2], STARTING_ID + 0x0031: ["hp refill", 8], STARTING_ID + 0x0034: ["1up", 0], - #0xBD0032: ["small weapon refill"], - #0xBD0033: ["large weapon refill"] + STARTING_ID + 0x0032: ["weapon refill", 2], + STARTING_ID + 0x0033: ["weapon refill", 8], } boss_weakness_offsets = { From 51482f0a77aed907015a3008c45b5088ea1e8389 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 24 May 2024 20:53:11 -0700 Subject: [PATCH 35/71] Spark Mandrill no longer has armor --- worlds/mmx/Names/LocationName.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/mmx/Names/LocationName.py b/worlds/mmx/Names/LocationName.py index f0ac7266ac1a..56e80f2e51cf 100644 --- a/worlds/mmx/Names/LocationName.py +++ b/worlds/mmx/Names/LocationName.py @@ -83,7 +83,7 @@ sigma_fortress_3_sting_chameleon = "Defeated Sting Chameleon (Rematch)" sigma_fortress_3_hp_2 = "Sigma's Fortress 3 - Weapon Energy Pickup 1 (Before Spark Mandrill rematch)" sigma_fortress_3_energy_1 = "Sigma's Fortress 3 - HP Pickup 2 (Before Spark Mandrill rematch)" -sigma_fortress_3_spark_mandrill = "Defeated Armored Spark Mandrill (Rematch)" +sigma_fortress_3_spark_mandrill = "Defeated Spark Mandrill (Rematch)" sigma_fortress_3_hp_3 = "Sigma's Fortress 3 - HP Pickup 3 (Before Launch Octopus rematch)" sigma_fortress_3_energy_2 = "Sigma's Fortress 3 - Weapon Energy Pickup 2 (Before Launch Octopus rematch)" sigma_fortress_3_launch_octopus = "Defeated Launch Octopus (Rematch)" From f4193c60ae7f2b3f14614df7793409c576360bce Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 24 May 2024 23:55:19 -0700 Subject: [PATCH 36/71] Spark mandrill now has legs --- worlds/mmx/Rules.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/worlds/mmx/Rules.py b/worlds/mmx/Rules.py index 99b890355e7c..6cd37140c3db 100644 --- a/worlds/mmx/Rules.py +++ b/worlds/mmx/Rules.py @@ -167,7 +167,10 @@ def set_rules(world: MMXWorld): set_rule(multiworld.get_location(LocationName.spark_mandrill_sub_tank, player), lambda state: state.has(ItemName.boomerang_cutter, player)) set_rule(multiworld.get_location(LocationName.spark_mandrill_heart_tank, player), - lambda state: state.has(ItemName.boomerang_cutter, player)) + lambda state: ( + state.has(ItemName.boomerang_cutter, player) or + state.has(ItemName.legs, player) + )) # Storm Eagle collectibles set_rule(multiworld.get_location(LocationName.storm_eagle_heart_tank, player), From 1cac2d9eff21c83754358ae0bd44f5a202a1f4eb Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Sat, 25 May 2024 12:57:55 -0700 Subject: [PATCH 37/71] Fixes heart tanks creating additional items --- worlds/mmx/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index 792c4074cd92..ec54a532308a 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -141,7 +141,6 @@ def create_regions(self) -> None: i = self.options.sigma_heart_tank_count.value itempool += [self.create_item(ItemName.heart_tank) for _ in range(i)] if i != 8: - i = 8 - i itempool += [self.create_item(ItemName.heart_tank, ItemClassification.useful) for _ in range(8 - i)] else: itempool += [self.create_item(ItemName.heart_tank, ItemClassification.useful) for _ in range(8)] From ff77aa47036001e11f622895940c6edf19685cea Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Sat, 25 May 2024 13:50:47 -0700 Subject: [PATCH 38/71] bump version number --- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 4901 -> 4933 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 6fcd01b044703ac49c0ee70a855421d35a93169d..a3ee1daad4924fa0c137fae36fdd3907881ea3d5 100644 GIT binary patch delta 4540 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 delta 4520 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 Date: Sun, 26 May 2024 19:28:16 -0700 Subject: [PATCH 39/71] Send level id to DataStorage via the client --- worlds/mmx/Client.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index 156d240b370e..51ffd53577db 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -481,12 +481,14 @@ async def game_watcher(self, ctx): if menu_state is None: self.game_state = False self.energy_link_enabled = False + ctx.current_level_value = 42 ctx.item_queue = [] return if game_state[0] == 0: self.game_state = False ctx.item_queue = [] + ctx.current_level_value = 42 return validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) @@ -598,6 +600,34 @@ async def game_watcher(self, ctx): f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) + # Send Current Room for Tracker + current_level = int.from_bytes(await snes_read(ctx, MMX_LEVEL_INDEX, 0x1), "little") + + if game_state[0] == 0x00 or \ + (game_state[0] == 0x02 and menu_state[0] != 0x04): + current_level = -1 + + if ctx.current_level_value != (current_level + 1): + ctx.current_level_value = current_level + 1 + + # Send level id data to tracker + await ctx.send_msgs( + [ + { + "cmd": "Set", + "key": f"mmx1_level_id_{ctx.team}_{ctx.slot}", + "default": 0, + "want_reply": False, + "operations": [ + { + "operation": "replace", + "value": ctx.current_level_value, + } + ], + } + ] + ) + recv_count = await snes_read(ctx, MMX_RECV_INDEX, 2) if recv_count is None: # Add a small failsafe in case we get a None. Other SNI games do this... From c4c67cade8a7b10e685f95c8226dcfb20a623bda Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Sun, 26 May 2024 20:23:04 -0700 Subject: [PATCH 40/71] Added a hint blacklist --- worlds/mmx/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index ec54a532308a..c89559fe6212 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -65,6 +65,17 @@ class MMXWorld(World): location_name_to_id = all_locations item_name_groups = item_groups location_name_groups = location_groups + hint_blacklist = { + LocationName.armored_armadillo_clear, + LocationName.chill_penguin_clear, + LocationName.boomer_kuwanger_clear, + LocationName.sting_chameleon_clear, + LocationName.storm_eagle_clear, + LocationName.flame_mammoth_clear, + LocationName.spark_mandrill_clear, + LocationName.launch_octopus_clear, + LocationName.intro_completed, + } def __init__(self, multiworld: MultiWorld, player: int): self.rom_name_available_event = threading.Event() From c9fcc7cd92239e154428994a9fa126c1d442ac30 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Mon, 27 May 2024 19:37:53 -0700 Subject: [PATCH 41/71] Disallow weapon refills be part of the filler items --- worlds/mmx/Items.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/mmx/Items.py b/worlds/mmx/Items.py index 955b20c630bf..e679fe9890ec 100644 --- a/worlds/mmx/Items.py +++ b/worlds/mmx/Items.py @@ -64,6 +64,9 @@ class MMXItem(Item): ItemName.small_hp: ItemData(STARTING_ID + 0x0030, False), ItemName.large_hp: ItemData(STARTING_ID + 0x0031, False), ItemName.life: ItemData(STARTING_ID + 0x0034, False), +} + +junk_weapon_table = { ItemName.small_weapon: ItemData(STARTING_ID + 0x0032, False), ItemName.large_weapon: ItemData(STARTING_ID + 0x0033, False), } @@ -105,6 +108,7 @@ class MMXItem(Item): **upgrade_table, **tanks_table, **junk_table, + **junk_weapon_table, **special_weapons, } From 9d3122f6baf09baf0d0c6d54f9aae279cd05dc2f Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Mon, 27 May 2024 19:39:21 -0700 Subject: [PATCH 42/71] Update exclusion list and fix upgraded buster option --- worlds/mmx/Weaknesses.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/worlds/mmx/Weaknesses.py b/worlds/mmx/Weaknesses.py index 998414a60fb4..f71629c522fc 100644 --- a/worlds/mmx/Weaknesses.py +++ b/worlds/mmx/Weaknesses.py @@ -107,7 +107,7 @@ 0x80,0x80,0x80,0x7F,0x80,0x01 ], "Allow Upgraded Buster": [ - 0x80,0x80,0x80,0x03,0x20,0x00,0x80,0x80, + 0x80,0x80,0x03,0x80,0x20,0x00,0x80,0x80, 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, 0x80,0x80,0x80,0x7F,0x80,0x01 @@ -221,6 +221,7 @@ boss_excluded_weapons = { "Sting Chameleon": [ + "Charged Shotgun Ice", ], "Storm Eagle": [ ], @@ -231,6 +232,7 @@ "Spark Mandrill": [ ], "Armored Armadillo": [ + "Fire Wave", ], "Launch Octopus": [ "Fire Wave", @@ -244,6 +246,7 @@ "Bospider": [ ], "Rangda Bangda": [ + "Charged Shotgun Ice", ], "D-Rex": [ ], From 054193f5918bd091f2cd0f9a8805d9ad9e5184c1 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Mon, 27 May 2024 20:14:21 -0700 Subject: [PATCH 43/71] Skip shuffling D-Rex and Rangda Bangda HP --- worlds/mmx/Rom.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index 7663d8cc1f40..f9a52f3f6345 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -83,8 +83,8 @@ "Boomer Kuwanger": 0x38BE8, "Vile": 0x45C34, "Bospider": 0x15C0E, - "Rangda Bangda": 0x42A38, - "D-Rex": 0x440FD, + #"Rangda Bangda": 0x42A38, + #"D-Rex": 0x440FD, "Velguarder": 0x148DF, "Sigma": 0x4467B, "Wolf Sigma": 0x44B78, From d475cb795a09807c2f5d08d669198d67b0d0f095 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Mon, 27 May 2024 20:44:29 -0700 Subject: [PATCH 44/71] Reduced charged rolling shield dmg --- worlds/mmx/Weaknesses.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/mmx/Weaknesses.py b/worlds/mmx/Weaknesses.py index f71629c522fc..dfe617addcfc 100644 --- a/worlds/mmx/Weaknesses.py +++ b/worlds/mmx/Weaknesses.py @@ -232,7 +232,6 @@ "Spark Mandrill": [ ], "Armored Armadillo": [ - "Fire Wave", ], "Launch Octopus": [ "Fire Wave", @@ -277,7 +276,7 @@ ], "Rolling Shield": [ [[ItemName.rolling_shield], 0x09, WEAKNESS_UNCHARGED_DMG], - [[ItemName.rolling_shield], 0x12, WEAKNESS_CHARGED_DMG+2], + [[ItemName.rolling_shield], 0x12, WEAKNESS_CHARGED_DMG+1], ], "Fire Wave": [ [[ItemName.fire_wave], 0x0A, WEAKNESS_UNCHARGED_DMG], From d4293dc3f035cb8f580f44c0a430d0f41bfcbf16 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Mon, 27 May 2024 20:44:43 -0700 Subject: [PATCH 45/71] Various fixes to the basepatch --- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 4933 -> 4904 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index a3ee1daad4924fa0c137fae36fdd3907881ea3d5..0060eb3dd01f794f9b2e22dd94e4eb3a63f44dca 100644 GIT binary patch delta 4625 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 HbNiKCn_6CR delta 4654 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 From 29b7a28ef8d6529f4a6364a94af72a41c2fb06b2 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Tue, 28 May 2024 00:04:32 -0700 Subject: [PATCH 46/71] Somehow I missed adding Thunder Slimer to the hint bosses --- worlds/mmx/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index c89559fe6212..8ac6a1c47d51 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -313,6 +313,7 @@ def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, s 0x10: "Spark Mandrill", 0x11: "Launch Octopus", 0x12: "Flame Mammoth", + 0x17: "Thunder Slimer", 0x1E: "D-Rex", 0x13: "Velguarder", 0x1F: "Sigma", From ef27c491e0a70a157a4c35d754bd027c0aded086 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Tue, 28 May 2024 00:04:44 -0700 Subject: [PATCH 47/71] Hadouken no longer deletes HP in a few frames --- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 4904 -> 4922 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 0060eb3dd01f794f9b2e22dd94e4eb3a63f44dca..7ede873c4b62cc821d46e03d168443a9257f30d0 100644 GIT binary patch literal 4922 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 I5IYC_2W%|cbN~PV literal 4904 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 From 6dc1b787db862c9651dd57328972614b1f99482c Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 31 May 2024 18:11:53 -0700 Subject: [PATCH 48/71] Better (?) deathlink handling --- worlds/mmx/Client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index 51ffd53577db..18fac075dafa 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -110,6 +110,7 @@ async def deathlink_kill_player(self, ctx): snes_buffered_write(ctx, WRAM_START + 0x00BAA, bytes([0x0C])) snes_buffered_write(ctx, WRAM_START + 0x00C12, bytes([0x0C])) snes_buffered_write(ctx, WRAM_START + 0x00BAB, bytes([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x00BD7, bytes([0x08])) await snes_flush_writes(ctx) From dbaf25f58fa35b09a030b56cb89f7a199c05af36 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 14 Jun 2024 13:25:36 -0700 Subject: [PATCH 49/71] v1.1.0 changes --- worlds/mmx/Client.py | 102 ++++++++++++++++++++------ worlds/mmx/Options.py | 65 +++++++++++++++- worlds/mmx/Rom.py | 35 +++++++++ worlds/mmx/Weaknesses.py | 29 +++++--- worlds/mmx/__init__.py | 14 +++- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 4922 -> 6035 bytes 6 files changed, 204 insertions(+), 41 deletions(-) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index 18fac075dafa..2d7bb3beee83 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -28,6 +28,7 @@ MMX_MAX_HP = WRAM_START + 0x01F9A MMX_CURRENT_HP = WRAM_START + 0x00BCF MMX_UNLOCKED_CHARGED_SHOT = WRAM_START + 0x1EE16 +MMX_UNLOCKED_AIR_DASH = WRAM_START + 0x1EE22 MMX_SFX_FLAG = WRAM_START + 0x1EE03 MMX_SFX_NUMBER = WRAM_START + 0x1EE04 @@ -60,6 +61,7 @@ MMX_ENERGY_LINK_ENABLED = ROM_START + 0x17FFE8 MMX_DEATH_LINK_ACTIVE = ROM_START + 0x17FFE9 MMX_JAMMED_BUSTER_ACTIVE = ROM_START + 0x17FFEA +MMX_ABILITIES_FLAGS = ROM_START + 0x17FFF1 EXCHANGE_RATE = 500000000 @@ -82,6 +84,8 @@ def __init__(self): self.energy_link_enabled = False self.heal_request_command = None self.weapon_refill_request_command = None + self.using_newer_client = False + self.energy_link_details = False self.trade_request = None self.item_queue = [] @@ -132,6 +136,8 @@ async def validate_rom(self, ctx): ctx.command_processor.commands.pop("heal") if "refill" in ctx.command_processor.commands: ctx.command_processor.commands.pop("refill") + if "details" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("details") return False ctx.game = self.game @@ -148,6 +154,8 @@ async def validate_rom(self, ctx): ctx.command_processor.commands["heal"] = cmd_heal if "refill" not in ctx.command_processor.commands: ctx.command_processor.commands["refill"] = cmd_refill + if "details" not in ctx.command_processor.commands: + ctx.command_processor.commands["details"] = cmd_details if "trade" not in ctx.command_processor.commands: ctx.command_processor.commands["trade"] = cmd_trade @@ -219,7 +227,8 @@ async def handle_energy_link(self, ctx): {"operation": "max", "value": 0}], }]) pool = ((ctx.stored_data[f'EnergyLink{ctx.team}'] or 0) / EXCHANGE_RATE) + (energy_packet_raw / 16) - logger.info(f"Deposited {energy_packet_raw / 16:.2f} into the energy pool. Energy available: {pool:.2f}") + if self.energy_link_details: + logger.info(f"Deposited {energy_packet_raw / 16:.2f} into the energy pool. Energy available: {pool:.2f}") snes_buffered_write(ctx, MMX_ENERGY_LINK_PACKET, bytearray([0x00, 0x00])) await snes_flush_writes(ctx) @@ -335,12 +344,6 @@ async def handle_item_queue(self, ctx): next_item = self.item_queue[0] item_id = next_item[1] - - if next_item[0] == "boss access": - snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) - snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x2D])) - self.item_queue.pop(0) - return # Do not give items if you can't move, are in pause state, not in the correct mode or not in gameplay state receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) @@ -428,13 +431,21 @@ async def handle_item_queue(self, ctx): bit = 1 << upgrade[0] check = upgrades[0] & bit + if bit == 0x08: + air_dash_check = int.from_bytes(await snes_read(ctx, MMX_ABILITIES_FLAGS, 0x1), "little") & 0x02 + if air_dash_check != 0: + # check now becomes the air dash flag + check = int.from_bytes(await snes_read(ctx, MMX_UNLOCKED_AIR_DASH, 0x1), "little") + if check == 0: # Armor upgrades = upgrades[0] + original_value = upgrades upgrades |= bit if bit == 0x01: snes_buffered_write(ctx, WRAM_START + 0x0BBE, bytearray([0x18])) snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) + snes_buffered_write(ctx, WRAM_START + 0x1EE19, bytearray([0x80])) elif bit == 0x02: jam_check = await snes_read(ctx, MMX_JAMMED_BUSTER_ACTIVE, 0x1) charge_shot_unlocked = await snes_read(ctx, MMX_UNLOCKED_CHARGED_SHOT, 0x1) @@ -457,13 +468,16 @@ async def handle_item_queue(self, ctx): snes_buffered_write(ctx, WRAM_START + 0x0C68, bytearray([0x5D])) snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) elif bit == 0x08: - value = await snes_read(ctx, WRAM_START + 0x0C78, 0x1) - snes_buffered_write(ctx, WRAM_START + 0x0C78, bytearray([value[0] + 1])) - snes_buffered_write(ctx, WRAM_START + 0x0C82, bytearray([0x00])) - snes_buffered_write(ctx, WRAM_START + 0x0C83, bytearray([0x02])) - snes_buffered_write(ctx, WRAM_START + 0x0C79, bytearray([0x00])) - snes_buffered_write(ctx, WRAM_START + 0x0C88, bytearray([0x5D])) - snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) + if air_dash_check != 0 and original_value & bit == 0x08: + snes_buffered_write(ctx, MMX_UNLOCKED_AIR_DASH, bytearray([0x01])) + else: + value = await snes_read(ctx, WRAM_START + 0x0C78, 0x1) + snes_buffered_write(ctx, WRAM_START + 0x0C78, bytearray([value[0] + 1])) + snes_buffered_write(ctx, WRAM_START + 0x0C82, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C83, bytearray([0x02])) + snes_buffered_write(ctx, WRAM_START + 0x0C79, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C88, bytearray([0x5D])) + snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x2B])) self.item_queue.pop(0) @@ -511,14 +525,20 @@ async def game_watcher(self, ctx): # This is going to be rewritten whenever SNIClient supports on_package energy_link = await snes_read(ctx, MMX_ENERGY_LINK_ENABLED, 0x1) - if energy_link[0] != 0: - if self.energy_link_enabled and f'EnergyLink{ctx.team}' in ctx.stored_data: + if self.using_newer_client: + if energy_link[0] != 0: await self.handle_energy_link(ctx) + else: + if energy_link[0] != 0: + if self.energy_link_enabled and f'EnergyLink{ctx.team}' in ctx.stored_data: + await self.handle_energy_link(ctx) - if ctx.server and ctx.server.socket.open and not self.energy_link_enabled and ctx.team is not None: - self.energy_link_enabled = True - ctx.set_notify(f"EnergyLink{ctx.team}") - logger.info(f"Initialized EnergyLink{ctx.team}") + if ctx.server and ctx.server.socket.open and not self.energy_link_enabled and ctx.team is not None: + self.energy_link_enabled = True + ctx.set_notify(f"EnergyLink{ctx.team}") + logger.info(f"Initialized EnergyLink{ctx.team}") + self.energy_link_details = True + logger.info(f"EnergyLink detailed deposit activity enabled.") from worlds.mmx.Rom import weapon_rom_data, upgrades_rom_data, boss_access_rom_data, refill_rom_data from worlds.mmx.Levels import location_id_to_level_id @@ -667,7 +687,8 @@ async def game_watcher(self, ctx): level = boss_access_rom_data[item.item] boss_access[level[0]] = 0x01 snes_buffered_write(ctx, MMX_UNLOCKED_LEVELS, boss_access) - self.add_item_to_queue("boss access", item.item) + snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) + snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x2D])) elif item.item in refill_rom_data: self.add_item_to_queue(refill_rom_data[item.item][0], item.item, refill_rom_data[item.item][1]) @@ -760,6 +781,29 @@ async def game_watcher(self, ctx): snes_buffered_write(ctx, MMX_COLLECTED_HEART_TANKS, bytearray([collected_heart_tanks_data])) await snes_flush_writes(ctx) + def on_package(self, ctx, cmd: str, args: dict): + super().on_package(ctx, cmd, args) + + if cmd == "Connected": + slot_data = args.get("slot_data", None) + self.using_newer_client = True + if slot_data["energy_link"]: + ctx.set_notify(f"EnergyLink{ctx.team}") + if ctx.ui: + ctx.ui.enable_energy_link() + ctx.ui.energy_link_label.text = "Energy: Standby" + logger.info(f"Initialized EnergyLink{ctx.team}") + + elif cmd == "SetReply" and args["key"].startswith("EnergyLink"): + if ctx.ui: + pool = (args["value"] or 0) / EXCHANGE_RATE + ctx.ui.energy_link_label.text = f"Energy: {pool:.2f}" + + elif cmd == "Retrieved": + if f"EnergyLink{ctx.team}" in args["keys"] and args["keys"][f"EnergyLink{ctx.team}"] and ctx.ui: + pool = (args["keys"][f"EnergyLink{ctx.team}"] or 0) / EXCHANGE_RATE + ctx.ui.energy_link_label.text = f"Energy: {pool:.2f}" + def cmd_pool(self): """ @@ -870,3 +914,19 @@ def cmd_trade(self, amount: str = ""): logger.info(f"Set up trade for {amount} Weapon Energy. Pause the game to process the trade.") else: logger.info(f"You need to specify how much Weapon Energy you will request.") + +def cmd_details(self): + """ + Toggles displaying energy deposit activity into the console when EnergyLink is active. + """ + if self.ctx.game != "Mega Man X": + logger.warning("This command can only be used while playing Mega Man X") + if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: + logger.info(f"Must be connected to server and in game.") + else: + if self.ctx.client_handler.energy_link_details: + self.ctx.client_handler.energy_link_details = False + logger.info(f"EnergyLink detailed deposit activity disabled.") + else: + self.ctx.client_handler.energy_link_details = True + logger.info(f"EnergyLink detailed deposit activity enabled.") diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index c482c8f8595d..b1c3b8b45d57 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -1,8 +1,12 @@ from dataclasses import dataclass import typing -#from Options import OptionGroup, Choice, Range, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, StartInventoryPool -from Options import Choice, Range, Toggle, DefaultOnToggle, OptionSet, DeathLink, PerGameCommonOptions, StartInventoryPool +#from Options import OptionGroup, Choice, Range, Toggle, DefaultOnToggle, OptionSet, OptionDict, DeathLink, PerGameCommonOptions, StartInventoryPool +from Options import Choice, Range, Toggle, DefaultOnToggle, OptionSet, OptionDict, DeathLink, PerGameCommonOptions, StartInventoryPool +from schema import Schema, And, Use, Optional + +from .Rom import action_buttons, action_names +from .Weaknesses import boss_weaknesses, weapons_chaotic class EnergyLink(DefaultOnToggle): """ @@ -59,8 +63,8 @@ class BossWeaknessRando(Choice): display_name = "Boss Weakness Randomization" option_vanilla = 0 option_shuffled = 1 - option_chaotic_double = 2 - option_chaotic_single = 3 + option_chaotic_double = 3 + option_chaotic_single = 2 default = 0 class BossWeaknessStrictness(Choice): @@ -214,6 +218,53 @@ class SigmaSubTankCount(Range): range_end = 4 default = 4 +class ButtonConfiguration(OptionDict): + """ + Default buttons for every action. + """ + display_name = "Button Configuration" + schema = Schema({action_name: And(str, Use(str.upper), lambda s: s in action_buttons) for action_name in action_names}) + default = { + "SHOT": "Y", + "JUMP": "B", + "DASH": "A", + "SELECT_L": "L", + "SELECT_R": "R", + "MENU": "START" + } + +class PlandoWeaknesses(OptionDict): + """ + Forces bosses to have a specific weakness. Uses the names that appear on the chaotic weakness set. + + Format: + Boss Name: Weakness Name + """ + display_name = "Button Configuration" + schema = Schema({ + Optional(boss_name): + And(str, lambda weapon: weapon in weapons_chaotic.keys()) for boss_name in boss_weaknesses.keys() + }) + default = {} + +class BetterWallJump(Toggle): + """ + Enables performing a dash wall jump by holding down the button instead of pressing it every time. + """ + display_name = "Better Wall Jump" + +class AirDash(Toggle): + """ + Adds another Legs Upgrade that allows X to perform an Air Dash. + """ + display_name = "Air Dash" + +class LongJumps(Toggle): + """ + Allows X to perform longer jumps when holding down the Dash button. Only works after getting a Legs Upgrade. + """ + display_name = "Long Jumps" + mmx_option_groups = [ """ OptionGroup("Sigma Fortress Options", [ @@ -235,6 +286,7 @@ class SigmaSubTankCount(Range): StartingHP, HeartTankEffectiveness, JammedBuster, + BetterWallJump, ]), OptionGroup("Logic", [ LogicBossWeakness, @@ -249,13 +301,18 @@ class MMXOptions(PerGameCommonOptions): start_inventory_from_pool: StartInventoryPool death_link: DeathLink energy_link: EnergyLink + button_configuration: ButtonConfiguration starting_life_count: StartingLifeCount starting_hp: StartingHP heart_tank_effectiveness: HeartTankEffectiveness boss_weakness_rando: BossWeaknessRando boss_weakness_strictness: BossWeaknessStrictness + boss_weakness_plando: PlandoWeaknesses boss_randomize_hp: BossRandomizedHP jammed_buster: JammedBuster + better_walljump: BetterWallJump + air_dash: AirDash + long_jumps: LongJumps hadouken_in_pool: HadoukenInPool pickupsanity: PickupSanity early_legs: EarlyLegs diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index f9a52f3f6345..de8bcc3c5667 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -14,6 +14,9 @@ STARTING_ID = 0xBE0800 +action_names = ("SHOT", "JUMP", "DASH", "SELECT_L", "SELECT_R", "MENU") +action_buttons = ("Y", "B", "A", "L", "R", "X", "START", "SELECT") + weapon_rom_data = { STARTING_ID + 0x000E: [0x1F88, 0xFF], STARTING_ID + 0x0010: [0x1F8A, 0xFF], @@ -196,6 +199,29 @@ def patch_rom(world: World, patch: MMXProcedurePatch): patch.write_byte(0x014FF, world.options.starting_hp.value) patch.write_byte(0x01DDC, 0x7F) + # Remap buttons + button_values = { + "A": 0x20, + "B": 0x80, + "X": 0x10, + "Y": 0x40, + "L": 0x08, + "R": 0x04, + "START": 0x01, + "SELECT": 0x02, + } + action_offsets = { + "SHOT": 0x36E20, + "JUMP": 0x36E21, + "DASH": 0x36E22, + "SELECT_L": 0x36E23, + "SELECT_R": 0x36E24, + "MENU": 0x36E25, + } + button_config = world.options.button_configuration.value + for action, button in button_config.items(): + patch.write_byte(action_offsets[action], button_values[button]) + # Edit the ROM header from Utils import __version__ patch.name = bytearray(f'MMX1{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] @@ -233,6 +259,15 @@ def patch_rom(world: World, patch: MMXProcedurePatch): patch.write_byte(0x17FFEF, world.options.sigma_all_levels.value) patch.write_byte(0x17FFF0, world.options.boss_weakness_strictness.value) + value = 0 + if world.options.better_walljump.value: + value |= 0x01 + if world.options.air_dash.value: + value |= 0x02 + if world.options.long_jumps.value: + value |= 0x04 + patch.write_byte(0x17FFF1, value) + patch.write_file("token_patch.bin", patch.get_token_binary()) diff --git a/worlds/mmx/Weaknesses.py b/worlds/mmx/Weaknesses.py index dfe617addcfc..bac8fe916850 100644 --- a/worlds/mmx/Weaknesses.py +++ b/worlds/mmx/Weaknesses.py @@ -362,10 +362,10 @@ ], } - def handle_weaknesses(world): shuffle_type = world.options.boss_weakness_rando.value strictness_type = world.options.boss_weakness_strictness.value + boss_weakness_plando = world.options.boss_weakness_plando.value if shuffle_type != "vanilla": weapon_list = weapons.keys() @@ -385,6 +385,20 @@ def handle_weaknesses(world): else: damage_table = damage_templates["Only Weakness"].copy() + if boss in boss_weakness_plando.keys(): + if shuffle_type != "vanilla": + chosen_weapon = boss_weakness_plando[boss] + if chosen_weapon not in boss_excluded_weapons[boss]: + data = weapons_chaotic[chosen_weapon].copy() + for entry in data: + world.boss_weaknesses[boss].append(entry) + damage = entry[2] + damage_table[entry[1]] = damage + world.boss_weakness_data[boss] = damage_table.copy() + continue + + print (f"[{world.multiworld.player_name[world.player]}] Weakness plando failed for {boss}, contains an excluded weapon. Choosing an alternate weapon...") + if shuffle_type != "vanilla": copied_weapon_list = weapon_list.copy() for weapon in boss_excluded_weapons[boss]: @@ -400,8 +414,8 @@ def handle_weaknesses(world): damage_table[entry[1]] = damage world.boss_weakness_data[boss] = damage_table.copy() - elif shuffle_type == 2: - for _ in range(2): + elif shuffle_type >= 2: + for _ in range(shuffle_type - 1): chosen_weapon = world.random.choice(copied_weapon_list) data = weapons_chaotic[chosen_weapon].copy() copied_weapon_list.remove(chosen_weapon) @@ -411,15 +425,6 @@ def handle_weaknesses(world): damage_table[entry[1]] = damage world.boss_weakness_data[boss] = damage_table.copy() - elif shuffle_type == 3: - chosen_weapon = world.random.choice(copied_weapon_list) - data = weapons_chaotic[chosen_weapon].copy() - for entry in data: - world.boss_weaknesses[boss].append(entry) - damage = entry[2] - damage_table[entry[1]] = damage - world.boss_weakness_data[boss] = damage_table.copy() - else: for entry in boss_weaknesses[boss]: world.boss_weaknesses[boss].append(entry) diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index 8ac6a1c47d51..d4c25f349097 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -141,11 +141,19 @@ def create_regions(self) -> None: itempool += [self.create_item(ItemName.body)] else: itempool += [self.create_item(ItemName.body, ItemClassification.useful)] + itempool += [self.create_item(ItemName.arms)] if self.options.jammed_buster.value: itempool += [self.create_item(ItemName.arms)] + itempool += [self.create_item(ItemName.helmet)] + itempool += [self.create_item(ItemName.legs)] + if self.options.air_dash.value: + if "Armor Upgrades" in sigma_open and self.options.sigma_upgrade_count.value > 0: + itempool += [self.create_item(ItemName.legs)] + else: + itempool += [self.create_item(ItemName.legs, ItemClassification.useful)] # Add heart tanks into the pool if "Heart Tanks" in sigma_open and self.options.sigma_heart_tank_count.value > 0: @@ -226,6 +234,7 @@ def set_rules(self): def fill_slot_data(self): slot_data = {} # Write options to slot_data + slot_data["energy_link"] = self.options.energy_link.value slot_data["boss_weakness_rando"] = self.options.boss_weakness_rando.value slot_data["boss_weakness_strictness"] = self.options.boss_weakness_strictness.value slot_data["pickupsanity"] = self.options.pickupsanity.value @@ -266,15 +275,12 @@ def fill_slot_data(self): def generate_early(self): - if self.options.early_legs: + if ItemName.legs not in self.options.start_inventory_from_pool and self.options.early_legs: self.multiworld.early_items[self.player][ItemName.legs] = 1 self.boss_weaknesses = {} self.boss_weakness_data = {} handle_weaknesses(self) - - early_stage = self.random.choice(list(item_groups["Access Codes"])) - self.multiworld.local_early_items[self.player][early_stage] = 1 def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 7ede873c4b62cc821d46e03d168443a9257f30d0..aec55270ce7d6950036dc73c7a4ba517305e6d98 100644 GIT binary patch literal 6035 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 I5IYC_2W%|cbN~PV From a5782bf24d0c653426efb166ab62b62b9ffcd11e Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 14 Jun 2024 13:29:35 -0700 Subject: [PATCH 50/71] typos begone --- worlds/mmx/Options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index b1c3b8b45d57..965d7eecb3e7 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -57,7 +57,7 @@ class BossWeaknessRando(Choice): chaotic_single: Bosses will have one random weakness under the chaotic set The chaotic set makes every weapon charge level a separate weakness instead of keeping - them together, meaning that a boss can be weak to Charged Frost Shield but not its + them together, meaning that a boss can be weak to Charged Rolling Shield but not its uncharged version. """ display_name = "Boss Weakness Randomization" @@ -72,7 +72,7 @@ class BossWeaknessStrictness(Choice): How strict boss weaknesses will be. not_strict: Allow every weapon to deal damage to the bosses weakness_and_buster: Only allow the weakness and buster to deal damage to the bosses - weakness_and_upgraded_buster: Only allow the weakness and buster charge levels 3 & 4 to deal damage to the bosses + weakness_and_upgraded_buster: Only allow the weakness and buster charge level 3 to deal damage to the bosses only_weakness: Only the weakness will deal damage to the bosses """ display_name = "Boss Weakness Strictness" From 19c0599468b6398bad6e801681748ebf1fc7f057 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 14 Jun 2024 15:08:45 -0700 Subject: [PATCH 51/71] Fix boss weakness ROM offset --- worlds/mmx/Rom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index de8bcc3c5667..d15c37739bcf 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -124,7 +124,7 @@ def adjust_boss_damage_table(world: World, patch: MMXProcedurePatch): patch.write_byte(0x12E62, 0x01) # Write weaknesses to a table - offset = 0x17EC00 + offset = 0x17E9A2 for _, entries in world.boss_weaknesses.items(): data = [0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF] i = 0 From 2f4ab7ea5e007da46bf7e920c106afcffda76e86 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 14 Jun 2024 15:12:51 -0700 Subject: [PATCH 52/71] Fix air dash and unjammed buster upgrades being retained on soft rests --- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 6035 -> 6034 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index aec55270ce7d6950036dc73c7a4ba517305e6d98..9f1b4024439fc85c723f21f0d15deb5cc6404536 100644 GIT binary patch literal 6034 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 Mr(&A^&qv7r0G7YK&Hw-a literal 6035 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 o1&(iS(~q!k*qLe%L2tKguHTi*XPCr4P Date: Fri, 14 Jun 2024 15:38:33 -0700 Subject: [PATCH 53/71] Title screen no longer loads up the demo stage --- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 6034 -> 6020 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 9f1b4024439fc85c723f21f0d15deb5cc6404536..4aa8b1ec1a87583f941f6cc2952c833e29953734 100644 GIT binary patch delta 1279 zcmVg0e?pHK0*Kh5C8xF|NH*$|Lwo~ zwwr(c{dfQWKYzRR{{6SR|NFn+`}@Dp1AD*)?t8NC>Jbu5O)>&{m27mwn0000082|tP00000001-q00w{nXaE3_ zQ53>xXaLEe8X5otLqN~~0000013&-(000000Douz13(%80WHb_+2b&_ROlWVgrnpD zO|R=PF%l^P=^i1^0FL9bQx^RaWxNXteBkqGeP~KYHY6*o$|PC_{wJBDO^Zjhfn6Sv zlFTC-wlh4!PI3eK_7P45AZKJ?vAG;v6{dUIth9ZlU2%|6<MxtECNA- zMqz|=NEF*d>q%kRB+^KXVn9hSR)zqhSbqWjVpH~n2-P$I0zrtF1wmp!Q5c0P(1d^m zGR}h$bz)Gk5P$_9z)Q-+l1U_u)MwC};>DrR$V?;=^OMYTn7Q>~k7>5<%}&Tn!V$42 zkjTqnrmT^9OtsPAkLw?R1jf;cSOk)fY-5Owuzybv zID7uX015|^jB$^PYN!u{5E&f_Z2UmkvJevEN`PQ=AOJ)fsaHp)5~`sRAK$7i8*~2|3t-AKlE{e{w|@k~k0_OCgjU-7vTd14V?-4Kf!z3j=>Q@$d|`m{ z01*aS;Uoh?aauMgy*QXa-avANyUK_z6Q|f+OD9SI2mnCPL0U9Em7h11icLIJXyBNB~KI=z%=TvS`KFPk#;Y{fLY}uv-ZTc!1oT-V86`qxBCGNLnapfEzT6*+csg zM!+xwn-3)d^LLvpdLYl(zaZ6ylqOhFb>Om|AxU5bCJn7C55dcDq-+ip;Z3lRyaX z(ZMYtVUzoF6fO7okyh=yQYYoWKlm=NQkst{H&20&RKsc7!)Cu7G^%f7HTlp~PQh=~ pfpD)0aR3MaLJ))&o%LuKVtq^Y$GXMbkxmpO5xozPfU|oB7ZD@U8;Jk_ delta 1311 zcmV+)1>pLGFOn}2LQ_OZMn*I+Z2t}=D0Mvtvl0Fq!%IkuLXE0G9^#wlJtb`^tQc-jtoN;Q&9QqBiUVui2MN2!Fn z1W!2^Qfi*jjcF3hB^o25KohK$X+|+NYH9FBFs3mCYeX$0Y|*Y_?X*~$0E=i!O`xo> z7qNgYs}fcgl2!I0NVF!nA*-cDbO!P_lLmUor75v9SA|T#6Tm%arxPK>re{v;* zL5H#3av?OL(D{Eg07`$YTG-pig>TEewO%%7q45~a5oZ{`F(xg__Do5^Nn&&$JH^SK z-KeUb2?GSM4>#X^nfVd*8F&OWm8^ps=FjeS}U>5@@iL4ViuHr(3x@}8p56m!x`a%-(g@UF`_n=dN z#dVhHo@!Eg;MR3a?fkG{qOW+p3=Sv);0d9bHsKq z@9IAm)9cizR9;FzWuPXTYz9OxN(QNjEg!yl8b&u9@~Obq_`iWxwOTU_S-LVwgMK>o zzT66A*}+?DUY`aE%BYo*>7k@R0ss(%Ap@GlOx~FIl2tW-@pmLsg$V^k5(ZF0T4*^j VL0KkKSrrbjwg3Q%f3trE1rZ?DD~ Date: Fri, 14 Jun 2024 15:49:22 -0700 Subject: [PATCH 54/71] Disabled title screen demo --- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 6020 -> 6067 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 4aa8b1ec1a87583f941f6cc2952c833e29953734..f6b27a46e965c0ef78e8dd9a70daeb3f5dbbfb4f 100644 GIT binary patch delta 1328 zcmV-01<(3~FS9QaLQ_OZMn*I+Z2&8yZ`&Y-~0Q&&;xtG1&-F%oe@*?NtBJ~QKzAzdPZpmn43@s zsA;FE>Utw;8Z^xjnueM%nhgwt6Gx~$OeQ9tN$P2c4WwzK(l(*#8Kh{_AUz@IH8k}M znKabUXwZKHKmci=0B8UjGy$Lh&;S4dpa1{>GypUJ0000QGynlq{V2%NVrY*>rc4G? z%6djkF)=V|Vr0P&P-JLfHlqPB0iZ?!7!w3$pwl2dMrwMRVhkDx#-;$&F#rGoqd))w zqfGz|ng9T38UQp5fB+c{4FGAN4Kx5TG#LOI27rG6Xk^F<6%;aRXvok{Ob{9;piM9h zG&BNa#Kgb}qecW~fDx0>13;KYO)wKC0it6d6DB9AjV9AT4MJdA!HX$Li=+gjDV;$8 z6mGeEj!7a?Wa}X5z)3LPD~wx`aX1(iLs0&+Js3}elN}1^vb!HAzK;IdCK@#xco)j` zElz*1h}HR;RudHP9`{Ece$auWl!EHXOtco}R5fjA^iDt%ObT~O(xQ&y!XllD<;2lK zvJ4ODsrD{YN;4+duI>jc(F`w0Y+dY-C{{3l1A<&QA@&)m6ahfP)4Va%enj|Br%$8yfalYSJ;0- zEOdl2letYF=RG0DVuM08JEQE!j6&R1-r=A|w8{h8LL4yF`Uye^AVw!35%YJ&Fd=Xu z1VcyDAOiuxsW7>$4@6Oxq6VZUohbv=TtHDla0L&X2oU9+9Z8Hy8yK10#EVNxhxP*7 zg{Z{0qf~G`8X?d2TFW`IW1-7b$SHq6!qKXm?l}1XO7V!o4Fh zK_tmwERbjg2V4|3AIYtK)6Dra13jhcJK=xq0THaDN)Ff7Z78-w&=TaRO zu9)tF<{Va;MaCZlPwn|D;L>tTM~nai@RS#UWQ}Pf2Ox{M=XuotD0M$|!f~@AS5lkZ z*jWf*oWPHB2&T!=P-}0=GMJhniI9;j+V-#B)?}(*1Rdz0fOR#Dx8kDlW&sVc2uE^| zlfrxX!58SF3oJJ*c{vKv7cPJ9b2UMgOfStQ#O}DxkU(|7OV`6@q~w(_T9%wC>erPqxNPQ=JaZ9ENZOnm*A;_+hZj;KUO_B!B|*7Yj3uu@KNE z&%#4&3Rn*(ndc#bpgzYO4N!YQHz5kGft2+QzInQ4@XGMdUKKc@-TiGpFv=&sk1YA`QCM@V#pXjV-G?Q<-)p@^yqGT_LGDX} z$Az-5?$FcMzS8VgIk8gxCNQsq{d810nh82;nZDGOvXAebn(KS?F=w3DUXnMGUtw3h mutch?0D&Ztte{4>Y=sZ>D<*&OcO+AV2?Fd!XP~of1{V=gBtO&u delta 1280 zcmV+b1^@c9FN7}=LQ_OZMn*I+Z2KPtDA&KH9OaNr{O{t({(V!Xt0ie@BXwWn? zG5|CH004i`00000000^Q8UO$Q02%>Q{V2&ao|>9wf_f7PjDlbZfB`T8pb3B`1PvGw zf?*h#i~!WY00w}V13(181i@85B8>n5Xa;})0000002u%P000000000q000Jn0B8UJ zkx>-FXlMY*pc)zg14BU20000000Te(00000004hz00Tf800Aw^0omg)wp8dI8HA(c z08Ov!FfkG-0_h$h&j60&vQrlQ5@oy#3Vh)6X?ZrYtjZ)>2L30RqD_lOwSiq8 zk&?_K8n!b$!cKAn`t}h{1R!T*V6nLzTotB!+N`vFrCq>LC$GMGEs_M!08N7wfboFFto5l>yvc$sf`%MkuJip(LaoUJ?yWvqgb0YG{_@Z zQdojHq85?1XxA}z+AIwKMQBP@pq5}SV*p)7B&)3?tBgXEXicD31_+}t5khGs5-b8i zf<|G4b4V21MC(am*(B0PjAB4ZFjj^Dqga0d{$f-1gb39%00KdXm<2&%Kv5WlD$s<0 z1v1Wq5p`lvun>R+9>7b=#F9xQjnrq*o8rZx(8x?A5%ZJGbC|jHVUKCH?#)ifOu`Yd zCy>a?VWzB+c}%s@;E(Gct|Vd?RPVV!+PV_v&2NZ)q5m#PP6Wo$iC6@Zk8ERzjIe)C z4>)`N!vG2gl8kYWi)yG3gb*1W32gj8*|HE4<4S;FbRYmk8mU)DrV^^566IS@p{*|@ zJej9Y;wm?Sq0Bkqv>rhhr|q(8EnnSlF^qi!dO~(cfccm^pc+kvMO93Xmji6FjRPK5 zdssMl>>uB%EgN(H84F;_Gm^-O7q@=|!;dJHX@pkV`?77BN@GM70)gE4faw4tG<;!z z@&FMATHz!ELvdO*D7`qCK;A%dg}cg#E)%ENT}vlQ00;m;&_P->K9!$0l!{F}RA}dU z#DQ_}4!|;jX+BRZvq*_+Jbb%EtD$EgA0qh);}

  • $l`(;In_$pesG&#*=>`>>aU9 zsBCD-l-U}@Asz?>q=fR2j~eL>gJ*q-Bw95Bo~7{zV|yX*Y6YaSKllBXT_^2Q3LXXJ z5X;kJR#0R&3=mTh9!%452o=heVl%M%U+Khw8E+y*T3mtx5Cn-8M{4Ef>CNAQOWS#z z^HpI7;7V8o!03TI%d%+2*iU~A@coF4K(Jd02zY?poZbvC;G^{q5=dGoXn-3ui`hf_ z5k|l;1Dg*e0`qs9EP5c%*uNmvhLk2)QFY+5o*_wK1ttxxD-XfTaHMPw6ymwgHsI+h zW{BP#V73F(CvK*FpQjr>J;ou)0>=F72L|VwNeB2`Kt7@m(yQ$F$gjM$df<_ z@6o|6Az_pIa}+K2_>orayHY3Rz(4pduu__jDmPDok5t2H+QVkQ9W<(MV>S8ER8GNf q)PZoX32^`j074Li7M=BI7-D@(_Q$%#+>uTcBoVz2kbtwC1{V?L;TzBZ From b468f8766b25f58dcc3849720c2ecfa42444e6a1 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 14 Jun 2024 19:16:37 -0700 Subject: [PATCH 55/71] Updated bosses group to not include rematches in Sigma's Fortress --- worlds/mmx/Locations.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/worlds/mmx/Locations.py b/worlds/mmx/Locations.py index 9c2b465c18a3..a08b2707cc96 100644 --- a/worlds/mmx/Locations.py +++ b/worlds/mmx/Locations.py @@ -123,7 +123,23 @@ def __init__(self, player: int, name: str = '', address: int = None, parent=None location_table = {} location_groups = { - "Bosses": {location for location in all_locations.keys() if "Defeated" in location}, + "Bosses": { + LocationName.armored_armadillo_boss, + LocationName.chill_penguin_boss, + LocationName.spark_mandrill_boss, + LocationName.launch_octopus_boss, + LocationName.boomer_kuwanger_boss, + LocationName.sting_chameleon_boss, + LocationName.storm_eagle_boss, + LocationName.flame_mammoth_boss, + LocationName.sigma_fortress_1_vile, + LocationName.sigma_fortress_1_bospider, + LocationName.sigma_fortress_2_rangda_bangda, + LocationName.sigma_fortress_3_d_rex, + LocationName.sigma_fortress_4_velguarder, + LocationName.sigma_fortress_4_sigma, + LocationName.spark_mandrill_mini_boss, + }, "Heart Tanks": {location for location in all_locations.keys() if "- Heart Tank" in location}, "Sub Tanks": {location for location in all_locations.keys() if "- Sub Tank" in location}, "Upgrade Capsules": {location for location in all_locations.keys() if "Capsule" in location}, From ec864e9c2f4e661c4d96f6acf96683872c5001d1 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Mon, 1 Jul 2024 23:37:50 -0700 Subject: [PATCH 56/71] better 0.5.0 support + fixes fixes pickups not recollecting partial fix to soft resets fixes hadouken capsule not being recollected --- worlds/mmx/Client.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index 2d7bb3beee83..3846d76c1461 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -540,8 +540,8 @@ async def game_watcher(self, ctx): self.energy_link_details = True logger.info(f"EnergyLink detailed deposit activity enabled.") - from worlds.mmx.Rom import weapon_rom_data, upgrades_rom_data, boss_access_rom_data, refill_rom_data - from worlds.mmx.Levels import location_id_to_level_id + from .Rom import weapon_rom_data, upgrades_rom_data, boss_access_rom_data, refill_rom_data + from .Levels import location_id_to_level_id from worlds import AutoWorldRegister defeated_bosses_data = await snes_read(ctx, MMX_DEFEATED_BOSSES, 0x20) @@ -616,7 +616,7 @@ async def game_watcher(self, ctx): for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_game(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) @@ -659,10 +659,11 @@ async def game_watcher(self, ctx): if recv_index < len(ctx.items_received): item = ctx.items_received[recv_index] recv_index += 1 + sending_game = ctx.slot_info[item.player].game logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) snes_buffered_write(ctx, MMX_RECV_INDEX, bytes([recv_index])) await snes_flush_writes(ctx) @@ -702,6 +703,7 @@ async def game_watcher(self, ctx): # Handle collected locations game_state = await snes_read(ctx, MMX_GAME_STATE, 0x1) if game_state[0] != 0x02: + ctx.locations_checked = set() return new_boss_clears = False new_cleared_level = False @@ -725,7 +727,7 @@ async def game_watcher(self, ctx): for loc_id in ctx.checked_locations: if loc_id not in ctx.locations_checked: ctx.locations_checked.add(loc_id) - loc_name = ctx.location_names[loc_id] + loc_name = ctx.location_names.lookup_in_game(loc_id) if loc_name not in location_id_to_level_id: continue @@ -762,7 +764,7 @@ async def game_watcher(self, ctx): # Hadouken collected_hadouken_data = 0xFF new_hadouken = True - elif internal_id >= 0x100: + elif internal_id == 0x20: # Pickups collected_pickups[data_bit] = 0x01 new_pickup = True @@ -774,7 +776,7 @@ async def game_watcher(self, ctx): if new_pickup: snes_buffered_write(ctx, MMX_COLLECTED_PICKUPS, bytes(collected_pickups)) if new_hadouken: - snes_buffered_write(ctx, MMX_COLLECTED_PICKUPS, bytearray([collected_hadouken_data])) + snes_buffered_write(ctx, MMX_COLLECTED_HADOUKEN, bytearray([collected_hadouken_data])) if new_upgrade: snes_buffered_write(ctx, MMX_COLLECTED_UPGRADES, bytearray([collected_upgrades_data])) if new_heart_tank: From 416b61cb18b5c98ddf5296367ff69a11e12fd130 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Mon, 1 Jul 2024 23:37:58 -0700 Subject: [PATCH 57/71] option groups --- worlds/mmx/Options.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index 965d7eecb3e7..9712416de3ab 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -1,8 +1,7 @@ from dataclasses import dataclass import typing -#from Options import OptionGroup, Choice, Range, Toggle, DefaultOnToggle, OptionSet, OptionDict, DeathLink, PerGameCommonOptions, StartInventoryPool -from Options import Choice, Range, Toggle, DefaultOnToggle, OptionSet, OptionDict, DeathLink, PerGameCommonOptions, StartInventoryPool +from Options import OptionGroup, Choice, Range, Toggle, DefaultOnToggle, OptionSet, OptionDict, DeathLink, PerGameCommonOptions, StartInventoryPool from schema import Schema, And, Use, Optional from .Rom import action_buttons, action_names @@ -266,7 +265,16 @@ class LongJumps(Toggle): display_name = "Long Jumps" mmx_option_groups = [ - """ + OptionGroup("Gameplay Options", [ + StartingLifeCount, + StartingHP, + HeartTankEffectiveness, + JammedBuster, + BetterWallJump, + LongJumps, + HadoukenInPool, + LogicChargedShotgunIce, + ]), OptionGroup("Sigma Fortress Options", [ SigmaOpen, SigmaMedalCount, @@ -275,25 +283,14 @@ class LongJumps(Toggle): SigmaHeartTankCount, SigmaSubTankCount, FortressBundleUnlock, + LogicLegSigma, ]), OptionGroup("Boss Weaknesses", [ BossWeaknessRando, BossWeaknessStrictness, BossRandomizedHP, - ]), - OptionGroup("Gameplay Options", [ - StartingLifeCount, - StartingHP, - HeartTankEffectiveness, - JammedBuster, - BetterWallJump, - ]), - OptionGroup("Logic", [ LogicBossWeakness, - LogicLegSigma, - LogicChargedShotgunIce, ]), - """ ] @dataclass From 287b875da0827178e261e89b9da3ef3df1783d54 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Mon, 1 Jul 2024 23:38:06 -0700 Subject: [PATCH 58/71] option groups 2 --- worlds/mmx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index d4c25f349097..8ab31a520653 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -44,7 +44,7 @@ class MMXWeb(WebWorld): tutorials = [setup_en] - #option_groups = mmx_option_groups + option_groups = mmx_option_groups class MMXWorld(World): From f29820fc63b825a5d85db6e5fe2f8d5088ed1072 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Mon, 1 Jul 2024 23:38:13 -0700 Subject: [PATCH 59/71] basepatch adjustments --- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 6067 -> 6032 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index f6b27a46e965c0ef78e8dd9a70daeb3f5dbbfb4f..ebabb2c058a64bccd040972fd026dc0e9d79e069 100644 GIT binary patch literal 6032 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 SWlHF={r}GpjjhxS;C}!%boP+| literal 6067 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 F{{!A5*Gm8Z From 3ee43c17de3f810cc4cd82f737784bb6348a428b Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Thu, 4 Jul 2024 21:08:59 -0700 Subject: [PATCH 60/71] Add initial support for Enemy Tweaks --- worlds/mmx/Options.py | 68 ++++++++++++++++++++++++++ worlds/mmx/Rom.py | 60 +++++++++++++++++++++++ worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 6032 -> 7460 bytes 3 files changed, 128 insertions(+) diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index 9712416de3ab..aa4538d28022 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -264,6 +264,65 @@ class LongJumps(Toggle): """ display_name = "Long Jumps" +class ChillPenguinTweaks(OptionSet): + """ + Additional behavior to Chill Penguin + """ + display_name = "Chill Penguin Tweaks" + valid_keys = { + "Random horizontal slide speed", + "Jumps when starting slide", + "Random ice block horizontal speed", + "Random ice block vertical speed", + "Shoot random amount of ice blocks", + "Ice block shooting rate enhancer #1", + "Ice block shooting rate enhancer #2", + "Ice block shooting rate enhancer #3", + "Random blizzard strength", + "Fast falls after jumping", + "Random mist range", + "Can't be stunned/set on fire with incoming damage", + "Can't be set on fire with weakness", + } + default = {} + + +class ArmoredArmadilloTweaks(OptionSet): + """ + Additional behavior to Armored Armadillo + """ + display_name = "Armored Armadillo Tweaks" + valid_keys = { + "Random bouncing speed", + "Random bouncing angle", + "Random energy horizontal speed", + "Random energy vertical speed", + "Energy shooting rate enhancer #1", + "Energy shooting rate enhancer #2", + "Don't absorb any projectile", + "Absorbs any projectile except weakness", + "Don't flinch from incoming damage without armor", + "Can't block incoming projectiles", + } + default = {} + + +class SparkMandrillTweaks(OptionSet): + """ + Additional behavior to Spark Mandrill + """ + display_name = "Spark Mandrill Tweaks" + valid_keys = { + "Random Electric Spark speed", + "Additional Electric Spark #1", + "Additional Electric Spark #2", + "Landing creates Electric Spark", + "Hitting a wall creates Electric Spark", + "Can't be stunned during Dash Punch with weakness", + "Can't be frozen with weakness", + } + default = {} + mmx_option_groups = [ OptionGroup("Gameplay Options", [ StartingLifeCount, @@ -272,6 +331,7 @@ class LongJumps(Toggle): JammedBuster, BetterWallJump, LongJumps, + AirDash, HadoukenInPool, LogicChargedShotgunIce, ]), @@ -291,6 +351,11 @@ class LongJumps(Toggle): BossRandomizedHP, LogicBossWeakness, ]), + OptionGroup("Enemy Tweaks", [ + ChillPenguinTweaks, + ArmoredArmadilloTweaks, + SparkMandrillTweaks, + ]), ] @dataclass @@ -323,3 +388,6 @@ class MMXOptions(PerGameCommonOptions): sigma_upgrade_count: SigmaArmorUpgradeCount sigma_heart_tank_count: SigmaHeartTankCount sigma_sub_tank_count: SigmaSubTankCount + chill_penguin_tweaks: ChillPenguinTweaks + armored_armadillo_tweaks: ArmoredArmadilloTweaks + spark_mandrill_tweaks: SparkMandrillTweaks diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index d15c37739bcf..1c0f4da099c9 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -93,6 +93,51 @@ "Wolf Sigma": 0x44B78, } +enemy_tweaks_offsets = { + "Chill Penguin": 0x158000, + "Armored Armadillo": 0x158002, + "Spark Mandrill": 0x158004, +} + +enemy_tweaks_indexes = { + "Chill Penguin": { + "Random horizontal slide speed": 0x0001, + "Jumps when starting slide": 0x0002, + "Random ice block horizontal speed": 0x0004, + "Random ice block vertical speed": 0x0008, + "Shoot random amount of ice blocks": 0x0010, + "Ice block shooting rate enhancer #1": 0x0020, + "Ice block shooting rate enhancer #2": 0x0040, + "Ice block shooting rate enhancer #3": 0x0080, + "Random blizzard strength": 0x0100, + "Fast falls after jumping": 0x0200, + "Random mist range": 0x0400, + "Can't be stunned/set on fire with incoming damage": 0x4000, + "Can't be set on fire with weakness": 0x8000, + }, + "Armored Armadillo": { + "Random bouncing speed": 0x0001, + "Random bouncing angle": 0x0002, + "Random energy horizontal speed": 0x0004, + "Random energy vertical speed": 0x0008, + "Energy shooting rate enhancer #1": 0x0010, + "Energy shooting rate enhancer #2": 0x0020, + "Don't absorb any projectile": 0x1000, + "Absorbs any projectile except weakness": 0x2000, + "Don't flinch from incoming damage without armor": 0x4000, + "Can't block incoming projectiles": 0x8000, + }, + "Spark Mandrill": { + "Random Electric Spark speed": 0x0001, + "Additional Electric Spark #1": 0x0002, + "Additional Electric Spark #2": 0x0004, + "Landing creates Electric Spark": 0x0008, + "Hitting a wall creates Electric Spark": 0x0010, + "Can't be stunned during Dash Punch with weakness": 0x4000, + "Can't be frozen with weakness": 0x8000, + } +} + class MMXProcedurePatch(APProcedurePatch, APTokenMixin): hash = [HASH_US, HASH_LEGACY] game = "Mega Man X" @@ -132,6 +177,7 @@ def adjust_boss_damage_table(world: World, patch: MMXProcedurePatch): data[i] = entry[1] i += 1 patch.write_bytes(offset, bytearray(data)) + print (f"Boss: {_} | Offset: 0x{offset - 0x17E9A2:04X}") offset += 8 @@ -222,6 +268,20 @@ def patch_rom(world: World, patch: MMXProcedurePatch): for action, button in button_config.items(): patch.write_byte(action_offsets[action], button_values[button]) + # Write tweaks + enemy_tweaks_available = { + "Chill Penguin": world.options.chill_penguin_tweaks.value, + "Armored Armadillo": world.options.armored_armadillo_tweaks.value, + "Spark Mandrill": world.options.spark_mandrill_tweaks.value, + } + for boss, offset in enemy_tweaks_offsets.items(): + selected_tweaks = enemy_tweaks_available[boss] + final_value = 0 + for tweak in selected_tweaks: + final_value |= enemy_tweaks_indexes[boss][tweak] + print (f"Boss: {boss} | Offset: 0x{offset:06X} | Config: 0x{final_value:04X}") + patch.write_bytes(offset, bytearray([final_value & 0xFF, (final_value >> 8) & 0xFF])) + # Edit the ROM header from Utils import __version__ patch.name = bytearray(f'MMX1{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index ebabb2c058a64bccd040972fd026dc0e9d79e069..31e2958df80ab7db8e81cd9ff172e42484245339 100644 GIT binary patch literal 7460 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 literal 6032 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 SWlHF={r}GpjjhxS;C}!%boP+| From 0a1bf063141977bb6a5a5b1f8953fb89ee6ee6e7 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Wed, 7 Aug 2024 00:22:59 -0700 Subject: [PATCH 61/71] v1.3.0 update --- worlds/mmx/Client.py | 1267 ++++++++++++++----------- worlds/mmx/Options.py | 15 +- worlds/mmx/Regions.py | 4 +- worlds/mmx/Rom.py | 40 +- worlds/mmx/Rules.py | 39 +- worlds/mmx/__init__.py | 40 +- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 7460 -> 12305 bytes worlds/mmx/docs/en_Mega Man X.md | 76 +- worlds/mmx/docs/setup_en.md | 162 ++-- worlds/mmx/docs/setup_es.md | 103 ++ 10 files changed, 1039 insertions(+), 707 deletions(-) create mode 100644 worlds/mmx/docs/setup_es.md diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index 3846d76c1461..740705c6644a 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -14,6 +14,8 @@ WRAM_SIZE = 0x20000 SRAM_START = 0xE00000 +MMX_RAM = WRAM_START + 0x1EE00 + MMX_GAME_STATE = WRAM_START + 0x000D1 MMX_MENU_STATE = WRAM_START + 0x000D2 MMX_GAMEPLAY_STATE = WRAM_START + 0x000D3 @@ -27,41 +29,52 @@ MMX_LIFE_COUNT = WRAM_START + 0x01F80 MMX_MAX_HP = WRAM_START + 0x01F9A MMX_CURRENT_HP = WRAM_START + 0x00BCF -MMX_UNLOCKED_CHARGED_SHOT = WRAM_START + 0x1EE16 -MMX_UNLOCKED_AIR_DASH = WRAM_START + 0x1EE22 - -MMX_SFX_FLAG = WRAM_START + 0x1EE03 -MMX_SFX_NUMBER = WRAM_START + 0x1EE04 - -MMX_SIGMA_ACCESS = WRAM_START + 0x1EE02 -MMX_COLLECTED_HEART_TANKS = WRAM_START + 0x1EE05 -MMX_COLLECTED_UPGRADES = WRAM_START + 0x1EE06 -MMX_COLLECTED_HADOUKEN = WRAM_START + 0x1EE07 -MMX_DEFEATED_BOSSES = WRAM_START + 0x1EE80 -MMX_COMPLETED_LEVELS = WRAM_START + 0x1EE60 -MMX_COLLECTED_PICKUPS = WRAM_START + 0x1EEC0 -MMX_UNLOCKED_LEVELS = WRAM_START + 0x1EE40 - -MMX_RECV_INDEX = WRAM_START + 0x1EE00 -MMX_ENERGY_LINK_PACKET = WRAM_START + 0x1EE09 -MMX_VALIDATION_CHECK = WRAM_START + 0x1EE13 - -MMX_RECEIVING_ITEM = WRAM_START + 0x1EE15 -MMX_ENABLE_HEART_TANK = WRAM_START + 0x1EE0B -MMX_ENABLE_HP_REFILL = WRAM_START + 0x1EE0F -MMX_HP_REFILL_AMOUNT = WRAM_START + 0x1EE10 -MMX_ENABLE_GIVE_1UP = WRAM_START + 0x1EE12 -MMX_ENABLE_WEAPON_REFILL = WRAM_START + 0x1EE1A -MMX_WEAPON_REFILL_AMOUNT = WRAM_START + 0x1EE1B - +MMX_UNLOCKED_CHARGED_SHOT = MMX_RAM + 0x0016 +MMX_UNLOCKED_AIR_DASH = MMX_RAM + 0x0022 + +MMX_SFX_FLAG = MMX_RAM + 0x0003 +MMX_SFX_NUMBER = MMX_RAM + 0x0004 + +MMX_SIGMA_ACCESS = MMX_RAM + 0x0002 +MMX_COLLECTED_HEART_TANKS = MMX_RAM + 0x0005 +MMX_COLLECTED_UPGRADES = MMX_RAM + 0x0006 +MMX_COLLECTED_HADOUKEN = MMX_RAM + 0x0007 +MMX_DEFEATED_BOSSES = MMX_RAM + 0x0080 +MMX_COMPLETED_LEVELS = MMX_RAM + 0x0060 +MMX_COLLECTED_PICKUPS = MMX_RAM + 0x00C0 +MMX_UNLOCKED_LEVELS = MMX_RAM + 0x0040 + +MMX_RECV_INDEX = MMX_RAM + 0x0000 +MMX_ENERGY_LINK_PACKET = MMX_RAM + 0x0009 +MMX_VALIDATION_CHECK = MMX_RAM + 0x0013 + +MMX_RECEIVING_ITEM = MMX_RAM + 0x0015 +MMX_ENABLE_HEART_TANK = MMX_RAM + 0x000B +MMX_ENABLE_HP_REFILL = MMX_RAM + 0x000F +MMX_HP_REFILL_AMOUNT = MMX_RAM + 0x0010 +MMX_ENABLE_GIVE_1UP = MMX_RAM + 0x0012 +MMX_ENABLE_WEAPON_REFILL = MMX_RAM + 0x001A +MMX_WEAPON_REFILL_AMOUNT = MMX_RAM + 0x001B + +MMX_SCREEN_BRIGHTNESS = WRAM_START + 0x000B3 MMX_PAUSE_STATE = WRAM_START + 0x01F24 MMX_CAN_MOVE = WRAM_START + 0x01F13 -MMX_PICKUPSANITY_ACTIVE = ROM_START + 0x17FFE7 -MMX_ENERGY_LINK_ENABLED = ROM_START + 0x17FFE8 -MMX_DEATH_LINK_ACTIVE = ROM_START + 0x17FFE9 -MMX_JAMMED_BUSTER_ACTIVE = ROM_START + 0x17FFEA -MMX_ABILITIES_FLAGS = ROM_START + 0x17FFF1 +MMX_PICKUPSANITY_ACTIVE = ROM_START + 0x167C27 +MMX_ENERGY_LINK_ENABLED = ROM_START + 0x167C28 +MMX_DEATH_LINK_ACTIVE = ROM_START + 0x167C29 +MMX_JAMMED_BUSTER_ACTIVE = ROM_START + 0x167C2A +MMX_ABILITIES_FLAGS = ROM_START + 0x167C31 + +MMX_ENERGY_LINK_COUNT = MMX_RAM + 0x00100 +MMX_GLOBAL_TIMER = MMX_RAM + 0x00106 +MMX_GLOBAL_DEATHS = MMX_RAM + 0x0010A +MMX_GLOBAL_DMG_DEALT = MMX_RAM + 0x0010C +MMX_GLOBAL_DMG_TAKEN = MMX_RAM + 0x0010E +MMX_CHECKPOINTS_REACHED = MMX_RAM + 0x00120 +MMX_REFILL_REQUEST = MMX_RAM + 0x00110 +MMX_REFILL_TARGET = MMX_RAM + 0x00111 +MMX_ARSENAL_SYNC = MMX_RAM + 0x00112 EXCHANGE_RATE = 500000000 @@ -80,13 +93,16 @@ def __init__(self): super().__init__() self.game_state = False self.last_death_link = 0 - self.auto_heal = False self.energy_link_enabled = False self.heal_request_command = None self.weapon_refill_request_command = None self.using_newer_client = False self.energy_link_details = False self.trade_request = None + self.data_storage_enabled = False + self.save_arsenal = False + self.resync_request = False + self.current_level_value = 42 self.item_queue = [] @@ -128,16 +144,14 @@ async def validate_rom(self, ctx): energy_link = await snes_read(ctx, MMX_ENERGY_LINK_ENABLED, 0x1) rom_name = await snes_read(ctx, MMX_ROMHASH_START, ROMHASH_SIZE) if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:4] != b"MMX1": - if "pool" in ctx.command_processor.commands: - ctx.command_processor.commands.pop("pool") - if "autoheal" in ctx.command_processor.commands: - ctx.command_processor.commands.pop("autoheal") + if "resync" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("resync") + if "trade" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("trade") if "heal" in ctx.command_processor.commands: ctx.command_processor.commands.pop("heal") if "refill" in ctx.command_processor.commands: ctx.command_processor.commands.pop("refill") - if "details" in ctx.command_processor.commands: - ctx.command_processor.commands.pop("details") return False ctx.game = self.game @@ -146,16 +160,12 @@ async def validate_rom(self, ctx): ctx.send_option = 0 ctx.allow_collect = True if energy_link[0]: - if "pool" not in ctx.command_processor.commands: - ctx.command_processor.commands["pool"] = cmd_pool - if "autoheal" not in ctx.command_processor.commands: - ctx.command_processor.commands["autoheal"] = cmd_autoheal if "refill" not in ctx.command_processor.commands: ctx.command_processor.commands["heal"] = cmd_heal if "refill" not in ctx.command_processor.commands: ctx.command_processor.commands["refill"] = cmd_refill - if "details" not in ctx.command_processor.commands: - ctx.command_processor.commands["details"] = cmd_details + if "resync" not in ctx.command_processor.commands: + ctx.command_processor.commands["resync"] = cmd_resync if "trade" not in ctx.command_processor.commands: ctx.command_processor.commands["trade"] = cmd_trade @@ -167,199 +177,547 @@ async def validate_rom(self, ctx): return True - async def handle_hp_trade(self, ctx): + + async def game_watcher(self, ctx): from SNIClient import snes_buffered_write, snes_flush_writes, snes_read - validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) - if validation is None: + game_state = await snes_read(ctx, MMX_GAME_STATE, 0x1) + menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) + gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) + + # Discard uninitialized ROMs + if menu_state is None: + self.game_state = False + self.energy_link_enabled = False + self.current_level_value = 42 + self.item_queue = [] return + + validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) validation = validation[0] | (validation[1] << 8) if validation != 0xDEAD: + snes_logger.info(f'ROM not properly validated.') + self.game_state = False return - # Can only process trades during the pause state - receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) - menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) - gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) - if menu_state[0] != 0x04 or \ - gameplay_state[0] != 0x04 or \ - receiving_item[0] != 0x00: + if game_state[0] == 0: + self.game_state = False + self.item_queue = [] + self.current_level_value = 42 + ctx.locations_checked = set() + + # Resync data if solicited + if self.resync_request: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx_arsenal_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": dict()}], + }]) + self.resync_request = False + logger.info(f"Successfully cleared save data!") return - pause_state = await snes_read(ctx, MMX_PAUSE_STATE, 0x1) - if pause_state[0] == 0x00: - return + if self.resync_request: + self.resync_request = False + logger.info(f"Invalid environment for a resync. Please try again during the Title Menu screen.") + + self.game_state = True + if "DeathLink" in ctx.tags and menu_state[0] == 0x04 and ctx.last_death_link + 1 < time.time(): + currently_dead = gameplay_state[0] == 0x06 + await ctx.handle_deathlink_state(currently_dead) + + if game_state[0] != 0x00 and self.data_storage_enabled is True: + await self.handle_data_storage(ctx) + + # Handle DataStorage + if ctx.server and ctx.server.socket.open and not self.data_storage_enabled and ctx.team is not None: + self.data_storage_enabled = True + ctx.set_notify(f"mmx_global_timer_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_deaths_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_damage_taken_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_damage_dealt_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_checkpoints_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_arsenal_{ctx.team}_{ctx.slot}") - for item in self.item_queue: - if item[0] == "weapon refill": - self.trade_request = None - logger.info(f"You already have a Weapon Energy request pending to be received.") - return + if self.trade_request is not None: + await self.handle_hp_trade(ctx) - # Can trade HP -> WPN if HP is above 1 - current_hp = await snes_read(ctx, MMX_CURRENT_HP, 0x1) - if current_hp[0] > 0x01: - max_trade = current_hp[0] - 1 - set_trade = self.trade_request if self.trade_request <= max_trade else max_trade - self.add_item_to_queue("weapon refill", None, set_trade) - new_hp = current_hp[0] - set_trade - snes_buffered_write(ctx, MMX_CURRENT_HP, bytearray([new_hp])) - await snes_flush_writes(ctx) - self.trade_request = None - logger.info(f"Traded {set_trade} HP for {set_trade} Weapon Energy.") + await self.handle_item_queue(ctx) + + # This is going to be rewritten whenever SNIClient supports on_package + energy_link = await snes_read(ctx, MMX_ENERGY_LINK_ENABLED, 0x1) + if self.using_newer_client: + if energy_link[0] != 0: + await self.handle_energy_link(ctx) else: - logger.info("Couldn't process trade. HP is too low.") + if energy_link[0] != 0: + if self.energy_link_enabled and f'EnergyLink{ctx.team}' in ctx.stored_data: + await self.handle_energy_link(ctx) + if ctx.server and ctx.server.socket.open and not self.energy_link_enabled and ctx.team is not None: + self.energy_link_enabled = True + ctx.set_notify(f"EnergyLink{ctx.team}") + logger.info(f"Initialized EnergyLink{ctx.team}, use /help to get information about the EnergyLink commands.") - async def handle_energy_link(self, ctx): - from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + from .Rom import weapon_rom_data, upgrades_rom_data, boss_access_rom_data, refill_rom_data + from .Levels import location_id_to_level_id + from worlds import AutoWorldRegister - # Deposit heals into the pool regardless of energy_link setting - energy_packet = await snes_read(ctx, MMX_ENERGY_LINK_PACKET, 0x2) - if energy_packet is None: + defeated_bosses = list(await snes_read(ctx, MMX_DEFEATED_BOSSES, 0x20)) + cleared_levels = list(await snes_read(ctx, MMX_COMPLETED_LEVELS, 0x20)) + collected_heart_tanks = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_HEART_TANKS, 0x01)) + collected_upgrades = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_UPGRADES, 0x01)) + collected_hadouken = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_HADOUKEN, 0x01)) + collected_pickups = list(await snes_read(ctx, MMX_COLLECTED_PICKUPS, 0x20)) + pickupsanity_enabled = int.from_bytes(await snes_read(ctx, MMX_PICKUPSANITY_ACTIVE, 0x1)) + completed_intro_level = int.from_bytes(await snes_read(ctx, WRAM_START + 0x01F9B, 0x1)) + new_checks = [] + for loc_name, data in location_id_to_level_id.items(): + loc_id = AutoWorldRegister.world_types[ctx.game].location_name_to_id[loc_name] + if loc_id not in ctx.locations_checked: + internal_id = data[1] + data_bit = data[2] + + if internal_id == 0x000: + # Boss clear + if defeated_bosses[data_bit] != 0: + new_checks.append(loc_id) + elif internal_id == 0x001: + # Maverick Medal + if cleared_levels[data_bit] != 0: + new_checks.append(loc_id) + elif internal_id == 0x002: + # Heart Tank + masked_data = collected_heart_tanks & data_bit + if masked_data != 0: + new_checks.append(loc_id) + elif internal_id == 0x003: + # Mega Man upgrades + masked_data = collected_upgrades & data_bit + if masked_data != 0: + new_checks.append(loc_id) + elif internal_id == 0x004: + # Sub Tank + masked_data = collected_upgrades & data_bit + if masked_data != 0: + new_checks.append(loc_id) + elif internal_id == 0x005: + # Hadouken + if collected_hadouken != 0x00: + new_checks.append(loc_id) + elif internal_id == 0x007: + # Intro + if game_state[0] == 0x02 and \ + menu_state[0] == 0x00 and \ + gameplay_state[0] == 0x01 and \ + completed_intro_level == 0x04: + new_checks.append(loc_id) + elif internal_id == 0x020: + # Pickups + if not pickupsanity_enabled or pickupsanity_enabled == 0: + continue + if collected_pickups[data_bit] != 0: + new_checks.append(loc_id) + + verify_game_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 1) + if verify_game_state is None: + snes_logger.info(f'Exit Game.') return - energy_packet_raw = energy_packet[0] | (energy_packet[1] << 8) - energy_packet = (energy_packet_raw * EXCHANGE_RATE) >> 4 - if energy_packet != 0: - await ctx.send_msgs([{ - "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": - [{"operation": "add", "value": energy_packet}, - {"operation": "max", "value": 0}], - }]) - pool = ((ctx.stored_data[f'EnergyLink{ctx.team}'] or 0) / EXCHANGE_RATE) + (energy_packet_raw / 16) - if self.energy_link_details: - logger.info(f"Deposited {energy_packet_raw / 16:.2f} into the energy pool. Energy available: {pool:.2f}") - snes_buffered_write(ctx, MMX_ENERGY_LINK_PACKET, bytearray([0x00, 0x00])) - await snes_flush_writes(ctx) + + rom = await snes_read(ctx, MMX_ROMHASH_START, ROMHASH_SIZE) + if rom != ctx.rom: + ctx.rom = None + snes_logger.info(f'Exit ROM.') + return + + for new_check_id in new_checks: + ctx.locations_checked.add(new_check_id) + location = ctx.location_names.lookup_in_game(new_check_id) + snes_logger.info( + f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) - energy_link = await snes_read(ctx, MMX_ENERGY_LINK_ENABLED, 0x1) - if energy_link is None: + # Send Current Room for Tracker + current_level = int.from_bytes(await snes_read(ctx, MMX_LEVEL_INDEX, 0x1), "little") + + if game_state[0] == 0x00 or \ + (game_state[0] == 0x02 and menu_state[0] != 0x04): + current_level = -1 + + if self.current_level_value != (current_level + 1): + self.current_level_value = current_level + 1 + + # Send level id data to tracker + await ctx.send_msgs( + [ + { + "cmd": "Set", + "key": f"mmx1_level_id_{ctx.team}_{ctx.slot}", + "default": 0, + "want_reply": False, + "operations": [ + { + "operation": "replace", + "value": self.current_level_value, + } + ], + } + ] + ) + + recv_count = await snes_read(ctx, MMX_RECV_INDEX, 2) + if recv_count is None: + # Add a small failsafe in case we get a None. Other SNI games do this... return - if energy_link[0]: - validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) - if validation is None: - return - validation = validation[0] | (validation[1] << 8) - if validation != 0xDEAD: - return + recv_index = int.from_bytes(recv_count, "little") + sync_arsenal = int.from_bytes(await snes_read(ctx, MMX_ARSENAL_SYNC, 0x2), "little") - receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) - menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) - gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) - can_move = await snes_read(ctx, MMX_CAN_MOVE, 0x7) - pause_state = await snes_read(ctx, MMX_PAUSE_STATE, 0x1) - if menu_state[0] != 0x04 or \ - gameplay_state[0] != 0x04 or \ - pause_state[0] != 0x00 or \ - can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ - receiving_item[0] != 0x00: - return + if recv_index < len(ctx.items_received) and sync_arsenal != 0x1337: + item = ctx.items_received[recv_index] + recv_index += 1 + sending_game = ctx.slot_info[item.player].game + logging.info('Received %s from %s (%s) (%d/%d in list)' % ( + color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'), + color(ctx.player_names[item.player], 'yellow'), + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) - skip_hp = False - skip_weapon = False - for item in self.item_queue: - if item[0] == "hp refill": - skip_hp = True - self.heal_request_command = None - elif item[0] == "weapon refill": - skip_weapon = True - self.weapon_refill_request_command = None + snes_buffered_write(ctx, MMX_RECV_INDEX, bytes([recv_index])) + await snes_flush_writes(ctx) - pool = ctx.stored_data[f'EnergyLink{ctx.team}'] or 0 - if not skip_hp: - # Perform auto heals - if self.auto_heal: - if self.heal_request_command is None: - if pool < EXCHANGE_RATE: - return - current_hp = await snes_read(ctx, MMX_CURRENT_HP, 0x1) - max_hp = await snes_read(ctx, MMX_MAX_HP, 0x1) - if max_hp[0] > current_hp[0]: - self.heal_request_command = max_hp[0] - current_hp[0] - - # Handle heal requests - if self.heal_request_command: - heal_needed = self.heal_request_command - heal_needed_rate = heal_needed * EXCHANGE_RATE - if pool < EXCHANGE_RATE: - logger.info(f"There's not enough Energy for your request ({heal_needed}). Energy available: {pool / EXCHANGE_RATE:.2f}") - self.heal_request_command = None - return - elif pool < heal_needed_rate: - heal_needed = int(pool / EXCHANGE_RATE) - heal_needed_rate = heal_needed * EXCHANGE_RATE - await ctx.send_msgs([{ - "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": - [{"operation": "add", "value": -heal_needed_rate}, - {"operation": "max", "value": 0}], - }]) - self.add_item_to_queue("hp refill", None, self.heal_request_command) - pool = (pool / EXCHANGE_RATE) - heal_needed - logger.info(f"Healed by {heal_needed}. Energy available: {pool:.2f}") - self.heal_request_command = None + if item.item in weapon_rom_data: + self.add_item_to_queue("weapon", item.item) - if not skip_weapon: - # Handle weapon refill requests - if self.weapon_refill_request_command: - heal_needed = self.weapon_refill_request_command - heal_needed_rate = heal_needed * EXCHANGE_RATE - if pool < EXCHANGE_RATE: - logger.info(f"There's not enough Energy for your request ({heal_needed}). Energy available: {pool / EXCHANGE_RATE:.2f}") - self.weapon_refill_request_command = None - return - elif pool < heal_needed_rate: - heal_needed = int(pool / EXCHANGE_RATE) - heal_needed_rate = heal_needed * EXCHANGE_RATE - await ctx.send_msgs([{ - "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": - [{"operation": "add", "value": -heal_needed_rate}, - {"operation": "max", "value": 0}], - }]) - self.add_item_to_queue("weapon refill", None, self.weapon_refill_request_command) - pool = (pool / EXCHANGE_RATE) - heal_needed - logger.info(f"Refilled current weapon by {heal_needed}. Energy available: {pool:.2f}") - self.weapon_refill_request_command = None + elif item.item == STARTING_ID + 0x0013: + self.add_item_to_queue("heart tank", item.item) + elif item.item == STARTING_ID + 0x0014: + self.add_item_to_queue("sub tank", item.item) - def add_item_to_queue(self, item_type, item_id, item_additional = None): - if not hasattr(self, "item_queue"): - self.item_queue = [] - self.item_queue.append([item_type, item_id, item_additional]) + elif item.item in upgrades_rom_data: + self.add_item_to_queue("upgrade", item.item) + elif item.item in boss_access_rom_data: + if item.item == STARTING_ID + 0x000A: + snes_buffered_write(ctx, MMX_SIGMA_ACCESS, bytearray([0x00])) + boss_access = bytearray(await snes_read(ctx, MMX_UNLOCKED_LEVELS, 0x20)) + level = boss_access_rom_data[item.item] + boss_access[level[0]] = 0x01 + snes_buffered_write(ctx, MMX_UNLOCKED_LEVELS, boss_access) + snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) + snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x2D])) + self.save_arsenal = True - async def handle_item_queue(self, ctx): - from SNIClient import snes_buffered_write, snes_flush_writes, snes_read - from worlds.mmx.Rom import weapon_rom_data, upgrades_rom_data + elif item.item in refill_rom_data: + self.add_item_to_queue(refill_rom_data[item.item][0], item.item, refill_rom_data[item.item][1]) + self.save_arsenal = True - if not hasattr(self, "item_queue") or len(self.item_queue) == 0: + elif item.item == STARTING_ID: + # Handle goal + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + self.save_arsenal = True + return + + # Handle collected locations + game_state = await snes_read(ctx, MMX_GAME_STATE, 0x1) + if game_state[0] != 0x02: + ctx.locations_checked = set() return + new_boss_clears = False + new_cleared_level = False + new_heart_tank = False + new_upgrade = False + new_pickup = False + new_hadouken = False + cleared_levels = list(await snes_read(ctx, MMX_COMPLETED_LEVELS, 0x20)) + collected_pickups = list(await snes_read(ctx, MMX_COLLECTED_PICKUPS, 0x40)) + collected_heart_tanks = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_HEART_TANKS, 0x01)) + collected_upgrades = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_UPGRADES, 0x01)) + defeated_bosses = list(await snes_read(ctx, MMX_DEFEATED_BOSSES, 0x20)) + collected_hadouken = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_HADOUKEN, 0x01)) + i = 0 + for loc_id in ctx.checked_locations: + if loc_id not in ctx.locations_checked: + ctx.locations_checked.add(loc_id) + loc_name = ctx.location_names.lookup_in_game(loc_id) - validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) - if validation is None: - return - validation = validation[0] | (validation[1] << 8) - if validation != 0xDEAD: - return + if loc_name not in location_id_to_level_id: + continue - next_item = self.item_queue[0] - item_id = next_item[1] - - # Do not give items if you can't move, are in pause state, not in the correct mode or not in gameplay state - receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) - menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) + logging.info(f"Recovered checks ({i:03}): {loc_name}") + i += 1 + + data = location_id_to_level_id[loc_name] + internal_id = data[1] + data_bit = data[2] + + if internal_id == 0x000: + # Boss clear + defeated_bosses[data_bit] = 1 + new_boss_clears = True + elif internal_id == 0x001: + # Maverick Medal + cleared_levels[data_bit] = 0xFF + new_cleared_level = True + elif internal_id == 0x002: + # Heart Tank + collected_heart_tanks |= data_bit + new_heart_tank = True + elif internal_id == 0x003: + # Mega Man upgrades + collected_upgrades |= data_bit + new_upgrade = True + elif internal_id == 0x004: + # Sub Tank + collected_upgrades |= data_bit + new_upgrade = True + elif internal_id == 0x005: + # Hadouken + collected_hadouken = 0xFF + new_hadouken = True + elif internal_id == 0x20: + # Pickups + collected_pickups[data_bit] = 0x01 + new_pickup = True + + if new_cleared_level: + snes_buffered_write(ctx, MMX_COMPLETED_LEVELS, bytes(cleared_levels)) + if new_boss_clears: + snes_buffered_write(ctx, MMX_DEFEATED_BOSSES, bytes(defeated_bosses)) + if new_pickup: + snes_buffered_write(ctx, MMX_COLLECTED_PICKUPS, bytes(collected_pickups)) + if new_hadouken: + snes_buffered_write(ctx, MMX_COLLECTED_HADOUKEN, bytearray([collected_hadouken])) + if new_upgrade: + snes_buffered_write(ctx, MMX_COLLECTED_UPGRADES, bytearray([collected_upgrades])) + if new_heart_tank: + snes_buffered_write(ctx, MMX_COLLECTED_HEART_TANKS, bytearray([collected_heart_tanks])) + await snes_flush_writes(ctx) + + def on_package(self, ctx, cmd: str, args: dict): + super().on_package(ctx, cmd, args) + + if cmd == "Connected": + ctx.set_notify(f"mmx_global_timer_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_deaths_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_damage_taken_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_damage_dealt_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_checkpoints_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_arsenal_{ctx.team}_{ctx.slot}") + slot_data = args.get("slot_data", None) + self.using_newer_client = True + if slot_data["energy_link"]: + ctx.set_notify(f"EnergyLink{ctx.team}") + if ctx.ui: + ctx.ui.enable_energy_link() + ctx.ui.energy_link_label.text = "Energy: Standby" + logger.info(f"Initialized EnergyLink{ctx.team}") + + elif cmd == "SetReply" and args["key"].startswith("EnergyLink"): + if ctx.ui: + pool = (args["value"] or 0) / EXCHANGE_RATE + ctx.ui.energy_link_label.text = f"Energy: {pool:.2f}" + + elif cmd == "Retrieved": + if f"EnergyLink{ctx.team}" in args["keys"] and args["keys"][f"EnergyLink{ctx.team}"] and ctx.ui: + pool = (args["keys"][f"EnergyLink{ctx.team}"] or 0) / EXCHANGE_RATE + ctx.ui.energy_link_label.text = f"Energy: {pool:.2f}" + + + async def handle_hp_trade(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + + validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) + if validation is None: + return + validation = validation[0] | (validation[1] << 8) + if validation != 0xDEAD: + return + + # Can only process trades during the pause state + receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) + menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) - can_move = await snes_read(ctx, MMX_CAN_MOVE, 0x7) + if menu_state[0] != 0x04 or \ + gameplay_state[0] != 0x04 or \ + receiving_item[0] != 0x00: + return + + for item in self.item_queue: + if item[0] == "weapon refill": + self.trade_request = None + logger.info(f"You already have a Weapon Energy request pending to be received.") + return + + # Can trade HP -> WPN if HP is above 1 + current_hp = await snes_read(ctx, MMX_CURRENT_HP, 0x1) + if current_hp[0] > 0x01: + max_trade = current_hp[0] - 1 + set_trade = self.trade_request if self.trade_request <= max_trade else max_trade + self.add_item_to_queue("weapon refill", None, set_trade) + new_hp = current_hp[0] - set_trade + snes_buffered_write(ctx, MMX_CURRENT_HP, bytearray([new_hp])) + await snes_flush_writes(ctx) + self.trade_request = None + logger.info(f"Traded {set_trade} HP for {set_trade} Weapon Energy.") + else: + logger.info("Couldn't process trade. HP is too low.") + + + async def handle_energy_link(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + + validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) + if validation is None: + return + validation = validation[0] | (validation[1] << 8) + if validation != 0xDEAD: + return + + # Deposit heals into the pool regardless of energy_link setting + energy_packet = await snes_read(ctx, MMX_ENERGY_LINK_PACKET, 0x2) + if energy_packet is None: + return + energy_packet_raw = energy_packet[0] | (energy_packet[1] << 8) + energy_packet = (energy_packet_raw * EXCHANGE_RATE) >> 4 + if energy_packet != 0: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": + [{"operation": "add", "value": energy_packet}, + {"operation": "max", "value": 0}], + }]) + pool = ((ctx.stored_data[f'EnergyLink{ctx.team}'] or 0) / EXCHANGE_RATE) + (energy_packet_raw / 16) + snes_buffered_write(ctx, MMX_ENERGY_LINK_PACKET, bytearray([0x00, 0x00])) + await snes_flush_writes(ctx) + + # Expose EnergyLink to the ROM pause_state = await snes_read(ctx, MMX_PAUSE_STATE, 0x1) + screen_brightness = await snes_read(ctx, MMX_SCREEN_BRIGHTNESS, 0x1) + if pause_state[0] != 0x00 or screen_brightness[0] == 0x0F: + pool = ctx.stored_data[f'EnergyLink{ctx.team}'] or 0 + total_energy = int(pool / EXCHANGE_RATE) + if total_energy < 9999: + snes_buffered_write(ctx, MMX_ENERGY_LINK_COUNT, bytearray([total_energy & 0xFF, (total_energy >> 8) & 0xFF])) + else: + snes_buffered_write(ctx, MMX_ENERGY_LINK_COUNT, bytearray([0x0F, 0x27])) + + receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) + menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) + gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) + can_move = await snes_read(ctx, MMX_CAN_MOVE, 0x7) + if menu_state[0] != 0x04 or \ + gameplay_state[0] != 0x04 or \ + can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ + receiving_item[0] != 0x00: + return + + skip_hp = False + skip_weapon = False + for item in self.item_queue: + if item[0] == "hp refill": + skip_hp = True + self.heal_request_command = None + elif item[0] == "weapon refill": + skip_weapon = True + self.weapon_refill_request_command = None + + pool = ctx.stored_data[f'EnergyLink{ctx.team}'] or 0 + if not skip_hp or not skip_weapon: + # Handle in-game requests + request = int.from_bytes(await snes_read(ctx, MMX_REFILL_REQUEST, 0x1), "little") + target = int.from_bytes(await snes_read(ctx, MMX_REFILL_TARGET, 0x1), "little") + if request != 0: + if target == 0: + if self.heal_request_command is None: + self.heal_request_command = request + else: + if self.weapon_refill_request_command is None: + self.weapon_refill_request_command = request + snes_buffered_write(ctx, MMX_REFILL_REQUEST, bytearray([0x00])) + + if not skip_hp: + # Handle heal requests + if self.heal_request_command: + heal_needed = self.heal_request_command + heal_needed_rate = heal_needed * EXCHANGE_RATE + if pool < EXCHANGE_RATE: + logger.info(f"There's not enough Energy for your request ({heal_needed}). Energy available: {pool / EXCHANGE_RATE:.2f}") + self.heal_request_command = None + return + elif pool < heal_needed_rate: + heal_needed = int(pool / EXCHANGE_RATE) + heal_needed_rate = heal_needed * EXCHANGE_RATE + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": + [{"operation": "add", "value": -heal_needed_rate}, + {"operation": "max", "value": 0}], + }]) + self.add_item_to_queue("hp refill", None, heal_needed) + pool = (pool / EXCHANGE_RATE) - heal_needed + logger.info(f"Healed by {heal_needed}. Energy available: {pool:.2f}") + self.heal_request_command = None + + if not skip_weapon: + # Handle weapon refill requests + if self.weapon_refill_request_command: + heal_needed = self.weapon_refill_request_command + heal_needed_rate = heal_needed * EXCHANGE_RATE + if pool < EXCHANGE_RATE: + logger.info(f"There's not enough Energy for your request ({heal_needed}). Energy available: {pool / EXCHANGE_RATE:.2f}") + self.weapon_refill_request_command = None + return + elif pool < heal_needed_rate: + heal_needed = int(pool / EXCHANGE_RATE) + heal_needed_rate = heal_needed * EXCHANGE_RATE + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": + [{"operation": "add", "value": -heal_needed_rate}, + {"operation": "max", "value": 0}], + }]) + self.add_item_to_queue("weapon refill", None, heal_needed) + pool = (pool / EXCHANGE_RATE) - heal_needed + logger.info(f"Refilled current weapon by {heal_needed}. Energy available: {pool:.2f}") + self.weapon_refill_request_command = None + + + def add_item_to_queue(self, item_type, item_id, item_additional = None): + if not hasattr(self, "item_queue"): + self.item_queue = [] + self.item_queue.append([item_type, item_id, item_additional]) + + + async def handle_item_queue(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + from .Rom import weapon_rom_data, upgrades_rom_data + + if not hasattr(self, "item_queue") or len(self.item_queue) == 0: + return + + validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) + if validation is None: + return + validation = validation[0] | (validation[1] << 8) + if validation != 0xDEAD: + return + + # Do not give items if you can't move, are in pause state, not in the correct mode or not in gameplay state + receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) + menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) + gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) + can_move = await snes_read(ctx, MMX_CAN_MOVE, 0x7) + hp_refill = await snes_read(ctx, MMX_ENABLE_HP_REFILL, 0x1) + weapon_refill = await snes_read(ctx, MMX_ENABLE_WEAPON_REFILL, 0x1) if menu_state[0] != 0x04 or \ gameplay_state[0] != 0x04 or \ - pause_state[0] != 0x00 or \ + hp_refill[0] != 0x00 or \ + weapon_refill[0] != 0x00 or \ can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ receiving_item[0] != 0x00: - backup_item = self.item_queue.pop(0) - self.item_queue.append(backup_item) return + next_item = self.item_queue[0] + item_id = next_item[1] + if next_item[0] in PICKUP_ITEMS: backup_item = self.item_queue.pop(0) @@ -382,19 +740,29 @@ async def handle_item_queue(self, ctx): elif next_item[0] == "1up": life_count = await snes_read(ctx, MMX_LIFE_COUNT, 0x1) - if life_count[0] < 9: + if life_count[0] < 99: snes_buffered_write(ctx, MMX_ENABLE_GIVE_1UP, bytearray([0x01])) snes_buffered_write(ctx, MMX_RECEIVING_ITEM, bytearray([0x01])) - pass + self.save_arsenal = True else: self.item_queue.append(backup_item) + pause_state = await snes_read(ctx, MMX_PAUSE_STATE, 0x1) + screen_brightness = await snes_read(ctx, MMX_SCREEN_BRIGHTNESS, 0x1) + if pause_state[0] != 0x00 or screen_brightness[0] != 0x0F: + await snes_flush_writes(ctx) + if len(self.item_queue) != 0: + backup_item = self.item_queue.pop(0) + self.item_queue.append(backup_item) + return + if next_item[0] == "weapon": weapon = weapon_rom_data[item_id] snes_buffered_write(ctx, WRAM_START + weapon[0], bytearray([weapon[1]])) snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x0D])) self.item_queue.pop(0) + self.save_arsenal = True elif next_item[0] == "heart tank": heart_tanks = await snes_read(ctx, MMX_HEART_TANKS, 0x1) @@ -406,6 +774,7 @@ async def handle_item_queue(self, ctx): snes_buffered_write(ctx, MMX_ENABLE_HEART_TANK, bytearray([0x02])) snes_buffered_write(ctx, MMX_RECEIVING_ITEM, bytearray([0x01])) self.item_queue.pop(0) + self.save_arsenal = True elif next_item[0] == "sub tank": upgrades = await snes_read(ctx, MMX_UPGRADES, 0x1) @@ -423,6 +792,7 @@ async def handle_item_queue(self, ctx): snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x17])) self.item_queue.pop(0) + self.save_arsenal = True elif next_item[0] == "upgrade": upgrades = await snes_read(ctx, MMX_UPGRADES, 0x1) @@ -481,343 +851,149 @@ async def handle_item_queue(self, ctx): snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x2B])) self.item_queue.pop(0) + self.save_arsenal = True await snes_flush_writes(ctx) - async def game_watcher(self, ctx): - from SNIClient import snes_buffered_write, snes_flush_writes, snes_read - - game_state = await snes_read(ctx, MMX_GAME_STATE, 0x1) - menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) - gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) - - # Discard uninitialized ROMs - if menu_state is None: - self.game_state = False - self.energy_link_enabled = False - ctx.current_level_value = 42 - ctx.item_queue = [] - return - - if game_state[0] == 0: - self.game_state = False - ctx.item_queue = [] - ctx.current_level_value = 42 - return - - validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) - validation = validation[0] | (validation[1] << 8) - if validation != 0xDEAD: - snes_logger.info(f'ROM not properly validated.') - self.game_state = False - return - - self.game_state = True - if "DeathLink" in ctx.tags and menu_state[0] == 0x04 and ctx.last_death_link + 1 < time.time(): - currently_dead = gameplay_state[0] == 0x06 - await ctx.handle_deathlink_state(currently_dead) + async def handle_data_storage(self, ctx): + from SNIClient import snes_read, snes_buffered_write, snes_flush_writes + # Only do arsenal after the map's initial load or the intro stage is selected + menu_state = int.from_bytes(await snes_read(ctx, MMX_MENU_STATE, 0x1)) + gameplay_state = int.from_bytes(await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1)) + map_state = int.from_bytes(await snes_read(ctx, WRAM_START + 0x1E49, 0x1)) + sync_arsenal = int.from_bytes(await snes_read(ctx, MMX_ARSENAL_SYNC, 0x2), "little") + if (menu_state == 0x00 and map_state == 0x04) or (menu_state == 0x04 and gameplay_state == 0x04): + # Load Arsenal + if sync_arsenal == 0x1337: + arsenal = ctx.stored_data[f"mmx_arsenal_{ctx.team}_{ctx.slot}"] or dict() + if arsenal: + # Data in arsenal + snes_buffered_write(ctx, MMX_RECV_INDEX, bytes(arsenal["recv_index"].to_bytes(2, 'little'))) + snes_buffered_write(ctx, MMX_LIFE_COUNT, bytes(arsenal["life_count"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX_UPGRADES, bytes(arsenal["upgrades"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX_MAX_HP, bytes(arsenal["max_hp"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX_HEART_TANKS, bytes(arsenal["heart_tanks"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX_SUB_TANK_ARRAY, bytearray(arsenal["sub_tanks"])) + snes_buffered_write(ctx, MMX_UNLOCKED_CHARGED_SHOT, bytes(arsenal["unlocked_buster"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX_UNLOCKED_AIR_DASH, bytes(arsenal["unlocked_air_dash"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX_WEAPON_ARRAY, bytearray(arsenal["weapons"])) + snes_buffered_write(ctx, MMX_HADOUKEN, bytes(arsenal["hadouken"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX_UNLOCKED_LEVELS, bytearray(arsenal["levels"])) + snes_buffered_write(ctx, MMX_SIGMA_ACCESS, bytes(arsenal["sigma_access"].to_bytes(1, 'little'))) + + snes_buffered_write(ctx, MMX_ARSENAL_SYNC, bytearray([0x00,0x00])) + await snes_flush_writes(ctx) + + # Save Arsenal + if self.save_arsenal and sync_arsenal != 0x1337: + arsenal = dict() + arsenal["recv_index"] = int.from_bytes(await snes_read(ctx, MMX_RECV_INDEX, 0x2), "little") + arsenal["life_count"] = int.from_bytes(await snes_read(ctx, MMX_LIFE_COUNT, 0x1), "little") + arsenal["upgrades"] = int.from_bytes(await snes_read(ctx, MMX_UPGRADES, 0x1), "little") + arsenal["max_hp"] = int.from_bytes(await snes_read(ctx, MMX_MAX_HP, 0x1), "little") + arsenal["heart_tanks"] = int.from_bytes(await snes_read(ctx, MMX_HEART_TANKS, 0x1), "little") + arsenal["sub_tanks"] = list(await snes_read(ctx, MMX_SUB_TANK_ARRAY, 0x4)) + arsenal["unlocked_buster"] = int.from_bytes(await snes_read(ctx, MMX_UNLOCKED_CHARGED_SHOT, 0x1), "little") + arsenal["unlocked_air_dash"] = int.from_bytes(await snes_read(ctx, MMX_UNLOCKED_AIR_DASH, 0x1), "little") + arsenal["weapons"] = list(await snes_read(ctx, MMX_WEAPON_ARRAY, 0x10)) + arsenal["hadouken"] = int.from_bytes(await snes_read(ctx, MMX_HADOUKEN, 0x1), "little") + arsenal["levels"] = list(await snes_read(ctx, MMX_UNLOCKED_LEVELS, 0x20)) + arsenal["sigma_access"] = int.from_bytes(await snes_read(ctx, MMX_SIGMA_ACCESS, 0x1), "little") - if self.trade_request is not None: - await self.handle_hp_trade(ctx) + # Attempt to not lose any previously saved data in case of RAM corruption + saved_arsenal = ctx.stored_data[f"mmx_arsenal_{ctx.team}_{ctx.slot}"] or dict() + if saved_arsenal: + if saved_arsenal["recv_index"] > arsenal["recv_index"]: + arsenal["recv_index"] = saved_arsenal["recv_index"] + if saved_arsenal["life_count"] > arsenal["life_count"]: + arsenal["life_count"] = saved_arsenal["life_count"] + if saved_arsenal["max_hp"] > arsenal["max_hp"]: + arsenal["max_hp"] = saved_arsenal["max_hp"] + for i in range(0x10): + arsenal["weapons"][i] |= saved_arsenal["weapons"][i] & 0x40 + for level in range(0x20): + arsenal["levels"][level] |= saved_arsenal["levels"][level] + arsenal["sigma_access"] = min(saved_arsenal["sigma_access"], arsenal["sigma_access"]) + + arsenal["upgrades"] |= saved_arsenal["upgrades"] + arsenal["unlocked_buster"] |= saved_arsenal["unlocked_buster"] + arsenal["unlocked_air_dash"] |= saved_arsenal["unlocked_air_dash"] + arsenal["hadouken"] |= saved_arsenal["hadouken"] & 0xE0 + arsenal["heart_tanks"] |= saved_arsenal["heart_tanks"] + for i in range(0x4): + arsenal["sub_tanks"][i] |= saved_arsenal["sub_tanks"][i] & 0x80 - await self.handle_item_queue(ctx) - - # This is going to be rewritten whenever SNIClient supports on_package - energy_link = await snes_read(ctx, MMX_ENERGY_LINK_ENABLED, 0x1) - if self.using_newer_client: - if energy_link[0] != 0: - await self.handle_energy_link(ctx) + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx_arsenal_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": arsenal}], + }]) + self.save_arsenal = False + + # Checkpoints reached + checkpoints = list(await snes_read(ctx, MMX_CHECKPOINTS_REACHED, 0xF)) + data_storage_checkpoints = ctx.stored_data[f"mmx_checkpoints_{ctx.team}_{ctx.slot}"] or [0 for _ in range(0xF)] + computed_checkpoints = list() + for i in range(0xF): + if checkpoints[i] >= data_storage_checkpoints[i]: + computed_checkpoints.append(checkpoints[i]) + else: + computed_checkpoints.append(data_storage_checkpoints[i]) + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx_checkpoints_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": computed_checkpoints}], + }]) + snes_buffered_write(ctx, MMX_CHECKPOINTS_REACHED, bytes(computed_checkpoints)) + + # Global timer + timer = int.from_bytes(await snes_read(ctx, MMX_GLOBAL_TIMER, 0x4), "little") + data_storage_timer = ctx.stored_data[f"mmx_global_timer_{ctx.team}_{ctx.slot}"] or 0 + if timer >= data_storage_timer: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx_global_timer_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": timer}, + {"operation": "min", "value": 0x03E73B3B}], + }]) else: - if energy_link[0] != 0: - if self.energy_link_enabled and f'EnergyLink{ctx.team}' in ctx.stored_data: - await self.handle_energy_link(ctx) - - if ctx.server and ctx.server.socket.open and not self.energy_link_enabled and ctx.team is not None: - self.energy_link_enabled = True - ctx.set_notify(f"EnergyLink{ctx.team}") - logger.info(f"Initialized EnergyLink{ctx.team}") - self.energy_link_details = True - logger.info(f"EnergyLink detailed deposit activity enabled.") + snes_buffered_write(ctx, MMX_GLOBAL_TIMER, data_storage_timer.to_bytes(4, "little")) - from .Rom import weapon_rom_data, upgrades_rom_data, boss_access_rom_data, refill_rom_data - from .Levels import location_id_to_level_id - from worlds import AutoWorldRegister - - defeated_bosses_data = await snes_read(ctx, MMX_DEFEATED_BOSSES, 0x20) - defeated_bosses = list(defeated_bosses_data) - cleared_levels_data = await snes_read(ctx, MMX_COMPLETED_LEVELS, 0x20) - cleared_levels = list(cleared_levels_data) - collected_heart_tanks_data = await snes_read(ctx, MMX_COLLECTED_HEART_TANKS, 0x01) - collected_upgrades_data = await snes_read(ctx, MMX_COLLECTED_UPGRADES, 0x01) - collected_hadouken_data = await snes_read(ctx, MMX_COLLECTED_HADOUKEN, 0x01) - collected_pickups_data = await snes_read(ctx, MMX_COLLECTED_PICKUPS, 0x20) - collected_pickups = list(collected_pickups_data) - pickupsanity_enabled = await snes_read(ctx, MMX_PICKUPSANITY_ACTIVE, 0x1) - completed_intro_level = await snes_read(ctx, WRAM_START + 0x01F9B, 0x1) - new_checks = [] - for loc_name, data in location_id_to_level_id.items(): - loc_id = AutoWorldRegister.world_types[ctx.game].location_name_to_id[loc_name] - if loc_id not in ctx.locations_checked: - internal_id = data[1] - data_bit = data[2] + # Death count + deaths = int.from_bytes(await snes_read(ctx, MMX_GLOBAL_DEATHS, 0x2), "little") + data_storage_deaths = ctx.stored_data[f"mmx_deaths_{ctx.team}_{ctx.slot}"] or 0 + if deaths >= data_storage_deaths: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx_deaths_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": deaths}, + {"operation": "min", "value": 9999}], + }]) + else: + snes_buffered_write(ctx, MMX_GLOBAL_DEATHS, data_storage_deaths.to_bytes(2, "little")) - if internal_id == 0x000: - # Boss clear - if defeated_bosses_data[data_bit] != 0: - new_checks.append(loc_id) - elif internal_id == 0x001: - # Maverick Medal - if cleared_levels_data[data_bit] != 0: - new_checks.append(loc_id) - elif internal_id == 0x002: - # Heart Tank - masked_data = collected_heart_tanks_data[0] & data_bit - if masked_data != 0: - new_checks.append(loc_id) - elif internal_id == 0x003: - # Mega Man upgrades - masked_data = collected_upgrades_data[0] & data_bit - if masked_data != 0: - new_checks.append(loc_id) - elif internal_id == 0x004: - # Sub Tank - masked_data = collected_upgrades_data[0] & data_bit - if masked_data != 0: - new_checks.append(loc_id) - elif internal_id == 0x005: - # Hadouken - if collected_hadouken_data[0] != 0x00: - new_checks.append(loc_id) - elif internal_id == 0x007: - # Intro - if game_state[0] == 0x02 and \ - menu_state[0] == 0x00 and \ - gameplay_state[0] == 0x01 and \ - completed_intro_level[0] == 0x04: - new_checks.append(loc_id) - elif internal_id == 0x020: - # Pickups - if not pickupsanity_enabled or pickupsanity_enabled[0] == 0: - continue - if collected_pickups_data[data_bit] != 0: - new_checks.append(loc_id) - - verify_game_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 1) - if verify_game_state is None: - snes_logger.info(f'Exit Game.') - return + # Damage dealt + dmg_dealt = int.from_bytes(await snes_read(ctx, MMX_GLOBAL_DMG_DEALT, 0x2), "little") + data_storage_dmg_dealt = ctx.stored_data[f"mmx_damage_dealt_{ctx.team}_{ctx.slot}"] or 0 + if dmg_dealt >= data_storage_dmg_dealt: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx_damage_dealt_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": dmg_dealt}, + {"operation": "min", "value": 9999}], + }]) + else: + snes_buffered_write(ctx, MMX_GLOBAL_DMG_DEALT, data_storage_dmg_dealt.to_bytes(2, "little")) - rom = await snes_read(ctx, MMX_ROMHASH_START, ROMHASH_SIZE) - if rom != ctx.rom: - ctx.rom = None - snes_logger.info(f'Exit ROM.') - return + # Damage taken + dmg_taken = int.from_bytes(await snes_read(ctx, MMX_GLOBAL_DMG_TAKEN, 0x2), "little") + data_storage_dmg_taken = ctx.stored_data[f"mmx_damage_taken_{ctx.team}_{ctx.slot}"] or 0 + if dmg_taken >= data_storage_dmg_taken: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx_damage_taken_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": dmg_taken}, + {"operation": "min", "value": 9999}], + }]) + else: + snes_buffered_write(ctx, MMX_GLOBAL_DMG_TAKEN, data_storage_dmg_taken.to_bytes(2, "little")) - for new_check_id in new_checks: - ctx.locations_checked.add(new_check_id) - location = ctx.location_names.lookup_in_game(new_check_id) - snes_logger.info( - f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') - await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) - - # Send Current Room for Tracker - current_level = int.from_bytes(await snes_read(ctx, MMX_LEVEL_INDEX, 0x1), "little") - - if game_state[0] == 0x00 or \ - (game_state[0] == 0x02 and menu_state[0] != 0x04): - current_level = -1 - - if ctx.current_level_value != (current_level + 1): - ctx.current_level_value = current_level + 1 - - # Send level id data to tracker - await ctx.send_msgs( - [ - { - "cmd": "Set", - "key": f"mmx1_level_id_{ctx.team}_{ctx.slot}", - "default": 0, - "want_reply": False, - "operations": [ - { - "operation": "replace", - "value": ctx.current_level_value, - } - ], - } - ] - ) - - recv_count = await snes_read(ctx, MMX_RECV_INDEX, 2) - if recv_count is None: - # Add a small failsafe in case we get a None. Other SNI games do this... - return - - recv_index = recv_count[0] | (recv_count[1] << 8) - - if recv_index < len(ctx.items_received): - item = ctx.items_received[recv_index] - recv_index += 1 - sending_game = ctx.slot_info[item.player].game - logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'), - color(ctx.player_names[item.player], 'yellow'), - ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) - - snes_buffered_write(ctx, MMX_RECV_INDEX, bytes([recv_index])) - await snes_flush_writes(ctx) - - if item.item in weapon_rom_data: - self.add_item_to_queue("weapon", item.item) - - elif item.item == STARTING_ID + 0x0013: - self.add_item_to_queue("heart tank", item.item) - - elif item.item == STARTING_ID + 0x0014: - self.add_item_to_queue("sub tank", item.item) - - elif item.item in upgrades_rom_data: - self.add_item_to_queue("upgrade", item.item) - - elif item.item in boss_access_rom_data: - if item.item == STARTING_ID + 0x000A: - snes_buffered_write(ctx, MMX_SIGMA_ACCESS, bytearray([0x00])) - boss_access = await snes_read(ctx, MMX_UNLOCKED_LEVELS, 0x20) - boss_access = bytearray(boss_access) - level = boss_access_rom_data[item.item] - boss_access[level[0]] = 0x01 - snes_buffered_write(ctx, MMX_UNLOCKED_LEVELS, boss_access) - snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) - snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x2D])) - - elif item.item in refill_rom_data: - self.add_item_to_queue(refill_rom_data[item.item][0], item.item, refill_rom_data[item.item][1]) - - elif item.item == STARTING_ID: - # Handle goal - await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) - ctx.finished_game = True - return - - # Handle collected locations - game_state = await snes_read(ctx, MMX_GAME_STATE, 0x1) - if game_state[0] != 0x02: - ctx.locations_checked = set() - return - new_boss_clears = False - new_cleared_level = False - new_heart_tank = False - new_upgrade = False - new_pickup = False - new_hadouken = False - cleared_levels_data = await snes_read(ctx, MMX_COMPLETED_LEVELS, 0x20) - cleared_levels = list(cleared_levels_data) - collected_pickups_data = await snes_read(ctx, MMX_COLLECTED_PICKUPS, 0x40) - collected_pickups = list(collected_pickups_data) - collected_heart_tanks_data = await snes_read(ctx, MMX_COLLECTED_HEART_TANKS, 0x01) - collected_heart_tanks_data = collected_heart_tanks_data[0] - collected_upgrades_data = await snes_read(ctx, MMX_COLLECTED_UPGRADES, 0x01) - collected_upgrades_data = collected_upgrades_data[0] - defeated_bosses_data = await snes_read(ctx, MMX_DEFEATED_BOSSES, 0x20) - defeated_bosses = list(defeated_bosses_data) - collected_hadouken_data = await snes_read(ctx, MMX_COLLECTED_HADOUKEN, 0x01) - collected_hadouken_data = collected_hadouken_data[0] - i = 0 - for loc_id in ctx.checked_locations: - if loc_id not in ctx.locations_checked: - ctx.locations_checked.add(loc_id) - loc_name = ctx.location_names.lookup_in_game(loc_id) - - if loc_name not in location_id_to_level_id: - continue - - logging.info(f"Recovered checks ({i:03}): {loc_name}") - i += 1 - - data = location_id_to_level_id[loc_name] - level_id = data[0] - internal_id = data[1] - data_bit = data[2] - - if internal_id == 0x000: - # Boss clear - defeated_bosses[data_bit] = 1 - new_boss_clears = True - elif internal_id == 0x001: - # Maverick Medal - cleared_levels[data_bit] = 0xFF - new_cleared_level = True - elif internal_id == 0x002: - # Heart Tank - collected_heart_tanks_data |= data_bit - new_heart_tank = True - elif internal_id == 0x003: - # Mega Man upgrades - collected_upgrades_data |= data_bit - new_upgrade = True - elif internal_id == 0x004: - # Sub Tank - collected_upgrades_data |= data_bit - new_upgrade = True - elif internal_id == 0x005: - # Hadouken - collected_hadouken_data = 0xFF - new_hadouken = True - elif internal_id == 0x20: - # Pickups - collected_pickups[data_bit] = 0x01 - new_pickup = True - - if new_cleared_level: - snes_buffered_write(ctx, MMX_COMPLETED_LEVELS, bytes(cleared_levels)) - if new_boss_clears: - snes_buffered_write(ctx, MMX_DEFEATED_BOSSES, bytes(defeated_bosses)) - if new_pickup: - snes_buffered_write(ctx, MMX_COLLECTED_PICKUPS, bytes(collected_pickups)) - if new_hadouken: - snes_buffered_write(ctx, MMX_COLLECTED_HADOUKEN, bytearray([collected_hadouken_data])) - if new_upgrade: - snes_buffered_write(ctx, MMX_COLLECTED_UPGRADES, bytearray([collected_upgrades_data])) - if new_heart_tank: - snes_buffered_write(ctx, MMX_COLLECTED_HEART_TANKS, bytearray([collected_heart_tanks_data])) - await snes_flush_writes(ctx) - - def on_package(self, ctx, cmd: str, args: dict): - super().on_package(ctx, cmd, args) - - if cmd == "Connected": - slot_data = args.get("slot_data", None) - self.using_newer_client = True - if slot_data["energy_link"]: - ctx.set_notify(f"EnergyLink{ctx.team}") - if ctx.ui: - ctx.ui.enable_energy_link() - ctx.ui.energy_link_label.text = "Energy: Standby" - logger.info(f"Initialized EnergyLink{ctx.team}") - - elif cmd == "SetReply" and args["key"].startswith("EnergyLink"): - if ctx.ui: - pool = (args["value"] or 0) / EXCHANGE_RATE - ctx.ui.energy_link_label.text = f"Energy: {pool:.2f}" - - elif cmd == "Retrieved": - if f"EnergyLink{ctx.team}" in args["keys"] and args["keys"][f"EnergyLink{ctx.team}"] and ctx.ui: - pool = (args["keys"][f"EnergyLink{ctx.team}"] or 0) / EXCHANGE_RATE - ctx.ui.energy_link_label.text = f"Energy: {pool:.2f}" - - -def cmd_pool(self): - """ - Check how much energy is in the pool. - """ - if self.ctx.game != "Mega Man X": - logger.warning("This command can only be used while playing Mega Man X") - if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: - logger.info(f"Must be connected to server and in game.") - else: - pool = (self.ctx.stored_data[f'EnergyLink{self.ctx.team}'] or 0) / EXCHANGE_RATE - logger.info(f"Energy available: {pool:.2f}") + await snes_flush_writes(ctx) def cmd_heal(self, amount: str = ""): @@ -873,24 +1049,6 @@ def cmd_refill(self, amount: str = ""): else: logger.info(f"You need to specify how much Weapon Energy you will request.") - -def cmd_autoheal(self): - """ - Enable auto heal from EnergyLink. - """ - if self.ctx.game != "Mega Man X": - logger.warning("This command can only be used while playing Mega Man X") - if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: - logger.info(f"Must be connected to server and in game.") - else: - if self.ctx.client_handler.auto_heal: - self.ctx.client_handler.auto_heal = False - logger.info(f"Auto healing disabled.") - else: - self.ctx.client_handler.auto_heal = True - logger.info(f"Auto healing enabled.") - - def cmd_trade(self, amount: str = ""): """ Trades HP to Weapon Energy. 1:1 ratio. @@ -917,18 +1075,19 @@ def cmd_trade(self, amount: str = ""): else: logger.info(f"You need to specify how much Weapon Energy you will request.") -def cmd_details(self): + +def cmd_resync(self): """ - Toggles displaying energy deposit activity into the console when EnergyLink is active. + Resets the save data to force Archipelago to send over every item again. Locations reached aren't affected. """ if self.ctx.game != "Mega Man X": logger.warning("This command can only be used while playing Mega Man X") - if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: - logger.info(f"Must be connected to server and in game.") + if (not self.ctx.server) or self.ctx.server.socket.closed or self.ctx.client_handler.game_state: + logger.info(f"Must be connected to server and in the title screen.") else: - if self.ctx.client_handler.energy_link_details: - self.ctx.client_handler.energy_link_details = False - logger.info(f"EnergyLink detailed deposit activity disabled.") + if self.ctx.client_handler.resync_request: + logger.info(f"You already placed a resync request.") + return else: - self.ctx.client_handler.energy_link_details = True - logger.info(f"EnergyLink detailed deposit activity enabled.") + self.ctx.client_handler.resync_request = True + logger.info(f"Placing a resync request...") diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index aa4538d28022..47dc789ecbb4 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -10,8 +10,11 @@ class EnergyLink(DefaultOnToggle): """ Enable EnergyLink support. - EnergyLink works as a big Sub Tank/HP pool where players can request HP manually or automatically when - they lose HP. You make use of this feature by typing /pool, /heal or /autoheal in the client. + + EnergyLink in MMX2 works as a big HP and Weapon Energy pool that the players can use to request HP + or Weapon Energy whenever they need to. + + You make use of this feature by typing /heal or /refill in the client. """ display_name = "Energy Link" @@ -22,7 +25,7 @@ class StartingLifeCount(Range): """ display_name = "Starting Life Count" range_start = 0 - range_end = 9 + range_end = 99 default = 2 class StartingHP(Range): @@ -266,7 +269,7 @@ class LongJumps(Toggle): class ChillPenguinTweaks(OptionSet): """ - Additional behavior to Chill Penguin + Behavior options for Chill Penguin. Everything can be stacked. """ display_name = "Chill Penguin Tweaks" valid_keys = { @@ -289,7 +292,7 @@ class ChillPenguinTweaks(OptionSet): class ArmoredArmadilloTweaks(OptionSet): """ - Additional behavior to Armored Armadillo + Behavior options for Armored Armadillo. Everything can be stacked. """ display_name = "Armored Armadillo Tweaks" valid_keys = { @@ -309,7 +312,7 @@ class ArmoredArmadilloTweaks(OptionSet): class SparkMandrillTweaks(OptionSet): """ - Additional behavior to Spark Mandrill + Behavior options for Spark Mandrill. Everything can be stacked. """ display_name = "Spark Mandrill Tweaks" valid_keys = { diff --git a/worlds/mmx/Regions.py b/worlds/mmx/Regions.py index 29791b10c216..b5c412d50da8 100644 --- a/worlds/mmx/Regions.py +++ b/worlds/mmx/Regions.py @@ -389,18 +389,18 @@ def connect_regions(world: World): connect(world, RegionName.sigma_fortress_3_rematch_4, RegionName.sigma_fortress_3_rematch_5) connect(world, RegionName.sigma_fortress_3_rematch_5, RegionName.sigma_fortress_3_boss) - connect(world, RegionName.sigma_fortress_3_boss, RegionName.sigma_fortress_4) connect(world, RegionName.sigma_fortress_4, RegionName.sigma_fortress_4_dog) connect(world, RegionName.sigma_fortress_4_dog, RegionName.sigma_fortress_4_sigma) if world.options.sigma_all_levels: connect(world, RegionName.sigma_fortress, RegionName.sigma_fortress_2) connect(world, RegionName.sigma_fortress, RegionName.sigma_fortress_3) + connect(world, RegionName.sigma_fortress, RegionName.sigma_fortress_4) else: connect(world, RegionName.sigma_fortress_1_boss, RegionName.sigma_fortress_2) connect(world, RegionName.sigma_fortress_2_boss, RegionName.sigma_fortress_3) + connect(world, RegionName.sigma_fortress_3_boss, RegionName.sigma_fortress_4) - def create_region(multiworld: MultiWorld, player: int, active_locations, name: str, locations=None): ret = Region(name, player, multiworld) diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index 1c0f4da099c9..79d0dfe98656 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -177,7 +177,7 @@ def adjust_boss_damage_table(world: World, patch: MMXProcedurePatch): data[i] = entry[1] i += 1 patch.write_bytes(offset, bytearray(data)) - print (f"Boss: {_} | Offset: 0x{offset - 0x17E9A2:04X}") + #print (f"Boss: {_} | Offset: 0x{offset - 0x17E9A2:04X}") offset += 8 @@ -279,7 +279,7 @@ def patch_rom(world: World, patch: MMXProcedurePatch): final_value = 0 for tweak in selected_tweaks: final_value |= enemy_tweaks_indexes[boss][tweak] - print (f"Boss: {boss} | Offset: 0x{offset:06X} | Config: 0x{final_value:04X}") + #print (f"Boss: {boss} | Offset: 0x{offset:06X} | Config: 0x{final_value:04X}") patch.write_bytes(offset, bytearray([final_value & 0xFF, (final_value >> 8) & 0xFF])) # Edit the ROM header @@ -301,23 +301,23 @@ def patch_rom(world: World, patch: MMXProcedurePatch): value |= 0x08 if "Sub Tanks" in sigma_open: value |= 0x10 - patch.write_byte(0x17FFE0, value) - patch.write_byte(0x17FFE1, world.options.sigma_medal_count.value) - patch.write_byte(0x17FFE2, world.options.sigma_weapon_count.value) - patch.write_byte(0x17FFE3, world.options.sigma_upgrade_count.value) - patch.write_byte(0x17FFE4, world.options.sigma_heart_tank_count.value) - patch.write_byte(0x17FFE5, world.options.sigma_sub_tank_count.value) - patch.write_byte(0x17FFE6, world.options.starting_life_count.value) - patch.write_byte(0x17FFE7, world.options.pickupsanity.value) - patch.write_byte(0x17FFE8, world.options.energy_link.value) - patch.write_byte(0x17FFE9, world.options.death_link.value) - patch.write_byte(0x17FFEA, world.options.jammed_buster.value) - patch.write_byte(0x17FFEB, world.options.logic_boss_weakness.value) - patch.write_byte(0x17FFEC, world.options.boss_weakness_rando.value) - patch.write_byte(0x17FFED, world.options.starting_hp.value) - patch.write_byte(0x17FFEE, world.options.heart_tank_effectiveness.value) - patch.write_byte(0x17FFEF, world.options.sigma_all_levels.value) - patch.write_byte(0x17FFF0, world.options.boss_weakness_strictness.value) + patch.write_byte(0x167C20, value) + patch.write_byte(0x167C21, world.options.sigma_medal_count.value) + patch.write_byte(0x167C22, world.options.sigma_weapon_count.value) + patch.write_byte(0x167C23, world.options.sigma_upgrade_count.value) + patch.write_byte(0x167C24, world.options.sigma_heart_tank_count.value) + patch.write_byte(0x167C25, world.options.sigma_sub_tank_count.value) + patch.write_byte(0x167C26, world.options.starting_life_count.value) + patch.write_byte(0x167C27, world.options.pickupsanity.value) + patch.write_byte(0x167C28, world.options.energy_link.value) + patch.write_byte(0x167C29, world.options.death_link.value) + patch.write_byte(0x167C2A, world.options.jammed_buster.value) + patch.write_byte(0x167C2B, world.options.logic_boss_weakness.value) + patch.write_byte(0x167C2C, world.options.boss_weakness_rando.value) + patch.write_byte(0x167C2D, world.options.starting_hp.value) + patch.write_byte(0x167C2E, world.options.heart_tank_effectiveness.value) + patch.write_byte(0x167C2F, world.options.sigma_all_levels.value) + patch.write_byte(0x167C30, world.options.boss_weakness_strictness.value) value = 0 if world.options.better_walljump.value: @@ -326,7 +326,7 @@ def patch_rom(world: World, patch: MMXProcedurePatch): value |= 0x02 if world.options.long_jumps.value: value |= 0x04 - patch.write_byte(0x17FFF1, value) + patch.write_byte(0x167C31, value) patch.write_file("token_patch.bin", patch.get_token_binary()) diff --git a/worlds/mmx/Rules.py b/worlds/mmx/Rules.py index 6cd37140c3db..1b712871a951 100644 --- a/worlds/mmx/Rules.py +++ b/worlds/mmx/Rules.py @@ -1,4 +1,5 @@ from worlds.generic.Rules import add_rule, set_rule +from BaseClasses import CollectionState from . import MMXWorld from .Names import LocationName, ItemName, RegionName, EventName @@ -111,7 +112,7 @@ def set_rules(world: MMXWorld): # Sigma Fortress level rules if world.options.sigma_all_levels: - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_boss} -> {RegionName.sigma_fortress_4}", player), + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress} -> {RegionName.sigma_fortress_4}", player), lambda state: ( state.has(EventName.sigma_fortress_1_clear, player) and state.has(EventName.sigma_fortress_2_clear, player) and @@ -233,6 +234,14 @@ def add_pickupsanity_logic(world: MMXWorld): )) +def check_weaknesses(state: CollectionState, player: int, rulesets: list) -> bool: + states = list() + for i in range(len(rulesets)): + valid = state.has_all_counts(rulesets[i], player) + states.append(valid) + return any(states) + + def add_boss_weakness_logic(world: MMXWorld): player = world.player multiworld = world.multiworld @@ -240,22 +249,28 @@ def add_boss_weakness_logic(world: MMXWorld): for boss, regions in bosses.items(): weaknesses = world.boss_weaknesses[boss] + rulesets = list() for weakness in weaknesses: if weakness[0] is None: - continue + rulesets = None + break weakness = weakness[0] + ruleset = dict() + if "Check Charge" in weakness[0]: + ruleset[ItemName.arms] = jammed_buster + int(weakness[0][-1:]) - 1 + elif "Check Dash" in weakness[0]: + ruleset[ItemName.legs] = 1 + else: + ruleset[weakness[0]] = 1 + if len(weakness) != 1: + ruleset[weakness[1]] = 1 + rulesets.append(ruleset) + + if rulesets is not None: for region in regions: - ruleset = {} - if "Check Charge" in weakness[0]: - ruleset[ItemName.arms] = jammed_buster + int(weakness[0][-1:]) - 1 - elif "Check Dash" in weakness[0]: - ruleset[ItemName.legs] = 1 - else: - ruleset[weakness[0]] = 1 - if len(weakness) != 1: - ruleset[weakness[1]] = 1 add_rule(multiworld.get_entrance(region, player), - lambda state, ruleset=ruleset: state.has_all_counts(ruleset, player)) + lambda state, rulesets=rulesets: check_weaknesses(state, player, rulesets)) + def add_charged_shotgun_ice_logic(world: MMXWorld): player = world.player diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index 8ab31a520653..3284dc51f746 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -41,15 +41,27 @@ class MMXWeb(WebWorld): "setup/en", ["lx5"] ) + + setup_es = Tutorial( + "Guía de configuración de Multiworld", + "Guía para jugar Mega Man X en Archipelago", + "Spanish", + "setup_es.md", + "setup/es", + ["lx5"] + ) - tutorials = [setup_en] + tutorials = [setup_en, setup_es] option_groups = mmx_option_groups class MMXWorld(World): """ - Mega Man X WIP + Mega Man X for the SNES is a classic action-platformer game released in 1993. It's a spin-off of + the original Mega Man series and introduces players to a new protagonist, X, a Maverick Hunter in a + futuristic world. The game features upgraded gameplay mechanics, such as the ability to dash and + climb walls, which were new to the series at the time. """ game = "Mega Man X" web = MMXWeb() @@ -59,7 +71,7 @@ class MMXWorld(World): options_dataclass = MMXOptions options: MMXOptions - required_client_version = (0, 4, 6) + required_client_version = (0, 5, 0) item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = all_locations @@ -228,9 +240,15 @@ def create_item(self, name: str, force_classification=False) -> Item: def set_rules(self): from .Rules import set_rules + if hasattr(self.multiworld, "generation_is_fake"): + if hasattr(self.multiworld, "re_gen_passthrough"): + if "Mega Man X" in self.multiworld.re_gen_passthrough: + slot_data = self.multiworld.re_gen_passthrough["Mega Man X"] + self.boss_weaknesses = slot_data["weakness_rules"] set_rules(self) + def fill_slot_data(self): slot_data = {} # Write options to slot_data @@ -264,13 +282,15 @@ def fill_slot_data(self): slot_data["sigma_heart_tank_count"] = self.options.sigma_heart_tank_count.value slot_data["sigma_sub_tank_count"] = self.options.sigma_sub_tank_count.value - # Write boss weaknesses to slot_data + # Write boss weaknesses to slot_data (and for UT) slot_data["boss_weaknesses"] = {} + slot_data["weakness_rules"] = {} for boss, entries in self.boss_weaknesses.items(): + slot_data["weakness_rules"][boss] = entries.copy() slot_data["boss_weaknesses"][boss] = [] for entry in entries: slot_data["boss_weaknesses"][boss].append(entry[1]) - + return slot_data @@ -278,11 +298,19 @@ def generate_early(self): if ItemName.legs not in self.options.start_inventory_from_pool and self.options.early_legs: self.multiworld.early_items[self.player][ItemName.legs] = 1 - self.boss_weaknesses = {} + # Generate weaknesses self.boss_weakness_data = {} + self.boss_weaknesses = {} handle_weaknesses(self) + def interpret_slot_data(self, slot_data): + local_weaknesses = dict() + for boss, entries in slot_data["weakness_rules"].items(): + local_weaknesses[boss] = entries.copy() + return {"weakness_rules": local_weaknesses} + + def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: if self.options.boss_weakness_rando != "vanilla": spoiler_handle.write(f"\nMega Man X boss weaknesses for {self.multiworld.player_name[self.player]}:\n") diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 31e2958df80ab7db8e81cd9ff172e42484245339..54cd4f9df0f3556bf075cd1a81a8f924b527ed5a 100644 GIT binary patch literal 12305 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 KINL}Kfd2zWY~?Ee literal 7460 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 diff --git a/worlds/mmx/docs/en_Mega Man X.md b/worlds/mmx/docs/en_Mega Man X.md index 6336d3433c97..9fd83dfb94c3 100644 --- a/worlds/mmx/docs/en_Mega Man X.md +++ b/worlds/mmx/docs/en_Mega Man X.md @@ -1,3 +1,75 @@ -# Mega Man X3 +# Mega Man X -WIP +## Where is the options page? + +The [player options page for this game](../player-options) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Access to each Maverick Stage, weapons obtained from Mavericks and upgrades obtained from Dr. Light's capsules +are randomized in the multiworld. The requirements for entering Sigma's Fortress can be randomized to require different +amount of items (Medals from Mavericks, Weapon count, Upgrade count, Heart Tank and Sub Tank count). + +The game will be marked as completed when Wolf Sigma is defeated. + +## What Mega Man X items can appear in other players' worlds? +- Maverick Access Codes +- Maverick Weapons +- Armor Upgrades (Helmet/Arms/Body/Legs) +- Heart Tanks +- Sub Tanks +- 1-Ups +- HP Refill + +## What is considered a location check in Mega Man X? +- Defeating a Boss Enemy +- Using a Dr. Light Capsule +- Collecting a Heart Tank or a Sub Tank item +- Optionally, collecting a Pickup Item (1-Up/HP/Weapon) present within stages + +## When the player receives an item, what happens? +A sound effect will play based on the type of item received, and the effects of the item will be immediately applied, +such as unlocking the use of a weapon mid-stage. If the effects of the item cannot be fully applied (such as receiving +a HP refill while at full health), the remaining are withheld until they can be applied. + +## Quality of Life +The implementation features several enhancements to the original game's systems which attempt to make Mega Man X a +much smoother experience. +- **Checkpoint Selector:** Allows you to travel to any previously visited checkpoint in the game by selecting a +checkpoint at the stage select screen. Switch between different checkpoints with `L` or `R`. +- **Enhanced Helmet:** By getting the Helmet Upgrade item, the Checkpoint Selector will allow you to travel to any +checkpoint regardless if you have visited them or not. +- **Sigma's Fortress Selector:** You can switch which X-Hunter Base level you will travel to by pressing `SELECT` at +the stage select screen. + +## What is EnergyLink? +EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld. In Mega Man + X, when enabled, deposits a certain amount of Energy to the EnergyLink pool. Only a quarter of the collected Energy is +successfully sent to the EnergyLink pool. + +Energy from the EnergyLink pool can be transmuted into HP and Weapon Energy with the same conversion rate. +The transmutation can happen within the game itself and the client. In the client, you use `/heal ` to request +a heal by `` or use a `/refill ` to request a weapon refill. In the game, you press `SELECT` on the item +you want to request a refill of during the pause menu screen. + +Weapon refills will be applied to either the current weapon, the current selected weapon on the pause menu or will be +filled from top to bottom according to the pause menu's order if none of them are selected or being used. + +## Boss weakness plando +You can enforce a singular weakness into a boss with this option, ignoring weaknesses generated by the world in case +weaknesses are shuffled. The format is the following: +```yaml +boss_weakness_plando: + Spark Mandrill: Lemon (Dash) + Armored Armadillo: Electric Spark +``` +This will force `Spark Mandrill` to receive increased damage from the basic shot performed when dashing and will force +`Armored Armadillo` to receive increased damage from Electric Spark. + +## Unique Local Commands +- `/resync` Deletes the current saved data in the server which will force every item to be given again. Only has +effect during the title screen. +- `/heal ` Only present with EnergyLink. Request a HP refill using EnergyLink's pool. +- `/refill ` Only present with EnergyLink. Request a Weapon Energy refill using EnergyLink's pool. +- `/trade ` Exchanges HP for Weapon Energy. The conversion rate is 1:1. diff --git a/worlds/mmx/docs/setup_en.md b/worlds/mmx/docs/setup_en.md index 39f22b126691..ea6f9c71b84b 100644 --- a/worlds/mmx/docs/setup_en.md +++ b/worlds/mmx/docs/setup_en.md @@ -1,28 +1,35 @@ -# Mega Man X3 Randomizer Setup Guide +# Mega Man X setup guide ## Required Software -- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). - -- Hardware or software capable of loading and playing SNES ROM files - - An emulator capable of connecting to SNI such as: - - snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases), - - BizHawk from: [TASVideos](https://tasvideos.org/BizHawk) - - RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or, - - An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other - compatible hardware -- Your legally obtained Mega Man X3 ROM file. Can be either a dump of the original SNES cartridge or - the Mega Man X Legacy Collection version. - -## Installation Procedures - -### Windows Setup +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). +- [SNI](https://github.com/alttpo/sni/releases). This is automatically included with your Archipelago installation above. +- Software capable of loading and playing SNES ROM files: + - [snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases) + - [snes9x-rr](https://github.com/gocha/snes9x-rr/releases) + - [BSNES-plus](https://github.com/black-sliver/bsnes-plus). **Note:** Do not reset within the emulator. It will cause + RAM corruption. +- Your Mega Man X US ROM file from the original cartridge or extracted from the Legacy Collection. Archipelago can't +provide these. + - SNES US MD5: `cfe8c11f0dce19e4fa5f3fd75775e47c` + - Legacy Collection US MD5: `ff683b75e75e9b59f0c713c7512a016b` + +## Optional Software +- [Map & Level tracker for Mega Man X Archipelago](https://github.com/BrianCumminger/megamanx-ap-poptracker/releases), +para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases) +- [Emulator Lua Scripts](https://github.com/Coltaho/emulator_lua_scripts), +for [snes9x-rr](https://github.com/gocha/snes9x-rr/releases) and [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) + +### Alternative ways of playing +- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) has reports of working fine but it isn't an officially endorsed way to play the game by the developer. Proceed at your own risk. +- RetroArch doesn't have any report of working fine. Proceed at your own risk. +- sd2snes/FX Pak don't work with this game due to limitations on the cartridge's internal hardware. + +## Installation process 1. Download and install [Archipelago](). **The installer file is located in the assets section at the bottom of the version information.** -2. The first time you patch your game, you will be asked to locate your base ROM file. - This is your Mega Man X3 ROM file. This only needs to be done once. -3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM +2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM files. 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. 2. Right-click on a ROM file and select **Open with...** @@ -31,118 +38,63 @@ 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you extracted in step one. -## Create a Config (.yaml) File - -### What is a config file and why do I need one? +## Setup your YAML -See the guide on setting up a basic YAML at the Archipelago setup -guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) +### What is a YAML file and why do I need one? -### Where do I get a config file? +Your YAML file contains a set of configuration options which provide the generator with information about how it should +generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy +an experience customized for their taste, and different players in the same multiworld can all have different options. -The Player Settings page on the website allows you to configure your personal settings and export a config file from -them. Player settings page: [Mega Man X3 Player Settings Page](/games/Mega%20Man%20X3/player-settings) +### Where do I get a YAML file? -### Verifying your config file - -If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML -validator page: [YAML Validation page](/check) +You can generate a yaml or download a template by visiting the [Mega Man X Player Options Page](/games/Mega%20Man%20X%20/player-options) ## Joining a MultiWorld Game -### Obtain your patch file and create your ROM +### Get your Mega Man X patch -When you join a multiworld game, you will be asked to provide your config file to whomever is hosting. Once that is done, +When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch -files. Your patch file should have a `.apmmx3` extension. +files. Your patch file should have a `.apmmx` extension. Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the client, and will also create your ROM in the same place as your patch file. -### Connect to the client - -#### With an emulator +### Conectarse al multiserver When the client launched automatically, SNI should have also automatically launched in the background. If this is its first time launching, you may be prompted to allow it to communicate through the Windows Firewall. -##### snes9x-rr +To connect the client with the server, write `
    :` in the text box located at the top and hit Enter (if the +server has a password, then write `/connect
    : [password]` in the bottom text box) + +Each emulator requires following a specific procedure to be able to play. Follow whichever fits your preferences. + +#### snes9x-nwa + +1. Click on the Network Menu and check **Enable Emu Network Control** +2. Load your ROM file if it hasn't already been loaded. +3. The emulator should automatically connect while SNI is running. + +#### snes9x-rr 1. Load your ROM file if it hasn't already been loaded. 2. Click on the File menu and hover on **Lua Scripting** 3. Click on **New Lua Script Window...** 4. In the new window, click **Browse...** 5. Select the connector lua file included with your client - - Look in the Archipelago folder for `/SNI/lua/Connector.lua`. -6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of + - Look in the Archipelago folder for `/SNI/lua/`. +6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install. -##### BizHawk - -1. Ensure you have the BSNES core loaded. This is done with the main menubar, under: - - (≤ 2.8) `Config` 〉 `Cores` 〉 `SNES` 〉 `BSNES` - - (≥ 2.9) `Config` 〉 `Preferred Cores` 〉 `SNES` 〉 `BSNESv115+` -2. Load your ROM file if it hasn't already been loaded. - If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R). -3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window. - - Look in the Archipelago folder for `/SNI/lua/Connector.lua`. - - You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua` - with the file picker. +#### BSNES-Plus -##### RetroArch 1.10.3 or newer - -You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.3. - -1. Enter the RetroArch main menu screen. -2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON. -3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default - Network Command Port at 55355. - -![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) -4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury - Performance)". - -When loading a ROM, be sure to select a **bsnes-mercury** core. These are the only cores that allow external tools to -read ROM data. - -#### With hardware - -This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do -this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES -releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases) - -Other hardware may find helpful information on the usb2snes platforms -page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms) - -1. Close your emulator, which may have auto-launched. -2. Power on your device and load the ROM. - -### Connect to the Archipelago Server - -The patch file which launched your client should have automatically connected you to the AP Server. There are a few -reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the -client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it -into the "Server" input field then press enter. - -The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". +1. Load your ROM file if it hasn't already been loaded. +2. The emulator should automatically connect while SNI is running. -### Play the game +## Final notes When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on -successfully joining a multiworld game! - -## Hosting a MultiWorld game - -The recommended way to host a game is to use our hosting service. The process is relatively simple: - -1. Collect config files from your players. -2. Create a zip file containing your players' config files. -3. Upload that zip file to the Generate page above. - - Generate page: [WebHost Seed Generation Page](/generate) -4. Wait a moment while the seed is generated. -5. When the seed is generated, you will be redirected to a "Seed Info" page. -6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so - they may download their patch files from there. -7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all - players in the game. Any observers may also be given the link to this page. -8. Once all players have joined, you may begin playing. +successfully joining a multiworld game! You can execute various commands in your client. For more information regarding +these commands you can use `/help` for local client commands and `!help` for server commands. diff --git a/worlds/mmx/docs/setup_es.md b/worlds/mmx/docs/setup_es.md new file mode 100644 index 000000000000..34895d023038 --- /dev/null +++ b/worlds/mmx/docs/setup_es.md @@ -0,0 +1,103 @@ +# Mega Man X guía de instalación + +## Software requerido + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). +- [SNI](https://github.com/alttpo/sni/releases). Este viene proporcionado junto a la instalación de Archipelago. +- Software capaz de cargar y permitir jugar archivos ROM de SNES: + - [snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases) + - [snes9x-rr](https://github.com/gocha/snes9x-rr/releases) + - [BSNES-plus](https://github.com/black-sliver/bsnes-plus). **Nota:** No usen el `Reset` del emulador, causa + corrupción de RAM y puede mandar Checks de manera aleatoria. +- Una copia de tu Mega Man X US proveniente del cartucho original o de la Legacy Collection. La comunidad de +Archipelago no puede proveer ni uno de estos. + - SNES US MD5: `cfe8c11f0dce19e4fa5f3fd75775e47c` + - Legacy Collection US MD5: `ff683b75e75e9b59f0c713c7512a016b` + +## Software opcional +- [Tracker de mapa y niveles para Mega Man X Archipelago](https://github.com/BrianCumminger/megamanx-ap-poptracker/releases), +para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases) +- [Scripts Lua que muestran información variada en el mismo emulador](https://github.com/Coltaho/emulator_lua_scripts), +para [snes9x-rr](https://github.com/gocha/snes9x-rr/releases) y [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) + +### Métodos de jugar no soportados oficialmente +- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) tiene reportes de funcionar adecuadamente, pero no es un +método de jugar que el desarollador utiliza. Procede bajo tu propio riesgo. +- RetroArch no tiene reportes de funcionar. Procede bajo tu propio riesgo. +- sd2snes/FX Pak no funcionan en esta implementación debido a limitantes de los componentes internos de dichos cartuchos. + +## Procedimiento de instalación + +1. Descarga e instala [Archipelago](). **El instalador se +encuentra en la sección de `Assets` después de la información de la versión.** +2. Asocia los archivos `.sfc` con el emulador deseado: + 1. Extrae el emulador y sus archivos en algún lugar de tu computadora que puedas recordar. + 2. Da clic derecho en un ROM y selecciona **Abrir con...** + 3. Activa la casilla enseguida de **Siempre usar esta aplicación para abrir archivos .sfc** + 4. Mueve el menú hasta encontrar al final de la lista la opción llamada **Buscar otra aplicación en el equipo** + 5. Busca el archivo ejecutable del emulador (`.exe`) y da click en **Abrir**. El archivo puede encontrarse en donde + extrajíste el emulador en el primer paso. + +## Configura tu archivo YAML + +### ¿Qué es un archivo YAML y por qué necesito uno? + +Tu archivo YAML contiene un número de opciones que proveen al generador con información sobre como debe generar tu +juego. Cada jugador de un multiworld entregará su propio archivo YAML. Esto permite que cada jugador disfrute de una +experiencia personalizada a su manera, y que diferentes jugadores dentro del mismo multiworld pueden tener diferentes +opciones. + +### ¿Dónde puedo obtener un archivo YAML? + +Puedes generar un archivo YAML or descargar su plantilla en la [página de configuración de jugador de Mega Man X](/games/Mega%20Man%20X%20/player-options) + +## Unirse a un juego MultiWorld + +### Obtener tu parche de Mega Man X + +Cuando te unes a un juego multiworld, se te pedirá que entregues tu archivo YAML a quien lo esté organizando. +Una vez que la generación acabe, el anfitrión te dará un enlace a tu archivo, o un .zip con los archivos de +todos. Tu archivo tiene una extensión `.apmmx`. + +Haz doble clic en tu archivo `.apmmx` para que se ejecute el cliente y realice el parcheado del ROM. +Una vez acabe ese proceso (esto puede tardar un poco), el cliente y el emulador se abrirán automáticamente (si es que se +ha asociado la extensión al emulador tal como fue recomendado) + +### Conectarse al multiserver + +Cuando el cliente se ejecuta automaticamente, SNI también se debe ejecutar automaticamente en segundo plano. Si es la +primera vez que ejecutas el cliente, es posible que se te pida que permitas a la aplicación a través del Firewall de +Windows. + +Para conectar el cliente con el servidor, simplemente pon `:` en la caja de texto superior y presiona +enter (si el servidor tiene contraseña, en la caja de texto inferior escribe `/connect : [contraseña]`) + +Cada emulador tiene un procedimiento distinto para poder jugar, sigue el que se acomode a tus preferencias. + +#### snes9x-nwa + +1. Da click en el menú de Network y activa **Enable Emu Network Control** +2. Carga tu ROM parcheado si aún no ha sido cargado +3. El emulador debe de conectarse automáticamente mientras SNI está ejecutandose en segundo plano + +#### snes9x-rr + +1. Carga tu ROM parcheado si aún no ha sido cargado +2. Da click en el menú de File y coloca el cursor sobre **Lua Scripting** +3. Da click en **New Lua Script Window...** +4. En la ventana que aparece da click en **Browse..** +5. Selecciona el archivo conector incluido con el cliente + - Busca en la carpeta de Archipelago el directorio de `/SNI/lua/` +6. Si llega a aparecer un error al cargar el script que diga que no cuentas con `socket.dll` o algo similar, ve a la +carpeta del lua que estás utilizando y copia el archivo `socket.dll` a la carpeta raíz de tu snes9x + +#### BSNES-Plus + +1. Carga tu ROM parcheado si aún no ha sido cargado +2. El emulador debe de conectarse automáticamente mientras SNI está ejecutándose en segundo plano + +## Notas finales para jugar + +Cuando el cliente muestra que el dispositivo de SNES y el Server están conectados, estas listo para comenzar a jugar. +Dentro del mismo cliente puedes encontrar diferentes comandos que puedes ejecutar. Para más información acerca de los +comandos disponibles puedes escribir `/help` para comandos del cliente y `!help` para comandos del server. From c3d875af1916df4ae2e2a88e29a251bccf33b2b0 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Wed, 7 Aug 2024 01:06:40 -0700 Subject: [PATCH 62/71] lower amount of items required for fortress --- worlds/mmx/Options.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index 47dc789ecbb4..a9714435e0da 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -190,8 +190,8 @@ class SigmaWeaponCount(Range): """ display_name = "Sigma Weapon Count" range_start = 0 - range_end = 8 - default = 8 + range_end = 6 + default = 6 class SigmaArmorUpgradeCount(Range): """ @@ -208,8 +208,8 @@ class SigmaHeartTankCount(Range): """ display_name = "Sigma Heart Tank Count" range_start = 0 - range_end = 8 - default = 8 + range_end = 6 + default = 6 class SigmaSubTankCount(Range): """ @@ -217,8 +217,8 @@ class SigmaSubTankCount(Range): """ display_name = "Sigma Sub Tank Count" range_start = 0 - range_end = 4 - default = 4 + range_end = 2 + default = 2 class ButtonConfiguration(OptionDict): """ From 2a63cbb1808e74d982688811f27cf559047c10e1 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Thu, 8 Aug 2024 13:17:09 -0700 Subject: [PATCH 63/71] make collect only write to RAM once per game_watcher run --- worlds/mmx/Client.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index 740705c6644a..5f2a582d88ac 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -431,9 +431,9 @@ async def game_watcher(self, ctx): new_hadouken = False cleared_levels = list(await snes_read(ctx, MMX_COMPLETED_LEVELS, 0x20)) collected_pickups = list(await snes_read(ctx, MMX_COLLECTED_PICKUPS, 0x40)) + defeated_bosses = list(await snes_read(ctx, MMX_DEFEATED_BOSSES, 0x20)) collected_heart_tanks = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_HEART_TANKS, 0x01)) collected_upgrades = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_UPGRADES, 0x01)) - defeated_bosses = list(await snes_read(ctx, MMX_DEFEATED_BOSSES, 0x20)) collected_hadouken = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_HADOUKEN, 0x01)) i = 0 for loc_id in ctx.checked_locations: @@ -480,19 +480,19 @@ async def game_watcher(self, ctx): collected_pickups[data_bit] = 0x01 new_pickup = True - if new_cleared_level: - snes_buffered_write(ctx, MMX_COMPLETED_LEVELS, bytes(cleared_levels)) - if new_boss_clears: - snes_buffered_write(ctx, MMX_DEFEATED_BOSSES, bytes(defeated_bosses)) - if new_pickup: - snes_buffered_write(ctx, MMX_COLLECTED_PICKUPS, bytes(collected_pickups)) - if new_hadouken: - snes_buffered_write(ctx, MMX_COLLECTED_HADOUKEN, bytearray([collected_hadouken])) - if new_upgrade: - snes_buffered_write(ctx, MMX_COLLECTED_UPGRADES, bytearray([collected_upgrades])) - if new_heart_tank: - snes_buffered_write(ctx, MMX_COLLECTED_HEART_TANKS, bytearray([collected_heart_tanks])) - await snes_flush_writes(ctx) + if new_cleared_level: + snes_buffered_write(ctx, MMX_COMPLETED_LEVELS, bytes(cleared_levels)) + if new_boss_clears: + snes_buffered_write(ctx, MMX_DEFEATED_BOSSES, bytes(defeated_bosses)) + if new_pickup: + snes_buffered_write(ctx, MMX_COLLECTED_PICKUPS, bytes(collected_pickups)) + if new_hadouken: + snes_buffered_write(ctx, MMX_COLLECTED_HADOUKEN, bytearray([collected_hadouken])) + if new_upgrade: + snes_buffered_write(ctx, MMX_COLLECTED_UPGRADES, bytearray([collected_upgrades])) + if new_heart_tank: + snes_buffered_write(ctx, MMX_COLLECTED_HEART_TANKS, bytearray([collected_heart_tanks])) + await snes_flush_writes(ctx) def on_package(self, ctx, cmd: str, args: dict): super().on_package(ctx, cmd, args) From 35bebdec7e4917f9b384e56647abda340728e543 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 9 Aug 2024 11:36:01 -0700 Subject: [PATCH 64/71] Update setup_en.md --- worlds/mmx/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/mmx/docs/setup_en.md b/worlds/mmx/docs/setup_en.md index ea6f9c71b84b..d935d361ea89 100644 --- a/worlds/mmx/docs/setup_en.md +++ b/worlds/mmx/docs/setup_en.md @@ -61,7 +61,7 @@ files. Your patch file should have a `.apmmx` extension. Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the client, and will also create your ROM in the same place as your patch file. -### Conectarse al multiserver +### Connect to the multiserver When the client launched automatically, SNI should have also automatically launched in the background. If this is its first time launching, you may be prompted to allow it to communicate through the Windows Firewall. From 3f18bbcbcaa179b9cfa086efb36fd906b96a7c9a Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 9 Aug 2024 11:37:00 -0700 Subject: [PATCH 65/71] Update setup_en.md --- worlds/mmx/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/mmx/docs/setup_en.md b/worlds/mmx/docs/setup_en.md index d935d361ea89..771f61e0da33 100644 --- a/worlds/mmx/docs/setup_en.md +++ b/worlds/mmx/docs/setup_en.md @@ -16,7 +16,7 @@ provide these. ## Optional Software - [Map & Level tracker for Mega Man X Archipelago](https://github.com/BrianCumminger/megamanx-ap-poptracker/releases), -para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases) +to be used with [PopTracker](https://github.com/black-sliver/PopTracker/releases) - [Emulator Lua Scripts](https://github.com/Coltaho/emulator_lua_scripts), for [snes9x-rr](https://github.com/gocha/snes9x-rr/releases) and [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) From 92a5058c2f544fc92bc6afab8b22c8ae91dbc837 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 9 Aug 2024 15:05:12 -0700 Subject: [PATCH 66/71] bunch of changes for 1.3.1 --- worlds/mmx/Client.py | 10 ++++ worlds/mmx/Names/RegionName.py | 7 +++ worlds/mmx/Options.py | 8 +++ worlds/mmx/Regions.py | 68 ++++++++++++++++++++------ worlds/mmx/Rules.py | 46 ++++++++++++++--- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 12305 -> 12310 bytes 6 files changed, 117 insertions(+), 22 deletions(-) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index 5f2a582d88ac..2639ed734b52 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -930,6 +930,16 @@ async def handle_data_storage(self, ctx): }]) self.save_arsenal = False + keys = { + f"mmx_checkpoints_{ctx.team}_{ctx.slot}", + f"mmx_global_timer_{ctx.team}_{ctx.slot}", + f"mmx_deaths_{ctx.team}_{ctx.slot}", + f"mmx_damage_dealt_{ctx.team}_{ctx.slot}", + f"mmx_damage_taken_{ctx.team}_{ctx.slot}", + } + if not all(key in ctx.stored_data.keys() for key in keys): + return + # Checkpoints reached checkpoints = list(await snes_read(ctx, MMX_CHECKPOINTS_REACHED, 0xF)) data_storage_checkpoints = ctx.stored_data[f"mmx_checkpoints_{ctx.team}_{ctx.slot}"] or [0 for _ in range(0xF)] diff --git a/worlds/mmx/Names/RegionName.py b/worlds/mmx/Names/RegionName.py index 925d03b76b48..065a6f6ad276 100644 --- a/worlds/mmx/Names/RegionName.py +++ b/worlds/mmx/Names/RegionName.py @@ -62,6 +62,7 @@ sigma_fortress_1_vile = "Sigma Fortress 1 - Vile" sigma_fortress_1_vertical = "Sigma Fortress 1 - Vertical Room" sigma_fortress_1_rematch_1 = "Sigma Fortress 1 - Rematch 1" +sigma_fortress_1_before_boss = "Sigma Fortress 1 - Before Boss" sigma_fortress_1_boss = "Sigma Fortress 1 - Boss" sigma_fortress_2 = "Sigma Fortress 2" @@ -69,6 +70,7 @@ sigma_fortress_2_rematch_1 = "Sigma Fortress 2 - Rematch 1" sigma_fortress_2_ride = "Sigma Fortress 2 - Ride Armor" sigma_fortress_2_rematch_2 = "Sigma Fortress 2 - Rematch 2" +sigma_fortress_2_before_boss = "Sigma Fortress 2 - Before Boss" sigma_fortress_2_boss = "Sigma Fortress 2 - Boss" sigma_fortress_3 = "Sigma Fortress 3" @@ -77,6 +79,11 @@ sigma_fortress_3_rematch_3 = "Sigma Fortress 3 - Rematch 3" sigma_fortress_3_rematch_4 = "Sigma Fortress 3 - Rematch 4" sigma_fortress_3_rematch_5 = "Sigma Fortress 3 - Rematch 5" +sigma_fortress_3_after_rematch_1 = "Sigma Fortress 3 - After Rematch 1" +sigma_fortress_3_after_rematch_2 = "Sigma Fortress 3 - After Rematch 2" +sigma_fortress_3_after_rematch_3 = "Sigma Fortress 3 - After Rematch 3" +sigma_fortress_3_after_rematch_4 = "Sigma Fortress 3 - After Rematch 4" +sigma_fortress_3_after_rematch_5 = "Sigma Fortress 3 - After Rematch 5" sigma_fortress_3_boss = "Sigma Fortress 3 - Boss" sigma_fortress_4 = "Sigma Fortress 4" diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index a9714435e0da..cf01642c1f8c 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -267,6 +267,12 @@ class LongJumps(Toggle): """ display_name = "Long Jumps" +class LogicHelmetCheckpoints(Toggle): + """ + Makes the "Use Any Checkpoint" feature from the Helmet Upgrade be in logic + """ + display_name = "Helmet Checkpoints In Logic" + class ChillPenguinTweaks(OptionSet): """ Behavior options for Chill Penguin. Everything can be stacked. @@ -337,6 +343,7 @@ class SparkMandrillTweaks(OptionSet): AirDash, HadoukenInPool, LogicChargedShotgunIce, + LogicHelmetCheckpoints, ]), OptionGroup("Sigma Fortress Options", [ SigmaOpen, @@ -384,6 +391,7 @@ class MMXOptions(PerGameCommonOptions): logic_boss_weakness: LogicBossWeakness logic_leg_sigma: LogicLegSigma logic_charged_shotgun_ice: LogicChargedShotgunIce + logic_helmet_checkpoints: LogicHelmetCheckpoints sigma_all_levels: FortressBundleUnlock sigma_open: SigmaOpen sigma_medal_count: SigmaMedalCount diff --git a/worlds/mmx/Regions.py b/worlds/mmx/Regions.py index b5c412d50da8..bb373c02177c 100644 --- a/worlds/mmx/Regions.py +++ b/worlds/mmx/Regions.py @@ -74,6 +74,7 @@ def create_regions(multiworld: MultiWorld, player: int, world: World, active_loc sigma_fortress_1_vile = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_vile) sigma_fortress_1_vertical = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_vertical) sigma_fortress_1_rematch_1 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_rematch_1) + sigma_fortress_1_before_boss = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_before_boss) sigma_fortress_1_boss = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_boss) sigma_fortress_2 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2) @@ -81,6 +82,7 @@ def create_regions(multiworld: MultiWorld, player: int, world: World, active_loc sigma_fortress_2_rematch_1 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_rematch_1) sigma_fortress_2_ride = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_ride) sigma_fortress_2_rematch_2 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_rematch_2) + sigma_fortress_2_before_boss = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_before_boss) sigma_fortress_2_boss = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_boss) sigma_fortress_3 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3) @@ -89,6 +91,11 @@ def create_regions(multiworld: MultiWorld, player: int, world: World, active_loc sigma_fortress_3_rematch_3 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_3) sigma_fortress_3_rematch_4 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_4) sigma_fortress_3_rematch_5 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_5) + sigma_fortress_3_after_rematch_1 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_1) + sigma_fortress_3_after_rematch_2 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_2) + sigma_fortress_3_after_rematch_3 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_3) + sigma_fortress_3_after_rematch_4 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_4) + sigma_fortress_3_after_rematch_5 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_5) sigma_fortress_3_boss = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_boss) sigma_fortress_4 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_4) @@ -151,12 +158,14 @@ def create_regions(multiworld: MultiWorld, player: int, world: World, active_loc sigma_fortress_1_vile, sigma_fortress_1_vertical, sigma_fortress_1_rematch_1, + sigma_fortress_1_before_boss, sigma_fortress_1_boss, sigma_fortress_2, sigma_fortress_2_start, sigma_fortress_2_rematch_1, sigma_fortress_2_ride, sigma_fortress_2_rematch_2, + sigma_fortress_2_before_boss, sigma_fortress_2_boss, sigma_fortress_3, sigma_fortress_3_rematch_1, @@ -164,6 +173,11 @@ def create_regions(multiworld: MultiWorld, player: int, world: World, active_loc sigma_fortress_3_rematch_3, sigma_fortress_3_rematch_4, sigma_fortress_3_rematch_5, + sigma_fortress_3_after_rematch_1, + sigma_fortress_3_after_rematch_2, + sigma_fortress_3_after_rematch_3, + sigma_fortress_3_after_rematch_4, + sigma_fortress_3_after_rematch_5, sigma_fortress_3_boss, sigma_fortress_4, sigma_fortress_4_dog, @@ -299,14 +313,15 @@ def create_regions(multiworld: MultiWorld, player: int, world: World, active_loc add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_1, LocationName.flame_mammoth_1up) # Sigma's Fortress 3 - add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_2, LocationName.sigma_fortress_3_hp_1) - add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_3, LocationName.sigma_fortress_3_hp_2) - add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_3, LocationName.sigma_fortress_3_energy_1) - add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_4, LocationName.sigma_fortress_3_hp_3) - add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_4, LocationName.sigma_fortress_3_energy_2) - add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_5, LocationName.sigma_fortress_3_hp_4) - add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_5, LocationName.sigma_fortress_3_energy_3) - add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_5, LocationName.sigma_fortress_3_1up) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_1, LocationName.sigma_fortress_3_hp_1) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_2, LocationName.sigma_fortress_3_hp_2) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_2, LocationName.sigma_fortress_3_energy_1) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_3, LocationName.sigma_fortress_3_hp_3) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_3, LocationName.sigma_fortress_3_energy_2) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_4, LocationName.sigma_fortress_3_hp_4) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_4, LocationName.sigma_fortress_3_energy_3) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_4, LocationName.sigma_fortress_3_1up) + def connect_regions(world: World): connect(world, "Menu", RegionName.intro) @@ -374,20 +389,27 @@ def connect_regions(world: World): connect(world, RegionName.sigma_fortress_1_outside, RegionName.sigma_fortress_1_vile) connect(world, RegionName.sigma_fortress_1_vile, RegionName.sigma_fortress_1_vertical) connect(world, RegionName.sigma_fortress_1_vertical, RegionName.sigma_fortress_1_rematch_1) - connect(world, RegionName.sigma_fortress_1_rematch_1, RegionName.sigma_fortress_1_boss) + connect(world, RegionName.sigma_fortress_1_rematch_1, RegionName.sigma_fortress_1_before_boss) + connect(world, RegionName.sigma_fortress_1_before_boss, RegionName.sigma_fortress_1_boss) connect(world, RegionName.sigma_fortress_2, RegionName.sigma_fortress_2_start) connect(world, RegionName.sigma_fortress_2_start, RegionName.sigma_fortress_2_rematch_1) connect(world, RegionName.sigma_fortress_2_rematch_1, RegionName.sigma_fortress_2_ride) connect(world, RegionName.sigma_fortress_2_ride, RegionName.sigma_fortress_2_rematch_2) - connect(world, RegionName.sigma_fortress_2_rematch_2, RegionName.sigma_fortress_2_boss) + connect(world, RegionName.sigma_fortress_2_rematch_2, RegionName.sigma_fortress_2_before_boss) + connect(world, RegionName.sigma_fortress_2_before_boss, RegionName.sigma_fortress_2_boss) connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_rematch_1) - connect(world, RegionName.sigma_fortress_3_rematch_1, RegionName.sigma_fortress_3_rematch_2) - connect(world, RegionName.sigma_fortress_3_rematch_2, RegionName.sigma_fortress_3_rematch_3) - connect(world, RegionName.sigma_fortress_3_rematch_3, RegionName.sigma_fortress_3_rematch_4) - connect(world, RegionName.sigma_fortress_3_rematch_4, RegionName.sigma_fortress_3_rematch_5) - connect(world, RegionName.sigma_fortress_3_rematch_5, RegionName.sigma_fortress_3_boss) + connect(world, RegionName.sigma_fortress_3_rematch_1, RegionName.sigma_fortress_3_after_rematch_1) + connect(world, RegionName.sigma_fortress_3_after_rematch_1, RegionName.sigma_fortress_3_rematch_2) + connect(world, RegionName.sigma_fortress_3_rematch_2, RegionName.sigma_fortress_3_after_rematch_2) + connect(world, RegionName.sigma_fortress_3_after_rematch_2, RegionName.sigma_fortress_3_rematch_3) + connect(world, RegionName.sigma_fortress_3_rematch_3, RegionName.sigma_fortress_3_after_rematch_3) + connect(world, RegionName.sigma_fortress_3_after_rematch_3, RegionName.sigma_fortress_3_rematch_4) + connect(world, RegionName.sigma_fortress_3_rematch_4, RegionName.sigma_fortress_3_after_rematch_4) + connect(world, RegionName.sigma_fortress_3_after_rematch_4, RegionName.sigma_fortress_3_rematch_5) + connect(world, RegionName.sigma_fortress_3_rematch_5, RegionName.sigma_fortress_3_after_rematch_5) + connect(world, RegionName.sigma_fortress_3_after_rematch_5, RegionName.sigma_fortress_3_boss) connect(world, RegionName.sigma_fortress_4, RegionName.sigma_fortress_4_dog) connect(world, RegionName.sigma_fortress_4_dog, RegionName.sigma_fortress_4_sigma) @@ -401,6 +423,22 @@ def connect_regions(world: World): connect(world, RegionName.sigma_fortress_2_boss, RegionName.sigma_fortress_3) connect(world, RegionName.sigma_fortress_3_boss, RegionName.sigma_fortress_4) + # Connect checkpoints + if world.options.logic_helmet_checkpoints.value: + connect(world, RegionName.spark_mandrill, RegionName.spark_mandrill_deep) + + connect(world, RegionName.sigma_fortress_1, RegionName.sigma_fortress_1_vertical) + connect(world, RegionName.sigma_fortress_1, RegionName.sigma_fortress_1_before_boss) + + connect(world, RegionName.sigma_fortress_2, RegionName.sigma_fortress_2_ride) + connect(world, RegionName.sigma_fortress_2, RegionName.sigma_fortress_2_before_boss) + + connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_after_rematch_1) + connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_after_rematch_2) + connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_after_rematch_3) + connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_after_rematch_4) + connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_after_rematch_5) + def create_region(multiworld: MultiWorld, player: int, active_locations, name: str, locations=None): ret = Region(name, player, multiworld) diff --git a/worlds/mmx/Rules.py b/worlds/mmx/Rules.py index 1b712871a951..742bfa0bd6c8 100644 --- a/worlds/mmx/Rules.py +++ b/worlds/mmx/Rules.py @@ -7,7 +7,7 @@ bosses = { "Sting Chameleon": [ f"{RegionName.sting_chameleon_swamp} -> {RegionName.sting_chameleon_boss}", - f"{RegionName.sigma_fortress_3_rematch_1} -> {RegionName.sigma_fortress_3_rematch_2}" + f"{RegionName.sigma_fortress_3_after_rematch_1} -> {RegionName.sigma_fortress_3_rematch_2}" ], "Storm Eagle": [ f"{RegionName.storm_eagle_aircraft} -> {RegionName.storm_eagle_boss}", @@ -15,7 +15,7 @@ ], "Flame Mammoth": [ f"{RegionName.flame_mammoth_lava_river_2} -> {RegionName.flame_mammoth_boss}", - f"{RegionName.sigma_fortress_3_rematch_4} -> {RegionName.sigma_fortress_3_rematch_5}" + f"{RegionName.sigma_fortress_3_after_rematch_4} -> {RegionName.sigma_fortress_3_rematch_5}" ], "Chill Penguin": [ f"{RegionName.chill_penguin_ride} -> {RegionName.chill_penguin_boss}", @@ -23,7 +23,7 @@ ], "Spark Mandrill": [ f"{RegionName.spark_mandrill_deep} -> {RegionName.spark_mandrill_boss}", - f"{RegionName.sigma_fortress_3_rematch_2} -> {RegionName.sigma_fortress_3_rematch_3}" + f"{RegionName.sigma_fortress_3_after_rematch_2} -> {RegionName.sigma_fortress_3_rematch_3}" ], "Armored Armadillo": [ f"{RegionName.armored_armadillo_ride_3} -> {RegionName.armored_armadillo_boss}", @@ -31,7 +31,7 @@ ], "Launch Octopus": [ f"{RegionName.launch_octopus_sea} -> {RegionName.launch_octopus_boss}", - f"{RegionName.sigma_fortress_3_rematch_3} -> {RegionName.sigma_fortress_3_rematch_4}" + f"{RegionName.sigma_fortress_3_after_rematch_3} -> {RegionName.sigma_fortress_3_rematch_4}" ], "Boomer Kuwanger": [ f"{RegionName.boomer_kuwanger_top} -> {RegionName.boomer_kuwanger_boss}", @@ -44,13 +44,13 @@ f"{RegionName.sigma_fortress_1_outside} -> {RegionName.sigma_fortress_1_vile}" ], "Bospider": [ - f"{RegionName.sigma_fortress_1_rematch_1} -> {RegionName.sigma_fortress_1_boss}" + f"{RegionName.sigma_fortress_1_before_boss} -> {RegionName.sigma_fortress_1_boss}" ], "Rangda Bangda": [ - f"{RegionName.sigma_fortress_2_rematch_2} -> {RegionName.sigma_fortress_2_boss}" + f"{RegionName.sigma_fortress_2_before_boss} -> {RegionName.sigma_fortress_2_boss}" ], "D-Rex": [ - f"{RegionName.sigma_fortress_3_rematch_5} -> {RegionName.sigma_fortress_3_boss}" + f"{RegionName.sigma_fortress_3_after_rematch_5} -> {RegionName.sigma_fortress_3_boss}" ], "Velguarder": [ f"{RegionName.sigma_fortress_4} -> {RegionName.sigma_fortress_4_dog}" @@ -191,6 +191,10 @@ def set_rules(world: MMXWorld): if world.options.logic_charged_shotgun_ice.value: add_charged_shotgun_ice_logic(world) + # Handle helmet logic + if world.options.logic_helmet_checkpoints.value: + add_helmet_logic(world) + def add_pickupsanity_logic(world: MMXWorld): player = world.player @@ -296,3 +300,31 @@ def add_charged_shotgun_ice_logic(world: MMXWorld): state.has(ItemName.shotgun_ice, player) and state.has(ItemName.arms, player, jammed_buster + 1) )) + +def add_helmet_logic(world: MMXWorld): + player = world.player + multiworld = world.multiworld + + set_rule(multiworld.get_entrance(f"{RegionName.spark_mandrill} -> {RegionName.spark_mandrill_deep}", player), + lambda state: state.has(ItemName.helmet, player, 1)) + + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_1} -> {RegionName.sigma_fortress_1_vertical}", player), + lambda state: state.has(ItemName.helmet, player, 1)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_1} -> {RegionName.sigma_fortress_1_before_boss}", player), + lambda state: state.has(ItemName.helmet, player, 1)) + + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_2} -> {RegionName.sigma_fortress_2_ride}", player), + lambda state: state.has(ItemName.helmet, player, 1)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_2} -> {RegionName.sigma_fortress_2_before_boss}", player), + lambda state: state.has(ItemName.helmet, player, 1)) + + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_1}", player), + lambda state: state.has(ItemName.helmet, player, 1)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_2}", player), + lambda state: state.has(ItemName.helmet, player, 1)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_3}", player), + lambda state: state.has(ItemName.helmet, player, 1)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_4}", player), + lambda state: state.has(ItemName.helmet, player, 1)) + set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_5}", player), + lambda state: state.has(ItemName.helmet, player, 1)) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 54cd4f9df0f3556bf075cd1a81a8f924b527ed5a..d57429af9c56ed33a9b9fa638981085afa4ceb0c 100644 GIT binary patch delta 11523 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 delta 11523 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 From ffaf618a195859a1e5b72773d4cb1b45df178e3e Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 23 Aug 2024 20:28:36 -0700 Subject: [PATCH 67/71] v1.4.0 --- worlds/mmx/Aesthetics.py | 134 ++++++++++++++++++++++++++ worlds/mmx/Client.py | 11 ++- worlds/mmx/Options.py | 133 ++++++++++++++++++++++++- worlds/mmx/Rom.py | 65 ++++++++++--- worlds/mmx/Weaknesses.py | 9 +- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 12310 -> 12367 bytes worlds/mmx/docs/setup_en.md | 2 +- 7 files changed, 338 insertions(+), 16 deletions(-) create mode 100644 worlds/mmx/Aesthetics.py diff --git a/worlds/mmx/Aesthetics.py b/worlds/mmx/Aesthetics.py new file mode 100644 index 000000000000..5da0135cb061 --- /dev/null +++ b/worlds/mmx/Aesthetics.py @@ -0,0 +1,134 @@ +import struct + +from typing import Dict, List + +player_palettes = { + "blue": [ + "$35D0","$7BDE","$239D","$091E","$7B6F","$7A8A","$5963","$7E00", + "$7900","$40C4","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "gold_armor": [ + "$35D0","$7BDE","$239D","$091E","$7BFF","$1F9F","$09DF","$73DF", + "$0ADE","$055D","$42DF","$2597","$1110","$4FBF","$121D","$0C63", + ], + "acid_burst": [ + "$35D0","$7BDE","$239D","$091E","$1F1E","$11F8","$0132","$43F7", + "$1BAC","$1689","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "parasitic_bomb": [ + "$35D0","$7BDE","$239D","$091E","$6776","$424E","$2547","$03FF", + "$02D7","$018D","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "triad_thunder": [ + "$35D0","$7BDE","$239D","$091E","$4F9A","$274F","$222C","$7E14", + "$5D0E","$3C8C","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "spinning_blade": [ + "$35D0","$7BDE","$239D","$091E","$6318","$4210","$2108","$195F", + "$0098","$0010","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "ray_splasher": [ + "$35D0","$7BDE","$239D","$091E","$03FF","$02D7","$018D","$1EDE", + "$11B8","$00F2","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "gravity_well": [ + "$35D0","$7BDE","$239D","$091E","$531C","$3214","$110C","$7DFE", + "$6518","$5093","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "frost_shield": [ + "$35D0","$7BDE","$239D","$091E","$7E3A","$7174","$58EF","$7BAF", + "$6B29","$5264","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "tornado_fang": [ + "$35D0","$7BDE","$239D","$091E","$1EDE","$0976","$0050","$73D0", + "$5B08","$3A00","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "crystal_hunter": [ + "$35D0","$7BDE","$239D","$091E","$7F3E","$7256","$5DB1","$7B6F", + "$7A8A","$59A5","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "bubble_splash": [ + "$35D0","$7BDE","$239D","$091E","$773F","$5639","$3D16","$131F", + "$0657","$01B1","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "silk_shot": [ + "$35D0","$7BDE","$239D","$091E","$5FF4","$3F2C","$1E65","$1E1F", + "$011F","$00F6","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "spin_wheel": [ + "$35D0","$7BDE","$239D","$091E","$7F7F","$7E59","$75B5","$2B00", + "$2240","$1DC4","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "sonic_slicer": [ + "$35D0","$7BDE","$239D","$091E","$031F","$021F","$017B","$75AB", + "$590A","$28E8","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "strike_chain": [ + "$35D0","$7BDE","$239D","$091E","$779C","$62D6","$4E10","$7A7A", + "$75D2","$5CEF","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "magnet_mine": [ + "$35D0","$7BDE","$239D","$091E","$2FFF","$0F18","$0A52","$4E31", + "$398C","$2908","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "speed_burner": [ + "$35D0","$7BDE","$239D","$091E","$7F98","$7ACF","$6645","$10DF", + "$0CB7","$0452","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "homing_torpedo": [ + "$35D0","$7BDE","$22FF","$0059","$5B7F","$46DB","$3657","$4250", + "$35ED","$2168","$42DF","$2597","$1110","$7BDE","$4E73","$14A5", + ], + "chameleon_sting": [ + "$35D0","$7BDE","$22FF","$0059","$73D8","$574C","$3EE4","$2AC3", + "$2A44","$1541","$42DF","$2597","$1110","$7BDE","$4E73","$14A5", + ], + "rolling_shield": [ + "$35D0","$7BDE","$22FF","$0059","$6BB9","$5714","$46B0","$4E1E", + "$311C","$209A","$42DF","$2597","$1110","$7BDE","$4E73","$14A5", + ], + "fire_wave": [ + "$35D0","$7BDE","$22FF","$0059","$3F9F","$229F","$0D9E","$055E", + "$0CFA","$0074","$42DF","$2597","$1110","$7BDE","$4E73","$14A5", + ], + "storm_tornado": [ + "$35D0","$7BDE","$22FF","$0059","$731F","$625C","$5A1A","$4973", + "$4953","$3CCE","$42DF","$2597","$1110","$7BDE","$4E73","$14A5", + ], + "electric_spark": [ + "$35D0","$7BDE","$22FF","$0059","$6F7B","$5AD6","$5294","$06DF", + "$05F8","$014F","$42DF","$2597","$1110","$7BDE","$4E73","$14A5", + ], + "boomerang_cutter": [ + "$35D0","$7BDE","$22FF","$0059","$6BB9","$5714","$46B0","$49CE", + "$418C","$2908","$42DF","$2597","$1110","$7BDE","$4E73","$14A5", + ], + "shotgun_ice": [ + "$35D0","$7BDE","$239D","$091E","$2B9F","$1ABD","$11B5","$7F87", + "$766A","$79C7","$42DF","$2597","$1110","$7BDE","$4210","$0C63", + ], +} + +def get_palette_bytes(palette: Dict[str, List]) -> bytearray: + output_data = bytearray() + for hexcol in palette: + if hexcol.startswith("$"): + hexcol = hexcol.replace("$", "") + colint = int(hexcol, 16) + output_data.extend(bytearray(struct.pack("H", colint))) + else: + if hexcol.startswith("#"): + hexcol = hexcol.replace("#", "") + colint = int(hexcol, 16) + col = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF) + col = tuple(x for x in col) + byte_data = rgb888_to_bgr555(col[0], col[1], col[2]) + output_data.extend(bytearray(byte_data)) + return output_data + +def rgb888_to_bgr555(red, green, blue) -> bytes: + red = red >> 3 + green = green >> 3 + blue = blue >> 3 + outcol = (blue << 10) + (green << 5) + red + return struct.pack("H", outcol) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index 2639ed734b52..a139ef41de8e 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -31,6 +31,7 @@ MMX_CURRENT_HP = WRAM_START + 0x00BCF MMX_UNLOCKED_CHARGED_SHOT = MMX_RAM + 0x0016 MMX_UNLOCKED_AIR_DASH = MMX_RAM + 0x0022 +MMX_FORTRESS_PROGRESS = WRAM_START + 0x01F7B MMX_SFX_FLAG = MMX_RAM + 0x0003 MMX_SFX_NUMBER = MMX_RAM + 0x0004 @@ -605,10 +606,16 @@ async def handle_energy_link(self, ctx): menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) can_move = await snes_read(ctx, MMX_CAN_MOVE, 0x7) + heart_tank = await snes_read(ctx, MMX_ENABLE_HEART_TANK, 0x1) + hp_refill = await snes_read(ctx, MMX_ENABLE_HP_REFILL, 0x1) + weapon_refill = await snes_read(ctx, MMX_ENABLE_WEAPON_REFILL, 0x1) if menu_state[0] != 0x04 or \ gameplay_state[0] != 0x04 or \ can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ - receiving_item[0] != 0x00: + receiving_item[0] != 0x00 or \ + heart_tank[0] != 0x00 or \ + hp_refill[0] != 0x00 or \ + weapon_refill[0] != 0x00: return skip_hp = False @@ -704,11 +711,13 @@ async def handle_item_queue(self, ctx): receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) + progress = await snes_read(ctx, MMX_FORTRESS_PROGRESS, 0x1) can_move = await snes_read(ctx, MMX_CAN_MOVE, 0x7) hp_refill = await snes_read(ctx, MMX_ENABLE_HP_REFILL, 0x1) weapon_refill = await snes_read(ctx, MMX_ENABLE_WEAPON_REFILL, 0x1) if menu_state[0] != 0x04 or \ gameplay_state[0] != 0x04 or \ + progress[0] >= 0x04 or \ hp_refill[0] != 0x00 or \ weapon_refill[0] != 0x00 or \ can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py index cf01642c1f8c..72232622c066 100644 --- a/worlds/mmx/Options.py +++ b/worlds/mmx/Options.py @@ -4,7 +4,7 @@ from Options import OptionGroup, Choice, Range, Toggle, DefaultOnToggle, OptionSet, OptionDict, DeathLink, PerGameCommonOptions, StartInventoryPool from schema import Schema, And, Use, Optional -from .Rom import action_buttons, action_names +from .Rom import action_buttons, action_names, x_palette_set_offsets from .Weaknesses import boss_weaknesses, weapons_chaotic class EnergyLink(DefaultOnToggle): @@ -332,6 +332,114 @@ class SparkMandrillTweaks(OptionSet): } default = {} +class BasePalette(Choice): + """ + Base class for palettes + """ + option_blue = 0 + option_gold_armor = 1 + option_acid_burst = 6 + option_parasitic_bomb = 7 + option_triad_thunder = 8 + option_spinning_blade = 9 + option_ray_splasher = 10 + option_gravity_well = 11 + option_frost_shield = 12 + option_tornado_fang = 13 + option_crystal_hunter = 14 + option_bubble_splash = 15 + option_silk_shot = 16 + option_spin_wheel = 17 + option_sonic_slicer = 18 + option_strike_chain = 19 + option_magnet_mine = 20 + option_speed_burner = 21 + option_homing_torpedo = 22 + option_chameleon_sting = 23 + option_rolling_shield = 24 + option_fire_wave = 25 + option_storm_tornado = 26 + option_electric_spark = 27 + option_boomerang_cutter = 28 + option_shotgun_ice = 29 + +class PaletteDefault(BasePalette): + """ + Which color to use for X's default color + """ + display_name = "X Palette" + default = 0 + +class PaletteHomingTorpedo(BasePalette): + """ + Which color to use for X's Homing Torpedo + """ + display_name = "Homing Torpedo Palette" + default = 22 + +class PaletteChameleonSting(BasePalette): + """ + Which color to use for X's Chameleon Sting + """ + display_name = "Chameleon Sting Palette" + default = 23 + +class PaletteRollingShield(BasePalette): + """ + Which color to use for X's Rolling Shield + """ + display_name = "Rolling Shield Palette" + default = 24 + +class PaletteFireWave(BasePalette): + """ + Which color to use for X's Fire Wave + """ + display_name = "Fire Wave Palette" + default = 25 + +class PaletteStormTornado(BasePalette): + """ + Which color to use for X's Storm Tornado + """ + display_name = "Storm Tornado Palette" + default = 26 + +class PaletteElectricSpark(BasePalette): + """ + Which color to use for X's Electric Spark + """ + display_name = "Electric Spark Palette" + default = 27 + +class PaletteBoomerangCutter(BasePalette): + """ + Which color to use for X's Boomerang Cutter + """ + display_name = "Boomerang Cutter Palette" + default = 28 + +class PaletteShotgunIce(BasePalette): + """ + Which color to use for X's Shotgun Ice + """ + display_name = "Shotgun Ice Palette" + default = 29 + +class SetPalettes(OptionDict): + """ + Allows you to create colors for each weapon X has. Includes charge levels and Gold Armor customization. + This will override the option preset + + Each one expects 16 values which are mapped to X's colors. + The values can be in SNES RGB (bgr555) with the $ prefix or PC RGB (rgb888) with the # prefix. + """ + display_name = "Set Custom Palettes" + schema = Schema({ + Optional(color_set): list for color_set in x_palette_set_offsets.keys() + }) + default = {} + mmx_option_groups = [ OptionGroup("Gameplay Options", [ StartingLifeCount, @@ -357,6 +465,7 @@ class SparkMandrillTweaks(OptionSet): ]), OptionGroup("Boss Weaknesses", [ BossWeaknessRando, + PlandoWeaknesses, BossWeaknessStrictness, BossRandomizedHP, LogicBossWeakness, @@ -366,6 +475,18 @@ class SparkMandrillTweaks(OptionSet): ArmoredArmadilloTweaks, SparkMandrillTweaks, ]), + OptionGroup("Aesthetic", [ + SetPalettes, + PaletteDefault, + PaletteHomingTorpedo, + PaletteChameleonSting, + PaletteRollingShield, + PaletteFireWave, + PaletteStormTornado, + PaletteElectricSpark, + PaletteBoomerangCutter, + PaletteShotgunIce, + ]), ] @dataclass @@ -402,3 +523,13 @@ class MMXOptions(PerGameCommonOptions): chill_penguin_tweaks: ChillPenguinTweaks armored_armadillo_tweaks: ArmoredArmadilloTweaks spark_mandrill_tweaks: SparkMandrillTweaks + player_palettes: SetPalettes + palette_default: PaletteDefault + palette_homing_torpedo: PaletteHomingTorpedo + palette_chameleon_sting: PaletteChameleonSting + palette_rolling_shield: PaletteRollingShield + palette_fire_wave: PaletteFireWave + palette_storm_tornado: PaletteStormTornado + palette_electric_spark: PaletteElectricSpark + palette_boomerang_cutter: PaletteBoomerangCutter + palette_shotgun_ice: PaletteShotgunIce diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index 79d0dfe98656..d7c561fdc7ed 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -1,13 +1,17 @@ -import typing -import bsdiff4 import Utils import hashlib import os -from pkgutil import get_data + +from typing import TYPE_CHECKING, Iterable + +if TYPE_CHECKING: + from . import MMXWorld from worlds.AutoWorld import World from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes +from .Aesthetics import get_palette_bytes, player_palettes + HASH_US = 'a10071fa78554b57538d0b459e00d224' HASH_US_REV_1 = 'df1cc0c8c8c4b61e3b834cc03366611c' HASH_LEGACY = 'f1dfbbcdc3d8cdeafa4b4b9aa51a56d6' @@ -56,6 +60,18 @@ STARTING_ID + 0x0033: ["weapon refill", 8], } +x_palette_set_offsets = { + "Default": 0x02B700, + "Homing Torpedo": 0x02CC40, + "Chameleon Sting": 0x02CC60, + "Rolling Shield": 0x02CD20, + "Fire Wave": 0x02CD00, + "Storm Tornado": 0x02CCC0, + "Electric Spark": 0x02CCE0, + "Boomerang Cutter": 0x02CCA0, + "Shotgun Ice": 0x02CC80, +} + boss_weakness_offsets = { "Sting Chameleon": 0x37E20, "Storm Eagle": 0x37E60, @@ -156,11 +172,37 @@ def get_source_data(cls) -> bytes: def write_byte(self, offset, value): self.write_token(APTokenTypes.WRITE, offset, value.to_bytes(1, "little")) - def write_bytes(self, offset, value: typing.Iterable[int]): + def write_bytes(self, offset, value: Iterable[int]): self.write_token(APTokenTypes.WRITE, offset, bytes(value)) -def adjust_boss_damage_table(world: World, patch: MMXProcedurePatch): +def adjust_palettes(world: "MMXWorld", patch: MMXProcedurePatch): + player_palette_options = { + "Default": world.options.palette_default.current_key, + "Homing Torpedo": world.options.palette_homing_torpedo.current_key, + "Chameleon Sting": world.options.palette_chameleon_sting.current_key, + "Rolling Shield": world.options.palette_rolling_shield.current_key, + "Fire Wave": world.options.palette_fire_wave.current_key, + "Storm Tornado": world.options.palette_storm_tornado.current_key, + "Electric Spark": world.options.palette_electric_spark.current_key, + "Boomerang Cutter": world.options.palette_boomerang_cutter.current_key, + "Shotgun Ice": world.options.palette_shotgun_ice.current_key, + } + player_custom_palettes = world.options.player_palettes + for palette_set, offset in x_palette_set_offsets.items(): + palette_option = player_palette_options[palette_set] + palette = player_palettes[palette_option] + + if palette_set in player_custom_palettes.keys(): + if len(player_custom_palettes[palette_set]) == 0x10: + palette = player_custom_palettes[palette_set] + else: + print (f"[{world.multiworld.player_name[world.player]}] Custom palette set for {palette_set} doesn't have exactly 16 colors. Falling back to the selected preset ({palette_option})") + data = get_palette_bytes(palette) + patch.write_bytes(offset, data) + + +def adjust_boss_damage_table(world: "MMXWorld", patch: MMXProcedurePatch): for boss, data in world.boss_weakness_data.items(): offset = boss_weakness_offsets[boss] patch.write_bytes(offset, bytearray(data)) @@ -171,17 +213,16 @@ def adjust_boss_damage_table(world: World, patch: MMXProcedurePatch): # Write weaknesses to a table offset = 0x17E9A2 for _, entries in world.boss_weaknesses.items(): - data = [0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF] + data = [0xFF for _ in range(16)] i = 0 for entry in entries: data[i] = entry[1] i += 1 patch.write_bytes(offset, bytearray(data)) - #print (f"Boss: {_} | Offset: 0x{offset - 0x17E9A2:04X}") - offset += 8 + offset += 16 -def adjust_boss_hp(world: World, patch: MMXProcedurePatch): +def adjust_boss_hp(world: "MMXWorld", patch: MMXProcedurePatch): option = world.options.boss_randomize_hp if option == "weak": ranges = [1,32] @@ -196,7 +237,7 @@ def adjust_boss_hp(world: World, patch: MMXProcedurePatch): patch.write_byte(offset, world.random.randint(ranges[0], ranges[1])) -def patch_rom(world: World, patch: MMXProcedurePatch): +def patch_rom(world: "MMXWorld", patch: MMXProcedurePatch): # Prepare some ROM locations to receive the basepatch output patch.write_bytes(0x00098C, bytearray([0xFF,0xFF,0xFF])) patch.write_bytes(0x0009AE, bytearray([0xFF,0xFF,0xFF])) @@ -238,7 +279,8 @@ def patch_rom(world: World, patch: MMXProcedurePatch): 0xFF,0xFF,0xFF,0xFF,0xFF])) adjust_boss_damage_table(world, patch) - + adjust_palettes(world, patch) + if world.options.boss_randomize_hp != "off": adjust_boss_hp(world, patch) @@ -279,7 +321,6 @@ def patch_rom(world: World, patch: MMXProcedurePatch): final_value = 0 for tweak in selected_tweaks: final_value |= enemy_tweaks_indexes[boss][tweak] - #print (f"Boss: {boss} | Offset: 0x{offset:06X} | Config: 0x{final_value:04X}") patch.write_bytes(offset, bytearray([final_value & 0xFF, (final_value >> 8) & 0xFF])) # Edit the ROM header diff --git a/worlds/mmx/Weaknesses.py b/worlds/mmx/Weaknesses.py index bac8fe916850..5cffd5f4536e 100644 --- a/worlds/mmx/Weaknesses.py +++ b/worlds/mmx/Weaknesses.py @@ -1,5 +1,10 @@ from .Names import ItemName +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import MMXWorld + WEAKNESS_UNCHARGED_DMG = 0x03 WEAKNESS_CHARGED_DMG = 0x05 @@ -362,7 +367,7 @@ ], } -def handle_weaknesses(world): +def handle_weaknesses(world: "MMXWorld"): shuffle_type = world.options.boss_weakness_rando.value strictness_type = world.options.boss_weakness_strictness.value boss_weakness_plando = world.options.boss_weakness_plando.value @@ -382,6 +387,8 @@ def handle_weaknesses(world): damage_table = damage_templates["Allow Buster"].copy() elif strictness_type == 2: damage_table = damage_templates["Allow Upgraded Buster"].copy() + world.boss_weaknesses[boss].append(weapons_chaotic["Charged Shot (Level 3)"][0]) + world.boss_weaknesses[boss].append(weapons_chaotic["Charged Shot (Level 3)"][1]) else: damage_table = damage_templates["Only Weakness"].copy() diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index d57429af9c56ed33a9b9fa638981085afa4ceb0c..394f24ae8aeda38564f7773ba74b5ba179e5a6f5 100644 GIT binary patch literal 12367 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 P53_b8F2M)6=iC1WB!QN} diff --git a/worlds/mmx/docs/setup_en.md b/worlds/mmx/docs/setup_en.md index 771f61e0da33..e5b82c2f1397 100644 --- a/worlds/mmx/docs/setup_en.md +++ b/worlds/mmx/docs/setup_en.md @@ -61,7 +61,7 @@ files. Your patch file should have a `.apmmx` extension. Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the client, and will also create your ROM in the same place as your patch file. -### Connect to the multiserver +### Connect to the multiworld When the client launched automatically, SNI should have also automatically launched in the background. If this is its first time launching, you may be prompted to allow it to communicate through the Windows Firewall. From c72c0e5e353569d4d4b23c11773fab646c7866a1 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 23 Aug 2024 22:22:44 -0700 Subject: [PATCH 68/71] Update mmx_basepatch.bsdiff4 --- worlds/mmx/data/mmx_basepatch.bsdiff4 | Bin 12367 -> 12456 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 index 394f24ae8aeda38564f7773ba74b5ba179e5a6f5..f3e9dc0045db8aa4052d4deefd3ef2384082b1f0 100644 GIT binary patch literal 12456 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 Date: Sat, 7 Sep 2024 02:12:26 -0700 Subject: [PATCH 69/71] Update en_Mega Man X.md --- worlds/mmx/docs/en_Mega Man X.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/mmx/docs/en_Mega Man X.md b/worlds/mmx/docs/en_Mega Man X.md index 9fd83dfb94c3..f39d36844751 100644 --- a/worlds/mmx/docs/en_Mega Man X.md +++ b/worlds/mmx/docs/en_Mega Man X.md @@ -40,7 +40,7 @@ much smoother experience. checkpoint at the stage select screen. Switch between different checkpoints with `L` or `R`. - **Enhanced Helmet:** By getting the Helmet Upgrade item, the Checkpoint Selector will allow you to travel to any checkpoint regardless if you have visited them or not. -- **Sigma's Fortress Selector:** You can switch which X-Hunter Base level you will travel to by pressing `SELECT` at +- **Sigma's Fortress Selector:** You can switch which Fortress level you will travel to by pressing `SELECT` at the stage select screen. ## What is EnergyLink? From f1631776eb091ad140ae32f70f50c8cfa7195ab7 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Sat, 7 Sep 2024 12:37:09 -0700 Subject: [PATCH 70/71] typing, removed some imports and made some changes suggested for X3 --- worlds/mmx/Aesthetics.py | 2 + worlds/mmx/Items.py | 13 ++-- worlds/mmx/Levels.py | 2 - worlds/mmx/Locations.py | 12 ++-- worlds/mmx/Regions.py | 26 ++++--- worlds/mmx/Rom.py | 25 +++---- worlds/mmx/Rules.py | 143 ++++++++++++++++++++------------------- worlds/mmx/Weaknesses.py | 2 +- worlds/mmx/__init__.py | 47 ++++++------- 9 files changed, 138 insertions(+), 134 deletions(-) diff --git a/worlds/mmx/Aesthetics.py b/worlds/mmx/Aesthetics.py index 5da0135cb061..9be8c02191c5 100644 --- a/worlds/mmx/Aesthetics.py +++ b/worlds/mmx/Aesthetics.py @@ -109,6 +109,7 @@ ], } + def get_palette_bytes(palette: Dict[str, List]) -> bytearray: output_data = bytearray() for hexcol in palette: @@ -126,6 +127,7 @@ def get_palette_bytes(palette: Dict[str, List]) -> bytearray: output_data.extend(bytearray(byte_data)) return output_data + def rgb888_to_bgr555(red, green, blue) -> bytes: red = red >> 3 green = green >> 3 diff --git a/worlds/mmx/Items.py b/worlds/mmx/Items.py index e679fe9890ec..1680664fbcc9 100644 --- a/worlds/mmx/Items.py +++ b/worlds/mmx/Items.py @@ -1,11 +1,10 @@ -import typing - -from BaseClasses import Item, ItemClassification -from worlds.AutoWorld import World +from BaseClasses import Item from .Names import ItemName -class ItemData(typing.NamedTuple): - code: typing.Optional[int] +from typing import NamedTuple, Optional, Dict + +class ItemData(NamedTuple): + code: Optional[int] progression: bool trap: bool = False quantity: int = 1 @@ -112,4 +111,4 @@ class MMXItem(Item): **special_weapons, } -lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} \ No newline at end of file +lookup_id_to_name: Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} \ No newline at end of file diff --git a/worlds/mmx/Levels.py b/worlds/mmx/Levels.py index 2a4fd2e71688..63c7e5e10b1c 100644 --- a/worlds/mmx/Levels.py +++ b/worlds/mmx/Levels.py @@ -1,5 +1,3 @@ - -from worlds.AutoWorld import World from .Names import LocationName location_id_to_level_id = { diff --git a/worlds/mmx/Locations.py b/worlds/mmx/Locations.py index a08b2707cc96..99218645f435 100644 --- a/worlds/mmx/Locations.py +++ b/worlds/mmx/Locations.py @@ -1,9 +1,11 @@ -import typing - from BaseClasses import Location -from worlds.AutoWorld import World from .Names import LocationName +from typing import TYPE_CHECKING, Dict + +if TYPE_CHECKING: + from . import MMXWorld + class MMXLocation(Location): game = "Mega Man X" @@ -180,7 +182,7 @@ def __init__(self, player: int, name: str = '', address: int = None, parent=None }, } -def setup_locations(world: World): +def setup_locations(world: "MMXWorld") -> Dict[int, str]: location_table = { **stage_clears, **stage_location_table, @@ -193,4 +195,4 @@ def setup_locations(world: World): return location_table -lookup_id_to_name: typing.Dict[int, str] = {id: name for name, _ in all_locations.items()} +lookup_id_to_name: Dict[int, str] = {id: name for name, _ in all_locations.items()} diff --git a/worlds/mmx/Regions.py b/worlds/mmx/Regions.py index bb373c02177c..51299c1fa28e 100644 --- a/worlds/mmx/Regions.py +++ b/worlds/mmx/Regions.py @@ -1,13 +1,19 @@ -import typing - -from BaseClasses import CollectionState, MultiWorld, Region, Entrance, ItemClassification +from BaseClasses import MultiWorld, Region, ItemClassification from .Locations import MMXLocation from .Items import MMXItem -from .Names import LocationName, ItemName, RegionName, EventName +from .Names import LocationName, RegionName, EventName from worlds.AutoWorld import World +from typing import TYPE_CHECKING, Dict + +if TYPE_CHECKING: + from . import MMXWorld + + +def create_regions(world: "MMXWorld", active_locations: Dict[int, str]) -> None: + multiworld = world.multiworld + player = world.player -def create_regions(multiworld: MultiWorld, player: int, world: World, active_locations): menu = create_region(multiworld, player, active_locations, 'Menu') intro = create_region(multiworld, player, active_locations, RegionName.intro) @@ -323,7 +329,7 @@ def create_regions(multiworld: MultiWorld, player: int, world: World, active_loc add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_4, LocationName.sigma_fortress_3_1up) -def connect_regions(world: World): +def connect_regions(world: World) -> None: connect(world, "Menu", RegionName.intro) connect(world, RegionName.intro, RegionName.armored_armadillo) @@ -440,7 +446,7 @@ def connect_regions(world: World): connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_after_rematch_5) -def create_region(multiworld: MultiWorld, player: int, active_locations, name: str, locations=None): +def create_region(multiworld: MultiWorld, player: int, active_locations, name: str, locations=None) -> Region: ret = Region(name, player, multiworld) if locations: for locationName in locations: @@ -452,7 +458,7 @@ def create_region(multiworld: MultiWorld, player: int, active_locations, name: s return ret -def add_event_to_region(multiworld: MultiWorld, player: int, region_name: str, event_name: str, event_item=None): +def add_event_to_region(multiworld: MultiWorld, player: int, region_name: str, event_name: str, event_item=None) -> None: region = multiworld.get_region(region_name, player) event = MMXLocation(player, event_name, None, region) if event_item: @@ -462,7 +468,7 @@ def add_event_to_region(multiworld: MultiWorld, player: int, region_name: str, e region.locations.append(event) -def add_location_to_region(multiworld: MultiWorld, player: int, active_locations, region_name: str, location_name: str): +def add_location_to_region(multiworld: MultiWorld, player: int, active_locations, region_name: str, location_name: str) -> None: region = multiworld.get_region(region_name, player) loc_id = active_locations.get(location_name, 0) if loc_id: @@ -470,7 +476,7 @@ def add_location_to_region(multiworld: MultiWorld, player: int, active_locations region.locations.append(location) -def connect(world: World, source: str, target: str): +def connect(world: World, source: str, target: str) -> None: source_region: Region = world.multiworld.get_region(source, world.player) target_region: Region = world.multiworld.get_region(target, world.player) source_region.connect(target_region) diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py index d7c561fdc7ed..3dc5befe0ec0 100644 --- a/worlds/mmx/Rom.py +++ b/worlds/mmx/Rom.py @@ -1,17 +1,18 @@ import Utils import hashlib import os - -from typing import TYPE_CHECKING, Iterable - -if TYPE_CHECKING: - from . import MMXWorld +import settings from worlds.AutoWorld import World from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes from .Aesthetics import get_palette_bytes, player_palettes +from typing import TYPE_CHECKING, Iterable + +if TYPE_CHECKING: + from . import MMXWorld + HASH_US = 'a10071fa78554b57538d0b459e00d224' HASH_US_REV_1 = 'df1cc0c8c8c4b61e3b834cc03366611c' HASH_LEGACY = 'f1dfbbcdc3d8cdeafa4b4b9aa51a56d6' @@ -169,14 +170,14 @@ class MMXProcedurePatch(APProcedurePatch, APTokenMixin): def get_source_data(cls) -> bytes: return get_base_rom_bytes() - def write_byte(self, offset, value): + def write_byte(self, offset, value) -> None: self.write_token(APTokenTypes.WRITE, offset, value.to_bytes(1, "little")) - def write_bytes(self, offset, value: Iterable[int]): + def write_bytes(self, offset, value: Iterable[int]) -> None: self.write_token(APTokenTypes.WRITE, offset, bytes(value)) -def adjust_palettes(world: "MMXWorld", patch: MMXProcedurePatch): +def adjust_palettes(world: "MMXWorld", patch: MMXProcedurePatch) -> None: player_palette_options = { "Default": world.options.palette_default.current_key, "Homing Torpedo": world.options.palette_homing_torpedo.current_key, @@ -202,7 +203,7 @@ def adjust_palettes(world: "MMXWorld", patch: MMXProcedurePatch): patch.write_bytes(offset, data) -def adjust_boss_damage_table(world: "MMXWorld", patch: MMXProcedurePatch): +def adjust_boss_damage_table(world: "MMXWorld", patch: MMXProcedurePatch) -> None: for boss, data in world.boss_weakness_data.items(): offset = boss_weakness_offsets[boss] patch.write_bytes(offset, bytearray(data)) @@ -222,7 +223,7 @@ def adjust_boss_damage_table(world: "MMXWorld", patch: MMXProcedurePatch): offset += 16 -def adjust_boss_hp(world: "MMXWorld", patch: MMXProcedurePatch): +def adjust_boss_hp(world: "MMXWorld", patch: MMXProcedurePatch) -> None: option = world.options.boss_randomize_hp if option == "weak": ranges = [1,32] @@ -237,7 +238,7 @@ def adjust_boss_hp(world: "MMXWorld", patch: MMXProcedurePatch): patch.write_byte(offset, world.random.randint(ranges[0], ranges[1])) -def patch_rom(world: "MMXWorld", patch: MMXProcedurePatch): +def patch_rom(world: "MMXWorld", patch: MMXProcedurePatch) -> None: # Prepare some ROM locations to receive the basepatch output patch.write_bytes(0x00098C, bytearray([0xFF,0xFF,0xFF])) patch.write_bytes(0x0009AE, bytearray([0xFF,0xFF,0xFF])) @@ -388,7 +389,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: def get_base_rom_path(file_name: str = "") -> str: - options = Utils.get_options() + options: settings.Settings = settings.get_settings() if not file_name: file_name = options["mmx_options"]["rom_file"] if not os.path.exists(file_name): diff --git a/worlds/mmx/Rules.py b/worlds/mmx/Rules.py index 742bfa0bd6c8..be13670b1e5b 100644 --- a/worlds/mmx/Rules.py +++ b/worlds/mmx/Rules.py @@ -1,9 +1,13 @@ from worlds.generic.Rules import add_rule, set_rule from BaseClasses import CollectionState -from . import MMXWorld from .Names import LocationName, ItemName, RegionName, EventName +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import MMXWorld + bosses = { "Sting Chameleon": [ f"{RegionName.sting_chameleon_swamp} -> {RegionName.sting_chameleon_boss}", @@ -64,7 +68,7 @@ } -def set_rules(world: MMXWorld): +def build_rules(world: "MMXWorld") -> None: player = world.player multiworld = world.multiworld jammed_buster = world.options.jammed_buster.value @@ -72,75 +76,75 @@ def set_rules(world: MMXWorld): multiworld.completion_condition[player] = lambda state: state.has(ItemName.victory, player) # Intro entrance rules - set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.armored_armadillo}", player), + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.armored_armadillo}"), lambda state: state.has(ItemName.stage_armored_armadillo, player)) - set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.boomer_kuwanger}", player), + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.boomer_kuwanger}"), lambda state: state.has(ItemName.stage_boomer_kuwanger, player)) - set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.chill_penguin}", player), + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.chill_penguin}"), lambda state: state.has(ItemName.stage_chill_penguin, player)) - set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.flame_mammoth}", player), + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.flame_mammoth}"), lambda state: state.has(ItemName.stage_flame_mammoth, player)) - set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.launch_octopus}", player), + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.launch_octopus}"), lambda state: state.has(ItemName.stage_launch_octopus, player)) - set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.spark_mandrill}", player), + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.spark_mandrill}"), lambda state: state.has(ItemName.stage_spark_mandrill, player)) - set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.sting_chameleon}", player), + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.sting_chameleon}"), lambda state: state.has(ItemName.stage_sting_chameleon, player)) - set_rule(multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.storm_eagle}", player), + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.storm_eagle}"), lambda state: state.has(ItemName.stage_storm_eagle, player)) # Fortress entrance rules fortress_open = world.options.sigma_open.value - entrance = multiworld.get_entrance(f"{RegionName.intro} -> {RegionName.sigma_fortress}", player) + entrance = world.get_entrance(f"{RegionName.intro} -> {RegionName.sigma_fortress}") if len(fortress_open) == 0: add_rule(entrance, lambda state: state.has(ItemName.stage_sigma_fortress, player)) else: - if "Medals" in fortress_open and world.options.sigma_medal_count.value > 0: + if "Medals" in fortress_open and world.options.sigma_medal_count > 0: add_rule(entrance, lambda state: state.has(ItemName.maverick_medal, player, world.options.sigma_medal_count.value)) - if "Weapons" in fortress_open and world.options.sigma_weapon_count.value > 0: - add_rule(entrance, lambda state: state.has_group("Weapons", player, world.options.sigma_weapon_count.value)) - if "Armor Upgrades" in fortress_open and world.options.sigma_upgrade_count.value > 0: - add_rule(entrance, lambda state: state.has_group("Armor Upgrades", player, world.options.sigma_upgrade_count.value)) - if "Heart Tanks" in fortress_open and world.options.sigma_heart_tank_count.value > 0: + if "Weapons" in fortress_open and world.options.sigma_weapon_count > 0: + add_rule(entrance, lambda state: state.has_group_unique("Weapons", player, world.options.sigma_weapon_count.value)) + if "Armor Upgrades" in fortress_open and world.options.sigma_upgrade_count > 0: + add_rule(entrance, lambda state: state.has_group_unique("Armor Upgrades", player, world.options.sigma_upgrade_count.value)) + if "Heart Tanks" in fortress_open and world.options.sigma_heart_tank_count > 0: add_rule(entrance, lambda state: state.has(ItemName.heart_tank, player, world.options.sigma_heart_tank_count.value)) - if "Sub Tanks" in fortress_open and world.options.sigma_sub_tank_count.value > 0: + if "Sub Tanks" in fortress_open and world.options.sigma_sub_tank_count > 0: add_rule(entrance, lambda state: state.has(ItemName.sub_tank, player, world.options.sigma_sub_tank_count.value)) - if world.options.logic_leg_sigma.value: + if world.options.logic_leg_sigma: add_rule(entrance, lambda state: state.has(ItemName.legs, player)) # Sigma Fortress level rules if world.options.sigma_all_levels: - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress} -> {RegionName.sigma_fortress_4}", player), + set_rule(world.get_entrance(f"{RegionName.sigma_fortress} -> {RegionName.sigma_fortress_4}"), lambda state: ( state.has(EventName.sigma_fortress_1_clear, player) and state.has(EventName.sigma_fortress_2_clear, player) and state.has(EventName.sigma_fortress_3_clear, player) )) else: - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_1_boss} -> {RegionName.sigma_fortress_2}", player), + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_1_boss} -> {RegionName.sigma_fortress_2}"), lambda state: state.has(EventName.sigma_fortress_1_clear, player)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_2_boss} -> {RegionName.sigma_fortress_3}", player), + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_2_boss} -> {RegionName.sigma_fortress_3}"), lambda state: state.has(EventName.sigma_fortress_2_clear, player)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3_boss} -> {RegionName.sigma_fortress_4}", player), + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_3_boss} -> {RegionName.sigma_fortress_4}"), lambda state: state.has(EventName.sigma_fortress_3_clear, player)) # Sigma rules - add_rule(multiworld.get_location(LocationName.sigma_fortress_4_sigma, player), + add_rule(world.get_location(LocationName.sigma_fortress_4_sigma), lambda state: state.has(ItemName.arms, player, jammed_buster + 1)) # Chill Penguin collectibles - set_rule(multiworld.get_location(LocationName.chill_penguin_heart_tank, player), + set_rule(world.get_location(LocationName.chill_penguin_heart_tank), lambda state: state.has(ItemName.fire_wave, player)) # Flame Mammoth collectibles - set_rule(multiworld.get_location(LocationName.flame_mammoth_arms, player), + set_rule(world.get_location(LocationName.flame_mammoth_arms), lambda state: ( state.has(ItemName.legs, player) and state.has(ItemName.helmet, player) )) - set_rule(multiworld.get_location(LocationName.flame_mammoth_heart_tank, player), + set_rule(world.get_location(LocationName.flame_mammoth_heart_tank), lambda state: ( state.has(EventName.chill_penguin_clear, player) or ( @@ -148,87 +152,86 @@ def set_rules(world: MMXWorld): state.has(ItemName.arms, player, jammed_buster + 1) ) )) - set_rule(multiworld.get_location(LocationName.flame_mammoth_sub_tank, player), + set_rule(world.get_location(LocationName.flame_mammoth_sub_tank), lambda state: state.has(ItemName.legs, player)) # Boomer Kuwanger collectibles - set_rule(multiworld.get_location(LocationName.boomer_kuwanger_heart_tank, player), + set_rule(world.get_location(LocationName.boomer_kuwanger_heart_tank), lambda state: state.has(ItemName.boomerang_cutter, player)) # Sting Chameleon collectibles - set_rule(multiworld.get_location(LocationName.sting_chameleon_body, player), + set_rule(world.get_location(LocationName.sting_chameleon_body), lambda state: state.has(ItemName.legs, player)) - set_rule(multiworld.get_location(LocationName.sting_chameleon_heart_tank, player), + set_rule(world.get_location(LocationName.sting_chameleon_heart_tank), lambda state: ( state.has(ItemName.legs, player) and state.has(EventName.launch_octopus_clear, player) )) # Spark Mandrill collectibles - set_rule(multiworld.get_location(LocationName.spark_mandrill_sub_tank, player), + set_rule(world.get_location(LocationName.spark_mandrill_sub_tank), lambda state: state.has(ItemName.boomerang_cutter, player)) - set_rule(multiworld.get_location(LocationName.spark_mandrill_heart_tank, player), + set_rule(world.get_location(LocationName.spark_mandrill_heart_tank), lambda state: ( state.has(ItemName.boomerang_cutter, player) or state.has(ItemName.legs, player) )) # Storm Eagle collectibles - set_rule(multiworld.get_location(LocationName.storm_eagle_heart_tank, player), + set_rule(world.get_location(LocationName.storm_eagle_heart_tank), lambda state: state.has(ItemName.legs, player)) - set_rule(multiworld.get_location(LocationName.storm_eagle_helmet, player), + set_rule(world.get_location(LocationName.storm_eagle_helmet), lambda state: state.has(ItemName.legs, player)) # Handle pickupsanity - if world.options.pickupsanity.value: + if world.options.pickupsanity: add_pickupsanity_logic(world) # Handle bosses weakness - if world.options.logic_boss_weakness.value or world.options.boss_weakness_strictness.value >= 2: + if world.options.logic_boss_weakness or world.options.boss_weakness_strictness >= 2: add_boss_weakness_logic(world) # Handle charged shotgun ice logic - if world.options.logic_charged_shotgun_ice.value: + if world.options.logic_charged_shotgun_ice: add_charged_shotgun_ice_logic(world) # Handle helmet logic - if world.options.logic_helmet_checkpoints.value: + if world.options.logic_helmet_checkpoints: add_helmet_logic(world) -def add_pickupsanity_logic(world: MMXWorld): +def add_pickupsanity_logic(world: "MMXWorld") -> None: player = world.player - multiworld = world.multiworld jammed_buster = world.options.jammed_buster.value - set_rule(multiworld.get_location(LocationName.chill_penguin_hp_1, player), + set_rule(world.get_location(LocationName.chill_penguin_hp_1), lambda state: state.has(ItemName.fire_wave, player)) - set_rule(multiworld.get_location(LocationName.armored_armadillo_hp_1, player), + set_rule(world.get_location(LocationName.armored_armadillo_hp_1), lambda state: state.has(ItemName.helmet, player)) - set_rule(multiworld.get_location(LocationName.armored_armadillo_hp_2, player), + set_rule(world.get_location(LocationName.armored_armadillo_hp_2), lambda state: state.has(ItemName.helmet, player)) - set_rule(multiworld.get_location(LocationName.sigma_fortress_3_hp_1, player), + set_rule(world.get_location(LocationName.sigma_fortress_3_hp_1), lambda state: ( state.has(ItemName.legs, player) and state.has(ItemName.boomerang_cutter, player) )) - set_rule(multiworld.get_location(LocationName.sigma_fortress_3_hp_2, player), + set_rule(world.get_location(LocationName.sigma_fortress_3_hp_2), lambda state: state.has(ItemName.boomerang_cutter, player)) - set_rule(multiworld.get_location(LocationName.sigma_fortress_3_energy_1, player), + set_rule(world.get_location(LocationName.sigma_fortress_3_energy_1), lambda state: state.has(ItemName.boomerang_cutter, player)) - set_rule(multiworld.get_location(LocationName.sigma_fortress_3_hp_4, player), + set_rule(world.get_location(LocationName.sigma_fortress_3_hp_4), lambda state: ( state.has(ItemName.arms, player, jammed_buster + 1) and state.has(ItemName.chameleon_sting, player) )) - set_rule(multiworld.get_location(LocationName.sigma_fortress_3_energy_3, player), + set_rule(world.get_location(LocationName.sigma_fortress_3_energy_3), lambda state: ( state.has(ItemName.arms, player, jammed_buster + 1) and state.has(ItemName.chameleon_sting, player) )) - set_rule(multiworld.get_location(LocationName.sigma_fortress_3_1up, player), + set_rule(world.get_location(LocationName.sigma_fortress_3_1up), lambda state: ( state.has(ItemName.arms, player, jammed_buster + 1) and ( @@ -246,9 +249,8 @@ def check_weaknesses(state: CollectionState, player: int, rulesets: list) -> boo return any(states) -def add_boss_weakness_logic(world: MMXWorld): +def add_boss_weakness_logic(world: "MMXWorld") -> None: player = world.player - multiworld = world.multiworld jammed_buster = world.options.jammed_buster.value for boss, regions in bosses.items(): @@ -272,59 +274,58 @@ def add_boss_weakness_logic(world: MMXWorld): if rulesets is not None: for region in regions: - add_rule(multiworld.get_entrance(region, player), - lambda state, rulesets=rulesets: check_weaknesses(state, player, rulesets)) + add_rule(world.get_entrance(region), + lambda state, rulesets=rulesets: check_weaknesses(state, player, rulesets)) -def add_charged_shotgun_ice_logic(world: MMXWorld): +def add_charged_shotgun_ice_logic(world: "MMXWorld") -> None: player = world.player - multiworld = world.multiworld jammed_buster = world.options.jammed_buster.value # Flame Mammoth collectibles - add_rule(multiworld.get_location(LocationName.flame_mammoth_sub_tank, player), + add_rule(world.get_location(LocationName.flame_mammoth_sub_tank), lambda state: ( state.has(ItemName.arms, player, jammed_buster + 1) and state.has(ItemName.boomerang_cutter, player) and state.has(ItemName.shotgun_ice, player) )) # Boomer Kuwanger collectibles - add_rule(multiworld.get_location(LocationName.boomer_kuwanger_heart_tank, player), + add_rule(world.get_location(LocationName.boomer_kuwanger_heart_tank), lambda state: ( state.has(ItemName.shotgun_ice, player) and state.has(ItemName.arms, player, jammed_buster + 1) )) # Sting Chameleon collectibles - add_rule(multiworld.get_location(LocationName.sting_chameleon_body, player), + add_rule(world.get_location(LocationName.sting_chameleon_body), lambda state: ( state.has(ItemName.shotgun_ice, player) and state.has(ItemName.arms, player, jammed_buster + 1) )) -def add_helmet_logic(world: MMXWorld): + +def add_helmet_logic(world: "MMXWorld") -> None: player = world.player - multiworld = world.multiworld - set_rule(multiworld.get_entrance(f"{RegionName.spark_mandrill} -> {RegionName.spark_mandrill_deep}", player), + set_rule(world.get_entrance(f"{RegionName.spark_mandrill} -> {RegionName.spark_mandrill_deep}"), lambda state: state.has(ItemName.helmet, player, 1)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_1} -> {RegionName.sigma_fortress_1_vertical}", player), + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_1} -> {RegionName.sigma_fortress_1_vertical}"), lambda state: state.has(ItemName.helmet, player, 1)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_1} -> {RegionName.sigma_fortress_1_before_boss}", player), + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_1} -> {RegionName.sigma_fortress_1_before_boss}"), lambda state: state.has(ItemName.helmet, player, 1)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_2} -> {RegionName.sigma_fortress_2_ride}", player), + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_2} -> {RegionName.sigma_fortress_2_ride}"), lambda state: state.has(ItemName.helmet, player, 1)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_2} -> {RegionName.sigma_fortress_2_before_boss}", player), + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_2} -> {RegionName.sigma_fortress_2_before_boss}"), lambda state: state.has(ItemName.helmet, player, 1)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_1}", player), + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_1}"), lambda state: state.has(ItemName.helmet, player, 1)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_2}", player), + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_2}"), lambda state: state.has(ItemName.helmet, player, 1)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_3}", player), + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_3}"), lambda state: state.has(ItemName.helmet, player, 1)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_4}", player), + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_4}"), lambda state: state.has(ItemName.helmet, player, 1)) - set_rule(multiworld.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_5}", player), + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_5}"), lambda state: state.has(ItemName.helmet, player, 1)) diff --git a/worlds/mmx/Weaknesses.py b/worlds/mmx/Weaknesses.py index 5cffd5f4536e..77acca52831b 100644 --- a/worlds/mmx/Weaknesses.py +++ b/worlds/mmx/Weaknesses.py @@ -367,7 +367,7 @@ ], } -def handle_weaknesses(world: "MMXWorld"): +def handle_weaknesses(world: "MMXWorld") -> None: shuffle_type = world.options.boss_weakness_rando.value strictness_type = world.options.boss_weakness_strictness.value boss_weakness_plando = world.options.boss_weakness_plando.value diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py index 3284dc51f746..7eb23b27371c 100644 --- a/worlds/mmx/__init__.py +++ b/worlds/mmx/__init__.py @@ -1,25 +1,23 @@ -import dataclasses import os -import typing -import math import settings -import hashlib import threading import pkgutil from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification -from Options import PerGameCommonOptions from worlds.AutoWorld import World, WebWorld -from .Items import MMXItem, ItemData, item_table, junk_table, item_groups -from .Locations import MMXLocation, setup_locations, all_locations, location_groups +from .Items import MMXItem, item_table, junk_table, item_groups +from .Locations import setup_locations, all_locations, location_groups from .Regions import create_regions, connect_regions -from .Names import ItemName, LocationName, EventName +from .Names import ItemName, LocationName from .Options import MMXOptions, mmx_option_groups from .Client import MMXSNIClient +from .Rules import build_rules from .Levels import location_id_to_level_id from .Weaknesses import handle_weaknesses, weapon_id from .Rom import patch_rom, MMXProcedurePatch, HASH_US, HASH_LEGACY +from typing import Dict, List, Any, ClassVar, TextIO + class MMXSettings(settings.Group): class RomFile(settings.SNESRomPath): """File name of the Mega Man X US ROM""" @@ -66,7 +64,7 @@ class MMXWorld(World): game = "Mega Man X" web = MMXWeb() - settings: typing.ClassVar[MMXSettings] + settings: ClassVar[MMXSettings] options_dataclass = MMXOptions options: MMXOptions @@ -89,15 +87,15 @@ class MMXWorld(World): LocationName.intro_completed, } - def __init__(self, multiworld: MultiWorld, player: int): + def __init__(self, multiworld: MultiWorld, player: int) -> None: self.rom_name_available_event = threading.Event() super().__init__(multiworld, player) def create_regions(self) -> None: location_table = setup_locations(self) - create_regions(self.multiworld, self.player, self, location_table) + create_regions(self, location_table) - itempool: typing.List[MMXItem] = [] + itempool: List[MMXItem] = [] connect_regions(self) @@ -221,7 +219,7 @@ def create_regions(self) -> None: self.multiworld.itempool += itempool - def create_item(self, name: str, force_classification=False) -> Item: + def create_item(self, name: str, force_classification: ItemClassification = False) -> Item: data = item_table[name] if force_classification: @@ -238,18 +236,15 @@ def create_item(self, name: str, force_classification=False) -> Item: return created_item - def set_rules(self): - from .Rules import set_rules + def set_rules(self) -> None: if hasattr(self.multiworld, "generation_is_fake"): if hasattr(self.multiworld, "re_gen_passthrough"): if "Mega Man X" in self.multiworld.re_gen_passthrough: - slot_data = self.multiworld.re_gen_passthrough["Mega Man X"] - self.boss_weaknesses = slot_data["weakness_rules"] - set_rules(self) - + self.boss_weaknesses = self.multiworld.re_gen_passthrough["Mega Man X"]["weakness_rules"] + build_rules(self) - def fill_slot_data(self): + def fill_slot_data(self) -> Dict[int, Any]: slot_data = {} # Write options to slot_data slot_data["energy_link"] = self.options.energy_link.value @@ -294,7 +289,7 @@ def fill_slot_data(self): return slot_data - def generate_early(self): + def generate_early(self) -> None: if ItemName.legs not in self.options.start_inventory_from_pool and self.options.early_legs: self.multiworld.early_items[self.player][ItemName.legs] = 1 @@ -304,14 +299,14 @@ def generate_early(self): handle_weaknesses(self) - def interpret_slot_data(self, slot_data): + def interpret_slot_data(self, slot_data: dict) -> Dict[str, Any]: local_weaknesses = dict() for boss, entries in slot_data["weakness_rules"].items(): local_weaknesses[boss] = entries.copy() return {"weakness_rules": local_weaknesses} - def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: + def write_spoiler(self, spoiler_handle: TextIO) -> None: if self.options.boss_weakness_rando != "vanilla": spoiler_handle.write(f"\nMega Man X boss weaknesses for {self.multiworld.player_name[self.player]}:\n") @@ -323,7 +318,7 @@ def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: spoiler_handle.writelines(f"{boss + ':':<30s}{weaknesses}\n") - def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]): + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: if not self.options.boss_weakness_rando: return @@ -380,7 +375,7 @@ def get_filler_item_name(self) -> str: return self.random.choice(list(junk_table.keys())) - def generate_output(self, output_directory: str): + def generate_output(self, output_directory: str) -> None: try: patch = MMXProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) patch.write_file("mmx_basepatch.bsdiff4", pkgutil.get_data(__name__, "data/mmx_basepatch.bsdiff4")) @@ -396,7 +391,7 @@ def generate_output(self, output_directory: str): self.rom_name_available_event.set() # make sure threading continues and errors are collected - def modify_multidata(self, multidata: dict): + def modify_multidata(self, multidata: dict) -> None: import base64 # wait for self.rom_name to be available. self.rom_name_available_event.wait() From c7d8257410adec79273391aa8bb193f2e545a550 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Sat, 7 Sep 2024 12:37:45 -0700 Subject: [PATCH 71/71] client may be more stable now --- worlds/mmx/Client.py | 484 ++++++++++++++++++++++--------------------- 1 file changed, 248 insertions(+), 236 deletions(-) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py index a139ef41de8e..cc2008d9f20e 100644 --- a/worlds/mmx/Client.py +++ b/worlds/mmx/Client.py @@ -15,6 +15,8 @@ SRAM_START = 0xE00000 MMX_RAM = WRAM_START + 0x1EE00 +MMX_UPGRADE_DATA = WRAM_START + 0x01F10 +MMX_SETTINGS = ROM_START + 0x167C20 MMX_GAME_STATE = WRAM_START + 0x000D1 MMX_MENU_STATE = WRAM_START + 0x000D2 @@ -61,11 +63,11 @@ MMX_PAUSE_STATE = WRAM_START + 0x01F24 MMX_CAN_MOVE = WRAM_START + 0x01F13 -MMX_PICKUPSANITY_ACTIVE = ROM_START + 0x167C27 -MMX_ENERGY_LINK_ENABLED = ROM_START + 0x167C28 -MMX_DEATH_LINK_ACTIVE = ROM_START + 0x167C29 -MMX_JAMMED_BUSTER_ACTIVE = ROM_START + 0x167C2A -MMX_ABILITIES_FLAGS = ROM_START + 0x167C31 +MMX_PICKUPSANITY_ACTIVE = MMX_SETTINGS + 0x07 +MMX_ENERGY_LINK_ENABLED = MMX_SETTINGS + 0x08 +MMX_DEATH_LINK_ACTIVE = MMX_SETTINGS + 0x09 +MMX_JAMMED_BUSTER_ACTIVE = MMX_SETTINGS + 0x0A +MMX_ABILITIES_FLAGS = MMX_SETTINGS + 0x11 MMX_ENERGY_LINK_COUNT = MMX_RAM + 0x00100 MMX_GLOBAL_TIMER = MMX_RAM + 0x00106 @@ -110,21 +112,26 @@ def __init__(self): async def deathlink_kill_player(self, ctx): from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read - validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) - validation = validation[0] | (validation[1] << 8) - if validation != 0xDEAD: + game_data = await snes_read(ctx, MMX_RAM, 0x0140) + game_state_data = await snes_read(ctx, MMX_GAME_STATE, 0x3) + game_progress_data = await snes_read(ctx, MMX_UPGRADE_DATA, 0xF0) + if game_data is None or game_state_data is None or game_progress_data is None: return - receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) - menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) - gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) - can_move = await snes_read(ctx, MMX_CAN_MOVE, 0x7) - pause_state = await snes_read(ctx, MMX_PAUSE_STATE, 0x1) - if menu_state[0] != 0x04 or \ - gameplay_state[0] != 0x04 or \ - pause_state[0] != 0x00 or \ + validation = int.from_bytes(game_data[0x13:0x15], "little") + if validation != 0xDEAD: + return + + receiving_item = game_data[0x15] + menu_state = game_state_data[1] + gameplay_state = game_state_data[2] + can_move = game_progress_data[3:10] + pause_state = game_progress_data[14] + if menu_state != 0x04 or \ + gameplay_state != 0x04 or \ + pause_state != 0x00 or \ can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ - receiving_item[0] != 0x00: + receiving_item != 0x00: return snes_buffered_write(ctx, MMX_CURRENT_HP, bytes([0x80])) @@ -142,9 +149,10 @@ async def deathlink_kill_player(self, ctx): async def validate_rom(self, ctx): from SNIClient import snes_read - energy_link = await snes_read(ctx, MMX_ENERGY_LINK_ENABLED, 0x1) + game_settings = await snes_read(ctx, MMX_SETTINGS, 0x20) rom_name = await snes_read(ctx, MMX_ROMHASH_START, ROMHASH_SIZE) - if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:4] != b"MMX1": + + if rom_name is None or game_settings is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:4] != b"MMX1": if "resync" in ctx.command_processor.commands: ctx.command_processor.commands.pop("resync") if "trade" in ctx.command_processor.commands: @@ -160,7 +168,9 @@ async def validate_rom(self, ctx): ctx.receive_option = 0 ctx.send_option = 0 ctx.allow_collect = True - if energy_link[0]: + + energy_link = game_settings[0x08] + if energy_link: if "refill" not in ctx.command_processor.commands: ctx.command_processor.commands["heal"] = cmd_heal if "refill" not in ctx.command_processor.commands: @@ -170,9 +180,9 @@ async def validate_rom(self, ctx): if "trade" not in ctx.command_processor.commands: ctx.command_processor.commands["trade"] = cmd_trade - death_link = await snes_read(ctx, MMX_DEATH_LINK_ACTIVE, 1) - if death_link[0]: - await ctx.update_death_link(bool(death_link[0] & 0b1)) + death_link = game_settings[0x09] + if death_link: + await ctx.update_death_link(bool(death_link & 0b1)) ctx.rom = rom_name @@ -182,26 +192,30 @@ async def validate_rom(self, ctx): async def game_watcher(self, ctx): from SNIClient import snes_buffered_write, snes_flush_writes, snes_read - game_state = await snes_read(ctx, MMX_GAME_STATE, 0x1) - menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) - gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) + game_data = await snes_read(ctx, MMX_RAM, 0x0140) + game_state_data = await snes_read(ctx, MMX_GAME_STATE, 0x3) + game_progress_data = await snes_read(ctx, MMX_UPGRADE_DATA, 0xF0) + game_settings = await snes_read(ctx, MMX_SETTINGS, 0x20) # Discard uninitialized ROMs - if menu_state is None: + if game_data is None or game_state_data is None or game_progress_data is None or game_settings is None: self.game_state = False self.energy_link_enabled = False self.current_level_value = 42 self.item_queue = [] return - - validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) - validation = validation[0] | (validation[1] << 8) + + validation = int.from_bytes(game_data[0x13:0x15], "little") if validation != 0xDEAD: snes_logger.info(f'ROM not properly validated.') self.game_state = False return - if game_state[0] == 0: + game_state = game_state_data[0] + menu_state = game_state_data[1] + gameplay_state = game_state_data[2] + + if game_state == 0: self.game_state = False self.item_queue = [] self.current_level_value = 42 @@ -222,55 +236,81 @@ async def game_watcher(self, ctx): logger.info(f"Invalid environment for a resync. Please try again during the Title Menu screen.") self.game_state = True - if "DeathLink" in ctx.tags and menu_state[0] == 0x04 and ctx.last_death_link + 1 < time.time(): - currently_dead = gameplay_state[0] == 0x06 + if "DeathLink" in ctx.tags and menu_state == 0x04 and ctx.last_death_link + 1 < time.time(): + currently_dead = gameplay_state == 0x06 await ctx.handle_deathlink_state(currently_dead) - - if game_state[0] != 0x00 and self.data_storage_enabled is True: - await self.handle_data_storage(ctx) - # Handle DataStorage - if ctx.server and ctx.server.socket.open and not self.data_storage_enabled and ctx.team is not None: - self.data_storage_enabled = True - ctx.set_notify(f"mmx_global_timer_{ctx.team}_{ctx.slot}") - ctx.set_notify(f"mmx_deaths_{ctx.team}_{ctx.slot}") - ctx.set_notify(f"mmx_damage_taken_{ctx.team}_{ctx.slot}") - ctx.set_notify(f"mmx_damage_dealt_{ctx.team}_{ctx.slot}") - ctx.set_notify(f"mmx_checkpoints_{ctx.team}_{ctx.slot}") - ctx.set_notify(f"mmx_arsenal_{ctx.team}_{ctx.slot}") - - if self.trade_request is not None: - await self.handle_hp_trade(ctx) + current_hp = await snes_read(ctx, MMX_CURRENT_HP, 0x1) + screen_brightness = await snes_read(ctx, MMX_SCREEN_BRIGHTNESS, 0x1) - await self.handle_item_queue(ctx) + if current_hp is not None and screen_brightness is not None: + game_ram = [ + game_data, + game_state_data, + game_progress_data, + game_settings, + current_hp, + ] + + keys = { + f"mmx_arsenal_{ctx.team}_{ctx.slot}", + f"mmx_checkpoints_{ctx.team}_{ctx.slot}", + f"mmx_global_timer_{ctx.team}_{ctx.slot}", + f"mmx_deaths_{ctx.team}_{ctx.slot}", + f"mmx_damage_dealt_{ctx.team}_{ctx.slot}", + f"mmx_damage_taken_{ctx.team}_{ctx.slot}", + } + + if game_state != 0x00 and self.data_storage_enabled is True and \ + all(key in ctx.stored_data.keys() for key in keys): + await self.handle_data_storage(ctx, game_ram) + + # Handle DataStorage + if not self.using_newer_client: + if ctx.server and ctx.server.socket.open and not self.data_storage_enabled and ctx.team is not None: + self.data_storage_enabled = True + ctx.set_notify(f"mmx_global_timer_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_deaths_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_damage_taken_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_damage_dealt_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_checkpoints_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_arsenal_{ctx.team}_{ctx.slot}") + + if screen_brightness[0] == 0x0F: + self.handle_item_queue(ctx, game_ram) + + if self.trade_request is not None: + self.handle_hp_trade(ctx, game_ram) + + # This is going to be rewritten whenever SNIClient supports on_package + energy_link = game_settings[0x08] + if self.using_newer_client: + if energy_link != 0: + await self.handle_energy_link(ctx, game_ram) + else: + if energy_link != 0: + if self.energy_link_enabled and f'EnergyLink{ctx.team}' in ctx.stored_data: + await self.handle_energy_link(ctx, game_ram) - # This is going to be rewritten whenever SNIClient supports on_package - energy_link = await snes_read(ctx, MMX_ENERGY_LINK_ENABLED, 0x1) - if self.using_newer_client: - if energy_link[0] != 0: - await self.handle_energy_link(ctx) - else: - if energy_link[0] != 0: - if self.energy_link_enabled and f'EnergyLink{ctx.team}' in ctx.stored_data: - await self.handle_energy_link(ctx) + if ctx.server and ctx.server.socket.open and not self.energy_link_enabled and ctx.team is not None: + self.energy_link_enabled = True + ctx.set_notify(f"EnergyLink{ctx.team}") + logger.info(f"Initialized EnergyLink{ctx.team}, use /help to get information about the EnergyLink commands.") - if ctx.server and ctx.server.socket.open and not self.energy_link_enabled and ctx.team is not None: - self.energy_link_enabled = True - ctx.set_notify(f"EnergyLink{ctx.team}") - logger.info(f"Initialized EnergyLink{ctx.team}, use /help to get information about the EnergyLink commands.") + await snes_flush_writes(ctx) from .Rom import weapon_rom_data, upgrades_rom_data, boss_access_rom_data, refill_rom_data from .Levels import location_id_to_level_id from worlds import AutoWorldRegister - defeated_bosses = list(await snes_read(ctx, MMX_DEFEATED_BOSSES, 0x20)) - cleared_levels = list(await snes_read(ctx, MMX_COMPLETED_LEVELS, 0x20)) - collected_heart_tanks = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_HEART_TANKS, 0x01)) - collected_upgrades = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_UPGRADES, 0x01)) - collected_hadouken = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_HADOUKEN, 0x01)) - collected_pickups = list(await snes_read(ctx, MMX_COLLECTED_PICKUPS, 0x20)) - pickupsanity_enabled = int.from_bytes(await snes_read(ctx, MMX_PICKUPSANITY_ACTIVE, 0x1)) - completed_intro_level = int.from_bytes(await snes_read(ctx, WRAM_START + 0x01F9B, 0x1)) + defeated_bosses = list(game_data[0x80:0xA0]) + cleared_levels = list(game_data[0x60:0x80]) + collected_heart_tanks = game_data[0x05] + collected_upgrades = game_data[0x06] + collected_hadouken = game_data[0x07] + collected_pickups = list(game_data[0xC0:0xE0]) + pickupsanity_enabled = game_settings[0x07] + completed_intro_level = game_progress_data[0x8B] new_checks = [] for loc_name, data in location_id_to_level_id.items(): loc_id = AutoWorldRegister.world_types[ctx.game].location_name_to_id[loc_name] @@ -307,10 +347,7 @@ async def game_watcher(self, ctx): new_checks.append(loc_id) elif internal_id == 0x007: # Intro - if game_state[0] == 0x02 and \ - menu_state[0] == 0x00 and \ - gameplay_state[0] == 0x01 and \ - completed_intro_level == 0x04: + if game_state_data == b'\x02\x00\x01' and completed_intro_level == 0x04: new_checks.append(loc_id) elif internal_id == 0x020: # Pickups @@ -319,8 +356,13 @@ async def game_watcher(self, ctx): if collected_pickups[data_bit] != 0: new_checks.append(loc_id) - verify_game_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 1) - if verify_game_state is None: + # Verify if game still active + game_data = await snes_read(ctx, MMX_RAM, 0x0140) + game_state_data = await snes_read(ctx, MMX_GAME_STATE, 0x3) + game_progress_data = await snes_read(ctx, MMX_UPGRADE_DATA, 0xF0) + game_settings = await snes_read(ctx, MMX_SETTINGS, 0x20) + + if game_data is None or game_state_data is None or game_progress_data is None or game_settings is None: snes_logger.info(f'Exit Game.') return @@ -338,10 +380,9 @@ async def game_watcher(self, ctx): await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) # Send Current Room for Tracker - current_level = int.from_bytes(await snes_read(ctx, MMX_LEVEL_INDEX, 0x1), "little") + current_level = game_progress_data[0x6A] - if game_state[0] == 0x00 or \ - (game_state[0] == 0x02 and menu_state[0] != 0x04): + if game_state_data[0] == 0x00 or game_state_data[0:2] == b'\x02\x04': current_level = -1 if self.current_level_value != (current_level + 1): @@ -365,13 +406,8 @@ async def game_watcher(self, ctx): ] ) - recv_count = await snes_read(ctx, MMX_RECV_INDEX, 2) - if recv_count is None: - # Add a small failsafe in case we get a None. Other SNI games do this... - return - - recv_index = int.from_bytes(recv_count, "little") - sync_arsenal = int.from_bytes(await snes_read(ctx, MMX_ARSENAL_SYNC, 0x2), "little") + recv_index = int.from_bytes(game_data[0:2], "little") + sync_arsenal = int.from_bytes(game_data[0x112:0x114], "little") if recv_index < len(ctx.items_received) and sync_arsenal != 0x1337: item = ctx.items_received[recv_index] @@ -400,7 +436,7 @@ async def game_watcher(self, ctx): elif item.item in boss_access_rom_data: if item.item == STARTING_ID + 0x000A: snes_buffered_write(ctx, MMX_SIGMA_ACCESS, bytearray([0x00])) - boss_access = bytearray(await snes_read(ctx, MMX_UNLOCKED_LEVELS, 0x20)) + boss_access = bytearray(game_data[0x40:0x60]) level = boss_access_rom_data[item.item] boss_access[level[0]] = 0x01 snes_buffered_write(ctx, MMX_UNLOCKED_LEVELS, boss_access) @@ -420,22 +456,31 @@ async def game_watcher(self, ctx): return # Handle collected locations - game_state = await snes_read(ctx, MMX_GAME_STATE, 0x1) - if game_state[0] != 0x02: + game_data = await snes_read(ctx, MMX_RAM, 0x0140) + game_state_data = await snes_read(ctx, MMX_GAME_STATE, 0x3) + game_progress_data = await snes_read(ctx, MMX_UPGRADE_DATA, 0xF0) + game_settings = await snes_read(ctx, MMX_SETTINGS, 0x20) + + if game_data is None or game_state_data is None or game_progress_data is None or game_settings is None: + ctx.locations_checked = set() + return + + if game_state_data[0] != 0x02: ctx.locations_checked = set() return + new_boss_clears = False new_cleared_level = False new_heart_tank = False new_upgrade = False new_pickup = False new_hadouken = False - cleared_levels = list(await snes_read(ctx, MMX_COMPLETED_LEVELS, 0x20)) - collected_pickups = list(await snes_read(ctx, MMX_COLLECTED_PICKUPS, 0x40)) - defeated_bosses = list(await snes_read(ctx, MMX_DEFEATED_BOSSES, 0x20)) - collected_heart_tanks = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_HEART_TANKS, 0x01)) - collected_upgrades = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_UPGRADES, 0x01)) - collected_hadouken = int.from_bytes(await snes_read(ctx, MMX_COLLECTED_HADOUKEN, 0x01)) + defeated_bosses = list(game_data[0x80:0xA0]) + cleared_levels = list(game_data[0x60:0x80]) + collected_pickups = list(game_data[0xC0:0xE0]) + collected_heart_tanks = game_data[0x05] + collected_upgrades = game_data[0x06] + collected_hadouken = game_data[0x07] i = 0 for loc_id in ctx.checked_locations: if loc_id not in ctx.locations_checked: @@ -505,6 +550,7 @@ def on_package(self, ctx, cmd: str, args: dict): ctx.set_notify(f"mmx_damage_dealt_{ctx.team}_{ctx.slot}") ctx.set_notify(f"mmx_checkpoints_{ctx.team}_{ctx.slot}") ctx.set_notify(f"mmx_arsenal_{ctx.team}_{ctx.slot}") + self.data_storage_enabled = True slot_data = args.get("slot_data", None) self.using_newer_client = True if slot_data["energy_link"]: @@ -525,23 +571,18 @@ def on_package(self, ctx, cmd: str, args: dict): ctx.ui.energy_link_label.text = f"Energy: {pool:.2f}" - async def handle_hp_trade(self, ctx): - from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + def handle_hp_trade(self, ctx, game_ram): + from SNIClient import snes_buffered_write - validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) - if validation is None: - return - validation = validation[0] | (validation[1] << 8) - if validation != 0xDEAD: - return + game_data = game_ram[0] + game_state_data = game_ram[1] + current_hp = game_ram[4] # Can only process trades during the pause state - receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) - menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) - gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) - if menu_state[0] != 0x04 or \ - gameplay_state[0] != 0x04 or \ - receiving_item[0] != 0x00: + receiving_item = game_data[0x15] + menu_state = game_state_data[1] + gameplay_state = game_state_data[2] + if menu_state != 0x04 or gameplay_state != 0x04 or receiving_item != 0x00: return for item in self.item_queue: @@ -551,35 +592,27 @@ async def handle_hp_trade(self, ctx): return # Can trade HP -> WPN if HP is above 1 - current_hp = await snes_read(ctx, MMX_CURRENT_HP, 0x1) if current_hp[0] > 0x01: max_trade = current_hp[0] - 1 set_trade = self.trade_request if self.trade_request <= max_trade else max_trade self.add_item_to_queue("weapon refill", None, set_trade) new_hp = current_hp[0] - set_trade snes_buffered_write(ctx, MMX_CURRENT_HP, bytearray([new_hp])) - await snes_flush_writes(ctx) self.trade_request = None logger.info(f"Traded {set_trade} HP for {set_trade} Weapon Energy.") else: logger.info("Couldn't process trade. HP is too low.") - async def handle_energy_link(self, ctx): - from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + async def handle_energy_link(self, ctx, game_ram): + from SNIClient import snes_buffered_write + + game_data = game_ram[0] + game_state_data = game_ram[1] + game_progress_data = game_ram[2] - validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) - if validation is None: - return - validation = validation[0] | (validation[1] << 8) - if validation != 0xDEAD: - return - # Deposit heals into the pool regardless of energy_link setting - energy_packet = await snes_read(ctx, MMX_ENERGY_LINK_PACKET, 0x2) - if energy_packet is None: - return - energy_packet_raw = energy_packet[0] | (energy_packet[1] << 8) + energy_packet_raw = int.from_bytes(game_data[0x09:0x0B], "little") energy_packet = (energy_packet_raw * EXCHANGE_RATE) >> 4 if energy_packet != 0: await ctx.send_msgs([{ @@ -589,12 +622,10 @@ async def handle_energy_link(self, ctx): }]) pool = ((ctx.stored_data[f'EnergyLink{ctx.team}'] or 0) / EXCHANGE_RATE) + (energy_packet_raw / 16) snes_buffered_write(ctx, MMX_ENERGY_LINK_PACKET, bytearray([0x00, 0x00])) - await snes_flush_writes(ctx) # Expose EnergyLink to the ROM - pause_state = await snes_read(ctx, MMX_PAUSE_STATE, 0x1) - screen_brightness = await snes_read(ctx, MMX_SCREEN_BRIGHTNESS, 0x1) - if pause_state[0] != 0x00 or screen_brightness[0] == 0x0F: + pause_state = game_progress_data[0x14] + if pause_state != 0x00: pool = ctx.stored_data[f'EnergyLink{ctx.team}'] or 0 total_energy = int(pool / EXCHANGE_RATE) if total_energy < 9999: @@ -602,20 +633,20 @@ async def handle_energy_link(self, ctx): else: snes_buffered_write(ctx, MMX_ENERGY_LINK_COUNT, bytearray([0x0F, 0x27])) - receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) - menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) - gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) - can_move = await snes_read(ctx, MMX_CAN_MOVE, 0x7) - heart_tank = await snes_read(ctx, MMX_ENABLE_HEART_TANK, 0x1) - hp_refill = await snes_read(ctx, MMX_ENABLE_HP_REFILL, 0x1) - weapon_refill = await snes_read(ctx, MMX_ENABLE_WEAPON_REFILL, 0x1) - if menu_state[0] != 0x04 or \ - gameplay_state[0] != 0x04 or \ - can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ - receiving_item[0] != 0x00 or \ - heart_tank[0] != 0x00 or \ - hp_refill[0] != 0x00 or \ - weapon_refill[0] != 0x00: + receiving_item = game_data[0x15] + menu_state = game_state_data[1] + gameplay_state = game_state_data[2] + can_move = game_progress_data[3:10] + heart_tank = game_data[0x0B] + hp_refill = game_data[0x0F] + weapon_refill = game_data[0x1A] + if menu_state != 0x04 or \ + gameplay_state != 0x04 or \ + can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ + receiving_item != 0x00 or \ + heart_tank != 0x00 or \ + hp_refill != 0x00 or \ + weapon_refill != 0x00: return skip_hp = False @@ -631,8 +662,8 @@ async def handle_energy_link(self, ctx): pool = ctx.stored_data[f'EnergyLink{ctx.team}'] or 0 if not skip_hp or not skip_weapon: # Handle in-game requests - request = int.from_bytes(await snes_read(ctx, MMX_REFILL_REQUEST, 0x1), "little") - target = int.from_bytes(await snes_read(ctx, MMX_REFILL_TARGET, 0x1), "little") + request = game_data[0x110] + target = game_data[0x111] if request != 0: if target == 0: if self.heal_request_command is None: @@ -693,37 +724,38 @@ def add_item_to_queue(self, item_type, item_id, item_additional = None): self.item_queue.append([item_type, item_id, item_additional]) - async def handle_item_queue(self, ctx): - from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + def handle_item_queue(self, ctx, game_ram): + from SNIClient import snes_buffered_write from .Rom import weapon_rom_data, upgrades_rom_data if not hasattr(self, "item_queue") or len(self.item_queue) == 0: return - - validation = await snes_read(ctx, MMX_VALIDATION_CHECK, 0x2) - if validation is None: - return - validation = validation[0] | (validation[1] << 8) - if validation != 0xDEAD: - return + + game_data = game_ram[0] + game_state_data = game_ram[1] + game_progress_data = game_ram[2] + game_settings = game_ram[3] + current_hp = game_ram[4] # Do not give items if you can't move, are in pause state, not in the correct mode or not in gameplay state - receiving_item = await snes_read(ctx, MMX_RECEIVING_ITEM, 0x1) - menu_state = await snes_read(ctx, MMX_MENU_STATE, 0x1) - gameplay_state = await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1) - progress = await snes_read(ctx, MMX_FORTRESS_PROGRESS, 0x1) - can_move = await snes_read(ctx, MMX_CAN_MOVE, 0x7) - hp_refill = await snes_read(ctx, MMX_ENABLE_HP_REFILL, 0x1) - weapon_refill = await snes_read(ctx, MMX_ENABLE_WEAPON_REFILL, 0x1) - if menu_state[0] != 0x04 or \ - gameplay_state[0] != 0x04 or \ - progress[0] >= 0x04 or \ - hp_refill[0] != 0x00 or \ - weapon_refill[0] != 0x00 or \ + receiving_item = game_data[0x15] + menu_state = game_state_data[1] + gameplay_state = game_state_data[2] + progress = game_progress_data[0x6B] + can_move = game_progress_data[3:10] + hp_refill = game_data[0x0F] + weapon_refill = game_data[0x1A] + + if menu_state != 0x04 or \ + gameplay_state != 0x04 or \ + progress >= 0x04 or \ + hp_refill != 0x00 or \ + weapon_refill != 0x00 or \ can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ - receiving_item[0] != 0x00: + receiving_item != 0x00: return + next_item = self.item_queue[0] item_id = next_item[1] @@ -731,10 +763,9 @@ async def handle_item_queue(self, ctx): backup_item = self.item_queue.pop(0) if "hp refill" in next_item[0]: - current_hp = await snes_read(ctx, MMX_CURRENT_HP, 0x1) - max_hp = await snes_read(ctx, MMX_MAX_HP, 0x1) + max_hp = game_progress_data[0x8A] - if current_hp[0] < max_hp[0]: + if current_hp[0] < max_hp: snes_buffered_write(ctx, MMX_ENABLE_HP_REFILL, bytearray([0x02])) snes_buffered_write(ctx, MMX_HP_REFILL_AMOUNT, bytearray([next_item[2]])) snes_buffered_write(ctx, MMX_RECEIVING_ITEM, bytearray([0x01])) @@ -748,18 +779,16 @@ async def handle_item_queue(self, ctx): snes_buffered_write(ctx, MMX_RECEIVING_ITEM, bytearray([0x01])) elif next_item[0] == "1up": - life_count = await snes_read(ctx, MMX_LIFE_COUNT, 0x1) - if life_count[0] < 99: + life_count = game_progress_data[0x70] + if life_count < 99: snes_buffered_write(ctx, MMX_ENABLE_GIVE_1UP, bytearray([0x01])) snes_buffered_write(ctx, MMX_RECEIVING_ITEM, bytearray([0x01])) self.save_arsenal = True else: self.item_queue.append(backup_item) - pause_state = await snes_read(ctx, MMX_PAUSE_STATE, 0x1) - screen_brightness = await snes_read(ctx, MMX_SCREEN_BRIGHTNESS, 0x1) - if pause_state[0] != 0x00 or screen_brightness[0] != 0x0F: - await snes_flush_writes(ctx) + pause_state = game_progress_data[0x14] + if pause_state != 0x00: if len(self.item_queue) != 0: backup_item = self.item_queue.pop(0) self.item_queue.append(backup_item) @@ -774,8 +803,7 @@ async def handle_item_queue(self, ctx): self.save_arsenal = True elif next_item[0] == "heart tank": - heart_tanks = await snes_read(ctx, MMX_HEART_TANKS, 0x1) - heart_tanks = heart_tanks[0] + heart_tanks = game_progress_data[0x8C] heart_tank_count = heart_tanks.bit_count() if heart_tank_count < 8: heart_tanks |= 1 << heart_tank_count @@ -786,17 +814,13 @@ async def handle_item_queue(self, ctx): self.save_arsenal = True elif next_item[0] == "sub tank": - upgrades = await snes_read(ctx, MMX_UPGRADES, 0x1) - sub_tanks = await snes_read(ctx, MMX_SUB_TANK_ARRAY, 0x4) - sub_tanks = list(sub_tanks) - upgrade = upgrades[0] - upgrade = upgrade & 0xF0 - sub_tank_count = upgrade.bit_count() + sub_tanks = list(game_progress_data[0x73:0x77]) + upgrades = game_progress_data[0x89] + sub_tank_count = (upgrades & 0xF0).bit_count() if sub_tank_count < 4: - upgrade = upgrades[0] - upgrade |= 0x10 << sub_tank_count + upgrades |= 0x10 << sub_tank_count sub_tanks[sub_tank_count] = 0x8E - snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrade])) + snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) snes_buffered_write(ctx, MMX_SUB_TANK_ARRAY, bytearray(sub_tanks)) snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x17])) @@ -804,21 +828,20 @@ async def handle_item_queue(self, ctx): self.save_arsenal = True elif next_item[0] == "upgrade": - upgrades = await snes_read(ctx, MMX_UPGRADES, 0x1) + upgrades = game_progress_data[0x89] upgrade = upgrades_rom_data[item_id] bit = 1 << upgrade[0] - check = upgrades[0] & bit + check = upgrades & bit if bit == 0x08: - air_dash_check = int.from_bytes(await snes_read(ctx, MMX_ABILITIES_FLAGS, 0x1), "little") & 0x02 + air_dash_check = game_settings[0x11] & 0x02 if air_dash_check != 0: # check now becomes the air dash flag - check = int.from_bytes(await snes_read(ctx, MMX_UNLOCKED_AIR_DASH, 0x1), "little") + check = game_data[0x22] if check == 0: # Armor - upgrades = upgrades[0] original_value = upgrades upgrades |= bit if bit == 0x01: @@ -826,21 +849,19 @@ async def handle_item_queue(self, ctx): snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) snes_buffered_write(ctx, WRAM_START + 0x1EE19, bytearray([0x80])) elif bit == 0x02: - jam_check = await snes_read(ctx, MMX_JAMMED_BUSTER_ACTIVE, 0x1) - charge_shot_unlocked = await snes_read(ctx, MMX_UNLOCKED_CHARGED_SHOT, 0x1) - if jam_check[0] == 1 and charge_shot_unlocked[0] == 0: + jam_check = game_settings[0x0A] + charge_shot_unlocked = game_data[0x16] + if jam_check == 1 and charge_shot_unlocked == 0: snes_buffered_write(ctx, MMX_UNLOCKED_CHARGED_SHOT, bytearray([0x01])) else: - value = await snes_read(ctx, WRAM_START + 0x0C38, 0x1) - snes_buffered_write(ctx, WRAM_START + 0x0C38, bytearray([value[0] + 1])) + snes_buffered_write(ctx, WRAM_START + 0x0C38, bytearray([0x01])) snes_buffered_write(ctx, WRAM_START + 0x0C42, bytearray([0x00])) snes_buffered_write(ctx, WRAM_START + 0x0C43, bytearray([0x00])) snes_buffered_write(ctx, WRAM_START + 0x0C39, bytearray([0x00])) snes_buffered_write(ctx, WRAM_START + 0x0C48, bytearray([0x5D])) snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) elif bit == 0x04: - value = await snes_read(ctx, WRAM_START + 0x0C58, 0x1) - snes_buffered_write(ctx, WRAM_START + 0x0C58, bytearray([value[0] + 1])) + snes_buffered_write(ctx, WRAM_START + 0x0C58, bytearray([0x01])) snes_buffered_write(ctx, WRAM_START + 0x0C62, bytearray([0x00])) snes_buffered_write(ctx, WRAM_START + 0x0C63, bytearray([0x01])) snes_buffered_write(ctx, WRAM_START + 0x0C59, bytearray([0x00])) @@ -850,8 +871,7 @@ async def handle_item_queue(self, ctx): if air_dash_check != 0 and original_value & bit == 0x08: snes_buffered_write(ctx, MMX_UNLOCKED_AIR_DASH, bytearray([0x01])) else: - value = await snes_read(ctx, WRAM_START + 0x0C78, 0x1) - snes_buffered_write(ctx, WRAM_START + 0x0C78, bytearray([value[0] + 1])) + snes_buffered_write(ctx, WRAM_START + 0x0C78, bytearray([0x01])) snes_buffered_write(ctx, WRAM_START + 0x0C82, bytearray([0x00])) snes_buffered_write(ctx, WRAM_START + 0x0C83, bytearray([0x02])) snes_buffered_write(ctx, WRAM_START + 0x0C79, bytearray([0x00])) @@ -862,16 +882,20 @@ async def handle_item_queue(self, ctx): self.item_queue.pop(0) self.save_arsenal = True - await snes_flush_writes(ctx) - - async def handle_data_storage(self, ctx): + async def handle_data_storage(self, ctx, game_ram): from SNIClient import snes_read, snes_buffered_write, snes_flush_writes + + game_data = game_ram[0] + game_state_data = game_ram[1] + game_progress_data = game_ram[2] + # Only do arsenal after the map's initial load or the intro stage is selected - menu_state = int.from_bytes(await snes_read(ctx, MMX_MENU_STATE, 0x1)) - gameplay_state = int.from_bytes(await snes_read(ctx, MMX_GAMEPLAY_STATE, 0x1)) - map_state = int.from_bytes(await snes_read(ctx, WRAM_START + 0x1E49, 0x1)) - sync_arsenal = int.from_bytes(await snes_read(ctx, MMX_ARSENAL_SYNC, 0x2), "little") + menu_state = game_state_data[1] + gameplay_state = game_state_data[2] + map_state = await snes_read(ctx, WRAM_START + 0x1E49, 0x1) + map_state = 0 if map_state is None else map_state[0] + sync_arsenal = int.from_bytes(game_data[0x112:0x114], "little") if (menu_state == 0x00 and map_state == 0x04) or (menu_state == 0x04 and gameplay_state == 0x04): # Load Arsenal if sync_arsenal == 0x1337: @@ -897,18 +921,18 @@ async def handle_data_storage(self, ctx): # Save Arsenal if self.save_arsenal and sync_arsenal != 0x1337: arsenal = dict() - arsenal["recv_index"] = int.from_bytes(await snes_read(ctx, MMX_RECV_INDEX, 0x2), "little") - arsenal["life_count"] = int.from_bytes(await snes_read(ctx, MMX_LIFE_COUNT, 0x1), "little") - arsenal["upgrades"] = int.from_bytes(await snes_read(ctx, MMX_UPGRADES, 0x1), "little") - arsenal["max_hp"] = int.from_bytes(await snes_read(ctx, MMX_MAX_HP, 0x1), "little") - arsenal["heart_tanks"] = int.from_bytes(await snes_read(ctx, MMX_HEART_TANKS, 0x1), "little") - arsenal["sub_tanks"] = list(await snes_read(ctx, MMX_SUB_TANK_ARRAY, 0x4)) - arsenal["unlocked_buster"] = int.from_bytes(await snes_read(ctx, MMX_UNLOCKED_CHARGED_SHOT, 0x1), "little") - arsenal["unlocked_air_dash"] = int.from_bytes(await snes_read(ctx, MMX_UNLOCKED_AIR_DASH, 0x1), "little") - arsenal["weapons"] = list(await snes_read(ctx, MMX_WEAPON_ARRAY, 0x10)) - arsenal["hadouken"] = int.from_bytes(await snes_read(ctx, MMX_HADOUKEN, 0x1), "little") - arsenal["levels"] = list(await snes_read(ctx, MMX_UNLOCKED_LEVELS, 0x20)) - arsenal["sigma_access"] = int.from_bytes(await snes_read(ctx, MMX_SIGMA_ACCESS, 0x1), "little") + arsenal["recv_index"] = int.from_bytes(game_data[0x00:0x02], "little") + arsenal["life_count"] = game_progress_data[0x70] + arsenal["upgrades"] = game_progress_data[0x89] + arsenal["max_hp"] = game_progress_data[0x8A] + arsenal["heart_tanks"] = game_progress_data[0x8C] + arsenal["sub_tanks"] = list(game_progress_data[0x73:0x77]) + arsenal["unlocked_buster"] = game_data[0x16] + arsenal["unlocked_air_dash"] = game_data[0x22] + arsenal["weapons"] = list(game_progress_data[0x78:0x88]) + arsenal["hadouken"] = game_progress_data[0x6E] + arsenal["levels"] = list(game_data[0x40:0x60]) + arsenal["sigma_access"] = game_data[0x02] # Attempt to not lose any previously saved data in case of RAM corruption saved_arsenal = ctx.stored_data[f"mmx_arsenal_{ctx.team}_{ctx.slot}"] or dict() @@ -939,18 +963,8 @@ async def handle_data_storage(self, ctx): }]) self.save_arsenal = False - keys = { - f"mmx_checkpoints_{ctx.team}_{ctx.slot}", - f"mmx_global_timer_{ctx.team}_{ctx.slot}", - f"mmx_deaths_{ctx.team}_{ctx.slot}", - f"mmx_damage_dealt_{ctx.team}_{ctx.slot}", - f"mmx_damage_taken_{ctx.team}_{ctx.slot}", - } - if not all(key in ctx.stored_data.keys() for key in keys): - return - # Checkpoints reached - checkpoints = list(await snes_read(ctx, MMX_CHECKPOINTS_REACHED, 0xF)) + checkpoints = list(game_data[0x120:0x130]) data_storage_checkpoints = ctx.stored_data[f"mmx_checkpoints_{ctx.team}_{ctx.slot}"] or [0 for _ in range(0xF)] computed_checkpoints = list() for i in range(0xF): @@ -965,7 +979,7 @@ async def handle_data_storage(self, ctx): snes_buffered_write(ctx, MMX_CHECKPOINTS_REACHED, bytes(computed_checkpoints)) # Global timer - timer = int.from_bytes(await snes_read(ctx, MMX_GLOBAL_TIMER, 0x4), "little") + timer = int.from_bytes(game_data[0x106:0x10A], "little") data_storage_timer = ctx.stored_data[f"mmx_global_timer_{ctx.team}_{ctx.slot}"] or 0 if timer >= data_storage_timer: await ctx.send_msgs([{ @@ -977,7 +991,7 @@ async def handle_data_storage(self, ctx): snes_buffered_write(ctx, MMX_GLOBAL_TIMER, data_storage_timer.to_bytes(4, "little")) # Death count - deaths = int.from_bytes(await snes_read(ctx, MMX_GLOBAL_DEATHS, 0x2), "little") + deaths = int.from_bytes(game_data[0x10A:0x10C], "little") data_storage_deaths = ctx.stored_data[f"mmx_deaths_{ctx.team}_{ctx.slot}"] or 0 if deaths >= data_storage_deaths: await ctx.send_msgs([{ @@ -989,7 +1003,7 @@ async def handle_data_storage(self, ctx): snes_buffered_write(ctx, MMX_GLOBAL_DEATHS, data_storage_deaths.to_bytes(2, "little")) # Damage dealt - dmg_dealt = int.from_bytes(await snes_read(ctx, MMX_GLOBAL_DMG_DEALT, 0x2), "little") + dmg_dealt = int.from_bytes(game_data[0x10C:0x10E], "little") data_storage_dmg_dealt = ctx.stored_data[f"mmx_damage_dealt_{ctx.team}_{ctx.slot}"] or 0 if dmg_dealt >= data_storage_dmg_dealt: await ctx.send_msgs([{ @@ -1001,7 +1015,7 @@ async def handle_data_storage(self, ctx): snes_buffered_write(ctx, MMX_GLOBAL_DMG_DEALT, data_storage_dmg_dealt.to_bytes(2, "little")) # Damage taken - dmg_taken = int.from_bytes(await snes_read(ctx, MMX_GLOBAL_DMG_TAKEN, 0x2), "little") + dmg_taken = int.from_bytes(game_data[0x10E:0x110], "little") data_storage_dmg_taken = ctx.stored_data[f"mmx_damage_taken_{ctx.team}_{ctx.slot}"] or 0 if dmg_taken >= data_storage_dmg_taken: await ctx.send_msgs([{ @@ -1011,8 +1025,6 @@ async def handle_data_storage(self, ctx): }]) else: snes_buffered_write(ctx, MMX_GLOBAL_DMG_TAKEN, data_storage_dmg_taken.to_bytes(2, "little")) - - await snes_flush_writes(ctx) def cmd_heal(self, amount: str = ""):