LttP: move game specific fill to new AutoWorld fill_hook

This commit is contained in:
Fabian Dill 2021-08-10 09:03:44 +02:00
parent 299036ecca
commit 9ec0680ce5
6 changed files with 111 additions and 100 deletions

54
Fill.py
View File

@ -7,6 +7,7 @@ from BaseClasses import CollectionState, Location, MultiWorld
from worlds.alttp.Items import ItemFactory from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import key_drop_data from worlds.alttp.Regions import key_drop_data
from worlds.generic import PlandoItem from worlds.generic import PlandoItem
from worlds.AutoWorld import call_all
class FillError(RuntimeError): class FillError(RuntimeError):
@ -69,7 +70,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
itempool.extend(unplaced_items) itempool.extend(unplaced_items)
def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_locations=None): def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
# If not passed in, then get a shuffled list of locations to fill in # If not passed in, then get a shuffled list of locations to fill in
if not fill_locations: if not fill_locations:
fill_locations = world.get_unfilled_locations() fill_locations = world.get_unfilled_locations()
@ -92,51 +93,9 @@ def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_lo
else: else:
restitempool.append(item) restitempool.append(item)
standard_keyshuffle_players = set()
# fill in gtower locations with trash first
for player in world.get_game_players("A Link to the Past"):
if not gftower_trash or not world.ganonstower_vanilla[player] or \
world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
gtower_trash_count = 0
elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1):
gtower_trash_count = world.random.randint(world.crystals_needed_for_gt[player] * 2,
world.crystals_needed_for_gt[player] * 4)
else:
gtower_trash_count = world.random.randint(0, world.crystals_needed_for_gt[player] * 2)
if gtower_trash_count:
gtower_locations = [location for location in fill_locations if
'Ganons Tower' in location.name and location.player == player]
world.random.shuffle(gtower_locations)
trashcnt = 0
localrest = localrestitempool[player]
if localrest:
gt_item_pool = restitempool + localrest
world.random.shuffle(gt_item_pool)
else:
gt_item_pool = restitempool.copy()
while gtower_locations and gt_item_pool and trashcnt < gtower_trash_count:
spot_to_fill = gtower_locations.pop()
item_to_place = gt_item_pool.pop()
if item_to_place in localrest:
localrest.remove(item_to_place)
else:
restitempool.remove(item_to_place)
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill)
trashcnt += 1
if world.mode[player] == 'standard' and world.keyshuffle[player] is True:
standard_keyshuffle_players.add(player)
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
if standard_keyshuffle_players:
progitempool.sort(
key=lambda item: 1 if item.name == 'Small Key (Hyrule Castle)' and
item.player in standard_keyshuffle_players else 0)
world.random.shuffle(fill_locations) world.random.shuffle(fill_locations)
call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, restitempool, fill_locations)
fill_restrictive(world, world.state, fill_locations, progitempool) fill_restrictive(world, world.state, fill_locations, progitempool)
if nonexcludeditempool: if nonexcludeditempool:
@ -167,11 +126,8 @@ def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_lo
unplaced = [item for item in progitempool + restitempool] unplaced = [item for item in progitempool + restitempool]
unfilled = [location.name for location in fill_locations] unfilled = [location.name for location in fill_locations]
for location in fill_locations:
world.push_item(location, ItemFactory('Nothing', location.player), False)
if unplaced or unfilled: if unplaced or unfilled:
logging.warning(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}') raise FillError(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
def fast_fill(world: MultiWorld, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]: def fast_fill(world: MultiWorld, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]:

View File

@ -218,12 +218,8 @@ def main(args, seed=None):
if world.algorithm == 'flood': if world.algorithm == 'flood':
flood_items(world) # different algo, biased towards early game progress items flood_items(world) # different algo, biased towards early game progress items
elif world.algorithm == 'vt25':
distribute_items_restrictive(world, False)
elif world.algorithm == 'vt26':
distribute_items_restrictive(world, True)
elif world.algorithm == 'balanced': elif world.algorithm == 'balanced':
distribute_items_restrictive(world, True) distribute_items_restrictive(world)
logger.info("Filling Shop Slots") logger.info("Filling Shop Slots")

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Dict, Set, Tuple from typing import Dict, Set, Tuple, List
from BaseClasses import MultiWorld, Item, CollectionState from BaseClasses import MultiWorld, Item, CollectionState, Location
class AutoWorldRegister(type): class AutoWorldRegister(type):
@ -126,6 +126,12 @@ class World(metaclass=AutoWorldRegister):
"""Optional method that is supposed to be used for special fill stages. This is run *after* plando.""" """Optional method that is supposed to be used for special fill stages. This is run *after* plando."""
pass pass
def fill_hook(cls, progitempool: List[Item], nonexcludeditempool: List[Item],
localrestitempool: Dict[int, List[Item]], restitempool: List[Item], fill_locations: List[Location]):
"""Special method that gets called as part of distribute_items_restrictive (main fill).
This gets called once per present world type."""
pass
def generate_output(self, output_directory: str): def generate_output(self, output_directory: str):
"""This method gets called from a threadpool, do not use world.random here. """This method gets called from a threadpool, do not use world.random here.
If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead.""" If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead."""

View File

@ -143,6 +143,7 @@ class HeartColor(Choice):
# remove when this becomes a base Choice feature # remove when this becomes a base Choice feature
if text == "random": if text == "random":
return cls(random.randint(0, 3)) return cls(random.randint(0, 3))
return super(HeartColor, cls).from_text(text)
class QuickSwap(DefaultOnToggle): class QuickSwap(DefaultOnToggle):
displayname = "L/R Quickswapping" displayname = "L/R Quickswapping"

View File

@ -184,63 +184,69 @@ class ALTTPWorld(World):
def generate_output(self, output_directory: str): def generate_output(self, output_directory: str):
world = self.world world = self.world
player = self.player player = self.player
try:
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])
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] rom = LocalRom(world.alttp_rom)
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(world.alttp_rom) patch_rom(world, rom, player, use_enemizer)
patch_rom(world, rom, player, use_enemizer) if use_enemizer:
patch_enemizer(world, player, rom, world.enemizer, output_directory)
if use_enemizer: if world.is_race:
patch_enemizer(world, player, rom, world.enemizer, output_directory) patch_race_rom(rom, world, player)
if world.is_race: world.spoiler.hashes[player] = get_hash_string(rom.hash)
patch_race_rom(rom, world, player)
world.spoiler.hashes[player] = get_hash_string(rom.hash) palettes_options = {
'dungeon': world.uw_palettes[player],
'overworld': world.ow_palettes[player],
'hud': world.hud_palettes[player],
'sword': world.sword_palettes[player],
'shield': world.shield_palettes[player],
'link': world.link_palettes[player]
}
palettes_options = {key: option.current_key for key, option in palettes_options.items()}
palettes_options = { apply_rom_settings(rom, world.heartbeep[player].current_key,
'dungeon': world.uw_palettes[player], world.heartcolor[player].current_key,
'overworld': world.ow_palettes[player], world.quickswap[player],
'hud': world.hud_palettes[player], world.menuspeed[player].current_key,
'sword': world.sword_palettes[player], world.music[player],
'shield': world.shield_palettes[player], world.sprite[player],
'link': world.link_palettes[player] palettes_options, world, player, True,
} reduceflashing=world.reduceflashing[player] or world.is_race,
palettes_options = {key: option.current_key for key, option in palettes_options.items()} triforcehud=world.triforcehud[player].current_key)
apply_rom_settings(rom, world.heartbeep[player].current_key, outfilepname = f'_P{player}'
world.heartcolor[player].current_key, outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \
world.quickswap[player], if world.player_name[player] != 'Player%d' % player else ''
world.menuspeed[player].current_key,
world.music[player],
world.sprite[player],
palettes_options, world, player, True,
reduceflashing=world.reduceflashing[player] or world.is_race,
triforcehud=world.triforcehud[player].current_key)
outfilepname = f'_P{player}' rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc')
outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \ rom.write_to_file(rompath, hide_enemizer=True)
if world.player_name[player] != 'Player%d' % player else '' Patch.create_patch_file(rompath, player=player, player_name=world.player_name[player])
os.unlink(rompath)
rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc') self.rom_name = rom.name
rom.write_to_file(rompath, hide_enemizer=True) except:
Patch.create_patch_file(rompath, player=player, player_name=world.player_name[player]) raise
os.unlink(rompath) finally:
self.rom_name = rom.name self.rom_name_available_event.set() # make sure threading continues and errors are collected
self.rom_name_available_event.set()
def modify_multidata(self, multidata: dict): def modify_multidata(self, multidata: dict):
import base64 import base64
# wait for self.rom_name to be available. # wait for self.rom_name to be available.
self.rom_name_available_event.wait() self.rom_name_available_event.wait()
new_name = base64.b64encode(bytes(self.rom_name)).decode() rom_name = getattr(self, "rom_name", None)
payload = multidata["connect_names"][self.world.player_name[self.player]] # we skip in case of error, so that the original error in the output thread is the one that gets raised
multidata["connect_names"][new_name] = payload if rom_name:
del (multidata["connect_names"][self.world.player_name[self.player]]) new_name = base64.b64encode(bytes(self.rom_name)).decode()
payload = multidata["connect_names"][self.world.player_name[self.player]]
multidata["connect_names"][new_name] = payload
del (multidata["connect_names"][self.world.player_name[self.player]])
def get_required_client_version(self) -> tuple: def get_required_client_version(self) -> tuple:
return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version()) return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version())
@ -248,4 +254,51 @@ class ALTTPWorld(World):
def create_item(self, name: str) -> Item: def create_item(self, name: str) -> Item:
return ALttPItem(name, self.player, **as_dict_item_table[name]) return ALttPItem(name, self.player, **as_dict_item_table[name])
@classmethod
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, restitempool, fill_locations):
trash_counts = {}
standard_keyshuffle_players = set()
for player in world.get_game_players("A Link to the Past"):
if world.mode[player] == 'standard' and world.keyshuffle[player] is True:
standard_keyshuffle_players.add(player)
if not world.ganonstower_vanilla[player] or \
world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
pass
elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1):
trash_counts[player] = world.random.randint(world.crystals_needed_for_gt[player] * 2,
world.crystals_needed_for_gt[player] * 4)
else:
trash_counts[player] = world.random.randint(0, world.crystals_needed_for_gt[player] * 2)
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
if standard_keyshuffle_players:
progitempool.sort(
key=lambda item: 1 if item.name == 'Small Key (Hyrule Castle)' and
item.player in standard_keyshuffle_players else 0)
if trash_counts:
locations_mapping = {player: [] for player in trash_counts}
for location in fill_locations:
if 'Ganons Tower' in location.name and location.player in locations_mapping:
locations_mapping[location.player].append(location)
for player, trash_count in trash_counts.items():
gtower_locations = locations_mapping[player]
world.random.shuffle(gtower_locations)
localrest = localrestitempool[player]
if localrest:
gt_item_pool = restitempool + localrest
world.random.shuffle(gt_item_pool)
else:
gt_item_pool = restitempool.copy()
while gtower_locations and gt_item_pool and trash_count > 0:
spot_to_fill = gtower_locations.pop()
item_to_place = gt_item_pool.pop()
if item_to_place in localrest:
localrest.remove(item_to_place)
else:
restitempool.remove(item_to_place)
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill) # very slow, unfortunately
trash_count -= 1

View File

@ -231,7 +231,6 @@ def set_location_rule(world, player, loc):
def set_rules(world, player): def set_rules(world, player):
logging.warning(type(location_table))
for loc in location_table: for loc in location_table:
set_location_rule(world, player, loc) set_location_rule(world, player, loc)