Multiple: Followed a rabbit hole of moving LttP Rom generation to AutoWorld

Generator: Re-allow names with spaces (and see what breaks)
Generator: Removed teams (Note that teams are intended to move from a generation step feature to a server runtime feature, allowing dynamic creation of an already generated MW)
LttP: All Rom Options are now on the new system
LttP: palette option "random" is now called "good"
LttP: Roms are now created as part of the general output file creation step
LttP: disable Music is now Music, removing potential double negatives
LttP & Factorio: Progressive option random is now grouped_random
LttP: Enemy damage option random is now Enemy damage: chaos
This commit is contained in:
Fabian Dill
2021-08-09 09:15:41 +02:00
parent 01d88c362a
commit ba3bb201cd
23 changed files with 328 additions and 279 deletions

107
Main.py
View File

@@ -10,17 +10,15 @@ import tempfile
import zipfile
from typing import Dict, Tuple
from BaseClasses import MultiWorld, CollectionState, Region, Item
from BaseClasses import MultiWorld, CollectionState, Region
from worlds.alttp.Items import item_name_groups
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
from worlds.alttp.Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
from worlds.alttp.Shops import ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from worlds.alttp.ItemPool import difficulties, fill_prizes
from Utils import output_path, parse_player_names, get_options, __version__, version_tuple
from worlds.alttp.ItemPool import difficulties
from Utils import output_path, get_options, __version__, version_tuple
from worlds.generic.Rules import locality_rules, exclusion_rules
from worlds import AutoWorld
import Patch
seeddigits = 20
@@ -66,7 +64,6 @@ def main(args, seed=None):
world.difficulty = args.difficulty.copy()
world.item_functionality = args.item_functionality.copy()
world.timer = args.timer.copy()
world.progressive = args.progressive.copy()
world.goal = args.goal.copy()
world.local_items = args.local_items.copy()
if hasattr(args, "algorithm"): # current GUI options
@@ -99,7 +96,6 @@ def main(args, seed=None):
world.blue_clock_time = args.blue_clock_time.copy()
world.green_clock_time = args.green_clock_time.copy()
world.shufflepots = args.shufflepots.copy()
world.progressive = args.progressive.copy()
world.dungeon_counters = args.dungeon_counters.copy()
world.glitch_boots = args.glitch_boots.copy()
world.triforce_pieces_available = args.triforce_pieces_available.copy()
@@ -117,6 +113,10 @@ def main(args, seed=None):
world.required_medallions = args.required_medallions.copy()
world.game = args.game.copy()
world.set_options(args)
world.player_name = args.name.copy()
world.alttp_rom = args.rom
world.enemizer = args.enemizercli
world.sprite = args.sprite.copy()
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
world.slot_seeds = {player: random.Random(world.random.getrandbits(64)) for player in
@@ -148,13 +148,6 @@ def main(args, seed=None):
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | {len(cls.location_names):3} Locations")
parsed_names = parse_player_names(args.names, world.players, args.teams)
world.teams = len(parsed_names)
for i, team in enumerate(parsed_names, 1):
if world.players > 1:
logger.info('%s%s', 'Team%d: ' % i if world.teams > 1 else 'Players: ', ', '.join(team))
for player, name in enumerate(team, 1):
world.player_names[player].append(name)
logger.info('')
for player in world.get_game_players("A Link to the Past"):
@@ -241,63 +234,18 @@ def main(args, seed=None):
logger.info('Generating output files.')
outfilebase = 'AP_' + world.seed_name
rom_names = []
def _gen_rom(team: int, player: int, output_directory:str):
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.shufflepots[player] or world.bush_shuffle[player]
or world.killable_thieves[player])
rom = LocalRom(args.rom)
patch_rom(world, rom, player, team, use_enemizer)
if use_enemizer:
patch_enemizer(world, team, player, rom, args.enemizercli, output_directory)
if args.race:
patch_race_rom(rom, world, player)
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
palettes_options = {
'dungeon': args.uw_palettes[player],
'overworld': args.ow_palettes[player],
'hud': args.hud_palettes[player],
'sword': args.sword_palettes[player],
'shield': args.shield_palettes[player],
'link': args.link_palettes[player]
}
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
palettes_options, world, player, True,
reduceflashing=args.reduceflashing[player] or args.race,
triforcehud=args.triforcehud[player])
outfilepname = f'_P{player}'
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" \
if world.player_names[player][team] != 'Player%d' % player else ''
rompath = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc')
rom.write_to_file(rompath, hide_enemizer=True)
Patch.create_patch_file(rompath, player=player, player_name=world.player_names[player][team])
os.unlink(rompath)
return player, team, bytes(rom.name)
pool = concurrent.futures.ThreadPoolExecutor()
output = tempfile.TemporaryDirectory()
with output as temp_dir:
check_accessibility_task = pool.submit(world.fulfills_accessibility)
rom_futures = []
output_file_futures = []
for team in range(world.teams):
for player in world.get_game_players("A Link to the Past"):
rom_futures.append(pool.submit(_gen_rom, team, player, temp_dir))
for player in world.player_ids:
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
output_file_futures.append(pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir))
def get_entrance_to_region(region: Region):
for entrance in region.entrances:
@@ -365,12 +313,8 @@ def main(args, seed=None):
FillDisabledShopSlots(world)
def write_multidata(roms, outputs):
import base64
def write_multidata():
import NetUtils
for future in roms:
rom_name = future.result()
rom_names.append(rom_name)
slot_data = {}
client_versions = {}
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
@@ -378,8 +322,6 @@ def main(args, seed=None):
for slot in world.player_ids:
client_versions[slot] = world.worlds[slot].get_required_client_version()
games[slot] = world.game[slot]
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
slot, team, rom_name in rom_names}
precollected_items = {player: [] for player in range(1, world.players + 1)}
for item in world.precollected_items:
precollected_items[item.player].append(item.code)
@@ -390,11 +332,6 @@ def main(args, seed=None):
if world.tech_tree_information[player].value == 2:
sending_visible_players.add(player)
for i, team in enumerate(parsed_names):
for player, name in enumerate(team, 1):
if player not in world.get_game_players("A Link to the Past"):
connect_names[name] = (i, player)
for slot in world.player_ids:
slot_data[slot] = world.worlds[slot].fill_slot_data()
@@ -414,11 +351,11 @@ def main(args, seed=None):
precollected_hints[location.player].add(hint)
precollected_hints[location.item.player].add(hint)
multidata = zlib.compress(pickle.dumps({
multidata = {
"slot_data": slot_data,
"games": games,
"names": parsed_names,
"connect_names": connect_names,
"names": [{player: name for player, name in world.player_name.items()}],
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
"remote_items": {player for player in world.player_ids if
world.worlds[player].remote_items},
"locations": locations_data,
@@ -431,15 +368,17 @@ def main(args, seed=None):
"tags": ["AP"],
"minimum_versions": minimum_versions,
"seed_name": world.seed_name
}), 9)
}
AutoWorld.call_all(world, "modify_multidata", multidata)
with open(os.path.join(temp_dir, '%s.archipelago' % outfilebase), 'wb') as f:
multidata = zlib.compress(pickle.dumps(multidata), 9)
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
f.write(bytes([1])) # version of format
f.write(multidata)
for future in outputs:
future.result() # collect errors if they occured
multidata_task = pool.submit(write_multidata, rom_futures, output_file_futures)
multidata_task = pool.submit(write_multidata)
if not check_accessibility_task.result():
if not world.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.")
@@ -451,8 +390,10 @@ def main(args, seed=None):
if not args.skip_playthrough:
logger.info('Calculating playthrough.')
create_playthrough(world)
if args.create_spoiler: # needs spoiler.hashes to be filled, that depend on rom_futures being done
if args.create_spoiler:
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
for future in output_file_futures:
future.result()
zipfilename = output_path(f"AP_{world.seed_name}.zip")
logger.info(f'Creating final archive at {zipfilename}.')
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,