 81a239325d
			
		
	
	81a239325d
	
	
	
		
			
			Adds Link's Awakening: DX. Fully imports and forks LADXR, with permission - https://github.com/daid/LADXR
		
			
				
	
	
		
			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)) |