120 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			120 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | 
 | ||
|  | from NetUtils import ClientStatus, color | ||
|  | from worlds.AutoSNIClient import SNIClient | ||
|  | from .Regions import offset | ||
|  | import logging | ||
|  | 
 | ||
|  | snes_logger = logging.getLogger("SNES") | ||
|  | 
 | ||
|  | ROM_NAME = (0x7FC0, 0x7FD4 + 1 - 0x7FC0) | ||
|  | 
 | ||
|  | READ_DATA_START = 0xF50EA8 | ||
|  | READ_DATA_END = 0xF50FE7 + 1 | ||
|  | 
 | ||
|  | GAME_FLAGS = (0xF50EA8, 64) | ||
|  | COMPLETED_GAME = (0xF50F22, 1) | ||
|  | BATTLEFIELD_DATA = (0xF50FD4, 20) | ||
|  | 
 | ||
|  | RECEIVED_DATA = (0xE01FF0, 3) | ||
|  | 
 | ||
|  | ITEM_CODE_START = 0x420000 | ||
|  | 
 | ||
|  | IN_GAME_FLAG = (4 * 8) + 2 | ||
|  | 
 | ||
|  | NPC_CHECKS = { | ||
|  |     4325676: ((6 * 8) + 4, False),  # Old Man Level Forest | ||
|  |     4325677: ((3 * 8) + 6, True),  # Kaeli Level Forest | ||
|  |     4325678: ((25 * 8) + 1, True),  # Tristam | ||
|  |     4325680: ((26 * 8) + 0, True),  # Aquaria Vendor Girl | ||
|  |     4325681: ((29 * 8) + 2, True),  # Phoebe Wintry Cave | ||
|  |     4325682: ((25 * 8) + 6, False),  # Mysterious Man (Life Temple) | ||
|  |     4325683: ((29 * 8) + 3, True),  # Reuben Mine | ||
|  |     4325684: ((29 * 8) + 7, True),  # Spencer | ||
|  |     4325685: ((29 * 8) + 6, False),  # Venus Chest | ||
|  |     4325686: ((29 * 8) + 1, True),  # Fireburg Tristam | ||
|  |     4325687: ((26 * 8) + 1, True),  # Fireburg Vendor Girl | ||
|  |     4325688: ((14 * 8) + 4, True),  # MegaGrenade Dude | ||
|  |     4325689: ((29 * 8) + 5, False),  # Tristam's Chest | ||
|  |     4325690: ((29 * 8) + 4, True),  # Arion | ||
|  |     4325691: ((29 * 8) + 0, True),  # Windia Kaeli | ||
|  |     4325692: ((26 * 8) + 2, True),  # Windia Vendor Girl | ||
|  | 
 | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_flag(data, flag): | ||
|  |     byte = int(flag / 8) | ||
|  |     bit = int(0x80 / (2 ** (flag % 8))) | ||
|  |     return (data[byte] & bit) > 0 | ||
|  | 
 | ||
|  | 
 | ||
|  | class FFMQClient(SNIClient): | ||
|  |     game = "Final Fantasy Mystic Quest" | ||
|  | 
 | ||
|  |     async def validate_rom(self, ctx): | ||
|  |         from SNIClient import snes_read | ||
|  |         rom_name = await snes_read(ctx, *ROM_NAME) | ||
|  |         if rom_name is None: | ||
|  |             return False | ||
|  |         if rom_name[:2] != b"MQ": | ||
|  |             return False | ||
|  | 
 | ||
|  |         ctx.rom = rom_name | ||
|  |         ctx.game = self.game | ||
|  |         ctx.items_handling = 0b001 | ||
|  |         return True | ||
|  | 
 | ||
|  |     async def game_watcher(self, ctx): | ||
|  |         from SNIClient import snes_buffered_write, snes_flush_writes, snes_read | ||
|  | 
 | ||
|  |         check_1 = await snes_read(ctx, 0xF53749, 1) | ||
|  |         received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1]) | ||
|  |         data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START) | ||
|  |         check_2 = await snes_read(ctx, 0xF53749, 1) | ||
|  |         if check_1 == b'\x00' or check_2 == b'\x00': | ||
|  |             return | ||
|  | 
 | ||
|  |         def get_range(data_range): | ||
|  |             return data[data_range[0] - READ_DATA_START:data_range[0] + data_range[1] - READ_DATA_START] | ||
|  |         completed_game = get_range(COMPLETED_GAME) | ||
|  |         battlefield_data = get_range(BATTLEFIELD_DATA) | ||
|  |         game_flags = get_range(GAME_FLAGS) | ||
|  | 
 | ||
|  |         if game_flags is None: | ||
|  |             return | ||
|  |         if not get_flag(game_flags, IN_GAME_FLAG): | ||
|  |             return | ||
|  | 
 | ||
|  |         if not ctx.finished_game: | ||
|  |             if completed_game[0] & 0x80 and game_flags[30] & 0x18: | ||
|  |                 await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) | ||
|  |                 ctx.finished_game = True | ||
|  | 
 | ||
|  |         old_locations_checked = ctx.locations_checked.copy() | ||
|  | 
 | ||
|  |         for container in range(256): | ||
|  |             if get_flag(game_flags, (0x20 * 8) + container): | ||
|  |                 ctx.locations_checked.add(offset["Chest"] + container) | ||
|  | 
 | ||
|  |         for location, data in NPC_CHECKS.items(): | ||
|  |             if get_flag(game_flags, data[0]) is data[1]: | ||
|  |                 ctx.locations_checked.add(location) | ||
|  | 
 | ||
|  |         for battlefield in range(20): | ||
|  |             if battlefield_data[battlefield] == 0: | ||
|  |                 ctx.locations_checked.add(offset["BattlefieldItem"] + battlefield + 1) | ||
|  | 
 | ||
|  |         if old_locations_checked != ctx.locations_checked: | ||
|  |             await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": ctx.locations_checked}]) | ||
|  | 
 | ||
|  |         if received[0] == 0: | ||
|  |             received_index = int.from_bytes(received[1:], "big") | ||
|  |             if received_index < len(ctx.items_received): | ||
|  |                 item = ctx.items_received[received_index] | ||
|  |                 received_index += 1 | ||
|  |                 code = (item.item - ITEM_CODE_START) + 1 | ||
|  |                 if code > 256: | ||
|  |                     code -= 256 | ||
|  |                 snes_buffered_write(ctx, RECEIVED_DATA[0], bytes([code, *received_index.to_bytes(2, "big")])) | ||
|  |         await snes_flush_writes(ctx) |