Merge remote-tracking branch 'origin/OverworldShuffle' into OverworldShuffle

This commit is contained in:
2023-02-27 10:21:25 -06:00
92 changed files with 13130 additions and 3047 deletions

108
Main.py
View File

@@ -16,7 +16,7 @@ from OverworldGlitchRules import create_owg_connections
from PotShuffle import shuffle_pots, shuffle_pot_switches
from Regions import create_regions, create_shops, mark_light_dark_world_regions, create_dungeon_regions, adjust_locations
from OWEdges import create_owedges
from OverworldShuffle import link_overworld, update_world_regions, create_flute_exits
from OverworldShuffle import link_overworld, update_world_regions, create_dynamic_exits
from EntranceShuffle import link_entrances
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string
from Doors import create_doors
@@ -25,14 +25,18 @@ from RoomData import create_rooms
from Rules import set_rules
from Dungeons import create_dungeons
from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dungeons_restrictive, ensure_good_pots
from Fill import dungeon_tracking
from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations, set_prize_drops
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, create_farm_locations
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, fill_specific_items, create_farm_locations
from Utils import output_path, parse_player_names
from source.item.District import init_districts
from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config
from source.overworld.EntranceShuffle2 import link_entrances_new
from source.tools.BPS import create_bps_from_data
from source.classes.CustomSettings import CustomSettings
__version__ = '1.0.1.3-u'
__version__ = '1.2.0.9-u'
from source.classes.BabelFish import BabelFish
@@ -83,32 +87,44 @@ def main(args, seed=None, fish=None):
if args.securerandom:
random.use_secure()
seeded = False
# initialize the world
if args.code:
for player, code in args.code.items():
if code:
Settings.adjust_args_from_code(code, player, args)
customized = None
if args.customizer:
customized = CustomSettings()
customized.load_yaml(args.customizer)
seed = customized.determine_seed(seed)
seeded = True
customized.adjust_args(args)
world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords,
args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm,
args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints)
args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints)
world.customizer = customized if customized else None
logger = logging.getLogger('')
if seed is None:
random.seed(None)
world.seed = random.randint(0, 999999999)
else:
world.seed = int(seed)
random.seed(world.seed)
if not seeded:
random.seed(world.seed)
if args.securerandom:
world.seed = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(9))
world.boots_hint = args.boots_hint.copy()
world.remote_items = args.remote_items.copy()
world.mapshuffle = args.mapshuffle.copy()
world.compassshuffle = args.compassshuffle.copy()
world.keyshuffle = args.keyshuffle.copy()
world.bigkeyshuffle = args.bigkeyshuffle.copy()
world.bombbag = args.bombbag.copy()
world.flute_mode = args.flute_mode.copy()
world.bow_mode = args.bow_mode.copy()
world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)}
world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)}
world.crystals_ganon_orig = args.crystals_ganon.copy()
@@ -127,6 +143,10 @@ def main(args, seed=None, fish=None):
world.enemy_damage = args.enemy_damage.copy()
world.beemizer = args.beemizer.copy()
world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)}
world.door_type_mode = args.door_type_mode.copy()
world.trap_door_mode = args.trap_door_mode.copy()
world.key_logic_algorithm = args.key_logic_algorithm.copy()
world.decoupledoors = args.decoupledoors.copy()
world.experimental = args.experimental.copy()
world.dungeon_counters = args.dungeon_counters.copy()
world.fish = fish
@@ -139,13 +159,16 @@ def main(args, seed=None, fish=None):
world.treasure_hunt_count = {k: int(v) for k, v in args.triforce_goal.items()}
world.treasure_hunt_total = {k: int(v) for k, v in args.triforce_pool.items()}
world.shufflelinks = args.shufflelinks.copy()
world.shuffletavern = args.shuffletavern.copy()
world.pseudoboots = args.pseudoboots.copy()
world.overworld_map = args.overworld_map.copy()
world.take_any = args.take_any.copy()
world.restrict_boss_items = args.restrict_boss_items.copy()
world.collection_rate = args.collection_rate.copy()
world.colorizepots = args.colorizepots.copy()
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
world.finish_init()
from OverworldShuffle import __version__ as ORVersion
logger.info(
@@ -166,7 +189,9 @@ def main(args, seed=None, fish=None):
for player, name in enumerate(team, 1):
world.player_names[player].append(name)
logger.info('')
world.settings = CustomSettings()
world.settings.create_from_world(world, args.race)
outfilebase = f'OR_{args.outputname if args.outputname else world.seed}'
for player in range(1, world.players + 1):
@@ -178,7 +203,9 @@ def main(args, seed=None, fish=None):
if args.usestartinventory[player]:
for tok in filter(None, args.startinventory[player].split(',')):
item = ItemFactory(tok.replace("_", " ").strip(), player)
name = tok.replace("_", " ").strip()
name = name if name != 'Ocarina' or world.flute_mode[player] != 'active' else 'Ocarina (Activated)'
item = ItemFactory(name, player)
if item:
world.push_precollected(item)
@@ -199,6 +226,15 @@ def main(args, seed=None, fish=None):
adjust_locations(world, player)
place_bosses(world, player)
if world.customizer and world.customizer.get_start_inventory():
for p, inv_list in world.customizer.get_start_inventory().items():
for inv_item in inv_list:
item = ItemFactory(inv_item.strip(), p)
if item:
world.push_precollected(item)
if args.print_custom_yaml:
world.settings.record_info(world)
if any(world.potshuffle.values()):
logger.info(world.fish.translate("cli", "cli", "shuffling.pots"))
for player in range(1, world.players + 1):
@@ -215,17 +251,22 @@ def main(args, seed=None, fish=None):
create_shops(world, player)
update_world_regions(world, player)
mark_light_dark_world_regions(world, player)
create_flute_exits(world, player)
create_dynamic_exits(world, player)
init_districts(world)
logger.info(world.fish.translate("cli","cli","shuffling.world"))
for player in range(1, world.players + 1):
link_entrances(world, player)
link_entrances_new(world, player)
logger.info(world.fish.translate("cli", "cli", "shuffling.prep"))
for player in range(1, world.players + 1):
link_doors_prep(world, player)
if args.print_custom_yaml:
world.settings.record_entrances(world)
create_item_pool_config(world)
logger.info(world.fish.translate("cli", "cli", "shuffling.dungeons"))
@@ -233,6 +274,9 @@ def main(args, seed=None, fish=None):
for player in range(1, world.players + 1):
link_doors(world, player)
mark_light_dark_world_regions(world, player)
if args.print_custom_yaml:
world.settings.record_doors(world)
logger.info(world.fish.translate("cli", "cli", "generating.itempool"))
for player in range(1, world.players + 1):
@@ -251,19 +295,21 @@ def main(args, seed=None, fish=None):
for player in range(1, world.players + 1):
if world.shopsanity[player]:
sell_potions(world, player)
if world.universal_keys[player]:
if world.keyshuffle[player] == 'universal':
sell_keys(world, player)
else:
lock_shop_locations(world, player)
massage_item_pool(world)
if args.print_custom_yaml:
world.settings.record_item_pool(world)
dungeon_tracking(world)
fill_specific_items(world)
logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes"))
fill_prizes(world)
# used for debugging
# fill_specific_items(world)
logger.info(world.fish.translate("cli","cli","placing.dungeon.items"))
if args.algorithm != 'equitable':
@@ -297,6 +343,7 @@ def main(args, seed=None, fish=None):
balance_multiworld_progression(world)
# if we only check for beatable, we can do this sanity check first before creating the rom
world.clear_exp_cache()
if not world.can_beat_game(log_error=True):
raise RuntimeError(world.fish.translate("cli", "cli", "cannot.beat.game"))
@@ -307,6 +354,10 @@ def main(args, seed=None, fish=None):
balance_money_progression(world)
ensure_good_pots(world, True)
if args.print_custom_yaml:
world.settings.record_item_placements(world)
world.settings.write_to_file(output_path(f'{outfilebase}_custom.yaml'))
rom_names = []
jsonout = {}
enemized = False
@@ -425,7 +476,7 @@ def copy_world(world):
# ToDo: Not good yet
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm,
world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints)
ret.teams = world.teams
ret.player_names = copy.deepcopy(world.player_names)
ret.remote_items = world.remote_items.copy()
@@ -454,6 +505,7 @@ def copy_world(world):
ret.keyshuffle = world.keyshuffle.copy()
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
ret.bombbag = world.bombbag.copy()
ret.flute_mode = world.flute_mode.copy()
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
ret.crystals_ganon_orig = world.crystals_ganon_orig.copy()
@@ -474,6 +526,7 @@ def copy_world(world):
ret.enemy_damage = world.enemy_damage.copy()
ret.beemizer = world.beemizer.copy()
ret.intensity = world.intensity.copy()
ret.decoupledoors = world.decoupledoors.copy()
ret.experimental = world.experimental.copy()
ret.shopsanity = world.shopsanity.copy()
ret.dropshuffle = world.dropshuffle.copy()
@@ -492,10 +545,11 @@ def copy_world(world):
update_world_regions(ret, player)
if world.logic[player] in ('owglitches', 'nologic'):
create_owg_connections(ret, player)
create_flute_exits(ret, player)
create_dynamic_exits(ret, player)
create_dungeon_regions(ret, player)
create_owedges(ret, player)
create_shops(ret, player)
#create_doors(ret, player)
create_rooms(ret, player)
create_dungeons(ret, player)
@@ -546,12 +600,9 @@ def copy_world(world):
new_location.item = item
item.location = new_location
item.world = ret
if location.event:
new_location.event = True
if location.locked:
new_location.locked = True
if location.skip:
new_location.skip = True
new_location.event = location.event
new_location.locked = location.locked
new_location.skip = location.skip
# these need to be modified properly by set_rules
new_location.access_rule = lambda state: True
new_location.item_rule = lambda state: True
@@ -605,7 +656,7 @@ def copy_world_premature(world, player):
# ToDo: Not good yet
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm,
world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints)
ret.teams = world.teams
ret.player_names = copy.deepcopy(world.player_names)
ret.remote_items = world.remote_items.copy()
@@ -671,7 +722,7 @@ def copy_world_premature(world, player):
update_world_regions(ret, player)
if world.logic[player] in ('owglitches', 'nologic'):
create_owg_connections(ret, player)
create_flute_exits(ret, player)
create_dynamic_exits(ret, player)
create_dungeon_regions(ret, player)
create_owedges(ret, player)
create_shops(ret, player)
@@ -760,7 +811,8 @@ def create_playthrough(world):
world = copy_world(world)
# get locations containing progress items
prog_locations = [location for location in world.get_filled_locations() if location.item.advancement]
prog_locations = [location for location in world.get_filled_locations() if location.item.advancement
or world.goal[location.player] == 'completionist']
optional_locations = ['Trench 1 Switch', 'Trench 2 Switch', 'Ice Block Drop', 'Skull Star Tile']
optional_locations.extend(['Hyrule Castle Courtyard Tree Pull', 'Mountain Entry Area Tree Pull']) # adding pre-aga tree pulls
optional_locations.extend(['Lumberjack Area Crab Drop', 'South Pass Area Crab Drop']) # adding pre-aga bush crabs
@@ -799,13 +851,15 @@ def create_playthrough(world):
for num, sphere in reversed(list(enumerate(collection_spheres))):
to_delete = set()
for location in sphere:
if world.goal[location.player] == 'completionist':
continue # every location for that player is required
# we remove the item at location and check if game is still beatable
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
old_item = location.item
location.item = None
# todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic
# world.clear_exp_cache()
if world.can_beat_game(state_cache[num]):
world.clear_exp_cache()
if world.can_beat_game(state_cache[max(num-1, 0)]):
logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required')
to_delete.add(location)
else: