mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
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.
218 lines
10 KiB
Python
218 lines
10 KiB
Python
import Utils
|
|
import settings
|
|
import base64
|
|
import threading
|
|
import requests
|
|
import yaml
|
|
from worlds.AutoWorld import World, WebWorld
|
|
from BaseClasses import Tutorial
|
|
from .Regions import create_regions, location_table, set_rules, stage_set_rules, rooms, non_dead_end_crest_rooms,\
|
|
non_dead_end_crest_warps
|
|
from .Items import item_table, item_groups, create_items, FFMQItem, fillers
|
|
from .Output import generate_output
|
|
from .Options import option_definitions
|
|
from .Client import FFMQClient
|
|
|
|
|
|
# removed until lists are supported
|
|
# class FFMQSettings(settings.Group):
|
|
# class APIUrls(list):
|
|
# """A list of API URLs to get map shuffle, crest shuffle, and battlefield reward shuffle data from."""
|
|
# api_urls: APIUrls = [
|
|
# "https://api.ffmqrando.net/",
|
|
# "http://ffmqr.jalchavware.com:5271/"
|
|
# ]
|
|
|
|
|
|
class FFMQWebWorld(WebWorld):
|
|
tutorials = [Tutorial(
|
|
"Multiworld Setup Guide",
|
|
"A guide to playing Final Fantasy Mystic Quest with Archipelago.",
|
|
"English",
|
|
"setup_en.md",
|
|
"setup/en",
|
|
["Alchav"]
|
|
)]
|
|
|
|
|
|
class FFMQWorld(World):
|
|
"""Final Fantasy: Mystic Quest is a simple, humorous RPG for the Super Nintendo. You travel across four continents,
|
|
linked in the middle of the world by the Focus Tower, which has been locked by four magical coins. Make your way to
|
|
the bottom of the Focus Tower, then straight up through the top!"""
|
|
# -Giga Otomia
|
|
|
|
game = "Final Fantasy Mystic Quest"
|
|
|
|
item_name_to_id = {name: data.id for name, data in item_table.items() if data.id is not None}
|
|
location_name_to_id = location_table
|
|
option_definitions = option_definitions
|
|
|
|
topology_present = True
|
|
|
|
item_name_groups = item_groups
|
|
|
|
generate_output = generate_output
|
|
create_items = create_items
|
|
create_regions = create_regions
|
|
set_rules = set_rules
|
|
stage_set_rules = stage_set_rules
|
|
|
|
data_version = 1
|
|
|
|
web = FFMQWebWorld()
|
|
# settings: FFMQSettings
|
|
|
|
def __init__(self, world, player: int):
|
|
self.rom_name_available_event = threading.Event()
|
|
self.rom_name = None
|
|
self.rooms = None
|
|
super().__init__(world, player)
|
|
|
|
def generate_early(self):
|
|
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
|
self.multiworld.brown_boxes[self.player].value = 1
|
|
if self.multiworld.enemies_scaling_lower[self.player].value > \
|
|
self.multiworld.enemies_scaling_upper[self.player].value:
|
|
(self.multiworld.enemies_scaling_lower[self.player].value,
|
|
self.multiworld.enemies_scaling_upper[self.player].value) =\
|
|
(self.multiworld.enemies_scaling_upper[self.player].value,
|
|
self.multiworld.enemies_scaling_lower[self.player].value)
|
|
if self.multiworld.bosses_scaling_lower[self.player].value > \
|
|
self.multiworld.bosses_scaling_upper[self.player].value:
|
|
(self.multiworld.bosses_scaling_lower[self.player].value,
|
|
self.multiworld.bosses_scaling_upper[self.player].value) =\
|
|
(self.multiworld.bosses_scaling_upper[self.player].value,
|
|
self.multiworld.bosses_scaling_lower[self.player].value)
|
|
|
|
@classmethod
|
|
def stage_generate_early(cls, multiworld):
|
|
|
|
# api_urls = Utils.get_options()["ffmq_options"].get("api_urls", None)
|
|
api_urls = [
|
|
"https://api.ffmqrando.net/",
|
|
"http://ffmqr.jalchavware.com:5271/"
|
|
]
|
|
|
|
rooms_data = {}
|
|
|
|
for world in multiworld.get_game_worlds("Final Fantasy Mystic Quest"):
|
|
if (world.multiworld.map_shuffle[world.player] or world.multiworld.crest_shuffle[world.player] or
|
|
world.multiworld.crest_shuffle[world.player]):
|
|
if world.multiworld.map_shuffle_seed[world.player].value.isdigit():
|
|
multiworld.random.seed(int(world.multiworld.map_shuffle_seed[world.player].value))
|
|
elif world.multiworld.map_shuffle_seed[world.player].value != "random":
|
|
multiworld.random.seed(int(hash(world.multiworld.map_shuffle_seed[world.player].value))
|
|
+ int(world.multiworld.seed))
|
|
|
|
seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper()
|
|
map_shuffle = multiworld.map_shuffle[world.player].value
|
|
crest_shuffle = multiworld.crest_shuffle[world.player].current_key
|
|
battlefield_shuffle = multiworld.shuffle_battlefield_rewards[world.player].current_key
|
|
|
|
query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}"
|
|
|
|
if query in rooms_data:
|
|
world.rooms = rooms_data[query]
|
|
continue
|
|
|
|
if not api_urls:
|
|
raise Exception("No FFMQR API URLs specified in host.yaml")
|
|
|
|
errors = []
|
|
for api_url in api_urls.copy():
|
|
try:
|
|
response = requests.get(f"{api_url}GenerateRooms?{query}")
|
|
except (ConnectionError, requests.exceptions.HTTPError, requests.exceptions.ConnectionError,
|
|
requests.exceptions.RequestException) as err:
|
|
api_urls.remove(api_url)
|
|
errors.append([api_url, err])
|
|
else:
|
|
if response.ok:
|
|
world.rooms = rooms_data[query] = yaml.load(response.text, yaml.Loader)
|
|
break
|
|
else:
|
|
api_urls.remove(api_url)
|
|
errors.append([api_url, response])
|
|
else:
|
|
error_text = f"Failed to fetch map shuffle data for FFMQ player {world.player}"
|
|
for error in errors:
|
|
error_text += f"\n{error[0]} - got error {error[1].status_code} {error[1].reason} {error[1].text}"
|
|
raise Exception(error_text)
|
|
api_urls.append(api_urls.pop(0))
|
|
else:
|
|
world.rooms = rooms
|
|
|
|
def create_item(self, name: str):
|
|
return FFMQItem(name, self.player)
|
|
|
|
def collect_item(self, state, item, remove=False):
|
|
if "Progressive" in item.name:
|
|
i = item.code - 256
|
|
if state.has(self.item_id_to_name[i], self.player):
|
|
if state.has(self.item_id_to_name[i+1], self.player):
|
|
return self.item_id_to_name[i+2]
|
|
return self.item_id_to_name[i+1]
|
|
return self.item_id_to_name[i]
|
|
return item.name if item.advancement else None
|
|
|
|
def modify_multidata(self, multidata):
|
|
# wait for self.rom_name to be available.
|
|
self.rom_name_available_event.wait()
|
|
rom_name = getattr(self, "rom_name", None)
|
|
# we skip in case of error, so that the original error in the output thread is the one that gets raised
|
|
if rom_name:
|
|
new_name = base64.b64encode(bytes(self.rom_name)).decode()
|
|
payload = multidata["connect_names"][self.multiworld.player_name[self.player]]
|
|
multidata["connect_names"][new_name] = payload
|
|
|
|
def get_filler_item_name(self):
|
|
r = self.multiworld.random.randint(0, 201)
|
|
for item, count in fillers.items():
|
|
r -= count
|
|
r -= fillers[item]
|
|
if r <= 0:
|
|
return item
|
|
|
|
def extend_hint_information(self, hint_data):
|
|
hint_data[self.player] = {}
|
|
if self.multiworld.map_shuffle[self.player]:
|
|
single_location_regions = ["Subregion Volcano Battlefield", "Subregion Mac's Ship", "Subregion Doom Castle"]
|
|
for subregion in ["Subregion Foresta", "Subregion Aquaria", "Subregion Frozen Fields", "Subregion Fireburg",
|
|
"Subregion Volcano Battlefield", "Subregion Windia", "Subregion Mac's Ship",
|
|
"Subregion Doom Castle"]:
|
|
region = self.multiworld.get_region(subregion, self.player)
|
|
for location in region.locations:
|
|
if location.address and self.multiworld.map_shuffle[self.player] != "dungeons":
|
|
hint_data[self.player][location.address] = (subregion.split("Subregion ")[-1]
|
|
+ (" Region" if subregion not in
|
|
single_location_regions else ""))
|
|
for overworld_spot in region.exits:
|
|
if ("Subregion" in overworld_spot.connected_region.name or
|
|
overworld_spot.name == "Overworld - Mac Ship Doom" or "Focus Tower" in overworld_spot.name
|
|
or "Doom Castle" in overworld_spot.name or overworld_spot.name == "Overworld - Giant Tree"):
|
|
continue
|
|
exits = list(overworld_spot.connected_region.exits) + [overworld_spot]
|
|
checked_regions = set()
|
|
while exits:
|
|
exit_check = exits.pop()
|
|
if (exit_check.connected_region not in checked_regions and "Subregion" not in
|
|
exit_check.connected_region.name):
|
|
checked_regions.add(exit_check.connected_region)
|
|
exits.extend(exit_check.connected_region.exits)
|
|
for location in exit_check.connected_region.locations:
|
|
if location.address:
|
|
hint = []
|
|
if self.multiworld.map_shuffle[self.player] != "dungeons":
|
|
hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not
|
|
in single_location_regions else "")))
|
|
if self.multiworld.map_shuffle[self.player] != "overworld" and subregion not in \
|
|
("Subregion Mac's Ship", "Subregion Doom Castle"):
|
|
hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu",
|
|
"Pazuzu's"))
|
|
hint = " - ".join(hint)
|
|
if location.address in hint_data[self.player]:
|
|
hint_data[self.player][location.address] += f"/{hint}"
|
|
else:
|
|
hint_data[self.player][location.address] = hint
|
|
|