|
| 1 | + |
| 2 | +from NetUtils import ClientStatus, color |
| 3 | +from worlds.AutoSNIClient import SNIClient |
| 4 | +from .Regions import offset |
| 5 | +import logging |
| 6 | + |
| 7 | +snes_logger = logging.getLogger("SNES") |
| 8 | + |
| 9 | +ROM_NAME = (0x7FC0, 0x7FD4 + 1 - 0x7FC0) |
| 10 | + |
| 11 | +READ_DATA_START = 0xF50EA8 |
| 12 | +READ_DATA_END = 0xF50FE7 + 1 |
| 13 | + |
| 14 | +GAME_FLAGS = (0xF50EA8, 64) |
| 15 | +COMPLETED_GAME = (0xF50F22, 1) |
| 16 | +BATTLEFIELD_DATA = (0xF50FD4, 20) |
| 17 | + |
| 18 | +RECEIVED_DATA = (0xE01FF0, 3) |
| 19 | + |
| 20 | +ITEM_CODE_START = 0x420000 |
| 21 | + |
| 22 | +IN_GAME_FLAG = (4 * 8) + 2 |
| 23 | + |
| 24 | +NPC_CHECKS = { |
| 25 | + 4325676: ((6 * 8) + 4, False), # Old Man Level Forest |
| 26 | + 4325677: ((3 * 8) + 6, True), # Kaeli Level Forest |
| 27 | + 4325678: ((25 * 8) + 1, True), # Tristam |
| 28 | + 4325680: ((26 * 8) + 0, True), # Aquaria Vendor Girl |
| 29 | + 4325681: ((29 * 8) + 2, True), # Phoebe Wintry Cave |
| 30 | + 4325682: ((25 * 8) + 6, False), # Mysterious Man (Life Temple) |
| 31 | + 4325683: ((29 * 8) + 3, True), # Reuben Mine |
| 32 | + 4325684: ((29 * 8) + 7, True), # Spencer |
| 33 | + 4325685: ((29 * 8) + 6, False), # Venus Chest |
| 34 | + 4325686: ((29 * 8) + 1, True), # Fireburg Tristam |
| 35 | + 4325687: ((26 * 8) + 1, True), # Fireburg Vendor Girl |
| 36 | + 4325688: ((14 * 8) + 4, True), # MegaGrenade Dude |
| 37 | + 4325689: ((29 * 8) + 5, False), # Tristam's Chest |
| 38 | + 4325690: ((29 * 8) + 4, True), # Arion |
| 39 | + 4325691: ((29 * 8) + 0, True), # Windia Kaeli |
| 40 | + 4325692: ((26 * 8) + 2, True), # Windia Vendor Girl |
| 41 | + |
| 42 | +} |
| 43 | + |
| 44 | + |
| 45 | +def get_flag(data, flag): |
| 46 | + byte = int(flag / 8) |
| 47 | + bit = int(0x80 / (2 ** (flag % 8))) |
| 48 | + return (data[byte] & bit) > 0 |
| 49 | + |
| 50 | + |
| 51 | +class FFMQClient(SNIClient): |
| 52 | + game = "Final Fantasy Mystic Quest" |
| 53 | + |
| 54 | + async def validate_rom(self, ctx): |
| 55 | + from SNIClient import snes_read |
| 56 | + rom_name = await snes_read(ctx, *ROM_NAME) |
| 57 | + if rom_name is None: |
| 58 | + return False |
| 59 | + if rom_name[:2] != b"MQ": |
| 60 | + return False |
| 61 | + |
| 62 | + ctx.rom = rom_name |
| 63 | + ctx.game = self.game |
| 64 | + ctx.items_handling = 0b001 |
| 65 | + return True |
| 66 | + |
| 67 | + async def game_watcher(self, ctx): |
| 68 | + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read |
| 69 | + |
| 70 | + check_1 = await snes_read(ctx, 0xF53749, 1) |
| 71 | + received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1]) |
| 72 | + data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START) |
| 73 | + check_2 = await snes_read(ctx, 0xF53749, 1) |
| 74 | + if check_1 == b'\x00' or check_2 == b'\x00': |
| 75 | + return |
| 76 | + |
| 77 | + def get_range(data_range): |
| 78 | + return data[data_range[0] - READ_DATA_START:data_range[0] + data_range[1] - READ_DATA_START] |
| 79 | + completed_game = get_range(COMPLETED_GAME) |
| 80 | + battlefield_data = get_range(BATTLEFIELD_DATA) |
| 81 | + game_flags = get_range(GAME_FLAGS) |
| 82 | + |
| 83 | + if game_flags is None: |
| 84 | + return |
| 85 | + if not get_flag(game_flags, IN_GAME_FLAG): |
| 86 | + return |
| 87 | + |
| 88 | + if not ctx.finished_game: |
| 89 | + if completed_game[0] & 0x80 and game_flags[30] & 0x18: |
| 90 | + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) |
| 91 | + ctx.finished_game = True |
| 92 | + |
| 93 | + old_locations_checked = ctx.locations_checked.copy() |
| 94 | + |
| 95 | + for container in range(256): |
| 96 | + if get_flag(game_flags, (0x20 * 8) + container): |
| 97 | + ctx.locations_checked.add(offset["Chest"] + container) |
| 98 | + |
| 99 | + for location, data in NPC_CHECKS.items(): |
| 100 | + if get_flag(game_flags, data[0]) is data[1]: |
| 101 | + ctx.locations_checked.add(location) |
| 102 | + |
| 103 | + for battlefield in range(20): |
| 104 | + if battlefield_data[battlefield] == 0: |
| 105 | + ctx.locations_checked.add(offset["BattlefieldItem"] + battlefield + 1) |
| 106 | + |
| 107 | + if old_locations_checked != ctx.locations_checked: |
| 108 | + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": ctx.locations_checked}]) |
| 109 | + |
| 110 | + if received[0] == 0: |
| 111 | + received_index = int.from_bytes(received[1:], "big") |
| 112 | + if received_index < len(ctx.items_received): |
| 113 | + item = ctx.items_received[received_index] |
| 114 | + received_index += 1 |
| 115 | + code = (item.item - ITEM_CODE_START) + 1 |
| 116 | + if code > 256: |
| 117 | + code -= 256 |
| 118 | + snes_buffered_write(ctx, RECEIVED_DATA[0], bytes([code, *received_index.to_bytes(2, "big")])) |
| 119 | + await snes_flush_writes(ctx) |
0 commit comments