Compare commits

...

131 Commits
0.1.1 ... 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
Fabian Dill
4b5ac3f926 Update VC Redist 2021-06-07 11:53:33 +02:00
Fabian Dill
72e5acfb86 Factorio recipe time: adjust triangular mode 2021-06-07 11:32:39 +02: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
4b283242fe FactorioClient: remove duplicate log 2021-06-06 23:59:15 +02:00
Fabian Dill
353ea0fbbe encode correct color 2021-06-06 23:44:04 +02:00
Fabian Dill
fc941f55ef FactorioClientGUI.py: disable multitouch emulation on mouse 2021-06-06 23:23:06 +02:00
Fabian Dill
12600a8cbd FactorioClientGUI.py: fix frozen logging 2021-06-06 23:13:19 +02:00
Fabian Dill
33fa9542e0 move FactorioJSONtoTextParser 2021-06-06 22:49:37 +02:00
Fabian Dill
d872ea32af Update various links 2021-06-06 22:14:13 +02:00
Fabian Dill
46bb2d1367 Factorio: add chaos recipe time and use random.triangular distribution 2021-06-06 21:38:53 +02:00
Fabian Dill
403ddd603f Factorio: implement random recipe times 2021-06-06 21:11:58 +02:00
Fabian Dill
7907838c24 Factorio: Revamp Tech Tree Layouts 2021-06-06 20:26:40 +02:00
Fabian Dill
15bd79186a remove player_name feature in MultiMystery
MultiMystery is slated to be integrated into Mystery and the auto-launch feature is not maintainable for a growing games list
2021-06-06 18:12:19 +02:00
Fabian Dill
4555b77204 FactorioClient.py formatting 2021-06-06 17:50:48 +02:00
Fabian Dill
dd3c612dec Factorio: Colored ingame text relay for AP texts 2021-06-06 17:41:06 +02:00
Fabian Dill
09b6698de8 revamp some spoiler log conditions 2021-06-06 17:13:34 +02:00
Fabian Dill
27ee156706 tiny cleanup 2021-06-06 17:10:49 +02:00
espeon65536
48c3d1fa4a Added campfire for Sticky Situation, by popular demand 2021-06-06 15:10:45 +00:00
espeon65536
286254c5cd require end crystals for Free the End, since it's possible to kill the dragon with beds and not receive the advancement 2021-06-06 15:10:45 +00:00
espeon65536
82cd51f5f4 structure plando for Minecraft 2021-06-06 15:10:45 +00:00
espeon65536
08bf993146 only write Medallions section to spoiler log if there is an ALttP world 2021-06-06 15:10:45 +00:00
espeon65536
a55bcae3ec Minecraft logic improvements
- Very Very Frightening now properly accounts for getting a villager into the overworld by curing a zombie villager
- Hot Tourist Destinations no longer requires striders, since no one was using them anyway
- Saddles are now also obtainable from raids by killing a ravager (100% drop rate)
2021-06-06 15:10:45 +00:00
Fabian Dill
607a14e921 FactorioClient: log kivy exceptions 2021-06-06 16:09:00 +02:00
Fabian Dill
c71387ad00 Factorio: fix single-player static node placement 2021-06-06 16:08:17 +02:00
Fabian Dill
c095c28618 Split requirements into world types, automatically discover and resolve them. 2021-06-06 15:30:20 +02:00
Fabian Dill
cae1188ff8 Allow ModuleUpdate to use multiple requirements files, no longer need to care about naming, and use conventional requirement parsing. Also add WebHost to it. 2021-06-06 15:11:17 +02:00
CaitSith2
7e599c51f8 Make defaults for missing options in host.yaml consistent. 2021-06-05 21:15:54 -07:00
CaitSith2
6ccb9d2dc2 Fix adjuster reference 2021-06-05 13:58:59 -07:00
Fabian Dill
1d00ed463e fix updated name aliases for tracker 2021-06-05 03:54:16 +02:00
Fabian Dill
c99054e479 add /build_factorio to gitignore 2021-06-04 01:00:03 +02:00
Fabian Dill
85a9e0d0bc write Factorio options to spoiler 2021-06-04 00:29:59 +02:00
Fabian Dill
8b4ea3c80c fix max progressive item icon in per-player tracker 2021-06-03 01:02:31 +02:00
Fabian Dill
30dec34b72 update websockets 2021-06-02 04:40:43 +02:00
Fabian Dill
a3d2df7c45 Merge branch 'factorio_gui_client' into Archipelago_Main 2021-06-02 04:31:39 +02:00
Fabian Dill
034f338f45 set default hint cost to 10 2021-06-01 04:28:15 +02:00
Fabian Dill
1d84346705 Factorio: Don't trigger bridge file on receiving a technology from server 2021-05-29 20:02:36 +02:00
Fabian Dill
6e916ebd45 bake correct minimum version for Factorio into multidata 2021-05-29 06:23:35 +02:00
Fabian Dill
a993bed8dc move factorio_client_setup.py into setup.py 2021-05-27 12:26:08 +02:00
Fabian Dill
aa6f65ee1f Prevent logical lockout from Pedestal/Pyramid Fairy in ice rod hunt 2021-05-27 12:14:20 +02:00
Fabian Dill
573931930c remove debugging helper 2021-05-25 01:06:15 +02:00
Fabian Dill
252bb69808 FactorioClient: Read Bridge file after a server log indicates that the file was written 2021-05-25 01:03:04 +02:00
Fabian Dill
0175c8ab8a move FactorioClient log to logs folder 2021-05-24 16:09:10 +02:00
Fabian Dill
f78bb2078d make sure Factorio subprocess is terminated properly 2021-05-24 13:51:27 +02:00
Fabian Dill
bc028a63cd first version of a Factorio Graphical Client 2021-05-24 12:49:01 +02:00
Fabian Dill
4b04f2b918 update icons 2021-05-24 12:48:18 +02:00
Fabian Dill
887a3b0922 update flask and jinja 2021-05-24 05:03:45 +02:00
Fabian Dill
3df78fa387 Factorio add ap_unimportant.png 2021-05-23 20:13:19 +02:00
Fabian Dill
c36ac5baba consider the ability to craft a rocket-silo for factorio completion 2021-05-22 21:13:53 +02:00
Fabian Dill
d8e33fe596 Factorio: Differentiate advancement items. 2021-05-22 10:46:27 +02:00
Fabian Dill
80b7e2e188 Factorio: Build logic for rocket launch, allow beatable only to work correctly
Convert Science requirements to Event of "automate <pack>"
2021-05-22 10:06:21 +02:00
Fabian Dill
14b430a168 Factorio: simplify resulting data-final-fixes.lua after templating a bit. 2021-05-22 08:08:37 +02:00
Fabian Dill
22aa4cbb9f Factorio: Fix Rocket Launch event getting encoded into mod 2021-05-22 07:54:12 +02:00
Fabian Dill
71bb5b850e set correct player ID for Factorio Victory 2021-05-22 07:06:09 +02:00
Fabian Dill
066c830a43 Fix LttP progressive starting Items not writing to ROM 2021-05-22 06:27:22 +02:00
Fabian Dill
760107becf remove no longer needed imports 2021-05-22 03:00:24 +02:00
Fabian Dill
8dad49e385 assign generic tracker's checked locations to correct player 2021-05-20 01:22:18 +02:00
Fabian Dill
518e5db55b use item_name filter for generic tracker 2021-05-19 21:57:10 +02:00
Fabian Dill
31a3c1cf33 Add a generic fallback tracker for all games 2021-05-19 21:55:18 +02:00
Fabian Dill
e1b4975a11 Add Crafting Machine awareness to Factorio logic
(should have no effect on vanilla, mostly for modded gameplay)
2021-05-19 06:52:53 +02:00
Fabian Dill
f8a5e8bfc7 add Factorio Victory Event 2021-05-19 05:33:44 +02:00
Fabian Dill
a656ad5cd2 potential fix for rcon timing issue 2021-05-18 20:45:56 +02:00
Fabian Dill
109eb5b9dc start of split 2021-05-13 01:34:59 +02:00
111 changed files with 3054 additions and 1612 deletions

5
.gitignore vendored
View File

@@ -16,6 +16,7 @@
*.apsave *.apsave
build build
/build_factorio/
bundle/components.wxs bundle/components.wxs
dist dist
README.html README.html
@@ -142,4 +143,6 @@ dmypy.json
.pytype/ .pytype/
# 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)
@@ -1193,6 +1196,14 @@ class Location():
return True return True
return False return False
def place_locked_item(self, item: Item):
if self.item:
raise Exception(f"Location {self} already filled.")
self.item = item
self.event = item.advancement
self.item.world = self.parent_region.world
self.locked = True
def __repr__(self): def __repr__(self):
return self.__str__() return self.__str__()
@@ -1221,7 +1232,7 @@ class Item():
zora_credit_text = None zora_credit_text = None
fluteboy_credit_text = None fluteboy_credit_text = None
def __init__(self, name: str, advancement: bool, code: int, player: int): def __init__(self, name: str, advancement: bool, code: Optional[int], player: int):
self.name = name self.name = name
self.advancement = advancement self.advancement = advancement
self.player = player self.player = player
@@ -1229,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
@@ -1440,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,
@@ -1467,6 +1478,7 @@ class Spoiler(object):
return json.dumps(out) return json.dumps(out)
def to_file(self, filename): def to_file(self, filename):
import Options
self.parse_data() self.parse_data()
def bool_to_text(variable: Union[bool, str]) -> str: def bool_to_text(variable: Union[bool, str]) -> str:
@@ -1490,16 +1502,21 @@ class Spoiler(object):
'Yes' if self.metadata['progression_balancing'][player] else 'No')) 'Yes' if self.metadata['progression_balancing'][player] else 'No'))
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
if player in self.world.hk_player_ids: if player in self.world.hk_player_ids:
import Options
for hk_option in Options.hollow_knight_options: for hk_option in Options.hollow_knight_options:
res = getattr(self.world, hk_option)[player] res = getattr(self.world, hk_option)[player]
outfile.write(f'{hk_option+":":33}{res}\n') outfile.write(f'{hk_option+":":33}{res}\n')
if player in self.world.minecraft_player_ids:
import Options elif player in self.world.factorio_player_ids:
for f_option in Options.factorio_options:
res = getattr(self.world, f_option)[player]
outfile.write(f'{f_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
elif player in self.world.minecraft_player_ids:
for mc_option in Options.minecraft_options: for mc_option in Options.minecraft_options:
res = getattr(self.world, mc_option)[player] res = getattr(self.world, mc_option)[player]
outfile.write(f'{mc_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n') outfile.write(f'{mc_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
if player in self.world.alttp_player_ids:
elif player in self.world.alttp_player_ids:
for team in range(self.world.teams): for team in range(self.world.teams):
outfile.write('%s%s\n' % ( outfile.write('%s%s\n' % (
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if
@@ -1551,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]))
@@ -1575,17 +1592,24 @@ class Spoiler(object):
'<=>' if entry['direction'] == 'both' else '<=>' if entry['direction'] == 'both' else
'<=' if entry['direction'] == 'exit' else '=>', '<=' if entry['direction'] == 'exit' else '=>',
entry['exit']) for entry in self.entrances.values()])) entry['exit']) for entry in self.entrances.values()]))
outfile.write('\n\nMedallions:\n')
for dungeon, medallion in self.medallions.items(): if self.medallions:
outfile.write(f'\n{dungeon}: {medallion}') outfile.write('\n\nMedallions:\n')
for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion}')
if self.startinventory: if self.startinventory:
outfile.write('\n\nStarting Inventory:\n\n') outfile.write('\n\nStarting Inventory:\n\n')
outfile.write('\n'.join(self.startinventory)) outfile.write('\n'.join(self.startinventory))
outfile.write('\n\nLocations:\n\n') outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()])) outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
outfile.write('\n\nShops:\n\n')
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops)) if self.shops:
for player in range(1, self.world.players + 1): outfile.write('\n\nShops:\n\n')
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
for player in self.world.alttp_player_ids:
if self.world.boss_shuffle[player] != 'none': if self.world.boss_shuffle[player] != 'none':
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
outfile.write(f'\n\nBosses{(f" ({self.world.get_player_names(player)})" if self.world.players > 1 else "")}:\n') outfile.write(f'\n\nBosses{(f" ({self.world.get_player_names(player)})" if self.world.players > 1 else "")}:\n')
@@ -1595,19 +1619,20 @@ class Spoiler(object):
if self.unreachables: if self.unreachables:
outfile.write('\n\nUnreachable Items:\n\n') outfile.write('\n\nUnreachable Items:\n\n')
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables])) outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
outfile.write('\n\nPaths:\n\n')
path_listings = [] if self.paths:
for location, path in sorted(self.paths.items()): outfile.write('\n\nPaths:\n\n')
path_lines = [] path_listings = []
for region, exit in path: for location, path in sorted(self.paths.items()):
if exit is not None: path_lines = []
path_lines.append("{} -> {}".format(region, exit)) for region, exit in path:
else: if exit is not None:
path_lines.append(region) path_lines.append("{} -> {}".format(region, exit))
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines))) else:
path_lines.append(region)
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
outfile.write('\n'.join(path_listings)) outfile.write('\n'.join(path_listings))
from worlds.alttp.Items import item_name_groups from worlds.alttp.Items import item_name_groups
from worlds.generic import PlandoItem, PlandoConnection from worlds.generic import PlandoItem, PlandoConnection

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

@@ -1,8 +1,10 @@
from __future__ import annotations
import os import os
import logging import logging
import json import json
import string import string
import copy import copy
import sys
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
import colorama import colorama
@@ -13,7 +15,7 @@ from MultiServer import mark_raw
import Utils import Utils
import random import random
from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
from worlds.factorio.Technologies import lookup_id_to_name from worlds.factorio.Technologies import lookup_id_to_name
@@ -21,7 +23,6 @@ rcon_port = 24242
rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32)) rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32))
save_name = "Archipelago" save_name = "Archipelago"
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO) logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
options = Utils.get_options() options = Utils.get_options()
executable = options["factorio_options"]["executable"] executable = options["factorio_options"]["executable"]
@@ -34,13 +35,14 @@ if not os.path.exists(executable):
else: else:
raise FileNotFoundError(executable) raise FileNotFoundError(executable)
import sys
server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:]) server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:])
threadpool = ThreadPoolExecutor(10) thread_pool = ThreadPoolExecutor(10)
class FactorioCommandProcessor(ClientCommandProcessor): class FactorioCommandProcessor(ClientCommandProcessor):
ctx: FactorioContext
@mark_raw @mark_raw
def _cmd_factorio(self, text: str) -> bool: def _cmd_factorio(self, text: str) -> bool:
"""Send the following command to the bound Factorio Server.""" """Send the following command to the bound Factorio Server."""
@@ -65,14 +67,16 @@ class FactorioContext(CommonContext):
super(FactorioContext, self).__init__(*args, **kwargs) super(FactorioContext, self).__init__(*args, **kwargs)
self.send_index = 0 self.send_index = 0
self.rcon_client = None self.rcon_client = None
self.awaiting_bridge = False
self.raw_json_text_parser = RawJSONtoTextParser(self) self.raw_json_text_parser = RawJSONtoTextParser(self)
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
async def server_auth(self, password_requested): async def server_auth(self, password_requested):
if password_requested and not self.password: if password_requested and not self.password:
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"
}]) }])
@@ -81,43 +85,50 @@ class FactorioContext(CommonContext):
logger.info(args["text"]) logger.info(args["text"])
if self.rcon_client: if self.rcon_client:
cleaned_text = args['text'].replace('"', '') cleaned_text = args['text'].replace('"', '')
self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")") self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
f"{cleaned_text}\")")
def on_print_json(self, args: dict): def on_print_json(self, args: dict):
if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]: if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
pass # don't want info on other player's local pickups. pass # don't want info on other player's local pickups.
copy_data = copy.deepcopy(args["data"]) # jsontotextparser is destructive currently text = self.raw_json_text_parser(copy.deepcopy(args["data"]))
logger.info(self.jsontotextparser(args["data"])) logger.info(text)
if self.rcon_client: if self.rcon_client:
cleaned_text = self.raw_json_text_parser(copy_data).replace('"', '') text = self.factorio_json_text_parser(args["data"])
self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")") cleaned_text = text.replace('"', '')
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
f"{cleaned_text}\")")
async def game_watcher(ctx: FactorioContext, bridge_file: str): async def game_watcher(ctx: FactorioContext, bridge_file: str):
bridge_logger = logging.getLogger("FactorioWatcher") bridge_logger = logging.getLogger("FactorioWatcher")
from worlds.factorio.Technologies import lookup_id_to_name from worlds.factorio.Technologies import lookup_id_to_name
bridge_counter = 0 bridge_counter = 0
try: try:
while 1: while not ctx.exit_event.is_set():
if os.path.exists(bridge_file): if os.path.exists(bridge_file):
bridge_logger.info("Found Factorio Bridge file.") bridge_logger.info("Found Factorio Bridge file.")
while 1: while not ctx.exit_event.is_set():
with open(bridge_file) as f: if ctx.awaiting_bridge:
data = json.load(f) ctx.awaiting_bridge = False
research_data = data["research_done"] with open(bridge_file) as f:
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data} data = json.load(f)
victory = data["victory"] research_data = data["research_done"]
ctx.auth = data["slot_name"] research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
ctx.seed_name = data["seed_name"] victory = data["victory"]
ctx.auth = data["slot_name"]
ctx.seed_name = data["seed_name"]
if not ctx.finished_game and victory: if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True ctx.finished_game = True
if ctx.locations_checked != research_data: if ctx.locations_checked != research_data:
bridge_logger.info(f"New researches done: " bridge_logger.info(
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}") f"New researches done: "
ctx.locations_checked = research_data f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) ctx.locations_checked = research_data
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
await asyncio.sleep(1) await asyncio.sleep(1)
else: else:
bridge_counter += 1 bridge_counter += 1
@@ -160,12 +171,13 @@ async def factorio_server_watcher(ctx: FactorioContext):
stream_factorio_output(factorio_process.stdout, factorio_queue) stream_factorio_output(factorio_process.stdout, factorio_queue)
stream_factorio_output(factorio_process.stderr, factorio_queue) stream_factorio_output(factorio_process.stderr, factorio_queue)
script_folder = None script_folder = None
progression_watcher = None
try: try:
while 1: while not ctx.exit_event.is_set():
while not factorio_queue.empty(): while not factorio_queue.empty():
msg = factorio_queue.get() msg = factorio_queue.get()
factorio_server_logger.info(msg) factorio_server_logger.info(msg)
if not ctx.rcon_client and "Hosting game at IP ADDR:" in msg: if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
# trigger lua interface confirmation # trigger lua interface confirmation
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')") ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
@@ -177,7 +189,10 @@ async def factorio_server_watcher(ctx: FactorioContext):
if os.path.exists(bridge_file): if os.path.exists(bridge_file):
os.remove(bridge_file) os.remove(bridge_file)
logging.info(f"Bridge File Path: {bridge_file}") logging.info(f"Bridge File Path: {bridge_file}")
asyncio.create_task(game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher") progression_watcher = asyncio.create_task(
game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher")
if not ctx.awaiting_bridge and "Archipelago Bridge File written for game tick " in msg:
ctx.awaiting_bridge = True
if ctx.rcon_client: if ctx.rcon_client:
while ctx.send_index < len(ctx.items_received): while ctx.send_index < len(ctx.items_received):
transfer_item: NetworkItem = ctx.items_received[ctx.send_index] transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
@@ -196,6 +211,11 @@ async def factorio_server_watcher(ctx: FactorioContext):
logging.exception(e) logging.exception(e)
logging.error("Aborted Factorio Server Bridge") logging.error("Aborted Factorio Server Bridge")
finally:
factorio_process.terminate()
if progression_watcher:
await progression_watcher
async def main(): async def main():
ctx = FactorioContext(None, None, True) ctx = FactorioContext(None, None, True)
@@ -226,6 +246,20 @@ async def main():
await input_task await input_task
class FactorioJSONtoTextParser(JSONtoTextParser):
def _handle_color(self, node: JSONMessagePart):
colors = node["color"].split(";")
for color in colors:
if color in {"red", "green", "blue", "orange", "yellow", "pink", "purple", "white", "black", "gray",
"brown", "cyan", "acid"}:
node["text"] = f"[color={color}]{node['text']}[/color]"
return self._handle_text(node)
elif color == "magenta":
node["text"] = f"[color=pink]{node['text']}[/color]"
return self._handle_text(node)
return self._handle_text(node)
if __name__ == '__main__': if __name__ == '__main__':
colorama.init() colorama.init()
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()

167
FactorioClientGUI.py Normal file
View File

@@ -0,0 +1,167 @@
import os
import logging
import sys
os.makedirs("logs", exist_ok=True)
if getattr(sys, "frozen", False):
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
filename=os.path.join("logs", "FactorioClient.txt"), filemode="w")
else:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w"))
os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1"
os.environ["KIVY_NO_ARGS"] = "1"
import asyncio
from CommonClient import server_loop, logger
from FactorioClient import FactorioContext, factorio_server_watcher
async def main():
ctx = FactorioContext(None, None, True)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
ui_app = FactorioManager(ctx)
ui_task = asyncio.create_task(ui_app.async_run(), name="UI")
await ctx.exit_event.wait() # wait for signal to exit application
ui_app.stop()
ctx.server_address = None
ctx.snes_reconnect_address = None
# allow tasks to quit
await ui_task
await factorio_server_task
await ctx.server_task
if ctx.server is not None and not ctx.server.socket.closed:
await ctx.server.socket.close()
if ctx.server_task is not None:
await ctx.server_task
while ctx.input_requests > 0: # clear queue for shutdown
ctx.input_queue.put_nowait(None)
ctx.input_requests -= 1
from kivy.app import App
from kivy.uix.label import Label
from kivy.base import ExceptionHandler, ExceptionManager, Config
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
from kivy.uix.recycleview import RecycleView
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
from kivy.lang import Builder
class FactorioManager(App):
def __init__(self, ctx):
super(FactorioManager, self).__init__()
self.ctx = ctx
self.commandprocessor = ctx.command_processor(ctx)
self.icon = "data/icon.png"
def build(self):
self.grid = GridLayout()
self.grid.cols = 1
self.tabs = TabbedPanel()
self.tabs.default_tab_text = "All"
self.title = "Archipelago Factorio Client"
pairs = [
("Client", "Archipelago"),
("FactorioServer", "Factorio Server Log"),
("FactorioWatcher", "Bridge File Log"),
]
self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name) for logger_name, name in pairs))
for logger_name, display_name in pairs:
bridge_logger = logging.getLogger(logger_name)
panel = TabbedPanelItem(text=display_name)
panel.content = UILog(bridge_logger)
self.tabs.add_widget(panel)
self.grid.add_widget(self.tabs)
textinput = TextInput(size_hint_y=None, height=30, multiline=False)
textinput.bind(on_text_validate=self.on_message)
self.grid.add_widget(textinput)
self.commandprocessor("/help")
return self.grid
def on_stop(self):
self.ctx.exit_event.set()
def on_message(self, textinput: TextInput):
try:
input_text = textinput.text.strip()
textinput.text = ""
if self.ctx.input_requests > 0:
self.ctx.input_requests -= 1
self.ctx.input_queue.put_nowait(input_text)
elif input_text:
self.commandprocessor(input_text)
except Exception as e:
logger.exception(e)
class LogtoUI(logging.Handler):
def __init__(self, on_log):
super(LogtoUI, self).__init__(logging.DEBUG)
self.on_log = on_log
def handle(self, record: logging.LogRecord) -> None:
self.on_log(record)
class UILog(RecycleView):
cols = 1
def __init__(self, *loggers_to_handle, **kwargs):
super(UILog, self).__init__(**kwargs)
self.data = []
for logger in loggers_to_handle:
logger.addHandler(LogtoUI(self.on_log))
def on_log(self, record: logging.LogRecord) -> None:
self.data.append({"text": record.getMessage()})
class E(ExceptionHandler):
def handle_exception(self, inst):
logger.exception(inst)
return ExceptionManager.RAISE
ExceptionManager.add_handler(E())
Config.set("input", "mouse", "mouse,disable_multitouch")
Builder.load_string('''
<TabbedPanel>
tab_width: 200
<Row@Label>:
canvas.before:
Color:
rgba: 0.2, 0.2, 0.2, 1
Rectangle:
size: self.size
pos: self.pos
text_size: self.width, None
size_hint_y: None
height: self.texture_size[1]
font_size: dp(20)
<UILog>:
viewclass: 'Row'
scroll_y: 0
effect_cls: "ScrollEffect"
RecycleBoxLayout:
default_size: None, dp(20)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(3)
''')
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

143
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,73 +363,76 @@ 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:
placement: PlandoItem try:
for placement in world.plando_items[player]: placement: PlandoItem
if placement.location in key_drop_data: for placement in world.plando_items[player]:
placement.warn( if placement.location in key_drop_data:
f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.") placement.warn(
continue f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
item = ItemFactory(placement.item, player) continue
target_world: int = placement.world item = ItemFactory(placement.item, player)
if target_world is False or world.players == 1: target_world: int = placement.world
target_world = player # in own world if target_world is False or world.players == 1:
elif target_world is True: # in any other world target_world = player # in own world
unfilled = list(location for location in world.get_unfilled_locations_for_players( elif target_world is True: # in any other world
placement.location, unfilled = list(location for location in world.get_unfilled_locations_for_players(
set(world.player_ids) - {player}) if location.item_rule(item) placement.location,
) set(world.player_ids) - {player}) if location.item_rule(item)
if not unfilled: )
placement.failed(f"Could not find a world with an unfilled location {placement.location}", if not unfilled:
FillError) placement.failed(f"Could not find a world with an unfilled location {placement.location}",
FillError)
continue
target_world = world.random.choice(unfilled).player
elif target_world is None: # any random world
unfilled = list(location for location in world.get_unfilled_locations_for_players(
placement.location,
set(world.player_ids)) if location.item_rule(item)
)
if not unfilled:
placement.failed(f"Could not find a world with an unfilled location {placement.location}",
FillError)
continue
target_world = world.random.choice(unfilled).player
elif type(target_world) == int: # target world by player id
if target_world not in range(1, world.players + 1):
placement.failed(
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
ValueError)
continue
else: # find world by name
if target_world not in world_name_lookup:
placement.failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
ValueError)
continue
target_world = world_name_lookup[target_world]
location = world.get_location(placement.location, target_world)
if location.item:
placement.failed(f"Cannot place item into already filled location {location}.")
continue continue
target_world = world.random.choice(unfilled).player if location.can_fill(world.state, item, False):
world.push_item(location, item, collect=False)
elif target_world is None: # any random world location.event = True # flag location to be checked during fill
unfilled = list(location for location in world.get_unfilled_locations_for_players( location.locked = True
placement.location, logging.debug(f"Plando placed {item} at {location}")
set(world.player_ids)) if location.item_rule(item) else:
) placement.failed(f"Can't place {item} at {location} due to fill condition not met.")
if not unfilled:
placement.failed(f"Could not find a world with an unfilled location {placement.location}",
FillError)
continue continue
target_world = world.random.choice(unfilled).player if placement.from_pool: # Should happen AFTER the item is placed, in case it was allowed to skip failed placement.
try:
elif type(target_world) == int: # target world by player id world.itempool.remove(item)
if target_world not in range(1, world.players + 1): except ValueError:
placement.failed( placement.warn(f"Could not remove {item} from pool as it's already missing from it.")
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})", except Exception as e:
ValueError) raise Exception(f"Error running plando for player {player} ({world.player_names[player]})") from e
continue
else: # find world by name
if target_world not in world_name_lookup:
placement.failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
ValueError)
continue
target_world = world_name_lookup[target_world]
location = world.get_location(placement.location, target_world)
if location.item:
placement.failed(f"Cannot place item into already filled location {location}.")
continue
if location.can_fill(world.state, item, False):
world.push_item(location, item, collect=False)
location.event = True # flag location to be checked during fill
location.locked = True
logging.debug(f"Plando placed {item} at {location}")
else:
placement.failed(f"Can't place {item} at {location} due to fill condition not met.")
continue
if placement.from_pool: # Should happen AFTER the item is placed, in case it was allowed to skip failed placement.
try:
world.itempool.remove(item)
except ValueError:
placement.warn(f"Could not remove {item} from pool as it's already missing from it.")

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

@@ -5,10 +5,8 @@ import tkinter as tk
from Utils import local_path from Utils import local_path
def set_icon(window): def set_icon(window):
er16 = tk.PhotoImage(file=local_path('data', 'ER16.gif')) logo = tk.PhotoImage(file=local_path('data', 'icon.png'))
er32 = tk.PhotoImage(file=local_path('data', 'ER32.gif')) window.tk.call('wm', 'iconphoto', window._w, logo)
er48 = tk.PhotoImage(file=local_path('data', 'ER32.gif'))
window.tk.call('wm', 'iconphoto', window._w, er16, er32, er48) # pylint: disable=protected-access
# Although tkinter is intended to be thread safe, there are many reports of issues # Although tkinter is intended to be thread safe, there are many reports of issues
# some which may be platform specific, or depend on if the TCL library was compiled without # some which may be platform specific, or depend on if the TCL library was compiled without

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):
@@ -153,8 +153,8 @@ def adjust(args):
def adjustGUI(): def adjustGUI():
from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, Tk, LEFT, RIGHT, BOTTOM, TOP, \ from tkinter import Tk, LEFT, BOTTOM, TOP, \
StringVar, IntVar, Frame, Label, W, E, X, BOTH, Entry, Spinbox, Button, filedialog, messagebox, ttk StringVar, Frame, Label, X, Entry, Button, filedialog, messagebox, ttk
from Gui import get_rom_options_frame, get_rom_frame from Gui import get_rom_options_frame, get_rom_frame
from GuiUtils import set_icon from GuiUtils import set_icon
from argparse import Namespace from argparse import Namespace

View File

@@ -1,18 +1,13 @@
import argparse import argparse
import atexit import atexit
import time import time
import functools
import webbrowser
import multiprocessing import multiprocessing
import socket
import os import os
import subprocess import subprocess
import base64 import base64
import shutil import shutil
from json import loads, dumps from json import loads, dumps
from random import randrange
from Utils import get_item_name_from_id from Utils import get_item_name_from_id
exit_func = atexit.register(input, "Press enter to close.") exit_func = atexit.register(input, "Press enter to close.")
@@ -102,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"
}]) }])
@@ -143,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),
@@ -709,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 = []
@@ -723,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)

229
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
@@ -79,7 +76,7 @@ def main(args, seed=None):
world.progressive = args.progressive.copy() world.progressive = args.progressive.copy()
world.goal = args.goal.copy() world.goal = args.goal.copy()
world.local_items = args.local_items.copy() world.local_items = args.local_items.copy()
if hasattr(args, "algorithm"): # current GUI options if hasattr(args, "algorithm"): # current GUI options
world.algorithm = args.algorithm world.algorithm = args.algorithm
world.shuffleganon = args.shuffleganon world.shuffleganon = args.shuffleganon
world.custom = args.custom world.custom = args.custom
@@ -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,18 +123,13 @@ 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))
if "-" in world.shuffle[player]: if "-" in world.shuffle[player]:
@@ -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):
@@ -171,14 +161,15 @@ def main(args, seed=None):
world.player_names[player].append(name) world.player_names[player].append(name)
logger.info('') logger.info('')
for player in world.alttp_player_ids:
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
for player in world.player_ids: for player in world.player_ids:
for item_name in args.startinventory[player]: for item_name in args.startinventory[player]:
item = Item(item_name, True, lookup_any_item_name_to_id[item_name], player) item = Item(item_name, True, lookup_any_item_name_to_id[item_name], player)
item.game = world.game[player]
world.push_precollected(item) world.push_precollected(item)
for player in world.alttp_player_ids:
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
for player in world.player_ids: for player in world.player_ids:
# enforce pre-defined local items. # enforce pre-defined local items.
@@ -208,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)
@@ -264,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)
@@ -336,13 +331,13 @@ def main(args, seed=None):
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash) world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
palettes_options={} palettes_options = {}
palettes_options['dungeon']=args.uw_palettes[player] palettes_options['dungeon'] = args.uw_palettes[player]
palettes_options['overworld']=args.ow_palettes[player] palettes_options['overworld'] = args.ow_palettes[player]
palettes_options['hud']=args.hud_palettes[player] palettes_options['hud'] = args.hud_palettes[player]
palettes_options['sword']=args.sword_palettes[player] palettes_options['sword'] = args.sword_palettes[player]
palettes_options['shield']=args.shield_palettes[player] palettes_options['shield'] = args.shield_palettes[player]
palettes_options['link']=args.link_palettes[player] palettes_options['link'] = args.link_palettes[player]
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
args.fastmenu[player], args.disablemusic[player], args.sprite[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player],
@@ -358,8 +353,8 @@ def main(args, seed=None):
world.bigkeyshuffle[player]].count(True) == 1: world.bigkeyshuffle[player]].count(True) == 1:
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else \ mcsb_name = '-mapshuffle' if world.mapshuffle[player] else \
'-compassshuffle' if world.compassshuffle[player] else \ '-compassshuffle' if world.compassshuffle[player] else \
'-universal_keys' if world.keyshuffle[player] == "universal" else \ '-universal_keys' if world.keyshuffle[player] == "universal" else \
'-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle' '-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
world.bigkeyshuffle[player]]): world.bigkeyshuffle[player]]):
mcsb_name = '-%s%s%s%sshuffle' % ( mcsb_name = '-%s%s%s%sshuffle' % (
@@ -371,59 +366,59 @@ def main(args, seed=None):
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" \ outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" \
if world.player_names[player][team] != 'Player%d' % player else '' if world.player_names[player][team] != 'Player%d' % player else ''
outfilestuffs = { outfilestuffs = {
"logic": world.logic[player], # 0 "logic": world.logic[player], # 0
"difficulty": world.difficulty[player], # 1 "difficulty": world.difficulty[player], # 1
"item_functionality": world.item_functionality[player], # 2 "item_functionality": world.item_functionality[player], # 2
"mode": world.mode[player], # 3 "mode": world.mode[player], # 3
"goal": world.goal[player], # 4 "goal": world.goal[player], # 4
"timer": str(world.timer[player]), # 5 "timer": str(world.timer[player]), # 5
"shuffle": world.shuffle[player], # 6 "shuffle": world.shuffle[player], # 6
"algorithm": world.algorithm, # 7 "algorithm": world.algorithm, # 7
"mscb": mcsb_name, # 8 "mscb": mcsb_name, # 8
"retro": world.retro[player], # 9 "retro": world.retro[player], # 9
"progressive": world.progressive, # A "progressive": world.progressive, # A
"hints": 'True' if world.hints[player] else 'False' # B "hints": 'True' if world.hints[player] else 'False' # B
} }
# 0 1 2 3 4 5 6 7 8 9 A B # 0 1 2 3 4 5 6 7 8 9 A B
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % ( outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (
# 0 1 2 3 4 5 6 7 8 9 A B C # 0 1 2 3 4 5 6 7 8 9 A B C
# _noglitches_normal-normal-open-ganon-ohko_simple-balanced-keysanity-retro-prog_random-nohints # _noglitches_normal-normal-open-ganon-ohko_simple-balanced-keysanity-retro-prog_random-nohints
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity-retro # _noglitches_normal-normal-open-ganon _simple-balanced-keysanity-retro
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -prog_random # _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -prog_random
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -nohints # _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -nohints
outfilestuffs["logic"], # 0 outfilestuffs["logic"], # 0
outfilestuffs["difficulty"], # 1 outfilestuffs["difficulty"], # 1
outfilestuffs["item_functionality"], # 2 outfilestuffs["item_functionality"], # 2
outfilestuffs["mode"], # 3 outfilestuffs["mode"], # 3
outfilestuffs["goal"], # 4 outfilestuffs["goal"], # 4
"" if outfilestuffs["timer"] in ['False', 'none', 'display'] else "-" + outfilestuffs["timer"], # 5 "" if outfilestuffs["timer"] in ['False', 'none', 'display'] else "-" + outfilestuffs["timer"], # 5
outfilestuffs["shuffle"], # 6 outfilestuffs["shuffle"], # 6
outfilestuffs["algorithm"], # 7 outfilestuffs["algorithm"], # 7
outfilestuffs["mscb"], # 8 outfilestuffs["mscb"], # 8
"-retro" if outfilestuffs["retro"] == "True" else "", # 9 "-retro" if outfilestuffs["retro"] == "True" else "", # 9
"-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # A "-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # A
"-nohints" if not outfilestuffs["hints"] == "True" else "") # B "-nohints" if not outfilestuffs["hints"] == "True" else "") # B
) if not args.outputname else '' ) if not args.outputname else ''
rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc') rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
rom.write_to_file(rompath, hide_enemizer=True) rom.write_to_file(rompath, hide_enemizer=True)
if args.create_diff: if args.create_diff:
Patch.create_patch_file(rompath, player=player, player_name = world.player_names[player][team]) Patch.create_patch_file(rompath, player=player, player_name=world.player_names[player][team])
return player, team, bytes(rom.name) return player, team, bytes(rom.name)
pool = concurrent.futures.ThreadPoolExecutor() pool = concurrent.futures.ThreadPoolExecutor()
multidata_task = 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:
@@ -433,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:
@@ -459,7 +455,7 @@ def main(args, seed=None):
checks_in_area[location.player]["Light World"].append(location.address) checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.dungeon: elif location.parent_region.dungeon:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower', dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'}\ 'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address) checks_in_area[location.player][dungeonname].append(location.address)
elif main_entrance.parent_region.type == RegionType.LightWorld: elif main_entrance.parent_region.type == RegionType.LightWorld:
@@ -471,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
@@ -486,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:
@@ -501,29 +497,34 @@ def main(args, seed=None):
minimum_versions = {"server": (0, 1, 1), "clients": client_versions} minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
games = {} games = {}
for slot in world.player_ids: for slot in world.player_ids:
client_versions[slot] = (0, 0, 3) if world.game[slot] == "Factorio":
client_versions[slot] = (0, 1, 2)
else:
client_versions[slot] = (0, 0, 3)
games[slot] = world.game[slot] games[slot] = world.game[slot]
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
slot, team, rom_name in rom_names} slot, team, rom_name in rom_names}
precollected_items = {player: [] for player in range(1, world.players+1)} precollected_items = {player: [] for player in range(1, world.players + 1)}
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)
for slot in world.hk_player_ids: if world.hk_player_ids:
slots_data = slot_data[slot] = {} import Options
for option_name in Options.hollow_knight_options: for slot in world.hk_player_ids:
option = getattr(world, option_name)[slot] slots_data = slot_data[slot] = {}
slots_data[option_name] = int(option.value) for option_name in Options.hollow_knight_options:
option = getattr(world, option_name)[slot]
slots_data[option_name] = int(option.value)
for slot in world.minecraft_player_ids: for slot in world.minecraft_player_ids:
slot_data[slot] = fill_minecraft_slot_data(world, slot) slot_data[slot] = fill_minecraft_slot_data(world, slot)
@@ -544,7 +545,7 @@ def main(args, seed=None):
precollected_hints[location.item.player].add(hint) precollected_hints[location.item.player].add(hint)
multidata = zlib.compress(pickle.dumps({ multidata = zlib.compress(pickle.dumps({
"slot_data" : slot_data, "slot_data": slot_data,
"games": games, "games": games,
"names": parsed_names, "names": parsed_names,
"connect_names": connect_names, "connect_names": connect_names,
@@ -556,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
@@ -565,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.")
@@ -577,7 +578,7 @@ def main(args, seed=None):
if multidata_task: if multidata_task:
multidata_task.result() # retrieve exception if one exists multidata_task.result() # retrieve exception if one exists
pool.shutdown() # wait for all queued tasks to complete pool.shutdown() # wait for all queued tasks to complete
for player in world.minecraft_player_ids: # Doing this after shutdown prevents the .apmc from being generated if there's an error for player in world.minecraft_player_ids: # Doing this after shutdown prevents the .apmc from being generated if there's an error
generate_mc_data(world, player) generate_mc_data(world, player)
if not args.skip_playthrough: if not args.skip_playthrough:
logger.info('Calculating playthrough.') logger.info('Calculating playthrough.')
@@ -632,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]):
@@ -677,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}')
@@ -696,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

@@ -1,55 +1,48 @@
import os import os
import sys import sys
import subprocess import subprocess
import importlib import pkg_resources
requirements_files = {'requirements.txt'}
if sys.version_info < (3, 8, 6): if sys.version_info < (3, 8, 6):
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.") raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
update_ran = hasattr(sys, "frozen") and getattr(sys, "frozen") # don't run update if environment is frozen/compiled update_ran = getattr(sys, "frozen", False) # don't run update if environment is frozen/compiled
if not update_ran:
for entry in os.scandir("worlds"):
if entry.is_dir():
req_file = os.path.join(entry.path, "requirements.txt")
if os.path.exists(req_file):
requirements_files.add(req_file)
def update_command(): def update_command():
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt', '--upgrade']) for file in requirements_files:
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade'])
naming_specialties = {"PyYAML": "yaml", # PyYAML is imported as the name yaml
"maseya-z3pr": "maseya",
"factorio-rcon-py": "factorio_rcon"}
def update(): def update():
global update_ran global update_ran
if not update_ran: if not update_ran:
update_ran = True update_ran = True
path = os.path.join(os.path.dirname(sys.argv[0]), 'requirements.txt') for req_file in requirements_files:
if not os.path.exists(path): path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
path = os.path.join(os.path.dirname(__file__), 'requirements.txt') if not os.path.exists(path):
with open(path) as requirementsfile: path = os.path.join(os.path.dirname(__file__), req_file)
for line in requirementsfile.readlines(): with open(path) as requirementsfile:
module, remote_version = line.split(">=") requirements = pkg_resources.parse_requirements(requirementsfile)
module = naming_specialties.get(module, module) for requirement in requirements:
try: requirement = str(requirement)
module = importlib.import_module(module) try:
except: pkg_resources.require(requirement)
import traceback except pkg_resources.ResolutionError:
traceback.print_exc() import traceback
input(f'Required python module {module} not found, press enter to install it') traceback.print_exc()
update_command() input(f'Requirement {requirement} is not satisfied, press enter to install it')
return update_command()
else: return
if hasattr(module, "__version__"):
module_version = module.__version__
module = module.__name__ # also unloads the module to make it writable
if type(module_version) == str:
module_version = tuple(int(part.strip()) for part in module_version.split("."))
remote_version = tuple(int(part.strip()) for part in remote_version.split("."))
if module_version < remote_version:
input(f'Required python module {module} is outdated ({module_version}<{remote_version}),'
' press enter to upgrade it')
update_command()
return
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -45,7 +45,6 @@ if __name__ == "__main__":
zip_multidata = multi_mystery_options["zip_multidata"] zip_multidata = multi_mystery_options["zip_multidata"]
zip_format = multi_mystery_options["zip_format"] zip_format = multi_mystery_options["zip_format"]
# zip_password = multi_mystery_options["zip_password"] not at this time # zip_password = multi_mystery_options["zip_password"] not at this time
player_name = multi_mystery_options["player_name"]
meta_file_path = multi_mystery_options["meta_file_path"] meta_file_path = multi_mystery_options["meta_file_path"]
weights_file_path = multi_mystery_options["weights_file_path"] weights_file_path = multi_mystery_options["weights_file_path"]
pre_roll = multi_mystery_options["pre_roll"] pre_roll = multi_mystery_options["pre_roll"]
@@ -76,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
@@ -124,15 +123,6 @@ if __name__ == "__main__":
spoilername = f"AP_{seed_name}_Spoiler.txt" spoilername = f"AP_{seed_name}_Spoiler.txt"
romfilename = "" romfilename = ""
if player_name:
for file in os.listdir(output_path):
if player_name in file:
import MultiClient
import asyncio
asyncio.run(MultiClient.run_game(os.path.join(output_path, file)))
break
if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs, zip_apmcs)): if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs, zip_apmcs)):
import zipfile import zipfile
@@ -167,7 +157,7 @@ if __name__ == "__main__":
def _handle_sfc_file(file: str): def _handle_sfc_file(file: str):
if zip_roms: if zip_roms:
pack_file(file) pack_file(file)
if zip_roms == 2 and player_name.lower() not in file.lower(): if zip_roms == 2:
remove_zipped_file(file) remove_zipped_file(file)
@@ -217,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] = []
@@ -75,10 +76,10 @@ class Context(Node):
self.save_filename = None self.save_filename = None
self.saving = False self.saving = False
self.player_names = {} self.player_names = {}
self.connect_names = {} # names of slots clients can connect to self.connect_names = {} # names of slots clients can connect to
self.allow_forfeits = {} self.allow_forfeits = {}
self.remote_items = set() self.remote_items = set()
self.locations:typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]] = {} self.locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]] = {}
self.host = host self.host = host
self.port = port self.port = port
self.server_password = server_password self.server_password = server_password
@@ -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()]
@@ -174,7 +174,7 @@ class Context(Node):
for key, value in server_options.items(): for key, value in server_options.items():
data_type = self.simple_options.get(key, None) data_type = self.simple_options.get(key, None)
if data_type is not None: if data_type is not None:
if value not in {False, True, None}: # some can be boolean OR text, such as password if value not in {False, True, None}: # some can be boolean OR text, such as password
try: try:
value = data_type(value) value = data_type(value)
except Exception as e: except Exception as e:
@@ -200,7 +200,7 @@ class Context(Node):
return False return False
def _save(self, exit_save:bool=False) -> bool: def _save(self, exit_save: bool = False) -> bool:
try: try:
encoded_save = pickle.dumps(self.get_save()) encoded_save = pickle.dumps(self.get_save())
with open(self.save_filename, "wb") as f: with open(self.save_filename, "wb") as f:
@@ -366,7 +366,8 @@ 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:
logging.info("Disconnected") if ctx.log_network:
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': [
in ctx.endpoints if client.auth], 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],
# 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)
@@ -447,7 +452,7 @@ def get_received_items(ctx: Context, team: int, player: int) -> typing.List[Netw
def send_new_items(ctx: Context): def send_new_items(ctx: Context):
for client in ctx.endpoints: for client in ctx.endpoints:
if client.auth: # can't send to disconnected client if client.auth: # can't send to disconnected client
items = get_received_items(ctx, client.team, client.slot) items = get_received_items(ctx, client.team, client.slot)
if len(items) > client.send_index: if len(items) > client.send_index:
asyncio.create_task(ctx.send_msgs(client, [{ asyncio.create_task(ctx.send_msgs(client, [{
@@ -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]
@@ -684,11 +690,12 @@ class CommonCommandProcessor(CommandProcessor):
"""List all current options. Warning: lists password.""" """List all current options. Warning: lists password."""
self.output("Current options:") self.output("Current options:")
for option in self.ctx.simple_options: for option in self.ctx.simple_options:
if option == "server_password" and self.marker == "!": #Do not display the server password to the client. if option == "server_password" and self.marker == "!": # Do not display the server password to the client.
self.output(f"Option server_password is set to {('*' * random.randint(4,16))}") self.output(f"Option server_password is set to {('*' * random.randint(4, 16))}")
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:
@@ -959,7 +971,7 @@ def get_client_points(ctx: Context, client: Client) -> int:
async def process_client_cmd(ctx: Context, client: Client, args: dict): async def process_client_cmd(ctx: Context, client: Client, args: dict):
try: try:
cmd:str = args["cmd"] cmd: str = args["cmd"]
except: except:
logging.exception(f"Could not get command from {args}") logging.exception(f"Could not get command from {args}")
raise raise
@@ -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}")
@@ -1045,7 +1057,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
items = get_received_items(ctx, client.team, client.slot) items = get_received_items(ctx, client.team, client.slot)
if items: if items:
client.send_index = len(items) client.send_index = len(items)
await ctx.send_msgs(client, [{"cmd": "ReceivedItems","index": 0, await ctx.send_msgs(client, [{"cmd": "ReceivedItems", "index": 0,
"items": items}]) "items": items}])
elif cmd == 'LocationChecks': elif cmd == 'LocationChecks':
@@ -1067,11 +1079,12 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
if cmd == 'Say': if cmd == 'Say':
if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable(): if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable():
await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text" : 'Say'}]) await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'Say'}])
return return
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:
@@ -1356,7 +1372,7 @@ async def auto_shutdown(ctx, to_cancel=None):
async def main(args: argparse.Namespace): async def main(args: argparse.Namespace):
logging.basicConfig(force = True, logging.basicConfig(force=True,
format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO)) format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points, ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,

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,35 +392,32 @@ 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
return weights return weights
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,41 +544,57 @@ 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
ret.plando_connections = []
if "connections" in plando_options:
options = game_weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
get_choice("direction", placement, "both")
))
else: else:
raise Exception(f"Unsupported game {ret.game}") raise Exception(f"Unsupported game {ret.game}")
return ret return ret
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")
@@ -612,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':
@@ -636,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)))
@@ -644,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:
@@ -670,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)
@@ -782,49 +788,43 @@ 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 ''
ret.sprite += '-enter' if get_choice('on_enter', randomoneventweights, False) else '' ret.sprite += '-enter' if get_choice('on_enter', randomoneventweights, False) else ''
ret.sprite += '-exit' if get_choice('on_exit', randomoneventweights, False) else '' ret.sprite += '-exit' if get_choice('on_exit', randomoneventweights, False) else ''
ret.sprite += '-slash' if get_choice('on_slash', randomoneventweights, False) else '' ret.sprite += '-slash' if get_choice('on_slash', randomoneventweights, False) else ''
ret.sprite += '-item' if get_choice('on_item', randomoneventweights, False) else '' ret.sprite += '-item' if get_choice('on_item', randomoneventweights, False) else ''
ret.sprite += '-bonk' if get_choice('on_bonk', randomoneventweights, False) else '' ret.sprite += '-bonk' if get_choice('on_bonk', randomoneventweights, False) else ''
ret.sprite = 'randomonall' if get_choice('on_everything', randomoneventweights, False) else ret.sprite ret.sprite = 'randomonall' if get_choice('on_everything', randomoneventweights, False) else ret.sprite
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,8 +8,9 @@ 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:
options.update(base.options) if hasattr(base, "options"):
name_lookup.update(name_lookup) options.update(base.options)
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
name.startswith("option_")} name.startswith("option_")}
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()}) attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
@@ -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()
@@ -139,13 +209,18 @@ class OptionDict(Option):
else: else:
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
def get_option_name(self):
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):
@@ -171,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):
@@ -204,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)] = {
@@ -282,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):
@@ -309,28 +375,48 @@ class TechTreeLayout(Choice):
option_single = 0 option_single = 0
option_small_diamonds = 1 option_small_diamonds = 1
option_medium_diamonds = 2 option_medium_diamonds = 2
option_pyramid = 3 option_large_diamonds = 3
option_funnel = 4 option_small_pyramids = 4
option_medium_pyramids = 5
option_large_pyramids = 6
option_small_funnels = 7
option_medium_funnels = 8
option_large_funnels = 9
option_funnels = 4
alias_pyramid = 6
alias_funnel = 9
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):
option_vanilla = 0
option_fast = 1
option_normal = 2
option_slow = 4
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)] = {
"tech_tree_layout": TechTreeLayout, "max_science_pack": MaxSciencePack,
"tech_cost": TechCost, "tech_tree_layout": TechTreeLayout,
"free_samples": FreeSamples, "tech_cost": TechCost,
"visibility": Visibility, "free_samples": FreeSamples,
"random_tech_ingredients": Toggle, "tech_tree_information": TechTreeInformation,
"starting_items": FactorioStartItems} "starting_items": FactorioStartItems,
"recipe_time": RecipeTime,
"imported_blueprints": DefaultOnToggle,
}
class AdvancementGoal(Choice): class AdvancementGoal(Choice):
@@ -356,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.1" __version__ = "0.1.3"
_version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
import builtins import builtins
import os import os
@@ -188,7 +188,7 @@ def get_default_options() -> dict:
"server_password": None, "server_password": None,
"disable_item_cheat": False, "disable_item_cheat": False,
"location_check_points": 1, "location_check_points": 1,
"hint_cost": 1000, "hint_cost": 10,
"forfeit_mode": "goal", "forfeit_mode": "goal",
"remaining_mode": "goal", "remaining_mode": "goal",
"auto_shutdown": 0, "auto_shutdown": 0,
@@ -203,10 +203,10 @@ def get_default_options() -> dict:
"weights_file_path": "weights.yaml", "weights_file_path": "weights.yaml",
"meta_file_path": "meta.yaml", "meta_file_path": "meta.yaml",
"pre_roll": False, "pre_roll": False,
"player_name": "",
"create_spoiler": 1, "create_spoiler": 1,
"zip_roms": 0, "zip_roms": 0,
"zip_diffs": 2, "zip_diffs": 2,
"zip_apmcs": 1,
"zip_spoiler": 0, "zip_spoiler": 0,
"zip_multidata": 1, "zip_multidata": 1,
"zip_format": 1, "zip_format": 1,
@@ -345,12 +345,12 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
f"Enter yes, no or never: ") f"Enter yes, no or never: ")
if adjust_wanted and adjust_wanted.startswith("y"): if adjust_wanted and adjust_wanted.startswith("y"):
if hasattr(adjuster_settings, "sprite_pool"): if hasattr(adjuster_settings, "sprite_pool"):
from Adjuster import AdjusterWorld from LttPAdjuster import AdjusterWorld
adjuster_settings.world = AdjusterWorld(getattr(adjuster_settings, "sprite_pool")) adjuster_settings.world = AdjusterWorld(getattr(adjuster_settings, "sprite_pool"))
adjusted = True adjusted = True
import Adjuster import LttPAdjuster
_, romfile = Adjuster.adjust(adjuster_settings) _, romfile = LttPAdjuster.adjust(adjuster_settings)
if hasattr(adjuster_settings, "world"): if hasattr(adjuster_settings, "world"):
delattr(adjuster_settings, "world") delattr(adjuster_settings, "world")

View File

@@ -2,6 +2,10 @@ import os
import multiprocessing import multiprocessing
import logging import logging
import ModuleUpdate
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
ModuleUpdate.update()
from WebHostLib import app as raw_app from WebHostLib import app as raw_app
from waitress import serve from waitress import serve

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

@@ -1,4 +1,4 @@
flask>=2.0.0 flask>=2.0.1
pony>=0.7.14 pony>=0.7.14
waitress>=2.0.0 waitress>=2.0.0
flask-caching>=1.10.1 flask-caching>=1.10.1

View File

@@ -1,7 +1,7 @@
# A Link to the Past Randomizer Setup Guide # A Link to the Past Randomizer Setup Guide
## Benötigte Software ## Benötigte Software
- [MultiWorld Utilities](https://github.com/Berserker66/MultiWorld-Utilities/releases) - [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities) - [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien - Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien
- Ein Emulator, der lua-scripts abspielen kann - Ein Emulator, der lua-scripts abspielen kann
@@ -15,7 +15,7 @@
### Windows ### Windows
1. Lade die Multiworld Utilities herunter und führe die Installation aus. Sei sicher, dass du immer die 1. Lade die Multiworld Utilities herunter und führe die Installation aus. Sei sicher, dass du immer die
aktuellste Version installiert hast.**Die Datei befindet sich im "assets"-Kasten unter der jeweiligen Versionsinfo!**. aktuellste Version installiert hast.**Die Datei befindet sich im "assets"-Kasten unter der jeweiligen Versionsinfo!**.
Für normale Multiworld-Spiele lädst du die `Setup.BerserkerMultiworld.exe` herunter. Für normale Multiworld-Spiele lädst du die `Setup.Archipelago.exe` herunter.
- Für den Doorrandomizer muss die alternative doors-Variante geladen werden. - Für den Doorrandomizer muss die alternative doors-Variante geladen werden.
- Während der Installation fragt dich das Programm nach der japanischen 1.0 ROM-Datei. Wenn du die Software - Während der Installation fragt dich das Programm nach der japanischen 1.0 ROM-Datei. Wenn du die Software
bereits installiert hast und einfach nur updaten willst, wirst du nicht nochmal danach gefragt. bereits installiert hast und einfach nur updaten willst, wirst du nicht nochmal danach gefragt.

View File

@@ -7,7 +7,7 @@
</div> </div>
## Required Software ## Required Software
- [MultiWorld Utilities](https://github.com/Berserker66/MultiWorld-Utilities/releases) - [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities) - [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
- Hardware or software capable of loading and playing SNES ROM files - Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of running Lua scripts - An emulator capable of running Lua scripts
@@ -21,7 +21,7 @@
### Windows Setup ### Windows Setup
1. Download and install the MultiWorld Utilities from the link above, making sure to install the most recent version. 1. Download and install the MultiWorld Utilities from the link above, making sure to install the most recent version.
**The file is located in the assets section at the bottom of the version information**. If you intend to play normal **The file is located in the assets section at the bottom of the version information**. If you intend to play normal
multiworld games, you want `Setup.BerserkerMultiWorld.exe` multiworld games, you want `Setup.Archipelago.exe`
- If you intend to play the doors variant of multiworld, you will want to download the alternate doors file. - If you intend to play the doors variant of multiworld, you will want to download the alternate doors file.
- During the installation process, you will be asked to browse for your Japanese 1.0 ROM file. If you have - During the installation process, you will be asked to browse for your Japanese 1.0 ROM file. If you have
installed this software before and are simply upgrading now, you will not be prompted to locate your installed this software before and are simply upgrading now, you will not be prompted to locate your
@@ -144,7 +144,7 @@ on successfully joining a multiworld game!
## Hosting a MultiWorld game ## Hosting a MultiWorld game
The recommended way to host a game is to use the hosting service provided on The recommended way to host a game is to use the hosting service provided on
[the website](https://berserkermulti.world/generate). The process is relatively simple: [the website](/generate). The process is relatively simple:
1. Collect YAML files from your players. 1. Collect YAML files from your players.
2. Create a zip file containing your players' YAML files. 2. Create a zip file containing your players' YAML files.

View File

@@ -7,7 +7,7 @@
</div> </div>
## Software requerido ## Software requerido
- [MultiWorld Utilities](https://github.com/Berserker66/MultiWorld-Utilities/releases) - [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Incluido en Multiworld Utilities) - [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Incluido en Multiworld Utilities)
- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES - Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES
- Un emulador capaz de ejecutar scripts Lua - Un emulador capaz de ejecutar scripts Lua
@@ -20,7 +20,7 @@
### Instalación en Windows ### Instalación en Windows
1. Descarga e instala MultiWorld Utilities desde el enlace anterior, asegurando que instalamos la versión más reciente. 1. Descarga e instala MultiWorld Utilities desde el enlace anterior, asegurando que instalamos la versión más reciente.
**El archivo esta localizado en la sección "assets" en la parte inferior de la información de versión**. Si tu intención es jugar la versión normal de multiworld, necesitarás el archivo `Setup.BerserkerMultiWorld.exe` **El archivo esta localizado en la sección "assets" en la parte inferior de la información de versión**. Si tu intención es jugar la versión normal de multiworld, necesitarás el archivo `Setup.Archipelago.exe`
- Si estas interesado en jugar la variante que aleatoriza las puertas internas de las mazmorras, necesitaras bajar 'Setup.BerserkerMultiWorld.Doors.exe' - Si estas interesado en jugar la variante que aleatoriza las puertas internas de las mazmorras, necesitaras bajar 'Setup.BerserkerMultiWorld.Doors.exe'
- Durante el proceso de instalación, se te pedirá donde esta situado tu archivo ROM japonés v1.0. Si ya habías instalado este software con anterioridad y simplemente estas actualizando, no se te pedirá la localización del archivo una segunda vez. - Durante el proceso de instalación, se te pedirá donde esta situado tu archivo ROM japonés v1.0. Si ya habías instalado este software con anterioridad y simplemente estas actualizando, no se te pedirá la localización del archivo una segunda vez.
- Puede ser que el programa pida la instalación de Microsoft Visual C++. Si ya lo tienes en tu ordenador (posiblemente por que un juego de Steam ya lo haya instalado), el instalador no te pedirá su instalación. - Puede ser que el programa pida la instalación de Microsoft Visual C++. Si ya lo tienes en tu ordenador (posiblemente por que un juego de Steam ya lo haya instalado), el instalador no te pedirá su instalación.
@@ -136,7 +136,7 @@ por unirte satisfactoriamente a una partida de multiworld!
## Hospedando una partida de multiworld ## Hospedando una partida de multiworld
La manera recomendad para hospedar una partida es usar el servicio proveído en La manera recomendad para hospedar una partida es usar el servicio proveído en
[el sitio web](https://berserkermulti.world/generate). El proceso es relativamente sencillo: [el sitio web](/generate). El proceso es relativamente sencillo:
1. Recolecta los ficheros YAML de todos los jugadores que participen. 1. Recolecta los ficheros YAML de todos los jugadores que participen.
2. Crea un fichero ZIP conteniendo esos ficheros. 2. Crea un fichero ZIP conteniendo esos ficheros.

View File

@@ -7,7 +7,7 @@
</div> </div>
## Logiciels requis ## Logiciels requis
- [Utilitaires du MultiWorld](https://github.com/Berserker66/MultiWorld-Utilities/releases) - [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents) - [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents)
- Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES - Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES
- Un émulateur capable d'éxécuter des scripts Lua - Un émulateur capable d'éxécuter des scripts Lua

View File

@@ -2,7 +2,7 @@
## Configuration ## Configuration
1. Plando features have to be enabled first, before they can be used (opt-in). 1. Plando features have to be enabled first, before they can be used (opt-in).
2. To do so, go to your installation directory (Windows default: `C:\ProgramData\BerserkerMultiWorld`), 2. To do so, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`),
then open the host.yaml file with a text editor. then open the host.yaml file with a text editor.
3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the 3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the
value to value to
@@ -13,7 +13,7 @@
### Bosses ### Bosses
- This module is enabled by default and available to be used on - This module is enabled by default and available to be used on
[https://archipelago.gg/generate](https://archipelago.gg/generate) [https://archipelago.gg/generate](/generate)
- Plando versions of boss shuffles can be added like any other boss shuffle option in a yaml and weighted. - Plando versions of boss shuffles can be added like any other boss shuffle option in a yaml and weighted.
- Boss Plando works as a list of instructions from left to right, if any arenas are empty at the end, - Boss Plando works as a list of instructions from left to right, if any arenas are empty at the end,
it defaults to vanilla it defaults to vanilla

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 @@
#base-header{ html{
background-image: url('../../static/backgrounds/grass/grass-0007-large.png');
background-repeat: repeat;
background-size: 650px 650px;
}
#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

@@ -31,7 +31,7 @@
<p> <p>
After generation is complete, you will have the option to download a patch file. After generation is complete, you will have the option to download a patch file.
This patch file can be opened with the This patch file can be opened with the
<a href="https://github.com/Berserker66/MultiWorld-Utilities/releases">client</a>, which can be <a href="https://github.com/ArchipelagoMW/Archipelago/releases">client</a>, which can be
used to to create a rom file. In-browser patching is planned for the future. used to to create a rom file. In-browser patching is planned for the future.
</p> </p>
<div id="generate-game-form-wrapper"> <div id="generate-game-form-wrapper">

View File

@@ -0,0 +1,63 @@
{% extends 'tablepage.html' %}
{% block head %}
{{ super() }}
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script>
{% endblock %}
{% block body %}
{% include 'header/dirtHeader.html' %}
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}">
<div id="tracker-header-bar">
<input placeholder="Search" id="search"/>
<span class="info">This tracker will automatically update itself periodically.</span>
</div>
<div class="table-wrapper">
<table class="table non-unique-item-table">
<thead>
<tr>
<th>Item</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
{% for name, count in inventory.items() %}
<tr>
<td>{{ name | item_name }}</td>
<td>{{ count }}</td>
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
<div class="table-wrapper">
<table class="table non-unique-item-table">
<thead>
<tr>
<th>Location</th>
<th>Checked</th>
</tr>
</thead>
<tbody>
{% for name in checked_locations %}
<tr>
<td>{{ name | location_name}}</td>
<td></td>
</tr>
{%- endfor -%}
{% for name in not_checked_locations %}
<tr>
<td>{{ name | location_name}}</td>
<td></td>
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
</div>
</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

@@ -2,13 +2,13 @@
<footer id="island-footer"> <footer id="island-footer">
<div id="copyright-notice">Copyright 2021 Archipelago</div> <div id="copyright-notice">Copyright 2021 Archipelago</div>
<div id="links"> <div id="links">
<a href="https://github.com/Berserker66/MultiWorld-Utilities">Source Code</a> <a href="https://github.com/ArchipelagoMW/Archipelago">Source Code</a>
- -
<a href="https://github.com/Berserker66/MultiWorld-Utilities/wiki">Wiki</a> <a href="https://github.com/ArchipelagoMW/Archipelago/wiki">Wiki</a>
- -
<a href="https://github.com/Berserker66/MultiWorld-Utilities/graphs/contributors">Contributors</a> <a href="https://github.com/ArchipelagoMW/Archipelago/graphs/contributors">Contributors</a>
- -
<a href="https://github.com/Berserker66/MultiWorld-Utilities/issues">Bug Report</a> <a href="https://github.com/ArchipelagoMW/Archipelago/issues">Bug Report</a>
</div> </div>
</footer> </footer>
{% endblock %} {% endblock %}

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,19 +34,18 @@
</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/Berserker66/MultiWorld-Utilities/graphs/contributors">talented people.</a> <a href="https://github.com/ArchipelagoMW/Archipelago/graphs/contributors">talented people.</a>
Together, they have spent countless hours creating a huge repository of Together, they have spent countless hours creating a huge repository of
<a href="https://github.com/Berserker66/MultiWorld-Utilities">source code</a> which has turned <a href="https://github.com/ArchipelagoMW/Archipelago">source code</a> which has turned
our crazy idea into a reality. our crazy idea into a reality.
</p> </p>
<p> <p>

View File

@@ -5,7 +5,7 @@ from werkzeug.exceptions import abort
import datetime import datetime
from uuid import UUID from uuid import UUID
from worlds.alttp import Items, Regions from worlds.alttp import Items
from WebHostLib import app, cache, Room from WebHostLib import app, cache, Room
from Utils import restricted_loads from Utils import restricted_loads
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
@@ -327,7 +327,8 @@ def get_static_room_data(room: Room):
player_small_key_locations[item_player].add(ids_small_key[item_id]) player_small_key_locations[item_player].add(ids_small_key[item_id])
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \ result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
player_big_key_locations, player_small_key_locations, multidata["precollected_items"] player_big_key_locations, player_small_key_locations, multidata["precollected_items"], \
multidata["games"]
_multidata_cache[room.seed.id] = result _multidata_cache[room.seed.id] = result
return result return result
@@ -344,9 +345,9 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
abort(404) abort(404)
# Collect seed information and pare it down to a single player # Collect seed information and pare it down to a single player
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations, precollected_items = get_static_room_data(room) locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
player_big_key_locations, player_small_key_locations, precollected_items, games = get_static_room_data(room)
player_name = names[tracked_team][tracked_player - 1] player_name = names[tracked_team][tracked_player - 1]
seed_checks_in_area = seed_checks_in_area[tracked_player]
location_to_area = player_location_to_area[tracked_player] location_to_area = player_location_to_area[tracked_player]
inventory = collections.Counter() inventory = collections.Counter()
checks_done = {loc_name: 0 for loc_name in default_locations} checks_done = {loc_name: 0 for loc_name in default_locations}
@@ -377,52 +378,58 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
if ms_player == tracked_player: # a check done by the tracked player if ms_player == tracked_player: # a check done by the tracked player
checks_done[location_to_area[location]] += 1 checks_done[location_to_area[location]] += 1
checks_done["Total"] += 1 checks_done["Total"] += 1
if games[tracked_player] == "A Link to the Past":
# Note the presence of the triforce item
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
if game_state == 30:
inventory[106] = 1 # Triforce
# Note the presence of the triforce item # Progressive items need special handling for icons and class
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0) progressive_items = {
if game_state == 30: "Progressive Sword": 94,
inventory[106] = 1 # Triforce "Progressive Glove": 97,
"Progressive Bow": 100,
"Progressive Mail": 96,
"Progressive Shield": 95,
}
progressive_names = {
"Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
"Progressive Glove": [None, 'Power Glove', 'Titan Mitts'],
"Progressive Bow": [None, "Bow", "Silver Bow"],
"Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
"Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"]
}
# Progressive items need special handling for icons and class # Determine which icon to use
progressive_items = { display_data = {}
"Progressive Sword": 94, for item_name, item_id in progressive_items.items():
"Progressive Glove": 97, level = min(inventory[item_id], len(progressive_names[item_name])-1)
"Progressive Bow": 100, display_name = progressive_names[item_name][level]
"Progressive Mail": 96, acquired = True
"Progressive Shield": 95, if not display_name:
} acquired = False
progressive_names = { display_name = progressive_names[item_name][level+1]
"Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'], base_name = item_name.split(maxsplit=1)[1].lower()
"Progressive Glove": [None, 'Power Glove', 'Titan Mitts'], display_data[base_name+"_acquired"] = acquired
"Progressive Bow": [None, "Bow", "Silver Bow"], display_data[base_name+"_url"] = icons[display_name]
"Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
"Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"]
}
# Determine which icon to use
display_data = {}
for item_name, item_id in progressive_items.items():
level = min(inventory[item_id], len(progressive_names[item_name]))
display_name = progressive_names[item_name][level]
acquired = True
if not display_name:
acquired = False
display_name = progressive_names[item_name][level+1]
base_name = item_name.split(maxsplit=1)[1].lower()
display_data[base_name+"_acquired"] = acquired
display_data[base_name+"_url"] = icons[display_name]
# The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should? # The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
sp_areas = ordered_areas[2:15] sp_areas = ordered_areas[2:15]
return render_template("playerTracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id, return render_template("lttpTracker.html", inventory=inventory,
player_name=player_name, room=room, icons=icons, checks_done=checks_done, player_name=player_name, room=room, icons=icons, checks_done=checks_done,
checks_in_area=seed_checks_in_area, acquired_items={lookup_any_item_id_to_name[id] for id in inventory}, checks_in_area=seed_checks_in_area[tracked_player], acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas, small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
key_locations=player_small_key_locations[tracked_player], key_locations=player_small_key_locations[tracked_player],
big_key_locations=player_big_key_locations[tracked_player], big_key_locations=player_big_key_locations[tracked_player],
**display_data) **display_data)
else:
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set())
return render_template("genericTracker.html",
inventory=inventory,
player=tracked_player, team=tracked_team, room=room, player_name=player_name,
checked_locations= checked_locations, not_checked_locations = set(locations[tracked_player])-checked_locations)
@app.route('/tracker/<suuid:tracker>') @app.route('/tracker/<suuid:tracker>')
@@ -432,7 +439,7 @@ def getTracker(tracker: UUID):
if not room: if not room:
abort(404) abort(404)
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, \ locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, \
player_small_key_locations, precollected_items = get_static_room_data(room) player_small_key_locations, precollected_items, games = get_static_room_data(room)
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)} inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
for teamnumber, team in enumerate(names)} for teamnumber, team in enumerate(names)}
@@ -447,7 +454,6 @@ def getTracker(tracker: UUID):
else: else:
multisave = {} multisave = {}
if "hints" in multisave: if "hints" in multisave:
for (team, slot), slot_hints in multisave["hints"].items(): for (team, slot), slot_hints in multisave["hints"].items():
hints[team] |= set(slot_hints) hints[team] |= set(slot_hints)
@@ -486,7 +492,7 @@ def getTracker(tracker: UUID):
for player, name in enumerate(names, 1): for player, name in enumerate(names, 1):
player_names[(team, player)] = name player_names[(team, player)] = name
long_player_names = player_names.copy() long_player_names = player_names.copy()
for (team, player), alias in multisave.get("name_aliases", []): for (team, player), alias in multisave.get("name_aliases", {}).items():
player_names[(team, player)] = alias player_names[(team, player)] = alias
long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})" long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})"

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 882 B

View File

@@ -0,0 +1 @@
{"stone-furnace":{"smelting":true},"steel-furnace":{"smelting":true},"electric-furnace":{"smelting":true},"assembling-machine-1":{"crafting":true,"basic-crafting":true,"advanced-crafting":true},"assembling-machine-2":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"assembling-machine-3":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"oil-refinery":{"oil-processing":true},"chemical-plant":{"chemistry":true},"centrifuge":{"centrifuging":true},"rocket-silo":{"rocket-building":true},"character":{"crafting":true}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,17 +1,21 @@
{% macro dict_to_lua(dict) -%} {% from "macros.lua" import dict_to_lua %}
{ -- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
{% for key, value in dict.items() %}
["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %}
{% endfor %}
}
{%- endmacro %}
require "lib" require "lib"
require "util" 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)
@@ -68,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
@@ -92,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)
@@ -120,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.
@@ -138,16 +141,18 @@ script.on_init(function()
end) end)
-- for testing -- for testing
script.on_event(defines.events.on_tick, function(event) -- script.on_event(defines.events.on_tick, function(event)
if event.tick%600 == 300 then -- if event.tick%3600 == 300 then
dumpInfo(game.forces["player"]) -- dumpInfo(game.forces["player"])
end -- end
end) -- end)
-- hook into researches done -- hook into researches done
script.on_event(defines.events.on_research_finished, function(event) script.on_event(defines.events.on_research_finished, function(event)
local technology = event.research local technology = event.research
dumpInfo(technology.force) if technology.researched and string.find(technology.name, "ap%-") == 1 then
dumpInfo(technology.force) --is sendable
end
if FREE_SAMPLES == 0 then if FREE_SAMPLES == 0 then
return -- Nothing else to do return -- Nothing else to do
end end
@@ -191,6 +196,7 @@ function dumpInfo(force)
end end
end end
game.write_file("ap_bridge.json", game.table_to_json(data_collection), false, 0) game.write_file("ap_bridge.json", game.table_to_json(data_collection), false, 0)
log("Archipelago Bridge File written for game tick ".. game.tick .. ".")
-- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0) -- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0)
-- game.print("Sent progress to Archipelago.") -- game.print("Sent progress to Archipelago.")
end end

View File

@@ -1,13 +1,14 @@
{% from "macros.lua" import dict_to_recipe %}
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template -- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
require('lib') require('lib')
data.raw["recipe"]["rocket-part"].ingredients = {{ rocket_recipe | safe }} data.raw["recipe"]["rocket-part"].ingredients = {{ dict_to_recipe(rocket_recipe) }}
local technologies = data.raw["technology"] 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,
@@ -22,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
@@ -30,31 +31,51 @@ function prep_copy(new_copy, old_tech)
end end
end end
function set_ap_icon(tech)
tech.icon = "__{{ mod_name }}__/graphics/icons/ap.png"
tech.icons = nil
tech.icon_size = 128
end
function set_ap_unimportant_icon(tech)
tech.icon = "__{{ mod_name }}__/graphics/icons/ap_unimportant.png"
tech.icons = nil
tech.icon_size = 128
end
function copy_factorio_icon(tech, tech_source)
tech.icon = table.deepcopy(technologies[tech_source].icon)
tech.icons = table.deepcopy(technologies[tech_source].icons)
tech.icon_size = table.deepcopy(technologies[tech_source].icon_size)
end
function adjust_energy(recipe_name, factor)
local energy = data.raw.recipe[recipe_name].energy_required
if (energy == nil) then
energy = 1
end
data.raw.recipe[recipe_name].energy_required = energy * factor
end
table.insert(data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories, "crafting-with-fluid") table.insert(data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories, "crafting-with-fluid")
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #} {# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
{%- for original_tech_name, item_name, receiving_player in locations %} {%- for original_tech_name, item_name, receiving_player, advancement in locations %}
original_tech = technologies["{{original_tech_name}}"] original_tech = technologies["{{original_tech_name}}"]
{#- the tech researched by the local player #} {#- the tech researched by the local player #}
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 %}
if new_tree_copy.unit.count then 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 }}))
end
{% endif %}
{% if item_name in tech_table and visibility %}
{#- copy Factorio Technology Icon #}
new_tree_copy.icon = table.deepcopy(technologies["{{ item_name }}"].icon)
new_tree_copy.icons = table.deepcopy(technologies["{{ item_name }}"].icons)
new_tree_copy.icon_size = table.deepcopy(technologies["{{ item_name }}"].icon_size)
{% else %}
{#- use default AP icon if no Factorio graphics exist #}
new_tree_copy.icon = "__{{ mod_name }}__/graphics/icons/ap.png"
new_tree_copy.icons = nil
new_tree_copy.icon_size = 512
{% endif %} {% endif %}
{%- if item_name in tech_table and tech_tree_information == 2 -%}
{#- copy Factorio Technology Icon -#}
copy_factorio_icon(new_tree_copy, "{{ item_name }}")
{%- else -%}
{#- use default AP icon if no Factorio graphics exist -#}
{% if advancement or not tech_tree_information %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %}
{%- endif -%}
{#- connect Technology #} {#- connect Technology #}
{%- if original_tech_name in tech_tree_layout_prerequisites %} {%- if original_tech_name in tech_tree_layout_prerequisites %}
{%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %} {%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %}
@@ -63,5 +84,9 @@ table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequesite] }}-")
{% endif -%} {% endif -%}
{#- add new Technology to game #} {#- add new Technology to game #}
data:extend{new_tree_copy} data:extend{new_tree_copy}
{% endfor %}
{% endfor %} {% if recipe_time_scale %}
{%- for recipe in recipes %}
adjust_energy("{{ recipe }}", {{ random.triangular(*recipe_time_scale) }})
{%- endfor -%}
{% endif %}

View File

@@ -1,18 +1,20 @@
[technology-name] [technology-name]
{% for original_tech_name, item_name, receiving_player 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
{%- endif -%} {%- endif -%}
{% endfor %} {% endfor %}
[technology-description] [technology-description]
{% for original_tech_name, item_name, receiving_player 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] }}. 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

@@ -0,0 +1,14 @@
{% macro dict_to_lua(dict) -%}
{
{%- for key, value in dict.items() -%}
["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %}
{% endfor -%}
}
{%- endmacro %}
{% macro dict_to_recipe(dict) -%}
{
{%- for key, value in dict.items() -%}
{"{{ key }}", {{ value | safe }}}{% if not loop.last %},{% endif %}
{% endfor -%}
}
{%- endmacro %}

BIN
data/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,80 @@
#define sourcepath "build_factorio\exe.win-amd64-3.8\"
#define MyAppName "Archipelago Factorio Client"
#define MyAppExeName "ArchipelagoGraphicalFactorioClient.exe"
#define MyAppIcon "icon.ico"
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
AppId={{D13CEBD0-F1D5-4435-A4A6-5243F934613F}}
AppName={#MyAppName}
AppVerName={#MyAppName}
DefaultDirName={commonappdata}\{#MyAppName}
DisableProgramGroupPage=yes
DefaultGroupName=Archipelago
OutputDir=setups
OutputBaseFilename=Setup {#MyAppName}
Compression=lzma2
SolidCompression=yes
LZMANumBlockThreads=8
ArchitecturesInstallIn64BitMode=x64
ChangesAssociations=yes
ArchitecturesAllowed=x64
AllowNoIcons=yes
SetupIconFile={#MyAppIcon}
UninstallDisplayIcon={app}\{#MyAppExeName}
SignTool= signtool
LicenseFile= LICENSE
WizardStyle= modern
SetupLogging=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
[Dirs]
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
[Files]
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
Source: "{#sourcepath}*"; Excludes: "*.sfc, *.log, data\sprites\alttpr"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}";
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
[UninstallDelete]
Type: dirifempty; Name: "{app}"
[Code]
// See: https://stackoverflow.com/a/51614652/2287576
function IsVCRedist64BitNeeded(): boolean;
var
strVersion: string;
begin
if (RegQueryStringValue(HKEY_LOCAL_MACHINE,
'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Version', strVersion)) then
begin
// Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion);
Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
end
else
begin
// Not even an old version installed
Log('VC Redist x64 is not already installed');
Result := True;
end;
end;

View File

@@ -21,7 +21,7 @@ server_options:
location_check_points: 1 location_check_points: 1
# Relative point cost to receive a hint via !hint for players # Relative point cost to receive a hint via !hint for players
# so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint, for a total of 5 # so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint, for a total of 5
hint_cost: 1000 # Set to 0 if you want free hints hint_cost: 10 # Set to 0 if you want free hints
# Forfeit modes # Forfeit modes
# "disabled" -> clients can't forfeit, # "disabled" -> clients can't forfeit,
# "enabled" -> clients can always forfeit # "enabled" -> clients can always forfeit
@@ -63,10 +63,6 @@ multi_mystery_options:
# If using a pre-rolled yaml fails with "Please fix your yaml.", please file a bug report including both the original yaml # If using a pre-rolled yaml fails with "Please fix your yaml.", please file a bug report including both the original yaml
# as well as the generated pre-rolled yaml. # as well as the generated pre-rolled yaml.
pre_roll: false pre_roll: false
# Automatically launches {player_name}.yaml's ROM file using the OS's default program once generation completes. (likely your emulator)
# Does nothing if the name is not found
# Example: player_name = "Berserker"
player_name: "" # The hosts name
# Create a spoiler file # Create a spoiler file
# 0 -> None # 0 -> None
# 1 -> Full spoiler # 1 -> Full spoiler

View File

@@ -82,7 +82,7 @@ begin
begin begin
// Is the installed version at least the packaged one ? // Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion); Log('VC Redist x64 Version : found ' + strVersion);
Result := (CompareStr(strVersion, 'v14.28.29325') < 0); Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
end end
else else
begin begin

View File

@@ -82,7 +82,7 @@ begin
begin begin
// Is the installed version at least the packaged one ? // Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion); Log('VC Redist x64 Version : found ' + strVersion);
Result := (CompareStr(strVersion, 'v14.28.29325') < 0); Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
end end
else else
begin begin

103
meta.yaml
View File

@@ -11,54 +11,55 @@
# 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
progression_balancing: # Progression balancing tries to make sure that the player has *something* towards any players goal in each "sphere" null:
on: 0 # Force every player into progression balancing progression_balancing: # Progression balancing tries to make sure that the player has *something* towards any players goal in each "sphere"
off: 0 # Force every player out of progression balancing, then prepare for a lot of logical BK on: 0 # Force every player into progression balancing
null: 1 # Let players decide via their own progression_balancing flag in their yaml, defaulting to on off: 0 # Force every player out of progression balancing, then prepare for a lot of logical BK
goals: null: 1 # Let players decide via their own progression_balancing flag in their yaml, defaulting to on
ganon: 100 # Climb GT, defeat Agahnim 2, and then kill Ganon A Link to the Past:
fast_ganon: 250 # Only killing Ganon is required. The hole is always open. However, items may still be placed in GT goals:
dungeons: 50 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2) ganon: 100 # Climb GT, defeat Agahnim 2, and then kill Ganon
pedestal: 100 # Pull the Triforce from the Master Sword pedestal fast_ganon: 250 # Only killing Ganon is required. The hole is always open. However, items may still be placed in GT
triforce-hunt: 5 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle dungeons: 50 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
local_triforce_hunt: 5 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle pedestal: 100 # Pull the Triforce from the Master Sword pedestal
ganon_triforce_hunt: 10 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon triforce-hunt: 5 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
local_ganon_triforce_hunt: 10 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon local_triforce_hunt: 5 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
ganon_pedestal: 10 # Pull the Master Sword pedestal, then kill Ganon ganon_triforce_hunt: 10 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
null: 0 # Maintain individual goals local_ganon_triforce_hunt: 10 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
mode: ganon_pedestal: 10 # Pull the Master Sword pedestal, then kill Ganon
standard: 10 null: 0 # Maintain individual goals
open: 60 mode:
inverted: 10 standard: 10
null: 10 # Maintain individual world states open: 60
tower_open: inverted: 10
'0': 8 null: 10 # Maintain individual world states
'1': 7 tower_open:
'2': 6 '0': 8
'3': 5 '1': 7
'4': 4 '2': 6
'5': 3 '3': 5
'6': 2 '4': 4
'7': 1 '5': 3
random: 10 # A different GT open time should not usually result in a vastly different completion time, unless ganon goal and tower_open > ganon_open '6': 2
ganon_open: '7': 1
'0': 3 random: 10 # A different GT open time should not usually result in a vastly different completion time, unless ganon goal and tower_open > ganon_open
'1': 4 ganon_open:
'2': 5 '0': 3
'3': 6 '1': 4
'4': 7 '2': 5
'5': 8 '3': 6
'6': 9 '4': 7
'7': 10 '5': 8
random: 5 # This will mean differing completion times. But leaving it for that surprise effect '6': 9
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces. '7': 10
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required random: 5 # This will mean differing completion times. But leaving it for that surprise effect
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
available: 50 # available = triforce_pieces_available extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1 percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
# Format "pieces: chance" available: 50 # available = triforce_pieces_available
30: 50 triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. 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" 30: 50
25: 50 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
# Do not use meta rom options at this time # Format "pieces: chance"
25: 50

View File

@@ -25,9 +25,10 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc
#{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1. #{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1.
game: game:
A Link to the Past: 1 A Link to the Past: 1
Hollow Knight: 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
@@ -36,437 +37,349 @@ 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:
tech_tree_layout:
single: 1
small_diamonds: 1
medium_diamonds: 1
pyramid: 1
funnel: 1
max_science_pack:
automation_science_pack: 0
logistic_science_pack: 0
military_science_pack: 0
chemical_science_pack: 0
production_science_pack: 0
utility_science_pack: 0
space_science_pack: 1
tech_cost:
very_easy : 0
easy : 0
kind : 0
normal : 1
hard : 0
very_hard : 0
insane : 0
free_samples:
none: 1
single_craft: 0
half_stack: 0
stack: 0
visibility:
none: 0
sending: 1
random_tech_ingredients:
on: 1
off: 0
starting_items:
burner-mining-drill: 19
stone-furnace: 19
# Minecraft options:
advancement_goal: # Number of advancements required (out of 92 total) to spawn the Ender Dragon and complete the game.
few: 0 # 30 advancements
normal: 1 # 50
many: 0 # 70
combat_difficulty: # Modifies the level of items logically required for exploring dangerous areas and fighting bosses.
easy: 0
normal: 1
hard: 0
include_hard_advancements: # Junk-fills certain RNG-reliant or tedious advancements with XP rewards.
on: 0
off: 1
include_insane_advancements: # Junk-fills extremely difficult advancements; this is only How Did We Get Here? and Adventuring Time.
on: 0
off: 1
include_postgame_advancements: # Some advancements require defeating the Ender Dragon; this will junk-fill them so you won't have to finish to send some items.
on: 0
off: 1
shuffle_structures: # CURRENTLY DISABLED; enables shuffling of villages, outposts, fortresses, bastions, and end cities.
on: 0
off: 1
# A Link to the Past options:
### 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
none: 50 # No glitches required
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
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
dark_room_logic: # Logic for unlit dark rooms
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
none: 0 # all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness
restrict_dungeon_item_on_boss: # aka ambrosia boss items
on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
off: 50
### End of Logic Section ###
map_shuffle: # Shuffle dungeon maps into the world and other dungeons, including other players' worlds
on: 0
off: 50
compass_shuffle: # Shuffle compasses into the world and other dungeons, including other players' worlds
on: 0
off: 50
smallkey_shuffle: # Shuffle small keys into the world and other dungeons, including other players' worlds
on: 0
universal: 0 # allows small keys to be used in any dungeon and adds shops to buy more
off: 50
bigkey_shuffle: # Shuffle big keys into the world and other dungeons, including other players' worlds
on: 0
off: 50
local_keys: # Keep small keys and big keys local to your world
on: 0
off: 50
dungeon_items: # Alternative to the 4 shuffles and local_keys above this, does nothing until the respective 4 shuffles and local_keys above are deleted
mc: 0 # Shuffle maps and compasses
none: 50 # Shuffle none of the 4
mcsb: 0 # Shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here
lmcsb: 0 # Like mcsb above, but with keys kept local to your world. l is what makes your keys local, or not if it's missing
ub: 0 # universal small keys and shuffled big keys
# you can add more combos of these letters here
dungeon_counters:
on: 0 # Always display amount of items checked in a dungeon
pickup: 50 # Show when compass is picked up
default: 0 # Show when compass is picked up if the compass itself is shuffled
off: 0 # Never show item count in dungeons
progressive: # Enable or disable progressive items (swords, shields, bow)
on: 50 # All items are progressive
off: 0 # No items are progressive
random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
entrance_shuffle:
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons, but keep dungeons to a specific world
dungeonscrossed: 0 # like dungeonsfull, but allow cross-world traversal through a dungeon. Warning: May force repeated dungeon traversal
simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules
restricted: 0 # Less strict than simple
full: 0 # Less strict than restricted
crossed: 0 # Less strict than full
insanity: 0 # Very few grouping rules. Good luck
# you can also define entrance shuffle seed, like so:
crossed-1000: 0 # using this method, you can have the same layout as another player and share entrance information
# however, many other settings like logic, world state, retro etc. may affect the shuffle result as well.
crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed
goals:
ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon
crystals: 0 # Only killing Ganon is required. However, items may still be placed in GT
bosses: 0 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
pedestal: 0 # Pull the Triforce from the Master Sword pedestal
ganon_pedestal: 0 # Pull the Master Sword pedestal, then kill Ganon
triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
local_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock.
open_pyramid:
goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons
auto: 0 # Same as Goal, but also is closed if holes are shuffled and ganon is part of the shuffle pool
yes: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt
no: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
available: 50 # available = triforce_pieces_available
triforce_pieces_extra: # Set to how many extra triforces pieces are available to collect in the world.
# Format "pieces: chance"
0: 0
5: 50
10: 50
15: 0
20: 0
triforce_pieces_percentage: # Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world.
# Format "pieces: chance"
100: 0 #No extra
150: 50 #Half the required will be added as extra
200: 0 #There are the double of the required ones available.
triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1
# Format "pieces: chance"
25: 0
30: 50
40: 0
50: 0
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"
15: 0
20: 50
30: 0
40: 0
50: 0
tower_open: # Crystals required to open GT
'0': 80
'1': 70
'2': 60
'3': 50
'4': 40
'5': 30
'6': 20
'7': 10
random: 0
ganon_open: # Crystals required to hurt Ganon
'0': 80
'1': 70
'2': 60
'3': 50
'4': 40
'5': 30
'6': 20
'7': 10
random: 0
mode:
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
inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered
retro:
on: 0 # you must buy a quiver to use the bow, take-any caves and an old-man cave are added to the world. You may need to find your sword from the old man's cave
off: 50
hints:
'on': 50 # Hint tiles sometimes give item location hints
'off': 0 # Hint tiles provide gameplay tips
swordless:
on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
off: 1
item_pool:
easy: 0 # Doubled upgrades, progressives, and etc
normal: 50 # Item availability remains unchanged from vanilla game
hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless)
expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless)
item_functionality:
easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
normal: 50 # Vanilla item functionality
hard: 0 # Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon)
expert: 0 # Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon)
tile_shuffle: # Randomize the tile layouts in flying tile rooms
on: 0
off: 50
misery_mire_medallion: # required medallion to open Misery Mire front entrance
random: 50
Ether: 0
Bombos: 0
Quake: 0
turtle_rock_medallion: # required medallion to open Turtle Rock front entrance
random: 50
Ether: 0
Bombos: 0
Quake: 0
### Enemizer Section ###
boss_shuffle:
none: 50 # Vanilla bosses
basic: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons
normal: 0 # 3 bosses can occur twice
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
enemy_shuffle: # Randomize enemy placement
on: 0
off: 50
killable_thieves: # Make thieves killable
on: 0 # Usually turned on together with enemy_shuffle to make annoying thief placement more manageable
off: 50
bush_shuffle: # Randomize the chance that bushes have enemies and the enemies under said bush
on: 0
off: 50
enemy_damage:
default: 50 # Vanilla enemy damage
shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps
random: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage
enemy_health:
default: 50 # Vanilla enemy HP
easy: 0 # Enemies have reduced health
hard: 0 # Enemies have increased health
expert: 0 # Enemies have greatly increased health
pot_shuffle:
'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile
'off': 50 # Default pot item locations
### End of Enemizer Section ###
beemizer: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps
0: 50 # No bee traps are placed
1: 0 # 25% of rupees, bombs and arrows are replaced with bees, of which 60% are traps and 40% single bees
2: 0 # 50% of rupees, bombs and arrows are replaced with bees, of which 70% are traps and 30% single bees
3: 0 # 75% of rupees, bombs and arrows are replaced with bees, of which 80% are traps and 20% 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
### Shop Settings ###
shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
0: 50
5: 0
15: 0
30: 0
random: 0 # 0 to 30 evenly distributed
shop_shuffle:
none: 50
g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
f: 0 # Generate new default inventories for every shop independently
i: 0 # Shuffle default inventories of the shops around
p: 0 # Randomize the prices of the items in shop inventories
u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
ip: 0 # Shuffle inventories and randomize prices
fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool
uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool
# You can add more combos
### End of Shop Section ###
shuffle_prizes: # aka drops
none: 0 # do not shuffle prize packs
g: 50 # shuffle "general" prize packs, as in enemy, tree pull, dig etc.
b: 0 # shuffle "bonk" prize packs
bg: 0 # shuffle both
timer:
none: 50 # No timer will be displayed.
timed: 0 # Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.
timed_ohko: 0 # Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.
ohko: 0 # Timer always at zero. Permanent OHKO.
timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.
display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool.
countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with
0: 0 # For timed_ohko, starts in OHKO mode when starting the game
10: 50
20: 0
30: 0
60: 0
red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock
-2: 50
1: 0
blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock
1: 0
2: 50
green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock
4: 50
10: 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" # local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords"
# - "Moon Pearl" # - "Moon Pearl"
# - "Small Keys" # - "Small Keys"
# - "Big Keys" # - "Big Keys"
glitch_boots: # non_local_items: # Force certain items to appear outside your world only, unless in single-player. Recognizes some group names, like "Swords"
on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them # - "Progressive Weapons"
off: 0
# meta_ignore, linked_options and triggers work for any game Factorio:
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 tech_tree_layout:
single: 1
small_diamonds: 1
medium_diamonds: 1
large_diamonds: 1
small_pyramids: 1
medium_pyramids: 1
large_pyramids: 1
small_funnels: 1
medium_funnels: 1
large_funnels: 1
recipe_time: # randomize the time it takes for any recipe to craft, this includes smelting, chemical lab, hand crafting etc.
vanilla: 1
fast: 0 # 25% to 100% of original time
normal: 0 # 50 % to 200% of original time
slow: 0 # 100% to 400% of original time
chaos: 0 # 25% to 400% of original time
max_science_pack:
automation_science_pack: 0
logistic_science_pack: 0
military_science_pack: 0
chemical_science_pack: 0
production_science_pack: 0
utility_science_pack: 0
space_science_pack: 1
tech_cost:
very_easy : 0
easy : 0
kind : 0
normal : 1
hard : 0
very_hard : 0
insane : 0
free_samples:
none: 1
single_craft: 0
half_stack: 0
stack: 0
tech_tree_information:
none: 0
advancement: 0 # show which items are a logical advancement
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
off: 0
starting_items:
burner-mining-drill: 19
stone-furnace: 19
Minecraft:
advancement_goal: # Number of advancements required (out of 92 total) to spawn the Ender Dragon and complete the game.
few: 0 # 30 advancements
normal: 1 # 50
many: 0 # 70
combat_difficulty: # Modifies the level of items logically required for exploring dangerous areas and fighting bosses.
easy: 0
normal: 1
hard: 0
include_hard_advancements: # Junk-fills certain RNG-reliant or tedious advancements with XP rewards.
on: 0
off: 1
include_insane_advancements: # Junk-fills extremely difficult advancements; this is only How Did We Get Here? and Adventuring Time.
on: 0
off: 1
include_postgame_advancements: # Some advancements require defeating the Ender Dragon; this will junk-fill them so you won't have to finish to send some items.
on: 0
off: 1
shuffle_structures: # CURRENTLY DISABLED; enables shuffling of villages, outposts, fortresses, bastions, and end cities.
on: 0
off: 1
A Link to the Past:
### Logic Section ###
glitches_required: # Determine the logic required to complete the seed
none: 50 # No glitches required
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
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.
# Other players items are placed into your world under HMG logic
dark_room_logic: # Logic for unlit dark rooms
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
none: 0 # all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness
restrict_dungeon_item_on_boss: # aka ambrosia boss items
on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
off: 50
### End of Logic Section ###
map_shuffle: # Shuffle dungeon maps into the world and other dungeons, including other players' worlds
on: 0
off: 50
compass_shuffle: # Shuffle compasses into the world and other dungeons, including other players' worlds
on: 0
off: 50
smallkey_shuffle: # Shuffle small keys into the world and other dungeons, including other players' worlds
on: 0
universal: 0 # allows small keys to be used in any dungeon and adds shops to buy more
off: 50
bigkey_shuffle: # Shuffle big keys into the world and other dungeons, including other players' worlds
on: 0
off: 50
local_keys: # Keep small keys and big keys local to your world
on: 0
off: 50
dungeon_items: # Alternative to the 4 shuffles and local_keys above this, does nothing until the respective 4 shuffles and local_keys above are deleted
mc: 0 # Shuffle maps and compasses
none: 50 # Shuffle none of the 4
mcsb: 0 # Shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here
lmcsb: 0 # Like mcsb above, but with keys kept local to your world. l is what makes your keys local, or not if it's missing
ub: 0 # universal small keys and shuffled big keys
# you can add more combos of these letters here
dungeon_counters:
on: 0 # Always display amount of items checked in a dungeon
pickup: 50 # Show when compass is picked up
default: 0 # Show when compass is picked up if the compass itself is shuffled
off: 0 # Never show item count in dungeons
progressive: # Enable or disable progressive items (swords, shields, bow)
on: 50 # All items are progressive
off: 0 # No items are progressive
random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
entrance_shuffle:
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons, but keep dungeons to a specific world
dungeonscrossed: 0 # like dungeonsfull, but allow cross-world traversal through a dungeon. Warning: May force repeated dungeon traversal
simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules
restricted: 0 # Less strict than simple
full: 0 # Less strict than restricted
crossed: 0 # Less strict than full
insanity: 0 # Very few grouping rules. Good luck
# you can also define entrance shuffle seed, like so:
crossed-1000: 0 # using this method, you can have the same layout as another player and share entrance information
# however, many other settings like logic, world state, retro etc. may affect the shuffle result as well.
crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed
goals:
ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon
crystals: 0 # Only killing Ganon is required. However, items may still be placed in GT
bosses: 0 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
pedestal: 0 # Pull the Triforce from the Master Sword pedestal
ganon_pedestal: 0 # Pull the Master Sword pedestal, then kill Ganon
triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
local_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock.
open_pyramid:
goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons
auto: 0 # Same as Goal, but also is closed if holes are shuffled and ganon is part of the shuffle pool
yes: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt
no: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
available: 50 # available = triforce_pieces_available
triforce_pieces_extra: # Set to how many extra triforces pieces are available to collect in the world.
# Format "pieces: chance"
0: 0
5: 50
10: 50
15: 0
20: 0
triforce_pieces_percentage: # Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world.
# Format "pieces: chance"
100: 0 #No extra
150: 50 #Half the required will be added as extra
200: 0 #There are the double of the required ones available.
triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1
# Format "pieces: chance"
25: 0
30: 50
40: 0
50: 0
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"
15: 0
20: 50
30: 0
40: 0
50: 0
crystals_needed_for_gt: # Crystals required to open GT
0: 0
1: 0
2: 0
3: 0
4: 0
5: 0
6: 0
7: 0
random: 0
random-low: 50 # any valid number, weighted towards the lower end
random-middle: 0 # any valid number, weighted towards the central range
random-high: 0 # any valid number, weighted towards the higher end
crystals_needed_for_ganon: # Crystals required to hurt Ganon
0: 0
1: 0
2: 0
3: 0
4: 0
5: 0
6: 0
7: 0
random: 0
random-low: 0
random-middle: 0
random-high: 50
mode: mode:
- inverted # Never play inverted seeds 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
inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered
retro: retro:
- on # Never play retro seeds on: 0 # you must buy a quiver to use the bow, take-any caves and an old-man cave are added to the world. You may need to find your sword from the old man's cave
off: 50
hints:
'on': 50 # Hint tiles sometimes give item location hints
'off': 0 # Hint tiles provide gameplay tips
swordless: swordless:
- on # Never play a swordless seed on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
linked_options: off: 1
- name: crosskeys item_pool:
options: # These overwrite earlier options if the percentage chance triggers easy: 0 # Doubled upgrades, progressives, and etc
entrance_shuffle: crossed normal: 50 # Item availability remains unchanged from vanilla game
bigkey_shuffle: true hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless)
compass_shuffle: true expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless)
map_shuffle: true item_functionality:
smallkey_shuffle: true easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
percentage: 0 # Set this to the percentage chance you want crosskeys normal: 50 # Vanilla item functionality
- name: localcrosskeys hard: 0 # Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon)
options: # These overwrite earlier options if the percentage chance triggers expert: 0 # Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon)
entrance_shuffle: crossed tile_shuffle: # Randomize the tile layouts in flying tile rooms
bigkey_shuffle: true on: 0
compass_shuffle: true off: 50
map_shuffle: true misery_mire_medallion: # required medallion to open Misery Mire front entrance
smallkey_shuffle: true random: 50
local_items: # Forces keys to be local to your own world Ether: 0
- "Small Keys" Bombos: 0
- "Big Keys" Quake: 0
percentage: 0 # Set this to the percentage chance you want local crosskeys turtle_rock_medallion: # required medallion to open Turtle Rock front entrance
- name: enemizer random: 50
options: Ether: 0
boss_shuffle: # Subchances can be injected too, which then get rolled Bombos: 0
basic: 1 Quake: 0
full: 1 ### Enemizer Section ###
chaos: 1 boss_shuffle:
singularity: 1 none: 50 # Vanilla bosses
enemy_damage: basic: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons
shuffled: 1 full: 0 # 3 bosses can occur twice
random: 1 chaos: 0 # Any boss can appear any amount of times
enemy_health: singularity: 0 # Picks a boss, tries to put it everywhere that works, if there's spaces remaining it picks a boss to fill those
easy: 1 enemy_shuffle: # Randomize enemy placement
hard: 1 on: 0
expert: 1 off: 50
percentage: 0 # Set this to the percentage chance you want enemizer killable_thieves: # Make thieves killable
# triggers that replace options upon rolling certain options on: 0 # Usually turned on together with enemy_shuffle to make annoying thief placement more manageable
legacy_weapons: # this is not an actual option, just a set of weights to trigger from off: 50
trigger_disabled: 50 bush_shuffle: # Randomize the chance that bushes have enemies and the enemies under said bush
randomized: 0 # Swords are placed randomly throughout the world on: 0
assured: 0 # Begin with a sword, the rest are placed randomly throughout the world off: 50
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal) enemy_damage:
swordless: 0 # swordless mode default: 50 # Vanilla enemy damage
triggers: shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps
# trigger block for legacy weapons mode, to enable these add weights to legacy_weapons random: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage
- option_name: legacy_weapons enemy_health:
option_result: randomized default: 50 # Vanilla enemy HP
options: easy: 0 # Enemies have reduced health
swordless: off hard: 0 # Enemies have increased health
- option_name: legacy_weapons expert: 0 # Enemies have greatly increased health
option_result: assured pot_shuffle:
options: 'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile
swordless: off 'off': 50 # Default pot item locations
startinventory: ### End of Enemizer Section ###
Progressive Sword: 1 beemizer: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps
- option_name: legacy_weapons 0: 50 # No bee traps are placed
option_result: vanilla 1: 0 # 25% of rupees, bombs and arrows are replaced with bees, of which 60% are traps and 40% single bees
options: 2: 0 # 50% of rupees, bombs and arrows are replaced with bees, of which 70% are traps and 30% single bees
swordless: off 3: 0 # 75% of rupees, bombs and arrows are replaced with bees, of which 80% are traps and 20% single bees
plando_items: 4: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 90% are traps and 10% single bees
- items: 5: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 100% are traps and 0% single bees
Progressive Sword: 4 ### Shop Settings ###
locations: shop_item_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
- Master Sword Pedestal 0: 50
- Pyramid Fairy - Left 5: 0
- Blacksmith 15: 0
- Link's Uncle 30: 0
- option_name: legacy_weapons random: 0 # 0 to 30 evenly distributed
option_result: swordless shop_shuffle:
options: none: 50
swordless: on g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
# end of legacy weapons block f: 0 # Generate new default inventories for every shop independently
- option_name: enemy_damage # targets enemy_damage i: 0 # Shuffle default inventories of the shops around
option_result: shuffled # if it rolls shuffled p: 0 # Randomize the prices of the items in shop inventories
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works) u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
options: # then inserts these options w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
swordless: off ip: 0 # Shuffle inventories and randomize prices
### door rando only options (not supported at all yet on this branch) ### fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool
door_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool
vanilla: 50 # Everything should be like in vanilla # You can add more combos
basic: 0 # Dungeons are shuffled within themselves ### End of Shop Section ###
crossed: 0 # Dungeons are shuffled across each other shuffle_prizes: # aka drops
# you can also define door shuffle seed, like so: none: 0 # do not shuffle prize packs
crossed-1000: 0 # using this method, you can have the same dungeon layout as another player and share dungeon layout information. g: 50 # shuffle "general" prize packs, as in enemy, tree pull, dig etc.
# however, other settings like intensity, universal keys, etc. may affect the shuffle result as well. b: 0 # shuffle "bonk" prize packs
crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed bg: 0 # shuffle both
intensity: # Only available if the host uses the doors branch, it is ignored otherwise timer:
1: 50 # Shuffles normal doors and spiral staircases none: 50 # No timer will be displayed.
2: 0 # And shuffles open edges and straight staircases timed: 0 # Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.
3: 0 # And shuffles dungeon lobbies timed_ohko: 0 # Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.
random: 0 # Picks one of those at random ohko: 0 # Timer always at zero. Permanent OHKO.
key_drop_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.
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. display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool.
off: 50 countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with
experimental: # Only available if the host uses the doors branch, it is ignored otherwise 0: 0 # For timed_ohko, starts in OHKO mode when starting the game
on: 0 # Enables experimental features. 10: 50
off: 50 20: 0
debug: # Only available if the host uses the doors branch, it is ignored otherwise 30: 0
on: 0 # Enables debugging features. Currently, these are the Item collection counter. (overwrites total triforce pieces) and Castle Gate closed indicator. 60: 0
off: 50 red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock
### end of door rando only options ### -2: 50
rom: 1: 0
blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock
1: 0
2: 50
green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock
4: 50
10: 0
15: 0
glitch_boots:
on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them
off: 0
# rom options section
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
@@ -508,7 +421,7 @@ rom:
randomonslash: 0 # Random sprite on sword slashes randomonslash: 0 # Random sprite on sword slashes
randomonitem: 0 # Random sprite on getting items. randomonitem: 0 # Random sprite on getting items.
randomonbonk: 0 # Random sprite on bonk. randomonbonk: 0 # Random sprite on bonk.
# You can combine these events like this. randomonhit-enter-exit if you want it on hit, enter, exit. # You can combine these events like this. randomonhit-enter-exit if you want it on hit, enter, exit.
randomonall: 0 # Random sprite on any and all currently supported events. Refer to above for the supported events. randomonall: 0 # Random sprite on any and all currently supported events. Refer to above for the supported events.
Link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it Link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it
disablemusic: # If "on", all in-game music will be disabled disablemusic: # If "on", all in-game music will be disabled
@@ -594,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

@@ -1,11 +1,7 @@
colorama>=0.4.4 colorama>=0.4.4
websockets>=9.0.2 websockets>=9.1
PyYAML>=5.4.1 PyYAML>=5.4.1
fuzzywuzzy>=0.18.0 fuzzywuzzy>=0.18.0
bsdiff4>=1.2.1
prompt_toolkit>=3.0.18 prompt_toolkit>=3.0.18
appdirs>=1.4.4 appdirs>=1.4.4
maseya-z3pr>=1.0.0rc1 jinja2>=3.0.1
xxtea>=2.0.0.post0
factorio-rcon-py>=1.2.1
jinja2>=3.0.0

View File

@@ -38,20 +38,20 @@ def _threaded_hash(filepath):
os.makedirs(buildfolder, exist_ok=True) os.makedirs(buildfolder, exist_ok=True)
def manifest_creation(): def manifest_creation(folder):
hashes = {} hashes = {}
manifestpath = os.path.join(buildfolder, "manifest.json") manifestpath = os.path.join(folder, "manifest.json")
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor() pool = ThreadPoolExecutor()
for dirpath, dirnames, filenames in os.walk(buildfolder): for dirpath, dirnames, filenames in os.walk(folder):
for filename in filenames: for filename in filenames:
path = os.path.join(dirpath, filename) path = os.path.join(dirpath, filename)
hashes[os.path.relpath(path, start=buildfolder)] = 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")
@@ -161,4 +161,79 @@ for file in os.listdir(alttpr_sprites_folder):
if file != ".gitignore": if file != ".gitignore":
os.remove(alttpr_sprites_folder / file) os.remove(alttpr_sprites_folder / file)
manifest_creation() manifest_creation(buildfolder)
buildfolder = Path("build_factorio", folder)
sbuildfolder = str(buildfolder)
libfolder = Path(buildfolder, "lib")
library = Path(libfolder, "library.zip")
print("Outputting Factorio Client to: " + sbuildfolder)
os.makedirs(buildfolder, exist_ok=True)
scripts = {"FactorioClient.py": "ArchipelagoConsoleFactorioClient"}
exes = []
for script, scriptname in scripts.items():
exes.append(cx_Freeze.Executable(
script=script,
target_name=scriptname + ("" if sys.platform == "linux" else ".exe"),
icon=icon,
))
exes.append(cx_Freeze.Executable(
script="FactorioClientGUI.py",
target_name="ArchipelagoGraphicalFactorioClient" + ("" if sys.platform == "linux" else ".exe"),
icon=icon,
base="Win32GUI"
))
import datetime
buildtime = datetime.datetime.utcnow()
cx_Freeze.setup(
name="Archipelago Factorio Client",
version=f"{buildtime.year}.{buildtime.month}.{buildtime.day}.{buildtime.hour}",
description="Archipelago Factorio Client",
executables=exes,
options={
"build_exe": {
"packages": ["websockets", "kivy"],
"includes": [],
"excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas"],
"zip_include_packages": ["*"],
"zip_exclude_packages": ["kivy"],
"include_files": [],
"include_msvcr": True,
"replace_paths": [("*", "")],
"optimize": 2,
"build_exe": buildfolder
},
},
)
extra_data = ["LICENSE", "data", "host.yaml", "meta.yaml"]
from kivy_deps import sdl2, glew
for folder in sdl2.dep_bins+glew.dep_bins:
shutil.copytree(folder, buildfolder, dirs_exist_ok=True)
for data in extra_data:
installfile(Path(data))
os.makedirs(buildfolder / "Players", exist_ok=True)
shutil.copyfile("playerSettings.yaml", buildfolder / "Players" / "weightedSettings.yaml")
if signtool:
for exe in exes:
print(f"Signing {exe.target_name}")
os.system(signtool + os.path.join(buildfolder, exe.target_name))
alttpr_sprites_folder = buildfolder / "data" / "sprites" / "alttpr"
for file in os.listdir(alttpr_sprites_folder):
if file != ".gitignore":
os.remove(alttpr_sprites_folder / file)
manifest_creation(buildfolder)

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

@@ -201,14 +201,9 @@ class TestAdvancements(TestMinecraft):
["Hot Tourist Destinations", False, [], ['Ingot Crafting']], ["Hot Tourist Destinations", False, [], ['Ingot Crafting']],
["Hot Tourist Destinations", False, [], ['Flint and Steel']], ["Hot Tourist Destinations", False, [], ['Flint and Steel']],
["Hot Tourist Destinations", False, [], ['Progressive Tools']], ["Hot Tourist Destinations", False, [], ['Progressive Tools']],
["Hot Tourist Destinations", False, [], ['Progressive Weapons']],
["Hot Tourist Destinations", False, [], ['Progressive Armor', 'Shield']],
["Hot Tourist Destinations", False, [], ['Fishing Rod']],
["Hot Tourist Destinations", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], ["Hot Tourist Destinations", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor', 'Flint and Steel', 'Bucket', 'Fishing Rod']], ["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket']],
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Fishing Rod']], ["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools']],
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Progressive Weapons', 'Shield', 'Flint and Steel', 'Bucket', 'Fishing Rod']],
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Progressive Weapons', 'Shield', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Fishing Rod']],
]) ])
def test_42015(self): def test_42015(self):
@@ -979,7 +974,8 @@ class TestAdvancements(TestMinecraft):
["Sticky Situation", False, []], ["Sticky Situation", False, []],
["Sticky Situation", False, [], ['Bottles']], ["Sticky Situation", False, [], ['Bottles']],
["Sticky Situation", False, [], ['Ingot Crafting']], ["Sticky Situation", False, [], ['Ingot Crafting']],
["Sticky Situation", True, ['Bottles', 'Ingot Crafting']], ["Sticky Situation", False, [], ['Campfire']],
["Sticky Situation", True, ['Bottles', 'Ingot Crafting', 'Campfire']],
]) ])
def test_42075(self): def test_42075(self):
@@ -1099,16 +1095,17 @@ class TestAdvancements(TestMinecraft):
self.run_location_tests([ self.run_location_tests([
["When Pigs Fly", False, []], ["When Pigs Fly", False, []],
["When Pigs Fly", False, [], ['Ingot Crafting']], ["When Pigs Fly", False, [], ['Ingot Crafting']],
["When Pigs Fly", False, [], ['Flint and Steel']],
["When Pigs Fly", False, [], ['Progressive Tools']], ["When Pigs Fly", False, [], ['Progressive Tools']],
["When Pigs Fly", False, [], ['Progressive Weapons']], ["When Pigs Fly", False, [], ['Progressive Weapons']],
["When Pigs Fly", False, [], ['Progressive Armor', 'Shield']], ["When Pigs Fly", False, [], ['Progressive Armor', 'Shield']],
["When Pigs Fly", False, [], ['Fishing Rod']], ["When Pigs Fly", False, [], ['Fishing Rod']],
["When Pigs Fly", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], ["When Pigs Fly", False, ['Progressive Weapons'], ['Flint and Steel', 'Progressive Weapons', 'Progressive Weapons']],
["When Pigs Fly", False, ['Progressive Tools', 'Progressive Tools', 'Progressive Weapons'], ['Bucket', 'Progressive Tools', 'Progressive Weapons', 'Progressive Weapons']],
["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Progressive Armor', 'Fishing Rod']], ["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Progressive Armor', 'Fishing Rod']],
["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor', 'Fishing Rod']], ["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor', 'Fishing Rod']],
["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield', 'Fishing Rod']], ["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield', 'Fishing Rod']],
["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Shield', 'Fishing Rod']], ["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Shield', 'Fishing Rod']],
["When Pigs Fly", True, ['Progressive Weapons', 'Progressive Weapons', 'Progressive Armor', 'Shield', 'Ingot Crafting', 'Progressive Tools', 'Fishing Rod']],
]) ])
def test_42089(self): def test_42089(self):

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,25 +241,11 @@ 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] in ['triforcehunt', 'localtriforcehunt']:
region = world.get_region('Light World', player)
loc = ALttPLocation(player, "Murahdahla", parent=region)
loc.access_rule = lambda state: state.has_triforce_pieces(state.world.treasure_hunt_count[player], player)
region.locations.append(loc)
world.dynamic_locations.append(loc)
world.clear_location_cache()
world.push_item(loc, ItemFactory('Triforce', player), False)
loc.event = True
loc.locked = True
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)
world.push_item(loc, ItemFactory('Triforce', player), False) world.push_item(loc, ItemFactory('Triforce Piece', player), False)
world.treasure_hunt_count[player] = 1
if world.boss_shuffle[player] != 'none': if world.boss_shuffle[player] != 'none':
if 'turtle rock-' not in world.boss_shuffle[player]: if 'turtle rock-' not in world.boss_shuffle[player]:
world.boss_shuffle[player] = f'Turtle Rock-Trinexx;{world.boss_shuffle[player]}' world.boss_shuffle[player] = f'Turtle Rock-Trinexx;{world.boss_shuffle[player]}'
@@ -267,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)
@@ -295,6 +280,19 @@ def generate_itempool(world, player: int):
for item in itempool: for item in itempool:
world.push_precollected(ItemFactory(item, player)) world.push_precollected(ItemFactory(item, player))
if world.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']:
region = world.get_region('Light World', player)
loc = ALttPLocation(player, "Murahdahla", parent=region)
loc.access_rule = lambda state: state.has_triforce_pieces(state.world.treasure_hunt_count[player], player)
region.locations.append(loc)
world.dynamic_locations.append(loc)
world.clear_location_cache()
world.push_item(loc, ItemFactory('Triforce', player), False)
loc.event = True
loc.locked = True
world.get_location('Ganon', player).event = True world.get_location('Ganon', player).event = True
world.get_location('Ganon', player).locked = True world.get_location('Ganon', player).locked = True
@@ -570,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)
@@ -1730,7 +1735,7 @@ def write_custom_shops(rom, world, player):
item_game = 'Factorio' item_game = 'Factorio'
elif item_name in mc_lookup.values(): elif item_name in mc_lookup.values():
item_game = 'Minecraft' item_game = 'Minecraft'
else: else:
item_game = 'Generic' item_game = 'Generic'
item_code = get_nonnative_item_sprite(item_game) item_code = get_nonnative_item_sprite(item_game)
else: else:
@@ -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()
@@ -2337,9 +2342,13 @@ def write_strings(rom, world, player, team):
tt['sign_ganon'] = f'You need {world.crystals_needed_for_ganon[player]} crystals to beat Ganon and ' \ tt['sign_ganon'] = f'You need {world.crystals_needed_for_ganon[player]} crystals to beat Ganon and ' \
f'have beaten Agahnim atop Ganons Tower' f'have beaten Agahnim atop Ganons Tower'
elif world.goal[player] == "icerodhunt": elif world.goal[player] == "icerodhunt":
tt['sign_ganon'] = 'Go find the Ice Rod and Kill Trinexx... Ganon is invincible!' tt['sign_ganon'] = 'Go find the Ice Rod and Kill Trinexx, then talk to Murahdahla... Ganon is invincible!'
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Go kill Trinexx instead.' tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Go kill Trinexx instead.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
"hidden in a hollow tree. " \
"If you bring me the Triforce piece from Turtle Rock, I can reassemble it."
else: else:
if world.crystals_needed_for_ganon[player] == 1: if world.crystals_needed_for_ganon[player] == 1:
tt['sign_ganon'] = 'You need a crystal to beat Ganon.' tt['sign_ganon'] = 'You need a crystal to beat Ganon.'
@@ -2354,7 +2363,7 @@ def write_strings(rom, world, player, team):
tt['sahasrahla_quest_have_master_sword'] = Sahasrahla2_texts[local_random.randint(0, len(Sahasrahla2_texts) - 1)] tt['sahasrahla_quest_have_master_sword'] = Sahasrahla2_texts[local_random.randint(0, len(Sahasrahla2_texts) - 1)]
tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)] tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)]
if world.goal[player] in ['triforcehunt', 'localtriforcehunt']: if world.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.' tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
if world.goal[player] == 'triforcehunt' and world.players > 1: if world.goal[player] == 'triforcehunt' and world.players > 1:
@@ -2364,12 +2373,12 @@ def write_strings(rom, world, player, team):
if world.treasure_hunt_count[player] > 1: if world.treasure_hunt_count[player] > 1:
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \ tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
"hidden in a hollow tree. If you bring\n%d triforce pieces out of %d, I can reassemble it." % \ "hidden in a hollow tree. If you bring\n%d Triforce pieces out of %d, I can reassemble it." % \
(world.treasure_hunt_count[player], world.triforce_pieces_available[player]) (world.treasure_hunt_count[player], world.triforce_pieces_available[player])
else: else:
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \ tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
"hidden in a hollow tree. If you bring\n%d triforce piece out of %d, I can reassemble it." % \ "hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \
(world.treasure_hunt_count[player], world.triforce_pieces_available[player]) (world.treasure_hunt_count[player], world.triforce_pieces_available[player])
elif world.goal[player] in ['pedestal']: elif world.goal[player] in ['pedestal']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.' tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'

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

Some files were not shown because too many files have changed in this diff Show More