forked from mirror/Archipelago
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d19868119 | ||
|
|
840e634161 | ||
|
|
731eef8c2f | ||
|
|
135ee018a9 | ||
|
|
7633392eea | ||
|
|
daea0f3e5e | ||
|
|
c525c80b49 | ||
|
|
311fb04647 | ||
|
|
219bd9c10e | ||
|
|
6d704eadd7 | ||
|
|
32da1993e1 | ||
|
|
d4cad980e5 | ||
|
|
53340ab22c | ||
|
|
2d3767a35c | ||
|
|
aaa9bc906e | ||
|
|
7503317d49 | ||
|
|
3fc93a33c8 | ||
|
|
d7d1d54a0b | ||
|
|
34b9344084 | ||
|
|
779f3a8a61 | ||
|
|
8c1690ef65 | ||
|
|
85f32d9a97 | ||
|
|
54c7ec5873 | ||
|
|
8d260708d3 | ||
|
|
f8009e4b84 | ||
|
|
a2260ee6b2 | ||
|
|
6193eafb7b | ||
|
|
a4eea3325f | ||
|
|
b93e61b758 | ||
|
|
14448ad97e | ||
|
|
3d17f0d588 | ||
|
|
ee5ea09cbc | ||
|
|
aac8ca97ed | ||
|
|
e4d6da47a4 | ||
|
|
9f7dbb394e | ||
|
|
f98063b97a | ||
|
|
ed607bdc37 | ||
|
|
a3c3e4cbd4 | ||
|
|
bffb8a034e | ||
|
|
8242d4fe92 | ||
|
|
279b682ac2 | ||
|
|
43ff476d98 | ||
|
|
28201a6c38 | ||
|
|
6923800081 | ||
|
|
700b83572e | ||
|
|
6e53cb2deb | ||
|
|
8e04182b3f | ||
|
|
9fd6d1b81f | ||
|
|
60379d9ae6 | ||
|
|
29ba1d4809 | ||
|
|
dc4b064c73 | ||
|
|
0f20888563 | ||
|
|
2361f8f9d3 | ||
|
|
feba54d5d2 | ||
|
|
3cecab25c7 | ||
|
|
814851ba60 | ||
|
|
6333cc3bea | ||
|
|
00bf9c569a | ||
|
|
6def1bce25 | ||
|
|
3ab5c90d7c | ||
|
|
0507d6923e | ||
|
|
e85baa8068 | ||
|
|
cbed5a0c14 | ||
|
|
e0628ec6c9 | ||
|
|
82637ff072 | ||
|
|
a95a18a8b5 | ||
|
|
d36637ed13 | ||
|
|
dd5e5dcda7 | ||
|
|
0ff7fe8479 | ||
|
|
8c638bcfd8 | ||
|
|
0bd252e7f5 | ||
|
|
ddd3073132 | ||
|
|
1788422abc |
142
BaseClasses.py
142
BaseClasses.py
@@ -6,12 +6,29 @@ import logging
|
||||
import json
|
||||
import functools
|
||||
from collections import OrderedDict, Counter, deque
|
||||
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple
|
||||
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, TYPE_CHECKING, Callable
|
||||
import secrets
|
||||
import random
|
||||
|
||||
import Options
|
||||
import Utils
|
||||
import NetUtils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from worlds import AutoWorld
|
||||
|
||||
auto_world = AutoWorld.World
|
||||
else:
|
||||
auto_world = object
|
||||
|
||||
|
||||
class Group(TypedDict, total=False):
|
||||
name: str
|
||||
game: str
|
||||
world: auto_world
|
||||
players: Set[int]
|
||||
item_pool: Set[str]
|
||||
replacement_items: Dict[int, Optional[str]]
|
||||
|
||||
|
||||
class MultiWorld():
|
||||
@@ -26,8 +43,10 @@ class MultiWorld():
|
||||
plando_items: List
|
||||
plando_connections: List
|
||||
worlds: Dict[int, Any]
|
||||
groups: Dict[int, Group]
|
||||
is_race: bool = False
|
||||
precollected_items: Dict[int, List[Item]]
|
||||
state: CollectionState
|
||||
|
||||
class AttributeProxy():
|
||||
def __init__(self, rule):
|
||||
@@ -39,16 +58,17 @@ class MultiWorld():
|
||||
def __init__(self, players: int):
|
||||
self.random = random.Random() # world-local random state is saved for multiple generations running concurrently
|
||||
self.players = players
|
||||
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
|
||||
self.glitch_triforce = False
|
||||
self.algorithm = 'balanced'
|
||||
self.dungeons: Dict[Tuple[str, int], Dungeon] = {}
|
||||
self.groups = {}
|
||||
self.regions = []
|
||||
self.shops = []
|
||||
self.itempool = []
|
||||
self.seed = None
|
||||
self.seed_name: str = "Unavailable"
|
||||
self.precollected_items = {player: [] for player in self.player_ids}
|
||||
self.state = CollectionState(self)
|
||||
self._cached_entrances = None
|
||||
self._cached_locations = None
|
||||
self._entrance_cache = {}
|
||||
@@ -63,8 +83,6 @@ class MultiWorld():
|
||||
self.custom = False
|
||||
self.customitemarray = []
|
||||
self.shuffle_ganon = True
|
||||
self.dynamic_regions = []
|
||||
self.dynamic_locations = []
|
||||
self.spoiler = Spoiler(self)
|
||||
self.fix_trock_doors = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
||||
@@ -130,6 +148,40 @@ class MultiWorld():
|
||||
self.worlds = {}
|
||||
self.slot_seeds = {}
|
||||
|
||||
def get_all_ids(self):
|
||||
return self.player_ids + tuple(self.groups)
|
||||
|
||||
def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
|
||||
"""Create a group with name and return the assigned player ID and group.
|
||||
If a group of this name already exists, the set of players is extended instead of creating a new one."""
|
||||
for group_id, group in self.groups.items():
|
||||
if group["name"] == name:
|
||||
group["players"] |= players
|
||||
return group_id, group
|
||||
new_id: int = self.players + len(self.groups) + 1
|
||||
from worlds import AutoWorld
|
||||
self.game[new_id] = game
|
||||
self.custom_data[new_id] = {}
|
||||
self.player_types[new_id] = NetUtils.SlotType.group
|
||||
world_type = AutoWorld.AutoWorldRegister.world_types[game]
|
||||
for option_key, option in world_type.options.items():
|
||||
getattr(self, option_key)[new_id] = option(option.default)
|
||||
for option_key, option in Options.common_options.items():
|
||||
getattr(self, option_key)[new_id] = option(option.default)
|
||||
for option_key, option in Options.per_game_common_options.items():
|
||||
getattr(self, option_key)[new_id] = option(option.default)
|
||||
|
||||
self.worlds[new_id] = world_type(self, new_id)
|
||||
self.player_name[new_id] = name
|
||||
|
||||
new_group = self.groups[new_id] = Group(name=name, game=game, players=players,
|
||||
world=self.worlds[new_id])
|
||||
|
||||
return new_id, new_group
|
||||
|
||||
def get_player_groups(self, player) -> Set[int]:
|
||||
return {group_id for group_id, group in self.groups.items() if player in group["players"]}
|
||||
|
||||
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
|
||||
self.seed = get_seed(seed)
|
||||
if secure:
|
||||
@@ -142,23 +194,56 @@ class MultiWorld():
|
||||
|
||||
def set_options(self, args):
|
||||
from worlds import AutoWorld
|
||||
for option_key in Options.common_options:
|
||||
setattr(self, option_key, getattr(args, option_key, {}))
|
||||
for option_key in Options.per_game_common_options:
|
||||
setattr(self, option_key, getattr(args, option_key, {}))
|
||||
|
||||
for player in self.player_ids:
|
||||
self.custom_data[player] = {}
|
||||
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
||||
for option_key in world_type.options:
|
||||
setattr(self, option_key, getattr(args, option_key, {}))
|
||||
for option_key in Options.common_options:
|
||||
setattr(self, option_key, getattr(args, option_key, {}))
|
||||
for option_key in Options.per_game_common_options:
|
||||
setattr(self, option_key, getattr(args, option_key, {}))
|
||||
|
||||
self.worlds[player] = world_type(self, player)
|
||||
|
||||
def set_item_links(self):
|
||||
item_links = {}
|
||||
|
||||
for player in self.player_ids:
|
||||
for item_link in self.item_links[player].value:
|
||||
if item_link["name"] in item_links:
|
||||
item_links[item_link["name"]]["players"][player] = item_link["replacement_item"]
|
||||
item_links[item_link["name"]]["item_pool"] &= set(item_link["item_pool"])
|
||||
else:
|
||||
if item_link["name"] in self.player_name.values():
|
||||
raise Exception(f"Cannot name a ItemLink group the same as a player ({item_link['name']}).")
|
||||
item_links[item_link["name"]] = {
|
||||
"players": {player: item_link["replacement_item"]},
|
||||
"item_pool": set(item_link["item_pool"]),
|
||||
"game": self.game[player]
|
||||
}
|
||||
|
||||
for name, item_link in item_links.items():
|
||||
current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups
|
||||
pool = set()
|
||||
for item in item_link["item_pool"]:
|
||||
pool |= current_item_name_groups.get(item, {item})
|
||||
item_link["item_pool"] = pool
|
||||
|
||||
for group_name, item_link in item_links.items():
|
||||
game = item_link["game"]
|
||||
group_id, group = self.add_group(group_name, game, set(item_link["players"]))
|
||||
group["item_pool"] = item_link["item_pool"]
|
||||
group["replacement_items"] = item_link["players"]
|
||||
|
||||
# intended for unittests
|
||||
def set_default_common_options(self):
|
||||
for option_key, option in Options.common_options.items():
|
||||
setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids})
|
||||
for option_key, option in Options.per_game_common_options.items():
|
||||
setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids})
|
||||
self.state = CollectionState(self)
|
||||
|
||||
def secure(self):
|
||||
self.random = secrets.SystemRandom()
|
||||
@@ -174,7 +259,8 @@ class MultiWorld():
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_game_worlds(self, game_name: str):
|
||||
return tuple(world for player, world in self.worlds.items() if self.game[player] == game_name)
|
||||
return tuple(world for player, world in self.worlds.items() if
|
||||
player not in self.groups and self.game[player] == game_name)
|
||||
|
||||
def get_name_string_for_object(self, obj) -> str:
|
||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
|
||||
@@ -241,10 +327,9 @@ class MultiWorld():
|
||||
|
||||
for item in self.itempool:
|
||||
self.worlds[item.player].collect(ret, item)
|
||||
from worlds.alttp.Dungeons import get_dungeon_item_pool
|
||||
for item in get_dungeon_item_pool(self):
|
||||
subworld = self.worlds[item.player]
|
||||
if item.name in subworld.dungeon_local_item_names:
|
||||
for player in self.player_ids:
|
||||
subworld = self.worlds[player]
|
||||
for item in subworld.get_pre_fill_items():
|
||||
subworld.collect(ret, item)
|
||||
ret.sweep_for_events()
|
||||
|
||||
@@ -252,7 +337,7 @@ class MultiWorld():
|
||||
self._all_state = ret
|
||||
return ret
|
||||
|
||||
def get_items(self) -> list:
|
||||
def get_items(self) -> List[Item]:
|
||||
return [loc.item for loc in self.get_filled_locations()] + self.itempool
|
||||
|
||||
def find_item_locations(self, item, player: int) -> List[Location]:
|
||||
@@ -471,20 +556,24 @@ class MultiWorld():
|
||||
return False
|
||||
|
||||
|
||||
class CollectionState(object):
|
||||
class CollectionState():
|
||||
additional_init_functions: List[Callable] = []
|
||||
additional_copy_functions: List[Callable] = []
|
||||
|
||||
def __init__(self, parent: MultiWorld):
|
||||
self.prog_items = Counter()
|
||||
self.world = parent
|
||||
self.reachable_regions = {player: set() for player in range(1, parent.players + 1)}
|
||||
self.blocked_connections = {player: set() for player in range(1, parent.players + 1)}
|
||||
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
|
||||
self.blocked_connections = {player: set() for player in parent.get_all_ids()}
|
||||
self.events = set()
|
||||
self.path = {}
|
||||
self.locations_checked = set()
|
||||
self.stale = {player: True for player in range(1, parent.players + 1)}
|
||||
self.stale = {player: True for player in parent.get_all_ids()}
|
||||
for items in parent.precollected_items.values():
|
||||
for item in items:
|
||||
self.collect(item, True)
|
||||
for function in self.additional_init_functions:
|
||||
function(self, parent)
|
||||
|
||||
def update_reachable_regions(self, player: int):
|
||||
from worlds.alttp.EntranceShuffle import indirect_connections
|
||||
@@ -523,12 +612,14 @@ class CollectionState(object):
|
||||
ret = CollectionState(self.world)
|
||||
ret.prog_items = self.prog_items.copy()
|
||||
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in
|
||||
range(1, self.world.players + 1)}
|
||||
self.reachable_regions}
|
||||
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in
|
||||
range(1, self.world.players + 1)}
|
||||
self.blocked_connections}
|
||||
ret.events = copy.copy(self.events)
|
||||
ret.path = copy.copy(self.path)
|
||||
ret.locations_checked = copy.copy(self.locations_checked)
|
||||
for function in self.additional_copy_functions:
|
||||
ret = function(self, ret)
|
||||
return ret
|
||||
|
||||
def can_reach(self, spot, resolution_hint=None, player=None) -> bool:
|
||||
@@ -841,7 +932,7 @@ class Entrance(object):
|
||||
|
||||
return False
|
||||
|
||||
def connect(self, region: Region, addresses=None, target = None):
|
||||
def connect(self, region: Region, addresses=None, target=None):
|
||||
self.connected_region = region
|
||||
self.target = target
|
||||
self.addresses = addresses
|
||||
@@ -916,6 +1007,7 @@ class LocationProgressType(Enum):
|
||||
PRIORITY = 2
|
||||
EXCLUDED = 3
|
||||
|
||||
|
||||
class Location():
|
||||
# If given as integer, then this is the shop's inventory index
|
||||
shop_slot: Optional[int] = None
|
||||
@@ -925,7 +1017,6 @@ class Location():
|
||||
spot_type = 'Location'
|
||||
game: str = "Generic"
|
||||
show_in_spoiler: bool = True
|
||||
excluded: bool = False
|
||||
crystal: bool = False
|
||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||
always_allow = staticmethod(lambda item, state: False)
|
||||
@@ -1045,6 +1136,7 @@ class Item():
|
||||
|
||||
class Spoiler():
|
||||
world: MultiWorld
|
||||
unreachables: Set[Location]
|
||||
|
||||
def __init__(self, world):
|
||||
self.world = world
|
||||
@@ -1052,7 +1144,7 @@ class Spoiler():
|
||||
self.entrances = OrderedDict()
|
||||
self.medallions = {}
|
||||
self.playthrough = {}
|
||||
self.unreachables = []
|
||||
self.unreachables = set()
|
||||
self.locations = {}
|
||||
self.paths = {}
|
||||
self.shops = []
|
||||
@@ -1204,9 +1296,9 @@ class Spoiler():
|
||||
|
||||
def write_option(option_key: str, option_obj: type(Options.Option)):
|
||||
res = getattr(self.world, option_key)[player]
|
||||
displayname = getattr(option_obj, "displayname", option_key)
|
||||
display_name = getattr(option_obj, "display_name", option_key)
|
||||
try:
|
||||
outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n')
|
||||
outfile.write(f'{display_name + ":":33}{res.get_current_option_name()}\n')
|
||||
except:
|
||||
raise Exception
|
||||
|
||||
|
||||
34
Fill.py
34
Fill.py
@@ -38,7 +38,8 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
|
||||
for items in reachable_items.values() if items]
|
||||
for item in items_to_place:
|
||||
itempool.remove(item)
|
||||
maximum_exploration_state = sweep_from_pool(base_state, itempool)
|
||||
maximum_exploration_state = sweep_from_pool(
|
||||
base_state, itempool + unplaced_items)
|
||||
|
||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
@@ -106,19 +107,23 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
|
||||
placed_item.location = location
|
||||
|
||||
if spot_to_fill is None:
|
||||
# Maybe the game can be beaten anyway?
|
||||
# Can't place this item, move on to the next
|
||||
unplaced_items.append(item_to_place)
|
||||
if world.accessibility[item_to_place.player] != 'minimal' and world.can_beat_game():
|
||||
logging.warning(
|
||||
f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})')
|
||||
continue
|
||||
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. '
|
||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
continue
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
spot_to_fill.locked = lock
|
||||
placements.append(spot_to_fill)
|
||||
spot_to_fill.event = True
|
||||
spot_to_fill.event = item_to_place.advancement
|
||||
|
||||
if len(unplaced_items) > 0 and len(locations) > 0:
|
||||
# There are leftover unplaceable items and locations that won't accept them
|
||||
if world.can_beat_game():
|
||||
logging.warning(
|
||||
f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})')
|
||||
else:
|
||||
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
|
||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
@@ -152,7 +157,7 @@ def distribute_items_restrictive(world: MultiWorld):
|
||||
localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
|
||||
|
||||
locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
|
||||
type: [] for type in LocationProgressType}
|
||||
loc_type: [] for loc_type in LocationProgressType}
|
||||
|
||||
for loc in fill_locations:
|
||||
locations[loc.progress_type].append(loc)
|
||||
@@ -174,8 +179,8 @@ def distribute_items_restrictive(world: MultiWorld):
|
||||
if nonexcludeditempool:
|
||||
world.random.shuffle(defaultlocations)
|
||||
# needs logical fill to not conflict with local items
|
||||
nonexcludeditempool, defaultlocations = fast_fill(
|
||||
world, nonexcludeditempool, defaultlocations)
|
||||
fill_restrictive(
|
||||
world, world.state, defaultlocations, nonexcludeditempool)
|
||||
if nonexcludeditempool:
|
||||
raise FillError(
|
||||
f'Not enough locations for non-excluded items. There are {len(nonexcludeditempool)} more items than locations')
|
||||
@@ -305,7 +310,7 @@ def balance_multiworld_progression(world: MultiWorld):
|
||||
checked_locations = set()
|
||||
unchecked_locations = set(world.get_locations())
|
||||
|
||||
reachable_locations_count = {player: 0 for player in world.player_ids}
|
||||
reachable_locations_count = {player: 0 for player in world.get_all_ids()}
|
||||
|
||||
def get_sphere_locations(sphere_state, locations):
|
||||
sphere_state.sweep_for_events(key_only=True, locations=locations)
|
||||
@@ -426,7 +431,8 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked=
|
||||
location_1.item.location = location_1
|
||||
location_2.item.location = location_2
|
||||
location_1.event, location_2.event = location_2.event, location_1.event
|
||||
|
||||
|
||||
|
||||
def distribute_planned(world: MultiWorld):
|
||||
def warn(warning: str, force):
|
||||
if force in [True, 'fail', 'failure', 'none', False, 'warn', 'warning']:
|
||||
|
||||
21
Generate.py
21
Generate.py
@@ -14,7 +14,7 @@ ModuleUpdate.update()
|
||||
|
||||
import Utils
|
||||
from worlds.alttp import Options as LttPOptions
|
||||
from worlds.generic import PlandoItem, PlandoConnection
|
||||
from worlds.generic import PlandoConnection
|
||||
from Utils import parse_yaml, version_tuple, __version__, tuplize_version, get_options
|
||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from Main import main as ERmain
|
||||
@@ -180,7 +180,7 @@ def main(args=None, callback=ERmain):
|
||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||
|
||||
if len(set(erargs.name.values())) != len(erargs.name):
|
||||
raise Exception(f"Names have to be unique. Names: {erargs.name}")
|
||||
raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}")
|
||||
|
||||
if args.yaml_output:
|
||||
import yaml
|
||||
@@ -426,17 +426,8 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
|
||||
except Exception as e:
|
||||
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
|
||||
else:
|
||||
# verify item names existing
|
||||
if getattr(player_option, "verify_item_name", False):
|
||||
for item_name in player_option.value:
|
||||
if item_name not in AutoWorldRegister.world_types[ret.game].item_names:
|
||||
raise Exception(f"Item {item_name} from option {player_option} "
|
||||
f"is not a valid item name from {ret.game}")
|
||||
elif getattr(player_option, "verify_location_name", False):
|
||||
for location_name in player_option.value:
|
||||
if location_name not in AutoWorldRegister.world_types[ret.game].location_names:
|
||||
raise Exception(f"Location {location_name} from option {player_option} "
|
||||
f"is not a valid location name from {ret.game}")
|
||||
if hasattr(player_option, "verify"):
|
||||
player_option.verify(AutoWorldRegister.world_types[ret.game])
|
||||
else:
|
||||
setattr(ret, option_key, option(option.default))
|
||||
|
||||
@@ -511,8 +502,12 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
roll_alttp_settings(ret, game_weights, plando_options)
|
||||
else:
|
||||
raise Exception(f"Unsupported game {ret.game}")
|
||||
# not meant to stay here, intended to be removed when itemlinks are stable
|
||||
if not "item_links" in plando_options:
|
||||
ret.item_links.value = []
|
||||
return ret
|
||||
|
||||
|
||||
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
if "dungeon_items" in weights and get_choice_legacy('dungeon_items', weights, "none") != "none":
|
||||
raise Exception(f"dungeon_items key in A Link to the Past was removed, but is present in these weights as {get_choice_legacy('dungeon_items', weights, False)}.")
|
||||
|
||||
4
LICENSE
4
LICENSE
@@ -1,8 +1,8 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 LLCoolDave
|
||||
Copyright (c) 2021 Berserker66
|
||||
Copyright (c) 2021 CaitSith2
|
||||
Copyright (c) 2022 Berserker66
|
||||
Copyright (c) 2022 CaitSith2
|
||||
Copyright (c) 2021 LegendaryLinux
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
||||
109
Main.py
109
Main.py
@@ -1,3 +1,5 @@
|
||||
import copy
|
||||
import collections
|
||||
from itertools import zip_longest, chain
|
||||
import logging
|
||||
import os
|
||||
@@ -7,9 +9,9 @@ import concurrent.futures
|
||||
import pickle
|
||||
import tempfile
|
||||
import zipfile
|
||||
from typing import Dict, Tuple, Optional
|
||||
from typing import Dict, Tuple, Optional, Set
|
||||
|
||||
from BaseClasses import Item, MultiWorld, CollectionState, Region, RegionType
|
||||
from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location
|
||||
from worlds.alttp.Items import item_name_groups
|
||||
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
|
||||
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
||||
@@ -18,7 +20,6 @@ from Utils import output_path, get_options, __version__, version_tuple
|
||||
from worlds.generic.Rules import locality_rules, exclusion_rules
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
ordered_areas = (
|
||||
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
||||
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
||||
@@ -47,13 +48,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
world.item_functionality = args.item_functionality.copy()
|
||||
world.timer = args.timer.copy()
|
||||
world.goal = args.goal.copy()
|
||||
|
||||
if hasattr(args, "algorithm"): # current GUI options
|
||||
world.algorithm = args.algorithm
|
||||
world.shuffleganon = args.shuffleganon
|
||||
world.custom = args.custom
|
||||
world.customitemarray = args.customitemarray
|
||||
|
||||
world.open_pyramid = args.open_pyramid.copy()
|
||||
world.boss_shuffle = args.shufflebosses.copy()
|
||||
world.enemy_health = args.enemy_health.copy()
|
||||
@@ -77,12 +71,14 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
world.plando_connections = args.plando_connections.copy()
|
||||
world.required_medallions = args.required_medallions.copy()
|
||||
world.game = args.game.copy()
|
||||
world.set_options(args)
|
||||
world.player_name = args.name.copy()
|
||||
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.set_options(args)
|
||||
world.set_item_links()
|
||||
world.state = CollectionState(world)
|
||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
||||
|
||||
logger.info("Found World Types:")
|
||||
@@ -137,9 +133,79 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
for player in world.player_ids:
|
||||
exclusion_rules(world, player, world.exclude_locations[player].value)
|
||||
world.priority_locations[player].value -= world.exclude_locations[player].value
|
||||
for location_name in world.priority_locations[player].value:
|
||||
world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
AutoWorld.call_all(world, "generate_basic")
|
||||
|
||||
# temporary home for item links, should be moved out of Main
|
||||
for group_id, group in world.groups.items():
|
||||
# TODO: remove when LttP options are transitioned over
|
||||
world.difficulty_requirements[group_id] = world.difficulty_requirements[next(iter(group["players"]))]
|
||||
|
||||
def find_common_pool(players: Set[int], shared_pool: Set[str]):
|
||||
advancement = set()
|
||||
counters = {player: {name: 0 for name in shared_pool} for player in players}
|
||||
for item in world.itempool:
|
||||
if item.player in counters and item.name in shared_pool:
|
||||
counters[item.player][item.name] += 1
|
||||
if item.advancement:
|
||||
advancement.add(item.name)
|
||||
|
||||
for item in shared_pool:
|
||||
count = min(counters[player][item] for player in players)
|
||||
if count:
|
||||
for player in players:
|
||||
counters[player][item] = count
|
||||
else:
|
||||
for player in players:
|
||||
del(counters[player][item])
|
||||
return counters, advancement
|
||||
|
||||
common_item_count, common_advancement_items = find_common_pool(group["players"], group["item_pool"])
|
||||
# TODO: fix logic
|
||||
if common_advancement_items:
|
||||
logger.warning(f"Logical requirements for {', '.join(common_advancement_items)} in group {group['name']} "
|
||||
f"will be incorrect.")
|
||||
new_itempool = []
|
||||
for item_name, item_count in next(iter(common_item_count.values())).items():
|
||||
advancement = item_name in common_advancement_items
|
||||
for _ in range(item_count):
|
||||
new_item = group["world"].create_item(item_name)
|
||||
new_item.advancement = advancement
|
||||
new_itempool.append(new_item)
|
||||
|
||||
region = Region("Menu", RegionType.Generic, "ItemLink", group_id, world)
|
||||
world.regions.append(region)
|
||||
locations = region.locations = []
|
||||
for item in world.itempool:
|
||||
count = common_item_count.get(item.player, {}).get(item.name, 0)
|
||||
if count:
|
||||
loc = Location(group_id, f"Item Link: {item.name} -> {world.player_name[item.player]} {count}",
|
||||
None, region)
|
||||
loc.access_rule = lambda state: state.has(item.name, group_id, count)
|
||||
locations.append(loc)
|
||||
loc.place_locked_item(item)
|
||||
common_item_count[item.player][item.name] -= 1
|
||||
else:
|
||||
new_itempool.append(item)
|
||||
|
||||
itemcount = len(world.itempool)
|
||||
world.itempool = new_itempool
|
||||
|
||||
# can produce more items than were removed
|
||||
while itemcount > len(world.itempool):
|
||||
for player in group["players"]:
|
||||
if group["replacement_items"][player]:
|
||||
world.itempool.append(AutoWorld.call_single(world, "create_item", player,
|
||||
group["replacement_items"][player]))
|
||||
else:
|
||||
AutoWorld.call_single(world, "create_filler", player)
|
||||
if any(world.item_links.values()):
|
||||
world._recache()
|
||||
world._all_state = None
|
||||
|
||||
logger.info("Running Item Plando")
|
||||
|
||||
for item in world.itempool:
|
||||
@@ -250,14 +316,22 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
import NetUtils
|
||||
slot_data = {}
|
||||
client_versions = {}
|
||||
minimum_versions = {"server": (0, 1, 8), "clients": client_versions}
|
||||
games = {}
|
||||
minimum_versions = {"server": (0, 2, 4), "clients": client_versions}
|
||||
slot_info = {}
|
||||
names = [[name for player, name in sorted(world.player_name.items())]]
|
||||
for slot in world.player_ids:
|
||||
client_versions[slot] = world.worlds[slot].get_required_client_version()
|
||||
games[slot] = world.game[slot]
|
||||
slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot],
|
||||
world.player_types[slot])
|
||||
for slot, group in world.groups.items():
|
||||
games[slot] = world.game[slot]
|
||||
slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot],
|
||||
group_members=sorted(group["players"]))
|
||||
precollected_items = {player: [item.code for item in world_precollected]
|
||||
for player, world_precollected in world.precollected_items.items()}
|
||||
precollected_hints = {player: set() for player in range(1, world.players + 1)}
|
||||
precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))}
|
||||
|
||||
sending_visible_players = set()
|
||||
|
||||
@@ -288,8 +362,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
multidata = {
|
||||
"slot_data": slot_data,
|
||||
"games": games,
|
||||
"names": [[name for player, name in sorted(world.player_name.items())]],
|
||||
"slot_info": slot_info,
|
||||
"names": names, # TODO: remove around 0.2.5 in favor of slot_info
|
||||
"games": games, # TODO: remove around 0.2.5 in favor of slot_info
|
||||
"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},
|
||||
@@ -311,7 +386,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
multidata = zlib.compress(pickle.dumps(multidata), 9)
|
||||
|
||||
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
|
||||
f.write(bytes([2])) # version of format
|
||||
f.write(bytes([3])) # version of format
|
||||
f.write(multidata)
|
||||
|
||||
multidata_task = pool.submit(write_multidata)
|
||||
@@ -321,7 +396,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
else:
|
||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||
|
||||
# retrieve exceptions via .result() if they occured.
|
||||
# retrieve exceptions via .result() if they occurred.
|
||||
multidata_task.result()
|
||||
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
|
||||
if i % 10 == 0 or i == len(output_file_futures):
|
||||
|
||||
180
MultiServer.py
180
MultiServer.py
@@ -33,7 +33,8 @@ from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_
|
||||
import Utils
|
||||
from Utils import get_item_name_from_id, get_location_name_from_id, \
|
||||
version_tuple, restricted_loads, Version
|
||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission
|
||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
||||
SlotType
|
||||
|
||||
colorama.init()
|
||||
|
||||
@@ -97,6 +98,7 @@ class Context:
|
||||
# team -> slot id -> list of clients authenticated to slot.
|
||||
clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]]
|
||||
locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
|
||||
groups: typing.Dict[int, typing.Set[int]]
|
||||
save_version = 2
|
||||
|
||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||
@@ -104,6 +106,7 @@ class Context:
|
||||
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
|
||||
log_network: bool = False):
|
||||
super(Context, self).__init__()
|
||||
self.slot_info: typing.Dict[int, NetworkSlot] = {}
|
||||
self.log_network = log_network
|
||||
self.endpoints = []
|
||||
self.clients = {}
|
||||
@@ -156,6 +159,7 @@ class Context:
|
||||
self.games: typing.Dict[int, str] = {}
|
||||
self.minimum_client_versions: typing.Dict[int, Utils.Version] = {}
|
||||
self.seed_name = ""
|
||||
self.groups = {}
|
||||
self.random = random.Random()
|
||||
|
||||
# General networking
|
||||
@@ -261,7 +265,7 @@ class Context:
|
||||
@staticmethod
|
||||
def decompress(data: bytes) -> dict:
|
||||
format_version = data[0]
|
||||
if format_version > 2:
|
||||
if format_version > 3:
|
||||
raise Utils.VersionException("Incompatible multidata.")
|
||||
return restricted_loads(zlib.decompress(data[1:]))
|
||||
|
||||
@@ -292,18 +296,36 @@ class Context:
|
||||
self.slot_data = decoded_obj['slot_data']
|
||||
self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()}
|
||||
for player, loc_data in decoded_obj["er_hint_data"].items()}
|
||||
self.games = decoded_obj["games"]
|
||||
|
||||
# load start inventory:
|
||||
for slot, item_codes in decoded_obj["precollected_items"].items():
|
||||
self.start_inventory[slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
|
||||
|
||||
for team in range(len(decoded_obj['names'])):
|
||||
for slot, hints in decoded_obj["precollected_hints"].items():
|
||||
self.hints[team, slot].update(hints)
|
||||
# declare slots without checks as done, as they're assumed to be spectators
|
||||
for slot, locations in self.locations.items():
|
||||
if not locations:
|
||||
if "slot_info" in decoded_obj:
|
||||
self.slot_info = decoded_obj["slot_info"]
|
||||
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
|
||||
self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items()
|
||||
if slot_info.type == SlotType.group}
|
||||
else:
|
||||
self.games = decoded_obj["games"]
|
||||
self.groups = {}
|
||||
self.slot_info = {
|
||||
slot: NetworkSlot(
|
||||
self.player_names[0, slot],
|
||||
self.games[slot],
|
||||
SlotType(int(bool(locations))))
|
||||
for slot, locations in self.locations.items()
|
||||
}
|
||||
|
||||
# declare slots that aren't players as done
|
||||
for slot, slot_info in self.slot_info.items():
|
||||
if slot_info.type.always_goal:
|
||||
for team in self.clients:
|
||||
self.client_game_state[team, slot] = ClientStatus.CLIENT_GOAL
|
||||
|
||||
if use_embedded_server_options:
|
||||
server_options = decoded_obj.get("server_options", {})
|
||||
self._set_options(server_options)
|
||||
@@ -398,7 +420,7 @@ class Context:
|
||||
self.received_items[(*old, False)] = items.copy()
|
||||
for (team, slot, remote) in self.received_items:
|
||||
# remove start inventory from items, since this is separate now
|
||||
start_inventory = get_start_inventory(self, team, slot, slot in self.remote_start_inventory)
|
||||
start_inventory = get_start_inventory(self, slot, slot in self.remote_start_inventory)
|
||||
if start_inventory:
|
||||
del self.received_items[team, slot, remote][:len(start_inventory)]
|
||||
logging.info("Upgraded save data")
|
||||
@@ -420,8 +442,9 @@ class Context:
|
||||
self.location_checks.update(savedata["location_checks"])
|
||||
if "random_state" in savedata:
|
||||
self.random.setstate(savedata["random_state"])
|
||||
logging.info(f'Loaded save file with {sum([len(p) for p in self.received_items.values()])} received items '
|
||||
f'for {len(self.received_items)} players')
|
||||
# count items and slots from lists for item_handling = remote
|
||||
logging.info(f'Loaded save file with {sum([len(v) for k,v in self.received_items.items() if k[2]])} received items '
|
||||
f'for {sum(k[2] for k in self.received_items)} players')
|
||||
|
||||
# rest
|
||||
|
||||
@@ -478,20 +501,26 @@ class Context:
|
||||
|
||||
|
||||
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]):
|
||||
"""Send and remember hints"""
|
||||
concerns = collections.defaultdict(list)
|
||||
for hint in hints:
|
||||
net_msg = hint.as_network_message()
|
||||
concerns[hint.receiving_player].append(net_msg)
|
||||
if not hint.local:
|
||||
concerns[hint.finding_player].append(net_msg)
|
||||
# remember hints in all cases
|
||||
if not hint.found:
|
||||
ctx.hints[team, hint.finding_player].add(hint)
|
||||
ctx.hints[team, hint.receiving_player].add(hint)
|
||||
for text in (format_hint(ctx, team, hint) for hint in hints):
|
||||
logging.info("Notice (Team #%d): %s" % (team + 1, text))
|
||||
|
||||
for slot, clients in ctx.clients[team].items():
|
||||
client_hints = concerns[slot]
|
||||
if client_hints:
|
||||
for client in clients:
|
||||
asyncio.create_task(ctx.send_msgs(client, client_hints))
|
||||
if hints:
|
||||
for slot, clients in ctx.clients[team].items():
|
||||
client_hints = concerns[slot]
|
||||
if client_hints:
|
||||
for client in clients:
|
||||
asyncio.create_task(ctx.send_msgs(client, client_hints))
|
||||
|
||||
|
||||
def update_aliases(ctx: Context, team: int):
|
||||
@@ -541,6 +570,8 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||
'cmd': 'RoomInfo',
|
||||
'password': bool(ctx.password),
|
||||
'players': players,
|
||||
# TODO remove around 0.2.5 in favor of slot_info ?
|
||||
# Maybe convert into a list of games that are present to fetch relevant datapackage entries before Connect?
|
||||
'games': [ctx.games[x] for x in range(1, len(ctx.games) + 1)],
|
||||
# tags are for additional features in the communication.
|
||||
# Name them by feature or fork, as you feel is appropriate.
|
||||
@@ -579,7 +610,7 @@ async def on_client_joined(ctx: Context, client: Client):
|
||||
f"{verb} {ctx.games[client.slot]} has joined. "
|
||||
f"Client({version_str}), {client.tags}).")
|
||||
ctx.notify_client(client, "Now that you are connected, "
|
||||
"you can use !help to list commands to run via the server."
|
||||
"you can use !help to list commands to run via the server. "
|
||||
"If your client supports it, "
|
||||
"you may have additional local commands you can list with /help.")
|
||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
@@ -613,14 +644,15 @@ def get_players_string(ctx: Context):
|
||||
current_team = -1
|
||||
text = ''
|
||||
for team, slot in player_names:
|
||||
player_name = ctx.player_names[team, slot]
|
||||
if team != current_team:
|
||||
text += f':: Team #{team + 1}: '
|
||||
current_team = team
|
||||
if (team, slot) in auth_clients:
|
||||
text += f'{player_name} '
|
||||
else:
|
||||
text += f'({player_name}) '
|
||||
if ctx.slot_info[slot].type == SlotType.player:
|
||||
player_name = ctx.player_names[team, slot]
|
||||
if team != current_team:
|
||||
text += f':: Team #{team + 1}: '
|
||||
current_team = team
|
||||
if (team, slot) in auth_clients:
|
||||
text += f'{player_name} '
|
||||
else:
|
||||
text += f'({player_name}) '
|
||||
return f'{len(auth_clients)} players of {len(ctx.player_names)} connected ' + text[:-1]
|
||||
|
||||
|
||||
@@ -641,7 +673,7 @@ def get_received_items(ctx: Context, team: int, player: int, remote_items: bool)
|
||||
return ctx.received_items.setdefault((team, player, remote_items), [])
|
||||
|
||||
|
||||
def get_start_inventory(ctx: Context, team: int, player: int, remote_start_inventory: bool) -> typing.List[NetworkItem]:
|
||||
def get_start_inventory(ctx: Context, player: int, remote_start_inventory: bool) -> typing.List[NetworkItem]:
|
||||
return ctx.start_inventory.setdefault(player, []) if remote_start_inventory else []
|
||||
|
||||
|
||||
@@ -651,7 +683,7 @@ def send_new_items(ctx: Context):
|
||||
for client in clients:
|
||||
if client.no_items:
|
||||
continue
|
||||
start_inventory = get_start_inventory(ctx, team, slot, client.remote_start_inventory)
|
||||
start_inventory = get_start_inventory(ctx, slot, client.remote_start_inventory)
|
||||
items = get_received_items(ctx, team, slot, client.remote_items)
|
||||
if len(start_inventory) + len(items) > client.send_index:
|
||||
first_new_item = max(0, client.send_index - len(start_inventory))
|
||||
@@ -697,6 +729,15 @@ def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||
return sorted(items)
|
||||
|
||||
|
||||
def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem):
|
||||
targets = ctx.groups.get(target_slot, [target_slot])
|
||||
for target in targets:
|
||||
for item in items:
|
||||
if item.player != target_slot:
|
||||
get_received_items(ctx, team, target, False).append(item)
|
||||
get_received_items(ctx, team, target, True).append(item)
|
||||
|
||||
|
||||
def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int],
|
||||
count_activity: bool = True):
|
||||
new_locations = set(locations) - ctx.location_checks[team, slot]
|
||||
@@ -705,17 +746,9 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
if count_activity:
|
||||
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
for location in new_locations:
|
||||
if len(ctx.locations[slot][location]) == 3:
|
||||
item_id, target_player, flags = ctx.locations[slot][location]
|
||||
else:
|
||||
# TODO: remove around version 0.2.5
|
||||
item_id, target_player = ctx.locations[slot][location]
|
||||
flags = 0
|
||||
|
||||
item_id, target_player, flags = ctx.locations[slot][location]
|
||||
new_item = NetworkItem(item_id, location, slot, flags)
|
||||
if target_player != slot:
|
||||
get_received_items(ctx, team, target_player, False).append(new_item)
|
||||
get_received_items(ctx, team, target_player, True).append(new_item)
|
||||
send_items_to(ctx, team, target_player, new_item)
|
||||
|
||||
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
|
||||
@@ -739,12 +772,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[
|
||||
seeked_item_id = proxy_worlds[ctx.games[slot]].item_name_to_id[item]
|
||||
for finding_player, check_data in ctx.locations.items():
|
||||
for location_id, result in check_data.items():
|
||||
if len(result) == 3:
|
||||
item_id, receiving_player, item_flags = result
|
||||
else:
|
||||
# TODO: remove around version 0.2.5
|
||||
item_id, receiving_player = result
|
||||
item_flags = 0
|
||||
item_id, receiving_player, item_flags = result
|
||||
|
||||
if receiving_player == slot and item_id == seeked_item_id:
|
||||
found = location_id in ctx.location_checks[team, finding_player]
|
||||
@@ -755,16 +783,15 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[
|
||||
return hints
|
||||
|
||||
|
||||
def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
|
||||
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
|
||||
seeked_location: int = proxy_worlds[ctx.games[slot]].location_name_to_id[location]
|
||||
return collect_hint_location_id(ctx, team, slot, seeked_location)
|
||||
|
||||
|
||||
def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int) -> typing.List[NetUtils.Hint]:
|
||||
result = ctx.locations[slot].get(seeked_location, (None, None, None))
|
||||
if result:
|
||||
if len(result) == 3:
|
||||
item_id, receiving_player, item_flags = result
|
||||
else:
|
||||
# TODO: remove around version 0.2.5
|
||||
item_id, receiving_player = result
|
||||
item_flags = 0
|
||||
item_id, receiving_player, item_flags = result
|
||||
|
||||
found = seeked_location in ctx.location_checks[team, slot]
|
||||
entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "")
|
||||
@@ -1170,14 +1197,14 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
if hint_name in world.hint_blacklist:
|
||||
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
||||
hints = []
|
||||
elif not for_location and hint_name in world.item_name_groups: # item group name
|
||||
elif not for_location and hint_name in world.item_name_groups: # item group name
|
||||
hints = []
|
||||
for item in world.item_name_groups[hint_name]:
|
||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
|
||||
elif not for_location and hint_name in world.item_names: # item name
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
else: # location name
|
||||
hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
cost = self.ctx.get_hint_cost(self.client.slot)
|
||||
if hints:
|
||||
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
|
||||
@@ -1209,10 +1236,6 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.ctx.hints_used[self.client.team, self.client.slot] += 1
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
|
||||
if not hint.found:
|
||||
self.ctx.hints[self.client.team, hint.finding_player].add(hint)
|
||||
self.ctx.hints[self.client.team, hint.receiving_player].add(hint)
|
||||
|
||||
if not_found_hints:
|
||||
if hints and cost and int((points_available // cost) == 0):
|
||||
self.output(
|
||||
@@ -1300,7 +1323,6 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
errors.add('InvalidPassword')
|
||||
|
||||
if args['name'] not in ctx.connect_names:
|
||||
logging.info((args["name"], ctx.connect_names))
|
||||
errors.add('InvalidSlot')
|
||||
else:
|
||||
team, slot = ctx.connect_names[args['name']]
|
||||
@@ -1347,9 +1369,10 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
"players": ctx.get_players_package(),
|
||||
"missing_locations": get_missing_checks(ctx, team, slot),
|
||||
"checked_locations": get_checked_checks(ctx, team, slot),
|
||||
"slot_data": ctx.slot_data[client.slot]
|
||||
"slot_data": ctx.slot_data[client.slot],
|
||||
"slot_info": ctx.slot_info
|
||||
}]
|
||||
start_inventory = get_start_inventory(ctx, team, slot, client.remote_start_inventory)
|
||||
start_inventory = get_start_inventory(ctx, slot, client.remote_start_inventory)
|
||||
items = get_received_items(ctx, client.team, client.slot, client.remote_items)
|
||||
if (start_inventory or items) and not client.no_items:
|
||||
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": start_inventory + items})
|
||||
@@ -1384,7 +1407,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
if args.get('items_handling', None) is not None and client.items_handling != args['items_handling']:
|
||||
try:
|
||||
client.items_handling = args['items_handling']
|
||||
start_inventory = get_start_inventory(ctx, client.team, client.slot, client.remote_start_inventory)
|
||||
start_inventory = get_start_inventory(ctx, client.slot, client.remote_start_inventory)
|
||||
items = get_received_items(ctx, client.team, client.slot, client.remote_items)
|
||||
if (items or start_inventory) and not client.no_items:
|
||||
client.send_index = len(start_inventory) + len(items)
|
||||
@@ -1408,7 +1431,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
f"from {old_tags} to {client.tags}.")
|
||||
|
||||
elif cmd == 'Sync':
|
||||
start_inventory = get_start_inventory(ctx, client.team, client.slot, client.remote_start_inventory)
|
||||
start_inventory = get_start_inventory(ctx, client.slot, client.remote_start_inventory)
|
||||
items = get_received_items(ctx, client.team, client.slot, client.remote_items)
|
||||
if (start_inventory or items) and not client.no_items:
|
||||
client.send_index = len(start_inventory) + len(items)
|
||||
@@ -1425,21 +1448,20 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
|
||||
elif cmd == 'LocationScouts':
|
||||
locs = []
|
||||
create_as_hint = args.get("create_as_hint", False)
|
||||
hints = []
|
||||
for location in args["locations"]:
|
||||
if type(location) is not int or location not in lookup_any_location_id_to_name:
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
|
||||
"original_cmd": cmd}])
|
||||
return
|
||||
if len(ctx.locations[client.slot][location]) == 3:
|
||||
target_item, target_player, flags = ctx.locations[client.slot][location]
|
||||
else:
|
||||
# TODO: remove around version 0.2.5
|
||||
target_item, target_player = ctx.locations[client.slot][location]
|
||||
flags = 0
|
||||
|
||||
target_item, target_player, flags = ctx.locations[client.slot][location]
|
||||
if create_as_hint:
|
||||
hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location))
|
||||
locs.append(NetworkItem(target_item, location, target_player, flags))
|
||||
|
||||
notify_hints(ctx, client.team, hints)
|
||||
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
|
||||
|
||||
elif cmd == 'StatusUpdate':
|
||||
@@ -1588,8 +1610,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
self.output(f"Could not find player {player_name} to forbid the !forfeit command for.")
|
||||
return False
|
||||
|
||||
def _cmd_send(self, player_name: str, *item_name: str) -> bool:
|
||||
"""Sends an item to the specified player"""
|
||||
def _cmd_send_multiple(self, amount: typing.Union[int, str], player_name: str, *item_name: str) -> bool:
|
||||
"""Sends multiples of an item to the specified player"""
|
||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||
if usable:
|
||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||
@@ -1597,12 +1619,14 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
world = proxy_worlds[self.ctx.games[slot]]
|
||||
item, usable, response = get_intended_text(item, world.item_names)
|
||||
if usable:
|
||||
new_item = NetworkItem(world.item_name_to_id[item], -1, 0)
|
||||
get_received_items(self.ctx, team, slot, True).append(new_item)
|
||||
get_received_items(self.ctx, team, slot, False).append(new_item)
|
||||
self.ctx.notify_all('Cheat console: sending "' + item + '" to ' +
|
||||
self.ctx.get_aliased_name(team, slot))
|
||||
amount: int = int(amount)
|
||||
new_items = [NetworkItem(world.item_name_to_id[item], -1, 0) for i in range(int(amount))]
|
||||
send_items_to(self.ctx, team, slot, *new_items)
|
||||
|
||||
send_new_items(self.ctx)
|
||||
self.ctx.notify_all(
|
||||
'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') +
|
||||
f'"{item}" to {self.ctx.get_aliased_name(team, slot)}')
|
||||
return True
|
||||
else:
|
||||
self.output(response)
|
||||
@@ -1611,6 +1635,10 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
self.output(response)
|
||||
return False
|
||||
|
||||
def _cmd_send(self, player_name: str, *item_name: str) -> bool:
|
||||
"""Sends an item to the specified player"""
|
||||
return self._cmd_send_multiple(1, player_name, *item_name)
|
||||
|
||||
def _cmd_hint(self, player_name: str, *item: str) -> bool:
|
||||
"""Send out a hint for a player's item to their team"""
|
||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||
@@ -1626,8 +1654,10 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
hints.extend(collect_hints(self.ctx, team, slot, item))
|
||||
else: # item name
|
||||
hints = collect_hints(self.ctx, team, slot, item)
|
||||
|
||||
if hints:
|
||||
notify_hints(self.ctx, team, hints)
|
||||
|
||||
else:
|
||||
self.output("No hints found.")
|
||||
return True
|
||||
@@ -1648,7 +1678,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
world = proxy_worlds[self.ctx.games[slot]]
|
||||
item, usable, response = get_intended_text(item, world.location_names)
|
||||
if usable:
|
||||
hints = collect_hints_location(self.ctx, team, slot, item)
|
||||
hints = collect_hint_location_name(self.ctx, team, slot, item)
|
||||
if hints:
|
||||
notify_hints(self.ctx, team, hints)
|
||||
else:
|
||||
@@ -1679,6 +1709,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
||||
if option_name in {"forfeit_mode", "remaining_mode", "collect_mode"}:
|
||||
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
|
||||
elif option_name in {"hint_cost", "location_check_points"}:
|
||||
self.ctx.broadcast_all([{"cmd": "RoomUpdate", option_name: getattr(self.ctx, option_name)}])
|
||||
return True
|
||||
else:
|
||||
known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items())
|
||||
|
||||
26
NetUtils.py
26
NetUtils.py
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import typing
|
||||
import enum
|
||||
from json import JSONEncoder, JSONDecoder
|
||||
@@ -29,7 +28,18 @@ class ClientStatus(enum.IntEnum):
|
||||
CLIENT_GOAL = 30
|
||||
|
||||
|
||||
class Permission(enum.IntEnum):
|
||||
class SlotType(enum.IntFlag):
|
||||
spectator = 0b00
|
||||
player = 0b01
|
||||
group = 0b10
|
||||
|
||||
@property
|
||||
def always_goal(self) -> bool:
|
||||
"""Mark this slot has having reached its goal instantly."""
|
||||
return self.value != 0b01
|
||||
|
||||
|
||||
class Permission(enum.IntFlag):
|
||||
disabled = 0b000 # 0, completely disables access
|
||||
enabled = 0b001 # 1, allows manual use
|
||||
goal = 0b010 # 2, allows manual use after goal completion
|
||||
@@ -49,12 +59,21 @@ class Permission(enum.IntEnum):
|
||||
|
||||
|
||||
class NetworkPlayer(typing.NamedTuple):
|
||||
"""Represents a particular player on a particular team."""
|
||||
team: int
|
||||
slot: int
|
||||
alias: str
|
||||
name: str
|
||||
|
||||
|
||||
class NetworkSlot(typing.NamedTuple):
|
||||
"""Represents a particular slot across teams."""
|
||||
name: str
|
||||
game: str
|
||||
type: SlotType
|
||||
group_members: typing.Union[typing.List[int], typing.Tuple] = () # only populated if type == group
|
||||
|
||||
|
||||
class NetworkItem(typing.NamedTuple):
|
||||
item: int
|
||||
location: int
|
||||
@@ -122,9 +141,6 @@ class Endpoint:
|
||||
def __init__(self, socket):
|
||||
self.socket = socket
|
||||
|
||||
async def disconnect(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HandlerMeta(type):
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
|
||||
@@ -87,7 +87,7 @@ def adjustGUI():
|
||||
option = sfx_options[option_name]
|
||||
optionFrame = Frame(romSettingsFrame)
|
||||
optionFrame.grid(row=row, column=column, sticky=E)
|
||||
optionLabel = Label(optionFrame, text=option.displayname)
|
||||
optionLabel = Label(optionFrame, text=option.display_name)
|
||||
optionLabel.pack(side=LEFT)
|
||||
setattr(opts, option_name, StringVar())
|
||||
getattr(opts, option_name).set(option.name_lookup[option.default])
|
||||
@@ -143,7 +143,7 @@ def adjustGUI():
|
||||
option = cosmetic_options['sword_trail_duration']
|
||||
optionFrame = Frame(romSettingsFrame)
|
||||
optionFrame.grid(row=8, column=2, sticky=E)
|
||||
optionLabel = Label(optionFrame, text=option.displayname)
|
||||
optionLabel = Label(optionFrame, text=option.display_name)
|
||||
optionLabel.pack(side=LEFT)
|
||||
setattr(opts, 'sword_trail_duration', StringVar())
|
||||
getattr(opts, 'sword_trail_duration').set(option.default)
|
||||
|
||||
148
Options.py
148
Options.py
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
import typing
|
||||
import random
|
||||
|
||||
from schema import Schema, And, Or
|
||||
|
||||
|
||||
class AssembleOptions(type):
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
@@ -25,14 +27,28 @@ class AssembleOptions(type):
|
||||
|
||||
# auto-validate schema on __init__
|
||||
if "schema" in attrs.keys():
|
||||
def validate_decorator(func):
|
||||
def validate(self, *args, **kwargs):
|
||||
func(self, *args, **kwargs)
|
||||
|
||||
if "__init__" in attrs:
|
||||
def validate_decorator(func):
|
||||
def validate(self, *args, **kwargs):
|
||||
ret = func(self, *args, **kwargs)
|
||||
self.value = self.schema.validate(self.value)
|
||||
return ret
|
||||
|
||||
return validate
|
||||
attrs["__init__"] = validate_decorator(attrs["__init__"])
|
||||
else:
|
||||
# construct an __init__ that calls parent __init__
|
||||
|
||||
cls = super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
||||
|
||||
def meta__init__(self, *args, **kwargs):
|
||||
super(cls, self).__init__(*args, **kwargs)
|
||||
self.value = self.schema.validate(self.value)
|
||||
|
||||
return validate
|
||||
cls.__init__ = meta__init__
|
||||
return cls
|
||||
|
||||
attrs["__init__"] = validate_decorator(attrs["__init__"])
|
||||
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
@@ -41,9 +57,9 @@ class Option(metaclass=AssembleOptions):
|
||||
name_lookup: typing.Dict[int, str]
|
||||
default = 0
|
||||
|
||||
# convert option_name_long into Name Long as displayname, otherwise name_long is the result.
|
||||
# convert option_name_long into Name Long as display_name, otherwise name_long is the result.
|
||||
# Handled in get_option_name()
|
||||
autodisplayname = False
|
||||
auto_display_name = False
|
||||
|
||||
# can be weighted between selections
|
||||
supports_weighting = True
|
||||
@@ -64,7 +80,7 @@ class Option(metaclass=AssembleOptions):
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: typing.Any) -> str:
|
||||
if cls.autodisplayname:
|
||||
if cls.auto_display_name:
|
||||
return cls.name_lookup[value].replace("_", " ").title()
|
||||
else:
|
||||
return cls.name_lookup[value]
|
||||
@@ -133,7 +149,7 @@ class DefaultOnToggle(Toggle):
|
||||
|
||||
|
||||
class Choice(Option):
|
||||
autodisplayname = True
|
||||
auto_display_name = True
|
||||
|
||||
def __init__(self, value: int):
|
||||
self.value: int = value
|
||||
@@ -143,8 +159,8 @@ class Choice(Option):
|
||||
text = text.lower()
|
||||
if text == "random":
|
||||
return cls(random.choice(list(cls.name_lookup)))
|
||||
for optionname, value in cls.options.items():
|
||||
if optionname == text:
|
||||
for option_name, value in cls.options.items():
|
||||
if option_name == text:
|
||||
return cls(value)
|
||||
raise KeyError(
|
||||
f'Could not find option "{text}" for "{cls.__name__}", '
|
||||
@@ -213,20 +229,22 @@ class Range(Option, int):
|
||||
elif text.startswith("random-range-"):
|
||||
textsplit = text.split("-")
|
||||
try:
|
||||
randomrange = [int(textsplit[len(textsplit)-2]), int(textsplit[len(textsplit)-1])]
|
||||
random_range = [int(textsplit[len(textsplit) - 2]), int(textsplit[len(textsplit) - 1])]
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid random range {text} for option {cls.__name__}")
|
||||
randomrange.sort()
|
||||
if randomrange[0] < cls.range_start or randomrange[1] > cls.range_end:
|
||||
raise Exception(f"{randomrange[0]}-{randomrange[1]} is outside allowed range {cls.range_start}-{cls.range_end} for option {cls.__name__}")
|
||||
random_range.sort()
|
||||
if random_range[0] < cls.range_start or random_range[1] > cls.range_end:
|
||||
raise Exception(
|
||||
f"{random_range[0]}-{random_range[1]} is outside allowed range "
|
||||
f"{cls.range_start}-{cls.range_end} for option {cls.__name__}")
|
||||
if text.startswith("random-range-low"):
|
||||
return cls(int(round(random.triangular(randomrange[0], randomrange[1], randomrange[0]))))
|
||||
return cls(int(round(random.triangular(random_range[0], random_range[1], random_range[0]))))
|
||||
elif text.startswith("random-range-middle"):
|
||||
return cls(int(round(random.triangular(randomrange[0], randomrange[1]))))
|
||||
return cls(int(round(random.triangular(random_range[0], random_range[1]))))
|
||||
elif text.startswith("random-range-high"):
|
||||
return cls(int(round(random.triangular(randomrange[0], randomrange[1], randomrange[1]))))
|
||||
return cls(int(round(random.triangular(random_range[0], random_range[1], random_range[1]))))
|
||||
else:
|
||||
return cls(int(round(random.randint(randomrange[0], randomrange[1]))))
|
||||
return cls(int(round(random.randint(random_range[0], random_range[1]))))
|
||||
else:
|
||||
return cls(random.randint(cls.range_start, cls.range_end))
|
||||
return cls(int(text))
|
||||
@@ -244,26 +262,12 @@ class Range(Option, int):
|
||||
return str(self.value)
|
||||
|
||||
|
||||
class OptionNameSet(Option):
|
||||
default = frozenset()
|
||||
|
||||
def __init__(self, value: typing.Set[str]):
|
||||
self.value: typing.Set[str] = value
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> OptionNameSet:
|
||||
return cls({option.strip() for option in text.split(",")})
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any) -> OptionNameSet:
|
||||
if type(data) == set:
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
|
||||
class VerifyKeys:
|
||||
valid_keys = frozenset()
|
||||
valid_keys_casefold: bool = False
|
||||
verify_item_name = False
|
||||
verify_location_name = False
|
||||
value: typing.Any
|
||||
|
||||
@classmethod
|
||||
def verify_keys(cls, data):
|
||||
@@ -275,6 +279,18 @@ class VerifyKeys:
|
||||
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
|
||||
f"Allowed keys: {cls.valid_keys}.")
|
||||
|
||||
def verify(self, world):
|
||||
if self.verify_item_name:
|
||||
for item_name in self.value:
|
||||
if item_name not in world.item_names:
|
||||
raise Exception(f"Item {item_name} from option {self} "
|
||||
f"is not a valid item name from {world.game}")
|
||||
elif self.verify_location_name:
|
||||
for location_name in self.value:
|
||||
if location_name not in world.world_types[world.game].location_names:
|
||||
raise Exception(f"Location {location_name} from option {self} "
|
||||
f"is not a valid location name from {world.game}")
|
||||
|
||||
|
||||
class OptionDict(Option, VerifyKeys):
|
||||
default = {}
|
||||
@@ -336,7 +352,7 @@ class OptionList(Option, VerifyKeys):
|
||||
return item in self.value
|
||||
|
||||
|
||||
class OptionSet(Option):
|
||||
class OptionSet(Option, VerifyKeys):
|
||||
default = frozenset()
|
||||
supports_weighting = False
|
||||
value: set
|
||||
@@ -352,8 +368,10 @@ class OptionSet(Option):
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
if type(data) == list:
|
||||
cls.verify_keys(data)
|
||||
return cls(data)
|
||||
elif type(data) == set:
|
||||
cls.verify_keys(data)
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
@@ -372,7 +390,7 @@ class Accessibility(Choice):
|
||||
Locations: ensure everything can be reached and acquired.
|
||||
Items: ensure all logically relevant items can be acquired.
|
||||
Minimal: ensure what is needed to reach your goal can be acquired."""
|
||||
displayname = "Accessibility"
|
||||
display_name = "Accessibility"
|
||||
option_locations = 0
|
||||
option_items = 1
|
||||
option_minimal = 2
|
||||
@@ -382,7 +400,7 @@ class Accessibility(Choice):
|
||||
|
||||
class ProgressionBalancing(DefaultOnToggle):
|
||||
"""A system that moves progression earlier, to try and prevent the player from getting stuck and bored early."""
|
||||
displayname = "Progression Balancing"
|
||||
display_name = "Progression Balancing"
|
||||
|
||||
|
||||
common_options = {
|
||||
@@ -398,39 +416,68 @@ class ItemSet(OptionSet):
|
||||
|
||||
class LocalItems(ItemSet):
|
||||
"""Forces these items to be in their native world."""
|
||||
displayname = "Local Items"
|
||||
display_name = "Local Items"
|
||||
|
||||
|
||||
class NonLocalItems(ItemSet):
|
||||
"""Forces these items to be outside their native world."""
|
||||
displayname = "Not Local Items"
|
||||
display_name = "Not Local Items"
|
||||
|
||||
|
||||
class StartInventory(ItemDict):
|
||||
"""Start with these items."""
|
||||
verify_item_name = True
|
||||
displayname = "Start Inventory"
|
||||
display_name = "Start Inventory"
|
||||
|
||||
|
||||
class StartHints(ItemSet):
|
||||
"""Start with these item's locations prefilled into the !hint command."""
|
||||
displayname = "Start Hints"
|
||||
display_name = "Start Hints"
|
||||
|
||||
|
||||
class StartLocationHints(OptionSet):
|
||||
"""Start with these locations and their item prefilled into the !hint command"""
|
||||
displayname = "Start Location Hints"
|
||||
display_name = "Start Location Hints"
|
||||
|
||||
|
||||
class ExcludeLocations(OptionSet):
|
||||
"""Prevent these locations from having an important item"""
|
||||
displayname = "Excluded Locations"
|
||||
display_name = "Excluded Locations"
|
||||
verify_location_name = True
|
||||
|
||||
|
||||
class PriorityLocations(OptionSet):
|
||||
"""Prevent these locations from having an unimportant item"""
|
||||
display_name = "Priority Locations"
|
||||
verify_location_name = True
|
||||
|
||||
|
||||
class DeathLink(Toggle):
|
||||
"""When you die, everyone dies. Of course the reverse is true too."""
|
||||
displayname = "Death Link"
|
||||
display_name = "Death Link"
|
||||
|
||||
|
||||
class ItemLinks(OptionList):
|
||||
"""Share part of your item pool with other players."""
|
||||
default = []
|
||||
schema = Schema([
|
||||
{
|
||||
"name": And(str, len),
|
||||
"item_pool": [And(str, len)],
|
||||
"replacement_item": Or(And(str, len), None)
|
||||
}
|
||||
])
|
||||
|
||||
def verify(self, world):
|
||||
super(ItemLinks, self).verify(world)
|
||||
for link in self.value:
|
||||
for item_name in link["item_pool"]:
|
||||
if item_name not in world.item_names and item_name not in world.item_name_groups:
|
||||
raise Exception(f"Item {item_name} from option {self} "
|
||||
f"is not a valid item name from {world.game}")
|
||||
if link["replacement_item"] and link["replacement_item"] not in world.item_names:
|
||||
raise Exception(f"Item {link['replacement_item']} from option {self} "
|
||||
f"is not a valid item name from {world.game}")
|
||||
|
||||
|
||||
per_game_common_options = {
|
||||
@@ -440,9 +487,12 @@ per_game_common_options = {
|
||||
"start_inventory": StartInventory,
|
||||
"start_hints": StartHints,
|
||||
"start_location_hints": StartLocationHints,
|
||||
"exclude_locations": ExcludeLocations
|
||||
"exclude_locations": ExcludeLocations,
|
||||
"priority_locations": PriorityLocations,
|
||||
"item_links": ItemLinks
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
from worlds.alttp.Options import Logic
|
||||
@@ -450,8 +500,8 @@ if __name__ == "__main__":
|
||||
|
||||
map_shuffle = Toggle
|
||||
compass_shuffle = Toggle
|
||||
keyshuffle = Toggle
|
||||
bigkey_shuffle = Toggle
|
||||
key_shuffle = Toggle
|
||||
big_key_shuffle = Toggle
|
||||
hints = Toggle
|
||||
test = argparse.Namespace()
|
||||
test.logic = Logic.from_text("no_logic")
|
||||
|
||||
8
Utils.py
8
Utils.py
@@ -24,7 +24,7 @@ class Version(typing.NamedTuple):
|
||||
build: int
|
||||
|
||||
|
||||
__version__ = "0.2.4"
|
||||
__version__ = "0.2.5"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
from yaml import load, dump, SafeLoader
|
||||
@@ -182,7 +182,7 @@ def get_default_options() -> dict:
|
||||
"output_path": "output",
|
||||
},
|
||||
"factorio_options": {
|
||||
"executable": "factorio\\bin\\x64\\factorio",
|
||||
"executable": os.path.join("factorio", "bin", "x64", "factorio"),
|
||||
},
|
||||
"sm_options": {
|
||||
"rom_file": "Super Metroid (JU).sfc",
|
||||
@@ -219,7 +219,7 @@ def get_default_options() -> dict:
|
||||
},
|
||||
"generator": {
|
||||
"teams": 1,
|
||||
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
|
||||
"enemizer_path": os.path.join("EnemizerCLI", "EnemizerCLI.Core.exe"),
|
||||
"player_files_path": "Players",
|
||||
"players": 0,
|
||||
"weights_file_path": "weights.yaml",
|
||||
@@ -349,7 +349,7 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||
if module == "builtins" and name in safe_builtins:
|
||||
return getattr(builtins, name)
|
||||
# used by MultiServer -> savegame/multidata
|
||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint"}:
|
||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}:
|
||||
return getattr(self.net_utils_module, name)
|
||||
# Options and Plando are unpickled by WebHost -> Generate
|
||||
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
|
||||
|
||||
@@ -5,6 +5,7 @@ import json
|
||||
import zipfile
|
||||
from collections import Counter
|
||||
from typing import Dict, Optional as TypeOptional
|
||||
from Utils import __version__
|
||||
|
||||
from flask import request, flash, redirect, url_for, session, render_template
|
||||
|
||||
@@ -78,7 +79,7 @@ def generate(race=False):
|
||||
|
||||
return redirect(url_for("view_seed", seed=seed_id))
|
||||
|
||||
return render_template("generate.html", race=race)
|
||||
return render_template("generate.html", race=race, version=__version__)
|
||||
|
||||
|
||||
def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None):
|
||||
@@ -120,7 +121,8 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No
|
||||
if not erargs.name[player]:
|
||||
erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0]
|
||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||
|
||||
if len(set(erargs.name.values())) != len(erargs.name):
|
||||
raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}")
|
||||
ERmain(erargs, seed, baked_server_options=meta)
|
||||
|
||||
return upload_to_db(target.name, sid, owner, race)
|
||||
|
||||
@@ -69,7 +69,7 @@ def create():
|
||||
elif option.options:
|
||||
game_options[option_name] = this_option = {
|
||||
"type": "select",
|
||||
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"defaultValue": None,
|
||||
"options": []
|
||||
@@ -92,7 +92,7 @@ def create():
|
||||
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
|
||||
game_options[option_name] = {
|
||||
"type": "range",
|
||||
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"defaultValue": option.default if hasattr(option, "default") else option.range_start,
|
||||
"min": option.range_start,
|
||||
@@ -102,14 +102,14 @@ def create():
|
||||
elif getattr(option, "verify_item_name", False):
|
||||
game_options[option_name] = {
|
||||
"type": "items-list",
|
||||
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
}
|
||||
|
||||
elif getattr(option, "verify_location_name", False):
|
||||
game_options[option_name] = {
|
||||
"type": "locations-list",
|
||||
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ def create():
|
||||
if option.valid_keys:
|
||||
game_options[option_name] = {
|
||||
"type": "custom-list",
|
||||
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"options": list(option.valid_keys),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
flask>=2.0.2
|
||||
pony>=0.7.14
|
||||
flask>=2.0.3
|
||||
pony>=0.7.16
|
||||
waitress>=2.0.0
|
||||
flask-caching>=1.10.1
|
||||
Flask-Compress>=1.10.1
|
||||
Flask-Limiter>=2.1
|
||||
Flask-Limiter>=2.1.3
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
# Advanced YAML Guide
|
||||
|
||||
This guide covers more the more advanced options available in YAML files. This guide is intended for the user who is
|
||||
intent on editing their YAML file manually. This guide should take about 10 minutes to read.
|
||||
This guide covers more the more advanced options available in YAML files. This guide is intended for the user who is intent on editing their YAML file manually. This guide should take about 10 minutes to read.
|
||||
|
||||
If you would like to generate a basic, fully playable, YAML without editing a file then visit the settings page for the
|
||||
game you intend to play.
|
||||
If you would like to generate a basic, fully playable, YAML without editing a file then visit the settings page for the game you intend to play. The weighted settings page can also handle most of the advanced settings discussed here.
|
||||
|
||||
The settings page can be found on the supported games page, just click the "Settings Page" link under the name of the
|
||||
game you would like. Supported games page: [Archipelago Games List](https://archipelago.gg/games)
|
||||
The settings page can be found on the supported games page, just click the "Settings Page" link under the name of the game you would like.
|
||||
* Supported games page: [Archipelago Games List](/games)
|
||||
* Weighted settings page: [Archipelago Weighted Settings](/weighted-settings)
|
||||
|
||||
Clicking on the "Export Settings" button at the bottom-left will provide you with a pre-filled YAML with your options.
|
||||
The player settings page also has an option to download a fully filled out yaml containing every option with every
|
||||
|
||||
@@ -91,5 +91,6 @@ including the exclamation point.
|
||||
- `/allow_forfeit <player name>` Allows the given player to use the `!forfeit` command.
|
||||
- `/forbid_forfeit <player name>` Bars the given player from using the `!forfeit` command.
|
||||
- `/send <player name> <item name>` Grants the given player the specified item.
|
||||
- `/send_multiple <amount> <player name> <item name>` Grants the given player the stated amount of the specified item.
|
||||
- `/hint <player name> <item or location name>` Send out a hint for the given item or location for the specified player.
|
||||
- `/option <option name> <option value>` Set a server option. For a list of options, use the `/options` command.
|
||||
|
||||
@@ -19,9 +19,7 @@ enabled (opt-in).
|
||||
editor and find the `plando_options` key. The available plando modules can be enabled by adding them after this such
|
||||
as
|
||||
`plando_options: bosses, items, texts, connections`.
|
||||
* If you are not the one doing the generation or even if you are you can add to the `requires` section of your yaml so
|
||||
that it will throw an error if the options that you need to generate properly are not enabled to ensure you will get
|
||||
the results you desire. Only enter in the plando modules that you are using here but it should look like:
|
||||
* You can add the necessary plando modules for your settings to the `requires` section of your yaml. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like:
|
||||
|
||||
```yaml
|
||||
requires:
|
||||
@@ -66,38 +64,9 @@ list of specific locations both in their own game or in another player's game.
|
||||
* If `min` and `max` are defined, it will try to place a number of items between these two numbers at random
|
||||
|
||||
|
||||
### Available Items
|
||||
### Available Items and Locations
|
||||
|
||||
* [A Link to the Past](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Items.py#L52)
|
||||
* [Factorio Non-Progressive](https://wiki.factorio.com/Technologies) Note that these use the *internal names*. For
|
||||
example, `advanced-electronics`
|
||||
* [Factorio Progressive](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/factorio/Technologies.py#L374)
|
||||
* [Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/ff1/data/items.json)
|
||||
* [Minecraft](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Items.py#L14)
|
||||
* [Ocarina of Time](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/Items.py#L61)
|
||||
* [Risk of Rain 2](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/ror2/Items.py#L8)
|
||||
* [Rogue Legacy](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/rogue-legacy/Names/ItemName.py)
|
||||
* [Slay the Spire](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/spire/Items.py#L13)
|
||||
* [Subnautica](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/subnautica/items.json)
|
||||
* [Super Metroid](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/sm/variaRandomizer/rando/Items.py#L37) Look for "Name="
|
||||
* [Timespinner](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/timespinner/Items.py#L11)
|
||||
|
||||
### Available Locations
|
||||
|
||||
* [A Link to the Past](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Regions.py#L429)
|
||||
* [Factorio](https://wiki.factorio.com/Technologies) Same as items
|
||||
* [Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/ff1/data/locations.json)
|
||||
* [Minecraft](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Locations.py#L18)
|
||||
* [Ocarina of Time](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/LocationList.py#L38)
|
||||
* [Risk of Rain 2](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/ror2/Locations.py#L17) This is a
|
||||
special case. The locations are "ItemPickup[number]" up to the maximum set in the yaml.
|
||||
* [Rogue Legacy](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/rogue-legacy/Names/LocationName.py)
|
||||
* [Slay the Spire](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/spire/Locations.py)
|
||||
* [Subnautica](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/subnautica/locations.json)
|
||||
* [Super Metroid](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/sm/variaRandomizer/graph/location.py#L132)
|
||||
* [Timespinner](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/timespinner/Locations.py#L13)
|
||||
|
||||
A list of all available items and locations can also be found in the [server's datapackage](/api/datapackage).
|
||||
A list of all available items and locations can be found in the [website's datapackage](/datapackage). The items and locations will be in the `"item_name_to_id"` and `"location_name_to_id"` sections of the relevant game. You do not need the quotes but the name must be entered in the same as it appears on that page and is caps-sensitive.
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Required Software
|
||||
|
||||
- Super Mario 64 US Rom (Japanese may work also. Europe and Shindou not supported)
|
||||
- Either of [sm64pclauncher](https://github.com/N00byKing/sm64pclauncher/actions/workflows/ci.yml?query=branch%3Aarchipelago+event%3Apush) or
|
||||
- Either of [sm64pclauncher](https://github.com/N00byKing/sm64pclauncher/releases) or
|
||||
- Cloning and building [sm64ex](https://github.com/N00byKing/sm64ex) manually.
|
||||
|
||||
NOTE: The above linked sm64pclauncher is a special version designed to work with the Archipelago build of sm64ex.
|
||||
@@ -61,6 +61,7 @@ Start the game from the command line to view helpful messages regarding SM64EX.
|
||||
### Game doesn't start after compiling
|
||||
|
||||
Most likely you forgot to set the launch options. `--sm64ap_name YourName` and `--sm64ap_ip ServerIP:Port` are required for startup.
|
||||
If your Name or Password have spaces in them, surround them in quotes.
|
||||
|
||||
## Game Troubleshooting
|
||||
|
||||
|
||||
@@ -3,18 +3,17 @@
|
||||
## Required Software
|
||||
|
||||
- VVVVVV (Bought from the [Steam Store](https://store.steampowered.com/app/70300/VVVVVV/) or [GOG Store](https://www.gog.com/game/vvvvvv) Page, NOT Make and Play Edition!)
|
||||
- [V6AP](https://github.com/N00byKing/VVVVVV/actions/workflows/ci.yml?query=branch%3Aarchipelago)
|
||||
- [V6AP](https://github.com/N00byKing/VVVVVV/releases)
|
||||
|
||||
## Installation and Game Start Procedures
|
||||
|
||||
1. Install VVVVVV through either Steam or GOG
|
||||
2. Go to the page linked for V6AP, and press on the topmost entry
|
||||
3. Scroll down, and download the zip file corresponding to your platform (NOTE: Linux currently does not build automatically. Linux users will have to compile manually for now. Mac is unsupported, but may work if [APCpp](https://github.com/N00byKing/APCpp) is compiled and supplied)
|
||||
4. Unpack the zip file where you have VVVVVV installed.
|
||||
2. Go to the page linked for V6AP, and download the latest release
|
||||
3. Unpack the zip file where you have VVVVVV installed.
|
||||
|
||||
# Joining a MultiWorld Game
|
||||
|
||||
To join, set the following launch options: `-v6ap_name "YourName" -v6ap_ip "ServerIP"`.
|
||||
To join, set the following launch options: `-v6ap_name YourName -v6ap_ip ServerIP:Port`.
|
||||
Optionally, add `-v6ap_passwd "YourPassword"` if the room you are using requires a password. All parameters without quotation marks.
|
||||
The Name in this case is the one specified in your generated .yaml file.
|
||||
In case you are using the Archipelago Website, the IP should be `archipelago.gg`.
|
||||
@@ -27,7 +26,8 @@ Start the game from the command line to view helpful messages regarding V6AP. Th
|
||||
|
||||
### Game no longer starts after copying the .exe
|
||||
|
||||
Most likely you forgot to set the launch options. `-v6ap_name "YourName"` and `-v6ap_ip "ServerIP"` are required for startup.
|
||||
Most likely you forgot to set the launch options. `-v6ap_name YourName` and `-v6ap_ip ServerIP:Port` are required for startup.
|
||||
If your Name or Password have spaces in them, surround them in quotes.
|
||||
|
||||
## Game Troubleshooting
|
||||
|
||||
|
||||
@@ -3,16 +3,16 @@ window.addEventListener('load', () => {
|
||||
let settingHash = localStorage.getItem('weighted-settings-hash');
|
||||
if (!settingHash) {
|
||||
// If no hash data has been set before, set it now
|
||||
localStorage.setItem('weighted-settings-hash', md5(results));
|
||||
localStorage.setItem('weighted-settings-hash', md5(JSON.stringify(results)));
|
||||
localStorage.removeItem('weighted-settings');
|
||||
settingHash = md5(results);
|
||||
settingHash = md5(JSON.stringify(results));
|
||||
}
|
||||
|
||||
if (settingHash !== md5(results)) {
|
||||
if (settingHash !== md5(JSON.stringify(results))) {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.innerText = "Your settings are out of date! Click here to update them! Be aware this will reset " +
|
||||
"them all to default.";
|
||||
userMessage.style.display = "block";
|
||||
userMessage.classList.add('visible');
|
||||
userMessage.addEventListener('click', resetSettings);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ const resetSettings = () => {
|
||||
|
||||
const fetchSettingData = () => new Promise((resolve, reject) => {
|
||||
fetch(new Request(`${window.location.origin}/static/generated/weighted-settings.json`)).then((response) => {
|
||||
try{ resolve(response.json()); }
|
||||
try{ response.json().then((jsonObj) => resolve(jsonObj)); }
|
||||
catch(error){ reject(error); }
|
||||
});
|
||||
});
|
||||
|
||||
@@ -213,6 +213,7 @@ html{
|
||||
|
||||
#weighted-settings #user-message.visible{
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings h1{
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
files. If you do not have a config (.yaml) file yet, you may create one on the game's settings page,
|
||||
which you can find via the <a href="{{ url_for("games") }}">supported games list</a>.
|
||||
</p>
|
||||
<p>
|
||||
Note: This website will always generate games using the current release version of Archipelago,
|
||||
currently {{ version }}.
|
||||
</p>
|
||||
<p>
|
||||
{% if race -%}
|
||||
This game will be generated in race mode,
|
||||
|
||||
@@ -18,9 +18,8 @@
|
||||
generated a game on your own computer, you may upload the zip file created by the generator to
|
||||
host the game here. This will also provide a tracker, and the ability for your players to download
|
||||
their patch files.
|
||||
<br /><br />
|
||||
In addition to the zip file created by the generator, you may upload a multidata file here as well.
|
||||
</p>
|
||||
<p>In addition to the zip file created by the generator, you may upload a multidata file here as well.</p>
|
||||
<div id="host-game-form-wrapper">
|
||||
<form id="host-game-form" method="post" enctype="multipart/form-data">
|
||||
<input id="file-input" type="file" name="file">
|
||||
|
||||
@@ -11,6 +11,7 @@ from pony.orm import flush, select
|
||||
from WebHostLib import app, Seed, Room, Slot
|
||||
from Utils import parse_yaml, VersionException
|
||||
from Patch import preferred_endings
|
||||
from Utils import __version__
|
||||
|
||||
banned_zip_contents = (".sfc",)
|
||||
|
||||
@@ -125,7 +126,7 @@ def uploads():
|
||||
return redirect(url_for("view_seed", seed=seed.id))
|
||||
else:
|
||||
flash("Not recognized file format. Awaiting a .archipelago file or .zip containing one.")
|
||||
return render_template("hostGame.html")
|
||||
return render_template("hostGame.html", version=__version__)
|
||||
|
||||
|
||||
@app.route('/user-content', methods=['GET'])
|
||||
|
||||
@@ -82,6 +82,7 @@ Each location has a `name` and an `id` (a.k.a. "code" or "address"), is placed
|
||||
in a Region and has access rules.
|
||||
The name needs to be unique in each game, the ID needs to be unique across all
|
||||
games and is best in the same range as the item IDs.
|
||||
World-specific IDs are 1 to 2<sup>53</sup>-1, IDs ≤ 0 are global and reserved.
|
||||
|
||||
Special locations with ID `None` can hold events.
|
||||
|
||||
@@ -217,7 +218,7 @@ By convention options are defined in `Options.py` and will be used when parsing
|
||||
the players' yaml files.
|
||||
|
||||
Each option has its own class, inherits from a base option type, has a docstring
|
||||
to describe it and a `displayname` property for display on the website and in
|
||||
to describe it and a `display_name` property for display on the website and in
|
||||
spoiler logs.
|
||||
|
||||
The actual name as used in the yaml is defined in a `dict[str, Option]`, that is
|
||||
@@ -263,7 +264,7 @@ import typing
|
||||
|
||||
class Difficulty(Choice):
|
||||
"""Sets overall game difficulty."""
|
||||
displayname = "Difficulty"
|
||||
display_name = "Difficulty"
|
||||
option_easy = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
@@ -273,14 +274,14 @@ class Difficulty(Choice):
|
||||
|
||||
class FinalBossHP(Range):
|
||||
"""Sets the HP of the final boss"""
|
||||
displayname = "Final Boss HP"
|
||||
display_name = "Final Boss HP"
|
||||
range_start = 100
|
||||
range_end = 10000
|
||||
default = 2000
|
||||
|
||||
class FixXYZGlitch(Toggle):
|
||||
"""Fixes ABC when you do XYZ"""
|
||||
displayname = "Fix XYZ Glitch"
|
||||
display_name = "Fix XYZ Glitch"
|
||||
|
||||
# By convention we call the options dict variable `<world>_options`.
|
||||
mygame_options: typing.Dict[str, type(Option)] = {
|
||||
|
||||
@@ -13,7 +13,7 @@ These steps should be followed in order to establish a gameplay connection with
|
||||
|
||||
In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet.
|
||||
|
||||
There are libraries available that implement the this network protocol in [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py), [Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java) and [.Net](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net)
|
||||
There are libraries available that implement this network protocol in [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py), [Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java), [.Net](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net) and [C++](https://github.com/black-sliver/apclientpp)
|
||||
|
||||
For Super Nintendo games there are clients available in either [Node](https://github.com/ArchipelagoMW/SuperNintendoClient) or [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py), There are also game specific clients available for [The Legend of Zelda: Ocarina of Time](https://github.com/ArchipelagoMW/Z5Client) or [Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/FF1Client.py)
|
||||
|
||||
@@ -117,8 +117,9 @@ Sent to clients when the connection handshake is successfully completed.
|
||||
| slot | int | Your slot number on your team. See [NetworkPlayer](#NetworkPlayer) for more info on the slot number. |
|
||||
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | List denoting other players in the multiworld, whether connected or not. |
|
||||
| missing_locations | list\[int\] | Contains ids of remaining locations that need to be checked. Useful for trackers, among other things. |
|
||||
| checked_locations | list\[int\] | Contains ids of all locations that have been checked. Useful for trackers, among other things. |
|
||||
| checked_locations | list\[int\] | Contains ids of all locations that have been checked. Useful for trackers, among other things. Location ids are in the range of ± 2<sup>53</sup>-1. |
|
||||
| slot_data | dict | Contains a json object for slot related data, differs per game. Empty if not required. |
|
||||
| slot_info | dict\[int, NetworkSlot\] | maps each slot to a NetworkSlot information |
|
||||
|
||||
### ReceivedItems
|
||||
Sent to clients when they receive an item.
|
||||
@@ -262,6 +263,7 @@ Sent to the server to inform it of locations the client has seen, but not checke
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| locations | list\[int\] | The ids of the locations seen by the client. May contain any number of locations, even ones sent before; duplicates do not cause issues with the Archipelago server. |
|
||||
| create_as_hint | bool | If True, the scouted locations get created and broadcasted as a player-visible hint. |
|
||||
|
||||
### StatusUpdate
|
||||
Sent to the server to update on the sender's status. Examples include readiness or goal completion. (Example: defeated Ganon in A Link to the Past)
|
||||
@@ -349,16 +351,16 @@ class NetworkItem(NamedTuple):
|
||||
In JSON this may look like:
|
||||
```js
|
||||
[
|
||||
{"item": 1, "location": 1, "player": 0, "flags": 1},
|
||||
{"item": 2, "location": 2, "player": 0, "flags": 2},
|
||||
{"item": 3, "location": 3, "player": 0, "flags": 0}
|
||||
{"item": 1, "location": 1, "player": 1, "flags": 1},
|
||||
{"item": 2, "location": 2, "player": 2, "flags": 2},
|
||||
{"item": 3, "location": 3, "player": 3, "flags": 0}
|
||||
]
|
||||
```
|
||||
`item` is the item id of the item
|
||||
`item` is the item id of the item. Item ids are in the range of ± 2<sup>53</sup>-1.
|
||||
|
||||
`location` is the location id of the item inside the world
|
||||
`location` is the location id of the item inside the world. Location ids are in the range of ± 2<sup>53</sup>-1.
|
||||
|
||||
`player` is the player slot of the world the item is located in
|
||||
`player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item
|
||||
|
||||
`flags` are bit flags:
|
||||
| Flag | Meaning |
|
||||
@@ -445,6 +447,30 @@ class Version(NamedTuple):
|
||||
build: int
|
||||
```
|
||||
|
||||
### SlotType
|
||||
An enum representing the nature of a slot.
|
||||
|
||||
```python
|
||||
import enum
|
||||
class SlotType(enum.IntFlag):
|
||||
spectator = 0b00
|
||||
player = 0b01
|
||||
group = 0b10
|
||||
```
|
||||
|
||||
### NetworkSlot
|
||||
An object representing static information about a slot.
|
||||
|
||||
```python
|
||||
import typing
|
||||
from NetUtils import SlotType
|
||||
class NetworkSlot(typing.NamedTuple):
|
||||
name: str
|
||||
game: str
|
||||
type: SlotType
|
||||
group_members: typing.List[int] = [] # only populated if type == group
|
||||
```
|
||||
|
||||
### Permission
|
||||
An enumeration containing the possible command permission, for commands that may be restricted.
|
||||
```python
|
||||
|
||||
2
kvui.py
2
kvui.py
@@ -186,6 +186,8 @@ class SelectableLabel(RecycleDataViewBehavior, Label):
|
||||
"? (")
|
||||
cmdinput.text = f"!{App.get_running_app().last_autofillable_command} {name}"
|
||||
break
|
||||
elif not cmdinput.text and text.startswith("Missing: "):
|
||||
cmdinput.text = text.replace("Missing: ", "!hint_location ")
|
||||
|
||||
Clipboard.copy(text)
|
||||
return self.parent.select_with_touch(self.index, touch)
|
||||
|
||||
@@ -3,7 +3,7 @@ import unittest
|
||||
from worlds.AutoWorld import World
|
||||
from Fill import FillError, balance_multiworld_progression, fill_restrictive, distribute_items_restrictive
|
||||
from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, RegionType, Item, Location
|
||||
from worlds.generic.Rules import CollectionRule, set_rule
|
||||
from worlds.generic.Rules import CollectionRule, locality_rules, set_rule
|
||||
|
||||
|
||||
def generate_multi_world(players: int = 1) -> MultiWorld:
|
||||
@@ -342,14 +342,14 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has_all(
|
||||
names(player1.prog_items), player1.id)
|
||||
|
||||
region1 = player1.generate_region(player1.menu, 5)
|
||||
region2 = player1.generate_region(player1.menu, 5, lambda state: state.has_all(
|
||||
player1.generate_region(player1.menu, 5)
|
||||
player1.generate_region(player1.menu, 5, lambda state: state.has_all(
|
||||
names(items[2:7]), player1.id))
|
||||
region3 = player1.generate_region(player1.menu, 5, lambda state: state.has_all(
|
||||
player1.generate_region(player1.menu, 5, lambda state: state.has_all(
|
||||
names(items[7:12]), player1.id))
|
||||
region4 = player1.generate_region(player1.menu, 5, lambda state: state.has_all(
|
||||
player1.generate_region(player1.menu, 5, lambda state: state.has_all(
|
||||
names(items[12:17]), player1.id))
|
||||
region5 = player1.generate_region(player1.menu, 5, lambda state: state.has_all(
|
||||
player1.generate_region(player1.menu, 5, lambda state: state.has_all(
|
||||
names(items[17:22]), player1.id))
|
||||
|
||||
locations = multi_world.get_unfilled_locations()
|
||||
@@ -370,9 +370,13 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
distribute_items_restrictive(multi_world)
|
||||
|
||||
self.assertEqual(locations[0].item, basic_items[0])
|
||||
self.assertFalse(locations[0].event)
|
||||
self.assertEqual(locations[1].item, prog_items[0])
|
||||
self.assertTrue(locations[1].event)
|
||||
self.assertEqual(locations[2].item, prog_items[1])
|
||||
self.assertTrue(locations[2].event)
|
||||
self.assertEqual(locations[3].item, basic_items[1])
|
||||
self.assertFalse(locations[3].event)
|
||||
|
||||
def test_excluded_distribute(self):
|
||||
multi_world = generate_multi_world()
|
||||
@@ -540,6 +544,44 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
self.assertEqual(gen1.locations[2].item, gen2.locations[2].item)
|
||||
self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
|
||||
|
||||
def test_can_reserve_advancement_items_for_general_fill(self):
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, location_count=5, prog_item_count=5)
|
||||
items = player1.prog_items
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has_all(
|
||||
names(items), player1.id)
|
||||
|
||||
location = player1.locations[0]
|
||||
location.progress_type = LocationProgressType.PRIORITY
|
||||
location.item_rule = lambda item: item != items[
|
||||
0] and item != items[1] and item != items[2] and item != items[3]
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
|
||||
self.assertEqual(location.item, items[4])
|
||||
|
||||
def test_non_excluded_local_items(self):
|
||||
multi_world = generate_multi_world(2)
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, location_count=5, basic_item_count=5)
|
||||
player2 = generate_player_data(
|
||||
multi_world, 2, location_count=5, basic_item_count=5)
|
||||
|
||||
for item in multi_world.get_items():
|
||||
item.never_exclude = True
|
||||
|
||||
multi_world.local_items[player1.id].value = set(names(player1.basic_items))
|
||||
multi_world.local_items[player2.id].value = set(names(player2.basic_items))
|
||||
locality_rules(multi_world, player1.id)
|
||||
locality_rules(multi_world, player2.id)
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
|
||||
for item in multi_world.get_items():
|
||||
self.assertEqual(item.player, item.location.player)
|
||||
self.assertFalse(item.location.event, False)
|
||||
|
||||
|
||||
class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
def assertRegionContains(self, region: Region, item: Item):
|
||||
|
||||
@@ -18,14 +18,14 @@ class TestIDs(unittest.TestCase):
|
||||
self.assertEqual(len(known_location_ids) - len(world_type.location_id_to_name), current)
|
||||
|
||||
def testRangeItems(self):
|
||||
"""There are Javascript clients, which are limited to 2**53 integer size."""
|
||||
"""There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
for item_id in world_type.item_id_to_name:
|
||||
self.assertLess(item_id, 2**53)
|
||||
|
||||
def testRangeLocations(self):
|
||||
"""There are Javascript clients, which are limited to 2**53 integer size."""
|
||||
"""There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
for location_id in world_type.location_id_to_name:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from argparse import Namespace
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from BaseClasses import MultiWorld, CollectionState
|
||||
from worlds.AutoWorld import call_all
|
||||
|
||||
gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
|
||||
@@ -18,4 +18,4 @@ def setup_default_world(world_type):
|
||||
world.set_default_common_options()
|
||||
for step in gen_steps:
|
||||
call_all(world, step)
|
||||
return world
|
||||
return world
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
from test.hollow_knight import TestVanilla
|
||||
|
||||
|
||||
class TestBasic(TestVanilla):
|
||||
|
||||
def testSimple(self):
|
||||
self.run_location_tests([
|
||||
["200_Geo-False_Knight_Chest", True, [], []],
|
||||
["380_Geo-Soul_Master_Chest", False, [], ["Mantis_Claw"]],
|
||||
])
|
||||
@@ -1,20 +0,0 @@
|
||||
from worlds.hk import HKWorld
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds import AutoWorld
|
||||
from worlds.hk.Options import hollow_knight_randomize_options, hollow_knight_skip_options
|
||||
|
||||
from test.TestBase import TestBase
|
||||
|
||||
|
||||
class TestVanilla(TestBase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
self.world.game[1] = "Hollow Knight"
|
||||
self.world.worlds[1] = HKWorld(self.world, 1)
|
||||
for hk_option in hollow_knight_randomize_options:
|
||||
setattr(self.world, hk_option, {1: True})
|
||||
for hk_option, option in hollow_knight_skip_options.items():
|
||||
setattr(self.world, hk_option, {1: option.default})
|
||||
AutoWorld.call_single(self.world, "create_regions", 1)
|
||||
AutoWorld.call_single(self.world, "generate_basic", 1)
|
||||
AutoWorld.call_single(self.world, "set_rules", 1)
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
from typing import Dict, Set, Tuple, List, Optional, TextIO, Any
|
||||
|
||||
import logging
|
||||
from typing import Dict, Set, Tuple, List, Optional, TextIO, Any, Callable
|
||||
|
||||
from BaseClasses import MultiWorld, Item, CollectionState, Location
|
||||
from Options import Option
|
||||
@@ -18,6 +20,8 @@ class AutoWorldRegister(type):
|
||||
|
||||
# build rest
|
||||
dct["item_names"] = frozenset(dct["item_name_to_id"])
|
||||
dct["item_name_groups"] = dct.get("item_name_groups", {})
|
||||
dct["item_name_groups"]["Everything"] = dct["item_names"]
|
||||
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
||||
dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {})))
|
||||
|
||||
@@ -31,8 +35,13 @@ class AutoWorldRegister(type):
|
||||
class AutoLogicRegister(type):
|
||||
def __new__(cls, name, bases, dct):
|
||||
new_class = super().__new__(cls, name, bases, dct)
|
||||
function: Callable
|
||||
for item_name, function in dct.items():
|
||||
if not item_name.startswith("__"):
|
||||
if item_name == "copy_mixin":
|
||||
CollectionState.additional_copy_functions.append(function)
|
||||
elif item_name == "init_mixin":
|
||||
CollectionState.additional_init_functions.append(function)
|
||||
elif not item_name.startswith("__"):
|
||||
if hasattr(CollectionState, item_name):
|
||||
raise Exception(f"Name conflict on Logic Mixin {name} trying to overwrite {item_name}")
|
||||
setattr(CollectionState, item_name, function)
|
||||
@@ -87,6 +96,8 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
hint_blacklist: Set[str] = frozenset() # any names that should not be hintable
|
||||
|
||||
# NOTE: remote_items and remote_start_inventory are now available in the network protocol for the client to set.
|
||||
# These values will be removed.
|
||||
# if a world is set to remote_items, then it just needs to send location checks to the server and the server
|
||||
# sends back the items
|
||||
# if a world is set to remote_items = False, then the server never sends an item where receiver == finder,
|
||||
@@ -187,37 +198,53 @@ class World(metaclass=AutoWorldRegister):
|
||||
def write_spoiler_end(self, spoiler_handle: TextIO):
|
||||
"""Write to the end of the spoiler"""
|
||||
pass
|
||||
# end of ordered Main.py calls
|
||||
|
||||
def collect_item(self, state: CollectionState, item: Item, remove: bool = False) -> Optional[str]:
|
||||
"""Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
|
||||
Collect None to skip item.
|
||||
:param remove: indicate if this is meant to remove from state instead of adding."""
|
||||
if item.advancement:
|
||||
return item.name
|
||||
# end of ordered Main.py calls
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
"""Create an item for this world type and player.
|
||||
Warning: this may be called with self.world = None, for example by MultiServer"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
"""Called when the item pool needs to be filled with additional items to match location count."""
|
||||
logging.warning(f"World {self} is generating a filler item without custom filler pool.")
|
||||
return self.world.random.choice(self.item_name_to_id)
|
||||
|
||||
# decent place to implement progressive items, in most cases can stay as-is
|
||||
def collect_item(self, state: CollectionState, item: Item, remove: bool = False) -> Optional[str]:
|
||||
"""Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
|
||||
Collect None to skip item.
|
||||
:param state: CollectionState to collect into
|
||||
:param item: Item to decide on if it should be collected into state
|
||||
:param remove: indicate if this is meant to remove from state instead of adding."""
|
||||
if item.advancement:
|
||||
return item.name
|
||||
|
||||
# called to create all_state, return Items that are created during pre_fill
|
||||
def get_pre_fill_items(self) -> List[Item]:
|
||||
return []
|
||||
|
||||
# following methods should not need to be overridden.
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
name = self.collect_item(state, item)
|
||||
if name:
|
||||
state.prog_items[name, item.player] += 1
|
||||
state.prog_items[name, self.player] += 1
|
||||
return True
|
||||
return False
|
||||
|
||||
def remove(self, state: CollectionState, item: Item) -> bool:
|
||||
name = self.collect_item(state, item, True)
|
||||
if name:
|
||||
state.prog_items[name, item.player] -= 1
|
||||
if state.prog_items[name, item.player] < 1:
|
||||
del (state.prog_items[name, item.player])
|
||||
state.prog_items[name, self.player] -= 1
|
||||
if state.prog_items[name, self.player] < 1:
|
||||
del (state.prog_items[name, self.player])
|
||||
return True
|
||||
return False
|
||||
|
||||
def create_filler(self):
|
||||
self.world.itempool.append(self.create_item(self.get_filler_item_name()))
|
||||
|
||||
|
||||
# any methods attached to this can be used as part of CollectionState,
|
||||
# please use a prefix as all of them get clobbered together
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import Dungeon
|
||||
from worlds.alttp.Bosses import BossFactory
|
||||
from Fill import fill_restrictive
|
||||
@@ -13,6 +15,7 @@ def create_dungeons(world, player):
|
||||
dungeon_items, player)
|
||||
for item in dungeon.all_items:
|
||||
item.dungeon = dungeon
|
||||
item.world = world
|
||||
dungeon.boss = BossFactory(default_boss, player) if default_boss else None
|
||||
for region in dungeon.regions:
|
||||
world.get_region(region, player).dungeon = dungeon
|
||||
@@ -108,21 +111,15 @@ def create_dungeons(world, player):
|
||||
world.dungeons[dungeon.name, dungeon.player] = dungeon
|
||||
|
||||
|
||||
def get_dungeon_item_pool(world):
|
||||
items = [item for dungeon in world.dungeons.values() for item in dungeon.all_items]
|
||||
for item in items:
|
||||
item.world = world
|
||||
return items
|
||||
def get_dungeon_item_pool(world) -> typing.List:
|
||||
return [item for dungeon in world.dungeons.values() for item in dungeon.all_items]
|
||||
|
||||
|
||||
def get_dungeon_item_pool_player(world, player):
|
||||
items = [item for dungeon in world.dungeons.values() if dungeon.player == player for item in dungeon.all_items]
|
||||
for item in items:
|
||||
item.world = world
|
||||
return items
|
||||
def get_dungeon_item_pool_player(world, player) -> typing.List:
|
||||
return [item for dungeon in world.dungeons.values() if dungeon.player == player for item in dungeon.all_items]
|
||||
|
||||
|
||||
def fill_dungeons_restrictive(autoworld, world):
|
||||
def fill_dungeons_restrictive(world):
|
||||
"""Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside."""
|
||||
localized: set = set()
|
||||
dungeon_specific: set = set()
|
||||
|
||||
@@ -290,7 +290,6 @@ def generate_itempool(world):
|
||||
loc.access_rule = lambda state: state.has_triforce_pieces(state.world.treasure_hunt_count[player], player)
|
||||
|
||||
region.locations.append(loc)
|
||||
world.dynamic_locations.append(loc)
|
||||
world.clear_location_cache()
|
||||
|
||||
world.push_item(loc, ItemFactory('Triforce', player), False)
|
||||
@@ -474,7 +473,6 @@ def set_up_take_anys(world, player):
|
||||
|
||||
old_man_take_any = Region("Old Man Sword Cave", RegionType.Cave, 'the sword cave', player)
|
||||
world.regions.append(old_man_take_any)
|
||||
world.dynamic_regions.append(old_man_take_any)
|
||||
|
||||
reg = regions.pop()
|
||||
entrance = world.get_region(reg, player).entrances[0]
|
||||
@@ -495,7 +493,6 @@ def set_up_take_anys(world, player):
|
||||
for num in range(4):
|
||||
take_any = Region("Take-Any #{}".format(num+1), RegionType.Cave, 'a cave of choice', player)
|
||||
world.regions.append(take_any)
|
||||
world.dynamic_regions.append(take_any)
|
||||
|
||||
target, room_id = world.random.choice([(0x58, 0x0112), (0x60, 0x010F), (0x46, 0x011F)])
|
||||
reg = regions.pop()
|
||||
@@ -519,7 +516,6 @@ def create_dynamic_shop_locations(world, player):
|
||||
if item['create_location']:
|
||||
loc = ALttPLocation(player, f"{shop.region.name} {shop.slot_names[i]}", parent=shop.region)
|
||||
shop.region.locations.append(loc)
|
||||
world.dynamic_locations.append(loc)
|
||||
|
||||
world.clear_location_cache()
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import typing
|
||||
|
||||
def GetBeemizerItem(world, player, item):
|
||||
|
||||
def GetBeemizerItem(world, player: int, item):
|
||||
item_name = item if isinstance(item, str) else item.name
|
||||
|
||||
if item_name not in trap_replaceable:
|
||||
@@ -16,6 +17,7 @@ def GetBeemizerItem(world, player, item):
|
||||
else:
|
||||
return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player)
|
||||
|
||||
|
||||
# should be replaced with direct world.create_item(item) call in the future
|
||||
def ItemFactory(items, player: int):
|
||||
from worlds.alttp import ALTTPWorld
|
||||
@@ -35,6 +37,7 @@ def ItemFactory(items, player: int):
|
||||
return ret[0]
|
||||
return ret
|
||||
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
advancement: bool
|
||||
type: typing.Optional[str]
|
||||
@@ -274,8 +277,8 @@ for basename, substring in _simple_groups:
|
||||
del (_simple_groups)
|
||||
|
||||
progression_items = {name for name, data in item_table.items() if type(data.item_code) == int and data.advancement}
|
||||
item_name_groups['Everything'] = {name for name, data in item_table.items() if type(data.item_code) == int}
|
||||
everything = {name for name, data in item_table.items() if type(data.item_code) == int}
|
||||
item_name_groups['Progression Items'] = progression_items
|
||||
item_name_groups['Non Progression Items'] = item_name_groups['Everything'] - progression_items
|
||||
item_name_groups['Non Progression Items'] = everything - progression_items
|
||||
|
||||
trap_replaceable = item_name_groups['Rupees'] | {'Arrows (10)', 'Single Bomb', 'Bombs (3)', 'Bombs (10)', 'Nothing'}
|
||||
|
||||
@@ -42,30 +42,35 @@ class DungeonItem(Choice):
|
||||
def in_dungeon(self):
|
||||
return self.value in {0, 1}
|
||||
|
||||
@property
|
||||
def hints_useful(self):
|
||||
"""Indicates if hints for this Item are useful in any way."""
|
||||
return self.value in {1, 2, 3, 4}
|
||||
|
||||
|
||||
class bigkey_shuffle(DungeonItem):
|
||||
"""Big Key Placement"""
|
||||
item_name_group = "Big Keys"
|
||||
displayname = "Big Key Shuffle"
|
||||
display_name = "Big Key Shuffle"
|
||||
|
||||
|
||||
class smallkey_shuffle(DungeonItem):
|
||||
"""Small Key Placement"""
|
||||
option_universal = 5
|
||||
item_name_group = "Small Keys"
|
||||
displayname = "Small Key Shuffle"
|
||||
display_name = "Small Key Shuffle"
|
||||
|
||||
|
||||
class compass_shuffle(DungeonItem):
|
||||
"""Compass Placement"""
|
||||
item_name_group = "Compasses"
|
||||
displayname = "Compass Shuffle"
|
||||
display_name = "Compass Shuffle"
|
||||
|
||||
|
||||
class map_shuffle(DungeonItem):
|
||||
"""Map Placement"""
|
||||
item_name_group = "Maps"
|
||||
displayname = "Map Shuffle"
|
||||
display_name = "Map Shuffle"
|
||||
|
||||
|
||||
class Crystals(Range):
|
||||
@@ -118,7 +123,7 @@ class Enemies(Choice):
|
||||
|
||||
|
||||
class Progressive(Choice):
|
||||
displayname = "Progressive Items"
|
||||
display_name = "Progressive Items"
|
||||
option_off = 0
|
||||
option_grouped_random = 1
|
||||
option_on = 2
|
||||
@@ -137,24 +142,24 @@ class Swordless(Toggle):
|
||||
can be opened without a sword. Hammer damages Ganon.
|
||||
Ether and Bombos Tablet can be activated with Hammer
|
||||
(and Book)."""
|
||||
displayname = "Swordless"
|
||||
display_name = "Swordless"
|
||||
|
||||
|
||||
class Retro(Toggle):
|
||||
"""Zelda-1 like mode. You have to purchase a quiver to shoot arrows using rupees
|
||||
and there are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion."""
|
||||
displayname = "Retro"
|
||||
display_name = "Retro"
|
||||
|
||||
|
||||
class RestrictBossItem(Toggle):
|
||||
"""Don't place dungeon-native items on the dungeon's boss."""
|
||||
displayname = "Prevent Dungeon Item on Boss"
|
||||
display_name = "Prevent Dungeon Item on Boss"
|
||||
|
||||
|
||||
class Hints(Choice):
|
||||
"""Vendors: King Zora and Bottle Merchant say what they're selling.
|
||||
On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints."""
|
||||
displayname = "Hints"
|
||||
display_name = "Hints"
|
||||
option_off = 0
|
||||
option_vendors = 1
|
||||
option_on = 2
|
||||
@@ -167,27 +172,27 @@ class Hints(Choice):
|
||||
class EnemyShuffle(Toggle):
|
||||
"""Randomize every enemy spawn.
|
||||
If mode is Standard, Hyrule Castle is left out (may result in visually wrong enemy sprites in that area.)"""
|
||||
displayname = "Enemy Shuffle"
|
||||
display_name = "Enemy Shuffle"
|
||||
|
||||
|
||||
class KillableThieves(Toggle):
|
||||
"""Makes Thieves killable."""
|
||||
displayname = "Killable Thieves"
|
||||
display_name = "Killable Thieves"
|
||||
|
||||
|
||||
class BushShuffle(Toggle):
|
||||
"""Randomize chance that a bush contains an enemy as well as which enemy may spawn."""
|
||||
displayname = "Bush Shuffle"
|
||||
display_name = "Bush Shuffle"
|
||||
|
||||
|
||||
class TileShuffle(Toggle):
|
||||
"""Randomize flying tiles floor patterns."""
|
||||
displayname = "Tile Shuffle"
|
||||
display_name = "Tile Shuffle"
|
||||
|
||||
|
||||
class PotShuffle(Toggle):
|
||||
"""Shuffle contents of pots within "supertiles" (item will still be nearby original placement)."""
|
||||
displayname = "Pot Shuffle"
|
||||
display_name = "Pot Shuffle"
|
||||
|
||||
|
||||
class Palette(Choice):
|
||||
@@ -203,31 +208,31 @@ class Palette(Choice):
|
||||
|
||||
|
||||
class OWPalette(Palette):
|
||||
displayname = "Overworld Palette"
|
||||
display_name = "Overworld Palette"
|
||||
|
||||
|
||||
class UWPalette(Palette):
|
||||
displayname = "Underworld Palette"
|
||||
display_name = "Underworld Palette"
|
||||
|
||||
|
||||
class HUDPalette(Palette):
|
||||
displayname = "Menu Palette"
|
||||
display_name = "Menu Palette"
|
||||
|
||||
|
||||
class SwordPalette(Palette):
|
||||
displayname = "Sword Palette"
|
||||
display_name = "Sword Palette"
|
||||
|
||||
|
||||
class ShieldPalette(Palette):
|
||||
displayname = "Shield Palette"
|
||||
display_name = "Shield Palette"
|
||||
|
||||
|
||||
class LinkPalette(Palette):
|
||||
displayname = "Link Palette"
|
||||
display_name = "Link Palette"
|
||||
|
||||
|
||||
class HeartBeep(Choice):
|
||||
displayname = "Heart Beep Rate"
|
||||
display_name = "Heart Beep Rate"
|
||||
option_normal = 0
|
||||
option_double = 1
|
||||
option_half = 2
|
||||
@@ -237,7 +242,7 @@ class HeartBeep(Choice):
|
||||
|
||||
|
||||
class HeartColor(Choice):
|
||||
displayname = "Heart Color"
|
||||
display_name = "Heart Color"
|
||||
option_red = 0
|
||||
option_blue = 1
|
||||
option_green = 2
|
||||
@@ -245,11 +250,11 @@ class HeartColor(Choice):
|
||||
|
||||
|
||||
class QuickSwap(DefaultOnToggle):
|
||||
displayname = "L/R Quickswapping"
|
||||
display_name = "L/R Quickswapping"
|
||||
|
||||
|
||||
class MenuSpeed(Choice):
|
||||
displayname = "Menu Speed"
|
||||
display_name = "Menu Speed"
|
||||
option_normal = 0
|
||||
option_instant = 1,
|
||||
option_double = 2
|
||||
@@ -259,7 +264,7 @@ class MenuSpeed(Choice):
|
||||
|
||||
|
||||
class Music(DefaultOnToggle):
|
||||
displayname = "Play music"
|
||||
display_name = "Play music"
|
||||
|
||||
|
||||
class ReduceFlashing(DefaultOnToggle):
|
||||
|
||||
@@ -35,7 +35,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts
|
||||
LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \
|
||||
SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
||||
from Utils import local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen
|
||||
from worlds.alttp.Items import ItemFactory, item_table
|
||||
from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items
|
||||
from worlds.alttp.EntranceShuffle import door_addresses
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
import Patch
|
||||
@@ -1018,17 +1018,15 @@ def patch_rom(world, rom, player, enemized):
|
||||
# Set overflow items for progressive equipment
|
||||
rom.write_bytes(0x180090,
|
||||
[difficulty.progressive_sword_limit if not world.swordless[player] else 0,
|
||||
overflow_replacement,
|
||||
difficulty.progressive_shield_limit, overflow_replacement,
|
||||
difficulty.progressive_armor_limit, overflow_replacement,
|
||||
difficulty.progressive_bottle_limit, overflow_replacement])
|
||||
|
||||
# Work around for json patch ordering issues - write bow limit separately so that it is replaced in the patch
|
||||
rom.write_bytes(0x180098, [difficulty.progressive_bow_limit, overflow_replacement])
|
||||
item_table[difficulty.basicsword[-1]].item_code,
|
||||
difficulty.progressive_shield_limit, item_table[difficulty.basicshield[-1]].item_code,
|
||||
difficulty.progressive_armor_limit, item_table[difficulty.basicarmor[-1]].item_code,
|
||||
difficulty.progressive_bottle_limit, overflow_replacement,
|
||||
difficulty.progressive_bow_limit, item_table[difficulty.basicbow[-1]].item_code])
|
||||
|
||||
if difficulty.progressive_bow_limit < 2 and (
|
||||
world.swordless[player] or world.logic[player] == 'noglitches'):
|
||||
rom.write_bytes(0x180098, [2, overflow_replacement])
|
||||
rom.write_bytes(0x180098, [2, item_table["Silver Bow"].item_code])
|
||||
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
|
||||
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
|
||||
|
||||
@@ -1653,9 +1651,10 @@ def patch_rom(world, rom, player, enemized):
|
||||
rom.write_bytes(0x7FC0, rom.name)
|
||||
|
||||
# set player names
|
||||
for p in range(1, min(world.players, ROM_PLAYER_LIMIT) + 1):
|
||||
encoded_players = world.players + len(world.groups)
|
||||
for p in range(1, min(encoded_players, ROM_PLAYER_LIMIT) + 1):
|
||||
rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(world.player_name[p]))
|
||||
if world.players > ROM_PLAYER_LIMIT:
|
||||
if encoded_players > ROM_PLAYER_LIMIT:
|
||||
rom.write_bytes(0x195FFC + ((ROM_PLAYER_LIMIT - 1) * 32), hud_format_text("Archipelago"))
|
||||
|
||||
# Write title screen Code
|
||||
@@ -2271,13 +2270,13 @@ def write_strings(rom, world, player):
|
||||
this_hint = location + ' contains ' + hint_text(world.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
|
||||
# Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well.
|
||||
# Lastly we write hints to show where certain interesting items are.
|
||||
items_to_hint = RelevantItems.copy()
|
||||
if world.smallkey_shuffle[player]:
|
||||
items_to_hint.extend(SmallKeys)
|
||||
if world.bigkey_shuffle[player]:
|
||||
items_to_hint.extend(BigKeys)
|
||||
local_random.shuffle(items_to_hint)
|
||||
if world.smallkey_shuffle[player].hints_useful:
|
||||
items_to_hint |= item_name_groups["Small Keys"]
|
||||
if world.bigkey_shuffle[player].hints_useful:
|
||||
items_to_hint |= item_name_groups["Big Keys"]
|
||||
|
||||
if world.hints[player] == "full":
|
||||
hint_count = len(hint_locations) # fill all remaining hint locations with Item hints.
|
||||
else:
|
||||
@@ -2285,7 +2284,7 @@ def write_strings(rom, world, player):
|
||||
'dungeonscrossed'] else 8
|
||||
hint_count = min(hint_count, len(items_to_hint), len(hint_locations))
|
||||
if hint_count:
|
||||
locations = world.find_items_in_locations(set(items_to_hint), player)
|
||||
locations = world.find_items_in_locations(items_to_hint, player)
|
||||
local_random.shuffle(locations)
|
||||
for x in range(min(hint_count, len(locations))):
|
||||
this_location = locations.pop()
|
||||
@@ -2873,88 +2872,10 @@ InconvenientLocations = ['Spike Cave',
|
||||
InconvenientVanillaLocations = ['Graveyard Cave',
|
||||
'Mimic Cave']
|
||||
|
||||
RelevantItems = ['Bow',
|
||||
'Progressive Bow',
|
||||
'Book of Mudora',
|
||||
'Hammer',
|
||||
'Hookshot',
|
||||
'Magic Mirror',
|
||||
'Flute',
|
||||
'Pegasus Boots',
|
||||
'Power Glove',
|
||||
'Cape',
|
||||
'Mushroom',
|
||||
'Shovel',
|
||||
'Lamp',
|
||||
'Magic Powder',
|
||||
'Moon Pearl',
|
||||
'Cane of Somaria',
|
||||
'Fire Rod',
|
||||
'Flippers',
|
||||
'Ice Rod',
|
||||
'Titans Mitts',
|
||||
'Ether',
|
||||
'Bombos',
|
||||
'Quake',
|
||||
'Bottle',
|
||||
'Bottle (Red Potion)',
|
||||
'Bottle (Green Potion)',
|
||||
'Bottle (Blue Potion)',
|
||||
'Bottle (Fairy)',
|
||||
'Bottle (Bee)',
|
||||
'Bottle (Good Bee)',
|
||||
'Master Sword',
|
||||
'Tempered Sword',
|
||||
'Fighter Sword',
|
||||
'Golden Sword',
|
||||
'Progressive Sword',
|
||||
'Progressive Glove',
|
||||
'Master Sword',
|
||||
'Power Star',
|
||||
'Triforce Piece',
|
||||
'Single Arrow',
|
||||
'Blue Mail',
|
||||
'Red Mail',
|
||||
'Progressive Mail',
|
||||
'Blue Boomerang',
|
||||
'Red Boomerang',
|
||||
'Blue Shield',
|
||||
'Red Shield',
|
||||
'Mirror Shield',
|
||||
'Progressive Shield',
|
||||
'Bug Catching Net',
|
||||
'Cane of Byrna',
|
||||
'Magic Upgrade (1/2)',
|
||||
'Magic Upgrade (1/4)'
|
||||
]
|
||||
|
||||
SmallKeys = ['Small Key (Eastern Palace)',
|
||||
'Small Key (Hyrule Castle)',
|
||||
'Small Key (Desert Palace)',
|
||||
'Small Key (Tower of Hera)',
|
||||
'Small Key (Agahnims Tower)',
|
||||
'Small Key (Palace of Darkness)',
|
||||
'Small Key (Thieves Town)',
|
||||
'Small Key (Swamp Palace)',
|
||||
'Small Key (Skull Woods)',
|
||||
'Small Key (Ice Palace)',
|
||||
'Small Key (Misery Mire)',
|
||||
'Small Key (Turtle Rock)',
|
||||
'Small Key (Ganons Tower)',
|
||||
]
|
||||
RelevantItems = progression_items - {"Triforce", "Activated Flute"} - item_name_groups["Small Keys"] - item_name_groups["Big Keys"] \
|
||||
| item_name_groups["Mails"] | item_name_groups["Shields"]
|
||||
|
||||
BigKeys = ['Big Key (Eastern Palace)',
|
||||
'Big Key (Desert Palace)',
|
||||
'Big Key (Tower of Hera)',
|
||||
'Big Key (Palace of Darkness)',
|
||||
'Big Key (Thieves Town)',
|
||||
'Big Key (Swamp Palace)',
|
||||
'Big Key (Skull Woods)',
|
||||
'Big Key (Ice Palace)',
|
||||
'Big Key (Misery Mire)',
|
||||
'Big Key (Turtle Rock)',
|
||||
'Big Key (Ganons Tower)'
|
||||
]
|
||||
|
||||
hash_alphabet = [
|
||||
"Bow", "Boomerang", "Hookshot", "Bomb", "Mushroom", "Powder", "Rod", "Pendant", "Bombos", "Ether", "Quake",
|
||||
|
||||
@@ -337,7 +337,6 @@ def create_shops(world, player: int):
|
||||
loc.shop_slot_disabled = True
|
||||
loc.item.world = world
|
||||
shop.region.locations.append(loc)
|
||||
world.dynamic_locations.append(loc)
|
||||
world.clear_location_cache()
|
||||
|
||||
|
||||
|
||||
@@ -1716,7 +1716,7 @@ class TextTable(object):
|
||||
text['telepathic_tile_tower_of_hera_entrance'] = CompressedTextMapper.convert(
|
||||
"{NOBORDER}\nThis is a bad place, with a guy who will make you fall…\n\n\na lot.")
|
||||
text['houlihan_room'] = CompressedTextMapper.convert(
|
||||
"Have a Multiworld Tournament\nand we can list the winners here.")
|
||||
"Multiworld Tournament winners\nSGLive 2021 BadmoonZ")
|
||||
text['caught_a_bee'] = CompressedTextMapper.convert("Caught a Bee\n ≥ keep\n release\n{CHOICE}")
|
||||
text['caught_a_fairy'] = CompressedTextMapper.convert("Caught Fairy!\n ≥ keep\n release\n{CHOICE}")
|
||||
text['no_empty_bottles'] = CompressedTextMapper.convert("Whoa, bucko!\nNo empty bottles.")
|
||||
|
||||
@@ -194,7 +194,7 @@ class ALTTPWorld(World):
|
||||
return
|
||||
elif state.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
|
||||
return 'Mirror Shield'
|
||||
elif state.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
||||
elif state.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
||||
return 'Red Shield'
|
||||
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
||||
return 'Blue Shield'
|
||||
@@ -249,7 +249,7 @@ class ALTTPWorld(World):
|
||||
@classmethod
|
||||
def stage_pre_fill(cls, world):
|
||||
from .Dungeons import fill_dungeons_restrictive
|
||||
fill_dungeons_restrictive(cls, world)
|
||||
fill_dungeons_restrictive(world)
|
||||
|
||||
@classmethod
|
||||
def stage_post_fill(cls, world):
|
||||
@@ -319,9 +319,7 @@ class ALTTPWorld(World):
|
||||
# we skip in case of error, so that the original error in the output thread is the one that gets raised
|
||||
if rom_name:
|
||||
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]])
|
||||
multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]]
|
||||
|
||||
def get_required_client_version(self) -> tuple:
|
||||
return max((0, 2, 4), super(ALTTPWorld, self).get_required_client_version())
|
||||
@@ -365,17 +363,9 @@ class ALTTPWorld(World):
|
||||
fill_locations.remove(loc)
|
||||
world.random.shuffle(fill_locations)
|
||||
# TODO: investigate not creating the key in the first place
|
||||
if __debug__:
|
||||
# keeping this here while I'm not sure we caught all instances of multiple HC small keys in the pool
|
||||
count = len(progitempool)
|
||||
progitempool[:] = [item for item in progitempool if
|
||||
item.player not in standard_keyshuffle_players or
|
||||
item.name != "Small Key (Hyrule Castle)"]
|
||||
assert len(progitempool) + len(standard_keyshuffle_players) == count
|
||||
else:
|
||||
progitempool[:] = [item for item in progitempool if
|
||||
item.player not in standard_keyshuffle_players or
|
||||
item.name != "Small Key (Hyrule Castle)"]
|
||||
progitempool[:] = [item for item in progitempool if
|
||||
item.player not in standard_keyshuffle_players or
|
||||
item.name != "Small Key (Hyrule Castle)"]
|
||||
|
||||
if trash_counts:
|
||||
locations_mapping = {player: [] for player in trash_counts}
|
||||
@@ -404,6 +394,19 @@ class ALTTPWorld(World):
|
||||
fill_locations.remove(spot_to_fill) # very slow, unfortunately
|
||||
trash_count -= 1
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return "Rupees (5)" # temporary
|
||||
|
||||
def get_pre_fill_items(self):
|
||||
res = []
|
||||
if self.dungeon_local_item_names:
|
||||
for (name, player), dungeon in self.world.dungeons.items():
|
||||
if player == self.player:
|
||||
for item in dungeon.all_items:
|
||||
if item.name in self.dungeon_local_item_names:
|
||||
res.append(item)
|
||||
return res
|
||||
|
||||
|
||||
def get_same_seed(world, seed_def: tuple) -> str:
|
||||
seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {})
|
||||
|
||||
@@ -11,7 +11,7 @@ LuaBool = Or(bool, And(int, lambda n: n in (0, 1)))
|
||||
|
||||
class MaxSciencePack(Choice):
|
||||
"""Maximum level of science pack required to complete the game."""
|
||||
displayname = "Maximum Required Science Pack"
|
||||
display_name = "Maximum Required Science Pack"
|
||||
option_automation_science_pack = 0
|
||||
option_logistic_science_pack = 1
|
||||
option_military_science_pack = 2
|
||||
@@ -35,7 +35,7 @@ class MaxSciencePack(Choice):
|
||||
|
||||
class Goal(Choice):
|
||||
"""Goal required to complete the game."""
|
||||
displayname = "Goal"
|
||||
display_name = "Goal"
|
||||
option_rocket = 0
|
||||
option_satellite = 1
|
||||
default = 0
|
||||
@@ -43,7 +43,7 @@ class Goal(Choice):
|
||||
|
||||
class TechCost(Choice):
|
||||
"""How expensive are the technologies."""
|
||||
displayname = "Technology Cost Scale"
|
||||
display_name = "Technology Cost Scale"
|
||||
option_very_easy = 0
|
||||
option_easy = 1
|
||||
option_kind = 2
|
||||
@@ -56,7 +56,7 @@ class TechCost(Choice):
|
||||
|
||||
class Silo(Choice):
|
||||
"""Ingredients to craft rocket silo or auto-place if set to spawn."""
|
||||
displayname = "Rocket Silo"
|
||||
display_name = "Rocket Silo"
|
||||
option_vanilla = 0
|
||||
option_randomize_recipe = 1
|
||||
option_spawn = 2
|
||||
@@ -65,7 +65,7 @@ class Silo(Choice):
|
||||
|
||||
class Satellite(Choice):
|
||||
"""Ingredients to craft satellite."""
|
||||
displayname = "Satellite"
|
||||
display_name = "Satellite"
|
||||
option_vanilla = 0
|
||||
option_randomize_recipe = 1
|
||||
default = 0
|
||||
@@ -73,7 +73,7 @@ class Satellite(Choice):
|
||||
|
||||
class FreeSamples(Choice):
|
||||
"""Get free items with your technologies."""
|
||||
displayname = "Free Samples"
|
||||
display_name = "Free Samples"
|
||||
option_none = 0
|
||||
option_single_craft = 1
|
||||
option_half_stack = 2
|
||||
@@ -83,7 +83,7 @@ class FreeSamples(Choice):
|
||||
|
||||
class TechTreeLayout(Choice):
|
||||
"""Selects how the tech tree nodes are interwoven."""
|
||||
displayname = "Technology Tree Layout"
|
||||
display_name = "Technology Tree Layout"
|
||||
option_single = 0
|
||||
option_small_diamonds = 1
|
||||
option_medium_diamonds = 2
|
||||
@@ -101,7 +101,7 @@ class TechTreeLayout(Choice):
|
||||
|
||||
class TechTreeInformation(Choice):
|
||||
"""How much information should be displayed in the tech tree."""
|
||||
displayname = "Technology Tree Information"
|
||||
display_name = "Technology Tree Information"
|
||||
option_none = 0
|
||||
option_advancement = 1
|
||||
option_full = 2
|
||||
@@ -119,7 +119,7 @@ class RecipeTime(Choice):
|
||||
New Normal: 0.25 - 10 seconds
|
||||
New Slow: 5 - 10 seconds
|
||||
"""
|
||||
displayname = "Recipe Time"
|
||||
display_name = "Recipe Time"
|
||||
option_vanilla = 0
|
||||
option_fast = 1
|
||||
option_normal = 2
|
||||
@@ -133,7 +133,7 @@ class RecipeTime(Choice):
|
||||
class Progressive(Choice):
|
||||
"""Merges together Technologies like "automation-1" to "automation-3" into 3 copies of "Progressive Automation",
|
||||
which awards them in order."""
|
||||
displayname = "Progressive Technologies"
|
||||
display_name = "Progressive Technologies"
|
||||
option_off = 0
|
||||
option_grouped_random = 1
|
||||
option_on = 2
|
||||
@@ -147,26 +147,26 @@ class Progressive(Choice):
|
||||
|
||||
class RecipeIngredients(Choice):
|
||||
"""Select if rocket, or rocket + science pack ingredients should be random."""
|
||||
displayname = "Random Recipe Ingredients Level"
|
||||
display_name = "Random Recipe Ingredients Level"
|
||||
option_rocket = 0
|
||||
option_science_pack = 1
|
||||
|
||||
|
||||
class FactorioStartItems(ItemDict):
|
||||
"""Mapping of Factorio internal item-name to amount granted on start."""
|
||||
displayname = "Starting Items"
|
||||
display_name = "Starting Items"
|
||||
verify_item_name = False
|
||||
default = {"burner-mining-drill": 19, "stone-furnace": 19}
|
||||
|
||||
|
||||
class FactorioFreeSampleBlacklist(OptionSet):
|
||||
"""Set of items that should never be granted from Free Samples"""
|
||||
displayname = "Free Sample Blacklist"
|
||||
display_name = "Free Sample Blacklist"
|
||||
|
||||
|
||||
class FactorioFreeSampleWhitelist(OptionSet):
|
||||
"""Overrides any free sample blacklist present. This may ruin the balance of the mod, be warned."""
|
||||
displayname = "Free Sample Whitelist"
|
||||
display_name = "Free Sample Whitelist"
|
||||
|
||||
|
||||
class TrapCount(Range):
|
||||
@@ -175,19 +175,19 @@ class TrapCount(Range):
|
||||
|
||||
class AttackTrapCount(TrapCount):
|
||||
"""Trap items that when received trigger an attack on your base."""
|
||||
displayname = "Attack Traps"
|
||||
display_name = "Attack Traps"
|
||||
|
||||
|
||||
class EvolutionTrapCount(TrapCount):
|
||||
"""Trap items that when received increase the enemy evolution."""
|
||||
displayname = "Evolution Traps"
|
||||
display_name = "Evolution Traps"
|
||||
|
||||
|
||||
class EvolutionTrapIncrease(Range):
|
||||
"""How much an Evolution Trap increases the enemy evolution.
|
||||
Increases scale down proportionally to the session's current evolution factor
|
||||
(40 increase at 0.50 will add 0.20... 40 increase at 0.75 will add 0.10...)"""
|
||||
displayname = "Evolution Trap % Effect"
|
||||
display_name = "Evolution Trap % Effect"
|
||||
range_start = 1
|
||||
default = 10
|
||||
range_end = 100
|
||||
@@ -196,7 +196,7 @@ class EvolutionTrapIncrease(Range):
|
||||
class FactorioWorldGen(OptionDict):
|
||||
"""World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator,
|
||||
with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings"""
|
||||
displayname = "World Generation"
|
||||
display_name = "World Generation"
|
||||
# FIXME: do we want default be a rando-optimized default or in-game DS?
|
||||
value: typing.Dict[str, typing.Dict[str, typing.Any]]
|
||||
default = {
|
||||
@@ -330,7 +330,7 @@ class FactorioWorldGen(OptionDict):
|
||||
|
||||
class ImportedBlueprint(DefaultOnToggle):
|
||||
"""Allow or Disallow Blueprints from outside the current savegame."""
|
||||
displayname = "Blueprints"
|
||||
display_name = "Blueprints"
|
||||
|
||||
|
||||
factorio_options: typing.Dict[str, type(Option)] = {
|
||||
|
||||
@@ -38,7 +38,9 @@ class Factorio(World):
|
||||
|
||||
item_name_to_id = all_items
|
||||
location_name_to_id = base_tech_table
|
||||
|
||||
item_name_groups = {
|
||||
"Progressive": set(progressive_tech_table.values()),
|
||||
}
|
||||
data_version = 5
|
||||
|
||||
def __init__(self, world, player: int):
|
||||
@@ -74,7 +76,6 @@ class Factorio(World):
|
||||
|
||||
self.sending_visible = self.world.tech_tree_information[player] == TechTreeInformation.option_full
|
||||
|
||||
|
||||
generate_output = generate_mod
|
||||
|
||||
def create_regions(self):
|
||||
@@ -145,12 +146,12 @@ class Factorio(World):
|
||||
silo_recipe = None
|
||||
if self.world.silo[self.player] == Silo.option_spawn:
|
||||
silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \
|
||||
else next(iter(all_product_sources.get("rocket-silo")))
|
||||
else next(iter(all_product_sources.get("rocket-silo")))
|
||||
part_recipe = self.custom_recipes["rocket-part"]
|
||||
satellite_recipe = None
|
||||
if self.world.goal[self.player] == Goal.option_satellite:
|
||||
satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \
|
||||
else next(iter(all_product_sources.get("satellite")))
|
||||
else next(iter(all_product_sources.get("satellite")))
|
||||
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe)
|
||||
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
|
||||
for technology in
|
||||
@@ -205,7 +206,8 @@ class Factorio(World):
|
||||
new_ingredient = pool.pop()
|
||||
liquids_used += 1
|
||||
new_ingredients[new_ingredient] = 1
|
||||
return Recipe(original.name, self.get_category(original.category, liquids_used), new_ingredients, original.products, original.energy)
|
||||
return Recipe(original.name, self.get_category(original.category, liquids_used), new_ingredients,
|
||||
original.products, original.energy)
|
||||
|
||||
def make_balanced_recipe(self, original: Recipe, pool: list, factor: float = 1, allow_liquids: int = 2) -> \
|
||||
Recipe:
|
||||
@@ -225,7 +227,7 @@ class Factorio(World):
|
||||
while remaining_num_ingredients > 0 and pool:
|
||||
ingredient = pool.pop()
|
||||
if liquids_used == allow_liquids and ingredient in liquids:
|
||||
continue # can't use this ingredient as we already have maximum liquid in our recipe.
|
||||
continue # can't use this ingredient as we already have maximum liquid in our recipe.
|
||||
if ingredient in all_product_sources:
|
||||
ingredient_recipe = min(all_product_sources[ingredient], key=lambda recipe: recipe.rel_cost)
|
||||
ingredient_raw = sum((count for ingredient, count in ingredient_recipe.base_cost.items()))
|
||||
@@ -292,7 +294,8 @@ class Factorio(World):
|
||||
if remaining_num_ingredients > 1:
|
||||
logging.warning("could not randomize recipe")
|
||||
|
||||
return Recipe(original.name, self.get_category(original.category, liquids_used), new_ingredients, original.products, original.energy)
|
||||
return Recipe(original.name, self.get_category(original.category, liquids_used), new_ingredients,
|
||||
original.products, original.energy)
|
||||
|
||||
def set_custom_technologies(self):
|
||||
custom_technologies = {}
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
factorio-rcon-py>=1.2.1
|
||||
schema>=0.7.4
|
||||
|
||||
@@ -4,15 +4,15 @@ from Options import OptionDict
|
||||
|
||||
|
||||
class Locations(OptionDict):
|
||||
displayname = "locations"
|
||||
display_name = "locations"
|
||||
|
||||
|
||||
class Items(OptionDict):
|
||||
displayname = "items"
|
||||
display_name = "items"
|
||||
|
||||
|
||||
class Rules(OptionDict):
|
||||
displayname = "rules"
|
||||
display_name = "rules"
|
||||
|
||||
|
||||
ff1_options: Dict[str, OptionDict] = {
|
||||
|
||||
@@ -32,9 +32,9 @@ def exclusion_rules(world, player: int, exclude_locations: typing.Set[str]):
|
||||
raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e
|
||||
else:
|
||||
add_item_rule(location, lambda i: not (i.advancement or i.never_exclude))
|
||||
location.excluded = True
|
||||
location.progress_type = LocationProgressType.EXCLUDED
|
||||
|
||||
|
||||
def set_rule(spot, rule: CollectionRule):
|
||||
spot.access_rule = rule
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ class GenericWorld(World):
|
||||
}
|
||||
hidden = True
|
||||
|
||||
def generate_early(self):
|
||||
self.world.player_types[self.player] = 0 # mark as spectator
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
if name == "Nothing":
|
||||
return Item(name, False, -1, self.player)
|
||||
|
||||
@@ -4,7 +4,7 @@ from Options import Choice, Option, Toggle, Range, OptionList, DeathLink
|
||||
|
||||
class AdvancementGoal(Range):
|
||||
"""Number of advancements required to spawn bosses."""
|
||||
displayname = "Advancement Goal"
|
||||
display_name = "Advancement Goal"
|
||||
range_start = 0
|
||||
range_end = 92
|
||||
default = 40
|
||||
@@ -12,7 +12,7 @@ class AdvancementGoal(Range):
|
||||
|
||||
class EggShardsRequired(Range):
|
||||
"""Number of dragon egg shards to collect to spawn bosses."""
|
||||
displayname = "Egg Shards Required"
|
||||
display_name = "Egg Shards Required"
|
||||
range_start = 0
|
||||
range_end = 40
|
||||
default = 0
|
||||
@@ -20,7 +20,7 @@ class EggShardsRequired(Range):
|
||||
|
||||
class EggShardsAvailable(Range):
|
||||
"""Number of dragon egg shards available to collect."""
|
||||
displayname = "Egg Shards Available"
|
||||
display_name = "Egg Shards Available"
|
||||
range_start = 0
|
||||
range_end = 40
|
||||
default = 0
|
||||
@@ -28,7 +28,7 @@ class EggShardsAvailable(Range):
|
||||
|
||||
class BossGoal(Choice):
|
||||
"""Bosses which must be defeated to finish the game."""
|
||||
displayname = "Required Bosses"
|
||||
display_name = "Required Bosses"
|
||||
option_none = 0
|
||||
option_ender_dragon = 1
|
||||
option_wither = 2
|
||||
@@ -38,19 +38,19 @@ class BossGoal(Choice):
|
||||
|
||||
class ShuffleStructures(Toggle):
|
||||
"""Enables shuffling of villages, outposts, fortresses, bastions, and end cities."""
|
||||
displayname = "Shuffle Structures"
|
||||
display_name = "Shuffle Structures"
|
||||
default = 1
|
||||
|
||||
|
||||
class StructureCompasses(Toggle):
|
||||
"""Adds structure compasses to the item pool, which point to the nearest indicated structure."""
|
||||
displayname = "Structure Compasses"
|
||||
display_name = "Structure Compasses"
|
||||
default = 1
|
||||
|
||||
|
||||
class BeeTraps(Range):
|
||||
"""Replaces a percentage of junk items with bee traps, which spawn multiple angered bees around every player when received."""
|
||||
displayname = "Bee Trap Percentage"
|
||||
display_name = "Bee Trap Percentage"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 0
|
||||
@@ -58,7 +58,7 @@ class BeeTraps(Range):
|
||||
|
||||
class CombatDifficulty(Choice):
|
||||
"""Modifies the level of items logically required for exploring dangerous areas and fighting bosses."""
|
||||
displayname = "Combat Difficulty"
|
||||
display_name = "Combat Difficulty"
|
||||
option_easy = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
@@ -67,31 +67,31 @@ class CombatDifficulty(Choice):
|
||||
|
||||
class HardAdvancements(Toggle):
|
||||
"""Enables certain RNG-reliant or tedious advancements."""
|
||||
displayname = "Include Hard Advancements"
|
||||
display_name = "Include Hard Advancements"
|
||||
default = 0
|
||||
|
||||
|
||||
class UnreasonableAdvancements(Toggle):
|
||||
"""Enables the extremely difficult advancements "How Did We Get Here?" and "Adventuring Time.\""""
|
||||
displayname = "Include Unreasonable Advancements"
|
||||
display_name = "Include Unreasonable Advancements"
|
||||
default = 0
|
||||
|
||||
|
||||
class PostgameAdvancements(Toggle):
|
||||
"""Enables advancements that require spawning and defeating the required bosses."""
|
||||
displayname = "Include Postgame Advancements"
|
||||
display_name = "Include Postgame Advancements"
|
||||
default = 0
|
||||
|
||||
|
||||
class SendDefeatedMobs(Toggle):
|
||||
"""Send killed mobs to other Minecraft worlds which have this option enabled."""
|
||||
displayname = "Send Defeated Mobs"
|
||||
display_name = "Send Defeated Mobs"
|
||||
default = 0
|
||||
|
||||
|
||||
class StartingItems(OptionList):
|
||||
"""Start with these items. Each entry should be of this format: {item: "item_name", amount: #, nbt: "nbt_string"}"""
|
||||
displayname = "Starting Items"
|
||||
display_name = "Starting Items"
|
||||
|
||||
|
||||
minecraft_options: typing.Dict[str, type(Option)] = {
|
||||
|
||||
@@ -4,7 +4,7 @@ from Options import Choice
|
||||
|
||||
class kokiri_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Kokiri Tunic"
|
||||
display_name = "Kokiri Tunic"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_kokiri_green = 2
|
||||
@@ -43,7 +43,7 @@ class kokiri_color(Choice):
|
||||
|
||||
class goron_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Goron Tunic"
|
||||
display_name = "Goron Tunic"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_kokiri_green = 2
|
||||
@@ -82,7 +82,7 @@ class goron_color(Choice):
|
||||
|
||||
class zora_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Zora Tunic"
|
||||
display_name = "Zora Tunic"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_kokiri_green = 2
|
||||
@@ -121,7 +121,7 @@ class zora_color(Choice):
|
||||
|
||||
class silver_gauntlets_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Silver Gauntlets Color"
|
||||
display_name = "Silver Gauntlets Color"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_silver = 2
|
||||
@@ -142,7 +142,7 @@ class silver_gauntlets_color(Choice):
|
||||
|
||||
class golden_gauntlets_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Golden Gauntlets Color"
|
||||
display_name = "Golden Gauntlets Color"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_silver = 2
|
||||
@@ -163,7 +163,7 @@ class golden_gauntlets_color(Choice):
|
||||
|
||||
class mirror_shield_frame_color(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Mirror Shield Frame Color"
|
||||
display_name = "Mirror Shield Frame Color"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_red = 2
|
||||
@@ -181,7 +181,7 @@ class mirror_shield_frame_color(Choice):
|
||||
|
||||
class navi_color_default_inner(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Navi Idle Inner"
|
||||
display_name = "Navi Idle Inner"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
@@ -209,7 +209,7 @@ class navi_color_default_inner(Choice):
|
||||
|
||||
class navi_color_default_outer(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. "match_inner" copies the inner color for this option."""
|
||||
displayname = "Navi Idle Outer"
|
||||
display_name = "Navi Idle Outer"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
@@ -238,7 +238,7 @@ class navi_color_default_outer(Choice):
|
||||
|
||||
class navi_color_enemy_inner(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Navi Targeting Enemy Inner"
|
||||
display_name = "Navi Targeting Enemy Inner"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
@@ -266,7 +266,7 @@ class navi_color_enemy_inner(Choice):
|
||||
|
||||
class navi_color_enemy_outer(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. "match_inner" copies the inner color for this option."""
|
||||
displayname = "Navi Targeting Enemy Outer"
|
||||
display_name = "Navi Targeting Enemy Outer"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
@@ -295,7 +295,7 @@ class navi_color_enemy_outer(Choice):
|
||||
|
||||
class navi_color_npc_inner(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Navi Targeting NPC Inner"
|
||||
display_name = "Navi Targeting NPC Inner"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
@@ -323,7 +323,7 @@ class navi_color_npc_inner(Choice):
|
||||
|
||||
class navi_color_npc_outer(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. "match_inner" copies the inner color for this option."""
|
||||
displayname = "Navi Targeting NPC Outer"
|
||||
display_name = "Navi Targeting NPC Outer"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
@@ -352,7 +352,7 @@ class navi_color_npc_outer(Choice):
|
||||
|
||||
class navi_color_prop_inner(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Navi Targeting Prop Inner"
|
||||
display_name = "Navi Targeting Prop Inner"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
@@ -380,7 +380,7 @@ class navi_color_prop_inner(Choice):
|
||||
|
||||
class navi_color_prop_outer(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. "match_inner" copies the inner color for this option."""
|
||||
displayname = "Navi Targeting Prop Outer"
|
||||
display_name = "Navi Targeting Prop Outer"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
@@ -409,7 +409,7 @@ class navi_color_prop_outer(Choice):
|
||||
|
||||
class sword_trail_color_inner(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Sword Trail Inner"
|
||||
display_name = "Sword Trail Inner"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
@@ -428,7 +428,7 @@ class sword_trail_color_inner(Choice):
|
||||
|
||||
class sword_trail_color_outer(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. "match_inner" copies the inner color for this option."""
|
||||
displayname = "Sword Trail Outer"
|
||||
display_name = "Sword Trail Outer"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
@@ -448,7 +448,7 @@ class sword_trail_color_outer(Choice):
|
||||
|
||||
class bombchu_trail_color_inner(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Bombchu Trail Inner"
|
||||
display_name = "Bombchu Trail Inner"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
@@ -466,7 +466,7 @@ class bombchu_trail_color_inner(Choice):
|
||||
|
||||
class bombchu_trail_color_outer(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. "match_inner" copies the inner color for this option."""
|
||||
displayname = "Bombchu Trail Outer"
|
||||
display_name = "Bombchu Trail Outer"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
@@ -485,7 +485,7 @@ class bombchu_trail_color_outer(Choice):
|
||||
|
||||
class boomerang_trail_color_inner(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = "Boomerang Trail Inner"
|
||||
display_name = "Boomerang Trail Inner"
|
||||
option_random_choice = 0
|
||||
option_completely_random = 1
|
||||
option_rainbow = 2
|
||||
|
||||
@@ -365,15 +365,15 @@ def shuffle_random_entrances(ootworld):
|
||||
|
||||
if ootworld.owl_drops:
|
||||
one_way_entrance_pools['OwlDrop'] = ootworld.get_shufflable_entrances(type='OwlDrop')
|
||||
if ootworld.spawn_positions:
|
||||
one_way_entrance_pools['Spawn'] = ootworld.get_shufflable_entrances(type='Spawn')
|
||||
if ootworld.warp_songs:
|
||||
one_way_entrance_pools['WarpSong'] = ootworld.get_shufflable_entrances(type='WarpSong')
|
||||
if world.accessibility[player].current_key != 'minimal' and ootworld.logic_rules == 'glitchless':
|
||||
if ootworld.logic_rules == 'glitchless':
|
||||
one_way_priorities['Bolero'] = priority_entrance_table['Bolero']
|
||||
one_way_priorities['Nocturne'] = priority_entrance_table['Nocturne']
|
||||
if not ootworld.shuffle_dungeon_entrances and not ootworld.shuffle_overworld_entrances:
|
||||
one_way_priorities['Requiem'] = priority_entrance_table['Requiem']
|
||||
if ootworld.spawn_positions:
|
||||
one_way_entrance_pools['Spawn'] = ootworld.get_shufflable_entrances(type='Spawn')
|
||||
|
||||
if ootworld.shuffle_dungeon_entrances:
|
||||
entrance_pools['Dungeon'] = ootworld.get_shufflable_entrances(type='Dungeon', only_primary=True)
|
||||
@@ -577,11 +577,16 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al
|
||||
for entrance in avail_pool:
|
||||
if entrance.replaces:
|
||||
continue
|
||||
# With mask hints, child needs to be able to access the gossip stone.
|
||||
if entrance.parent_region.name == 'Adult Spawn' and (priority_name != 'Nocturne' or ootworld.hints == 'mask'):
|
||||
continue
|
||||
# With dungeons unshuffled, adult needs to be able to access Shadow Temple.
|
||||
if not ootworld.shuffle_dungeon_entrances and priority_name == 'Nocturne':
|
||||
if entrance.type != 'WarpSong' and entrance.parent_region.name != 'Adult Spawn':
|
||||
continue
|
||||
# With overworld unshuffled, child can't spawn at Desert Colossus
|
||||
if not ootworld.shuffle_overworld_entrances and priority_name == 'Requiem' and entrance.parent_region.name == 'Child Spawn':
|
||||
continue
|
||||
for target in one_way_target_entrance_pools[entrance.type]:
|
||||
if target.connected_region and target.connected_region.name in allowed_regions:
|
||||
if replace_entrance(ootworld, entrance, target, rollbacks, locations_to_ensure_reachable, all_state, none_state):
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import random
|
||||
|
||||
from BaseClasses import LocationProgressType
|
||||
|
||||
# Abbreviations
|
||||
# DMC Death Mountain Crater
|
||||
# DMT Death Mountain Trail
|
||||
@@ -1257,7 +1259,7 @@ def hintExclusions(world, clear_cache=False):
|
||||
world.hint_exclusions = []
|
||||
|
||||
for location in world.get_locations():
|
||||
if (location.locked and (location.item.type != 'Song' or world.shuffle_song_items != 'song')) or location.excluded:
|
||||
if (location.locked and (location.item.type != 'Song' or world.shuffle_song_items != 'song')) or location.progress_type == LocationProgressType.EXCLUDED:
|
||||
world.hint_exclusions.append(location.name)
|
||||
|
||||
world_location_names = [
|
||||
|
||||
@@ -33,6 +33,7 @@ class OOTItem(Item):
|
||||
self.looks_like_item = None
|
||||
self.price = special.get('price', None) if special else None
|
||||
self.internal = False
|
||||
self.trap = name == 'Ice Trap'
|
||||
if force_not_advancement:
|
||||
self.never_exclude = True
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from .ColorSFXOptions import *
|
||||
|
||||
class Logic(Choice):
|
||||
"""Set the logic used for the generator."""
|
||||
displayname = "Logic Rules"
|
||||
display_name = "Logic Rules"
|
||||
option_glitchless = 0
|
||||
option_glitched = 1
|
||||
option_no_logic = 2
|
||||
@@ -14,12 +14,12 @@ class Logic(Choice):
|
||||
|
||||
class NightTokens(Toggle):
|
||||
"""Nighttime skulltulas will logically require Sun's Song."""
|
||||
displayname = "Nighttime Skulltulas Expect Sun's Song"
|
||||
display_name = "Nighttime Skulltulas Expect Sun's Song"
|
||||
|
||||
|
||||
class Forest(Choice):
|
||||
"""Set the state of Kokiri Forest and the path to Deku Tree."""
|
||||
displayname = "Forest"
|
||||
display_name = "Forest"
|
||||
option_open = 0
|
||||
option_closed_deku = 1
|
||||
option_closed = 2
|
||||
@@ -29,7 +29,7 @@ class Forest(Choice):
|
||||
|
||||
class Gate(Choice):
|
||||
"""Set the state of the Kakariko Village gate."""
|
||||
displayname = "Kakariko Gate"
|
||||
display_name = "Kakariko Gate"
|
||||
option_open = 0
|
||||
option_zelda = 1
|
||||
option_closed = 2
|
||||
@@ -37,12 +37,12 @@ class Gate(Choice):
|
||||
|
||||
class DoorOfTime(DefaultOnToggle):
|
||||
"""Open the Door of Time by default, without the Song of Time."""
|
||||
displayname = "Open Door of Time"
|
||||
display_name = "Open Door of Time"
|
||||
|
||||
|
||||
class Fountain(Choice):
|
||||
"""Set the state of King Zora, blocking the way to Zora's Fountain."""
|
||||
displayname = "Zora's Fountain"
|
||||
display_name = "Zora's Fountain"
|
||||
option_open = 0
|
||||
option_adult = 1
|
||||
option_closed = 2
|
||||
@@ -51,7 +51,7 @@ class Fountain(Choice):
|
||||
|
||||
class Fortress(Choice):
|
||||
"""Set the requirements for access to Gerudo Fortress."""
|
||||
displayname = "Gerudo Fortress"
|
||||
display_name = "Gerudo Fortress"
|
||||
option_normal = 0
|
||||
option_fast = 1
|
||||
option_open = 2
|
||||
@@ -60,7 +60,7 @@ class Fortress(Choice):
|
||||
|
||||
class Bridge(Choice):
|
||||
"""Set the requirements for the Rainbow Bridge."""
|
||||
displayname = "Rainbow Bridge Requirement"
|
||||
display_name = "Rainbow Bridge Requirement"
|
||||
option_open = 0
|
||||
option_vanilla = 1
|
||||
option_stones = 2
|
||||
@@ -72,7 +72,7 @@ class Bridge(Choice):
|
||||
|
||||
class Trials(Range):
|
||||
"""Set the number of required trials in Ganon's Castle."""
|
||||
displayname = "Ganon's Trials Count"
|
||||
display_name = "Ganon's Trials Count"
|
||||
range_start = 0
|
||||
range_end = 6
|
||||
|
||||
@@ -90,14 +90,14 @@ open_options: typing.Dict[str, type(Option)] = {
|
||||
|
||||
class StartingAge(Choice):
|
||||
"""Choose which age Link will start as."""
|
||||
displayname = "Starting Age"
|
||||
display_name = "Starting Age"
|
||||
option_child = 0
|
||||
option_adult = 1
|
||||
|
||||
|
||||
class InteriorEntrances(Choice):
|
||||
"""Shuffles interior entrances. "Simple" shuffles houses and Great Fairies; "All" includes Windmill, Link's House, Temple of Time, and Kak potion shop."""
|
||||
displayname = "Shuffle Interior Entrances"
|
||||
display_name = "Shuffle Interior Entrances"
|
||||
option_off = 0
|
||||
option_simple = 1
|
||||
option_all = 2
|
||||
@@ -107,37 +107,37 @@ class InteriorEntrances(Choice):
|
||||
|
||||
class GrottoEntrances(Toggle):
|
||||
"""Shuffles grotto and grave entrances."""
|
||||
displayname = "Shuffle Grotto/Grave Entrances"
|
||||
display_name = "Shuffle Grotto/Grave Entrances"
|
||||
|
||||
|
||||
class DungeonEntrances(Toggle):
|
||||
"""Shuffles dungeon entrances, excluding Ganon's Castle. Opens Deku, Fire and BotW to both ages."""
|
||||
displayname = "Shuffle Dungeon Entrances"
|
||||
display_name = "Shuffle Dungeon Entrances"
|
||||
|
||||
|
||||
class OverworldEntrances(Toggle):
|
||||
"""Shuffles overworld loading zones."""
|
||||
displayname = "Shuffle Overworld Entrances"
|
||||
display_name = "Shuffle Overworld Entrances"
|
||||
|
||||
|
||||
class OwlDrops(Toggle):
|
||||
"""Randomizes owl drops from Lake Hylia or Death Mountain Trail as child."""
|
||||
displayname = "Randomize Owl Drops"
|
||||
display_name = "Randomize Owl Drops"
|
||||
|
||||
|
||||
class WarpSongs(Toggle):
|
||||
"""Randomizes warp song destinations."""
|
||||
displayname = "Randomize Warp Songs"
|
||||
display_name = "Randomize Warp Songs"
|
||||
|
||||
|
||||
class SpawnPositions(Toggle):
|
||||
"""Randomizes the starting position on loading a save. Consistent between savewarps."""
|
||||
displayname = "Randomize Spawn Positions"
|
||||
display_name = "Randomize Spawn Positions"
|
||||
|
||||
|
||||
class MixEntrancePools(Choice):
|
||||
"""Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all" mixes them in."""
|
||||
displayname = "Mix Entrance Pools"
|
||||
display_name = "Mix Entrance Pools"
|
||||
option_off = 0
|
||||
option_indoor = 1
|
||||
option_all = 2
|
||||
@@ -146,17 +146,17 @@ class MixEntrancePools(Choice):
|
||||
|
||||
class DecoupleEntrances(Toggle):
|
||||
"""Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if overworld is shuffled."""
|
||||
displayname = "Decouple Entrances"
|
||||
display_name = "Decouple Entrances"
|
||||
|
||||
|
||||
class TriforceHunt(Toggle):
|
||||
"""Gather pieces of the Triforce scattered around the world to complete the game."""
|
||||
displayname = "Triforce Hunt"
|
||||
display_name = "Triforce Hunt"
|
||||
|
||||
|
||||
class TriforceGoal(Range):
|
||||
"""Number of Triforce pieces required to complete the game."""
|
||||
displayname = "Required Triforce Pieces"
|
||||
display_name = "Required Triforce Pieces"
|
||||
range_start = 1
|
||||
range_end = 100
|
||||
default = 20
|
||||
@@ -164,7 +164,7 @@ class TriforceGoal(Range):
|
||||
|
||||
class ExtraTriforces(Range):
|
||||
"""Percentage of additional Triforce pieces in the pool, separate from the item pool setting."""
|
||||
displayname = "Percentage of Extra Triforce Pieces"
|
||||
display_name = "Percentage of Extra Triforce Pieces"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 50
|
||||
|
||||
@@ -31,7 +31,7 @@ from BaseClasses import MultiWorld, CollectionState, RegionType
|
||||
from Options import Range, Toggle, OptionList
|
||||
from Fill import fill_restrictive, FillError
|
||||
from worlds.generic.Rules import exclusion_rules
|
||||
from ..AutoWorld import World
|
||||
from ..AutoWorld import World, AutoLogicRegister
|
||||
|
||||
location_id_offset = 67000
|
||||
|
||||
@@ -39,6 +39,33 @@ location_id_offset = 67000
|
||||
i_o_limiter = threading.Semaphore(2)
|
||||
|
||||
|
||||
class OOTCollectionState(metaclass=AutoLogicRegister):
|
||||
def init_mixin(self, parent: MultiWorld):
|
||||
all_ids = parent.get_all_ids()
|
||||
self.child_reachable_regions = {player: set() for player in all_ids}
|
||||
self.adult_reachable_regions = {player: set() for player in all_ids}
|
||||
self.child_blocked_connections = {player: set() for player in all_ids}
|
||||
self.adult_blocked_connections = {player: set() for player in all_ids}
|
||||
self.day_reachable_regions = {player: set() for player in all_ids}
|
||||
self.dampe_reachable_regions = {player: set() for player in all_ids}
|
||||
self.age = {player: None for player in all_ids}
|
||||
|
||||
def copy_mixin(self, ret) -> CollectionState:
|
||||
ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in
|
||||
self.child_reachable_regions}
|
||||
ret.adult_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
|
||||
self.adult_reachable_regions}
|
||||
ret.child_blocked_connections = {player: copy.copy(self.child_blocked_connections[player]) for player in
|
||||
self.child_blocked_connections}
|
||||
ret.adult_blocked_connections = {player: copy.copy(self.adult_blocked_connections[player]) for player in
|
||||
self.adult_blocked_connections}
|
||||
ret.day_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
|
||||
self.day_reachable_regions}
|
||||
ret.dampe_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
|
||||
self.dampe_reachable_regions}
|
||||
return ret
|
||||
|
||||
|
||||
class OOTWorld(World):
|
||||
"""
|
||||
The Legend of Zelda: Ocarina of Time is a 3D action/adventure game. Travel through Hyrule in two time periods,
|
||||
@@ -56,55 +83,10 @@ class OOTWorld(World):
|
||||
|
||||
data_version = 1
|
||||
|
||||
def __new__(cls, world, player):
|
||||
# Add necessary objects to CollectionState on initialization
|
||||
orig_init = CollectionState.__init__
|
||||
orig_copy = CollectionState.copy
|
||||
|
||||
def oot_init(self, parent: MultiWorld):
|
||||
orig_init(self, parent)
|
||||
self.child_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
|
||||
self.adult_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
|
||||
self.child_blocked_connections = {player: set() for player in range(1, parent.players + 1)}
|
||||
self.adult_blocked_connections = {player: set() for player in range(1, parent.players + 1)}
|
||||
self.day_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
|
||||
self.dampe_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
|
||||
self.age = {player: None for player in range(1, parent.players + 1)}
|
||||
|
||||
def oot_copy(self):
|
||||
ret = orig_copy(self)
|
||||
ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in
|
||||
range(1, self.world.players + 1)}
|
||||
ret.adult_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
|
||||
range(1, self.world.players + 1)}
|
||||
ret.child_blocked_connections = {player: copy.copy(self.child_blocked_connections[player]) for player in
|
||||
range(1, self.world.players + 1)}
|
||||
ret.adult_blocked_connections = {player: copy.copy(self.adult_blocked_connections[player]) for player in
|
||||
range(1, self.world.players + 1)}
|
||||
ret.day_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
|
||||
range(1, self.world.players + 1)}
|
||||
ret.dampe_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
|
||||
range(1, self.world.players + 1)}
|
||||
return ret
|
||||
|
||||
CollectionState.__init__ = oot_init
|
||||
CollectionState.copy = oot_copy
|
||||
# also need to add the names to the passed MultiWorld's CollectionState, since it was initialized before we could get to it
|
||||
if world:
|
||||
world.state.child_reachable_regions = {player: set() for player in range(1, world.players + 1)}
|
||||
world.state.adult_reachable_regions = {player: set() for player in range(1, world.players + 1)}
|
||||
world.state.child_blocked_connections = {player: set() for player in range(1, world.players + 1)}
|
||||
world.state.adult_blocked_connections = {player: set() for player in range(1, world.players + 1)}
|
||||
world.state.day_reachable_regions = {player: set() for player in range(1, world.players + 1)}
|
||||
world.state.dampe_reachable_regions = {player: set() for player in range(1, world.players + 1)}
|
||||
world.state.age = {player: None for player in range(1, world.players + 1)}
|
||||
|
||||
return super().__new__(cls)
|
||||
|
||||
def __init__(self, world, player):
|
||||
self.hint_data_available = threading.Event()
|
||||
super(OOTWorld, self).__init__(world, player)
|
||||
|
||||
|
||||
def generate_early(self):
|
||||
# Player name MUST be at most 16 bytes ascii-encoded, otherwise won't write to ROM correctly
|
||||
if len(bytes(self.world.get_player_name(self.player), 'ascii')) > 16:
|
||||
@@ -159,6 +141,9 @@ class OOTWorld(World):
|
||||
# Closed forest and adult start are not compatible; closed forest takes priority
|
||||
if self.open_forest == 'closed':
|
||||
self.starting_age = 'child'
|
||||
# These ER options force closed forest to become closed deku
|
||||
if (self.shuffle_interior_entrances == 'all' or self.shuffle_overworld_entrances or self.warp_songs or self.spawn_positions):
|
||||
self.open_forest = 'closed_deku'
|
||||
|
||||
# Skip child zelda and shuffle egg are not compatible; skip-zelda takes priority
|
||||
if self.skip_child_zelda:
|
||||
@@ -214,7 +199,8 @@ class OOTWorld(World):
|
||||
self.shopsanity = str(self.shop_slots)
|
||||
|
||||
# fixing some options
|
||||
self.starting_tod = self.starting_tod.replace('_', '-') # Fixes starting time spelling: "witching_hour" -> "witching-hour"
|
||||
# Fixes starting time spelling: "witching_hour" -> "witching-hour"
|
||||
self.starting_tod = self.starting_tod.replace('_', '-')
|
||||
self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '')
|
||||
|
||||
# Get hint distribution
|
||||
@@ -255,14 +241,14 @@ class OOTWorld(World):
|
||||
|
||||
# Determine items which are not considered advancement based on settings. They will never be excluded.
|
||||
self.nonadvancement_items = {'Double Defense', 'Ice Arrows'}
|
||||
if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and
|
||||
if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and
|
||||
self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances):
|
||||
# nayru's love may be required to prevent forced damage
|
||||
self.nonadvancement_items.add('Nayrus Love')
|
||||
if getattr(self, 'logic_grottos_without_agony', False) and self.hints != 'agony':
|
||||
# Stone of Agony skippable if not used for hints or grottos
|
||||
self.nonadvancement_items.add('Stone of Agony')
|
||||
if (not self.shuffle_special_interior_entrances and not self.shuffle_overworld_entrances and
|
||||
if (not self.shuffle_special_interior_entrances and not self.shuffle_overworld_entrances and
|
||||
not self.warp_songs and not self.spawn_positions):
|
||||
# Serenade and Prelude are never required unless one of those settings is enabled
|
||||
self.nonadvancement_items.add('Serenade of Water')
|
||||
@@ -271,7 +257,10 @@ class OOTWorld(World):
|
||||
# Both two-handed swords can be required in glitch logic, so only consider them nonprogression in glitchless
|
||||
self.nonadvancement_items.add('Biggoron Sword')
|
||||
self.nonadvancement_items.add('Giants Knife')
|
||||
|
||||
if not getattr(self, 'logic_water_central_gs_fw', False):
|
||||
# Farore's Wind skippable if not used for this logic trick in Water Temple
|
||||
self.nonadvancement_items.add('Farores Wind')
|
||||
|
||||
def load_regions_from_json(self, file_path):
|
||||
region_json = read_json(file_path)
|
||||
|
||||
@@ -422,8 +411,9 @@ class OOTWorld(World):
|
||||
|
||||
def create_item(self, name: str):
|
||||
if name in item_table:
|
||||
return OOTItem(name, self.player, item_table[name], False,
|
||||
(name in self.nonadvancement_items if getattr(self, 'nonadvancement_items', None) else False))
|
||||
return OOTItem(name, self.player, item_table[name], False,
|
||||
(name in self.nonadvancement_items if getattr(self, 'nonadvancement_items',
|
||||
None) else False))
|
||||
return OOTItem(name, self.player, ('Event', True, None, None), True, False)
|
||||
|
||||
def make_event_item(self, name, location, item=None):
|
||||
@@ -501,7 +491,8 @@ class OOTWorld(World):
|
||||
shuffle_random_entrances(self)
|
||||
except EntranceShuffleError as e:
|
||||
tries -= 1
|
||||
logging.getLogger('').debug(f"Failed shuffling entrances for world {self.player}, retrying {tries} more times")
|
||||
logger.debug(
|
||||
f"Failed shuffling entrances for world {self.player}, retrying {tries} more times")
|
||||
if tries == 0:
|
||||
raise e
|
||||
# Restore original state and delete assumed entrances
|
||||
@@ -580,8 +571,10 @@ class OOTWorld(World):
|
||||
"Spirit Temple Twinrova Heart",
|
||||
"Song from Impa",
|
||||
"Sheik in Ice Cavern",
|
||||
"Bottom of the Well Lens of Truth Chest", "Bottom of the Well MQ Lens of Truth Chest", # only one exists
|
||||
"Gerudo Training Grounds Maze Path Final Chest", "Gerudo Training Grounds MQ Ice Arrows Chest", # only one exists
|
||||
# only one exists
|
||||
"Bottom of the Well Lens of Truth Chest", "Bottom of the Well MQ Lens of Truth Chest",
|
||||
# only one exists
|
||||
"Gerudo Training Grounds Maze Path Final Chest", "Gerudo Training Grounds MQ Ice Arrows Chest",
|
||||
]
|
||||
|
||||
# Place/set rules for dungeon items
|
||||
@@ -606,7 +599,7 @@ class OOTWorld(World):
|
||||
# We can't put a dungeon item on the end of a dungeon if a song is supposed to go there. Make sure not to include it.
|
||||
dungeon_locations = [loc for region in dungeon.regions for loc in region.locations
|
||||
if loc.item is None and (
|
||||
self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)]
|
||||
self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)]
|
||||
if itempools['dungeon']: # only do this if there's anything to shuffle
|
||||
for item in itempools['dungeon']:
|
||||
self.world.itempool.remove(item)
|
||||
@@ -617,28 +610,32 @@ class OOTWorld(World):
|
||||
|
||||
# Now fill items that can go into any dungeon. Retrieve the Gerudo Fortress keys from the pool if necessary
|
||||
if self.shuffle_fortresskeys == 'any_dungeon':
|
||||
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.world.itempool)
|
||||
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey',
|
||||
self.world.itempool)
|
||||
itempools['any_dungeon'].extend(fortresskeys)
|
||||
if itempools['any_dungeon']:
|
||||
for item in itempools['any_dungeon']:
|
||||
self.world.itempool.remove(item)
|
||||
itempools['any_dungeon'].sort(key=lambda item:
|
||||
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0))
|
||||
itempools['any_dungeon'].sort(key=lambda item:
|
||||
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0))
|
||||
self.world.random.shuffle(any_dungeon_locations)
|
||||
fill_restrictive(self.world, self.world.get_all_state(False), any_dungeon_locations,
|
||||
itempools['any_dungeon'], True, True)
|
||||
|
||||
# If anything is overworld-only, fill into local non-dungeon locations
|
||||
if self.shuffle_fortresskeys == 'overworld':
|
||||
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.world.itempool)
|
||||
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey',
|
||||
self.world.itempool)
|
||||
itempools['overworld'].extend(fortresskeys)
|
||||
if itempools['overworld']:
|
||||
for item in itempools['overworld']:
|
||||
self.world.itempool.remove(item)
|
||||
itempools['overworld'].sort(key=lambda item:
|
||||
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0))
|
||||
non_dungeon_locations = [loc for loc in self.get_locations() if not loc.item and loc not in any_dungeon_locations
|
||||
and loc.type != 'Shop' and (loc.type != 'Song' or self.shuffle_song_items != 'song')]
|
||||
itempools['overworld'].sort(key=lambda item:
|
||||
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0))
|
||||
non_dungeon_locations = [loc for loc in self.get_locations() if
|
||||
not loc.item and loc not in any_dungeon_locations
|
||||
and loc.type != 'Shop' and (
|
||||
loc.type != 'Song' or self.shuffle_song_items != 'song')]
|
||||
self.world.random.shuffle(non_dungeon_locations)
|
||||
fill_restrictive(self.world, self.world.get_all_state(False), non_dungeon_locations,
|
||||
itempools['overworld'], True, True)
|
||||
@@ -660,8 +657,8 @@ class OOTWorld(World):
|
||||
for song in songs:
|
||||
self.world.itempool.remove(song)
|
||||
|
||||
important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or
|
||||
self.warp_songs or self.spawn_positions)
|
||||
important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or
|
||||
self.warp_songs or self.spawn_positions)
|
||||
song_order = {
|
||||
'Zeldas Lullaby': 1,
|
||||
'Eponas Song': 1,
|
||||
@@ -703,15 +700,17 @@ class OOTWorld(World):
|
||||
# Place shop items
|
||||
# fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items
|
||||
if self.shopsanity != 'off':
|
||||
shop_items = list(filter(lambda item: item.player == self.player and item.type == 'Shop', self.world.itempool))
|
||||
shop_items = list(
|
||||
filter(lambda item: item.player == self.player and item.type == 'Shop', self.world.itempool))
|
||||
shop_locations = list(
|
||||
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
|
||||
self.world.get_unfilled_locations(player=self.player)))
|
||||
shop_items.sort(key=lambda item: {
|
||||
'Buy Deku Shield': 3*int(self.open_forest == 'closed'),
|
||||
'Buy Goron Tunic': 2,
|
||||
'Buy Deku Shield': 3 * int(self.open_forest == 'closed'),
|
||||
'Buy Goron Tunic': 2,
|
||||
'Buy Zora Tunic': 2
|
||||
}.get(item.name, int(item.advancement))) # place Deku Shields if needed, then tunics, then other advancement, then junk
|
||||
}.get(item.name,
|
||||
int(item.advancement))) # place Deku Shields if needed, then tunics, then other advancement, then junk
|
||||
self.world.random.shuffle(shop_locations)
|
||||
for item in shop_items:
|
||||
self.world.itempool.remove(item)
|
||||
@@ -806,13 +805,13 @@ class OOTWorld(World):
|
||||
@classmethod
|
||||
def stage_generate_output(cls, world: MultiWorld, output_directory: str):
|
||||
def hint_type_players(hint_type: str) -> set:
|
||||
return {autoworld.player for autoworld in world.get_game_worlds("Ocarina of Time")
|
||||
return {autoworld.player for autoworld in world.get_game_worlds("Ocarina of Time")
|
||||
if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0}
|
||||
|
||||
try:
|
||||
item_hint_players = hint_type_players('item')
|
||||
item_hint_players = hint_type_players('item')
|
||||
barren_hint_players = hint_type_players('barren')
|
||||
woth_hint_players = hint_type_players('woth')
|
||||
woth_hint_players = hint_type_players('woth')
|
||||
|
||||
items_by_region = {}
|
||||
for player in barren_hint_players:
|
||||
@@ -828,12 +827,12 @@ class OOTWorld(World):
|
||||
for loc in world.get_locations():
|
||||
player = loc.item.player
|
||||
autoworld = world.worlds[player]
|
||||
if ((player in item_hint_players and (autoworld.is_major_item(loc.item) or loc.item.name in autoworld.item_added_hint_types['item']))
|
||||
if ((player in item_hint_players and (autoworld.is_major_item(loc.item) or loc.item.name in autoworld.item_added_hint_types['item']))
|
||||
or (loc.player in item_hint_players and loc.name in world.worlds[loc.player].added_hint_types['item'])):
|
||||
autoworld.major_item_locations.append(loc)
|
||||
|
||||
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or
|
||||
(loc.item.type == 'Song' or
|
||||
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or
|
||||
(loc.item.type == 'Song' or
|
||||
(loc.item.type == 'SmallKey' and world.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or
|
||||
(loc.item.type == 'FortressSmallKey' and world.worlds[loc.player].shuffle_fortresskeys == 'any_dungeon') or
|
||||
(loc.item.type == 'BossKey' and world.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or
|
||||
@@ -841,7 +840,7 @@ class OOTWorld(World):
|
||||
if loc.player in barren_hint_players:
|
||||
hint_area = get_hint_area(loc)
|
||||
items_by_region[loc.player][hint_area]['weight'] += 1
|
||||
if loc.item.advancement:
|
||||
if loc.item.advancement or loc.item.never_exclude:
|
||||
items_by_region[loc.player][hint_area]['is_barren'] = False
|
||||
if loc.player in woth_hint_players and loc.item.advancement:
|
||||
# Skip item at location and see if game is still beatable
|
||||
@@ -864,7 +863,8 @@ class OOTWorld(World):
|
||||
if not world.can_beat_game(state):
|
||||
world.worlds[player].required_locations.append(loc)
|
||||
for player in barren_hint_players:
|
||||
world.worlds[player].empty_areas = {region: info for (region, info) in items_by_region[player].items() if info['is_barren']}
|
||||
world.worlds[player].empty_areas = {region: info for (region, info) in items_by_region[player].items()
|
||||
if info['is_barren']}
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
@@ -880,7 +880,7 @@ class OOTWorld(World):
|
||||
hint_entrances.add(entrance[2][0])
|
||||
|
||||
def get_entrance_to_region(region):
|
||||
if region.name == 'Root':
|
||||
if region.name == 'Root':
|
||||
return None
|
||||
for entrance in region.entrances:
|
||||
if entrance.name in hint_entrances:
|
||||
@@ -894,7 +894,8 @@ class OOTWorld(World):
|
||||
try:
|
||||
multidata["precollected_items"][self.player].remove(item_id)
|
||||
except ValueError as e:
|
||||
logger.warning(f"Attempted to remove nonexistent item id {item_id} from OoT precollected items ({item_name})")
|
||||
logger.warning(
|
||||
f"Attempted to remove nonexistent item id {item_id} from OoT precollected items ({item_name})")
|
||||
|
||||
# Add ER hint data
|
||||
if self.shuffle_interior_entrances != 'off' or self.shuffle_dungeon_entrances or self.shuffle_grotto_entrances:
|
||||
@@ -907,15 +908,15 @@ class OOTWorld(World):
|
||||
er_hint_data[location.address] = main_entrance.name
|
||||
multidata['er_hint_data'][self.player] = er_hint_data
|
||||
|
||||
|
||||
# Helper functions
|
||||
def get_shufflable_entrances(self, type=None, only_primary=False):
|
||||
return [entrance for entrance in self.world.get_entrances() if (entrance.player == self.player and
|
||||
(type == None or entrance.type == type) and
|
||||
(not only_primary or entrance.primary))]
|
||||
return [entrance for entrance in self.world.get_entrances() if (entrance.player == self.player and
|
||||
(type == None or entrance.type == type) and
|
||||
(not only_primary or entrance.primary))]
|
||||
|
||||
def get_shuffled_entrances(self, type=None, only_primary=False):
|
||||
return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if entrance.shuffled]
|
||||
return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if
|
||||
entrance.shuffled]
|
||||
|
||||
def get_locations(self):
|
||||
for region in self.regions:
|
||||
|
||||
@@ -17,7 +17,7 @@ def assemble_color_option(f, internal_name: str, func, display_name: str, defaul
|
||||
|
||||
f.write(f"class {internal_name}(Choice):\n")
|
||||
f.write(f" \"\"\"{docstring}\"\"\"\n")
|
||||
f.write(f" displayname = \"{display_name}\"\n")
|
||||
f.write(f" display_name = \"{display_name}\"\n")
|
||||
for color, id in color_to_id.items():
|
||||
f.write(f" option_{color} = {id}\n")
|
||||
f.write(f" default = {color_options.index(default_option)}")
|
||||
@@ -31,7 +31,7 @@ def assemble_sfx_option(f, internal_name: str, sound_hook: sfx.SoundHooks, displ
|
||||
|
||||
f.write(f"class {internal_name}(Choice):\n")
|
||||
f.write(f" \"\"\"{docstring}\"\"\"\n")
|
||||
f.write(f" displayname = \"{display_name}\"\n")
|
||||
f.write(f" display_name = \"{display_name}\"\n")
|
||||
for sound, id in sfx_to_id.items():
|
||||
f.write(f" option_{sound} = {id}\n")
|
||||
f.write(f"\n\n\n")
|
||||
|
||||
@@ -2,18 +2,18 @@ from Options import Range, Toggle, DefaultOnToggle, Choice
|
||||
|
||||
class UseResourcePacks(DefaultOnToggle):
|
||||
"""Uses Resource Packs to fill out the item pool from Raft. Resource Packs have basic earlygame items such as planks, plastic, or food."""
|
||||
displayname = "Use resource packs"
|
||||
display_name = "Use resource packs"
|
||||
|
||||
class MinimumResourcePackAmount(Range):
|
||||
"""The minimum amount of resources available in a resource pack"""
|
||||
displayname = "Minimum resource pack amount"
|
||||
display_name = "Minimum resource pack amount"
|
||||
range_start = 1
|
||||
range_end = 15
|
||||
default = 1
|
||||
|
||||
class MaximumResourcePackAmount(Range):
|
||||
"""The maximum amount of resources available in a resource pack"""
|
||||
displayname = "Maximum resource pack amount"
|
||||
display_name = "Maximum resource pack amount"
|
||||
range_start = 1
|
||||
range_end = 15
|
||||
default = 5
|
||||
@@ -22,7 +22,7 @@ class DuplicateItems(Choice):
|
||||
"""Adds duplicates of items to the item pool. These will be selected alongside
|
||||
Resource Packs (if configured). Note that there are not many progression items,
|
||||
and selecting Progression may produce many of the same duplicate item."""
|
||||
displayname = "Duplicate items"
|
||||
display_name = "Duplicate items"
|
||||
option_disabled = 0
|
||||
option_progression = 1
|
||||
option_non_progression = 2
|
||||
@@ -30,7 +30,7 @@ class DuplicateItems(Choice):
|
||||
|
||||
class IslandFrequencyLocations(Choice):
|
||||
"""Sets where frequencies for story islands are located."""
|
||||
displayname = "Frequency locations"
|
||||
display_name = "Frequency locations"
|
||||
option_vanilla = 0
|
||||
option_random_on_island = 1
|
||||
option_progressive = 2
|
||||
@@ -39,15 +39,15 @@ class IslandFrequencyLocations(Choice):
|
||||
|
||||
class ProgressiveItems(DefaultOnToggle):
|
||||
"""Makes some items, like the Bow and Arrow, progressive rather than raw unlocks."""
|
||||
displayname = "Progressive items"
|
||||
display_name = "Progressive items"
|
||||
|
||||
class BigIslandEarlyCrafting(Toggle):
|
||||
"""Allows recipes that require items from big islands (eg leather) to lock earlygame items like the Receiver, Bolt, or Smelter."""
|
||||
displayname = "Early recipes behind big islands"
|
||||
display_name = "Early recipes behind big islands"
|
||||
|
||||
class PaddleboardMode(Toggle):
|
||||
"""Sets later story islands to in logic without an Engine or Steering Wheel. May require lots of paddling. Not recommended."""
|
||||
displayname = "Paddleboard Mode"
|
||||
display_name = "Paddleboard Mode"
|
||||
|
||||
raft_options = {
|
||||
"use_resource_packs": UseResourcePacks,
|
||||
|
||||
@@ -7,7 +7,7 @@ class StartingGender(Choice):
|
||||
"""
|
||||
Determines the gender of your initial 'Sir Lee' character.
|
||||
"""
|
||||
displayname = "Starting Gender"
|
||||
display_name = "Starting Gender"
|
||||
option_sir = 0
|
||||
option_lady = 1
|
||||
alias_male = 0
|
||||
@@ -19,7 +19,7 @@ class StartingClass(Choice):
|
||||
"""
|
||||
Determines the starting class of your initial 'Sir Lee' character.
|
||||
"""
|
||||
displayname = "Starting Class"
|
||||
display_name = "Starting Class"
|
||||
option_knight = 0
|
||||
option_mage = 1
|
||||
option_barbarian = 2
|
||||
@@ -36,7 +36,7 @@ class NewGamePlus(Choice):
|
||||
Puts the castle in new game plus mode which vastly increases enemy level, but increases gold gain by 50%. Not
|
||||
recommended for those inexperienced to Rogue Legacy!
|
||||
"""
|
||||
displayname = "New Game Plus"
|
||||
display_name = "New Game Plus"
|
||||
option_normal = 0
|
||||
option_new_game_plus = 1
|
||||
option_new_game_plus_2 = 2
|
||||
@@ -51,7 +51,7 @@ class LevelScaling(Range):
|
||||
100% level scaling (normal). Setting this too high will result in enemies with absurdly high levels, you have been
|
||||
warned.
|
||||
"""
|
||||
displayname = "Enemy Level Scaling Percentage"
|
||||
display_name = "Enemy Level Scaling Percentage"
|
||||
range_start = 1
|
||||
range_end = 300
|
||||
default = 100
|
||||
@@ -62,7 +62,7 @@ class FairyChestsPerZone(Range):
|
||||
Determines the number of Fairy Chests in a given zone that contain items. After these have been checked, only stat
|
||||
bonuses can be found in Fairy Chests.
|
||||
"""
|
||||
displayname = "Fairy Chests Per Zone"
|
||||
display_name = "Fairy Chests Per Zone"
|
||||
range_start = 5
|
||||
range_end = 15
|
||||
default = 5
|
||||
@@ -73,7 +73,7 @@ class ChestsPerZone(Range):
|
||||
Determines the number of Non-Fairy Chests in a given zone that contain items. After these have been checked, only
|
||||
gold or stat bonuses can be found in Chests.
|
||||
"""
|
||||
displayname = "Chests Per Zone"
|
||||
display_name = "Chests Per Zone"
|
||||
range_start = 15
|
||||
range_end = 30
|
||||
default = 15
|
||||
@@ -83,21 +83,21 @@ class UniversalFairyChests(Toggle):
|
||||
"""
|
||||
Determines if fairy chests should be combined into one pool instead of per zone, similar to Risk of Rain 2.
|
||||
"""
|
||||
displayname = "Universal Fairy Chests"
|
||||
display_name = "Universal Fairy Chests"
|
||||
|
||||
|
||||
class UniversalChests(Toggle):
|
||||
"""
|
||||
Determines if non-fairy chests should be combined into one pool instead of per zone, similar to Risk of Rain 2.
|
||||
"""
|
||||
displayname = "Universal Non-Fairy Chests"
|
||||
display_name = "Universal Non-Fairy Chests"
|
||||
|
||||
|
||||
class Vendors(Choice):
|
||||
"""
|
||||
Determines where to place the Blacksmith and Enchantress unlocks in logic (or start with them unlocked).
|
||||
"""
|
||||
displayname = "Vendors"
|
||||
display_name = "Vendors"
|
||||
option_start_unlocked = 0
|
||||
option_early = 1
|
||||
option_normal = 2
|
||||
@@ -109,7 +109,7 @@ class Architect(Choice):
|
||||
"""
|
||||
Determines where the Architect sits in the item pool.
|
||||
"""
|
||||
displayname = "Architect"
|
||||
display_name = "Architect"
|
||||
option_start_unlocked = 0
|
||||
option_normal = 2
|
||||
option_disabled = 3
|
||||
@@ -121,7 +121,7 @@ class ArchitectFee(Range):
|
||||
Determines how large of a percentage the architect takes from the player when utilizing his services. 100 means he
|
||||
takes all your gold. 0 means his services are free.
|
||||
"""
|
||||
displayname = "Architect Fee Percentage"
|
||||
display_name = "Architect Fee Percentage"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 40
|
||||
@@ -131,7 +131,7 @@ class DisableCharon(Toggle):
|
||||
"""
|
||||
Prevents Charon from taking your money when you re-enter the castle. Also removes Haggling from the Item Pool.
|
||||
"""
|
||||
displayname = "Disable Charon"
|
||||
display_name = "Disable Charon"
|
||||
|
||||
|
||||
class RequirePurchasing(DefaultOnToggle):
|
||||
@@ -139,7 +139,7 @@ class RequirePurchasing(DefaultOnToggle):
|
||||
Determines where you will be required to purchase equipment and runes from the Blacksmith and Enchantress before
|
||||
equipping them. If you disable require purchasing, Manor Renovations are scaled to take this into account.
|
||||
"""
|
||||
displayname = "Require Purchasing"
|
||||
display_name = "Require Purchasing"
|
||||
|
||||
|
||||
class ProgressiveBlueprints(Toggle):
|
||||
@@ -147,14 +147,14 @@ class ProgressiveBlueprints(Toggle):
|
||||
Instead of shuffling blueprints randomly into the pool, blueprint unlocks are progressively unlocked. You would get
|
||||
Squire first, then Knight, etc., until finally Dark.
|
||||
"""
|
||||
displayname = "Progressive Blueprints"
|
||||
display_name = "Progressive Blueprints"
|
||||
|
||||
|
||||
class GoldGainMultiplier(Choice):
|
||||
"""
|
||||
Adjusts the multiplier for gaining gold from all sources.
|
||||
"""
|
||||
displayname = "Gold Gain Multiplier"
|
||||
display_name = "Gold Gain Multiplier"
|
||||
option_normal = 0
|
||||
option_quarter = 1
|
||||
option_half = 2
|
||||
@@ -167,7 +167,7 @@ class NumberOfChildren(Range):
|
||||
"""
|
||||
Determines the number of offspring you can choose from on the lineage screen after a death.
|
||||
"""
|
||||
displayname = "Number of Children"
|
||||
display_name = "Number of Children"
|
||||
range_start = 1
|
||||
range_end = 5
|
||||
default = 3
|
||||
@@ -179,7 +179,7 @@ class AdditionalNames(OptionList):
|
||||
of names your children can have. The first value will also be your initial character's name depending on Starting
|
||||
Gender.
|
||||
"""
|
||||
displayname = "Additional Names"
|
||||
display_name = "Additional Names"
|
||||
|
||||
|
||||
class AllowDefaultNames(DefaultOnToggle):
|
||||
@@ -187,7 +187,7 @@ class AllowDefaultNames(DefaultOnToggle):
|
||||
Determines if the default names defined in the vanilla game are allowed to be used. Warning: Your world will not
|
||||
generate if the number of Additional Names defined is less than the Number of Children value.
|
||||
"""
|
||||
displayname = "Allow Default Names"
|
||||
display_name = "Allow Default Names"
|
||||
|
||||
|
||||
class CastleScaling(Range):
|
||||
|
||||
@@ -4,7 +4,7 @@ from Options import Option, DefaultOnToggle, Range, Choice
|
||||
|
||||
class TotalLocations(Range):
|
||||
"""Number of location checks which are added to the Risk of Rain playthrough."""
|
||||
displayname = "Total Locations"
|
||||
display_name = "Total Locations"
|
||||
range_start = 10
|
||||
range_end = 500
|
||||
default = 20
|
||||
@@ -12,7 +12,7 @@ class TotalLocations(Range):
|
||||
|
||||
class TotalRevivals(Range):
|
||||
"""Total Percentage of `Dio's Best Friend` item put in the item pool."""
|
||||
displayname = "Total Percentage Revivals Available"
|
||||
display_name = "Total Percentage Revivals Available"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 4
|
||||
@@ -22,7 +22,7 @@ class ItemPickupStep(Range):
|
||||
"""Number of items to pick up before an AP Check is completed.
|
||||
Setting to 1 means every other pickup.
|
||||
Setting to 2 means every third pickup. So on..."""
|
||||
displayname = "Item Pickup Step"
|
||||
display_name = "Item Pickup Step"
|
||||
range_start = 0
|
||||
range_end = 5
|
||||
default = 2
|
||||
@@ -30,17 +30,17 @@ class ItemPickupStep(Range):
|
||||
|
||||
class AllowLunarItems(DefaultOnToggle):
|
||||
"""Allows Lunar items in the item pool."""
|
||||
displayname = "Enable Lunar Item Shuffling"
|
||||
display_name = "Enable Lunar Item Shuffling"
|
||||
|
||||
|
||||
class StartWithRevive(DefaultOnToggle):
|
||||
"""Start the game with a `Dio's Best Friend` item."""
|
||||
displayname = "Start with a Revive"
|
||||
display_name = "Start with a Revive"
|
||||
|
||||
|
||||
class GreenScrap(Range):
|
||||
"""Weight of Green Scraps in the item pool."""
|
||||
displayname = "Green Scraps"
|
||||
display_name = "Green Scraps"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 16
|
||||
@@ -48,7 +48,7 @@ class GreenScrap(Range):
|
||||
|
||||
class RedScrap(Range):
|
||||
"""Weight of Red Scraps in the item pool."""
|
||||
displayname = "Red Scraps"
|
||||
display_name = "Red Scraps"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 4
|
||||
@@ -56,7 +56,7 @@ class RedScrap(Range):
|
||||
|
||||
class YellowScrap(Range):
|
||||
"""Weight of yellow scraps in the item pool."""
|
||||
displayname = "Yellow Scraps"
|
||||
display_name = "Yellow Scraps"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 1
|
||||
@@ -64,7 +64,7 @@ class YellowScrap(Range):
|
||||
|
||||
class WhiteScrap(Range):
|
||||
"""Weight of white scraps in the item pool."""
|
||||
displayname = "White Scraps"
|
||||
display_name = "White Scraps"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 32
|
||||
@@ -72,7 +72,7 @@ class WhiteScrap(Range):
|
||||
|
||||
class CommonItem(Range):
|
||||
"""Weight of common items in the item pool."""
|
||||
displayname = "Common Items"
|
||||
display_name = "Common Items"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 64
|
||||
@@ -80,7 +80,7 @@ class CommonItem(Range):
|
||||
|
||||
class UncommonItem(Range):
|
||||
"""Weight of uncommon items in the item pool."""
|
||||
displayname = "Uncommon Items"
|
||||
display_name = "Uncommon Items"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 32
|
||||
@@ -88,7 +88,7 @@ class UncommonItem(Range):
|
||||
|
||||
class LegendaryItem(Range):
|
||||
"""Weight of legendary items in the item pool."""
|
||||
displayname = "Legendary Items"
|
||||
display_name = "Legendary Items"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 8
|
||||
@@ -96,7 +96,7 @@ class LegendaryItem(Range):
|
||||
|
||||
class BossItem(Range):
|
||||
"""Weight of boss items in the item pool."""
|
||||
displayname = "Boss Items"
|
||||
display_name = "Boss Items"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 4
|
||||
@@ -104,7 +104,7 @@ class BossItem(Range):
|
||||
|
||||
class LunarItem(Range):
|
||||
"""Weight of lunar items in the item pool."""
|
||||
displayname = "Lunar Items"
|
||||
display_name = "Lunar Items"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 16
|
||||
@@ -112,7 +112,7 @@ class LunarItem(Range):
|
||||
|
||||
class Equipment(Range):
|
||||
"""Weight of equipment items in the item pool."""
|
||||
displayname = "Equipment"
|
||||
display_name = "Equipment"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 32
|
||||
@@ -120,7 +120,7 @@ class Equipment(Range):
|
||||
|
||||
class ItemPoolPresetToggle(DefaultOnToggle):
|
||||
"""Will use the item weight presets when set to true, otherwise will use the custom set item pool weights."""
|
||||
displayname = "Item Weight Presets"
|
||||
display_name = "Item Weight Presets"
|
||||
|
||||
class ItemWeights(Choice):
|
||||
"""Preset choices for determining the weights of the item pool.<br>
|
||||
@@ -132,7 +132,7 @@ class ItemWeights(Choice):
|
||||
No Scraps removes all scrap items from the item pool.<br>
|
||||
Even generates the item pool with every item having an even weight.<br>
|
||||
Scraps Only will be only scrap items in the item pool."""
|
||||
displayname = "Item Weights"
|
||||
display_name = "Item Weights"
|
||||
option_default = 0
|
||||
option_new = 1
|
||||
option_uncommon = 2
|
||||
|
||||
@@ -2,11 +2,12 @@ import typing
|
||||
from Options import Choice, Range, OptionDict, OptionList, Option, Toggle, DefaultOnToggle
|
||||
|
||||
class StartItemsRemovesFromPool(Toggle):
|
||||
displayname = "StartItems Removes From Item Pool"
|
||||
"""Remove items in starting inventory from pool."""
|
||||
display_name = "StartItems Removes From Item Pool"
|
||||
|
||||
class Preset(Choice):
|
||||
"""choose one of the preset or specify "varia_custom" to use varia_custom_preset option or specify "custom" to use custom_preset option"""
|
||||
displayname = "Preset"
|
||||
"""Choose one of the presets or specify "varia_custom" to use varia_custom_preset option or specify "custom" to use custom_preset option."""
|
||||
display_name = "Preset"
|
||||
option_newbie = 0
|
||||
option_casual = 1
|
||||
option_regular = 2
|
||||
@@ -22,7 +23,8 @@ class Preset(Choice):
|
||||
default = 2
|
||||
|
||||
class StartLocation(Choice):
|
||||
displayname = "Start Location"
|
||||
"""Choose where you want to start the game."""
|
||||
display_name = "Start Location"
|
||||
option_Ceres = 0
|
||||
option_Landing_Site = 1
|
||||
option_Gauntlet_Top = 2
|
||||
@@ -42,7 +44,7 @@ class StartLocation(Choice):
|
||||
|
||||
class DeathLink(Choice):
|
||||
"""When DeathLink is enabled and someone dies, you will die. With survive reserve tanks can save you."""
|
||||
displayname = "Death Link"
|
||||
display_name = "Death Link"
|
||||
option_disable = 0
|
||||
option_enable = 1
|
||||
option_enable_survive = 3
|
||||
@@ -51,7 +53,8 @@ class DeathLink(Choice):
|
||||
default = 0
|
||||
|
||||
class MaxDifficulty(Choice):
|
||||
displayname = "Maximum Difficulty"
|
||||
"""Depending on the perceived difficulties of the techniques, bosses, hell runs etc. from the preset, it will prevent the Randomizer from placing an item in a location too difficult to reach with the current items."""
|
||||
display_name = "Maximum Difficulty"
|
||||
option_easy = 0
|
||||
option_medium = 1
|
||||
option_hard = 2
|
||||
@@ -62,40 +65,47 @@ class MaxDifficulty(Choice):
|
||||
default = 4
|
||||
|
||||
class MorphPlacement(Choice):
|
||||
displayname = "Morph Placement"
|
||||
"""Influences where the Morphing Ball with be placed."""
|
||||
display_name = "Morph Placement"
|
||||
option_early = 0
|
||||
option_normal = 1
|
||||
default = 0
|
||||
|
||||
class StrictMinors(Toggle):
|
||||
displayname = "Strict Minors"
|
||||
"""Instead of using the Minors proportions as probabilities, enforce a strict distribution to match the proportions as closely as possible."""
|
||||
display_name = "Strict Minors"
|
||||
|
||||
class MissileQty(Range):
|
||||
displayname = "Missile Quantity"
|
||||
"""The higher the number the higher the probability of choosing missles when placing a minor."""
|
||||
display_name = "Missile Quantity"
|
||||
range_start = 10
|
||||
range_end = 90
|
||||
default = 30
|
||||
|
||||
class SuperQty(Range):
|
||||
displayname = "Super Quantity"
|
||||
"""The higher the number the higher the probability of choosing super missles when placing a minor."""
|
||||
display_name = "Super Quantity"
|
||||
range_start = 10
|
||||
range_end = 90
|
||||
default = 20
|
||||
|
||||
class PowerBombQty(Range):
|
||||
displayname = "Power Bomb Quantity"
|
||||
"""The higher the number the higher the probability of choosing power bombs when placing a minor."""
|
||||
display_name = "Power Bomb Quantity"
|
||||
range_start = 10
|
||||
range_end = 90
|
||||
default = 10
|
||||
|
||||
class MinorQty(Range):
|
||||
displayname = "Minor Quantity"
|
||||
"""From 7%, minimum number of minors required to finish the game, to 100%."""
|
||||
display_name = "Minor Quantity"
|
||||
range_start = 7
|
||||
range_end = 100
|
||||
default = 100
|
||||
|
||||
class EnergyQty(Choice):
|
||||
displayname = "Energy Quantity"
|
||||
"""Choose how many Energy/Reserve Tanks will be available, from 0-1 in ultra sparse, 4-6 in sparse, 8-12 in medium and 18 in vanilla."""
|
||||
display_name = "Energy Quantity"
|
||||
option_ultra_sparse = 0
|
||||
option_sparse = 1
|
||||
option_medium = 2
|
||||
@@ -103,7 +113,8 @@ class EnergyQty(Choice):
|
||||
default = 3
|
||||
|
||||
class AreaRandomization(Choice):
|
||||
displayname = "Area Randomization"
|
||||
"""Randomize areas together using bidirectional access portals."""
|
||||
display_name = "Area Randomization"
|
||||
option_off = 0
|
||||
option_light = 1
|
||||
option_on = 2
|
||||
@@ -112,67 +123,83 @@ class AreaRandomization(Choice):
|
||||
default = 0
|
||||
|
||||
class AreaLayout(Toggle):
|
||||
displayname = "Area Layout"
|
||||
"""Some layout tweaks to make your life easier in areas randomizer."""
|
||||
display_name = "Area Layout"
|
||||
|
||||
class DoorsColorsRando(Toggle):
|
||||
displayname = "Doors Colors Rando"
|
||||
"""Randomize the color of Red/Green/Yellow doors. Add four new type of doors which require Ice/Wave/Spazer/Plasma beams to open them."""
|
||||
display_name = "Doors Colors Rando"
|
||||
|
||||
class AllowGreyDoors(Toggle):
|
||||
displayname = "Allow Grey Doors"
|
||||
"""When randomizing the color of Red/Green/Yellow doors, some doors can be randomized to Grey. Grey doors will never open, you will have to go around them."""
|
||||
display_name = "Allow Grey Doors"
|
||||
|
||||
class BossRandomization(Toggle):
|
||||
displayname = "Boss Randomization"
|
||||
"""Randomize Golden 4 bosses access doors using bidirectional access portals."""
|
||||
display_name = "Boss Randomization"
|
||||
|
||||
class FunCombat(Toggle):
|
||||
"""if used, might force 'items' accessibility"""
|
||||
displayname = "Fun Combat"
|
||||
"""Forces removal of Plasma Beam and Screw Attack if the preset and settings allow it. In addition, can randomly remove Spazer and Wave Beam from the Combat set. If used, might force 'items' accessibility."""
|
||||
display_name = "Fun Combat"
|
||||
|
||||
class FunMovement(Toggle):
|
||||
"""if used, might force 'items' accessibility"""
|
||||
displayname = "Fun Movement"
|
||||
"""Forces removal of Space Jump if the preset allows it. In addition, can randomly remove High Jump, Grappling Beam, Spring Ball, Speed Booster, and Bombs from the Movement set. If used, might force 'items' accessibility."""
|
||||
display_name = "Fun Movement"
|
||||
|
||||
class FunSuits(Toggle):
|
||||
"""if used, might force 'items' accessibility"""
|
||||
displayname = "Fun Suits"
|
||||
"""If the preset and seed layout allow it, will force removal of at least one of Varia Suit and/or Gravity Suit. If used, might force 'items' accessibility."""
|
||||
display_name = "Fun Suits"
|
||||
|
||||
class LayoutPatches(DefaultOnToggle):
|
||||
displayname = "Layout Patches"
|
||||
"""Include the anti-softlock layout patches. Disable at your own softlocking risk!"""
|
||||
display_name = "Layout Patches"
|
||||
|
||||
class VariaTweaks(Toggle):
|
||||
displayname = "Varia Tweaks"
|
||||
"""Include minor tweaks for the game to behave 'as it should' in a randomizer context"""
|
||||
display_name = "Varia Tweaks"
|
||||
|
||||
class NerfedCharge(Toggle):
|
||||
displayname = "Nerfed Charge"
|
||||
"""Samus begins with a starter Charge Beam that does one third of charged shot damage that can damage bosses. Pseudo Screws also do one third damage. Special Beam Attacks do normal damage but cost 3 Power Bombs instead of 1. Once the Charge Beam item has been collected, it does full damage and special attacks are back to normal."""
|
||||
display_name = "Nerfed Charge"
|
||||
|
||||
class GravityBehaviour(Choice):
|
||||
displayname = "Gravity Behaviour"
|
||||
"""Modify the heat damage and enemy damage reduction qualities of the Gravity and Varia Suits."""
|
||||
display_name = "Gravity Behaviour"
|
||||
option_Vanilla = 0
|
||||
option_Balanced = 1
|
||||
option_Progressive = 2
|
||||
default = 1
|
||||
|
||||
class ElevatorsDoorsSpeed(DefaultOnToggle):
|
||||
displayname = "Elevators doors speed"
|
||||
"""Accelerate doors and elevators transitions."""
|
||||
display_name = "Elevators doors speed"
|
||||
|
||||
class SpinJumpRestart(Toggle):
|
||||
"""Allows Samus to start spinning in mid air after jumping or falling."""
|
||||
displayname = "Spin Jump Restart"
|
||||
|
||||
class InfiniteSpaceJump(Toggle):
|
||||
"""Space jumps can be done quicker and at any time in air, water or lava, even after falling long distances."""
|
||||
displayname = "Infinite Space Jump"
|
||||
|
||||
class RefillBeforeSave(Toggle):
|
||||
"""Refill energy and ammo when saving."""
|
||||
displayname = "Refill Before Save"
|
||||
|
||||
class Hud(Toggle):
|
||||
"""Displays the current area name and the number of remaining items of selected item split in the HUD for the current area."""
|
||||
displayname = "Hud"
|
||||
|
||||
class Animals(Toggle):
|
||||
"""Replace saving the animals in the escape sequence by a random surprise."""
|
||||
displayname = "Animals"
|
||||
|
||||
class NoMusic(Toggle):
|
||||
"""Disable the background music."""
|
||||
displayname = "No Music"
|
||||
|
||||
class RandomMusic(Toggle):
|
||||
"""Randomize the background music."""
|
||||
displayname = "Random Music"
|
||||
|
||||
class CustomPreset(OptionDict):
|
||||
|
||||
@@ -15,7 +15,7 @@ from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT
|
||||
import Utils
|
||||
|
||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, CollectionState
|
||||
from ..AutoWorld import World
|
||||
from ..AutoWorld import World, AutoLogicRegister
|
||||
import Patch
|
||||
|
||||
from logic.smboolmanager import SMBoolManager
|
||||
@@ -28,6 +28,21 @@ from logic.logic import Logic
|
||||
from randomizer import VariaRandomizer
|
||||
|
||||
|
||||
class SMCollectionState(metaclass=AutoLogicRegister):
|
||||
def init_mixin(self, parent: MultiWorld):
|
||||
# for unit tests where MultiWorld is instantiated before worlds
|
||||
if hasattr(parent, "state"):
|
||||
self.smbm = {player: SMBoolManager(player, parent.state.smbm[player].maxDiff,
|
||||
parent.state.smbm[player].onlyBossLeft) for player in
|
||||
parent.get_game_players("Super Metroid")}
|
||||
else:
|
||||
self.smbm = {}
|
||||
|
||||
def copy_mixin(self, ret) -> CollectionState:
|
||||
ret.smbm = {player: copy.deepcopy(self.smbm[player]) for player in self.world.get_game_players("Super Metroid")}
|
||||
return ret
|
||||
|
||||
|
||||
class SMWorld(World):
|
||||
game: str = "Super Metroid"
|
||||
topology_present = True
|
||||
@@ -44,38 +59,13 @@ class SMWorld(World):
|
||||
itemManager: ItemManager
|
||||
|
||||
locations = {}
|
||||
hint_blacklist = {'Nothing', 'NoEnergy'}
|
||||
hint_blacklist = {'Nothing', 'No Energy'}
|
||||
|
||||
Logic.factory('vanilla')
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
self.rom_name_available_event = threading.Event()
|
||||
super().__init__(world, player)
|
||||
|
||||
def __new__(cls, world, player):
|
||||
|
||||
# Add necessary objects to CollectionState on initialization
|
||||
orig_init = CollectionState.__init__
|
||||
orig_copy = CollectionState.copy
|
||||
|
||||
def sm_init(self, parent: MultiWorld):
|
||||
if (hasattr(parent, "state")): # for unit tests where MultiWorld is instanciated before worlds
|
||||
self.smbm = {player: SMBoolManager(player, parent.state.smbm[player].maxDiff, parent.state.smbm[player].onlyBossLeft) for player in parent.get_game_players("Super Metroid")}
|
||||
orig_init(self, parent)
|
||||
|
||||
|
||||
def sm_copy(self):
|
||||
ret = orig_copy(self)
|
||||
ret.smbm = {player: copy.deepcopy(self.smbm[player]) for player in self.world.get_game_players("Super Metroid")}
|
||||
return ret
|
||||
|
||||
CollectionState.__init__ = sm_init
|
||||
CollectionState.copy = sm_copy
|
||||
|
||||
if world:
|
||||
world.state.smbm = {}
|
||||
|
||||
return super().__new__(cls)
|
||||
|
||||
def generate_early(self):
|
||||
Logic.factory('vanilla')
|
||||
@@ -86,7 +76,7 @@ class SMWorld(World):
|
||||
# keeps Nothing items local so no player will ever pickup Nothing
|
||||
# doing so reduces contribution of this world to the Multiworld the more Nothing there is though
|
||||
self.world.local_items[self.player].value.add('Nothing')
|
||||
self.world.local_items[self.player].value.add('NoEnergy')
|
||||
self.world.local_items[self.player].value.add('No Energy')
|
||||
|
||||
if (self.variaRando.args.morphPlacement == "early"):
|
||||
self.world.local_items[self.player].value.add('Morph')
|
||||
@@ -450,9 +440,7 @@ class SMWorld(World):
|
||||
# we skip in case of error, so that the original error in the output thread is the one that gets raised
|
||||
if rom_name:
|
||||
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]])
|
||||
multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]]
|
||||
|
||||
|
||||
def fill_slot_data(self):
|
||||
@@ -460,11 +448,12 @@ class SMWorld(World):
|
||||
return slot_data
|
||||
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
state.smbm[item.player].addItem(item.type)
|
||||
if item.advancement:
|
||||
state.prog_items[item.name, item.player] += 1
|
||||
return True # indicate that a logical state change has occured
|
||||
return False
|
||||
state.smbm[self.player].addItem(item.type)
|
||||
return super(SMWorld, self).collect(state, item)
|
||||
|
||||
def remove(self, state: CollectionState, item: Item) -> bool:
|
||||
state.smbm[self.player].removeItem(item.type)
|
||||
return super(SMWorld, self).remove(state, item)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
item = next(x for x in ItemManager.Items.values() if x.Name == name)
|
||||
|
||||
@@ -3,11 +3,28 @@ from BaseClasses import Item
|
||||
class SM64Item(Item):
|
||||
game: str = "Super Mario 64"
|
||||
|
||||
item_table = {
|
||||
generic_item_table = {
|
||||
"Power Star": 3626000,
|
||||
"Basement Key": 3626178,
|
||||
"Second Floor Key": 3626179,
|
||||
"Wing Cap": 3626180,
|
||||
"Metal Cap": 3626181,
|
||||
"Vanish Cap": 3626182
|
||||
}
|
||||
"Progressive Key": 3626180,
|
||||
"Wing Cap": 3626181,
|
||||
"Metal Cap": 3626182,
|
||||
"Vanish Cap": 3626183,
|
||||
"1Up Mushroom": 3626184
|
||||
}
|
||||
|
||||
cannon_item_table = {
|
||||
"Cannon Unlock BoB": 3626200,
|
||||
"Cannon Unlock WF": 3626201,
|
||||
"Cannon Unlock JRB": 3626202,
|
||||
"Cannon Unlock CCM": 3626203,
|
||||
"Cannon Unlock SSL": 3626207,
|
||||
"Cannon Unlock SL": 3626209,
|
||||
"Cannon Unlock WDW": 3626210,
|
||||
"Cannon Unlock TTM": 3626211,
|
||||
"Cannon Unlock THI": 3626212,
|
||||
"Cannon Unlock RR": 3626214
|
||||
}
|
||||
|
||||
item_table = {**generic_item_table, **cannon_item_table}
|
||||
@@ -10,7 +10,8 @@ locBoB_table = {
|
||||
"BoB: Shoot to the Island in the Sky": 3626002,
|
||||
"BoB: Find the 8 Red Coins": 3626003,
|
||||
"BoB: Mario Wings to the Sky": 3626004,
|
||||
"BoB: Behind Chain Chomp's Gate": 3626005
|
||||
"BoB: Behind Chain Chomp's Gate": 3626005,
|
||||
"BoB: Bob-omb Buddy": 3626200,
|
||||
}
|
||||
|
||||
#Whomp's Fortress
|
||||
@@ -20,7 +21,8 @@ locWhomp_table = {
|
||||
"WF: Shoot into the Wild Blue": 3626009,
|
||||
"WF: Red Coins on the Floating Isle": 3626010,
|
||||
"WF: Fall onto the Caged Island": 3626011,
|
||||
"WF: Blast Away the Wall": 3626012
|
||||
"WF: Blast Away the Wall": 3626012,
|
||||
"WF: Bob-omb Buddy": 3626201,
|
||||
}
|
||||
|
||||
#Jolly Roger Bay
|
||||
@@ -30,7 +32,8 @@ locJRB_table = {
|
||||
"JRB: Treasure of the Ocean Cave": 3626016,
|
||||
"JRB: Red Coins on the Ship Afloat": 3626017,
|
||||
"JRB: Blast to the Stone Pillar": 3626018,
|
||||
"JRB: Through the Jet Stream": 3626019
|
||||
"JRB: Through the Jet Stream": 3626019,
|
||||
"JRB: Bob-omb Buddy": 3626202,
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +44,8 @@ locCCM_table = {
|
||||
"CCM: Big Penguin Race": 3626023,
|
||||
"CCM: Frosty Slide for 8 Red Coins": 3626024,
|
||||
"CCM: Snowman's Lost His Head": 3626025,
|
||||
"CCM: Wall Kicks Will Work": 3626026
|
||||
"CCM: Wall Kicks Will Work": 3626026,
|
||||
"CCM: Bob-omb Buddy": 3626203,
|
||||
}
|
||||
|
||||
#Big Boo's Haunt
|
||||
@@ -81,7 +85,8 @@ locSSL_table = {
|
||||
"SSL: Inside the Ancient Pyramid": 3626051,
|
||||
"SSL: Stand Tall on the Four Pillars": 3626052,
|
||||
"SSL: Free Flying for 8 Red Coins": 3626053,
|
||||
"SSL: Pyramid Puzzle": 3626054
|
||||
"SSL: Pyramid Puzzle": 3626054,
|
||||
"SSL: Bob-omb Buddy": 3626207,
|
||||
}
|
||||
|
||||
#Dire, Dire Docks
|
||||
@@ -101,7 +106,8 @@ locSL_table = {
|
||||
"SL: In the Deep Freeze": 3626065,
|
||||
"SL: Whirl from the Freezing Pond": 3626066,
|
||||
"SL: Shell Shreddin' for Red Coins": 3626067,
|
||||
"SL: Into the Igloo": 3626068
|
||||
"SL: Into the Igloo": 3626068,
|
||||
"SL: Bob-omb Buddy": 3626209,
|
||||
}
|
||||
|
||||
#Wet-Dry World
|
||||
@@ -111,7 +117,8 @@ locWDW_table = {
|
||||
"WDW: Secrets in the Shallows & Sky": 3626072,
|
||||
"WDW: Express Elevator--Hurry Up!": 3626073,
|
||||
"WDW: Go to Town for Red Coins": 3626074,
|
||||
"WDW: Quick Race Through Downtown!": 3626075
|
||||
"WDW: Quick Race Through Downtown!": 3626075,
|
||||
"WDW: Bob-omb Buddy": 3626210,
|
||||
}
|
||||
|
||||
#Tall, Tall Mountain
|
||||
@@ -121,7 +128,8 @@ locTTM_table = {
|
||||
"TTM: Scary 'Shrooms, Red Coins": 3626079,
|
||||
"TTM: Mysterious Mountainside": 3626080,
|
||||
"TTM: Breathtaking View from Bridge": 3626081,
|
||||
"TTM: Blast to the Lonely Mushroom": 3626082
|
||||
"TTM: Blast to the Lonely Mushroom": 3626082,
|
||||
"TTM: Bob-omb Buddy": 3626211,
|
||||
}
|
||||
|
||||
#Tiny-Huge Island
|
||||
@@ -131,7 +139,8 @@ locTHI_table = {
|
||||
"THI: Rematch with Koopa the Quick": 3626086,
|
||||
"THI: Five Itty Bitty Secrets": 3626087,
|
||||
"THI: Wiggler's Red Coins": 3626088,
|
||||
"THI: Make Wiggler Squirm": 3626089
|
||||
"THI: Make Wiggler Squirm": 3626089,
|
||||
"THI: Bob-omb Buddy": 3626212,
|
||||
}
|
||||
|
||||
#Tick Tock Clock
|
||||
@@ -151,7 +160,8 @@ locRR_table = {
|
||||
"RR: Coins Amassed in a Maze": 3626100,
|
||||
"RR: Swingin' in the Breeze": 3626101,
|
||||
"RR: Tricky Triangles!": 3626102,
|
||||
"RR: Somewhere Over the Rainbow": 3626103
|
||||
"RR: Somewhere Over the Rainbow": 3626103,
|
||||
"RR: Bob-omb Buddy": 3626214,
|
||||
}
|
||||
|
||||
loc100Coin_table = {
|
||||
@@ -172,10 +182,18 @@ loc100Coin_table = {
|
||||
"RR: 100 Coins": 3626104
|
||||
}
|
||||
|
||||
locBitDW_table = {
|
||||
"Bowser in the Dark World Red Coins": 3626105,
|
||||
"Bowser in the Dark World Key": 3626178
|
||||
}
|
||||
|
||||
locBitFS_table = {
|
||||
"Bowser in the Fire Sea Red Coins": 3626112,
|
||||
"Bowser in the Fire Sea Key": 3626179
|
||||
}
|
||||
|
||||
#Secret Stars and Stages
|
||||
locSS_table = {
|
||||
"Bowser in the Dark World Red Coins": 3626105,
|
||||
"Bowser in the Fire Sea Red Coins": 3626112,
|
||||
"Bowser in the Sky Red Coins": 3626119,
|
||||
"The Princess's Secret Slide Block": 3626126,
|
||||
"The Princess's Secret Slide Fast": 3626127,
|
||||
@@ -191,21 +209,15 @@ locSS_table = {
|
||||
"MIPS 2": 3626172
|
||||
}
|
||||
|
||||
#Keys
|
||||
locKey_table = {
|
||||
"Bowser in the Dark World Key": 3626178,
|
||||
"Bowser in the Fire Sea Key": 3626179
|
||||
}
|
||||
|
||||
#Caps
|
||||
locCap_table = {
|
||||
"Tower of the Wing Cap Switch": 3626180,
|
||||
"Cavern of the Metal Cap Switch": 3626181,
|
||||
"Vanish Cap Under the Moat Switch": 3626182
|
||||
"Tower of the Wing Cap Switch": 3626181,
|
||||
"Cavern of the Metal Cap Switch": 3626182,
|
||||
"Vanish Cap Under the Moat Switch": 3626183
|
||||
}
|
||||
|
||||
# Correspond to 3626000 + course index * 7 + star index, then secret stars, then keys, then 100 Coin Stars
|
||||
location_table = {**locBoB_table,**locWhomp_table,**locJRB_table,**locCCM_table,**locBBH_table, \
|
||||
**locHMC_table,**locLLL_table,**locSSL_table,**locDDD_table,**locSL_table, \
|
||||
**locWDW_table,**locTTM_table,**locTHI_table,**locTTC_table,**locRR_table, \
|
||||
**loc100Coin_table,**locSS_table,**locKey_table,**locCap_table}
|
||||
**loc100Coin_table,**locBitDW_table,**locBitFS_table,**locSS_table,**locCap_table}
|
||||
@@ -1,13 +1,17 @@
|
||||
import typing
|
||||
from Options import Option, DefaultOnToggle, Range
|
||||
from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink
|
||||
|
||||
class EnableCoinStars(DefaultOnToggle):
|
||||
"""Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything"""
|
||||
displayname = "Enable 100 Coin Stars"
|
||||
display_name = "Enable 100 Coin Stars"
|
||||
|
||||
class StrictCapRequirements(DefaultOnToggle):
|
||||
"""If disabled, Stars that expect special caps may have to be acquired without the caps"""
|
||||
displayname = "Strict Cap Requirements"
|
||||
display_name = "Strict Cap Requirements"
|
||||
|
||||
class StrictCannonRequirements(DefaultOnToggle):
|
||||
"""If disabled, Stars that expect cannons may have to be acquired without them. Only makes a difference if Buddy Checks are enabled"""
|
||||
display_name = "Strict Cannon Requirements"
|
||||
|
||||
class StarsToFinish(Range):
|
||||
"""How many stars are required at the infinite stairs"""
|
||||
@@ -15,8 +19,32 @@ class StarsToFinish(Range):
|
||||
range_end = 100
|
||||
default = 70
|
||||
|
||||
class ExtraStars(Range):
|
||||
"""How many stars exist beyond those set for StarsToFinish"""
|
||||
range_start = 0
|
||||
range_end = 50
|
||||
default = 50
|
||||
|
||||
class AreaRandomizer(Toggle):
|
||||
"""Randomize Entrances to Courses"""
|
||||
display_name = "Course Randomizer"
|
||||
|
||||
class BuddyChecks(Toggle):
|
||||
"""Bob-omb Buddies are checks, Cannon Unlocks are items"""
|
||||
display_name = "Bob-omb Buddy Checks"
|
||||
|
||||
class ProgressiveKeys(DefaultOnToggle):
|
||||
"""Keys will first grant you access to the Basement, then to the Secound Floor"""
|
||||
display_name = "Progressive Keys"
|
||||
|
||||
sm64_options: typing.Dict[str,type(Option)] = {
|
||||
"AreaRandomizer": AreaRandomizer,
|
||||
"ProgressiveKeys": ProgressiveKeys,
|
||||
"EnableCoinStars": EnableCoinStars,
|
||||
"StrictCapRequirements": StrictCapRequirements,
|
||||
"StarsToFinish": StarsToFinish
|
||||
"StrictCannonRequirements": StrictCannonRequirements,
|
||||
"StarsToFinish": StarsToFinish,
|
||||
"ExtraStars": ExtraStars,
|
||||
"DeathLink": DeathLink,
|
||||
"BuddyChecks": BuddyChecks,
|
||||
}
|
||||
@@ -3,13 +3,16 @@ from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType
|
||||
from .Locations import SM64Location, location_table,locBoB_table,locWhomp_table,locJRB_table,locCCM_table,locBBH_table, \
|
||||
locHMC_table,locLLL_table,locSSL_table,locDDD_table,locSL_table, \
|
||||
locWDW_table,locTTM_table,locTHI_table,locTTC_table,locRR_table, \
|
||||
locSS_table, locKey_table, locCap_table
|
||||
locBitDW_table, locBitFS_table, locSS_table, locCap_table
|
||||
|
||||
sm64courses = ["Bob-omb Battlefield", "Whomp's Fortress", "Jolly Roger Bay", "Cool, Cool Mountain", "Big Boo's Haunt",
|
||||
"Hazy Maze Cave", "Lethal Lava Land", "Shifting Sand Land", "Dire, Dire Docks", "Snowman's Land", "Wet-Dry World",
|
||||
"Tall, Tall Mountain", "Tiny-Huge Island", "Tick Tock Clock", "Rainbow Ride"]
|
||||
|
||||
def create_regions(world: MultiWorld, player: int):
|
||||
|
||||
regSS = Region("Menu", RegionType.Generic, "Castle Area", player, world)
|
||||
locSS_names = [name for name, id in locSS_table.items()]
|
||||
locSS_names += [name for name, id in locKey_table.items()]
|
||||
locSS_names += [name for name, id in locCap_table.items()]
|
||||
regSS.locations += [SM64Location(player, loc_name, location_table[loc_name], regSS) for loc_name in locSS_names]
|
||||
world.regions.append(regSS)
|
||||
@@ -49,6 +52,11 @@ def create_regions(world: MultiWorld, player: int):
|
||||
regBBH.locations.append(SM64Location(player, "BBH: 100 Coins", location_table["BBH: 100 Coins"], regBBH))
|
||||
world.regions.append(regBBH)
|
||||
|
||||
regBitDW = Region("Bowser in the Dark World", RegionType.Generic, "Bowser in the Dark World", player, world)
|
||||
locBitDW_names = [name for name, id in locBitDW_table.items()]
|
||||
regBitDW.locations += [SM64Location(player, loc_name, location_table[loc_name], regBitDW) for loc_name in locBitDW_names]
|
||||
world.regions.append(regBitDW)
|
||||
|
||||
regBasement = Region("Basement", RegionType.Generic, "Basement", player, world)
|
||||
world.regions.append(regBasement)
|
||||
|
||||
@@ -80,6 +88,11 @@ def create_regions(world: MultiWorld, player: int):
|
||||
regDDD.locations.append(SM64Location(player, "DDD: 100 Coins", location_table["DDD: 100 Coins"], regDDD))
|
||||
world.regions.append(regDDD)
|
||||
|
||||
regBitFS = Region("Bowser in the Fire Sea", RegionType.Generic, "Bowser in the Fire Sea", player, world)
|
||||
locBitFS_names = [name for name, id in locBitFS_table.items()]
|
||||
regBitFS.locations += [SM64Location(player, loc_name, location_table[loc_name], regBitFS) for loc_name in locBitFS_names]
|
||||
world.regions.append(regBitFS)
|
||||
|
||||
regFloor2 = Region("Second Floor", RegionType.Generic, "Second Floor", player, world)
|
||||
world.regions.append(regFloor2)
|
||||
|
||||
|
||||
@@ -1,60 +1,50 @@
|
||||
import typing
|
||||
from ..generic.Rules import add_rule
|
||||
from .Regions import connect_regions
|
||||
from .Regions import connect_regions, sm64courses
|
||||
|
||||
def set_rules(world,player):
|
||||
connect_regions(world, player, "Menu", "Bob-omb Battlefield", lambda state: True)
|
||||
connect_regions(world, player, "Menu", "Whomp's Fortress", lambda state: state.has("Power Star", player, 1))
|
||||
connect_regions(world, player, "Menu", "Jolly Roger Bay", lambda state: state.has("Power Star", player, 3))
|
||||
connect_regions(world, player, "Menu", "Cool, Cool Mountain", lambda state: state.has("Power Star", player, 3))
|
||||
connect_regions(world, player, "Menu", "Big Boo's Haunt", lambda state: state.has("Power Star", player, 12))
|
||||
def set_rules(world,player,area_connections):
|
||||
courseshuffle = list(range(len(sm64courses)))
|
||||
if (world.AreaRandomizer[player].value):
|
||||
world.random.shuffle(courseshuffle)
|
||||
area_connections.update({index: value for index, value in enumerate(courseshuffle)})
|
||||
|
||||
connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player))
|
||||
connect_regions(world, player, "Basement", "Menu", lambda state: True)
|
||||
connect_regions(world, player, "Menu", sm64courses[area_connections[0]], lambda state: True)
|
||||
connect_regions(world, player, "Menu", sm64courses[area_connections[1]], lambda state: state.has("Power Star", player, 1))
|
||||
connect_regions(world, player, "Menu", sm64courses[area_connections[2]], lambda state: state.has("Power Star", player, 3))
|
||||
connect_regions(world, player, "Menu", sm64courses[area_connections[3]], lambda state: state.has("Power Star", player, 3))
|
||||
connect_regions(world, player, "Menu", "Bowser in the Dark World", lambda state: state.has("Power Star", player, 8))
|
||||
connect_regions(world, player, "Menu", sm64courses[area_connections[4]], lambda state: state.has("Power Star", player, 12))
|
||||
|
||||
connect_regions(world, player, "Basement", "Hazy Maze Cave", lambda state: True)
|
||||
connect_regions(world, player, "Basement", "Lethal Lava Land", lambda state: True)
|
||||
connect_regions(world, player, "Basement", "Shifting Sand Land", lambda state: True)
|
||||
connect_regions(world, player, "Basement", "Dire, Dire Docks", lambda state: state.has("Power Star", player, 30))
|
||||
connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player) or state.has("Progressive Key", player, 1))
|
||||
|
||||
connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player))
|
||||
connect_regions(world, player, "Second Floor", "Menu", lambda state: True)
|
||||
connect_regions(world, player, "Basement", sm64courses[area_connections[5]], lambda state: True)
|
||||
connect_regions(world, player, "Basement", sm64courses[area_connections[6]], lambda state: True)
|
||||
connect_regions(world, player, "Basement", sm64courses[area_connections[7]], lambda state: True)
|
||||
connect_regions(world, player, "Basement", sm64courses[area_connections[8]], lambda state: state.has("Power Star", player, 30))
|
||||
connect_regions(world, player, "Basement", "Bowser in the Fire Sea", lambda state: state.has("Power Star", player, 30) and
|
||||
state.can_reach("Dire, Dire Docks", 'Region', player))
|
||||
|
||||
connect_regions(world, player, "Second Floor", "Snowman's Land", lambda state: True)
|
||||
connect_regions(world, player, "Second Floor", "Wet-Dry World", lambda state: True)
|
||||
connect_regions(world, player, "Second Floor", "Tall, Tall Mountain", lambda state: True)
|
||||
connect_regions(world, player, "Second Floor", "Tiny-Huge Island", lambda state: True)
|
||||
connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2))
|
||||
|
||||
connect_regions(world, player, "Second Floor", sm64courses[area_connections[9]], lambda state: True)
|
||||
connect_regions(world, player, "Second Floor", sm64courses[area_connections[10]], lambda state: True)
|
||||
connect_regions(world, player, "Second Floor", sm64courses[area_connections[11]], lambda state: True)
|
||||
connect_regions(world, player, "Second Floor", sm64courses[area_connections[12]], lambda state: True)
|
||||
|
||||
connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, 50))
|
||||
connect_regions(world, player, "Third Floor", "Second Floor", lambda state: True)
|
||||
|
||||
connect_regions(world, player, "Third Floor", "Tick Tock Clock", lambda state: True)
|
||||
connect_regions(world, player, "Third Floor", "Rainbow Ride", lambda state: True)
|
||||
|
||||
connect_regions(world, player, "Bob-omb Battlefield", "Menu", lambda state: True)
|
||||
connect_regions(world, player, "Whomp's Fortress", "Menu", lambda state: True)
|
||||
connect_regions(world, player, "Jolly Roger Bay", "Menu", lambda state: True)
|
||||
connect_regions(world, player, "Cool, Cool Mountain", "Menu", lambda state: True)
|
||||
connect_regions(world, player, "Big Boo's Haunt", "Menu", lambda state: True)
|
||||
connect_regions(world, player, "Hazy Maze Cave", "Basement", lambda state: True)
|
||||
connect_regions(world, player, "Lethal Lava Land", "Basement", lambda state: True)
|
||||
connect_regions(world, player, "Shifting Sand Land", "Basement", lambda state: True)
|
||||
connect_regions(world, player, "Dire, Dire Docks", "Basement", lambda state: True)
|
||||
connect_regions(world, player, "Snowman's Land", "Second Floor", lambda state: True)
|
||||
connect_regions(world, player, "Wet-Dry World", "Second Floor", lambda state: True)
|
||||
connect_regions(world, player, "Tall, Tall Mountain", "Second Floor", lambda state: True)
|
||||
connect_regions(world, player, "Tiny-Huge Island", "Second Floor", lambda state: True)
|
||||
connect_regions(world, player, "Tick Tock Clock", "Second Floor", lambda state: True)
|
||||
connect_regions(world, player, "Rainbow Ride", "Second Floor", lambda state: True)
|
||||
connect_regions(world, player, "Third Floor", sm64courses[area_connections[13]], lambda state: True)
|
||||
connect_regions(world, player, "Third Floor", sm64courses[area_connections[14]], lambda state: True)
|
||||
|
||||
#Special Rules for some Locations
|
||||
add_rule(world.get_location("Tower of the Wing Cap Switch", player), lambda state: state.has("Power Star", player, 10))
|
||||
add_rule(world.get_location("Cavern of the Metal Cap Switch", player), lambda state: state.can_reach("Basement", 'Region', player))
|
||||
add_rule(world.get_location("Cavern of the Metal Cap Switch", player), lambda state: state.can_reach("Hazy Maze Cave",'Region',player))
|
||||
add_rule(world.get_location("Vanish Cap Under the Moat Switch", player), lambda state: state.can_reach("Basement", 'Region', player))
|
||||
|
||||
add_rule(world.get_location("BBH: Eye to Eye in the Secret Room", player), lambda state: state.has("Vanish Cap", player))
|
||||
add_rule(world.get_location("DDD: Collect the Caps...", player), lambda state: state.has("Metal Cap", player) and
|
||||
state.has("Vanish Cap", player))
|
||||
add_rule(world.get_location("DDD: Pole-Jumping for Red Coins", player), lambda state: state.can_reach("Bowser in the Fire Sea",'Region',player))
|
||||
add_rule(world.get_location("SL: Into the Igloo", player), lambda state: state.has("Vanish Cap", player))
|
||||
add_rule(world.get_location("WDW: Quick Race Through Downtown!", player), lambda state: state.has("Vanish Cap", player))
|
||||
if (world.StrictCapRequirements[player].value):
|
||||
@@ -64,10 +54,13 @@ def set_rules(world,player):
|
||||
add_rule(world.get_location("SSL: Free Flying for 8 Red Coins", player), lambda state: state.has("Wing Cap", player))
|
||||
add_rule(world.get_location("DDD: Through the Jet Stream", player), lambda state: state.has("Metal Cap", player))
|
||||
add_rule(world.get_location("Vanish Cap Under the Moat Red Coins", player), lambda state: state.has("Vanish Cap", player))
|
||||
if (world.StrictCannonRequirements[player].value):
|
||||
add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Cannon Unlock BoB", player))
|
||||
add_rule(world.get_location("WF: Blast Away the Wall", player), lambda state: state.has("Cannon Unlock WF", player))
|
||||
add_rule(world.get_location("JRB: Blast to the Stone Pillar", player), lambda state: state.has("Cannon Unlock JRB", player))
|
||||
add_rule(world.get_location("RR: Somewhere Over the Rainbow", player), lambda state: state.has("Cannon Unlock RR", player))
|
||||
|
||||
#Rules for Secret Stars
|
||||
add_rule(world.get_location("Bowser in the Dark World Red Coins", player), lambda state: state.has("Power Star", player, 8))
|
||||
add_rule(world.get_location("Bowser in the Fire Sea Red Coins", player), lambda state: state.can_reach("Basement",'Region',player) and state.has("Power Star", player, 30))
|
||||
add_rule(world.get_location("Bowser in the Sky Red Coins", player), lambda state: state.can_reach("Third Floor",'Region',player) and state.has("Power Star", player, world.StarsToFinish[player].value))
|
||||
add_rule(world.get_location("The Princess's Secret Slide Block", player), lambda state: state.has("Power Star", player, 1))
|
||||
add_rule(world.get_location("The Princess's Secret Slide Fast", player), lambda state: state.has("Power Star", player, 1))
|
||||
@@ -75,15 +68,11 @@ def set_rules(world,player):
|
||||
add_rule(world.get_location("Tower of the Wing Cap Red Coins", player), lambda state: state.can_reach("Tower of the Wing Cap Switch", 'Location', player))
|
||||
add_rule(world.get_location("Vanish Cap Under the Moat Red Coins", player), lambda state: state.can_reach("Vanish Cap Under the Moat Switch", 'Location', player))
|
||||
add_rule(world.get_location("Wing Mario Over the Rainbow", player), lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Wing Cap", player))
|
||||
add_rule(world.get_location("The Secret Aquarium", player), lambda state: state.can_reach("Jolly Roger Bay", 'Region', player))
|
||||
add_rule(world.get_location("Toad (Basement)", player), lambda state: state.can_reach("Basement",'Region',player))
|
||||
add_rule(world.get_location("Toad (Second Floor)", player), lambda state: state.can_reach("Second Floor",'Region',player))
|
||||
add_rule(world.get_location("Toad (Third Floor)", player), lambda state: state.can_reach("Third Floor",'Region',player))
|
||||
add_rule(world.get_location("The Secret Aquarium", player), lambda state: state.has("Power Star", player, 3))
|
||||
add_rule(world.get_location("Toad (Basement)", player), lambda state: state.can_reach("Basement",'Region',player) and state.has("Power Star", player, 12))
|
||||
add_rule(world.get_location("Toad (Second Floor)", player), lambda state: state.can_reach("Second Floor",'Region',player) and state.has("Power Star", player, 25))
|
||||
add_rule(world.get_location("Toad (Third Floor)", player), lambda state: state.can_reach("Third Floor",'Region',player) and state.has("Power Star", player, 35))
|
||||
add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement",'Region',player) and state.has("Power Star", player, 15))
|
||||
add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement",'Region',player) and state.has("Power Star", player, 50))
|
||||
|
||||
#Rules for Keys
|
||||
add_rule(world.get_location("Bowser in the Dark World Key", player), lambda state: state.has("Power Star", player, 8))
|
||||
add_rule(world.get_location("Bowser in the Fire Sea Key", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, 30))
|
||||
|
||||
world.completion_condition[player] = lambda state: state.can_reach("Third Floor",'Region',player) and state.has("Power Star", player, world.StarsToFinish[player].value)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import string
|
||||
from .Items import item_table, SM64Item
|
||||
import typing
|
||||
from .Items import item_table, cannon_item_table, SM64Item
|
||||
from .Locations import location_table, SM64Location
|
||||
from .Options import sm64_options
|
||||
from .Rules import set_rules
|
||||
@@ -21,40 +21,67 @@ class SM64World(World):
|
||||
item_name_to_id = item_table
|
||||
location_name_to_id = location_table
|
||||
|
||||
data_version = 3
|
||||
data_version = 6
|
||||
forced_auto_forfeit = False
|
||||
|
||||
area_connections: typing.Dict[int, int]
|
||||
|
||||
options = sm64_options
|
||||
|
||||
def generate_early(self):
|
||||
self.topology_present = self.world.AreaRandomizer[self.player].value
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self.world,self.player)
|
||||
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self.world,self.player)
|
||||
self.area_connections = {}
|
||||
set_rules(self.world, self.player, self.area_connections)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
item_id = item_table[name]
|
||||
item = SM64Item(name, True, item_id, self.player)
|
||||
item = SM64Item(name, name != "1Up Mushroom", item_id, self.player)
|
||||
return item
|
||||
|
||||
def generate_basic(self):
|
||||
staritem = self.create_item("Power Star")
|
||||
if (self.world.EnableCoinStars[self.player].value):
|
||||
self.world.itempool += [staritem for i in range(0,120)]
|
||||
else:
|
||||
self.world.itempool += [staritem for i in range(0,105)]
|
||||
starcount = min(self.world.StarsToFinish[self.player].value + self.world.ExtraStars[self.player].value,120)
|
||||
if (not self.world.EnableCoinStars[self.player].value):
|
||||
starcount = max(starcount - 15,self.world.StarsToFinish[self.player].value)
|
||||
self.world.itempool += [staritem for i in range(0,starcount)]
|
||||
mushroomitem = self.create_item("1Up Mushroom")
|
||||
self.world.itempool += [mushroomitem for i in range(starcount,120 - (15 if not self.world.EnableCoinStars[self.player].value else 0))]
|
||||
|
||||
key1 = self.create_item("Basement Key")
|
||||
key2 = self.create_item("Second Floor Key")
|
||||
self.world.itempool += [key1,key2]
|
||||
if (not self.world.ProgressiveKeys[self.player].value):
|
||||
key1 = self.create_item("Basement Key")
|
||||
key2 = self.create_item("Second Floor Key")
|
||||
self.world.itempool += [key1,key2]
|
||||
else:
|
||||
key = self.create_item("Progressive Key")
|
||||
self.world.itempool += [key,key]
|
||||
|
||||
wingcap = self.create_item("Wing Cap")
|
||||
metalcap = self.create_item("Metal Cap")
|
||||
vanishcap = self.create_item("Vanish Cap")
|
||||
self.world.itempool += [wingcap,metalcap,vanishcap]
|
||||
|
||||
if (self.world.BuddyChecks[self.player].value):
|
||||
self.world.itempool += [self.create_item(name) for name, id in cannon_item_table.items()]
|
||||
else:
|
||||
self.world.get_location("BoB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock BoB"))
|
||||
self.world.get_location("WF: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock WF"))
|
||||
self.world.get_location("JRB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock JRB"))
|
||||
self.world.get_location("CCM: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock CCM"))
|
||||
self.world.get_location("SSL: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock SSL"))
|
||||
self.world.get_location("SL: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock SL"))
|
||||
self.world.get_location("WDW: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock WDW"))
|
||||
self.world.get_location("TTM: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock TTM"))
|
||||
self.world.get_location("THI: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock THI"))
|
||||
self.world.get_location("RR: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock RR"))
|
||||
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"StarsToFinish": self.world.StarsToFinish[self.player].value
|
||||
"AreaRando": self.area_connections,
|
||||
"StarsToFinish": self.world.StarsToFinish[self.player].value,
|
||||
"DeathLink": self.world.DeathLink[self.player].value,
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class OffOnFullChoice(Choice):
|
||||
|
||||
class Difficulty(EvermizerFlags, Choice):
|
||||
"""Changes relative spell cost and stuff"""
|
||||
displayname = "Difficulty"
|
||||
display_name = "Difficulty"
|
||||
option_easy = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
@@ -39,7 +39,7 @@ class Difficulty(EvermizerFlags, Choice):
|
||||
|
||||
class MoneyModifier(Range):
|
||||
"""Money multiplier in %"""
|
||||
displayname = "Money Modifier"
|
||||
display_name = "Money Modifier"
|
||||
range_start = 1
|
||||
range_end = 2500
|
||||
default = 200
|
||||
@@ -47,7 +47,7 @@ class MoneyModifier(Range):
|
||||
|
||||
class ExpModifier(Range):
|
||||
"""EXP multiplier for Weapons, Characters and Spells in %"""
|
||||
displayname = "Exp Modifier"
|
||||
display_name = "Exp Modifier"
|
||||
range_start = 1
|
||||
range_end = 2500
|
||||
default = 200
|
||||
@@ -55,76 +55,76 @@ class ExpModifier(Range):
|
||||
|
||||
class FixSequence(EvermizerFlag, DefaultOnToggle):
|
||||
"""Fix some sequence breaks"""
|
||||
displayname = "Fix Sequence"
|
||||
display_name = "Fix Sequence"
|
||||
flag = '1'
|
||||
|
||||
|
||||
class FixCheats(EvermizerFlag, DefaultOnToggle):
|
||||
"""Fix cheats left in by the devs (not desert skip)"""
|
||||
displayname = "Fix Cheats"
|
||||
display_name = "Fix Cheats"
|
||||
flag = '2'
|
||||
|
||||
|
||||
class FixInfiniteAmmo(EvermizerFlag, Toggle):
|
||||
"""Fix infinite ammo glitch"""
|
||||
displayname = "Fix Infinite Ammo"
|
||||
display_name = "Fix Infinite Ammo"
|
||||
flag = '5'
|
||||
|
||||
|
||||
class FixAtlasGlitch(EvermizerFlag, Toggle):
|
||||
"""Fix atlas underflowing stats"""
|
||||
displayname = "Fix Atlas Glitch"
|
||||
display_name = "Fix Atlas Glitch"
|
||||
flag = '6'
|
||||
|
||||
|
||||
class FixWingsGlitch(EvermizerFlag, Toggle):
|
||||
"""Fix wings making you invincible in some areas"""
|
||||
displayname = "Fix Wings Glitch"
|
||||
display_name = "Fix Wings Glitch"
|
||||
flag = '7'
|
||||
|
||||
|
||||
class ShorterDialogs(EvermizerFlag, DefaultOnToggle):
|
||||
"""Cuts some dialogs"""
|
||||
displayname = "Shorter Dialogs"
|
||||
display_name = "Shorter Dialogs"
|
||||
flag = '9'
|
||||
|
||||
|
||||
class ShortBossRush(EvermizerFlag, DefaultOnToggle):
|
||||
"""Start boss rush at Metal Magmar, cut enemy HP in half"""
|
||||
displayname = "Short Boss Rush"
|
||||
display_name = "Short Boss Rush"
|
||||
flag = 'f'
|
||||
|
||||
|
||||
class Ingredienizer(EvermizerFlags, OffOnFullChoice):
|
||||
"""On Shuffles, Full randomizes spell ingredients"""
|
||||
displayname = "Ingredienizer"
|
||||
display_name = "Ingredienizer"
|
||||
default = 1
|
||||
flags = ['i', '', 'I']
|
||||
|
||||
|
||||
class Sniffamizer(EvermizerFlags, OffOnFullChoice):
|
||||
"""On Shuffles, Full randomizes drops in sniff locations"""
|
||||
displayname = "Sniffamizer"
|
||||
display_name = "Sniffamizer"
|
||||
default = 1
|
||||
flags = ['s', '', 'S']
|
||||
|
||||
|
||||
class Callbeadamizer(EvermizerFlags, OffOnFullChoice):
|
||||
"""On Shuffles call bead characters, Full shuffles individual spells"""
|
||||
displayname = "Callbeadamizer"
|
||||
display_name = "Callbeadamizer"
|
||||
default = 1
|
||||
flags = ['c', '', 'C']
|
||||
|
||||
|
||||
class Musicmizer(EvermizerFlag, Toggle):
|
||||
"""Randomize music for some rooms"""
|
||||
displayname = "Musicmizer"
|
||||
display_name = "Musicmizer"
|
||||
flag = 'm'
|
||||
|
||||
|
||||
class Doggomizer(EvermizerFlags, OffOnFullChoice):
|
||||
"""On shuffles dog per act, Full randomizes dog per screen, Pupdunk gives you Everpupper everywhere"""
|
||||
displayname = "Doggomizer"
|
||||
display_name = "Doggomizer"
|
||||
option_pupdunk = 3
|
||||
default = 0
|
||||
flags = ['', 'd', 'D', 'p']
|
||||
@@ -132,7 +132,7 @@ class Doggomizer(EvermizerFlags, OffOnFullChoice):
|
||||
|
||||
class TurdoMode(EvermizerFlag, Toggle):
|
||||
"""Replace offensive spells by Turd Balls with varying strength and make weapons weak"""
|
||||
displayname = "Turdo Mode"
|
||||
display_name = "Turdo Mode"
|
||||
flag = 't'
|
||||
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ class SoEWorld(World):
|
||||
if self.connect_name and self.connect_name != self.world.player_name[self.player]:
|
||||
payload = multidata["connect_names"][self.world.player_name[self.player]]
|
||||
multidata["connect_names"][self.connect_name] = payload
|
||||
del (multidata["connect_names"][self.world.player_name[self.player]])
|
||||
|
||||
|
||||
|
||||
class SoEItem(Item):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Dict
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Toggle, DeathLink
|
||||
from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option
|
||||
|
||||
class StartWithJewelryBox(Toggle):
|
||||
"Start with Jewelry Box unlocked"
|
||||
@@ -14,7 +14,7 @@ class StartWithJewelryBox(Toggle):
|
||||
# "Always find Security Keycard's in the following order D -> C -> B -> A"
|
||||
# display_name = "Progressive keycards"
|
||||
|
||||
class DownloadableItems(Toggle):
|
||||
class DownloadableItems(DefaultOnToggle):
|
||||
"With the tablet you will be able to download items at terminals"
|
||||
display_name = "Downloadable items"
|
||||
|
||||
@@ -58,8 +58,31 @@ class DamageRando(Toggle):
|
||||
"Each orb has a high chance of having lower base damage and a low chance of having much higher base damage."
|
||||
display_name = "Damage Rando"
|
||||
|
||||
class ShopFill(Choice):
|
||||
"""Sets the items for sale in Merchant Crow's shops.
|
||||
Default: No sunglasses or trendy jacket, but sand vials for sale.
|
||||
Randomized: Up to 4 random items in each shop.
|
||||
Vanilla: Keep shops the same as the base game.
|
||||
Empty: Sell no items at the shop."""
|
||||
display_name = "Shop Inventory"
|
||||
option_default = 0
|
||||
option_randomized = 1
|
||||
option_vanilla = 2
|
||||
option_empty = 3
|
||||
|
||||
class ShopWarpShards(DefaultOnToggle):
|
||||
"Shops always sell warp shards (when keys possessed), ignoring inventory setting."
|
||||
display_name = "Always Sell Warp Shards"
|
||||
|
||||
class ShopMultiplier(Range):
|
||||
"Multiplier for the cost of items in the shop. Set to 0 for free shops."
|
||||
display_name = "Shop Price Multiplier"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 1
|
||||
|
||||
# Some options that are available in the timespinner randomizer arent currently implemented
|
||||
timespinner_options: Dict[str, Toggle] = {
|
||||
timespinner_options: Dict[str, Option] = {
|
||||
"StartWithJewelryBox": StartWithJewelryBox,
|
||||
#"ProgressiveVerticalMovement": ProgressiveVerticalMovement,
|
||||
#"ProgressiveKeycards": ProgressiveKeycards,
|
||||
@@ -74,13 +97,19 @@ timespinner_options: Dict[str, Toggle] = {
|
||||
"Cantoran": Cantoran,
|
||||
"LoreChecks": LoreChecks,
|
||||
"DamageRando": DamageRando,
|
||||
"ShopFill": ShopFill,
|
||||
"ShopWarpShards": ShopWarpShards,
|
||||
"ShopMultiplier": ShopMultiplier,
|
||||
"DeathLink": DeathLink,
|
||||
}
|
||||
|
||||
def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool:
|
||||
return get_option_value(world, player, name) > 0
|
||||
|
||||
def get_option_value(world: MultiWorld, player: int, name: str) -> int:
|
||||
option = getattr(world, name, None)
|
||||
|
||||
if option == None:
|
||||
return False
|
||||
return 0
|
||||
|
||||
return int(option[player].value) > 0
|
||||
return int(option[player].value)
|
||||
|
||||
@@ -5,7 +5,7 @@ from .LogicMixin import TimespinnerLogic
|
||||
from .Items import get_item_names_per_category, item_table, starter_melee_weapons, starter_spells, starter_progression_items, filler_items
|
||||
from .Locations import get_locations, starter_progression_locations, EventId
|
||||
from .Regions import create_regions
|
||||
from .Options import is_option_enabled, timespinner_options
|
||||
from .Options import is_option_enabled, get_option_value, timespinner_options
|
||||
from .PyramidKeys import get_pyramid_keys_unlock
|
||||
|
||||
class TimespinnerWorld(World):
|
||||
@@ -54,6 +54,8 @@ class TimespinnerWorld(World):
|
||||
def create_item(self, name: str) -> Item:
|
||||
return create_item_with_correct_settings(self.world, self.player, name)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.world.random.choice(filler_items)
|
||||
|
||||
def set_rules(self):
|
||||
setup_events(self.world, self.player, self.locked_locations, self.location_cache)
|
||||
@@ -71,7 +73,7 @@ class TimespinnerWorld(World):
|
||||
|
||||
pool = get_item_pool(self.world, self.player, excluded_items)
|
||||
|
||||
fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations, self.location_cache, pool)
|
||||
fill_item_pool_with_dummy_items(self, self.world, self.player, self.locked_locations, self.location_cache, pool)
|
||||
|
||||
self.world.itempool += pool
|
||||
|
||||
@@ -80,7 +82,7 @@ class TimespinnerWorld(World):
|
||||
slot_data: Dict[str, object] = {}
|
||||
|
||||
for option_name in timespinner_options:
|
||||
slot_data[option_name] = is_option_enabled(self.world, self.player, option_name)
|
||||
slot_data[option_name] = get_option_value(self.world, self.player, option_name)
|
||||
|
||||
slot_data["StinkyMaw"] = True
|
||||
slot_data["ProgressiveVerticalMovement"] = False
|
||||
@@ -159,10 +161,10 @@ def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> L
|
||||
return pool
|
||||
|
||||
|
||||
def fill_item_pool_with_dummy_items(world: MultiWorld, player: int, locked_locations: List[str],
|
||||
def fill_item_pool_with_dummy_items(self: TimespinnerWorld, world: MultiWorld, player: int, locked_locations: List[str],
|
||||
location_cache: List[Location], pool: List[Item]):
|
||||
for _ in range(len(location_cache) - len(locked_locations) - len(pool)):
|
||||
item = create_item_with_correct_settings(world, player, world.random.choice(filler_items))
|
||||
item = create_item_with_correct_settings(world, player, self.get_filler_item_name())
|
||||
pool.append(item)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import typing
|
||||
from Options import Option, DeathLink, Range
|
||||
from Options import Option, DeathLink, Range, Toggle
|
||||
|
||||
class DoorCost(Range):
|
||||
"""Amount of Trinkets required to enter Areas. Set to 0 to disable artificial locks."""
|
||||
@@ -7,14 +7,29 @@ class DoorCost(Range):
|
||||
range_end = 3
|
||||
default = 3
|
||||
|
||||
class AreaCostRandomizer(Toggle):
|
||||
"""Randomize which Area requires which set of DoorCost Trinkets"""
|
||||
display_name = "Area Cost Randomizer"
|
||||
|
||||
class DeathLinkAmnesty(Range):
|
||||
"""Amount of Deaths to take before sending a DeathLink signal, for balancing difficulty"""
|
||||
range_start = 0
|
||||
range_end = 30
|
||||
default = 15
|
||||
|
||||
class AreaRandomizer(Toggle):
|
||||
"""Randomize Entrances to Areas"""
|
||||
displayname = "Area Randomizer"
|
||||
|
||||
class MusicRandomizer(Toggle):
|
||||
"""Randomize Music"""
|
||||
displayname = "Music Randomizer"
|
||||
|
||||
v6_options: typing.Dict[str,type(Option)] = {
|
||||
"MusicRandomizer": MusicRandomizer,
|
||||
"AreaRandomizer": AreaRandomizer,
|
||||
"DoorCost": DoorCost,
|
||||
"AreaCostRandomizer": AreaCostRandomizer,
|
||||
"DeathLink": DeathLink,
|
||||
"DeathLinkAmnesty": DeathLinkAmnesty
|
||||
}
|
||||
@@ -2,6 +2,9 @@ import typing
|
||||
from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType
|
||||
from .Locations import V6Location, location_table
|
||||
|
||||
v6areas = ["Laboratory", "The Tower", "Space Station 2", "Warp Zone"]
|
||||
|
||||
|
||||
def create_regions(world: MultiWorld, player: int):
|
||||
regOvr = Region("Menu", RegionType.Generic, "Dimension VVVVVV", player, world)
|
||||
locOvr_names = ["Overworld (Pipe-shaped Segment)", "Overworld (Left of Ship)", "Overworld (Square Room)", "Overworld (Sad Elephant)",
|
||||
@@ -29,6 +32,7 @@ def create_regions(world: MultiWorld, player: int):
|
||||
regWrp.locations += [V6Location(player, loc_name, location_table[loc_name], regWrp) for loc_name in locWrp_names]
|
||||
world.regions.append(regWrp)
|
||||
|
||||
|
||||
def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule):
|
||||
sourceRegion = world.get_region(source, player)
|
||||
targetRegion = world.get_region(target, player)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import typing
|
||||
from ..generic.Rules import add_rule
|
||||
from .Regions import connect_regions
|
||||
from .Regions import connect_regions, v6areas
|
||||
|
||||
|
||||
def _has_trinket_range(state,player,start,end) -> bool:
|
||||
for i in range(start,end):
|
||||
@@ -8,11 +9,21 @@ def _has_trinket_range(state,player,start,end) -> bool:
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_rules(world,player):
|
||||
connect_regions(world, player, "Menu", "Laboratory", lambda state: _has_trinket_range(state,player,0,world.DoorCost[player].value))
|
||||
connect_regions(world, player, "Menu", "The Tower", lambda state: _has_trinket_range(state,player,world.DoorCost[player].value,world.DoorCost[player].value*2))
|
||||
connect_regions(world, player, "Menu", "Space Station 2", lambda state: _has_trinket_range(state,player,world.DoorCost[player].value*2,world.DoorCost[player].value*3))
|
||||
connect_regions(world, player, "Menu", "Warp Zone", lambda state: _has_trinket_range(state,player,world.DoorCost[player].value*3,world.DoorCost[player].value*4))
|
||||
|
||||
def set_rules(world, player, area_connections: typing.Dict[int, int], area_cost_map: typing.Dict[int, int]):
|
||||
areashuffle = list(range(len(v6areas)))
|
||||
if world.AreaRandomizer[player].value:
|
||||
world.random.shuffle(areashuffle)
|
||||
area_connections.update({(index+1): (value+1) for index, value in enumerate(areashuffle)})
|
||||
area_connections.update({0:0})
|
||||
if world.AreaCostRandomizer[player].value:
|
||||
world.random.shuffle(areashuffle)
|
||||
area_cost_map.update({(index+1): (value+1) for index, value in enumerate(areashuffle)})
|
||||
area_cost_map.update({0:0})
|
||||
|
||||
for i in range(1,5):
|
||||
connect_regions(world, player, "Menu", v6areas[area_connections[i]-1], lambda state: _has_trinket_range(state,player,world.DoorCost[player].value*(area_cost_map[i]-1),
|
||||
world.DoorCost[player].value*area_cost_map[i]))
|
||||
|
||||
#Special Rule for V
|
||||
add_rule(world.get_location("V",player), lambda state : state.can_reach("Laboratory",'Region',player) and
|
||||
@@ -21,12 +32,9 @@ def set_rules(world,player):
|
||||
state.can_reach("Warp Zone",'Region',player))
|
||||
|
||||
#Special Rule for NPC Trinket
|
||||
add_rule(world.get_location("NPC Trinket",player), lambda state: state.can_reach("Laboratory",'Region',player) or
|
||||
state.can_reach("Space Station 2",'Region',player))
|
||||
|
||||
connect_regions(world, player, "Laboratory", "Menu", lambda state: True)
|
||||
connect_regions(world, player, "The Tower", "Menu", lambda state: True)
|
||||
connect_regions(world, player, "Space Station 2", "Menu", lambda state: True)
|
||||
connect_regions(world, player, "Warp Zone", "Menu", lambda state: True)
|
||||
add_rule(world.get_location("V",player), lambda state : state.can_reach("Laboratory",'Region',player) or
|
||||
( state.can_reach("The Tower",'Region',player) and
|
||||
state.can_reach("Space Station 2",'Region',player) and
|
||||
state.can_reach("Warp Zone",'Region',player) ))
|
||||
|
||||
world.completion_condition[player] = lambda state: state.can_reach("V",'Location',player)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import string
|
||||
import typing
|
||||
|
||||
from .Items import item_table, V6Item
|
||||
from .Locations import location_table, V6Location
|
||||
from .Options import v6_options
|
||||
@@ -23,23 +24,40 @@ class V6World(World):
|
||||
data_version = 1
|
||||
forced_auto_forfeit = False
|
||||
|
||||
area_connections: typing.Dict[int, int]
|
||||
area_cost_map: typing.Dict[int,int]
|
||||
|
||||
music_map: typing.Dict[int,int]
|
||||
|
||||
options = v6_options
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self.world,self.player)
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self.world,self.player)
|
||||
self.area_connections = {}
|
||||
self.area_cost_map = {}
|
||||
set_rules(self.world, self.player, self.area_connections, self.area_cost_map)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
return V6Item(name, True, item_table[name], self.player)
|
||||
|
||||
def generate_basic(self):
|
||||
self.world.itempool += [self.create_item(name) for name in self.item_names]
|
||||
trinkets = [self.create_item("Trinket " + str(i+1).zfill(2)) for i in range(0,20)]
|
||||
self.world.itempool += trinkets
|
||||
|
||||
musiclist_o = [1,2,3,4,9,12]
|
||||
musiclist_s = musiclist_o.copy()
|
||||
if self.world.MusicRandomizer[self.player].value:
|
||||
self.world.random.shuffle(musiclist_s)
|
||||
self.music_map = dict(zip(musiclist_o, musiclist_s))
|
||||
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"MusicRando": self.music_map,
|
||||
"AreaRando": self.area_connections,
|
||||
"DoorCost": self.world.DoorCost[self.player].value,
|
||||
"AreaCostRando": self.area_cost_map,
|
||||
"DeathLink": self.world.DeathLink[self.player].value,
|
||||
"DeathLink_Amnesty": self.world.DeathLinkAmnesty[self.player].value
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user