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)) |