 0e6e359747
			
		
	
	0e6e359747
	
	
	
		
			
			* initial (broken) commit * small work on init * Update Items.py * beginning work, some rom patches * commit progress from bh branch * deathlink, fix soft-reset kill, e-tank loss * begin work on targeting new bhclient * write font * definitely didn't forget to add the other two hashes no * update to modern options, begin colors * fix 6th letter bug * palette shuffle + logic rewrite * fix a bunch of pointers * fix color changes, deathlink, and add wily 5 req * adjust weapon weakness generation * Update Rules.py * attempt wily 5 softlock fix * add explicit test for rbm weaknesses * fix difficulty and hard reset * fix connect deathlink and off by one item color * fix atomic fire again * de-jank deathlink * rewrite wily5 rule * fix rare solo-gen fill issue, hopefully * Update Client.py * fix wily 5 requirements * undo fill hook * fix picopico-kun rules * for real this time * update minimum damage requirement * begin move to procedure patch * finish move to APPP, allow rando boobeam, color updates * fix color bug, UT support? * what do you mean I forgot the procedure * fix UT? * plando weakness and fixes * sfx when item received, more time stopper edge cases * Update test_weakness.py * fix rules and color bug * fix color bug, support reduced flashing * major world overhaul * Update Locations.py * fix first found bugs * mypy cleanup * headerless roms * Update Rom.py * further cleanup * work on energylink * el fixes * update to energylink 2.0 packet * energylink balancing * potentially break other clients, more balancing * Update Items.py * remove startup change from basepatch we write that in patch, since we also need to clean the area before applying * el balancing and feedback * hopefully less test failures? * implement world version check * add weapon/health option * Update Rom.py * x/x2 * specials * Update Color.py * Update Options.py * finally apply location groups * bump minor version number instead * fix duplicate stage sends * validate wily 5, tests * see if renaming fixes * add shuffled weakness * remove passwords * refresh rbm select, fix wily 5 validation * forgot we can't check 0 * oops I broke the basepatch (remove failing test later) * fix solo gen fill error? * fix webhost patch recognition * fix imports, basepatch * move to flexibility metric for boss validation * special case boobeam trap * block strobe on stage select init * more energylink balancing * bump world version * wily HP inaccurate in validation * fix validation edge case * save last completed wily to data storage * mypy and pep8 cleanup * fix file browse validation * fix test failure, add enemy weakness * remove test seed * update enemy damage * inno setup * Update en_Mega Man 2.md * setup guide * Update en_Mega Man 2.md * finish plando weakness section * starting rbm edge case * remove * imports * properly wrap later weakness additions in regen playthrough * fix import * forgot readme * remove time stopper special casing since we moved to proper wily 5 validation, this special casing is no longer important * properly type added locations * Update CODEOWNERS * add animation reduction * deprioritize Time Stopper in rush checks * special case wily phase 1 * fix key error * forgot the test * music and general cleanup * the great rename * fix import * thanks pycharm * reorder palette shuffle * account for alien on shuffled weakness * apply suggestions * fix seedbleed * fix invalid buster passthrough * fix weakness landing beneath required amount * fix failsafe * finish music * fix Time Stopper on Flash/Alien * asar pls * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * world helpers * init cleanup * apostrophes * clearer wording * mypy and cleanup * options doc cleanup * Update rom.py * rules cleanup * Update __init__.py * Update __init__.py * move to defaultdict * cleanup world helpers * Update __init__.py * remove unnecessary line from fill hook * forgot the other one * apply code review * remove collect * Update rules.py * forgot another --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
		
			
				
	
	
		
			563 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			563 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import logging
 | |
| import time
 | |
| from enum import IntEnum
 | |
| from base64 import b64encode
 | |
| from typing import TYPE_CHECKING, Dict, Tuple, List, Optional, Any
 | |
| from NetUtils import ClientStatus, color, NetworkItem
 | |
| from worlds._bizhawk.client import BizHawkClient
 | |
| 
 | |
| if TYPE_CHECKING:
 | |
|     from worlds._bizhawk.context import BizHawkClientContext, BizHawkClientCommandProcessor
 | |
| 
 | |
| nes_logger = logging.getLogger("NES")
 | |
| logger = logging.getLogger("Client")
 | |
| 
 | |
| MM2_ROBOT_MASTERS_UNLOCKED = 0x8A
 | |
| MM2_ROBOT_MASTERS_DEFEATED = 0x8B
 | |
| MM2_ITEMS_ACQUIRED = 0x8C
 | |
| MM2_LAST_WILY = 0x8D
 | |
| MM2_RECEIVED_ITEMS = 0x8E
 | |
| MM2_DEATHLINK = 0x8F
 | |
| MM2_ENERGYLINK = 0x90
 | |
| MM2_RBM_STROBE = 0x91
 | |
| MM2_WEAPONS_UNLOCKED = 0x9A
 | |
| MM2_ITEMS_UNLOCKED = 0x9B
 | |
| MM2_WEAPON_ENERGY = 0x9C
 | |
| MM2_E_TANKS = 0xA7
 | |
| MM2_LIVES = 0xA8
 | |
| MM2_DIFFICULTY = 0xCB
 | |
| MM2_HEALTH = 0x6C0
 | |
| MM2_COMPLETED_STAGES = 0x770
 | |
| MM2_CONSUMABLES = 0x780
 | |
| 
 | |
| MM2_SFX_QUEUE = 0x580
 | |
| MM2_SFX_STROBE = 0x66
 | |
| 
 | |
| MM2_CONSUMABLE_TABLE: Dict[int, Tuple[int, int]] = {
 | |
|     # Item: (byte offset, bit mask)
 | |
|     0x880201: (0, 8),
 | |
|     0x880202: (16, 1),
 | |
|     0x880203: (16, 2),
 | |
|     0x880204: (16, 4),
 | |
|     0x880205: (16, 8),
 | |
|     0x880206: (16, 16),
 | |
|     0x880207: (16, 32),
 | |
|     0x880208: (16, 64),
 | |
|     0x880209: (16, 128),
 | |
|     0x88020A: (20, 1),
 | |
|     0x88020B: (20, 4),
 | |
|     0x88020C: (20, 64),
 | |
|     0x88020D: (21, 1),
 | |
|     0x88020E: (21, 2),
 | |
|     0x88020F: (21, 4),
 | |
|     0x880210: (24, 1),
 | |
|     0x880211: (24, 2),
 | |
|     0x880212: (24, 4),
 | |
|     0x880213: (28, 1),
 | |
|     0x880214: (28, 2),
 | |
|     0x880215: (28, 4),
 | |
|     0x880216: (33, 4),
 | |
|     0x880217: (33, 8),
 | |
|     0x880218: (37, 8),
 | |
|     0x880219: (37, 16),
 | |
|     0x88021A: (38, 1),
 | |
|     0x88021B: (38, 2),
 | |
|     0x880227: (38, 4),
 | |
|     0x880228: (38, 32),
 | |
|     0x880229: (38, 128),
 | |
|     0x88022A: (39, 4),
 | |
|     0x88022B: (39, 2),
 | |
|     0x88022C: (39, 1),
 | |
|     0x88022D: (38, 64),
 | |
|     0x88022E: (38, 16),
 | |
|     0x88022F: (38, 8),
 | |
|     0x88021C: (39, 32),
 | |
|     0x88021D: (39, 64),
 | |
|     0x88021E: (39, 128),
 | |
|     0x88021F: (41, 16),
 | |
|     0x880220: (42, 2),
 | |
|     0x880221: (42, 4),
 | |
|     0x880222: (42, 8),
 | |
|     0x880223: (46, 1),
 | |
|     0x880224: (46, 2),
 | |
|     0x880225: (46, 4),
 | |
|     0x880226: (46, 8),
 | |
| }
 | |
| 
 | |
| 
 | |
| class MM2EnergyLinkType(IntEnum):
 | |
|     Life = 0
 | |
|     AtomicFire = 1
 | |
|     AirShooter = 2
 | |
|     LeafShield = 3
 | |
|     BubbleLead = 4
 | |
|     QuickBoomerang = 5
 | |
|     TimeStopper = 6
 | |
|     MetalBlade = 7
 | |
|     CrashBomber = 8
 | |
|     Item1 = 9
 | |
|     Item2 = 10
 | |
|     Item3 = 11
 | |
|     OneUP = 12
 | |
| 
 | |
| 
 | |
| request_to_name: Dict[str, str] = {
 | |
|     "HP": "health",
 | |
|     "AF": "Atomic Fire energy",
 | |
|     "AS": "Air Shooter energy",
 | |
|     "LS": "Leaf Shield energy",
 | |
|     "BL": "Bubble Lead energy",
 | |
|     "QB": "Quick Boomerang energy",
 | |
|     "TS": "Time Stopper energy",
 | |
|     "MB": "Metal Blade energy",
 | |
|     "CB": "Crash Bomber energy",
 | |
|     "I1": "Item 1 energy",
 | |
|     "I2": "Item 2 energy",
 | |
|     "I3": "Item 3 energy",
 | |
|     "1U": "lives"
 | |
| }
 | |
| 
 | |
| HP_EXCHANGE_RATE = 500000000
 | |
| WEAPON_EXCHANGE_RATE = 250000000
 | |
| ONEUP_EXCHANGE_RATE = 14000000000
 | |
| 
 | |
| 
 | |
| def cmd_pool(self: "BizHawkClientCommandProcessor") -> None:
 | |
|     """Check the current pool of EnergyLink, and requestable refills from it."""
 | |
|     if self.ctx.game != "Mega Man 2":
 | |
|         logger.warning("This command can only be used when playing Mega Man 2.")
 | |
|         return
 | |
|     if not self.ctx.server or not self.ctx.slot:
 | |
|         logger.warning("You must be connected to a server to use this command.")
 | |
|         return
 | |
|     energylink = self.ctx.stored_data.get(f"EnergyLink{self.ctx.team}", 0)
 | |
|     health_points = energylink // HP_EXCHANGE_RATE
 | |
|     weapon_points = energylink // WEAPON_EXCHANGE_RATE
 | |
|     lives = energylink // ONEUP_EXCHANGE_RATE
 | |
|     logger.info(f"Healing available: {health_points}\n"
 | |
|                 f"Weapon refill available: {weapon_points}\n"
 | |
|                 f"Lives available: {lives}")
 | |
| 
 | |
| 
 | |
| def cmd_request(self: "BizHawkClientCommandProcessor", amount: str, target: str) -> None:
 | |
|     from worlds._bizhawk.context import BizHawkClientContext
 | |
|     """Request a refill from EnergyLink."""
 | |
|     if self.ctx.game != "Mega Man 2":
 | |
|         logger.warning("This command can only be used when playing Mega Man 2.")
 | |
|         return
 | |
|     if not self.ctx.server or not self.ctx.slot:
 | |
|         logger.warning("You must be connected to a server to use this command.")
 | |
|         return
 | |
|     valid_targets: Dict[str, MM2EnergyLinkType] = {
 | |
|         "HP": MM2EnergyLinkType.Life,
 | |
|         "AF": MM2EnergyLinkType.AtomicFire,
 | |
|         "AS": MM2EnergyLinkType.AirShooter,
 | |
|         "LS": MM2EnergyLinkType.LeafShield,
 | |
|         "BL": MM2EnergyLinkType.BubbleLead,
 | |
|         "QB": MM2EnergyLinkType.QuickBoomerang,
 | |
|         "TS": MM2EnergyLinkType.TimeStopper,
 | |
|         "MB": MM2EnergyLinkType.MetalBlade,
 | |
|         "CB": MM2EnergyLinkType.CrashBomber,
 | |
|         "I1": MM2EnergyLinkType.Item1,
 | |
|         "I2": MM2EnergyLinkType.Item2,
 | |
|         "I3": MM2EnergyLinkType.Item3,
 | |
|         "1U": MM2EnergyLinkType.OneUP
 | |
|     }
 | |
|     if target.upper() not in valid_targets:
 | |
|         logger.warning(f"Unrecognized target {target.upper()}. Available targets: {', '.join(valid_targets.keys())}")
 | |
|         return
 | |
|     ctx = self.ctx
 | |
|     assert isinstance(ctx, BizHawkClientContext)
 | |
|     client = ctx.client_handler
 | |
|     assert isinstance(client, MegaMan2Client)
 | |
|     client.refill_queue.append((valid_targets[target.upper()], int(amount)))
 | |
|     logger.info(f"Restoring {amount} {request_to_name[target.upper()]}.")
 | |
| 
 | |
| 
 | |
| def cmd_autoheal(self) -> None:
 | |
|     """Enable auto heal from EnergyLink."""
 | |
|     if self.ctx.game != "Mega Man 2":
 | |
|         logger.warning("This command can only be used when playing Mega Man 2.")
 | |
|         return
 | |
|     if not self.ctx.server or not self.ctx.slot:
 | |
|         logger.warning("You must be connected to a server to use this command.")
 | |
|         return
 | |
|     else:
 | |
|         assert isinstance(self.ctx.client_handler, MegaMan2Client)
 | |
|         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 get_sfx_writes(sfx: int) -> Tuple[Tuple[int, bytes, str], ...]:
 | |
|     return (MM2_SFX_QUEUE, sfx.to_bytes(1, 'little'), "RAM"), (MM2_SFX_STROBE, 0x01.to_bytes(1, "little"), "RAM")
 | |
| 
 | |
| 
 | |
| class MegaMan2Client(BizHawkClient):
 | |
|     game = "Mega Man 2"
 | |
|     system = "NES"
 | |
|     patch_suffix = ".apmm2"
 | |
|     item_queue: List[NetworkItem] = []
 | |
|     pending_death_link: bool = False
 | |
|     # default to true, as we don't want to send a deathlink until Mega Man's HP is initialized once
 | |
|     sending_death_link: bool = True
 | |
|     death_link: bool = False
 | |
|     energy_link: bool = False
 | |
|     rom: Optional[bytes] = None
 | |
|     weapon_energy: int = 0
 | |
|     health_energy: int = 0
 | |
|     auto_heal: bool = False
 | |
|     refill_queue: List[Tuple[MM2EnergyLinkType, int]] = []
 | |
|     last_wily: Optional[int] = None  # default to wily 1
 | |
| 
 | |
|     async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
 | |
|         from worlds._bizhawk import RequestFailedError, read
 | |
|         from . import MM2World
 | |
| 
 | |
|         try:
 | |
|             game_name, version = (await read(ctx.bizhawk_ctx, [(0x3FFB0, 21, "PRG ROM"),
 | |
|                                                                (0x3FFC8, 3, "PRG ROM")]))
 | |
|             if game_name[:3] != b"MM2" or version != bytes(MM2World.world_version):
 | |
|                 if game_name[:3] == b"MM2":
 | |
|                     # I think this is an easier check than the other?
 | |
|                     older_version = "0.2.1" if version == b"\xFF\xFF\xFF" else f"{version[0]}.{version[1]}.{version[2]}"
 | |
|                     logger.warning(f"This Mega Man 2 patch was generated for an different version of the apworld. "
 | |
|                                    f"Please use that version to connect instead.\n"
 | |
|                                    f"Patch version: ({older_version})\n"
 | |
|                                    f"Client version: ({'.'.join([str(i) for i in MM2World.world_version])})")
 | |
|                 if "pool" in ctx.command_processor.commands:
 | |
|                     ctx.command_processor.commands.pop("pool")
 | |
|                 if "request" in ctx.command_processor.commands:
 | |
|                     ctx.command_processor.commands.pop("request")
 | |
|                 if "autoheal" in ctx.command_processor.commands:
 | |
|                     ctx.command_processor.commands.pop("autoheal")
 | |
|                 return False
 | |
|         except UnicodeDecodeError:
 | |
|             return False
 | |
|         except RequestFailedError:
 | |
|             return False  # Should verify on the next pass
 | |
| 
 | |
|         ctx.game = self.game
 | |
|         self.rom = game_name
 | |
|         ctx.items_handling = 0b111
 | |
|         ctx.want_slot_data = False
 | |
|         deathlink = (await read(ctx.bizhawk_ctx, [(0x3FFC5, 1, "PRG ROM")]))[0][0]
 | |
|         if deathlink & 0x01:
 | |
|             self.death_link = True
 | |
|         if deathlink & 0x02:
 | |
|             self.energy_link = True
 | |
| 
 | |
|         if self.energy_link:
 | |
|             if "pool" not in ctx.command_processor.commands:
 | |
|                 ctx.command_processor.commands["pool"] = cmd_pool
 | |
|             if "request" not in ctx.command_processor.commands:
 | |
|                 ctx.command_processor.commands["request"] = cmd_request
 | |
|             if "autoheal" not in ctx.command_processor.commands:
 | |
|                 ctx.command_processor.commands["autoheal"] = cmd_autoheal
 | |
| 
 | |
|         return True
 | |
| 
 | |
|     async def set_auth(self, ctx: "BizHawkClientContext") -> None:
 | |
|         if self.rom:
 | |
|             ctx.auth = b64encode(self.rom).decode()
 | |
| 
 | |
|     def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: Dict[str, Any]) -> None:
 | |
|         if cmd == "Bounced":
 | |
|             if "tags" in args:
 | |
|                 assert ctx.slot is not None
 | |
|                 if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
 | |
|                     self.on_deathlink(ctx)
 | |
|         elif cmd == "Retrieved":
 | |
|             if f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}" in args["keys"]:
 | |
|                 self.last_wily = args["keys"][f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}"]
 | |
|         elif cmd == "Connected":
 | |
|             if self.energy_link:
 | |
|                 ctx.set_notify(f"EnergyLink{ctx.team}")
 | |
|                 if ctx.ui:
 | |
|                     ctx.ui.enable_energy_link()
 | |
| 
 | |
|     async def send_deathlink(self, ctx: "BizHawkClientContext") -> None:
 | |
|         self.sending_death_link = True
 | |
|         ctx.last_death_link = time.time()
 | |
|         await ctx.send_death("Mega Man was defeated.")
 | |
| 
 | |
|     def on_deathlink(self, ctx: "BizHawkClientContext") -> None:
 | |
|         ctx.last_death_link = time.time()
 | |
|         self.pending_death_link = True
 | |
| 
 | |
|     async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
 | |
|         from worlds._bizhawk import read, write
 | |
| 
 | |
|         if ctx.server is None:
 | |
|             return
 | |
| 
 | |
|         if ctx.slot is None:
 | |
|             return
 | |
| 
 | |
|         # get our relevant bytes
 | |
|         robot_masters_unlocked, robot_masters_defeated, items_acquired, \
 | |
|             weapons_unlocked, items_unlocked, items_received, \
 | |
|             completed_stages, consumable_checks, \
 | |
|             e_tanks, lives, weapon_energy, health, difficulty, death_link_status, \
 | |
|             energy_link_packet, last_wily = await read(ctx.bizhawk_ctx, [
 | |
|                 (MM2_ROBOT_MASTERS_UNLOCKED, 1, "RAM"),
 | |
|                 (MM2_ROBOT_MASTERS_DEFEATED, 1, "RAM"),
 | |
|                 (MM2_ITEMS_ACQUIRED, 1, "RAM"),
 | |
|                 (MM2_WEAPONS_UNLOCKED, 1, "RAM"),
 | |
|                 (MM2_ITEMS_UNLOCKED, 1, "RAM"),
 | |
|                 (MM2_RECEIVED_ITEMS, 1, "RAM"),
 | |
|                 (MM2_COMPLETED_STAGES, 0xE, "RAM"),
 | |
|                 (MM2_CONSUMABLES, 52, "RAM"),
 | |
|                 (MM2_E_TANKS, 1, "RAM"),
 | |
|                 (MM2_LIVES, 1, "RAM"),
 | |
|                 (MM2_WEAPON_ENERGY, 11, "RAM"),
 | |
|                 (MM2_HEALTH, 1, "RAM"),
 | |
|                 (MM2_DIFFICULTY, 1, "RAM"),
 | |
|                 (MM2_DEATHLINK, 1, "RAM"),
 | |
|                 (MM2_ENERGYLINK, 1, "RAM"),
 | |
|                 (MM2_LAST_WILY, 1, "RAM"),
 | |
|             ])
 | |
| 
 | |
|         if difficulty[0] not in (0, 1):
 | |
|             return  # Game is not initialized
 | |
| 
 | |
|         if not ctx.finished_game and completed_stages[0xD] != 0:
 | |
|             # this sets on credits fade, no real better way to do this
 | |
|             await ctx.send_msgs([{
 | |
|                 "cmd": "StatusUpdate",
 | |
|                 "status": ClientStatus.CLIENT_GOAL
 | |
|             }])
 | |
|         writes = []
 | |
| 
 | |
|         # deathlink
 | |
|         if self.death_link:
 | |
|             await ctx.update_death_link(self.death_link)
 | |
|         if self.pending_death_link:
 | |
|             writes.append((MM2_DEATHLINK, bytes([0x01]), "RAM"))
 | |
|             self.pending_death_link = False
 | |
|             self.sending_death_link = True
 | |
|         if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
 | |
|             if health[0] == 0x00 and not self.sending_death_link:
 | |
|                 await self.send_deathlink(ctx)
 | |
|             elif health[0] != 0x00 and not death_link_status[0]:
 | |
|                 self.sending_death_link = False
 | |
| 
 | |
|         if self.last_wily != last_wily[0]:
 | |
|             if self.last_wily is None:
 | |
|                 # revalidate last wily from data storage
 | |
|                 await ctx.send_msgs([{"cmd": "Set", "key": f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
 | |
|                     {"operation": "default", "value": 8}
 | |
|                 ]}])
 | |
|                 await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}"]}])
 | |
|             elif last_wily[0] == 0:
 | |
|                 writes.append((MM2_LAST_WILY, self.last_wily.to_bytes(1, "little"), "RAM"))
 | |
|             else:
 | |
|                 # correct our setting
 | |
|                 self.last_wily = last_wily[0]
 | |
|                 await ctx.send_msgs([{"cmd": "Set", "key": f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
 | |
|                     {"operation": "replace", "value": self.last_wily}
 | |
|                 ]}])
 | |
| 
 | |
|         # handle receiving items
 | |
|         recv_amount = items_received[0]
 | |
|         if recv_amount < len(ctx.items_received):
 | |
|             item = ctx.items_received[recv_amount]
 | |
|             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_amount, len(ctx.items_received)))
 | |
| 
 | |
|             if item.item & 0x130 == 0:
 | |
|                 # Robot Master Weapon
 | |
|                 new_weapons = weapons_unlocked[0] | (1 << ((item.item & 0xF) - 1))
 | |
|                 writes.append((MM2_WEAPONS_UNLOCKED, new_weapons.to_bytes(1, 'little'), "RAM"))
 | |
|                 writes.extend(get_sfx_writes(0x21))
 | |
|             elif item.item & 0x30 == 0:
 | |
|                 # Robot Master Stage Access
 | |
|                 new_stages = robot_masters_unlocked[0] & ~(1 << ((item.item & 0xF) - 1))
 | |
|                 writes.append((MM2_ROBOT_MASTERS_UNLOCKED, new_stages.to_bytes(1, 'little'), "RAM"))
 | |
|                 writes.extend(get_sfx_writes(0x3a))
 | |
|                 writes.append((MM2_RBM_STROBE, b"\x01", "RAM"))
 | |
|             elif item.item & 0x20 == 0:
 | |
|                 # Items
 | |
|                 new_items = items_unlocked[0] | (1 << ((item.item & 0xF) - 1))
 | |
|                 writes.append((MM2_ITEMS_UNLOCKED, new_items.to_bytes(1, 'little'), "RAM"))
 | |
|                 writes.extend(get_sfx_writes(0x21))
 | |
|             else:
 | |
|                 # append to the queue, so we handle it later
 | |
|                 self.item_queue.append(item)
 | |
|             recv_amount += 1
 | |
|             writes.append((MM2_RECEIVED_ITEMS, recv_amount.to_bytes(1, 'little'), "RAM"))
 | |
| 
 | |
|         if energy_link_packet[0]:
 | |
|             pickup = energy_link_packet[0]
 | |
|             if pickup in (0x76, 0x77):
 | |
|                 # Health pickups
 | |
|                 if pickup == 0x77:
 | |
|                     value = 2
 | |
|                 else:
 | |
|                     value = 10
 | |
|                 exchange_rate = HP_EXCHANGE_RATE
 | |
|             elif pickup in (0x78, 0x79):
 | |
|                 # Weapon Energy
 | |
|                 if pickup == 0x79:
 | |
|                     value = 2
 | |
|                 else:
 | |
|                     value = 10
 | |
|                 exchange_rate = WEAPON_EXCHANGE_RATE
 | |
|             elif pickup == 0x7B:
 | |
|                 # 1-Up
 | |
|                 value = 1
 | |
|                 exchange_rate = ONEUP_EXCHANGE_RATE
 | |
|             else:
 | |
|                 # if we managed to pickup something else, we should just fall through
 | |
|                 value = 0
 | |
|                 exchange_rate = 0
 | |
|             contribution = (value * exchange_rate) >> 1
 | |
|             if contribution:
 | |
|                 await ctx.send_msgs([{
 | |
|                     "cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
 | |
|                         [{"operation": "add", "value": contribution},
 | |
|                          {"operation": "max", "value": 0}]}])
 | |
|             logger.info(f"Deposited {contribution / HP_EXCHANGE_RATE} health into the pool.")
 | |
|             writes.append((MM2_ENERGYLINK, 0x00.to_bytes(1, "little"), "RAM"))
 | |
| 
 | |
|         if self.weapon_energy:
 | |
|             # Weapon Energy
 | |
|             # We parse the whole thing to spread it as thin as possible
 | |
|             current_energy = self.weapon_energy
 | |
|             weapon_energy = bytearray(weapon_energy)
 | |
|             for i, weapon in zip(range(len(weapon_energy)), weapon_energy):
 | |
|                 if weapon < 0x1C:
 | |
|                     missing = 0x1C - weapon
 | |
|                     if missing > self.weapon_energy:
 | |
|                         missing = self.weapon_energy
 | |
|                     self.weapon_energy -= missing
 | |
|                     weapon_energy[i] = weapon + missing
 | |
|                     if not self.weapon_energy:
 | |
|                         writes.append((MM2_WEAPON_ENERGY, weapon_energy, "RAM"))
 | |
|                         break
 | |
|             else:
 | |
|                 if current_energy != self.weapon_energy:
 | |
|                     writes.append((MM2_WEAPON_ENERGY, weapon_energy, "RAM"))
 | |
| 
 | |
|         if self.health_energy or self.auto_heal:
 | |
|             # Health Energy
 | |
|             # We save this if the player has not taken any damage
 | |
|             current_health = health[0]
 | |
|             if 0 < current_health < 0x1C:
 | |
|                 health_diff = 0x1C - current_health
 | |
|                 if self.health_energy:
 | |
|                     if health_diff > self.health_energy:
 | |
|                         health_diff = self.health_energy
 | |
|                     self.health_energy -= health_diff
 | |
|                 else:
 | |
|                     pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
 | |
|                     if health_diff * HP_EXCHANGE_RATE > pool:
 | |
|                         health_diff = int(pool // HP_EXCHANGE_RATE)
 | |
|                     await ctx.send_msgs([{
 | |
|                         "cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
 | |
|                             [{"operation": "add", "value": -health_diff * HP_EXCHANGE_RATE},
 | |
|                              {"operation": "max", "value": 0}]}])
 | |
|                 current_health += health_diff
 | |
|                 writes.append((MM2_HEALTH, current_health.to_bytes(1, 'little'), "RAM"))
 | |
| 
 | |
|         if self.refill_queue:
 | |
|             refill_type, refill_amount = self.refill_queue.pop()
 | |
|             if refill_type == MM2EnergyLinkType.Life:
 | |
|                 exchange_rate = HP_EXCHANGE_RATE
 | |
|             elif refill_type == MM2EnergyLinkType.OneUP:
 | |
|                 exchange_rate = ONEUP_EXCHANGE_RATE
 | |
|             else:
 | |
|                 exchange_rate = WEAPON_EXCHANGE_RATE
 | |
|             pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
 | |
|             request = exchange_rate * refill_amount
 | |
|             if request > pool:
 | |
|                 logger.warning(
 | |
|                     f"Not enough energy to fulfill the request. Maximum request: {pool // exchange_rate}")
 | |
|             else:
 | |
|                 await ctx.send_msgs([{
 | |
|                     "cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
 | |
|                         [{"operation": "add", "value": -request},
 | |
|                          {"operation": "max", "value": 0}]}])
 | |
|                 if refill_type == MM2EnergyLinkType.Life:
 | |
|                     refill_ptr = MM2_HEALTH
 | |
|                 elif refill_type == MM2EnergyLinkType.OneUP:
 | |
|                     refill_ptr = MM2_LIVES
 | |
|                 else:
 | |
|                     refill_ptr = MM2_WEAPON_ENERGY - 1 + refill_type
 | |
|                 current_value = (await read(ctx.bizhawk_ctx, [(refill_ptr, 1, "RAM")]))[0][0]
 | |
|                 new_value = min(0x1C if refill_type != MM2EnergyLinkType.OneUP else 99, current_value + refill_amount)
 | |
|                 writes.append((refill_ptr, new_value.to_bytes(1, "little"), "RAM"))
 | |
| 
 | |
|         if len(self.item_queue):
 | |
|             item = self.item_queue.pop(0)
 | |
|             idx = item.item & 0xF
 | |
|             if idx == 0:
 | |
|                 # 1-Up
 | |
|                 current_lives = lives[0]
 | |
|                 if current_lives > 99:
 | |
|                     self.item_queue.append(item)
 | |
|                 else:
 | |
|                     current_lives += 1
 | |
|                     writes.append((MM2_LIVES, current_lives.to_bytes(1, 'little'), "RAM"))
 | |
|                     writes.extend(get_sfx_writes(0x42))
 | |
|             elif idx == 1:
 | |
|                 self.weapon_energy += 0xE
 | |
|                 writes.extend(get_sfx_writes(0x28))
 | |
|             elif idx == 2:
 | |
|                 self.health_energy += 0xE
 | |
|                 writes.extend(get_sfx_writes(0x28))
 | |
|             elif idx == 3:
 | |
|                 # E-Tank
 | |
|                 # visuals only allow 4, but we're gonna go up to 9 anyway? May change
 | |
|                 current_tanks = e_tanks[0]
 | |
|                 if current_tanks < 9:
 | |
|                     current_tanks += 1
 | |
|                     writes.append((MM2_E_TANKS, current_tanks.to_bytes(1, 'little'), "RAM"))
 | |
|                     writes.extend(get_sfx_writes(0x42))
 | |
|                 else:
 | |
|                     self.item_queue.append(item)
 | |
| 
 | |
|         await write(ctx.bizhawk_ctx, writes)
 | |
| 
 | |
|         new_checks = []
 | |
|         # check for locations
 | |
|         for i in range(8):
 | |
|             flag = 1 << i
 | |
|             if robot_masters_defeated[0] & flag:
 | |
|                 wep_id = 0x880101 + i
 | |
|                 if wep_id not in ctx.checked_locations:
 | |
|                     new_checks.append(wep_id)
 | |
| 
 | |
|         for i in range(3):
 | |
|             flag = 1 << i
 | |
|             if items_acquired[0] & flag:
 | |
|                 itm_id = 0x880111 + i
 | |
|                 if itm_id not in ctx.checked_locations:
 | |
|                     new_checks.append(itm_id)
 | |
| 
 | |
|         for i in range(0xD):
 | |
|             rbm_id = 0x880001 + i
 | |
|             if completed_stages[i] != 0:
 | |
|                 if rbm_id not in ctx.checked_locations:
 | |
|                     new_checks.append(rbm_id)
 | |
| 
 | |
|         for consumable in MM2_CONSUMABLE_TABLE:
 | |
|             if consumable not in ctx.checked_locations:
 | |
|                 is_checked = consumable_checks[MM2_CONSUMABLE_TABLE[consumable][0]] \
 | |
|                              & MM2_CONSUMABLE_TABLE[consumable][1]
 | |
|                 if is_checked:
 | |
|                     new_checks.append(consumable)
 | |
| 
 | |
|         for new_check_id in new_checks:
 | |
|             ctx.locations_checked.add(new_check_id)
 | |
|             location = ctx.location_names.lookup_in_game(new_check_id)
 | |
|             nes_logger.info(
 | |
|                 f'New Check: {location} ({len(ctx.locations_checked)}/'
 | |
|                 f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
 | |
|             await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
 |