| 
									
										
											  
											
												Mega Man 2: Implement New Game (#3256)
* 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>
											
										 
											2024-08-19 21:59:29 -05:00
										 |  |  | 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: | 
					
						
							| 
									
										
										
										
											2025-01-11 23:03:31 -08:00
										 |  |  |         from worlds._bizhawk import RequestFailedError, read, get_memory_size | 
					
						
							| 
									
										
											  
											
												Mega Man 2: Implement New Game (#3256)
* 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>
											
										 
											2024-08-19 21:59:29 -05:00
										 |  |  |         from . import MM2World | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         try: | 
					
						
							| 
									
										
										
										
											2025-01-11 23:03:31 -08:00
										 |  |  |             if (await get_memory_size(ctx.bizhawk_ctx, "PRG ROM")) < 0x3FFB0: | 
					
						
							|  |  |  |                 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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
											  
											
												Mega Man 2: Implement New Game (#3256)
* 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>
											
										 
											2024-08-19 21:59:29 -05:00
										 |  |  |             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]}]) |