import json gameStateAddress = 0xDB95 validGameStates = {0x0B, 0x0C} gameStateResetThreshold = 0x06 inventorySlotCount = 16 inventoryStartAddress = 0xDB00 inventoryEndAddress = inventoryStartAddress + inventorySlotCount inventoryItemIds = { 0x02: 'BOMB', 0x05: 'BOW', 0x06: 'HOOKSHOT', 0x07: 'MAGIC_ROD', 0x08: 'PEGASUS_BOOTS', 0x09: 'OCARINA', 0x0A: 'FEATHER', 0x0B: 'SHOVEL', 0x0C: 'MAGIC_POWDER', 0x0D: 'BOOMERANG', 0x0E: 'TOADSTOOL', 0x0F: 'ROOSTER', } dungeonKeyDoors = [ { # D1 0xD907: [0x04], 0xD909: [0x40], 0xD90F: [0x01], }, { # D2 0xD921: [0x02], 0xD925: [0x02], 0xD931: [0x02], 0xD932: [0x08], 0xD935: [0x04], }, { # D3 0xD945: [0x40], 0xD946: [0x40], 0xD949: [0x40], 0xD94A: [0x40], 0xD956: [0x01, 0x02, 0x04, 0x08], }, { # D4 0xD969: [0x04], 0xD96A: [0x40], 0xD96E: [0x40], 0xD978: [0x01], 0xD979: [0x04], }, { # D5 0xD98C: [0x40], 0xD994: [0x40], 0xD99F: [0x04], }, { # D6 0xD9C3: [0x40], 0xD9C6: [0x40], 0xD9D0: [0x04], }, { # D7 0xDA10: [0x04], 0xDA1E: [0x40], 0xDA21: [0x40], }, { # D8 0xDA39: [0x02], 0xDA3B: [0x01], 0xDA42: [0x40], 0xDA43: [0x40], 0xDA44: [0x40], 0xDA49: [0x40], 0xDA4A: [0x01], }, { # D0(9) 0xDDE5: [0x02], 0xDDE9: [0x04], 0xDDF0: [0x04], }, ] dungeonItemAddresses = [ 0xDB16, # D1 0xDB1B, # D2 0xDB20, # D3 0xDB25, # D4 0xDB2A, # D5 0xDB2F, # D6 0xDB34, # D7 0xDB39, # D8 0xDDDA, # Color Dungeon ] dungeonItemOffsets = { 'MAP{}': 0, 'COMPASS{}': 1, 'STONE_BEAK{}': 2, 'NIGHTMARE_KEY{}': 3, 'KEY{}': 4, } class Item: def __init__(self, id, address, threshold=0, mask=None, increaseOnly=False, count=False, max=None): self.id = id self.address = address self.threshold = threshold self.mask = mask self.increaseOnly = increaseOnly self.count = count self.value = 0 if increaseOnly else None self.rawValue = 0 self.diff = 0 self.max = max def set(self, byte, extra): oldValue = self.value if self.mask: byte = byte & self.mask if not self.count: byte = int(byte > self.threshold) else: # LADX seems to store one decimal digit per nibble byte = byte - (byte // 16 * 6) byte += extra if self.max and byte > self.max: byte = self.max if self.increaseOnly: if byte > self.rawValue: self.value += byte - self.rawValue else: self.value = byte self.rawValue = byte if oldValue != self.value: self.diff += self.value - (oldValue or 0) class ItemTracker: def __init__(self, gameboy) -> None: self.gameboy = gameboy self.loadItems() pass extraItems = {} async def readRamByte(self, byte): return (await self.gameboy.read_memory_cache([byte]))[byte] def loadItems(self): self.items = [ Item('BOMB', None), Item('BOW', None), Item('HOOKSHOT', None), Item('MAGIC_ROD', None), Item('PEGASUS_BOOTS', None), Item('OCARINA', None), Item('FEATHER', None), Item('SHOVEL', None), Item('MAGIC_POWDER', None), Item('BOOMERANG', None), Item('TOADSTOOL', None), Item('ROOSTER', None), Item('SWORD', 0xDB4E, count=True), Item('POWER_BRACELET', 0xDB43, count=True), Item('SHIELD', 0xDB44, count=True), Item('BOWWOW', 0xDB56), Item('MAX_POWDER_UPGRADE', 0xDB76, threshold=0x20), Item('MAX_BOMBS_UPGRADE', 0xDB77, threshold=0x30), Item('MAX_ARROWS_UPGRADE', 0xDB78, threshold=0x30), Item('TAIL_KEY', 0xDB11), Item('SLIME_KEY', 0xDB15), Item('ANGLER_KEY', 0xDB12), Item('FACE_KEY', 0xDB13), Item('BIRD_KEY', 0xDB14), Item('FLIPPERS', 0xDB3E), Item('SEASHELL', 0xDB41, count=True), Item('GOLD_LEAF', 0xDB42, count=True, max=5), Item('INSTRUMENT1', 0xDB65, mask=1 << 1), Item('INSTRUMENT2', 0xDB66, mask=1 << 1), Item('INSTRUMENT3', 0xDB67, mask=1 << 1), Item('INSTRUMENT4', 0xDB68, mask=1 << 1), Item('INSTRUMENT5', 0xDB69, mask=1 << 1), Item('INSTRUMENT6', 0xDB6A, mask=1 << 1), Item('INSTRUMENT7', 0xDB6B, mask=1 << 1), Item('INSTRUMENT8', 0xDB6C, mask=1 << 1), Item('TRADING_ITEM_YOSHI_DOLL', 0xDB40, mask=1 << 0), Item('TRADING_ITEM_RIBBON', 0xDB40, mask=1 << 1), Item('TRADING_ITEM_DOG_FOOD', 0xDB40, mask=1 << 2), Item('TRADING_ITEM_BANANAS', 0xDB40, mask=1 << 3), Item('TRADING_ITEM_STICK', 0xDB40, mask=1 << 4), Item('TRADING_ITEM_HONEYCOMB', 0xDB40, mask=1 << 5), Item('TRADING_ITEM_PINEAPPLE', 0xDB40, mask=1 << 6), Item('TRADING_ITEM_HIBISCUS', 0xDB40, mask=1 << 7), Item('TRADING_ITEM_LETTER', 0xDB7F, mask=1 << 0), Item('TRADING_ITEM_BROOM', 0xDB7F, mask=1 << 1), Item('TRADING_ITEM_FISHING_HOOK', 0xDB7F, mask=1 << 2), Item('TRADING_ITEM_NECKLACE', 0xDB7F, mask=1 << 3), Item('TRADING_ITEM_SCALE', 0xDB7F, mask=1 << 4), Item('TRADING_ITEM_MAGNIFYING_GLASS', 0xDB7F, mask=1 << 5), Item('SONG1', 0xDB49, mask=1 << 2), Item('SONG2', 0xDB49, mask=1 << 1), Item('SONG3', 0xDB49, mask=1 << 0), Item('RED_TUNIC', 0xDB6D, mask=1 << 0), Item('BLUE_TUNIC', 0xDB6D, mask=1 << 1), Item('GREAT_FAIRY', 0xDDE1, mask=1 << 4), ] for i in range(len(dungeonItemAddresses)): for item, offset in dungeonItemOffsets.items(): if item.startswith('KEY'): self.items.append(Item(item.format(i + 1), dungeonItemAddresses[i] + offset, count=True)) else: self.items.append(Item(item.format(i + 1), dungeonItemAddresses[i] + offset)) self.itemDict = {item.id: item for item in self.items} async def readItems(state): extraItems = state.extraItems missingItems = {x for x in state.items if x.address == None} # Add keys for opened key doors for i in range(len(dungeonKeyDoors)): item = f'KEY{i + 1}' extraItems[item] = 0 for address, masks in dungeonKeyDoors[i].items(): for mask in masks: value = await state.readRamByte(address) & mask if value > 0: extraItems[item] += 1 # Main inventory items for i in range(inventoryStartAddress, inventoryEndAddress): value = await state.readRamByte(i) if value in inventoryItemIds: item = state.itemDict[inventoryItemIds[value]] extra = extraItems[item.id] if item.id in extraItems else 0 item.set(1, extra) missingItems.remove(item) for item in missingItems: extra = extraItems[item.id] if item.id in extraItems else 0 item.set(0, extra) # All other items for item in [x for x in state.items if x.address]: extra = extraItems[item.id] if item.id in extraItems else 0 item.set(await state.readRamByte(item.address), extra) async def sendItems(self, socket, diff=False): if not self.items: return message = { "type":"item", "refresh": True, "version":"1.0", "diff": diff, "items": [], } items = self.items if diff: items = [item for item in items if item.diff != 0] if not items: return for item in items: value = item.diff if diff else item.value message["items"].append( { 'id': item.id, 'qty': value, } ) item.diff = 0 await socket.send(json.dumps(message))