Files
Grinch-AP/worlds/ffmq/Client.py
Alchav f54f8622bb Final Fantasy Mystic Quest: Implement new game (#1909)
FFMQR by @wildham0 
Uses an API created by wildham for Map Shuffle, Crest Shuffle and Battlefield Reward Shuffle, using a similar method of obtaining data from an external website to Super Metroid's Varia Preset option.
Generates a .apmq file which the user must bring to the FFMQR website https://www.ffmqrando.net/Archipelago to patch their rom. It is not an actual patch file but contains item placement and options data for the FFMQR website to generate a patched rom with for AP.
Some of the AP options may seem unusual, using Choice instead of Range where it may seem more appropriate, but these are options that are passed to FFMQR and I can only be as flexible as it is.

@wildham0 deserves the bulk of the credit for not only creating FFMQR in the first place but all the ASM work on the rom needed to make this possible, work on FFMQR to allow patching with the .apmq files, and creating the API that meant I did not have to recreate his map shuffle from scratch.
2023-11-26 17:17:59 +01:00

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)