283 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			283 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
								 | 
							
								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))
							 |