Merge branch 'main' into breaking_changes
# Conflicts: # Main.py # Utils.py
This commit is contained in:
commit
f7dc21ddcc
15
Main.py
15
Main.py
|
@ -39,11 +39,12 @@ def get_seed(seed=None):
|
|||
return seed
|
||||
|
||||
|
||||
seeds: Dict[tuple, str] = dict()
|
||||
def get_same_seed(world: MultiWorld, seed_def: tuple) -> str:
|
||||
seeds: Dict[tuple, str] = getattr(world, "__named_seeds", {})
|
||||
if seed_def in seeds:
|
||||
return seeds[seed_def]
|
||||
seeds[seed_def] = str(world.random.randint(0, 2 ** 64))
|
||||
world.__named_seeds = seeds
|
||||
return seeds[seed_def]
|
||||
|
||||
|
||||
|
@ -121,9 +122,15 @@ def main(args, seed=None):
|
|||
world.shuffle[player] = shuffle
|
||||
if shuffle == "vanilla":
|
||||
world.er_seeds[player] = "vanilla"
|
||||
elif seed.startswith("team-"):
|
||||
elif seed.startswith("group-"): # renamed from team to group to not confuse with existing team name use
|
||||
world.er_seeds[player] = get_same_seed(world, (shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
|
||||
else:
|
||||
elif seed.startswith("team-"): # TODO: remove on breaking_changes
|
||||
world.er_seeds[player] = get_same_seed(world, (shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
|
||||
elif not args.race:
|
||||
world.er_seeds[player] = seed
|
||||
elif seed: # race but with a set seed, ignore set seed and use group logic instead
|
||||
world.er_seeds[player] = get_same_seed(world, (shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
|
||||
else: # race but without a set seed
|
||||
world.er_seeds[player] = seed
|
||||
elif world.shuffle[player] == "vanilla":
|
||||
world.er_seeds[player] = "vanilla"
|
||||
|
@ -307,7 +314,7 @@ def main(args, seed=None):
|
|||
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] if not args.race else True,
|
||||
reduceflashing=args.reduceflashing[player] or args.race,
|
||||
triforcehud=args.triforcehud[player])
|
||||
|
||||
mcsb_name = ''
|
||||
|
|
73
Mystery.py
73
Mystery.py
|
@ -294,6 +294,8 @@ def prefer_int(input_data: str) -> typing.Union[str, int]:
|
|||
|
||||
available_boss_names: typing.Set[str] = {boss.lower() for boss in Bosses.boss_table if boss not in
|
||||
{'Agahnim', 'Agahnim2', 'Ganon'}}
|
||||
available_boss_locations: typing.Set[str] = {f"{loc.lower()}{f' {level}' if level else ''}" for loc, level in
|
||||
Bosses.boss_location_table}
|
||||
|
||||
boss_shuffle_options = {None: 'none',
|
||||
'none': 'none',
|
||||
|
@ -370,6 +372,42 @@ def roll_triggers(weights: dict) -> dict:
|
|||
f"Please fix your triggers.") from e
|
||||
return weights
|
||||
|
||||
|
||||
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
|
||||
if boss_shuffle in boss_shuffle_options:
|
||||
return boss_shuffle_options[boss_shuffle]
|
||||
elif "bosses" in plando_options:
|
||||
options = boss_shuffle.lower().split(";")
|
||||
remainder_shuffle = "none" # vanilla
|
||||
bosses = []
|
||||
for boss in options:
|
||||
if boss in boss_shuffle_options:
|
||||
remainder_shuffle = boss_shuffle_options[boss]
|
||||
elif "-" in boss:
|
||||
loc, boss_name = boss.split("-")
|
||||
if boss_name not in available_boss_names:
|
||||
raise ValueError(f"Unknown Boss name {boss_name}")
|
||||
if loc not in available_boss_locations:
|
||||
raise ValueError(f"Unknown Boss Location {loc}")
|
||||
level = ''
|
||||
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
|
||||
# split off level
|
||||
loc = loc.split(" ")
|
||||
level = f" {loc[-1]}"
|
||||
loc = " ".join(loc[:-1])
|
||||
loc = loc.title().replace("Of", "of")
|
||||
if not Bosses.can_place_boss(boss_name.title(), loc, level):
|
||||
raise ValueError(f"Cannot place {boss_name} at {loc}{level}")
|
||||
bosses.append(boss)
|
||||
elif boss not in available_boss_names:
|
||||
raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.")
|
||||
else:
|
||||
bosses.append(boss)
|
||||
return ";".join(bosses + [remainder_shuffle])
|
||||
else:
|
||||
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
|
||||
|
||||
|
||||
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses"))):
|
||||
if "pre_rolled" in weights:
|
||||
pre_rolled = weights["pre_rolled"]
|
||||
|
@ -380,10 +418,25 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
|||
item["world"],
|
||||
item["from_pool"],
|
||||
item["force"]) for item in pre_rolled["plando_items"]]
|
||||
if "items" not in plando_options and pre_rolled["plando_items"]:
|
||||
raise Exception("Item Plando is turned off. Reusing this pre-rolled setting not permitted.")
|
||||
|
||||
if "plando_connections" in pre_rolled:
|
||||
pre_rolled["plando_connections"] = [PlandoConnection(connection["entrance"],
|
||||
connection["exit"],
|
||||
connection["direction"]) for connection in pre_rolled["plando_connections"]]
|
||||
if "connections" not in plando_options and pre_rolled["plando_connections"]:
|
||||
raise Exception("Connection Plando is turned off. Reusing this pre-rolled setting not permitted.")
|
||||
|
||||
if "bosses" not in plando_options:
|
||||
try:
|
||||
pre_rolled["shufflebosses"] = get_plando_bosses(pre_rolled["shufflebosses"], plando_options)
|
||||
except Exception as ex:
|
||||
raise Exception("Boss Plando is turned off. Reusing this pre-rolled setting not permitted.") from ex
|
||||
|
||||
if pre_rolled.get("plando_texts") and "texts" not in plando_options:
|
||||
raise Exception("Text Plando is turned off. Reusing this pre-rolled setting not permitted.")
|
||||
|
||||
return argparse.Namespace(**pre_rolled)
|
||||
|
||||
if "linked_options" in weights:
|
||||
|
@ -514,23 +567,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
|||
ret.item_functionality = get_choice('item_functionality', weights)
|
||||
|
||||
boss_shuffle = get_choice('boss_shuffle', weights)
|
||||
|
||||
if boss_shuffle in boss_shuffle_options:
|
||||
ret.shufflebosses = boss_shuffle_options[boss_shuffle]
|
||||
elif "bosses" in plando_options:
|
||||
options = boss_shuffle.lower().split(";")
|
||||
remainder_shuffle = "none" # vanilla
|
||||
bosses = []
|
||||
for boss in options:
|
||||
if boss in boss_shuffle_options:
|
||||
remainder_shuffle = boss_shuffle_options[boss]
|
||||
elif boss not in available_boss_names and not "-" in boss:
|
||||
raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.")
|
||||
else:
|
||||
bosses.append(boss)
|
||||
ret.shufflebosses = ";".join(bosses + [remainder_shuffle])
|
||||
else:
|
||||
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
|
||||
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
|
||||
|
||||
ret.enemy_shuffle = {'none': False,
|
||||
'shuffled': 'shuffled',
|
||||
|
@ -601,7 +638,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
|||
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"}\
|
||||
.get(medallion.lower(), None)
|
||||
if not ret.required_medallions[index]:
|
||||
raise Exception(f"unknown Medallion {medallion}")
|
||||
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
|
||||
|
||||
inventoryweights = weights.get('startinventory', {})
|
||||
startitems = []
|
||||
|
|
5
Utils.py
5
Utils.py
|
@ -52,7 +52,10 @@ def snes_to_pc(value):
|
|||
def parse_player_names(names, players, teams):
|
||||
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
|
||||
if len(names) != len(set(names)):
|
||||
raise ValueError("Duplicate Player names is not supported.")
|
||||
import collections
|
||||
name_counter = collections.Counter(names)
|
||||
raise ValueError(f"Duplicate Player names is not supported, "
|
||||
f'found multiple "{name_counter.most_common(1)[0][0]}".')
|
||||
ret = []
|
||||
while names or len(ret) < teams:
|
||||
team = [n[:16] for n in names[:players]]
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 3px 3px 10px;
|
||||
width: 260px;
|
||||
width: 284px;
|
||||
background-color: #42b149;
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@
|
|||
}
|
||||
|
||||
#location-table{
|
||||
width: 260px;
|
||||
width: 284px;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-bottom: 2px solid #000000;
|
||||
|
@ -61,7 +61,7 @@
|
|||
}
|
||||
|
||||
#location-table td.counter{
|
||||
padding-right: 10px;
|
||||
padding-right: 8px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,12 @@ html{
|
|||
width: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
#tracker-wrapper a{
|
||||
color: #234ae4;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table-wrapper{
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
|
|
|
@ -44,7 +44,8 @@
|
|||
<tbody>
|
||||
{%- for player, items in players.items() -%}
|
||||
<tr>
|
||||
<td><a href="{{ room.tracker|suuid }}/{{ team + 1 }}/{{ player }}">{{ loop.index }}</a></td>
|
||||
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker,
|
||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||
{%- if (team, loop.index) in video -%}
|
||||
{%- if video[(team, loop.index)][0] == "Twitch" -%}
|
||||
<td>
|
||||
|
@ -120,7 +121,8 @@
|
|||
<tbody>
|
||||
{%- for player, checks in players.items() -%}
|
||||
<tr>
|
||||
<td><a href="{{ room.tracker|suuid }}/{{ team + 1 }}/{{ player }}">{{ loop.index }}</a></td>
|
||||
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker,
|
||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||
<td>{{ player_names[(team, loop.index)]|e }}</td>
|
||||
{%- for area in ordered_areas -%}
|
||||
{%- set checks_done = checks[area] -%}
|
||||
|
|
|
@ -164,7 +164,7 @@ tracking_names = ["Progressive Sword", "Progressive Bow", "Book of Mudora", "Ham
|
|||
"Red Boomerang", "Bug Catching Net", "Cape", "Shovel", "Lamp",
|
||||
"Mushroom", "Magic Powder",
|
||||
"Cane of Somaria", "Cane of Byrna", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake",
|
||||
"Bottle", "Triforce"] # TODO make sure this list has what we need and sort it better
|
||||
"Bottle", "Triforce"]
|
||||
|
||||
default_locations = {
|
||||
'Light World': {1572864, 1572865, 60034, 1572867, 1572868, 60037, 1572869, 1572866, 60040, 59788, 60046, 60175,
|
||||
|
@ -240,14 +240,18 @@ for item in tracking_names:
|
|||
|
||||
small_key_ids = {}
|
||||
big_key_ids = {}
|
||||
ids_small_key = {}
|
||||
ids_big_key = {}
|
||||
|
||||
for item_name, data in Items.item_table.items():
|
||||
if "Key" in item_name:
|
||||
area = item_name.split("(")[1][:-1]
|
||||
if "Small" in item_name:
|
||||
small_key_ids[area] = data[2]
|
||||
ids_small_key[data[2]] = area
|
||||
else:
|
||||
big_key_ids[area] = data[2]
|
||||
ids_big_key[data[2]] = area
|
||||
|
||||
from MultiServer import get_item_name_from_id
|
||||
|
||||
|
@ -318,16 +322,25 @@ def get_static_room_data(room: Room):
|
|||
for playernumber in range(1, len(names[0]) + 1)}
|
||||
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][f'{playernumber}'])
|
||||
for playernumber in range(1, len(names[0]) + 1)}
|
||||
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area
|
||||
|
||||
player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
|
||||
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
|
||||
for _, (item_id, item_player) in multidata["locations"]:
|
||||
if item_id in ids_big_key:
|
||||
player_big_key_locations[item_player].add(ids_big_key[item_id])
|
||||
if item_id in ids_small_key:
|
||||
player_small_key_locations[item_player].add(ids_small_key[item_id])
|
||||
|
||||
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations
|
||||
_multidata_cache[room.seed.id] = result
|
||||
return result
|
||||
|
||||
|
||||
@app.route('/tracker/<suuid:tracker>/<int:team>/<int:player>')
|
||||
@app.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
|
||||
@cache.memoize(timeout=15)
|
||||
def getPlayerTracker(tracker: UUID, team: int, player: int):
|
||||
def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
# Team and player must be positive and greater than zero
|
||||
if team < 1 or player < 1:
|
||||
if tracked_team < 0 or tracked_player < 1:
|
||||
abort(404)
|
||||
|
||||
room = Room.get(tracker=tracker)
|
||||
|
@ -335,15 +348,15 @@ def getPlayerTracker(tracker: UUID, team: int, player: int):
|
|||
abort(404)
|
||||
|
||||
# Collect seed information and pare it down to a single player
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area = get_static_room_data(room)
|
||||
player_name = names[team - 1][player - 1]
|
||||
seed_checks_in_area = seed_checks_in_area[player]
|
||||
player_location_to_area = player_location_to_area[player]
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations = get_static_room_data(room)
|
||||
player_name = names[tracked_team][tracked_player - 1]
|
||||
seed_checks_in_area = seed_checks_in_area[tracked_player]
|
||||
location_to_area = player_location_to_area[tracked_player]
|
||||
inventory = collections.Counter()
|
||||
checks_done = {loc_name: 0 for loc_name in default_locations}
|
||||
|
||||
# Add starting items to inventory
|
||||
starting_items = room.seed.multidata.get("precollected_items", None)[player - 1]
|
||||
starting_items = room.seed.multidata.get("precollected_items", None)[tracked_player - 1]
|
||||
if starting_items:
|
||||
for item_id in starting_items:
|
||||
attribute_item_solo(inventory, item_id)
|
||||
|
@ -352,26 +365,24 @@ def getPlayerTracker(tracker: UUID, team: int, player: int):
|
|||
for (ms_team, ms_player), locations_checked in room.multisave.get("location_checks", {}):
|
||||
# logging.info(f"{ms_team}, {ms_player}, {locations_checked}")
|
||||
# Skip teams and players not matching the request
|
||||
if ms_team != (team - 1):
|
||||
continue
|
||||
|
||||
# If the player does not have the item, do nothing
|
||||
for location in locations_checked:
|
||||
if (location, ms_player) not in locations or location not in player_location_to_area:
|
||||
continue
|
||||
if ms_team == tracked_team:
|
||||
# If the player does not have the item, do nothing
|
||||
for location in locations_checked:
|
||||
if (location, ms_player) not in locations:
|
||||
continue
|
||||
|
||||
item, recipient = locations[location, ms_player]
|
||||
if recipient == player:
|
||||
attribute_item_solo(inventory, item)
|
||||
if ms_player != player:
|
||||
continue
|
||||
checks_done[player_location_to_area[location]] += 1
|
||||
checks_done["Total"] += 1
|
||||
item, recipient = locations[location, ms_player]
|
||||
if recipient == tracked_player: # a check done for the tracked player
|
||||
attribute_item_solo(inventory, item)
|
||||
if ms_player == tracked_player: # a check done by the tracked player
|
||||
checks_done[location_to_area[location]] += 1
|
||||
checks_done["Total"] += 1
|
||||
|
||||
# Note the presence of the triforce item
|
||||
for (ms_team, ms_player), game_state in room.multisave.get("client_game_state", []):
|
||||
# Skip teams and players not matching the request
|
||||
if ms_team != (team - 1) or ms_player != player:
|
||||
if ms_team != tracked_team or ms_player != tracked_player:
|
||||
continue
|
||||
|
||||
if game_state:
|
||||
|
@ -462,8 +473,9 @@ def getPlayerTracker(tracker: UUID, team: int, player: int):
|
|||
sword_url=sword_url, sword_acquired=sword_acquired, gloves_url=gloves_url,
|
||||
gloves_acquired=gloves_acquired, bow_url=bow_url, bow_acquired=bow_acquired,
|
||||
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
|
||||
key_locations=key_locations, big_key_locations=big_key_locations, mail_url=mail_url,
|
||||
shield_url=shield_url, shield_acquired=shield_acquired)
|
||||
key_locations=player_small_key_locations[tracked_player],
|
||||
big_key_locations=player_big_key_locations[tracked_player],
|
||||
mail_url=mail_url, shield_url=shield_url, shield_acquired=shield_acquired)
|
||||
|
||||
|
||||
@app.route('/tracker/<suuid:tracker>')
|
||||
|
@ -472,7 +484,7 @@ def getTracker(tracker: UUID):
|
|||
room = Room.get(tracker=tracker)
|
||||
if not room:
|
||||
abort(404)
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area = get_static_room_data(room)
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations = get_static_room_data(room)
|
||||
|
||||
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
|
|
|
@ -90,6 +90,7 @@ entrance_shuffle:
|
|||
# you can also define entrance shuffle seed, like so:
|
||||
crossed-1000: 0 # using this method, you can have the same layout as another player and share entrance information
|
||||
# however, many other settings like logic, world state, retro etc. may affect the shuffle result as well.
|
||||
crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed
|
||||
goals:
|
||||
ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon
|
||||
fast_ganon: 0 # Only killing Ganon is required. However, items may still be placed in GT
|
||||
|
|
|
@ -147,6 +147,22 @@ boss_table = {
|
|||
'Agahnim2': ('Agahnim2', AgahnimDefeatRule)
|
||||
}
|
||||
|
||||
boss_location_table = [
|
||||
['Ganons Tower', 'top'],
|
||||
['Tower of Hera', None],
|
||||
['Skull Woods', None],
|
||||
['Ganons Tower', 'middle'],
|
||||
['Eastern Palace', None],
|
||||
['Desert Palace', None],
|
||||
['Palace of Darkness', None],
|
||||
['Swamp Palace', None],
|
||||
['Thieves Town', None],
|
||||
['Ice Palace', None],
|
||||
['Misery Mire', None],
|
||||
['Turtle Rock', None],
|
||||
['Ganons Tower', 'bottom'],
|
||||
]
|
||||
|
||||
|
||||
def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> bool:
|
||||
# blacklist approach
|
||||
|
@ -183,21 +199,7 @@ def place_bosses(world, player: int):
|
|||
if world.boss_shuffle[player] == 'none':
|
||||
return
|
||||
# Most to least restrictive order
|
||||
boss_locations = [
|
||||
['Ganons Tower', 'top'],
|
||||
['Tower of Hera', None],
|
||||
['Skull Woods', None],
|
||||
['Ganons Tower', 'middle'],
|
||||
['Eastern Palace', None],
|
||||
['Desert Palace', None],
|
||||
['Palace of Darkness', None],
|
||||
['Swamp Palace', None],
|
||||
['Thieves Town', None],
|
||||
['Ice Palace', None],
|
||||
['Misery Mire', None],
|
||||
['Turtle Rock', None],
|
||||
['Ganons Tower', 'bottom'],
|
||||
]
|
||||
boss_locations = boss_location_table.copy()
|
||||
|
||||
all_bosses = sorted(boss_table.keys()) # sorted to be deterministic on older pythons
|
||||
placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
|
||||
|
@ -217,13 +219,13 @@ def place_bosses(world, player: int):
|
|||
loc = loc.split(" ")
|
||||
level = loc[-1]
|
||||
loc = " ".join(loc[:-1])
|
||||
loc = loc.title()
|
||||
loc = loc.title().replace("Of", "of")
|
||||
if can_place_boss(boss, loc, level) and [loc, level] in boss_locations:
|
||||
place_boss(world, player, boss, loc, level)
|
||||
already_placed_bosses.append(boss)
|
||||
boss_locations.remove([loc, level])
|
||||
else:
|
||||
Exception("Cannot place", boss, "at", loc, level, "for player", player)
|
||||
raise Exception("Cannot place", boss, "at", loc, level, "for player", player)
|
||||
else:
|
||||
boss = boss.title()
|
||||
boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations)
|
||||
|
|
Loading…
Reference in New Issue