| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | import logging | 
					
						
							|  |  |  | import struct | 
					
						
							|  |  |  | import time | 
					
						
							|  |  |  | import typing | 
					
						
							|  |  |  | import uuid | 
					
						
							|  |  |  | from struct import unpack, pack | 
					
						
							|  |  |  | from collections import defaultdict | 
					
						
							|  |  |  | import random | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from MultiServer import mark_raw | 
					
						
							|  |  |  | from NetUtils import ClientStatus, color | 
					
						
							|  |  |  | from Utils import async_start | 
					
						
							|  |  |  | from worlds.AutoSNIClient import SNIClient | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  | from .locations import boss_locations | 
					
						
							|  |  |  | from .gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes | 
					
						
							|  |  |  | from .client_addrs import consumable_addrs, star_addrs | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | from typing import TYPE_CHECKING | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if TYPE_CHECKING: | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     from SNIClient import SNIClientCommandProcessor, SNIContext | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | snes_logger = logging.getLogger("SNES") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # FXPAK Pro protocol memory mapping used by SNI | 
					
						
							|  |  |  | ROM_START = 0x000000 | 
					
						
							|  |  |  | SRAM_1_START = 0xE00000 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # KDL3 | 
					
						
							|  |  |  | KDL3_HALKEN = SRAM_1_START + 0x80F0 | 
					
						
							|  |  |  | KDL3_NINTEN = SRAM_1_START + 0x8FF0 | 
					
						
							|  |  |  | KDL3_ROMNAME = SRAM_1_START + 0x8100 | 
					
						
							|  |  |  | KDL3_DEATH_LINK_ADDR = SRAM_1_START + 0x9010 | 
					
						
							|  |  |  | KDL3_GOAL_ADDR = SRAM_1_START + 0x9012 | 
					
						
							|  |  |  | KDL3_CONSUMABLE_FLAG = SRAM_1_START + 0x9018 | 
					
						
							|  |  |  | KDL3_STARS_FLAG = SRAM_1_START + 0x901A | 
					
						
							|  |  |  | KDL3_GIFTING_FLAG = SRAM_1_START + 0x901C | 
					
						
							|  |  |  | KDL3_LEVEL_ADDR = SRAM_1_START + 0x9020 | 
					
						
							|  |  |  | KDL3_IS_DEMO = SRAM_1_START + 0x5AD5 | 
					
						
							|  |  |  | KDL3_GAME_SAVE = SRAM_1_START + 0x3617 | 
					
						
							| 
									
										
										
										
											2024-03-28 09:41:32 -05:00
										 |  |  | KDL3_CURRENT_WORLD = SRAM_1_START + 0x363F | 
					
						
							|  |  |  | KDL3_CURRENT_LEVEL = SRAM_1_START + 0x3641 | 
					
						
							|  |  |  | KDL3_GAME_STATE = SRAM_1_START + 0x36D0 | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | KDL3_LIFE_COUNT = SRAM_1_START + 0x39CF | 
					
						
							|  |  |  | KDL3_KIRBY_HP = SRAM_1_START + 0x39D1 | 
					
						
							|  |  |  | KDL3_BOSS_HP = SRAM_1_START + 0x39D5 | 
					
						
							|  |  |  | KDL3_STAR_COUNT = SRAM_1_START + 0x39D7 | 
					
						
							|  |  |  | KDL3_LIFE_VISUAL = SRAM_1_START + 0x39E3 | 
					
						
							|  |  |  | KDL3_HEART_STARS = SRAM_1_START + 0x53A7 | 
					
						
							|  |  |  | KDL3_WORLD_UNLOCK = SRAM_1_START + 0x53CB | 
					
						
							|  |  |  | KDL3_LEVEL_UNLOCK = SRAM_1_START + 0x53CD | 
					
						
							|  |  |  | KDL3_BOSS_STATUS = SRAM_1_START + 0x53D5 | 
					
						
							|  |  |  | KDL3_INVINCIBILITY_TIMER = SRAM_1_START + 0x54B1 | 
					
						
							|  |  |  | KDL3_MG5_STATUS = SRAM_1_START + 0x5EE4 | 
					
						
							|  |  |  | KDL3_BOSS_BUTCH_STATUS = SRAM_1_START + 0x5EEA | 
					
						
							|  |  |  | KDL3_JUMPING_STATUS = SRAM_1_START + 0x5EF0 | 
					
						
							|  |  |  | KDL3_CURRENT_BGM = SRAM_1_START + 0x733E | 
					
						
							|  |  |  | KDL3_SOUND_FX = SRAM_1_START + 0x7F62 | 
					
						
							|  |  |  | KDL3_ANIMAL_FRIENDS = SRAM_1_START + 0x8000 | 
					
						
							|  |  |  | KDL3_ABILITY_ARRAY = SRAM_1_START + 0x8020 | 
					
						
							|  |  |  | KDL3_RECV_COUNT = SRAM_1_START + 0x8050 | 
					
						
							|  |  |  | KDL3_HEART_STAR_COUNT = SRAM_1_START + 0x8070 | 
					
						
							|  |  |  | KDL3_GOOEY_TRAP = SRAM_1_START + 0x8080 | 
					
						
							|  |  |  | KDL3_SLOWNESS_TRAP = SRAM_1_START + 0x8082 | 
					
						
							|  |  |  | KDL3_ABILITY_TRAP = SRAM_1_START + 0x8084 | 
					
						
							|  |  |  | KDL3_GIFTING_SEND = SRAM_1_START + 0x8086 | 
					
						
							|  |  |  | KDL3_COMPLETED_STAGES = SRAM_1_START + 0x8200 | 
					
						
							|  |  |  | KDL3_CONSUMABLES = SRAM_1_START + 0xA000 | 
					
						
							|  |  |  | KDL3_STARS = SRAM_1_START + 0xB000 | 
					
						
							|  |  |  | KDL3_ITEM_QUEUE = SRAM_1_START + 0xC000 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | deathlink_messages = defaultdict(lambda: " was defeated.", { | 
					
						
							|  |  |  |     0x0200: " was bonked by apples from Whispy Woods.", | 
					
						
							|  |  |  |     0x0201: " was out-maneuvered by Acro.", | 
					
						
							|  |  |  |     0x0202: " was out-numbered by Pon & Con.", | 
					
						
							|  |  |  |     0x0203: " was defeated by Ado's powerful paintings.", | 
					
						
							|  |  |  |     0x0204: " was clobbered by King Dedede.", | 
					
						
							| 
									
										
										
										
											2024-03-28 09:41:32 -05:00
										 |  |  |     0x0205: " lost their battle against Dark Matter.", | 
					
						
							|  |  |  |     0x0300: " couldn't overcome the Boss Butch.", | 
					
						
							|  |  |  |     0x0400: " is bad at jumping.", | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @mark_raw | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  | def cmd_gift(self: "SNIClientCommandProcessor") -> None: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |     """Toggles gifting for the current game.""" | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     handler = self.ctx.client_handler | 
					
						
							|  |  |  |     assert isinstance(handler, KDL3SNIClient) | 
					
						
							|  |  |  |     handler.gifting = not handler.gifting | 
					
						
							|  |  |  |     self.output(f"Gifting set to {handler.gifting}") | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |     async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", { | 
					
						
							|  |  |  |         f"{self.ctx.slot}": | 
					
						
							|  |  |  |             { | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |                 "IsOpen": handler.gifting, | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                 **kdl3_gifting_options | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |     })) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class KDL3SNIClient(SNIClient): | 
					
						
							|  |  |  |     game = "Kirby's Dream Land 3" | 
					
						
							| 
									
										
										
										
											2024-03-08 00:46:14 -06:00
										 |  |  |     patch_suffix = ".apkdl3" | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     levels: typing.Dict[int, typing.List[int]] = {} | 
					
						
							|  |  |  |     consumables: typing.Optional[bool] = None | 
					
						
							|  |  |  |     stars: typing.Optional[bool] = None | 
					
						
							|  |  |  |     item_queue: typing.List[int] = [] | 
					
						
							|  |  |  |     initialize_gifting: bool = False | 
					
						
							|  |  |  |     gifting: bool = False | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |     giftbox_key: str = "" | 
					
						
							|  |  |  |     motherbox_key: str = "" | 
					
						
							|  |  |  |     client_random: random.Random = random.Random() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     async def deathlink_kill_player(self, ctx: "SNIContext") -> None: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read | 
					
						
							|  |  |  |         game_state = await snes_read(ctx, KDL3_GAME_STATE, 1) | 
					
						
							|  |  |  |         if game_state[0] == 0xFF: | 
					
						
							|  |  |  |             return  # despite how funny it is, don't try to kill Kirby in a menu | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         current_stage = await snes_read(ctx, KDL3_CURRENT_LEVEL, 1) | 
					
						
							|  |  |  |         if current_stage[0] == 0x7:  # boss stage | 
					
						
							|  |  |  |             boss_hp = await snes_read(ctx, KDL3_BOSS_HP, 1) | 
					
						
							|  |  |  |             if boss_hp[0] == 0: | 
					
						
							|  |  |  |                 return  # receiving a deathlink after defeating a boss has softlock potential | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         current_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1) | 
					
						
							|  |  |  |         if current_hp[0] == 0: | 
					
						
							|  |  |  |             return  # don't kill Kirby while he's already dead | 
					
						
							|  |  |  |         snes_buffered_write(ctx, KDL3_KIRBY_HP, bytes([0x00])) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         await snes_flush_writes(ctx) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         ctx.death_state = DeathState.dead | 
					
						
							|  |  |  |         ctx.last_death_link = time.time() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     async def validate_rom(self, ctx: "SNIContext") -> bool: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         from SNIClient import snes_read | 
					
						
							|  |  |  |         rom_name = await snes_read(ctx, KDL3_ROMNAME, 0x15) | 
					
						
							|  |  |  |         if rom_name is None or rom_name == bytes([0] * 0x15) or rom_name[:4] != b"KDL3": | 
					
						
							|  |  |  |             if "gift" in ctx.command_processor.commands: | 
					
						
							|  |  |  |                 ctx.command_processor.commands.pop("gift") | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         ctx.game = self.game | 
					
						
							|  |  |  |         ctx.rom = rom_name | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |         ctx.items_handling = 0b101  # default local items with remote start inventory | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         ctx.allow_collect = True | 
					
						
							|  |  |  |         if "gift" not in ctx.command_processor.commands: | 
					
						
							|  |  |  |             ctx.command_processor.commands["gift"] = cmd_gift | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         death_link = await snes_read(ctx, KDL3_DEATH_LINK_ADDR, 1) | 
					
						
							|  |  |  |         if death_link: | 
					
						
							|  |  |  |             await ctx.update_death_link(bool(death_link[0] & 0b1)) | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |             ctx.items_handling |= (death_link[0] & 0b10)  # set local items if enabled | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     async def pop_item(self, ctx: "SNIContext", in_stage: bool) -> None: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         from SNIClient import snes_buffered_write, snes_read | 
					
						
							|  |  |  |         if len(self.item_queue) > 0: | 
					
						
							|  |  |  |             item = self.item_queue.pop() | 
					
						
							|  |  |  |             if not in_stage and item & 0xC0: | 
					
						
							|  |  |  |                 # can't handle this item right now, send it to the back and return to handle the rest | 
					
						
							|  |  |  |                 self.item_queue.append(item) | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  |             ingame_queue = list(unpack("HHHHHHHH", await snes_read(ctx, KDL3_ITEM_QUEUE, 16))) | 
					
						
							|  |  |  |             for i in range(len(ingame_queue)): | 
					
						
							|  |  |  |                 if ingame_queue[i] == 0x00: | 
					
						
							|  |  |  |                     ingame_queue[i] = item | 
					
						
							|  |  |  |                     snes_buffered_write(ctx, KDL3_ITEM_QUEUE, pack("HHHHHHHH", *ingame_queue)) | 
					
						
							|  |  |  |                     break | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.item_queue.append(item)  # no more slots, get it next go around | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     async def pop_gift(self, ctx: "SNIContext") -> None: | 
					
						
							|  |  |  |         if self.giftbox_key in ctx.stored_data and ctx.stored_data[self.giftbox_key]: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |             from SNIClient import snes_read, snes_buffered_write | 
					
						
							|  |  |  |             key, gift = ctx.stored_data[self.giftbox_key].popitem() | 
					
						
							|  |  |  |             await pop_object(ctx, self.giftbox_key, key) | 
					
						
							|  |  |  |             # first, special cases | 
					
						
							|  |  |  |             traits = [trait["Trait"] for trait in gift["Traits"]] | 
					
						
							|  |  |  |             if "Candy" in traits or "Invincible" in traits: | 
					
						
							|  |  |  |                 # apply invincibility candy | 
					
						
							|  |  |  |                 self.item_queue.append(0x43) | 
					
						
							|  |  |  |             elif "Tomato" in traits or "tomato" in gift["ItemName"].lower(): | 
					
						
							|  |  |  |                 # apply maxim tomato | 
					
						
							|  |  |  |                 # only want tomatos here, no other vegetable is that good | 
					
						
							|  |  |  |                 self.item_queue.append(0x42) | 
					
						
							|  |  |  |             elif "Life" in traits: | 
					
						
							|  |  |  |                 # Apply 1-Up | 
					
						
							|  |  |  |                 self.item_queue.append(0x41) | 
					
						
							|  |  |  |             elif "Currency" in traits or "Star" in traits: | 
					
						
							|  |  |  |                 value = gift["ItemValue"] | 
					
						
							|  |  |  |                 if value >= 50000: | 
					
						
							|  |  |  |                     self.item_queue.append(0x46) | 
					
						
							|  |  |  |                 elif value >= 30000: | 
					
						
							|  |  |  |                     self.item_queue.append(0x45) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     self.item_queue.append(0x44) | 
					
						
							|  |  |  |             elif "Trap" in traits: | 
					
						
							|  |  |  |                 # find the best trap to apply | 
					
						
							|  |  |  |                 if "Goo" in traits or "Gel" in traits: | 
					
						
							|  |  |  |                     self.item_queue.append(0x80) | 
					
						
							|  |  |  |                 elif "Slow" in traits or "Slowness" in traits: | 
					
						
							|  |  |  |                     self.item_queue.append(0x81) | 
					
						
							|  |  |  |                 elif "Eject" in traits or "Removal" in traits: | 
					
						
							|  |  |  |                     self.item_queue.append(0x82) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     # just deal damage to Kirby | 
					
						
							|  |  |  |                     kirby_hp = struct.unpack("H", await snes_read(ctx, KDL3_KIRBY_HP, 2))[0] | 
					
						
							|  |  |  |                     snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", max(kirby_hp - 1, 0))) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 # check if it's tasty | 
					
						
							|  |  |  |                 if any(x in traits for x in ["Consumable", "Food", "Drink", "Heal", "Health"]): | 
					
						
							|  |  |  |                     # it's tasty!, use quality to decide how much to heal | 
					
						
							|  |  |  |                     quality = max((trait["Quality"] for trait in gift["Traits"] | 
					
						
							|  |  |  |                                    if trait["Trait"] in ["Consumable", "Food", "Drink", "Heal", "Health"])) | 
					
						
							|  |  |  |                     quality = min(10, quality * 2) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     # it's not really edible, but he'll eat it anyway | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |                     quality = self.client_random.choices(range(0, 2), [75, 25])[0] | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                 kirby_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1) | 
					
						
							|  |  |  |                 gooey_hp = await snes_read(ctx, KDL3_KIRBY_HP + 2, 1) | 
					
						
							|  |  |  |                 snes_buffered_write(ctx, KDL3_SOUND_FX, bytes([0x26])) | 
					
						
							|  |  |  |                 if gooey_hp[0] > 0x00: | 
					
						
							|  |  |  |                     snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality // 2, 8))) | 
					
						
							|  |  |  |                     snes_buffered_write(ctx, KDL3_KIRBY_HP + 2, struct.pack("H", min(gooey_hp[0] + quality // 2, 8))) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality, 10))) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     async def pick_gift_recipient(self, ctx: "SNIContext", gift: int) -> None: | 
					
						
							|  |  |  |         assert ctx.slot | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         if gift != 4: | 
					
						
							|  |  |  |             gift_base = kdl3_gifts[gift] | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             gift_base = kdl3_trap_gifts[self.client_random.randint(0, 3)] | 
					
						
							|  |  |  |         most_applicable = -1 | 
					
						
							|  |  |  |         most_applicable_slot = ctx.slot | 
					
						
							|  |  |  |         for slot, info in ctx.stored_data[self.motherbox_key].items(): | 
					
						
							|  |  |  |             if int(slot) == ctx.slot and len(ctx.stored_data[self.motherbox_key]) > 1: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             desire = len(set(info["DesiredTraits"]).intersection([trait["Trait"] for trait in gift_base["Traits"]])) | 
					
						
							|  |  |  |             if desire > most_applicable: | 
					
						
							|  |  |  |                 most_applicable = desire | 
					
						
							|  |  |  |                 most_applicable_slot = int(slot) | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |             elif most_applicable_slot != ctx.slot and most_applicable == -1 and info["AcceptsAnyGift"]: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                 # only send to ourselves if no one else will take it | 
					
						
							|  |  |  |                 most_applicable_slot = int(slot) | 
					
						
							|  |  |  |         # print(most_applicable, most_applicable_slot) | 
					
						
							|  |  |  |         item_uuid = uuid.uuid4().hex | 
					
						
							|  |  |  |         item = { | 
					
						
							|  |  |  |             **gift_base, | 
					
						
							|  |  |  |             "ID": item_uuid, | 
					
						
							|  |  |  |             "Sender": ctx.player_names[ctx.slot], | 
					
						
							|  |  |  |             "Receiver": ctx.player_names[most_applicable_slot], | 
					
						
							|  |  |  |             "SenderTeam": ctx.team, | 
					
						
							|  |  |  |             "ReceiverTeam": ctx.team,  # for the moment | 
					
						
							|  |  |  |             "IsRefund": False | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         # print(item) | 
					
						
							|  |  |  |         await update_object(ctx, f"Giftbox;{ctx.team};{most_applicable_slot}", { | 
					
						
							|  |  |  |             item_uuid: item, | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     async def game_watcher(self, ctx: "SNIContext") -> None: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         try: | 
					
						
							|  |  |  |             from SNIClient import snes_buffered_write, snes_flush_writes, snes_read | 
					
						
							|  |  |  |             rom = await snes_read(ctx, KDL3_ROMNAME, 0x15) | 
					
						
							|  |  |  |             if rom != ctx.rom: | 
					
						
							|  |  |  |                 ctx.rom = None | 
					
						
							|  |  |  |             halken = await snes_read(ctx, KDL3_HALKEN, 6) | 
					
						
							|  |  |  |             if halken != b"halken": | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  |             ninten = await snes_read(ctx, KDL3_NINTEN, 6) | 
					
						
							|  |  |  |             if ninten != b"ninten": | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  |             if not ctx.slot: | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  |             if not self.initialize_gifting: | 
					
						
							|  |  |  |                 self.giftbox_key = f"Giftbox;{ctx.team};{ctx.slot}" | 
					
						
							|  |  |  |                 self.motherbox_key = f"Giftboxes;{ctx.team}" | 
					
						
							|  |  |  |                 enable_gifting = await snes_read(ctx, KDL3_GIFTING_FLAG, 0x01) | 
					
						
							|  |  |  |                 await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key, bool(enable_gifting[0])) | 
					
						
							|  |  |  |                 self.initialize_gifting = True | 
					
						
							|  |  |  |             # can't check debug anymore, without going and copying the value. might be important later. | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |             if not self.levels: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                 self.levels = dict() | 
					
						
							|  |  |  |                 for i in range(5): | 
					
						
							|  |  |  |                     level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14) | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |                     self.levels[i] = [int.from_bytes(level_data[idx:idx+1], "little") | 
					
						
							|  |  |  |                                       for idx in range(0, len(level_data), 2)] | 
					
						
							| 
									
										
										
										
											2024-03-28 09:41:32 -05:00
										 |  |  |                 self.levels[5] = [0x0205,  # Hyper Zone | 
					
						
							|  |  |  |                                   0,  # MG-5, can't send from here | 
					
						
							|  |  |  |                                   0x0300,  # Boss Butch | 
					
						
							|  |  |  |                                   0x0400,  # Jumping | 
					
						
							|  |  |  |                                   0, 0, 0] | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |             if self.consumables is None: | 
					
						
							|  |  |  |                 consumables = await snes_read(ctx, KDL3_CONSUMABLE_FLAG, 1) | 
					
						
							|  |  |  |                 self.consumables = consumables[0] == 0x01 | 
					
						
							|  |  |  |             if self.stars is None: | 
					
						
							|  |  |  |                 stars = await snes_read(ctx, KDL3_STARS_FLAG, 1) | 
					
						
							|  |  |  |                 self.stars = stars[0] == 0x01 | 
					
						
							|  |  |  |             is_demo = await snes_read(ctx, KDL3_IS_DEMO, 1) | 
					
						
							|  |  |  |             # 1 - recording a demo, 2 - playing back recorded, 3+ is a demo | 
					
						
							|  |  |  |             if is_demo[0] > 0x00: | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  |             current_save = await snes_read(ctx, KDL3_GAME_SAVE, 1) | 
					
						
							|  |  |  |             goal = await snes_read(ctx, KDL3_GOAL_ADDR, 1) | 
					
						
							|  |  |  |             boss_butch_status = await snes_read(ctx, KDL3_BOSS_BUTCH_STATUS + (current_save[0] * 2), 1) | 
					
						
							|  |  |  |             mg5_status = await snes_read(ctx, KDL3_MG5_STATUS + (current_save[0] * 2), 1) | 
					
						
							|  |  |  |             jumping_status = await snes_read(ctx, KDL3_JUMPING_STATUS + (current_save[0] * 2), 1) | 
					
						
							|  |  |  |             if boss_butch_status[0] == 0xFF: | 
					
						
							|  |  |  |                 return  # save file is not created, ignore | 
					
						
							|  |  |  |             if (goal[0] == 0x00 and boss_butch_status[0] == 0x01) \ | 
					
						
							|  |  |  |                     or (goal[0] == 0x01 and boss_butch_status[0] == 0x03) \ | 
					
						
							|  |  |  |                     or (goal[0] == 0x02 and mg5_status[0] == 0x03) \ | 
					
						
							|  |  |  |                     or (goal[0] == 0x03 and jumping_status[0] == 0x03): | 
					
						
							|  |  |  |                 await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) | 
					
						
							|  |  |  |                 ctx.finished_game = True | 
					
						
							|  |  |  |             current_bgm = await snes_read(ctx, KDL3_CURRENT_BGM, 1) | 
					
						
							|  |  |  |             if current_bgm[0] in (0x00, 0x21, 0x22, 0x23, 0x25, 0x2A, 0x2B): | 
					
						
							|  |  |  |                 return  # null, title screen, opening, save select, true and false endings | 
					
						
							|  |  |  |             game_state = await snes_read(ctx, KDL3_GAME_STATE, 1) | 
					
						
							|  |  |  |             if "DeathLink" in ctx.tags and game_state[0] == 0x00 and ctx.last_death_link + 1 < time.time(): | 
					
						
							| 
									
										
										
										
											2024-03-11 17:26:21 -05:00
										 |  |  |                 current_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1) | 
					
						
							|  |  |  |                 current_world = struct.unpack("H", await snes_read(ctx, KDL3_CURRENT_WORLD, 2))[0] | 
					
						
							|  |  |  |                 current_level = struct.unpack("H", await snes_read(ctx, KDL3_CURRENT_LEVEL, 2))[0] | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                 currently_dead = current_hp[0] == 0x00 | 
					
						
							| 
									
										
										
										
											2024-03-28 09:41:32 -05:00
										 |  |  |                 message = deathlink_messages[self.levels[current_world][current_level]] | 
					
						
							| 
									
										
										
										
											2024-03-11 17:26:21 -05:00
										 |  |  |                 await ctx.handle_deathlink_state(currently_dead, f"{ctx.player_names[ctx.slot]}{message}") | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |             recv_count = await snes_read(ctx, KDL3_RECV_COUNT, 2) | 
					
						
							|  |  |  |             recv_amount = unpack("H", recv_count)[0] | 
					
						
							|  |  |  |             if recv_amount < len(ctx.items_received): | 
					
						
							|  |  |  |                 item = ctx.items_received[recv_amount] | 
					
						
							|  |  |  |                 recv_amount += 1 | 
					
						
							|  |  |  |                 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( | 
					
						
							| 
									
										
										
										
											2024-06-16 05:37:05 -05:00
										 |  |  |                     color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'), | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                     color(ctx.player_names[item.player], 'yellow'), | 
					
						
							| 
									
										
										
										
											2024-06-01 06:07:13 -05:00
										 |  |  |                     ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received))) | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 snes_buffered_write(ctx, KDL3_RECV_COUNT, pack("H", recv_amount)) | 
					
						
							|  |  |  |                 item_idx = item.item & 0x00000F | 
					
						
							|  |  |  |                 if item.item & 0x000070 == 0: | 
					
						
							|  |  |  |                     self.item_queue.append(item_idx | 0x10) | 
					
						
							|  |  |  |                 elif item.item & 0x000010 > 0: | 
					
						
							|  |  |  |                     self.item_queue.append(item_idx | 0x20) | 
					
						
							|  |  |  |                 elif item.item & 0x000020 > 0: | 
					
						
							|  |  |  |                     # Positive | 
					
						
							|  |  |  |                     self.item_queue.append(item_idx | 0x40) | 
					
						
							|  |  |  |                 elif item.item & 0x000040 > 0: | 
					
						
							|  |  |  |                     self.item_queue.append(item_idx | 0x80) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # handle gifts here | 
					
						
							|  |  |  |             gifting_status = await snes_read(ctx, KDL3_GIFTING_FLAG, 0x01) | 
					
						
							|  |  |  |             if hasattr(ctx, "gifting") and ctx.gifting: | 
					
						
							|  |  |  |                 if gifting_status[0]: | 
					
						
							|  |  |  |                     gift = await snes_read(ctx, KDL3_GIFTING_SEND, 0x01) | 
					
						
							|  |  |  |                     if gift[0]: | 
					
						
							|  |  |  |                         # we have a gift to send | 
					
						
							|  |  |  |                         await self.pick_gift_recipient(ctx, gift[0]) | 
					
						
							|  |  |  |                         snes_buffered_write(ctx, KDL3_GIFTING_SEND, bytes([0x00])) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     snes_buffered_write(ctx, KDL3_GIFTING_FLAG, bytes([0x01])) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 if gifting_status[0]: | 
					
						
							|  |  |  |                     snes_buffered_write(ctx, KDL3_GIFTING_FLAG, bytes([0x00])) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             await snes_flush_writes(ctx) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             new_checks = [] | 
					
						
							|  |  |  |             # level completion status | 
					
						
							|  |  |  |             world_unlocks = await snes_read(ctx, KDL3_WORLD_UNLOCK, 1) | 
					
						
							|  |  |  |             if world_unlocks[0] > 0x06: | 
					
						
							|  |  |  |                 return  # save is not loaded, ignore | 
					
						
							|  |  |  |             stages_raw = await snes_read(ctx, KDL3_COMPLETED_STAGES, 60) | 
					
						
							|  |  |  |             stages = struct.unpack("HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", stages_raw) | 
					
						
							|  |  |  |             for i in range(30): | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |                 loc_id = 0x770000 + i | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                 if stages[i] == 1 and loc_id not in ctx.checked_locations: | 
					
						
							|  |  |  |                     new_checks.append(loc_id) | 
					
						
							|  |  |  |                 elif loc_id in ctx.checked_locations: | 
					
						
							|  |  |  |                     snes_buffered_write(ctx, KDL3_COMPLETED_STAGES + (i * 2), struct.pack("H", 1)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # heart star status | 
					
						
							|  |  |  |             heart_stars = await snes_read(ctx, KDL3_HEART_STARS, 35) | 
					
						
							|  |  |  |             for i in range(5): | 
					
						
							|  |  |  |                 start_ind = i * 7 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |                 for j in range(6): | 
					
						
							|  |  |  |                     level_ind = start_ind + j | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                     loc_id = 0x770100 + (6 * i) + j | 
					
						
							|  |  |  |                     if heart_stars[level_ind] and loc_id not in ctx.checked_locations: | 
					
						
							|  |  |  |                         new_checks.append(loc_id) | 
					
						
							|  |  |  |                     elif loc_id in ctx.checked_locations: | 
					
						
							|  |  |  |                         snes_buffered_write(ctx, KDL3_HEART_STARS + level_ind, bytes([0x01])) | 
					
						
							|  |  |  |             if self.consumables: | 
					
						
							|  |  |  |                 consumables = await snes_read(ctx, KDL3_CONSUMABLES, 1920) | 
					
						
							|  |  |  |                 for consumable in consumable_addrs: | 
					
						
							|  |  |  |                     # TODO: see if this can be sped up in any way | 
					
						
							|  |  |  |                     loc_id = 0x770300 + consumable | 
					
						
							|  |  |  |                     if loc_id not in ctx.checked_locations and consumables[consumable_addrs[consumable]] == 0x01: | 
					
						
							|  |  |  |                         new_checks.append(loc_id) | 
					
						
							|  |  |  |             if self.stars: | 
					
						
							|  |  |  |                 stars = await snes_read(ctx, KDL3_STARS, 1920) | 
					
						
							|  |  |  |                 for star in star_addrs: | 
					
						
							|  |  |  |                     if star not in ctx.checked_locations and stars[star_addrs[star]] == 0x01: | 
					
						
							|  |  |  |                         new_checks.append(star) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |             if not game_state: | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |             if game_state[0] != 0xFF: | 
					
						
							|  |  |  |                 await self.pop_gift(ctx) | 
					
						
							|  |  |  |             await self.pop_item(ctx, game_state[0] != 0xFF) | 
					
						
							|  |  |  |             await snes_flush_writes(ctx) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # boss status | 
					
						
							|  |  |  |             boss_flag_bytes = await snes_read(ctx, KDL3_BOSS_STATUS, 2) | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |             boss_flag = int.from_bytes(boss_flag_bytes, "little") | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |             for bitmask, boss in zip(range(1, 11, 2), boss_locations.keys()): | 
					
						
							|  |  |  |                 if boss_flag & (1 << bitmask) > 0 and boss not in ctx.checked_locations: | 
					
						
							|  |  |  |                     new_checks.append(boss) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             for new_check_id in new_checks: | 
					
						
							|  |  |  |                 ctx.locations_checked.add(new_check_id) | 
					
						
							| 
									
										
										
										
											2024-06-16 05:37:05 -05:00
										 |  |  |                 location = ctx.location_names.lookup_in_game(new_check_id) | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                 snes_logger.info( | 
					
						
							| 
									
										
										
										
											2024-03-08 00:46:14 -06:00
										 |  |  |                     f'New Check: {location} ({len(ctx.locations_checked)}/' | 
					
						
							|  |  |  |                     f'{len(ctx.missing_locations) + len(ctx.checked_locations)})') | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                 await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) | 
					
						
							|  |  |  |         except Exception as ex: | 
					
						
							|  |  |  |             # we crashed, so print log and clean up | 
					
						
							|  |  |  |             snes_logger.error("", exc_info=ex) | 
					
						
							|  |  |  |             if "gift" in ctx.command_processor.commands: | 
					
						
							|  |  |  |                 ctx.command_processor.commands.pop("gift") | 
					
						
							|  |  |  |             ctx.rom = None | 
					
						
							|  |  |  |             ctx.game = None |