Files
alttpr-python/source/classes/CustomSettings.py
aerinon 923ba14d61 feat: force_enemy feature
feat: rom-side enemy "spies"
2025-11-14 09:05:12 -07:00

470 lines
25 KiB
Python

import os
import urllib.request
import urllib.parse
import yaml
from typing import Any
from yaml.representer import Representer
from collections import defaultdict
from pathlib import Path
import RaceRandom as random
from BaseClasses import LocationType, DoorType
from source.tools.MysteryUtils import roll_settings, get_weights
class CustomSettings(object):
def __init__(self):
self.file_source = None
self.relative_dir = None
self.world_rep = {}
self.player_range = None
self.player_map = {} # player number to name
def load_yaml(self, file):
self.file_source = load_yaml(file)
head, filename = os.path.split(file)
self.relative_dir = head
if 'version' in self.file_source and self.file_source['version'].startswith('2'):
player_number = 1
for key in self.file_source.keys():
if key in ['meta', 'version']:
continue
else:
self.player_map[player_number] = key
player_number += 1
def determine_seed(self, default_seed):
if 'meta' in self.file_source:
meta = defaultdict(lambda: None, self.file_source['meta'])
seed = meta['seed']
if seed:
random.seed(seed)
return seed
if default_seed is None:
random.seed(None)
seed = random.randint(0, 999999999)
else:
seed = default_seed
random.seed(seed)
return seed
def determine_players(self):
if 'meta' not in self.file_source:
return None
meta = defaultdict(lambda: None, self.file_source['meta'])
return meta['players']
def adjust_args(self, args):
def get_setting(value: Any, default):
if value or value == 0:
if isinstance(value, dict):
return random.choices(list(value.keys()), list(value.values()), k=1)[0]
else:
return value
return default
if 'meta' in self.file_source:
meta = defaultdict(lambda: None, self.file_source['meta'])
args.multi = get_setting(meta['players'], args.multi)
args.algorithm = get_setting(meta['algorithm'], args.algorithm)
args.outputname = get_setting(meta['name'], args.outputname)
args.bps = get_setting(meta['bps'], args.bps)
args.suppress_rom = get_setting(meta['suppress_rom'], args.suppress_rom)
args.skip_playthrough = get_setting(meta['skip_playthrough'], args.skip_playthrough)
args.spoiler = get_setting(meta['spoiler'], args.spoiler)
args.names = get_setting(meta['names'], args.names)
args.race = get_setting(meta['race'], args.race)
args.notes = get_setting(meta['user_notes'], args.notes)
self.player_range = range(1, args.multi + 1)
if 'settings' in self.file_source:
for p in self.player_range:
player_setting = self.file_source['settings'][p]
if isinstance(player_setting, str):
weights = get_weights(os.path.join(self.relative_dir, player_setting))
settings = defaultdict(lambda: None, vars(roll_settings(weights)))
args.mystery = True
else:
settings = defaultdict(lambda: None, player_setting)
args.shuffle[p] = get_setting(settings['shuffle'], args.shuffle[p])
args.door_shuffle[p] = get_setting(settings['door_shuffle'], args.door_shuffle[p])
args.logic[p] = get_setting(settings['logic'], args.logic[p])
args.mode[p] = get_setting(settings['mode'], args.mode[p])
args.boots_hint[p] = get_setting(settings['boots_hint'], args.boots_hint[p])
args.swords[p] = get_setting(settings['swords'], args.swords[p])
args.flute_mode[p] = get_setting(settings['flute_mode'], args.flute_mode[p])
args.bow_mode[p] = get_setting(settings['bow_mode'], args.bow_mode[p])
args.item_functionality[p] = get_setting(settings['item_functionality'], args.item_functionality[p])
args.goal[p] = get_setting(settings['goal'], args.goal[p])
args.difficulty[p] = get_setting(settings['difficulty'], args.difficulty[p])
args.accessibility[p] = get_setting(settings['accessibility'], args.accessibility[p])
args.retro[p] = get_setting(settings['retro'], args.retro[p])
args.take_any[p] = get_setting(settings['take_any'], args.take_any[p])
args.hints[p] = get_setting(settings['hints'], args.hints[p])
args.shopsanity[p] = get_setting(settings['shopsanity'], args.shopsanity[p])
args.dropshuffle[p] = get_setting(settings['dropshuffle'], args.dropshuffle[p])
args.pottery[p] = get_setting(settings['pottery'], args.pottery[p])
if get_setting(settings['keydropshuffle'], args.keydropshuffle[p]):
if args.dropshuffle[p] == 'none':
args.dropshuffle[p] = 'keys'
if args.pottery[p] == 'none':
args.pottery[p] = 'keys'
if args.retro[p] or args.mode[p] == 'retro':
if args.bow_mode[p] == 'progressive':
args.bow_mode[p] = 'retro'
elif args.bow_mode[p] == 'silvers':
args.bow_mode[p] = 'retro_silvers'
args.take_any[p] = 'random' if args.take_any[p] == 'none' else args.take_any[p]
args.keyshuffle[p] = 'universal'
args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p])
args.standardize_palettes[p] = get_setting(settings['standardize_palettes'],
args.standardize_palettes[p])
args.intensity[p] = get_setting(settings['intensity'], args.intensity[p])
args.door_type_mode[p] = get_setting(settings['door_type_mode'], args.door_type_mode[p])
args.trap_door_mode[p] = get_setting(settings['trap_door_mode'], args.trap_door_mode[p])
args.key_logic_algorithm[p] = get_setting(settings['key_logic_algorithm'], args.key_logic_algorithm[p])
args.decoupledoors[p] = get_setting(settings['decoupledoors'], args.decoupledoors[p])
args.door_self_loops[p] = get_setting(settings['door_self_loops'], args.door_self_loops[p])
args.dungeon_counters[p] = get_setting(settings['dungeon_counters'], args.dungeon_counters[p])
args.crystals_gt[p] = get_setting(settings['crystals_gt'], args.crystals_gt[p])
args.crystals_ganon[p] = get_setting(settings['crystals_ganon'], args.crystals_ganon[p])
args.experimental[p] = get_setting(settings['experimental'], args.experimental[p])
args.collection_rate[p] = get_setting(settings['collection_rate'], args.collection_rate[p])
args.openpyramid[p] = get_setting(settings['openpyramid'], args.openpyramid[p])
args.bigkeyshuffle[p] = get_setting(settings['bigkeyshuffle'], args.bigkeyshuffle[p])
args.keyshuffle[p] = get_setting(settings['keyshuffle'], args.keyshuffle[p])
args.mapshuffle[p] = get_setting(settings['mapshuffle'], args.mapshuffle[p])
args.compassshuffle[p] = get_setting(settings['compassshuffle'], args.compassshuffle[p])
if get_setting(settings['keysanity'], args.keysanity):
args.bigkeyshuffle[p] = True
if args.keyshuffle[p] == 'none':
args.keyshuffle[p] = 'wild'
args.mapshuffle[p] = True
args.compassshuffle[p] = True
args.shufflebosses[p] = get_setting(settings['boss_shuffle'], get_setting(settings['shufflebosses'], args.shufflebosses[p]))
args.shuffleenemies[p] = get_setting(settings['enemy_shuffle'], get_setting(settings['shuffleenemies'], args.shuffleenemies[p]))
args.enemy_health[p] = get_setting(settings['enemy_health'], args.enemy_health[p])
args.enemy_damage[p] = get_setting(settings['enemy_damage'], args.enemy_damage[p])
args.any_enemy_logic[p] = get_setting(settings['any_enemy_logic'], args.any_enemy_logic[p])
args.shufflepots[p] = get_setting(settings['shufflepots'], args.shufflepots[p])
args.bombbag[p] = get_setting(settings['bombbag'], args.bombbag[p])
args.shufflelinks[p] = get_setting(settings['shufflelinks'], args.shufflelinks[p])
args.shuffletavern[p] = get_setting(settings['shuffletavern'], args.shuffletavern[p])
args.skullwoods[p] = get_setting(settings['skullwoods'], args.skullwoods[p])
args.linked_drops[p] = get_setting(settings['linked_drops'], args.linked_drops[p])
args.restrict_boss_items[p] = get_setting(settings['restrict_boss_items'], args.restrict_boss_items[p])
args.overworld_map[p] = get_setting(settings['overworld_map'], args.overworld_map[p])
args.pseudoboots[p] = get_setting(settings['pseudoboots'], args.pseudoboots[p])
args.mirrorscroll[p] = get_setting(settings['mirrorscroll'], args.mirrorscroll[p])
args.triforce_goal[p] = get_setting(settings['triforce_goal'], args.triforce_goal[p])
args.triforce_pool[p] = get_setting(settings['triforce_pool'], args.triforce_pool[p])
args.triforce_goal_min[p] = get_setting(settings['triforce_goal_min'], args.triforce_goal_min[p])
args.triforce_goal_max[p] = get_setting(settings['triforce_goal_max'], args.triforce_goal_max[p])
args.triforce_pool_min[p] = get_setting(settings['triforce_pool_min'], args.triforce_pool_min[p])
args.triforce_pool_max[p] = get_setting(settings['triforce_pool_max'], args.triforce_pool_max[p])
args.triforce_min_difference[p] = get_setting(settings['triforce_min_difference'], args.triforce_min_difference[p])
args.triforce_max_difference[p] = get_setting(settings['triforce_max_difference'], args.triforce_max_difference[p])
args.beemizer[p] = get_setting(settings['beemizer'], args.beemizer[p])
args.aga_randomness[p] = get_setting(settings['aga_randomness'], args.aga_randomness[p])
args.money_balance[p] = get_setting(settings['money_balance'], args.money_balance[p])
# mystery usage
args.usestartinventory[p] = get_setting(settings['usestartinventory'], args.usestartinventory[p])
args.startinventory[p] = get_setting(settings['startinventory'], args.startinventory[p])
# rom adjust stuff
args.sprite[p] = get_setting(settings['sprite'], args.sprite[p])
args.disablemusic[p] = get_setting(settings['disablemusic'], args.disablemusic[p])
args.quickswap[p] = get_setting(settings['quickswap'], args.quickswap[p])
args.reduce_flashing[p] = get_setting(settings['reduce_flashing'], args.reduce_flashing[p])
args.fastmenu[p] = get_setting(settings['fastmenu'], args.fastmenu[p])
args.heartcolor[p] = get_setting(settings['heartcolor'], args.heartcolor[p])
args.heartbeep[p] = get_setting(settings['heartbeep'], args.heartbeep[p])
args.ow_palettes[p] = get_setting(settings['ow_palettes'], args.ow_palettes[p])
args.uw_palettes[p] = get_setting(settings['uw_palettes'], args.uw_palettes[p])
args.shuffle_sfx[p] = get_setting(settings['shuffle_sfx'], args.shuffle_sfx[p])
args.msu_resume[p] = get_setting(settings['msu_resume'], args.msu_resume[p])
def has_setting(self, player, setting):
if 'settings' in self.file_source and player in self.file_source['settings']:
return setting in self.file_source['settings'][player]
return False
def get_setting(self, player, setting):
return self.file_source['settings'][player][setting]
def get_item_pool(self):
if 'item_pool' in self.file_source:
return self.file_source['item_pool']
return None
def get_placements(self):
if 'placements' in self.file_source:
return self.file_source['placements']
return None
def get_prices(self, player):
return self.get_attribute_by_player_composite('prices', player)
def get_advanced_placements(self):
if 'advanced_placements' in self.file_source:
return self.file_source['advanced_placements']
return None
def get_entrances(self):
if 'entrances' in self.file_source:
return self.file_source['entrances']
return None
def get_doors(self):
if 'doors' in self.file_source:
return self.file_source['doors']
return None
def get_bosses(self):
if 'bosses' in self.file_source:
return self.file_source['bosses']
return None
def get_start_inventory(self):
if 'start_inventory' in self.file_source:
return self.file_source['start_inventory']
return None
def get_medallions(self):
if 'medallions' in self.file_source:
return self.file_source['medallions']
return None
def get_drops(self):
if 'drops' in self.file_source:
return self.file_source['drops']
return None
def get_enemies(self):
if 'enemies' in self.file_source:
return self.file_source['enemies']
return None
def get_attribute_by_player_composite(self, attribute, player):
attempt = self.get_attribute_by_player_new(attribute, player)
if attempt is not None:
return attempt
attempt = self.get_attribute_by_player(attribute, player)
return attempt
def get_attribute_by_player(self, attribute, player):
if attribute in self.file_source:
if player in self.file_source[attribute]:
return self.file_source[attribute][player]
return None
def get_attribute_by_player_new(self, attribute, player):
player_id = self.get_player_id(player)
if player_id is not None:
if attribute in self.file_source[player_id]:
return self.file_source[player_id][attribute]
return None
def get_player_id(self, player):
if player in self.file_source:
return player
if player in self.player_map and self.player_map[player] in self.file_source:
return self.player_map[player]
return None
def create_from_world(self, world, settings):
self.player_range = range(1, world.players + 1)
settings_dict, meta_dict = {}, {}
self.world_rep['meta'] = meta_dict
meta_dict['players'] = world.players
meta_dict['algorithm'] = world.algorithm
meta_dict['seed'] = world.seed
meta_dict['race'] = settings.race
meta_dict['user_notes'] = settings.notes
self.world_rep['settings'] = settings_dict
for p in self.player_range:
settings_dict[p] = {}
settings_dict[p]['shuffle'] = world.shuffle[p]
settings_dict[p]['door_shuffle'] = world.doorShuffle[p]
settings_dict[p]['intensity'] = world.intensity[p]
settings_dict[p]['door_type_mode'] = world.door_type_mode[p]
settings_dict[p]['trap_door_mode'] = world.trap_door_mode[p]
settings_dict[p]['key_logic_algorithm'] = world.key_logic_algorithm[p]
settings_dict[p]['decoupledoors'] = world.decoupledoors[p]
settings_dict[p]['door_self_loops'] = world.door_self_loops[p]
settings_dict[p]['logic'] = world.logic[p]
settings_dict[p]['mode'] = world.mode[p]
settings_dict[p]['swords'] = world.swords[p]
settings_dict[p]['flute_mode'] = world.flute_mode[p]
settings_dict[p]['bow_mode'] = world.bow_mode[p]
settings_dict[p]['difficulty'] = world.difficulty[p]
settings_dict[p]['goal'] = world.goal[p]
settings_dict[p]['accessibility'] = world.accessibility[p]
settings_dict[p]['item_functionality'] = world.difficulty_adjustments[p]
settings_dict[p]['take_any'] = world.take_any[p]
settings_dict[p]['hints'] = world.hints[p]
settings_dict[p]['shopsanity'] = world.shopsanity[p]
settings_dict[p]['dropshuffle'] = world.dropshuffle[p]
settings_dict[p]['pottery'] = world.pottery[p]
settings_dict[p]['mixed_travel'] = world.mixed_travel[p]
settings_dict[p]['standardize_palettes'] = world.standardize_palettes[p]
settings_dict[p]['dungeon_counters'] = world.dungeon_counters[p]
settings_dict[p]['crystals_gt'] = world.crystals_gt_orig[p]
settings_dict[p]['crystals_ganon'] = world.crystals_ganon_orig[p]
settings_dict[p]['experimental'] = world.experimental[p]
settings_dict[p]['collection_rate'] = world.collection_rate[p]
settings_dict[p]['openpyramid'] = world.open_pyramid[p]
settings_dict[p]['bigkeyshuffle'] = world.bigkeyshuffle[p]
settings_dict[p]['keyshuffle'] = world.keyshuffle[p]
settings_dict[p]['mapshuffle'] = world.mapshuffle[p]
settings_dict[p]['compassshuffle'] = world.compassshuffle[p]
settings_dict[p]['boss_shuffle'] = world.boss_shuffle[p]
settings_dict[p]['enemy_shuffle'] = world.enemy_shuffle[p]
settings_dict[p]['enemy_health'] = world.enemy_health[p]
settings_dict[p]['enemy_damage'] = world.enemy_damage[p]
settings_dict[p]['any_enemy_logic'] = world.any_enemy_logic[p]
settings_dict[p]['shufflepots'] = world.potshuffle[p]
settings_dict[p]['bombbag'] = world.bombbag[p]
settings_dict[p]['shufflelinks'] = world.shufflelinks[p]
settings_dict[p]['shuffletavern'] = world.shuffletavern[p]
settings_dict[p]['skullwoods'] = world.skullwoods[p]
settings_dict[p]['linked_drops'] = world.linked_drops[p]
settings_dict[p]['overworld_map'] = world.overworld_map[p]
settings_dict[p]['pseudoboots'] = world.pseudoboots[p]
settings_dict[p]['mirrorscroll'] = world.mirrorscroll[p]
settings_dict[p]['triforce_goal'] = world.treasure_hunt_count[p]
settings_dict[p]['triforce_pool'] = world.treasure_hunt_total[p]
settings_dict[p]['beemizer'] = world.beemizer[p]
settings_dict[p]['aga_randomness'] = world.aga_randomness[p]
settings_dict[p]['money_balance'] = world.money_balance[p]
# rom adjust stuff
# settings_dict[p]['sprite'] = world.sprite[p]
# settings_dict[p]['disablemusic'] = world.disablemusic[p]
# settings_dict[p]['quickswap'] = world.quickswap[p]
# settings_dict[p]['reduce_flashing'] = world.reduce_flashing[p]
# settings_dict[p]['fastmenu'] = world.fastmenu[p]
# settings_dict[p]['heartcolor'] = world.heartcolor[p]
# settings_dict[p]['heartbeep'] = world.heartbeep[p]
# settings_dict[p]['ow_palettes'] = world.ow_palettes[p]
# settings_dict[p]['uw_palettes'] = world.uw_palettes[p]
# settings_dict[p]['shuffle_sfx'] = world.shuffle_sfx[p]
# more settings?
def record_info(self, world):
self.world_rep['bosses'] = bosses = {}
self.world_rep['start_inventory'] = start_inv = {}
for p in self.player_range:
bosses[p] = {}
start_inv[p] = []
for dungeon in world.dungeons:
for level, boss in dungeon.bosses.items():
location = dungeon.name if level is None else f'{dungeon.name} ({level})'
if boss and 'Agahnim' not in boss.name:
bosses[dungeon.player][location] = boss.name
for item in world.precollected_items:
start_inv[item.player].append(item.name)
def record_item_pool(self, world):
self.world_rep['item_pool'] = item_pool = {}
self.world_rep['medallions'] = medallions = {}
for p in self.player_range:
item_pool[p] = defaultdict(int)
medallions[p] = {}
for item in world.itempool:
item_pool[item.player][item.name] += 1
for p, req_medals in world.required_medallions.items():
medallions[p]['Misery Mire'] = req_medals[0]
medallions[p]['Turtle Rock'] = req_medals[1]
def record_item_placements(self, world):
self.world_rep['placements'] = placements = {}
for p in self.player_range:
placements[p] = {}
for location in world.get_locations():
if location.type != LocationType.Logical:
if location.player != location.item.player:
placements[location.player][location.name] = f'{location.item.name}#{location.item.player}'
else:
placements[location.player][location.name] = location.item.name
def record_entrances(self, world):
self.world_rep['entrances'] = entrances = {}
world.custom_entrances = {}
for p in self.player_range:
connections = entrances[p] = {}
connections['entrances'] = {}
connections['exits'] = {}
connections['two-way'] = {}
for key, data in world.spoiler.entrances.items():
player = data['player'] if 'player' in data else 1
connections = entrances[player]
sub = 'two-way' if data['direction'] == 'both' else 'exits' if data['direction'] == 'exit' else 'entrances'
connections[sub][data['entrance']] = data['exit']
def record_doors(self, world):
self.world_rep['doors'] = doors = {}
for p in self.player_range:
meta_doors = doors[p] = {}
lobbies = meta_doors['lobbies'] = {}
door_map = meta_doors['doors'] = {}
for portal in world.dungeon_portals[p]:
lobbies[portal.name] = portal.door.name
door_types = {DoorType.Normal, DoorType.SpiralStairs, DoorType.Interior}
if world.intensity[p] > 1:
door_types.update([DoorType.Open, DoorType.StraightStairs, DoorType.Ladder])
door_kinds, skip = {}, set()
for key, info in world.spoiler.doorTypes.items():
if key[1] == p:
if ' <-> ' in info['doorNames']:
dns = info['doorNames'].split(' <-> ')
for dn in dns:
door_kinds[dn] = info['type'] # Key Door, Bomb Door, Dash Door
else:
door_kinds[info['doorNames']] = info['type']
for door in world.doors:
if door.player == p and not door.entranceFlag and door.type in door_types and door not in skip:
if door.type == DoorType.Interior:
if door.name in door_kinds:
door_value = {'type': door_kinds[door.name]}
door_map[door.name] = door_value # intra-tile note
skip.add(door.dest)
elif door.dest:
if door.dest.dest == door:
door_value = door.dest.name
skip.add(door.dest)
if door.name in door_kinds:
door_value = {'dest': door_value, 'type': door_kinds[door.name]}
if door.name not in door_kinds and door.dest.name in door_kinds:
# tricky swap thing
door_value = {'dest': door.name, 'type': door_kinds[door.dest.name]}
door = door.dest # this is weird
elif door.name in door_kinds:
door_value = {'dest': door.dest.name, 'one-way': True, 'type': door_kinds[door.name]}
else:
door_value = {'dest': door.dest.name, 'one-way': True}
door_map[door.name] = door_value
def record_medallions(self):
pass
def write_to_file(self, destination):
yaml.add_representer(defaultdict, Representer.represent_dict)
with open(destination, 'w') as file:
yaml.dump(self.world_rep, file)
def load_yaml(path):
if os.path.exists(Path(path)):
with open(path, "r", encoding="utf-8") as f:
return yaml.load(f, Loader=yaml.SafeLoader)
elif urllib.parse.urlparse(path).scheme in ['http', 'https']:
return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader)