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