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