diff --git a/worlds/mmx3/Client.py b/worlds/mmx3/Client.py index 72153e123b2..7ea9a78dc85 100644 --- a/worlds/mmx3/Client.py +++ b/worlds/mmx3/Client.py @@ -14,10 +14,13 @@ WRAM_SIZE = 0x20000 SRAM_START = 0xE00000 +MMX3_RAM = WRAM_START + 0x0F400 + MMX3_GAME_STATE = WRAM_START + 0x000D0 MMX3_MENU_STATE = WRAM_START + 0x000D1 MMX3_GAMEPLAY_STATE = WRAM_START + 0x000D2 MMX3_PAUSE_STATE = WRAM_START + 0x01F37 +MMX3_SCREEN_BRIGHTNESS = WRAM_START + 0x000B4 MMX3_LEVEL_INDEX = WRAM_START + 0x01FAE MMX3_WEAPON_ARRAY = WRAM_START + 0x01FBC MMX3_HEART_TANKS = WRAM_START + 0x01FD4 @@ -32,18 +35,28 @@ MMX3_ZSABER = WRAM_START + 0x01FB2 MMX3_CAN_MOVE = WRAM_START + 0x01F45 MMX3_ON_RIDE_ARMOR = WRAM_START + 0x01F22 -MMX3_GOING_THROUGH_GATE = WRAM_START + 0x01F25 +MMX3_FROZEN_SYSTEMS = WRAM_START + 0x01F25 MMX3_HYPER_CANNON = WRAM_START + 0x01FCC MMX3_VICTORY = WRAM_START + 0x0F46B -MMX3_ENABLE_HEART_TANK = WRAM_START + 0x0F4E0 -MMX3_ENABLE_HP_REFILL = WRAM_START + 0x0F4E4 -MMX3_HP_REFILL_AMOUNT = WRAM_START + 0x0F4E5 -MMX3_ENABLE_GIVE_1UP = WRAM_START + 0x0F4E7 -MMX3_ENABLE_WEAPON_REFILL = WRAM_START + 0x0F4E8 -MMX3_WEAPON_REFILL_AMOUNT = WRAM_START + 0x0F4E9 -MMX3_RECEIVING_ITEM = WRAM_START + 0x0F4FF -MMX3_UNLOCKED_CHARGED_SHOT = WRAM_START + 0x0F46C +MMX3_ENABLE_HEART_TANK = MMX3_RAM + 0x000E0 +MMX3_ENABLE_HP_REFILL = MMX3_RAM + 0x000E4 +MMX3_HP_REFILL_AMOUNT = MMX3_RAM + 0x000E5 +MMX3_ENABLE_GIVE_1UP = MMX3_RAM + 0x000E7 +MMX3_ENABLE_WEAPON_REFILL = MMX3_RAM + 0x000E8 +MMX3_WEAPON_REFILL_AMOUNT = MMX3_RAM + 0x000E9 +MMX3_RECEIVING_ITEM = MMX3_RAM + 0x000FF +MMX3_UNLOCKED_CHARGED_SHOT = MMX3_RAM + 0x0006C + +MMX3_ENERGY_LINK_COUNT = MMX3_RAM + 0x00128 +MMX3_GLOBAL_TIMER = MMX3_RAM + 0x0012E +MMX3_GLOBAL_DEATHS = MMX3_RAM + 0x00132 +MMX3_GLOBAL_DMG_DEALT = MMX3_RAM + 0x00134 +MMX3_GLOBAL_DMG_TAKEN = MMX3_RAM + 0x00136 +MMX3_CHECKPOINTS_REACHED = MMX3_RAM + 0x00100 +MMX3_REFILL_REQUEST = MMX3_RAM + 0x00138 +MMX3_REFILL_TARGET = MMX3_RAM + 0x00139 +MMX3_ARSENAL_SYNC = MMX3_RAM + 0x0013A MMX3_SFX_FLAG = WRAM_START + 0x0F469 MMX3_SFX_NUMBER = WRAM_START + 0x0F46A @@ -72,17 +85,12 @@ EXCHANGE_RATE = 500000000 -MMX3_RECV_INDEX = WRAM_START + 0x0F460 +MMX3_RECV_INDEX = MMX3_RAM + 0x0006E MMX3_ROMHASH_START = 0x7FC0 ROMHASH_SIZE = 0x15 X_Z_ITEMS = ["1up", "hp refill", "weapon 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 MMX3SNIClient(SNIClient): game = "Mega Man X3" @@ -92,13 +100,14 @@ 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 = [] @@ -115,14 +124,14 @@ async def deathlink_kill_player(self, ctx): menu_state = await snes_read(ctx, MMX3_MENU_STATE, 0x1) gameplay_state = await snes_read(ctx, MMX3_GAMEPLAY_STATE, 0x1) can_move = await snes_read(ctx, MMX3_CAN_MOVE, 0x1) - going_through_gate = await snes_read(ctx, MMX3_GOING_THROUGH_GATE, 0x4) + frozen_systems = await snes_read(ctx, MMX3_FROZEN_SYSTEMS, 0x7) pause_state = await snes_read(ctx, MMX3_PAUSE_STATE, 0x1) if menu_state[0] != 0x04 or \ gameplay_state[0] != 0x04 or \ can_move[0] != 0x00 or \ pause_state[0] != 0x00 or \ receiving_item[0] != 0x00 or \ - going_through_gate != b'\x00\x00\x00\x00': + frozen_systems != b'\x00\x00\x00\x00\x00\x00\x00': return snes_buffered_write(ctx, MMX3_CURRENT_HP, bytes([0x80])) @@ -143,16 +152,14 @@ async def validate_rom(self, ctx): energy_link = await snes_read(ctx, MMX3_ENERGY_LINK_ENABLED, 0x1) rom_name = await snes_read(ctx, MMX3_ROMHASH_START, ROMHASH_SIZE) if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:4] != b"MMX3": - 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 "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") + if "trade" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("trade") + if "resync" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("resync") return False ctx.game = self.game @@ -161,18 +168,14 @@ 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: + if "heal" 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 "trade" not in ctx.command_processor.commands: ctx.command_processor.commands["trade"] = cmd_trade + if "resync" not in ctx.command_processor.commands: + ctx.command_processor.commands["resync"] = cmd_resync death_link = await snes_read(ctx, MMX3_DEATH_LINK_ACTIVE, 1) if death_link[0]: @@ -182,206 +185,620 @@ async def validate_rom(self, ctx): return True - - async def handle_hp_trade(self, 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 + ctx.set_notify(f"mmx3_global_timer_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx3_deaths_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx3_damage_taken_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx3_damage_dealt_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx3_checkpoints_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx3_arsenal_{ctx.team}_{ctx.slot}") + 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 game_watcher(self, ctx): from SNIClient import snes_buffered_write, snes_flush_writes, snes_read - validation = await snes_read(ctx, MMX3_VALIDATION_CHECK, 0x2) - if validation is None: + game_state = await snes_read(ctx, MMX3_GAME_STATE, 0x1) + menu_state = await snes_read(ctx, MMX3_MENU_STATE, 0x1) + gameplay_state = await snes_read(ctx, MMX3_GAMEPLAY_STATE, 0x1) + + # Discard uninitialized ROMs + if menu_state is None: + self.game_state = False + self.energy_link_enabled = False + self.item_queue = [] + self.current_level_value = 42 return + + validation = await snes_read(ctx, MMX3_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 - menu_state = await snes_read(ctx, MMX3_MENU_STATE, 0x1) - gameplay_state = await snes_read(ctx, MMX3_GAMEPLAY_STATE, 0x1) - can_move = await snes_read(ctx, MMX3_CAN_MOVE, 0x1) - if menu_state[0] != 0x04 or \ - gameplay_state[0] != 0x04 or \ - can_move[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"mmx3_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, MMX3_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) - 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 game_state[0] != 0x00 and self.data_storage_enabled is True: + await self.handle_data_storage(ctx) - # Can trade HP -> WPN if HP is above 1 - current_hp = await snes_read(ctx, MMX3_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, MMX3_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.") - + # 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"mmx3_global_timer_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx3_deaths_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx3_damage_taken_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx3_damage_dealt_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx3_checkpoints_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx3_arsenal_{ctx.team}_{ctx.slot}") - async def handle_energy_link(self, ctx): - from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + if self.trade_request is not None: + await self.handle_hp_trade(ctx) - # Deposit heals into the pool regardless of energy_link setting - energy_packet = await snes_read(ctx, MMX3_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) - 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, MMX3_ENERGY_LINK_PACKET, bytearray([0x00, 0x00])) - await snes_flush_writes(ctx) + await self.handle_item_queue(ctx) + # This is going to be rewritten whenever SNIClient supports on_package energy_link = await snes_read(ctx, MMX3_ENERGY_LINK_ENABLED, 0x1) - if energy_link is None: - return + 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 energy_link[0]: - validation = await snes_read(ctx, MMX3_VALIDATION_CHECK, 0x2) - if validation is None: - return - validation = validation[0] | (validation[1] << 8) - if validation != 0xDEAD: - return + 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.") - receiving_item = await snes_read(ctx, MMX3_RECEIVING_ITEM, 0x1) - menu_state = await snes_read(ctx, MMX3_MENU_STATE, 0x1) - gameplay_state = await snes_read(ctx, MMX3_GAMEPLAY_STATE, 0x1) - can_move = await snes_read(ctx, MMX3_CAN_MOVE, 0x1) - going_through_gate = await snes_read(ctx, MMX3_GOING_THROUGH_GATE, 0x4) - pause_state = await snes_read(ctx, MMX3_PAUSE_STATE, 0x1) - if menu_state[0] != 0x04 or \ - gameplay_state[0] != 0x04 or \ - can_move[0] != 0x00 or \ - pause_state[0] != 0x00 or \ - receiving_item[0] != 0x00 or \ - going_through_gate != b'\x00\x00\x00\x00': - 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: - # 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, MMX3_CURRENT_HP, 0x1) - max_hp = await snes_read(ctx, MMX3_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 + from .Rom import weapon_rom_data, ride_armor_rom_data, upgrades_rom_data, boss_access_rom_data, refill_rom_data + from .Levels import location_id_to_level_id + from worlds import AutoWorldRegister - 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 + bit_byte_vile = await snes_read(ctx, MMX3_BIT_BYTE_VILE, 0x01) + defeated_bosses = list(await snes_read(ctx, MMX3_DEFEATED_BOSSES, 0x20)) + cleared_levels = list(await snes_read(ctx, MMX3_LEVEL_CLEARED, 0x20)) + victory_ram = await snes_read(ctx, MMX3_VICTORY, 0x1) + collected_heart_tanks = await snes_read(ctx, MMX3_COLLECTED_HEART_TANKS, 0x01) + collected_ride_chips = await snes_read(ctx, MMX3_COLLECTED_RIDE_CHIPS, 0x01) + collected_upgrades = await snes_read(ctx, MMX3_COLLECTED_UPGRADES, 0x01) + collected_pickups = list(await snes_read(ctx, MMX3_COLLECTED_PICKUPS, 0x40)) + pickupsanity_enabled = await snes_read(ctx, MMX3_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: + level_id = data[0] + internal_id = data[1] + data_bit = data[2] + if internal_id == 0x02: + # Heart Tank + bit = 0x01 << (level_id - 1) + masked_data = collected_heart_tanks[0] & bit + if masked_data != 0: + new_checks.append(loc_id) + elif internal_id == 0x03: + # Sub Tank + masked_data = collected_upgrades[0] & data_bit + if masked_data != 0: + new_checks.append(loc_id) + elif internal_id == 0x04: + # Mega Man upgrades + masked_data = collected_upgrades[0] & data_bit + if masked_data != 0: + new_checks.append(loc_id) + elif internal_id == 0x05: + # Ride Armor + masked_data = collected_ride_chips[0] & data_bit + if masked_data != 0: + new_checks.append(loc_id) + elif internal_id == 0x06: + # Mega Man chips + masked_data = collected_ride_chips[0] & data_bit + if masked_data != 0: + new_checks.append(loc_id) + elif internal_id == 0x09: + # Vile Defeated + vile_defeated = bit_byte_vile[0] & 0x30 + if vile_defeated != 0: + new_checks.append(loc_id) + elif internal_id == 0x0A: + # Byte Defeated + byte_defeated = bit_byte_vile[0] & 0x0C + if byte_defeated != 0: + new_checks.append(loc_id) + elif internal_id == 0x0B: + # Bit Defeated + bit_defeated = bit_byte_vile[0] & 0x03 + if bit_defeated != 0: + new_checks.append(loc_id) + elif internal_id == 0x0E: + # Victory + if victory_ram[0]: + new_checks.append(loc_id) + elif internal_id >= 0x300: + # Maverick Medal + if cleared_levels[data_bit] != 0: + new_checks.append(loc_id) + elif internal_id >= 0x200: + # Boss clear + boss_id = internal_id & 0x1F + if defeated_bosses[boss_id] != 0: + new_checks.append(loc_id) + elif internal_id >= 0x100: + # Pickups + if not pickupsanity_enabled or pickupsanity_enabled[0] == 0: + continue + pickup_id = internal_id & 0x3F + if collected_pickups[pickup_id] != 0: + new_checks.append(loc_id) + + + verify_game_state = await snes_read(ctx, MMX3_GAMEPLAY_STATE, 1) + if verify_game_state is None: + snes_logger.info(f'Exit Game.') + return + + rom = await snes_read(ctx, MMX3_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]}]) - 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]) + # Send Current Room for Tracker + current_level = int.from_bytes(await snes_read(ctx, MMX3_LEVEL_INDEX, 0x1), "little") + if game_state[0] == 0x00 or \ + (game_state[0] == 0x02 and menu_state[0] != 0x04): + current_level = -1 - async def handle_item_queue(self, ctx): - from SNIClient import snes_buffered_write, snes_flush_writes, snes_read - from worlds.mmx3.Rom import weapon_rom_data, ride_armor_rom_data, upgrades_rom_data + if self.current_level_value != (current_level + 1): + self.current_level_value = current_level + 1 - if not hasattr(self, "item_queue") or len(self.item_queue) == 0: - return + # Send level id data to tracker + await ctx.send_msgs( + [ + { + "cmd": "Set", + "key": f"mmx3_level_id_{ctx.team}_{ctx.slot}", + "default": 0, + "want_reply": False, + "operations": [ + { + "operation": "replace", + "value": self.current_level_value, + } + ], + } + ] + ) - validation = await snes_read(ctx, MMX3_VALIDATION_CHECK, 0x2) - if validation is None: - return - validation = validation[0] | (validation[1] << 8) - if validation != 0xDEAD: + recv_count = await snes_read(ctx, MMX3_RECV_INDEX, 2) + if recv_count is None: + # Add a small failsafe in case we get a None. Other SNI games do this... return - 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, MMX3_RECEIVING_ITEM, 0x1) + recv_index = int.from_bytes(recv_count, "little") + sync_arsenal = int.from_bytes(await snes_read(ctx, MMX3_ARSENAL_SYNC, 0x2), "little") + + 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))) + + snes_buffered_write(ctx, MMX3_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 == 0xBD0013: + self.add_item_to_queue("heart tank", item.item) + + elif item.item == 0xBD0014: + 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 ride_armor_rom_data: + self.add_item_to_queue("ride", item.item) + + elif item.item in boss_access_rom_data: + boss_access = bytearray(await snes_read(ctx, MMX3_UNLOCKED_LEVELS, 0x20)) + level = boss_access_rom_data[item.item] + boss_access[level[0] * 2] = 0x01 + snes_buffered_write(ctx, MMX3_UNLOCKED_LEVELS, boss_access) + if item.item == 0xBD000A: + snes_buffered_write(ctx, MMX3_DOPPLER_ACCESS, bytearray([0x00])) + snes_buffered_write(ctx, MMX3_SFX_FLAG, bytearray([0x01])) + snes_buffered_write(ctx, MMX3_SFX_NUMBER, bytearray([0x1D])) + self.save_arsenal = True + + elif item.item == 0xBD0019: + # Unlock vile stage + snes_buffered_write(ctx, MMX3_VILE_ACCESS, bytearray([0x00])) + snes_buffered_write(ctx, MMX3_SFX_FLAG, bytearray([0x01])) + snes_buffered_write(ctx, MMX3_SFX_NUMBER, bytearray([0x1D])) + self.save_arsenal = True + + 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 + + elif item.item == 0xBD0000: + # Handle goal + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + self.save_arsenal = True + ctx.finished_game = True + return + + # Handle collected locations + game_state = await snes_read(ctx, MMX3_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_ride_chip = False + new_bit_byte_vile = False + new_pickup = False + cleared_levels = list(await snes_read(ctx, MMX3_LEVEL_CLEARED, 0x20)) + collected_pickups = list(await snes_read(ctx, MMX3_COLLECTED_PICKUPS, 0x40)) + collected_heart_tanks = int.from_bytes(await snes_read(ctx, MMX3_COLLECTED_HEART_TANKS, 0x01)) + collected_upgrades = int.from_bytes(await snes_read(ctx, MMX3_COLLECTED_UPGRADES, 0x01)) + collected_ride_chips = int.from_bytes(await snes_read(ctx, MMX3_COLLECTED_RIDE_CHIPS, 0x01)) + defeated_bosses = list(await snes_read(ctx, MMX3_DEFEATED_BOSSES, 0x20)) + bit_byte_vile = int.from_bytes(await snes_read(ctx, MMX3_BIT_BYTE_VILE, 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) + + 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 == 0x02: + # Heart Tank + bit = 0x01 << (level_id - 1) + collected_heart_tanks |= bit + new_heart_tank = True + elif internal_id == 0x03: + # Sub Tank + collected_upgrades |= data_bit + new_upgrade = True + elif internal_id == 0x04: + # Mega Man upgrades + collected_upgrades |= data_bit + new_upgrade = True + elif internal_id == 0x05: + # Ride Armor + collected_ride_chips |= data_bit + new_ride_chip = True + elif internal_id == 0x06: + # Mega Man chips + collected_ride_chips |= data_bit + new_ride_chip = True + elif internal_id == 0x09: + # Vile Defeated + bit_byte_vile |= 0x30 + new_bit_byte_vile = True + elif internal_id == 0x0A: + # Byte Defeated + bit_byte_vile |= 0x0C + new_bit_byte_vile = True + elif internal_id == 0x0B: + # Bit Defeated + bit_byte_vile |= 0x03 + new_bit_byte_vile = True + elif internal_id == 0x0E: + # Victory + pass + elif internal_id >= 0x300: + # Maverick Medal + cleared_levels[data_bit] = 0xFF + new_cleared_level = True + elif internal_id >= 0x200: + # Boss clear + boss_id = internal_id & 0x1F + defeated_bosses[boss_id] = 0xFF + new_boss_clears = 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, MMX3_LEVEL_CLEARED, bytes(cleared_levels)) + if new_boss_clears: + snes_buffered_write(ctx, MMX3_DEFEATED_BOSSES, bytes(defeated_bosses)) + if new_bit_byte_vile: + snes_buffered_write(ctx, MMX3_BIT_BYTE_VILE, bytearray([bit_byte_vile])) + if new_pickup: + snes_buffered_write(ctx, MMX3_COLLECTED_PICKUPS, bytes(collected_pickups)) + if new_upgrade: + snes_buffered_write(ctx, MMX3_COLLECTED_UPGRADES, bytearray([collected_upgrades])) + if new_heart_tank: + snes_buffered_write(ctx, MMX3_COLLECTED_HEART_TANKS, bytearray([collected_heart_tanks])) + if new_ride_chip: + snes_buffered_write(ctx, MMX3_COLLECTED_RIDE_CHIPS, bytearray([collected_ride_chips])) + await snes_flush_writes(ctx) + + + async def handle_hp_trade(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + + validation = await snes_read(ctx, MMX3_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 menu_state = await snes_read(ctx, MMX3_MENU_STATE, 0x1) gameplay_state = await snes_read(ctx, MMX3_GAMEPLAY_STATE, 0x1) can_move = await snes_read(ctx, MMX3_CAN_MOVE, 0x1) - on_ride_armor = await snes_read(ctx, MMX3_ON_RIDE_ARMOR, 0x1) - going_through_gate = await snes_read(ctx, MMX3_GOING_THROUGH_GATE, 0x4) + if menu_state[0] != 0x04 or \ + gameplay_state[0] != 0x04 or \ + can_move[0] != 0x00: + return + + pause_state = await snes_read(ctx, MMX3_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, MMX3_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, MMX3_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 + + # Handle validation + validation = await snes_read(ctx, MMX3_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, MMX3_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, MMX3_ENERGY_LINK_PACKET, bytearray([0x00, 0x00])) + await snes_flush_writes(ctx) + + # Expose EnergyLink to the ROM pause_state = await snes_read(ctx, MMX3_PAUSE_STATE, 0x1) + screen_brightness = await snes_read(ctx, MMX3_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, MMX3_ENERGY_LINK_COUNT, bytearray([total_energy & 0xFF, (total_energy >> 8) & 0xFF])) + else: + snes_buffered_write(ctx, MMX3_ENERGY_LINK_COUNT, bytearray([0x0F, 0x27])) + + receiving_item = await snes_read(ctx, MMX3_RECEIVING_ITEM, 0x1) + menu_state = await snes_read(ctx, MMX3_MENU_STATE, 0x1) + gameplay_state = await snes_read(ctx, MMX3_GAMEPLAY_STATE, 0x1) + can_move = await snes_read(ctx, MMX3_CAN_MOVE, 0x1) + frozen_systems = await snes_read(ctx, MMX3_FROZEN_SYSTEMS, 0x7) if menu_state[0] != 0x04 or \ gameplay_state[0] != 0x04 or \ can_move[0] != 0x00 or \ - on_ride_armor[0] == 0x0A or \ - pause_state[0] != 0x00 or \ receiving_item[0] != 0x00 or \ - going_through_gate != b'\x00\x00\x00\x00': - backup_item = self.item_queue.pop(0) - self.item_queue.append(backup_item) + frozen_systems != b'\x00\x00\x00\x00\x00\x00\x00': + 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, MMX3_REFILL_REQUEST, 0x1), "little") + target = int.from_bytes(await snes_read(ctx, MMX3_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, MMX3_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, ride_armor_rom_data, upgrades_rom_data + + if not hasattr(self, "item_queue") or len(self.item_queue) == 0: + return + + validation = await snes_read(ctx, MMX3_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, MMX3_RECEIVING_ITEM, 0x1) + menu_state = await snes_read(ctx, MMX3_MENU_STATE, 0x1) + gameplay_state = await snes_read(ctx, MMX3_GAMEPLAY_STATE, 0x1) + hp_refill = await snes_read(ctx, MMX3_ENABLE_HP_REFILL, 0x1) + weapon_refill = await snes_read(ctx, MMX3_ENABLE_WEAPON_REFILL, 0x1) + can_move = await snes_read(ctx, MMX3_CAN_MOVE, 0x1) + on_ride_armor = await snes_read(ctx, MMX3_ON_RIDE_ARMOR, 0x1) + frozen_systems = await snes_read(ctx, MMX3_FROZEN_SYSTEMS, 0x7) + if menu_state[0] != 0x04 or \ + gameplay_state[0] != 0x04 or \ + can_move[0] != 0x00 or \ + hp_refill[0] != 0x00 or \ + weapon_refill[0] != 0x00 or \ + on_ride_armor[0] == 0x0A or \ + receiving_item[0] != 0x00 or \ + frozen_systems != b'\x00\x00\x00\x00\x00\x00\x00': return + next_item = self.item_queue[0] + item_id = next_item[1] + # Handle items that Zero can also get if next_item[0] in X_Z_ITEMS: backup_item = self.item_queue.pop(0) @@ -405,25 +822,31 @@ async def handle_item_queue(self, ctx): elif next_item[0] == "1up": life_count = await snes_read(ctx, MMX3_LIFE_COUNT, 0x1) - if life_count[0] < 9: + if life_count[0] < 99: snes_buffered_write(ctx, MMX3_ENABLE_GIVE_1UP, bytearray([0x01])) snes_buffered_write(ctx, MMX3_RECEIVING_ITEM, bytearray([0x01])) + self.save_arsenal = True else: self.item_queue.append(backup_item) # Ignore Zero for the following items + pause_state = await snes_read(ctx, MMX3_PAUSE_STATE, 0x1) + screen_brightness = await snes_read(ctx, MMX3_SCREEN_BRIGHTNESS, 0x1) active_character = await snes_read(ctx, MMX3_ACTIVE_CHARACTER, 0x1) - if active_character[0] != 0x00: + if active_character[0] != 0x00 and (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": - # TODO: Send a signal to play back a SFX weapon = weapon_rom_data[item_id] snes_buffered_write(ctx, WRAM_START + weapon[0], bytearray([weapon[1]])) snes_buffered_write(ctx, MMX3_SFX_FLAG, bytearray([0x01])) snes_buffered_write(ctx, MMX3_SFX_NUMBER, bytearray([0x16])) self.item_queue.pop(0) + self.save_arsenal = True elif next_item[0] == "heart tank": heart_tanks = await snes_read(ctx, MMX3_HEART_TANKS, 0x1) @@ -435,6 +858,7 @@ async def handle_item_queue(self, ctx): snes_buffered_write(ctx, MMX3_ENABLE_HEART_TANK, bytearray([0x02])) snes_buffered_write(ctx, MMX3_RECEIVING_ITEM, bytearray([0x01])) self.item_queue.pop(0) + self.save_arsenal = True elif next_item[0] == "sub tank": upgrades = await snes_read(ctx, MMX3_UPGRADES, 0x1) @@ -452,6 +876,7 @@ async def handle_item_queue(self, ctx): snes_buffered_write(ctx, MMX3_SFX_FLAG, bytearray([0x01])) snes_buffered_write(ctx, MMX3_SFX_NUMBER, bytearray([0x17])) self.item_queue.pop(0) + self.save_arsenal = True elif next_item[0] == "upgrade": upgrades = await snes_read(ctx, MMX3_UPGRADES, 0x1) @@ -512,6 +937,7 @@ async def handle_item_queue(self, ctx): snes_buffered_write(ctx, MMX3_SFX_FLAG, bytearray([0x01])) snes_buffered_write(ctx, MMX3_SFX_NUMBER, bytearray([0x1B])) self.item_queue.pop(0) + self.save_arsenal = True elif next_item[0] == "ride": ride = await snes_read(ctx, MMX3_RIDE_CHIPS, 0x1) @@ -525,419 +951,155 @@ async def handle_item_queue(self, ctx): snes_buffered_write(ctx, MMX3_SFX_FLAG, bytearray([0x01])) snes_buffered_write(ctx, MMX3_SFX_NUMBER, bytearray([0x32])) 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, MMX3_GAME_STATE, 0x1) - menu_state = await snes_read(ctx, MMX3_MENU_STATE, 0x1) - gameplay_state = await snes_read(ctx, MMX3_GAMEPLAY_STATE, 0x1) - - # Discard uninitialized ROMs - if menu_state is None: - self.game_state = False - self.energy_link_enabled = False - self.item_queue = [] - self.current_level_value = 42 - return - - if game_state[0] == 0: - self.game_state = False - self.item_queue = [] - self.current_level_value = 42 - return - - validation = await snes_read(ctx, MMX3_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, MMX3_MENU_STATE, 0x1)) + gameplay_state = int.from_bytes(await snes_read(ctx, MMX3_GAMEPLAY_STATE, 0x1)) + map_state = int.from_bytes(await snes_read(ctx, WRAM_START + 0x1E59, 0x1)) + sync_arsenal = int.from_bytes(await snes_read(ctx, MMX3_ARSENAL_SYNC, 0x2), "little") + if (menu_state == 0x00 and map_state == 0x0A) or (menu_state == 0x04 and gameplay_state == 0x04): + # Load Arsenal + if sync_arsenal == 0x1337: + arsenal = ctx.stored_data[f"mmx3_arsenal_{ctx.team}_{ctx.slot}"] or dict() + if arsenal: + # Data in arsenal + snes_buffered_write(ctx, MMX3_RECV_INDEX, bytes(arsenal["recv_index"].to_bytes(2, 'little'))) + snes_buffered_write(ctx, MMX3_LIFE_COUNT, bytes(arsenal["life_count"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX3_UPGRADES, bytes(arsenal["upgrades"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX3_RIDE_CHIPS, bytes(arsenal["ride_chips"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX3_MAX_HP, bytes(arsenal["max_hp"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX3_HEART_TANKS, bytes(arsenal["heart_tanks"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX3_SUB_TANK_ARRAY, bytearray(arsenal["sub_tanks"])) + snes_buffered_write(ctx, MMX3_UNLOCKED_CHARGED_SHOT, bytes(arsenal["unlocked_buster"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX3_WEAPON_ARRAY, bytearray(arsenal["weapons"])) + snes_buffered_write(ctx, MMX3_HYPER_CANNON, bytes(arsenal["hyper_cannon"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX3_ZSABER, bytes(arsenal["z_saber"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX3_UNLOCKED_LEVELS, bytearray(arsenal["levels"])) + snes_buffered_write(ctx, MMX3_DOPPLER_ACCESS, bytes(arsenal["doppler_access"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX3_VILE_ACCESS, bytes(arsenal["vile_access"].to_bytes(1, 'little'))) + + snes_buffered_write(ctx, MMX3_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, MMX3_RECV_INDEX, 0x2), "little") + arsenal["life_count"] = int.from_bytes(await snes_read(ctx, MMX3_LIFE_COUNT, 0x1), "little") + arsenal["upgrades"] = int.from_bytes(await snes_read(ctx, MMX3_UPGRADES, 0x1), "little") + arsenal["ride_chips"] = int.from_bytes(await snes_read(ctx, MMX3_RIDE_CHIPS, 0x1), "little") + arsenal["max_hp"] = int.from_bytes(await snes_read(ctx, MMX3_MAX_HP, 0x1), "little") + arsenal["heart_tanks"] = int.from_bytes(await snes_read(ctx, MMX3_HEART_TANKS, 0x1), "little") + arsenal["sub_tanks"] = list(await snes_read(ctx, MMX3_SUB_TANK_ARRAY, 0x4)) + arsenal["unlocked_buster"] = int.from_bytes(await snes_read(ctx, MMX3_UNLOCKED_CHARGED_SHOT, 0x1), "little") + arsenal["weapons"] = list(await snes_read(ctx, MMX3_WEAPON_ARRAY, 0x10)) + arsenal["hyper_cannon"] = int.from_bytes(await snes_read(ctx, MMX3_HYPER_CANNON, 0x1), "little") + arsenal["z_saber"] = int.from_bytes(await snes_read(ctx, MMX3_ZSABER, 0x1), "little") + arsenal["levels"] = list(await snes_read(ctx, MMX3_UNLOCKED_LEVELS, 0x20)) + arsenal["doppler_access"] = int.from_bytes(await snes_read(ctx, MMX3_DOPPLER_ACCESS, 0x1), "little") + arsenal["vile_access"] = int.from_bytes(await snes_read(ctx, MMX3_VILE_ACCESS, 0x1), "little") - if self.trade_request is not None: - await self.handle_hp_trade(ctx) - - await self.handle_item_queue(ctx) + # Attempt to not lose any previously saved data in case of RAM corruption + saved_arsenal = ctx.stored_data[f"mmx3_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["doppler_access"] = min(saved_arsenal["doppler_access"], arsenal["doppler_access"]) + arsenal["vile_access"] = min(saved_arsenal["doppler_access"], arsenal["vile_access"]) + + arsenal["upgrades"] |= saved_arsenal["upgrades"] + arsenal["unlocked_buster"] |= saved_arsenal["unlocked_buster"] + arsenal["hyper_cannon"] |= saved_arsenal["hyper_cannon"] & 0x40 + arsenal["z_saber"] |= saved_arsenal["z_saber"] & 0xE0 + arsenal["ride_chips"] |= saved_arsenal["ride_chips"] + arsenal["heart_tanks"] |= saved_arsenal["heart_tanks"] + for i in range(0x4): + arsenal["sub_tanks"][i] |= saved_arsenal["sub_tanks"][i] & 0x80 - # This is going to be rewritten whenever SNIClient supports on_package - energy_link = await snes_read(ctx, MMX3_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"mmx3_arsenal_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": arsenal}], + }]) + self.save_arsenal = False + + # Checkpoints reached + checkpoints = list(await snes_read(ctx, MMX3_CHECKPOINTS_REACHED, 0xF)) + data_storage_checkpoints = ctx.stored_data[f"mmx3_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"mmx3_checkpoints_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": computed_checkpoints}], + }]) + snes_buffered_write(ctx, MMX3_CHECKPOINTS_REACHED, bytes(computed_checkpoints)) + + # Global timer + timer = int.from_bytes(await snes_read(ctx, MMX3_GLOBAL_TIMER, 0x4), "little") + data_storage_timer = ctx.stored_data[f"mmx3_global_timer_{ctx.team}_{ctx.slot}"] or 0 + if timer >= data_storage_timer: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx3_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.") - - from .Rom import weapon_rom_data, ride_armor_rom_data, upgrades_rom_data, boss_access_rom_data, refill_rom_data - from .Levels import location_id_to_level_id - from worlds import AutoWorldRegister + snes_buffered_write(ctx, MMX3_GLOBAL_TIMER, data_storage_timer.to_bytes(4, "little")) - bit_byte_vile_data = await snes_read(ctx, MMX3_BIT_BYTE_VILE, 0x01) - defeated_bosses_data = await snes_read(ctx, MMX3_DEFEATED_BOSSES, 0x20) - completed_rematches = await snes_read(ctx, MMX3_COMPLETED_REMATCHES, 0x1) - completed_rematches = completed_rematches[0] - required_rematches = await snes_read(ctx, MMX3_REQUIRED_REMATCHES, 0x1) - required_rematches = required_rematches[0] - defeated_bosses = list(defeated_bosses_data) - cleared_levels_data = await snes_read(ctx, MMX3_LEVEL_CLEARED, 0x20) - cleared_levels = list(cleared_levels_data) - victory_ram = await snes_read(ctx, MMX3_VICTORY, 0x1) - collected_heart_tanks_data = await snes_read(ctx, MMX3_COLLECTED_HEART_TANKS, 0x01) - collected_ride_chips_data = await snes_read(ctx, MMX3_COLLECTED_RIDE_CHIPS, 0x01) - collected_upgrades_data = await snes_read(ctx, MMX3_COLLECTED_UPGRADES, 0x01) - collected_pickups_data = await snes_read(ctx, MMX3_COLLECTED_PICKUPS, 0x40) - collected_pickups = list(collected_pickups_data) - pickupsanity_enabled = await snes_read(ctx, MMX3_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: - level_id = data[0] - internal_id = data[1] - data_bit = data[2] + # Death count + deaths = int.from_bytes(await snes_read(ctx, MMX3_GLOBAL_DEATHS, 0x2), "little") + data_storage_deaths = ctx.stored_data[f"mmx3_deaths_{ctx.team}_{ctx.slot}"] or 0 + if deaths >= data_storage_deaths: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx3_deaths_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": deaths}, + {"operation": "min", "value": 9999}], + }]) + else: + snes_buffered_write(ctx, MMX3_GLOBAL_DEATHS, data_storage_deaths.to_bytes(2, "little")) - if internal_id == 0x02: - # Heart Tank - bit = 0x01 << (level_id - 1) - masked_data = collected_heart_tanks_data[0] & bit - if masked_data != 0: - new_checks.append(loc_id) - elif internal_id == 0x03: - # Sub Tank - masked_data = collected_upgrades_data[0] & data_bit - if masked_data != 0: - new_checks.append(loc_id) - elif internal_id == 0x04: - # Mega Man upgrades - masked_data = collected_upgrades_data[0] & data_bit - if masked_data != 0: - new_checks.append(loc_id) - elif internal_id == 0x05: - # Ride Armor - masked_data = collected_ride_chips_data[0] & data_bit - if masked_data != 0: - new_checks.append(loc_id) - elif internal_id == 0x06: - # Mega Man chips - masked_data = collected_ride_chips_data[0] & data_bit - if masked_data != 0: - new_checks.append(loc_id) - elif internal_id == 0x09: - # Vile Defeated - vile_defeated = bit_byte_vile_data[0] & 0x30 - if vile_defeated != 0: - new_checks.append(loc_id) - elif internal_id == 0x0A: - # Byte Defeated - byte_defeated = bit_byte_vile_data[0] & 0x0C - if byte_defeated != 0: - new_checks.append(loc_id) - elif internal_id == 0x0B: - # Bit Defeated - bit_defeated = bit_byte_vile_data[0] & 0x03 - if bit_defeated != 0: - new_checks.append(loc_id) - elif internal_id == 0x0E: - # Victory - if victory_ram[0]: - new_checks.append(loc_id) - elif internal_id >= 0x300: - # Maverick Medal - if cleared_levels_data[data_bit] != 0: - new_checks.append(loc_id) - elif internal_id >= 0x200: - # Boss clear - boss_id = internal_id & 0x1F - if defeated_bosses_data[boss_id] != 0: - new_checks.append(loc_id) - elif internal_id >= 0x100: - # Pickups - if not pickupsanity_enabled or pickupsanity_enabled[0] == 0: - continue - pickup_id = internal_id & 0x3F - if collected_pickups_data[pickup_id] != 0: - new_checks.append(loc_id) - - snes_buffered_write(ctx, MMX3_COMPLETED_REMATCHES, bytearray([completed_rematches])) - snes_buffered_write(ctx, MMX3_DEFEATED_BOSSES, bytes(defeated_bosses)) - await snes_flush_writes(ctx) - - verify_game_state = await snes_read(ctx, MMX3_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, MMX3_GLOBAL_DMG_DEALT, 0x2), "little") + data_storage_dmg_dealt = ctx.stored_data[f"mmx3_damage_dealt_{ctx.team}_{ctx.slot}"] or 0 + if dmg_dealt >= data_storage_dmg_dealt: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx3_damage_dealt_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": dmg_dealt}, + {"operation": "min", "value": 9999}], + }]) + else: + snes_buffered_write(ctx, MMX3_GLOBAL_DMG_DEALT, data_storage_dmg_dealt.to_bytes(2, "little")) - rom = await snes_read(ctx, MMX3_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, MMX3_GLOBAL_DMG_TAKEN, 0x2), "little") + data_storage_dmg_taken = ctx.stored_data[f"mmx3_damage_taken_{ctx.team}_{ctx.slot}"] or 0 + if dmg_taken >= data_storage_dmg_taken: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx3_damage_taken_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": dmg_taken}, + {"operation": "min", "value": 9999}], + }]) + else: + snes_buffered_write(ctx, MMX3_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[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, MMX3_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"mmx3_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, MMX3_RECV_INDEX, 1) - 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] - - 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, MMX3_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 == 0xBD0013: - self.add_item_to_queue("heart tank", item.item) - - elif item.item == 0xBD0014: - 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 ride_armor_rom_data: - self.add_item_to_queue("ride", item.item) - - elif item.item in boss_access_rom_data: - boss_access = await snes_read(ctx, MMX3_UNLOCKED_LEVELS, 0x20) - boss_access = bytearray(boss_access) - level = boss_access_rom_data[item.item] - boss_access[level[0] * 2] = 0x01 - snes_buffered_write(ctx, MMX3_UNLOCKED_LEVELS, boss_access) - if item.item == 0xBD000A: - snes_buffered_write(ctx, MMX3_DOPPLER_ACCESS, bytearray([0x00])) - snes_buffered_write(ctx, MMX3_SFX_FLAG, bytearray([0x01])) - snes_buffered_write(ctx, MMX3_SFX_NUMBER, bytearray([0x1D])) - - elif item.item == 0xBD0019: - # Unlock vile stage - snes_buffered_write(ctx, MMX3_VILE_ACCESS, bytearray([0x00])) - snes_buffered_write(ctx, MMX3_SFX_FLAG, bytearray([0x01])) - snes_buffered_write(ctx, MMX3_SFX_NUMBER, bytearray([0x1D])) - - 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 == 0xBD0000: - # 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, MMX3_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_ride_chip = False - new_bit_byte_vile = False - new_pickup = False - completed_rematches = await snes_read(ctx, MMX3_COMPLETED_REMATCHES, 0x1) - completed_rematches = completed_rematches[0] - cleared_levels_data = await snes_read(ctx, MMX3_LEVEL_CLEARED, 0x20) - cleared_levels = list(cleared_levels_data) - collected_pickups_data = await snes_read(ctx, MMX3_COLLECTED_PICKUPS, 0x40) - collected_pickups = list(collected_pickups_data) - collected_heart_tanks_data = await snes_read(ctx, MMX3_COLLECTED_HEART_TANKS, 0x01) - collected_heart_tanks_data = collected_heart_tanks_data[0] - collected_upgrades_data = await snes_read(ctx, MMX3_COLLECTED_UPGRADES, 0x01) - collected_upgrades_data = collected_upgrades_data[0] - collected_ride_chips_data = await snes_read(ctx, MMX3_COLLECTED_RIDE_CHIPS, 0x01) - collected_ride_chips_data = collected_ride_chips_data[0] - defeated_bosses_data = await snes_read(ctx, MMX3_DEFEATED_BOSSES, 0x20) - defeated_bosses = list(defeated_bosses_data) - bit_byte_vile_data = await snes_read(ctx, MMX3_BIT_BYTE_VILE, 0x01) - bit_byte_vile_data = bit_byte_vile_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 == 0x02: - # Heart Tank - bit = 0x01 << (level_id - 1) - collected_heart_tanks_data |= bit - new_heart_tank = True - elif internal_id == 0x03: - # Sub Tank - collected_upgrades_data |= data_bit - new_upgrade = True - elif internal_id == 0x04: - # Mega Man upgrades - collected_upgrades_data |= data_bit - new_upgrade = True - elif internal_id == 0x05: - # Ride Armor - collected_ride_chips_data |= data_bit - new_ride_chip = True - elif internal_id == 0x06: - # Mega Man chips - collected_ride_chips_data |= data_bit - new_ride_chip = True - elif internal_id == 0x09: - # Vile Defeated - bit_byte_vile_data |= 0x30 - new_bit_byte_vile = True - elif internal_id == 0x0A: - # Byte Defeated - bit_byte_vile_data |= 0x0C - new_bit_byte_vile = True - elif internal_id == 0x0B: - # Bit Defeated - bit_byte_vile_data |= 0x03 - new_bit_byte_vile = True - elif internal_id == 0x0E: - # Victory - pass - elif internal_id >= 0x300: - # Maverick Medal - cleared_levels[data_bit] = 0xFF - new_cleared_level = True - elif internal_id >= 0x200: - # Boss clear - boss_id = internal_id & 0x1F - defeated_bosses[boss_id] = 0xFF - medal_id = BOSS_MEDAL[boss_id] - if medal_id != 0xFF: - cleared_levels[medal_id] = 0xFF - new_cleared_level = True - new_boss_clears = True - if boss_id >= 0x13 and boss_id <= 0x1A: - completed_rematches |= 0x1 << (boss_id - 0x13) - 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, MMX3_LEVEL_CLEARED, bytes(cleared_levels)) - if new_boss_clears: - snes_buffered_write(ctx, MMX3_DEFEATED_BOSSES, bytes(defeated_bosses)) - snes_buffered_write(ctx, MMX3_COMPLETED_REMATCHES, bytearray([completed_rematches])) - if new_bit_byte_vile: - snes_buffered_write(ctx, MMX3_BIT_BYTE_VILE, bytearray([bit_byte_vile_data])) - if new_pickup: - snes_buffered_write(ctx, MMX3_COLLECTED_PICKUPS, bytes(collected_pickups)) - if new_upgrade: - snes_buffered_write(ctx, MMX3_COLLECTED_UPGRADES, bytearray([collected_upgrades_data])) - if new_heart_tank: - snes_buffered_write(ctx, MMX3_COLLECTED_HEART_TANKS, bytearray([collected_heart_tanks_data])) - if new_ride_chip: - snes_buffered_write(ctx, MMX3_COLLECTED_RIDE_CHIPS, bytearray([collected_ride_chips_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 X3": - 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) / EXCHANGE_RATE - logger.info(f"Energy available: {pool:.2f}") + await snes_flush_writes(ctx) def cmd_heal(self, amount: str = ""): @@ -994,23 +1156,6 @@ def cmd_refill(self, amount: str = ""): 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 X3": - 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.") - - def cmd_trade(self, amount: str = ""): """ Trades HP to Weapon Energy. 1:1 ratio. @@ -1037,19 +1182,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 X3": 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.") + 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.") - \ No newline at end of file + self.ctx.client_handler.resync_request = True + logger.info(f"Placing a resync request...") diff --git a/worlds/mmx3/Graphics.py b/worlds/mmx3/Graphics.py new file mode 100644 index 00000000000..46b33b39df0 --- /dev/null +++ b/worlds/mmx3/Graphics.py @@ -0,0 +1,223 @@ +graphics_slots = { + 0x00: [0x1A8000, 0x0D0000, 0x030C, 0x03C0], + 0x01: [0x1A830C, 0x0D030C, 0x0532, 0x2600], + 0x02: [0x1A883E, 0x0D083E, 0x05B8, 0x07A0], + 0x03: [0x1A8DF6, 0x0D0DF6, 0x058C, 0x07E0], + 0x04: [0x1A9382, 0x0D1382, 0x0312, 0x0400], + 0x05: [0x1A9694, 0x0D1694, 0x0633, 0x09A0], + 0x06: [0x1A9CC7, 0x0D1CC7, 0x0662, 0x0800], + 0x07: [0x1AA329, 0x0D2329, 0x02FB, 0x0400], + 0x08: [0x1AA624, 0x0D2624, 0x02D3, 0x0400], + 0x09: [0x1AA8F7, 0x0D28F7, 0x0292, 0x0340], + 0x0A: [0x1AAB89, 0x0D2B89, 0x0BF4, 0x1000], + 0x0B: [0x1AB77D, 0x0D377D, 0x0010, 0x0020], + 0x0C: [0x1AB78D, 0x0D378D, 0x5EF5, 0x7760], + 0x0D: [0x1B9682, 0x0D9682, 0x541A, 0x7DC0], + 0x0E: [0x1BEA9C, 0x0DEA9C, 0x3208, 0x4C00], + 0x0F: [0x1C9CA4, 0x0E1CA4, 0x0663, 0x0A00], + 0x10: [0x1CA307, 0x0E2307, 0x5614, 0x7C00], + 0x11: [0x1CF91A, 0x0E791A, 0x04DA, 0x0900], + 0x12: [0x1CFDF4, 0x0E7DF4, 0x06C0, 0x07C0], + 0x13: [0x1D84B3, 0x0E84B3, 0x1B11, 0x2600], + 0x14: [0x1D9FC3, 0x0E9FC3, 0x01D9, 0x0800], + 0x15: [0x1DA19C, 0x0EA19C, 0x0813, 0x0940], + 0x16: [0x1DA9AF, 0x0EA9AF, 0x04A5, 0x0600], + 0x17: [0x1DAE54, 0x0EAE54, 0x0166, 0x02C0], + 0x18: [0x1DAFB9, 0x0EAFB9, 0x02D5, 0x0400], + 0x19: [0x1DB28E, 0x0EB28E, 0x00EA, 0x0160], + 0x1A: [0x1DB378, 0x0EB378, 0x06E6, 0x0960], + 0x1B: [0x1DBA5E, 0x0EBA5E, 0x494D, 0x7DA0], + 0x1C: [0x1E83AB, 0x0F03AB, 0x3ED9, 0x75E0], + 0x1D: [0x1EC284, 0x0F4284, 0x146F, 0x1800], + 0x1E: [0x1ED6F3, 0x0F56F3, 0x0993, 0x0C40], + 0x1F: [0x1EE085, 0x0F6085, 0x17B1, 0x1D00], + 0x20: [0x1EF836, 0x0F7836, 0x0DAF, 0x1000], + 0x21: [0x1F85E5, 0x0F85E5, 0x05F3, 0x0760], + 0x22: [0x1F8BD8, 0x0F8BD8, 0x0003, 0x0002], + 0x23: [0x1F8BDB, 0x0F8BDB, 0x148D, 0x1B80], + 0x24: [0x1FA068, 0x0FA068, 0x03D7, 0x0480], + 0x25: [0x1FA43E, 0x0FA43E, 0x0610, 0x0800], + 0x26: [0x1FAA4E, 0x0FAA4E, 0x1075, 0x1600], + 0x27: [0x1FBAC3, 0x0FBAC3, 0x50FF, 0x7E00], + 0x28: [0x208BC2, 0x100BC2, 0x06C7, 0x0A00], + 0x29: [0x209289, 0x101289, 0x0277, 0x0400], + 0x2A: [0x209500, 0x101500, 0x0052, 0x0800], + 0x2B: [0x209552, 0x101552, 0x029C, 0x04C0], + 0x2C: [0x2097ED, 0x1017ED, 0x0187, 0x03E0], + 0x2D: [0x209974, 0x101974, 0x01C5, 0x02C0], + 0x2E: [0x209B39, 0x101B39, 0x00DC, 0x00E0], + 0x2F: [0x209C15, 0x101C15, 0x019B, 0x01C0], + 0x30: [0x209DAF, 0x101DAF, 0x05F0, 0x07E0], + 0x31: [0x20A39F, 0x10239F, 0x03FB, 0x0600], + 0x32: [0x20A79A, 0x10279A, 0x0350, 0x03C0], + 0x33: [0x20AAEA, 0x102AEA, 0x0628, 0x0800], + 0x34: [0x20B112, 0x103112, 0x0410, 0x0560], + 0x35: [0x20B522, 0x103522, 0x0098, 0x0200], + 0x36: [0x20B5BA, 0x1035BA, 0x0153, 0x0180], + 0x37: [0x20B70D, 0x10370D, 0x55CC, 0x5CE0], + 0x38: [0x218CD9, 0x108CD9, 0x0133, 0x0160], + 0x39: [0x218E0C, 0x108E0C, 0x012E, 0x0420], + 0x3A: [0x218F3A, 0x108F3A, 0x03F0, 0x04A0], + 0x3B: [0x21932A, 0x10932A, 0x012C, 0x0200], + 0x3C: [0x219456, 0x109456, 0x0210, 0x0240], + 0x3D: [0x219666, 0x109666, 0x023E, 0x0400], + 0x3E: [0x2198A4, 0x1098A4, 0x06D2, 0x0800], + 0x3F: [0x219F76, 0x109F76, 0x0287, 0x03A0], + 0x40: [0x21A1FD, 0x10A1FD, 0x01C2, 0x0800], + 0x41: [0x21A3BF, 0x10A3BF, 0x0168, 0x0280], + 0x42: [0x21A526, 0x10A526, 0x16CF, 0x1E60], + 0x43: [0x21BBF5, 0x10BBF5, 0x01CE, 0x0200], + 0x44: [0x21BDC3, 0x10BDC3, 0x00DD, 0x1000], + 0x45: [0x21BEA0, 0x10BEA0, 0x0182, 0x0200], + 0x46: [0x21C022, 0x10C022, 0x069F, 0x0700], + 0x47: [0x21C6C1, 0x10C6C1, 0x01BA, 0x02E0], + 0x48: [0x21C87B, 0x10C87B, 0x12C3, 0x1920], + 0x49: [0x21DB3E, 0x10DB3E, 0x0230, 0x02A0], + 0x4A: [0x21DD6E, 0x10DD6E, 0x04CF, 0x0600], + 0x4B: [0x21E23C, 0x10E23C, 0x05A8, 0x0740], + 0x4C: [0x21E7E4, 0x10E7E4, 0x1069, 0x1DA0], + 0x4D: [0x21F84D, 0x10F84D, 0x00BF, 0x0800], + 0x4E: [0x21F90C, 0x10F90C, 0x0FAF, 0x1460], + 0x4F: [0x2288BA, 0x1108BA, 0x0415, 0x0560], + 0x50: [0x228CCF, 0x110CCF, 0x0455, 0x0600], + 0x51: [0x229123, 0x111123, 0x005C, 0x0800], + 0x52: [0x22917F, 0x11117F, 0x0255, 0x0800], + 0x53: [0x2293D4, 0x1113D4, 0x0049, 0x0800], + 0x54: [0x22941D, 0x11141D, 0x09FA, 0x0B60], + 0x55: [0x229E17, 0x111E17, 0x03DC, 0x1000], + 0x56: [0x22A1F3, 0x1121F3, 0x011F, 0x0160], + 0x57: [0x22A312, 0x112312, 0x06B1, 0x0800], + 0x58: [0x22A9C3, 0x1129C3, 0x02F6, 0x0300], + 0x59: [0x22ACB9, 0x112CB9, 0x1699, 0x1DE0], + 0x5A: [0x22C352, 0x114352, 0x1403, 0x1A00], + 0x5B: [0x22D755, 0x115755, 0x01C2, 0x0600], + 0x5C: [0x22D917, 0x115917, 0x0007, 0x0020], + 0x5D: [0x22D91E, 0x11591E, 0x069B, 0x09C0], + 0x5E: [0x22DFB9, 0x115FB9, 0x0155, 0x0240], + 0x5F: [0x22E10E, 0x11610E, 0x10A1, 0x14E0], + 0x60: [0x22F1AF, 0x1171AF, 0x08F8, 0x0D80], + 0x61: [0x22FAA7, 0x117AA7, 0x0199, 0x1000], + 0x62: [0x22FC40, 0x117C40, 0x0201, 0x0400], + 0x63: [0x22FE41, 0x117E41, 0x0003, 0x0002], + 0x64: [0x22FE44, 0x117E44, 0x0266, 0x0800], + 0x65: [0x2380AA, 0x1180AA, 0x10E1, 0x1AE0], + 0x66: [0x23918B, 0x11918B, 0x0246, 0x0800], + 0x67: [0x2393D1, 0x1193D1, 0x0139, 0x0300], + 0x68: [0x23950A, 0x11950A, 0x0048, 0x0800], + 0x69: [0x239552, 0x119552, 0x0052, 0x0800], + 0x6A: [0x2395A4, 0x1195A4, 0x031E, 0x03C0], + 0x6B: [0x2398C2, 0x1198C2, 0x0036, 0x0060], + 0x6C: [0x2398F8, 0x1198F8, 0x5E32, 0x79A0], + 0x6D: [0x23F72A, 0x11F72A, 0x01E4, 0x0800], + 0x6E: [0x23F90E, 0x11F90E, 0x0F94, 0x14E0], + 0x6F: [0x2488A2, 0x1208A2, 0x0090, 0x0800], + 0x70: [0x248932, 0x120932, 0x0135, 0x0220], + 0x71: [0x248A67, 0x120A67, 0x0181, 0x03C0], + 0x72: [0x248BE8, 0x120BE8, 0x0CF3, 0x0FE0], + 0x73: [0x2498DB, 0x1218DB, 0x0BC7, 0x0E00], + 0x74: [0x24A4A2, 0x1224A2, 0x1137, 0x1520], + 0x75: [0x24B5D8, 0x1235D8, 0x00A7, 0x1000], + 0x76: [0x24B67F, 0x12367F, 0x0003, 0x0002], + 0x77: [0x24B682, 0x123682, 0x05C6, 0x1000], + 0x78: [0x24BC48, 0x123C48, 0x001D, 0x0300], + 0x79: [0x24BC65, 0x123C65, 0x48E3, 0x6060], + 0x7A: [0x258548, 0x128548, 0x3FFF, 0x5C20], + 0x7B: [0x25C547, 0x12C547, 0x3A3A, 0x48C0], + 0x7C: [0x25FF81, 0x12FF81, 0x33F9, 0x4D40], + 0x7D: [0x26B37A, 0x13337A, 0x0052, 0x0800], + 0x7E: [0x26B3CC, 0x1333CC, 0x0036, 0x0060], + 0x7F: [0x26B402, 0x133402, 0x02DF, 0x0800], + 0x80: [0x26B6E1, 0x1336E1, 0x50B8, 0x6EC0], + 0x81: [0x278799, 0x138799, 0x125B, 0x1960], + 0x82: [0x2799F4, 0x1399F4, 0x15D2, 0x2000], + 0x83: [0x27AFC6, 0x13AFC6, 0x0128, 0x02E0], + 0x84: [0x27B0EE, 0x13B0EE, 0x354A, 0x4880], + 0x85: [0x27E638, 0x13E638, 0x0166, 0x01C0], + 0x86: [0x27E79E, 0x13E79E, 0x0104, 0x1000], + 0x87: [0x27E8A2, 0x13E8A2, 0x068E, 0x1000], + 0x88: [0x27EF30, 0x13EF30, 0x0316, 0x0400], + 0x89: [0x27F246, 0x13F246, 0x040B, 0x0400], + 0x8A: [0x27F651, 0x13F651, 0x0770, 0x0A00], + 0x8B: [0x27FDC1, 0x13FDC1, 0x00A7, 0x0280], + 0x8C: [0x27FE68, 0x13FE68, 0x00FD, 0x0160], + 0x8D: [0x27FF64, 0x13FF64, 0x04E4, 0x05A0], + 0x8E: [0x288448, 0x140448, 0x044C, 0x0480], + 0x8F: [0x288894, 0x140894, 0x012D, 0x0240], + 0x90: [0x2889C0, 0x1409C0, 0x0079, 0x0800], + 0x91: [0x288A39, 0x140A39, 0x006D, 0x0800], + 0x92: [0x288AA6, 0x140AA6, 0x0072, 0x0800], + 0x93: [0x288B18, 0x140B18, 0x007F, 0x0800], + 0x94: [0x288B97, 0x140B97, 0x017A, 0x03E0], + 0x95: [0x288D11, 0x140D11, 0x0187, 0x0300], + 0x96: [0x288E97, 0x140E97, 0x013E, 0x0320], + 0x97: [0x288FD5, 0x140FD5, 0x01C3, 0x0400], + 0x98: [0x289198, 0x141198, 0x01DA, 0x01E0], + 0x99: [0x289372, 0x141372, 0x167D, 0x1A00], + 0x9A: [0x28A9EE, 0x1429EE, 0x0BC7, 0x11E0], + 0x9B: [0x28B5B5, 0x1435B5, 0x007B, 0x0800], + 0x9C: [0x28B630, 0x143630, 0x0072, 0x0800], + 0x9D: [0x28B6A2, 0x1436A2, 0x0076, 0x0800], + 0x9E: [0x28B718, 0x143718, 0x0064, 0x0800], + 0x9F: [0x28B77C, 0x14377C, 0x0072, 0x0800], + 0xA0: [0x28B7ED, 0x1437ED, 0x006F, 0x0800], + 0xA1: [0x28B85C, 0x14385C, 0x0066, 0x0800], + 0xA2: [0x28B8C2, 0x1438C2, 0x0732, 0x18A0], + 0xA3: [0x28BFF4, 0x143FF4, 0x0525, 0x1000], + 0xA4: [0x28C519, 0x144519, 0x0192, 0x01C0], + 0xA5: [0x28C6AB, 0x1446AB, 0x0059, 0x0800], + 0xA6: [0x28C704, 0x144704, 0x0011, 0x0040], + 0xA7: [0x28C715, 0x144715, 0x03AF, 0x0400], + 0xA8: [0x28CAC4, 0x144AC4, 0x0160, 0x0200], + 0xA9: [0x28CC24, 0x144C24, 0x1265, 0x1800], + 0xAA: [0x28DE88, 0x145E88, 0x09A4, 0x0DA0], + 0xAB: [0x28E82C, 0x14682C, 0x0658, 0x09A0], + 0xAC: [0x28EE84, 0x146E84, 0x0CD5, 0x0FC0], + 0xAD: [0x28FB59, 0x147B59, 0x0F3F, 0x12A0], + 0xAE: [0x298A98, 0x148A98, 0x0233, 0x0800], + 0xAF: [0x298CCB, 0x148CCB, 0x008A, 0x0800], + 0xB0: [0x298D55, 0x148D55, 0x15C8, 0x1CE0], + 0xB1: [0x29A31D, 0x14A31D, 0x084B, 0x1000], + 0xB2: [0x29AB68, 0x14AB68, 0x0072, 0x0800], + 0xB3: [0x29ABDA, 0x14ABDA, 0x17BF, 0x2840], + 0xB4: [0x29C399, 0x14C399, 0x0051, 0x0800], + 0xB5: [0x29C3EA, 0x14C3EA, 0x0003, 0x0002], + 0xB6: [0x29C3ED, 0x14C3ED, 0x0003, 0x0002], + 0xB7: [0x29C3F0, 0x14C3F0, 0x0003, 0x0002], + 0xB8: [0x29C3F3, 0x14C3F3, 0x0003, 0x0002], + 0xB9: [0x29C3F6, 0x14C3F6, 0x0003, 0x0002], + 0xBA: [0x29C3F9, 0x14C3F9, 0x0003, 0x0002], + 0xBB: [0x29C3FC, 0x14C3FC, 0x0003, 0x0002], + 0xBC: [0x29C3FF, 0x14C3FF, 0x0003, 0x0002], + 0xBD: [0x29C402, 0x14C402, 0x008D, 0x0080], + 0xBE: [0x29C48F, 0x14C48F, 0x00F6, 0x0200], + 0xBF: [0x29C585, 0x14C585, 0x00D4, 0x0140], + 0xC0: [0x29C659, 0x14C659, 0x0112, 0x0200], + 0xC1: [0x29C76A, 0x14C76A, 0x021F, 0x0800], + 0xC2: [0x29C989, 0x14C989, 0x0166, 0x0800], + 0xC3: [0x29CAEF, 0x14CAEF, 0x0048, 0x0800], + 0xC4: [0x29CB37, 0x14CB37, 0x06C1, 0x0800], + 0xC5: [0x29D1F8, 0x14D1F8, 0x0159, 0x0800], + 0xC6: [0x29D350, 0x14D350, 0x026D, 0x0580], + 0xC7: [0x29D5BD, 0x14D5BD, 0x10F2, 0x15E0], + 0xC8: [0x29E6AF, 0x14E6AF, 0x1409, 0x19E0], + 0xC9: [0x29FAB8, 0x14FAB8, 0x0007, 0x0020], + 0xCA: [0x29FABF, 0x14FABF, 0x0007, 0x0020], + 0xCB: [0x29FAC6, 0x14FAC6, 0x00EE, 0x01C0], + 0xCC: [0x29FBB4, 0x14FBB4, 0x12D4, 0x1580], + 0xCD: [0x2A8E88, 0x150E88, 0x0DCB, 0x1600], + 0xCE: [0x2A9C53, 0x151C53, 0x1811, 0x1C20], + 0xCF: [0x2AB464, 0x153464, 0x1858, 0x1E00], + 0xD0: [0x2ACCBC, 0x154CBC, 0x0D8D, 0x1520], + 0xD1: [0x2ADA49, 0x155A49, 0x0007, 0x0020], + 0xD2: [0x2ADA50, 0x155A50, 0x0706, 0x0C20], + 0xD3: [0x2AE156, 0x156156, 0x0007, 0x0020], + 0xD4: [0x2AE15D, 0x15615D, 0x0007, 0x0020], + 0xD5: [0x2AE164, 0x156164, 0x1129, 0x13A0], + 0xD6: [0x2AF28C, 0x15728C, 0x0261, 0x02C0], + 0xD7: [0x2AF4ED, 0x1574ED, 0x0117, 0x0520], + 0xD8: [0x2AF604, 0x157604, 0x06BB, 0x0800], + 0xD9: [0x2AFCBF, 0x157CBF, 0x07B1, 0x0980], + 0xDA: [0x2AF4ED, 0x1574ED, 0x0117, 0x0520], + 0xDB: [0x2AF604, 0x157604, 0x06BB, 0x0800], + 0xDC: [0x2AFCBF, 0x157CBF, 0x07B1, 0x0980], +} \ No newline at end of file diff --git a/worlds/mmx3/Options.py b/worlds/mmx3/Options.py index d8fd38cb3f0..ad116d14d20 100644 --- a/worlds/mmx3/Options.py +++ b/worlds/mmx3/Options.py @@ -11,10 +11,10 @@ class EnergyLink(DefaultOnToggle): """ Enable EnergyLink support. - EnergyLink in MMX3 works as a big HP and Weapon Energy pool that the players can use to request HP + 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 /pool, /heal , /refill or /autoheal in the client. + You make use of this feature by typing /heal or /refill in the client. """ display_name = "Energy Link" @@ -26,7 +26,7 @@ class StartingLifeCount(Range): """ display_name = "Starting Life Count" range_start = 0 - range_end = 9 + range_end = 99 default = 2 class StartingHP(Range): diff --git a/worlds/mmx3/Regions.py b/worlds/mmx3/Regions.py index 2376bb2f726..7f8a2f56991 100644 --- a/worlds/mmx3/Regions.py +++ b/worlds/mmx3/Regions.py @@ -433,16 +433,19 @@ def connect_regions(world: World): connect(world, RegionName.intro_stage, RegionName.dr_doppler_lab) # Connect Dr. Doppler Lab levels - connect(world, RegionName.dr_doppler_lab, RegionName.dr_doppler_lab_1) - - connect(world, RegionName.dr_doppler_lab_1, RegionName.dr_doppler_lab_2) - - connect(world, RegionName.dr_doppler_lab_2, RegionName.dr_doppler_lab_3) + if world.options.doppler_all_labs.value: + connect(world, RegionName.dr_doppler_lab, RegionName.dr_doppler_lab_1) + connect(world, RegionName.dr_doppler_lab, RegionName.dr_doppler_lab_2) + connect(world, RegionName.dr_doppler_lab, RegionName.dr_doppler_lab_3) + connect(world, RegionName.dr_doppler_lab, RegionName.dr_doppler_lab_4) + else: + connect(world, RegionName.dr_doppler_lab, RegionName.dr_doppler_lab_1) + connect(world, RegionName.dr_doppler_lab_1, RegionName.dr_doppler_lab_2) + connect(world, RegionName.dr_doppler_lab_2, RegionName.dr_doppler_lab_3) + connect(world, RegionName.dr_doppler_lab_3_boss, RegionName.dr_doppler_lab_4) + connect(world, RegionName.dr_doppler_lab_3, RegionName.dr_doppler_lab_3_rematches) connect(world, RegionName.dr_doppler_lab_3_rematches, RegionName.dr_doppler_lab_3_boss) - - connect(world, RegionName.dr_doppler_lab_3_boss, RegionName.dr_doppler_lab_4) - def create_region(multiworld: MultiWorld, player: int, active_locations, name: str, locations=None): ret = Region(name, player, multiworld) diff --git a/worlds/mmx3/Rom.py b/worlds/mmx3/Rom.py index b5b266ea40c..d7f247ae7e7 100644 --- a/worlds/mmx3/Rom.py +++ b/worlds/mmx3/Rom.py @@ -1,13 +1,15 @@ import typing -import bsdiff4 import Utils import hashlib import os -from typing import Optional +import io +from pathlib import Path from pkgutil import get_data from worlds.AutoWorld import World -from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes +from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension + +from .Graphics import graphics_slots action_names = ("SHOT", "JUMP", "DASH", "SELECT_L", "SELECT_R", "MENU") action_buttons = ("Y", "B", "A", "L", "R", "X", "START", "SELECT") @@ -109,6 +111,32 @@ } +class MMX3PatchExtension(APPatchExtension): + game = "Mega Man X3" + + @staticmethod + def relocate_graphics(caller: APProcedurePatch, rom: bytes): + stream = io.BytesIO(rom) + rom = bytearray(rom) + rom += bytearray([0xFF for _ in range(0x100000)]) + + for _, data in graphics_slots.items(): + pc_ptr = data[1] + compressed_size = data[2] + new_pc_ptr = pc_ptr + 0x130000 + stream.seek(pc_ptr) + rom[new_pc_ptr:new_pc_ptr + compressed_size] = stream.read(compressed_size) + rom[pc_ptr:pc_ptr + compressed_size] = bytearray([0x00 for _ in range(compressed_size)]) + + return bytes(rom) + + @staticmethod + def output_xml(caller: APProcedurePatch, rom: bytes): + manifest = caller.get_file("mmx3_manifest_for_bsnes.xml") + with open(f"{Path(caller.path).stem}.xml", "wb") as f: + f.write(manifest) + return rom + class MMX3ProcedurePatch(APProcedurePatch, APTokenMixin): hash = [HASH_US, HASH_LEGACY] game = "Mega Man X3" @@ -116,8 +144,10 @@ class MMX3ProcedurePatch(APProcedurePatch, APTokenMixin): result_file_ending = ".sfc" name: bytearray procedure = [ + ("relocate_graphics", []), ("apply_tokens", ["token_patch.bin"]), ("apply_bsdiff4", ["mmx3_basepatch.bsdiff4"]), + ("output_xml", []), ] @classmethod @@ -332,7 +362,6 @@ def patch_rom(world: World, patch: MMX3ProcedurePatch): patch.write_byte(0x17FFF1, 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) diff --git a/worlds/mmx3/Rules.py b/worlds/mmx3/Rules.py index bcc9b7321e8..df166746b51 100644 --- a/worlds/mmx3/Rules.py +++ b/worlds/mmx3/Rules.py @@ -1,4 +1,5 @@ from worlds.generic.Rules import add_rule, set_rule +from BaseClasses import CollectionState from . import MMX3World, item_groups from .Names import LocationName, ItemName, RegionName, EventName @@ -149,7 +150,7 @@ def set_rules(world: MMX3World): # Doppler Lab level rules if world.options.doppler_all_labs: - set_rule(multiworld.get_entrance(f"{RegionName.dr_doppler_lab_3_boss} -> {RegionName.dr_doppler_lab_4}", player), + set_rule(multiworld.get_entrance(f"{RegionName.dr_doppler_lab} -> {RegionName.dr_doppler_lab_4}", player), lambda state: ( state.has(EventName.dr_doppler_lab_1_clear, player) and state.has(EventName.dr_doppler_lab_2_clear, player) and @@ -335,36 +336,51 @@ def set_rules(world: MMX3World): add_pickupsanity_logic(world) +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: MMX3World): player = world.player multiworld = world.multiworld jammed_buster = world.options.jammed_buster.value - if world.options.doppler_lab_3_boss_rematch_count.value == 0: - for boss in mavericks: - bosses[boss].pop() - bosses[boss].pop() - + boss_dict = {} for boss, regions in bosses.items(): + boss_dict[boss] = regions.copy() + if boss in mavericks and world.options.doppler_lab_3_boss_rematch_count.value == 0: + boss_dict[boss].pop() + boss_dict[boss].pop() + + for boss, regions in boss_dict.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.third_armor_arms] = jammed_buster + int(weakness[0][-1:]) - 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.third_armor_arms] = jammed_buster + int(weakness[0][-1:]) - 1 - else: - ruleset[weakness[0]] = 1 - if len(weakness) != 1: - ruleset[weakness[1]] = 1 if "->" in region: 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)) else: add_rule(multiworld.get_location(region, player), - lambda state, ruleset=ruleset: state.has_all_counts(ruleset, player)) + lambda state, rulesets=rulesets: check_weaknesses(state, player, rulesets)) def add_pickupsanity_logic(world: MMX3World): @@ -372,9 +388,9 @@ def add_pickupsanity_logic(world: MMX3World): multiworld = world.multiworld # Volt Catfish - set_rule(multiworld.get_location(LocationName.volt_catfish_energy_2, player), + set_rule(multiworld.get_location(LocationName.volt_catfish_energy_1, player), lambda state: state.has_group("Ride Armors", player, 1)) - set_rule(multiworld.get_location(LocationName.volt_catfish_energy_3, player), + set_rule(multiworld.get_location(LocationName.volt_catfish_energy_2, player), lambda state: state.has_group("Ride Armors", player, 1)) set_rule(multiworld.get_location(LocationName.volt_catfish_hp_3, player), lambda state: state.has_group("Ride Armors", player, 1)) diff --git a/worlds/mmx3/Weaknesses.py b/worlds/mmx3/Weaknesses.py index 95aae42051e..1758262e783 100644 --- a/worlds/mmx3/Weaknesses.py +++ b/worlds/mmx3/Weaknesses.py @@ -612,7 +612,8 @@ def handle_weaknesses(world): if world.options.doppler_lab_2_boss == "volt_kurageil": world.boss_weaknesses["Dr. Doppler's Lab 2 Boss"] = world.boss_weaknesses["Volt Kurageil"].copy() else: - world.boss_weaknesses["Dr. Doppler's Lab 2 Boss"] = [ - world.boss_weaknesses["Vile"][0].copy(), - world.boss_weaknesses["Vile Goliath"][0].copy(), - ] + world.boss_weaknesses["Dr. Doppler's Lab 2 Boss"] = [] + for weakness in world.boss_weaknesses["Vile"]: + world.boss_weaknesses["Dr. Doppler's Lab 2 Boss"].append(weakness) + for weakness in world.boss_weaknesses["Vile Goliath"]: + world.boss_weaknesses["Dr. Doppler's Lab 2 Boss"].append(weakness) diff --git a/worlds/mmx3/__init__.py b/worlds/mmx3/__init__.py index 233089c8dba..dffb7d107b6 100644 --- a/worlds/mmx3/__init__.py +++ b/worlds/mmx3/__init__.py @@ -8,7 +8,6 @@ import pkgutil from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification -from Options import PerGameCommonOptions from worlds.AutoWorld import World, WebWorld from .Items import MMX3Item, ItemData, item_table, junk_table, item_groups from .Locations import MMX3Location, setup_locations, all_locations, location_groups @@ -20,6 +19,8 @@ from .Weaknesses import handle_weaknesses, weapon_id from .Rom import patch_rom, MMX3ProcedurePatch, HASH_US, HASH_LEGACY +from typing import Dict, Any, TYPE_CHECKING, Optional, Sequence, Tuple, ClassVar, List + class MMX3Settings(settings.Group): class RomFile(settings.SNESRomPath): """File name of the Mega Man X3 US ROM""" @@ -41,15 +42,26 @@ class MMX3Web(WebWorld): "setup/en", ["lx5"] ) + + setup_es = Tutorial( + "Guía de configuración de Multiworld", + "Guía para jugar Mega Man X3 en Archipelago", + "Spanish", + "setup_es.md", + "setup/es", + ["lx5"] + ) - tutorials = [setup_en] + tutorials = [setup_en, setup_es] option_groups = mmx3_option_groups class MMX3World(World): """ - Mega Man X3 WIP + Mega Man X3, released in 1995 for the SNES, is the third game in Capcom's "Mega Man X" series. + Players once again control Mega Man X, who must thwart a rebellion led by the Maverick Reploid scientist + Dr. Doppler. The game introduces the ability to play as Zero for limited segments, adding variety to the gameplay. """ game = "Mega Man X3" web = MMX3Web() @@ -59,7 +71,7 @@ class MMX3World(World): options_dataclass = MMX3Options options: MMX3Options - 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 @@ -234,8 +246,14 @@ 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 X3" in self.multiworld.re_gen_passthrough: + slot_data = self.multiworld.re_gen_passthrough["Mega Man X3"] + self.boss_weaknesses = slot_data["weakness_rules"] set_rules(self) - + def fill_slot_data(self): slot_data = {} @@ -293,9 +311,11 @@ def fill_slot_data(self): slot_data["logic_boss_weakness"] = self.options.logic_boss_weakness.value slot_data["logic_vile_required"] = self.options.logic_vile_required.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]) @@ -304,49 +324,18 @@ def fill_slot_data(self): def generate_early(self): - # Attempt to fix potential Fill issues by lowering Doppler & Vile values if they're too high if pickupsanity is disabled - if self.options.pickupsanity: - doppler_item_count = 0 - doppler_open = self.options.doppler_open.value - if "Medals" in doppler_open: - doppler_item_count += self.options.doppler_medal_count.value - if "Weapons" in doppler_open: - doppler_item_count += self.options.doppler_weapon_count.value - if "Armor Upgrades" in doppler_open: - doppler_item_count += self.options.doppler_upgrade_count.value - if "Heart Tanks" in doppler_open: - doppler_item_count += self.options.doppler_heart_tank_count.value - if "Sub Tanks" in doppler_open: - doppler_item_count += self.options.doppler_sub_tank_count.value - vile_item_count = 0 - vile_open = self.options.vile_open.value - if "Medals" in vile_open: - vile_item_count += self.options.vile_medal_count.value - if "Weapons" in vile_open: - vile_item_count += self.options.vile_weapon_count.value - if "Armor Upgrades" in vile_open: - vile_item_count += self.options.vile_upgrade_count.value - if "Heart Tanks" in vile_open: - vile_item_count += self.options.vile_heart_tank_count.value - if "Sub Tanks" in vile_open: - vile_item_count += self.options.vile_sub_tank_count.value - - if doppler_item_count > 31: - print (f"[{self.multiworld.player_name[self.player]}] Doppler item counts will be lowered due to a high concentration of progressive items") - self.options.doppler_medal_count.value -= min(self.options.doppler_medal_count.value, 1) - self.options.doppler_weapon_count.value -= min(self.options.doppler_weapon_count.value, 1) - self.options.doppler_upgrade_count.value -= min(self.options.doppler_upgrade_count.value, 1) - self.options.doppler_heart_tank_count.value -= min(self.options.doppler_heart_tank_count.value, 1) - self.options.doppler_sub_tank_count.value -= min(self.options.doppler_sub_tank_count.value, 1) - - if vile_item_count > 31: - print (f"[{self.multiworld.player_name[self.player]}] Vile item counts will be lowered due to a high concentration of progressive items") - self.options.vile_medal_count.value -= min(self.options.vile_medal_count.value, 1) - self.options.vile_weapon_count.value -= min(self.options.vile_weapon_count.value, 1) - self.options.vile_upgrade_count.value -= min(self.options.vile_upgrade_count.value, 1) - self.options.vile_heart_tank_count.value -= min(self.options.vile_heart_tank_count.value, 1) - self.options.vile_sub_tank_count.value -= min(self.options.vile_sub_tank_count.value, 1) - + # Enforce Vile stage options to have lower count than the Lab + if self.options.doppler_medal_count.value >= self.options.vile_medal_count.value: + self.options.vile_medal_count.value = max(self.options.doppler_medal_count.value - 1, 0) + if self.options.doppler_weapon_count.value >= self.options.vile_weapon_count.value: + self.options.vile_weapon_count.value = max(self.options.doppler_weapon_count.value - 1, 0) + if self.options.doppler_upgrade_count.value >= self.options.vile_upgrade_count.value: + self.options.vile_upgrade_count.value = max(self.options.doppler_upgrade_count.value - 1, 0) + if self.options.doppler_heart_tank_count.value >= self.options.vile_heart_tank_count.value: + self.options.vile_heart_tank_count.value = max(self.options.doppler_heart_tank_count.value - 1, 0) + if self.options.doppler_sub_tank_count.value >= self.options.vile_sub_tank_count.value: + self.options.vile_sub_tank_count.value = max(self.options.doppler_sub_tank_count.value - 1, 0) + # Adjust bit and byte medal counts if self.options.bit_medal_count.value == 0 and self.options.byte_medal_count.value == 0: self.options.byte_medal_count.value = 1 @@ -355,11 +344,19 @@ def generate_early(self): self.options.bit_medal_count.value = 6 self.options.byte_medal_count.value = self.options.bit_medal_count.value + 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: spoiler_handle.write(f"\nMega Man X3 boss weaknesses for {self.multiworld.player_name[self.player]}:\n") @@ -452,6 +449,7 @@ def generate_output(self, output_directory: str): try: patch = MMX3ProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) patch.write_file("mmx3_basepatch.bsdiff4", pkgutil.get_data(__name__, "data/mmx3_basepatch.bsdiff4")) + patch.write_file("mmx3_manifest_for_bsnes.xml", pkgutil.get_data(__name__, "data/mmx3_manifest_for_bsnes.xml")) patch_rom(self, patch) self.rom_name = patch.name diff --git a/worlds/mmx3/data/mmx3_basepatch.bsdiff4 b/worlds/mmx3/data/mmx3_basepatch.bsdiff4 index fd597ce2997..12d55145066 100644 Binary files a/worlds/mmx3/data/mmx3_basepatch.bsdiff4 and b/worlds/mmx3/data/mmx3_basepatch.bsdiff4 differ diff --git a/worlds/mmx3/data/mmx3_manifest_for_bsnes.xml b/worlds/mmx3/data/mmx3_manifest_for_bsnes.xml new file mode 100644 index 00000000000..88ff50ce029 --- /dev/null +++ b/worlds/mmx3/data/mmx3_manifest_for_bsnes.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/worlds/mmx3/docs/en_Mega Man X3.md b/worlds/mmx3/docs/en_Mega Man X3.md index 6336d3433c9..479814d6c45 100644 --- a/worlds/mmx3/docs/en_Mega Man X3.md +++ b/worlds/mmx3/docs/en_Mega Man X3.md @@ -1,3 +1,83 @@ # Mega Man X3 -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 Dr. Doppler's Lab 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 Sigma Virus is defeated. + +## What Mega Man X3 items can appear in other players' worlds? +- Maverick Access Codes +- Maverick Weapons +- Armor Upgrades (Helmet/Arms/Body/Legs) + - Includes chip upgrades +- Ride Armors +- Heart Tanks +- Sub Tanks +- 1-Ups +- HP Refill + +## What is considered a location check in Mega Man X3? +- Defeating a Boss Enemy +- Using a Dr. Light Capsule +- Collecting a Ride Armor, 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 X3 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. +- **Bit/Byte Selector:** You can decide if you're going to face Bit or Byte in a level by pressing `SELECT` at the +stage select screen. +- **Lab Stage Selector:** You can switch which Lab level you will travel to by pressing `SELECT` at the stage +select screen. +- **Holo Map Anywhere:** The holographic map can be summoned at any time if the environment allows it by pressing +`SELECT` during a valid stage. +- **Enhanced Holo Map:** The holographic map shows a lot of additional information to the player regarding the level's +checks. + +## What is EnergyLink? +EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld. In Mega Man + X3, 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 (X.Buster and Hyper C. will provide HP). + +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: + Blizzard Buffalo: Lemon (Dash) + Volt Catfish: Tornado Fang +``` +This will force `Blizzard Buffalo` to receive increased damage from the basic shot performed when dashing and will force +`Volt Catfish` to receive increased damage from Tornado Fang. + +## 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/mmx3/docs/setup_en.md b/worlds/mmx3/docs/setup_en.md index 39f22b12669..8233be69046 100644 --- a/worlds/mmx3/docs/setup_en.md +++ b/worlds/mmx3/docs/setup_en.md @@ -1,28 +1,35 @@ -# Mega Man X3 Randomizer Setup Guide +# Mega Man X3 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 X3 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 X3 Archipelago](https://github.com/BrianCumminger/megamanx3-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 X3 Player Options Page](/games/Mega%20Man%20X3%20/player-options) ## Joining a MultiWorld Game -### Obtain your patch file and create your ROM +### Get your Mega Man X3 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. 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/mmx3/docs/setup_es.md b/worlds/mmx3/docs/setup_es.md new file mode 100644 index 00000000000..8ec714a3224 --- /dev/null +++ b/worlds/mmx3/docs/setup_es.md @@ -0,0 +1,103 @@ +# Mega Man X3 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 X3 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 X3 Archipelago](https://github.com/BrianCumminger/megamanx3-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 X3](/games/Mega%20Man%20X3%20/player-options) + +## Unirse a un juego MultiWorld + +### Obtener tu parche de Mega Man X3 + +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 `.apmmx3`. + +Haz doble clic en tu archivo `.apmmx3` 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.