Compare commits

...

73 Commits
0.2.4 ... 0.2.5

Author SHA1 Message Date
Yussur Mustafa Oraji
1d19868119 v6: Update NPC Trinket Rule 2022-02-18 19:32:36 +01:00
Fabian Dill
840e634161 update docs with NetworkSlot and create_as_hint 2022-02-18 18:54:26 +01:00
Fabian Dill
731eef8c2f bump version 2022-02-18 17:58:45 +01:00
CaitSith2
135ee018a9 update Copyright 2022-02-17 19:03:11 -08:00
Fabian Dill
7633392eea update Copyright 2022-02-17 08:21:26 +01:00
Fabian Dill
daea0f3e5e Core: provide a way to add to CollectionState init and copy
SM: use that way
OoT: use that way
2022-02-17 07:07:34 +01:00
Fabian Dill
c525c80b49 ItemLinks: move item links to events, mess up their logic in doing so and lock them behind plando option "item_links" until they're fixed. 2022-02-17 06:07:20 +01:00
N00byKing
311fb04647 sm64ex: Add option for Bob-omb Buddy Checks 2022-02-16 19:46:28 +01:00
Hussein Farran
219bd9c10e Merge pull request #285 from JarnoWesthof/add_reference_to_cpp_lib
[Docs] Added reference to the cpp lib
2022-02-16 09:01:00 -05:00
Jarno Westhof
6d704eadd7 [Docs] Added reference to the cpp lib 2022-02-16 13:05:47 +01:00
Hussein Farran
32da1993e1 Merge pull request #283 from alwaysintreble/tutorials
Tutorials: Clean up plando guide a bit; explain datapackage page. Add…
2022-02-15 15:58:03 -05:00
alwaysintreble
d4cad980e5 Tutorials: remove /api 2022-02-15 14:17:17 -06:00
Fabian Dill
53340ab22c Core: remove legacy "dynamic_regions", as all regions are now dynamic 2022-02-15 06:29:57 +01:00
alwaysintreble
2d3767a35c Tutorials: Clean up plando guide a bit; explain datapackage page. Add link to the weighted settings page in advanced tutorial. 2022-02-14 17:21:19 -06:00
Fabian Dill
aaa9bc906e WebHost: update dependencies 2022-02-14 21:37:50 +01:00
N00byKing
7503317d49 sm64ex: Add DeathLink Support 2022-02-14 16:37:49 +01:00
Fabian Dill
3fc93a33c8 WebHost: check for duplicate names
Generate: use Counter for duplicate names to make finding the dupes easier
2022-02-14 04:58:21 +01:00
Fabian Dill
d7d1d54a0b Core: generalize pre_fill item pool handling 2022-02-13 23:02:18 +01:00
Fabian Dill
34b9344084 ItemLink; correct validation to allow for None replacement item 2022-02-13 20:19:17 +01:00
espeon65536
779f3a8a61 OoT: regions are not barren if they contain never-exclude items 2022-02-12 17:29:06 +01:00
espeon65536
8c1690ef65 OoT: invert logic of previous commit 2022-02-12 17:29:06 +01:00
espeon65536
85f32d9a97 OoT: make Farore's Wind a never-exclude item if the relevant trick is off 2022-02-12 17:29:06 +01:00
espeon65536
54c7ec5873 OoT: ice traps have the trap attribute 2022-02-12 17:29:06 +01:00
espeon65536
8d260708d3 OoT: ER fixes
Don't allow beatable only to influence priority placements
Shuffle spawns after warp songs to prevent spawn points going to Desert Colossus
Prevent child spawn from priority placing at Colossus if overworld ER is off
2022-02-12 17:29:06 +01:00
espeon65536
f8009e4b84 OoT: certain ER options convert closed forest into closed deku + child start 2022-02-12 17:29:06 +01:00
Alchav
a2260ee6b2 [SM] Fix "No Energy" bugs 2022-02-12 17:28:23 +01:00
Bondo
6193eafb7b Update Text.py (#274)
Changed the Houlihan hint tile to list the winner of the SGLive 2021 tournament in similar style to alttp tournament winners.
2022-02-12 03:01:41 +01:00
black-sliver
a4eea3325f Document id range for items and locations 2022-02-12 03:00:09 +01:00
Jarno Westhof
b93e61b758 [Timnespinner] Implemented get_filler_item_name 2022-02-09 21:08:07 +01:00
Fabian Dill
14448ad97e Multidata: allow SoE/SM/LttP to connect via player name for use in Tracker/Text clients 2022-02-09 21:06:50 +01:00
Yussur Mustafa Oraji
3d17f0d588 sm64ex: Add Course Randomizer and Progressive Keys (#256) 2022-02-09 20:57:38 +01:00
CaitSith2
ee5ea09cbc Add an autofill !hint_location for clicking on a Missing: line, when user uses !missing. 2022-02-08 14:29:24 -08:00
Fabian Dill
aac8ca97ed Core: define unreachables as set 2022-02-07 00:26:44 +01:00
Fabian Dill
e4d6da47a4 LttP: fix rom writing crash because I accidentally defaulted to pep8 naming 2022-02-06 21:44:19 +01:00
Fabian Dill
9f7dbb394e LttP: convert overflow progressive items into highest-allowed-tier of non-progressive item 2022-02-06 20:11:40 +01:00
Fabian Dill
f98063b97a Options: move name verification into class methods, out of Generate.py 2022-02-06 16:37:21 +01:00
black-sliver
ed607bdc37 Fix wrong message when loading apsave
from doubling received_items that happened when moving from world-based to client-based remote_items
2022-02-06 12:28:46 +01:00
N00byKing
a3c3e4cbd4 v6: Add Area Cost Shuffle 2022-02-05 20:24:42 +01:00
ScootyPuffJr1
bffb8a034e [SM]Update Options.py (#268)
* [SM] Update Options.py
2022-02-05 20:23:17 +01:00
Fabian Dill
8242d4fe92 ItemLink: fix wrong variable use 2022-02-05 20:15:56 +01:00
Fabian Dill
279b682ac2 ItemLink: hopefully fix coop functionality 2022-02-05 17:35:12 +01:00
Fabian Dill
43ff476d98 AutoWorld: add "Everything" item_name_group to all worlds 2022-02-05 16:55:11 +01:00
Fabian Dill
28201a6c38 Core: implement first version of ItemLinks 2022-02-05 15:49:19 +01:00
N00byKing
6923800081 v6: Music Randomizer 2022-02-04 23:04:05 +01:00
Jarno Westhof
700b83572e [Timespinner] Added new shop options (#264)
* [Timespinner] Added new shop options
2022-02-04 21:53:47 +01:00
Fabian Dill
6e53cb2deb V6: some cleanup 2022-02-04 21:34:39 +01:00
Yussur Mustafa Oraji
8e04182b3f v6: Add Area Randomizer (#249)
* v6: Add Area Randomizer
2022-02-04 21:22:26 +01:00
Jarno Westhof
9fd6d1b81f [Server] Broadcast hint_cost and location_check_points update changes via RoomUpdate 2022-02-03 13:09:59 +01:00
Fabian Dill
60379d9ae6 LttP: when generating hint tiles, no longer consider Single Arrow as useful, but do consider all varieties of Bow. Additionally, don't create hints for Universal Small Keys 2022-02-03 10:41:31 +01:00
black-sliver
29ba1d4809 Doc: change displayname to display_name in api.md 2022-02-02 23:38:00 +01:00
Fabian Dill
dc4b064c73 Options: change displayname to display_name 2022-02-02 16:29:29 +01:00
Fabian Dill
0f20888563 Options: allow yaml access to Priority Locations 2022-02-01 16:36:14 +01:00
Brad Humphrey
2361f8f9d3 Use logic when placing non-excluded items 2022-02-01 16:35:18 +01:00
Chris Wilson
feba54d5d2 Fix filename for Super Mario 64 info page 2022-01-31 18:39:17 -05:00
Brad Humphrey
3cecab25c7 Add unplaced_items into the fill sweep 2022-01-31 19:17:06 +01:00
Brad Humphrey
814851ba60 Don't require every item to fill 2022-01-31 19:17:06 +01:00
Fabian Dill
6333cc3bea Server: optimize send_multiple 2022-01-31 19:05:00 +01:00
N00byKing
00bf9c569a Add send_multiple command 2022-01-31 18:56:46 +01:00
Jarno
6def1bce25 [Docs] Made LocationInfoPacket more specific 2022-01-31 18:55:20 +01:00
Jarno Westhof
3ab5c90d7c [Docs] updated description on player property of NetworkItem 2022-01-31 18:55:20 +01:00
N00byKing
0507d6923e sm64ex: Add Option to limit stars, replace with junk 2022-01-31 18:54:54 +01:00
N00byKing
e85baa8068 sm64ex: Link to release page 2022-01-31 10:57:57 +01:00
N00byKing
cbed5a0c14 sm64ex,v6: Add Note regarding spaces in arguments 2022-01-31 10:57:43 +01:00
Fabian Dill
e0628ec6c9 WebHost: correct some texts 2022-01-31 10:11:39 +01:00
Chris Wilson
82637ff072 [WebHost] Add version notice to /generate and /uploads 2022-01-30 20:06:03 -05:00
Chris Wilson
a95a18a8b5 [WebHost] weighted-settings: Add cursor hover to user-message 2022-01-30 16:53:53 -05:00
Chris Wilson
d36637ed13 Fix a bug causing the /weighted-settings page to fail to detect a change in the source JSON file 2022-01-30 16:50:04 -05:00
N00byKing
dd5e5dcda7 v6: Add missing info regarding Server Port 2022-01-30 18:49:39 +01:00
Jarno Westhof
0ff7fe8479 [Generation] Fixed creation of new Slot-Info 2022-01-30 17:09:10 +01:00
Fabian Dill
8c638bcfd8 Server: allow LocationScouts to create free hints 2022-01-30 14:14:49 +01:00
Fabian Dill
0bd252e7f5 Server: add slot_info key to Connected 2022-01-30 13:57:12 +01:00
Jarno Westhof
ddd3073132 [Docs] Fixed typo 2022-01-30 13:52:51 +01:00
N00byKing
1788422abc v6: Link to release instead of actions 2022-01-30 10:58:48 +01:00
74 changed files with 1346 additions and 919 deletions

View File

@@ -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
View File

@@ -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']:

View File

@@ -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)}.")

View File

@@ -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
View File

@@ -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):

View File

@@ -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())

View File

@@ -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):

View File

@@ -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)

View File

@@ -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")

View File

@@ -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"}:

View File

@@ -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)

View File

@@ -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),
}

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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); }
});
});

View File

@@ -213,6 +213,7 @@ html{
#weighted-settings #user-message.visible{
display: block;
cursor: pointer;
}
#weighted-settings h1{

View File

@@ -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,

View File

@@ -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">

View 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'])

View File

@@ -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)] = {

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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:

View File

@@ -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

View File

@@ -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"]],
])

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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'}

View File

@@ -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):

View File

@@ -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",

View File

@@ -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()

View File

@@ -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.")

View File

@@ -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", {})

View File

@@ -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)] = {

View File

@@ -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 = {}

View File

@@ -1,2 +1 @@
factorio-rcon-py>=1.2.1
schema>=0.7.4

View File

@@ -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] = {

View File

@@ -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

View File

@@ -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)

View File

@@ -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)] = {

View File

@@ -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

View File

@@ -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):

View File

@@ -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 = [

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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")

View File

@@ -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,

View File

@@ -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):

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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}

View File

@@ -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}

View File

@@ -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,
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,
}

View File

@@ -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'

View File

@@ -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):

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
}