Compare commits

..

71 Commits
0.1.2 ... 0.1.3

Author SHA1 Message Date
Fabian Dill
6c1d164330 LttP: set non-native items to Power Star 2021-06-22 06:25:19 +02:00
Fabian Dill
937fee9019 Factorio: fix locale file formatting 2021-06-22 02:00:35 +02:00
Fabian Dill
023a798ac1 Factorio: refactor visibility option into tech_tree_information
set vanilla technologies to be hidden instead of disabled
          fix advancement icon still showing when no information in tech was supposed to be given
2021-06-21 22:25:49 +02:00
Fabian Dill
07d61f6d47 fix playerSettings.yaml post-merge 2021-06-21 02:51:54 +02:00
Fabian Dill
304f63aedf Merge branch 'espeon' into Archipelago_Main
# Conflicts:
#	playerSettings.yaml
2021-06-21 02:49:06 +02:00
Fabian Dill
30190f373a send /received output to self.output 2021-06-21 02:14:25 +02:00
espeon65536
b51b094cc1 Added HMG to playerSettings 2021-06-18 23:45:03 -05:00
Fabian Dill
f4a2f344a7 format MultiServer.py 2021-06-19 03:03:06 +02:00
Fabian Dill
1e7214a86b fix required plando options triggering on empty string 2021-06-19 01:00:41 +02:00
Fabian Dill
f8fd8b3585 Factorio: add toggle to disable imported blueprints 2021-06-19 01:00:21 +02:00
CaitSith2
644d62c915 Ignore Factorio AP savegame file. 2021-06-18 14:23:55 -07:00
Fabian Dill
741ec36ee1 all requires to be modified by trigggers and linked options 2021-06-18 23:17:12 +02:00
Fabian Dill
a08d7bb1b2 Settings: add requires 2021-06-18 22:15:54 +02:00
Fabian Dill
a5bf3a8407 Factorio: remove option to turn off random_tech_ingredients 2021-06-16 23:41:43 +02:00
Chris Wilson
3ee88fd8fe Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into main 2021-06-15 21:18:24 -04:00
Chris Wilson
bc9c93b180 Improvements to the WebHost
- Improved routing structure
- Improved style imports across site
- Added placeholder player-settings pages for Factorio and Minecraft
2021-06-15 21:18:14 -04:00
Fabian Dill
167958c002 fix legacy weapons trigger 2021-06-15 23:23:39 +02:00
Fabian Dill
8b16ffb629 fix LttP rom options 2021-06-15 21:15:57 +02:00
Fabian Dill
b5193162bf update playerSettings.yaml 2021-06-15 20:26:31 +02:00
Fabian Dill
bc34c237b6 move minecraft plando connections into minecraft 2021-06-15 16:34:36 +02:00
Fabian Dill
d9824d26d2 make Factorio rocket silo a static (and therefore local) node 2021-06-15 15:32:40 +02:00
Fabian Dill
8d08b55e69 move item referencing options into their game's category 2021-06-15 15:10:31 +02:00
Fabian Dill
503c844971 categorize game options 2021-06-15 14:11:46 +02:00
espeon65536
deff356910 Added HMG check to all checks for OWG and NL 2021-06-14 22:10:26 -05:00
Chris Wilson
883ebbf267 Updating WebHost structure 2021-06-14 22:27:43 -04:00
Fabian Dill
cd45116dce dynamify games listing 2021-06-15 02:35:40 +02:00
Chris Wilson
d80362c4b8 Fix 404 pages 2021-06-14 20:20:23 -04:00
Chris Wilson
384e06d6fe Subdirectory pages currently 404. I'll look into this later 2021-06-14 20:18:40 -04:00
Fabian Dill
e6f44a70d0 use flask convention for template fetching 2021-06-15 01:51:40 +02:00
Chris Wilson
0ca90ee7e8 Add subdirectory handling for zelda3, factorio, and minecraft. Add generic 404 page. 2021-06-14 19:35:02 -04:00
Fabian Dill
59a56c803a Log which player's plando has caused a placement failure 2021-06-14 23:42:13 +02:00
Fabian Dill
1e0b44bdc5 set Triforce Piece Defaults 2021-06-14 23:41:47 +02:00
Fabian Dill
2f3296bada remove _ and - from pedestal hint texts 2021-06-14 02:23:41 +02:00
Fabian Dill
434d8e0977 remove _ and - from item hint texts 2021-06-14 02:20:13 +02:00
Fabian Dill
0a89eaaf62 update trigger result key before trigger 2021-06-14 02:14:02 +02:00
Fabian Dill
cea2f81b86 remove IRH special rule now that it's a 1/1 triforce piece hunt 2021-06-13 07:57:34 +02:00
Fabian Dill
86b612f3b5 implement random-middle 2021-06-12 21:05:45 +02:00
espeon65536
d425e5eb6a disable GT junk fill in hybrid 2021-06-12 13:11:14 -05:00
Fabian Dill
183fd33f3f MultiMystery linux compatibility 2021-06-12 16:10:56 +02:00
Chris Wilson
8c82d3e747 Added a page to describe the games currently supported by AP 2021-06-12 02:49:36 -04:00
Chris Wilson
7b495f3d81 Website landing page preliminary update 2021-06-11 20:22:47 -04:00
Fabian Dill
3ea7f1cb03 Factorio Funnels: only sort current funnel, not all funnels 2021-06-11 20:18:28 +02:00
Fabian Dill
2a13fe05c6 fix import error for Hollow Knight 2021-06-11 18:05:49 +02:00
Fabian Dill
2c4c899179 move more Factorio stuff around 2021-06-11 18:02:48 +02:00
Fabian Dill
760fb32016 fix Factorio Recipe Time randomization not being deterministic 2021-06-11 14:47:13 +02:00
Fabian Dill
278f40471b fix open_pyramid default 2021-06-11 14:26:12 +02:00
Fabian Dill
20ca09c730 remove test modules 2021-06-11 14:23:59 +02:00
Fabian Dill
568a71cdbe Start implementing object oriented scaffold for world types
(There's still a lot of work ahead, such as:
registering locations and items to the World, as well as methods to create_item_from_name()
many more method names for various stages
embedding Options into the world type
and many more...)
2021-06-11 14:22:44 +02:00
Fabian Dill
753a5f7cb2 Merge branch 'split' into Archipelago_Main
# Conflicts:
#	Main.py
2021-06-11 13:27:28 +02:00
espeon65536
96e13786cd Fixed broken mirrorless swamp rules 2021-06-10 18:10:25 -05:00
espeon65536
5d6592f296 Merge branch 'main' of https://github.com/espeon65536/Archipelago into main 2021-06-09 11:00:33 -05:00
Fabian Dill
534dd331b9 document item locality options properly 2021-06-09 10:13:18 +02:00
espeon65536
b3b56fcafd removed unnecessary import 2021-06-08 19:32:27 -05:00
espeon65536
671fd50cfb Moved the add_rule for mirrorless swamp to speed it up on invalid entrance shuffle type 2021-06-08 19:19:11 -05:00
espeon65536
eaf19643a9 Cleaned up code for assigning dungeon reentry rules 2021-06-08 19:18:28 -05:00
espeon65536
a582a3781b Moved the addition of HMG-specific connections to fix crossed ER 2021-06-08 18:32:22 -05:00
espeon65536
e0d90e0b21 Properly accounting for agatower not freely opening for dungeon reentry 2021-06-08 18:17:21 -05:00
espeon65536
a73189338c Fixed full ER HMG not ignoring pearl requirements on entrances 2021-06-08 18:15:47 -05:00
Fabian Dill
1e414dd370 fix tests 2021-06-08 22:14:56 +02:00
Fabian Dill
5ea03c71c0 start moving some alttp options over to the new system 2021-06-08 21:58:11 +02:00
Fabian Dill
622f8f8158 always check legal range for Range 2021-06-08 15:39:34 +02:00
Fabian Dill
821b0f0f92 document random-high and random-low 2021-06-08 14:56:41 +02:00
Fabian Dill
471b217e99 add random-high and random-low to Range Options 2021-06-08 14:48:00 +02:00
Fabian Dill
adda0eff4a implement Range option type 2021-06-08 14:15:23 +02:00
espeon65536
2001ca6566 Fixed the check on dungeon reentry not working properly 2021-06-08 01:22:16 -05:00
espeon65536
b9a783d7d7 Fixed open connections breaking non-HMG seed generation 2021-06-08 00:50:28 -05:00
espeon65536
eb9ee9f41e Hybrid Major Glitches connections and logic 2021-06-07 20:19:03 -05:00
espeon65536
fae14ad283 Mystery.py correctly recognizes HMG as an option 2021-06-07 19:34:00 -05:00
espeon65536
16c6e17a49 Initial handling of hybrid glitch logic outside of UnderworldGlitchRules 2021-06-07 01:19:27 -05:00
espeon65536
ac31671914 initial hybridmg logic file commit 2021-06-07 00:38:30 -05:00
Fabian Dill
109eb5b9dc start of split 2021-05-13 01:34:59 +02:00
74 changed files with 1971 additions and 1265 deletions

2
.gitignore vendored
View File

@@ -144,3 +144,5 @@ dmypy.json
# Cython debug symbols # Cython debug symbols
cython_debug/ cython_debug/
Archipelago.zip

View File

@@ -23,6 +23,7 @@ class MultiWorld():
plando_items: List[PlandoItem] plando_items: List[PlandoItem]
plando_connections: List[PlandoConnection] plando_connections: List[PlandoConnection]
er_seeds: Dict[int, str] er_seeds: Dict[int, str]
worlds: Dict[int, "AutoWorld.World"]
class AttributeProxy(): class AttributeProxy():
def __init__(self, rule): def __init__(self, rule):
@@ -32,8 +33,6 @@ class MultiWorld():
return self.rule(player) return self.rule(player)
def __init__(self, players: int): def __init__(self, players: int):
# TODO: move per-player settings into new classes per game-type instead of clumping it all together here
self.random = random.Random() # world-local random state is saved for multiple generations running concurrently self.random = random.Random() # world-local random state is saved for multiple generations running concurrently
self.players = players self.players = players
self.teams = 1 self.teams = 1
@@ -113,8 +112,6 @@ class MultiWorld():
set_player_attr('bush_shuffle', False) set_player_attr('bush_shuffle', False)
set_player_attr('beemizer', 0) set_player_attr('beemizer', 0)
set_player_attr('escape_assist', []) set_player_attr('escape_assist', [])
set_player_attr('crystals_needed_for_ganon', 7)
set_player_attr('crystals_needed_for_gt', 7)
set_player_attr('open_pyramid', False) set_player_attr('open_pyramid', False)
set_player_attr('treasure_hunt_icon', 'Triforce Piece') set_player_attr('treasure_hunt_icon', 'Triforce Piece')
set_player_attr('treasure_hunt_count', 0) set_player_attr('treasure_hunt_count', 0)
@@ -131,7 +128,6 @@ class MultiWorld():
set_player_attr('triforce_pieces_available', 30) set_player_attr('triforce_pieces_available', 30)
set_player_attr('triforce_pieces_required', 20) set_player_attr('triforce_pieces_required', 20)
set_player_attr('shop_shuffle', 'off') set_player_attr('shop_shuffle', 'off')
set_player_attr('shop_shuffle_slots', 0)
set_player_attr('shuffle_prizes', "g") set_player_attr('shuffle_prizes', "g")
set_player_attr('sprite_pool', []) set_player_attr('sprite_pool', [])
set_player_attr('dark_room_logic', "lamp") set_player_attr('dark_room_logic', "lamp")
@@ -141,15 +137,19 @@ class MultiWorld():
set_player_attr('plando_connections', []) set_player_attr('plando_connections', [])
set_player_attr('game', "A Link to the Past") set_player_attr('game', "A Link to the Past")
set_player_attr('completion_condition', lambda state: True) set_player_attr('completion_condition', lambda state: True)
import Options
for hk_option in Options.hollow_knight_options:
set_player_attr(hk_option, False)
self.custom_data = {} self.custom_data = {}
for player in range(1, players+1): self.worlds = {}
def set_options(self, args):
import Options
from worlds import AutoWorld
for option_set in Options.option_sets:
for option in option_set:
setattr(self, option, getattr(args, option, {}))
for player in self.player_ids:
self.custom_data[player] = {} self.custom_data[player] = {}
# self.worlds = [] self.worlds[player] = AutoWorld.AutoWorldRegister.world_types[self.game[player]](self, player)
# for i in range(players):
# self.worlds.append(worlds.alttp.ALTTPWorld({}, i))
def secure(self): def secure(self):
self.random = secrets.SystemRandom() self.random = secrets.SystemRandom()
@@ -813,6 +813,9 @@ class CollectionState(object):
rules.append(self.has('Moon Pearl', player)) rules.append(self.has('Moon Pearl', player))
return all(rules) return all(rules)
def can_bomb_clip(self, region: Region, player: int) -> bool:
return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player)
# Minecraft logic functions # Minecraft logic functions
def has_iron_ingots(self, player: int): def has_iron_ingots(self, player: int):
return self.has('Progressive Tools', player) and self.has('Ingot Crafting', player) return self.has('Progressive Tools', player) and self.has('Ingot Crafting', player)
@@ -1237,11 +1240,11 @@ class Item():
@property @property
def hint_text(self): def hint_text(self):
return getattr(self, "_hint_text", self.name) return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
@property @property
def pedestal_hint_text(self): def pedestal_hint_text(self):
return getattr(self, "_pedestal_hint_text", self.name) return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " "))
def __eq__(self, other): def __eq__(self, other):
return self.name == other.name and self.player == other.player return self.name == other.name and self.player == other.player
@@ -1448,7 +1451,7 @@ class Spoiler(object):
'triforce_pieces_available': self.world.triforce_pieces_available, 'triforce_pieces_available': self.world.triforce_pieces_available,
'triforce_pieces_required': self.world.triforce_pieces_required, 'triforce_pieces_required': self.world.triforce_pieces_required,
'shop_shuffle': self.world.shop_shuffle, 'shop_shuffle': self.world.shop_shuffle,
'shop_shuffle_slots': self.world.shop_shuffle_slots, 'shop_item_slots': self.world.shop_item_slots,
'shuffle_prizes': self.world.shuffle_prizes, 'shuffle_prizes': self.world.shuffle_prizes,
'sprite_pool': self.world.sprite_pool, 'sprite_pool': self.world.sprite_pool,
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss, 'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss,
@@ -1565,8 +1568,8 @@ class Spoiler(object):
"f" in self.metadata["shop_shuffle"][player])) "f" in self.metadata["shop_shuffle"][player]))
outfile.write('Custom Potion Shop: %s\n' % outfile.write('Custom Potion Shop: %s\n' %
bool_to_text("w" in self.metadata["shop_shuffle"][player])) bool_to_text("w" in self.metadata["shop_shuffle"][player]))
outfile.write('Shop Slots: %s\n' % outfile.write('Shop Item Slots: %s\n' %
self.metadata["shop_shuffle_slots"][player]) self.metadata["shop_item_slots"][player])
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player]) outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
outfile.write( outfile.write(
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player])) 'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))

View File

@@ -47,12 +47,9 @@ class ClientCommandProcessor(CommandProcessor):
def _cmd_received(self) -> bool: def _cmd_received(self) -> bool:
"""List all received items""" """List all received items"""
logger.info('Received items:') logger.info(f'{len(self.ctx.items_received)} received items:')
for index, item in enumerate(self.ctx.items_received, 1): for index, item in enumerate(self.ctx.items_received, 1):
logging.info('%s from %s (%s) (%d/%d in list)' % ( self.output(f"{self.ctx.item_name_getter(item.item)} from {self.ctx.player_names[item.player]}")
color(self.ctx.item_name_getter(item.item), 'red', 'bold'),
color(self.ctx.player_names[item.player], 'yellow'),
self.ctx.location_name_getter(item.location), index, len(self.ctx.items_received)))
return True return True
def _cmd_missing(self) -> bool: def _cmd_missing(self) -> bool:

View File

@@ -76,7 +76,7 @@ class FactorioContext(CommonContext):
await super(FactorioContext, self).server_auth(password_requested) await super(FactorioContext, self).server_auth(password_requested)
await self.send_msgs([{"cmd": 'Connect', await self.send_msgs([{"cmd": 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils._version_tuple, 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
'tags': ['AP'], 'tags': ['AP'],
'uuid': Utils.get_unique_identifier(), 'game': "Factorio" 'uuid': Utils.get_unique_identifier(), 'game': "Factorio"
}]) }])

19
Fill.py
View File

@@ -3,7 +3,7 @@ import typing
import collections import collections
import itertools import itertools
from BaseClasses import CollectionState, PlandoItem, Location from BaseClasses import CollectionState, PlandoItem, Location, MultiWorld
from worlds.alttp.Items import ItemFactory from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import key_drop_data from worlds.alttp.Regions import key_drop_data
@@ -12,7 +12,7 @@ class FillError(RuntimeError):
pass pass
def fill_restrictive(world, base_state: CollectionState, locations, itempool, single_player_placement=False, def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, itempool, single_player_placement=False,
lock=False): lock=False):
def sweep_from_pool(): def sweep_from_pool():
new_state = base_state.copy() new_state = base_state.copy()
@@ -68,7 +68,7 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
itempool.extend(unplaced_items) itempool.extend(unplaced_items)
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None): def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_locations=None):
# If not passed in, then get a shuffled list of locations to fill in # If not passed in, then get a shuffled list of locations to fill in
if not fill_locations: if not fill_locations:
fill_locations = world.get_unfilled_locations() fill_locations = world.get_unfilled_locations()
@@ -93,7 +93,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
# fill in gtower locations with trash first # fill in gtower locations with trash first
for player in world.alttp_player_ids: for player in world.alttp_player_ids:
if not gftower_trash or not world.ganonstower_vanilla[player] or \ if not gftower_trash or not world.ganonstower_vanilla[player] or \
world.logic[player] in {'owglitches', "nologic"}: world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
gtower_trash_count = 0 gtower_trash_count = 0
elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1): elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1):
gtower_trash_count = world.random.randint(world.crystals_needed_for_gt[player] * 2, gtower_trash_count = world.random.randint(world.crystals_needed_for_gt[player] * 2,
@@ -167,14 +167,14 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
logging.warning(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}') logging.warning(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
def fast_fill(world, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]: def fast_fill(world: MultiWorld, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]:
placing = min(len(item_pool), len(fill_locations)) placing = min(len(item_pool), len(fill_locations))
for item, location in zip(item_pool, fill_locations): for item, location in zip(item_pool, fill_locations):
world.push_item(location, item, False) world.push_item(location, item, False)
return item_pool[placing:], fill_locations[placing:] return item_pool[placing:], fill_locations[placing:]
def flood_items(world): def flood_items(world: MultiWorld):
# get items to distribute # get items to distribute
world.random.shuffle(world.itempool) world.random.shuffle(world.itempool)
itempool = world.itempool itempool = world.itempool
@@ -234,7 +234,7 @@ def flood_items(world):
break break
def balance_multiworld_progression(world): def balance_multiworld_progression(world: MultiWorld):
balanceable_players = {player for player in range(1, world.players + 1) if world.progression_balancing[player]} balanceable_players = {player for player in range(1, world.players + 1) if world.progression_balancing[player]}
if not balanceable_players: if not balanceable_players:
logging.info('Skipping multiworld progression balancing.') logging.info('Skipping multiworld progression balancing.')
@@ -363,10 +363,11 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked=
location_1.event, location_2.event = location_2.event, location_1.event location_1.event, location_2.event = location_2.event, location_1.event
def distribute_planned(world): def distribute_planned(world: MultiWorld):
world_name_lookup = world.world_name_lookup world_name_lookup = world.world_name_lookup
for player in world.player_ids: for player in world.player_ids:
try:
placement: PlandoItem placement: PlandoItem
for placement in world.plando_items[player]: for placement in world.plando_items[player]:
if placement.location in key_drop_data: if placement.location in key_drop_data:
@@ -433,3 +434,5 @@ def distribute_planned(world):
world.itempool.remove(item) world.itempool.remove(item)
except ValueError: except ValueError:
placement.warn(f"Could not remove {item} from pool as it's already missing from it.") placement.warn(f"Could not remove {item} from pool as it's already missing from it.")
except Exception as e:
raise Exception(f"Error running plando for player {player} ({world.player_names[player]})") from e

2
Gui.py
View File

@@ -468,7 +468,7 @@ def guiMain(args=None):
if shopWitchShuffleVar.get(): if shopWitchShuffleVar.get():
guiargs.shop_shuffle += "w" guiargs.shop_shuffle += "w"
if shopPoolShuffleVar.get(): if shopPoolShuffleVar.get():
guiargs.shop_shuffle_slots = 30 guiargs.shop_item_slots = 30
guiargs.shuffle_prizes = {"none": "", guiargs.shuffle_prizes = {"none": "",
"bonk": "b", "bonk": "b",
"general": "g", "general": "g",

View File

@@ -18,7 +18,7 @@ class AdjusterWorld(object):
def __init__(self, sprite_pool): def __init__(self, sprite_pool):
import random import random
self.sprite_pool = {1: sprite_pool} self.sprite_pool = {1: sprite_pool}
self.rom_seeds = {1: random} self.slot_seeds = {1: random}
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):

View File

@@ -97,7 +97,7 @@ class Context(CommonContext):
self.auth = self.rom self.auth = self.rom
auth = base64.b64encode(self.rom).decode() auth = base64.b64encode(self.rom).decode()
await self.send_msgs([{"cmd": 'Connect', await self.send_msgs([{"cmd": 'Connect',
'password': self.password, 'name': auth, 'version': Utils._version_tuple, 'password': self.password, 'name': auth, 'version': Utils.version_tuple,
'tags': get_tags(self), 'tags': get_tags(self),
'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past" 'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past"
}]) }])
@@ -138,8 +138,6 @@ SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes
location_shop_order = [name for name, info in
Shops.shop_table.items()] # probably don't leave this here. This relies on python 3.6+ dictionary keys having defined order
location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()]) location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()])
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10), location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
@@ -704,9 +702,6 @@ def get_tags(ctx: Context):
return tags return tags
async def track_locations(ctx: Context, roomid, roomdata): async def track_locations(ctx: Context, roomid, roomdata):
new_locations = [] new_locations = []
@@ -718,7 +713,7 @@ async def track_locations(ctx: Context, roomid, roomdata):
try: try:
if roomid in location_shop_ids: if roomid in location_shop_ids:
misc_data = await snes_read(ctx, SHOP_ADDR, (len(location_shop_order) * 3) + 5) misc_data = await snes_read(ctx, SHOP_ADDR, (len(Shops.shop_table) * 3) + 5)
for cnt, b in enumerate(misc_data): for cnt, b in enumerate(misc_data):
if int(b) and (Shops.SHOP_ID_START + cnt) not in ctx.locations_checked: if int(b) and (Shops.SHOP_ID_START + cnt) not in ctx.locations_checked:
new_check(Shops.SHOP_ID_START + cnt) new_check(Shops.SHOP_ID_START + cnt)

107
Main.py
View File

@@ -1,4 +1,3 @@
import copy
from itertools import zip_longest from itertools import zip_longest
import logging import logging
import os import os
@@ -21,15 +20,13 @@ from worlds.alttp.Dungeons import create_dungeons, fill_dungeons, fill_dungeons_
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple from Utils import output_path, parse_player_names, get_options, __version__, version_tuple
from worlds.hk import gen_hollow from worlds.hk import gen_hollow
from worlds.hk import create_regions as hk_create_regions from worlds.hk import create_regions as hk_create_regions
from worlds.factorio import gen_factorio, factorio_create_regions
from worlds.factorio.Mod import generate_mod
from worlds.minecraft import gen_minecraft, fill_minecraft_slot_data, generate_mc_data from worlds.minecraft import gen_minecraft, fill_minecraft_slot_data, generate_mc_data
from worlds.minecraft.Regions import minecraft_create_regions from worlds.minecraft.Regions import minecraft_create_regions
from worlds.generic.Rules import locality_rules from worlds.generic.Rules import locality_rules
from worlds import Games, lookup_any_item_name_to_id from worlds import Games, lookup_any_item_name_to_id, AutoWorld
import Patch import Patch
seeddigits = 20 seeddigits = 20
@@ -94,12 +91,6 @@ def main(args, seed=None):
world.compassshuffle = args.compassshuffle.copy() world.compassshuffle = args.compassshuffle.copy()
world.keyshuffle = args.keyshuffle.copy() world.keyshuffle = args.keyshuffle.copy()
world.bigkeyshuffle = args.bigkeyshuffle.copy() world.bigkeyshuffle = args.bigkeyshuffle.copy()
world.crystals_needed_for_ganon = {
player: world.random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(
args.crystals_ganon[player]) for player in range(1, world.players + 1)}
world.crystals_needed_for_gt = {
player: world.random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player])
for player in range(1, world.players + 1)}
world.open_pyramid = args.open_pyramid.copy() world.open_pyramid = args.open_pyramid.copy()
world.boss_shuffle = args.shufflebosses.copy() world.boss_shuffle = args.shufflebosses.copy()
world.enemy_shuffle = args.enemy_shuffle.copy() world.enemy_shuffle = args.enemy_shuffle.copy()
@@ -121,7 +112,6 @@ def main(args, seed=None):
world.triforce_pieces_available = args.triforce_pieces_available.copy() world.triforce_pieces_available = args.triforce_pieces_available.copy()
world.triforce_pieces_required = args.triforce_pieces_required.copy() world.triforce_pieces_required = args.triforce_pieces_required.copy()
world.shop_shuffle = args.shop_shuffle.copy() world.shop_shuffle = args.shop_shuffle.copy()
world.shop_shuffle_slots = args.shop_shuffle_slots.copy()
world.progression_balancing = args.progression_balancing.copy() world.progression_balancing = args.progression_balancing.copy()
world.shuffle_prizes = args.shuffle_prizes.copy() world.shuffle_prizes = args.shuffle_prizes.copy()
world.sprite_pool = args.sprite_pool.copy() world.sprite_pool = args.sprite_pool.copy()
@@ -133,16 +123,11 @@ def main(args, seed=None):
world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy() world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy()
world.required_medallions = args.required_medallions.copy() world.required_medallions = args.required_medallions.copy()
world.game = args.game.copy() world.game = args.game.copy()
import Options world.set_options(args)
for hk_option in Options.hollow_knight_options:
setattr(world, hk_option, getattr(args, hk_option, {}))
for factorio_option in Options.factorio_options:
setattr(world, factorio_option, getattr(args, factorio_option, {}))
for minecraft_option in Options.minecraft_options:
setattr(world, minecraft_option, getattr(args, minecraft_option, {}))
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)} world.slot_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in
range(1, world.players + 1)}
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64)) world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
@@ -154,7 +139,8 @@ def main(args, seed=None):
world.er_seeds[player] = "vanilla" world.er_seeds[player] = "vanilla"
elif seed.startswith("group-") or args.race: elif seed.startswith("group-") or args.race:
# renamed from team to group to not confuse with existing team name use # renamed from team to group to not confuse with existing team name use
world.er_seeds[player] = get_same_seed(world, (shuffle, seed, world.retro[player], world.mode[player], world.logic[player])) world.er_seeds[player] = get_same_seed(world, (
shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
else: # not a race or group seed, use set seed as is. else: # not a race or group seed, use set seed as is.
world.er_seeds[player] = seed world.er_seeds[player] = seed
elif world.shuffle[player] == "vanilla": elif world.shuffle[player] == "vanilla":
@@ -162,6 +148,10 @@ def main(args, seed=None):
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed) logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
logger.info("Found World Types:")
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
logger.info(f" {name:30} {cls}")
parsed_names = parse_player_names(args.names, world.players, args.teams) parsed_names = parse_player_names(args.names, world.players, args.teams)
world.teams = len(parsed_names) world.teams = len(parsed_names)
for i, team in enumerate(parsed_names, 1): for i, team in enumerate(parsed_names, 1):
@@ -209,23 +199,26 @@ def main(args, seed=None):
for player in world.hk_player_ids: for player in world.hk_player_ids:
hk_create_regions(world, player) hk_create_regions(world, player)
for player in world.factorio_player_ids: AutoWorld.call_all(world, "create_regions")
factorio_create_regions(world, player)
for player in world.minecraft_player_ids: for player in world.minecraft_player_ids:
minecraft_create_regions(world, player) minecraft_create_regions(world, player)
for player in world.alttp_player_ids: for player in world.alttp_player_ids:
if world.open_pyramid[player] == 'goal': if world.open_pyramid[player] == 'goal':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
'localganontriforcehunt', 'ganonpedestal'}
elif world.open_pyramid[player] == 'auto': elif world.open_pyramid[player] == 'auto':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} and \ world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not world.shuffle_ganon) 'localganontriforcehunt', 'ganonpedestal'} and \
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull',
'dungeonscrossed'} or not world.shuffle_ganon)
else: else:
world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(world.open_pyramid[player], world.open_pyramid[player]) world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(
world.open_pyramid[player], 'auto')
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player],
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player]) world.triforce_pieces_required[player])
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
create_regions(world, player) create_regions(world, player)
@@ -265,14 +258,15 @@ def main(args, seed=None):
for player in world.player_ids: for player in world.player_ids:
locality_rules(world, player) locality_rules(world, player)
AutoWorld.call_all(world, "set_rules")
for player in world.alttp_player_ids: for player in world.alttp_player_ids:
set_rules(world, player) set_rules(world, player)
for player in world.hk_player_ids: for player in world.hk_player_ids:
gen_hollow(world, player) gen_hollow(world, player)
for player in world.factorio_player_ids: AutoWorld.call_all(world, "generate_basic")
gen_factorio(world, player)
for player in world.minecraft_player_ids: for player in world.minecraft_player_ids:
gen_minecraft(world, player) gen_minecraft(world, player)
@@ -419,12 +413,12 @@ def main(args, seed=None):
check_accessibility_task = pool.submit(world.fulfills_accessibility) check_accessibility_task = pool.submit(world.fulfills_accessibility)
rom_futures = [] rom_futures = []
mod_futures = [] output_file_futures = []
for team in range(world.teams): for team in range(world.teams):
for player in world.alttp_player_ids: for player in world.alttp_player_ids:
rom_futures.append(pool.submit(_gen_rom, team, player)) rom_futures.append(pool.submit(_gen_rom, team, player))
for player in world.factorio_player_ids: for player in world.player_ids:
mod_futures.append(pool.submit(generate_mod, world, player)) output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player))
def get_entrance_to_region(region: Region): def get_entrance_to_region(region: Region):
for entrance in region.entrances: for entrance in region.entrances:
@@ -434,7 +428,8 @@ def main(args, seed=None):
return get_entrance_to_region(entrance.parent_region) return get_entrance_to_region(entrance.parent_region)
# collect ER hint info # collect ER hint info
er_hint_data = {player: {} for player in range(1, world.players + 1) if world.shuffle[player] != "vanilla" or world.retro[player]} er_hint_data = {player: {} for player in range(1, world.players + 1) if
world.shuffle[player] != "vanilla" or world.retro[player]}
from worlds.alttp.Regions import RegionType from worlds.alttp.Regions import RegionType
for region in world.regions: for region in world.regions:
if region.player in er_hint_data and region.locations: if region.player in er_hint_data and region.locations:
@@ -472,8 +467,10 @@ def main(args, seed=None):
oldmancaves = [] oldmancaves = []
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"] takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
for index, take_any in enumerate(takeanyregions): for index, take_any in enumerate(takeanyregions):
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if world.retro[player]]: for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if
item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], region.player) world.retro[player]]:
item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
region.player)
player = region.player player = region.player
location_id = SHOP_ID_START + total_shop_slots + index location_id = SHOP_ID_START + total_shop_slots + index
@@ -487,11 +484,9 @@ def main(args, seed=None):
er_hint_data[player][location_id] = main_entrance.name er_hint_data[player][location_id] = main_entrance.name
oldmancaves.append(((location_id, player), (item.code, player))) oldmancaves.append(((location_id, player), (item.code, player)))
FillDisabledShopSlots(world) FillDisabledShopSlots(world)
def write_multidata(roms, mods): def write_multidata(roms, outputs):
import base64 import base64
import NetUtils import NetUtils
for future in roms: for future in roms:
@@ -513,16 +508,18 @@ def main(args, seed=None):
for item in world.precollected_items: for item in world.precollected_items:
precollected_items[item.player].append(item.code) precollected_items[item.player].append(item.code)
precollected_hints = {player: set() for player in range(1, world.players + 1)} precollected_hints = {player: set() for player in range(1, world.players + 1)}
# for now special case Factorio visibility # for now special case Factorio tech_tree_information
sending_visible_players = set() sending_visible_players = set()
for player in world.factorio_player_ids: for player in world.factorio_player_ids:
if world.visibility[player]: if world.tech_tree_information[player].value == 2:
sending_visible_players.add(player) sending_visible_players.add(player)
for i, team in enumerate(parsed_names): for i, team in enumerate(parsed_names):
for player, name in enumerate(team, 1): for player, name in enumerate(team, 1):
if player not in world.alttp_player_ids: if player not in world.alttp_player_ids:
connect_names[name] = (i, player) connect_names[name] = (i, player)
if world.hk_player_ids:
import Options
for slot in world.hk_player_ids: for slot in world.hk_player_ids:
slots_data = slot_data[slot] = {} slots_data = slot_data[slot] = {}
for option_name in Options.hollow_knight_options: for option_name in Options.hollow_knight_options:
@@ -560,7 +557,7 @@ def main(args, seed=None):
"er_hint_data": er_hint_data, "er_hint_data": er_hint_data,
"precollected_items": precollected_items, "precollected_items": precollected_items,
"precollected_hints": precollected_hints, "precollected_hints": precollected_hints,
"version": tuple(_version_tuple), "version": tuple(version_tuple),
"tags": ["AP"], "tags": ["AP"],
"minimum_versions": minimum_versions, "minimum_versions": minimum_versions,
"seed_name": world.seed_name "seed_name": world.seed_name
@@ -569,10 +566,10 @@ def main(args, seed=None):
with open(output_path('%s.archipelago' % outfilebase), 'wb') as f: with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
f.write(bytes([1])) # version of format f.write(bytes([1])) # version of format
f.write(multidata) f.write(multidata)
for future in mods: for future in outputs:
future.result() # collect errors if they occured future.result() # collect errors if they occured
multidata_task = pool.submit(write_multidata, rom_futures, mod_futures) multidata_task = pool.submit(write_multidata, rom_futures, output_file_futures)
if not check_accessibility_task.result(): if not check_accessibility_task.result():
if not world.can_beat_game(): if not world.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.") raise Exception("Game appears as unbeatable. Aborting.")
@@ -636,7 +633,8 @@ def create_playthrough(world):
to_delete = set() to_delete = set()
for location in sphere: for location in sphere:
# we remove the item at location and check if game is still beatable # we remove the item at location and check if game is still beatable
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
location.item.player)
old_item = location.item old_item = location.item
location.item = None location.item = None
if world.can_beat_game(state_cache[num]): if world.can_beat_game(state_cache[num]):
@@ -681,7 +679,8 @@ def create_playthrough(world):
collection_spheres.append(sphere) collection_spheres.append(sphere)
logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(required_locations)) logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres),
len(sphere), len(required_locations))
if not sphere: if not sphere:
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}') raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
@@ -700,14 +699,22 @@ def create_playthrough(world):
world.spoiler.paths = dict() world.spoiler.paths = dict()
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
world.spoiler.paths.update({ str(location) : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player}) world.spoiler.paths.update(
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
sphere if location.player == player})
if player in world.alttp_player_ids: if player in world.alttp_player_ids:
for path in dict(world.spoiler.paths).values(): for path in dict(world.spoiler.paths).values():
if any(exit == 'Pyramid Fairy' for (_, exit) in path): if any(exit == 'Pyramid Fairy' for (_, exit) in path):
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player)) world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state,
world.get_region(
'Big Bomb Shop',
player))
else: else:
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player)) world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state,
world.get_region(
'Inverted Big Bomb Shop',
player))
# we can finally output our playthrough # we can finally output our playthrough
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])} world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}

View File

@@ -75,7 +75,7 @@ if __name__ == "__main__":
if os.path.exists("ArchipelagoMystery.exe"): if os.path.exists("ArchipelagoMystery.exe"):
basemysterycommand = "ArchipelagoMystery.exe" # compiled windows basemysterycommand = "ArchipelagoMystery.exe" # compiled windows
elif os.path.exists("ArchipelagoMystery"): elif os.path.exists("ArchipelagoMystery"):
basemysterycommand = "ArchipelagoMystery" # compiled linux basemysterycommand = "./ArchipelagoMystery" # compiled linux
else: else:
basemysterycommand = f"py -{py_version} Mystery.py" # source basemysterycommand = f"py -{py_version} Mystery.py" # source
@@ -207,14 +207,13 @@ if __name__ == "__main__":
if not args.disable_autohost: if not args.disable_autohost:
if os.path.exists(os.path.join(output_path, multidataname)): if os.path.exists(os.path.join(output_path, multidataname)):
if os.path.exists("ArchipelagoServer.exe"): if os.path.exists("ArchipelagoServer.exe"):
baseservercommand = "ArchipelagoServer.exe" # compiled windows baseservercommand = ["ArchipelagoServer.exe"] # compiled windows
elif os.path.exists("ArchipelagoServer"): elif os.path.exists("ArchipelagoServer"):
baseservercommand = "ArchipelagoServer" # compiled linux baseservercommand = ["./ArchipelagoServer"] # compiled linux
else: else:
baseservercommand = f"py -{py_version} MultiServer.py" # source baseservercommand = ["py", f"-{py_version}", "MultiServer.py"] # source
# don't have a mac to test that. If you try to run compiled on mac, good luck. # don't have a mac to test that. If you try to run compiled on mac, good luck.
subprocess.call(baseservercommand + ["--multidata", os.path.join(output_path, multidataname)])
subprocess.call(f"{baseservercommand} --multidata {os.path.join(output_path, multidataname)}")
except: except:
import traceback import traceback

View File

@@ -30,7 +30,7 @@ from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_
lookup_any_location_id_to_name, lookup_any_location_name_to_id lookup_any_location_id_to_name, lookup_any_location_name_to_id
import Utils import Utils
from Utils import get_item_name_from_id, get_location_name_from_id, \ from Utils import get_item_name_from_id, get_location_name_from_id, \
_version_tuple, restricted_loads, Version version_tuple, restricted_loads, Version
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode, NetworkPlayer from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode, NetworkPlayer
colorama.init() colorama.init()
@@ -39,6 +39,7 @@ all_items = frozenset(lookup_any_item_name_to_id)
all_locations = frozenset(lookup_any_location_name_to_id) all_locations = frozenset(lookup_any_location_name_to_id)
all_console_names = frozenset(all_items | all_locations) all_console_names = frozenset(all_items | all_locations)
class Client(Endpoint): class Client(Endpoint):
version = Version(0, 0, 0) version = Version(0, 0, 0)
tags: typing.List[str] = [] tags: typing.List[str] = []
@@ -136,9 +137,9 @@ class Context(Node):
def _load(self, decoded_obj: dict, use_embedded_server_options: bool): def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
mdata_ver = decoded_obj["minimum_versions"]["server"] mdata_ver = decoded_obj["minimum_versions"]["server"]
if mdata_ver > Utils._version_tuple: if mdata_ver > Utils.version_tuple:
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}," raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
f"however this server is of version {Utils._version_tuple}") f"however this server is of version {Utils.version_tuple}")
clients_ver = decoded_obj["minimum_versions"].get("clients", {}) clients_ver = decoded_obj["minimum_versions"].get("clients", {})
self.minimum_client_versions = {} self.minimum_client_versions = {}
for player, version in clients_ver.items(): for player, version in clients_ver.items():
@@ -166,7 +167,6 @@ class Context(Node):
server_options = decoded_obj.get("server_options", {}) server_options = decoded_obj.get("server_options", {})
self._set_options(server_options) self._set_options(server_options)
def get_players_package(self): def get_players_package(self):
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()] return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
@@ -366,6 +366,7 @@ async def server(websocket, path, ctx: Context):
if not isinstance(e, websockets.WebSocketException): if not isinstance(e, websockets.WebSocketException):
logging.exception(e) logging.exception(e)
finally: finally:
if ctx.log_network:
logging.info("Disconnected") logging.info("Disconnected")
await ctx.disconnect(client) await ctx.disconnect(client)
@@ -374,12 +375,14 @@ async def on_client_connected(ctx: Context, client: Client):
await ctx.send_msgs(client, [{ await ctx.send_msgs(client, [{
'cmd': 'RoomInfo', 'cmd': 'RoomInfo',
'password': ctx.password is not None, 'password': ctx.password is not None,
'players': [NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name), client.name) for client 'players': [
NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name),
client.name) for client
in ctx.endpoints if client.auth], in ctx.endpoints if client.auth],
# tags are for additional features in the communication. # tags are for additional features in the communication.
# Name them by feature or fork, as you feel is appropriate. # Name them by feature or fork, as you feel is appropriate.
'tags': ctx.tags, 'tags': ctx.tags,
'version': Utils._version_tuple, 'version': Utils.version_tuple,
'forfeit_mode': ctx.forfeit_mode, 'forfeit_mode': ctx.forfeit_mode,
'remaining_mode': ctx.remaining_mode, 'remaining_mode': ctx.remaining_mode,
'hint_cost': ctx.hint_cost, 'hint_cost': ctx.hint_cost,
@@ -403,9 +406,11 @@ async def on_client_joined(ctx: Context, client: Client):
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
async def on_client_left(ctx: Context, client: Client): async def on_client_left(ctx: Context, client: Client):
update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN) update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN)
ctx.notify_all("%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1)) ctx.notify_all(
"%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1))
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
@@ -504,7 +509,6 @@ def notify_team(ctx: Context, team: int, text: str):
ctx.broadcast_team(team, [['Print', {"text": text}]]) ctx.broadcast_team(team, [['Print', {"text": text}]])
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]: def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]:
hints = [] hints = []
seeked_item_id = lookup_any_item_name_to_id[item] seeked_item_id = lookup_any_item_name_to_id[item]
@@ -520,7 +524,6 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[
def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]: def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
seeked_location: int = Regions.lookup_name_to_id[location] seeked_location: int = Regions.lookup_name_to_id[location]
item_id, receiving_player = ctx.locations[slot].get(seeked_location, (None, None)) item_id, receiving_player = ctx.locations[slot].get(seeked_location, (None, None))
if item_id: if item_id:
@@ -540,6 +543,7 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
text += f" at {hint.entrance}" text += f" at {hint.entrance}"
return text + (". (found)" if hint.found else ".") return text + (". (found)" if hint.found else ".")
def json_format_send_event(net_item: NetworkItem, receiving_player: int): def json_format_send_event(net_item: NetworkItem, receiving_player: int):
parts = [] parts = []
NetUtils.add_json_text(parts, net_item.player, type=NetUtils.JSONTypes.player_id) NetUtils.add_json_text(parts, net_item.player, type=NetUtils.JSONTypes.player_id)
@@ -559,7 +563,9 @@ def json_format_send_event(net_item: NetworkItem, receiving_player: int):
return {"cmd": "PrintJSON", "data": parts, "type": "ItemSend", return {"cmd": "PrintJSON", "data": parts, "type": "ItemSend",
"receiving": receiving_player, "sending": net_item.player} "receiving": receiving_player, "sending": net_item.player}
def get_intended_text(input_text: str, possible_answers: typing.Iterable[str]= all_console_names) -> typing.Tuple[str, bool, str]:
def get_intended_text(input_text: str, possible_answers: typing.Iterable[str] = all_console_names) -> typing.Tuple[
str, bool, str]:
picks = fuzzy_process.extract(input_text, possible_answers, limit=2) picks = fuzzy_process.extract(input_text, possible_answers, limit=2)
if len(picks) > 1: if len(picks) > 1:
dif = picks[0][1] - picks[1][1] dif = picks[0][1] - picks[1][1]
@@ -689,6 +695,7 @@ class CommonCommandProcessor(CommandProcessor):
else: else:
self.output(f"Option {option} is set to {getattr(self.ctx, option)}") self.output(f"Option {option} is set to {getattr(self.ctx, option)}")
class ClientMessageProcessor(CommonCommandProcessor): class ClientMessageProcessor(CommonCommandProcessor):
marker = "!" marker = "!"
@@ -715,11 +722,14 @@ class ClientMessageProcessor(CommonCommandProcessor):
"""Allow remote administration of the multiworld server""" """Allow remote administration of the multiworld server"""
output = f"!admin {command}" output = f"!admin {command}"
if output.lower().startswith("!admin login"): # disallow others from seeing the supplied password, whether or not it is correct. if output.lower().startswith(
"!admin login"): # disallow others from seeing the supplied password, whether or not it is correct.
output = f"!admin login {('*' * random.randint(4, 16))}" output = f"!admin login {('*' * random.randint(4, 16))}"
elif output.lower().startswith("!admin /option server_password"): # disallow others from knowing what the new remote administration password is. elif output.lower().startswith(
"!admin /option server_password"): # disallow others from knowing what the new remote administration password is.
output = f"!admin /option server_password {('*' * random.randint(4, 16))}" output = f"!admin /option server_password {('*' * random.randint(4, 16))}"
self.ctx.notify_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + output) # Otherwise notify the others what is happening. self.ctx.notify_all(self.ctx.get_aliased_name(self.client.team,
self.client.slot) + ': ' + output) # Otherwise notify the others what is happening.
if not self.ctx.server_password: if not self.ctx.server_password:
self.output("Sorry, Remote administration is disabled") self.output("Sorry, Remote administration is disabled")
@@ -727,7 +737,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
if not command: if not command:
if self.is_authenticated(): if self.is_authenticated():
self.output("Usage: !admin [Server command].\nUse !admin /help for help.\nUse !admin logout to log out of the current session.") self.output(
"Usage: !admin [Server command].\nUse !admin /help for help.\nUse !admin logout to log out of the current session.")
else: else:
self.output("Usage: !admin login [password]") self.output("Usage: !admin login [password]")
return True return True
@@ -810,7 +821,6 @@ class ClientMessageProcessor(CommonCommandProcessor):
"Sorry, !remaining requires you to have beaten the game on this server") "Sorry, !remaining requires you to have beaten the game on this server")
return False return False
def _cmd_missing(self) -> bool: def _cmd_missing(self) -> bool:
"""List all missing location checks from the server's perspective""" """List all missing location checks from the server's perspective"""
@@ -850,7 +860,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
if usable: if usable:
new_item = NetworkItem(Items.item_table[item_name][2], -1, self.client.slot) new_item = NetworkItem(Items.item_table[item_name][2], -1, self.client.slot)
get_received_items(self.ctx, self.client.team, self.client.slot).append(new_item) get_received_items(self.ctx, self.client.team, self.client.slot).append(new_item)
self.ctx.notify_all('Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team, self.client.slot)) self.ctx.notify_all(
'Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team,
self.client.slot))
send_new_items(self.ctx) send_new_items(self.ctx)
return True return True
else: else:
@@ -1011,7 +1023,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
errors.add('IncompatibleVersion') errors.add('IncompatibleVersion')
# only exact version match allowed # only exact version match allowed
if ctx.compatibility == 0 and args['version'] != _version_tuple: if ctx.compatibility == 0 and args['version'] != version_tuple:
errors.add('IncompatibleVersion') errors.add('IncompatibleVersion')
if errors: if errors:
logging.info(f"A client connection was refused due to: {errors}") logging.info(f"A client connection was refused due to: {errors}")
@@ -1072,6 +1084,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
client.messageprocessor(args["text"]) client.messageprocessor(args["text"])
def update_client_status(ctx: Context, client: Client, new_status: ClientStatus): def update_client_status(ctx: Context, client: Client, new_status: ClientStatus):
current = ctx.client_game_state[client.team, client.slot] current = ctx.client_game_state[client.team, client.slot]
if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion
@@ -1083,6 +1096,7 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus)
ctx.client_game_state[client.team, client.slot] = new_status ctx.client_game_state[client.team, client.slot] = new_status
class ServerCommandProcessor(CommonCommandProcessor): class ServerCommandProcessor(CommonCommandProcessor):
def __init__(self, ctx: Context): def __init__(self, ctx: Context):
self.ctx = ctx self.ctx = ctx
@@ -1190,7 +1204,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
for (team, slot), name in self.ctx.player_names.items(): for (team, slot), name in self.ctx.player_names.items():
if name.lower() == seeked_player: if name.lower() == seeked_player:
self.ctx.allow_forfeits[(team, slot)] = False self.ctx.allow_forfeits[(team, slot)] = False
self.output(f"Player {player_name} has to follow the server restrictions on use of the !forfeit command.") self.output(
f"Player {player_name} has to follow the server restrictions on use of the !forfeit command.")
return True return True
self.output(f"Could not find player {player_name} to forbid the !forfeit command for.") self.output(f"Could not find player {player_name} to forbid the !forfeit command for.")
@@ -1270,6 +1285,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
f"{', '.join(known)}") f"{', '.join(known)}")
return False return False
async def console(ctx: Context): async def console(ctx: Context):
session = prompt_toolkit.PromptSession() session = prompt_toolkit.PromptSession()
while ctx.running: while ctx.running:

View File

@@ -13,7 +13,7 @@ from worlds.generic import PlandoItem, PlandoConnection
ModuleUpdate.update() ModuleUpdate.update()
from Utils import parse_yaml from Utils import parse_yaml, version_tuple, __version__, tuplize_version
from worlds.alttp.EntranceRandomizer import parse_arguments from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain from Main import main as ERmain
from Main import get_seed, seeddigits from Main import get_seed, seeddigits
@@ -23,7 +23,9 @@ from worlds.alttp.Items import item_name_groups, item_table
from worlds.alttp import Bosses from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable from worlds.alttp.Text import TextTable
from worlds.alttp.Regions import location_table, key_drop_data from worlds.alttp.Regions import location_table, key_drop_data
from worlds.AutoWorld import AutoWorldRegister
categories = set(AutoWorldRegister.world_types)
def mystery_argparse(): def mystery_argparse():
parser = argparse.ArgumentParser(add_help=False) parser = argparse.ArgumentParser(add_help=False)
@@ -61,9 +63,11 @@ def mystery_argparse():
args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")} args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
return args return args
def get_seed_name(random): def get_seed_name(random):
return f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits) return f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
def main(args=None, callback=ERmain): def main(args=None, callback=ERmain):
if not args: if not args:
args = mystery_argparse() args = mystery_argparse()
@@ -79,14 +83,14 @@ def main(args=None, callback=ERmain):
weights_cache = {} weights_cache = {}
if args.weights: if args.weights:
try: try:
weights_cache[args.weights] = get_weights(args.weights) weights_cache[args.weights] = read_weights_yaml(args.weights)
except Exception as e: except Exception as e:
raise ValueError(f"File {args.weights} is destroyed. Please fix your yaml.") from e raise ValueError(f"File {args.weights} is destroyed. Please fix your yaml.") from e
print(f"Weights: {args.weights} >> " print(f"Weights: {args.weights} >> "
f"{get_choice('description', weights_cache[args.weights], 'No description specified')}") f"{get_choice('description', weights_cache[args.weights], 'No description specified')}")
if args.meta: if args.meta:
try: try:
weights_cache[args.meta] = get_weights(args.meta) weights_cache[args.meta] = read_weights_yaml(args.meta)
except Exception as e: except Exception as e:
raise ValueError(f"File {args.meta} is destroyed. Please fix your yaml.") from e raise ValueError(f"File {args.meta} is destroyed. Please fix your yaml.") from e
meta_weights = weights_cache[args.meta] meta_weights = weights_cache[args.meta]
@@ -99,7 +103,7 @@ def main(args=None, callback=ERmain):
if path: if path:
try: try:
if path not in weights_cache: if path not in weights_cache:
weights_cache[path] = get_weights(path) weights_cache[path] = read_weights_yaml(path)
print(f"P{player} Weights: {path} >> " print(f"P{player} Weights: {path} >> "
f"{get_choice('description', weights_cache[path], 'No description specified')}") f"{get_choice('description', weights_cache[path], 'No description specified')}")
@@ -254,7 +258,7 @@ def main(args=None, callback=ERmain):
callback(erargs, seed) callback(erargs, seed)
def get_weights(path): def read_weights_yaml(path):
try: try:
if urllib.parse.urlparse(path).scheme: if urllib.parse.urlparse(path).scheme:
yaml = str(urllib.request.urlopen(path).read(), "utf-8") yaml = str(urllib.request.urlopen(path).read(), "utf-8")
@@ -342,19 +346,6 @@ goals = {
'ice_rod_hunt': 'icerodhunt', 'ice_rod_hunt': 'icerodhunt',
} }
# remove sometime before 1.0.0, warn before
legacy_boss_shuffle_options = {
# legacy, will go away:
'simple': 'basic',
'random': 'full',
'normal': 'full'
}
legacy_goals = {
'dungeons': 'bosses',
'fast_ganon': 'crystals',
}
def roll_percentage(percentage: typing.Union[int, float]) -> bool: def roll_percentage(percentage: typing.Union[int, float]) -> bool:
"""Roll a percentage chance. """Roll a percentage chance.
@@ -382,13 +373,12 @@ def roll_linked_options(weights: dict) -> dict:
try: try:
if roll_percentage(option_set["percentage"]): if roll_percentage(option_set["percentage"]):
logging.debug(f"Linked option {option_set['name']} triggered.") logging.debug(f"Linked option {option_set['name']} triggered.")
if "options" in option_set: new_options = option_set["options"]
weights = update_weights(weights, option_set["options"], "Linked", option_set["name"]) for category_name, category_options in new_options.items():
if "rom_options" in option_set: currently_targeted_weights = weights
rom_weights = weights.get("rom", dict()) if category_name:
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Linked Rom", currently_targeted_weights = currently_targeted_weights[category_name]
option_set["name"]) update_weights(currently_targeted_weights, category_options, "Linked", option_set["name"])
weights["rom"] = rom_weights
else: else:
logging.debug(f"linked option {option_set['name']} skipped.") logging.debug(f"linked option {option_set['name']} skipped.")
except Exception as e: except Exception as e:
@@ -402,23 +392,25 @@ def roll_triggers(weights: dict) -> dict:
weights["_Generator_Version"] = "Archipelago" # Some means for triggers to know if the seed is on main or doors. weights["_Generator_Version"] = "Archipelago" # Some means for triggers to know if the seed is on main or doors.
for i, option_set in enumerate(weights["triggers"]): for i, option_set in enumerate(weights["triggers"]):
try: try:
currently_targeted_weights = weights
category = option_set.get("option_category", None)
if category:
currently_targeted_weights = currently_targeted_weights[category]
key = get_choice("option_name", option_set) key = get_choice("option_name", option_set)
if key not in weights: if key not in currently_targeted_weights:
logging.warning(f'Specified option name {option_set["option_name"]} did not ' logging.warning(f'Specified option name {option_set["option_name"]} did not '
f'match with a root option. ' f'match with a root option. '
f'This is probably in error.') f'This is probably in error.')
trigger_result = get_choice("option_result", option_set) trigger_result = get_choice("option_result", option_set)
result = get_choice(key, weights) result = get_choice(key, currently_targeted_weights)
currently_targeted_weights[key] = result
if result == trigger_result and roll_percentage(get_choice("percentage", option_set, 100)): if result == trigger_result and roll_percentage(get_choice("percentage", option_set, 100)):
if "options" in option_set: for category_name, category_options in option_set["options"].items():
weights = update_weights(weights, option_set["options"], "Triggered", option_set["option_name"]) currently_targeted_weights = weights
if category_name:
currently_targeted_weights = currently_targeted_weights[category_name]
update_weights(currently_targeted_weights, category_options, "Triggered", option_set["option_name"])
if "rom_options" in option_set:
rom_weights = weights.get("rom", dict())
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Triggered Rom",
option_set["option_name"])
weights["rom"] = rom_weights
weights[key] = result
except Exception as e: except Exception as e:
raise ValueError(f"Your trigger number {i + 1} is destroyed. " raise ValueError(f"Your trigger number {i + 1} is destroyed. "
f"Please fix your triggers.") from e f"Please fix your triggers.") from e
@@ -426,11 +418,6 @@ def roll_triggers(weights: dict) -> dict:
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str: def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
if boss_shuffle in legacy_boss_shuffle_options:
new_boss_shuffle = legacy_boss_shuffle_options[boss_shuffle]
logging.warning(f"Boss shuffle {boss_shuffle} is deprecated, "
f"please use {new_boss_shuffle} instead")
return new_boss_shuffle
if boss_shuffle in boss_shuffle_options: if boss_shuffle in boss_shuffle_options:
return boss_shuffle_options[boss_shuffle] return boss_shuffle_options[boss_shuffle]
elif "bosses" in plando_options: elif "bosses" in plando_options:
@@ -438,10 +425,6 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str
remainder_shuffle = "none" # vanilla remainder_shuffle = "none" # vanilla
bosses = [] bosses = []
for boss in options: for boss in options:
if boss in legacy_boss_shuffle_options:
remainder_shuffle = legacy_boss_shuffle_options[boss_shuffle]
logging.warning(f"Boss shuffle {boss} is deprecated, "
f"please use {remainder_shuffle} instead")
if boss in boss_shuffle_options: if boss in boss_shuffle_options:
remainder_shuffle = boss_shuffle_options[boss] remainder_shuffle = boss_shuffle_options[boss]
elif "-" in boss: elif "-" in boss:
@@ -507,14 +490,34 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
if "triggers" in weights: if "triggers" in weights:
weights = roll_triggers(weights) weights = roll_triggers(weights)
requirements = weights.get("requires", {})
if requirements:
version = requirements.get("version", __version__)
if tuplize_version(version) > version_tuple:
raise Exception(f"Settings reports required version of generator is at least {version}, "
f"however generator is of version {__version__}")
required_plando_options = requirements.get("plando", "")
if required_plando_options:
required_plando_options = set(option.strip() for option in required_plando_options.split(","))
required_plando_options -= plando_options
if required_plando_options:
if len(required_plando_options) == 1:
raise Exception(f"Settings reports required plando module {', '.join(required_plando_options)}, "
f"which is not enabled.")
else:
raise Exception(f"Settings reports required plando modules {', '.join(required_plando_options)}, "
f"which are not enabled.")
ret = argparse.Namespace() ret = argparse.Namespace()
ret.name = get_choice('name', weights) ret.name = get_choice('name', weights)
ret.accessibility = get_choice('accessibility', weights) ret.accessibility = get_choice('accessibility', weights)
ret.progression_balancing = get_choice('progression_balancing', weights, True) ret.progression_balancing = get_choice('progression_balancing', weights, True)
ret.game = get_choice("game", weights, "A Link to the Past") ret.game = get_choice("game", weights)
if ret.game not in weights:
raise Exception(f"No game options for selected game \"{ret.game}\" found.")
game_weights = weights[ret.game]
ret.local_items = set() ret.local_items = set()
for item_name in weights.get('local_items', []): for item_name in game_weights.get('local_items', []):
items = item_name_groups.get(item_name, {item_name}) items = item_name_groups.get(item_name, {item_name})
for item in items: for item in items:
if item in lookup_any_item_name_to_id: if item in lookup_any_item_name_to_id:
@@ -523,7 +526,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
raise Exception(f"Could not force item {item} to be world-local, as it was not recognized.") raise Exception(f"Could not force item {item} to be world-local, as it was not recognized.")
ret.non_local_items = set() ret.non_local_items = set()
for item_name in weights.get('non_local_items', []): for item_name in game_weights.get('non_local_items', []):
items = item_name_groups.get(item_name, {item_name}) items = item_name_groups.get(item_name, {item_name})
for item in items: for item in items:
if item in lookup_any_item_name_to_id: if item in lookup_any_item_name_to_id:
@@ -531,7 +534,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
else: else:
raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.") raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.")
inventoryweights = weights.get('startinventory', {}) inventoryweights = game_weights.get('start_inventory', {})
startitems = [] startitems = []
for item in inventoryweights.keys(): for item in inventoryweights.keys():
itemvalue = get_choice(item, inventoryweights) itemvalue = get_choice(item, inventoryweights)
@@ -541,33 +544,32 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
elif itemvalue: elif itemvalue:
startitems.append(item) startitems.append(item)
ret.startinventory = startitems ret.startinventory = startitems
ret.start_hints = set(weights.get('start_hints', [])) ret.start_hints = set(game_weights.get('start_hints', []))
if ret.game == "A Link to the Past": if ret.game == "A Link to the Past":
roll_alttp_settings(ret, weights, plando_options) roll_alttp_settings(ret, game_weights, plando_options)
elif ret.game == "Hollow Knight": elif ret.game == "Hollow Knight":
for option_name, option in Options.hollow_knight_options.items(): for option_name, option in Options.hollow_knight_options.items():
setattr(ret, option_name, option.from_any(get_choice(option_name, weights, True))) setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights, True)))
elif ret.game == "Factorio": elif ret.game == "Factorio":
for option_name, option in Options.factorio_options.items(): for option_name, option in Options.factorio_options.items():
if option_name in weights: if option_name in game_weights:
if issubclass(option, Options.OptionDict): # get_choice should probably land in the Option class if issubclass(option, Options.OptionDict): # get_choice should probably land in the Option class
setattr(ret, option_name, option.from_any(weights[option_name])) setattr(ret, option_name, option.from_any(game_weights[option_name]))
else: else:
setattr(ret, option_name, option.from_any(get_choice(option_name, weights))) setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
else: else:
setattr(ret, option_name, option(option.default)) setattr(ret, option_name, option(option.default))
elif ret.game == "Minecraft": elif ret.game == "Minecraft":
for option_name, option in Options.minecraft_options.items(): for option_name, option in Options.minecraft_options.items():
if option_name in weights: if option_name in game_weights:
setattr(ret, option_name, option.from_any(get_choice(option_name, weights))) setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
else: else:
setattr(ret, option_name, option(option.default)) setattr(ret, option_name, option(option.default))
# bad hardcoded behavior to make this work for now # bad hardcoded behavior to make this work for now
ret.plando_connections = [] ret.plando_connections = []
if "connections" in plando_options: if "connections" in plando_options:
options = weights.get("plando_connections", []) options = game_weights.get("plando_connections", [])
for placement in options: for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)): if roll_percentage(get_choice("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection( ret.plando_connections.append(PlandoConnection(
@@ -581,12 +583,18 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
for option_name, option in Options.alttp_options.items():
if option_name in weights:
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
else:
setattr(ret, option_name, option(option.default))
glitches_required = get_choice('glitches_required', weights) glitches_required = get_choice('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']: if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
logging.warning("Only NMG, OWG and No Logic supported") logging.warning("Only NMG, OWG, HMG and No Logic supported")
glitches_required = 'none' glitches_required = 'none'
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches', ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches',
'minor_glitches': 'minorglitches'}[ 'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
glitches_required] glitches_required]
ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp") ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp")
@@ -623,23 +631,15 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
goal = get_choice('goals', weights, 'ganon') goal = get_choice('goals', weights, 'ganon')
if goal in legacy_goals:
logging.warning(f"Goal {goal} is depcrecated, please use {legacy_goals[goal]} instead.")
goal = legacy_goals[goal]
ret.goal = goals[goal] ret.goal = goals[goal]
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when # TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
# fast ganon + ganon at hole # fast ganon + ganon at hole
ret.open_pyramid = get_choice('open_pyramid', weights, 'goal') ret.open_pyramid = get_choice('open_pyramid', weights, 'goal')
ret.crystals_gt = prefer_int(get_choice('tower_open', weights))
ret.crystals_ganon = prefer_int(get_choice('ganon_open', weights))
extra_pieces = get_choice('triforce_pieces_mode', weights, 'available') extra_pieces = get_choice('triforce_pieces_mode', weights, 'available')
ret.triforce_pieces_required = int(get_choice('triforce_pieces_required', weights, 20)) ret.triforce_pieces_required = Options.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights, 20))
ret.triforce_pieces_required = min(max(1, int(ret.triforce_pieces_required)), 90)
# sum a percentage to required # sum a percentage to required
if extra_pieces == 'percentage': if extra_pieces == 'percentage':
@@ -647,7 +647,8 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0)) ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
# vanilla mode (specify how many pieces are) # vanilla mode (specify how many pieces are)
elif extra_pieces == 'available': elif extra_pieces == 'available':
ret.triforce_pieces_available = int(get_choice('triforce_pieces_available', weights, 30)) ret.triforce_pieces_available = Options.TriforcePieces.from_any(
get_choice('triforce_pieces_available', weights, 30))
# required pieces + fixed extra # required pieces + fixed extra
elif extra_pieces == 'extra': elif extra_pieces == 'extra':
extra_pieces = max(0, int(get_choice('triforce_pieces_extra', weights, 10))) extra_pieces = max(0, int(get_choice('triforce_pieces_extra', weights, 10)))
@@ -655,11 +656,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
# change minimum to required pieces to avoid problems # change minimum to required pieces to avoid problems
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90) ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
shuffle_slots = get_choice('shop_shuffle_slots', weights, '0')
if str(shuffle_slots).lower() == "random":
ret.shop_shuffle_slots = random.randint(0, 30)
else:
ret.shop_shuffle_slots = int(shuffle_slots)
ret.shop_shuffle = get_choice('shop_shuffle', weights, '') ret.shop_shuffle = get_choice('shop_shuffle', weights, '')
if not ret.shop_shuffle: if not ret.shop_shuffle:
@@ -681,7 +677,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False)) ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False))
ret.killable_thieves = get_choice('killable_thieves', weights, False) ret.killable_thieves = get_choice('killable_thieves', weights, False)
ret.tile_shuffle = get_choice('tile_shuffle', weights, False) ret.tile_shuffle = get_choice('tile_shuffle', weights, False)
ret.bush_shuffle = get_choice('bush_shuffle', weights, False) ret.bush_shuffle = get_choice('bush_shuffle', weights, False)
@@ -793,13 +788,11 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
get_choice("direction", placement, "both") get_choice("direction", placement, "both")
)) ))
if 'rom' in weights:
romweights = weights['rom']
ret.sprite_pool = romweights['sprite_pool'] if 'sprite_pool' in romweights else [] ret.sprite_pool = weights.get('sprite_pool', [])
ret.sprite = get_choice('sprite', romweights, "Link") ret.sprite = get_choice('sprite', weights, "Link")
if 'random_sprite_on_event' in romweights: if 'random_sprite_on_event' in weights:
randomoneventweights = romweights['random_sprite_on_event'] randomoneventweights = weights['random_sprite_on_event']
if get_choice('enabled', randomoneventweights, False): if get_choice('enabled', randomoneventweights, False):
ret.sprite = 'randomon' ret.sprite = 'randomon'
ret.sprite += '-hit' if get_choice('on_hit', randomoneventweights, True) else '' ret.sprite += '-hit' if get_choice('on_hit', randomoneventweights, True) else ''
@@ -812,30 +805,26 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.sprite = 'randomonnone' if ret.sprite == 'randomon' else ret.sprite ret.sprite = 'randomonnone' if ret.sprite == 'randomon' else ret.sprite
if (not ret.sprite_pool or get_choice('use_weighted_sprite_pool', randomoneventweights, False)) \ if (not ret.sprite_pool or get_choice('use_weighted_sprite_pool', randomoneventweights, False)) \
and 'sprite' in romweights: # Use sprite as a weighted sprite pool, if a sprite pool is not already defined. and 'sprite' in weights: # Use sprite as a weighted sprite pool, if a sprite pool is not already defined.
for key, value in romweights['sprite'].items(): for key, value in weights['sprite'].items():
if key.startswith('random'): if key.startswith('random'):
ret.sprite_pool += ['random'] * int(value) ret.sprite_pool += ['random'] * int(value)
else: else:
ret.sprite_pool += [key] * int(value) ret.sprite_pool += [key] * int(value)
ret.disablemusic = get_choice('disablemusic', romweights, False) ret.disablemusic = get_choice('disablemusic', weights, False)
ret.triforcehud = get_choice('triforcehud', romweights, 'hide_goal') ret.triforcehud = get_choice('triforcehud', weights, 'hide_goal')
ret.quickswap = get_choice('quickswap', romweights, True) ret.quickswap = get_choice('quickswap', weights, True)
ret.fastmenu = get_choice('menuspeed', romweights, "normal") ret.fastmenu = get_choice('menuspeed', weights, "normal")
ret.reduceflashing = get_choice('reduceflashing', romweights, False) ret.reduceflashing = get_choice('reduceflashing', weights, False)
ret.heartcolor = get_choice('heartcolor', romweights, "red") ret.heartcolor = get_choice('heartcolor', weights, "red")
ret.heartbeep = convert_to_on_off(get_choice('heartbeep', romweights, "normal")) ret.heartbeep = convert_to_on_off(get_choice('heartbeep', weights, "normal"))
ret.ow_palettes = get_choice('ow_palettes', romweights, "default") ret.ow_palettes = get_choice('ow_palettes', weights, "default")
ret.uw_palettes = get_choice('uw_palettes', romweights, "default") ret.uw_palettes = get_choice('uw_palettes', weights, "default")
ret.hud_palettes = get_choice('hud_palettes', romweights, "default") ret.hud_palettes = get_choice('hud_palettes', weights, "default")
ret.sword_palettes = get_choice('sword_palettes', romweights, "default") ret.sword_palettes = get_choice('sword_palettes', weights, "default")
ret.shield_palettes = get_choice('shield_palettes', romweights, "default") ret.shield_palettes = get_choice('shield_palettes', weights, "default")
ret.link_palettes = get_choice('link_palettes', romweights, "default") ret.link_palettes = get_choice('link_palettes', weights, "default")
else:
ret.quickswap = True
ret.sprite = "Link"
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import typing import typing
import random
class AssembleOptions(type): class AssembleOptions(type):
@@ -7,6 +8,7 @@ class AssembleOptions(type):
options = attrs["options"] = {} options = attrs["options"] = {}
name_lookup = attrs["name_lookup"] = {} name_lookup = attrs["name_lookup"] = {}
for base in bases: for base in bases:
if hasattr(base, "options"):
options.update(base.options) options.update(base.options)
name_lookup.update(name_lookup) name_lookup.update(name_lookup)
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
@@ -20,6 +22,37 @@ class AssembleOptions(type):
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs) return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
class AssembleCategoryPath(type):
def __new__(mcs, name, bases, attrs):
path = []
for base in bases:
if hasattr(base, "segment"):
path += base.segment
path += attrs["segment"]
attrs["path"] = path
return super(AssembleCategoryPath, mcs).__new__(mcs, name, bases, attrs)
class RootCategory(metaclass=AssembleCategoryPath):
segment = []
class LttPCategory(RootCategory):
segment = ["A Link to the Past"]
class LttPRomCategory(LttPCategory):
segment = ["rom"]
class FactorioCategory(RootCategory):
segment = ["Factorio"]
class MinecraftCategory(RootCategory):
segment = ["Minecraft"]
class Option(metaclass=AssembleOptions): class Option(metaclass=AssembleOptions):
value: int value: int
name_lookup: typing.Dict[int, str] name_lookup: typing.Dict[int, str]
@@ -88,6 +121,8 @@ class Toggle(Option):
def get_option_name(self): def get_option_name(self):
return bool(self.value) return bool(self.value)
class DefaultOnToggle(Toggle):
default = 1
class Choice(Option): class Choice(Option):
def __init__(self, value: int): def __init__(self, value: int):
@@ -109,6 +144,41 @@ class Choice(Option):
return cls.from_text(str(data)) return cls.from_text(str(data))
class Range(Option, int):
range_start = 0
range_end = 1
def __init__(self, value: int):
if value < self.range_start:
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}")
elif value > self.range_end:
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__}")
self.value = value
@classmethod
def from_text(cls, text: str) -> Range:
text = text.lower()
if text.startswith("random"):
if text == "random-low":
return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_start), 0)))
elif text == "random-high":
return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_end), 0)))
elif text == "random-middle":
return cls(int(round(random.triangular(cls.range_start, cls.range_end), 0)))
else:
return cls(random.randint(cls.range_start, cls.range_end))
return cls(int(text))
@classmethod
def from_any(cls, data: typing.Any) -> Range:
if type(data) == int:
return cls(data)
return cls.from_text(str(data))
def __str__(self):
return str(self.value)
class OptionNameSet(Option): class OptionNameSet(Option):
default = frozenset() default = frozenset()
@@ -142,12 +212,15 @@ class OptionDict(Option):
def get_option_name(self): def get_option_name(self):
return str(self.value) return str(self.value)
class Logic(Choice): class Logic(Choice):
option_no_glitches = 0 option_no_glitches = 0
option_minor_glitches = 1 option_minor_glitches = 1
option_overworld_glitches = 2 option_overworld_glitches = 2
option_hybrid_major_glitches = 3
option_no_logic = 4 option_no_logic = 4
alias_owg = 2 alias_owg = 2
alias_hmg = 3
class Objective(Choice): class Objective(Choice):
@@ -173,17 +246,28 @@ class Accessibility(Choice):
option_beatable = 2 option_beatable = 2
class Crystals(Choice): class Crystals(Range):
# can't use IntEnum since there's also random range_start = 0
option_0 = 0 range_end = 7
option_1 = 1
option_2 = 2
option_3 = 3 class CrystalsTower(Crystals):
option_4 = 4 default = 7
option_5 = 5
option_6 = 6
option_7 = 7 class CrystalsGanon(Crystals):
option_random = -1 default = 7
class TriforcePieces(Range):
default = 30
range_start = 1
range_end = 90
class ShopItemSlots(Range):
range_start = 0
range_end = 30
class WorldState(Choice): class WorldState(Choice):
@@ -206,56 +290,36 @@ class Enemies(Choice):
option_chaos = 2 option_chaos = 2
mapshuffle = Toggle alttp_options: typing.Dict[str, type(Option)] = {
compassshuffle = Toggle "crystals_needed_for_gt": CrystalsTower,
keyshuffle = Toggle "crystals_needed_for_ganon": CrystalsGanon,
bigkeyshuffle = Toggle "shop_item_slots": ShopItemSlots,
hints = Toggle }
RandomizeDreamers = Toggle
RandomizeSkills = Toggle
RandomizeCharms = Toggle
RandomizeKeys = Toggle
RandomizeGeoChests = Toggle
RandomizeMaskShards = Toggle
RandomizeVesselFragments = Toggle
RandomizeCharmNotches = Toggle
RandomizePaleOre = Toggle
RandomizeRancidEggs = Toggle
RandomizeRelics = Toggle
RandomizeMaps = Toggle
RandomizeStags = Toggle
RandomizeGrubs = Toggle
RandomizeWhisperingRoots = Toggle
RandomizeRocks = Toggle
RandomizeSoulTotems = Toggle
RandomizePalaceTotems = Toggle
RandomizeLoreTablets = Toggle
RandomizeLifebloodCocoons = Toggle
RandomizeFlames = Toggle
hollow_knight_randomize_options: typing.Dict[str, Option] = {
"RandomizeDreamers": RandomizeDreamers, hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {
"RandomizeSkills": RandomizeSkills, "RandomizeDreamers": DefaultOnToggle,
"RandomizeCharms": RandomizeCharms, "RandomizeSkills": DefaultOnToggle,
"RandomizeKeys": RandomizeKeys, "RandomizeCharms": DefaultOnToggle,
"RandomizeGeoChests": RandomizeGeoChests, "RandomizeKeys": DefaultOnToggle,
"RandomizeMaskShards": RandomizeMaskShards, "RandomizeGeoChests": Toggle,
"RandomizeVesselFragments": RandomizeVesselFragments, "RandomizeMaskShards": DefaultOnToggle,
"RandomizeCharmNotches": RandomizeCharmNotches, "RandomizeVesselFragments": DefaultOnToggle,
"RandomizePaleOre": RandomizePaleOre, "RandomizeCharmNotches": Toggle,
"RandomizeRancidEggs": RandomizeRancidEggs, "RandomizePaleOre": DefaultOnToggle,
"RandomizeRelics": RandomizeRelics, "RandomizeRancidEggs": Toggle,
"RandomizeMaps": RandomizeMaps, "RandomizeRelics": DefaultOnToggle,
"RandomizeStags": RandomizeStags, "RandomizeMaps": Toggle,
"RandomizeGrubs": RandomizeGrubs, "RandomizeStags": Toggle,
"RandomizeWhisperingRoots": RandomizeWhisperingRoots, "RandomizeGrubs": Toggle,
"RandomizeRocks": RandomizeRocks, "RandomizeWhisperingRoots": Toggle,
"RandomizeSoulTotems": RandomizeSoulTotems, "RandomizeRocks": Toggle,
"RandomizePalaceTotems": RandomizePalaceTotems, "RandomizeSoulTotems": Toggle,
"RandomizeLoreTablets": RandomizeLoreTablets, "RandomizePalaceTotems": Toggle,
"RandomizeLifebloodCocoons": RandomizeLifebloodCocoons, "RandomizeLoreTablets": Toggle,
"RandomizeFlames": RandomizeFlames "RandomizeLifebloodCocoons": Toggle,
"RandomizeFlames": Toggle
} }
hollow_knight_skip_options: typing.Dict[str, type(Option)] = { hollow_knight_skip_options: typing.Dict[str, type(Option)] = {
@@ -284,8 +348,8 @@ class MaxSciencePack(Choice):
default = 6 default = 6
def get_allowed_packs(self): def get_allowed_packs(self):
return {option.replace("_", "-") for option, value in self.options.items() return {option.replace("_", "-") for option, value in self.options.items() if value <= self.value} - \
if value <= self.value} {"space-science-pack"} # with rocket launch being the goal, post-launch techs don't make sense
class TechCost(Choice): class TechCost(Choice):
@@ -324,10 +388,12 @@ class TechTreeLayout(Choice):
default = 0 default = 0
class Visibility(Choice): class TechTreeInformation(Choice):
option_none = 0 option_none = 0
option_sending = 1 option_advancement = 1
default = 1 option_full = 2
default = 2
class RecipeTime(Choice): class RecipeTime(Choice):
option_vanilla = 0 option_vanilla = 0
@@ -336,18 +402,21 @@ class RecipeTime(Choice):
option_slow = 4 option_slow = 4
option_chaos = 5 option_chaos = 5
class FactorioStartItems(OptionDict): class FactorioStartItems(OptionDict):
default = {"burner-mining-drill": 19, "stone-furnace": 19} default = {"burner-mining-drill": 19, "stone-furnace": 19}
factorio_options: typing.Dict[str, type(Option)] = {"max_science_pack": MaxSciencePack, factorio_options: typing.Dict[str, type(Option)] = {
"max_science_pack": MaxSciencePack,
"tech_tree_layout": TechTreeLayout, "tech_tree_layout": TechTreeLayout,
"tech_cost": TechCost, "tech_cost": TechCost,
"free_samples": FreeSamples, "free_samples": FreeSamples,
"visibility": Visibility, "tech_tree_information": TechTreeInformation,
"random_tech_ingredients": Toggle,
"starting_items": FactorioStartItems, "starting_items": FactorioStartItems,
"recipe_time": RecipeTime} "recipe_time": RecipeTime,
"imported_blueprints": DefaultOnToggle,
}
class AdvancementGoal(Choice): class AdvancementGoal(Choice):
@@ -373,9 +442,20 @@ minecraft_options: typing.Dict[str, type(Option)] = {
"shuffle_structures": Toggle "shuffle_structures": Toggle
} }
option_sets = (
minecraft_options,
factorio_options,
alttp_options,
hollow_knight_options
)
if __name__ == "__main__": if __name__ == "__main__":
import argparse import argparse
mapshuffle = Toggle
compassshuffle = Toggle
keyshuffle = Toggle
bigkeyshuffle = Toggle
hints = Toggle
test = argparse.Namespace() test = argparse.Namespace()
test.logic = Logic.from_text("no_logic") test.logic = Logic.from_text("no_logic")
test.mapshuffle = mapshuffle.from_text("ON") test.mapshuffle = mapshuffle.from_text("ON")

View File

@@ -12,8 +12,8 @@ class Version(typing.NamedTuple):
minor: int minor: int
build: int build: int
__version__ = "0.1.2" __version__ = "0.1.3"
_version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
import builtins import builtins
import os import os

View File

@@ -3,8 +3,10 @@ import uuid
import base64 import base64
import socket import socket
import jinja2.exceptions
from pony.flask import Pony from pony.flask import Pony
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from flask import Blueprint
from flask_caching import Cache from flask_caching import Cache
from flask_compress import Compress from flask_compress import Compress
@@ -74,6 +76,51 @@ def register_session():
session["_id"] = uuid4() # uniquely identify each session without needing a login session["_id"] = uuid4() # uniquely identify each session without needing a login
@app.errorhandler(404)
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
def page_not_found(err):
return render_template('404.html'), 404
games_list = {
"zelda3": ("The Legend of Zelda: A Link to the Past",
"""
The Legend of Zelda: A Link to the Past is an action/adventure game. Take on the role of Link,
a boy who is destined to save the land of Hyrule. Delve through three palaces and nine dungeons on
your quest to rescue the descendents of the seven wise men and defeat the evil Ganon!"""),
"factorio": ("Factorio",
"""
Factorio is a game about automation. You play as an engineer who has crash landed on the planet
Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory,
research new technologies, and become more efficient in your quest to build a rocket and return home.
"""),
"minecraft": ("Minecraft",
"""
Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine,
craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient
structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim
victory!""")
}
# Game sub-pages
@app.route('/games/<string:game>/<string:page>')
def game_pages(game, page):
return render_template(f"/games/{game}/{page}.html")
# Game landing pages
@app.route('/games/<game>')
def game_page(game):
return render_template(f"/games/{game}/{game}.html")
# List of supported games
@app.route('/games')
def games():
return render_template("games/games.html", games_list=games_list)
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>') @app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
def tutorial(game, file, lang): def tutorial(game, file, lang):
return render_template("tutorial.html", game=game, file=file, lang=lang) return render_template("tutorial.html", game=game, file=file, lang=lang)
@@ -84,13 +131,8 @@ def tutorial_landing():
return render_template("tutorialLanding.html") return render_template("tutorialLanding.html")
@app.route('/player-settings')
def player_settings_simple():
return render_template("playerSettings.html")
@app.route('/weighted-settings') @app.route('/weighted-settings')
def player_settings(): def weighted_settings():
return render_template("weightedSettings.html") return render_template("weightedSettings.html")

View File

@@ -0,0 +1,10 @@
#page-not-found{
width: 40em;
margin-left: auto;
margin-right: auto;
text-align: center;
}
#page-not-found h1{
margin-bottom: 0.5rem;
}

View File

@@ -0,0 +1,3 @@
#factorio{
margin: 1rem;
}

View File

@@ -1,5 +1,5 @@
html{ html{
background-image: url('../static/backgrounds/grass/grass-0007-large.png'); background-image: url('../../static/backgrounds/grass/grass-0007-large.png');
background-repeat: repeat; background-repeat: repeat;
background-size: 650px 650px; background-size: 650px 650px;
} }

View File

@@ -0,0 +1,61 @@
#games{
max-width: 1000px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
}
#games p{
margin-top: 0.25rem;
}
#games code{
background-color: #d9cd8e;
border-radius: 4px;
padding-left: 0.25rem;
padding-right: 0.25rem;
color: #000000;
}
#games #user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
}
#games h1{
font-size: 2.5rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
#games h2{
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
#games h3, #games h4, #games h5, #games h6{
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
#games a{
color: #ffef00;
}

View File

@@ -4,9 +4,6 @@
} }
html{ html{
background-image: url('../static/backgrounds/oceans/oceans-0002.png');
background-repeat: repeat;
background-size: 250px 250px;
font-family: 'Jost', sans-serif; font-family: 'Jost', sans-serif;
font-size: 1.1rem; font-size: 1.1rem;
color: #000000; color: #000000;

View File

@@ -1,3 +1,9 @@
html{
background-image: url('../../static/backgrounds/dirt/dirt-0005-large.png');
background-repeat: repeat;
background-size: 900px 900px;
}
#base-header{ #base-header{
background: url('../../static/backgrounds/header/dirt-header.png') repeat-x; background: url('../../static/backgrounds/header/dirt-header.png') repeat-x;
} }

View File

@@ -1,3 +1,9 @@
html{
background-image: url('../../static/backgrounds/grass/grass-0007-large.png');
background-repeat: repeat;
background-size: 650px 650px;
}
#base-header { #base-header {
background: url('../../static/backgrounds/header/grass-header.png') repeat-x; background: url('../../static/backgrounds/header/grass-header.png') repeat-x;
} }

View File

@@ -1,3 +1,9 @@
html{
background-image: url('../../static/backgrounds/oceans/oceans-0002.png');
background-repeat: repeat;
background-size: 250px 250px;
}
#base-header{ #base-header{
background: url('../../static/backgrounds/header/ocean-header.png') repeat-x; background: url('../../static/backgrounds/header/ocean-header.png') repeat-x;
} }

View File

@@ -1,9 +1,3 @@
html{
background-image: url('../static/backgrounds/grass/grass-0007-large.png');
background-repeat: repeat;
background-size: 650px 650px;
}
#host-room{ #host-room{
width: calc(100% - 5rem); width: calc(100% - 5rem);
margin-left: auto; margin-left: auto;

View File

@@ -7,7 +7,6 @@ html{
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 60px;
} }
#landing-header{ #landing-header{
@@ -53,18 +52,19 @@ html{
font-size: 1.4rem; font-size: 1.4rem;
} }
#uploads-button{ #far-left-button{
top: 65px; top: 115px;
left: calc(50% - 416px - 200px - 75px); left: calc(50% - 416px - 200px - 75px);
background-image: url("/static/static/button-images/button-a.png"); background-image: url("/static/static/button-images/button-a.png");
background-size: 200px auto; background-size: 200px auto;
width: 200px; width: 200px;
height: calc(156px - 40px); height: calc(156px - 40px);
padding-top: 40px; padding-top: 40px;
cursor: default;
} }
#setup-guide-button{ #mid-left-button{
top: 270px; top: 320px;
left: calc(50% - 416px - 200px + 140px); left: calc(50% - 416px - 200px + 140px);
background-image: url("/static/static/button-images/button-b.png"); background-image: url("/static/static/button-images/button-b.png");
background-size: 260px auto; background-size: 260px auto;
@@ -73,8 +73,8 @@ html{
padding-top: 35px; padding-top: 35px;
} }
#player-settings-button{ #mid-button{
top: 350px; top: 400px;
left: calc(50% - 100px); left: calc(50% - 100px);
background-image: url("/static/static/button-images/button-a.png"); background-image: url("/static/static/button-images/button-a.png");
background-size: 200px auto; background-size: 200px auto;
@@ -83,8 +83,8 @@ html{
padding-top: 38px; padding-top: 38px;
} }
#discord-button{ #mid-right-button{
top: 250px; top: 300px;
left: calc(50% + 416px - 166px); left: calc(50% + 416px - 166px);
background-image: url("/static/static/button-images/button-c.png"); background-image: url("/static/static/button-images/button-c.png");
background-size: 250px auto; background-size: 250px auto;
@@ -94,14 +94,15 @@ html{
padding-left: 20px; padding-left: 20px;
} }
#generate-button{ #far-right-button{
top: 75px; top: 125px;
left: calc(50% + 416px + 75px); left: calc(50% + 416px + 75px);
background-image: url("/static/static/button-images/button-b.png"); background-image: url("/static/static/button-images/button-b.png");
background-size: 260px auto; background-size: 260px auto;
width: 260px; width: 260px;
height: calc(130px - 35px); height: calc(130px - 35px);
padding-top: 35px; padding-top: 35px;
cursor: default;
} }
#landing-clouds{ #landing-clouds{
@@ -111,7 +112,7 @@ html{
#landing-clouds #cloud1{ #landing-clouds #cloud1{
position: absolute; position: absolute;
left: 10px; left: 10px;
top: 265px; top: 365px;
width: 400px; width: 400px;
height: 350px; height: 350px;
@@ -147,23 +148,23 @@ html{
@keyframes c1-float{ @keyframes c1-float{
from{ from{
left: 10px; left: 10px;
top: 265px; top: 365px;
} }
25%{ 25%{
left: 14px; left: 14px;
top: 267px; top: 367px;
} }
50%{ 50%{
left: 17px; left: 17px;
top: 265px; top: 365px;
} }
75%{ 75%{
left: 14px; left: 14px;
top: 262px; top: 362px;
} }
to{ to{
left: 10px; left: 10px;
top: 265px; top: 365px;
} }
} }
@@ -241,32 +242,32 @@ html{
} }
#landing-deco-1{ #landing-deco-1{
top: 430px; top: 480px;
left: calc(50% - 276px); left: calc(50% - 276px);
} }
#landing-deco-2{ #landing-deco-2{
top: 200px; top: 250px;
left: calc(50% + 150px); left: calc(50% + 150px);
} }
#landing-deco-3{ #landing-deco-3{
top: 300px; top: 350px;
left: calc(50% - 150px); left: calc(50% - 150px);
} }
#landing-deco-4{ #landing-deco-4{
top: 240px; top: 290px;
left: calc(50% - 580px); left: calc(50% - 580px);
} }
#landing-deco-5{ #landing-deco-5{
top: 40px; top: 90px;
left: calc(50% + 450px); left: calc(50% + 450px);
} }
#landing-deco-6{ #landing-deco-6{
top: 412px; top: 462px;
left: calc(50% + 196px); left: calc(50% + 196px);
} }

View File

@@ -0,0 +1,3 @@
#minecraft{
margin: 1rem;
}

View File

@@ -0,0 +1,129 @@
html{
background-image: url('../../static/backgrounds/grass/grass-0007-large.png');
background-repeat: repeat;
background-size: 650px 650px;
}
#player-settings{
max-width: 1000px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
}
#player-settings #player-settings-button-row{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#player-settings code{
background-color: #d9cd8e;
border-radius: 4px;
padding-left: 0.25rem;
padding-right: 0.25rem;
color: #000000;
}
#player-settings #user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
}
#player-settings #user-message.visible{
display: block;
}
#player-settings h1{
font-size: 2.5rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
#player-settings h2{
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
#player-settings a{
color: #ffef00;
}
#player-settings input:not([type]){
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
}
#player-settings input:not([type]):focus{
border: 1px solid #ffffff;
}
#player-settings select{
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
background-color: #ffffff;
}
#player-settings #game-options, #player-settings #rom-options{
display: flex;
flex-direction: row;
}
#player-settings .left, #player-settings .right{
flex-grow: 1;
}
#player-settings table select{
width: 250px;
}
#player-settings table label{
display: block;
min-width: 200px;
margin-right: 4px;
cursor: default;
}
@media all and (max-width: 1000px), all and (orientation: portrait){
#player-settings #game-options, #player-settings #rom-options{
justify-content: flex-start;
flex-wrap: wrap;
}
#player-settings .left, #player-settings .right{
flex-grow: unset;
}
#game-options table label, #rom-options table label{
display: block;
min-width: 200px;
}
}

View File

@@ -1,9 +1,3 @@
html{
background-image: url('../static/backgrounds/dirt/dirt-0005-large.png');
background-repeat: repeat;
background-size: 900px 900px;
}
#tracker-wrapper { #tracker-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -1,9 +1,3 @@
html{
background-image: url('../static/backgrounds/grass/grass-0007-large.png');
background-repeat: repeat;
background-size: 650px 650px;
}
#tutorial-wrapper{ #tutorial-wrapper{
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -1,9 +1,3 @@
html{
background-image: url('../static/backgrounds/grass/grass-0007-large.png');
background-repeat: repeat;
background-size: 650px 650px;
}
#tutorial-landing{ #tutorial-landing{
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -1,9 +1,3 @@
html{
background-image: url('../static/backgrounds/grass/grass-0007-large.png');
background-repeat: repeat;
background-size: 650px 650px;
}
#weighted-settings{ #weighted-settings{
width: 60rem; width: 60rem;
margin-left: auto; margin-left: auto;

View File

@@ -0,0 +1,129 @@
html{
background-image: url('../../static/backgrounds/grass/grass-0007-large.png');
background-repeat: repeat;
background-size: 650px 650px;
}
#player-settings{
max-width: 1000px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
}
#player-settings #player-settings-button-row{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#player-settings code{
background-color: #d9cd8e;
border-radius: 4px;
padding-left: 0.25rem;
padding-right: 0.25rem;
color: #000000;
}
#player-settings #user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
}
#player-settings #user-message.visible{
display: block;
}
#player-settings h1{
font-size: 2.5rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
#player-settings h2{
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
#player-settings a{
color: #ffef00;
}
#player-settings input:not([type]){
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
}
#player-settings input:not([type]):focus{
border: 1px solid #ffffff;
}
#player-settings select{
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
background-color: #ffffff;
}
#player-settings #game-options, #player-settings #rom-options{
display: flex;
flex-direction: row;
}
#player-settings .left, #player-settings .right{
flex-grow: 1;
}
#player-settings table select{
width: 250px;
}
#player-settings table label{
display: block;
min-width: 200px;
margin-right: 4px;
cursor: default;
}
@media all and (max-width: 1000px), all and (orientation: portrait){
#player-settings #game-options, #player-settings #rom-options{
justify-content: flex-start;
flex-wrap: wrap;
}
#player-settings .left, #player-settings .right{
flex-grow: unset;
}
#game-options table label, #rom-options table label{
display: block;
min-width: 200px;
}
}

View File

@@ -0,0 +1,3 @@
#zelda3{
margin: 1rem;
}

View File

@@ -0,0 +1,17 @@
{% extends 'pageWrapper.html' %}
{% import "macros.html" as macros %}
{% block head %}
<title>Page Not Found (404)</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/404.css") }}" />
{% endblock %}
{% block body %}
{% include 'header/oceanHeader.html' %}
<div id="page-not-found" class="grass-island">
<h1>This page is out of logic!</h1>
The page you're looking for doesn&apos;t exist.<br />
<a href="/">Click here to return to safety.</a>
</div>
{% include 'islandFooter.html' %}
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'pageWrapper.html' %}
{% block head %}
<title>Factorio</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/factorio/factorio.css") }}" />
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
{% endblock %}
{% block body %}
{% include 'header/grassHeader.html' %}
<div id="factorio">
Coming Soon™
</div>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends 'pageWrapper.html' %}
{% block head %}
<title>Factorio Settings</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/factorio/player-settings.css") }}" />
{% endblock %}
{% block body %}
{% include 'header/grassHeader.html' %}
<div id="player-settings">
<div id="user-message"></div>
<h1>Factorio Settings</h1>
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
or download a settings file you can use to participate in a MultiWorld. If you would like to make
your settings extra random, check out the advanced <a href="/weighted-settings">weighted settings</a>
page. There, you will find examples of all available sprites as well.</p>
<p>A list of all games you have generated can be found <a href="/user-content">here</a>.</p>
<div>
More content coming soon™.
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,17 @@
{% extends 'pageWrapper.html' %}
{% block head %}
<title>Player Settings</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/games.css") }}" />
{% endblock %}
{% block body %}
{% include 'header/grassHeader.html' %}
<div id="games">
<h1>Currently Supported Games</h1>
{% for game, (display_name, description) in games_list.items() %}
<h3><a href="{{ url_for("game_page", game=game) }}">{{ display_name}}</a></h3>
<p>{{ description}}</p>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'pageWrapper.html' %}
{% block head %}
<title>Minecraft</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/minecraft/minecraft.css") }}" />
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
{% endblock %}
{% block body %}
{% include 'header/grassHeader.html' %}
<div id="minecraft">
Coming Soon™
</div>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends 'pageWrapper.html' %}
{% block head %}
<title>Minecraft Settings</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/minecraft/player-settings.css") }}" />
{% endblock %}
{% block body %}
{% include 'header/grassHeader.html' %}
<div id="player-settings">
<div id="user-message"></div>
<h1>Minecraft Settings</h1>
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
or download a settings file you can use to participate in a MultiWorld. If you would like to make
your settings extra random, check out the advanced <a href="/weighted-settings">weighted settings</a>
page. There, you will find examples of all available sprites as well.</p>
<p>A list of all games you have generated can be found <a href="/user-content">here</a>.</p>
<div>
More content coming soon™.
</div>
</div>
{% endblock %}

View File

@@ -1,18 +1,18 @@
{% extends 'pageWrapper.html' %} {% extends 'pageWrapper.html' %}
{% block head %} {% block head %}
<title>Player Settings</title> <title>A Link to the Past Settings</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/playerSettings.css") }}" /> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/zelda3/player-settings.css") }}" />
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script> <script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/playerSettings.js") }}"></script> <script type="application/ecmascript" src="{{ url_for('static', filename="assets/zelda3/player-settings.js") }}"></script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% include 'header/grassHeader.html' %} {% include 'header/grassHeader.html' %}
<div id="player-settings"> <div id="player-settings">
<div id="user-message"></div> <div id="user-message"></div>
<h1>Start Game</h1> <h1>A Link to the Past Settings</h1>
<p>Choose the options you would like to play with! You may generate a single-player game from this page, <p>Choose the options you would like to play with! You may generate a single-player game from this page,
or download a settings file you can use to participate in a MultiWorld. If you would like to make or download a settings file you can use to participate in a MultiWorld. If you would like to make
your settings extra random, check out the advanced <a href="/weighted-settings">weighted settings</a> your settings extra random, check out the advanced <a href="/weighted-settings">weighted settings</a>

View File

@@ -0,0 +1,15 @@
{% extends 'pageWrapper.html' %}
{% block head %}
<title>A Link to the Past</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/zelda3/zelda3.css") }}" />
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
{% endblock %}
{% block body %}
{% include 'header/grassHeader.html' %}
<div id="zelda3">
Coming Soon™
</div>
{% endblock %}

View File

@@ -11,10 +11,8 @@
<a href="/">archipelago</a> <a href="/">archipelago</a>
</div> </div>
<div id="base-header-right"> <div id="base-header-right">
<a href="/player-settings">start game</a> <a href="/games">games</a>
<a href="/uploads">host game</a>
<a href="/tutorial">setup guides</a> <a href="/tutorial">setup guides</a>
<a href="/generate">upload config</a>
<a href="https://discord.gg/8Z65BR2">discord</a> <a href="https://discord.gg/8Z65BR2">discord</a>
</div> </div>
</header> </header>

View File

@@ -6,17 +6,18 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% include 'header/oceanHeader.html' %}
<div id="landing-wrapper"> <div id="landing-wrapper">
<div id="landing-header"> <div id="landing-header">
<h4>the legend of zelda: a link to the past</h4> <h1>ARCHIPELAGO</h1>
<h1>MULTIWORLD RANDOMIZER</h1> <h4>multiworld randomizer ecosystem</h4>
</div> </div>
<div id="landing-links"> <div id="landing-links">
<a href="/player-settings" id="player-settings-button">start<br />playing</a> <a href="/games" id="mid-button">start<br />playing</a>
<a href="/uploads" id="uploads-button">host<br />game</a> <a id="far-left-button"></a>
<a href="/tutorial" id="setup-guide-button">setup guides</a> <a href="/tutorial" id="mid-left-button">setup guide</a>
<a href="/generate" id="generate-button">upload config</a> <a id="far-right-button"></a>
<a href="https://discord.gg/8Z65BR2" id="discord-button">discord</a> <a href="https://discord.gg/8Z65BR2" id="mid-right-button">discord</a>
</div> </div>
<div id="landing-clouds"> <div id="landing-clouds">
<img id="cloud1" src="/static/static/backgrounds/clouds/cloud-0001.png"/> <img id="cloud1" src="/static/static/backgrounds/clouds/cloud-0001.png"/>
@@ -33,14 +34,13 @@
</div> </div>
<div id="landing" class="grass-island"> <div id="landing" class="grass-island">
<div id="landing-body"> <div id="landing-body">
<p id="first-line">Welcome to the Archipelago Multiworld Randomizer!</p> <p id="first-line">Welcome to Archipelago!</p>
<p>This is a <span data-tooltip="Allegedly.">randomizer</span> for The Legend of Zelda: A <p>
Link to the Past.</p> This is a cross-game modification system which randomizes different games, then uses the result to
<p>It is also a multi-world, meaning Link's items may have been placed into other players' games. build a single unified multi-player game. Items from one game may be present in another, and
When a player picks up an item which does not belong to them, it is sent back to the player you will need your fellow players to find items you need in their games to help you complete
it belongs to.</p> your own.
<p>On this website you are able to generate and host multiworld games, and item and location </p>
trackers are provided for games hosted here.</p>
<p> <p>
This project is the cumulative effort of many This project is the cumulative effort of many
<a href="https://github.com/ArchipelagoMW/Archipelago/graphs/contributors">talented people.</a> <a href="https://github.com/ArchipelagoMW/Archipelago/graphs/contributors">talented people.</a>

View File

@@ -6,7 +6,16 @@ require "util"
FREE_SAMPLES = {{ free_samples }} FREE_SAMPLES = {{ free_samples }}
SLOT_NAME = "{{ slot_name }}" SLOT_NAME = "{{ slot_name }}"
SEED_NAME = "{{ seed_name }}" SEED_NAME = "{{ seed_name }}"
--SUPPRESS_INVENTORY_EVENTS = false
{% if not imported_blueprints -%}
function set_permissions()
local group = game.permissions.get_group("Default")
group.set_allows_action(defines.input_action.open_blueprint_library_gui, false)
group.set_allows_action(defines.input_action.import_blueprint, false)
group.set_allows_action(defines.input_action.import_blueprint_string, false)
group.set_allows_action(defines.input_action.import_blueprints_filtered, false)
end
{%- endif %}
-- Initialize force data, either from it being created or already being part of the game when the mod was added. -- Initialize force data, either from it being created or already being part of the game when the mod was added.
function on_force_created(event) function on_force_created(event)
@@ -63,7 +72,7 @@ function update_player(index)
local sent local sent
--player.print(serpent.block(data['pending_samples'])) --player.print(serpent.block(data['pending_samples']))
local stack = {} local stack = {}
--SUPPRESS_INVENTORY_EVENTS = true
for name, count in pairs(samples) do for name, count in pairs(samples) do
stack.name = name stack.name = name
stack.count = count stack.count = count
@@ -87,16 +96,14 @@ function update_player(index)
samples[name] = nil -- Remove from the list samples[name] = nil -- Remove from the list
end end
end end
--SUPPRESS_INVENTORY_EVENTS = false
end end
-- Update players upon them connecting, since updates while they're offline are suppressed. -- Update players upon them connecting, since updates while they're offline are suppressed.
script.on_event(defines.events.on_player_joined_game, function(event) update_player(event.player_index) end) script.on_event(defines.events.on_player_joined_game, function(event) update_player(event.player_index) end)
function update_player_event(event) function update_player_event(event)
--if not SUPPRESS_INVENTORY_EVENTS then
update_player(event.player_index) update_player(event.player_index)
--end
end end
script.on_event(defines.events.on_player_main_inventory_changed, update_player_event) script.on_event(defines.events.on_player_main_inventory_changed, update_player_event)
@@ -115,6 +122,7 @@ function add_samples(force, name, count)
end end
script.on_init(function() script.on_init(function()
{% if not imported_blueprints %}set_permissions(){% endif %}
global.forcedata = {} global.forcedata = {}
global.playerdata = {} global.playerdata = {}
-- Fire dummy events for all currently existing forces. -- Fire dummy events for all currently existing forces.

View File

@@ -8,7 +8,7 @@ local technologies = data.raw["technology"]
local original_tech local original_tech
local new_tree_copy local new_tree_copy
allowed_ingredients = {} allowed_ingredients = {}
{%- for tech_name, technology in custom_data["custom_technologies"].items() %} {%- for tech_name, technology in custom_technologies.items() %}
allowed_ingredients["{{ tech_name }}"] = { allowed_ingredients["{{ tech_name }}"] = {
{%- for ingredient in technology.ingredients %} {%- for ingredient in technology.ingredients %}
["{{ingredient}}"] = 1, ["{{ingredient}}"] = 1,
@@ -23,7 +23,7 @@ template_tech.effects = {}
template_tech.prerequisites = {} template_tech.prerequisites = {}
function prep_copy(new_copy, old_tech) function prep_copy(new_copy, old_tech)
old_tech.enabled = false old_tech.hidden = true
new_copy.unit = table.deepcopy(old_tech.unit) new_copy.unit = table.deepcopy(old_tech.unit)
local ingredient_filter = allowed_ingredients[old_tech.name] local ingredient_filter = allowed_ingredients[old_tech.name]
if ingredient_filter ~= nil then if ingredient_filter ~= nil then
@@ -66,15 +66,15 @@ original_tech = technologies["{{original_tech_name}}"]
new_tree_copy = table.deepcopy(template_tech) new_tree_copy = table.deepcopy(template_tech)
new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #} new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #}
prep_copy(new_tree_copy, original_tech) prep_copy(new_tree_copy, original_tech)
{% if tech_cost != 1 %} {% if tech_cost_scale != 1 %}
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }})) new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
{% endif %} {% endif %}
{%- if item_name in tech_table and visibility -%} {%- if item_name in tech_table and tech_tree_information == 2 -%}
{#- copy Factorio Technology Icon -#} {#- copy Factorio Technology Icon -#}
copy_factorio_icon(new_tree_copy, "{{ item_name }}") copy_factorio_icon(new_tree_copy, "{{ item_name }}")
{%- else -%} {%- else -%}
{#- use default AP icon if no Factorio graphics exist -#} {#- use default AP icon if no Factorio graphics exist -#}
{% if advancement %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %} {% if advancement or not tech_tree_information %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %}
{%- endif -%} {%- endif -%}
{#- connect Technology #} {#- connect Technology #}
{%- if original_tech_name in tech_tree_layout_prerequisites %} {%- if original_tech_name in tech_tree_layout_prerequisites %}

View File

@@ -1,7 +1,7 @@
[technology-name] [technology-name]
{% for original_tech_name, item_name, receiving_player, advancement in locations %} {% for original_tech_name, item_name, receiving_player, advancement in locations %}
{%- if visibility -%} {%- if tech_tree_information == 2 -%}
ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }} ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }}
{% else %} {% else %}
ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable
@@ -10,9 +10,11 @@ ap-{{ tech_table[original_tech_name] }}-= An Archipelago Sendable
[technology-description] [technology-description]
{% for original_tech_name, item_name, receiving_player, advancement in locations %} {% for original_tech_name, item_name, receiving_player, advancement in locations %}
{%- if visibility -%} {%- if tech_tree_information == 2 %}
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}. ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}.
{% else %} {%- elif tech_tree_information == 1 and advancement %}
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone, which is considered a logical advancement.
{%- else %}
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone.
{%- endif -%} {%- endif -%}
{% endfor %} {% endfor %}

View File

@@ -11,10 +11,12 @@
# inverted # inverted
# This means, if mode is meta-rolled and the result happens to be inverted, then defer to the player's yaml instead. # This means, if mode is meta-rolled and the result happens to be inverted, then defer to the player's yaml instead.
meta_description: Meta-Mystery file with the intention of having similar-length completion times for a hopefully better experience meta_description: Meta-Mystery file with the intention of having similar-length completion times for a hopefully better experience
null:
progression_balancing: # Progression balancing tries to make sure that the player has *something* towards any players goal in each "sphere" progression_balancing: # Progression balancing tries to make sure that the player has *something* towards any players goal in each "sphere"
on: 0 # Force every player into progression balancing on: 0 # Force every player into progression balancing
off: 0 # Force every player out of progression balancing, then prepare for a lot of logical BK off: 0 # Force every player out of progression balancing, then prepare for a lot of logical BK
null: 1 # Let players decide via their own progression_balancing flag in their yaml, defaulting to on null: 1 # Let players decide via their own progression_balancing flag in their yaml, defaulting to on
A Link to the Past:
goals: goals:
ganon: 100 # Climb GT, defeat Agahnim 2, and then kill Ganon ganon: 100 # Climb GT, defeat Agahnim 2, and then kill Ganon
fast_ganon: 250 # Only killing Ganon is required. The hole is always open. However, items may still be placed in GT fast_ganon: 250 # Only killing Ganon is required. The hole is always open. However, items may still be placed in GT
@@ -61,4 +63,3 @@ triforce_pieces_available: # Set to how many triforces pieces are available to c
triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1 triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1
# Format "pieces: chance" # Format "pieces: chance"
25: 50 25: 50
# Do not use meta rom options at this time

View File

@@ -27,6 +27,8 @@ game:
A Link to the Past: 1 A Link to the Past: 1
Factorio: 1 Factorio: 1
Minecraft: 1 Minecraft: 1
requires:
version: 0.1.3 # Version of Archipelago required for this yaml to work as expected.
# Shared Options supported by all games: # Shared Options supported by all games:
accessibility: accessibility:
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
@@ -35,15 +37,22 @@ accessibility:
progression_balancing: progression_balancing:
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items. off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
# Can be uncommented to use it # The following 4 options can be uncommented and moved into a game's section they should affect
# startinventory: # Begin the file with the listed items/upgrades # start_inventory: # Begin the file with the listed items/upgrades
# Please only use items for the correct game, use triggers if need to be have seperated lists. # Please only use items for the correct game, use triggers if need to be have seperated lists.
# Pegasus Boots: on # Pegasus Boots: on
# Bomb Upgrade (+10): 4 # Bomb Upgrade (+10): 4
# Arrow Upgrade (+10): 4 # Arrow Upgrade (+10): 4
# start_hints: # Begin the game with these items' locations revealed to you at the start of the game. Get the info via !hint in your client. # start_hints: # Begin the game with these items' locations revealed to you at the start of the game. Get the info via !hint in your client.
# - Moon Pearl # - Moon Pearl
# Factorio options: # local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords"
# - "Moon Pearl"
# - "Small Keys"
# - "Big Keys"
# non_local_items: # Force certain items to appear outside your world only, unless in single-player. Recognizes some group names, like "Swords"
# - "Progressive Weapons"
Factorio:
tech_tree_layout: tech_tree_layout:
single: 1 single: 1
small_diamonds: 1 small_diamonds: 1
@@ -82,16 +91,17 @@ free_samples:
single_craft: 0 single_craft: 0
half_stack: 0 half_stack: 0
stack: 0 stack: 0
visibility: tech_tree_information:
none: 0 none: 0
sending: 1 advancement: 0 # show which items are a logical advancement
random_tech_ingredients: full: 1 # show full info on each tech node
imported_blueprints: # can be turned off to prevent access to blueprints created outside the current world
on: 1 on: 1
off: 0 off: 0
starting_items: starting_items:
burner-mining-drill: 19 burner-mining-drill: 19
stone-furnace: 19 stone-furnace: 19
# Minecraft options: Minecraft:
advancement_goal: # Number of advancements required (out of 92 total) to spawn the Ender Dragon and complete the game. advancement_goal: # Number of advancements required (out of 92 total) to spawn the Ender Dragon and complete the game.
few: 0 # 30 advancements few: 0 # 30 advancements
normal: 1 # 50 normal: 1 # 50
@@ -112,15 +122,15 @@ include_postgame_advancements: # Some advancements require defeating the Ender D
shuffle_structures: # CURRENTLY DISABLED; enables shuffling of villages, outposts, fortresses, bastions, and end cities. shuffle_structures: # CURRENTLY DISABLED; enables shuffling of villages, outposts, fortresses, bastions, and end cities.
on: 0 on: 0
off: 1 off: 1
# A Link to the Past options: A Link to the Past:
### Logic Section ### ### Logic Section ###
# Warning: overworld_glitches is not available and minor_glitches is only partially implemented on the door-rando version
glitches_required: # Determine the logic required to complete the seed glitches_required: # Determine the logic required to complete the seed
none: 50 # No glitches required none: 50 # No glitches required
minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic
overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches
hybrid_major_glitches: 0 # In addition to overworld glitches, also requires underworld clips between dungeons.
no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx. no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx.
# Other players items are placed into your world under OWG logic # Other players items are placed into your world under HMG logic
dark_room_logic: # Logic for unlit dark rooms dark_room_logic: # Logic for unlit dark rooms
lamp: 50 # require the Lamp for these rooms to be considered accessible. lamp: 50 # require the Lamp for these rooms to be considered accessible.
torches: 0 # in addition to lamp, allow the fire rod and presence of easily accessible torches for access torches: 0 # in addition to lamp, allow the fire rod and presence of easily accessible torches for access
@@ -220,26 +230,32 @@ triforce_pieces_required: # Set to how many out of X triforce pieces you need to
30: 0 30: 0
40: 0 40: 0
50: 0 50: 0
tower_open: # Crystals required to open GT crystals_needed_for_gt: # Crystals required to open GT
'0': 80 0: 0
'1': 70 1: 0
'2': 60 2: 0
'3': 50 3: 0
'4': 40 4: 0
'5': 30 5: 0
'6': 20 6: 0
'7': 10 7: 0
random: 0 random: 0
ganon_open: # Crystals required to hurt Ganon random-low: 50 # any valid number, weighted towards the lower end
'0': 80 random-middle: 0 # any valid number, weighted towards the central range
'1': 70 random-high: 0 # any valid number, weighted towards the higher end
'2': 60 crystals_needed_for_ganon: # Crystals required to hurt Ganon
'3': 50 0: 0
'4': 40 1: 0
'5': 30 2: 0
'6': 20 3: 0
'7': 10 4: 0
5: 0
6: 0
7: 0
random: 0 random: 0
random-low: 0
random-middle: 0
random-high: 50
mode: mode:
standard: 50 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary standard: 50 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary
open: 50 # Begin the game from your choice of Link's House or the Sanctuary open: 50 # Begin the game from your choice of Link's House or the Sanctuary
@@ -280,7 +296,7 @@ turtle_rock_medallion: # required medallion to open Turtle Rock front entrance
boss_shuffle: boss_shuffle:
none: 50 # Vanilla bosses none: 50 # Vanilla bosses
basic: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons basic: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons
normal: 0 # 3 bosses can occur twice full: 0 # 3 bosses can occur twice
chaos: 0 # Any boss can appear any amount of times chaos: 0 # Any boss can appear any amount of times
singularity: 0 # Picks a boss, tries to put it everywhere that works, if there's spaces remaining it picks a boss to fill those singularity: 0 # Picks a boss, tries to put it everywhere that works, if there's spaces remaining it picks a boss to fill those
enemy_shuffle: # Randomize enemy placement enemy_shuffle: # Randomize enemy placement
@@ -313,7 +329,7 @@ beemizer: # Remove items from the global item pool and replace them with single
4: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 90% are traps and 10% single bees 4: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 90% are traps and 10% single bees
5: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 100% are traps and 0% single bees 5: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 100% are traps and 0% single bees
### Shop Settings ### ### Shop Settings ###
shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl) shop_item_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
0: 50 0: 50
5: 0 5: 0
15: 0 15: 0
@@ -360,123 +376,10 @@ green_clock_time: # For all timer modes, the amount of time in minutes to gain o
4: 50 4: 50
10: 0 10: 0
15: 0 15: 0
# Can be uncommented to use it
# local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords"
# - "Moon Pearl"
# - "Small Keys"
# - "Big Keys"
glitch_boots: glitch_boots:
on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them
off: 0 off: 0
# meta_ignore, linked_options and triggers work for any game # rom options section
meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option here guarantees it will not occur in your seed, even if the .yaml file specifies it
mode:
- inverted # Never play inverted seeds
retro:
- on # Never play retro seeds
swordless:
- on # Never play a swordless seed
linked_options:
- name: crosskeys
options: # These overwrite earlier options if the percentage chance triggers
entrance_shuffle: crossed
bigkey_shuffle: true
compass_shuffle: true
map_shuffle: true
smallkey_shuffle: true
percentage: 0 # Set this to the percentage chance you want crosskeys
- name: localcrosskeys
options: # These overwrite earlier options if the percentage chance triggers
entrance_shuffle: crossed
bigkey_shuffle: true
compass_shuffle: true
map_shuffle: true
smallkey_shuffle: true
local_items: # Forces keys to be local to your own world
- "Small Keys"
- "Big Keys"
percentage: 0 # Set this to the percentage chance you want local crosskeys
- name: enemizer
options:
boss_shuffle: # Subchances can be injected too, which then get rolled
basic: 1
full: 1
chaos: 1
singularity: 1
enemy_damage:
shuffled: 1
random: 1
enemy_health:
easy: 1
hard: 1
expert: 1
percentage: 0 # Set this to the percentage chance you want enemizer
# triggers that replace options upon rolling certain options
legacy_weapons: # this is not an actual option, just a set of weights to trigger from
trigger_disabled: 50
randomized: 0 # Swords are placed randomly throughout the world
assured: 0 # Begin with a sword, the rest are placed randomly throughout the world
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
swordless: 0 # swordless mode
triggers:
# trigger block for legacy weapons mode, to enable these add weights to legacy_weapons
- option_name: legacy_weapons
option_result: randomized
options:
swordless: off
- option_name: legacy_weapons
option_result: assured
options:
swordless: off
startinventory:
Progressive Sword: 1
- option_name: legacy_weapons
option_result: vanilla
options:
swordless: off
plando_items:
- items:
Progressive Sword: 4
locations:
- Master Sword Pedestal
- Pyramid Fairy - Left
- Blacksmith
- Link's Uncle
- option_name: legacy_weapons
option_result: swordless
options:
swordless: on
# end of legacy weapons block
- option_name: enemy_damage # targets enemy_damage
option_result: shuffled # if it rolls shuffled
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
options: # then inserts these options
swordless: off
### door rando only options (not supported at all yet on this branch) ###
door_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise
vanilla: 50 # Everything should be like in vanilla
basic: 0 # Dungeons are shuffled within themselves
crossed: 0 # Dungeons are shuffled across each other
# you can also define door shuffle seed, like so:
crossed-1000: 0 # using this method, you can have the same dungeon layout as another player and share dungeon layout information.
# however, other settings like intensity, universal keys, etc. may affect the shuffle result as well.
crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed
intensity: # Only available if the host uses the doors branch, it is ignored otherwise
1: 50 # Shuffles normal doors and spiral staircases
2: 0 # And shuffles open edges and straight staircases
3: 0 # And shuffles dungeon lobbies
random: 0 # Picks one of those at random
key_drop_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise
on: 0 # Enables the small keys dropped by enemies or under pots, and the big key dropped by the Ball & Chain guard to be shuffled into the pool. This extends the number of checks to 249.
off: 50
experimental: # Only available if the host uses the doors branch, it is ignored otherwise
on: 0 # Enables experimental features.
off: 50
debug: # Only available if the host uses the doors branch, it is ignored otherwise
on: 0 # Enables debugging features. Currently, these are the Item collection counter. (overwrites total triforce pieces) and Castle Gate closed indicator.
off: 50
### end of door rando only options ###
rom:
random_sprite_on_event: # An alternative to specifying randomonhit / randomonexit / etc... in sprite down below. random_sprite_on_event: # An alternative to specifying randomonhit / randomonexit / etc... in sprite down below.
enabled: # If enabled, sprite down below is ignored completely, (although it may become the sprite pool) enabled: # If enabled, sprite down below is ignored completely, (although it may become the sprite pool)
on: 0 on: 0
@@ -604,3 +507,102 @@ rom:
dizzy: 0 dizzy: 0
sick: 0 sick: 0
puke: 0 puke: 0
# triggers that replace options upon rolling certain options
legacy_weapons: # this is not an actual option, just a set of weights to trigger from
trigger_disabled: 50
randomized: 0 # Swords are placed randomly throughout the world
assured: 0 # Begin with a sword, the rest are placed randomly throughout the world
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
swordless: 0 # swordless mode
# meta_ignore, linked_options and triggers work for any game
meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option here guarantees it will not occur in your seed, even if the .yaml file specifies it
mode:
- inverted # Never play inverted seeds
retro:
- on # Never play retro seeds
swordless:
- on # Never play a swordless seed
linked_options:
- name: crosskeys
options: # These overwrite earlier options if the percentage chance triggers
A Link to the Past:
entrance_shuffle: crossed
bigkey_shuffle: true
compass_shuffle: true
map_shuffle: true
smallkey_shuffle: true
percentage: 0 # Set this to the percentage chance you want crosskeys
- name: localcrosskeys
options: # These overwrite earlier options if the percentage chance triggers
A Link to the Past:
entrance_shuffle: crossed
bigkey_shuffle: true
compass_shuffle: true
map_shuffle: true
smallkey_shuffle: true
local_items: # Forces keys to be local to your own world
- "Small Keys"
- "Big Keys"
percentage: 0 # Set this to the percentage chance you want local crosskeys
- name: enemizer
options:
A Link to the Past:
boss_shuffle: # Subchances can be injected too, which then get rolled
basic: 1
full: 1
chaos: 1
singularity: 1
enemy_damage:
shuffled: 1
random: 1
enemy_health:
easy: 1
hard: 1
expert: 1
percentage: 0 # Set this to the percentage chance you want enemizer
triggers:
# trigger block for legacy weapons mode, to enable these add weights to legacy_weapons
- option_name: legacy_weapons
option_result: randomized
option_category: A Link to the Past
options:
A Link to the Past:
swordless: off
- option_name: legacy_weapons
option_result: assured
option_category: A Link to the Past
options:
A Link to the Past:
swordless: off
start_inventory:
Progressive Sword: 1
- option_name: legacy_weapons
option_result: vanilla
option_category: A Link to the Past
options:
A Link to the Past:
swordless: off
plando_items:
- items:
Progressive Sword: 4
locations:
- Master Sword Pedestal
- Pyramid Fairy - Left
- Blacksmith
- Link's Uncle
- option_name: legacy_weapons
option_result: swordless
option_category: A Link to the Past
options:
A Link to the Past:
swordless: on
# end of legacy weapons block
- option_name: enemy_damage # targets enemy_damage
option_category: A Link to the Past
option_result: shuffled # if it rolls shuffled
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
options: # then inserts these options
A Link to the Past:
swordless: off

View File

@@ -48,10 +48,10 @@ def manifest_creation(folder):
path = os.path.join(dirpath, filename) path = os.path.join(dirpath, filename)
hashes[os.path.relpath(path, start=folder)] = pool.submit(_threaded_hash, path) hashes[os.path.relpath(path, start=folder)] = pool.submit(_threaded_hash, path)
import json import json
from Utils import _version_tuple from Utils import version_tuple
manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"), manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"),
"hashes": {path: hash.result() for path, hash in hashes.items()}, "hashes": {path: hash.result() for path, hash in hashes.items()},
"version": _version_tuple} "version": version_tuple}
json.dump(manifest, open(manifestpath, "wt"), indent=4) json.dump(manifest, open(manifestpath, "wt"), indent=4)
print("Created Manifest") print("Created Manifest")

View File

@@ -8,11 +8,14 @@ from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import create_regions from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules from worlds.alttp.Rules import set_rules
from Options import alttp_options
class TestDungeon(unittest.TestCase): class TestDungeon(unittest.TestCase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
for option_name, option in alttp_options.items():
setattr(self.world, option_name, {1: option.default})
self.starting_regions = [] # Where to start exploring self.starting_regions = [] # Where to start exploring
self.remove_exits = [] # Block dungeon exits self.remove_exits = [] # Block dungeon exits
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']

View File

@@ -11,6 +11,8 @@ class TestVanilla(TestBase):
self.world.game[1] = "Hollow Knight" self.world.game[1] = "Hollow Knight"
import Options import Options
for hk_option in Options.hollow_knight_randomize_options: for hk_option in Options.hollow_knight_randomize_options:
getattr(self.world, hk_option)[1] = True setattr(self.world, hk_option, {1: True})
for hk_option, option in Options.hollow_knight_skip_options.items():
setattr(self.world, hk_option, {1: option.default})
create_regions(self.world, 1) create_regions(self.world, 1)
gen_hollow(self.world, 1) gen_hollow(self.world, 1)

View File

@@ -8,11 +8,13 @@ from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase from test.TestBase import TestBase
from Options import alttp_options
class TestInverted(TestBase): class TestInverted(TestBase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
for option_name, option in alttp_options.items():
setattr(self.world, option_name, {1: option.default})
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']
self.world.mode[1] = "inverted" self.world.mode[1] = "inverted"
create_inverted_regions(self.world, 1) create_inverted_regions(self.world, 1)

View File

@@ -8,11 +8,13 @@ from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase from test.TestBase import TestBase
from Options import alttp_options
class TestInvertedMinor(TestBase): class TestInvertedMinor(TestBase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
for option_name, option in alttp_options.items():
setattr(self.world, option_name, {1: option.default})
self.world.mode[1] = "inverted" self.world.mode[1] = "inverted"
self.world.logic[1] = "minorglitches" self.world.logic[1] = "minorglitches"
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']

View File

@@ -8,11 +8,14 @@ from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase from test.TestBase import TestBase
from Options import alttp_options
class TestInvertedOWG(TestBase): class TestInvertedOWG(TestBase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
for option_name, option in alttp_options.items():
setattr(self.world, option_name, {1: option.default})
self.world.logic[1] = "owglitches" self.world.logic[1] = "owglitches"
self.world.mode[1] = "inverted" self.world.mode[1] = "inverted"
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']

View File

@@ -8,11 +8,13 @@ from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase from test.TestBase import TestBase
from Options import alttp_options
class TestMinor(TestBase): class TestMinor(TestBase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
for option_name, option in alttp_options.items():
setattr(self.world, option_name, {1: option.default})
self.world.logic[1] = "minorglitches" self.world.logic[1] = "minorglitches"
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']
create_regions(self.world, 1) create_regions(self.world, 1)

View File

@@ -8,11 +8,14 @@ from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase from test.TestBase import TestBase
from Options import alttp_options
class TestVanillaOWG(TestBase): class TestVanillaOWG(TestBase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
for option_name, option in alttp_options.items():
setattr(self.world, option_name, {1: option.default})
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']
self.world.logic[1] = "owglitches" self.world.logic[1] = "owglitches"
create_regions(self.world, 1) create_regions(self.world, 1)

View File

@@ -8,11 +8,13 @@ from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase from test.TestBase import TestBase
from Options import alttp_options
class TestVanilla(TestBase): class TestVanilla(TestBase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
for option_name, option in alttp_options.items():
setattr(self.world, option_name, {1: option.default})
self.world.logic[1] = "noglitches" self.world.logic[1] = "noglitches"
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']
create_regions(self.world, 1) create_regions(self.world, 1)

46
worlds/AutoWorld.py Normal file
View File

@@ -0,0 +1,46 @@
from BaseClasses import MultiWorld
class AutoWorldRegister(type):
world_types = {}
def __new__(cls, name, bases, dct):
new_class = super().__new__(cls, name, bases, dct)
if "game" in dct:
AutoWorldRegister.world_types[dct["game"]] = new_class
return new_class
def call_single(world: MultiWorld, method_name: str, player: int):
method = getattr(world.worlds[player], method_name)
return method()
def call_all(world: MultiWorld, method_name: str):
for player in world.player_ids:
call_single(world, method_name, player)
class World(metaclass=AutoWorldRegister):
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
A Game should have its own subclass of World in which it defines the required data structures."""
world: MultiWorld
player: int
def __init__(self, world: MultiWorld, player: int):
self.world = world
self.player = player
# overwritable methods that get called by Main.py
def generate_basic(self):
pass
def set_rules(self):
pass
def create_regions(self):
pass
def generate_output(self):
pass

View File

@@ -121,8 +121,8 @@ def GanonDefeatRule(state, player: int):
can_hurt = state.has_beam_sword(player) can_hurt = state.has_beam_sword(player)
common = can_hurt and state.has_fire_source(player) common = can_hurt and state.has_fire_source(player)
# silverless ganon may be needed in minor glitches # silverless ganon may be needed in anything higher than no glitches
if state.world.logic[player] in {"owglitches", "minorglitches", "none"}: if state.world.logic[player] != 'noglitches':
# need to light torch a sufficient amount of times # need to light torch a sufficient amount of times
return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or ( return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (
state.has('Silver Bow', player) and state.can_shoot_arrows(player)) or state.has('Silver Bow', player) and state.can_shoot_arrows(player)) or

View File

@@ -21,13 +21,14 @@ def parse_arguments(argv, no_defaults=False):
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--create_spoiler', help='Output a Spoiler File', action='store_true') parser.add_argument('--create_spoiler', help='Output a Spoiler File', action='store_true')
parser.add_argument('--logic', default=defval('noglitches'), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'owglitches', 'nologic'], parser.add_argument('--logic', default=defval('noglitches'), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'owglitches', 'hybridglitches', 'nologic'],
help='''\ help='''\
Select Enforcement of Item Requirements. (default: %(default)s) Select Enforcement of Item Requirements. (default: %(default)s)
No Glitches: No Glitches:
Minor Glitches: May require Fake Flippers, Bunny Revival Minor Glitches: May require Fake Flippers, Bunny Revival
and Dark Room Navigation. and Dark Room Navigation.
Overworld Glitches: May require overworld glitches. Overworld Glitches: May require overworld glitches.
Hybrid Major Glitches: May require both overworld and underworld clipping.
No Logic: Distribute items without regard for No Logic: Distribute items without regard for
item requirements. item requirements.
''') ''')
@@ -196,23 +197,6 @@ def parse_arguments(argv, no_defaults=False):
The dungeon variants only mix up dungeons and keep the rest of The dungeon variants only mix up dungeons and keep the rest of
the overworld vanilla. the overworld vanilla.
''') ''')
parser.add_argument('--crystals_ganon', default=defval('7'), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
help='''\
How many crystals are needed to defeat ganon. Any other
requirements for ganon for the selected goal still apply.
This setting does not apply when the all dungeons goal is
selected. (default: %(default)s)
Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed
''')
parser.add_argument('--crystals_gt', default=defval('7'), const='7', nargs='?',
choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
help='''\
How many crystals are needed to open GT. For inverted mode
this applies to the castle tower door instead. (default: %(default)s)
Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed
''')
parser.add_argument('--open_pyramid', default=defval('auto'), help='''\ parser.add_argument('--open_pyramid', default=defval('auto'), help='''\
Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it. Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it.
Depending on goal, you might still need to beat Agahnim 2 in order to beat ganon. Depending on goal, you might still need to beat Agahnim 2 in order to beat ganon.
@@ -338,11 +322,6 @@ def parse_arguments(argv, no_defaults=False):
u: shuffle capacity upgrades into the item pool u: shuffle capacity upgrades into the item pool
w: consider witch's hut like any other shop and shuffle/randomize it too w: consider witch's hut like any other shop and shuffle/randomize it too
''') ''')
parser.add_argument('--shop_shuffle_slots', default=defval(0),
type=lambda value: min(max(int(value), 1), 96),
help='''
Maximum amount of shop slots able to be filled by items from the item pool.
''')
parser.add_argument('--shuffle_prizes', default=defval('g'), choices=['', 'g', 'b', 'gb']) parser.add_argument('--shuffle_prizes', default=defval('g'), choices=['', 'g', 'b', 'gb'])
parser.add_argument('--sprite_pool', help='''\ parser.add_argument('--sprite_pool', help='''\
Specifies a colon separated list of sprites used for random/randomonevent. If not specified, the full sprite pool is used.''') Specifies a colon separated list of sprites used for random/randomonevent. If not specified, the full sprite pool is used.''')
@@ -398,14 +377,14 @@ def parse_arguments(argv, no_defaults=False):
playerargs = parse_arguments(shlex.split(getattr(ret, f"p{player}")), True) playerargs = parse_arguments(shlex.split(getattr(ret, f"p{player}")), True)
for name in ['logic', 'mode', 'swordless', 'goal', 'difficulty', 'item_functionality', for name in ['logic', 'mode', 'swordless', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'crystals_ganon', 'crystals_gt', 'open_pyramid', 'timer', 'shuffle', 'open_pyramid', 'timer',
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time', 'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer', 'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer',
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots', 'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
'heartbeep', "progression_balancing", "triforce_pieces_available", 'heartbeep', "progression_balancing", "triforce_pieces_available",
"triforce_pieces_required", "shop_shuffle", "shop_shuffle_slots", "triforce_pieces_required", "shop_shuffle",
"required_medallions", "start_hints", "required_medallions", "start_hints",
"plando_items", "plando_texts", "plando_connections", "er_seeds", "plando_items", "plando_texts", "plando_connections", "er_seeds",
'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves',

View File

@@ -1,6 +1,6 @@
# ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave. # ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave.
from collections import defaultdict from collections import defaultdict
from worlds.alttp.UnderworldGlitchRules import underworld_glitch_connections
def link_entrances(world, player): def link_entrances(world, player):
connect_two_way(world, 'Links House', 'Links House Exit', player) # unshuffled. For now connect_two_way(world, 'Links House', 'Links House Exit', player) # unshuffled. For now
@@ -1066,6 +1066,10 @@ def link_entrances(world, player):
raise NotImplementedError( raise NotImplementedError(
f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_names(player)}') f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_names(player)}')
# mandatory hybrid major glitches connections
if world.logic[player] in ['hybridglitches', 'nologic']:
underworld_glitch_connections(world, player)
# check for swamp palace fix # check for swamp palace fix
if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':
world.swamp_patch_required[player] = True world.swamp_patch_required[player] = True
@@ -1767,6 +1771,10 @@ def link_inverted_entrances(world, player):
else: else:
raise NotImplementedError('Shuffling not supported yet') raise NotImplementedError('Shuffling not supported yet')
# mandatory hybrid major glitches connections
if world.logic[player] in ['hybridglitches', 'nologic']:
underworld_glitch_connections(world, player)
# patch swamp drain # patch swamp drain
if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':
world.swamp_patch_required[player] = True world.swamp_patch_required[player] = True
@@ -1941,7 +1949,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
invalid_connections = Must_Exit_Invalid_Connections.copy() invalid_connections = Must_Exit_Invalid_Connections.copy()
invalid_cave_connections = defaultdict(set) invalid_cave_connections = defaultdict(set)
if world.logic[player] in ['owglitches', 'nologic']: if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']:
from worlds.alttp import OverworldGlitchRules from worlds.alttp import OverworldGlitchRules
for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'): for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'):
invalid_connections[entrance] = set() invalid_connections[entrance] = set()

View File

@@ -241,8 +241,6 @@ def generate_itempool(world, player: int):
else: else:
world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False) world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False)
if world.goal[player] == 'icerodhunt': if world.goal[player] == 'icerodhunt':
world.progression_balancing[player] = False world.progression_balancing[player] = False
loc = world.get_location('Turtle Rock - Boss', player) loc = world.get_location('Turtle Rock - Boss', player)
@@ -255,7 +253,6 @@ def generate_itempool(world, player: int):
logging.warning(f'Cannot guarantee that Trinexx is the boss of Turtle Rock for player {player}') logging.warning(f'Cannot guarantee that Trinexx is the boss of Turtle Rock for player {player}')
loc.event = True loc.event = True
loc.locked = True loc.locked = True
forbid_items_for_player(loc, {'Red Pendant', 'Green Pendant', 'Blue Pendant', 'Crystal 5', 'Crystal 6'}, player)
itemdiff = difficulties[world.difficulty[player]] itemdiff = difficulties[world.difficulty[player]]
itempool = [] itempool = []
itempool.extend(itemdiff.alwaysitems) itempool.extend(itemdiff.alwaysitems)
@@ -571,7 +568,7 @@ def get_pool_core(world, player: int):
return world.random.choice([True, False]) if progressive == 'random' else progressive == 'on' return world.random.choice([True, False]) if progressive == 'random' else progressive == 'on'
# provide boots to major glitch dependent seeds # provide boots to major glitch dependent seeds
if logic in {'owglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt': if logic in {'owglitches', 'hybridglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt':
precollected_items.append('Pegasus Boots') precollected_items.append('Pegasus Boots')
pool.remove('Pegasus Boots') pool.remove('Pegasus Boots')
pool.append('Rupees (20)') pool.append('Rupees (20)')

View File

@@ -80,7 +80,7 @@ class LocalRom(object):
self.write_bytes(startaddress + i, bytearray(data)) self.write_bytes(startaddress + i, bytearray(data))
def encrypt(self, world, player): def encrypt(self, world, player):
local_random = world.rom_seeds[player] local_random = world.slot_seeds[player]
key = bytes(local_random.getrandbits(8 * 16).to_bytes(16, 'big')) key = bytes(local_random.getrandbits(8 * 16).to_bytes(16, 'big'))
self.write_bytes(0x1800B0, bytearray(key)) self.write_bytes(0x1800B0, bytearray(key))
self.write_int16(0x180087, 1) self.write_int16(0x180087, 1)
@@ -384,7 +384,7 @@ def patch_enemizer(world, team: int, player: int, rom: LocalRom, enemizercli):
max_enemizer_tries = 5 max_enemizer_tries = 5
for i in range(max_enemizer_tries): for i in range(max_enemizer_tries):
enemizer_seed = str(world.rom_seeds[player].randint(0, 999999999)) enemizer_seed = str(world.slot_seeds[player].randint(0, 999999999))
enemizer_command = [os.path.abspath(enemizercli), enemizer_command = [os.path.abspath(enemizercli),
'--rom', randopatch_path, '--rom', randopatch_path,
'--seed', enemizer_seed, '--seed', enemizer_seed,
@@ -414,7 +414,7 @@ def patch_enemizer(world, team: int, player: int, rom: LocalRom, enemizercli):
continue continue
for j in range(i + 1, max_enemizer_tries): for j in range(i + 1, max_enemizer_tries):
world.rom_seeds[player].randint(0, 999999999) world.slot_seeds[player].randint(0, 999999999)
# Sacrifice all remaining random numbers that would have been used for unused enemizer tries. # Sacrifice all remaining random numbers that would have been used for unused enemizer tries.
# This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness # This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness
break break
@@ -751,16 +751,21 @@ bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028,
0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3, 0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3,
0x4D504, 0x4D507, 0x4D55E, 0x4D56A] 0x4D504, 0x4D507, 0x4D55E, 0x4D56A]
def get_nonnative_item_sprite(game): def get_nonnative_item_sprite(game: str) -> int:
game_to_id = { return 0x6B # set all non-native sprites to Power Star as per 13 to 2 vote at
"Factorio": 0x09, # Hammer # https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
"Hollow Knight": 0x21, # Bug Catching Net
"Minecraft": 0x13, # Shovel # def get_nonnative_item_sprite(game):
} # game_to_id = {
return game_to_id.get(game, 0x6B) # default to Power Star # "Factorio": 0x09, # Hammer
# "Hollow Knight": 0x21, # Bug Catching Net
# "Minecraft": 0x13, # Shovel
# }
# return game_to_id.get(game, 0x6B) # default to Power Star
def patch_rom(world, rom, player, team, enemized): def patch_rom(world, rom, player, team, enemized):
local_random = world.rom_seeds[player] local_random = world.slot_seeds[player]
# progressive bow silver arrow hint hack # progressive bow silver arrow hint hack
prog_bow_locs = world.find_items('Progressive Bow', player) prog_bow_locs = world.find_items('Progressive Bow', player)
@@ -885,7 +890,7 @@ def patch_rom(world, rom, player, team, enemized):
credits_total = 216 credits_total = 216
if world.retro[player]: # Old man cave and Take any caves will count towards collection rate. if world.retro[player]: # Old man cave and Take any caves will count towards collection rate.
credits_total += 5 credits_total += 5
if world.shop_shuffle_slots[player]: # Potion shop only counts towards collection rate if included in the shuffle. if world.shop_item_slots[player]: # Potion shop only counts towards collection rate if included in the shuffle.
credits_total += 30 if 'w' in world.shop_shuffle[player] else 27 credits_total += 30 if 'w' in world.shop_shuffle[player] else 27
rom.write_byte(0x187010, credits_total) # dynamic credits rom.write_byte(0x187010, credits_total) # dynamic credits
@@ -1643,7 +1648,7 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit
if world.tile_shuffle[player]: if world.tile_shuffle[player]:
tile_set = TileSet.get_random_tile_set(world.rom_seeds[player]) tile_set = TileSet.get_random_tile_set(world.slot_seeds[player])
rom.write_byte(0x4BA21, tile_set.get_speed()) rom.write_byte(0x4BA21, tile_set.get_speed())
rom.write_byte(0x4BA1D, tile_set.get_len()) rom.write_byte(0x4BA1D, tile_set.get_len())
rom.write_bytes(0x4BA2A, tile_set.get_bytes()) rom.write_bytes(0x4BA2A, tile_set.get_bytes())
@@ -1705,7 +1710,7 @@ def write_custom_shops(rom, world, player):
slot = 0 if shop.type == ShopType.TakeAny else index slot = 0 if shop.type == ShopType.TakeAny else index
if item is None: if item is None:
break break
if world.shop_shuffle_slots[player] or shop.type == ShopType.TakeAny: if world.shop_item_slots[player] or shop.type == ShopType.TakeAny:
count_shop = (shop.region.name != 'Potion Shop' or 'w' in world.shop_shuffle[player]) and \ count_shop = (shop.region.name != 'Potion Shop' or 'w' in world.shop_shuffle[player]) and \
shop.region.name != 'Capacity Upgrade' shop.region.name != 'Capacity Upgrade'
rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0) rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0)
@@ -1774,7 +1779,7 @@ def hud_format_text(text):
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite: str, palettes_options, def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite: str, palettes_options,
world=None, player=1, allow_random_on_event=False, reduceflashing=False, world=None, player=1, allow_random_on_event=False, reduceflashing=False,
triforcehud: str = None): triforcehud: str = None):
local_random = random if not world else world.rom_seeds[player] local_random = random if not world else world.slot_seeds[player]
# enable instant item menu # enable instant item menu
if fastmenu == 'instant': if fastmenu == 'instant':
@@ -2091,7 +2096,7 @@ def write_string_to_rom(rom, target, string):
def write_strings(rom, world, player, team): def write_strings(rom, world, player, team):
local_random = world.rom_seeds[player] local_random = world.slot_seeds[player]
tt = TextTable() tt = TextTable()
tt.removeUnwantedText() tt.removeUnwantedText()

View File

@@ -4,6 +4,7 @@ from worlds.alttp import OverworldGlitchRules
from BaseClasses import RegionType, MultiWorld, Entrance from BaseClasses import RegionType, MultiWorld, Entrance
from worlds.alttp.Items import ItemFactory, progression_items, item_name_groups from worlds.alttp.Items import ItemFactory, progression_items, item_name_groups
from worlds.alttp.OverworldGlitchRules import overworld_glitches_rules, no_logic_rules from worlds.alttp.OverworldGlitchRules import overworld_glitches_rules, no_logic_rules
from worlds.alttp.UnderworldGlitchRules import underworld_glitches_rules
from worlds.alttp.Bosses import GanonDefeatRule from worlds.alttp.Bosses import GanonDefeatRule
from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \ from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \
item_name item_name
@@ -47,12 +48,17 @@ def set_rules(world, player):
if world.logic[player] == 'noglitches': if world.logic[player] == 'noglitches':
no_glitches_rules(world, player) no_glitches_rules(world, player)
elif world.logic[player] in ['owglitches', 'nologic']: elif world.logic[player] == 'owglitches':
# Initially setting no_glitches_rules to set the baseline rules for some # Initially setting no_glitches_rules to set the baseline rules for some
# entrances. The overworld_glitches_rules set is primarily additive. # entrances. The overworld_glitches_rules set is primarily additive.
no_glitches_rules(world, player) no_glitches_rules(world, player)
fake_flipper_rules(world, player) fake_flipper_rules(world, player)
overworld_glitches_rules(world, player) overworld_glitches_rules(world, player)
elif world.logic[player] in ['hybridglitches', 'nologic']:
no_glitches_rules(world, player)
fake_flipper_rules(world, player)
overworld_glitches_rules(world, player)
underworld_glitches_rules(world, player)
elif world.logic[player] == 'minorglitches': elif world.logic[player] == 'minorglitches':
no_glitches_rules(world, player) no_glitches_rules(world, player)
fake_flipper_rules(world, player) fake_flipper_rules(world, player)
@@ -68,25 +74,26 @@ def set_rules(world, player):
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
set_big_bomb_rules(world, player) set_big_bomb_rules(world, player)
if world.logic[player] in {'owglitches', 'nologic'} and world.shuffle[player] not in {'insanity', 'insanity_legacy', 'madness'}: if world.logic[player] in {'owglitches', 'hybridglitches', 'nologic'} and world.shuffle[player] not in {'insanity', 'insanity_legacy', 'madness'}:
path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player) path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player)
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.world.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or') add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.world.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or')
else: else:
set_inverted_big_bomb_rules(world, player) set_inverted_big_bomb_rules(world, player)
# if swamp and dam have not been moved we require mirror for swamp palace # if swamp and dam have not been moved we require mirror for swamp palace
if not world.swamp_patch_required[player]: # however there is mirrorless swamp in hybrid MG, so we don't necessarily want this. HMG handles this requirement itself.
if not world.swamp_patch_required[player] and world.logic[player] not in ['hybridglitches', 'nologic']:
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
# GT Entrance may be required for Turtle Rock for OWG and < 7 required # GT Entrance may be required for Turtle Rock for OWG and < 7 required
ganons_tower = world.get_entrance('Inverted Ganons Tower' if world.mode[player] == 'inverted' else 'Ganons Tower', player) ganons_tower = world.get_entrance('Inverted Ganons Tower' if world.mode[player] == 'inverted' else 'Ganons Tower', player)
if world.crystals_needed_for_gt[player] == 7 and not (world.logic[player] in ['owglitches', 'nologic'] and world.mode[player] != 'inverted'): if world.crystals_needed_for_gt[player] == 7 and not (world.logic[player] in ['owglitches', 'hybridglitches', 'nologic'] and world.mode[player] != 'inverted'):
set_rule(ganons_tower, lambda state: False) set_rule(ganons_tower, lambda state: False)
set_trock_key_rules(world, player) set_trock_key_rules(world, player)
set_rule(ganons_tower, lambda state: state.has_crystals(state.world.crystals_needed_for_gt[player], player)) set_rule(ganons_tower, lambda state: state.has_crystals(state.world.crystals_needed_for_gt[player], player))
if world.mode[player] != 'inverted' and world.logic[player] in ['owglitches', 'nologic']: if world.mode[player] != 'inverted' and world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']:
add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.world.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or') add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.world.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or')
set_bunny_rules(world, player, world.mode[player] == 'inverted') set_bunny_rules(world, player, world.mode[player] == 'inverted')
@@ -1387,7 +1394,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
def get_rule_to_add(region, location = None, connecting_entrance = None): def get_rule_to_add(region, location = None, connecting_entrance = None):
# In OWG, a location can potentially be superbunny-mirror accessible or # In OWG, a location can potentially be superbunny-mirror accessible or
# bunny revival accessible. # bunny revival accessible.
if world.logic[player] in ['minorglitches', 'owglitches', 'nologic']: if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic']:
if region.name == 'Swamp Palace (Entrance)': # Need to 0hp revive - not in logic if region.name == 'Swamp Palace (Entrance)': # Need to 0hp revive - not in logic
return lambda state: state.has('Moon Pearl', player) return lambda state: state.has('Moon Pearl', player)
if region.name == 'Tower of Hera (Bottom)': # Need to hit the crystal switch if region.name == 'Tower of Hera (Bottom)': # Need to hit the crystal switch
@@ -1427,7 +1434,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
seen.add(new_region) seen.add(new_region)
if not is_link(new_region): if not is_link(new_region):
# For glitch rulesets, establish superbunny and revival rules. # For glitch rulesets, establish superbunny and revival rules.
if world.logic[player] in ['minorglitches', 'owglitches', 'nologic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions(): if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions():
possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and state.has_sword(player)) possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and state.has_sword(player))
elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions() elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions()
@@ -1465,7 +1472,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
# Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival # Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival
for entrance in world.get_entrances(): for entrance in world.get_entrances():
if entrance.player == player and is_bunny(entrance.connected_region): if entrance.player == player and is_bunny(entrance.connected_region):
if world.logic[player] in ['minorglitches', 'owglitches', 'nologic'] : if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] :
if entrance.connected_region.type == RegionType.Dungeon: if entrance.connected_region.type == RegionType.Dungeon:
if entrance.parent_region.type != RegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): if entrance.parent_region.type != RegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
add_rule(entrance, get_rule_to_add(entrance.connected_region, None, entrance)) add_rule(entrance, get_rule_to_add(entrance.connected_region, None, entrance))
@@ -1473,7 +1480,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
if entrance.connected_region.name == 'Turtle Rock (Entrance)': if entrance.connected_region.name == 'Turtle Rock (Entrance)':
add_rule(world.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance)) add_rule(world.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance))
for location in entrance.connected_region.locations: for location in entrance.connected_region.locations:
if world.logic[player] in ['minorglitches', 'owglitches', 'nologic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances(): if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances():
continue continue
if location.name in bunny_accessible_locations: if location.name in bunny_accessible_locations:
continue continue

View File

@@ -243,7 +243,7 @@ def create_shops(world, player: int):
else: else:
dynamic_shop_slots = total_dynamic_shop_slots dynamic_shop_slots = total_dynamic_shop_slots
num_slots = min(dynamic_shop_slots, max(0, int(world.shop_shuffle_slots[player]))) # 0 to 30 num_slots = min(dynamic_shop_slots, world.shop_item_slots[player])
single_purchase_slots: List[bool] = [True] * num_slots + [False] * (dynamic_shop_slots - num_slots) single_purchase_slots: List[bool] = [True] * num_slots + [False] * (dynamic_shop_slots - num_slots)
world.random.shuffle(single_purchase_slots) world.random.shuffle(single_purchase_slots)

View File

@@ -0,0 +1,113 @@
from BaseClasses import Entrance
from worlds.generic.Rules import set_rule, add_rule
# We actually need the logic to properly "mark" these regions as Light or Dark world.
# Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules.
def underworld_glitch_connections(world, player):
specrock = world.get_region('Spectacle Rock Cave (Bottom)', player)
mire = world.get_region('Misery Mire (West)', player)
kikiskip = Entrance(player, 'Kiki Skip', specrock)
mire_to_hera = Entrance(player, 'Mire to Hera Clip', mire)
mire_to_swamp = Entrance(player, 'Hera to Swamp Clip', mire)
specrock.exits.append(kikiskip)
mire.exits.extend([mire_to_hera, mire_to_swamp])
if world.fix_fake_world[player]:
kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region)
mire_to_hera.connect(world.get_entrance('Tower of Hera Exit', player).connected_region)
mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit', player).connected_region)
else:
kikiskip.connect(world.get_region('Palace of Darkness (Entrance)', player))
mire_to_hera.connect(world.get_region('Tower of Hera (Bottom)', player))
mire_to_swamp.connect(world.get_region('Swamp Palace (Entrance)', player))
# For some entrances, we need to fake having pearl, because we're in fake DW/LW.
# This creates a copy of the input state that has Moon Pearl.
def fake_pearl_state(state, player):
if state.has('Moon Pearl', player):
return state
fake_state = state.copy()
fake_state.prog_items['Moon Pearl', player] += 1
return fake_state
# Sets the rules on where we can actually go using this clip.
# Behavior differs based on what type of ER shuffle we're playing.
def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, dungeon_exit: str):
fix_dungeon_exits = world.fix_palaceofdarkness_exit[player]
fix_fake_worlds = world.fix_fake_world[player]
dungeon_entrance = [r for r in world.get_region(dungeon_region, player).entrances if r.name != clip.name][0]
if not fix_dungeon_exits: # vanilla, simple, restricted, dungeonssimple; should never have fake worlds fix
# Dungeons are only shuffled among themselves. We need to check SW, MM, and AT because they can't be reentered trivially.
if dungeon_entrance.name == 'Skull Woods Final Section':
set_rule(clip, lambda state: False) # entrance doesn't exist until you fire rod it from the other side
elif dungeon_entrance.name == 'Misery Mire':
add_rule(clip, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) # open the dungeon
elif dungeon_entrance.name == 'Agahnims Tower':
add_rule(clip, lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier
# Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally.
add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
elif not fix_fake_worlds: # full, dungeonsfull; fixed dungeon exits, but no fake worlds fix
# Entry requires the entrance's requirements plus a fake pearl, but you don't gain logical access to the surrounding region.
add_rule(clip, lambda state: dungeon_entrance.access_rule(fake_pearl_state(state, player)))
# exiting restriction
add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
# Otherwise, the shuffle type is crossed, dungeonscrossed, or insanity; all of these do not need additional rules on where we can go,
# since the clip links directly to the exterior region.
def underworld_glitches_rules(world, player):
fix_dungeon_exits = world.fix_palaceofdarkness_exit[player]
fix_fake_worlds = world.fix_fake_world[player]
# Ice Palace Entrance Clip
# This is the easiest one since it's a simple internal clip. Just need to also add melting to freezor chest since it's otherwise assumed.
add_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_bomb_clip(world.get_region('Ice Palace (Entrance)', player), player), combine='or')
add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.can_melt_things(player))
# Kiki Skip
kikiskip = world.get_entrance('Kiki Skip', player)
set_rule(kikiskip, lambda state: state.can_bomb_clip(kikiskip.parent_region, player))
dungeon_reentry_rules(world, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit')
# Mire -> Hera -> Swamp
# Using mire keys on other dungeon doors
mire = world.get_region('Misery Mire (West)', player)
mire_clip = lambda state: state.can_reach('Misery Mire (West)', 'Region', player) and state.can_bomb_clip(mire, player) and state.has_fire_source(player)
hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and state.can_bomb_clip(world.get_region('Tower of Hera (Top)', player), player)
add_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: mire_clip(state) and state.has('Big Key (Misery Mire)', player), combine='or')
add_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or')
add_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or')
# Build the rule for SP moat.
# We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT.
# First we require a certain type of entrance shuffle, then build the rule from its pieces.
if not world.swamp_patch_required[player]:
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
rule_map = {
'Misery Mire (Entrance)': (lambda state: True),
'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player))
}
inverted = world.mode[player] == 'inverted'
hera_rule = lambda state: (state.has('Moon Pearl', player) or not inverted) and \
rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state)
gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \
rule_map.get(world.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state)
mirrorless_moat_rule = lambda state: state.can_reach('Old Man S&Q', 'Entrance', player) and mire_clip(state) and (hera_rule(state) or gt_rule(state))
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or mirrorless_moat_rule(state))
else:
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
# Using the entrances for various ER types. Hera -> Swamp never matters because you can only logically traverse with the mire keys
mire_to_hera = world.get_entrance('Mire to Hera Clip', player)
mire_to_swamp = world.get_entrance('Hera to Swamp Clip', player)
set_rule(mire_to_hera, mire_clip)
set_rule(mire_to_swamp, lambda state: mire_clip(state) and state.has('Flippers', player))
dungeon_reentry_rules(world, player, mire_to_hera, 'Tower of Hera (Bottom)', 'Tower of Hera Exit')
dungeon_reentry_rules(world, player, mire_to_swamp, 'Swamp Palace (Entrance)', 'Swamp Palace Exit')

View File

@@ -1,96 +1,10 @@
from typing import Optional from typing import Optional
from BaseClasses import Location, Item from BaseClasses import Location, Item
from ..AutoWorld import World
class ALTTPWorld(World):
#class ALTTPWorld(World): game: str = "A Link to the Past"
# """WIP"""
# def __init__(self, options, slot: int):
# self._region_cache = {}
# self.slot = slot
# self.shuffle = shuffle
# self.logic = logic
# self.mode = mode
# self.swords = swords
# self.difficulty = difficulty
# self.difficulty_adjustments = difficulty_adjustments
# self.timer = timer
# self.progressive = progressive
# self.goal = goal
# self.dungeons = []
# self.regions = []
# self.shops = []
# self.itempool = []
# self.seed = None
# self.precollected_items = []
# self.state = CollectionState(self)
# self._cached_entrances = None
# self._cached_locations = None
# self._entrance_cache = {}
# self._location_cache = {}
# self.required_locations = []
# self.light_world_light_cone = False
# self.dark_world_light_cone = False
# self.rupoor_cost = 10
# self.aga_randomness = True
# self.lock_aga_door_in_escape = False
# self.save_and_quit_from_boss = True
# self.accessibility = accessibility
# self.shuffle_ganon = shuffle_ganon
# self.fix_gtower_exit = self.shuffle_ganon
# self.retro = retro
# self.custom = custom
# self.customitemarray: List[int] = customitemarray
# self.hints = hints
# self.dynamic_regions = []
# self.dynamic_locations = []
#
#
# self.remote_items = False
# self.required_medallions = ['Ether', 'Quake']
# self.swamp_patch_required = False
# self.powder_patch_required = False
# self.ganon_at_pyramid = True
# self.ganonstower_vanilla = True
#
#
# self.can_access_trock_eyebridge = None
# self.can_access_trock_front = None
# self.can_access_trock_big_chest = None
# self.can_access_trock_middle = None
# self.fix_fake_world = True
# self.mapshuffle = False
# self.compassshuffle = False
# self.keyshuffle = False
# self.bigkeyshuffle = False
# self.difficulty_requirements = None
# self.boss_shuffle = 'none'
# self.enemy_shuffle = False
# self.enemy_health = 'default'
# self.enemy_damage = 'default'
# self.killable_thieves = False
# self.tile_shuffle = False
# self.bush_shuffle = False
# self.beemizer = 0
# self.escape_assist = []
# self.crystals_needed_for_ganon = 7
# self.crystals_needed_for_gt = 7
# self.open_pyramid = False
# self.treasure_hunt_icon = 'Triforce Piece'
# self.treasure_hunt_count = 0
# self.clock_mode = False
# self.can_take_damage = True
# self.glitch_boots = True
# self.progression_balancing = True
# self.local_items = set()
# self.triforce_pieces_available = 30
# self.triforce_pieces_required = 20
# self.shop_shuffle = 'off'
# self.shuffle_prizes = "g"
# self.sprite_pool = []
# self.dark_room_logic = "lamp"
# self.restrict_dungeon_item_on_boss = False
#
class ALttPLocation(Location): class ALttPLocation(Location):

View File

@@ -60,21 +60,22 @@ def generate_mod(world: MultiWorld, player: int):
if location.address: if location.address:
locations.append((location.name, location.item.name, location.item.player, location.item.advancement)) locations.append((location.name, location.item.name, location.item.player, location.item.advancement))
mod_name = f"AP-{world.seed_name}-P{player}-{world.player_names[player][0]}" mod_name = f"AP-{world.seed_name}-P{player}-{world.player_names[player][0]}"
tech_cost = {0: 0.1, tech_cost_scale = {0: 0.1,
1: 0.25, 1: 0.25,
2: 0.5, 2: 0.5,
3: 1, 3: 1,
4: 2, 4: 2,
5: 5, 5: 5,
6: 10}[world.tech_cost[player].value] 6: 10}[world.tech_cost[player].value]
template_data = {"locations": locations, "player_names": player_names, "tech_table": tech_table, template_data = {"locations": locations, "player_names": player_names, "tech_table": tech_table,
"mod_name": mod_name, "allowed_science_packs": world.max_science_pack[player].get_allowed_packs(), "mod_name": mod_name, "allowed_science_packs": world.max_science_pack[player].get_allowed_packs(),
"tech_cost_scale": tech_cost, "custom_data": world.custom_data[player], "tech_cost_scale": tech_cost_scale, "custom_technologies": world.worlds[player].custom_technologies,
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites[player], "tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites[player],
"rocket_recipe": rocket_recipes[world.max_science_pack[player].value], "rocket_recipe": rocket_recipes[world.max_science_pack[player].value],
"slot_name": world.player_names[player][0], "seed_name": world.seed_name, "slot_name": world.player_names[player][0], "seed_name": world.seed_name,
"starting_items": world.starting_items[player], "recipes": recipes, "starting_items": world.starting_items[player], "recipes": recipes,
"random": world.random, "random": world.slot_seeds[player],
"recipe_time_scale": recipe_time_scales[world.recipe_time[player].value]} "recipe_time_scale": recipe_time_scales[world.recipe_time[player].value]}
for factorio_option in Options.factorio_options: for factorio_option in Options.factorio_options:

View File

@@ -11,11 +11,13 @@ funnel_slice_sizes = {TechTreeLayout.option_small_funnels: 6,
TechTreeLayout.option_medium_funnels: 10, TechTreeLayout.option_medium_funnels: 10,
TechTreeLayout.option_large_funnels: 15} TechTreeLayout.option_large_funnels: 15}
def get_shapes(world: MultiWorld, player: int) -> Dict[str, List[str]]: def get_shapes(factorio_world) -> Dict[str, List[str]]:
world = factorio_world.world
player = factorio_world.player
prerequisites: Dict[str, Set[str]] = {} prerequisites: Dict[str, Set[str]] = {}
layout = world.tech_tree_layout[player].value layout = world.tech_tree_layout[player].value
custom_technologies = world.custom_data[player]["custom_technologies"] custom_technologies = factorio_world.custom_technologies
tech_names: List[str] = list(set(custom_technologies) - world._static_nodes) tech_names: List[str] = list(set(custom_technologies) - world.worlds[player].static_nodes)
tech_names.sort() tech_names.sort()
world.random.shuffle(tech_names) world.random.shuffle(tech_names)
@@ -171,15 +173,14 @@ def get_shapes(world: MultiWorld, player: int) -> Dict[str, List[str]]:
elif layout in funnel_layers: elif layout in funnel_layers:
slice_size = funnel_slice_sizes[layout] slice_size = funnel_slice_sizes[layout]
world.random.shuffle(tech_names) world.random.shuffle(tech_names)
tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies()))
while len(tech_names) > slice_size: while len(tech_names) > slice_size:
tech_names = tech_names[slice_size:] tech_names = tech_names[slice_size:]
current_tech_names = tech_names[:slice_size] current_tech_names = tech_names[:slice_size]
layer_size = funnel_layers[layout] layer_size = funnel_layers[layout]
previous_slice = [] previous_slice = []
current_tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies()))
for layer in range(funnel_layers[layout]): for layer in range(funnel_layers[layout]):
slice = current_tech_names[:layer_size] slice = current_tech_names[:layer_size]
current_tech_names = current_tech_names[layer_size:] current_tech_names = current_tech_names[layer_size:]

View File

@@ -66,7 +66,7 @@ class CustomTechnology(Technology):
def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int): def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int):
ingredients = origin.ingredients & allowed_packs ingredients = origin.ingredients & allowed_packs
self.player = player self.player = player
if world.random_tech_ingredients[player]: if origin.name not in world.worlds[player].static_nodes:
ingredients = list(ingredients) ingredients = list(ingredients)
ingredients.sort() # deterministic sample ingredients.sort() # deterministic sample
ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients))) ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients)))

View File

@@ -1,30 +1,39 @@
from ..AutoWorld import World
from BaseClasses import Region, Entrance, Location, MultiWorld, Item from BaseClasses import Region, Entrance, Location, MultiWorld, Item
from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, \ from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, \
all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes
from .Shapes import get_shapes from .Shapes import get_shapes
from .Mod import generate_mod
def gen_factorio(world: MultiWorld, player: int): class Factorio(World):
static_nodes = world._static_nodes = {"automation", "logistics"} # turn dynamic/option? game: str = "Factorio"
victory_tech_names = get_rocket_requirements(frozenset(rocket_recipes[world.max_science_pack[player].value])) static_nodes = {"automation", "logistics", "rocket-silo"}
def generate_basic(self):
victory_tech_names = get_rocket_requirements(
frozenset(rocket_recipes[self.world.max_science_pack[self.player].value]))
for tech_name, tech_id in tech_table.items(): for tech_name, tech_id in tech_table.items():
tech_item = Item(tech_name, tech_name in advancement_technologies or tech_name in victory_tech_names, tech_item = Item(tech_name, tech_name in advancement_technologies or tech_name in victory_tech_names,
tech_id, player) tech_id, self.player)
tech_item.game = "Factorio" tech_item.game = "Factorio"
if tech_name in static_nodes: if tech_name in self.static_nodes:
world.get_location(tech_name, player).place_locked_item(tech_item) self.world.get_location(tech_name, self.player).place_locked_item(tech_item)
else: else:
world.itempool.append(tech_item) self.world.itempool.append(tech_item)
world.custom_data[player]["custom_technologies"] = custom_technologies = set_custom_technologies(world, player)
set_rules(world, player, custom_technologies)
def generate_output(self):
generate_mod(self.world, self.player)
def factorio_create_regions(world: MultiWorld, player: int): def create_regions(self):
player = self.player
menu = Region("Menu", None, "Menu", player) menu = Region("Menu", None, "Menu", player)
crash = Entrance(player, "Crash Land", menu) crash = Entrance(player, "Crash Land", menu)
menu.exits.append(crash) menu.exits.append(crash)
nauvis = Region("Nauvis", None, "Nauvis", player) nauvis = Region("Nauvis", None, "Nauvis", player)
nauvis.world = menu.world = world nauvis.world = menu.world = self.world
for tech_name, tech_id in tech_table.items(): for tech_name, tech_id in tech_table.items():
tech = Location(player, tech_name, tech_id, nauvis) tech = Location(player, tech_name, tech_id, nauvis)
@@ -33,38 +42,29 @@ def factorio_create_regions(world: MultiWorld, player: int):
location = Location(player, "Rocket Launch", None, nauvis) location = Location(player, "Rocket Launch", None, nauvis)
nauvis.locations.append(location) nauvis.locations.append(location)
event = Item("Victory", True, None, player) event = Item("Victory", True, None, player)
world.push_item(location, event, False) self.world.push_item(location, event, False)
location.event = location.locked = True location.event = location.locked = True
for ingredient in all_ingredient_names: for ingredient in all_ingredient_names:
location = Location(player, f"Automate {ingredient}", None, nauvis) location = Location(player, f"Automate {ingredient}", None, nauvis)
nauvis.locations.append(location) nauvis.locations.append(location)
event = Item(f"Automated {ingredient}", True, None, player) event = Item(f"Automated {ingredient}", True, None, player)
world.push_item(location, event, False) self.world.push_item(location, event, False)
location.event = location.locked = True location.event = location.locked = True
crash.connect(nauvis) crash.connect(nauvis)
world.regions += [menu, nauvis] self.world.regions += [menu, nauvis]
def set_rules(self):
def set_custom_technologies(world: MultiWorld, player: int): world = self.world
custom_technologies = {} player = self.player
world_custom = getattr(world, "_custom_technologies", {}) self.custom_technologies = set_custom_technologies(self.world, self.player)
world_custom[player] = custom_technologies shapes = get_shapes(self)
world._custom_technologies = world_custom
allowed_packs = world.max_science_pack[player].get_allowed_packs()
for technology_name, technology in technology_table.items():
custom_technologies[technology_name] = technology.get_custom(world, allowed_packs, player)
return custom_technologies
def set_rules(world: MultiWorld, player: int, custom_technologies):
shapes = get_shapes(world, player)
if world.logic[player] != 'nologic': if world.logic[player] != 'nologic':
from worlds.generic import Rules from worlds.generic import Rules
for ingredient in all_ingredient_names: for ingredient in all_ingredient_names:
location = world.get_location(f"Automate {ingredient}", player) location = world.get_location(f"Automate {ingredient}", player)
location.access_rule = lambda state, ingredient=ingredient: \ location.access_rule = lambda state, ingredient=ingredient: \
all(state.has(technology.name, player) for technology in required_technologies[ingredient]) all(state.has(technology.name, player) for technology in required_technologies[ingredient])
for tech_name, technology in custom_technologies.items(): for tech_name, technology in self.custom_technologies.items():
location = world.get_location(tech_name, player) location = world.get_location(tech_name, player)
Rules.set_rule(location, technology.build_rule(player)) Rules.set_rule(location, technology.build_rule(player))
prequisites = shapes.get(tech_name) prequisites = shapes.get(tech_name)
@@ -79,3 +79,10 @@ def set_rules(world: MultiWorld, player: int, custom_technologies):
victory_tech_names) victory_tech_names)
world.completion_condition[player] = lambda state: state.has('Victory', player) world.completion_condition[player] = lambda state: state.has('Victory', player)
def set_custom_technologies(world: MultiWorld, player: int):
custom_technologies = {}
allowed_packs = world.max_science_pack[player].get_allowed_packs()
for technology_name, technology in technology_table.items():
custom_technologies[technology_name] = technology.get_custom(world, allowed_packs, player)
return custom_technologies

View File

@@ -8,7 +8,10 @@ from .Regions import create_regions
from .Rules import set_rules from .Rules import set_rules
from BaseClasses import Region, Entrance, Location, MultiWorld, Item from BaseClasses import Region, Entrance, Location, MultiWorld, Item
from ..AutoWorld import World
class HKWorld(World):
game: str = "Hollow Knight"
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
ret = Region(name, None, name, player) ret = Region(name, None, name, player)

View File

@@ -4,15 +4,24 @@ from .Locations import exclusion_table, events_table
from .Regions import link_minecraft_structures from .Regions import link_minecraft_structures
from .Rules import set_rules from .Rules import set_rules
from BaseClasses import Region, Entrance, Location, MultiWorld, Item from BaseClasses import MultiWorld
from Options import minecraft_options from Options import minecraft_options
from ..AutoWorld import World
class MinecraftWorld(World):
game: str = "Minecraft"
client_version = (0, 3) client_version = (0, 3)
def get_mc_data(world: MultiWorld, player: int): def get_mc_data(world: MultiWorld, player: int):
exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2", "The End Structure"] exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2",
"The End Structure"]
return { return {
'world_seed': Random(world.rom_seeds[player]).getrandbits(32), # consistent and doesn't interfere with other generation 'world_seed': world.slot_seeds[player].getrandbits(32),
# consistent and doesn't interfere with other generation
'seed_name': world.seed_name, 'seed_name': world.seed_name,
'player_name': world.get_player_names(player), 'player_name': world.get_player_names(player),
'player_id': player, 'player_id': player,
@@ -20,6 +29,7 @@ def get_mc_data(world: MultiWorld, player: int):
'structures': {exit: world.get_entrance(exit, player).connected_region.name for exit in exits} 'structures': {exit: world.get_entrance(exit, player).connected_region.name for exit in exits}
} }
def generate_mc_data(world: MultiWorld, player: int): def generate_mc_data(world: MultiWorld, player: int):
import base64, json import base64, json
from Utils import output_path from Utils import output_path
@@ -29,6 +39,7 @@ def generate_mc_data(world: MultiWorld, player: int):
with open(output_path(filename), 'wb') as f: with open(output_path(filename), 'wb') as f:
f.write(base64.b64encode(bytes(json.dumps(data), 'utf-8'))) f.write(base64.b64encode(bytes(json.dumps(data), 'utf-8')))
def fill_minecraft_slot_data(world: MultiWorld, player: int): def fill_minecraft_slot_data(world: MultiWorld, player: int):
slot_data = get_mc_data(world, player) slot_data = get_mc_data(world, player)
for option_name in minecraft_options: for option_name in minecraft_options:
@@ -36,9 +47,9 @@ def fill_minecraft_slot_data(world: MultiWorld, player: int):
slot_data[option_name] = int(option.value) slot_data[option_name] = int(option.value)
return slot_data return slot_data
# Generates the item pool given the table and frequencies in Items.py. # Generates the item pool given the table and frequencies in Items.py.
def minecraft_gen_item_pool(world: MultiWorld, player: int): def minecraft_gen_item_pool(world: MultiWorld, player: int):
pool = [] pool = []
for item_name, item_data in item_table.items(): for item_name, item_data in item_table.items():
for count in range(item_frequencies.get(item_name, 1)): for count in range(item_frequencies.get(item_name, 1)):
@@ -62,9 +73,9 @@ def minecraft_gen_item_pool(world: MultiWorld, player: int):
world.itempool += pool world.itempool += pool
# Generate Minecraft world. # Generate Minecraft world.
def gen_minecraft(world: MultiWorld, player: int): def gen_minecraft(world: MultiWorld, player: int):
link_minecraft_structures(world, player) link_minecraft_structures(world, player)
minecraft_gen_item_pool(world, player) minecraft_gen_item_pool(world, player)
set_rules(world, player) set_rules(world, player)