Compare commits

..

126 Commits

Author SHA1 Message Date
CaitSith2
5ce892cbb8 docstring updated to clarify impact on collecting player. 2023-03-14 14:14:11 -07:00
CaitSith2
bc7389fbaa Add a todo regarding backwards compatibility 2023-03-14 13:46:24 -07:00
CaitSith2
9cb5a7fc3a Merge branch 'main' into allow_collect 2023-03-14 13:33:39 -07:00
alwaysintreble
e433246f0c The Messenger: docs improvement (#1545)
* The Messenger: docs improvement

* more wordy mod link

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* indent

* revert accidental indent

oop

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-14 21:18:55 +01:00
black-sliver
3a190a8fb2 CI: more filters, update CodeQL (#1540)
* CI: fix and more greedy filtering

* CI: only run lint if *.py changed

* CI: only run CodeQL if supported file changed

* CI: fix unittests still triggering for build.yml

* CI: update CodeQL action

* CI: trigger codeql when changing the workflow
2023-03-14 19:29:20 +01:00
Alchav
4b7033fce7 Pokemon R/B: Version 3 final touches (#1542)
* Pokémon R/B: Dexsanity balls

* Pokémon R/B: Early Parcel improvement

* Pokémon R/B: Early Parcel dexsanity stuff only when dexsanity
2023-03-14 18:36:17 +01:00
lordlou
37499b40a1 SMZ3: shop check fix 2 (#1538) 2023-03-14 18:31:51 +01:00
black-sliver
ca2c0e6ce2 CI: update stuff (#1534)
* CI: skip SNI, skip unittests if not needed, run build for setup.py

* CI: update actions

* CI: update upload-artifact

Fixes more warnings
2023-03-14 01:32:00 +01:00
black-sliver
2a28a6de28 Pokemon: apply rename of location_item_name 2023-03-14 01:30:58 +01:00
alwaysintreble
573a1a8402 Core: Add a function to allow worlds to easily allow self-locking items (#1383)
* implement function to allow self locking items for items accessibility

* swap some lttp locations to use new functionality

* lambda capture `item_name` and `location`

* don't lambda capture location

* Revert weird visual indent

* make location.always_allow additive

* fix always_allow rule for multiple items

* don't need to lambda capture item_names

* oop

* move player assignment to the beginning

* always_allow should only be for that player so prevent non_local_items

* messenger got merged so have it use this

* Core: fix doc string indentation for allow_self_locking_items

* Core: fix doc string indentation for allow_self_locking_items, number two

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-14 00:55:34 +01:00
Jarno
060ee926e7 Core: type specified missing per_game_common_options (#1509) 2023-03-13 23:45:56 +01:00
Alchav
df55455fc0 Pokémon R/B: Version 3 (#1520)
* Coin items received or found in the Game Corner are now shuffled, locations require Coin Case
* Prizesanity option (shuffle Game Corner Prizes)
* DexSanity option: location checks for marking Pokémon as caught in your Pokédex. Also an option to set all Pokémon in your Pokédex as seen from the start, to aid in locating them.
* Option to randomize the layout of the Rock Tunnel.
* Area 1-to-1 mapping: When one instance of a Wild Pokémon in a given area is randomized, all instances of that Pokémon will be the same. So that if a route had 3 different Pokémon before, it will have 3 after randomization.
* Option to randomize the moves taught by TMs.
* Exact controls for TM/HM compatibility chances.
* Option to randomize Pokémon's pallets or set them based on primary type.
* Added Cinnabar Gym trainers to Trainersanity and randomized the quiz questions and answers. Getting a correct answer will flag the trainer as defeated so that you can obtain the Trainersanity check without defeating the trainer if you answer correctly.
2023-03-13 23:40:55 +01:00
Fabian Dill
4d7bd929bc WebHost: update modules 2023-03-13 21:34:24 +01:00
Fabian Dill
030e41363a Setup: update cx-Freeze 2023-03-13 21:34:07 +01:00
Qwazzy
4bc0e84a7f Docs: SM64 Guide update to explain how to launch the game with batch files (#768)
* Update setup_en.md

Added several sections in regards to opening the completed SM64 build with batch files instead of SM64PCLauncher.

* Update setup_en.md

* Update setup_en.md

* Update setup_en.md

* Update setup_en.md

* Apply suggestions from code review

Co-authored-by: Yussur Mustafa Oraji <N00byKing@hotmail.de>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* Apply more suggestions from code review

matches the original suggestion from SoldierofOrder

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Yussur Mustafa Oraji <N00byKing@hotmail.de>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
2023-03-13 00:58:17 +01:00
alwaysintreble
070a92e76c The Messenger: implement new game (#1494)
* initial commit of messenger integration

* setup no_logic and needed slot_data

* fix some typos and determinism

* make all of it deterministic

* add documentation

* swapped to non local items so change the fed data

* ~~deathlink~~

* satisfy the docs test

* update doc test to show expected name

* split custom classes into a separate file and fix an errant rule

* make access dependency test give more useful errors

* implement tests

* remove some unneccessary back entrances and make names clearer

* fix some big dumbs

* successful unit tests are good also some slight reorganizing

* add astral tea quest line, and potentially power seals as items

* if TYPE_CHECKING... aahhhhhh

* oop forgot to remove legacy code

* having the seed and leaves as actual items doesn't seem to do anything so remove them. locations still work though

* update setup guide with some changes

* Tower HQ was creating duplicate locations

* allow self locking items

* cleanup

* move self_locking_items function to core

* docstring

* implement choice of notes needed for music box

* test the default value

* don't create any starting inventory items

* make item creation faster

* change default accessibility and power seals options

* improve documentation

* precollected_items is a dict of Items...

* implement shop chest goal

* tests

* always assign total and required seals

* add new goals and set music box as requiring shop chest on shop chest goals instead of just setting it as the completion

* fix dumb test quirk

* implement music box skip as an option

* world rewrite/cleanup

* default to apworld and add game to readme

* revert bleeding commits from other PRs

* more bleeds

* fix some errors in options docstrings

* ???

* make my set rules method not have an awful name

* test cleanup

* add a test for item accessibility

* fix issues with tests

* make the self locking item behavior work correctly

* misc cleanup

* more general cleanup to be a good example

* quick rules rewrite

* more general cleanup and typing

* more speed, more clean

* bump data version

* make sure the locked item belongs to current player

* fix bad name and indent. call MessengerItem directly for events

* add poptracker pack to docs

* doc cleanup and "known issues" section that I probably won't be able to fix any time soon.

* missed some spots

* add another bug i forgot about

* be consistently wrong
2023-03-12 15:05:50 +01:00
Fabian Dill
39563cc347 WebHost: add game and Factorio to multitracker (#1526)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-12 12:38:13 +01:00
alwaysintreble
54cce4c392 LTTP: fix ice rod hunt boss shuffle (#1529) 2023-03-12 11:03:48 +01:00
el-u
426a81a065 oot/alttp: fix bugs found through MMBN3 testing (#1527) 2023-03-11 20:15:30 +01:00
alwaysintreble
04e6a8eae8 FFR: add option __doc__s 2023-03-11 13:38:27 +01:00
NewSoupVi
0cfdc973f6 The Witness - Expert logic bug (could lead to broken seeds) (#1525) 2023-03-11 10:09:09 +01:00
alwaysintreble
f3ca0a21c9 Docs: Add an option api doc (#1181)
* write up an option api doc

* address reviews

* some clarification

* add note about using schema

* Add ItemSet and formatting

* bulletpoint option defining

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* split random description to new sentence

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* use inclusive and parallel language for example

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* changes from review

* commas

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* capitalize Toggle

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* the sliver conventions

---------

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
2023-03-11 01:14:44 +01:00
Chris Wilson
5fef41eb97 [WebHost] Make site header mobile-friendly (#1523) 2023-03-10 17:21:17 -05:00
alwaysintreble
4068ba2f15 LTTP: add option __doc__s (#1521)
* LTTP: add option `__doc__`s

* review comments
2023-03-10 07:59:47 +01:00
NewSoupVi
b1599c557f The Witness: Death Link + Small bug fixes (#1515)
* Fully functional DeathLink implementation. But it's always on right now :D

* Death Link options. Last commit: All entity names being sent through slot_data

* Tutorial Gate Close logic fix

* Improved option tooltip wording

* Fixed shuffle_postgame: false not excluding some locations

* Link to latest stable client rather than full releases page
2023-03-10 07:58:00 +01:00
Fabian Dill
7fdf38b2ad WebHost: automatically fill PATCH_TARGET -> HOST_ADDRESS and re-use it for rooms (#1518) 2023-03-09 21:31:00 +01:00
Freya Arbjerg
2e76085cf1 WebHost: Fix generic tracker Datatables (#1519) 2023-03-09 19:24:38 +01:00
Fabian Dill
c61f467218 WebHost: fix location_name_group related spinup crash 2023-03-09 12:31:35 +01:00
KonoTyran
942d689093 [Slay the Spire] Enable support for modded characters, and add downfall support (#1368)
* add ability to choose custom characters in STS

* bump required protocol (client?) version.

* fix slot data fill.

* add downfall mode, as well as characters.

* small change in documentation for character choice as it now uses internal ID's instead of visible titles... because other languages are a thing.
2023-03-08 20:14:54 -08:00
espeon65536
5e1aa52373 Minecraft rewrite (#1493)
* Minecraft: rewrite to modern AP standards

* Fix gitignore to not try to ignore the entire minecraft world

* minecraft: clean up MC-specific tests

* minecraft: use pkgutil instead of open

* minecraft: ship as apworld

* mc: update region to new api

* Increase upper limit on advancement and egg shard goals

* mc: reduce egg shard count by 5 for structure compasses

* Minecraft: add more tests
Ensures data loading works; tests beatability with various options at their max setting; new tests for 1.19 advancements

* test improvements

* mc: typing and imports cleanup

* parens

* mc: condense filler item code and override get_filler_item_name
2023-03-08 20:13:52 -08:00
Freya Arbjerg
a95e51deda Add generic multiworld tracker, move lttp multiworld tracker (#1478)
Co-authored-by: Berserker
2023-03-08 22:39:15 +01:00
alwaysintreble
738319462d Spoiler: Don't double print if world overrides common options (#1505) 2023-03-08 22:19:38 +01:00
alwaysintreble
e3deb822ad Core: implement location_name_groups (#1502) 2023-03-08 22:15:28 +01:00
Jarno
d57314a407 Timespinner: Bring back starter progression item (#1508) 2023-03-08 20:05:30 +01:00
t3hf1gm3nt
5a8e6e61f5 TLOZ: Code Cleanup (#1514)
- consolidated declaration and population of level location lists
- moved floor_location_game_ids_late declaration for consistency
- moved generate_itempool to create_items, where it belongs
- mention that expanded pool includes take any caves in the option description again
- removed unnecessary StartingPosition check regarding Take Any Caves (leftover from older StartingPosition behavior I believe)
- use proper comparisons to option keys instead of hardcoded ints
2023-03-08 11:22:14 +01:00
Magnemania
17e90ce12c SC2: Greater variety on short generations (#1367)
Originally, short generations used an artificial cull to create balanced mission distributions. This resulted in campaigns that were somewhat too consistent, and on some standard settings combinations, this resulted in campaigns having The Outlaws as the second mission 100% of the time. It also caused generation to fail a bit too easily if the player excluded too many missions.

This removes the cull and adds an additional early Easy mission slot to all of the reduced sized campaigns.

When playing on No Build settings, this also pushes many of the missions down a difficulty level to ensure greater variety, and pushes additional missions down on Advanced Tactics.

Additional small fixes:

The in-world Excluded Missions validation check is replaced by the core OptionSet check.
Fixed issue with Existing Items not getting their upgrades locked with Units Always Have Upgrades on.
2023-03-07 14:14:49 +01:00
NewSoupVi
016157a0eb Witness: Fixed settings combination not rolling (see description)
Settings combination:
- EP Shuffle
- disable_non_randomized
- doors: panels or doors: none

An Event Item was being created that is inaccessible. This is fixed now.
(The fix makes sure that player_logic is not trying to create events for the sake of EPs that are disabled)

Note: These two sets should probably be merged anyway, they used to behave differenty but no longer really do. But that will require some extra care on the client side as well.
2023-03-07 09:13:54 +01:00
Fabian Dill
5b64c5f934 Subnautica: fix exported radiation logic (#1507) 2023-03-07 09:09:24 +01:00
alwaysintreble
414166f6a2 Core: Minor Options cleanup (#1182)
* Options.py cleanup

* TextChoice cleanup

* make Option.current_option_name a property

* title the TextChoce option names

* satisfy the linter

* a little more options cleanup

* move the typing import

* typing should be PlandoSettings

* fix incorrect conflict merging

* make imports local

* the tests seem to want me to import these twice though i hate it.

* changes from review. Make the various Location verifying Options `LocationSet`

* remove unnecessary fluff

* begrudgingly support get_current_option_name. Leave a comment that worlds shouldn't be touching this

* log a deprecation warning and return the property for `get_current_option_name()`

---------

Co-authored-by: beauxq <beauxq@yahoo.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-07 08:44:20 +01:00
beauxq
e6109394ad Zillion: use Option.current_key
and other minor fixes
2023-03-07 08:33:33 +01:00
Fabian Dill
8ca25fed63 Setup: clean up imports 2023-03-06 12:54:32 +01:00
0rganics
227d59ecfb WebHost: Add a ChecksFinder tracker (#1333)
Co-authored-by: Chris Wilson <chris@legendserver.info>
2023-03-05 14:17:04 +01:00
Fabian Dill
08c17c83d4 Setup: auto download SNI (#1312)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-05 14:10:05 +01:00
Rosalie-A
efb2ab4505 TLoZ: Implementing The Legend of Zelda (#1354)
Co-authored-by: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com>
Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com>
2023-03-05 13:31:31 +01:00
Joethepic
3a68ce3faa PKMN: Make Exp All early (#1422) 2023-03-05 10:08:32 +01:00
black-sliver
e78800d1bc Item Plando: make world selection deterministic 2023-03-05 02:16:55 +01:00
Jérémie Bolduc
96d7a3a64c Stardew Valley: Fix generation issue with Master Angler goal and vanilla tools (#1498)
* - Can Catch every fish doesn't need fishing rods if they are not shuffled

* add has_max_fishing_rod

* add test for master angler + vanilla tools

---------

Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-03-04 18:34:51 +01:00
recklesscoder
30b70b2055 Misc collected fixes (#1497) 2023-03-04 16:34:10 +01:00
Jarno
cd234fc04a Timespinner: Fixed Dry lake serene oddity (#1501) 2023-03-04 16:31:44 +01:00
zig-for
d74c4c4c94 Core: Remove ALTTP cruft from BaseClasses (#1451) 2023-03-04 08:23:52 +01:00
Jarno
a4b61118cf Timespinner: Refactorings + fix for #1460 (#1484) 2023-03-04 08:16:05 +01:00
FlySniper
9fa1f4e85f Wargroove: Fixed Wargroove Client not removing communication files (#1492) 2023-03-03 18:24:09 +01:00
black-sliver
3a926849a0 CI: run unittests on macos
this is to ensure dependencies can be installed and loaded on macos (on AMD64)
2023-03-03 18:22:31 +01:00
Alchav
798d823397 Core: Check for show_in_spoiler (#1500) 2023-03-03 17:22:46 +01:00
Fabian Dill
4ea582f14e Windows: allow arm64 setup (#1496) 2023-03-03 09:51:36 +01:00
alwaysintreble
21fb16291d Tests: test that the game is beatable for WorldTestBases (#1495)
* Tests: test that the game is beatable for WorldTestBases

* update docstring

* don't test the bases with default options for real this time

* invert the property so worlds can use it easier

* setup check should be or

* test class needs to always be constructed

* skip default tests before multiworld setup

* check if the calling method is in the base's __dict__

* shorter property and functional setup skipping

* shorter property and functional setup skipping
2023-03-03 00:30:40 +01:00
NewSoupVi
805f33c39e Witness: Bugfixes in response to beta tests (#1473)
* Make all Keep Pressure Plates logically required for the Laser Panel

* Added more Tutorial checks

* Added the remaining two Shipwreck Boat EPs to the exclude list for normal

* Improved itempool filling system, added warning if usefuls had to be eaten

* Moved creation of said warning string to utils

* Fixed logic bug causing broken seeds on Mountain Floor 2

* Hints system change

* Expert Logic Fix

* Fixed typo

* Better wording

* Added missing games to junk hints

* Made sure Entrance names are unique

* Fixed missing Obelisk Side

* Disable Non Randomized + EP Shuffle fix

* Fixed disable_non_randomized precompleted EPs being 'disabled' instead of 'precompleted'

* Fixed if/elif error

* Tutorial Gate Open local symbol item becomes local_early_item in expert instead

* Bump required client version. There is a beta client that sends 0.3.9.

* Removed print statement, oops

* Fixed itempool manipulation in pre_fill

* Replaced string concats with fstrings

* Improved make_warning_string function signature

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Improved performance on removing multiple items from multiworld itempool

* Comment

* Fixed errors with the code

* Made removal from itempool not fail unit test for multiple references

* Moved all item creation to create_items, got rid of itempool modifying system

* Colored Squares is no longer a good item, that's outdated

* Removed double if

* React to from_pool: false by removing a junk item

* Fixed warning if only Fnc Brain was removed

* Make use of string truthiness instead

* Made reading of plandoed items safer

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-03 00:08:24 +01:00
el-u
0cf8206660 launcher: add .apl2ac support 2023-03-01 22:56:14 +01:00
Fabian Dill
2c20b56478 Core: count the world types 2023-03-01 05:47:46 +01:00
FlySniper
1d2f7d8669 Wargroove: Fixed the find all dogs check activating prematurely (#1486) 2023-02-28 16:26:48 +01:00
Fabian Dill
0733775f2c Subnautica: Allow either utility room for progression 2023-02-28 11:22:47 +01:00
black-sliver
d6f3b27695 DKC3, SMW: use user_path for file
Same as for other games, this will resolve to ~/Archipelago on Linux, if the install folder is read-only
2023-02-28 09:51:32 +01:00
black-sliver
ce7e6bcf33 Readme: fix order 2023-02-28 09:15:23 +01:00
Jarno
2c4658a7e0 Docs: More games more fun 2023-02-27 23:19:33 +01:00
TheLynk
79b8733b13 OoT, MC: add new translation setup in french (#1410)
* add new translation

* Add translation for OOT Setup in french

* Update setup_fr.md

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update setup_fr.md

Fix treu to true

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Marech <marechal-l@gmx.com>

* Update OOT Init and Update Minecraft Init

* Fix formatting errors

---------

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-27 23:17:54 +01:00
alwaysintreble
9cb9cbe47d Tests: test that worlds don't create regions or locations after create_items (#1465)
* Tests: test that worlds don't create regions or locations after `create_items`

* recache during the location counts just to be extra safe

* adjust typing and use a Tuple instead of a list

* remove unused import
2023-02-27 02:13:24 +01:00
alwaysintreble
7cad53c31a Docs: add docstrings to the World class 2023-02-27 01:39:30 +01:00
alwaysintreble
f3bdf0c5ed Tests: test all state and empty state on world test bases (#1476)
* Tests: test all state and empty state on world test bases

* actually add the test methods to the dict

* only test if the world test base has non default options

* remove temp logging

* ditch the meta class and document methods

* Tests: WorldTestBase comment and docstring cleanup

* skip default tests if setUp or world_setup are modified and use a property

* negation hurts my head

* docstring

* use a better name for the property

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-27 01:24:54 +01:00
Jérémie Bolduc
af7d0dbf37 Stardew Valley: implement new game (#1455)
* Stardew Valley Archipelago implementation

* fix breaking changes

* - Added and Updated Documentation for the game

* Removed fun

* Remove entire idea of step, due to possible inconsistency with the main AP core

* Commented out the desired steps, fix renaming after rebase

* Fixed wording

* tests now passes on 3.8

* run flake8

* remove dependency so apworld work again

* remove dependency for real

* - Fix Formatting in the Game Page
- Removed disabled Option Descriptions for Entrance Randomizer
- Improved Game Page's description of the Arcade Machine buffs
- Trimmed down the text on the Options page for Arcade Machines, so that it is smaller

* - Removed blankspace

* remove player field

* remove None check in options

* document the scripts

* fix pytest warning

* use importlib.resources.files

* fix

* add version requirement to importlib_resources

* remove __init__.py from data folder

* increment data version

* let the __init__.py for 3.9

* use sorted() instead of list()

* replace frozenset from fish_data with tuples

* remove dependency on pytest

* - Add a bit of text to the guide to tell them about how to redeem some received items

* - Added a comment about which mod version to use

* change single quotes for double quotes

* Minimum client version both ways

* Changed version number to be more specific. The mod will handle deciding

---------

Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-02-27 01:19:15 +01:00
Fabian Dill
0286edf20c Subnautica: fix the test that I didn't mean to push yet 2023-02-26 09:51:02 +01:00
Fabian Dill
05e36cab1c Subnautica: increment version 2023-02-26 09:48:51 +01:00
Klenoa
50425985c4 Subnautica: Rename location check (id 33029) (#1120)
Update southwest grassy plateaus wreck to 'Grassy Plateaus Southwest Wreck - Databox' instead of 'Grassy Plateaus West Wreck - Databox'
2023-02-26 09:48:08 +01:00
Marech
062d6eeace DS3: Added DLC Items/Locations + corresponding option and added an option to enable materials/consumables/estus randomization (#1301)
- Added more progressive locations and associated items.
- Added an option to enable materials/consumables/estus randomization, some players complain about the number of locations and the randomness of those items.
- Added an option to add DLC Items and Locations to the pool, the player must own both the ASHES OF ARIANDEL and the RINGED CITY DLC.

Co-authored-by: Br00ty <83629348+Br00ty@users.noreply.github.com>
Co-authored-by: Friðberg Reynir Traustason <fridberg.traustason@gmail.com>
2023-02-26 06:35:03 +01:00
alwaysintreble
6c460bcbf7 LTTP: Move LTTP spoiler writing out of core (#1467) 2023-02-25 04:02:51 +01:00
toasterparty
b8659d28cc [OC2] DeathLink (#1470) 2023-02-24 08:32:15 +01:00
alwaysintreble
0b12d80008 Tracker: get game names from slot_info instead of multidata["games"] and render custom game names on generic tracker (#1453) 2023-02-24 08:30:11 +01:00
FlySniper
5966aa5327 Wargroove: Implement New Game (#1401)
This adds Wargroove to the list of supported games. Wargroove uses a custom non-linear campaign over the vanilla and double trouble campaigns. A Wargroove client has been added which does a lot of heavy lifting for the Wargroove implementation and must be always on during gameplay. The mod source files can be found here: https://github.com/FlySniper/WargrooveArchipelagoMod
2023-02-24 07:35:09 +01:00
Trevor L
7c68e91d4a Blasphemous: Implement new game (#1446)
Adds @BrandenEK's Blasphemous Randomizer as a new Archipelago game.
2023-02-24 07:33:09 +01:00
alwaysintreble
1d6ab13015 ArchipIDLE: add a completion condition instead of hard coding tests around a game (#1444) 2023-02-23 21:16:10 -05:00
CaitSith2
cb3d40624c Timespinner: Make RisingTidesOverrides consistent with normal yaml behaviour. (#1474)
* Make RisingTidesOverrides consistent with normal yaml behaviour.

* Each of the options can be either string directly specifying the option, or dictionary.
* If dictionary, ensure that at least one of the options is greater than zero.

* Made keys optional

* A lot less copy/pasta.

---------

Co-authored-by: Jarno Westhof <jarnowesthof@gmail.com>
2023-02-22 17:11:27 -08:00
alwaysintreble
0eb66957b1 SMW: change random in generate_output to use slot random 2023-02-20 18:43:23 +01:00
CaitSith2
4311e8dbe2 Merge branch 'main' into allow_collect 2023-02-19 19:19:28 -08:00
alwaysintreble
53e2232f29 Docs: document world docs and tests (#1463)
* Docs: document world docs and tests

* regions and items shouldn't be created after `create_items`

* Changes from review

* Restructure game info section

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* w

* urls can have extension probably

* reorder the methods by call order

* fix grammar mistake in ordered method list

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-19 23:16:56 +01:00
Fabian Dill
ecd2675ea8 Tests: check that Regions are reachable (#1034)
* Tests: check that Regions are reachable
try to prevent errors from unconnected/never reachable Regions

* Test region access (#1039)

* Tests: note oot's default unreachable regions

* [SM] Fixed failing testAllStateCanReachEverything (#1087)

* [SM] Fixed failing testAllStateCanReachEverything

- by adding exclusion for Regions used only when corresponding Starting Location is used
- by removing unnecessary VARIA Regions used only for EscapeRando (not supported in AP anyway)

* Update worlds/sm/Regions.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update worlds/sm/Rules.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update worlds/sm/Regions.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update test/general/TestReachability.py

---------

Co-authored-by: espeon65536 <81029175+espeon65536@users.noreply.github.com>
Co-authored-by: lordlou <87331798+lordlou@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-19 23:09:54 +01:00
Jarno
fc2e555b4a Timespinner: many new stuffs (#1433)
* Timespinner: added RisingTides and DadPercent flags

* Implemented logic for DadPercent and RisingTides

* Fixed TODO's

* Logic fixes

* Fixed + removed LogicMixins

* Fixes

* More Fixes

* Added UnchainedKeys flag

* Fixed available items in pool with UnchainedKeys

* Fixed typing callable

* Fixed generation failures

* More refactorings

* Implemented traps

* Fixed more typo

* Fixed copy paste bug

* Fixed teleporter logic

* Fixed traps from pool

* Fixed pyramid gates bug that causes a crash on connecting

* Fixed seed reproduceability

* Fixed logic eye for eye spy
Now consider warp beacons as starter progression items

* Attempt to add tracker icons using table

* Replaced table layout with css grid

* Fixed tracker + added Timespinner was apworld capatible

* Updated archipelago items description

* updated URL

* Cleared up text

* Fixed based on self review of PR

* Fixed unit tests

* Fixed seed reproduceability when the traps yaml option is not provided

* Fixed logic for flooded basement

* Implemented Beserkers review result

I am not sure why, i guess this is just to make adding future games less conflicting?

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>

* Added two new options (thanks to WeffJebster)

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Addition review results

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-19 21:22:30 +01:00
black-sliver
df020bb389 Style Guide: add world consistency 2023-02-19 19:34:45 +01:00
PoryGone
7760034ff7 DKC3 and SMW: Remove relative imports (#1472) 2023-02-19 09:10:32 +01:00
black-sliver
3e7794d5dc SoE: update evermizer to 044
* Fix energy core despawning when looting failed
* Fix fish guy dialog when cure was already obtained
see https://github.com/black-sliver/pyevermizer/releases/tag/v0.44.0 for more details
2023-02-18 15:18:51 +01:00
black-sliver
a3e8bb474a ModuleUpdater: allow new syntax, nicer output 2023-02-18 15:18:51 +01:00
kindasneaki
e4c95c940a RoR2: regions unreachable fix (#1459) 2023-02-17 22:08:18 +01:00
recklesscoder
daa1809a0f WebHost: Tweaks to search on tracker pages (#1307)
* WebHost: Tweaks to search on tracker pages
- Pressing `Ctrl+F` or `/` now focuses the search box.
- Typing now automatically focuses the search box.
- Pressing `Escape` now clears the search and scrolls to the top.

* WebHost/Trackers: Focus search box on load

* WebHost/Trackers: Remove overriding of Ctrl+F and /
2023-02-17 13:24:21 -05:00
Jarno
0a1261eb84 WebHost: Add tutorials to sitemap and hide settings link for games without settings (#1452)
* WebHost: Add tutorials to sitemap and hide settings link for games without settings

* Fixed some typing imports
2023-02-17 13:16:37 -05:00
toasterparty
b62be6f7f4 [OC2] Colored Ramp Button Items (#1466)
Before: 1 item activates all 4
After: 7 items activate 7 buttons, creating more divergent routes

Also, I consolidated the 6 filler emotes into a single "Emote Wheel" item to make space in the item pool.

I bumped my data version and min AP version to indicate this change.

The corresponding oc2-modding update is **v1.6.0**
2023-02-17 09:25:56 +01:00
toasterparty
ce2553a2b3 [OC2] Location Balancing (#1458) 2023-02-17 09:21:56 +01:00
Fabian Dill
18c4b4b1fe Subnautica: move code to be a better example 2023-02-17 08:50:22 +01:00
Fabian Dill
a85ca9cc87 Subnautica: add logic dumper for mod (#1386)
* Subnautica: add logic dumper for mod

* Subnautica: export more data

* Subnautica: fix some Cyclops logic
2023-02-16 00:40:19 +01:00
el-u
ad4846cedd core: clarify usage of classmethods in World class (#1449) 2023-02-16 00:28:02 +01:00
recklesscoder
b20be3ccec Docs/Factorio: Document EnergyLink (#1456)
* Docs/Factorio: Document EnergyLink

* Docs/Factorio: EnergyLink clarification
2023-02-16 00:25:46 +01:00
alwaysintreble
8af7908cd0 Tests: datapackage and more multiworld renaming (#1454)
* Tests: add a test that created items and locations exist in the datapackage

* move FF validation to `assert_generate` and remove test exclusion

* test created location addresses are correct

* make the assertion proper and more verbose

* make item count test ~~a bit faster~~ a lot nicer

* 120 blaze it

* name test multiworld setup better and fix another over 120 line in FFR
2023-02-15 22:46:10 +01:00
alwaysintreble
f078750b72 LTTP: make the enemizer check a property and only check for it once instead of per world (#1448)
* LTTP: do the enemizer check in `stage_assert_generate` and break after checking any world for enemizer succeeds

* use multiworld

* catch a missed `used_enemizer` check and add typing

* more typing
2023-02-14 22:22:39 +01:00
alwaysintreble
7cbeb8438b core: rip out RegionType and rework Region class (#814) 2023-02-14 01:06:43 +01:00
Fabian Dill
f7a0542898 kvui: limit UI side logs to by default 1000 messages 2023-02-13 09:02:19 +01:00
recklesscoder
cc61f16e57 Protocol: Improve machine-readability of prints (#1388)
* Protocol: Improve machine-readability of prints

* Factorio: Make use of new PrintJSON fields for echo detection.

* Protocol: Add message field to chat prints.
2023-02-13 03:17:25 +01:00
alwaysintreble
9e3c2e2464 Tests: test that exits to Regions are the parents of the Entrance (#1442) 2023-02-13 02:05:52 +01:00
Fabian Dill
f528175d8a Core: prepare server for removal of names in multidata (#1430) 2023-02-13 01:56:20 +01:00
Fabian Dill
803d7105a1 kivy: allow user-defined text colors in data/client.kv (#1429) 2023-02-13 01:55:43 +01:00
el-u
a40f6058b5 lufia2ac: fix mismatched exits/parent region 2023-02-13 00:46:46 +01:00
toasterparty
0ff3c693d5 [OC2] Relax Horde Logic for Horde H-8 and Winter H-4 (#1439) 2023-02-10 21:42:28 +01:00
Fabian Dill
873a374a69 SNIclient: connect fixes (#1436) 2023-02-07 10:16:39 +01:00
black-sliver
60584b7617 CI: more pip to fix the build 2023-02-07 10:10:27 +01:00
black-sliver
e24a85ca5c CI: update SNI to v0.0.88 2023-02-07 10:10:27 +01:00
lordlou
cc0540d3fb SMZ3: keysanity accessibility fix (#1428) 2023-02-07 03:14:03 +01:00
NewSoupVi
c360b9266c Witness: Renaming: Mill -> Stoneworks, correcting order for 'First, Second, ...', removed all instances of 'Door (Door)' (#1435) 2023-02-07 03:12:47 +01:00
beauxq
6148213e43 Zillion: fix name data overflow 2023-02-07 03:11:48 +01:00
Jarno
ff175008a1 Core: Phase out Print packets (#1364) 2023-02-05 22:06:38 +01:00
kindasneaki
cae1e683e2 RoR2: 1.20 content update (#1396)
## Adding in Explore Mode:

Features include:
* Added in `environments` to be items.
* `Location checks` are now `environment based` instead of being able to get them from anywhere.
* Added in support for the `DLC Survivors of the void` which include `Void Items` and `3 new maps` that come with it. (option added to use DLC)

---------

Co-authored-by: Dogpetkid <dogpetkid@gmail.com>
2023-02-05 21:51:03 +01:00
vgZerst
fb1a9e9c5a WebHost: add checks percent done column to tracker (#1376)
* WebHost: add checks percent done column to tracker

* WebHost: add checks percent done column to tracker
2023-02-04 00:04:00 -05:00
CaitSith2
555a0da46d Core: Rename the missed slot_seeds. (#1432)
* Rename the missed slot_seeds.

* Fixed a threaded context random error.
2023-02-03 19:39:18 +01:00
NewSoupVi
0817305d5b Witness: Added an option tooltip for "Environmental Puzzles Difficulty" option (+ another bugfix) (#1431)
* Added an option tooltip

* Fixed eclipse being on in EP difficulty normal
2023-02-03 19:38:54 +01:00
Fabian Dill
995c978628 Core: replace global random state with descriptive error (#1424)
* Core: replace global random state with descriptive error

* Core: make random a proxy object and rename slot_seeds
2023-02-02 01:14:23 +01:00
NewSoupVi
4de7ebd8b0 The Witness: v4 Content Update (#1338)
## New Features:

- EP Shuffle (Individual or Obelisk Sides, with varying difficulty levels)
- Ability to play without Puzzle Randomization (I.e. vanilla + AP layer)
- Pet the Dog to get a Puzzle Skip :) (No, really.)

## Changes:

- Starting inventory behavior improved (Consider starting items like doors and lasers logically even if they aren't part of the mode)
- Audio Log hint system improved (On low hint counts, you will no longer get the same locations hinted every time, i.e. always hints are shuffled)

## Fixes:

- Many fixes to symbol requirements
- Fixes to "shuffle_postgame" (What checks are evaluated as "postgame" in specific modes)
- Logically irrelevant doors are now "useful" instead of "progression"
2023-02-01 21:18:07 +01:00
PoryGone
3cef39a387 SMW & DKC3: Ship as .apworld (#1426) 2023-02-01 21:15:01 +01:00
el-u
ffff9ece55 core: properly declare from_any as an abstract classmethod 2023-02-01 21:14:11 +01:00
PoryGone
dc2aa5f41e SMW: v1.1 Content Update (#1344)
* Make Bowser unkillable on Egg Hunt

* Increment Data Package version

Changed a location name.

* Baseline for Bowser Rooms shuffling

* Add boss shuffle

* Remove extra space

* Overworld Palette Shuffle

* Fix Literature Trap typo

* Handle Queuing traps and new Timer Trap

* Fix trap name and actually create them

* Early Climb and Overworld Speed

* Add correct tooltip for Early Climb

* Tooltip text edit

* Address unconnected regions

* Add option to fully exclude Special Zone levels from the seed

* Fix Chocolate Island 4 Dragon Coins logic

* Update worlds/smw/Client.py to use `getattr`
2023-01-30 05:53:56 +01:00
black-sliver
428344b6bc setup: honor build automation ...
... and reorder imports to PEP it up
2023-01-30 02:33:41 +01:00
CaitSith2
d961022bff Add option for player to allow/disallow collection from their slot. 2023-01-28 05:04:55 -08:00
338 changed files with 29416 additions and 5592 deletions

View File

@@ -2,10 +2,20 @@
name: Build name: Build
on: workflow_dispatch on:
push:
paths:
- '.github/workflows/build.yml'
- 'setup.py'
- 'requirements.txt'
pull_request:
paths:
- '.github/workflows/build.yml'
- 'setup.py'
- 'requirements.txt'
workflow_dispatch:
env: env:
SNI_VERSION: v0.0.84
ENEMIZER_VERSION: 7.1 ENEMIZER_VERSION: 7.1
APPIMAGETOOL_VERSION: 13 APPIMAGETOOL_VERSION: 13
@@ -15,15 +25,13 @@ jobs:
build-win-py38: # RCs will still be built and signed by hand build-win-py38: # RCs will still be built and signed by hand
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install python - name: Install python
uses: actions/setup-python@v3 uses: actions/setup-python@v4
with: with:
python-version: '3.8' python-version: '3.8'
- name: Download run-time dependencies - name: Download run-time dependencies
run: | run: |
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/${Env:SNI_VERSION}/sni-${Env:SNI_VERSION}-windows-amd64.zip -OutFile sni.zip
Expand-Archive -Path sni.zip -DestinationPath SNI -Force
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
- name: Build - name: Build
@@ -39,7 +47,7 @@ jobs:
Rename-Item exe.$NAME Archipelago Rename-Item exe.$NAME Archipelago
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago 7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
- name: Store 7z - name: Store 7z
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: ${{ env.ZIP_NAME }} name: ${{ env.ZIP_NAME }}
path: dist/${{ env.ZIP_NAME }} path: dist/${{ env.ZIP_NAME }}
@@ -49,14 +57,14 @@ jobs:
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
steps: steps:
# - copy code below to release.yml - # - copy code below to release.yml -
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install base dependencies - name: Install base dependencies
run: | run: |
sudo apt update sudo apt update
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0 sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
- name: Get a recent python - name: Get a recent python
uses: actions/setup-python@v3 uses: actions/setup-python@v4
with: with:
python-version: '3.9' python-version: '3.9'
- name: Install build-time dependencies - name: Install build-time dependencies
@@ -69,18 +77,15 @@ jobs:
chmod a+rx appimagetool chmod a+rx appimagetool
- name: Download run-time dependencies - name: Download run-time dependencies
run: | run: |
wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz
tar xf sni-*.tar.xz
rm sni-*.tar.xz
mv sni-* SNI
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z 7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
- name: Build - name: Build
run: | run: |
# pygobject is an optional dependency for kivy that's not in requirements # pygobject is an optional dependency for kivy that's not in requirements
"${{ env.PYTHON }}" -m pip install --upgrade pip virtualenv PyGObject setuptools # charset-normalizer was somehow incomplete in the github runner
"${{ env.PYTHON }}" -m venv venv "${{ env.PYTHON }}" -m venv venv
source venv/bin/activate source venv/bin/activate
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject setuptools charset-normalizer
pip install -r requirements.txt pip install -r requirements.txt
python setup.py build_exe --yes bdist_appimage --yes python setup.py build_exe --yes bdist_appimage --yes
echo -e "setup.py build output:\n `ls build`" echo -e "setup.py build output:\n `ls build`"
@@ -92,13 +97,13 @@ jobs:
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
# - copy code above to release.yml - # - copy code above to release.yml -
- name: Store AppImage - name: Store AppImage
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: ${{ env.APPIMAGE_NAME }} name: ${{ env.APPIMAGE_NAME }}
path: dist/${{ env.APPIMAGE_NAME }} path: dist/${{ env.APPIMAGE_NAME }}
retention-days: 7 retention-days: 7
- name: Store .tar.gz - name: Store .tar.gz
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: ${{ env.TAR_NAME }} name: ${{ env.TAR_NAME }}
path: dist/${{ env.TAR_NAME }} path: dist/${{ env.TAR_NAME }}

View File

@@ -14,9 +14,17 @@ name: "CodeQL"
on: on:
push: push:
branches: [ main ] branches: [ main ]
paths:
- '**.py'
- '**.js'
- '.github/workflows/codeql-analysis.yml'
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ main ] branches: [ main ]
paths:
- '**.py'
- '**.js'
- '.github/workflows/codeql-analysis.yml'
schedule: schedule:
- cron: '44 8 * * 1' - cron: '44 8 * * 1'
@@ -35,11 +43,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -50,7 +58,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -64,4 +72,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v2

View File

@@ -3,7 +3,13 @@
name: lint name: lint
on: [push, pull_request] on:
push:
paths:
- '**.py'
pull_request:
paths:
- '**.py'
jobs: jobs:
build: build:
@@ -11,9 +17,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python 3.9 - name: Set up Python 3.9
uses: actions/setup-python@v1 uses: actions/setup-python@v4
with: with:
python-version: 3.9 python-version: 3.9
- name: Install dependencies - name: Install dependencies

View File

@@ -8,7 +8,6 @@ on:
- '*.*.*' - '*.*.*'
env: env:
SNI_VERSION: v0.0.84
ENEMIZER_VERSION: 7.1 ENEMIZER_VERSION: 7.1
APPIMAGETOOL_VERSION: 13 APPIMAGETOOL_VERSION: 13
@@ -36,14 +35,14 @@ jobs:
- name: Set env - name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
# - code below copied from build.yml - # - code below copied from build.yml -
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install base dependencies - name: Install base dependencies
run: | run: |
sudo apt update sudo apt update
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0 sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
- name: Get a recent python - name: Get a recent python
uses: actions/setup-python@v3 uses: actions/setup-python@v4
with: with:
python-version: '3.9' python-version: '3.9'
- name: Install build-time dependencies - name: Install build-time dependencies
@@ -56,18 +55,15 @@ jobs:
chmod a+rx appimagetool chmod a+rx appimagetool
- name: Download run-time dependencies - name: Download run-time dependencies
run: | run: |
wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz
tar xf sni-*.tar.xz
rm sni-*.tar.xz
mv sni-* SNI
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z 7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
- name: Build - name: Build
run: | run: |
# pygobject is an optional dependency for kivy that's not in requirements # pygobject is an optional dependency for kivy that's not in requirements
"${{ env.PYTHON }}" -m pip install --upgrade pip virtualenv PyGObject setuptools # charset-normalizer was somehow incomplete in the github runner
"${{ env.PYTHON }}" -m venv venv "${{ env.PYTHON }}" -m venv venv
source venv/bin/activate source venv/bin/activate
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject setuptools charset-normalizer
pip install -r requirements.txt pip install -r requirements.txt
python setup.py build --yes bdist_appimage --yes python setup.py build --yes bdist_appimage --yes
echo -e "setup.py build output:\n `ls build`" echo -e "setup.py build output:\n `ls build`"

View File

@@ -3,7 +3,25 @@
name: unittests name: unittests
on: [push, pull_request] on:
push:
paths:
- '**'
- '!docs/**'
- '!setup.py'
- '!*.iss'
- '!.gitignore'
- '!.github/workflows/**'
- '.github/workflows/unittests.yml'
pull_request:
paths:
- '**'
- '!docs/**'
- '!setup.py'
- '!*.iss'
- '!.gitignore'
- '!.github/workflows/**'
- '.github/workflows/unittests.yml'
jobs: jobs:
build: build:
@@ -23,11 +41,13 @@ jobs:
os: windows-latest os: windows-latest
- python: {version: '3.10'} # current - python: {version: '3.10'} # current
os: windows-latest os: windows-latest
- python: {version: '3.10'} # current
os: macos-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python.version }} - name: Set up Python ${{ matrix.python.version }}
uses: actions/setup-python@v1 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python.version }} python-version: ${{ matrix.python.version }}
- name: Install dependencies - name: Install dependencies

4
.gitignore vendored
View File

@@ -8,6 +8,7 @@
*.apm3 *.apm3
*.apmc *.apmc
*.apz5 *.apz5
*.aptloz
*.pyc *.pyc
*.pyd *.pyd
*.sfc *.sfc
@@ -50,6 +51,7 @@ Output Logs/
/Archipelago.zip /Archipelago.zip
/setup.ini /setup.ini
/installdelete.iss /installdelete.iss
/data/user.kv
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
@@ -137,6 +139,7 @@ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
.code-workspace .code-workspace
shell.nix
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
@@ -166,6 +169,7 @@ cython_debug/
jdk*/ jdk*/
minecraft*/ minecraft*/
minecraft_versions.json minecraft_versions.json
!worlds/minecraft/
# pyenv # pyenv
.python-version .python-version

View File

@@ -2,14 +2,13 @@ from __future__ import annotations
import copy import copy
import functools import functools
import json
import logging import logging
import random import random
import secrets import secrets
import typing # this can go away when Python 3.8 support is dropped import typing # this can go away when Python 3.8 support is dropped
from argparse import Namespace from argparse import Namespace
from collections import OrderedDict, Counter, deque from collections import OrderedDict, Counter, deque, ChainMap
from enum import unique, IntEnum, IntFlag from enum import IntEnum, IntFlag
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple
import NetUtils import NetUtils
@@ -29,6 +28,20 @@ class Group(TypedDict, total=False):
link_replacement: bool link_replacement: bool
class ThreadBarrierProxy():
"""Passes through getattr while passthrough is True"""
def __init__(self, obj: Any):
self.passthrough = True
self.obj = obj
def __getattr__(self, item):
if self.passthrough:
return getattr(self.obj, item)
else:
raise RuntimeError("You are in a threaded context and global random state was removed for your safety. "
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")
class MultiWorld(): class MultiWorld():
debug_types = False debug_types = False
player_name: Dict[int, str] player_name: Dict[int, str]
@@ -54,13 +67,22 @@ class MultiWorld():
local_early_items: Dict[int, Dict[str, int]] local_early_items: Dict[int, Dict[str, int]]
local_items: Dict[int, Options.LocalItems] local_items: Dict[int, Options.LocalItems]
non_local_items: Dict[int, Options.NonLocalItems] non_local_items: Dict[int, Options.NonLocalItems]
allow_collect: Dict[int, Options.AllowCollect]
progression_balancing: Dict[int, Options.ProgressionBalancing] progression_balancing: Dict[int, Options.ProgressionBalancing]
completion_condition: Dict[int, Callable[[CollectionState], bool]] completion_condition: Dict[int, Callable[[CollectionState], bool]]
indirect_connections: Dict[Region, Set[Entrance]] indirect_connections: Dict[Region, Set[Entrance]]
exclude_locations: Dict[int, Options.ExcludeLocations] exclude_locations: Dict[int, Options.ExcludeLocations]
priority_locations: Dict[int, Options.PriorityLocations]
start_inventory: Dict[int, Options.StartInventory]
start_hints: Dict[int, Options.StartHints]
start_location_hints: Dict[int, Options.StartLocationHints]
item_links: Dict[int, Options.ItemLinks]
game: Dict[int, str] game: Dict[int, str]
random: random.Random
per_slot_randoms: Dict[int, random.Random]
class AttributeProxy(): class AttributeProxy():
def __init__(self, rule): def __init__(self, rule):
self.rule = rule self.rule = rule
@@ -69,7 +91,8 @@ class MultiWorld():
return self.rule(player) return self.rule(player)
def __init__(self, players: int): def __init__(self, players: int):
self.random = random.Random() # world-local random state is saved for multiple generations running concurrently # world-local random state is saved for multiple generations running concurrently
self.random = ThreadBarrierProxy(random.Random())
self.players = players self.players = players
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids} self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
self.glitch_triforce = False self.glitch_triforce = False
@@ -160,7 +183,7 @@ class MultiWorld():
set_player_attr('completion_condition', lambda state: True) set_player_attr('completion_condition', lambda state: True)
self.custom_data = {} self.custom_data = {}
self.worlds = {} self.worlds = {}
self.slot_seeds = {} self.per_slot_randoms = {}
self.plando_options = PlandoOptions.none self.plando_options = PlandoOptions.none
def get_all_ids(self) -> Tuple[int, ...]: def get_all_ids(self) -> Tuple[int, ...]:
@@ -206,8 +229,8 @@ class MultiWorld():
else: else:
self.random.seed(self.seed) self.random.seed(self.seed)
self.seed_name = name if name else str(self.seed) self.seed_name = name if name else str(self.seed)
self.slot_seeds = {player: random.Random(self.random.getrandbits(64)) for player in self.per_slot_randoms = {player: random.Random(self.random.getrandbits(64)) for player in
range(1, self.players + 1)} range(1, self.players + 1)}
def set_options(self, args: Namespace) -> None: def set_options(self, args: Namespace) -> None:
for option_key in Options.common_options: for option_key in Options.common_options:
@@ -291,7 +314,7 @@ class MultiWorld():
self.state = CollectionState(self) self.state = CollectionState(self)
def secure(self): def secure(self):
self.random = secrets.SystemRandom() self.random = ThreadBarrierProxy(secrets.SystemRandom())
self.is_race = True self.is_race = True
@functools.cached_property @functools.cached_property
@@ -742,169 +765,9 @@ class CollectionState():
found += self.prog_items[item_name, player] found += self.prog_items[item_name, player]
return found return found
def can_buy_unlimited(self, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for
shop in self.multiworld.shops)
def can_buy(self, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(self) for
shop in self.multiworld.shops)
def item_count(self, item: str, player: int) -> int: def item_count(self, item: str, player: int) -> int:
return self.prog_items[item, player] return self.prog_items[item, player]
def has_triforce_pieces(self, count: int, player: int) -> bool:
return self.item_count('Triforce Piece', player) + self.item_count('Power Star', player) >= count
def has_crystals(self, count: int, player: int) -> bool:
found: int = 0
for crystalnumber in range(1, 8):
found += self.prog_items[f"Crystal {crystalnumber}", player]
if found >= count:
return True
return False
def can_lift_rocks(self, player: int):
return self.has('Power Glove', player) or self.has('Titans Mitts', player)
def bottle_count(self, player: int) -> int:
return min(self.multiworld.difficulty_requirements[player].progressive_bottle_limit,
self.count_group("Bottles", player))
def has_hearts(self, player: int, count: int) -> int:
# Warning: This only considers items that are marked as advancement items
return self.heart_count(player) >= count
def heart_count(self, player: int) -> int:
# Warning: This only considers items that are marked as advancement items
diff = self.multiworld.difficulty_requirements[player]
return min(self.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \
+ self.item_count('Sanctuary Heart Container', player) \
+ min(self.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
+ 3 # starting hearts
def can_lift_heavy_rocks(self, player: int) -> bool:
return self.has('Titans Mitts', player)
def can_extend_magic(self, player: int, smallmagic: int = 16,
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
basemagic = 8
if self.has('Magic Upgrade (1/4)', player):
basemagic = 32
elif self.has('Magic Upgrade (1/2)', player):
basemagic = 16
if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player):
if self.multiworld.item_functionality[player] == 'hard' and not fullrefill:
basemagic = basemagic + int(basemagic * 0.5 * self.bottle_count(player))
elif self.multiworld.item_functionality[player] == 'expert' and not fullrefill:
basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count(player))
else:
basemagic = basemagic + basemagic * self.bottle_count(player)
return basemagic >= smallmagic
def can_kill_most_things(self, player: int, enemies: int = 5) -> bool:
return (self.has_melee_weapon(player)
or self.has('Cane of Somaria', player)
or (self.has('Cane of Byrna', player) and (enemies < 6 or self.can_extend_magic(player)))
or self.can_shoot_arrows(player)
or self.has('Fire Rod', player)
or (self.has('Bombs (10)', player) and enemies < 6))
def can_shoot_arrows(self, player: int) -> bool:
if self.multiworld.retro_bow[player]:
return (self.has('Bow', player) or self.has('Silver Bow', player)) and self.can_buy('Single Arrow', player)
return self.has('Bow', player) or self.has('Silver Bow', player)
def can_get_good_bee(self, player: int) -> bool:
cave = self.multiworld.get_region('Good Bee Cave', player)
return (
self.has_group("Bottles", player) and
self.has('Bug Catching Net', player) and
(self.has('Pegasus Boots', player) or (self.has_sword(player) and self.has('Quake', player))) and
cave.can_reach(self) and
self.is_not_bunny(cave, player)
)
def can_retrieve_tablet(self, player: int) -> bool:
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
(self.multiworld.swordless[player] and
self.has("Hammer", player)))
def has_sword(self, player: int) -> bool:
return self.has('Fighter Sword', player) \
or self.has('Master Sword', player) \
or self.has('Tempered Sword', player) \
or self.has('Golden Sword', player)
def has_beam_sword(self, player: int) -> bool:
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword',
player)
def has_melee_weapon(self, player: int) -> bool:
return self.has_sword(player) or self.has('Hammer', player)
def has_fire_source(self, player: int) -> bool:
return self.has('Fire Rod', player) or self.has('Lamp', player)
def can_melt_things(self, player: int) -> bool:
return self.has('Fire Rod', player) or \
(self.has('Bombos', player) and
(self.multiworld.swordless[player] or
self.has_sword(player)))
def can_avoid_lasers(self, player: int) -> bool:
return self.has('Mirror Shield', player) or self.has('Cane of Byrna', player) or self.has('Cape', player)
def is_not_bunny(self, region: Region, player: int) -> bool:
if self.has('Moon Pearl', player):
return True
return region.is_light_world if self.multiworld.mode[player] != 'inverted' else region.is_dark_world
def can_reach_light_world(self, player: int) -> bool:
if True in [i.is_light_world for i in self.reachable_regions[player]]:
return True
return False
def can_reach_dark_world(self, player: int) -> bool:
if True in [i.is_dark_world for i in self.reachable_regions[player]]:
return True
return False
def has_misery_mire_medallion(self, player: int) -> bool:
return self.has(self.multiworld.required_medallions[player][0], player)
def has_turtle_rock_medallion(self, player: int) -> bool:
return self.has(self.multiworld.required_medallions[player][1], player)
def can_boots_clip_lw(self, player: int) -> bool:
if self.multiworld.mode[player] == 'inverted':
return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player)
return self.has('Pegasus Boots', player)
def can_boots_clip_dw(self, player: int) -> bool:
if self.multiworld.mode[player] != 'inverted':
return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player)
return self.has('Pegasus Boots', player)
def can_get_glitched_speed_lw(self, player: int) -> bool:
rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])]
if self.multiworld.mode[player] == 'inverted':
rules.append(self.has('Moon Pearl', player))
return all(rules)
def can_superbunny_mirror_with_sword(self, player: int) -> bool:
return self.has('Magic Mirror', player) and self.has_sword(player)
def can_get_glitched_speed_dw(self, player: int) -> bool:
rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])]
if self.multiworld.mode[player] != 'inverted':
rules.append(self.has('Moon Pearl', player))
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)
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool: def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
if location: if location:
self.locations_checked.add(location) self.locations_checked.add(location)
@@ -931,45 +794,23 @@ class CollectionState():
self.stale[item.player] = True self.stale[item.player] = True
@unique
class RegionType(IntEnum):
Generic = 0
LightWorld = 1
DarkWorld = 2
Cave = 3 # Also includes Houses
Dungeon = 4
@property
def is_indoors(self) -> bool:
"""Shorthand for checking if Cave or Dungeon"""
return self in (RegionType.Cave, RegionType.Dungeon)
class Region: class Region:
name: str name: str
type: RegionType _hint_text: str
hint_text: str
player: int player: int
multiworld: Optional[MultiWorld] multiworld: Optional[MultiWorld]
entrances: List[Entrance] entrances: List[Entrance]
exits: List[Entrance] exits: List[Entrance]
locations: List[Location] locations: List[Location]
dungeon: Optional[Dungeon] = None dungeon: Optional[Dungeon] = None
shop: Optional = None
# LttP specific. TODO: move to a LttPRegion def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
# will be set after making connections.
is_light_world: bool = False
is_dark_world: bool = False
def __init__(self, name: str, type_: RegionType, hint: str, player: int, world: Optional[MultiWorld] = None):
self.name = name self.name = name
self.type = type_
self.entrances = [] self.entrances = []
self.exits = [] self.exits = []
self.locations = [] self.locations = []
self.multiworld = world self.multiworld = multiworld
self.hint_text = hint self._hint_text = hint
self.player = player self.player = player
def can_reach(self, state: CollectionState) -> bool: def can_reach(self, state: CollectionState) -> bool:
@@ -985,6 +826,10 @@ class Region:
return True return True
return False return False
@property
def hint_text(self) -> str:
return self._hint_text if self._hint_text else self.name
def get_connecting_entrance(self, is_main_entrance: typing.Callable[[Entrance], bool]) -> Entrance: def get_connecting_entrance(self, is_main_entrance: typing.Callable[[Entrance], bool]) -> Entrance:
for entrance in self.entrances: for entrance in self.entrances:
if is_main_entrance(entrance): if is_main_entrance(entrance):
@@ -1122,7 +967,7 @@ class Location:
self.parent_region = parent self.parent_region = parent
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return (self.always_allow(state, item) return ((self.always_allow(state, item) and item.name not in state.multiworld.non_local_items[item.player])
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful)) or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
and self.item_rule(item) and self.item_rule(item)
and (not check_access or self.can_reach(state)))) and (not check_access or self.can_reach(state))))
@@ -1254,13 +1099,9 @@ class Spoiler():
self.multiworld = world self.multiworld = world
self.hashes = {} self.hashes = {}
self.entrances = OrderedDict() self.entrances = OrderedDict()
self.medallions = {}
self.playthrough = {} self.playthrough = {}
self.unreachables = set() self.unreachables = set()
self.locations = {}
self.paths = {} self.paths = {}
self.shops = []
self.bosses = OrderedDict()
def set_entrance(self, entrance: str, exit_: str, direction: str, player: int): def set_entrance(self, entrance: str, exit_: str, direction: str, player: int):
if self.multiworld.players == 1: if self.multiworld.players == 1:
@@ -1270,117 +1111,6 @@ class Spoiler():
self.entrances[(entrance, direction, player)] = OrderedDict( self.entrances[(entrance, direction, player)] = OrderedDict(
[('player', player), ('entrance', entrance), ('exit', exit_), ('direction', direction)]) [('player', player), ('entrance', entrance), ('exit', exit_), ('direction', direction)])
def parse_data(self):
self.medallions = OrderedDict()
for player in self.multiworld.get_game_players("A Link to the Past"):
self.medallions[f'Misery Mire ({self.multiworld.get_player_name(player)})'] = \
self.multiworld.required_medallions[player][0]
self.medallions[f'Turtle Rock ({self.multiworld.get_player_name(player)})'] = \
self.multiworld.required_medallions[player][1]
self.locations = OrderedDict()
listed_locations = set()
lw_locations = [loc for loc in self.multiworld.get_locations() if
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and loc.show_in_spoiler]
self.locations['Light World'] = OrderedDict(
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
lw_locations])
listed_locations.update(lw_locations)
dw_locations = [loc for loc in self.multiworld.get_locations() if
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and loc.show_in_spoiler]
self.locations['Dark World'] = OrderedDict(
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
dw_locations])
listed_locations.update(dw_locations)
cave_locations = [loc for loc in self.multiworld.get_locations() if
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and loc.show_in_spoiler]
self.locations['Caves'] = OrderedDict(
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
cave_locations])
listed_locations.update(cave_locations)
for dungeon in self.multiworld.dungeons.values():
dungeon_locations = [loc for loc in self.multiworld.get_locations() if
loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and loc.show_in_spoiler]
self.locations[str(dungeon)] = OrderedDict(
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
dungeon_locations])
listed_locations.update(dungeon_locations)
other_locations = [loc for loc in self.multiworld.get_locations() if
loc not in listed_locations and loc.show_in_spoiler]
if other_locations:
self.locations['Other Locations'] = OrderedDict(
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
other_locations])
listed_locations.update(other_locations)
self.shops = []
from worlds.alttp.Shops import ShopType, price_type_display_name, price_rate_display
for shop in self.multiworld.shops:
if not shop.custom:
continue
shopdata = {
'location': str(shop.region),
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
}
for index, item in enumerate(shop.inventory):
if item is None:
continue
my_price = item['price'] // price_rate_display.get(item['price_type'], 1)
shopdata['item_{}'.format(
index)] = f"{item['item']}{my_price} {price_type_display_name[item['price_type']]}"
if item['player'] > 0:
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('',
'(Player {}) — '.format(
item['player']))
if item['max'] == 0:
continue
shopdata['item_{}'.format(index)] += " x {}".format(item['max'])
if item['replacement'] is None:
continue
shopdata['item_{}'.format(
index)] += f", {item['replacement']} - {item['replacement_price']} {price_type_display_name[item['replacement_price_type']]}"
self.shops.append(shopdata)
for player in self.multiworld.get_game_players("A Link to the Past"):
self.bosses[str(player)] = OrderedDict()
self.bosses[str(player)]["Eastern Palace"] = self.multiworld.get_dungeon("Eastern Palace", player).boss.name
self.bosses[str(player)]["Desert Palace"] = self.multiworld.get_dungeon("Desert Palace", player).boss.name
self.bosses[str(player)]["Tower Of Hera"] = self.multiworld.get_dungeon("Tower of Hera", player).boss.name
self.bosses[str(player)]["Hyrule Castle"] = "Agahnim"
self.bosses[str(player)]["Palace Of Darkness"] = self.multiworld.get_dungeon("Palace of Darkness",
player).boss.name
self.bosses[str(player)]["Swamp Palace"] = self.multiworld.get_dungeon("Swamp Palace", player).boss.name
self.bosses[str(player)]["Skull Woods"] = self.multiworld.get_dungeon("Skull Woods", player).boss.name
self.bosses[str(player)]["Thieves Town"] = self.multiworld.get_dungeon("Thieves Town", player).boss.name
self.bosses[str(player)]["Ice Palace"] = self.multiworld.get_dungeon("Ice Palace", player).boss.name
self.bosses[str(player)]["Misery Mire"] = self.multiworld.get_dungeon("Misery Mire", player).boss.name
self.bosses[str(player)]["Turtle Rock"] = self.multiworld.get_dungeon("Turtle Rock", player).boss.name
if self.multiworld.mode[player] != 'inverted':
self.bosses[str(player)]["Ganons Tower Basement"] = \
self.multiworld.get_dungeon('Ganons Tower', player).bosses['bottom'].name
self.bosses[str(player)]["Ganons Tower Middle"] = self.multiworld.get_dungeon('Ganons Tower', player).bosses[
'middle'].name
self.bosses[str(player)]["Ganons Tower Top"] = self.multiworld.get_dungeon('Ganons Tower', player).bosses[
'top'].name
else:
self.bosses[str(player)]["Ganons Tower Basement"] = \
self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].name
self.bosses[str(player)]["Ganons Tower Middle"] = \
self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].name
self.bosses[str(player)]["Ganons Tower Top"] = \
self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['top'].name
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
self.bosses[str(player)]["Ganon"] = "Ganon"
def create_playthrough(self, create_paths: bool = True): def create_playthrough(self, create_paths: bool = True):
"""Destructive to the world while it is run, damage gets repaired afterwards.""" """Destructive to the world while it is run, damage gets repaired afterwards."""
from itertools import chain from itertools import chain
@@ -1532,35 +1262,12 @@ class Spoiler():
self.paths[str(multiworld.get_region('Inverted Big Bomb Shop', player))] = \ self.paths[str(multiworld.get_region('Inverted Big Bomb Shop', player))] = \
get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player)) get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player))
def to_json(self):
self.parse_data()
out = OrderedDict()
out['Entrances'] = list(self.entrances.values())
out.update(self.locations)
out['Special'] = self.medallions
if self.hashes:
out['Hashes'] = self.hashes
if self.shops:
out['Shops'] = self.shops
out['playthrough'] = self.playthrough
out['paths'] = self.paths
out['Bosses'] = self.bosses
return json.dumps(out)
def to_file(self, filename: str): def to_file(self, filename: str):
self.parse_data()
def bool_to_text(variable: Union[bool, str]) -> str:
if type(variable) == str:
return variable
return 'Yes' if variable else 'No'
def write_option(option_key: str, option_obj: type(Options.Option)): def write_option(option_key: str, option_obj: type(Options.Option)):
res = getattr(self.multiworld, option_key)[player] res = getattr(self.multiworld, option_key)[player]
display_name = getattr(option_obj, "display_name", option_key) display_name = getattr(option_obj, "display_name", option_key)
try: try:
outfile.write(f'{display_name + ":":33}{res.get_current_option_name()}\n') outfile.write(f'{display_name + ":":33}{res.current_option_name}\n')
except: except:
raise Exception raise Exception
@@ -1577,46 +1284,13 @@ class Spoiler():
if self.multiworld.players > 1: if self.multiworld.players > 1:
outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player))) outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player)))
outfile.write('Game: %s\n' % self.multiworld.game[player]) outfile.write('Game: %s\n' % self.multiworld.game[player])
for f_option, option in Options.per_game_common_options.items():
options = ChainMap(Options.per_game_common_options, self.multiworld.worlds[player].option_definitions)
for f_option, option in options.items():
write_option(f_option, option) write_option(f_option, option)
options = self.multiworld.worlds[player].option_definitions
if options:
for f_option, option in options.items():
write_option(f_option, option)
AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile) AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile)
if player in self.multiworld.get_game_players("A Link to the Past"):
outfile.write('%s%s\n' % ('Hash: ', self.hashes[player]))
outfile.write('Logic: %s\n' % self.multiworld.logic[player])
outfile.write('Dark Room Logic: %s\n' % self.multiworld.dark_room_logic[player])
outfile.write('Mode: %s\n' % self.multiworld.mode[player])
outfile.write('Goal: %s\n' % self.multiworld.goal[player])
if "triforce" in self.multiworld.goal[player]: # triforce hunt
outfile.write("Pieces available for Triforce: %s\n" %
self.multiworld.triforce_pieces_available[player])
outfile.write("Pieces required for Triforce: %s\n" %
self.multiworld.triforce_pieces_required[player])
outfile.write('Difficulty: %s\n' % self.multiworld.difficulty[player])
outfile.write('Item Functionality: %s\n' % self.multiworld.item_functionality[player])
outfile.write('Entrance Shuffle: %s\n' % self.multiworld.shuffle[player])
if self.multiworld.shuffle[player] != "vanilla":
outfile.write('Entrance Shuffle Seed %s\n' % self.multiworld.worlds[player].er_seed)
outfile.write('Shop inventory shuffle: %s\n' %
bool_to_text("i" in self.multiworld.shop_shuffle[player]))
outfile.write('Shop price shuffle: %s\n' %
bool_to_text("p" in self.multiworld.shop_shuffle[player]))
outfile.write('Shop upgrade shuffle: %s\n' %
bool_to_text("u" in self.multiworld.shop_shuffle[player]))
outfile.write('New Shop inventory: %s\n' %
bool_to_text("g" in self.multiworld.shop_shuffle[player] or
"f" in self.multiworld.shop_shuffle[player]))
outfile.write('Custom Potion Shop: %s\n' %
bool_to_text("w" in self.multiworld.shop_shuffle[player]))
outfile.write('Enemy health: %s\n' % self.multiworld.enemy_health[player])
outfile.write('Enemy damage: %s\n' % self.multiworld.enemy_damage[player])
outfile.write('Prize shuffle %s\n' %
self.multiworld.shuffle_prizes[player])
if self.entrances: if self.entrances:
outfile.write('\n\nEntrances:\n\n') outfile.write('\n\nEntrances:\n\n')
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.multiworld.get_player_name(entry["player"])}: ' outfile.write('\n'.join(['%s%s %s %s' % (f'{self.multiworld.get_player_name(entry["player"])}: '
@@ -1625,30 +1299,14 @@ class Spoiler():
'<=' 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()]))
if self.medallions:
outfile.write('\n\nMedallions:\n')
for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion}')
AutoWorld.call_all(self.multiworld, "write_spoiler", outfile) AutoWorld.call_all(self.multiworld, "write_spoiler", outfile)
locations = [(str(location), str(location.item) if location.item is not None else "Nothing")
for location in self.multiworld.get_locations() if location.show_in_spoiler]
outfile.write('\n\nLocations:\n\n') outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join( outfile.write('\n'.join(
['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in ['%s: %s' % (location, item) for location, item in locations]))
grouping.items()]))
if self.shops:
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.multiworld.get_game_players("A Link to the Past"):
if self.multiworld.boss_shuffle[player] != 'none':
bossmap = self.bosses[str(player)] if self.multiworld.players > 1 else self.bosses
outfile.write(
f'\n\nBosses{(f" ({self.multiworld.get_player_name(player)})" if self.multiworld.players > 1 else "")}:\n')
outfile.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
outfile.write('\n\nPlaythrough:\n\n') outfile.write('\n\nPlaythrough:\n\n')
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join( outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join(
[' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [ [' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [

View File

@@ -63,7 +63,7 @@ class ClientCommandProcessor(CommandProcessor):
def _cmd_received(self) -> bool: def _cmd_received(self) -> bool:
"""List all received items""" """List all received items"""
logger.info(f'{len(self.ctx.items_received)} received items:') self.output(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):
self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}") self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}")
return True return True
@@ -341,6 +341,11 @@ class CommonContext:
return self.slot in self.slot_info[slot].group_members return self.slot in self.slot_info[slot].group_members
return False return False
def is_echoed_chat(self, print_json_packet: dict) -> bool:
return print_json_packet.get("type", "") == "Chat" \
and print_json_packet.get("team", None) == self.team \
and print_json_packet.get("slot", None) == self.slot
def is_uninteresting_item_send(self, print_json_packet: dict) -> bool: def is_uninteresting_item_send(self, print_json_packet: dict) -> bool:
"""Helper function for filtering out ItemSend prints that do not concern the local player.""" """Helper function for filtering out ItemSend prints that do not concern the local player."""
return print_json_packet.get("type", "") == "ItemSend" \ return print_json_packet.get("type", "") == "ItemSend" \

View File

@@ -109,9 +109,10 @@ class FactorioContext(CommonContext):
def on_print_json(self, args: dict): def on_print_json(self, args: dict):
if self.rcon_client: if self.rcon_client:
if not self.filter_item_sends or not self.is_uninteresting_item_send(args): if (not self.filter_item_sends or not self.is_uninteresting_item_send(args)) \
and not self.is_echoed_chat(args):
text = self.factorio_json_text_parser(copy.deepcopy(args["data"])) text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
if not text.startswith(self.player_names[self.slot] + ":"): if not text.startswith(self.player_names[self.slot] + ":"): # TODO: Remove string heuristic in the future.
self.print_to_game(text) self.print_to_game(text)
super(FactorioContext, self).on_print_json(args) super(FactorioContext, self).on_print_json(args)

View File

@@ -840,8 +840,7 @@ def distribute_planned(world: MultiWorld) -> None:
maxcount = placement['count']['target'] maxcount = placement['count']['target']
from_pool = placement['from_pool'] from_pool = placement['from_pool']
candidates = list(location for location in world.get_unfilled_locations_for_players(locations, candidates = list(world.get_unfilled_locations_for_players(locations, sorted(worlds)))
worlds))
world.random.shuffle(candidates) world.random.shuffle(candidates)
world.random.shuffle(items) world.random.shuffle(items)
count = 0 count = 0

View File

@@ -107,7 +107,7 @@ def main(args=None, callback=ERmain):
player_files = {} player_files = {}
for file in os.scandir(args.player_files_path): for file in os.scandir(args.player_files_path):
fname = file.name fname = file.name
if file.is_file() and not file.name.startswith(".") and \ if file.is_file() and not fname.startswith(".") and \
os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}: os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}:
path = os.path.join(args.player_files_path, fname) path = os.path.join(args.player_files_path, fname)
try: try:

View File

@@ -132,7 +132,8 @@ components: Iterable[Component] = (
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'), Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'),
# SNI # SNI
Component('SNI Client', 'SNIClient', Component('SNI Client', 'SNIClient',
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', '.apsmw')), file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3',
'.apsmw', '.apl2ac')),
Component('LttP Adjuster', 'LttPAdjuster'), Component('LttP Adjuster', 'LttPAdjuster'),
# Factorio # Factorio
Component('Factorio Client', 'FactorioClient'), Component('Factorio Client', 'FactorioClient'),
@@ -147,10 +148,14 @@ components: Iterable[Component] = (
Component('FF1 Client', 'FF1Client'), Component('FF1 Client', 'FF1Client'),
# Pokémon # Pokémon
Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')), Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
# TLoZ
Component('Zelda 1 Client', 'Zelda1Client'),
# ChecksFinder # ChecksFinder
Component('ChecksFinder Client', 'ChecksFinderClient'), Component('ChecksFinder Client', 'ChecksFinderClient'),
# Starcraft 2 # Starcraft 2
Component('Starcraft 2 Client', 'Starcraft2Client'), Component('Starcraft 2 Client', 'Starcraft2Client'),
# Wargroove
Component('Wargroove Client', 'WargrooveClient'),
# Zillion # Zillion
Component('Zillion Client', 'ZillionClient', Component('Zillion Client', 'ZillionClient',
file_identifier=SuffixIdentifier('.apzl')), file_identifier=SuffixIdentifier('.apzl')),

View File

@@ -35,7 +35,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.slot_seeds = {1: random} self.per_slot_randoms = {1: random}
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):

30
Main.py
View File

@@ -9,11 +9,12 @@ import tempfile
import zipfile import zipfile
from typing import Dict, List, Tuple, Optional, Set from typing import Dict, List, Tuple, Optional, Set
from BaseClasses import Item, MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location from BaseClasses import Item, MultiWorld, CollectionState, Region, LocationProgressType, Location
import worlds import worlds
from worlds.alttp.SubClasses import LTTPRegionType
from worlds.alttp.Regions import is_main_entrance from worlds.alttp.Regions import is_main_entrance
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 SHOP_ID_START, total_shop_slots, FillDisabledShopSlots from worlds.alttp.Shops import FillDisabledShopSlots
from Utils import output_path, get_options, __version__, version_tuple from Utils import output_path, get_options, __version__, version_tuple
from worlds.generic.Rules import locality_rules, exclusion_rules from worlds.generic.Rules import locality_rules, exclusion_rules
from worlds import AutoWorld from worlds import AutoWorld
@@ -37,7 +38,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world = MultiWorld(args.multi) world = MultiWorld(args.multi)
logger = logging.getLogger() logger = logging.getLogger()
world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed)) world.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
world.plando_options = args.plando_options world.plando_options = args.plando_options
world.shuffle = args.shuffle.copy() world.shuffle = args.shuffle.copy()
@@ -52,7 +53,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.enemy_damage = args.enemy_damage.copy() world.enemy_damage = args.enemy_damage.copy()
world.beemizer_total_chance = args.beemizer_total_chance.copy() world.beemizer_total_chance = args.beemizer_total_chance.copy()
world.beemizer_trap_chance = args.beemizer_trap_chance.copy() world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
world.timer = args.timer.copy()
world.countdown_start_time = args.countdown_start_time.copy() world.countdown_start_time = args.countdown_start_time.copy()
world.red_clock_time = args.red_clock_time.copy() world.red_clock_time = args.red_clock_time.copy()
world.blue_clock_time = args.blue_clock_time.copy() world.blue_clock_time = args.blue_clock_time.copy()
@@ -78,7 +78,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.state = CollectionState(world) world.state = CollectionState(world)
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:") logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types) longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
max_item = 0 max_item = 0
@@ -191,7 +191,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
new_item.classification |= classifications[item_name] new_item.classification |= classifications[item_name]
new_itempool.append(new_item) new_itempool.append(new_item)
region = Region("Menu", RegionType.Generic, "ItemLink", group_id, world) region = Region("Menu", group_id, world, "ItemLink")
world.regions.append(region) world.regions.append(region)
locations = region.locations = [] locations = region.locations = []
for item in world.itempool: for item in world.itempool:
@@ -251,6 +251,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
balance_multiworld_progression(world) balance_multiworld_progression(world)
logger.info(f'Beginning output...') logger.info(f'Beginning output...')
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
world.random.passthrough = False
outfilebase = 'AP_' + world.seed_name outfilebase = 'AP_' + world.seed_name
output = tempfile.TemporaryDirectory() output = tempfile.TemporaryDirectory()
@@ -286,13 +290,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
'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 location.parent_region.type == RegionType.LightWorld: elif location.parent_region.type == LTTPRegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address) checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.type == RegionType.DarkWorld: elif location.parent_region.type == LTTPRegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address) checks_in_area[location.player]["Dark World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.LightWorld: elif main_entrance.parent_region.type == LTTPRegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address) checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.DarkWorld: elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address) checks_in_area[location.player]["Dark World"].append(location.address)
checks_in_area[location.player]["Total"] += 1 checks_in_area[location.player]["Total"] += 1
@@ -312,7 +316,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
client_versions[slot] = player_world.required_client_version client_versions[slot] = player_world.required_client_version
games[slot] = world.game[slot] games[slot] = world.game[slot]
slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot], slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot],
world.player_types[slot]) world.player_types[slot], bool(world.allow_collect[slot].value))
for slot, group in world.groups.items(): for slot, group in world.groups.items():
games[slot] = world.game[slot] games[slot] = world.game[slot]
slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot], slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot],
@@ -357,12 +361,12 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
if game_world.data_version == 0 and game_world.game not in datapackage: if game_world.data_version == 0 and game_world.game not in datapackage:
datapackage[game_world.game] = worlds.network_data_package["games"][game_world.game] datapackage[game_world.game] = worlds.network_data_package["games"][game_world.game]
datapackage[game_world.game]["item_name_groups"] = game_world.item_name_groups datapackage[game_world.game]["item_name_groups"] = game_world.item_name_groups
datapackage[game_world.game]["location_name_groups"] = game_world.location_name_groups
multidata = { multidata = {
"slot_data": slot_data, "slot_data": slot_data,
"slot_info": slot_info, "slot_info": slot_info,
"names": names, # TODO: remove around 0.2.5 in favor of slot_info "names": names, # TODO: remove after 0.3.9
"games": games, # TODO: remove around 0.2.5 in favor of slot_info
"connect_names": {name: (0, player) for player, name in world.player_name.items()}, "connect_names": {name: (0, player) for player, name in world.player_name.items()},
"locations": locations_data, "locations": locations_data,
"checks_in_area": checks_in_area, "checks_in_area": checks_in_area,

View File

@@ -2,6 +2,7 @@ import os
import sys import sys
import subprocess import subprocess
import pkg_resources import pkg_resources
import warnings
local_dir = os.path.dirname(__file__) local_dir = os.path.dirname(__file__)
requirements_files = {os.path.join(local_dir, 'requirements.txt')} requirements_files = {os.path.join(local_dir, 'requirements.txt')}
@@ -39,6 +40,8 @@ def update(yes=False, force=False):
path = os.path.join(os.path.dirname(__file__), req_file) path = os.path.join(os.path.dirname(__file__), req_file)
with open(path) as requirementsfile: with open(path) as requirementsfile:
for line in requirementsfile: for line in requirementsfile:
if not line or line[0] == "#":
continue # ignore comments
if line.startswith(("https://", "git+https://")): if line.startswith(("https://", "git+https://")):
# extract name and version for url # extract name and version for url
rest = line.split('/')[-1] rest = line.split('/')[-1]
@@ -46,8 +49,10 @@ def update(yes=False, force=False):
if "#egg=" in rest: if "#egg=" in rest:
# from egg info # from egg info
rest, egg = rest.split("#egg=", 1) rest, egg = rest.split("#egg=", 1)
egg = egg.split(";", 1)[0] egg = egg.split(";", 1)[0].rstrip()
if any(compare in egg for compare in ("==", ">=", ">", "<", "<=", "!=")): if any(compare in egg for compare in ("==", ">=", ">", "<", "<=", "!=")):
warnings.warn(f"Specifying version as #egg={egg} will become unavailable in pip 25.0. "
"Use name @ url#version instead.", DeprecationWarning)
line = egg line = egg
else: else:
egg = "" egg = ""
@@ -58,16 +63,27 @@ def update(yes=False, force=False):
rest = rest.replace(".zip", "-").replace(".tar.gz", "-") rest = rest.replace(".zip", "-").replace(".tar.gz", "-")
name, version, _ = rest.split("-", 2) name, version, _ = rest.split("-", 2)
line = f'{egg or name}=={version}' line = f'{egg or name}=={version}'
elif "@" in line and "#" in line:
# PEP 508 does not allow us to specify a version, so we use custom syntax
# name @ url#version ; marker
name, rest = line.split("@", 1)
version = rest.split("#", 1)[1].split(";", 1)[0].rstrip()
line = f"{name.rstrip()}=={version}"
if ";" in rest: # keep marker
line += rest[rest.find(";"):]
requirements = pkg_resources.parse_requirements(line) requirements = pkg_resources.parse_requirements(line)
for requirement in requirements: for requirement in map(str, requirements):
requirement = str(requirement)
try: try:
pkg_resources.require(requirement) pkg_resources.require(requirement)
except pkg_resources.ResolutionError: except pkg_resources.ResolutionError:
if not yes: if not yes:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
input(f'Requirement {requirement} is not satisfied, press enter to install it') try:
input(f"\nRequirement {requirement} is not satisfied, press enter to install it")
except KeyboardInterrupt:
print("\nAborting")
sys.exit(1)
update_command() update_command()
return return

View File

@@ -41,7 +41,6 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ
SlotType SlotType
min_client_version = Version(0, 1, 6) min_client_version = Version(0, 1, 6)
print_command_compatability_threshold = Version(0, 3, 5) # Remove backwards compatibility around 0.3.7
colorama.init() colorama.init()
@@ -159,11 +158,14 @@ class Context:
stored_data: typing.Dict[str, object] stored_data: typing.Dict[str, object]
read_data: typing.Dict[str, object] read_data: typing.Dict[str, object]
stored_data_notification_clients: typing.Dict[str, typing.Set[Client]] stored_data_notification_clients: typing.Dict[str, typing.Set[Client]]
slot_info: typing.Dict[int, NetworkSlot]
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
all_item_and_group_names: typing.Dict[str, typing.Set[str]] all_item_and_group_names: typing.Dict[str, typing.Set[str]]
all_location_and_group_names: typing.Dict[str, typing.Set[str]]
non_hintable_names: typing.Dict[str, typing.Set[str]] non_hintable_names: typing.Dict[str, typing.Set[str]]
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
@@ -171,7 +173,7 @@ class Context:
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
log_network: bool = False): log_network: bool = False):
super(Context, self).__init__() super(Context, self).__init__()
self.slot_info: typing.Dict[int, NetworkSlot] = {} self.slot_info = {}
self.log_network = log_network self.log_network = log_network
self.endpoints = [] self.endpoints = []
self.clients = {} self.clients = {}
@@ -220,6 +222,7 @@ class Context:
self.save_dirty = False self.save_dirty = False
self.tags = ['AP'] self.tags = ['AP']
self.games: typing.Dict[int, str] = {} self.games: typing.Dict[int, str] = {}
self.allow_collect: typing.Dict[int, bool] = {}
self.minimum_client_versions: typing.Dict[int, Utils.Version] = {} self.minimum_client_versions: typing.Dict[int, Utils.Version] = {}
self.seed_name = "" self.seed_name = ""
self.groups = {} self.groups = {}
@@ -232,7 +235,9 @@ class Context:
# init empty to satisfy linter, I suppose # init empty to satisfy linter, I suppose
self.gamespackage = {} self.gamespackage = {}
self.item_name_groups = {} self.item_name_groups = {}
self.location_name_groups = {}
self.all_item_and_group_names = {} self.all_item_and_group_names = {}
self.all_location_and_group_names = {}
self.non_hintable_names = collections.defaultdict(frozenset) self.non_hintable_names = collections.defaultdict(frozenset)
self._load_game_data() self._load_game_data()
@@ -244,6 +249,8 @@ class Context:
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()} worlds.AutoWorldRegister.world_types.items()}
self.location_name_groups = {world_name: world.location_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()}
for world_name, world in worlds.AutoWorldRegister.world_types.items(): for world_name, world in worlds.AutoWorldRegister.world_types.items():
self.non_hintable_names[world_name] = world.hint_blacklist self.non_hintable_names[world_name] = world.hint_blacklist
@@ -255,6 +262,8 @@ class Context:
self.location_names[location_id] = location_name self.location_names[location_id] = location_name
self.all_item_and_group_names[game_name] = \ self.all_item_and_group_names[game_name] = \
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name]) set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
self.all_location_and_group_names[game_name] = \
set(game_package["location_name_to_id"]) | set(self.location_name_groups[game_name])
def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]: def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None
@@ -309,6 +318,10 @@ class Context:
endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth) endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth)
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs)) async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
def broadcast_text_all(self, text: str, additional_arguments: dict = {}):
logging.info("Notice (all): %s" % text)
self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
def broadcast_team(self, team: int, msgs: typing.List[dict]): def broadcast_team(self, team: int, msgs: typing.List[dict]):
msgs = self.dumper(msgs) msgs = self.dumper(msgs)
endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values())) endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values()))
@@ -325,29 +338,18 @@ class Context:
self.clients[endpoint.team][endpoint.slot].remove(endpoint) self.clients[endpoint.team][endpoint.slot].remove(endpoint)
await on_client_disconnected(self, endpoint) await on_client_disconnected(self, endpoint)
# text def notify_client(self, client: Client, text: str, additional_arguments: dict = {}):
def notify_all(self, text: str):
logging.info("Notice (all): %s" % text)
broadcast_text_all(self, text)
def notify_client(self, client: Client, text: str):
if not client.auth: if not client.auth:
return return
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text)) logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
if client.version >= print_command_compatability_threshold: async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }]}]))
else:
async_start(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
def notify_client_multiple(self, client: Client, texts: typing.List[str]): def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}):
if not client.auth: if not client.auth:
return return
if client.version >= print_command_compatability_threshold: async_start(self.send_msgs(client,
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}
[{"cmd": "PrintJSON", "data": [{ "text": text }]} for text in texts])) for text in texts]))
else:
async_start(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
# loading # loading
@@ -386,15 +388,26 @@ class Context:
for player, version in clients_ver.items(): for player, version in clients_ver.items():
self.minimum_client_versions[player] = max(Utils.Version(*version), min_client_version) self.minimum_client_versions[player] = max(Utils.Version(*version), min_client_version)
self.clients = {} self.slot_info = decoded_obj["slot_info"]
for team, names in enumerate(decoded_obj['names']): self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
self.clients[team] = {} self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items()
for player, name in enumerate(names, 1): if slot_info.type == SlotType.group}
self.clients[team][player] = [] # TODO: around 0.4.2 or so, remove the if/else backwards compatibility check.
self.player_names[team, player] = name self.allow_collect = {slot: slot_info.allow_collect if type(slot_info.allow_collect) is bool else True
self.player_name_lookup[name] = team, player for slot, slot_info in self.slot_info.items()}
self.read_data[f"hints_{team}_{player}"] = lambda local_team=team, local_player=player: \
list(self.get_rechecked_hints(local_team, local_player)) self.clients = {0: {}}
slot_info: NetworkSlot
slot_id: int
team_0 = self.clients[0]
for slot_id, slot_info in self.slot_info.items():
team_0[slot_id] = []
self.player_names[0, slot_id] = slot_info.name
self.player_name_lookup[slot_info.name] = 0, slot_id
self.read_data[f"hints_{0}_{slot_id}"] = lambda local_team=0, local_player=slot_id: \
list(self.get_rechecked_hints(local_team, local_player))
self.seed_name = decoded_obj["seed_name"] self.seed_name = decoded_obj["seed_name"]
self.random.seed(self.seed_name) self.random.seed(self.seed_name)
self.connect_names = decoded_obj['connect_names'] self.connect_names = decoded_obj['connect_names']
@@ -409,29 +422,9 @@ class Context:
for slot, item_codes in decoded_obj["precollected_items"].items(): for slot, item_codes in decoded_obj["precollected_items"].items():
self.start_inventory[slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes] self.start_inventory[slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
for team in range(len(decoded_obj['names'])): for slot, hints in decoded_obj["precollected_hints"].items():
for slot, hints in decoded_obj["precollected_hints"].items(): self.hints[0, slot].update(hints)
self.hints[team, slot].update(hints)
if "slot_info" in decoded_obj:
self.slot_info = decoded_obj["slot_info"]
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items()
if slot_info.type == SlotType.group}
else:
self.games = decoded_obj["games"]
self.groups = {}
self.slot_info = {
slot: NetworkSlot(
self.player_names[0, slot],
self.games[slot],
SlotType(int(bool(locations))))
for slot, locations in self.locations.items()
}
# locations may need converting
for slot, locations in self.locations.items():
for location, item_data in locations.items():
if len(item_data) < 3:
locations[location] = (*item_data, 0)
# declare slots that aren't players as done # declare slots that aren't players as done
for slot, slot_info in self.slot_info.items(): for slot, slot_info in self.slot_info.items():
if slot_info.type.always_goal: if slot_info.type.always_goal:
@@ -447,10 +440,14 @@ class Context:
logging.info(f"Loading custom datapackage for game {game_name}") logging.info(f"Loading custom datapackage for game {game_name}")
self.gamespackage[game_name] = data self.gamespackage[game_name] = data
self.item_name_groups[game_name] = data["item_name_groups"] self.item_name_groups[game_name] = data["item_name_groups"]
self.location_name_groups[game_name] = data["location_name_groups"]
del data["item_name_groups"] # remove from datapackage, but keep in self.item_name_groups del data["item_name_groups"] # remove from datapackage, but keep in self.item_name_groups
del data["location_name_groups"]
self._init_game_data() self._init_game_data()
for game_name, data in self.item_name_groups.items(): for game_name, data in self.item_name_groups.items():
self.read_data[f"item_name_groups_{game_name}"] = lambda lgame=game_name: self.item_name_groups[lgame] self.read_data[f"item_name_groups_{game_name}"] = lambda lgame=game_name: self.item_name_groups[lgame]
for game_name, data in self.location_name_groups.items():
self.read_data[f"location_name_groups_{game_name}"] = lambda lgame=game_name: self.location_name_groups[lgame]
# saving # saving
@@ -685,7 +682,7 @@ class Context:
def on_goal_achieved(self, client: Client): def on_goal_achieved(self, client: Client):
finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \ finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \
f' has completed their goal.' f' has completed their goal.'
self.notify_all(finished_msg) self.broadcast_text_all(finished_msg, {"type": "Goal", "team": client.team, "slot": client.slot})
if "auto" in self.collect_mode: if "auto" in self.collect_mode:
collect_player(self, client.team, client.slot) collect_player(self, client.team, client.slot)
if "auto" in self.release_mode: if "auto" in self.release_mode:
@@ -778,55 +775,42 @@ async def on_client_joined(ctx: Context, client: Client):
update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED) update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED)
version_str = '.'.join(str(x) for x in client.version) version_str = '.'.join(str(x) for x in client.version)
verb = "tracking" if "Tracker" in client.tags else "playing" verb = "tracking" if "Tracker" in client.tags else "playing"
ctx.notify_all( ctx.broadcast_text_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) " f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
f"{verb} {ctx.games[client.slot]} has joined. " f"{verb} {ctx.games[client.slot]} has joined. "
f"Client({version_str}), {client.tags}).") f"Client({version_str}), {client.tags}).",
{"type": "Join", "team": client.team, "slot": client.slot, "tags": client.tags})
ctx.notify_client(client, "Now that you are connected, " ctx.notify_client(client, "Now that you are connected, "
"you can use !help to list commands to run via the server. " "you can use !help to list commands to run via the server. "
"If your client supports it, " "If your client supports it, "
"you may have additional local commands you can list with /help.") "you may have additional local commands you can list with /help.",
{"type": "Tutorial"})
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( ctx.broadcast_text_all(
"%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1)) "%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1),
{"type": "Part", "team": client.team, "slot": client.slot})
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 countdown(ctx: Context, timer: int): async def countdown(ctx: Context, timer: int):
broadcast_countdown(ctx, timer, f"[Server]: Starting countdown of {timer}s") ctx.broadcast_text_all(f"[Server]: Starting countdown of {timer}s", {"type": "Countdown", "countdown": timer})
if ctx.countdown_timer: if ctx.countdown_timer:
ctx.countdown_timer = timer # timer is already running, set it to a different time ctx.countdown_timer = timer # timer is already running, set it to a different time
else: else:
ctx.countdown_timer = timer ctx.countdown_timer = timer
while ctx.countdown_timer > 0: while ctx.countdown_timer > 0:
broadcast_countdown(ctx, ctx.countdown_timer, f"[Server]: {ctx.countdown_timer}") ctx.broadcast_text_all(f"[Server]: {ctx.countdown_timer}",
{"type": "Countdown", "countdown": ctx.countdown_timer})
ctx.countdown_timer -= 1 ctx.countdown_timer -= 1
await asyncio.sleep(1) await asyncio.sleep(1)
broadcast_countdown(ctx, 0, f"[Server]: GO") ctx.broadcast_text_all(f"[Server]: GO", {"type": "Countdown", "countdown": 0})
ctx.countdown_timer = 0 ctx.countdown_timer = 0
def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {}):
old_clients, new_clients = [], []
for teams in ctx.clients.values():
for clients in teams.values():
for client in clients:
new_clients.append(client) if client.version >= print_command_compatability_threshold \
else old_clients.append(client)
ctx.broadcast(old_clients, [{"cmd": "Print", "text": text }])
ctx.broadcast(new_clients, [{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
def broadcast_countdown(ctx: Context, timer: int, message: str):
broadcast_text_all(ctx, message, {"type": "Countdown", "countdown": timer})
def get_players_string(ctx: Context): def get_players_string(ctx: Context):
auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth} auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth}
@@ -894,7 +878,9 @@ def update_checked_locations(ctx: Context, team: int, slot: int):
def release_player(ctx: Context, team: int, slot: int): def release_player(ctx: Context, team: int, slot: int):
"""register any locations that are in the multidata""" """register any locations that are in the multidata"""
all_locations = set(ctx.locations[slot]) all_locations = set(ctx.locations[slot])
ctx.notify_all("%s (Team #%d) has released all remaining items from their world." % (ctx.player_names[(team, slot)], team + 1)) ctx.broadcast_text_all("%s (Team #%d) has released all remaining items from their world."
% (ctx.player_names[(team, slot)], team + 1),
{"type": "Release", "team": team, "slot": slot})
register_location_checks(ctx, team, slot, all_locations) register_location_checks(ctx, team, slot, all_locations)
update_checked_locations(ctx, team, slot) update_checked_locations(ctx, team, slot)
@@ -903,11 +889,15 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
"""register any locations that are in the multidata, pointing towards this player""" """register any locations that are in the multidata, pointing towards this player"""
all_locations = collections.defaultdict(set) all_locations = collections.defaultdict(set)
for source_slot, location_data in ctx.locations.items(): for source_slot, location_data in ctx.locations.items():
if not ctx.allow_collect[source_slot]:
continue
for location_id, values in location_data.items(): for location_id, values in location_data.items():
if values[1] == slot: if values[1] == slot:
all_locations[source_slot].add(location_id) all_locations[source_slot].add(location_id)
ctx.notify_all("%s (Team #%d) has collected their items from other worlds." % (ctx.player_names[(team, slot)], team + 1)) ctx.broadcast_text_all("%s (Team #%d) has collected their items from other worlds."
% (ctx.player_names[(team, slot)], team + 1),
{"type": "Collect", "team": team, "slot": slot})
for source_player, location_ids in all_locations.items(): for source_player, location_ids in all_locations.items():
register_location_checks(ctx, team, source_player, location_ids, count_activity=False) register_location_checks(ctx, team, source_player, location_ids, count_activity=False)
update_checked_locations(ctx, team, source_player) update_checked_locations(ctx, team, source_player)
@@ -1177,11 +1167,15 @@ class ClientMessageProcessor(CommonCommandProcessor):
def __call__(self, raw: str) -> typing.Optional[bool]: def __call__(self, raw: str) -> typing.Optional[bool]:
if not raw.startswith("!admin"): if not raw.startswith("!admin"):
self.ctx.notify_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + raw) self.ctx.broadcast_text_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + raw,
{"type": "Chat", "team": self.client.team, "slot": self.client.slot, "message": raw})
return super(ClientMessageProcessor, self).__call__(raw) return super(ClientMessageProcessor, self).__call__(raw)
def output(self, text): def output(self, text: str):
self.ctx.notify_client(self.client, text) self.ctx.notify_client(self.client, text, {"type": "CommandResult"})
def output_multiple(self, texts: typing.List[str]):
self.ctx.notify_client_multiple(self.client, texts, {"type": "CommandResult"})
def default(self, raw: str): def default(self, raw: str):
pass # default is client sending just text pass # default is client sending just text
@@ -1204,9 +1198,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
# disallow others from knowing what the new remote administration password is. # disallow others from knowing what the new remote administration password is.
"!admin /option server_password"): "!admin /option server_password"):
output = f"!admin /option server_password {('*' * random.randint(4, 16))}" output = f"!admin /option server_password {('*' * random.randint(4, 16))}"
# Otherwise notify the others what is happening. self.ctx.broadcast_text_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + output,
self.ctx.notify_all(self.ctx.get_aliased_name(self.client.team, {"type": "Chat", "team": self.client.team, "slot": self.client.slot, "message": output})
self.client.slot) + ': ' + output)
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")
@@ -1243,7 +1236,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
def _cmd_players(self) -> bool: def _cmd_players(self) -> bool:
"""Get information about connected and missing players.""" """Get information about connected and missing players."""
if len(self.ctx.player_names) < 10: if len(self.ctx.player_names) < 10:
self.ctx.notify_all(get_players_string(self.ctx)) self.ctx.broadcast_text_all(get_players_string(self.ctx), {"type": "CommandResult"})
else: else:
self.output(get_players_string(self.ctx)) self.output(get_players_string(self.ctx))
return True return True
@@ -1332,7 +1325,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
if locations: if locations:
texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations] texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations]
texts.append(f"Found {len(locations)} missing location checks") texts.append(f"Found {len(locations)} missing location checks")
self.ctx.notify_client_multiple(self.client, texts) self.output_multiple(texts)
else: else:
self.output("No missing location checks found.") self.output("No missing location checks found.")
return True return True
@@ -1345,7 +1338,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
if locations: if locations:
texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations] texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations]
texts.append(f"Found {len(locations)} done location checks") texts.append(f"Found {len(locations)} done location checks")
self.ctx.notify_client_multiple(self.client, texts) self.output_multiple(texts)
else: else:
self.output("No done location checks found.") self.output("No done location checks found.")
return True return True
@@ -1381,9 +1374,10 @@ class ClientMessageProcessor(CommonCommandProcessor):
new_item = NetworkItem(names[item_name], -1, self.client.slot) new_item = NetworkItem(names[item_name], -1, self.client.slot)
get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item) get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item)
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item) get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
self.ctx.notify_all( self.ctx.broadcast_text_all(
'Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team, 'Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team,
self.client.slot)) self.client.slot),
{"type": "ItemCheat", "team": self.client.team, "receiving": self.client.slot, "item": new_item})
send_new_items(self.ctx) send_new_items(self.ctx)
return True return True
else: else:
@@ -1427,7 +1421,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
if game not in self.ctx.all_item_and_group_names: if game not in self.ctx.all_item_and_group_names:
self.output("Can't look up item/location for unknown game. Hint for ID instead.") self.output("Can't look up item/location for unknown game. Hint for ID instead.")
return False return False
names = self.ctx.location_names_for_game(game) \ names = self.ctx.all_location_and_group_names[game] \
if for_location else \ if for_location else \
self.ctx.all_item_and_group_names[game] self.ctx.all_item_and_group_names[game]
hint_name, usable, response = get_intended_text(input_text, names) hint_name, usable, response = get_intended_text(input_text, names)
@@ -1443,6 +1437,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name)) hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
elif hint_name in self.ctx.location_name_groups[game]: # location group name
hints = []
for loc_name in self.ctx.location_name_groups[game][hint_name]:
if loc_name in self.ctx.location_names_for_game(game):
hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name))
else: # location name else: # location name
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name) hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
@@ -1682,9 +1681,10 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
client.tags = args["tags"] client.tags = args["tags"]
if set(old_tags) != set(client.tags): if set(old_tags) != set(client.tags):
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
ctx.notify_all( ctx.broadcast_text_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags " f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
f"from {old_tags} to {client.tags}.") f"from {old_tags} to {client.tags}.",
{"type": "TagsChanged", "team": client.team, "slot": client.slot, "tags": client.tags})
elif cmd == 'Sync': elif cmd == 'Sync':
start_inventory = get_start_inventory(ctx, client.slot, client.remote_start_inventory) start_inventory = get_start_inventory(ctx, client.slot, client.remote_start_inventory)
@@ -1802,11 +1802,11 @@ class ServerCommandProcessor(CommonCommandProcessor):
def output(self, text: str): def output(self, text: str):
if self.client: if self.client:
self.ctx.notify_client(self.client, text) self.ctx.notify_client(self.client, text, {"type": "AdminCommandResult"})
super(ServerCommandProcessor, self).output(text) super(ServerCommandProcessor, self).output(text)
def default(self, raw: str): def default(self, raw: str):
self.ctx.notify_all('[Server]: ' + raw) self.ctx.broadcast_text_all('[Server]: ' + raw, {"type": "ServerChat", "message": raw})
def _cmd_save(self) -> bool: def _cmd_save(self) -> bool:
"""Save current state to multidata""" """Save current state to multidata"""
@@ -1947,7 +1947,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
send_items_to(self.ctx, team, slot, *new_items) send_items_to(self.ctx, team, slot, *new_items)
send_new_items(self.ctx) send_new_items(self.ctx)
self.ctx.notify_all( self.ctx.broadcast_text_all(
'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') + 'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') +
f'"{item_name}" to {self.ctx.get_aliased_name(team, slot)}') f'"{item_name}" to {self.ctx.get_aliased_name(team, slot)}')
return True return True

View File

@@ -71,6 +71,7 @@ class NetworkSlot(typing.NamedTuple):
name: str name: str
game: str game: str
type: SlotType type: SlotType
allow_collect: bool = True
group_members: typing.Union[typing.List[int], typing.Tuple] = () # only populated if type == group group_members: typing.Union[typing.List[int], typing.Tuple] = () # only populated if type == group

View File

@@ -197,7 +197,7 @@ def set_icon(window):
def adjust(args): def adjust(args):
# Create a fake world and OOTWorld to use as a base # Create a fake world and OOTWorld to use as a base
world = MultiWorld(1) world = MultiWorld(1)
world.slot_seeds = {1: random} world.per_slot_randoms = {1: random}
ootworld = OOTWorld(world, 1) ootworld = OOTWorld(world, 1)
# Set options in the fake OOTWorld # Set options in the fake OOTWorld
for name, option in chain(cosmetic_options.items(), sfx_options.items()): for name, option in chain(cosmetic_options.items(), sfx_options.items()):

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import abc import abc
import logging
from copy import deepcopy from copy import deepcopy
import math import math
import numbers import numbers
@@ -9,6 +10,10 @@ import random
from schema import Schema, And, Or, Optional from schema import Schema, And, Or, Optional
from Utils import get_fuzzy_results from Utils import get_fuzzy_results
if typing.TYPE_CHECKING:
from BaseClasses import PlandoOptions
from worlds.AutoWorld import World
class AssembleOptions(abc.ABCMeta): class AssembleOptions(abc.ABCMeta):
def __new__(mcs, name, bases, attrs): def __new__(mcs, name, bases, attrs):
@@ -79,9 +84,6 @@ class AssembleOptions(abc.ABCMeta):
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs) return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
@abc.abstractclassmethod
def from_any(cls, value: typing.Any) -> "Option[typing.Any]": ...
T = typing.TypeVar('T') T = typing.TypeVar('T')
@@ -98,11 +100,11 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
supports_weighting = True supports_weighting = True
# filled by AssembleOptions: # filled by AssembleOptions:
name_lookup: typing.Dict[int, str] name_lookup: typing.Dict[T, str]
options: typing.Dict[str, int] options: typing.Dict[str, int]
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.get_current_option_name()})" return f"{self.__class__.__name__}({self.current_option_name})"
def __hash__(self) -> int: def __hash__(self) -> int:
return hash(self.value) return hash(self.value)
@@ -112,7 +114,14 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
return self.name_lookup[self.value] return self.name_lookup[self.value]
def get_current_option_name(self) -> str: def get_current_option_name(self) -> str:
"""For display purposes.""" """Deprecated. use current_option_name instead. TODO remove around 0.4"""
logging.warning(DeprecationWarning(f"get_current_option_name for {self.__class__.__name__} is deprecated."
f" use current_option_name instead. Worlds should use {self}.current_key"))
return self.current_option_name
@property
def current_option_name(self) -> str:
"""For display purposes. Worlds should be using current_key."""
return self.get_option_name(self.value) return self.get_option_name(self.value)
@classmethod @classmethod
@@ -129,21 +138,19 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
return bool(self.value) return bool(self.value)
@classmethod @classmethod
@abc.abstractmethod
def from_any(cls, data: typing.Any) -> Option[T]: def from_any(cls, data: typing.Any) -> Option[T]:
raise NotImplementedError ...
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from Generate import PlandoOptions def verify(self, world: typing.Type[World], player_name: str, plando_options: PlandoOptions) -> None:
from worlds.AutoWorld import World
def verify(self, world: World, player_name: str, plando_options: PlandoOptions) -> None:
pass pass
else: else:
def verify(self, *args, **kwargs) -> None: def verify(self, *args, **kwargs) -> None:
pass pass
class FreeText(Option): class FreeText(Option[str]):
"""Text option that allows users to enter strings. """Text option that allows users to enter strings.
Needs to be validated by the world or option definition.""" Needs to be validated by the world or option definition."""
@@ -164,11 +171,11 @@ class FreeText(Option):
return cls.from_text(str(data)) return cls.from_text(str(data))
@classmethod @classmethod
def get_option_name(cls, value: T) -> str: def get_option_name(cls, value: str) -> str:
return value return value
class NumericOption(Option[int], numbers.Integral): class NumericOption(Option[int], numbers.Integral, abc.ABC):
default = 0 default = 0
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards # note: some of the `typing.Any`` here is a result of unresolved issue in python standards
# `int` is not a `numbers.Integral` according to the official typestubs # `int` is not a `numbers.Integral` according to the official typestubs
@@ -426,6 +433,7 @@ class Choice(NumericOption):
class TextChoice(Choice): class TextChoice(Choice):
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string""" """Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
value: typing.Union[str, int]
def __init__(self, value: typing.Union[str, int]): def __init__(self, value: typing.Union[str, int]):
assert isinstance(value, str) or isinstance(value, int), \ assert isinstance(value, str) or isinstance(value, int), \
@@ -436,8 +444,7 @@ class TextChoice(Choice):
def current_key(self) -> str: def current_key(self) -> str:
if isinstance(self.value, str): if isinstance(self.value, str):
return self.value return self.value
else: return super().current_key
return self.name_lookup[self.value]
@classmethod @classmethod
def from_text(cls, text: str) -> TextChoice: def from_text(cls, text: str) -> TextChoice:
@@ -452,7 +459,7 @@ class TextChoice(Choice):
def get_option_name(cls, value: T) -> str: def get_option_name(cls, value: T) -> str:
if isinstance(value, str): if isinstance(value, str):
return value return value
return cls.name_lookup[value] return super().get_option_name(value)
def __eq__(self, other: typing.Any): def __eq__(self, other: typing.Any):
if isinstance(other, self.__class__): if isinstance(other, self.__class__):
@@ -575,12 +582,11 @@ class PlandoBosses(TextChoice, metaclass=BossMeta):
def valid_location_name(cls, value: str) -> bool: def valid_location_name(cls, value: str) -> bool:
return value in cls.locations return value in cls.locations
def verify(self, world, player_name: str, plando_options) -> None: def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
if isinstance(self.value, int): if isinstance(self.value, int):
return return
from Generate import PlandoOptions from BaseClasses import PlandoOptions
if not(PlandoOptions.bosses & plando_options): if not(PlandoOptions.bosses & plando_options):
import logging
# plando is disabled but plando options were given so pull the option and change it to an int # plando is disabled but plando options were given so pull the option and change it to an int
option = self.value.split(";")[-1] option = self.value.split(";")[-1]
self.value = self.options[option] self.value = self.options[option]
@@ -718,7 +724,7 @@ class VerifyKeys:
value: typing.Any value: typing.Any
@classmethod @classmethod
def verify_keys(cls, data): def verify_keys(cls, data: typing.List[str]):
if cls.valid_keys: if cls.valid_keys:
data = set(data) data = set(data)
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data) dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
@@ -727,12 +733,17 @@ class VerifyKeys:
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. " raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
f"Allowed keys: {cls.valid_keys}.") f"Allowed keys: {cls.valid_keys}.")
def verify(self, world, player_name: str, plando_options) -> None: def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
if self.convert_name_groups and self.verify_item_name: if self.convert_name_groups and self.verify_item_name:
new_value = type(self.value)() # empty container of whatever value is new_value = type(self.value)() # empty container of whatever value is
for item_name in self.value: for item_name in self.value:
new_value |= world.item_name_groups.get(item_name, {item_name}) new_value |= world.item_name_groups.get(item_name, {item_name})
self.value = new_value self.value = new_value
elif self.convert_name_groups and self.verify_location_name:
new_value = type(self.value)()
for loc_name in self.value:
new_value |= world.location_name_groups.get(loc_name, {loc_name})
self.value = new_value
if self.verify_item_name: if self.verify_item_name:
for item_name in self.value: for item_name in self.value:
if item_name not in world.item_names: if item_name not in world.item_names:
@@ -832,7 +843,9 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
return item in self.value return item in self.value
local_objective = Toggle # local triforce pieces, local dungeon prizes etc. class ItemSet(OptionSet):
verify_item_name = True
convert_name_groups = True
class Accessibility(Choice): class Accessibility(Choice):
@@ -862,17 +875,20 @@ class ProgressionBalancing(SpecialRange):
} }
class AllowCollect(DefaultOnToggle):
"""Controls whether items are collected from the slot when a player does a !collect or not.
The impact for the collecting player is that the collector might not get all of their items, until
the player(s) that has disallowed collection actually completes or releases their location checks."""
display_name = "Allow Collect"
common_options = { common_options = {
"progression_balancing": ProgressionBalancing, "progression_balancing": ProgressionBalancing,
"accessibility": Accessibility "accessibility": Accessibility,
"allow_collect": AllowCollect
} }
class ItemSet(OptionSet):
verify_item_name = True
convert_name_groups = True
class LocalItems(ItemSet): class LocalItems(ItemSet):
"""Forces these items to be in their native world.""" """Forces these items to be in their native world."""
display_name = "Local Items" display_name = "Local Items"
@@ -894,22 +910,23 @@ class StartHints(ItemSet):
display_name = "Start Hints" display_name = "Start Hints"
class StartLocationHints(OptionSet): class LocationSet(OptionSet):
verify_location_name = True
class StartLocationHints(LocationSet):
"""Start with these locations and their item prefilled into the !hint command""" """Start with these locations and their item prefilled into the !hint command"""
display_name = "Start Location Hints" display_name = "Start Location Hints"
verify_location_name = True
class ExcludeLocations(OptionSet): class ExcludeLocations(LocationSet):
"""Prevent these locations from having an important item""" """Prevent these locations from having an important item"""
display_name = "Excluded Locations" display_name = "Excluded Locations"
verify_location_name = True
class PriorityLocations(OptionSet): class PriorityLocations(LocationSet):
"""Prevent these locations from having an unimportant item""" """Prevent these locations from having an unimportant item"""
display_name = "Priority Locations" display_name = "Priority Locations"
verify_location_name = True
class DeathLink(Toggle): class DeathLink(Toggle):
@@ -950,7 +967,7 @@ class ItemLinks(OptionList):
pool |= {item_name} pool |= {item_name}
return pool return pool
def verify(self, world, player_name: str, plando_options) -> None: def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
link: dict link: dict
super(ItemLinks, self).verify(world, player_name, plando_options) super(ItemLinks, self).verify(world, player_name, plando_options)
existing_links = set() existing_links = set()

View File

@@ -17,7 +17,7 @@ from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandP
from worlds.pokemon_rb.locations import location_data from worlds.pokemon_rb.locations import location_data
from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}} location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}}
location_bytes_bits = {} location_bytes_bits = {}
for location in location_data: for location in location_data:
if location.ram_address is not None: if location.ram_address is not None:
@@ -40,7 +40,7 @@ CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
DISPLAY_MSGS = True DISPLAY_MSGS = True
SCRIPT_VERSION = 1 SCRIPT_VERSION = 3
class GBCommandProcessor(ClientCommandProcessor): class GBCommandProcessor(ClientCommandProcessor):
@@ -70,6 +70,8 @@ class GBContext(CommonContext):
self.set_deathlink = False self.set_deathlink = False
self.client_compatibility_mode = 0 self.client_compatibility_mode = 0
self.items_handling = 0b001 self.items_handling = 0b001
self.sent_release = False
self.sent_collect = False
async def server_auth(self, password_requested: bool = False): async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password: if password_requested and not self.password:
@@ -124,7 +126,8 @@ def get_payload(ctx: GBContext):
"items": [item.item for item in ctx.items_received], "items": [item.item for item in ctx.items_received],
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items() "messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
if key[0] > current_time - 10}, if key[0] > current_time - 10},
"deathlink": ctx.deathlink_pending "deathlink": ctx.deathlink_pending,
"options": ((ctx.permissions['release'] in ('goal', 'enabled')) * 2) + (ctx.permissions['collect'] in ('goal', 'enabled'))
} }
) )
ctx.deathlink_pending = False ctx.deathlink_pending = False
@@ -134,10 +137,13 @@ def get_payload(ctx: GBContext):
async def parse_locations(data: List, ctx: GBContext): async def parse_locations(data: List, ctx: GBContext):
locations = [] locations = []
flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20], flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20],
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], "Rod": data[0x140 + 0x20 + 0x0E:]} "Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E],
"Rod": data[0x140 + 0x20 + 0x0E:0x140 + 0x20 + 0x0E + 0x01]}
if len(flags['Rod']) > 1: if len(data) > 0x140 + 0x20 + 0x0E + 0x01:
return flags["DexSanityFlag"] = data[0x140 + 0x20 + 0x0E + 0x01:]
else:
flags["DexSanityFlag"] = [0] * 19
for flag_type, loc_map in location_map.items(): for flag_type, loc_map in location_map.items():
for flag, loc_id in loc_map.items(): for flag, loc_id in loc_map.items():
@@ -207,6 +213,16 @@ async def gb_sync_task(ctx: GBContext):
async_start(parse_locations(data_decoded['locations'], ctx)) async_start(parse_locations(data_decoded['locations'], ctx))
if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags: if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags:
await ctx.send_death(ctx.auth + " is out of usable Pokémon! " + ctx.auth + " blacked out!") await ctx.send_death(ctx.auth + " is out of usable Pokémon! " + ctx.auth + " blacked out!")
if 'options' in data_decoded:
msgs = []
if data_decoded['options'] & 4 and not ctx.sent_release:
ctx.sent_release = True
msgs.append({"cmd": "Say", "text": "!release"})
if data_decoded['options'] & 8 and not ctx.sent_collect:
ctx.sent_collect = True
msgs.append({"cmd": "Say", "text": "!collect"})
if msgs:
await ctx.send_msgs(msgs)
if ctx.set_deathlink: if ctx.set_deathlink:
await ctx.update_death_link(True) await ctx.update_death_link(True)
except asyncio.TimeoutError: except asyncio.TimeoutError:

View File

@@ -34,6 +34,11 @@ Currently, the following games are supported:
* Overcooked! 2 * Overcooked! 2
* Zillion * Zillion
* Lufia II Ancient Cave * Lufia II Ancient Cave
* Blasphemous
* Wargroove
* Stardew Valley
* The Legend of Zelda
* The Messenger
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -56,7 +56,9 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
"""Connect to a snes. Optionally include network address of a snes to connect to, """Connect to a snes. Optionally include network address of a snes to connect to,
otherwise show available devices; and a SNES device number if more than one SNES is detected. otherwise show available devices; and a SNES device number if more than one SNES is detected.
Examples: "/snes", "/snes 1", "/snes localhost:23074 1" """ Examples: "/snes", "/snes 1", "/snes localhost:23074 1" """
if self.ctx.snes_state in {SNESState.SNES_ATTACHED, SNESState.SNES_CONNECTED, SNESState.SNES_CONNECTING}:
self.output("Already connected to SNES. Disconnecting first.")
self._cmd_snes_close()
return self.connect_to_snes(snes_options) return self.connect_to_snes(snes_options)
def connect_to_snes(self, snes_options: str = "") -> bool: def connect_to_snes(self, snes_options: str = "") -> bool:
@@ -84,7 +86,7 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
"""Close connection to a currently connected snes""" """Close connection to a currently connected snes"""
self.ctx.snes_reconnect_address = None self.ctx.snes_reconnect_address = None
self.ctx.cancel_snes_autoreconnect() self.ctx.cancel_snes_autoreconnect()
if self.ctx.snes_socket is not None and not self.ctx.snes_socket.closed: if self.ctx.snes_socket and not self.ctx.snes_socket.closed:
async_start(self.ctx.snes_socket.close()) async_start(self.ctx.snes_socket.close())
return True return True
else: else:
@@ -442,7 +444,8 @@ async def snes_connect(ctx: SNIContext, address: str, deviceIndex: int = -1) ->
recv_task = asyncio.create_task(snes_recv_loop(ctx)) recv_task = asyncio.create_task(snes_recv_loop(ctx))
except Exception as e: except Exception as e:
if recv_task is not None: ctx.snes_state = SNESState.SNES_DISCONNECTED
if task_alive(recv_task):
if not ctx.snes_socket.closed: if not ctx.snes_socket.closed:
await ctx.snes_socket.close() await ctx.snes_socket.close()
else: else:
@@ -450,15 +453,9 @@ async def snes_connect(ctx: SNIContext, address: str, deviceIndex: int = -1) ->
if not ctx.snes_socket.closed: if not ctx.snes_socket.closed:
await ctx.snes_socket.close() await ctx.snes_socket.close()
ctx.snes_socket = None ctx.snes_socket = None
ctx.snes_state = SNESState.SNES_DISCONNECTED snes_logger.error(f"Error connecting to snes ({e}), retrying in {_global_snes_reconnect_delay} seconds")
if not ctx.snes_reconnect_address: ctx.snes_autoreconnect_task = asyncio.create_task(snes_autoreconnect(ctx), name="snes auto-reconnect")
snes_logger.error("Error connecting to snes (%s)" % e)
else:
snes_logger.error(f"Error connecting to snes, retrying in {_global_snes_reconnect_delay} seconds")
assert ctx.snes_autoreconnect_task is None
ctx.snes_autoreconnect_task = asyncio.create_task(snes_autoreconnect(ctx), name="snes auto-reconnect")
_global_snes_reconnect_delay *= 2 _global_snes_reconnect_delay *= 2
else: else:
_global_snes_reconnect_delay = ctx.starting_reconnect_delay _global_snes_reconnect_delay = ctx.starting_reconnect_delay
snes_logger.info(f"Attached to {device}") snes_logger.info(f"Attached to {device}")
@@ -471,10 +468,17 @@ async def snes_disconnect(ctx: SNIContext) -> None:
ctx.snes_socket = None ctx.snes_socket = None
def task_alive(task: typing.Optional[asyncio.Task]) -> bool:
if task:
return not task.done()
return False
async def snes_autoreconnect(ctx: SNIContext) -> None: async def snes_autoreconnect(ctx: SNIContext) -> None:
await asyncio.sleep(_global_snes_reconnect_delay) await asyncio.sleep(_global_snes_reconnect_delay)
if ctx.snes_reconnect_address and not ctx.snes_socket and not ctx.snes_connect_task: if not ctx.snes_socket and not task_alive(ctx.snes_connect_task):
ctx.snes_connect_task = asyncio.create_task(snes_connect(ctx, ctx.snes_reconnect_address), name="SNES Connect") address = ctx.snes_reconnect_address if ctx.snes_reconnect_address else ctx.snes_address
ctx.snes_connect_task = asyncio.create_task(snes_connect(ctx, address), name="SNES Connect")
async def snes_recv_loop(ctx: SNIContext) -> None: async def snes_recv_loop(ctx: SNIContext) -> None:

View File

@@ -52,9 +52,9 @@ class StarcraftClientProcessor(ClientCommandProcessor):
"""Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal""" """Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal"""
options = difficulty.split() options = difficulty.split()
num_options = len(options) num_options = len(options)
difficulty_choice = options[0].lower()
if num_options > 0: if num_options > 0:
difficulty_choice = options[0].lower()
if difficulty_choice == "casual": if difficulty_choice == "casual":
self.ctx.difficulty_override = 0 self.ctx.difficulty_override = 0
elif difficulty_choice == "normal": elif difficulty_choice == "normal":
@@ -71,7 +71,11 @@ class StarcraftClientProcessor(ClientCommandProcessor):
return True return True
else: else:
self.output("Difficulty needs to be specified in the command.") if self.ctx.difficulty == -1:
self.output("Please connect to a seed before checking difficulty.")
else:
self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][self.ctx.difficulty])
self.output("To change the difficulty, add the name of the difficulty after the command.")
return False return False
def _cmd_disable_mission_check(self) -> bool: def _cmd_disable_mission_check(self) -> bool:

View File

@@ -12,7 +12,7 @@ import io
import collections import collections
import importlib import importlib
import logging import logging
from typing import BinaryIO, ClassVar, Coroutine, Optional, Set from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
from yaml import load, load_all, dump, SafeLoader from yaml import load, load_all, dump, SafeLoader
@@ -38,7 +38,7 @@ class Version(typing.NamedTuple):
build: int build: int
__version__ = "0.3.8" __version__ = "0.3.9"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux") is_linux = sys.platform.startswith("linux")
@@ -195,11 +195,11 @@ def get_public_ipv4() -> str:
ip = socket.gethostbyname(socket.gethostname()) ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context() ctx = get_cert_none_ssl_context()
try: try:
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx).read().decode("utf8").strip() ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip()
except Exception as e: except Exception as e:
# noinspection PyBroadException # noinspection PyBroadException
try: try:
ip = urllib.request.urlopen("https://v4.ident.me", context=ctx).read().decode("utf8").strip() ip = urllib.request.urlopen("https://v4.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
except Exception: except Exception:
logging.exception(e) logging.exception(e)
pass # we could be offline, in a local game, so no point in erroring out pass # we could be offline, in a local game, so no point in erroring out
@@ -213,7 +213,7 @@ def get_public_ipv6() -> str:
ip = socket.gethostbyname(socket.gethostname()) ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context() ctx = get_cert_none_ssl_context()
try: try:
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx).read().decode("utf8").strip() ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
pass # we could be offline, in a local game, or ipv6 may not be available pass # we could be offline, in a local game, or ipv6 may not be available
@@ -310,6 +310,14 @@ def get_default_options() -> OptionsType:
"lufia2ac_options": { "lufia2ac_options": {
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc", "rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
}, },
"tloz_options": {
"rom_file": "Legend of Zelda, The (U) (PRG0) [!].nes",
"rom_start": True,
"display_msgs": True,
},
"wargroove_options": {
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
}
} }
return options return options
@@ -662,7 +670,10 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))): def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))):
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning.""" """Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
def sorter(element: str) -> str: def sorter(element: Union[str, Dict[str, Any]]) -> str:
if (not isinstance(element, str)):
element = element["title"]
parts = element.split(maxsplit=1) parts = element.split(maxsplit=1)
if parts[0].lower() in ignore: if parts[0].lower() in ignore:
return parts[1].lower() return parts[1].lower()

445
WargrooveClient.py Normal file
View File

@@ -0,0 +1,445 @@
from __future__ import annotations
import atexit
import os
import sys
import asyncio
import random
import shutil
from typing import Tuple, List, Iterable, Dict
from worlds.wargroove import WargrooveWorld
from worlds.wargroove.Items import item_table, faction_table, CommanderData, ItemData
import ModuleUpdate
ModuleUpdate.update()
import Utils
import json
import logging
if __name__ == "__main__":
Utils.init_logging("WargrooveClient", exception_logger="Client")
from NetUtils import NetworkItem, ClientStatus
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
CommonContext, server_loop
wg_logger = logging.getLogger("WG")
class WargrooveClientCommandProcessor(ClientCommandProcessor):
def _cmd_resync(self):
"""Manually trigger a resync."""
self.output(f"Syncing items.")
self.ctx.syncing = True
def _cmd_commander(self, *commander_name: Iterable[str]):
"""Set the current commander to the given commander."""
if commander_name:
self.ctx.set_commander(' '.join(commander_name))
else:
if self.ctx.can_choose_commander:
commanders = self.ctx.get_commanders()
wg_logger.info('Unlocked commanders: ' +
', '.join((commander.name for commander, unlocked in commanders if unlocked)))
wg_logger.info('Locked commanders: ' +
', '.join((commander.name for commander, unlocked in commanders if not unlocked)))
else:
wg_logger.error('Cannot set commanders in this game mode.')
class WargrooveContext(CommonContext):
command_processor: int = WargrooveClientCommandProcessor
game = "Wargroove"
items_handling = 0b111 # full remote
current_commander: CommanderData = faction_table["Starter"][0]
can_choose_commander: bool = False
commander_defense_boost_multiplier: int = 0
income_boost_multiplier: int = 0
starting_groove_multiplier: float
faction_item_ids = {
'Starter': 0,
'Cherrystone': 52025,
'Felheim': 52026,
'Floran': 52027,
'Heavensong': 52028,
'Requiem': 52029,
'Outlaw': 52030
}
buff_item_ids = {
'Income Boost': 52023,
'Commander Defense Boost': 52024,
}
def __init__(self, server_address, password):
super(WargrooveContext, self).__init__(server_address, password)
self.send_index: int = 0
self.syncing = False
self.awaiting_bridge = False
# self.game_communication_path: files go in this path to pass data between us and the actual game
if "appdata" in os.environ:
options = Utils.get_options()
root_directory = os.path.join(options["wargroove_options"]["root_directory"])
data_directory = os.path.join("lib", "worlds", "wargroove", "data")
dev_data_directory = os.path.join("worlds", "wargroove", "data")
appdata_wargroove = os.path.expandvars(os.path.join("%APPDATA%", "Chucklefish", "Wargroove"))
if not os.path.isfile(os.path.join(root_directory, "win64_bin", "wargroove64.exe")):
print_error_and_close("WargrooveClient couldn't find wargroove64.exe. "
"Unable to infer required game_communication_path")
self.game_communication_path = os.path.join(root_directory, "AP")
if not os.path.exists(self.game_communication_path):
os.makedirs(self.game_communication_path)
self.remove_communication_files()
atexit.register(self.remove_communication_files)
if not os.path.isdir(appdata_wargroove):
print_error_and_close("WargrooveClient couldn't find Wargoove in appdata!"
"Boot Wargroove and then close it to attempt to fix this error")
if not os.path.isdir(data_directory):
data_directory = dev_data_directory
if not os.path.isdir(data_directory):
print_error_and_close("WargrooveClient couldn't find Wargoove mod and save files in install!")
shutil.copytree(data_directory, appdata_wargroove, dirs_exist_ok=True)
else:
print_error_and_close("WargrooveClient couldn't detect system type. "
"Unable to infer required game_communication_path")
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(WargrooveContext, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
async def connection_closed(self):
await super(WargrooveContext, self).connection_closed()
self.remove_communication_files()
@property
def endpoints(self):
if self.server:
return [self.server]
else:
return []
async def shutdown(self):
await super(WargrooveContext, self).shutdown()
self.remove_communication_files()
def remove_communication_files(self):
for root, dirs, files in os.walk(self.game_communication_path):
for file in files:
os.remove(root + "/" + file)
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected"}:
filename = f"AP_settings.json"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
slot_data = args["slot_data"]
json.dump(args["slot_data"], f)
self.can_choose_commander = slot_data["can_choose_commander"]
print('can choose commander:', self.can_choose_commander)
self.starting_groove_multiplier = slot_data["starting_groove_multiplier"]
self.income_boost_multiplier = slot_data["income_boost"]
self.commander_defense_boost_multiplier = slot_data["commander_defense_boost"]
f.close()
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
self.update_commander_data()
self.ui.update_tracker()
random.seed(self.seed_name + str(self.slot))
# Our indexes start at 1 and we have 24 levels
for i in range(1, 25):
filename = f"seed{i}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.write(str(random.randint(0, 4294967295)))
f.close()
if cmd in {"RoomInfo"}:
self.seed_name = args["seed_name"]
if cmd in {"ReceivedItems"}:
received_ids = [item.item for item in self.items_received]
for network_item in self.items_received:
filename = f"AP_{str(network_item.item)}.item"
path = os.path.join(self.game_communication_path, filename)
# Newly-obtained items
if not os.path.isfile(path):
open(path, 'w').close()
# Announcing commander unlocks
item_name = self.item_names[network_item.item]
if item_name in faction_table.keys():
for commander in faction_table[item_name]:
logger.info(f"{commander.name} has been unlocked!")
with open(path, 'w') as f:
item_count = received_ids.count(network_item.item)
if self.buff_item_ids["Income Boost"] == network_item.item:
f.write(f"{item_count * self.income_boost_multiplier}")
elif self.buff_item_ids["Commander Defense Boost"] == network_item.item:
f.write(f"{item_count * self.commander_defense_boost_multiplier}")
else:
f.write(f"{item_count}")
f.close()
print_filename = f"AP_{str(network_item.item)}.item.print"
print_path = os.path.join(self.game_communication_path, print_filename)
if not os.path.isfile(print_path):
open(print_path, 'w').close()
with open(print_path, 'w') as f:
f.write("Received " +
self.item_names[network_item.item] +
" from " +
self.player_names[network_item.player])
f.close()
self.update_commander_data()
self.ui.update_tracker()
if cmd in {"RoomUpdate"}:
if "checked_locations" in args:
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
def run_gui(self):
"""Import kivy UI system and start running it as self.ui_task."""
from kvui import GameManager, HoverBehavior, ServerToolTip
from kivy.uix.tabbedpanel import TabbedPanelItem
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.image import AsyncImage, Image
from kivy.uix.stacklayout import StackLayout
from kivy.uix.label import Label
from kivy.properties import ColorProperty
from kivy.uix.image import Image
import pkgutil
class TrackerLayout(BoxLayout):
pass
class CommanderSelect(BoxLayout):
pass
class CommanderButton(ToggleButton):
pass
class FactionBox(BoxLayout):
pass
class CommanderGroup(BoxLayout):
pass
class ItemTracker(BoxLayout):
pass
class ItemLabel(Label):
pass
class WargrooveManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("WG", "WG Console"),
]
base_title = "Archipelago Wargroove Client"
ctx: WargrooveContext
unit_tracker: ItemTracker
trigger_tracker: BoxLayout
boost_tracker: BoxLayout
commander_buttons: Dict[int, List[CommanderButton]]
tracker_items = {
"Swordsman": ItemData(None, "Unit", False),
"Dog": ItemData(None, "Unit", False),
**item_table
}
def build(self):
container = super().build()
panel = TabbedPanelItem(text="Wargroove")
panel.content = self.build_tracker()
self.tabs.add_widget(panel)
return container
def build_tracker(self) -> TrackerLayout:
try:
tracker = TrackerLayout(orientation="horizontal")
commander_select = CommanderSelect(orientation="vertical")
self.commander_buttons = {}
for faction, commanders in faction_table.items():
faction_box = FactionBox(size_hint=(None, None), width=100 * len(commanders), height=70)
commander_group = CommanderGroup()
commander_buttons = []
for commander in commanders:
commander_button = CommanderButton(text=commander.name, group="commanders")
if faction == "Starter":
commander_button.disabled = False
commander_button.bind(on_press=lambda instance: self.ctx.set_commander(instance.text))
commander_buttons.append(commander_button)
commander_group.add_widget(commander_button)
self.commander_buttons[faction] = commander_buttons
faction_box.add_widget(Label(text=faction, size_hint_x=None, pos_hint={'left': 1}, size_hint_y=None, height=10))
faction_box.add_widget(commander_group)
commander_select.add_widget(faction_box)
item_tracker = ItemTracker(padding=[0,20])
self.unit_tracker = BoxLayout(orientation="vertical")
other_tracker = BoxLayout(orientation="vertical")
self.trigger_tracker = BoxLayout(orientation="vertical")
self.boost_tracker = BoxLayout(orientation="vertical")
other_tracker.add_widget(self.trigger_tracker)
other_tracker.add_widget(self.boost_tracker)
item_tracker.add_widget(self.unit_tracker)
item_tracker.add_widget(other_tracker)
tracker.add_widget(commander_select)
tracker.add_widget(item_tracker)
self.update_tracker()
return tracker
except Exception as e:
print(e)
def update_tracker(self):
received_ids = [item.item for item in self.ctx.items_received]
for faction, item_id in self.ctx.faction_item_ids.items():
for commander_button in self.commander_buttons[faction]:
commander_button.disabled = not (faction == "Starter" or item_id in received_ids)
self.unit_tracker.clear_widgets()
self.trigger_tracker.clear_widgets()
for name, item in self.tracker_items.items():
if item.type in ("Unit", "Trigger"):
status_color = (1, 1, 1, 1) if item.code is None or item.code in received_ids else (0.6, 0.2, 0.2, 1)
label = ItemLabel(text=name, color=status_color)
if item.type == "Unit":
self.unit_tracker.add_widget(label)
else:
self.trigger_tracker.add_widget(label)
self.boost_tracker.clear_widgets()
extra_income = received_ids.count(52023) * self.ctx.income_boost_multiplier
extra_defense = received_ids.count(52024) * self.ctx.commander_defense_boost_multiplier
income_boost = ItemLabel(text="Extra Income: " + str(extra_income))
defense_boost = ItemLabel(text="Comm Defense: " + str(100 + extra_defense))
self.boost_tracker.add_widget(income_boost)
self.boost_tracker.add_widget(defense_boost)
self.ui = WargrooveManager(self)
data = pkgutil.get_data(WargrooveWorld.__module__, "Wargroove.kv").decode()
Builder.load_string(data)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def update_commander_data(self):
if self.can_choose_commander:
faction_items = 0
faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()]
for network_item in self.items_received:
if self.item_names[network_item.item] in faction_item_names:
faction_items += 1
starting_groove = (faction_items - 1) * self.starting_groove_multiplier
# Must be an integer larger than 0
starting_groove = int(max(starting_groove, 0))
data = {
"commander": self.current_commander.internal_name,
"starting_groove": starting_groove
}
else:
data = {
"commander": "seed",
"starting_groove": 0
}
filename = 'commander.json'
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
json.dump(data, f)
if self.ui:
self.ui.update_tracker()
def set_commander(self, commander_name: str) -> bool:
"""Sets the current commander to the given one, if possible"""
if not self.can_choose_commander:
wg_logger.error("Cannot set commanders in this game mode.")
return
match_name = commander_name.lower()
for commander, unlocked in self.get_commanders():
if commander.name.lower() == match_name or commander.alt_name and commander.alt_name.lower() == match_name:
if unlocked:
self.current_commander = commander
self.syncing = True
wg_logger.info(f"Commander set to {commander.name}.")
self.update_commander_data()
return True
else:
wg_logger.error(f"Commander {commander.name} has not been unlocked.")
return False
else:
wg_logger.error(f"{commander_name} is not a recognized Wargroove commander.")
def get_commanders(self) -> List[Tuple[CommanderData, bool]]:
"""Gets a list of commanders with their unlocked status"""
commanders = []
received_ids = [item.item for item in self.items_received]
for faction in faction_table.keys():
unlocked = faction == 'Starter' or self.faction_item_ids[faction] in received_ids
commanders += [(commander, unlocked) for commander in faction_table[faction]]
return commanders
async def game_watcher(ctx: WargrooveContext):
from worlds.wargroove.Locations import location_table
while not ctx.exit_event.is_set():
if ctx.syncing == True:
sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
sending = []
victory = False
for root, dirs, files in os.walk(ctx.game_communication_path):
for file in files:
if file.find("send") > -1:
st = file.split("send", -1)[1]
sending = sending+[(int(st))]
if file.find("victory") > -1:
victory = True
ctx.locations_checked = sending
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
await asyncio.sleep(0.1)
def print_error_and_close(msg):
logger.error("Error: " + msg)
Utils.messagebox("Error", msg, error=True)
sys.exit(1)
if __name__ == '__main__':
async def main(args):
ctx = WargrooveContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="WargrooveProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await ctx.shutdown()
import colorama
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -33,6 +33,11 @@ def get_app():
import yaml import yaml
app.config.from_file(configpath, yaml.safe_load) app.config.from_file(configpath, yaml.safe_load)
logging.info(f"Updated config from {configpath}") logging.info(f"Updated config from {configpath}")
if not app.config["HOST_ADDRESS"]:
logging.info("Getting public IP, as HOST_ADDRESS is empty.")
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
db.bind(**app.config["PONY"]) db.bind(**app.config["PONY"])
db.generate_mapping(create_tables=True) db.generate_mapping(create_tables=True)
return app return app

View File

@@ -51,7 +51,7 @@ app.config["PONY"] = {
app.config["MAX_ROLL"] = 20 app.config["MAX_ROLL"] = 20
app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache" app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache"
app.config["JSON_AS_ASCII"] = False app.config["JSON_AS_ASCII"] = False
app.config["PATCH_TARGET"] = "archipelago.gg" app.config["HOST_ADDRESS"] = ""
cache = Cache(app) cache = Cache(app)
Compress(app) Compress(app)

View File

@@ -179,6 +179,7 @@ class MultiworldInstance():
self.ponyconfig = config["PONY"] self.ponyconfig = config["PONY"]
self.cert = config["SELFLAUNCHCERT"] self.cert = config["SELFLAUNCHCERT"]
self.key = config["SELFLAUNCHKEY"] self.key = config["SELFLAUNCHKEY"]
self.host = config["HOST_ADDRESS"]
def start(self): def start(self):
if self.process and self.process.is_alive(): if self.process and self.process.is_alive():
@@ -187,7 +188,7 @@ class MultiworldInstance():
logging.info(f"Spinning up {self.room_id}") logging.info(f"Spinning up {self.room_id}")
process = multiprocessing.Process(group=None, target=run_server_process, process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.room_id, self.ponyconfig, get_static_server_data(), args=(self.room_id, self.ponyconfig, get_static_server_data(),
self.cert, self.key), self.cert, self.key, self.host),
name="MultiHost") name="MultiHost")
process.start() process.start()
# bind after start to prevent thread sync issues with guardian. # bind after start to prevent thread sync issues with guardian.

View File

@@ -131,6 +131,8 @@ def get_static_server_data() -> dict:
"gamespackage": worlds.network_data_package["games"], "gamespackage": worlds.network_data_package["games"],
"item_name_groups": {world_name: world.item_name_groups for world_name, world in "item_name_groups": {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()}, worlds.AutoWorldRegister.world_types.items()},
"location_name_groups": {world_name: world.location_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()},
} }
for world_name, world in worlds.AutoWorldRegister.world_types.items(): for world_name, world in worlds.AutoWorldRegister.world_types.items():
@@ -140,7 +142,8 @@ def get_static_server_data() -> dict:
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str]): cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
host: str):
# establish DB connection for multidata and multisave # establish DB connection for multidata and multisave
db.bind(**ponyconfig) db.bind(**ponyconfig)
db.generate_mapping(check_tables=False) db.generate_mapping(check_tables=False)
@@ -165,17 +168,18 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
for wssocket in ctx.server.ws_server.sockets: for wssocket in ctx.server.ws_server.sockets:
socketname = wssocket.getsockname() socketname = wssocket.getsockname()
if wssocket.family == socket.AF_INET6: if wssocket.family == socket.AF_INET6:
logging.info(f'Hosting game at [{get_public_ipv6()}]:{socketname[1]}')
# Prefer IPv4, as most users seem to not have working ipv6 support # Prefer IPv4, as most users seem to not have working ipv6 support
if not port: if not port:
port = socketname[1] port = socketname[1]
elif wssocket.family == socket.AF_INET: elif wssocket.family == socket.AF_INET:
logging.info(f'Hosting game at {get_public_ipv4()}:{socketname[1]}')
port = socketname[1] port = socketname[1]
if port: if port:
logging.info(f'Hosting game at {host}:{port}')
with db_session: with db_session:
room = Room.get(id=ctx.room_id) room = Room.get(id=ctx.room_id)
room.last_port = port room.last_port = port
else:
logging.exception("Could not determine port. Likely hosting failure.")
with db_session: with db_session:
ctx.auto_shutdown = Room.get(id=room_id).timeout ctx.auto_shutdown = Room.get(id=room_id).timeout
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [])) ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))

View File

@@ -26,7 +26,7 @@ def download_patch(room_id, patch_id):
with zipfile.ZipFile(filelike, "a") as zf: with zipfile.ZipFile(filelike, "a") as zf:
with zf.open("archipelago.json", "r") as f: with zf.open("archipelago.json", "r") as f:
manifest = json.load(f) manifest = json.load(f)
manifest["server"] = f"{app.config['PATCH_TARGET']}:{last_port}" if last_port else None manifest["server"] = f"{app.config['HOST_ADDRESS']}:{last_port}" if last_port else None
with zipfile.ZipFile(new_file, "w") as new_zip: with zipfile.ZipFile(new_file, "w") as new_zip:
for file in zf.infolist(): for file in zf.infolist():
if file.filename == "archipelago.json": if file.filename == "archipelago.json":
@@ -64,7 +64,7 @@ def download_slot_file(room_id, player_id: int):
if slot_data.game == "Minecraft": if slot_data.game == "Minecraft":
from worlds.minecraft import mc_update_output from worlds.minecraft import mc_update_output
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc" fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port) data = mc_update_output(slot_data.data, server=app.config['HOST_ADDRESS'], port=room.last_port)
return send_file(io.BytesIO(data), as_attachment=True, download_name=fname) return send_file(io.BytesIO(data), as_attachment=True, download_name=fname)
elif slot_data.game == "Factorio": elif slot_data.game == "Factorio":
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf: with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:

View File

@@ -1,5 +1,6 @@
import datetime import datetime
import os import os
from typing import List, Dict, Union
import jinja2.exceptions import jinja2.exceptions
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
@@ -163,8 +164,9 @@ def get_datapackage():
@app.route('/index') @app.route('/index')
@app.route('/sitemap') @app.route('/sitemap')
def get_sitemap(): def get_sitemap():
available_games = [] available_games: List[Dict[str, Union[str, bool]]] = []
for game, world in AutoWorldRegister.world_types.items(): for game, world in AutoWorldRegister.world_types.items():
if not world.hidden: if not world.hidden:
available_games.append(game) has_settings: bool = isinstance(world.web.settings_page, bool) and world.web.settings_page
available_games.append({ 'title': game, 'has_settings': has_settings })
return render_template("siteMap.html", games=available_games) return render_template("siteMap.html", games=available_games)

View File

@@ -1,7 +1,7 @@
flask>=2.2.2 flask>=2.2.3
pony>=0.7.16 pony>=0.7.16
waitress>=2.1.2 waitress>=2.1.2
Flask-Caching>=2.0.1 Flask-Caching>=2.0.2
Flask-Compress>=1.13 Flask-Compress>=1.13
Flask-Limiter>=2.8.1 Flask-Limiter>=3.3.0
bokeh>=3.0.2 bokeh>=3.1.0

View File

@@ -0,0 +1,18 @@
window.addEventListener('load', () => {
const menuButton = document.getElementById('base-header-mobile-menu-button');
const mobileMenu = document.getElementById('base-header-mobile-menu');
menuButton.addEventListener('click', (evt) => {
evt.preventDefault();
if (!mobileMenu.style.display || mobileMenu.style.display === 'none') {
return mobileMenu.style.display = 'flex';
}
mobileMenu.style.display = 'none';
});
window.addEventListener('resize', () => {
mobileMenu.style.display = 'none';
});
});

View File

@@ -0,0 +1,49 @@
window.addEventListener('load', () => {
// Reload tracker every 60 seconds
const url = window.location;
setInterval(() => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
// Create a fake DOM using the returned HTML
const domParser = new DOMParser();
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
// Update item tracker
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
// Update only counters in the location-table
let counters = document.getElementsByClassName('counter');
const fakeCounters = fakeDOM.getElementsByClassName('counter');
for (let i = 0; i < counters.length; i++) {
counters[i].innerHTML = fakeCounters[i].innerHTML;
}
};
ajax.open('GET', url);
ajax.send();
}, 60000)
// Collapsible advancement sections
const categories = document.getElementsByClassName("location-category");
for (let i = 0; i < categories.length; i++) {
let hide_id = categories[i].id.split('-')[0];
if (hide_id == 'Total') {
continue;
}
categories[i].addEventListener('click', function() {
// Toggle the advancement list
document.getElementById(hide_id).classList.toggle("hide");
// Change text of the header
const tab_header = document.getElementById(hide_id+'-header').children[0];
const orig_text = tab_header.innerHTML;
let new_text;
if (orig_text.includes("▼")) {
new_text = orig_text.replace("▼", "▲");
}
else {
new_text = orig_text.replace("▲", "▼");
}
tab_header.innerHTML = new_text;
});
}
});

View File

@@ -0,0 +1,6 @@
window.addEventListener('load', () => {
$(".table-wrapper").scrollsync({
y_sync: true,
x_sync: true
});
});

View File

@@ -1,5 +1,7 @@
const adjustTableHeight = () => { const adjustTableHeight = () => {
const tablesContainer = document.getElementById('tables-container'); const tablesContainer = document.getElementById('tables-container');
if (!tablesContainer)
return;
const upperDistance = tablesContainer.getBoundingClientRect().top; const upperDistance = tablesContainer.getBoundingClientRect().top;
const containerHeight = window.innerHeight - upperDistance; const containerHeight = window.innerHeight - upperDistance;
@@ -18,7 +20,8 @@ window.addEventListener('load', () => {
info: false, info: false,
dom: "t", dom: "t",
stateSave: true, stateSave: true,
stateSaveCallback: function(settings,data) { stateSaveCallback: function(settings, data) {
delete data.search;
localStorage.setItem(`DataTables_${settings.sInstance}_/tracker`, JSON.stringify(data)); localStorage.setItem(`DataTables_${settings.sInstance}_/tracker`, JSON.stringify(data));
}, },
stateLoadCallback: function(settings) { stateLoadCallback: function(settings) {
@@ -70,10 +73,30 @@ window.addEventListener('load', () => {
// the tbody and render two separate tables. // the tbody and render two separate tables.
}); });
document.getElementById('search').addEventListener('keyup', (event) => { const searchBox = document.getElementById("search");
tables.search(event.target.value); searchBox.value = tables.search();
console.info(tables.search()); searchBox.focus();
searchBox.select();
const doSearch = () => {
tables.search(searchBox.value);
tables.draw(); tables.draw();
};
searchBox.addEventListener("keyup", doSearch);
window.addEventListener("keydown", (event) => {
if (!event.ctrlKey && !event.altKey && event.key.length === 1 && document.activeElement !== searchBox) {
searchBox.focus();
searchBox.select();
}
if (!event.ctrlKey && !event.altKey && !event.shiftKey && event.key === "Escape") {
if (searchBox.value !== "") {
searchBox.value = "";
doSearch();
}
searchBox.blur();
if (!document.getElementById("tables-container"))
window.scroll(0, 0);
event.preventDefault();
}
}); });
const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker'); const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3; const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3;
@@ -87,7 +110,7 @@ window.addEventListener('load', () => {
const update = () => { const update = () => {
const target = $("<div></div>"); const target = $("<div></div>");
console.log("Updating Tracker..."); console.log("Updating Tracker...");
target.load("/tracker/" + tracker, function (response, status) { target.load(location.href, function (response, status) {
if (status === "success") { if (status === "success") {
target.find(".table").each(function (i, new_table) { target.find(".table").each(function (i, new_table) {
const new_trs = $(new_table).find("tbody>tr"); const new_trs = $(new_table).find("tbody>tr");
@@ -114,10 +137,5 @@ window.addEventListener('load', () => {
tables.draw(); tables.draw();
}); });
$(".table-wrapper").scrollsync({
y_sync: true,
x_sync: true
});
adjustTableHeight(); adjustTableHeight();
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1,30 @@
#player-tracker-wrapper{
margin: 0;
}
#inventory-table{
padding: 8px 10px 2px 6px;
background-color: #42b149;
border-radius: 4px;
border: 2px solid black;
}
#inventory-table tr.column-headers td {
font-size: 1rem;
padding: 0 5rem 0 0;
}
#inventory-table td{
padding: 0 0.5rem 0.5rem;
font-family: LexendDeca-Light, monospace;
font-size: 2.5rem;
color: #ffffff;
}
#inventory-table td img{
vertical-align: middle;
}
.hide {
display: none;
}

View File

@@ -42,7 +42,7 @@ html{
margin-top: 4px; margin-top: 4px;
} }
#base-header a{ #base-header a, #base-header-mobile-menu a{
color: #2f6b83; color: #2f6b83;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
@@ -51,3 +51,64 @@ html{
font-family: LondrinaSolid-Light, sans-serif; font-family: LondrinaSolid-Light, sans-serif;
text-transform: uppercase; text-transform: uppercase;
} }
#base-header-right-mobile{
display: none;
margin-top: 2rem;
margin-right: 1rem;
}
#base-header-mobile-menu{
display: none;
flex-direction: column;
background-color: #ffffff;
text-align: center;
overflow-y: auto;
z-index: 10000;
width: 100vw;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
position: absolute;
top: 7rem;
right: 0;
padding-top: 1rem;
}
#base-header-mobile-menu a{
padding: 4rem 2rem;
font-size: 5rem;
line-height: 5rem;
color: #699ca8;
border-top: 1px solid #d3d3d3;
}
#base-header-right-mobile img{
height: 3rem;
}
@media all and (max-width: 1580px){
html{
padding-top: 260px;
scroll-padding-top: 230px;
}
#base-header{
height: 200px;
background-size: auto 200px;
}
#base-header #site-title img{
height: calc(38px * 2);
margin-top: 30px;
margin-left: 20px;
}
#base-header-right{
display: none;
}
#base-header-right-mobile{
display: unset;
}
}

View File

@@ -9,19 +9,54 @@
border-top-left-radius: 4px; border-top-left-radius: 4px;
border-top-right-radius: 4px; border-top-right-radius: 4px;
padding: 3px 3px 10px; padding: 3px 3px 10px;
width: 384px; width: 374px;
background-color: #8d60a7; background-color: #8d60a7;
}
#inventory-table td{ display: grid;
width: 40px; grid-template-rows: repeat(5, 48px);
height: 40px; }
text-align: center;
vertical-align: middle; #inventory-table img{
display: block;
}
#inventory-table div.table-row{
display: grid;
grid-template-columns: repeat(5, 1fr);
}
#inventory-table div.C1{
grid-column: 1;
place-content: center;
place-items: center;
display: flex;
}
#inventory-table div.C2{
grid-column: 2;
place-content: center;
place-items: center;
display: flex;
}
#inventory-table div.C3{
grid-column: 3;
place-content: center;
place-items: center;
display: flex;
}
#inventory-table div.C4{
grid-column: 4;
place-content: center;
place-items: center;
display: flex;
}
#inventory-table div.C5{
grid-column: 5;
place-content: center;
place-items: center;
display: flex;
} }
#inventory-table img{ #inventory-table img{
height: 100%;
max-width: 40px; max-width: 40px;
max-height: 40px; max-height: 40px;
filter: grayscale(100%) contrast(75%) brightness(30%); filter: grayscale(100%) contrast(75%) brightness(30%);
@@ -31,11 +66,70 @@
filter: none; filter: none;
} }
#inventory-table div.counted-item { #inventory-table img.acquired.purple{ /*00FFFF*/
filter: hue-rotate(270deg) saturate(6) brightness(0.8);
}
#inventory-table img.acquired.cyan{ /*FF00FF*/
filter: hue-rotate(138deg) saturate(10) brightness(0.8);
}
#inventory-table img.acquired.green{ /*32CD32*/
filter: hue-rotate(84deg) saturate(10) brightness(0.7);
}
#inventory-table div.image-stack{
display: grid;
position: relative;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
#inventory-table div.image-stack div.stack-back{
grid-column: 1;
grid-row: 1;
}
#inventory-table div.image-stack div.stack-front{
grid-column: 1;
grid-row: 1;
display: grid;
grid-template-columns: 20px 20px;
grid-template-rows: 20px 20px;
}
#inventory-table div.image-stack div.stack-top-left{
grid-column: 1;
grid-row: 1;
z-index: 1;
}
#inventory-table div.image-stack div.stack-top-right{
grid-column: 2;
grid-row: 1;
z-index: 1;
}
#inventory-table div.image-stack div.stack-bottum-left{
grid-column: 1;
grid-row: 2;
z-index: 1;
}
#inventory-table div.image-stack div.stack-bottum-right{
grid-column: 2;
grid-row: 2;
z-index: 1;
}
#inventory-table div.image-stack div.stack-front img{
width: 20px;
height: 20px;
}
#inventory-table div.counted-item{
position: relative; position: relative;
} }
#inventory-table div.item-count { #inventory-table div.item-count{
position: absolute; position: absolute;
color: white; color: white;
font-family: "Minecraftia", monospace; font-family: "Minecraftia", monospace;
@@ -69,16 +163,16 @@
line-height: 20px; line-height: 20px;
} }
#location-table td.counter { #location-table td.counter{
text-align: right; text-align: right;
font-size: 14px; font-size: 14px;
} }
#location-table td.toggle-arrow { #location-table td.toggle-arrow{
text-align: right; text-align: right;
} }
#location-table tr#Total-header { #location-table tr#Total-header{
font-weight: bold; font-weight: bold;
} }
@@ -88,14 +182,14 @@
max-height: 30px; max-height: 30px;
} }
#location-table tbody.locations { #location-table tbody.locations{
font-size: 12px; font-size: 12px;
} }
#location-table td.location-name { #location-table td.location-name{
padding-left: 16px; padding-left: 16px;
} }
.hide { .hide{
display: none; display: none;
} }

View File

@@ -119,6 +119,33 @@ img.alttp-sprite {
background-color: #d3c97d; background-color: #d3c97d;
} }
#tracker-navigation {
display: inline-flex;
background-color: #b0a77d;
margin: 0.5rem;
border-radius: 4px;
}
.tracker-navigation-button {
display: block;
margin: 4px;
padding-left: 12px;
padding-right: 12px;
border-radius: 4px;
text-align: center;
font-size: 14px;
color: #000;
font-weight: lighter;
}
.tracker-navigation-button:hover {
background-color: #e2eabb !important;
}
.tracker-navigation-button.selected {
background-color: rgb(220, 226, 189);
}
@media all and (max-width: 1700px) { @media all and (max-width: 1700px) {
table.dataTable thead th.upper-row{ table.dataTable thead th.upper-row{
position: -webkit-sticky; position: -webkit-sticky;

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/checksfinderTracker.css') }}" />
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/checksfinderTracker.js') }}"></script>
</head>
<body>
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<table id="inventory-table">
<tr class="column-headers">
<td colspan="2">Checks Available:</td>
<td colspan="2">Map Bombs:</td>
</tr>
<tr>
<td><img alt="Checks Available" src="{{ icons['Checks Available'] }}" /></td>
<td>{{ checks_available }}</td>
<td><img alt="Bombs Remaining" src="{{ icons['Map Bombs'] }}" /></td>
<td>{{ bombs_display }}/20</td>
</tr>
<tr class="column-headers">
<td colspan="2">Map Width:</td>
<td colspan="2">Map Height:</td>
</tr>
<tr>
<td><img alt="Map Width" src="{{ icons['Map Width'] }}" /></td>
<td>{{ width_display }}/10</td>
<td><img alt="Map Height" src="{{ icons['Map Height'] }}" /></td>
<td>{{ height_display }}/10</td>
</tr>
</table>
</div>
</body>
</html>

View File

@@ -4,7 +4,7 @@
<title>{{ player_name }}&apos;s Tracker</title> <title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/> <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/jquery.scrollsync.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script> <script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}

View File

@@ -1,5 +1,6 @@
{% block head %} {% block head %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/themes/base.css") }}" /> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/themes/base.css") }}" />
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/baseHeader.js") }}"></script>
{% endblock %} {% endblock %}
{% block header %} {% block header %}
@@ -16,5 +17,17 @@
<a href="/faq/en">f.a.q.</a> <a href="/faq/en">f.a.q.</a>
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a> <a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
</div> </div>
<div id="base-header-right-mobile">
<a id="base-header-mobile-menu-button" href="#">
<img src="/static/static/button-images/hamburger-menu-icon.png" alt="Menu" />
</a>
</div>
</header> </header>
<div id="base-header-mobile-menu">
<a href="/games">supported games</a>
<a href="/tutorial">setup guides</a>
<a href="/start-playing">start playing</a>
<a href="/faq/en">f.a.q.</a>
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
</div>
{% endblock %} {% endblock %}

View File

@@ -14,7 +14,7 @@
<br /> <br />
{% endif %} {% endif %}
{% if room.tracker %} {% if room.tracker %}
This room has a <a href="{{ url_for("getTracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled. This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled.
<br /> <br />
{% endif %} {% endif %}
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
@@ -25,8 +25,8 @@
The most likely failure reason is that the multiworld is too old to be loaded now. The most likely failure reason is that the multiworld is too old to be loaded now.
{% elif room.last_port %} {% elif room.last_port %}
You can connect to this room by using <span class="interactive" You can connect to this room by using <span class="interactive"
data-tooltip="This means address/ip is {{ config['PATCH_TARGET'] }} and port is {{ room.last_port }}."> data-tooltip="This means address/ip is {{ config['HOST_ADDRESS'] }} and port is {{ room.last_port }}.">
'/connect {{ config['PATCH_TARGET'] }}:{{ room.last_port }}' '/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}'
</span> </span>
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br> in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>
{% endif %} {% endif %}

View File

@@ -1,14 +1,16 @@
{% extends 'tablepage.html' %} {% extends 'tablepage.html' %}
{% block head %} {% block head %}
{{ super() }} {{ super() }}
<title>Multiworld Tracker</title> <title>ALttP Multiworld Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/> <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/jquery.scrollsync.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script> <script type="application/ecmascript" src="{{ url_for('static', filename="assets/lttpMultiTracker.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% include 'header/dirtHeader.html' %} {% include 'header/dirtHeader.html' %}
{% include 'multiTrackerNavigation.html' %}
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}"> <div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<div id="tracker-header-bar"> <div id="tracker-header-bar">
<input placeholder="Search" id="search"/> <input placeholder="Search" id="search"/>
@@ -98,6 +100,7 @@
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th> <th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
{%- endif -%} {%- endif -%}
{%- endfor -%} {%- endfor -%}
<th rowspan="2" class="center-column">&percnt;</th>
<th rowspan="2" class="center-column hours">Last<br>Activity</th> <th rowspan="2" class="center-column hours">Last<br>Activity</th>
</tr> </tr>
<tr> <tr>
@@ -140,6 +143,7 @@
<td class="center-column">{% if inventory[team][player][big_key_ids[area]] %}✔️{% endif %}</td> <td class="center-column">{% if inventory[team][player][big_key_ids[area]] %}✔️{% endif %}</td>
{%- endif -%} {%- endif -%}
{%- endfor -%} {%- endfor -%}
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
{%- if activity_timers[(team, player)] -%} {%- if activity_timers[(team, player)] -%}
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td> <td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
{%- else -%} {%- else -%}

View File

@@ -22,7 +22,7 @@
{% for patch in room.seed.slots|list|sort(attribute="player_id") %} {% for patch in room.seed.slots|list|sort(attribute="player_id") %}
<tr> <tr>
<td>{{ patch.player_id }}</td> <td>{{ patch.player_id }}</td>
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:@{{ config['PATCH_TARGET'] }}:{{ room.last_port }}">{{ patch.player_name }}<a/></td> <td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
<td>{{ patch.game }}</td> <td>{{ patch.game }}</td>
<td> <td>
{% if patch.game == "Minecraft" %} {% if patch.game == "Minecraft" %}

View File

@@ -0,0 +1,44 @@
{% extends "multiTracker.html" %}
{% block custom_table_headers %}
<th class="center-column">
<img src="https://wiki.factorio.com/images/thumb/Logistic_science_pack.png/32px-Logistic_science_pack.png"
alt="Logistic Science Pack">
</th>
<th class="center-column">
<img src="https://wiki.factorio.com/images/thumb/Military_science_pack.png/32px-Military_science_pack.png"
alt="Military Science Pack">
</th>
<th class="center-column">
<img src="https://wiki.factorio.com/images/thumb/Chemical_science_pack.png/32px-Chemical_science_pack.png"
alt="Chemical Science Pack">
</th>
<th class="center-column">
<img src="https://wiki.factorio.com/images/thumb/Production_science_pack.png/32px-Production_science_pack.png"
alt="Production Science Pack">
</th>
<th class="center-column">
<img src="https://wiki.factorio.com/images/thumb/Utility_science_pack.png/32px-Utility_science_pack.png"
alt="Utility Science Pack">
</th>
<th class="center-column">
<img src="https://wiki.factorio.com/images/thumb/Space_science_pack.png/32px-Space_science_pack.png"
alt="Space Science Pack">
</th>
{% endblock %}
{% block custom_table_row scoped %}
{% if games[player] == "Factorio" %}
<td class="center-column">{% if inventory[team][player][131161] or inventory[team][player][131281] %}✔{% endif %}</td>
<td class="center-column">{% if inventory[team][player][131172] or inventory[team][player][131281] > 1%}✔{% endif %}</td>
<td class="center-column">{% if inventory[team][player][131195] or inventory[team][player][131281] > 2%}✔{% endif %}</td>
<td class="center-column">{% if inventory[team][player][131240] or inventory[team][player][131281] > 3%}✔{% endif %}</td>
<td class="center-column">{% if inventory[team][player][131240] or inventory[team][player][131281] > 4%}✔{% endif %}</td>
<td class="center-column">{% if inventory[team][player][131220] or inventory[team][player][131281] > 5%}✔{% endif %}</td>
{% else %}
<td class="center-column"></td>
<td class="center-column"></td>
<td class="center-column"></td>
<td class="center-column"></td>
<td class="center-column"></td>
<td class="center-column"></td>
{% endif %}
{% endblock%}

View File

@@ -0,0 +1,95 @@
{% extends 'tablepage.html' %}
{% block head %}
{{ super() }}
<title>Multiworld 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/trackerCommon.js") }}"></script>
{% endblock %}
{% block body %}
{% include 'header/dirtHeader.html' %}
{% include 'multiTrackerNavigation.html' %}
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<div id="tracker-header-bar">
<input placeholder="Search" id="search"/>
<span{% if not video %} hidden{% endif %} id="multi-stream-link">
<a target="_blank" href="https://multistream.me/
{%- for platform, link in video.values()|unique(False, 1)-%}
{%- if platform == "Twitch" -%}t{%- else -%}yt{%- endif -%}:{{- link -}}/
{%- endfor -%}">
Multistream
</a>
</span>
<span class="info">Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically.</span>
</div>
<div id="tables-container">
{% for team, players in checks_done.items() %}
<div class="table-wrapper">
<table id="checks-table" class="table non-unique-item-table">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Game</th>
{% block custom_table_headers %}
{# implement this block in game-specific multi trackers #}
{% endblock %}
<th class="center-column">Checks</th>
<th class="center-column">&percnt;</th>
<th class="center-column hours">Last<br>Activity</th>
</tr>
</thead>
<tbody>
{%- for player, checks in players.items() -%}
<tr>
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
<td>{{ player_names[(team, loop.index)]|e }}</td>
<td>{{ games[player] }}</td>
{% block custom_table_row scoped %}
{# implement this block in game-specific multi trackers #}
{% endblock %}
<td class="center-column">{{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }}</td>
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
{%- if activity_timers[(team, player)] -%}
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
{%- else -%}
<td class="center-column">None</td>
{%- endif -%}
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
{% endfor %}
{% for team, hints in hints.items() %}
<div class="table-wrapper">
<table id="hints-table" class="table non-unique-item-table" data-order='[[5, "asc"], [0, "asc"]]'>
<thead>
<tr>
<th>Finder</th>
<th>Receiver</th>
<th>Item</th>
<th>Location</th>
<th>Entrance</th>
<th>Found</th>
</tr>
</thead>
<tbody>
{%- for hint in hints -%}
<tr>
<td>{{ long_player_names[team, hint.finding_player] }}</td>
<td>{{ long_player_names[team, hint.receiving_player] }}</td>
<td>{{ hint.item|item_name }}</td>
<td>{{ hint.location|location_name }}</td>
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
<td>{% if hint.found %}✔{% endif %}</td>
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,9 @@
{%- if enabled_multiworld_trackers|length > 1 -%}
<div id="tracker-navigation">
{% for enabled_tracker in enabled_multiworld_trackers %}
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %}
<a class="tracker-navigation-button{%- if enabled_tracker.current -%} selected{% endif %}"
href="{{ tracker_url }}">{{ enabled_tracker.name }}</a>
{% endfor %}
</div>
{%- endif -%}

View File

@@ -29,17 +29,30 @@
<li><a href="/glossary/en">Glossary</a></li> <li><a href="/glossary/en">Glossary</a></li>
</ul> </ul>
<h2>Tutorials</h2>
<ul>
<li><a href="/tutorial/Archipelago/setup/en">Multiworld Setup Tutorial</a></li>
<li><a href="/tutorial/Archipelago/using_website/en">Website User Guide</a></li>
<li><a href="/tutorial/Archipelago/mac/en">Setup Guide for Mac</a></li>
<li><a href="/tutorial/Archipelago/commands/en">Server and Client Commands</a></li>
<li><a href="/tutorial/Archipelago/advanced_settings/en">Advanced YAML Guide</a></li>
<li><a href="/tutorial/Archipelago/triggers/en">Triggers Guide</a></li>
<li><a href="/tutorial/Archipelago/plando/en">Plando Guide</a></li>
</ul>
<h2>Game Info Pages</h2> <h2>Game Info Pages</h2>
<ul> <ul>
{% for game in games | title_sorted %} {% for game in games | title_sorted %}
<li><a href="{{ url_for('game_info', game=game, lang='en') }}">{{ game }}</a></li> <li><a href="{{ url_for('game_info', game=game['title'], lang='en') }}">{{ game['title'] }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
<h2>Game Settings Pages</h2> <h2>Game Settings Pages</h2>
<ul> <ul>
{% for game in games | title_sorted %} {% for game in games | title_sorted %}
<li><a href="{{ url_for('player_settings', game=game) }}">{{ game }}</a></li> {% if game['has_settings'] %}
<li><a href="{{ url_for('player_settings', game=game['title']) }}">{{ game['title'] }}</a></li>
{% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View File

@@ -8,79 +8,94 @@
<body> <body>
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}"> <div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<table id="inventory-table"> <div id="inventory-table">
<tr> <div class="table-row">
<td><img src="{{ icons['Timespinner Wheel'] }}" class="{{ 'acquired' if 'Timespinner Wheel' in acquired_items }}" title="Timespinner Wheel" /></td> <div class="C1"><img src="{{ icons['Timespinner Wheel'] }}" class="{{ 'acquired' if 'Timespinner Wheel' in acquired_items }}" title="Timespinner Wheel" /></div>
<td><img src="{{ icons['Timespinner Spindle'] }}" class="{{ 'acquired' if 'Timespinner Spindle' in acquired_items }}" title="Timespinner Spindle" /></td> <div class="C2"><img src="{{ icons['Timespinner Spindle'] }}" class="{{ 'acquired' if 'Timespinner Spindle' in acquired_items }}" title="Timespinner Spindle" /></div>
<td><img src="{{ icons['Timespinner Gear 1'] }}" class="{{ 'acquired' if 'Timespinner Gear 1' in acquired_items }}" title="Timespinner Gear 1" /></td> <div class="C3"><img src="{{ icons['Timespinner Gear 1'] }}" class="{{ 'acquired' if 'Timespinner Gear 1' in acquired_items }}" title="Timespinner Gear 1" /></div>
<td><img src="{{ icons['Timespinner Gear 2'] }}" class="{{ 'acquired' if 'Timespinner Gear 2' in acquired_items }}" title="Timespinner Gear 2" /></td> <div class="C4"><img src="{{ icons['Timespinner Gear 2'] }}" class="{{ 'acquired' if 'Timespinner Gear 2' in acquired_items }}" title="Timespinner Gear 2" /></div>
<td><img src="{{ icons['Timespinner Gear 3'] }}" class="{{ 'acquired' if 'Timespinner Gear 3' in acquired_items }}" title="Timespinner Gear 3" /></td> <div class="C5"><img src="{{ icons['Timespinner Gear 3'] }}" class="{{ 'acquired' if 'Timespinner Gear 3' in acquired_items }}" title="Timespinner Gear 3" /></div>
</tr> </div>
<tr> <div class="table-row">
<td><img src="{{ icons['Talaria Attachment'] }}" class="{{ 'acquired' if 'Talaria Attachment' in acquired_items or 'QuickSeed' in options }}" title="Talaria Attachment" /></td> <div class="C1"><img src="{{ icons['Talaria Attachment'] }}" class="{{ 'acquired' if 'Talaria Attachment' in acquired_items or 'QuickSeed' in options }}" title="Talaria Attachment" /></div>
<td><img src="{{ icons['Succubus Hairpin'] }}" class="{{ 'acquired' if 'Succubus Hairpin' in acquired_items }}" title="Succubus Hairpin" /></td> <div class="C2"><img src="{{ icons['Succubus Hairpin'] }}" class="{{ 'acquired' if 'Succubus Hairpin' in acquired_items }}" title="Succubus Hairpin" /></div>
<td><img src="{{ icons['Lightwall'] }}" class="{{ 'acquired' if 'Lightwall' in acquired_items }}" title="Lightwall" /></td> <div class="C3"><img src="{{ icons['Lightwall'] }}" class="{{ 'acquired' if 'Lightwall' in acquired_items }}" title="Lightwall" /></div>
<td><img src="{{ icons['Celestial Sash'] }}" class="{{ 'acquired' if 'Celestial Sash' in acquired_items }}" title="Celestial Sash" /></td> <div class="C4"><img src="{{ icons['Celestial Sash'] }}" class="{{ 'acquired' if 'Celestial Sash' in acquired_items }}" title="Celestial Sash" /></div>
<td><img src="{{ icons['Twin Pyramid Key'] }}" class="{{ 'acquired' if 'Twin Pyramid Key' in acquired_items }}" title="Twin Pyramid Key" /></td> <div class="C5">
</tr> <div class="image-stack">
<tr> <div class="stack-back">
<td><img src="{{ icons['Security Keycard A'] }}" class="{{ 'acquired' if 'Security Keycard A' in acquired_items }}" title="Security Keycard A" /></td> <img src="{{ icons['Twin Pyramid Key'] }}" class="{{ 'acquired' if 'Twin Pyramid Key' in acquired_items or 'UnchainedKeys' in options }}" title="Twin Pyramid Key" />
<td><img src="{{ icons['Security Keycard B'] }}" class="{{ 'acquired' if 'Security Keycard B' in acquired_items }}" title="Security Keycard B" /></td> </div>
<td><img src="{{ icons['Security Keycard C'] }}" class="{{ 'acquired' if 'Security Keycard C' in acquired_items }}" title="Security Keycard C" /></td> <div class="stack-front">
<td><img src="{{ icons['Security Keycard D'] }}" class="{{ 'acquired' if 'Security Keycard D' in acquired_items }}" title="Security Keycard D" /></td> {% if 'UnchainedKeys' in options %}
{% if 'DownloadableItems' in options %} {% if 'EnterSandman' in options %}
<td><img src="{{ icons['Library Keycard V'] }}" class="{{ 'acquired' if 'Library Keycard V' in acquired_items }}" title="Library Keycard V" /></td> <div class="stack-top-right">
{% else %} <img src="{{ icons['Twin Pyramid Key'] }}" class="green {{ 'acquired' if 'Mysterious Warp Beacon' in acquired_items }}" title="Mysterious Warp Beacon" />
<td></td> </div>
{% endif %} {% endif %}
</tr> <div class="stack-bottum-left">
<tr> <img src="{{ icons['Twin Pyramid Key'] }}" class="cyan {{ 'acquired' if 'Timeworn Warp Beacon' in acquired_items }}" title="Timeworn Warp Beacon" />
{% if 'DownloadableItems' in options %} </div>
<td><img src="{{ icons['Tablet'] }}" class="{{ 'acquired' if 'Tablet' in acquired_items }}" title="Tablet" /></td> <div class="stack-bottum-right">
{% else %} <img src="{{ icons['Twin Pyramid Key'] }}" class="purple {{ 'acquired' if 'Modern Warp Beacon' in acquired_items }}" title="Modern Warp Beacon" />
<td></td> </div>
{% endif %} {% endif %}
<td><img src="{{ icons['Elevator Keycard'] }}" class="{{ 'acquired' if 'Elevator Keycard' in acquired_items }}" title="Elevator Keycard" /></td> </div>
{% if 'EyeSpy' in options %} </div>
<td><img src="{{ icons['Oculus Ring'] }}" class="{{ 'acquired' if 'Oculus Ring' in acquired_items }}" title="Oculus Ring" /></td> </div>
{% else %} </div>
<td></td> <div class="table-row">
{% endif %} <div class="C1"><img src="{{ icons['Security Keycard A'] }}" class="{{ 'acquired' if 'Security Keycard A' in acquired_items }}" title="Security Keycard A" /></div>
<td><img src="{{ icons['Water Mask'] }}" class="{{ 'acquired' if 'Water Mask' in acquired_items }}" title="Water Mask" /></td> <div class="C2"><img src="{{ icons['Security Keycard B'] }}" class="{{ 'acquired' if 'Security Keycard B' in acquired_items }}" title="Security Keycard B" /></div>
<td><img src="{{ icons['Gas Mask'] }}" class="{{ 'acquired' if 'Gas Mask' in acquired_items }}" title="Gas Mask" /></td> <div class="C3"><img src="{{ icons['Security Keycard C'] }}" class="{{ 'acquired' if 'Security Keycard C' in acquired_items }}" title="Security Keycard C" /></div>
</tr> <div class="C4"><img src="{{ icons['Security Keycard D'] }}" class="{{ 'acquired' if 'Security Keycard D' in acquired_items }}" title="Security Keycard D" /></div>
<tr> {% if 'DownloadableItems' in options %}
{% if 'GyreArchives' in options %} <div class="C5"><img src="{{ icons['Library Keycard V'] }}" class="{{ 'acquired' if 'Library Keycard V' in acquired_items }}" title="Library Keycard V" /></div>
<td><img src="{{ icons['Kobo'] }}" class="{{ 'acquired' if 'Kobo' in acquired_items }}" title="Kobo" /></td> {% endif %}
<td><img src="{{ icons['Merchant Crow'] }}" class="{{ 'acquired' if 'Merchant Crow' in acquired_items }}" title="Merchant Crow" /></td> </div>
{% else %} <div class="table-row">
<td></td> {% if 'DownloadableItems' in options %}
<td></td> <div class="C1"><img src="{{ icons['Tablet'] }}" class="{{ 'acquired' if 'Tablet' in acquired_items }}" title="Tablet" /></div>
{% endif %} {% endif %}
<div class="C2"><img src="{{ icons['Elevator Keycard'] }}" class="{{ 'acquired' if 'Elevator Keycard' in acquired_items }}" title="Elevator Keycard" /></div>
{% if 'EyeSpy' in options %}
<div class="C3"><img src="{{ icons['Oculus Ring'] }}" class="{{ 'acquired' if 'Oculus Ring' in acquired_items }}" title="Oculus Ring" /></div>
{% endif %}
<div class="C4"><img src="{{ icons['Water Mask'] }}" class="{{ 'acquired' if 'Water Mask' in acquired_items }}" title="Water Mask" /></div>
<div class="C5"><img src="{{ icons['Gas Mask'] }}" class="{{ 'acquired' if 'Gas Mask' in acquired_items }}" title="Gas Mask" /></div>
</div>
<div class="table-row">
{% if 'GyreArchives' in options %}
<div class="C1"><img src="{{ icons['Kobo'] }}" class="{{ 'acquired' if 'Kobo' in acquired_items }}" title="Kobo" /></div>
<div class="C2"><img src="{{ icons['Merchant Crow'] }}" class="{{ 'acquired' if 'Merchant Crow' in acquired_items }}" title="Merchant Crow" /></div>
{% endif %}
<div class="C3">
{% if 'Djinn Inferno' in acquired_items %} {% if 'Djinn Inferno' in acquired_items %}
<td><img src="{{ icons['Djinn Inferno'] }}" class="acquired" title="Djinn Inferno" /></td> <img src="{{ icons['Djinn Inferno'] }}" class="acquired" title="Djinn Inferno" />
{% elif 'Pyro Ring' in acquired_items %} {% elif 'Pyro Ring' in acquired_items %}
<td><img src="{{ icons['Pyro Ring'] }}" class="acquired" title="Pyro Ring" /></td> <img src="{{ icons['Pyro Ring'] }}" class="acquired" title="Pyro Ring" />
{% elif 'Fire Orb' in acquired_items %} {% elif 'Fire Orb' in acquired_items %}
<td><img src="{{ icons['Fire Orb'] }}" class="acquired" title="Fire Orb" /></td> <img src="{{ icons['Fire Orb'] }}" class="acquired" title="Fire Orb" />
{% elif 'Infernal Flames' in acquired_items %} {% elif 'Infernal Flames' in acquired_items %}
<td><img src="{{ icons['Infernal Flames'] }}" class="acquired" title="Infernal Flames" /></td> <img src="{{ icons['Infernal Flames'] }}" class="acquired" title="Infernal Flames" />
{% else %} {% else %}
<td><img src="{{ icons['Djinn Inferno'] }}" title="Djinn Inferno" /></td> <img src="{{ icons['Djinn Inferno'] }}" title="Djinn Inferno" />
{% endif %} {% endif %}
</div>
<div class="C4">
{% if 'Royal Ring' in acquired_items %} {% if 'Royal Ring' in acquired_items %}
<td><img src="{{ icons['Royal Ring'] }}" class="acquired" title="Royal Ring" /></td> <img src="{{ icons['Royal Ring'] }}" class="acquired" title="Royal Ring" />
{% elif 'Plasma Geyser' in acquired_items %} {% elif 'Plasma Geyser' in acquired_items %}
<td><img src="{{ icons['Plasma Geyser'] }}" class="acquired" title="Plasma Geyser" /></td> <img src="{{ icons['Plasma Geyser'] }}" class="acquired" title="Plasma Geyser" />
{% elif 'Plasma Orb' in acquired_items %} {% elif 'Plasma Orb' in acquired_items %}
<td><img src="{{ icons['Plasma Orb'] }}" class="acquired" title="Plasma Orb" /></td> <img src="{{ icons['Plasma Orb'] }}" class="acquired" title="Plasma Orb" />
{% else %} {% else %}
<td><img src="{{ icons['Royal Ring'] }}" title="Royal Ring" /></td> <img src="{{ icons['Royal Ring'] }}" title="Royal Ring" />
{% endif %} {% endif %}
</tr> </div>
</table> </div>
</div>
<table id="location-table"> <table id="location-table">
{% for area in checks_done %} {% for area in checks_done %}
<tr class="location-category" id="{{area}}-header"> <tr class="location-category" id="{{area}}-header">

View File

@@ -5,6 +5,7 @@ from typing import Counter, Optional, Dict, Any, Tuple
from uuid import UUID from uuid import UUID
from flask import render_template from flask import render_template
from jinja2 import pass_context, runtime
from werkzeug.exceptions import abort from werkzeug.exceptions import abort
from MultiServer import Context, get_saving_second from MultiServer import Context, get_saving_second
@@ -83,9 +84,6 @@ def get_alttp_id(item_name):
return Items.item_table[item_name][2] return Items.item_table[item_name][2]
app.jinja_env.filters["location_name"] = lambda location: lookup_any_location_id_to_name.get(location, location)
app.jinja_env.filters['item_name'] = lambda id: lookup_any_item_id_to_name.get(id, id)
links = {"Bow": "Progressive Bow", links = {"Bow": "Progressive Bow",
"Silver Arrows": "Progressive Bow", "Silver Arrows": "Progressive Bow",
"Silver Bow": "Progressive Bow", "Silver Bow": "Progressive Bow",
@@ -212,14 +210,6 @@ del data
del item del item
def attribute_item(inventory, team, recipient, item):
target_item = links.get(item, item)
if item in levels: # non-progressive
inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item])
else:
inventory[team][recipient][target_item] += 1
def attribute_item_solo(inventory, item): def attribute_item_solo(inventory, item):
"""Adds item to inventory counter, converts everything to progressive.""" """Adds item to inventory counter, converts everything to progressive."""
target_item = links.get(item, item) target_item = links.get(item, item)
@@ -237,6 +227,22 @@ def render_timedelta(delta: datetime.timedelta):
return f"{hours}:{minutes}" return f"{hours}:{minutes}"
@pass_context
def get_location_name(context: runtime.Context, loc: int) -> str:
context_locations = context.get("custom_locations", {})
return collections.ChainMap(lookup_any_location_id_to_name, context_locations).get(loc, loc)
@pass_context
def get_item_name(context: runtime.Context, item: int) -> str:
context_items = context.get("custom_items", {})
return collections.ChainMap(lookup_any_item_id_to_name, context_items).get(item, item)
app.jinja_env.filters["location_name"] = get_location_name
app.jinja_env.filters["item_name"] = get_item_name
_multidata_cache = {} _multidata_cache = {}
@@ -258,10 +264,23 @@ def get_static_room_data(room: Room):
# in > 100 players this can take a bit of time and is the main reason for the cache # in > 100 players this can take a bit of time and is the main reason for the cache
locations: Dict[int, Dict[int, Tuple[int, int, int]]] = multidata['locations'] locations: Dict[int, Dict[int, Tuple[int, int, int]]] = multidata['locations']
names: Dict[int, Dict[int, str]] = multidata["names"] names: Dict[int, Dict[int, str]] = multidata["names"]
games = {}
groups = {} groups = {}
custom_locations = {}
custom_items = {}
if "slot_info" in multidata: if "slot_info" in multidata:
games = {slot: slot_info.game for slot, slot_info in multidata["slot_info"].items()}
groups = {slot: slot_info.group_members for slot, slot_info in multidata["slot_info"].items() groups = {slot: slot_info.group_members for slot, slot_info in multidata["slot_info"].items()
if slot_info.type == SlotType.group} if slot_info.type == SlotType.group}
for game in games.values():
if game in multidata["datapackage"]:
custom_locations.update(
{id: name for name, id in multidata["datapackage"][game]["location_name_to_id"].items()})
custom_items.update(
{id: name for name, id in multidata["datapackage"][game]["item_name_to_id"].items()})
elif "games" in multidata:
games = multidata["games"]
seed_checks_in_area = checks_in_area.copy() seed_checks_in_area = checks_in_area.copy()
use_door_tracker = False use_door_tracker = False
@@ -282,7 +301,8 @@ def get_static_room_data(room: Room):
if playernumber not in groups} if playernumber not in groups}
saving_second = get_saving_second(multidata["seed_name"]) saving_second = get_saving_second(multidata["seed_name"])
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, \
multidata["precollected_items"], multidata["games"], multidata["slot_data"], groups, saving_second multidata["precollected_items"], games, multidata["slot_data"], groups, saving_second, \
custom_locations, custom_items
_multidata_cache[room.seed.id] = result _multidata_cache[room.seed.id] = result
return result return result
@@ -309,7 +329,8 @@ def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, w
# 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, \ locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
precollected_items, games, slot_data, groups, saving_second = get_static_room_data(room) precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
get_static_room_data(room)
player_name = names[tracked_team][tracked_player - 1] player_name = names[tracked_team][tracked_player - 1]
location_to_area = player_location_to_area[tracked_player] location_to_area = player_location_to_area[tracked_player]
inventory = collections.Counter() inventory = collections.Counter()
@@ -351,7 +372,7 @@ def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, w
seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second) seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second)
else: else:
tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name, tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
seed_checks_in_area, checks_done, saving_second) seed_checks_in_area, checks_done, saving_second, custom_locations, custom_items)
return (saving_second - datetime.datetime.now().second) % 60 or 60, tracker return (saving_second - datetime.datetime.now().second) % 60 or 60, tracker
@@ -457,7 +478,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png", "Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png", "Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
"Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png", "Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png",
"Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png", "Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png",
"Saddle": "https://i.imgur.com/2QtDyR0.png", "Saddle": "https://i.imgur.com/2QtDyR0.png",
"Channeling Book": "https://i.imgur.com/J3WsYZw.png", "Channeling Book": "https://i.imgur.com/J3WsYZw.png",
"Silk Touch Book": "https://i.imgur.com/iqERxHQ.png", "Silk Touch Book": "https://i.imgur.com/iqERxHQ.png",
@@ -465,7 +486,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D
} }
minecraft_location_ids = { minecraft_location_ids = {
"Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070, "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070,
42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077], 42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077],
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021, "Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014], 42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014],
@@ -627,7 +648,7 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
if base_name == "hookshot": if base_name == "hookshot":
display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level) display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level)
if base_name == "wallet": if base_name == "wallet":
display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level) display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level)
# Determine display for bottles. Show letter if it's obtained, determine bottle count # Determine display for bottles. Show letter if it's obtained, determine bottle count
@@ -645,7 +666,6 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
} }
for item_name, item_id in multi_items.items(): for item_name, item_id in multi_items.items():
base_name = item_name.split()[-1].lower() base_name = item_name.split()[-1].lower()
count = inventory[item_id]
display_data[base_name+"_count"] = inventory[item_id] display_data[base_name+"_count"] = inventory[item_id]
# Gather dungeon locations # Gather dungeon locations
@@ -775,7 +795,7 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
} }
timespinner_location_ids = { timespinner_location_ids = {
"Present": [ "Present": [
1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009, 1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009,
1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019, 1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019,
1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029, 1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029,
@@ -796,20 +816,20 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
1337150, 1337151, 1337152, 1337153, 1337154, 1337155, 1337150, 1337151, 1337152, 1337153, 1337154, 1337155,
1337171, 1337172, 1337173, 1337174, 1337175], 1337171, 1337172, 1337173, 1337174, 1337175],
"Ancient Pyramid": [ "Ancient Pyramid": [
1337236, 1337236,
1337246, 1337247, 1337248, 1337249] 1337246, 1337247, 1337248, 1337249]
} }
if(slot_data["DownloadableItems"]): if(slot_data["DownloadableItems"]):
timespinner_location_ids["Present"] += [ timespinner_location_ids["Present"] += [
1337156, 1337157, 1337159, 1337156, 1337157, 1337159,
1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169, 1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
1337170] 1337170]
if(slot_data["Cantoran"]): if(slot_data["Cantoran"]):
timespinner_location_ids["Past"].append(1337176) timespinner_location_ids["Past"].append(1337176)
if(slot_data["LoreChecks"]): if(slot_data["LoreChecks"]):
timespinner_location_ids["Present"] += [ timespinner_location_ids["Present"] += [
1337177, 1337178, 1337179, 1337177, 1337178, 1337179,
1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187] 1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187]
timespinner_location_ids["Past"] += [ timespinner_location_ids["Past"] += [
1337188, 1337189, 1337188, 1337189,
@@ -1190,11 +1210,89 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
**display_data) **display_data)
def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, saving_second: int) -> str:
icons = {
"Checks Available": "https://0rganics.org/archipelago/cf/spr_tiles_3.png",
"Map Width": "https://0rganics.org/archipelago/cf/spr_tiles_4.png",
"Map Height": "https://0rganics.org/archipelago/cf/spr_tiles_5.png",
"Map Bombs": "https://0rganics.org/archipelago/cf/spr_tiles_6.png",
"Nothing": "",
}
checksfinder_location_ids = {
"Tile 1": 81000,
"Tile 2": 81001,
"Tile 3": 81002,
"Tile 4": 81003,
"Tile 5": 81004,
"Tile 6": 81005,
"Tile 7": 81006,
"Tile 8": 81007,
"Tile 9": 81008,
"Tile 10": 81009,
"Tile 11": 81010,
"Tile 12": 81011,
"Tile 13": 81012,
"Tile 14": 81013,
"Tile 15": 81014,
"Tile 16": 81015,
"Tile 17": 81016,
"Tile 18": 81017,
"Tile 19": 81018,
"Tile 20": 81019,
"Tile 21": 81020,
"Tile 22": 81021,
"Tile 23": 81022,
"Tile 24": 81023,
"Tile 25": 81024,
}
display_data = {}
# Multi-items
multi_items = {
"Map Width": 80000,
"Map Height": 80001,
"Map Bombs": 80002
}
for item_name, item_id in multi_items.items():
base_name = item_name.split()[-1].lower()
count = inventory[item_id]
display_data[base_name + "_count"] = count
display_data[base_name + "_display"] = count + 5
# Get location info
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
lookup_name = lambda id: lookup_any_location_id_to_name[id]
location_info = {tile_name: {lookup_name(tile_location): (tile_location in checked_locations)} for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in set(locations[player])}
checks_done = {tile_name: len([tile_location]) for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in checked_locations and tile_location in set(locations[player])}
checks_done['Total'] = len(checked_locations)
checks_in_area = checks_done
# Calculate checks available
display_data["checks_unlocked"] = min(display_data["width_count"] + display_data["height_count"] + display_data["bombs_count"] + 5, 25)
display_data["checks_available"] = max(display_data["checks_unlocked"] - len(checked_locations), 0)
# Victory condition
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
display_data['game_finished'] = game_state == 30
return render_template("checksfinderTracker.html",
inventory=inventory, icons=icons,
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
id in lookup_any_item_id_to_name},
player=player, team=team, room=room, player_name=playerName,
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
**display_data)
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str, inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int],
saving_second: int) -> str: saving_second: int, custom_locations: Dict[int, str], custom_items: Dict[int, str]) -> str:
checked_locations = multisave.get("location_checks", {}).get((team, player), set()) checked_locations = multisave.get("location_checks", {}).get((team, player), set())
player_received_items = {} player_received_items = {}
@@ -1212,26 +1310,45 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic
player=player, team=team, room=room, player_name=playerName, player=player, team=team, room=room, player_name=playerName,
checked_locations=checked_locations, checked_locations=checked_locations,
not_checked_locations=set(locations[player]) - checked_locations, not_checked_locations=set(locations[player]) - checked_locations,
received_items=player_received_items, received_items=player_received_items, saving_second=saving_second,
saving_second=saving_second) custom_items=custom_items, custom_locations=custom_locations)
@app.route('/tracker/<suuid:tracker>') def get_enabled_multiworld_trackers(room: Room, current: str):
@cache.memoize(timeout=1) # multisave is currently created at most every minute enabled = [
def getTracker(tracker: UUID): {
"name": "Generic",
"endpoint": "get_multiworld_tracker",
"current": current == "Generic"
}
]
for game_name, endpoint in multi_trackers.items():
if any(slot.game == game_name for slot in room.seed.slots) or current == game_name:
enabled.append({
"name": game_name,
"endpoint": endpoint.__name__,
"current": current == game_name}
)
return enabled
def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]:
room: Room = Room.get(tracker=tracker) room: Room = Room.get(tracker=tracker)
if not room: if not room:
abort(404) return None
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
precollected_items, games, slot_data, groups, saving_second = get_static_room_data(room)
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if playernumber not in groups} locations, names, use_door_tracker, checks_in_area, player_location_to_area, \
for teamnumber, team in enumerate(names)} precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
get_static_room_data(room)
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations} checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
for playernumber in range(1, len(team) + 1) if playernumber not in groups} for playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)} for teamnumber, team in enumerate(names)}
percent_total_checks_done = {teamnumber: {playernumber: 0
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)}
hints = {team: set() for team in range(len(names))} hints = {team: set() for team in range(len(names))}
if room.multisave: if room.multisave:
multisave = restricted_loads(room.multisave) multisave = restricted_loads(room.multisave)
@@ -1241,6 +1358,126 @@ def getTracker(tracker: UUID):
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)
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
if player in groups:
continue
player_locations = locations[player]
checks_done[team][player]["Total"] = sum(1 for loc in locations_checked if loc in player_locations)
percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] /
checks_in_area[player]["Total"] * 100) \
if checks_in_area[player]["Total"] else 100
activity_timers = {}
now = datetime.datetime.utcnow()
for (team, player), timestamp in multisave.get("client_activity_timers", []):
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
player_names = {}
for team, names in enumerate(names):
for player, name in enumerate(names, 1):
player_names[(team, player)] = name
long_player_names = player_names.copy()
for (team, player), alias in multisave.get("name_aliases", {}).items():
player_names[(team, player)] = alias
long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})"
video = {}
for (team, player), data in multisave.get("video", []):
video[(team, player)] = data
return dict(player_names=player_names, room=room, checks_done=checks_done,
percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area,
activity_timers=activity_timers, video=video, hints=hints,
long_player_names=long_player_names,
multisave=multisave, precollected_items=precollected_items, groups=groups,
locations=locations, games=games)
def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]:
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
for teamnumber, team_data in data["checks_done"].items()}
groups = data["groups"]
for (team, player), locations_checked in data["multisave"].get("location_checks", {}).items():
if player in data["groups"]:
continue
player_locations = data["locations"][player]
precollected = data["precollected_items"][player]
for item_id in precollected:
inventory[team][player][item_id] += 1
for location in locations_checked:
item_id, recipient, flags = player_locations[location]
recipients = groups.get(recipient, [recipient])
for recipient in recipients:
inventory[team][recipient][item_id] += 1
return inventory
@app.route('/tracker/<suuid:tracker>')
@cache.memoize(timeout=60) # multisave is currently created at most every minute
def get_multiworld_tracker(tracker: UUID):
data = _get_multiworld_tracker_data(tracker)
if not data:
abort(404)
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic")
return render_template("multiTracker.html", **data)
@app.route('/tracker/<suuid:tracker>/Factorio')
@cache.memoize(timeout=60) # multisave is currently created at most every minute
def get_Factorio_multiworld_tracker(tracker: UUID):
data = _get_multiworld_tracker_data(tracker)
if not data:
abort(404)
data["inventory"] = _get_inventory_data(data)
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio")
return render_template("multiFactorioTracker.html", **data)
@app.route('/tracker/<suuid:tracker>/A Link to the Past')
@cache.memoize(timeout=60) # multisave is currently created at most every minute
def get_LttP_multiworld_tracker(tracker: UUID):
room: Room = Room.get(tracker=tracker)
if not room:
abort(404)
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
get_static_room_data(room)
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if
playernumber not in groups}
for teamnumber, team in enumerate(names)}
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)}
percent_total_checks_done = {teamnumber: {playernumber: 0
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)}
hints = {team: set() for team in range(len(names))}
if room.multisave:
multisave = restricted_loads(room.multisave)
else:
multisave = {}
if "hints" in multisave:
for (team, slot), slot_hints in multisave["hints"].items():
hints[team] |= set(slot_hints)
def attribute_item(team: int, recipient: int, item: int):
nonlocal inventory
target_item = links.get(item, item)
if item in levels: # non-progressive
inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item])
else:
inventory[team][recipient][target_item] += 1
for (team, player), locations_checked in multisave.get("location_checks", {}).items(): for (team, player), locations_checked in multisave.get("location_checks", {}).items():
if player in groups: if player in groups:
continue continue
@@ -1248,17 +1485,19 @@ def getTracker(tracker: UUID):
if precollected_items: if precollected_items:
precollected = precollected_items[player] precollected = precollected_items[player]
for item_id in precollected: for item_id in precollected:
attribute_item(inventory, team, player, item_id) attribute_item(team, player, item_id)
for location in locations_checked: for location in locations_checked:
if location not in player_locations or location not in player_location_to_area[player]: if location not in player_locations or location not in player_location_to_area[player]:
continue continue
item, recipient, flags = player_locations[location] item, recipient, flags = player_locations[location]
recipients = groups.get(recipient, [recipient])
if recipient in names: for recipient in recipients:
attribute_item(inventory, team, recipient, item) attribute_item(team, recipient, item)
checks_done[team][player][player_location_to_area[player][location]] += 1 checks_done[team][player][player_location_to_area[player][location]] += 1
checks_done[team][player]["Total"] += 1 checks_done[team][player]["Total"] += 1
percent_total_checks_done[team][player] = int(
checks_done[team][player]["Total"] / seed_checks_in_area[player]["Total"] * 100) if \
seed_checks_in_area[player]["Total"] else 100
for (team, player), game_state in multisave.get("client_game_state", {}).items(): for (team, player), game_state in multisave.get("client_game_state", {}).items():
if player in groups: if player in groups:
@@ -1300,14 +1539,19 @@ def getTracker(tracker: UUID):
for (team, player), data in multisave.get("video", []): for (team, player), data in multisave.get("video", []):
video[(team, player)] = data video[(team, player)] = data
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name, enabled_multiworld_trackers = get_enabled_multiworld_trackers(room, "A Link to the Past")
return render_template("lttpMultiTracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names, lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons, tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas, multi_items=multi_items, checks_done=checks_done,
checks_in_area=seed_checks_in_area, activity_timers=activity_timers, percent_total_checks_done=percent_total_checks_done,
ordered_areas=ordered_areas, checks_in_area=seed_checks_in_area,
activity_timers=activity_timers,
key_locations=group_key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids, key_locations=group_key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids,
video=video, big_key_locations=group_big_key_locations, video=video, big_key_locations=group_big_key_locations,
hints=hints, long_player_names=long_player_names) hints=hints, long_player_names=long_player_names,
enabled_multiworld_trackers=enabled_multiworld_trackers)
game_specific_trackers: typing.Dict[str, typing.Callable] = { game_specific_trackers: typing.Dict[str, typing.Callable] = {
@@ -1315,6 +1559,12 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = {
"Ocarina of Time": __renderOoTTracker, "Ocarina of Time": __renderOoTTracker,
"Timespinner": __renderTimespinnerTracker, "Timespinner": __renderTimespinnerTracker,
"A Link to the Past": __renderAlttpTracker, "A Link to the Past": __renderAlttpTracker,
"ChecksFinder": __renderChecksfinder,
"Super Metroid": __renderSuperMetroidTracker, "Super Metroid": __renderSuperMetroidTracker,
"Starcraft 2 Wings of Liberty": __renderSC2WoLTracker "Starcraft 2 Wings of Liberty": __renderSC2WoLTracker
} }
multi_trackers: typing.Dict[str, typing.Callable] = {
"A Link to the Past": get_LttP_multiworld_tracker,
"Factorio": get_Factorio_multiworld_tracker,
}

393
Zelda1Client.py Normal file
View File

@@ -0,0 +1,393 @@
# Based (read: copied almost wholesale and edited) off the FF1 Client.
import asyncio
import copy
import json
import logging
import os
import subprocess
import time
import typing
from asyncio import StreamReader, StreamWriter
from typing import List
import Utils
from Utils import async_start
from worlds import lookup_any_location_id_to_name
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
get_base_parser
from worlds.tloz.Items import item_game_ids
from worlds.tloz.Locations import location_ids
from worlds.tloz import Items, Locations, Rom
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart Zelda_connector.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure Zelda_connector.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart Zelda_connector.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
DISPLAY_MSGS = True
item_ids = item_game_ids
location_ids = location_ids
items_by_id = {id: item for item, id in item_ids.items()}
locations_by_id = {id: location for location, id in location_ids.items()}
class ZeldaCommandProcessor(ClientCommandProcessor):
def _cmd_nes(self):
"""Check NES Connection State"""
if isinstance(self.ctx, ZeldaContext):
logger.info(f"NES Status: {self.ctx.nes_status}")
def _cmd_toggle_msgs(self):
"""Toggle displaying messages in bizhawk"""
global DISPLAY_MSGS
DISPLAY_MSGS = not DISPLAY_MSGS
logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}")
class ZeldaContext(CommonContext):
command_processor = ZeldaCommandProcessor
items_handling = 0b101 # get sent remote and starting items
# Infinite Hyrule compatibility
overworld_item = 0x5F
armos_item = 0x24
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.bonus_items = []
self.nes_streams: (StreamReader, StreamWriter) = None
self.nes_sync_task = None
self.messages = {}
self.locations_array = None
self.nes_status = CONNECTION_INITIAL_STATUS
self.game = 'The Legend of Zelda'
self.awaiting_rom = False
self.shop_slots_left = 0
self.shop_slots_middle = 0
self.shop_slots_right = 0
self.shop_slots = [self.shop_slots_left, self.shop_slots_middle, self.shop_slots_right]
self.slot_data = dict()
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(ZeldaContext, self).server_auth(password_requested)
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to NES to get Player information')
return
await self.send_connect()
def _set_message(self, msg: str, msg_id: int):
if DISPLAY_MSGS:
self.messages[(time.time(), msg_id)] = msg
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
self.slot_data = args.get("slot_data", {})
asyncio.create_task(parse_locations(self.locations_array, self, True))
elif cmd == 'Print':
msg = args['text']
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
def on_print_json(self, args: dict):
if self.ui:
self.ui.print_json(copy.deepcopy(args["data"]))
else:
text = self.jsontotextparser(copy.deepcopy(args["data"]))
logger.info(text)
relevant = args.get("type", None) in {"Hint", "ItemSend"}
if relevant:
item = args["item"]
# goes to this world
if self.slot_concerns_self(args["receiving"]):
relevant = True
# found in this world
elif self.slot_concerns_self(item.player):
relevant = True
# not related
else:
relevant = False
if relevant:
item = args["item"]
msg = self.raw_text_parser(copy.deepcopy(args["data"]))
self._set_message(msg, item.item)
def run_gui(self):
from kvui import GameManager
class ZeldaManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Zelda 1 Client"
self.ui = ZeldaManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def get_payload(ctx: ZeldaContext):
current_time = time.time()
bonus_items = [item for item in ctx.bonus_items]
return json.dumps(
{
"items": [item.item for item in ctx.items_received],
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
if key[0] > current_time - 10},
"shops": {
"left": ctx.shop_slots_left,
"middle": ctx.shop_slots_middle,
"right": ctx.shop_slots_right
},
"bonusItems": bonus_items
}
)
def reconcile_shops(ctx: ZeldaContext):
checked_location_names = [lookup_any_location_id_to_name[location] for location in ctx.checked_locations]
shops = [location for location in checked_location_names if "Shop" in location]
left_slots = [shop for shop in shops if "Left" in shop]
middle_slots = [shop for shop in shops if "Middle" in shop]
right_slots = [shop for shop in shops if "Right" in shop]
for shop in left_slots:
ctx.shop_slots_left |= get_shop_bit_from_name(shop)
for shop in middle_slots:
ctx.shop_slots_middle |= get_shop_bit_from_name(shop)
for shop in right_slots:
ctx.shop_slots_right |= get_shop_bit_from_name(shop)
def get_shop_bit_from_name(location_name):
if "Potion" in location_name:
return Rom.potion_shop
elif "Arrow" in location_name:
return Rom.arrow_shop
elif "Shield" in location_name:
return Rom.shield_shop
elif "Ring" in location_name:
return Rom.ring_shop
elif "Candle" in location_name:
return Rom.candle_shop
elif "Take" in location_name:
return Rom.take_any
return 0 # this should never be hit
async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone="None"):
if locations_array == ctx.locations_array and not force:
return
else:
# print("New values")
ctx.locations_array = locations_array
locations_checked = []
location = None
for location in ctx.missing_locations:
location_name = lookup_any_location_id_to_name[location]
if location_name in Locations.overworld_locations and zone == "overworld":
status = locations_array[Locations.major_location_offsets[location_name]]
if location_name == "Ocean Heart Container":
status = locations_array[ctx.overworld_item]
if location_name == "Armos Knights":
status = locations_array[ctx.armos_item]
if status & 0x10:
ctx.locations_checked.add(location)
locations_checked.append(location)
elif location_name in Locations.underworld1_locations and zone == "underworld1":
status = locations_array[Locations.floor_location_game_offsets_early[location_name]]
if status & 0x10:
ctx.locations_checked.add(location)
locations_checked.append(location)
elif location_name in Locations.underworld2_locations and zone == "underworld2":
status = locations_array[Locations.floor_location_game_offsets_late[location_name]]
if status & 0x10:
ctx.locations_checked.add(location)
locations_checked.append(location)
elif (location_name in Locations.shop_locations or "Take" in location_name) and zone == "caves":
shop_bit = get_shop_bit_from_name(location_name)
slot = 0
context_slot = 0
if "Left" in location_name:
slot = "slot1"
context_slot = 0
elif "Middle" in location_name:
slot = "slot2"
context_slot = 1
elif "Right" in location_name:
slot = "slot3"
context_slot = 2
if locations_array[slot] & shop_bit > 0:
locations_checked.append(location)
ctx.shop_slots[context_slot] |= shop_bit
if locations_array["takeAnys"] and locations_array["takeAnys"] >= 4:
if "Take Any" in location_name:
short_name = None
if "Left" in location_name:
short_name = "TakeAnyLeft"
elif "Middle" in location_name:
short_name = "TakeAnyMiddle"
elif "Right" in location_name:
short_name = "TakeAnyRight"
if short_name is not None:
item_code = ctx.slot_data[short_name]
if item_code > 0:
ctx.bonus_items.append(item_code)
locations_checked.append(location)
if locations_checked:
await ctx.send_msgs([
{"cmd": "LocationChecks",
"locations": locations_checked}
])
async def nes_sync_task(ctx: ZeldaContext):
logger.info("Starting nes connector. Use /nes for status information")
while not ctx.exit_event.is_set():
error_status = None
if ctx.nes_streams:
(reader, writer) = ctx.nes_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with up to two fields:
# 1. A keepalive response of the Players Name (always)
# 2. An array representing the memory values of the locations area (if in game)
data = await asyncio.wait_for(reader.readline(), timeout=5)
data_decoded = json.loads(data.decode())
if data_decoded["overworldHC"] is not None:
ctx.overworld_item = data_decoded["overworldHC"]
if data_decoded["overworldPB"] is not None:
ctx.armos_item = data_decoded["overworldPB"]
if data_decoded['gameMode'] == 19 and ctx.finished_game == False:
await ctx.send_msgs([
{"cmd": "StatusUpdate",
"status": 30}
])
ctx.finished_game = True
if ctx.game is not None and 'overworld' in data_decoded:
# Not just a keep alive ping, parse
asyncio.create_task(parse_locations(data_decoded['overworld'], ctx, False, "overworld"))
if ctx.game is not None and 'underworld1' in data_decoded:
asyncio.create_task(parse_locations(data_decoded['underworld1'], ctx, False, "underworld1"))
if ctx.game is not None and 'underworld2' in data_decoded:
asyncio.create_task(parse_locations(data_decoded['underworld2'], ctx, False, "underworld2"))
if ctx.game is not None and 'caves' in data_decoded:
asyncio.create_task(parse_locations(data_decoded['caves'], ctx, False, "caves"))
if not ctx.auth:
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
if ctx.auth == '':
logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate"
"the ROM using the same link but adding your slot name")
if ctx.awaiting_rom:
await ctx.server_auth(False)
reconcile_shops(ctx)
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.nes_streams = None
except ConnectionResetError as e:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.nes_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.nes_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.nes_streams = None
if ctx.nes_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to NES")
ctx.nes_status = CONNECTION_CONNECTED_STATUS
else:
ctx.nes_status = f"Was tentatively connected but error occured: {error_status}"
elif error_status:
ctx.nes_status = error_status
logger.info("Lost connection to nes and attempting to reconnect. Use /nes for status updates")
else:
try:
logger.debug("Attempting to connect to NES")
ctx.nes_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 52980), timeout=10)
ctx.nes_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.nes_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.nes_status = CONNECTION_REFUSED_STATUS
continue
if __name__ == '__main__':
# Text Mode to use !hint and such with games that have no text entry
Utils.init_logging("ZeldaClient")
options = Utils.get_options()
DISPLAY_MSGS = options["tloz_options"]["display_msgs"]
async def run_game(romfile: str) -> None:
auto_start = typing.cast(typing.Union[bool, str],
Utils.get_options()["tloz_options"].get("rom_start", True))
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
elif isinstance(auto_start, str) and os.path.isfile(auto_start):
subprocess.Popen([auto_start, romfile],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def main(args):
if args.diff_file:
import Patch
logging.info("Patch file was supplied. Creating nes rom..")
meta, romfile = Patch.create_rom_file(args.diff_file)
if "server" in meta:
args.connect = meta["server"]
logging.info(f"Wrote rom file to {romfile}")
async_start(run_game(romfile))
ctx = ZeldaContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.nes_sync_task = asyncio.create_task(nes_sync_task(ctx), name="NES Sync")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.nes_sync_task:
await ctx.nes_sync_task
import colorama
parser = get_base_parser()
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a Archipelago Binary Patch file')
args = parser.parse_args()
colorama.init()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -1,4 +1,21 @@
<TabbedPanel> <TextColors>:
# Hex-format RGB colors used in clients. Resets after an update/install.
# To avoid, you can copy the TextColors section into a new "user.kv" next to this file
# and it will read from there instead.
black: "000000"
red: "EE0000"
green: "00FF7F" # typically a location
yellow: "FAFAD2" # typically other slots/players
blue: "6495ED" # typically extra info (such as entrance)
magenta: "EE00EE" # typically your slot/player
cyan: "00EEEE" # typically regular item
slateblue: "6D8BE8" # typically useful item
plum: "AF99EF" # typically progression item
salmon: "FA8072" # typically trap item
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
<Label>:
color: "FFFFFF"
<TabbedPanel>:
tab_width: root.width / app.tab_count tab_width: root.width / app.tab_count
<SelectableLabel>: <SelectableLabel>:
canvas.before: canvas.before:
@@ -13,6 +30,8 @@
font_size: dp(20) font_size: dp(20)
markup: True markup: True
<UILog>: <UILog>:
messages: 1000 # amount of messages stored in client logs.
cols: 1
viewclass: 'SelectableLabel' viewclass: 'SelectableLabel'
scroll_y: 0 scroll_y: 0
scroll_type: ["content", "bars"] scroll_type: ["content", "bars"]

View File

@@ -7,7 +7,7 @@ local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made" local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
local STATE_UNINITIALIZED = "Uninitialized" local STATE_UNINITIALIZED = "Uninitialized"
local SCRIPT_VERSION = 1 local SCRIPT_VERSION = 3
local APIndex = 0x1A6E local APIndex = 0x1A6E
local APDeathLinkAddress = 0x00FD local APDeathLinkAddress = 0x00FD
@@ -16,7 +16,8 @@ local EventFlagAddress = 0x1735
local MissableAddress = 0x161A local MissableAddress = 0x161A
local HiddenItemsAddress = 0x16DE local HiddenItemsAddress = 0x16DE
local RodAddress = 0x1716 local RodAddress = 0x1716
local InGame = 0x1A71 local DexSanityAddress = 0x1A71
local InGameAddress = 0x1A84
local ClientCompatibilityAddress = 0xFF00 local ClientCompatibilityAddress = 0xFF00
local ItemsReceived = nil local ItemsReceived = nil
@@ -34,6 +35,7 @@ local frame = 0
local u8 = nil local u8 = nil
local wU8 = nil local wU8 = nil
local u16 local u16
local compat = nil
local function defineMemoryFunctions() local function defineMemoryFunctions()
local memDomain = {} local memDomain = {}
@@ -70,18 +72,6 @@ function slice (tbl, s, e)
return new return new
end end
function processBlock(block)
if block == nil then
return
end
local itemsBlock = block["items"]
memDomain.wram()
if itemsBlock ~= nil then
ItemsReceived = itemsBlock
end
deathlink_rec = block["deathlink"]
end
function difference(a, b) function difference(a, b)
local aa = {} local aa = {}
for k,v in pairs(a) do aa[v]=true end for k,v in pairs(a) do aa[v]=true end
@@ -99,6 +89,7 @@ function generateLocationsChecked()
events = uRange(EventFlagAddress, 0x140) events = uRange(EventFlagAddress, 0x140)
missables = uRange(MissableAddress, 0x20) missables = uRange(MissableAddress, 0x20)
hiddenitems = uRange(HiddenItemsAddress, 0x0E) hiddenitems = uRange(HiddenItemsAddress, 0x0E)
dexsanity = uRange(DexSanityAddress, 19)
rod = u8(RodAddress) rod = u8(RodAddress)
data = {} data = {}
@@ -108,6 +99,9 @@ function generateLocationsChecked()
table.foreach(hiddenitems, function(k, v) table.insert(data, v) end) table.foreach(hiddenitems, function(k, v) table.insert(data, v) end)
table.insert(data, rod) table.insert(data, rod)
if compat > 1 then
table.foreach(dexsanity, function(k, v) table.insert(data, v) end)
end
return data return data
end end
@@ -141,7 +135,15 @@ function receive()
return return
end end
if l ~= nil then if l ~= nil then
processBlock(json.decode(l)) block = json.decode(l)
if block ~= nil then
local itemsBlock = block["items"]
if itemsBlock ~= nil then
ItemsReceived = itemsBlock
end
deathlink_rec = block["deathlink"]
end
end end
-- Determine Message to send back -- Determine Message to send back
memDomain.rom() memDomain.rom()
@@ -156,15 +158,31 @@ function receive()
seedName = newSeedName seedName = newSeedName
local retTable = {} local retTable = {}
retTable["scriptVersion"] = SCRIPT_VERSION retTable["scriptVersion"] = SCRIPT_VERSION
retTable["clientCompatibilityVersion"] = u8(ClientCompatibilityAddress)
if compat == nil then
compat = u8(ClientCompatibilityAddress)
if compat < 2 then
InGameAddress = 0x1A71
end
end
retTable["clientCompatibilityVersion"] = compat
retTable["playerName"] = playerName retTable["playerName"] = playerName
retTable["seedName"] = seedName retTable["seedName"] = seedName
memDomain.wram() memDomain.wram()
if u8(InGame) == 0xAC then
in_game = u8(InGameAddress)
if in_game == 0x2A or in_game == 0xAC then
retTable["locations"] = generateLocationsChecked() retTable["locations"] = generateLocationsChecked()
elseif in_game ~= 0 then
print("Game may have crashed")
curstate = STATE_UNINITIALIZED
return
end end
retTable["deathLink"] = deathlink_send retTable["deathLink"] = deathlink_send
deathlink_send = false deathlink_send = false
msg = json.encode(retTable).."\n" msg = json.encode(retTable).."\n"
local ret, error = gbSocket:send(msg) local ret, error = gbSocket:send(msg)
if ret == nil then if ret == nil then
@@ -193,16 +211,23 @@ function main()
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
if (frame % 5 == 0) then if (frame % 5 == 0) then
receive() receive()
if u8(InGame) == 0xAC and u8(APItemAddress) == 0x00 then in_game = u8(InGameAddress)
ItemIndex = u16(APIndex) if in_game == 0x2A or in_game == 0xAC then
if deathlink_rec == true then if u8(APItemAddress) == 0x00 then
wU8(APDeathLinkAddress, 1) ItemIndex = u16(APIndex)
elseif u8(APDeathLinkAddress) == 3 then if deathlink_rec == true then
wU8(APDeathLinkAddress, 0) wU8(APDeathLinkAddress, 1)
deathlink_send = true elseif u8(APDeathLinkAddress) == 3 then
end wU8(APDeathLinkAddress, 0)
if ItemsReceived[ItemIndex + 1] ~= nil then deathlink_send = true
wU8(APItemAddress, ItemsReceived[ItemIndex + 1] - 172000000) end
if ItemsReceived[ItemIndex + 1] ~= nil then
item_id = ItemsReceived[ItemIndex + 1] - 172000000
if item_id > 255 then
item_id = item_id - 256
end
wU8(APItemAddress, item_id)
end
end end
end end
end end

View File

@@ -0,0 +1,702 @@
--Shamelessly based off the FF1 lua
local socket = require("socket")
local json = require('json')
local math = require('math')
local STATE_OK = "Ok"
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
local STATE_UNINITIALIZED = "Uninitialized"
local itemMessages = {}
local consumableStacks = nil
local prevstate = ""
local curstate = STATE_UNINITIALIZED
local zeldaSocket = nil
local frame = 0
local gameMode = 0
local cave_index
local triforce_byte
local game_state
local u8 = nil
local wU8 = nil
local isNesHawk = false
local shopsChecked = {}
local shopSlotLeft = 0x0628
local shopSlotMiddle = 0x0629
local shopSlotRight = 0x062A
--N.B.: you won't find these in a RAM map. They're flag values that the base patch derives from the cave ID.
local blueRingShopBit = 0x40
local potionShopBit = 0x02
local arrowShopBit = 0x08
local candleShopBit = 0x10
local shieldShopBit = 0x20
local takeAnyCaveBit = 0x01
local sword = 0x0657
local bombs = 0x0658
local maxBombs = 0x067C
local keys = 0x066E
local arrow = 0x0659
local bow = 0x065A
local candle = 0x065B
local recorder = 0x065C
local food = 0x065D
local waterOfLife = 0x065E
local magicalRod = 0x065F
local raft = 0x0660
local bookOfMagic = 0x0661
local ring = 0x0662
local stepladder = 0x0663
local magicalKey = 0x0664
local powerBracelet = 0x0665
local letter = 0x0666
local clockItem = 0x066C
local heartContainers = 0x066F
local partialHearts = 0x0670
local triforceFragments = 0x0671
local boomerang = 0x0674
local magicalBoomerang = 0x0675
local magicalShield = 0x0676
local rupeesToAdd = 0x067D
local rupeesToSubtract = 0x067E
local itemsObtained = 0x0677
local takeAnyCavesChecked = 0x0678
local localTriforce = 0x0679
local bonusItemsObtained = 0x067A
itemAPids = {
["Boomerang"] = 7100,
["Bow"] = 7101,
["Magical Boomerang"] = 7102,
["Raft"] = 7103,
["Stepladder"] = 7104,
["Recorder"] = 7105,
["Magical Rod"] = 7106,
["Red Candle"] = 7107,
["Book of Magic"] = 7108,
["Magical Key"] = 7109,
["Red Ring"] = 7110,
["Silver Arrow"] = 7111,
["Sword"] = 7112,
["White Sword"] = 7113,
["Magical Sword"] = 7114,
["Heart Container"] = 7115,
["Letter"] = 7116,
["Magical Shield"] = 7117,
["Candle"] = 7118,
["Arrow"] = 7119,
["Food"] = 7120,
["Water of Life (Blue)"] = 7121,
["Water of Life (Red)"] = 7122,
["Blue Ring"] = 7123,
["Triforce Fragment"] = 7124,
["Power Bracelet"] = 7125,
["Small Key"] = 7126,
["Bomb"] = 7127,
["Recovery Heart"] = 7128,
["Five Rupees"] = 7129,
["Rupee"] = 7130,
["Clock"] = 7131,
["Fairy"] = 7132
}
itemCodes = {
["Boomerang"] = 0x1D,
["Bow"] = 0x0A,
["Magical Boomerang"] = 0x1E,
["Raft"] = 0x0C,
["Stepladder"] = 0x0D,
["Recorder"] = 0x05,
["Magical Rod"] = 0x10,
["Red Candle"] = 0x07,
["Book of Magic"] = 0x11,
["Magical Key"] = 0x0B,
["Red Ring"] = 0x13,
["Silver Arrow"] = 0x09,
["Sword"] = 0x01,
["White Sword"] = 0x02,
["Magical Sword"] = 0x03,
["Heart Container"] = 0x1A,
["Letter"] = 0x15,
["Magical Shield"] = 0x1C,
["Candle"] = 0x06,
["Arrow"] = 0x08,
["Food"] = 0x04,
["Water of Life (Blue)"] = 0x1F,
["Water of Life (Red)"] = 0x20,
["Blue Ring"] = 0x12,
["Triforce Fragment"] = 0x1B,
["Power Bracelet"] = 0x14,
["Small Key"] = 0x19,
["Bomb"] = 0x00,
["Recovery Heart"] = 0x22,
["Five Rupees"] = 0x0F,
["Rupee"] = 0x18,
["Clock"] = 0x21,
["Fairy"] = 0x23
}
--Sets correct memory access functions based on whether NesHawk or QuickNES is loaded
local function defineMemoryFunctions()
local memDomain = {}
local domains = memory.getmemorydomainlist()
if domains[1] == "System Bus" then
--NesHawk
isNesHawk = true
memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
memDomain["ram"] = function() memory.usememorydomain("RAM") end
memDomain["saveram"] = function() memory.usememorydomain("Battery RAM") end
memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
elseif domains[1] == "WRAM" then
--QuickNES
memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
memDomain["ram"] = function() memory.usememorydomain("RAM") end
memDomain["saveram"] = function() memory.usememorydomain("WRAM") end
memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
end
return memDomain
end
local memDomain = defineMemoryFunctions()
u8 = memory.read_u8
wU8 = memory.write_u8
uRange = memory.readbyterange
itemIDNames = {}
for key, value in pairs(itemAPids) do
itemIDNames[value] = key
end
local function determineItem(array)
memdomain.ram()
currentItemsObtained = u8(itemsObtained)
end
local function gotSword()
local currentSword = u8(sword)
wU8(sword, math.max(currentSword, 1))
end
local function gotWhiteSword()
local currentSword = u8(sword)
wU8(sword, math.max(currentSword, 2))
end
local function gotMagicalSword()
wU8(sword, 3)
end
local function gotBomb()
local currentBombs = u8(bombs)
local currentMaxBombs = u8(maxBombs)
wU8(bombs, math.min(currentBombs + 4, currentMaxBombs))
wU8(0x505, 0x29) -- Fake bomb to show item get.
end
local function gotArrow()
local currentArrow = u8(arrow)
wU8(arrow, math.max(currentArrow, 1))
end
local function gotSilverArrow()
wU8(arrow, 2)
end
local function gotBow()
wU8(bow, 1)
end
local function gotCandle()
local currentCandle = u8(candle)
wU8(candle, math.max(currentCandle, 1))
end
local function gotRedCandle()
wU8(candle, 2)
end
local function gotRecorder()
wU8(recorder, 1)
end
local function gotFood()
wU8(food, 1)
end
local function gotWaterOfLifeBlue()
local currentWaterOfLife = u8(waterOfLife)
wU8(waterOfLife, math.max(currentWaterOfLife, 1))
end
local function gotWaterOfLifeRed()
wU8(waterOfLife, 2)
end
local function gotMagicalRod()
wU8(magicalRod, 1)
end
local function gotBookOfMagic()
wU8(bookOfMagic, 1)
end
local function gotRaft()
wU8(raft, 1)
end
local function gotBlueRing()
local currentRing = u8(ring)
wU8(ring, math.max(currentRing, 1))
memDomain.saveram()
local currentTunicColor = u8(0x0B92)
if currentTunicColor == 0x29 then
wU8(0x0B92, 0x32)
wU8(0x0804, 0x32)
end
end
local function gotRedRing()
wU8(ring, 2)
memDomain.saveram()
wU8(0x0B92, 0x16)
wU8(0x0804, 0x16)
end
local function gotStepladder()
wU8(stepladder, 1)
end
local function gotMagicalKey()
wU8(magicalKey, 1)
end
local function gotPowerBracelet()
wU8(powerBracelet, 1)
end
local function gotLetter()
wU8(letter, 1)
end
local function gotHeartContainer()
local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4)
if currentHeartContainers < 16 then
currentHeartContainers = math.min(currentHeartContainers + 1, 16)
local currentHearts = bit.band(u8(heartContainers), 0x0F) + 1
wU8(heartContainers, bit.lshift(currentHeartContainers, 4) + currentHearts)
end
end
local function gotTriforceFragment()
local triforceByte = 0xFF
local newTriforceCount = u8(localTriforce) + 1
wU8(localTriforce, newTriforceCount)
end
local function gotBoomerang()
wU8(boomerang, 1)
end
local function gotMagicalBoomerang()
wU8(magicalBoomerang, 1)
end
local function gotMagicalShield()
wU8(magicalShield, 1)
end
local function gotRecoveryHeart()
local currentHearts = bit.band(u8(heartContainers), 0x0F)
local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4)
if currentHearts < currentHeartContainers then
currentHearts = currentHearts + 1
else
wU8(partialHearts, 0xFF)
end
currentHearts = bit.bor(bit.band(u8(heartContainers), 0xF0), currentHearts)
wU8(heartContainers, currentHearts)
end
local function gotFairy()
local currentHearts = bit.band(u8(heartContainers), 0x0F)
local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4)
if currentHearts < currentHeartContainers then
currentHearts = currentHearts + 3
if currentHearts > currentHeartContainers then
currentHearts = currentHeartContainers
wU8(partialHearts, 0xFF)
end
else
wU8(partialHearts, 0xFF)
end
currentHearts = bit.bor(bit.band(u8(heartContainers), 0xF0), currentHearts)
wU8(heartContainers, currentHearts)
end
local function gotClock()
wU8(clockItem, 1)
end
local function gotFiveRupees()
local currentRupeesToAdd = u8(rupeesToAdd)
wU8(rupeesToAdd, math.min(currentRupeesToAdd + 5, 255))
end
local function gotSmallKey()
wU8(keys, math.min(u8(keys) + 1, 9))
end
local function gotItem(item)
--Write itemCode to itemToLift
--Write 128 to itemLiftTimer
--Write 4 to sound effect queue
itemName = itemIDNames[item]
itemCode = itemCodes[itemName]
wU8(0x505, itemCode)
wU8(0x506, 128)
wU8(0x602, 4)
numberObtained = u8(itemsObtained) + 1
wU8(itemsObtained, numberObtained)
if itemName == "Boomerang" then gotBoomerang() end
if itemName == "Bow" then gotBow() end
if itemName == "Magical Boomerang" then gotMagicalBoomerang() end
if itemName == "Raft" then gotRaft() end
if itemName == "Stepladder" then gotStepladder() end
if itemName == "Recorder" then gotRecorder() end
if itemName == "Magical Rod" then gotMagicalRod() end
if itemName == "Red Candle" then gotRedCandle() end
if itemName == "Book of Magic" then gotBookOfMagic() end
if itemName == "Magical Key" then gotMagicalKey() end
if itemName == "Red Ring" then gotRedRing() end
if itemName == "Silver Arrow" then gotSilverArrow() end
if itemName == "Sword" then gotSword() end
if itemName == "White Sword" then gotWhiteSword() end
if itemName == "Magical Sword" then gotMagicalSword() end
if itemName == "Heart Container" then gotHeartContainer() end
if itemName == "Letter" then gotLetter() end
if itemName == "Magical Shield" then gotMagicalShield() end
if itemName == "Candle" then gotCandle() end
if itemName == "Arrow" then gotArrow() end
if itemName == "Food" then gotFood() end
if itemName == "Water of Life (Blue)" then gotWaterOfLifeBlue() end
if itemName == "Water of Life (Red)" then gotWaterOfLifeRed() end
if itemName == "Blue Ring" then gotBlueRing() end
if itemName == "Triforce Fragment" then gotTriforceFragment() end
if itemName == "Power Bracelet" then gotPowerBracelet() end
if itemName == "Small Key" then gotSmallKey() end
if itemName == "Bomb" then gotBomb() end
if itemName == "Recovery Heart" then gotRecoveryHeart() end
if itemName == "Five Rupees" then gotFiveRupees() end
if itemName == "Fairy" then gotFairy() end
if itemName == "Clock" then gotClock() end
end
local function StateOKForMainLoop()
memDomain.ram()
local gameMode = u8(0x12)
return gameMode == 5
end
local function checkCaveItemObtained()
memDomain.ram()
local returnTable = {}
returnTable["slot1"] = u8(shopSlotLeft)
returnTable["slot2"] = u8(shopSlotMiddle)
returnTable["slot3"] = u8(shopSlotRight)
returnTable["takeAnys"] = u8(takeAnyCavesChecked)
return returnTable
end
function table.empty (self)
for _, _ in pairs(self) do
return false
end
return true
end
function slice (tbl, s, e)
local pos, new = 1, {}
for i = s + 1, e do
new[pos] = tbl[i]
pos = pos + 1
end
return new
end
local bizhawk_version = client.getversion()
local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5")
local is26To28 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7") or (bizhawk_version:sub(1,3)=="2.8")
local function getMaxMessageLength()
if is23Or24Or25 then
return client.screenwidth()/11
elseif is26To28 then
return client.screenwidth()/12
end
end
local function drawText(x, y, message, color)
if is23Or24Or25 then
gui.addmessage(message)
elseif is26To28 then
gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", "middle", "bottom", nil, "client")
end
end
local function clearScreen()
if is23Or24Or25 then
return
elseif is26To28 then
drawText(0, 0, "", "black")
end
end
local function drawMessages()
if table.empty(itemMessages) then
clearScreen()
return
end
local y = 10
found = false
maxMessageLength = getMaxMessageLength()
for k, v in pairs(itemMessages) do
if v["TTL"] > 0 then
message = v["message"]
while true do
drawText(5, y, message:sub(1, maxMessageLength), v["color"])
y = y + 16
message = message:sub(maxMessageLength + 1, message:len())
if message:len() == 0 then
break
end
end
newTTL = 0
if is26To28 then
newTTL = itemMessages[k]["TTL"] - 1
end
itemMessages[k]["TTL"] = newTTL
found = true
end
end
if found == false then
clearScreen()
end
end
function generateOverworldLocationChecked()
memDomain.ram()
data = uRange(0x067E, 0x81)
data[0] = nil
return data
end
function getHCLocation()
memDomain.rom()
data = u8(0x1789A)
return data
end
function getPBLocation()
memDomain.rom()
data = u8(0x10CB2)
return data
end
function generateUnderworld16LocationChecked()
memDomain.ram()
data = uRange(0x06FE, 0x81)
data[0] = nil
return data
end
function generateUnderworld79LocationChecked()
memDomain.ram()
data = uRange(0x077E, 0x81)
data[0] = nil
return data
end
function updateTriforceFragments()
memDomain.ram()
local triforceByte = 0xFF
totalTriforceCount = u8(localTriforce)
local currentPieces = bit.rshift(triforceByte, 8 - math.min(8, totalTriforceCount))
wU8(triforceFragments, currentPieces)
end
function processBlock(block)
if block ~= nil then
local msgBlock = block['messages']
if msgBlock ~= nil then
for i, v in pairs(msgBlock) do
if itemMessages[i] == nil then
local msg = {TTL=450, message=v, color=0xFFFF0000}
itemMessages[i] = msg
end
end
end
local bonusItems = block["bonusItems"]
if bonusItems ~= nil and isInGame then
for i, item in ipairs(bonusItems) do
memDomain.ram()
if i > u8(bonusItemsObtained) then
if u8(0x505) == 0 then
gotItem(item)
wU8(itemsObtained, u8(itemsObtained) - 1)
wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1)
end
end
end
end
local itemsBlock = block["items"]
memDomain.saveram()
isInGame = StateOKForMainLoop()
updateTriforceFragments()
if itemsBlock ~= nil and isInGame then
memDomain.ram()
--get item from item code
--get function from item
--do function
for i, item in ipairs(itemsBlock) do
memDomain.ram()
if u8(0x505) == 0 then
if i > u8(itemsObtained) then
gotItem(item)
end
end
end
end
local shopsBlock = block["shops"]
if shopsBlock ~= nil then
wU8(shopSlotLeft, bit.bor(u8(shopSlotLeft), shopsBlock["left"]))
wU8(shopSlotMiddle, bit.bor(u8(shopSlotMiddle), shopsBlock["middle"]))
wU8(shopSlotRight, bit.bor(u8(shopSlotRight), shopsBlock["right"]))
end
end
end
function difference(a, b)
local aa = {}
for k,v in pairs(a) do aa[v]=true end
for k,v in pairs(b) do aa[v]=nil end
local ret = {}
local n = 0
for k,v in pairs(a) do
if aa[v] then n=n+1 ret[n]=v end
end
return ret
end
function receive()
l, e = zeldaSocket:receive()
if e == 'closed' then
if curstate == STATE_OK then
print("Connection closed")
end
curstate = STATE_UNINITIALIZED
return
elseif e == 'timeout' then
print("timeout")
return
elseif e ~= nil then
print(e)
curstate = STATE_UNINITIALIZED
return
end
processBlock(json.decode(l))
-- Determine Message to send back
memDomain.rom()
local playerName = uRange(0x1F, 0x10)
playerName[0] = nil
local retTable = {}
retTable["playerName"] = playerName
if StateOKForMainLoop() then
retTable["overworld"] = generateOverworldLocationChecked()
retTable["underworld1"] = generateUnderworld16LocationChecked()
retTable["underworld2"] = generateUnderworld79LocationChecked()
end
retTable["caves"] = checkCaveItemObtained()
memDomain.ram()
if gameMode ~= 19 then
gameMode = u8(0x12)
end
retTable["gameMode"] = gameMode
retTable["overworldHC"] = getHCLocation()
retTable["overworldPB"] = getPBLocation()
retTable["itemsObtained"] = u8(itemsObtained)
msg = json.encode(retTable).."\n"
local ret, error = zeldaSocket:send(msg)
if ret == nil then
print(error)
elseif curstate == STATE_INITIAL_CONNECTION_MADE then
curstate = STATE_TENTATIVELY_CONNECTED
elseif curstate == STATE_TENTATIVELY_CONNECTED then
print("Connected!")
itemMessages["(0,0)"] = {TTL=240, message="Connected", color="green"}
curstate = STATE_OK
end
end
function main()
if (is23Or24Or25 or is26To28) == false then
print("Must use a version of bizhawk 2.3.1 or higher")
return
end
server, error = socket.bind('localhost', 52980)
while true do
gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
frame = frame + 1
drawMessages()
if not (curstate == prevstate) then
-- console.log("Current state: "..curstate)
prevstate = curstate
end
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
if (frame % 60 == 0) then
gui.drawEllipse(248, 9, 6, 6, "Black", "Blue")
receive()
else
gui.drawEllipse(248, 9, 6, 6, "Black", "Green")
end
elseif (curstate == STATE_UNINITIALIZED) then
gui.drawEllipse(248, 9, 6, 6, "Black", "White")
if (frame % 60 == 0) then
gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
drawText(5, 8, "Waiting for client", 0xFFFF0000)
drawText(5, 32, "Please start Zelda1Client.exe", 0xFFFF0000)
-- Advance so the messages are drawn
emu.frameadvance()
server:settimeout(2)
print("Attempting to connect")
local client, timeout = server:accept()
if timeout == nil then
-- print('Initial Connection Made')
curstate = STATE_INITIAL_CONNECTION_MADE
zeldaSocket = client
zeldaSocket:settimeout(0)
end
end
end
emu.frameadvance()
end
end
main()

BIN
data/lua/TLoZ/core.dll Normal file

Binary file not shown.

380
data/lua/TLoZ/json.lua Normal file
View File

@@ -0,0 +1,380 @@
--
-- json.lua
--
-- Copyright (c) 2015 rxi
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.
--
local json = { _version = "0.1.0" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\\\",
[ "\"" ] = "\\\"",
[ "\b" ] = "\\b",
[ "\f" ] = "\\f",
[ "\n" ] = "\\n",
[ "\r" ] = "\\r",
[ "\t" ] = "\\t",
}
local escape_char_map_inv = { [ "\\/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte())
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if val[1] ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
--local line_count = 1
--local col_count = 1
--for i = 1, idx - 1 do
-- col_count = col_count + 1
-- if str:sub(i, i) == "\n" then
-- line_count = line_count + 1
-- col_count = 1
-- end
-- end
-- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(3, 6), 16 )
local n2 = tonumber( s:sub(9, 12), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local has_unicode_escape = false
local has_surrogate_escape = false
local has_escape = false
local last
for j = i + 1, #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
end
if last == 92 then -- "\\" (escape char)
if x == 117 then -- "u" (unicode escape sequence)
local hex = str:sub(j + 1, j + 5)
if not hex:find("%x%x%x%x") then
decode_error(str, j, "invalid unicode escape in string")
end
if hex:find("^[dD][89aAbB]") then
has_surrogate_escape = true
else
has_unicode_escape = true
end
else
local c = string.char(x)
if not escape_chars[c] then
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
end
has_escape = true
end
last = nil
elseif x == 34 then -- '"' (end of string)
local s = str:sub(i + 1, j - 1)
if has_surrogate_escape then
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
end
if has_unicode_escape then
s = s:gsub("\\u....", parse_unicode_escape)
end
if has_escape then
s = s:gsub("\\.", escape_char_map_inv)
end
return s, j + 1
else
last = x
end
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
return ( parse(str, next_char(str, 1, space_chars, true)) )
end
return json

132
data/lua/TLoZ/socket.lua Normal file
View File

@@ -0,0 +1,132 @@
-----------------------------------------------------------------------------
-- LuaSocket helper module
-- Author: Diego Nehab
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module and import dependencies
-----------------------------------------------------------------------------
local base = _G
local string = require("string")
local math = require("math")
local socket = require("socket.core")
module("socket")
-----------------------------------------------------------------------------
-- Exported auxiliar functions
-----------------------------------------------------------------------------
function connect(address, port, laddress, lport)
local sock, err = socket.tcp()
if not sock then return nil, err end
if laddress then
local res, err = sock:bind(laddress, lport, -1)
if not res then return nil, err end
end
local res, err = sock:connect(address, port)
if not res then return nil, err end
return sock
end
function bind(host, port, backlog)
local sock, err = socket.tcp()
if not sock then return nil, err end
sock:setoption("reuseaddr", true)
local res, err = sock:bind(host, port)
if not res then return nil, err end
res, err = sock:listen(backlog)
if not res then return nil, err end
return sock
end
try = newtry()
function choose(table)
return function(name, opt1, opt2)
if base.type(name) ~= "string" then
name, opt1, opt2 = "default", name, opt1
end
local f = table[name or "nil"]
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
else return f(opt1, opt2) end
end
end
-----------------------------------------------------------------------------
-- Socket sources and sinks, conforming to LTN12
-----------------------------------------------------------------------------
-- create namespaces inside LuaSocket namespace
sourcet = {}
sinkt = {}
BLOCKSIZE = 2048
sinkt["close-when-done"] = function(sock)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function(self, chunk, err)
if not chunk then
sock:close()
return 1
else return sock:send(chunk) end
end
})
end
sinkt["keep-open"] = function(sock)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function(self, chunk, err)
if chunk then return sock:send(chunk)
else return 1 end
end
})
end
sinkt["default"] = sinkt["keep-open"]
sink = choose(sinkt)
sourcet["by-length"] = function(sock, length)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function()
if length <= 0 then return nil end
local size = math.min(socket.BLOCKSIZE, length)
local chunk, err = sock:receive(size)
if err then return nil, err end
length = length - string.len(chunk)
return chunk
end
})
end
sourcet["until-closed"] = function(sock)
local done
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function()
if done then return nil end
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
if not err then return chunk
elseif err == "closed" then
sock:close()
done = 1
return partial
else return nil, err end
end
})
end
sourcet["default"] = sourcet["until-closed"]
source = choose(sourcet)

View File

@@ -9,7 +9,7 @@ These steps should be followed in order to establish a gameplay connection with
5. Client sends [Connect](#Connect) packet in order to authenticate with the server. 5. Client sends [Connect](#Connect) packet in order to authenticate with the server.
6. Server validates the client's packet and responds with [Connected](#Connected) or [ConnectionRefused](#ConnectionRefused). 6. Server validates the client's packet and responds with [Connected](#Connected) or [ConnectionRefused](#ConnectionRefused).
7. Server may send [ReceivedItems](#ReceivedItems) to the client, in the case that the client is missing items that are queued up for it. 7. Server may send [ReceivedItems](#ReceivedItems) to the client, in the case that the client is missing items that are queued up for it.
8. Server sends [Print](#Print) to all players to notify them of the new client connection. 8. Server sends [PrintJSON](#PrintJSON) to all players to notify them of the new client connection.
In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet. In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet.
@@ -54,7 +54,6 @@ These packets are are sent from the multiworld server to the client. They are no
* [ReceivedItems](#ReceivedItems) * [ReceivedItems](#ReceivedItems)
* [LocationInfo](#LocationInfo) * [LocationInfo](#LocationInfo)
* [RoomUpdate](#RoomUpdate) * [RoomUpdate](#RoomUpdate)
* [Print](#Print)
* [PrintJSON](#PrintJSON) * [PrintJSON](#PrintJSON)
* [DataPackage](#DataPackage) * [DataPackage](#DataPackage)
* [Bounced](#Bounced) * [Bounced](#Bounced)
@@ -160,35 +159,44 @@ The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring:
All arguments for this packet are optional, only changes are sent. All arguments for this packet are optional, only changes are sent.
### Print
Sent to clients purely to display a message to the player.
* *Deprecation warning: clients that connect with version 0.3.5 or higher will nolonger recieve Print packets, instead all messsages are send as [PrintJSON](#PrintJSON)*
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| text | str | Message to display to player. |
### PrintJSON ### PrintJSON
Sent to clients purely to display a message to the player. This packet differs from [Print](#Print) in that the data being sent with this packet allows for more configurable or specific messaging. Sent to clients purely to display a message to the player. While various message types provide additional arguments, clients only need to evaluate the `data` argument to construct the human-readable message text. All other arguments may be ignored safely.
#### Arguments #### Arguments
| Name | Type | Notes | | Name | Type | Message Types | Contents |
| ---- | ---- | ----- | | ---- | ---- | ------------- | -------- |
| data | list\[[JSONMessagePart](#JSONMessagePart)\] | Type of this part of the message. | | data | list\[[JSONMessagePart](#JSONMessagePart)\] | (all) | Textual content of this message |
| type | str | May be present to indicate the [PrintJsonType](#PrintJsonType) of this message. | | type | str | (any) | [PrintJsonType](#PrintJsonType) of this message (optional) |
| receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. | | receiving | int | ItemSend, ItemCheat, Hint | Destination player's ID |
| item | [NetworkItem](#NetworkItem) | Is present if type is Hint or ItemSend and marks the source player id, location id, item id and item flags. | | item | [NetworkItem](#NetworkItem) | ItemSend, ItemCheat, Hint | Source player's ID, location ID, item ID and item flags |
| found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. | | found | bool | Hint | Whether the location hinted for was checked |
| countdown | int | Is present if type is `Countdown`, denotes the amount of seconds remaining on the countdown. | | team | int | Join, Part, Chat, TagsChanged, Goal, Release, Collect, ItemCheat | Team of the triggering player |
| slot | int | Join, Part, Chat, TagsChanged, Goal, Release, Collect | Slot of the triggering player |
| message | str | Chat, ServerChat | Original chat message without sender prefix |
| tags | list\[str\] | Join, TagsChanged | Tags of the triggering player |
| countdown | int | Countdown | Amount of seconds remaining on the countdown |
##### PrintJsonType #### PrintJsonType
PrintJsonType indicates the type of [PrintJson](#PrintJson) packet, different types can be handled differently by the client and can also contain additional arguments. When receiving an unknown type the data's list\[[JSONMessagePart](#JSONMessagePart)\] should still be printed as normal. PrintJsonType indicates the type of a [PrintJSON](#PrintJSON) packet. Different types can be handled differently by the client and can also contain additional arguments. When receiving an unknown or missing type, the `data`'s list\[[JSONMessagePart](#JSONMessagePart)\] should still be displayed to the player as normal text.
Currently defined types are: Currently defined types are:
| Type | Notes |
| ---- | ----- | | Type | Subject |
| ItemSend | The message is in response to a player receiving an item. | | ---- | ------- |
| Hint | The message is in response to a player hinting. | | ItemSend | A player received an item. |
| Countdown | The message contains information about the current server Countdown. | | ItemCheat | A player used the `!getitem` command. |
| Hint | A player hinted. |
| Join | A player connected. |
| Part | A player disconnected. |
| Chat | A player sent a chat message. |
| ServerChat | The server broadcasted a message. |
| Tutorial | The client has triggered a tutorial message, such as when first connecting. |
| TagsChanged | A player changed their tags. |
| CommandResult | Someone (usually the client) entered an `!` command. |
| AdminCommandResult | The client entered an `!admin` command. |
| Goal | A player reached their goal. |
| Release | A player released the remaining items in their world. |
| Collect | A player collected the remaining items for their world. |
| Countdown | The current server countdown has progressed. |
### DataPackage ### DataPackage
Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most easily communicate with the Archipelago server. Contents include things like location id to name mappings, among others; see [Data Package Contents](#Data-Package-Contents) for more info. Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most easily communicate with the Archipelago server. Contents include things like location id to name mappings, among others; see [Data Package Contents](#Data-Package-Contents) for more info.

188
docs/options api.md Normal file
View File

@@ -0,0 +1,188 @@
# Archipelago Options API
This document covers some of the generic options available using Archipelago's options handling system.
For more information on where these options go in your world please refer to:
- [world api.md](/docs/world%20api.md)
Archipelago will be abbreviated as "AP" from now on.
## Option Definitions
Option parsing in AP is done using different Option classes. For each option you would like to have in your game, you
need to create:
- A new option class with a docstring detailing what the option will do to your user.
- A `display_name` to be displayed on the webhost.
- A new entry in the `option_definitions` dict for your World.
By style and convention, the internal names should be snake_case. If the option supports having multiple sub_options
such as Choice options, these can be defined with `option_my_sub_option`, where the preceding `option_` is required and
stripped for users, so will show as `my_sub_option` in yaml files and if `auto_display_name` is True `My Sub Option`
on the webhost. All options support `random` as a generic option. `random` chooses from any of the available
values for that option, and is reserved by AP. You can set this as your default value but you cannot define your own
new `option_random`.
### Option Creation
As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's
create our option class (with a docstring), give it a `display_name`, and add it to a dictionary that keeps track of our
options:
```python
# Options.py
class StartingSword(Toggle):
"""Adds a sword to your starting inventory."""
display_name = "Start With Sword"
example_options = {
"starting_sword": StartingSword
}
```
This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it
to our world's `__init__.py`:
```python
from worlds.AutoWorld import World
from .Options import options
class ExampleWorld(World):
option_definitions = options
```
### Option Checking
Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after
world instantiation. These are created as attributes on the MultiWorld and can be accessed with
`self.multiworld.my_option_name[self.player]`. This is the option class, which supports direct comparison methods to
relevant objects (like comparing a Toggle class to a `bool`). If you need to access the option result directly, this is
the option class's `value` attribute. For our example above we can do a simple check:
```python
if self.multiworld.starting_sword[self.player]:
do_some_things()
```
or if I need a boolean object, such as in my slot_data I can access it as:
```python
start_with_sword = bool(self.multiworld.starting_sword[self.player].value)
```
## Generic Option Classes
These options are generically available to every game automatically, but can be overridden for slightly different
behavior, if desired. See `worlds/soe/Options.py` for an example.
### Accessibility
Sets rules for availability of locations for the player. `Items` is for all items available but not necessarily all
locations, such as self-locking keys, but needs to be set by the world for this to be different from locations access.
### ProgressionBalancing
Algorithm for moving progression items into earlier spheres to make the gameplay experience a bit smoother. Can be
overridden if you want a different default value.
### LocalItems
Forces the players' items local to their world.
### NonLocalItems
Forces the players' items outside their world.
### StartInventory
Allows the player to define a dictionary of starting items with item name and quantity.
### StartHints
Gives the player starting hints for where the items defined here are.
### StartLocationHints
Gives the player starting hints for the items on locations defined here.
### ExcludeLocations
Marks locations given here as `LocationProgressType.Excluded` so that progression items can't be placed on them.
### PriorityLocations
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them.
### ItemLinks
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between
two players will combine their items in the link into a single item, which then gets replaced with `World.create_filler()`.
## Basic Option Classes
### Toggle
The example above. This simply has 0 and 1 as its available results with 0 (false) being the default value. Cannot be
compared to strings but can be directly compared to True and False.
### DefaultOnToggle
Like Toggle, but 1 (true) is the default value.
### Choice
A numeric option allowing you to define different sub options. Values are stored as integers, but you can also do
comparison methods with the class and strings, so if you have an `option_early_sword`, this can be compared with:
```python
if self.multiworld.sword_availability[self.player] == "early_sword":
do_early_sword_things()
```
or:
```python
from .Options import SwordAvailability
if self.multiworld.sword_availability[self.player] == SwordAvailability.option_early_sword:
do_early_sword_things()
```
### Range
A numeric option allowing a variety of integers including the endpoints. Has a default `range_start` of 0 and default
`range_end` of 1. Allows for negative values as well. This will always be an integer and has no methods for string
comparisons.
### SpecialRange
Like range but also allows you to define a dictionary of special names the user can use to equate to a specific value.
For example:
```python
special_range_names: {
"normal": 20,
"extreme": 99,
}
```
will let users use the names "normal" or "extreme" in their options selections, but will still return those as integers
to you. Useful if you want special handling regarding those specified values.
## More Advanced Options
### FreeText
This is an option that allows the user to enter any possible string value. Can only be compared with strings, and has
no validation step, so if this needs to be validated, you can either add a validation step to the option class or
within the world.
### TextChoice
Like choice allows you to predetermine options and has all of the same comparison methods and handling. Also accepts any
user defined string as a valid option, so will either need to be validated by adding a validation step to the option
class or within world, if necessary. Value for this class is `Union[str, int]` so if you need the value at a specified
point, `self.multiworld.my_option[self.player].current_key` will always return a string.
### PlandoBosses
An option specifically built for handling boss rando, if your game can use it. Is a subclass of TextChoice so supports
everything it does, as well as having multiple validation steps to automatically support boss plando from users. If
using this class, you must define `bosses`, a set of valid boss names, and `locations`, a set of valid boss location
names, and `def can_place_boss`, which passes a boss and location, allowing you to check if that placement is valid for
your game. When this function is called, `bosses`, `locations`, and the passed strings will all be lowercase. There is
also a `duplicate_bosses` attribute allowing you to define if a boss can be placed multiple times in your world. False
by default, and will reject duplicate boss names from the user. For an example of using this class, refer to
`worlds.alttp.options.py`
### OptionDict
This option returns a dictionary. Setting a default here is recommended as it will output the dictionary to the
template. If you set a [Schema](https://pypi.org/project/schema/) on the class with `schema = Schema()`, then the
options system will automatically validate the user supplied data against the schema to ensure it's in the correct
format.
### ItemDict
Like OptionDict, except this will verify that every key in the dictionary is a valid name for an item for your world.
### OptionList
This option defines a List, where the user can add any number of strings to said list, allowing duplicate values. You
can define a set of keys in `valid_keys`, and a default list if you want certain options to be available without editing
for this. If `valid_keys_casefold` is true, the verification will be case-insensitive; `verify_item_name` will check
that each value is a valid item name; and`verify_location_name` will check that each value is a valid location name.
### OptionSet
Like OptionList, but returns a set, preventing duplicates.
### ItemSet
Like OptionSet, but will verify that all the items in the set are a valid name for an item for your world.

View File

@@ -18,6 +18,7 @@
* Use type annotations where possible for function signatures and class members. * Use type annotations where possible for function signatures and class members.
* Use type annotations where appropriate for local variables (e.g. `var: List[int] = []`, or when the * Use type annotations where appropriate for local variables (e.g. `var: List[int] = []`, or when the
type is hard or impossible to deduce.) Clear annotations help developers look up and validate API calls. type is hard or impossible to deduce.) Clear annotations help developers look up and validate API calls.
* Worlds that do not follow PEP8 should still have a consistent style across its files to make reading easier.
## Markdown ## Markdown

View File

@@ -48,5 +48,5 @@
# TODO # TODO
#JSON_AS_ASCII: false #JSON_AS_ASCII: false
# Patch target. This is the address encoded into the patch that will be used for client auto-connect. # Host Address. This is the address encoded into the patch that will be used for client auto-connect.
#PATCH_TARGET: archipelago.gg #HOST_ADDRESS: archipelago.gg

View File

@@ -402,40 +402,43 @@ The world has to provide the following things for generation
* additions to the regions list: at least one called "Menu" * additions to the regions list: at least one called "Menu"
* locations placed inside those regions * locations placed inside those regions
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand * a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
* applying `self.world.push_precollected` for start inventory * applying `self.multiworld.push_precollected` for start inventory
* a `def generate_output(self, output_directory: str)` that creates the output * `required_client_version: Tuple(int, int, int)`
files if there is output to be generated. When this is Optional client version as tuple of 3 ints to make sure the client is compatible to
called, `self.world.get_locations(self.player)` has all locations for the player, with this world (e.g. implements all required features) when connecting.
attribute `item` pointing to the item.
`location.item.player` can be used to see if it's a local item.
In addition, the following methods can be implemented and attributes can be set In addition, the following methods can be implemented and are called in this order during generation
* `stage_assert_generate(cls, multiworld)` is a class method called at the start of
generation to check the existence of prerequisite files, usually a ROM for
games which require one.
* `def generate_early(self)` * `def generate_early(self)`
called per player before any items or locations are created. You can set called per player before any items or locations are created. You can set
properties on your world here. Already has access to player options and RNG. properties on your world here. Already has access to player options and RNG.
* `def create_regions(self)` * `def create_regions(self)`
called to place player's regions and their locations into the MultiWorld's regions list. If it's called to place player's regions and their locations into the MultiWorld's regions list. If it's
hard to separate, this can be done during `generate_early` or `basic` as well. hard to separate, this can be done during `generate_early` or `create_items` as well.
* `def create_items(self)` * `def create_items(self)`
called to place player's items into the MultiWorld's itempool. called to place player's items into the MultiWorld's itempool. After this step all regions and items have to be in
the MultiWorld's regions and itempool, and these lists should not be modified afterwards.
* `def set_rules(self)` * `def set_rules(self)`
called to set access and item rules on locations and entrances. called to set access and item rules on locations and entrances.
Locations have to be defined before this, or rule application can miss them. Locations have to be defined before this, or rule application can miss them.
* `def generate_basic(self)` * `def generate_basic(self)`
called after the previous steps. Some placement and player specific called after the previous steps. Some placement and player specific
randomizations can be done here. After this step all regions and items have randomizations can be done here.
to be in the MultiWorld's regions and itempool.
* `pre_fill`, `fill_hook` and `post_fill` are called to modify item placement * `pre_fill`, `fill_hook` and `post_fill` are called to modify item placement
before, during and after the regular fill process, before `generate_output`. before, during and after the regular fill process, before `generate_output`.
If items need to be placed during pre_fill, these items can be determined
and created using `get_prefill_items`
* `def generate_output(self, output_directory: str)` that creates the output
files if there is output to be generated. When this is
called, `self.multiworld.get_locations(self.player)` has all locations for the player, with
attribute `item` pointing to the item.
`location.item.player` can be used to see if it's a local item.
* `fill_slot_data` and `modify_multidata` can be used to modify the data that * `fill_slot_data` and `modify_multidata` can be used to modify the data that
will be used by the server to host the MultiWorld. will be used by the server to host the MultiWorld.
* `required_client_version: Tuple(int, int, int)`
Client version as tuple of 3 ints to make sure the client is compatible to
this world (e.g. implements all required features) when connecting.
* `assert_generate(cls, world)` is a class method called at the start of
generation to check the existence of prerequisite files, usually a ROM for
games which require one.
#### generate_early #### generate_early
@@ -497,21 +500,21 @@ def create_items(self) -> None:
```python ```python
def create_regions(self) -> None: def create_regions(self) -> None:
# Add regions to the multiworld. "Menu" is the required starting point. # Add regions to the multiworld. "Menu" is the required starting point.
# Arguments to Region() are name, type, human_readable_name, player, world # Arguments to Region() are name, player, world, and optionally hint_text
r = Region("Menu", RegionType.Generic, "Menu", self.player, self.multiworld) r = Region("Menu", self.player, self.multiworld)
# Set Region.exits to a list of entrances that are reachable from region # Set Region.exits to a list of entrances that are reachable from region
r.exits = [Entrance(self.player, "New game", r)] # or use r.exits.append r.exits = [Entrance(self.player, "New game", r)] # or use r.exits.append
# Append region to MultiWorld's regions # Append region to MultiWorld's regions
self.multiworld.regions.append(r) # or use += [r...] self.multiworld.regions.append(r) # or use += [r...]
r = Region("Main Area", RegionType.Generic, "Main Area", self.player, self.multiworld) r = Region("Main Area", self.player, self.multiworld)
# Add main area's locations to main area (all but final boss) # Add main area's locations to main area (all but final boss)
r.locations = [MyGameLocation(self.player, location.name, r.locations = [MyGameLocation(self.player, location.name,
self.location_name_to_id[location.name], r)] self.location_name_to_id[location.name], r)]
r.exits = [Entrance(self.player, "Boss Door", r)] r.exits = [Entrance(self.player, "Boss Door", r)]
self.multiworld.regions.append(r) self.multiworld.regions.append(r)
r = Region("Boss Room", RegionType.Generic, "Boss Room", self.player, self.multiworld) r = Region("Boss Room", self.player, self.multiworld)
# add event to Boss Room # add event to Boss Room
r.locations = [MyGameLocation(self.player, "Final Boss", None, r)] r.locations = [MyGameLocation(self.player, "Final Boss", None, r)]
self.multiworld.regions.append(r) self.multiworld.regions.append(r)
@@ -680,3 +683,60 @@ def generate_output(self, output_directory: str):
generate_mod(src, out_file, data) generate_mod(src, out_file, data)
``` ```
### Documentation
Each world implementation should have a tutorial and a game info page. These are both rendered on the website by reading
the `.md` files in your world's `/docs` directory.
#### Game Info
The game info page is for a short breakdown of what your game is and how it works in Archipelago. Any additional
information that may be useful to the player when learning your randomizer should also go here. The file name format
is `<language key>_<game name>.md`. While you can write these docs for multiple languages, currently only the english
version is displayed on the website.
#### Tutorials
Your game can have as many tutorials in as many languages as you like, with each one having a relevant `Tutorial`
defined in the `WebWorld`. The file name you use aren't particularly important, but it should be descriptive of what
the tutorial is covering, and the name of the file must match the relative URL provided in the `Tutorial`. Currently,
the JS that determines this ignores the provided file name and will search for `game/document_lang.md`, where
`game/document/lang` is the provided URL.
### Tests
Each world is expected to include unit tests that cover its logic, to ensure no logic bug regressions occur. This can be
done by creating a `/test` package within your world package. The `__init__.py` within this folder is where the world's
TestBase should be defined. This can be inherited from the main TestBase, which will automatically set up a solo
multiworld for each test written using it. Within subsequent modules, classes should be defined which inherit the world
TestBase, and can then define options to test in the class body, and run tests in each test method.
Example `__init__.py`
```python
from test.TestBase import WorldTestBase
class MyGameTestBase(WorldTestBase):
game = "My Game"
```
Next using the rules defined in the above `set_rules` we can test that the chests have the correct access rules.
Example `testChestAccess.py`
```python
from . import MyGameTestBase
class TestChestAccess(MyGameTestBase):
def testSwordChests(self):
"""Test locations that require a sword"""
locations = ["Chest1", "Chest2"]
items = [["Sword"]]
# this will test that each location can't be accessed without the "Sword", but can be accessed once obtained.
self.assertAccessDependency(locations, items)
def testAnyWeaponChests(self):
"""Test locations that require any weapon"""
locations = [f"Chest{i}" for i in range(3, 6)]
items = [["Sword"], ["Axe"], ["Spear"]]
# this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them.
self.assertAccessDependency(locations, items)
```

View File

@@ -107,7 +107,7 @@ factorio_options:
filter_item_sends: false filter_item_sends: false
# Whether to send chat messages from players on the Factorio server to Archipelago. # Whether to send chat messages from players on the Factorio server to Archipelago.
bridge_chat_out: true bridge_chat_out: true
minecraft_options: minecraft_options:
forge_directory: "Minecraft Forge server" forge_directory: "Minecraft Forge server"
max_heap_size: "2G" max_heap_size: "2G"
# release channel, currently "release", or "beta" # release channel, currently "release", or "beta"
@@ -125,6 +125,15 @@ soe_options:
rom_file: "Secret of Evermore (USA).sfc" rom_file: "Secret of Evermore (USA).sfc"
ffr_options: ffr_options:
display_msgs: true display_msgs: true
tloz_options:
# File name of the Zelda 1
rom_file: "Legend of Zelda, The (U) (PRG0) [!].nes"
# Set this to false to never autostart a rom (such as after patching)
# true for operating system default program
# Alternatively, a path to a program to open the .nes file with
rom_start: true
# Display message inside of Bizhawk
display_msgs: true
dkc3_options: dkc3_options:
# File name of the DKC3 US rom # File name of the DKC3 US rom
rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc" rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
@@ -139,6 +148,12 @@ pokemon_rb_options:
# True for operating system default program # True for operating system default program
# Alternatively, a path to a program to open the .gb file with # Alternatively, a path to a program to open the .gb file with
rom_start: true rom_start: true
wargroove_options:
# Locate the Wargroove root directory on your system.
# This is used by the Wargroove client, so it knows where to send communication files to
root_directory: "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
zillion_options: zillion_options:
# File name of the Zillion US rom # File name of the Zillion US rom
rom_file: "Zillion (UE) [!].sms" rom_file: "Zillion (UE) [!].sms"

View File

@@ -25,9 +25,9 @@ OutputBaseFilename=Setup {#MyAppName} {#MyAppVersionText}
Compression=lzma2 Compression=lzma2
SolidCompression=yes SolidCompression=yes
LZMANumBlockThreads=8 LZMANumBlockThreads=8
ArchitecturesInstallIn64BitMode=x64 ArchitecturesInstallIn64BitMode=x64 arm64
ChangesAssociations=yes ChangesAssociations=yes
ArchitecturesAllowed=x64 ArchitecturesAllowed=x64 arm64
AllowNoIcons=yes AllowNoIcons=yes
SetupIconFile={#MyAppIcon} SetupIconFile={#MyAppIcon}
UninstallDisplayIcon={app}\{#MyAppExeName} UninstallDisplayIcon={app}\{#MyAppExeName}

28
kvui.py
View File

@@ -1,7 +1,6 @@
import os import os
import logging import logging
import typing import typing
import asyncio
os.environ["KIVY_NO_CONSOLELOG"] = "1" os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1" os.environ["KIVY_NO_FILELOG"] = "1"
@@ -26,6 +25,7 @@ from kivy.base import ExceptionHandler, ExceptionManager
from kivy.clock import Clock from kivy.clock import Clock
from kivy.factory import Factory from kivy.factory import Factory
from kivy.properties import BooleanProperty, ObjectProperty from kivy.properties import BooleanProperty, ObjectProperty
from kivy.uix.widget import Widget
from kivy.uix.button import Button from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout from kivy.uix.gridlayout import GridLayout
from kivy.uix.layout import Layout from kivy.uix.layout import Layout
@@ -508,7 +508,7 @@ class LogtoUI(logging.Handler):
class UILog(RecycleView): class UILog(RecycleView):
cols = 1 messages: typing.ClassVar[int] # comes from kv file
def __init__(self, *loggers_to_handle, **kwargs): def __init__(self, *loggers_to_handle, **kwargs):
super(UILog, self).__init__(**kwargs) super(UILog, self).__init__(**kwargs)
@@ -518,9 +518,15 @@ class UILog(RecycleView):
def on_log(self, record: str) -> None: def on_log(self, record: str) -> None:
self.data.append({"text": escape_markup(record)}) self.data.append({"text": escape_markup(record)})
self.clean_old()
def on_message_markup(self, text): def on_message_markup(self, text):
self.data.append({"text": text}) self.data.append({"text": text})
self.clean_old()
def clean_old(self):
if len(self.data) > self.messages:
self.data.pop(0)
def fix_heights(self): def fix_heights(self):
"""Workaround fix for divergent texture and layout heights""" """Workaround fix for divergent texture and layout heights"""
@@ -538,6 +544,19 @@ class E(ExceptionHandler):
class KivyJSONtoTextParser(JSONtoTextParser): class KivyJSONtoTextParser(JSONtoTextParser):
# dummy class to absorb kvlang definitions
class TextColors(Widget):
pass
def __init__(self, *args, **kwargs):
# we grab the color definitions from the .kv file, then overwrite the JSONtoTextParser default entries
colors = self.TextColors()
color_codes = self.color_codes.copy()
for name, code in color_codes.items():
color_codes[name] = getattr(colors, name, code)
self.color_codes = color_codes
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
self.ref_count = 0 self.ref_count = 0
return super(KivyJSONtoTextParser, self).__call__(*args, **kwargs) return super(KivyJSONtoTextParser, self).__call__(*args, **kwargs)
@@ -587,3 +606,8 @@ class KivyJSONtoTextParser(JSONtoTextParser):
ExceptionManager.add_handler(E()) ExceptionManager.add_handler(E())
Builder.load_file(Utils.local_path("data", "client.kv")) Builder.load_file(Utils.local_path("data", "client.kv"))
user_file = Utils.local_path("data", "user.kv")
if os.path.exists(user_file):
logging.info("Loading user.kv into builder.")
Builder.load_file(user_file)

118
setup.py
View File

@@ -7,30 +7,21 @@ import sys
import sysconfig import sysconfig
import typing import typing
import zipfile import zipfile
import urllib.request
import io
import json
import threading
import subprocess
import pkg_resources
from collections.abc import Iterable from collections.abc import Iterable
from hashlib import sha3_512 from hashlib import sha3_512
from pathlib import Path from pathlib import Path
if __name__ == "__main__":
import ModuleUpdate
ModuleUpdate.update()
from Launcher import components, icon_paths
from Utils import version_tuple, is_windows, is_linux
# On Python < 3.10 LogicMixin is not currently supported.
apworlds: set = {
"Subnautica",
"Factorio",
"Rogue Legacy",
}
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
import subprocess
import pkg_resources
requirement = 'cx-Freeze>=6.14.1'
try: try:
requirement = 'cx-Freeze>=6.14.7'
pkg_resources.require(requirement) pkg_resources.require(requirement)
import cx_Freeze import cx_Freeze
except pkg_resources.ResolutionError: except pkg_resources.ResolutionError:
@@ -42,6 +33,92 @@ except pkg_resources.ResolutionError:
# .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line # .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line
import setuptools.command.build import setuptools.command.build
if __name__ == "__main__":
# need to run this early to import from Utils and Launcher
# TODO: move stuff to not require this
import ModuleUpdate
ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv)
ModuleUpdate.update_ran = False # restore for later
from Launcher import components, icon_paths
from Utils import version_tuple, is_windows, is_linux
# On Python < 3.10 LogicMixin is not currently supported.
apworlds: set = {
"Subnautica",
"Factorio",
"Rogue Legacy",
"Donkey Kong Country 3",
"Super Mario World",
"Stardew Valley",
"Timespinner",
"Minecraft",
"The Messenger",
}
def download_SNI():
print("Updating SNI")
machine_to_go = {
"x86_64": "amd64",
"aarch64": "arm64",
"armv7l": "arm"
}
platform_name = platform.system().lower()
machine_name = platform.machine().lower()
# force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH
machine_name = "amd64" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name)
with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request:
data = json.load(request)
files = data["assets"]
source_url = None
for file in files:
download_url: str = file["browser_download_url"]
machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name
if platform_name in download_url and machine_match:
# prefer "many" builds
if "many" in download_url:
source_url = download_url
break
source_url = download_url
if source_url and source_url.endswith(".zip"):
with urllib.request.urlopen(source_url) as download:
with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf:
for member in zf.infolist():
zf.extract(member, path="SNI")
print(f"Downloaded SNI from {source_url}")
elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")):
import tarfile
mode = "r:xz" if source_url.endswith(".tar.xz") else "r:gz"
with urllib.request.urlopen(source_url) as download:
sni_dir = None
with tarfile.open(fileobj=io.BytesIO(download.read()), mode=mode) as tf:
for member in tf.getmembers():
if member.name.startswith("/") or "../" in member.name:
raise ValueError(f"Unexpected file '{member.name}' in {source_url}")
elif member.isdir() and not sni_dir:
sni_dir = member.name
elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir):
raise ValueError(f"Expected folder before '{member.name}' in {source_url}")
elif member.isfile() and sni_dir:
tf.extract(member)
# sadly SNI is in its own folder on non-windows, so we need to rename
shutil.rmtree("SNI", True)
os.rename(sni_dir, "SNI")
print(f"Downloaded SNI from {source_url}")
elif source_url:
print(f"Don't know how to extract SNI from {source_url}")
else:
print(f"No SNI found for system spec {platform_name} {machine_name}")
if os.path.exists("X:/pw.txt"): if os.path.exists("X:/pw.txt"):
print("Using signtool") print("Using signtool")
with open("X:/pw.txt", encoding="utf-8-sig") as f: with open("X:/pw.txt", encoding="utf-8-sig") as f:
@@ -167,6 +244,10 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
print("Created Manifest") print("Created Manifest")
def run(self): def run(self):
# start downloading sni asap
sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader")
sni_thread.start()
# pre build steps # pre build steps
print(f"Outputting to: {self.buildfolder}") print(f"Outputting to: {self.buildfolder}")
os.makedirs(self.buildfolder, exist_ok=True) os.makedirs(self.buildfolder, exist_ok=True)
@@ -178,6 +259,9 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
self.buildtime = datetime.datetime.utcnow() self.buildtime = datetime.datetime.utcnow()
super().run() super().run()
# need to finish download before copying
sni_thread.join()
# include_files seems to not be done automatically. implement here # include_files seems to not be done automatically. implement here
for src, dst in self.include_files: for src, dst in self.include_files:
print(f"copying {src} -> {self.buildfolder / dst}") print(f"copying {src} -> {self.buildfolder / dst}")

View File

@@ -1,6 +1,6 @@
import pathlib
import typing import typing
import unittest import unittest
import pathlib
from argparse import Namespace from argparse import Namespace
import Utils import Utils
@@ -112,6 +112,12 @@ class WorldTestBase(unittest.TestCase):
self.world_setup() self.world_setup()
def world_setup(self, seed: typing.Optional[int] = None) -> None: def world_setup(self, seed: typing.Optional[int] = None) -> None:
if type(self) is WorldTestBase or \
(hasattr(WorldTestBase, self._testMethodName)
and not self.run_default_tests and
getattr(self, self._testMethodName).__code__ is
getattr(WorldTestBase, self._testMethodName, None).__code__):
return # setUp gets called for tests defined in the base class. We skip world_setup here.
if not hasattr(self, "game"): if not hasattr(self, "game"):
raise NotImplementedError("didn't define game name") raise NotImplementedError("didn't define game name")
self.multiworld = MultiWorld(1) self.multiworld = MultiWorld(1)
@@ -128,7 +134,9 @@ class WorldTestBase(unittest.TestCase):
for step in gen_steps: for step in gen_steps:
call_all(self.multiworld, step) call_all(self.multiworld, step)
# methods that can be called within tests
def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]) -> None: def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]) -> None:
"""Collects all pre-placed items and items in the multiworld itempool except those provided"""
if isinstance(item_names, str): if isinstance(item_names, str):
item_names = (item_names,) item_names = (item_names,)
for item in self.multiworld.get_items(): for item in self.multiworld.get_items():
@@ -136,12 +144,14 @@ class WorldTestBase(unittest.TestCase):
self.multiworld.state.collect(item) self.multiworld.state.collect(item)
def get_item_by_name(self, item_name: str) -> Item: def get_item_by_name(self, item_name: str) -> Item:
"""Returns the first item found in placed items, or in the itempool with the matching name"""
for item in self.multiworld.get_items(): for item in self.multiworld.get_items():
if item.name == item_name: if item.name == item_name:
return item return item
raise ValueError("No such item") raise ValueError("No such item")
def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
"""Returns actual items from the itempool that match the provided name(s)"""
if isinstance(item_names, str): if isinstance(item_names, str):
item_names = (item_names,) item_names = (item_names,)
return [item for item in self.multiworld.itempool if item.name in item_names] return [item for item in self.multiworld.itempool if item.name in item_names]
@@ -153,12 +163,14 @@ class WorldTestBase(unittest.TestCase):
return items return items
def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
"""Collects the provided item(s) into state"""
if isinstance(items, Item): if isinstance(items, Item):
items = (items,) items = (items,)
for item in items: for item in items:
self.multiworld.state.collect(item) self.multiworld.state.collect(item)
def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
"""Removes the provided item(s) from state"""
if isinstance(items, Item): if isinstance(items, Item):
items = (items,) items = (items,)
for item in items: for item in items:
@@ -167,17 +179,22 @@ class WorldTestBase(unittest.TestCase):
self.multiworld.state.remove(item) self.multiworld.state.remove(item)
def can_reach_location(self, location: str) -> bool: def can_reach_location(self, location: str) -> bool:
"""Determines if the current state can reach the provide location name"""
return self.multiworld.state.can_reach(location, "Location", 1) return self.multiworld.state.can_reach(location, "Location", 1)
def can_reach_entrance(self, entrance: str) -> bool: def can_reach_entrance(self, entrance: str) -> bool:
"""Determines if the current state can reach the provided entrance name"""
return self.multiworld.state.can_reach(entrance, "Entrance", 1) return self.multiworld.state.can_reach(entrance, "Entrance", 1)
def count(self, item_name: str) -> int: def count(self, item_name: str) -> int:
"""Returns the amount of an item currently in state"""
return self.multiworld.state.count(item_name, 1) return self.multiworld.state.count(item_name, 1)
def assertAccessDependency(self, def assertAccessDependency(self,
locations: typing.List[str], locations: typing.List[str],
possible_items: typing.Iterable[typing.Iterable[str]]) -> None: possible_items: typing.Iterable[typing.Iterable[str]]) -> None:
"""Asserts that the provided locations can't be reached without the listed items but can be reached with any
one of the provided combinations"""
all_items = [item_name for item_names in possible_items for item_name in item_names] all_items = [item_name for item_names in possible_items for item_name in item_names]
self.collect_all_but(all_items) self.collect_all_but(all_items)
@@ -190,4 +207,43 @@ class WorldTestBase(unittest.TestCase):
self.remove(items) self.remove(items)
def assertBeatable(self, beatable: bool): def assertBeatable(self, beatable: bool):
"""Asserts that the game can be beaten with the current state"""
self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable) self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable)
# following tests are automatically run
@property
def run_default_tests(self) -> bool:
"""Not possible or identical to the base test that's always being run already"""
return (self.options
or self.setUp.__code__ is not WorldTestBase.setUp.__code__
or self.world_setup.__code__ is not WorldTestBase.world_setup.__code__)
@property
def constructed(self) -> bool:
"""A multiworld has been constructed by this point"""
return hasattr(self, "game") and hasattr(self, "multiworld")
def testAllStateCanReachEverything(self):
"""Ensure all state can reach everything and complete the game with the defined options"""
if not (self.run_default_tests and self.constructed):
return
with self.subTest("Game", game=self.game):
excluded = self.multiworld.exclude_locations[1].value
state = self.multiworld.get_all_state(False)
for location in self.multiworld.get_locations():
if location.name not in excluded:
with self.subTest("Location should be reached", location=location):
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
with self.subTest("Beatable"):
self.multiworld.state = state
self.assertBeatable(True)
def testEmptyStateCanReachSomething(self):
"""Ensure empty state can reach at least one location with the defined options"""
if not (self.run_default_tests and self.constructed):
return
with self.subTest("Game", game=self.game):
state = CollectionState(self.multiworld)
locations = self.multiworld.get_reachable_locations(state, 1)
self.assertGreater(len(locations), 0,
"Need to be able to reach at least one location to get started.")

View File

@@ -3,7 +3,7 @@ import unittest
from worlds.AutoWorld import World from worlds.AutoWorld import World
from Fill import FillError, balance_multiworld_progression, fill_restrictive, \ from Fill import FillError, balance_multiworld_progression, fill_restrictive, \
distribute_early_items, distribute_items_restrictive distribute_early_items, distribute_items_restrictive
from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, RegionType, Item, Location, \ from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, Item, Location, \
ItemClassification ItemClassification
from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule
@@ -17,8 +17,7 @@ def generate_multi_world(players: int = 1) -> MultiWorld:
multi_world.game[player_id] = f"Game {player_id}" multi_world.game[player_id] = f"Game {player_id}"
multi_world.worlds[player_id] = world multi_world.worlds[player_id] = world
multi_world.player_name[player_id] = "Test Player " + str(player_id) multi_world.player_name[player_id] = "Test Player " + str(player_id)
region = Region("Menu", RegionType.Generic, region = Region("Menu", player_id, multi_world, "Menu Region Hint")
"Menu Region Hint", player_id, multi_world)
multi_world.regions.append(region) multi_world.regions.append(region)
multi_world.set_seed(0) multi_world.set_seed(0)
@@ -48,8 +47,7 @@ class PlayerDefinition(object):
def generate_region(self, parent: Region, size: int, access_rule: CollectionRule = lambda state: True) -> Region: def generate_region(self, parent: Region, size: int, access_rule: CollectionRule = lambda state: True) -> Region:
region_tag = "_region" + str(len(self.regions)) region_tag = "_region" + str(len(self.regions))
region_name = "player" + str(self.id) + region_tag region_name = "player" + str(self.id) + region_tag
region = Region("player" + str(self.id) + region_tag, RegionType.Generic, region = Region("player" + str(self.id) + region_tag, self.id, self.multiworld)
"Region Hint", self.id, self.multiworld)
self.locations += generate_locations(size, self.id, None, region, region_tag) self.locations += generate_locations(size, self.id, None, region, region_tag)
entrance = Entrance(self.id, region_name + "_entrance", parent) entrance = Entrance(self.id, region_name + "_entrance", parent)

View File

@@ -1,14 +1,33 @@
import unittest import unittest
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
from . import setup_default_world from . import setup_solo_multiworld
class TestImplemented(unittest.TestCase): class TestImplemented(unittest.TestCase):
def testCompletionCondition(self): def testCompletionCondition(self):
"""Ensure a completion condition is set that has requirements.""" """Ensure a completion condition is set that has requirements."""
for gamename, world_type in AutoWorldRegister.world_types.items(): for game_name, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden and gamename not in {"ArchipIDLE", "Final Fantasy", "Sudoku"}: if not world_type.hidden and game_name not in {"Sudoku"}:
with self.subTest(gamename): with self.subTest(game_name):
world = setup_default_world(world_type) multiworld = setup_solo_multiworld(world_type)
self.assertFalse(world.completion_condition[1](world.state)) self.assertFalse(multiworld.completion_condition[1](multiworld.state))
def testEntranceParents(self):
"""Tests that the parents of created Entrances match the exiting Region."""
for game_name, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden:
with self.subTest(game_name):
multiworld = setup_solo_multiworld(world_type)
for region in multiworld.regions:
for exit in region.exits:
self.assertEqual(exit.parent_region, region)
def testStageMethods(self):
"""Tests that worlds don't try to implement certain steps that are only ever called as stage."""
for game_name, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden:
with self.subTest(game_name):
for method in ("assert_generate",):
self.assertFalse(hasattr(world_type, method),
f"{method} must be implemented as a @classmethod named stage_{method}.")

View File

@@ -1,6 +1,6 @@
import unittest import unittest
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
from . import setup_default_world from . import setup_solo_multiworld
class TestBase(unittest.TestCase): class TestBase(unittest.TestCase):
@@ -43,14 +43,18 @@ class TestBase(unittest.TestCase):
def testItemCountGreaterEqualLocations(self): def testItemCountGreaterEqualLocations(self):
for game_name, world_type in AutoWorldRegister.world_types.items(): for game_name, world_type in AutoWorldRegister.world_types.items():
if game_name in {"Final Fantasy"}:
continue
with self.subTest("Game", game=game_name): with self.subTest("Game", game=game_name):
world = setup_default_world(world_type) multiworld = setup_solo_multiworld(world_type)
location_count = sum(0 if location.event or location.item else 1 for location in world.get_locations())
self.assertGreaterEqual( self.assertGreaterEqual(
len(world.itempool), len(multiworld.itempool),
location_count, len(multiworld.get_unfilled_locations()),
f"{game_name} Item count MUST meet or exceede the number of locations", f"{game_name} Item count MUST meet or exceed the number of locations",
) )
def testItemsInDatapackage(self):
"""Test that any created items in the itempool are in the datapackage"""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type)
for item in multiworld.itempool:
self.assertIn(item.name, world_type.item_name_to_id)

View File

@@ -1,16 +1,55 @@
import unittest import unittest
from collections import Counter from collections import Counter
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_default_world from . import setup_solo_multiworld
class TestBase(unittest.TestCase): class TestBase(unittest.TestCase):
def testCreateDuplicateLocations(self): def testCreateDuplicateLocations(self):
"""Tests that no two Locations share a name."""
for game_name, world_type in AutoWorldRegister.world_types.items(): for game_name, world_type in AutoWorldRegister.world_types.items():
if game_name in {"Final Fantasy"}: multiworld = setup_solo_multiworld(world_type)
continue
multiworld = setup_default_world(world_type)
locations = Counter(multiworld.get_locations()) locations = Counter(multiworld.get_locations())
if locations: if locations:
self.assertLessEqual(locations.most_common(1)[0][1], 1, self.assertLessEqual(locations.most_common(1)[0][1], 1,
f"{world_type.game} has duplicate of location {locations.most_common(1)}") f"{world_type.game} has duplicate of location {locations.most_common(1)}")
def testLocationsInDatapackage(self):
"""Tests that created locations not filled before fill starts exist in the datapackage."""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type)
locations = multiworld.get_unfilled_locations() # do unfilled locations to avoid Events
for location in locations:
self.assertIn(location.name, world_type.location_name_to_id)
self.assertEqual(location.address, world_type.location_name_to_id[location.name])
def testLocationCreationSteps(self):
"""Tests that Regions and Locations aren't created after `create_items`."""
gen_steps = ("generate_early", "create_regions", "create_items")
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
multiworld._recache()
region_count = len(multiworld.get_regions())
location_count = len(multiworld.get_locations())
call_all(multiworld, "set_rules")
self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during rule creation")
self.assertEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during rule creation")
multiworld._recache()
call_all(multiworld, "generate_basic")
self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during generate_basic")
self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during generate_basic")
multiworld._recache()
call_all(multiworld, "pre_fill")
self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during pre_fill")
self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during pre_fill")

View File

@@ -3,18 +3,41 @@ import unittest
from BaseClasses import CollectionState from BaseClasses import CollectionState
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
from . import setup_default_world from . import setup_solo_multiworld
class TestBase(unittest.TestCase): class TestBase(unittest.TestCase):
gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"] gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
default_settings_unreachable_regions = {
"A Link to the Past": {
"Chris Houlihan Room", # glitch room by definition
"Desert Northern Cliffs", # on top of mountain, only reachable via OWG
"Dark Death Mountain Bunny Descent Area" # OWG Mountain descent
},
"Ocarina of Time": {
"Prelude of Light Warp", # Prelude is not progression by default
"Serenade of Water Warp", # Serenade is not progression by default
"Lost Woods Mushroom Timeout", # trade quest starts after this item
"ZD Eyeball Frog Timeout", # trade quest starts after this item
"ZR Top of Waterfall", # dummy region used for entrance shuffle
},
# The following SM regions are only used when the corresponding StartLocation option is selected (so not with default settings).
# Also, those dont have any entrances as they serve as starting Region (that's why they have to be excluded for testAllStateCanReachEverything).
"Super Metroid": {
"Ceres",
"Gauntlet Top",
"Mama Turtle"
}
}
def testAllStateCanReachEverything(self): def testAllStateCanReachEverything(self):
for game_name, world_type in AutoWorldRegister.world_types.items(): for game_name, world_type in AutoWorldRegister.world_types.items():
# Final Fantasy logic is controlled by finalfantasyrandomizer.com # Final Fantasy logic is controlled by finalfantasyrandomizer.com
if game_name != "Ori and the Blind Forest" and game_name != "Final Fantasy": # TODO: fix Ori Logic if game_name not in {"Ori and the Blind Forest"}: # TODO: fix Ori Logic
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
with self.subTest("Game", game=game_name): with self.subTest("Game", game=game_name):
world = setup_default_world(world_type) world = setup_solo_multiworld(world_type)
excluded = world.exclude_locations[1].value excluded = world.exclude_locations[1].value
state = world.get_all_state(False) state = world.get_all_state(False)
for location in world.get_locations(): for location in world.get_locations():
@@ -22,15 +45,20 @@ class TestBase(unittest.TestCase):
with self.subTest("Location should be reached", location=location): with self.subTest("Location should be reached", location=location):
self.assertTrue(location.can_reach(state), f"{location.name} unreachable") self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
for region in world.get_regions():
if region.name not in unreachable_regions:
with self.subTest("Region should be reached", region=region):
self.assertTrue(region.can_reach(state))
with self.subTest("Completion Condition"): with self.subTest("Completion Condition"):
self.assertTrue(world.can_beat_game(state)) self.assertTrue(world.can_beat_game(state))
def testEmptyStateCanReachSomething(self): def testEmptyStateCanReachSomething(self):
for game_name, world_type in AutoWorldRegister.world_types.items(): for game_name, world_type in AutoWorldRegister.world_types.items():
# Final Fantasy logic is controlled by finalfantasyrandomizer.com # Final Fantasy logic is controlled by finalfantasyrandomizer.com
if game_name not in {"Archipelago", "Final Fantasy", "Sudoku"}: if game_name not in {"Archipelago", "Sudoku"}:
with self.subTest("Game", game=game_name): with self.subTest("Game", game=game_name):
world = setup_default_world(world_type) world = setup_solo_multiworld(world_type)
state = CollectionState(world) state = CollectionState(world)
locations = set() locations = set()
for location in world.get_locations(): for location in world.get_locations():

View File

@@ -1,12 +1,13 @@
from argparse import Namespace from argparse import Namespace
from typing import Type, Tuple
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from worlds.AutoWorld import call_all from worlds.AutoWorld import call_all, World
gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"] gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
def setup_default_world(world_type) -> MultiWorld: def setup_solo_multiworld(world_type: Type[World], steps: Tuple[str, ...] = gen_steps) -> MultiWorld:
multiworld = MultiWorld(1) multiworld = MultiWorld(1)
multiworld.game[1] = world_type.game multiworld.game[1] = world_type.game
multiworld.player_name = {1: "Tester"} multiworld.player_name = {1: "Tester"}
@@ -16,6 +17,6 @@ def setup_default_world(world_type) -> MultiWorld:
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
multiworld.set_options(args) multiworld.set_options(args)
multiworld.set_default_common_options() multiworld.set_default_common_options()
for step in gen_steps: for step in steps:
call_all(multiworld, step) call_all(multiworld, step)
return multiworld return multiworld

View File

@@ -131,54 +131,72 @@ class World(metaclass=AutoWorldRegister):
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. """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.""" A Game should have its own subclass of World in which it defines the required data structures."""
option_definitions: ClassVar[Dict[str, AssembleOptions]] = {} # link your Options mapping option_definitions: ClassVar[Dict[str, AssembleOptions]] = {}
game: ClassVar[str] # name the game """link your Options mapping"""
topology_present: ClassVar[bool] = False # indicate if world type has any meaningful layout/pathing game: ClassVar[str]
"""name the game"""
topology_present: ClassVar[bool] = False
"""indicate if world type has any meaningful layout/pathing"""
# gets automatically populated with all item and item group names
all_item_and_group_names: ClassVar[FrozenSet[str]] = frozenset() all_item_and_group_names: ClassVar[FrozenSet[str]] = frozenset()
"""gets automatically populated with all item and item group names"""
# map names to their IDs
item_name_to_id: ClassVar[Dict[str, int]] = {} item_name_to_id: ClassVar[Dict[str, int]] = {}
"""map item names to their IDs"""
location_name_to_id: ClassVar[Dict[str, int]] = {} location_name_to_id: ClassVar[Dict[str, int]] = {}
"""map location names to their IDs"""
# maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"}
item_name_groups: ClassVar[Dict[str, Set[str]]] = {} item_name_groups: ClassVar[Dict[str, Set[str]]] = {}
"""maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}"""
location_name_groups: ClassVar[Dict[str, Set[str]]] = {}
"""maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}"""
# increment this every time something in your world's names/id mappings changes.
# While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
# retrieved by clients on every connection.
data_version: ClassVar[int] = 1 data_version: ClassVar[int] = 1
"""
increment this every time something in your world's names/id mappings changes.
While this is set to 0, this world's DataPackage is considered in testing mode and will be inserted to the multidata
and retrieved by clients on every connection.
"""
# override this if changes to a world break forward-compatibility of the client
# The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the
# future. Protocol level compatibility check moved to MultiServer.min_client_version.
required_client_version: Tuple[int, int, int] = (0, 1, 6) required_client_version: Tuple[int, int, int] = (0, 1, 6)
"""
override this if changes to a world break forward-compatibility of the client
The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the
future. Protocol level compatibility check moved to MultiServer.min_client_version.
"""
# update this if the resulting multidata breaks forward-compatibility of the server
required_server_version: Tuple[int, int, int] = (0, 2, 4) required_server_version: Tuple[int, int, int] = (0, 2, 4)
"""update this if the resulting multidata breaks forward-compatibility of the server"""
hint_blacklist: ClassVar[FrozenSet[str]] = frozenset() # any names that should not be hintable hint_blacklist: ClassVar[FrozenSet[str]] = frozenset()
"""any names that should not be hintable"""
# Hide World Type from various views. Does not remove functionality.
hidden: ClassVar[bool] = False hidden: ClassVar[bool] = False
"""Hide World Type from various views. Does not remove functionality."""
# see WebWorld for options
web: ClassVar[WebWorld] = WebWorld() web: ClassVar[WebWorld] = WebWorld()
"""see WebWorld for options"""
# autoset on creation:
multiworld: "MultiWorld" multiworld: "MultiWorld"
"""autoset on creation. The MultiWorld object for the currently generating multiworld."""
player: int player: int
"""autoset on creation. The player number for this World"""
# automatically generated
item_id_to_name: ClassVar[Dict[int, str]] item_id_to_name: ClassVar[Dict[int, str]]
"""automatically generated reverse lookup of item id to name"""
location_id_to_name: ClassVar[Dict[int, str]] location_id_to_name: ClassVar[Dict[int, str]]
"""automatically generated reverse lookup of location id to name"""
item_names: ClassVar[Set[str]] # set of all potential item names item_names: ClassVar[Set[str]]
location_names: ClassVar[Set[str]] # set of all potential location names """set of all potential item names"""
location_names: ClassVar[Set[str]]
"""set of all potential location names"""
zip_path: ClassVar[Optional[pathlib.Path]] = None # If loaded from a .apworld, this is the Path to it. zip_path: ClassVar[Optional[pathlib.Path]] = None
__file__: ClassVar[str] # path it was loaded from """If loaded from a .apworld, this is the Path to it."""
__file__: ClassVar[str]
"""path it was loaded from"""
def __init__(self, multiworld: "MultiWorld", player: int): def __init__(self, multiworld: "MultiWorld", player: int):
self.multiworld = multiworld self.multiworld = multiworld
@@ -188,39 +206,52 @@ class World(metaclass=AutoWorldRegister):
# can also be implemented as a classmethod and called "stage_<original_name>", # can also be implemented as a classmethod and called "stage_<original_name>",
# in that case the MultiWorld object is passed as an argument and it gets called once for the entire multiworld. # in that case the MultiWorld object is passed as an argument and it gets called once for the entire multiworld.
# An example of this can be found in alttp as stage_pre_fill # An example of this can be found in alttp as stage_pre_fill
@classmethod @classmethod
def assert_generate(cls) -> None: def stage_assert_generate(cls, multiworld: "MultiWorld") -> None:
"""Checks that a game is capable of generating, usually checks for some base file like a ROM. """Checks that a game is capable of generating, usually checks for some base file like a ROM.
Not run for unittests since they don't produce output""" This gets called once per present world type. Not run for unittests since they don't produce output"""
pass pass
def generate_early(self) -> None: def generate_early(self) -> None:
"""
Run before any general steps of the MultiWorld other than options. Useful for getting and adjusting option
results and determining layouts for entrance rando etc. start inventory gets pushed after this step.
"""
pass pass
def create_regions(self) -> None: def create_regions(self) -> None:
"""Method for creating and connecting regions for the World."""
pass pass
def create_items(self) -> None: def create_items(self) -> None:
"""
Method for creating and submitting items to the itempool. Items and Regions should *not* be created and submitted
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_prefill_items`.
"""
pass pass
def set_rules(self) -> None: def set_rules(self) -> None:
"""Method for setting the rules on the World's regions and locations."""
pass pass
def generate_basic(self) -> None: def generate_basic(self) -> None:
"""
Useful for randomizing things that don't affect logic but are better to be determined before the output stage.
i.e. checking what the player has marked as priority or randomizing enemies
"""
pass pass
def pre_fill(self) -> None: def pre_fill(self) -> None:
"""Optional method that is supposed to be used for special fill stages. This is run *after* plando.""" """Optional method that is supposed to be used for special fill stages. This is run *after* plando."""
pass pass
@classmethod def fill_hook(self,
def fill_hook(cls,
progitempool: List["Item"], progitempool: List["Item"],
usefulitempool: List["Item"], usefulitempool: List["Item"],
filleritempool: List["Item"], filleritempool: List["Item"],
fill_locations: List["Location"]) -> None: fill_locations: List["Location"]) -> None:
"""Special method that gets called as part of distribute_items_restrictive (main fill). """Special method that gets called as part of distribute_items_restrictive (main fill)."""
This gets called once per present world type."""
pass pass
def post_fill(self) -> None: def post_fill(self) -> None:
@@ -229,7 +260,7 @@ class World(metaclass=AutoWorldRegister):
def generate_output(self, output_directory: str) -> None: def generate_output(self, output_directory: str) -> None:
"""This method gets called from a threadpool, do not use world.random here. """This method gets called from a threadpool, do not use world.random here.
If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead.""" If you need any last-second randomization, use MultiWorld.per_slot_randoms[slot] instead."""
pass pass
def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot

View File

@@ -4,7 +4,7 @@ from typing import Optional, Union, List, Tuple, Callable, Dict
from BaseClasses import Boss from BaseClasses import Boss
from Fill import FillError from Fill import FillError
from .Options import LTTPBosses as Bosses from .Options import LTTPBosses as Bosses
from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, has_melee_weapon, has_fire_source
def BossFactory(boss: str, player: int) -> Optional[Boss]: def BossFactory(boss: str, player: int) -> Optional[Boss]:
if boss in boss_table: if boss in boss_table:
@@ -16,33 +16,33 @@ def BossFactory(boss: str, player: int) -> Optional[Boss]:
def ArmosKnightsDefeatRule(state, player: int) -> bool: def ArmosKnightsDefeatRule(state, player: int) -> bool:
# Magic amounts are probably a bit overkill # Magic amounts are probably a bit overkill
return ( return (
state.has_melee_weapon(player) or has_melee_weapon(state, player) or
state.can_shoot_arrows(player) or can_shoot_arrows(state, player) or
(state.has('Cane of Somaria', player) and state.can_extend_magic(player, 10)) or (state.has('Cane of Somaria', player) and can_extend_magic(state, player, 10)) or
(state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or (state.has('Cane of Byrna', player) and can_extend_magic(state, player, 16)) or
(state.has('Ice Rod', player) and state.can_extend_magic(player, 32)) or (state.has('Ice Rod', player) and can_extend_magic(state, player, 32)) or
(state.has('Fire Rod', player) and state.can_extend_magic(player, 32)) or (state.has('Fire Rod', player) and can_extend_magic(state, player, 32)) or
state.has('Blue Boomerang', player) or state.has('Blue Boomerang', player) or
state.has('Red Boomerang', player)) state.has('Red Boomerang', player))
def LanmolasDefeatRule(state, player: int) -> bool: def LanmolasDefeatRule(state, player: int) -> bool:
return ( return (
state.has_melee_weapon(player) or has_melee_weapon(state, player) or
state.has('Fire Rod', player) or state.has('Fire Rod', player) or
state.has('Ice Rod', player) or state.has('Ice Rod', player) or
state.has('Cane of Somaria', player) or state.has('Cane of Somaria', player) or
state.has('Cane of Byrna', player) or state.has('Cane of Byrna', player) or
state.can_shoot_arrows(player)) can_shoot_arrows(state, player))
def MoldormDefeatRule(state, player: int) -> bool: def MoldormDefeatRule(state, player: int) -> bool:
return state.has_melee_weapon(player) return has_melee_weapon(state, player)
def HelmasaurKingDefeatRule(state, player: int) -> bool: def HelmasaurKingDefeatRule(state, player: int) -> bool:
# TODO: technically possible with the hammer # TODO: technically possible with the hammer
return state.has_sword(player) or state.can_shoot_arrows(player) return has_sword(state, player) or can_shoot_arrows(state, player)
def ArrghusDefeatRule(state, player: int) -> bool: def ArrghusDefeatRule(state, player: int) -> bool:
@@ -51,28 +51,28 @@ def ArrghusDefeatRule(state, player: int) -> bool:
# TODO: ideally we would have a check for bow and silvers, which combined with the # TODO: ideally we would have a check for bow and silvers, which combined with the
# hookshot is enough. This is not coded yet because the silvers that only work in pyramid feature # hookshot is enough. This is not coded yet because the silvers that only work in pyramid feature
# makes this complicated # makes this complicated
if state.has_melee_weapon(player): if has_melee_weapon(state, player):
return True return True
return ((state.has('Fire Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, return ((state.has('Fire Rod', player) and (can_shoot_arrows(state, player) or can_extend_magic(state, player,
12))) or # assuming mostly gitting two puff with one shot 12))) or # assuming mostly gitting two puff with one shot
(state.has('Ice Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16)))) (state.has('Ice Rod', player) and (can_shoot_arrows(state, player) or can_extend_magic(state, player, 16))))
def MothulaDefeatRule(state, player: int) -> bool: def MothulaDefeatRule(state, player: int) -> bool:
return ( return (
state.has_melee_weapon(player) or has_melee_weapon(state, player) or
(state.has('Fire Rod', player) and state.can_extend_magic(player, 10)) or (state.has('Fire Rod', player) and can_extend_magic(state, player, 10)) or
# TODO: Not sure how much (if any) extend magic is needed for these two, since they only apply # TODO: Not sure how much (if any) extend magic is needed for these two, since they only apply
# to non-vanilla locations, so are harder to test, so sticking with what VT has for now: # to non-vanilla locations, so are harder to test, so sticking with what VT has for now:
(state.has('Cane of Somaria', player) and state.can_extend_magic(player, 16)) or (state.has('Cane of Somaria', player) and can_extend_magic(state, player, 16)) or
(state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or (state.has('Cane of Byrna', player) and can_extend_magic(state, player, 16)) or
state.can_get_good_bee(player) can_get_good_bee(state, player)
) )
def BlindDefeatRule(state, player: int) -> bool: def BlindDefeatRule(state, player: int) -> bool:
return state.has_melee_weapon(player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player) return has_melee_weapon(state, player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player)
def KholdstareDefeatRule(state, player: int) -> bool: def KholdstareDefeatRule(state, player: int) -> bool:
@@ -81,56 +81,56 @@ def KholdstareDefeatRule(state, player: int) -> bool:
state.has('Fire Rod', player) or state.has('Fire Rod', player) or
( (
state.has('Bombos', player) and state.has('Bombos', player) and
(state.has_sword(player) or state.multiworld.swordless[player]) (has_sword(state, player) or state.multiworld.swordless[player])
) )
) and ) and
( (
state.has_melee_weapon(player) or has_melee_weapon(state, player) or
(state.has('Fire Rod', player) and state.can_extend_magic(player, 20)) or (state.has('Fire Rod', player) and can_extend_magic(state, player, 20)) or
( (
state.has('Fire Rod', player) and state.has('Fire Rod', player) and
state.has('Bombos', player) and state.has('Bombos', player) and
state.multiworld.swordless[player] and state.multiworld.swordless[player] and
state.can_extend_magic(player, 16) can_extend_magic(state, player, 16)
) )
) )
) )
def VitreousDefeatRule(state, player: int) -> bool: def VitreousDefeatRule(state, player: int) -> bool:
return state.can_shoot_arrows(player) or state.has_melee_weapon(player) return can_shoot_arrows(state, player) or has_melee_weapon(state, player)
def TrinexxDefeatRule(state, player: int) -> bool: def TrinexxDefeatRule(state, player: int) -> bool:
if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)): if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)):
return False return False
return state.has('Hammer', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', player) or \ return state.has('Hammer', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', player) or \
(state.has('Master Sword', player) and state.can_extend_magic(player, 16)) or \ (state.has('Master Sword', player) and can_extend_magic(state, player, 16)) or \
(state.has_sword(player) and state.can_extend_magic(player, 32)) (has_sword(state, player) and can_extend_magic(state, player, 32))
def AgahnimDefeatRule(state, player: int) -> bool: def AgahnimDefeatRule(state, player: int) -> bool:
return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player) return has_sword(state, player) or state.has('Hammer', player) or state.has('Bug Catching Net', player)
def GanonDefeatRule(state, player: int) -> bool: def GanonDefeatRule(state, player: int) -> bool:
if state.multiworld.swordless[player]: if state.multiworld.swordless[player]:
return state.has('Hammer', player) and \ return state.has('Hammer', player) and \
state.has_fire_source(player) and \ has_fire_source(state, player) and \
state.has('Silver Bow', player) and \ state.has('Silver Bow', player) and \
state.can_shoot_arrows(player) can_shoot_arrows(state, player)
can_hurt = state.has_beam_sword(player) can_hurt = has_beam_sword(state, player)
common = can_hurt and state.has_fire_source(player) common = can_hurt and has_fire_source(state, player)
# silverless ganon may be needed in anything higher than no glitches # silverless ganon may be needed in anything higher than no glitches
if state.multiworld.logic[player] != 'noglitches': if state.multiworld.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 can_shoot_arrows(state, player)) or
state.has('Lamp', player) or state.can_extend_magic(player, 12)) state.has('Lamp', player) or can_extend_magic(state, player, 12))
else: else:
return common and state.has('Silver Bow', player) and state.can_shoot_arrows(player) return common and state.has('Silver Bow', player) and can_shoot_arrows(state, player)
boss_table: Dict[str, Tuple[str, Optional[Callable]]] = { boss_table: Dict[str, Tuple[str, Optional[Callable]]] = {

View File

@@ -1,313 +1,531 @@
import collections import collections
from BaseClasses import RegionType
from worlds.alttp.Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region from worlds.alttp.Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region
from worlds.alttp.SubClasses import LTTPRegionType
def create_inverted_regions(world, player): def create_inverted_regions(world, player):
world.regions += [ world.regions += [
create_dw_region(player, 'Menu', None, ['Links House S&Q', 'Dark Sanctuary S&Q', 'Old Man S&Q', 'Castle Ledge S&Q']), create_dw_region(world, player, 'Menu', None,
create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest', 'Bombos Tablet'], ['Links House S&Q', 'Dark Sanctuary S&Q', 'Old Man S&Q', 'Castle Ledge S&Q']),
create_lw_region(world, player, 'Light World',
['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest',
'Bombos Tablet'],
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam', ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam',
'Inverted Big Bomb Shop', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave', 'Inverted Big Bomb Shop', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut',
'Blacksmiths Hut', 'Bat Cave Drop Ledge', 'Bat Cave Cave', 'Sick Kids House', 'Hobo Bridge', 'Lost Woods Hideout Drop', 'Lost Woods Hideout Stump', 'Kakariko Well Drop', 'Kakariko Well Cave',
'Lumberjack Tree Tree', 'Lumberjack Tree Cave', 'Mini Moldorm Cave', 'Ice Rod Cave', 'Lake Hylia Central Island Pier', 'Lake Hylia Island Pier', 'Lake Hylia Warp', 'Blacksmiths Hut', 'Bat Cave Drop Ledge', 'Bat Cave Cave', 'Sick Kids House', 'Hobo Bridge',
'Bonk Rock Cave', 'Library', 'Two Brothers House (East)', 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow', 'Lost Woods Hideout Drop', 'Lost Woods Hideout Stump',
'Lumberjack Tree Tree', 'Lumberjack Tree Cave', 'Mini Moldorm Cave', 'Ice Rod Cave',
'Lake Hylia Central Island Pier', 'Lake Hylia Island Pier', 'Lake Hylia Warp',
'Bonk Rock Cave', 'Library', 'Two Brothers House (East)', 'Desert Palace Stairs',
'Eastern Palace', 'Master Sword Meadow',
'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Light World River Drop', 'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Light World River Drop',
'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)', 'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop',
'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)',
'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave',
'East Dark World Mirror Spot', 'West Dark World Mirror Spot', 'South Dark World Mirror Spot', 'Cave 45', 'Checkerboard Cave', 'Mire Mirror Spot', 'Hammer Peg Area Mirror Spot', 'Cave Shop (Lake Hylia)',
'Shopping Mall Mirror Spot', 'Skull Woods Mirror Spot', 'Inverted Pyramid Entrance','Hyrule Castle Entrance (South)', 'Secret Passage Outer Bushes', 'Bush Covered Lawn Outer Bushes', 'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy',
'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller',
'Kakariko Gamble Game',
'East Dark World Mirror Spot', 'West Dark World Mirror Spot', 'South Dark World Mirror Spot',
'Cave 45', 'Checkerboard Cave', 'Mire Mirror Spot', 'Hammer Peg Area Mirror Spot',
'Shopping Mall Mirror Spot', 'Skull Woods Mirror Spot', 'Inverted Pyramid Entrance',
'Hyrule Castle Entrance (South)', 'Secret Passage Outer Bushes',
'Bush Covered Lawn Outer Bushes',
'Potion Shop Outer Bushes', 'Graveyard Cave Outer Bushes', 'Bomb Hut Outer Bushes']), 'Potion Shop Outer Bushes', 'Graveyard Cave Outer Bushes', 'Bomb Hut Outer Bushes']),
create_lw_region(player, 'Bush Covered Lawn', None, ['Bush Covered House', 'Bush Covered Lawn Inner Bushes', 'Bush Covered Lawn Mirror Spot']), create_lw_region(world, player, 'Bush Covered Lawn', None,
create_lw_region(player, 'Bomb Hut Area', None, ['Light World Bomb Hut', 'Bomb Hut Inner Bushes', 'Bomb Hut Mirror Spot']), ['Bush Covered House', 'Bush Covered Lawn Inner Bushes', 'Bush Covered Lawn Mirror Spot']),
create_lw_region(player, 'Hyrule Castle Secret Entrance Area', None, ['Hyrule Castle Secret Entrance Stairs', 'Secret Passage Inner Bushes']), create_lw_region(world, player, 'Bomb Hut Area', None,
create_lw_region(player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop', 'Bumper Cave Entrance Mirror Spot']), ['Light World Bomb Hut', 'Bomb Hut Inner Bushes', 'Bomb Hut Mirror Spot']),
create_lw_region(player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Mirror Spot']), create_lw_region(world, player, 'Hyrule Castle Secret Entrance Area', None,
create_cave_region(player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top", ['Hyrule Castle Secret Entrance Stairs', 'Secret Passage Inner Bushes']),
"Blind\'s Hideout - Left", create_lw_region(world, player, 'Death Mountain Entrance', None,
"Blind\'s Hideout - Right", ['Old Man Cave (West)', 'Death Mountain Entrance Drop', 'Bumper Cave Entrance Mirror Spot']),
"Blind\'s Hideout - Far Left", create_lw_region(world, player, 'Lake Hylia Central Island', None,
"Blind\'s Hideout - Far Right"]), ['Capacity Upgrade', 'Lake Hylia Central Island Mirror Spot']),
create_lw_region(player, 'Northeast Light World', None, ['Zoras River', 'Waterfall of Wishing Cave', 'Potion Shop Outer Rock', 'Catfish Mirror Spot', 'Northeast Light World Warp']), create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
create_lw_region(player, 'Waterfall of Wishing Cave', None, ['Waterfall of Wishing', 'Northeast Light World Return']), "Blind\'s Hideout - Left",
create_lw_region(player, 'Potion Shop Area', None, ['Potion Shop', 'Potion Shop Inner Bushes', 'Potion Shop Inner Rock', 'Potion Shop Mirror Spot', 'Potion Shop River Drop']), "Blind\'s Hideout - Right",
create_lw_region(player, 'Graveyard Cave Area', None, ['Graveyard Cave', 'Graveyard Cave Inner Bushes', 'Graveyard Cave Mirror Spot']), "Blind\'s Hideout - Far Left",
create_lw_region(player, 'River', None, ['Light World Pier', 'Potion Shop Pier']), "Blind\'s Hideout - Far Right"]),
create_cave_region(player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']), create_lw_region(world, player, 'Northeast Light World', None,
create_lw_region(player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']), ['Zoras River', 'Waterfall of Wishing Cave', 'Potion Shop Outer Rock', 'Catfish Mirror Spot',
create_cave_region(player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']), 'Northeast Light World Warp']),
create_lw_region(player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']), create_lw_region(world, player, 'Waterfall of Wishing Cave', None,
create_cave_region(player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']), ['Waterfall of Wishing', 'Northeast Light World Return']),
create_cave_region(player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']), create_lw_region(world, player, 'Potion Shop Area', None,
create_cave_region(player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']), ['Potion Shop', 'Potion Shop Inner Bushes', 'Potion Shop Inner Rock',
create_cave_region(player, 'Inverted Links House', 'your house', ['Link\'s House'], ['Inverted Links House Exit']), 'Potion Shop Mirror Spot', 'Potion Shop River Drop']),
create_cave_region(player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']), create_lw_region(world, player, 'Graveyard Cave Area', None,
create_cave_region(player, 'Tavern', 'the tavern', ['Kakariko Tavern']), ['Graveyard Cave', 'Graveyard Cave Inner Bushes', 'Graveyard Cave Mirror Spot']),
create_cave_region(player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']), create_lw_region(world, player, 'River', None, ['Light World Pier', 'Potion Shop Pier']),
create_cave_region(player, 'Snitch Lady (East)', 'a boring house'), create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit',
create_cave_region(player, 'Snitch Lady (West)', 'a boring house'), ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
create_cave_region(player, 'Bush Covered House', 'the grass man'), create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
create_cave_region(player, 'Tavern (Front)', 'the tavern'), create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests',
create_cave_region(player, 'Light World Bomb Hut', 'a restock room'), ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
create_cave_region(player, 'Kakariko Shop', 'a common shop'), create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
create_cave_region(player, 'Fortune Teller (Light)', 'a fortune teller'), create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
create_cave_region(player, 'Lake Hylia Fortune Teller', 'a fortune teller'), create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
create_cave_region(player, 'Lumberjack House', 'a boring house'), create_cave_region(world, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
create_cave_region(player, 'Bonk Fairy (Light)', 'a fairy fountain'), create_cave_region(world, player, 'Inverted Links House', 'your house', ['Link\'s House'],
create_cave_region(player, 'Bonk Fairy (Dark)', 'a fairy fountain'), ['Inverted Links House Exit']),
create_cave_region(player, 'Lake Hylia Healer Fairy', 'a fairy fountain'), create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
create_cave_region(player, 'Swamp Healer Fairy', 'a fairy fountain'), create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
create_cave_region(player, 'Desert Healer Fairy', 'a fairy fountain'), create_cave_region(world, player, 'Elder House', 'a connector', None,
create_cave_region(player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'), ['Elder House Exit (East)', 'Elder House Exit (West)']),
create_cave_region(player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'), create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'),
create_cave_region(player, 'Dark Desert Healer Fairy', 'a fairy fountain'), create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'),
create_cave_region(player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'), create_cave_region(world, player, 'Bush Covered House', 'the grass man'),
create_cave_region(player, 'Chicken House', 'a house with a chest', ['Chicken House']), create_cave_region(world, player, 'Tavern (Front)', 'the tavern'),
create_cave_region(player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']), create_cave_region(world, player, 'Light World Bomb Hut', 'a restock room'),
create_cave_region(player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']), create_cave_region(world, player, 'Kakariko Shop', 'a common shop'),
create_cave_region(player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle', create_cave_region(world, player, 'Fortune Teller (Light)', 'a fortune teller'),
'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']), create_cave_region(world, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
create_cave_region(player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']), create_cave_region(world, player, 'Lumberjack House', 'a boring house'),
create_cave_region(player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']), create_cave_region(world, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
create_lw_region(player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']), create_cave_region(world, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
create_cave_region(player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']), create_cave_region(world, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
create_cave_region(player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']), create_cave_region(world, player, 'Swamp Healer Fairy', 'a fairy fountain'),
create_cave_region(player, 'Sick Kids House', 'the sick kid', ['Sick Kid']), create_cave_region(world, player, 'Desert Healer Fairy', 'a fairy fountain'),
create_lw_region(player, 'Hobo Bridge', ['Hobo']), create_cave_region(world, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
create_cave_region(player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']), create_cave_region(world, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
create_cave_region(player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']), create_cave_region(world, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
create_cave_region(player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']), create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
create_cave_region(player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']), create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
create_cave_region(player, 'Cave 45', 'a cave with an item', ['Cave 45']), create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
create_cave_region(player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']), create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla',
create_cave_region(player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']), ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right',
create_cave_region(player, 'Long Fairy Cave', 'a fairy fountain'), 'Sahasrahla']),
create_cave_region(player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit',
'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']), ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
create_cave_region(player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']), 'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
create_cave_region(player, 'Good Bee Cave', 'a cold bee'), create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
create_cave_region(player, '20 Rupee Cave', 'a cave with some cash'), create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
create_cave_region(player, 'Cave Shop (Lake Hylia)', 'a common shop'), create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
create_cave_region(player, 'Cave Shop (Dark Death Mountain)', 'a common shop'), create_cave_region(world, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
create_cave_region(player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']), create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
create_cave_region(player, 'Library', 'the library', ['Library']), create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
create_cave_region(player, 'Kakariko Gamble Game', 'a game of chance'), create_lw_region(world, player, 'Hobo Bridge', ['Hobo']),
create_cave_region(player, 'Potion Shop', 'the potion shop', ['Potion Shop']), create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'],
create_lw_region(player, 'Lake Hylia Island', ['Lake Hylia Island']), ['Lost Woods Hideout (top to bottom)']),
create_cave_region(player, 'Capacity Upgrade', 'the queen of fairies'), create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None,
create_cave_region(player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']), ['Lost Woods Hideout Exit']),
create_lw_region(player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)', 'Maze Race Mirror Spot']), create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'],
create_cave_region(player, '50 Rupee Cave', 'a cave with some cash'), ['Lumberjack Tree (top to bottom)']),
create_lw_region(player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)', 'Desert Ledge Drop']), create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
create_lw_region(player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)', 'Desert Palace Stairs Mirror Spot']), create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
create_lw_region(player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']), create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
create_lw_region(player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks', 'Desert Palace North Mirror Spot']), create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
create_dungeon_region(player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'], create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'),
['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']), create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items',
create_dungeon_region(player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']), ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
create_dungeon_region(player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']), 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
create_dungeon_region(player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']), create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
create_dungeon_region(player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest', create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'),
'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'], ['Eastern Palace Exit']), create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'),
create_lw_region(player, 'Master Sword Meadow', ['Master Sword Pedestal']), create_cave_region(world, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
create_cave_region(player, 'Lost Woods Gamble', 'a game of chance'), create_cave_region(world, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
create_lw_region(player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']), create_cave_region(world, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
create_dungeon_region(player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest'], create_cave_region(world, player, 'Library', 'the library', ['Library']),
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']), create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'),
create_dungeon_region(player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
create_dungeon_region(player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross'], ['Sewers Door']), create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']),
create_dungeon_region(player, 'Sewers', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies'),
'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']), create_cave_region(world, player, 'Two Brothers House', 'a connector', None,
create_dungeon_region(player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']), ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
create_dungeon_region(player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']), create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'],
create_dungeon_region(player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None), ['Two Brothers House (West)', 'Maze Race Mirror Spot']),
create_cave_region(player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']), create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'),
create_cave_region(player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']), create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'],
create_cave_region(player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']), ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)',
create_lw_region(player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Desert Ledge Drop']),
'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Mirror Spot']), create_lw_region(world, player, 'Desert Palace Stairs', None,
create_cave_region(player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']), ['Desert Palace Entrance (South)', 'Desert Palace Stairs Mirror Spot']),
create_lw_region(player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)', 'Bumper Cave Ledge Mirror Spot']), create_lw_region(world, player, 'Desert Palace Lone Stairs', None,
create_cave_region(player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']), ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
create_cave_region(player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']), create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None,
create_cave_region(player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']), ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks',
create_lw_region(player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Mirror Spot (Bottom)', 'Hookshot Fairy', 'Desert Palace North Mirror Spot']),
'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']), create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace',
create_cave_region(player, 'Hookshot Fairy', 'fairies deep in a cave'), ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
create_cave_region(player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']), ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)',
create_cave_region(player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left', 'Desert Palace East Wing']),
'Paradox Cave Lower - Left', create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None,
'Paradox Cave Lower - Right', ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
'Paradox Cave Lower - Far Right', create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace',
'Paradox Cave Lower - Middle', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
'Paradox Cave Upper - Left', create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace',
'Paradox Cave Upper - Right'], ['Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace',
['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
'Eastern Palace - Cannonball Chest',
'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss',
'Eastern Palace - Prize'], ['Eastern Palace Exit']),
create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'),
create_lw_region(world, player, 'Hyrule Castle Ledge', None,
['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower',
'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']),
create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle',
['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
'Hyrule Castle - Zelda\'s Chest'],
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)',
'Throne Room']),
create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross'],
['Sewers Door']),
create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit',
['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']),
create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
create_dungeon_region(world, player, 'Inverted Agahnims Tower', 'Castle Tower',
['Castle Tower - Room 03', 'Castle Tower - Dark Maze'],
['Agahnim 1', 'Inverted Agahnims Tower Exit']),
create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'],
['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
create_cave_region(world, player, 'Old Man House', 'a connector', None,
['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
create_cave_region(world, player, 'Old Man House Back', 'a connector', None,
['Old Man House Exit (Top)', 'Old Man House Back to Front']),
create_lw_region(world, player, 'Death Mountain', None,
['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)',
'Death Mountain Return Cave (East)', 'Spectacle Rock Cave',
'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)',
'Death Mountain Mirror Spot']),
create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None,
['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
create_lw_region(world, player, 'Death Mountain Return Ledge', None,
['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)',
'Bumper Cave Ledge Mirror Spot']),
create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'],
['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None,
['Spectacle Rock Cave Exit']),
create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None,
['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
create_lw_region(world, player, 'East Death Mountain (Bottom)', None,
['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)',
'East Death Mountain Mirror Spot (Bottom)', 'Hookshot Fairy',
'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'),
create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None,
['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)',
'Light World Death Mountain Shop']),
create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
'Paradox Cave Lower - Left',
'Paradox Cave Lower - Right',
'Paradox Cave Lower - Far Right',
'Paradox Cave Lower - Middle',
'Paradox Cave Upper - Left',
'Paradox Cave Upper - Right'],
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']), ['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
create_cave_region(player, 'Paradox Cave', 'a connector', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']), create_cave_region(world, player, 'Paradox Cave', 'a connector', None,
create_cave_region(player, 'Light World Death Mountain Shop', 'a common shop'), ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
create_lw_region(player, 'East Death Mountain (Top)', ['Floating Island'], ['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access', 'East Death Mountain Drop', 'East Death Mountain Mirror Spot (Top)', 'Fairy Ascension Ledge Access', 'Mimic Cave Ledge Access', create_cave_region(world, player, 'Light World Death Mountain Shop', 'a common shop'),
'Floating Island Mirror Spot']), create_lw_region(world, player, 'East Death Mountain (Top)', ['Floating Island'],
create_lw_region(player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (West)']), ['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access',
create_lw_region(player, 'Mimic Cave Ledge', None, ['Mimic Cave', 'Mimic Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (East)']), 'East Death Mountain Drop', 'East Death Mountain Mirror Spot (Top)',
create_cave_region(player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'], ['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']), 'Fairy Ascension Ledge Access', 'Mimic Cave Ledge Access',
create_cave_region(player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']), 'Floating Island Mirror Spot']),
create_lw_region(player, 'Fairy Ascension Plateau', None, ['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']), create_lw_region(world, player, 'Spiral Cave Ledge', None,
create_cave_region(player, 'Fairy Ascension Cave (Bottom)', 'a connector', None, ['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']), ['Spiral Cave', 'Spiral Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (West)']),
create_cave_region(player, 'Fairy Ascension Cave (Drop)', 'a connector', None, ['Fairy Ascension Cave Pots']), create_lw_region(world, player, 'Mimic Cave Ledge', None,
create_cave_region(player, 'Fairy Ascension Cave (Top)', 'a connector', None, ['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']), ['Mimic Cave', 'Mimic Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (East)']),
create_lw_region(player, 'Fairy Ascension Ledge', None, ['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)', 'Laser Bridge Mirror Spot']), create_cave_region(world, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
create_lw_region(player, 'Death Mountain (Top)', ['Ether Tablet', 'Spectacle Rock'], ['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop', 'Death Mountain (Top) Mirror Spot']), ['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']),
create_dw_region(player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], ['Bumper Cave Ledge Drop', 'Bumper Cave (Top)']), create_cave_region(world, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
create_dungeon_region(player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']), create_lw_region(world, player, 'Fairy Ascension Plateau', None,
create_dungeon_region(player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']), ['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']),
create_dungeon_region(player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']), create_cave_region(world, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']),
create_cave_region(world, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
['Fairy Ascension Cave Pots']),
create_cave_region(world, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']),
create_lw_region(world, player, 'Fairy Ascension Ledge', None,
['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)', 'Laser Bridge Mirror Spot']),
create_lw_region(world, player, 'Death Mountain (Top)', ['Ether Tablet', 'Spectacle Rock'],
['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop',
'Death Mountain (Top) Mirror Spot']),
create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
['Bumper Cave Ledge Drop', 'Bumper Cave (Top)']),
create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera',
['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'],
['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera',
['Tower of Hera - Big Key Chest']),
create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera',
['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss',
'Tower of Hera - Prize']),
create_dw_region(player, 'East Dark World', ['Pyramid'], ['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness', 'Dark Lake Hylia Drop (East)', create_dw_region(world, player, 'East Dark World', ['Pyramid'],
'Dark Lake Hylia Fairy', 'Palace of Darkness Hint', 'East Dark World Hint', 'Northeast Dark World Broken Bridge Pass', 'East Dark World Teleporter', 'EDW Flute']), ['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness',
create_dw_region(player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']), 'Dark Lake Hylia Drop (East)',
create_dw_region(player, 'Northeast Dark World', None, ['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass', 'NEDW Flute', 'Dark Lake Hylia Teleporter', 'Catfish Entrance Rock']), 'Dark Lake Hylia Fairy', 'Palace of Darkness Hint', 'East Dark World Hint',
create_cave_region(player, 'Palace of Darkness Hint', 'a storyteller'), 'Northeast Dark World Broken Bridge Pass', 'East Dark World Teleporter', 'EDW Flute']),
create_cave_region(player, 'East Dark World Hint', 'a storyteller'), create_dw_region(world, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
create_dw_region(player, 'South Dark World', ['Stumpy', 'Digging Game'], ['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock', 'East Dark World Bridge', 'Inverted Links House', 'Archery Game', 'Bonk Fairy (Dark)', create_dw_region(world, player, 'Northeast Dark World', None,
'Dark Lake Hylia Shop', 'South Dark World Teleporter', 'Post Aga Teleporter', 'SDW Flute']), ['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass',
create_cave_region(player, 'Inverted Big Bomb Shop', 'the bomb shop'), 'NEDW Flute', 'Dark Lake Hylia Teleporter', 'Catfish Entrance Rock']),
create_cave_region(player, 'Archery Game', 'a game of skill'), create_cave_region(world, player, 'Palace of Darkness Hint', 'a storyteller'),
create_dw_region(player, 'Dark Lake Hylia', None, ['East Dark World Pier', 'Dark Lake Hylia Ledge Pier', 'Ice Palace Missing Wall']), create_cave_region(world, player, 'East Dark World Hint', 'a storyteller'),
create_dw_region(player, 'Dark Lake Hylia Central Island', None, ['Dark Lake Hylia Shallows', 'Ice Palace', 'Dark Lake Hylia Central Island Teleporter']), create_dw_region(world, player, 'South Dark World', ['Stumpy', 'Digging Game'],
create_dw_region(player, 'Dark Lake Hylia Ledge', None, ['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Spike Cave', 'DLHL Flute']), ['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock',
create_cave_region(player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'), 'East Dark World Bridge', 'Inverted Links House', 'Archery Game', 'Bonk Fairy (Dark)',
create_cave_region(player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'), 'Dark Lake Hylia Shop', 'South Dark World Teleporter', 'Post Aga Teleporter', 'SDW Flute']),
create_cave_region(player, 'Hype Cave', 'a bounty of five items', ['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', create_cave_region(world, player, 'Inverted Big Bomb Shop', 'the bomb shop'),
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']), create_cave_region(world, player, 'Archery Game', 'a game of skill'),
create_dw_region(player, 'West Dark World', ['Frog', 'Flute Activation Spot'], ['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House', 'Chest Game', 'Thieves Town', 'Bumper Cave Entrance Rock', create_dw_region(world, player, 'Dark Lake Hylia', None,
'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks', 'Red Shield Shop', 'Inverted Dark Sanctuary', 'Fortune Teller (Dark)', 'Dark World Lumberjack Shop', ['East Dark World Pier', 'Dark Lake Hylia Ledge Pier', 'Ice Palace Missing Wall']),
'West Dark World Teleporter', 'WDW Flute']), create_dw_region(world, player, 'Dark Lake Hylia Central Island', None,
create_dw_region(player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop', 'Dark Grassy Lawn Flute']), ['Dark Lake Hylia Shallows', 'Ice Palace', 'Dark Lake Hylia Central Island Teleporter']),
create_dw_region(player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'], ['Dark World Hammer Peg Cave', 'Peg Area Rocks', 'Hammer Peg Area Flute']), create_dw_region(world, player, 'Dark Lake Hylia Ledge', None,
create_dw_region(player, 'Bumper Cave Entrance', None, ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Drop']), ['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint',
create_cave_region(player, 'Fortune Teller (Dark)', 'a fortune teller'), 'Dark Lake Hylia Ledge Spike Cave', 'DLHL Flute']),
create_cave_region(player, 'Village of Outcasts Shop', 'a common shop'), create_cave_region(world, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
create_cave_region(player, 'Dark Lake Hylia Shop', 'a common shop'), create_cave_region(world, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
create_cave_region(player, 'Dark World Lumberjack Shop', 'a common shop'), create_cave_region(world, player, 'Hype Cave', 'a bounty of five items',
create_cave_region(player, 'Dark World Potion Shop', 'a common shop'), ['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left',
create_cave_region(player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']), 'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
create_cave_region(player, 'Pyramid Fairy', 'a cave with two chests', ['Pyramid Fairy - Left', 'Pyramid Fairy - Right']), create_dw_region(world, player, 'West Dark World', ['Frog', 'Flute Activation Spot'],
create_cave_region(player, 'Brewery', 'a house with a chest', ['Brewery']), ['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House',
create_cave_region(player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']), 'Chest Game', 'Thieves Town', 'Bumper Cave Entrance Rock',
create_cave_region(player, 'Chest Game', 'a game of 16 chests', ['Chest Game']), 'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks',
create_cave_region(player, 'Red Shield Shop', 'the rare shop'), 'Red Shield Shop', 'Inverted Dark Sanctuary', 'Fortune Teller (Dark)',
create_cave_region(player, 'Inverted Dark Sanctuary', 'a storyteller', None, ['Inverted Dark Sanctuary Exit']), 'Dark World Lumberjack Shop',
create_cave_region(player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']), 'West Dark World Teleporter', 'WDW Flute']),
create_dw_region(player, 'Skull Woods Forest', None, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', create_dw_region(world, player, 'Dark Grassy Lawn', None,
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']), ['Grassy Lawn Pegs', 'Village of Outcasts Shop', 'Dark Grassy Lawn Flute']),
create_dw_region(player, 'Skull Woods Forest (West)', None, ['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section']), create_dw_region(world, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
create_dw_region(player, 'Dark Desert', None, ['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy', 'DD Flute']), ['Dark World Hammer Peg Cave', 'Peg Area Rocks', 'Hammer Peg Area Flute']),
create_dw_region(player, 'Dark Desert Ledge', None, ['Dark Desert Drop', 'Dark Desert Teleporter']), create_dw_region(world, player, 'Bumper Cave Entrance', None,
create_cave_region(player, 'Mire Shed', 'a cave with two chests', ['Mire Shed - Left', 'Mire Shed - Right']), ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Drop']),
create_cave_region(player, 'Dark Desert Hint', 'a storyteller'), create_cave_region(world, player, 'Fortune Teller (Dark)', 'a fortune teller'),
create_dw_region(player, 'Dark Death Mountain', None, ['Dark Death Mountain Drop (East)', 'Inverted Agahnims Tower', 'Superbunny Cave (Top)', 'Hookshot Cave', 'Turtle Rock', create_cave_region(world, player, 'Village of Outcasts Shop', 'a common shop'),
'Spike Cave', 'Dark Death Mountain Fairy', 'Dark Death Mountain Teleporter (West)', 'Turtle Rock Tail Drop', 'DDM Flute']), create_cave_region(world, player, 'Dark Lake Hylia Shop', 'a common shop'),
create_dw_region(player, 'Dark Death Mountain Ledge', None, ['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)']), create_cave_region(world, player, 'Dark World Lumberjack Shop', 'a common shop'),
create_dw_region(player, 'Turtle Rock (Top)', None, ['Dark Death Mountain Teleporter (East)', 'Turtle Rock Drop']), create_cave_region(world, player, 'Dark World Potion Shop', 'a common shop'),
create_dw_region(player, 'Dark Death Mountain Isolated Ledge', None, ['Turtle Rock Isolated Ledge Entrance']), create_cave_region(world, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
create_dw_region(player, 'Dark Death Mountain (East Bottom)', None, ['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)', 'Dark Death Mountain Teleporter (East Bottom)', 'EDDM Flute']), create_cave_region(world, player, 'Pyramid Fairy', 'a cave with two chests',
create_cave_region(player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']), ['Pyramid Fairy - Left', 'Pyramid Fairy - Right']),
create_cave_region(player, 'Superbunny Cave (Bottom)', 'a connector', None, ['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']), create_cave_region(world, player, 'Brewery', 'a house with a chest', ['Brewery']),
create_cave_region(player, 'Spike Cave', 'Spike Cave', ['Spike Cave']), create_cave_region(world, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
create_cave_region(player, 'Hookshot Cave', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'], create_cave_region(world, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
create_cave_region(world, player, 'Red Shield Shop', 'the rare shop'),
create_cave_region(world, player, 'Inverted Dark Sanctuary', 'a storyteller', None,
['Inverted Dark Sanctuary Exit']),
create_cave_region(world, player, 'Bumper Cave', 'a connector', None,
['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
create_dw_region(world, player, 'Skull Woods Forest', None,
['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)',
'Skull Woods First Section Hole (North)',
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']),
create_dw_region(world, player, 'Skull Woods Forest (West)', None,
['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)',
'Skull Woods Final Section']),
create_dw_region(world, player, 'Dark Desert', None,
['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy', 'DD Flute']),
create_dw_region(world, player, 'Dark Desert Ledge', None, ['Dark Desert Drop', 'Dark Desert Teleporter']),
create_cave_region(world, player, 'Mire Shed', 'a cave with two chests',
['Mire Shed - Left', 'Mire Shed - Right']),
create_cave_region(world, player, 'Dark Desert Hint', 'a storyteller'),
create_dw_region(world, player, 'Dark Death Mountain', None,
['Dark Death Mountain Drop (East)', 'Inverted Agahnims Tower', 'Superbunny Cave (Top)',
'Hookshot Cave', 'Turtle Rock',
'Spike Cave', 'Dark Death Mountain Fairy', 'Dark Death Mountain Teleporter (West)',
'Turtle Rock Tail Drop', 'DDM Flute']),
create_dw_region(world, player, 'Dark Death Mountain Ledge', None,
['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)']),
create_dw_region(world, player, 'Turtle Rock (Top)', None,
['Dark Death Mountain Teleporter (East)', 'Turtle Rock Drop']),
create_dw_region(world, player, 'Dark Death Mountain Isolated Ledge', None,
['Turtle Rock Isolated Ledge Entrance']),
create_dw_region(world, player, 'Dark Death Mountain (East Bottom)', None,
['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)',
'Dark Death Mountain Teleporter (East Bottom)', 'EDDM Flute']),
create_cave_region(world, player, 'Superbunny Cave (Top)', 'a connector',
['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
create_cave_region(world, player, 'Superbunny Cave (Bottom)', 'a connector', None,
['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
create_cave_region(world, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
create_cave_region(world, player, 'Hookshot Cave', 'a connector',
['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right',
'Hookshot Cave - Bottom Left'],
['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']), ['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']),
create_dw_region(player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance']), create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None,
create_cave_region(player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']), ['Floating Island Drop', 'Hookshot Cave Back Entrance']),
create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
create_dungeon_region(player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']), create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None,
create_dungeon_region(player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']), ['Swamp Palace Moat', 'Swamp Palace Exit']),
create_dungeon_region(player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest'], ['Swamp Palace (Center)']), create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'],
create_dungeon_region(player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', ['Swamp Palace Small Key Door']),
'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest'], ['Swamp Palace (North)']), create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace',
create_dungeon_region(player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', ['Swamp Palace - Map Chest'], ['Swamp Palace (Center)']),
'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']), create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace',
create_dungeon_region(player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest',
'Thieves\' Town - Map Chest', 'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest'], ['Swamp Palace (North)']),
'Thieves\' Town - Compass Chest', create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace',
'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']), ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
create_dungeon_region(player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic', 'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']),
'Thieves\' Town - Big Chest', create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town',
'Thieves\' Town - Blind\'s Cell'], ['Blind Fight']), ['Thieves\' Town - Big Key Chest',
create_dungeon_region(player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']), 'Thieves\' Town - Map Chest',
create_dungeon_region(player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']), 'Thieves\' Town - Compass Chest',
create_dungeon_region(player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']), 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
create_dungeon_region(player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']), create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
create_dungeon_region(player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']), 'Thieves\' Town - Big Chest',
create_dungeon_region(player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']), 'Thieves\' Town - Blind\'s Cell'],
create_dungeon_region(player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']), ['Blind Fight']),
create_dungeon_region(player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']), create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town',
create_dungeon_region(player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Boss', 'Skull Woods - Prize']), ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
create_dungeon_region(player, 'Ice Palace (Entrance)', 'Ice Palace', None, ['Ice Palace Entrance Room', 'Ice Palace Exit']), create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'],
create_dungeon_region(player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest', ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump',
'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']), 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
create_dungeon_region(player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']), create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods',
create_dungeon_region(player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest']), ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
create_dungeon_region(player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']), create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods',
create_dungeon_region(player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']), ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'],
create_dungeon_region(player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', ['Skull Woods First Section (Left) Door to Exit',
'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'], ['Misery Mire (West)', 'Misery Mire Big Key Door']), 'Skull Woods First Section (Left) Door to Right']),
create_dungeon_region(player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']), create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods',
create_dungeon_region(player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']), ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
create_dungeon_region(player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']), create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None,
create_dungeon_region(player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']), ['Skull Woods Second Section (Drop)']),
create_dungeon_region(player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods',
'Turtle Rock - Roller Room - Right'], ['Turtle Rock Pokey Room', 'Turtle Rock Entrance Gap Reverse']), ['Skull Woods - Big Key Chest'],
create_dungeon_region(player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']), ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
create_dungeon_region(player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest'], ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door']), create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods',
create_dungeon_region(player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']), ['Skull Woods - Bridge Room'],
create_dungeon_region(player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']), ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
create_dungeon_region(player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']), create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods',
create_dungeon_region(player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', ['Skull Woods - Boss', 'Skull Woods - Prize']),
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'], create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', None,
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']), ['Ice Palace Entrance Room', 'Ice Palace Exit']),
create_dungeon_region(player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']), create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace',
create_dungeon_region(player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']), ['Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest',
create_dungeon_region(player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'], 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'],
['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']), ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
create_dungeon_region(player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']), create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'],
create_dungeon_region(player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']), ['Ice Palace (East Top)']),
create_dungeon_region(player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'], create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace',
['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest']),
create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace',
['Ice Palace - Boss', 'Ice Palace - Prize']),
create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None,
['Misery Mire Entrance Gap', 'Misery Mire Exit']),
create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire',
['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'],
['Misery Mire (West)', 'Misery Mire Big Key Door']),
create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire',
['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None,
['Misery Mire (Vitreous)']),
create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire',
['Misery Mire - Boss', 'Misery Mire - Prize']),
create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None,
['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock',
['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
'Turtle Rock - Roller Room - Right'],
['Turtle Rock Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock',
['Turtle Rock - Chain Chomps'],
['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
['Turtle Rock - Big Key Chest'],
['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase',
'Turtle Rock Big Key Door']),
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'],
['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock',
['Turtle Rock - Crystaroller Room'],
['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None,
['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock',
['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)',
'Turtle Rock Isolated Ledge Exit']),
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock',
['Turtle Rock - Boss', 'Turtle Rock - Prize']),
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness',
['Palace of Darkness - Shooter Room'],
['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall',
'Palace of Darkness Exit']),
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness',
['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)',
'Palace of Darkness Big Key Door']),
create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness',
['Palace of Darkness - Big Key Chest']),
create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness',
['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'],
['Palace of Darkness Hammer Peg Drop']),
create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness',
['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left',
'Palace of Darkness - Dark Basement - Right'],
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']), ['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
create_dungeon_region(player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']), create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness',
create_dungeon_region(player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']), ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom',
create_dungeon_region(player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']), 'Palace of Darkness - Big Chest']),
create_dungeon_region(player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left', 'Ganons Tower - Hope Room - Right'], create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness',
['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door', 'Inverted Ganons Tower Exit']), ['Palace of Darkness - Harmless Hellway']),
create_dungeon_region(player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']), create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness',
create_dungeon_region(player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right'], create_dungeon_region(world, player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower',
['Ganons Tower (Bottom) (East)']), ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
create_dungeon_region(player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - Hope Room - Right'],
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'], ['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door',
'Inverted Ganons Tower Exit']),
create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'],
['Ganons Tower (Tile Room) Key Door']),
create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower',
['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
'Ganons Tower - Compass Room - Bottom Left',
'Ganons Tower - Compass Room - Bottom Right'], ['Ganons Tower (Bottom) (East)']),
create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower',
['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'],
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']), ['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
create_dungeon_region(player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']), create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
create_dungeon_region(player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']), create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower',
create_dungeon_region(player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'], create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower',
['Ganons Tower (Bottom) (West)']), ['Ganons Tower - Randomizer Room - Top Left',
create_dungeon_region(player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Randomizer Room - Top Right',
'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']), 'Ganons Tower - Randomizer Room - Bottom Left',
create_dungeon_region(player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']), 'Ganons Tower - Randomizer Room - Bottom Right'], ['Ganons Tower (Bottom) (West)']),
create_dungeon_region(player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right', create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower',
'Ganons Tower - Pre-Moldorm Chest'], ['Ganons Tower Moldorm Door']), ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest',
create_dungeon_region(player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']), 'Ganons Tower - Big Key Room - Left',
create_dungeon_region(player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None), 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
create_cave_region(player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']), create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None,
create_cave_region(player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']), ['Ganons Tower Torch Rooms']),
create_dw_region(player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower',
['Ganons Tower - Mini Helmasaur Room - Left',
'Ganons Tower - Mini Helmasaur Room - Right',
'Ganons Tower - Pre-Moldorm Chest'], ['Ganons Tower Moldorm Door']),
create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None,
['Ganons Tower Moldorm Gap']),
create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower',
['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted
# to simplify flute connections # to simplify flute connections
create_cave_region(player, 'The Sky', 'A Dark Sky', None, ['DDM Landing','NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing', 'DLHL Landing']), create_cave_region(world, player, 'The Sky', 'A Dark Sky', None,
['DDM Landing', 'NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing',
'DLHL Landing']),
create_lw_region(player, 'Desert Northern Cliffs'), create_lw_region(world, player, 'Desert Northern Cliffs'),
create_lw_region(player, 'Death Mountain Bunny Descent Area') create_lw_region(world, player, 'Death Mountain Bunny Descent Area')
] ]
world.initialize_regions() world.initialize_regions()
@@ -316,26 +534,26 @@ def create_inverted_regions(world, player):
def mark_dark_world_regions(world, player): def mark_dark_world_regions(world, player):
# cross world caves may have some sections marked as both in_light_world, and in_dark_work. # cross world caves may have some sections marked as both in_light_world, and in_dark_work.
# That is ok. the bunny logic will check for this case and incorporate special rules. # That is ok. the bunny logic will check for this case and incorporate special rules.
queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.DarkWorld) queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
seen = set(queue) seen = set(queue)
while queue: while queue:
current = queue.popleft() current = queue.popleft()
current.is_dark_world = True current.is_dark_world = True
for exit in current.exits: for exit in current.exits:
if exit.connected_region.type == RegionType.LightWorld: if exit.connected_region.type == LTTPRegionType.LightWorld:
# Don't venture into the dark world # Don't venture into the dark world
continue continue
if exit.connected_region not in seen: if exit.connected_region not in seen:
seen.add(exit.connected_region) seen.add(exit.connected_region)
queue.append(exit.connected_region) queue.append(exit.connected_region)
queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.LightWorld) queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.LightWorld)
seen = set(queue) seen = set(queue)
while queue: while queue:
current = queue.popleft() current = queue.popleft()
current.is_light_world = True current.is_light_world = True
for exit in current.exits: for exit in current.exits:
if exit.connected_region.type == RegionType.DarkWorld: if exit.connected_region.type == LTTPRegionType.DarkWorld:
# Don't venture into the light world # Don't venture into the light world
continue continue
if exit.connected_region not in seen: if exit.connected_region not in seen:

View File

@@ -1,15 +1,16 @@
from collections import namedtuple from collections import namedtuple
import logging import logging
from BaseClasses import Region, RegionType, ItemClassification from BaseClasses import ItemClassification
from worlds.alttp.SubClasses import ALttPLocation from worlds.alttp.SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType
from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops, create_dynamic_shop_locations from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops, create_dynamic_shop_locations
from worlds.alttp.Bosses import place_bosses from worlds.alttp.Bosses import place_bosses
from worlds.alttp.Dungeons import get_dungeon_item_pool_player from worlds.alttp.Dungeons import get_dungeon_item_pool_player
from worlds.alttp.EntranceShuffle import connect_entrance from worlds.alttp.EntranceShuffle import connect_entrance
from Fill import FillError from Fill import FillError
from worlds.alttp.Items import ItemFactory, GetBeemizerItem from worlds.alttp.Items import ItemFactory, GetBeemizerItem
from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle, LTTPBosses
from .StateHelpers import has_triforce_pieces, has_melee_weapon
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. # This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided. # Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
@@ -249,8 +250,10 @@ def generate_itempool(world):
world.push_item(loc, ItemFactory('Triforce Piece', player), False) world.push_item(loc, ItemFactory('Triforce Piece', player), False)
world.treasure_hunt_count[player] = 1 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 isinstance(world.boss_shuffle[player].value, str) and 'turtle rock-' not in world.boss_shuffle[player].value:
world.boss_shuffle[player] = f'Turtle Rock-Trinexx;{world.boss_shuffle[player]}' world.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{world.boss_shuffle[player].current_key}')
elif isinstance(world.boss_shuffle[player].value, int):
world.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{world.boss_shuffle[player].current_key}')
else: else:
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
@@ -286,7 +289,7 @@ def generate_itempool(world):
region = world.get_region('Light World', player) region = world.get_region('Light World', player)
loc = ALttPLocation(player, "Murahdahla", parent=region) loc = ALttPLocation(player, "Murahdahla", parent=region)
loc.access_rule = lambda state: state.has_triforce_pieces(state.multiworld.treasure_hunt_count[player], player) loc.access_rule = lambda state: has_triforce_pieces(state, player)
region.locations.append(loc) region.locations.append(loc)
world.clear_location_cache() world.clear_location_cache()
@@ -327,7 +330,7 @@ def generate_itempool(world):
for item in precollected_items: for item in precollected_items:
world.push_precollected(ItemFactory(item, player)) world.push_precollected(ItemFactory(item, player))
if world.mode[player] == 'standard' and not world.state.has_melee_weapon(player): if world.mode[player] == 'standard' and not has_melee_weapon(world.state, player):
if "Link's Uncle" not in placed_items: if "Link's Uncle" not in placed_items:
found_sword = False found_sword = False
found_bow = False found_bow = False
@@ -471,7 +474,7 @@ def set_up_take_anys(world, player):
regions = world.random.sample(take_any_locs, 5) regions = world.random.sample(take_any_locs, 5)
old_man_take_any = Region("Old Man Sword Cave", RegionType.Cave, 'the sword cave', player) old_man_take_any = LTTPRegion("Old Man Sword Cave", LTTPRegionType.Cave, 'the sword cave', player, world)
world.regions.append(old_man_take_any) world.regions.append(old_man_take_any)
reg = regions.pop() reg = regions.pop()
@@ -491,7 +494,7 @@ def set_up_take_anys(world, player):
old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0, create_location=True) old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0, create_location=True)
for num in range(4): for num in range(4):
take_any = Region("Take-Any #{}".format(num+1), RegionType.Cave, 'a cave of choice', player) take_any = LTTPRegion("Take-Any #{}".format(num+1), LTTPRegionType.Cave, 'a cave of choice', player, world)
world.regions.append(take_any) world.regions.append(take_any)
target, room_id = world.random.choice([(0x58, 0x0112), (0x60, 0x010F), (0x46, 0x011F)]) target, room_id = world.random.choice([(0x58, 0x0112), (0x60, 0x010F), (0x46, 0x011F)])

View File

@@ -107,10 +107,14 @@ class Crystals(Range):
class CrystalsTower(Crystals): class CrystalsTower(Crystals):
"""Number of crystals needed to open Ganon's Tower"""
display_name = "Crystals for GT"
default = 7 default = 7
class CrystalsGanon(Crystals): class CrystalsGanon(Crystals):
"""Number of crystals needed to damage Ganon"""
display_name = "Crystals for Ganon"
default = 7 default = 7
@@ -121,12 +125,15 @@ class TriforcePieces(Range):
class ShopItemSlots(Range): class ShopItemSlots(Range):
"""Number of slots in all shops available to have items from the multiworld"""
display_name = "Available Shop Slots"
range_start = 0 range_start = 0
range_end = 30 range_end = 30
class ShopPriceModifier(Range): class ShopPriceModifier(Range):
"""Percentage modifier for shuffled item prices in shops""" """Percentage modifier for shuffled item prices in shops"""
display_name = "Shop Price Cost Percent"
range_start = 0 range_start = 0
default = 100 default = 100
range_end = 400 range_end = 400
@@ -144,7 +151,7 @@ class LTTPBosses(PlandoBosses):
Full chooses 3 bosses at random to be placed twice instead of Lanmolas, Moldorm, and Helmasaur. Full chooses 3 bosses at random to be placed twice instead of Lanmolas, Moldorm, and Helmasaur.
Chaos allows any boss to appear any number of times. Chaos allows any boss to appear any number of times.
Singularity places a single boss in as many places as possible, and a second boss in any remaining locations. Singularity places a single boss in as many places as possible, and a second boss in any remaining locations.
Supports plando placement. Formatting here: https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/plando/en""" Supports plando placement."""
display_name = "Boss Shuffle" display_name = "Boss Shuffle"
option_none = 0 option_none = 0
option_basic = 1 option_basic = 1
@@ -202,6 +209,7 @@ class Enemies(Choice):
class Progressive(Choice): class Progressive(Choice):
"""How item types that have multiple tiers (armor, bows, gloves, shields, and swords) should be rewarded"""
display_name = "Progressive Items" display_name = "Progressive Items"
option_off = 0 option_off = 0
option_grouped_random = 1 option_grouped_random = 1
@@ -305,22 +313,27 @@ class Palette(Choice):
class OWPalette(Palette): class OWPalette(Palette):
"""The type of palette shuffle to use for the overworld"""
display_name = "Overworld Palette" display_name = "Overworld Palette"
class UWPalette(Palette): class UWPalette(Palette):
"""The type of palette shuffle to use for the underworld (caves, dungeons, etc.)"""
display_name = "Underworld Palette" display_name = "Underworld Palette"
class HUDPalette(Palette): class HUDPalette(Palette):
"""The type of palette shuffle to use for the HUD"""
display_name = "Menu Palette" display_name = "Menu Palette"
class SwordPalette(Palette): class SwordPalette(Palette):
"""The type of palette shuffle to use for the sword"""
display_name = "Sword Palette" display_name = "Sword Palette"
class ShieldPalette(Palette): class ShieldPalette(Palette):
"""The type of palette shuffle to use for the shield"""
display_name = "Shield Palette" display_name = "Shield Palette"
@@ -329,6 +342,7 @@ class ShieldPalette(Palette):
class HeartBeep(Choice): class HeartBeep(Choice):
"""How quickly the heart beep sound effect will play"""
display_name = "Heart Beep Rate" display_name = "Heart Beep Rate"
option_normal = 0 option_normal = 0
option_double = 1 option_double = 1
@@ -338,6 +352,7 @@ class HeartBeep(Choice):
class HeartColor(Choice): class HeartColor(Choice):
"""The color of hearts in the HUD"""
display_name = "Heart Color" display_name = "Heart Color"
option_red = 0 option_red = 0
option_blue = 1 option_blue = 1
@@ -346,10 +361,12 @@ class HeartColor(Choice):
class QuickSwap(DefaultOnToggle): class QuickSwap(DefaultOnToggle):
"""Allows you to quickly swap items while playing with L/R"""
display_name = "L/R Quickswapping" display_name = "L/R Quickswapping"
class MenuSpeed(Choice): class MenuSpeed(Choice):
"""How quickly the menu appears/disappears"""
display_name = "Menu Speed" display_name = "Menu Speed"
option_normal = 0 option_normal = 0
option_instant = 1, option_instant = 1,
@@ -360,14 +377,17 @@ class MenuSpeed(Choice):
class Music(DefaultOnToggle): class Music(DefaultOnToggle):
"""Whether background music will play in game"""
display_name = "Play music" display_name = "Play music"
class ReduceFlashing(DefaultOnToggle): class ReduceFlashing(DefaultOnToggle):
"""Reduces flashing for certain scenes such as the Misery Mire and Ganon's Tower opening cutscenes"""
display_name = "Reduce Screen Flashes" display_name = "Reduce Screen Flashes"
class TriforceHud(Choice): class TriforceHud(Choice):
"""When and how the triforce hunt HUD should display"""
display_name = "Display Method for Triforce Hunt" display_name = "Display Method for Triforce Hunt"
option_normal = 0 option_normal = 0
option_hide_goal = 1 option_hide_goal = 1
@@ -375,6 +395,11 @@ class TriforceHud(Choice):
option_hide_both = 3 option_hide_both = 3
class GlitchBoots(DefaultOnToggle):
"""If this is enabled, the player will start with Pegasus Boots when playing with overworld glitches or harder logic."""
display_name = "Glitched Starting Boots"
class BeemizerRange(Range): class BeemizerRange(Range):
value: int value: int
range_start = 0 range_start = 0
@@ -437,7 +462,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
"music": Music, "music": Music,
"reduceflashing": ReduceFlashing, "reduceflashing": ReduceFlashing,
"triforcehud": TriforceHud, "triforcehud": TriforceHud,
"glitch_boots": DefaultOnToggle, "glitch_boots": GlitchBoots,
"beemizer_total_chance": BeemizerTotalChance, "beemizer_total_chance": BeemizerTotalChance,
"beemizer_trap_chance": BeemizerTrapChance, "beemizer_trap_chance": BeemizerTrapChance,
"death_link": DeathLink, "death_link": DeathLink,

View File

@@ -4,6 +4,7 @@ Helper functions to deliver entrance/exit/region sets to OWG rules.
from BaseClasses import Entrance from BaseClasses import Entrance
from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw
def get_sword_required_superbunny_mirror_regions(): def get_sword_required_superbunny_mirror_regions():
""" """
@@ -169,7 +170,7 @@ def get_boots_clip_exits_dw(inverted, player):
yield ('Ganons Tower Ascent', 'Dark Death Mountain (West Bottom)', 'Dark Death Mountain (Top)') # This only gets you to the GT entrance yield ('Ganons Tower Ascent', 'Dark Death Mountain (West Bottom)', 'Dark Death Mountain (Top)') # This only gets you to the GT entrance
yield ('Dark Death Mountain Glitched Bridge', 'Dark Death Mountain (West Bottom)', 'Dark Death Mountain (Top)') yield ('Dark Death Mountain Glitched Bridge', 'Dark Death Mountain (West Bottom)', 'Dark Death Mountain (Top)')
yield ('Turtle Rock (Top) Clip Spot', 'Dark Death Mountain (Top)', 'Turtle Rock (Top)') yield ('Turtle Rock (Top) Clip Spot', 'Dark Death Mountain (Top)', 'Turtle Rock (Top)')
yield ('Ice Palace Clip', 'South Dark World', 'Dark Lake Hylia Central Island', lambda state: state.can_boots_clip_dw(player) and state.has('Flippers', player)) yield ('Ice Palace Clip', 'South Dark World', 'Dark Lake Hylia Central Island', lambda state: can_boots_clip_dw(state, player) and state.has('Flippers', player))
else: else:
yield ('Dark Desert Teleporter Clip Spot', 'Dark Desert', 'Dark Desert Ledge') yield ('Dark Desert Teleporter Clip Spot', 'Dark Desert', 'Dark Desert Ledge')
@@ -203,7 +204,7 @@ def get_mirror_offset_spots_lw(player):
Mirror shenanigans placing a mirror portal with a broken camera Mirror shenanigans placing a mirror portal with a broken camera
""" """
yield ('Death Mountain Offset Mirror', 'Death Mountain', 'Light World') yield ('Death Mountain Offset Mirror', 'Death Mountain', 'Light World')
yield ('Death Mountain Offset Mirror (Houlihan Exit)', 'Death Mountain', 'Hyrule Castle Ledge', lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player) and state.has('Moon Pearl', player)) yield ('Death Mountain Offset Mirror (Houlihan Exit)', 'Death Mountain', 'Hyrule Castle Ledge', lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player) and state.has('Moon Pearl', player))
@@ -255,11 +256,11 @@ def overworld_glitch_connections(world, player):
def overworld_glitches_rules(world, player): def overworld_glitches_rules(world, player):
# Boots-accessible locations. # Boots-accessible locations.
set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: state.can_boots_clip_lw(player)) set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: can_boots_clip_lw(state, player))
set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: state.can_boots_clip_dw(player)) set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: can_boots_clip_dw(state, player))
# Glitched speed drops. # Glitched speed drops.
set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: state.can_get_glitched_speed_dw(player)) set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player))
# Dark Death Mountain Ledge Clip Spot also accessible with mirror. # Dark Death Mountain Ledge Clip Spot also accessible with mirror.
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player)) add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
@@ -267,20 +268,20 @@ def overworld_glitches_rules(world, player):
# Mirror clip spots. # Mirror clip spots.
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player)) set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_lw(player)) set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player))
else: else:
set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player)) set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player))
# Regions that require the boots and some other stuff. # Regions that require the boots and some other stuff.
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (state.can_boots_clip_lw(player) or state.can_lift_heavy_rocks(player)) and state.has('Hammer', player) world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player)
add_alternate_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player)) add_alternate_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player))
else: else:
add_alternate_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player)) add_alternate_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player))
world.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and state.can_lift_heavy_rocks(player) world.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and can_lift_heavy_rocks(state, player)
add_alternate_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: state.can_boots_clip_dw(player)) add_alternate_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_boots_clip_dw(state, player))
add_alternate_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.can_boots_clip_dw(player)) add_alternate_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: can_boots_clip_dw(state, player))
# Zora's Ledge via waterwalk setup. # Zora's Ledge via waterwalk setup.
add_alternate_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player)) add_alternate_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player))

View File

@@ -1,369 +1,566 @@
import collections import collections
import typing import typing
from BaseClasses import Region, Entrance, RegionType from BaseClasses import Entrance, MultiWorld
from .SubClasses import LTTPRegion, LTTPRegionType
def is_main_entrance(entrance: Entrance) -> bool: def is_main_entrance(entrance: Entrance) -> bool:
return entrance.parent_region.type in {RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic} return entrance.parent_region.type in {LTTPRegionType.DarkWorld, LTTPRegionType.LightWorld} if entrance.parent_region.type else True
def create_regions(world, player): def create_regions(world, player):
world.regions += [ world.regions += [
create_lw_region(player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']), create_lw_region(world, player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']),
create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', create_lw_region(world, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure',
'Purple Chest', 'Flute Activation Spot'], 'Purple Chest', 'Flute Activation Spot'],
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam', ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River',
'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave', 'Kings Grave Outer Rocks', 'Dam',
'Blacksmiths Hut', 'Bat Cave Drop Ledge', 'Bat Cave Cave', 'Sick Kids House', 'Hobo Bridge', 'Lost Woods Hideout Drop', 'Lost Woods Hideout Stump', 'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut',
'Lumberjack Tree Tree', 'Lumberjack Tree Cave', 'Mini Moldorm Cave', 'Ice Rod Cave', 'Lake Hylia Central Island Pier', 'Kakariko Well Drop', 'Kakariko Well Cave',
'Bonk Rock Cave', 'Library', 'Potion Shop', 'Two Brothers House (East)', 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow', 'Blacksmiths Hut', 'Bat Cave Drop Ledge', 'Bat Cave Cave', 'Sick Kids House', 'Hobo Bridge',
'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Flute Spot 1', 'Dark Desert Teleporter', 'East Hyrule Teleporter', 'South Hyrule Teleporter', 'Kakariko Teleporter', 'Lost Woods Hideout Drop', 'Lost Woods Hideout Stump',
'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)', 'Lumberjack Tree Tree', 'Lumberjack Tree Cave', 'Mini Moldorm Cave', 'Ice Rod Cave',
'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing', 'Hyrule Castle Main Gate', 'Lake Hylia Central Island Pier',
'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']), 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Two Brothers House (East)',
create_lw_region(player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']), 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow',
create_lw_region(player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']), 'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Flute Spot 1',
create_cave_region(player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top", 'Dark Desert Teleporter', 'East Hyrule Teleporter', 'South Hyrule Teleporter',
"Blind\'s Hideout - Left", 'Kakariko Teleporter',
"Blind\'s Hideout - Right", 'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop',
"Blind\'s Hideout - Far Left", 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)',
"Blind\'s Hideout - Far Right"]), 'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave',
create_cave_region(player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']), 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing',
create_lw_region(player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']), 'Hyrule Castle Main Gate',
create_cave_region(player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']), 'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy',
create_lw_region(player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']), 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller',
create_cave_region(player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']), 'Kakariko Gamble Game', 'Top of Pyramid']),
create_cave_region(player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']), create_lw_region(world, player, 'Death Mountain Entrance', None,
create_cave_region(player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']), ['Old Man Cave (West)', 'Death Mountain Entrance Drop']),
create_cave_region(player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']), create_lw_region(world, player, 'Lake Hylia Central Island', None,
create_cave_region(player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']), ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']),
create_cave_region(player, 'Tavern', 'the tavern', ['Kakariko Tavern']), create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
create_cave_region(player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']), "Blind\'s Hideout - Left",
create_cave_region(player, 'Snitch Lady (East)', 'a boring house'), "Blind\'s Hideout - Right",
create_cave_region(player, 'Snitch Lady (West)', 'a boring house'), "Blind\'s Hideout - Far Left",
create_cave_region(player, 'Bush Covered House', 'the grass man'), "Blind\'s Hideout - Far Right"]),
create_cave_region(player, 'Tavern (Front)', 'the tavern'), create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit',
create_cave_region(player, 'Light World Bomb Hut', 'a restock room'), ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
create_cave_region(player, 'Kakariko Shop', 'a common shop'), create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
create_cave_region(player, 'Fortune Teller (Light)', 'a fortune teller'), create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests',
create_cave_region(player, 'Lake Hylia Fortune Teller', 'a fortune teller'), ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
create_cave_region(player, 'Lumberjack House', 'a boring house'), create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
create_cave_region(player, 'Bonk Fairy (Light)', 'a fairy fountain'), create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
create_cave_region(player, 'Bonk Fairy (Dark)', 'a fairy fountain'), create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
create_cave_region(player, 'Lake Hylia Healer Fairy', 'a fairy fountain'), create_cave_region(world, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
create_cave_region(player, 'Swamp Healer Fairy', 'a fairy fountain'), create_cave_region(world, player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']),
create_cave_region(player, 'Desert Healer Fairy', 'a fairy fountain'), create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
create_cave_region(player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'), create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
create_cave_region(player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'), create_cave_region(world, player, 'Elder House', 'a connector', None,
create_cave_region(player, 'Dark Desert Healer Fairy', 'a fairy fountain'), ['Elder House Exit (East)', 'Elder House Exit (West)']),
create_cave_region(player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'), create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'),
create_cave_region(player, 'Chicken House', 'a house with a chest', ['Chicken House']), create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'),
create_cave_region(player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']), create_cave_region(world, player, 'Bush Covered House', 'the grass man'),
create_cave_region(player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']), create_cave_region(world, player, 'Tavern (Front)', 'the tavern'),
create_cave_region(player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle', create_cave_region(world, player, 'Light World Bomb Hut', 'a restock room'),
'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']), create_cave_region(world, player, 'Kakariko Shop', 'a common shop'),
create_cave_region(player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']), create_cave_region(world, player, 'Fortune Teller (Light)', 'a fortune teller'),
create_cave_region(player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']), create_cave_region(world, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
create_lw_region(player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']), create_cave_region(world, player, 'Lumberjack House', 'a boring house'),
create_cave_region(player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']), create_cave_region(world, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
create_cave_region(player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']), create_cave_region(world, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
create_cave_region(player, 'Sick Kids House', 'the sick kid', ['Sick Kid']), create_cave_region(world, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
create_lw_region(player, 'Hobo Bridge', ['Hobo']), create_cave_region(world, player, 'Swamp Healer Fairy', 'a fairy fountain'),
create_cave_region(player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']), create_cave_region(world, player, 'Desert Healer Fairy', 'a fairy fountain'),
create_cave_region(player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']), create_cave_region(world, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
create_cave_region(player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']), create_cave_region(world, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
create_cave_region(player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']), create_cave_region(world, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
create_lw_region(player, 'Cave 45 Ledge', None, ['Cave 45']), create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
create_cave_region(player, 'Cave 45', 'a cave with an item', ['Cave 45']), create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
create_lw_region(player, 'Graveyard Ledge', None, ['Graveyard Cave']), create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
create_cave_region(player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']), create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla',
create_cave_region(player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']), ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right',
create_cave_region(player, 'Long Fairy Cave', 'a fairy fountain'), 'Sahasrahla']),
create_cave_region(player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit',
'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']), ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
create_cave_region(player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']), 'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
create_cave_region(player, 'Good Bee Cave', 'a cold bee'), create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
create_cave_region(player, '20 Rupee Cave', 'a cave with some cash'), create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
create_cave_region(player, 'Cave Shop (Lake Hylia)', 'a common shop'), create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
create_cave_region(player, 'Cave Shop (Dark Death Mountain)', 'a common shop'), create_cave_region(world, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
create_cave_region(player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']), create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
create_cave_region(player, 'Library', 'the library', ['Library']), create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
create_cave_region(player, 'Kakariko Gamble Game', 'a game of chance'), create_lw_region(world, player, 'Hobo Bridge', ['Hobo']),
create_cave_region(player, 'Potion Shop', 'the potion shop', ['Potion Shop']), create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'],
create_lw_region(player, 'Lake Hylia Island', ['Lake Hylia Island']), ['Lost Woods Hideout (top to bottom)']),
create_cave_region(player, 'Capacity Upgrade', 'the queen of fairies'), create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None,
create_cave_region(player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']), ['Lost Woods Hideout Exit']),
create_lw_region(player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']), create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'],
create_cave_region(player, '50 Rupee Cave', 'a cave with some cash'), ['Lumberjack Tree (top to bottom)']),
create_lw_region(player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']), create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
create_lw_region(player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']), create_lw_region(world, player, 'Cave 45 Ledge', None, ['Cave 45']),
create_lw_region(player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']), create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
create_lw_region(player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']), create_lw_region(world, player, 'Graveyard Ledge', None, ['Graveyard Cave']),
create_lw_region(player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']), create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
create_dungeon_region(player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'], create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']), create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'),
create_dungeon_region(player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']), create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items',
create_dungeon_region(player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']), ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
create_dungeon_region(player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']), 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
create_dungeon_region(player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest', create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'], ['Eastern Palace Exit']), create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'),
create_lw_region(player, 'Master Sword Meadow', ['Master Sword Pedestal']), create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'),
create_cave_region(player, 'Lost Woods Gamble', 'a game of chance'), create_cave_region(world, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
create_lw_region(player, 'Hyrule Castle Courtyard', None, ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']), create_cave_region(world, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
create_lw_region(player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Hyrule Castle Ledge Courtyard Drop']), create_cave_region(world, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
create_dungeon_region(player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest'], create_cave_region(world, player, 'Library', 'the library', ['Library']),
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']), create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'),
create_dungeon_region(player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
create_dungeon_region(player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross'], ['Sewers Door']), create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']),
create_dungeon_region(player, 'Sewers', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies'),
'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']), create_cave_region(world, player, 'Two Brothers House', 'a connector', None,
create_dungeon_region(player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']), ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
create_dungeon_region(player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze'], ['Agahnim 1', 'Agahnims Tower Exit']), create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']),
create_dungeon_region(player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None), create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'),
create_cave_region(player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']), create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'],
create_cave_region(player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']), ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']),
create_cave_region(player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']), create_lw_region(world, player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']),
create_lw_region(player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']), create_lw_region(world, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']),
create_cave_region(player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']), create_lw_region(world, player, 'Desert Palace Lone Stairs', None,
create_lw_region(player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']), ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
create_cave_region(player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']), create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None,
create_cave_region(player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']), ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']),
create_cave_region(player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']), create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace',
create_lw_region(player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']), ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
create_cave_region(player, 'Hookshot Fairy', 'fairies deep in a cave'), ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)',
create_cave_region(player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']), 'Desert Palace East Wing']),
create_cave_region(player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left', create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None,
'Paradox Cave Lower - Left', ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
'Paradox Cave Lower - Right', create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace',
'Paradox Cave Lower - Far Right', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
'Paradox Cave Lower - Middle', create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace',
'Paradox Cave Upper - Left', ['Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
'Paradox Cave Upper - Right'], create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace',
['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
'Eastern Palace - Cannonball Chest',
'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss',
'Eastern Palace - Prize'], ['Eastern Palace Exit']),
create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'),
create_lw_region(world, player, 'Hyrule Castle Courtyard', None,
['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']),
create_lw_region(world, player, 'Hyrule Castle Ledge', None,
['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower',
'Hyrule Castle Ledge Courtyard Drop']),
create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle',
['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
'Hyrule Castle - Zelda\'s Chest'],
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)',
'Throne Room']),
create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross'],
['Sewers Door']),
create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit',
['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']),
create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
create_dungeon_region(world, player, 'Agahnims Tower', 'Castle Tower',
['Castle Tower - Room 03', 'Castle Tower - Dark Maze'],
['Agahnim 1', 'Agahnims Tower Exit']),
create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'],
['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
create_cave_region(world, player, 'Old Man House', 'a connector', None,
['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
create_cave_region(world, player, 'Old Man House Back', 'a connector', None,
['Old Man House Exit (Top)', 'Old Man House Back to Front']),
create_lw_region(world, player, 'Death Mountain', None,
['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)',
'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak',
'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']),
create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None,
['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
create_lw_region(world, player, 'Death Mountain Return Ledge', None,
['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']),
create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'],
['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None,
['Spectacle Rock Cave Exit']),
create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None,
['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
create_lw_region(world, player, 'East Death Mountain (Bottom)', None,
['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)',
'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks',
'Spiral Cave (Bottom)']),
create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'),
create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None,
['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)',
'Light World Death Mountain Shop']),
create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
'Paradox Cave Lower - Left',
'Paradox Cave Lower - Right',
'Paradox Cave Lower - Far Right',
'Paradox Cave Lower - Middle',
'Paradox Cave Upper - Left',
'Paradox Cave Upper - Right'],
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']), ['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
create_cave_region(player, 'Paradox Cave', 'a connector', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']), create_cave_region(world, player, 'Paradox Cave', 'a connector', None,
create_cave_region(player, 'Light World Death Mountain Shop', 'a common shop'), ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
create_lw_region(player, 'East Death Mountain (Top)', None, ['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access', 'East Death Mountain Drop', 'Turtle Rock Teleporter', 'Fairy Ascension Ledge']), create_cave_region(world, player, 'Light World Death Mountain Shop', 'a common shop'),
create_lw_region(player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']), create_lw_region(world, player, 'East Death Mountain (Top)', None,
create_cave_region(player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'], ['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']), ['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access',
create_cave_region(player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']), 'East Death Mountain Drop', 'Turtle Rock Teleporter', 'Fairy Ascension Ledge']),
create_lw_region(player, 'Fairy Ascension Plateau', None, ['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']), create_lw_region(world, player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']),
create_cave_region(player, 'Fairy Ascension Cave (Bottom)', 'a connector', None, ['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']), create_cave_region(world, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
create_cave_region(player, 'Fairy Ascension Cave (Drop)', 'a connector', None, ['Fairy Ascension Cave Pots']), ['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']),
create_cave_region(player, 'Fairy Ascension Cave (Top)', 'a connector', None, ['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']), create_cave_region(world, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
create_lw_region(player, 'Fairy Ascension Ledge', None, ['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)']), create_lw_region(world, player, 'Fairy Ascension Plateau', None,
create_lw_region(player, 'Death Mountain (Top)', ['Ether Tablet'], ['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop']), ['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']),
create_lw_region(player, 'Spectacle Rock', ['Spectacle Rock'], ['Spectacle Rock Drop']), create_cave_region(world, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
create_dungeon_region(player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']), ['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']),
create_dungeon_region(player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']), create_cave_region(world, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
create_dungeon_region(player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']), ['Fairy Ascension Cave Pots']),
create_cave_region(world, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']),
create_lw_region(world, player, 'Fairy Ascension Ledge', None,
['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)']),
create_lw_region(world, player, 'Death Mountain (Top)', ['Ether Tablet'],
['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop']),
create_lw_region(world, player, 'Spectacle Rock', ['Spectacle Rock'], ['Spectacle Rock Drop']),
create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera',
['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'],
['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera',
['Tower of Hera - Big Key Chest']),
create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera',
['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss',
'Tower of Hera - Prize']),
create_dw_region(player, 'East Dark World', ['Pyramid'], ['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness', 'Dark Lake Hylia Drop (East)', create_dw_region(world, player, 'East Dark World', ['Pyramid'],
'Hyrule Castle Ledge Mirror Spot', 'Dark Lake Hylia Fairy', 'Palace of Darkness Hint', 'East Dark World Hint', 'Pyramid Hole', 'Northeast Dark World Broken Bridge Pass',]), ['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness',
create_dw_region(player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']), 'Dark Lake Hylia Drop (East)',
create_dw_region(player, 'Northeast Dark World', None, ['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass', 'Catfish Entrance Rock', 'Dark Lake Hylia Teleporter']), 'Hyrule Castle Ledge Mirror Spot', 'Dark Lake Hylia Fairy', 'Palace of Darkness Hint',
create_cave_region(player, 'Palace of Darkness Hint', 'a storyteller'), 'East Dark World Hint', 'Pyramid Hole', 'Northeast Dark World Broken Bridge Pass', ]),
create_cave_region(player, 'East Dark World Hint', 'a storyteller'), create_dw_region(world, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
create_dw_region(player, 'South Dark World', ['Stumpy', 'Digging Game'], ['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock', 'Maze Race Mirror Spot', create_dw_region(world, player, 'Northeast Dark World', None,
'Cave 45 Mirror Spot', 'East Dark World Bridge', 'Big Bomb Shop', 'Archery Game', 'Bonk Fairy (Dark)', 'Dark Lake Hylia Shop', ['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass',
'Bombos Tablet Mirror Spot']), 'Catfish Entrance Rock', 'Dark Lake Hylia Teleporter']),
create_lw_region(player, 'Bombos Tablet Ledge', ['Bombos Tablet']), create_cave_region(world, player, 'Palace of Darkness Hint', 'a storyteller'),
create_cave_region(player, 'Big Bomb Shop', 'the bomb shop'), create_cave_region(world, player, 'East Dark World Hint', 'a storyteller'),
create_cave_region(player, 'Archery Game', 'a game of skill'), create_dw_region(world, player, 'South Dark World', ['Stumpy', 'Digging Game'],
create_dw_region(player, 'Dark Lake Hylia', None, ['Lake Hylia Island Mirror Spot', 'East Dark World Pier', 'Dark Lake Hylia Ledge']), ['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock',
create_dw_region(player, 'Dark Lake Hylia Central Island', None, ['Ice Palace', 'Lake Hylia Central Island Mirror Spot']), 'Maze Race Mirror Spot',
create_dw_region(player, 'Dark Lake Hylia Ledge', None, ['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Spike Cave']), 'Cave 45 Mirror Spot', 'East Dark World Bridge', 'Big Bomb Shop', 'Archery Game',
create_cave_region(player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'), 'Bonk Fairy (Dark)', 'Dark Lake Hylia Shop',
create_cave_region(player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'), 'Bombos Tablet Mirror Spot']),
create_cave_region(player, 'Hype Cave', 'a bounty of five items', ['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', create_lw_region(world, player, 'Bombos Tablet Ledge', ['Bombos Tablet']),
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']), create_cave_region(world, player, 'Big Bomb Shop', 'the bomb shop'),
create_dw_region(player, 'West Dark World', ['Frog'], ['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House', 'Chest Game', 'Thieves Town', 'Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot', 'Bumper Cave Entrance Rock', create_cave_region(world, player, 'Archery Game', 'a game of skill'),
'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks', 'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)', 'Dark World Lumberjack Shop']), create_dw_region(world, player, 'Dark Lake Hylia', None,
create_dw_region(player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']), ['Lake Hylia Island Mirror Spot', 'East Dark World Pier', 'Dark Lake Hylia Ledge']),
create_dw_region(player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'], ['Bat Cave Drop Ledge Mirror Spot', 'Dark World Hammer Peg Cave', 'Peg Area Rocks']), create_dw_region(world, player, 'Dark Lake Hylia Central Island', None,
create_dw_region(player, 'Bumper Cave Entrance', None, ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Mirror Spot', 'Bumper Cave Entrance Drop']), ['Ice Palace', 'Lake Hylia Central Island Mirror Spot']),
create_cave_region(player, 'Fortune Teller (Dark)', 'a fortune teller'), create_dw_region(world, player, 'Dark Lake Hylia Ledge', None,
create_cave_region(player, 'Village of Outcasts Shop', 'a common shop'), ['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint',
create_cave_region(player, 'Dark Lake Hylia Shop', 'a common shop'), 'Dark Lake Hylia Ledge Spike Cave']),
create_cave_region(player, 'Dark World Lumberjack Shop', 'a common shop'), create_cave_region(world, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
create_cave_region(player, 'Dark World Potion Shop', 'a common shop'), create_cave_region(world, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
create_cave_region(player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']), create_cave_region(world, player, 'Hype Cave', 'a bounty of five items',
create_cave_region(player, 'Pyramid Fairy', 'a cave with two chests', ['Pyramid Fairy - Left', 'Pyramid Fairy - Right']), ['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left',
create_cave_region(player, 'Brewery', 'a house with a chest', ['Brewery']), 'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
create_cave_region(player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']), create_dw_region(world, player, 'West Dark World', ['Frog'],
create_cave_region(player, 'Chest Game', 'a game of 16 chests', ['Chest Game']), ['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House',
create_cave_region(player, 'Red Shield Shop', 'the rare shop'), 'Chest Game', 'Thieves Town', 'Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot',
create_cave_region(player, 'Dark Sanctuary Hint', 'a storyteller'), 'Bumper Cave Entrance Rock',
create_cave_region(player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']), 'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks',
create_dw_region(player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], ['Bumper Cave Ledge Drop', 'Bumper Cave (Top)', 'Bumper Cave Ledge Mirror Spot']), 'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)',
create_dw_region(player, 'Skull Woods Forest', None, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Dark World Lumberjack Shop']),
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']), create_dw_region(world, player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']),
create_dw_region(player, 'Skull Woods Forest (West)', None, ['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section']), create_dw_region(world, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
create_dw_region(player, 'Dark Desert', None, ['Misery Mire', 'Mire Shed', 'Desert Ledge (Northeast) Mirror Spot', 'Desert Ledge Mirror Spot', 'Desert Palace Stairs Mirror Spot', ['Bat Cave Drop Ledge Mirror Spot', 'Dark World Hammer Peg Cave', 'Peg Area Rocks']),
'Desert Palace Entrance (North) Mirror Spot', 'Dark Desert Hint', 'Dark Desert Fairy']), create_dw_region(world, player, 'Bumper Cave Entrance', None,
create_cave_region(player, 'Mire Shed', 'a cave with two chests', ['Mire Shed - Left', 'Mire Shed - Right']), ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Mirror Spot', 'Bumper Cave Entrance Drop']),
create_cave_region(player, 'Dark Desert Hint', 'a storyteller'), create_cave_region(world, player, 'Fortune Teller (Dark)', 'a fortune teller'),
create_dw_region(player, 'Dark Death Mountain (West Bottom)', None, ['Spike Cave', 'Spectacle Rock Mirror Spot', 'Dark Death Mountain Fairy']), create_cave_region(world, player, 'Village of Outcasts Shop', 'a common shop'),
create_dw_region(player, 'Dark Death Mountain (Top)', None, ['Dark Death Mountain Drop (East)', 'Dark Death Mountain Drop (West)', 'Ganons Tower', 'Superbunny Cave (Top)', create_cave_region(world, player, 'Dark Lake Hylia Shop', 'a common shop'),
'Hookshot Cave', 'East Death Mountain (Top) Mirror Spot', 'Turtle Rock']), create_cave_region(world, player, 'Dark World Lumberjack Shop', 'a common shop'),
create_dw_region(player, 'Dark Death Mountain Ledge', None, ['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)', 'Mimic Cave Mirror Spot', 'Spiral Cave Mirror Spot']), create_cave_region(world, player, 'Dark World Potion Shop', 'a common shop'),
create_dw_region(player, 'Dark Death Mountain Isolated Ledge', None, ['Isolated Ledge Mirror Spot', 'Turtle Rock Isolated Ledge Entrance']), create_cave_region(world, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
create_dw_region(player, 'Dark Death Mountain (East Bottom)', None, ['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)', 'Fairy Ascension Mirror Spot']), create_cave_region(world, player, 'Pyramid Fairy', 'a cave with two chests',
create_cave_region(player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']), ['Pyramid Fairy - Left', 'Pyramid Fairy - Right']),
create_cave_region(player, 'Superbunny Cave (Bottom)', 'a connector', None, ['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']), create_cave_region(world, player, 'Brewery', 'a house with a chest', ['Brewery']),
create_cave_region(player, 'Spike Cave', 'Spike Cave', ['Spike Cave']), create_cave_region(world, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
create_cave_region(player, 'Hookshot Cave', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'], create_cave_region(world, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
create_cave_region(world, player, 'Red Shield Shop', 'the rare shop'),
create_cave_region(world, player, 'Dark Sanctuary Hint', 'a storyteller'),
create_cave_region(world, player, 'Bumper Cave', 'a connector', None,
['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
['Bumper Cave Ledge Drop', 'Bumper Cave (Top)', 'Bumper Cave Ledge Mirror Spot']),
create_dw_region(world, player, 'Skull Woods Forest', None,
['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)',
'Skull Woods First Section Hole (North)',
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']),
create_dw_region(world, player, 'Skull Woods Forest (West)', None,
['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)',
'Skull Woods Final Section']),
create_dw_region(world, player, 'Dark Desert', None,
['Misery Mire', 'Mire Shed', 'Desert Ledge (Northeast) Mirror Spot',
'Desert Ledge Mirror Spot', 'Desert Palace Stairs Mirror Spot',
'Desert Palace Entrance (North) Mirror Spot', 'Dark Desert Hint', 'Dark Desert Fairy']),
create_cave_region(world, player, 'Mire Shed', 'a cave with two chests',
['Mire Shed - Left', 'Mire Shed - Right']),
create_cave_region(world, player, 'Dark Desert Hint', 'a storyteller'),
create_dw_region(world, player, 'Dark Death Mountain (West Bottom)', None,
['Spike Cave', 'Spectacle Rock Mirror Spot', 'Dark Death Mountain Fairy']),
create_dw_region(world, player, 'Dark Death Mountain (Top)', None,
['Dark Death Mountain Drop (East)', 'Dark Death Mountain Drop (West)', 'Ganons Tower',
'Superbunny Cave (Top)',
'Hookshot Cave', 'East Death Mountain (Top) Mirror Spot', 'Turtle Rock']),
create_dw_region(world, player, 'Dark Death Mountain Ledge', None,
['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)',
'Mimic Cave Mirror Spot', 'Spiral Cave Mirror Spot']),
create_dw_region(world, player, 'Dark Death Mountain Isolated Ledge', None,
['Isolated Ledge Mirror Spot', 'Turtle Rock Isolated Ledge Entrance']),
create_dw_region(world, player, 'Dark Death Mountain (East Bottom)', None,
['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)',
'Fairy Ascension Mirror Spot']),
create_cave_region(world, player, 'Superbunny Cave (Top)', 'a connector',
['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
create_cave_region(world, player, 'Superbunny Cave (Bottom)', 'a connector', None,
['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
create_cave_region(world, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
create_cave_region(world, player, 'Hookshot Cave', 'a connector',
['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right',
'Hookshot Cave - Bottom Left'],
['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']), ['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']),
create_dw_region(player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']), create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None,
create_lw_region(player, 'Death Mountain Floating Island (Light World)', ['Floating Island']), ['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']),
create_dw_region(player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']), create_lw_region(world, player, 'Death Mountain Floating Island (Light World)', ['Floating Island']),
create_lw_region(player, 'Mimic Cave Ledge', None, ['Mimic Cave']), create_dw_region(world, player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']),
create_cave_region(player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']), create_lw_region(world, player, 'Mimic Cave Ledge', None, ['Mimic Cave']),
create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
create_dungeon_region(player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']), create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None,
create_dungeon_region(player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']), ['Swamp Palace Moat', 'Swamp Palace Exit']),
create_dungeon_region(player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest'], ['Swamp Palace (Center)']), create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'],
create_dungeon_region(player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', ['Swamp Palace Small Key Door']),
'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest'], ['Swamp Palace (North)']), create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace',
create_dungeon_region(player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', ['Swamp Palace - Map Chest'], ['Swamp Palace (Center)']),
'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']), create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace',
create_dungeon_region(player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest',
'Thieves\' Town - Map Chest', 'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest'], ['Swamp Palace (North)']),
'Thieves\' Town - Compass Chest', create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace',
'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']), ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
create_dungeon_region(player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic', 'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']),
'Thieves\' Town - Big Chest', create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town',
'Thieves\' Town - Blind\'s Cell'], ['Blind Fight']), ['Thieves\' Town - Big Key Chest',
create_dungeon_region(player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']), 'Thieves\' Town - Map Chest',
create_dungeon_region(player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']), 'Thieves\' Town - Compass Chest',
create_dungeon_region(player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']), 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
create_dungeon_region(player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']), create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
create_dungeon_region(player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']), 'Thieves\' Town - Big Chest',
create_dungeon_region(player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']), 'Thieves\' Town - Blind\'s Cell'],
create_dungeon_region(player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']), ['Blind Fight']),
create_dungeon_region(player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']), create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town',
create_dungeon_region(player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Boss', 'Skull Woods - Prize']), ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
create_dungeon_region(player, 'Ice Palace (Entrance)', 'Ice Palace', None, ['Ice Palace Entrance Room', 'Ice Palace Exit']), create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'],
create_dungeon_region(player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest', ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump',
'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']), 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
create_dungeon_region(player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']), create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods',
create_dungeon_region(player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest']), ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
create_dungeon_region(player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']), create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods',
create_dungeon_region(player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']), ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'],
create_dungeon_region(player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', ['Skull Woods First Section (Left) Door to Exit',
'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'], ['Misery Mire (West)', 'Misery Mire Big Key Door']), 'Skull Woods First Section (Left) Door to Right']),
create_dungeon_region(player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']), create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods',
create_dungeon_region(player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']), ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
create_dungeon_region(player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']), create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None,
create_dungeon_region(player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']), ['Skull Woods Second Section (Drop)']),
create_dungeon_region(player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods',
'Turtle Rock - Roller Room - Right'], ['Turtle Rock Pokey Room', 'Turtle Rock Entrance Gap Reverse']), ['Skull Woods - Big Key Chest'],
create_dungeon_region(player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']), ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
create_dungeon_region(player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest'], ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door']), create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods',
create_dungeon_region(player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']), ['Skull Woods - Bridge Room'],
create_dungeon_region(player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']), ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
create_dungeon_region(player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']), create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods',
create_dungeon_region(player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', ['Skull Woods - Boss', 'Skull Woods - Prize']),
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'], create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', None,
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']), ['Ice Palace Entrance Room', 'Ice Palace Exit']),
create_dungeon_region(player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']), create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace',
create_dungeon_region(player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']), ['Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest',
create_dungeon_region(player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'], 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'],
['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']), ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
create_dungeon_region(player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']), create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'],
create_dungeon_region(player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']), ['Ice Palace (East Top)']),
create_dungeon_region(player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'], create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace',
['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest']),
create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace',
['Ice Palace - Boss', 'Ice Palace - Prize']),
create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None,
['Misery Mire Entrance Gap', 'Misery Mire Exit']),
create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire',
['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'],
['Misery Mire (West)', 'Misery Mire Big Key Door']),
create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire',
['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None,
['Misery Mire (Vitreous)']),
create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire',
['Misery Mire - Boss', 'Misery Mire - Prize']),
create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None,
['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock',
['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
'Turtle Rock - Roller Room - Right'],
['Turtle Rock Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock',
['Turtle Rock - Chain Chomps'],
['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
['Turtle Rock - Big Key Chest'],
['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase',
'Turtle Rock Big Key Door']),
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'],
['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock',
['Turtle Rock - Crystaroller Room'],
['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None,
['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock',
['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)',
'Turtle Rock Isolated Ledge Exit']),
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock',
['Turtle Rock - Boss', 'Turtle Rock - Prize']),
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness',
['Palace of Darkness - Shooter Room'],
['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall',
'Palace of Darkness Exit']),
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness',
['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)',
'Palace of Darkness Big Key Door']),
create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness',
['Palace of Darkness - Big Key Chest']),
create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness',
['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'],
['Palace of Darkness Hammer Peg Drop']),
create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness',
['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left',
'Palace of Darkness - Dark Basement - Right'],
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']), ['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
create_dungeon_region(player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']), create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness',
create_dungeon_region(player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']), ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom',
create_dungeon_region(player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']), 'Palace of Darkness - Big Chest']),
create_dungeon_region(player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left', 'Ganons Tower - Hope Room - Right'], create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness',
['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door', 'Ganons Tower Exit']), ['Palace of Darkness - Harmless Hellway']),
create_dungeon_region(player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']), create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness',
create_dungeon_region(player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right'], create_dungeon_region(world, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower',
['Ganons Tower (Bottom) (East)']), ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
create_dungeon_region(player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - Hope Room - Right'],
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'], ['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door',
'Ganons Tower Exit']),
create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'],
['Ganons Tower (Tile Room) Key Door']),
create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower',
['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
'Ganons Tower - Compass Room - Bottom Left',
'Ganons Tower - Compass Room - Bottom Right'], ['Ganons Tower (Bottom) (East)']),
create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower',
['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'],
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']), ['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
create_dungeon_region(player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']), create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
create_dungeon_region(player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']), create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower',
create_dungeon_region(player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'], create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower',
['Ganons Tower (Bottom) (West)']), ['Ganons Tower - Randomizer Room - Top Left',
create_dungeon_region(player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Randomizer Room - Top Right',
'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']), 'Ganons Tower - Randomizer Room - Bottom Left',
create_dungeon_region(player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']), 'Ganons Tower - Randomizer Room - Bottom Right'], ['Ganons Tower (Bottom) (West)']),
create_dungeon_region(player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right', create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower',
'Ganons Tower - Pre-Moldorm Chest'], ['Ganons Tower Moldorm Door']), ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest',
create_dungeon_region(player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']), 'Ganons Tower - Big Key Room - Left',
create_dungeon_region(player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None), 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
create_cave_region(player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']), create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None,
create_cave_region(player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']), ['Ganons Tower Torch Rooms']),
create_dw_region(player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']), create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower',
create_lw_region(player, 'Desert Northern Cliffs'), ['Ganons Tower - Mini Helmasaur Room - Left',
create_dw_region(player, 'Dark Death Mountain Bunny Descent Area') 'Ganons Tower - Mini Helmasaur Room - Right',
'Ganons Tower - Pre-Moldorm Chest'], ['Ganons Tower Moldorm Door']),
create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None,
['Ganons Tower Moldorm Gap']),
create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower',
['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']),
create_lw_region(world, player, 'Desert Northern Cliffs'),
create_dw_region(world, player, 'Dark Death Mountain Bunny Descent Area')
] ]
world.initialize_regions() world.initialize_regions()
def create_lw_region(player: int, name: str, locations=None, exits=None): def create_lw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits) return _create_region(world, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits)
def create_dw_region(player: int, name: str, locations=None, exits=None): def create_dw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
return _create_region(player, name, RegionType.DarkWorld, 'Dark World', locations, exits) return _create_region(world, player, name, LTTPRegionType.DarkWorld, 'Dark World', locations, exits)
def create_cave_region(player: int, name: str, hint: str, locations=None, exits=None): def create_cave_region(world: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
return _create_region(player, name, RegionType.Cave, hint, locations, exits) return _create_region(world, player, name, LTTPRegionType.Cave, hint, locations, exits)
def create_dungeon_region(player: int, name: str, hint: str, locations=None, exits=None): def create_dungeon_region(world: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
return _create_region(player, name, RegionType.Dungeon, hint, locations, exits) return _create_region(world, player, name, LTTPRegionType.Dungeon, hint, locations, exits)
def _create_region(player: int, name: str, type: RegionType, hint: str, locations=None, exits=None): def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None,
exits=None):
from worlds.alttp.SubClasses import ALttPLocation from worlds.alttp.SubClasses import ALttPLocation
ret = Region(name, type, hint, player) ret = LTTPRegion(name, type, hint, player, world)
if locations is None: if exits:
locations = [] for exit in exits:
if exits is None: ret.exits.append(Entrance(player, exit, ret))
exits = [] if locations:
for location in locations:
for exit in exits: address, player_address, crystal, hint_text = location_table[location]
ret.exits.append(Entrance(player, exit, ret)) ret.locations.append(ALttPLocation(player, location, address, crystal, hint_text, ret, player_address))
for location in locations:
address, player_address, crystal, hint_text = location_table[location]
ret.locations.append(ALttPLocation(player, location, address, crystal, hint_text, ret, player_address))
return ret return ret
def mark_light_world_regions(world, player: int): def mark_light_world_regions(world, player: int):
# cross world caves may have some sections marked as both in_light_world, and in_dark_work. # cross world caves may have some sections marked as both in_light_world, and in_dark_work.
# That is ok. the bunny logic will check for this case and incorporate special rules. # That is ok. the bunny logic will check for this case and incorporate special rules.
queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.LightWorld) queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.LightWorld)
seen = set(queue) seen = set(queue)
while queue: while queue:
current = queue.popleft() current = queue.popleft()
current.is_light_world = True current.is_light_world = True
for exit in current.exits: for exit in current.exits:
if exit.connected_region.type == RegionType.DarkWorld: if exit.connected_region.type == LTTPRegionType.DarkWorld:
# Don't venture into the dark world # Don't venture into the dark world
continue continue
if exit.connected_region not in seen: if exit.connected_region not in seen:
seen.add(exit.connected_region) seen.add(exit.connected_region)
queue.append(exit.connected_region) queue.append(exit.connected_region)
queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.DarkWorld) queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
seen = set(queue) seen = set(queue)
while queue: while queue:
current = queue.popleft() current = queue.popleft()
current.is_dark_world = True current.is_dark_world = True
for exit in current.exits: for exit in current.exits:
if exit.connected_region.type == RegionType.LightWorld: if exit.connected_region.type == LTTPRegionType.LightWorld:
# Don't venture into the light world # Don't venture into the light world
continue continue
if exit.connected_region not in seen: if exit.connected_region not in seen:

View File

@@ -20,7 +20,7 @@ import concurrent.futures
import bsdiff4 import bsdiff4
from typing import Optional, List from typing import Optional, List
from BaseClasses import CollectionState, Region, Location from BaseClasses import CollectionState, Region, Location, MultiWorld
from worlds.alttp.Shops import ShopType, ShopPriceType from worlds.alttp.Shops import ShopType, ShopPriceType
from worlds.alttp.Dungeons import dungeon_music_addresses from worlds.alttp.Dungeons import dungeon_music_addresses
from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address
@@ -92,7 +92,7 @@ class LocalRom(object):
# cause crash to provide traceback # cause crash to provide traceback
import xxtea import xxtea
local_random = world.slot_seeds[player] local_random = world.per_slot_randoms[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, player: int, rom: LocalRom, enemizercli, output_direct
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.slot_seeds[player].randint(0, 999999999)) enemizer_seed = str(world.per_slot_randoms[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, player: int, rom: LocalRom, enemizercli, output_direct
continue continue
for j in range(i + 1, max_enemizer_tries): for j in range(i + 1, max_enemizer_tries):
world.slot_seeds[player].randint(0, 999999999) world.per_slot_randoms[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
@@ -765,8 +765,8 @@ def get_nonnative_item_sprite(item: str) -> int:
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886 # https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
def patch_rom(world, rom, player, enemized): def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
local_random = world.slot_seeds[player] local_random = world.per_slot_randoms[player]
# patch items # patch items
@@ -1646,7 +1646,7 @@ def patch_rom(world, rom, player, 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.slot_seeds[player]) tile_set = TileSet.get_random_tile_set(world.per_slot_randoms[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())
@@ -1779,7 +1779,7 @@ def hud_format_text(text):
def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, palettes_options, def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, 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, deathlink: bool = False, allowcollect: bool = False): triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False):
local_random = random if not world else world.slot_seeds[player] local_random = random if not world else world.per_slot_randoms[player]
disable_music: bool = not music disable_music: bool = not music
# enable instant item menu # enable instant item menu
if menuspeed == 'instant': if menuspeed == 'instant':
@@ -2105,7 +2105,7 @@ def write_string_to_rom(rom, target, string):
def write_strings(rom, world, player): def write_strings(rom, world, player):
from . import ALTTPWorld from . import ALTTPWorld
local_random = world.slot_seeds[player] local_random = world.per_slot_randoms[player]
w: ALTTPWorld = world.worlds[player] w: ALTTPWorld = world.worlds[player]
tt = TextTable() tt = TextTable()
@@ -2330,7 +2330,7 @@ def write_strings(rom, world, player):
if world.worlds[player].has_progressive_bows and (world.difficulty_requirements[player].progressive_bow_limit >= 2 or ( if world.worlds[player].has_progressive_bows and (world.difficulty_requirements[player].progressive_bow_limit >= 2 or (
world.swordless[player] or world.logic[player] == 'noglitches')): world.swordless[player] or world.logic[player] == 'noglitches')):
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True) prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
world.slot_seeds[player].shuffle(prog_bow_locs) world.per_slot_randoms[player].shuffle(prog_bow_locs)
found_bow = False found_bow = False
found_bow_alt = False found_bow_alt = False
while prog_bow_locs and not (found_bow and found_bow_alt): while prog_bow_locs and not (found_bow and found_bow_alt):

View File

@@ -2,16 +2,24 @@ import collections
import logging import logging
from typing import Iterator, Set from typing import Iterator, Set
from worlds.alttp import OverworldGlitchRules from BaseClasses import Entrance, MultiWorld
from BaseClasses import RegionType, MultiWorld, Entrance from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item,
from worlds.alttp.Items import ItemFactory, progression_items, item_name_groups, item_table item_in_locations, location_item_name, set_rule, allow_self_locking_items)
from worlds.alttp.OverworldGlitchRules import overworld_glitches_rules, no_logic_rules
from worlds.alttp.Regions import location_table from . import OverworldGlitchRules
from worlds.alttp.UnderworldGlitchRules import underworld_glitches_rules from .Bosses import GanonDefeatRule
from worlds.alttp.Bosses import GanonDefeatRule from .Items import ItemFactory, item_name_groups, item_table, progression_items
from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \ from .Options import smallkey_shuffle
item_name from .OverworldGlitchRules import no_logic_rules, overworld_glitches_rules
from worlds.alttp.Options import smallkey_shuffle from .Regions import LTTPRegionType, location_table
from .StateHelpers import (can_extend_magic, can_kill_most_things,
can_lift_heavy_rocks, can_lift_rocks,
can_melt_things, can_retrieve_tablet,
can_shoot_arrows, has_beam_sword, has_crystals,
has_fire_source, has_hearts,
has_misery_mire_medallion, has_sword, has_turtle_rock_medallion,
has_triforce_pieces)
from .UnderworldGlitchRules import underworld_glitches_rules
def set_rules(world): def set_rules(world):
@@ -75,7 +83,7 @@ def set_rules(world):
if world.goal[player] == 'bosses': if world.goal[player] == 'bosses':
# require all bosses to beat ganon # require all bosses to beat ganon
add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and state.has_crystals(7, player)) add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and has_crystals(state, 7, player))
elif world.goal[player] == 'ganon': elif world.goal[player] == 'ganon':
# require aga2 to beat ganon # require aga2 to beat ganon
add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player)) add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player))
@@ -100,7 +108,7 @@ def set_rules(world):
set_trock_key_rules(world, player) set_trock_key_rules(world, player)
set_rule(ganons_tower, lambda state: state.has_crystals(state.multiworld.crystals_needed_for_gt[player], player)) set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_gt[player], player))
if world.mode[player] != 'inverted' and world.logic[player] in ['owglitches', 'hybridglitches', '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.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or') add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or')
@@ -198,7 +206,7 @@ def global_rules(world, player):
set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player)) set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player))
set_rule(world.get_location('Purple Chest', player), set_rule(world.get_location('Purple Chest', player),
lambda state: state.has('Pick Up Purple Chest', player)) # Can S&Q with chest lambda state: state.has('Pick Up Purple Chest', player)) # Can S&Q with chest
set_rule(world.get_location('Ether Tablet', player), lambda state: state.can_retrieve_tablet(player)) set_rule(world.get_location('Ether Tablet', player), lambda state: can_retrieve_tablet(state, player))
set_rule(world.get_location('Master Sword Pedestal', player), lambda state: state.has('Red Pendant', player) and state.has('Blue Pendant', player) and state.has('Green Pendant', player)) set_rule(world.get_location('Master Sword Pedestal', player), lambda state: state.has('Red Pendant', player) and state.has('Blue Pendant', player) and state.has('Green Pendant', player))
set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith
@@ -211,11 +219,11 @@ def global_rules(world, player):
set_rule(world.get_location('Spike Cave', player), lambda state: set_rule(world.get_location('Spike Cave', player), lambda state:
state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Hammer', player) and can_lift_rocks(state, player) and
((state.has('Cape', player) and state.can_extend_magic(player, 16, True)) or ((state.has('Cape', player) and can_extend_magic(state, player, 16, True)) or
(state.has('Cane of Byrna', player) and (state.has('Cane of Byrna', player) and
(state.can_extend_magic(player, 12, True) or (can_extend_magic(state, player, 12, True) or
(state.multiworld.can_take_damage[player] and (state.has('Pegasus Boots', player) or state.has_hearts(player, 4)))))) (state.multiworld.can_take_damage[player] and (state.has('Pegasus Boots', player) or has_hearts(state, player, 4))))))
) )
set_rule(world.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player))
@@ -231,11 +239,11 @@ def global_rules(world, player):
set_rule(world.get_entrance('Sewers Back Door', player), set_rule(world.get_entrance('Sewers Back Door', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player)) lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
set_rule(world.get_entrance('Agahnim 1', player), set_rule(world.get_entrance('Agahnim 1', player),
lambda state: state.has_sword(player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) lambda state: has_sword(state, player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: state.can_kill_most_things(player, 8)) set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: can_kill_most_things(state, player, 8))
set_rule(world.get_location('Castle Tower - Dark Maze', player), set_rule(world.get_location('Castle Tower - Dark Maze', player),
lambda state: state.can_kill_most_things(player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)', lambda state: can_kill_most_things(state, player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)',
player)) player))
set_rule(world.get_location('Eastern Palace - Big Chest', player), set_rule(world.get_location('Eastern Palace - Big Chest', player),
@@ -247,62 +255,62 @@ def global_rules(world, player):
set_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and set_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and
ep_prize.parent_region.dungeon.boss.can_defeat(state)) ep_prize.parent_region.dungeon.boss.can_defeat(state))
if not world.enemy_shuffle[player]: if not world.enemy_shuffle[player]:
add_rule(ep_boss, lambda state: state.can_shoot_arrows(player)) add_rule(ep_boss, lambda state: can_shoot_arrows(state, player))
add_rule(ep_prize, lambda state: state.can_shoot_arrows(player)) add_rule(ep_prize, lambda state: can_shoot_arrows(state, player))
set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player)) set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player))
set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player))
set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player)) set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player))
set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state)) set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state)) set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
# logic patch to prevent placing a crystal in Desert that's required to reach the required keys # logic patch to prevent placing a crystal in Desert that's required to reach the required keys
if not (world.smallkey_shuffle[player] and world.bigkey_shuffle[player]): if not (world.smallkey_shuffle[player] and world.bigkey_shuffle[player]):
add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state)) add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state))
set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player)) set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or location_item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player))
set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player))
set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: state.has_fire_source(player)) set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player))
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player) set_always_allow(world.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player)) set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player)) set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player))
set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player))
set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player) or item_name(state, 'Swamp Palace - Big Chest', player) == ('Big Key (Swamp Palace)', player)) set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Swamp Palace - Big Chest', player), lambda state, item: item.name == 'Big Key (Swamp Palace)' and item.player == player) allow_self_locking_items(world.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player))
if not world.smallkey_shuffle[player] and world.logic[player] not in ['hybridglitches', 'nologic']: if not world.smallkey_shuffle[player] and world.logic[player] not in ['hybridglitches', 'nologic']:
forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player) forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player)) set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player)) set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player) or item_name(state, 'Thieves\' Town - Big Chest', player) == ('Small Key (Thieves Town)', player)) and state.has('Hammer', player)) set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player)) and state.has('Hammer', player))
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player and state.has('Hammer', player)) allow_self_locking_items(world.get_location('Thieves\' Town - Big Chest', player), 'Small Key (Thieves Town)')
set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player)) set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player)) set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player)) set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section
set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2))
set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) or item_name(state, 'Skull Woods - Big Chest', player) == ('Big Key (Skull Woods)', player)) set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player))
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Skull Woods - Big Chest', player), lambda state, item: item.name == 'Big Key (Skull Woods)' and item.player == player) allow_self_locking_items(world.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and state.has_sword(player)) # sword required for curtain set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_melt_things(player)) set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: can_melt_things(state, player))
set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player)) set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player))
set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1)))) set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1))))
set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or ( set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or (
item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player))) item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player))
set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player))) # need to defeat wizzrobes, bombs don't work ... set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (has_sword(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or can_shoot_arrows(state, player))) # need to defeat wizzrobes, bombs don't work ...
set_rule(world.get_location('Misery Mire - Big Chest', player), lambda state: state.has('Big Key (Misery Mire)', player)) set_rule(world.get_location('Misery Mire - Big Chest', player), lambda state: state.has('Big Key (Misery Mire)', player))
set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.multiworld.can_take_damage[player] and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.multiworld.can_take_damage[player] and has_hearts(state, player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
set_rule(world.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player)) set_rule(world.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player))
# you can squander the free small key from the pot by opening the south door to the north west switch room, locking you out of accessing a color switch ... # you can squander the free small key from the pot by opening the south door to the north west switch room, locking you out of accessing a color switch ...
# big key gives backdoor access to that from the teleporter in the north west # big key gives backdoor access to that from the teleporter in the north west
@@ -310,11 +318,11 @@ def global_rules(world, player):
set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state._lttp_has_key('Big Key (Misery Mire)', player)) set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state._lttp_has_key('Big Key (Misery Mire)', player))
# we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet # we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet
set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2) if (( set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2) if ((
item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or location_item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or
( (
item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3)) location_item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3))
set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: state.has_fire_source(player)) set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: has_fire_source(state, player))
set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: state.has_fire_source(player)) set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: has_fire_source(state, player))
set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('Turtle Rock Entrance Gap', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Turtle Rock Entrance Gap', player), lambda state: state.has('Cane of Somaria', player))
@@ -334,20 +342,20 @@ def global_rules(world, player):
set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
if not world.enemy_shuffle[player]: if not world.enemy_shuffle[player]:
set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_shoot_arrows(state, player))
set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area
set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and state.can_shoot_arrows(player) and state.has('Hammer', player)) set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and can_shoot_arrows(state, player) and state.has('Hammer', player))
set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)) set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))
set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player)) set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player))
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or ( set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))) location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3)))
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5)) set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or ( set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))) location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5)) set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
@@ -361,7 +369,7 @@ def global_rules(world, player):
set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player))) set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or ( set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))) location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player)) set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
@@ -398,9 +406,9 @@ def global_rules(world, player):
lambda state: state.has('Big Key (Ganons Tower)', player)) lambda state: state.has('Big Key (Ganons Tower)', player))
else: else:
set_rule(world.get_entrance('Ganons Tower Big Key Door', player), set_rule(world.get_entrance('Ganons Tower Big Key Door', player),
lambda state: state.has('Big Key (Ganons Tower)', player) and state.can_shoot_arrows(player)) lambda state: state.has('Big Key (Ganons Tower)', player) and can_shoot_arrows(state, player))
set_rule(world.get_entrance('Ganons Tower Torch Rooms', player), set_rule(world.get_entrance('Ganons Tower Torch Rooms', player),
lambda state: state.has_fire_source(player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state)) lambda state: has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player), set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player),
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3)) lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3))
set_rule(world.get_entrance('Ganons Tower Moldorm Door', player), set_rule(world.get_entrance('Ganons Tower Moldorm Door', player),
@@ -411,12 +419,12 @@ def global_rules(world, player):
ganon = world.get_location('Ganon', player) ganon = world.get_location('Ganon', player)
set_rule(ganon, lambda state: GanonDefeatRule(state, player)) set_rule(ganon, lambda state: GanonDefeatRule(state, player))
if world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']: if world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']:
add_rule(ganon, lambda state: state.has_triforce_pieces(state.multiworld.treasure_hunt_count[player], player)) add_rule(ganon, lambda state: has_triforce_pieces(state, player))
elif world.goal[player] == 'ganonpedestal': elif world.goal[player] == 'ganonpedestal':
add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player)) add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player))
else: else:
add_rule(ganon, lambda state: state.has_crystals(state.multiworld.crystals_needed_for_ganon[player], player)) add_rule(ganon, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_ganon[player], player))
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop set_rule(world.get_entrance('Ganon Drop', player), lambda state: has_beam_sword(state, player)) # need to damage ganon to get tiles to drop
set_rule(world.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player)) set_rule(world.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player))
@@ -425,51 +433,51 @@ def default_rules(world, player):
"""Default world rules when world state is not inverted.""" """Default world rules when world state is not inverted."""
# overworld requirements # overworld requirements
set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player))
set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('Kings Grave Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) set_rule(world.get_entrance('Kings Grave Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player))
# Caution: If king's grave is releaxed at all to account for reaching it via a two way cave's exit in insanity mode, then the bomb shop logic will need to be updated (that would involve create a small ledge-like Region for it) # Caution: If king's grave is releaxed at all to account for reaching it via a two way cave's exit in insanity mode, then the bomb shop logic will need to be updated (that would involve create a small ledge-like Region for it)
set_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player))
set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Beat Agahnim 1', player)) set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Beat Agahnim 1', player))
set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player))
set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player))
set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player))
set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player))
set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player))
set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player))
set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Flute Spot 1', player), lambda state: state.has('Activated Flute', player)) set_rule(world.get_entrance('Flute Spot 1', player), lambda state: state.has('Activated Flute', player))
set_rule(world.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer set_rule(world.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
set_rule(world.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer set_rule(world.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
set_rule(world.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and state.can_lift_rocks(player)) or state.can_lift_heavy_rocks(player)) and state.has('Moon Pearl', player)) # bunny cannot lift bushes set_rule(world.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) # bunny cannot lift bushes
set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player)) set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player))
set_rule(world.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player))
set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player)) set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player))
set_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Flippers', player))
set_rule(world.get_location('Frog', player), lambda state: state.can_lift_heavy_rocks(player)) # will get automatic moon pearl requirement set_rule(world.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player)) # will get automatic moon pearl requirement
set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player)) set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player))
set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player))
set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: state.can_lift_rocks(player)) # should we decide to place something that is not a dungeon end up there at some point set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player)) # should we decide to place something that is not a dungeon end up there at some point
set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player))
set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle
set_rule(world.get_entrance('Top of Pyramid', player), lambda state: state.has('Beat Agahnim 1', player)) set_rule(world.get_entrance('Top of Pyramid', player), lambda state: state.has('Beat Agahnim 1', player))
set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up
set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('East Death Mountain Teleporter', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('East Death Mountain Teleporter', player), lambda state: can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block
set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player))
set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_lift_rocks(state, player))
set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player))
set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (state.can_lift_rocks(player) or state.has('Hammer', player) or state.has('Flippers', player))) set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player) or state.has('Flippers', player)))
set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (state.can_lift_rocks(player) or state.has('Hammer', player))) set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player)))
set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Moon Pearl', player) and state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Moon Pearl', player) and state.has('Pegasus Boots', player))
set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Moon Pearl', player) and state.has('Hookshot', player)) set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Moon Pearl', player) and state.has('Hookshot', player))
@@ -477,12 +485,12 @@ def default_rules(world, player):
set_rule(world.get_entrance('Hyrule Castle Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Hyrule Castle Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Hyrule Castle Main Gate', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Hyrule Castle Main Gate', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: (state.has('Moon Pearl', player) and state.has('Flippers', player) or state.has('Magic Mirror', player))) # Overworld Bunny Revival set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: (state.has('Moon Pearl', player) and state.has('Flippers', player) or state.has('Magic Mirror', player))) # Overworld Bunny Revival
set_rule(world.get_location('Bombos Tablet', player), lambda state: state.can_retrieve_tablet(player)) set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player))
set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # ToDo any fake flipper set up? set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # ToDo any fake flipper set up?
set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player)) # bomb required set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player)) # bomb required
set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player)) # bomb required set_rule(world.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player)) # bomb required
set_rule(world.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player)) # bomb required set_rule(world.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player)) # bomb required
set_rule(world.get_entrance('Thieves Town', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot pull set_rule(world.get_entrance('Thieves Town', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot pull
@@ -496,26 +504,26 @@ def default_rules(world, player):
set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
set_rule(world.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) set_rule(world.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player))
set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_rocks(player)) set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_rocks(state, player))
set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Bat Cave Drop Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Bat Cave Drop Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player))
set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player))
set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player))
set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player))
set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player))
set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player) and state.has('Moon Pearl', player)) # bunny cannot use fire rod set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player) and state.has('Moon Pearl', player)) # bunny cannot use fire rod
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and state.has_sword(player) and state.has_misery_mire_medallion(player)) # sword required to cast magic (!) set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!)
set_rule(world.get_entrance('Desert Ledge (Northeast) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Desert Ledge (Northeast) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Desert Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Desert Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Desert Palace Entrance (North) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Desert Palace Entrance (North) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Spectacle Rock Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Spectacle Rock Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Hookshot Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('East Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('East Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Mimic Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Mimic Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
@@ -524,7 +532,7 @@ def default_rules(world, player):
set_rule(world.get_entrance('Isolated Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Isolated Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Superbunny Cave Exit (Bottom)', player), lambda state: False) # Cannot get to bottom exit from top. Just exists for shuffling set_rule(world.get_entrance('Superbunny Cave Exit (Bottom)', player), lambda state: False) # Cannot get to bottom exit from top. Just exists for shuffling
set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and state.has_sword(player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player].to_bool(world, player)) set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player].to_bool(world, player))
@@ -544,12 +552,12 @@ def inverted_rules(world, player):
set_rule(world.get_entrance('Potion Shop Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Potion Shop Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Light World Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Light World Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Potion Shop Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Potion Shop Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Potion Shop Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Potion Shop Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Potion Shop Outer Rock', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Potion Shop Outer Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Potion Shop Inner Rock', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Potion Shop Inner Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Graveyard Cave Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Graveyard Cave Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Graveyard Cave Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Graveyard Cave Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Secret Passage Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Secret Passage Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
@@ -559,23 +567,23 @@ def inverted_rules(world, player):
set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) and state.has('Beat Agahnim 1', player)) set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) and state.has('Beat Agahnim 1', player))
set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) # bunny can use book set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) # bunny can use book
set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Dark Lake Hylia Central Island Teleporter', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Dark Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer set_rule(world.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
set_rule(world.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer set_rule(world.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
set_rule(world.get_entrance('West Dark World Teleporter', player), lambda state: ((state.has('Hammer', player) and state.can_lift_rocks(player)) or state.can_lift_heavy_rocks(player)) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('West Dark World Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player))
set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player) and state.has('Moon Pearl', player)) set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player) and state.has('Moon Pearl', player))
set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Northeast Light World Return', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Northeast Light World Return', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
set_rule(world.get_location('Frog', player), lambda state: state.can_lift_heavy_rocks(player) and (state.has('Moon Pearl', player) or state.has('Beat Agahnim 1', player)) or (state.can_reach('Light World', 'Region', player) and state.has('Magic Mirror', player))) # Need LW access using Mirror or Portal set_rule(world.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player) and (state.has('Moon Pearl', player) or state.has('Beat Agahnim 1', player)) or (state.can_reach('Light World', 'Region', player) and state.has('Magic Mirror', player))) # Need LW access using Mirror or Portal
set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith
set_rule(world.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player)) set_rule(world.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player))
set_rule(world.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player) and state.has('Moon Pearl', player)) set_rule(world.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player) and state.has('Moon Pearl', player))
@@ -590,52 +598,52 @@ def inverted_rules(world, player):
set_rule(world.get_entrance('North Fairy Cave Drop', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('North Fairy Cave Drop', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Lost Woods Hideout Drop', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Lost Woods Hideout Drop', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy
set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # should we decide to place something that is not a dungeon end up there at some point set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # should we decide to place something that is not a dungeon end up there at some point
set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Hyrule Castle Secret Entrance Drop', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Hyrule Castle Secret Entrance Drop', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up
set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block
set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny cannot use hammer set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny can not use hammer set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny can not use hammer
set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player))
set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: ((state.can_lift_rocks(player) or state.has('Hammer', player)) or state.has('Flippers', player))) set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: ((can_lift_rocks(state, player) or state.has('Hammer', player)) or state.has('Flippers', player)))
set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: (state.can_lift_rocks(player) or state.has('Hammer', player))) set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: (can_lift_rocks(state, player) or state.has('Hammer', player)))
set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Pegasus Boots', player))
set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player))
set_rule(world.get_location('Bombos Tablet', player), lambda state: state.can_retrieve_tablet(player)) set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player))
set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Flippers', player)) # ToDo any fake flipper set up? set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Flippers', player)) # ToDo any fake flipper set up?
set_rule(world.get_entrance('Dark Lake Hylia Ledge Pier', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Dark Lake Hylia Ledge Pier', player), lambda state: state.has('Flippers', player))
set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player))
set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) # Fake Flippers set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) # Fake Flippers
set_rule(world.get_entrance('Dark Lake Hylia Shallows', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Dark Lake Hylia Shallows', player), lambda state: state.has('Flippers', player))
set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('East Dark World Bridge', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('East Dark World Bridge', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Flippers', player))
set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: can_lift_rocks(state, player))
set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Hammer Peg Area Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Hammer Peg Area Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player))
set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player))
set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player)) set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player))
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) # sword required to cast magic (!) set_rule(world.get_entrance('Misery Mire', player), lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!)
set_rule(world.get_entrance('Hookshot Cave', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player))
set_rule(world.get_entrance('East Death Mountain Mirror Spot (Top)', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('East Death Mountain Mirror Spot (Top)', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
@@ -645,7 +653,7 @@ def inverted_rules(world, player):
set_rule(world.get_entrance('Dark Death Mountain Ledge Mirror Spot (West)', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Dark Death Mountain Ledge Mirror Spot (West)', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Laser Bridge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Laser Bridge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_sword(player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) set_rule(world.get_entrance('Turtle Rock', player), lambda state: has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
# new inverted spots # new inverted spots
set_rule(world.get_entrance('Post Aga Teleporter', player), lambda state: state.has('Beat Agahnim 1', player)) set_rule(world.get_entrance('Post Aga Teleporter', player), lambda state: state.has('Beat Agahnim 1', player))
@@ -686,7 +694,7 @@ def inverted_rules(world, player):
def no_glitches_rules(world, player): def no_glitches_rules(world, player):
"""""" """"""
if world.mode[player] == 'inverted': if world.mode[player] == 'inverted':
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or state.can_lift_rocks(player))) set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or can_lift_rocks(state, player)))
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
set_rule(world.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to set_rule(world.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
@@ -697,7 +705,7 @@ def no_glitches_rules(world, player):
set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Flippers', player))
set_rule(world.get_entrance('East Dark World Pier', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('East Dark World Pier', player), lambda state: state.has('Flippers', player))
else: else:
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or state.can_lift_rocks(player)) set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or can_lift_rocks(state, player))
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to
set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player))
set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
@@ -819,19 +827,19 @@ def open_rules(world, player):
def swordless_rules(world, player): def swordless_rules(world, player):
set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or can_shoot_arrows(state, player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!)
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!) set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!)
else: else:
# only need ddm access for aga tower in inverted # only need ddm access for aga tower in inverted
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) set_rule(world.get_entrance('Turtle Rock', player), lambda state: has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!)
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!) set_rule(world.get_entrance('Misery Mire', player), lambda state: has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!)
def add_connection(parent_name, target_name, entrance_name, world, player): def add_connection(parent_name, target_name, entrance_name, world, player):
@@ -903,7 +911,7 @@ def set_trock_key_rules(world, player):
# Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we # Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we
# might open all the locked doors in any order so we need maximally restrictive rules. # might open all the locked doors in any order so we need maximally restrictive rules.
if can_reach_back: if can_reach_back:
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player))) set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4)) set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
# Only consider wasting the key on the Trinexx door for going from the front entrance to middle section. If other key doors are accessible, then these doors can be avoided # Only consider wasting the key on the Trinexx door for going from the front entrance to middle section. If other key doors are accessible, then these doors can be avoided
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3)) set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
@@ -923,7 +931,7 @@ def set_trock_key_rules(world, player):
def tr_big_key_chest_keys_needed(state): def tr_big_key_chest_keys_needed(state):
# This function handles the key requirements for the TR Big Chest in the situations it having the Big Key should logically require 2 keys, small key # This function handles the key requirements for the TR Big Chest in the situations it having the Big Key should logically require 2 keys, small key
# should logically require no keys, and anything else should logically require 4 keys. # should logically require no keys, and anything else should logically require 4 keys.
item = item_name(state, 'Turtle Rock - Big Key Chest', player) item = location_item_name(state, 'Turtle Rock - Big Key Chest', player)
if item in [('Small Key (Turtle Rock)', player)]: if item in [('Small Key (Turtle Rock)', player)]:
return 0 return 0
if item in [('Big Key (Turtle Rock)', player)]: if item in [('Big Key (Turtle Rock)', player)]:
@@ -1082,7 +1090,7 @@ def set_big_bomb_rules(world, player):
# returning via the eastern and southern teleporters needs the same items, so we use the southern teleporter for out routing. # returning via the eastern and southern teleporters needs the same items, so we use the southern teleporter for out routing.
# crossing preg bridge already requires hammer so we just add the gloves to the requirement # crossing preg bridge already requires hammer so we just add the gloves to the requirement
def southern_teleporter(state): def southern_teleporter(state):
return state.can_lift_rocks(player) and cross_peg_bridge(state) return can_lift_rocks(state, player) and cross_peg_bridge(state)
# the basic routes assume you can reach eastern light world with the bomb. # the basic routes assume you can reach eastern light world with the bomb.
# you can then use the southern teleporter, or (if you have beaten Aga1) the hyrule castle gate warp # you can then use the southern teleporter, or (if you have beaten Aga1) the hyrule castle gate warp
@@ -1109,13 +1117,13 @@ def set_big_bomb_rules(world, player):
#1. Mirror and basic routes #1. Mirror and basic routes
#2. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl #2. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl
# -> (Mitts and CPB) or (M and BR) # -> (Mitts and CPB) or (M and BR)
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and cross_peg_bridge(state)) or (state.has('Magic Mirror', player) and basic_routes(state))) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (state.has('Magic Mirror', player) and basic_routes(state)))
elif bombshop_entrance.name == 'Bumper Cave (Bottom)': elif bombshop_entrance.name == 'Bumper Cave (Bottom)':
#1. Mirror and Lift rock and basic_routes #1. Mirror and Lift rock and basic_routes
#2. Mirror and Flute and basic routes (can make difference if accessed via insanity or w/ mirror from connector, and then via hyrule castle gate, because no gloves are needed in that case) #2. Mirror and Flute and basic routes (can make difference if accessed via insanity or w/ mirror from connector, and then via hyrule castle gate, because no gloves are needed in that case)
#3. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl #3. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl
# -> (Mitts and CPB) or (((G or Flute) and M) and BR)) # -> (Mitts and CPB) or (((G or Flute) and M) and BR))
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and cross_peg_bridge(state)) or (((state.can_lift_rocks(player) or state.has('Flute', player)) and state.has('Magic Mirror', player)) and basic_routes(state))) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (((can_lift_rocks(state, player) or state.has('Flute', player)) and state.has('Magic Mirror', player)) and basic_routes(state)))
elif bombshop_entrance.name in Southern_DW_entrances: elif bombshop_entrance.name in Southern_DW_entrances:
#1. Mirror and enter via gate: Need mirror and Aga1 #1. Mirror and enter via gate: Need mirror and Aga1
#2. cross peg bridge: Need hammer and moon pearl #2. cross peg bridge: Need hammer and moon pearl
@@ -1143,7 +1151,7 @@ def set_big_bomb_rules(world, player):
elif bombshop_entrance.name == 'Fairy Ascension Cave (Bottom)': elif bombshop_entrance.name == 'Fairy Ascension Cave (Bottom)':
# Same as East_LW_DM_entrances except navigation without BR requires Mitts # Same as East_LW_DM_entrances except navigation without BR requires Mitts
# -> Flute and ((M and Hookshot and Mitts) or BR) # -> Flute and ((M and Hookshot and Mitts) or BR)
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player) and state.can_lift_heavy_rocks(player)) or basic_routes(state))) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player) and can_lift_heavy_rocks(state, player)) or basic_routes(state)))
elif bombshop_entrance.name in Castle_ledge_entrances: elif bombshop_entrance.name in Castle_ledge_entrances:
# 1. mirror on pyramid to castle ledge, grab bomb, return through mirror spot: Needs mirror # 1. mirror on pyramid to castle ledge, grab bomb, return through mirror spot: Needs mirror
# 2. flute then basic routes # 2. flute then basic routes
@@ -1159,7 +1167,7 @@ def set_big_bomb_rules(world, player):
# 1. Lift rock then basic_routes # 1. Lift rock then basic_routes
# 2. flute then basic_routes # 2. flute then basic_routes
# -> (Flute or G) and BR # -> (Flute or G) and BR
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or state.can_lift_rocks(player)) and basic_routes(state)) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_rocks(state, player)) and basic_routes(state))
elif bombshop_entrance.name == 'Graveyard Cave': elif bombshop_entrance.name == 'Graveyard Cave':
# 1. flute then basic routes # 1. flute then basic routes
# 2. (has west dark world access) use existing mirror spot (required Pearl), mirror again off ledge # 2. (has west dark world access) use existing mirror spot (required Pearl), mirror again off ledge
@@ -1175,13 +1183,13 @@ def set_big_bomb_rules(world, player):
# 2. walk down by hammering peg: needs hammer and pearl # 2. walk down by hammering peg: needs hammer and pearl
# 3. mirror and basic routes # 3. mirror and basic routes
# -> (P and (H or Gloves)) or (M and BR) # -> (P and (H or Gloves)) or (M and BR)
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Moon Pearl', player) and (state.has('Hammer', player) or state.can_lift_rocks(player))) or (state.has('Magic Mirror', player) and basic_routes(state))) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Moon Pearl', player) and (state.has('Hammer', player) or can_lift_rocks(state, player))) or (state.has('Magic Mirror', player) and basic_routes(state)))
elif bombshop_entrance.name == 'Kings Grave': elif bombshop_entrance.name == 'Kings Grave':
# same as the Normal_LW_entrances case except that the pre-existing mirror is only possible if you have mitts # same as the Normal_LW_entrances case except that the pre-existing mirror is only possible if you have mitts
# (because otherwise mirror was used to reach the grave, so would cancel a pre-existing mirror spot) # (because otherwise mirror was used to reach the grave, so would cancel a pre-existing mirror spot)
# to account for insanity, must consider a way to escape without a cave for basic_routes # to account for insanity, must consider a way to escape without a cave for basic_routes
# -> (M and Mitts) or ((Mitts or Flute or (M and P and West Dark World access)) and BR) # -> (M and Mitts) or ((Mitts or Flute or (M and P and West Dark World access)) and BR)
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and state.has('Magic Mirror', player)) or ((state.can_lift_heavy_rocks(player) or state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state))) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and state.has('Magic Mirror', player)) or ((can_lift_heavy_rocks(state, player) or state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state)))
elif bombshop_entrance.name == 'Waterfall of Wishing': elif bombshop_entrance.name == 'Waterfall of Wishing':
# same as the Normal_LW_entrances case except in insanity it's possible you could be here without Flippers which # same as the Normal_LW_entrances case except in insanity it's possible you could be here without Flippers which
# means you need an escape route of either Flippers or Flute # means you need an escape route of either Flippers or Flute
@@ -1328,7 +1336,7 @@ def set_inverted_big_bomb_rules(world, player):
elif bombshop_entrance.name in Northern_DW_entrances: elif bombshop_entrance.name in Northern_DW_entrances:
# You can just fly with the Flute, you can take a long walk with Mitts and Hammer, # You can just fly with the Flute, you can take a long walk with Mitts and Hammer,
# or you can leave a Mirror portal nearby and then walk to the castle to Mirror again. # or you can leave a Mirror portal nearby and then walk to the castle to Mirror again.
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player)))
elif bombshop_entrance.name in Southern_DW_entrances: elif bombshop_entrance.name in Southern_DW_entrances:
# This is the same as north DW without the Mitts rock present. # This is the same as north DW without the Mitts rock present.
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Hammer', player) or state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Hammer', player) or state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player)))
@@ -1340,22 +1348,22 @@ def set_inverted_big_bomb_rules(world, player):
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player)))
elif bombshop_entrance.name in LW_bush_entrances: elif bombshop_entrance.name in LW_bush_entrances:
# These entrances are behind bushes in LW so you need either Pearl or the tools to solve NDW bomb shop locations. # These entrances are behind bushes in LW so you need either Pearl or the tools to solve NDW bomb shop locations.
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)))) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player))))
elif bombshop_entrance.name == 'Village of Outcasts Shop': elif bombshop_entrance.name == 'Village of Outcasts Shop':
# This is mostly the same as NDW but the Mirror path requires the Pearl, or using the Hammer # This is mostly the same as NDW but the Mirror path requires the Pearl, or using the Hammer
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player)))) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player))))
elif bombshop_entrance.name == 'Bumper Cave (Bottom)': elif bombshop_entrance.name == 'Bumper Cave (Bottom)':
# This is mostly the same as NDW but the Mirror path requires being able to lift a rock. # This is mostly the same as NDW but the Mirror path requires being able to lift a rock.
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_lift_rocks(player) and state.can_reach('Light World', 'Region', player))) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and can_lift_rocks(state, player) and state.can_reach('Light World', 'Region', player)))
elif bombshop_entrance.name == 'Old Man Cave (West)': elif bombshop_entrance.name == 'Old Man Cave (West)':
# The three paths back are Mirror and DW walk, Mirror and Flute, or LW walk and then Mirror. # The three paths back are Mirror and DW walk, Mirror and Flute, or LW walk and then Mirror.
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and ((state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.can_lift_rocks(player) and state.has('Moon Pearl', player)) or state.has('Activated Flute', player))) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and ((can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (can_lift_rocks(state, player) and state.has('Moon Pearl', player)) or state.has('Activated Flute', player)))
elif bombshop_entrance.name == 'Dark World Potion Shop': elif bombshop_entrance.name == 'Dark World Potion Shop':
# You either need to Flute to 5 or cross the rock/hammer choice pass to the south. # You either need to Flute to 5 or cross the rock/hammer choice pass to the south.
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or state.has('Hammer', player) or state.can_lift_rocks(player)) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or state.has('Hammer', player) or can_lift_rocks(state, player))
elif bombshop_entrance.name == 'Kings Grave': elif bombshop_entrance.name == 'Kings Grave':
# Either lift the rock and walk to the castle to Mirror or Mirror immediately and Flute. # Either lift the rock and walk to the castle to Mirror or Mirror immediately and Flute.
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or state.can_lift_heavy_rocks(player)) and state.has('Magic Mirror', player)) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_heavy_rocks(state, player)) and state.has('Magic Mirror', player))
elif bombshop_entrance.name == 'Waterfall of Wishing': elif bombshop_entrance.name == 'Waterfall of Wishing':
# You absolutely must be able to swim to return it from here. # You absolutely must be able to swim to return it from here.
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))
@@ -1420,10 +1428,10 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
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
return lambda state: state.has('Magic Mirror', player) and state.has_sword(player) or state.has('Moon Pearl', player) return lambda state: state.has('Magic Mirror', player) and has_sword(state, player) or state.has('Moon Pearl', player)
if region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): if region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
return lambda state: state.has('Magic Mirror', player) or state.has('Moon Pearl', player) return lambda state: state.has('Magic Mirror', player) or state.has('Moon Pearl', player)
if region.type == RegionType.Dungeon: if region.type == LTTPRegionType.Dungeon:
return lambda state: True return lambda state: True
if (((location is None or location.name not in OverworldGlitchRules.get_superbunny_accessible_locations()) if (((location is None or location.name not in OverworldGlitchRules.get_superbunny_accessible_locations())
or (connecting_entrance is not None and connecting_entrance.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons())) or (connecting_entrance is not None and connecting_entrance.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons()))
@@ -1458,7 +1466,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
# For glitch rulesets, establish superbunny and revival rules. # For glitch rulesets, establish superbunny and revival rules.
if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', '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 has_sword(state, player))
elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions() elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions()
or location is not None and location.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_locations()): or location is not None and location.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_locations()):
possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and state.has('Pegasus Boots', player)) possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and state.has('Pegasus Boots', player))
@@ -1467,7 +1475,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
possible_options.append(lambda state: path_to_access_rule(new_path, entrance)) possible_options.append(lambda state: path_to_access_rule(new_path, entrance))
else: else:
possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player)) possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player))
if new_region.type != RegionType.Cave: if new_region.type != LTTPRegionType.Cave:
continue continue
else: else:
continue continue
@@ -1495,8 +1503,8 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
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', 'hybridglitches', 'nologic'] : if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] :
if entrance.connected_region.type == RegionType.Dungeon: if entrance.connected_region.type == LTTPRegionType.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 != LTTPRegionType.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))
continue continue
if entrance.connected_region.name == 'Turtle Rock (Entrance)': if entrance.connected_region.name == 'Turtle Rock (Entrance)':
@@ -1506,4 +1514,4 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
continue continue
if location.name in bunny_accessible_locations: if location.name in bunny_accessible_locations:
continue continue
add_rule(location, get_rule_to_add(entrance.connected_region, location)) add_rule(location, get_rule_to_add(entrance.connected_region, location))

View File

@@ -0,0 +1,137 @@
from .SubClasses import LTTPRegion
from BaseClasses import CollectionState
def is_not_bunny(state: CollectionState, region: LTTPRegion, player: int) -> bool:
if state.has('Moon Pearl', player):
return True
return region.is_light_world if state.multiworld.mode[player] != 'inverted' else region.is_dark_world
def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bool:
return is_not_bunny(state, region, player) and state.has('Pegasus Boots', player)
def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(state) for
shop in state.multiworld.shops)
def can_buy(state: CollectionState, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(state) for
shop in state.multiworld.shops)
def can_shoot_arrows(state: CollectionState, player: int) -> bool:
if state.multiworld.retro_bow[player]:
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_buy(state, 'Single Arrow', player)
return state.has('Bow', player) or state.has('Silver Bow', player)
def has_triforce_pieces(state: CollectionState, player: int) -> bool:
count = state.multiworld.treasure_hunt_count[player]
return state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= count
def has_crystals(state: CollectionState, count: int, player: int) -> bool:
found = state.count_group("Crystals", player)
return found >= count
def can_lift_rocks(state: CollectionState, player: int):
return state.has('Power Glove', player) or state.has('Titans Mitts', player)
def can_lift_heavy_rocks(state: CollectionState, player: int) -> bool:
return state.has('Titans Mitts', player)
def bottle_count(state: CollectionState, player: int) -> int:
return min(state.multiworld.difficulty_requirements[player].progressive_bottle_limit,
state.count_group("Bottles", player))
def has_hearts(state: CollectionState, player: int, count: int) -> int:
# Warning: This only considers items that are marked as advancement items
return heart_count(state, player) >= count
def heart_count(state: CollectionState, player: int) -> int:
# Warning: This only considers items that are marked as advancement items
diff = state.multiworld.difficulty_requirements[player]
return min(state.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \
+ state.item_count('Sanctuary Heart Container', player) \
+ min(state.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
+ 3 # starting hearts
def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16,
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
basemagic = 8
if state.has('Magic Upgrade (1/4)', player):
basemagic = 32
elif state.has('Magic Upgrade (1/2)', player):
basemagic = 16
if can_buy_unlimited(state, 'Green Potion', player) or can_buy_unlimited(state, 'Blue Potion', player):
if state.multiworld.item_functionality[player] == 'hard' and not fullrefill:
basemagic = basemagic + int(basemagic * 0.5 * bottle_count(state, player))
elif state.multiworld.item_functionality[player] == 'expert' and not fullrefill:
basemagic = basemagic + int(basemagic * 0.25 * bottle_count(state, player))
else:
basemagic = basemagic + basemagic * bottle_count(state, player)
return basemagic >= smallmagic
def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) -> bool:
return (has_melee_weapon(state, player)
or state.has('Cane of Somaria', player)
or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player)))
or can_shoot_arrows(state, player)
or state.has('Fire Rod', player)
or (state.has('Bombs (10)', player) and enemies < 6))
def can_get_good_bee(state: CollectionState, player: int) -> bool:
cave = state.multiworld.get_region('Good Bee Cave', player)
return (
state.has_group("Bottles", player) and
state.has('Bug Catching Net', player) and
(state.has('Pegasus Boots', player) or (has_sword(state, player) and state.has('Quake', player))) and
cave.can_reach(state) and
is_not_bunny(state, cave, player)
)
def can_retrieve_tablet(state: CollectionState, player: int) -> bool:
return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or
(state.multiworld.swordless[player] and
state.has("Hammer", player)))
def has_sword(state: CollectionState, player: int) -> bool:
return state.has('Fighter Sword', player) \
or state.has('Master Sword', player) \
or state.has('Tempered Sword', player) \
or state.has('Golden Sword', player)
def has_beam_sword(state: CollectionState, player: int) -> bool:
return state.has('Master Sword', player) or state.has('Tempered Sword', player) or state.has('Golden Sword',
player)
def has_melee_weapon(state: CollectionState, player: int) -> bool:
return has_sword(state, player) or state.has('Hammer', player)
def has_fire_source(state: CollectionState, player: int) -> bool:
return state.has('Fire Rod', player) or state.has('Lamp', player)
def can_melt_things(state: CollectionState, player: int) -> bool:
return state.has('Fire Rod', player) or \
(state.has('Bombos', player) and
(state.multiworld.swordless[player] or
has_sword(state, player)))
def has_misery_mire_medallion(state: CollectionState, player: int) -> bool:
return state.has(state.multiworld.required_medallions[player][0], player)
def has_turtle_rock_medallion(state: CollectionState, player: int) -> bool:
return state.has(state.multiworld.required_medallions[player][1], player)
def can_boots_clip_lw(state: CollectionState, player: int) -> bool:
if state.multiworld.mode[player] == 'inverted':
return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)
return state.has('Pegasus Boots', player)
def can_boots_clip_dw(state: CollectionState, player: int) -> bool:
if state.multiworld.mode[player] != 'inverted':
return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)
return state.has('Pegasus Boots', player)
def can_get_glitched_speed_dw(state: CollectionState, player: int) -> bool:
rules = [state.has('Pegasus Boots', player), any([state.has('Hookshot', player), has_sword(state, player)])]
if state.multiworld.mode[player] != 'inverted':
rules.append(state.has('Moon Pearl', player))
return all(rules)

View File

@@ -1,8 +1,8 @@
"""Module extending BaseClasses.py for aLttP""" """Module extending BaseClasses.py for aLttP"""
from typing import Optional from typing import Optional
from enum import IntEnum
from BaseClasses import Location, Item, ItemClassification from BaseClasses import Location, Item, ItemClassification, Region, MultiWorld
class ALttPLocation(Location): class ALttPLocation(Location):
game: str = "A Link to the Past" game: str = "A Link to the Past"
@@ -62,4 +62,38 @@ class ALttPItem(Item):
@property @property
def locked_dungeon_item(self): def locked_dungeon_item(self):
return self.location.locked and self.dungeon_item return self.location.locked and self.dungeon_item
class LTTPRegionType(IntEnum):
LightWorld = 1
DarkWorld = 2
Cave = 3 # also houses
Dungeon = 4
@property
def is_indoors(self) -> bool:
"""Shorthand for checking if Cave or Dungeon"""
return self in (LTTPRegionType.Cave, LTTPRegionType.Dungeon)
class LTTPRegion(Region):
type: LTTPRegionType
# will be set after making connections.
is_light_world: bool = False
is_dark_world: bool = False
shop: Optional = None
def __init__(self, name: str, type_: LTTPRegionType, hint: str, player: int, multiworld: MultiWorld):
super().__init__(name, player, multiworld, hint)
self.type = type_
@property
def get_entrance(self):
for entrance in self.entrances:
if entrance.parent_region.type in (LTTPRegionType.DarkWorld, LTTPRegionType.LightWorld):
return entrance
for entrance in self.entrances:
return entrance.parent_region.get_entrance

View File

@@ -1,6 +1,8 @@
from BaseClasses import Entrance from BaseClasses import Entrance
from .SubClasses import LTTPRegion
from worlds.generic.Rules import set_rule, add_rule from worlds.generic.Rules import set_rule, add_rule
from .StateHelpers import can_bomb_clip, has_sword, has_beam_sword, has_fire_source, can_melt_things, has_misery_mire_medallion
# We actually need the logic to properly "mark" these regions as Light or Dark world. # 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. # Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules.
@@ -46,9 +48,9 @@ def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, du
if dungeon_entrance.name == 'Skull Woods Final Section': 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 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': 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 add_rule(clip, lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # open the dungeon
elif dungeon_entrance.name == 'Agahnims Tower': 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 add_rule(clip, lambda state: state.has('Cape', player) or has_beam_sword(state, 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. # 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)) 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 elif not fix_fake_worlds: # full, dungeonsfull; fixed dungeon exits, but no fake worlds fix
@@ -66,21 +68,21 @@ def underworld_glitches_rules(world, player):
# Ice Palace Entrance Clip # 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. # 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_entrance('Ice Palace Entrance Room', player), lambda state: can_bomb_clip(state, 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)) add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player))
# Kiki Skip # Kiki Skip
kikiskip = world.get_entrance('Kiki Skip', player) kikiskip = world.get_entrance('Kiki Skip', player)
set_rule(kikiskip, lambda state: state.can_bomb_clip(kikiskip.parent_region, player)) set_rule(kikiskip, lambda state: can_bomb_clip(state, kikiskip.parent_region, player))
dungeon_reentry_rules(world, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit') dungeon_reentry_rules(world, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit')
# Mire -> Hera -> Swamp # Mire -> Hera -> Swamp
# Using mire keys on other dungeon doors # Using mire keys on other dungeon doors
mire = world.get_region('Misery Mire (West)', player) 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) mire_clip = lambda state: state.can_reach('Misery Mire (West)', 'Region', player) and can_bomb_clip(state, mire, player) and has_fire_source(state, 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) hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and can_bomb_clip(state, 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('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 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') add_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or')

View File

@@ -3,9 +3,10 @@ import os
import random import random
import threading import threading
import typing import typing
from collections import OrderedDict
import Utils import Utils
from BaseClasses import Item, CollectionState, Tutorial from BaseClasses import Item, CollectionState, Tutorial, MultiWorld
from .Dungeons import create_dungeons from .Dungeons import create_dungeons
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect, \ from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect, \
indirect_connections, indirect_connections_inverted, indirect_connections_not_inverted indirect_connections, indirect_connections_inverted, indirect_connections_not_inverted
@@ -19,9 +20,10 @@ from .Client import ALTTPSNIClient
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
get_hash_string, get_base_rom_path, LttPDeltaPatch get_hash_string, get_base_rom_path, LttPDeltaPatch
from .Rules import set_rules from .Rules import set_rules
from .Shops import create_shops, ShopSlotFill from .Shops import create_shops, Shop, ShopSlotFill, ShopType, price_rate_display, price_type_display_name
from .SubClasses import ALttPItem from .SubClasses import ALttPItem, LTTPRegionType
from worlds.AutoWorld import World, WebWorld, LogicMixin from worlds.AutoWorld import World, WebWorld, LogicMixin
from .StateHelpers import can_buy_unlimited
lttp_logger = logging.getLogger("A Link to the Past") lttp_logger = logging.getLogger("A Link to the Past")
@@ -115,6 +117,75 @@ class ALTTPWorld(World):
option_definitions = alttp_options option_definitions = alttp_options
topology_present = True topology_present = True
item_name_groups = item_name_groups item_name_groups = item_name_groups
location_name_groups = {
"Blind's Hideout": {"Blind's Hideout - Top", "Blind's Hideout - Left", "Blind's Hideout - Right",
"Blind's Hideout - Far Left", "Blind's Hideout - Far Right"},
"Kakariko Well": {"Kakariko Well - Top", "Kakariko Well - Left", "Kakariko Well - Middle",
"Kakariko Well - Right", "Kakariko Well - Bottom"},
"Mini Moldorm Cave": {"Mini Moldorm Cave - Far Left", "Mini Moldorm Cave - Left", "Mini Moldorm Cave - Right",
"Mini Moldorm Cave - Far Right", "Mini Moldorm Cave - Generous Guy"},
"Paradox Cave": {"Paradox Cave Lower - Far Left", "Paradox Cave Lower - Left", "Paradox Cave Lower - Right",
"Paradox Cave Lower - Far Right", "Paradox Cave Lower - Middle", "Paradox Cave Upper - Left",
"Paradox Cave Upper - Right"},
"Hype Cave": {"Hype Cave - Top", "Hype Cave - Middle Right", "Hype Cave - Middle Left",
"Hype Cave - Bottom", "Hype Cave - Generous Guy"},
"Hookshot Cave": {"Hookshot Cave - Top Right", "Hookshot Cave - Top Left", "Hookshot Cave - Bottom Right",
"Hookshot Cave - Bottom Left"},
"Hyrule Castle": {"Hyrule Castle - Boomerang Chest", "Hyrule Castle - Map Chest",
"Hyrule Castle - Zelda's Chest", "Sewers - Dark Cross", "Sewers - Secret Room - Left",
"Sewers - Secret Room - Middle", "Sewers - Secret Room - Right"},
"Eastern Palace": {"Eastern Palace - Compass Chest", "Eastern Palace - Big Chest",
"Eastern Palace - Cannonball Chest", "Eastern Palace - Big Key Chest",
"Eastern Palace - Map Chest", "Eastern Palace - Boss"},
"Desert Palace": {"Desert Palace - Big Chest", "Desert Palace - Torch", "Desert Palace - Map Chest",
"Desert Palace - Compass Chest", "Desert Palace Big Key Chest", "Desert Palace - Boss"},
"Tower of Hera": {"Tower of Hera - Basement Cage", "Tower of Hera - Map Chest", "Tower of Hera - Big Key Chest",
"Tower of Hera - Compass Chest", "Tower of Hera - Big Chest", "Tower of Hera - Boss"},
"Palace of Darkness": {"Palace of Darkness - Shooter Room", "Palace of Darkness - The Arena - Bridge",
"Palace of Darkness - Stalfos Basement", "Palace of Darkness - Big Key Chest",
"Palace of Darkness - The Arena - Ledge", "Palace of Darkness - Map Chest",
"Palace of Darkness - Compass Chest", "Palace of Darkness - Dark Basement - Left",
"Palace of Darkness - Dark Basement - Right", "Palace of Darkness - Dark Maze - Top",
"Palace of Darkness - Dark Maze - Bottom", "Palace of Darkness - Big Chest",
"Palace of Darkness - Harmless Hellway", "Palace of Darkness - Boss"},
"Swamp Palace": {"Swamp Palace - Entrance", "Swamp Palace - Swamp Palace - Map Chest",
"Swamp Palace - Big Chest", "Swamp Palace - Compass Chest", "Swamp Palace - Big Key Chest",
"Swamp Palace - West Chest", "Swamp Palace - Flooded Room - Left",
"Swamp Palace - Flooded Room - Right", "Swamp Palace - Waterfall Room", "Swamp Palace - Boss"},
"Thieves' Town": {"Thieves' Town - Big Key Chest", "Thieves' Town - Map Chest", "Thieves' Town - Compass Chest",
"Thieves' Town - Ambush Chest", "Thieves' Town - Attic", "Thieves' Town - Big Chest",
"Thieves' Town - Blind's Cell", "Thieves' Town - Boss"},
"Skull Woods": {"Skull Woods - Map Chest", "Skull Woods - Pinball Room", "Skull Woods - Compass Chest",
"Skull Woods - Pot Prison", "Skull Woods - Big Chest", "Skull Woods - Big Key Chest",
"Skull Woods - Bridge Room", "Skull Woods - Boss"},
"Ice Palace": {"Ice Palace - Compass Chest", "Ice Palace - Freezor Chest", "Ice Palace - Big Chest",
"Ice Palace - Freezor Chest", "Ice Palace - Big Chest", "Ice Palace - Iced T Room",
"Ice Palace - Spike Room", "Ice Palace - Big Key Chest", "Ice Palace - Map Chest",
"Ice Palace - Boss"},
"Misery Mire": {"Misery Mire - Big Chest", "Misery Mire - Map Chest", "Misery Mire - Main Lobby",
"Misery Mire - Bridge Chest", "Misery Mire - Spike Chest", "Misery Mire - Compass Chest",
"Misery Mire - Big Key Chest", "Misery Mire - Boss"},
"Turtle Rock": {"Turtle Rock - Compass Chest", "Turtle Rock - Roller Room - Left",
"Turtle Rock - Roller Room - Right", "Turtle Room - Chain Chomps", "Turtle Rock - Big Key Chest",
"Turtle Rock - Big Chest", "Turtle Rock - Crystaroller Room",
"Turtle Rock - Eye Bridge - Bottom Left", "Turtle Rock - Eye Bridge - Bottom Right",
"Turtle Rock - Eye Bridge - Top Left", "Turtle Rock - Eye Bridge - Top Right", "Turtle Rock - Boss"},
"Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganon's Tower - Hope Room - Left",
"Ganons Tower - Hope Room - Right", "Ganons Tower - Tile Room",
"Ganons Tower - Compass Room - Top Left", "Ganons Tower - Compass Room - Top Right",
"Ganons Tower - Compass Room - Bottom Left", "Ganons Tower - Compass Room - Bottom Left",
"Ganons Tower - DMs Room - Top Left", "Ganons Tower - DMs Room - Top Right",
"Ganons Tower - DMs Room - Bottom Left", "Ganons Tower - DMs Room - Bottom Right",
"Ganons Tower - Map Chest", "Ganons Tower - Firesnake Room",
"Ganons Tower - Randomizer Room - Top Left", "Ganons Tower - Randomizer Room - Top Right",
"Ganons Tower - Randomizer Room - Bottom Left", "Ganons Tower - Randomizer Room - Bottom Right",
"Ganons Tower - Bob's Chest", "Ganons Tower - Big Chest", "Ganons Tower - Big Key Room - Left",
"Ganons Tower - Big Key Room - Right", "Ganons Tower - Big Key Chest",
"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right",
"Ganons Tower - Pre-Moldorm Room", "Ganons Tower - Validation Chest"},
"Ganons Tower Climb": {"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right",
"Ganons Tower - Pre-Moldorm Room", "Ganons Tower - Validation Chest"},
}
hint_blacklist = {"Triforce"} hint_blacklist = {"Triforce"}
item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int} item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int}
@@ -151,16 +222,18 @@ class ALTTPWorld(World):
super(ALTTPWorld, self).__init__(*args, **kwargs) super(ALTTPWorld, self).__init__(*args, **kwargs)
@classmethod @classmethod
def stage_assert_generate(cls, world): def stage_assert_generate(cls, multiworld: MultiWorld):
rom_file = get_base_rom_path() rom_file = get_base_rom_path()
if not os.path.exists(rom_file): if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file) raise FileNotFoundError(rom_file)
if world.is_race: if multiworld.is_race:
import xxtea import xxtea
for player in multiworld.get_game_players(cls.game):
if multiworld.worlds[player].use_enemizer:
check_enemizer(multiworld.worlds[player].enemizer_path)
break
def generate_early(self): def generate_early(self):
if self.use_enemizer():
check_enemizer(self.enemizer_path)
player = self.player player = self.player
world = self.multiworld world = self.multiworld
@@ -369,19 +442,20 @@ class ALTTPWorld(World):
def stage_post_fill(cls, world): def stage_post_fill(cls, world):
ShopSlotFill(world) ShopSlotFill(world)
def use_enemizer(self): @property
def use_enemizer(self) -> bool:
world = self.multiworld world = self.multiworld
player = self.player player = self.player
return (world.boss_shuffle[player] or world.enemy_shuffle[player] return bool(world.boss_shuffle[player] or world.enemy_shuffle[player]
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.pot_shuffle[player] or world.bush_shuffle[player] or world.pot_shuffle[player] or world.bush_shuffle[player]
or world.killable_thieves[player]) or world.killable_thieves[player])
def generate_output(self, output_directory: str): def generate_output(self, output_directory: str):
world = self.multiworld world = self.multiworld
player = self.player player = self.player
try: try:
use_enemizer = self.use_enemizer() use_enemizer = self.use_enemizer
rom = LocalRom(get_base_rom_path()) rom = LocalRom(get_base_rom_path())
@@ -517,6 +591,122 @@ class ALTTPWorld(World):
else: else:
logging.warning(f"Could not trash fill Ganon's Tower for player {player}.") logging.warning(f"Could not trash fill Ganon's Tower for player {player}.")
def write_spoiler_header(self, spoiler_handle: typing.TextIO) -> None:
def bool_to_text(variable: typing.Union[bool, str]) -> str:
if type(variable) == str:
return variable
return "Yes" if variable else "No"
spoiler_handle.write('Logic: %s\n' % self.multiworld.logic[self.player])
spoiler_handle.write('Dark Room Logic: %s\n' % self.multiworld.dark_room_logic[self.player])
spoiler_handle.write('Mode: %s\n' % self.multiworld.mode[self.player])
spoiler_handle.write('Goal: %s\n' % self.multiworld.goal[self.player])
if "triforce" in self.multiworld.goal[self.player]: # triforce hunt
spoiler_handle.write("Pieces available for Triforce: %s\n" %
self.multiworld.triforce_pieces_available[self.player])
spoiler_handle.write("Pieces required for Triforce: %s\n" %
self.multiworld.triforce_pieces_required[self.player])
spoiler_handle.write('Difficulty: %s\n' % self.multiworld.difficulty[self.player])
spoiler_handle.write('Item Functionality: %s\n' % self.multiworld.item_functionality[self.player])
spoiler_handle.write('Entrance Shuffle: %s\n' % self.multiworld.shuffle[self.player])
if self.multiworld.shuffle[self.player] != "vanilla":
spoiler_handle.write('Entrance Shuffle Seed %s\n' % self.er_seed)
spoiler_handle.write('Shop inventory shuffle: %s\n' %
bool_to_text("i" in self.multiworld.shop_shuffle[self.player]))
spoiler_handle.write('Shop price shuffle: %s\n' %
bool_to_text("p" in self.multiworld.shop_shuffle[self.player]))
spoiler_handle.write('Shop upgrade shuffle: %s\n' %
bool_to_text("u" in self.multiworld.shop_shuffle[self.player]))
spoiler_handle.write('New Shop inventory: %s\n' %
bool_to_text("g" in self.multiworld.shop_shuffle[self.player] or
"f" in self.multiworld.shop_shuffle[self.player]))
spoiler_handle.write('Custom Potion Shop: %s\n' %
bool_to_text("w" in self.multiworld.shop_shuffle[self.player]))
spoiler_handle.write('Enemy health: %s\n' % self.multiworld.enemy_health[self.player])
spoiler_handle.write('Enemy damage: %s\n' % self.multiworld.enemy_damage[self.player])
spoiler_handle.write('Prize shuffle %s\n' % self.multiworld.shuffle_prizes[self.player])
def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
spoiler_handle.write("\n\nMedallions:\n")
spoiler_handle.write(f"\nMisery Mire ({self.multiworld.get_player_name(self.player)}):"
f" {self.multiworld.required_medallions[self.player][0]}")
spoiler_handle.write(
f"\nTurtle Rock ({self.multiworld.get_player_name(self.player)}):"
f" {self.multiworld.required_medallions[self.player][1]}")
if self.multiworld.boss_shuffle[self.player] != "none":
def create_boss_map() -> typing.Dict:
boss_map = {
"Eastern Palace": self.multiworld.get_dungeon("Eastern Palace", self.player).boss.name,
"Desert Palace": self.multiworld.get_dungeon("Desert Palace", self.player).boss.name,
"Tower Of Hera": self.multiworld.get_dungeon("Tower of Hera", self.player).boss.name,
"Hyrule Castle": "Agahnim",
"Palace Of Darkness": self.multiworld.get_dungeon("Palace of Darkness",
self.player).boss.name,
"Swamp Palace": self.multiworld.get_dungeon("Swamp Palace", self.player).boss.name,
"Skull Woods": self.multiworld.get_dungeon("Skull Woods", self.player).boss.name,
"Thieves Town": self.multiworld.get_dungeon("Thieves Town", self.player).boss.name,
"Ice Palace": self.multiworld.get_dungeon("Ice Palace", self.player).boss.name,
"Misery Mire": self.multiworld.get_dungeon("Misery Mire", self.player).boss.name,
"Turtle Rock": self.multiworld.get_dungeon("Turtle Rock", self.player).boss.name,
"Ganons Tower": "Agahnim 2",
"Ganon": "Ganon"
}
if self.multiworld.mode[self.player] != 'inverted':
boss_map.update({
"Ganons Tower Basement":
self.multiworld.get_dungeon("Ganons Tower", self.player).bosses["bottom"].name,
"Ganons Tower Middle": self.multiworld.get_dungeon("Ganons Tower", self.player).bosses[
"middle"].name,
"Ganons Tower Top": self.multiworld.get_dungeon("Ganons Tower", self.player).bosses[
"top"].name
})
else:
boss_map.update({
"Ganons Tower Basement": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["bottom"].name,
"Ganons Tower Middle": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["middle"].name,
"Ganons Tower Top": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["top"].name
})
return boss_map
bossmap = create_boss_map()
spoiler_handle.write(
f'\n\nBosses{(f" ({self.multiworld.get_player_name(self.player)})" if self.multiworld.players > 1 else "")}:\n')
spoiler_handle.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
def build_shop_info(shop: Shop) -> typing.Dict[str, str]:
shop_data = {
"location": str(shop.region),
"type": "Take Any" if shop.type == ShopType.TakeAny else "Shop"
}
for index, item in enumerate(shop.inventory):
if item is None:
continue
price = item["price"] // price_rate_display.get(item["price_type"], 1)
shop_data["item_{}".format(index)] = f"{item['item']} - {price} {price_type_display_name[item['price_type']]}"
if item["player"]:
shop_data["item_{}".format(index)] =\
shop_data["item_{}".format(index)].replace("", "(Player {}) — ".format(item["player"]))
if item["max"] == 0:
continue
shop_data["item_{}".format(index)] += " x {}".format(item["max"])
if item["replacement"] is None:
continue
shop_data["item_{}".format(index)] +=\
f", {item['replacement']} - {item['replacement_price']}" \
f" {price_type_display_name[item['replacement_price_type']]}"
return shop_data
if shop_info := [build_shop_info(shop) for shop in self.multiworld.shops if shop.custom]:
spoiler_handle.write('\n\nShops:\n\n')
for shop_data in shop_info:
spoiler_handle.write("{} [{}]\n {}\n".format(shop_data['location'], shop_data['type'], "\n ".join(
item for item in [shop_data.get('item_0', None), shop_data.get('item_1', None), shop_data.get('item_2', None)] if
item)))
def get_filler_item_name(self) -> str: def get_filler_item_name(self) -> str:
if self.multiworld.goal[self.player] == "icerodhunt": if self.multiworld.goal[self.player] == "icerodhunt":
item = "Nothing" item = "Nothing"
@@ -549,5 +739,5 @@ class ALttPLogic(LogicMixin):
if self.multiworld.logic[player] == 'nologic': if self.multiworld.logic[player] == 'nologic':
return True return True
if self.multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal: if self.multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
return self.can_buy_unlimited('Small Key (Universal)', player) return can_buy_unlimited(self, 'Small Key (Universal)', player)
return self.prog_items[item, player] >= count return self.prog_items[item, player] >= count

View File

@@ -1,5 +1,5 @@
import unittest import unittest
import Generate from BaseClasses import PlandoOptions
from Options import PlandoBosses from Options import PlandoBosses
@@ -123,14 +123,14 @@ class TestPlandoBosses(unittest.TestCase):
regular = MultiBosses.from_any(regular_string) regular = MultiBosses.from_any(regular_string)
# plando should work with boss plando # plando should work with boss plando
plandoed.verify(None, "Player", Generate.PlandoOptions.bosses) plandoed.verify(None, "Player", PlandoOptions.bosses)
self.assertTrue(plandoed.value.startswith(plandoed_string)) self.assertTrue(plandoed.value.startswith(plandoed_string))
# plando should fall back to default without boss plando # plando should fall back to default without boss plando
plandoed.verify(None, "Player", Generate.PlandoOptions.items) plandoed.verify(None, "Player", PlandoOptions.items)
self.assertEqual(plandoed, MultiBosses.option_vanilla) self.assertEqual(plandoed, MultiBosses.option_vanilla)
# mixed should fall back to mode # mixed should fall back to mode
mixed.verify(None, "Player", Generate.PlandoOptions.items) # should produce a warning and still work mixed.verify(None, "Player", PlandoOptions.items) # should produce a warning and still work
self.assertEqual(mixed, MultiBosses.option_shuffle) self.assertEqual(mixed, MultiBosses.option_shuffle)
# mode stuff should just work # mode stuff should just work
regular.verify(None, "Player", Generate.PlandoOptions.items) regular.verify(None, "Player", PlandoOptions.items)
self.assertEqual(regular, MultiBosses.option_shuffle) self.assertEqual(regular, MultiBosses.option_shuffle)

View File

@@ -14,26 +14,24 @@ class ArchipIDLELogic(LogicMixin):
def set_rules(world: MultiWorld, player: int): def set_rules(world: MultiWorld, player: int):
for i in range(1, 16):
set_rule(
world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) > 0 else 0} seconds", player),
lambda state: state._archipidle_location_is_accessible(player, 0)
)
for i in range(16, 31): for i in range(16, 31):
set_rule( set_rule(
world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) > 0 else 0} seconds", player), world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player),
lambda state: state._archipidle_location_is_accessible(player, 4) lambda state: state._archipidle_location_is_accessible(player, 4)
) )
for i in range(31, 51): for i in range(31, 51):
set_rule( set_rule(
world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) > 0 else 0} seconds", player), world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player),
lambda state: state._archipidle_location_is_accessible(player, 10) lambda state: state._archipidle_location_is_accessible(player, 10)
) )
for i in range(51, 101): for i in range(51, 101):
set_rule( set_rule(
world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) > 0 else 0} seconds", player), world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player),
lambda state: state._archipidle_location_is_accessible(player, 20) lambda state: state._archipidle_location_is_accessible(player, 20)
) )
world.completion_condition[player] =\
lambda state:\
state.can_reach(world.get_location("IDLE for at least 50 minutes 0 seconds", player), "Location", player)

View File

@@ -1,4 +1,4 @@
from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification, RegionType from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification
from .Items import item_table from .Items import item_table
from .Rules import set_rules from .Rules import set_rules
from ..AutoWorld import World, WebWorld from ..AutoWorld import World, WebWorld
@@ -38,7 +38,7 @@ class ArchipIDLEWorld(World):
location_name_to_id = {} location_name_to_id = {}
start_id = 9000 start_id = 9000
for i in range(1, 101): for i in range(1, 101):
location_name_to_id[f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) > 0 else 0} seconds"] = start_id location_name_to_id[f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds"] = start_id
start_id += 1 start_id += 1
def generate_basic(self): def generate_basic(self):
@@ -78,8 +78,7 @@ class ArchipIDLEWorld(World):
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
region = Region(name, RegionType.Generic, name, player) region = Region(name, player, world)
region.multiworld = world
if locations: if locations:
for location_name in locations.keys(): for location_name in locations.keys():
location = ArchipIDLELocation(player, location_name, locations[location_name], region) location = ArchipIDLELocation(player, location_name, locations[location_name], region)
@@ -98,6 +97,3 @@ class ArchipIDLEItem(Item):
class ArchipIDLELocation(Location): class ArchipIDLELocation(Location):
game: str = "ArchipIDLE" game: str = "ArchipIDLE"
def __init__(self, player: int, name: str, address=None, parent=None):
super(ArchipIDLELocation, self).__init__(player, name, address, parent)

View File

@@ -29,5 +29,5 @@ class Bk_SudokuWorld(World):
location_name_to_id: Dict[str, int] = {} location_name_to_id: Dict[str, int] = {}
@classmethod @classmethod
def stage_assert_generate(cls, world): def stage_assert_generate(cls, multiworld):
raise Exception("BK Sudoku cannot be used for generating worlds, the client can instead connect to any other world") raise Exception("BK Sudoku cannot be used for generating worlds, the client can instead connect to any other world")

135
worlds/blasphemous/Exits.py Normal file
View File

@@ -0,0 +1,135 @@
from typing import List, Dict
region_exit_table: Dict[str, List[str]] = {
"menu" : ["New Game"],
"albero" : ["To The Holy Line",
"To Desecrated Cistern",
"To Wasteland of the Buried Churches",
"To Dungeons"],
"attots" : ["To Mother of Mothers"],
"ar" : ["To Mother of Mothers",
"To Wall of the Holy Prohibitions",
"To Deambulatory of His Holiness"],
"bottc" : ["To Wasteland of the Buried Churches",
"To Ferrous Tree"],
"botss" : ["To The Holy Line",
"To Mountains of the Endless Dusk"],
"coolotcv" : ["To Graveyard of the Peaks",
"To Wall of the Holy Prohibitions"],
"dohh" : ["To Archcathedral Rooftops"],
"dc" : ["To Albero",
"To Mercy Dreams",
"To Mountains of the Endless Dusk",
"To Echoes of Salt",
"To Grievance Ascends"],
"eos" : ["To Jondo",
"To Mountains of the Endless Dusk",
"To Desecrated Cistern",
"To The Resting Place of the Sister",
"To Mourning and Havoc"],
"ft" : ["To Bridge of the Three Cavalries",
"To Hall of the Dawning",
"To Patio of the Silent Steps"],
"gotp" : ["To Where Olive Trees Wither",
"To Convent of Our Lady of the Charred Visage"],
"ga" : ["To Jondo",
"To Desecrated Cistern"],
"hotd" : ["To Ferrous Tree"],
"jondo" : ["To Mountains of the Endless Dusk",
"To Grievance Ascends"],
"kottw" : ["To Mother of Mothers"],
"lotnw" : ["To Mother of Mothers",
"To The Sleeping Canvases"],
"md" : ["To Wasteland of the Buried Churches",
"To Desecrated Cistern",
"To The Sleeping Canvases"],
"mom" : ["To Patio of the Silent Steps",
"To Archcathedral Rooftops",
"To Knot of the Three Words",
"To Library of the Negated Words",
"To All the Tears of the Sea"],
"moted" : ["To Brotherhood of the Silent Sorrow",
"To Jondo",
"To Desecrated Cistern"],
"mah" : ["To Echoes of Salt",
"To Mother of Mothers"],
"potss" : ["To Ferrous Tree",
"To Mother of Mothers",
"To Wall of the Holy Prohibitions"],
"petrous" : ["To The Holy Line"],
"thl" : ["To Brotherhood of the Silent Sorrow",
"To Petrous",
"To Albero"],
"trpots" : ["To Echoes of Salt"],
"tsc" : ["To Library of the Negated Words",
"To Mercy Dreams"],
"wothp" : ["To Archcathedral Rooftops",
"To Convent of Our Lady of the Charred Visage"],
"wotbc" : ["To Albero",
"To Where Olive Trees Wither",
"To Mercy Dreams"],
"wotw" : ["To Wasteland of the Buried Churches",
"To Graveyard of the Peaks"]
}
exit_lookup_table: Dict[str, str] = {
"New Game": "botss",
"To Albero": "albero",
"To All the Tears of the Sea": "attots",
"To Archcathedral Rooftops": "ar",
"To Bridge of the Three Cavalries": "bottc",
"To Brotherhood of the Silent Sorrow": "botss",
"To Convent of Our Lady of the Charred Visage": "coolotcv",
"To Deambulatory of His Holiness": "dohh",
"To Desecrated Cistern": "dc",
"To Echoes of Salt": "eos",
"To Ferrous Tree": "ft",
"To Graveyard of the Peaks": "gotp",
"To Grievance Ascends": "ga",
"To Hall of the Dawning": "hotd",
"To Jondo": "jondo",
"To Knot of the Three Words": "kottw",
"To Library of the Negated Words": "lotnw",
"To Mercy Dreams": "md",
"To Mother of Mothers": "mom",
"To Mountains of the Endless Dusk": "moted",
"To Mourning and Havoc": "mah",
"To Patio of the Silent Steps": "potss",
"To Petrous": "petrous",
"To The Holy Line": "thl",
"To The Resting Place of the Sister": "trpots",
"To The Sleeping Canvases": "tsc",
"To Wall of the Holy Prohibitions": "wothp",
"To Wasteland of the Buried Churches": "wotbc",
"To Where Olive Trees Wither": "wotw",
"To Dungeons": "dungeon"
}

754
worlds/blasphemous/Items.py Normal file
View File

@@ -0,0 +1,754 @@
from BaseClasses import ItemClassification
from typing import TypedDict, Dict, List, Set
class ItemDict(TypedDict):
name: str
count: int
classification: ItemClassification
base_id = 1909000
item_table: List[ItemDict] = [
# Rosary Beads
{'name': "Dove Skull",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Ember of the Holy Cremation",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Silver Grape",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Uvula of Proclamation",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Hollow Pearl",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Knot of Hair",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Painted Wood Bead",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Piece of a Golden Mask",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Moss Preserved in Glass",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Frozen Olive",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Quirce's Scorched Bead",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Wicker Knot",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Perpetva's Protection",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Thorned Symbol",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Piece of a Tombstone",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Sphere of the Sacred Smoke",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Bead of Red Wax",
'count': 3,
'classification': ItemClassification.progression},
{'name': "Little Toe made of Limestone",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Big Toe made of Limestone",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Fourth Toe made of Limestone",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Bead of Blue Wax",
'count': 3,
'classification': ItemClassification.progression},
{'name': "Pelican Effigy",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Drop of Coagulated Ink",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Amber Eye",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Muted Bell",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Consecrated Amethyst",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Embers of a Broken Star",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Scaly Coin",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Seashell of the Inverted Spiral",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Calcified Eye of Erudition",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Weight of True Guilt",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Reliquary of the Fervent Heart",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Reliquary of the Suffering Heart",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Reliquary of the Sorrowful Heart",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Token of Appreciation",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Cloistered Ruby",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Bead of Gold Thread",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Cloistered Sapphire",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Fire Enclosed in Enamel",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Light of the Lady of the Lamp",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Scale of Burnished Alabaster",
'count': 1,
'classification': ItemClassification.useful},
{'name': "The Young Mason's Wheel",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Crown of Gnawed Iron",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Crimson Heart of a Miura",
'count': 1,
'classification': ItemClassification.useful},
# Prayers
{'name': "Seguiriya to your Eyes like Stars",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Debla of the Lights",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Saeta Dolorosa",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Campanillero to the Sons of the Aurora",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Lorquiana",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Zarabanda of the Safe Haven",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Taranto to my Sister",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Solea of Excommunication",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Tiento to your Thorned Hairs",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Cante Jondo of the Three Sisters",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Verdiales of the Forsaken Hamlet",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Romance to the Crimson Mist",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Zambra to the Resplendent Crown",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Aubade of the Nameless Guardian",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Cantina of the Blue Rose",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Mirabras of the Return to Port",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Tirana of the Celestial Bastion",
'count': 1,
'classification': ItemClassification.progression},
# Relics
{'name': "Blood Perpetuated in Sand",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Incorrupt Hand of the Fraternal Master",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Nail Uprooted from Dirt",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Shroud of Dreamt Sins",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Linen of Golden Thread",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Silvered Lung of Dolphos",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Three Gnarled Tongues",
'count': 1,
'classification': ItemClassification.progression},
# Mea Culpa Hearts
{'name': "Smoking Heart of Incense",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Heart of the Virtuous Pain",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Heart of Saltpeter Blood",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Heart of Oils",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Heart of Cerulean Incense",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Heart of the Holy Purge",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Molten Heart of Boiling Blood",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Heart of the Single Tone",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Heart of the Unnamed Minstrel",
'count': 1,
'classification': ItemClassification.useful},
{'name': "Brilliant Heart of Dawn",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Apodictic Heart of Mea Culpa",
'count': 1,
'classification': ItemClassification.progression},
# Quest Items
{'name': "Cord of the True Burying",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Mark of the First Refuge",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Mark of the Second Refuge",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Mark of the Third Refuge",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Tentudia's Carnal Remains",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Remains of Tentudia's Hair",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Tentudia's Skeletal Remains",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Melted Golden Coins",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Torn Bridal Ribbon",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Black Grieving Veil",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Egg of Deformity",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Hatched Egg of Deformity",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Bouquet of Rosemary",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Incense Garlic",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Thorn Upgrade",
'count': 8,
'classification': ItemClassification.progression},
{'name': "Olive Seeds",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Holy Wound of Attrition",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Holy Wound of Contrition",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Holy Wound of Compunction",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Empty Bile Vessel",
'count': 8,
'classification': ItemClassification.progression},
{'name': "Knot of Rosary Rope",
'count': 6,
'classification': ItemClassification.progression},
{'name': "Golden Thimble Filled with Burning Oil",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Key to the Chamber of the Eldest Brother",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Empty Golden Thimble",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Deformed Mask of Orestes",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Mirrored Mask of Dolphos",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Embossed Mask of Crescente",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Dried Clove",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Sooty Garlic",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Bouquet of Thyme",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Linen Cloth",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Severed Hand",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Dried Flowers bathed in Tears",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Key of the Secular",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Key of the Scribe",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Key of the Inquisitor",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Key of the High Peaks",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Chalice of Inverted Verses",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Quicksilver",
'count': 5,
'classification': ItemClassification.useful},
{'name': "Petrified Bell",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Verses Spun from Gold",
'count': 4,
'classification': ItemClassification.progression},
{'name': "Severed Right Eye of the Traitor",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Broken Left Eye of the Traitor",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Incomplete Scapular",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Key Grown from Twisted Wood",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Holy Wound of Abnegation",
'count': 1,
'classification': ItemClassification.progression},
# Skills
{'name': "Combo Skill",
'count': 3,
'classification': ItemClassification.useful},
{'name': "Charged Skill",
'count': 3,
'classification': ItemClassification.progression},
{'name': "Ranged Skill",
'count': 3,
'classification': ItemClassification.progression},
{'name': "Dive Skill",
'count': 3,
'classification': ItemClassification.progression},
{'name': "Lunge Skill",
'count': 3,
'classification': ItemClassification.useful},
# Other
{'name': "Parietal bone of Lasser, the Inquisitor",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Jaw of Ashgan, the Inquisitor",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Cervical vertebra of Zicher, the Brewmaster",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Clavicle of Dalhuisen, the Schoolchild",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Sternum of Vitas, the Performer",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Ribs of Sabnock, the Guardian",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Vertebra of John, the Gambler",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Scapula of Carlos, the Executioner",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Humerus of McMittens, the Nurse",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Ulna of Koke, the Troubadour",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Radius of Helzer, the Poet",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Frontal of Martinus, the Ropemaker",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Metacarpus of Hodges, the Blacksmith",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Phalanx of Arthur, the Sailor",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Phalanx of Miriam, the Counsellor",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Phalanx of Brannon, the Gravedigger",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Coxal of June, the Prostitute",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Sacrum of the Dark Warlock",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Coccyx of Daniel, the Possessed",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Femur of Karpow, the Bounty Hunter",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Kneecap of Sebastien, the Puppeteer",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Tibia of Alsahli, the Mystic",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Fibula of Rysp, the Ranger",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Temporal of Joel, the Thief",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Metatarsus of Rikusyo, the Traveller",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Phalanx of Zeth, the Prisoner",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Phalanx of William, the Sceptic",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Phalanx of Aralcarim, the Archivist",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Occipital of Tequila, the Metalsmith",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Maxilla of Tarradax, the Cleric",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Nasal bone of Charles, the Artist",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Hyoid bone of Senex, the Beggar",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Vertebra of Lindquist, the Forger",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Trapezium of Jeremiah, the Hangman",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Trapezoid of Yeager, the Jeweller",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Capitate of Barock, the Herald",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Hamate of Vukelich, the Copyist",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Pisiform of Hernandez, the Explorer",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Triquetral of Luca, the Tailor",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Lunate of Keiya, the Butcher",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Scaphoid of Fierce, the Leper",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Anklebone of Weston, the Pilgrim",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Calcaneum of Persian, the Bandit",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Navicular of Kahnnyhoo, the Murderer",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Child of Moonlight",
'count': 38,
'classification': ItemClassification.progression},
{'name': "Life Upgrade",
'count': 6,
'classification': ItemClassification.progression},
{'name': "Fervour Upgrade",
'count': 6,
'classification': ItemClassification.progression},
{'name': "Mea Culpa Upgrade",
'count': 7,
'classification': ItemClassification.progression},
{'name': "Tears of Atonement (250)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (300)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (500)",
'count': 3,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (625)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (750)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (1000)",
'count': 4,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (1250)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (1500)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (1750)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (2000)",
'count': 2,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (2100)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (2500)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (2600)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (3000)",
'count': 2,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (4300)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (5000)",
'count': 4,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (5500)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (9000)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (10000)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (11250)",
'count': 1,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (18000)",
'count': 5,
'classification': ItemClassification.filler},
{'name': "Tears of Atonement (30000)",
'count': 1,
'classification': ItemClassification.filler}
]
group_table: Dict[str, Set[str]] = {
"wounds" : ["Holy Wound of Attrition",
"Holy Wound of Contrition",
"Holy Wound of Compunction"],
"masks" : ["Deformed Mask of Orestes",
"Mirrored Mask of Dolphos",
"Embossed Mask of Crescente"],
"tirso" : ["Bouquet of Rosemary",
"Incense Garlic",
"Olive Seeds",
"Dried Clove",
"Sooty Garlic",
"Bouquet of Thyme"],
"tentudia": ["Tentudia's Carnal Remains",
"Remains of Tentudia's Hair",
"Tentudia's Skeletal Remains"],
"egg" : ["Melted Golden Coins",
"Torn Bridal Ribbon",
"Black Grieving Veil"],
"bones" : ["Parietal bone of Lasser, the Inquisitor",
"Jaw of Ashgan, the Inquisitor",
"Cervical vertebra of Zicher, the Brewmaster",
"Clavicle of Dalhuisen, the Schoolchild",
"Sternum of Vitas, the Performer",
"Ribs of Sabnock, the Guardian",
"Vertebra of John, the Gambler",
"Scapula of Carlos, the Executioner",
"Humerus of McMittens, the Nurse",
"Ulna of Koke, the Troubadour",
"Radius of Helzer, the Poet",
"Frontal of Martinus, the Ropemaker",
"Metacarpus of Hodges, the Blacksmith",
"Phalanx of Arthur, the Sailor",
"Phalanx of Miriam, the Counsellor",
"Phalanx of Brannon, the Gravedigger",
"Coxal of June, the Prostitute",
"Sacrum of the Dark Warlock",
"Coccyx of Daniel, the Possessed",
"Femur of Karpow, the Bounty Hunter",
"Kneecap of Sebastien, the Puppeteer",
"Tibia of Alsahli, the Mystic",
"Fibula of Rysp, the Ranger",
"Temporal of Joel, the Thief",
"Metatarsus of Rikusyo, the Traveller",
"Phalanx of Zeth, the Prisoner",
"Phalanx of William, the Sceptic",
"Phalanx of Aralcarim, the Archivist",
"Occipital of Tequila, the Metalsmith",
"Maxilla of Tarradax, the Cleric",
"Nasal bone of Charles, the Artist",
"Hyoid bone of Senex, the Beggar",
"Vertebra of Lindquist, the Forger",
"Trapezium of Jeremiah, the Hangman",
"Trapezoid of Yeager, the Jeweller",
"Capitate of Barock, the Herald",
"Hamate of Vukelich, the Copyist",
"Pisiform of Hernandez, the Explorer",
"Triquetral of Luca, the Tailor",
"Lunate of Keiya, the Butcher",
"Scaphoid of Fierce, the Leper",
"Anklebone of Weston, the Pilgrim",
"Calcaneum of Persian, the Bandit",
"Navicular of Kahnnyhoo, the Murderer"],
"power" : ["Life Upgrade",
"Fervour Upgrade",
"Empty Bile Vessel",
"Quicksilver"],
"prayer" : ["Seguiriya to your Eyes like Stars",
"Debla of the Lights",
"Saeta Dolorosa",
"Campanillero to the Sons of the Aurora",
"Lorquiana",
"Zarabanda of the Safe Haven",
"Taranto to my Sister",
"Solea of Excommunication",
"Tiento to your Thorned Hairs",
"Cante Jondo of the Three Sisters",
"Verdiales of the Forsaken Hamlet",
"Romance to the Crimson Mist",
"Zambra to the Resplendent Crown",
"Cantina of the Blue Rose",
"Mirabras of the Return to Port"]
}
tears_set: Set[str] = [
"Tears of Atonement (500)",
"Tears of Atonement (625)",
"Tears of Atonement (750)",
"Tears of Atonement (1000)",
"Tears of Atonement (1250)",
"Tears of Atonement (1500)",
"Tears of Atonement (1750)",
"Tears of Atonement (2000)",
"Tears of Atonement (2100)",
"Tears of Atonement (2500)",
"Tears of Atonement (2600)",
"Tears of Atonement (3000)",
"Tears of Atonement (4300)",
"Tears of Atonement (5000)",
"Tears of Atonement (5500)",
"Tears of Atonement (9000)",
"Tears of Atonement (10000)",
"Tears of Atonement (11250)",
"Tears of Atonement (18000)",
"Tears of Atonement (30000)"
]
reliquary_set: Set[str] = [
"Reliquary of the Fervent Heart",
"Reliquary of the Suffering Heart",
"Reliquary of the Sorrowful Heart"
]
skill_set: Set[str] = [
"Combo Skill",
"Charged Skill",
"Ranged Skill",
"Dive Skill",
"Lunge Skill"
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,257 @@
from Options import Choice, Toggle, DefaultOnToggle, DeathLink
class PrieDieuWarp(DefaultOnToggle):
"""Automatically unlocks the ability to warp between Prie Dieu shrines."""
display_name = "Unlock Fast Travel"
class SkipCutscenes(DefaultOnToggle):
"""Automatically skips most cutscenes."""
display_name = "Auto Skip Cutscenes"
class CorpseHints(DefaultOnToggle):
"""Changes the 34 corpses in game to give various hints about item locations."""
display_name = "Corpse Hints"
class Difficulty(Choice):
"""Adjusts the logic required to defeat bosses.
Impossible: Removes all logic requirements for bosses. Good luck."""
display_name = "Difficulty"
option_easy = 0
option_normal = 1
option_hard = 2
option_impossible = 3
default = 1
class Penitence(Toggle):
"""Allows one of the three Penitences to be chosen at the beginning of the game."""
display_name = "Penitence"
class ExpertLogic(Toggle):
"""Expands the logic used by the randomizer to allow for some difficult and/or lesser known tricks."""
display_name = "Expert Logic"
class Ending(Choice):
"""Choose which ending is required to complete the game."""
display_name = "Ending"
option_any_ending = 0
option_ending_b = 1
option_ending_c = 2
default = 0
class ThornShuffle(Choice):
"""Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool."""
display_name = "Shuffle Thorn"
option_anywhere = 0
option_local_only = 1
option_vanilla = 2
default = 0
class ReliquaryShuffle(DefaultOnToggle):
"""Adds the True Torment exclusive Reliquary rosary beads into the item pool."""
display_name = "Shuffle Penitence Rewards"
class CherubShuffle(DefaultOnToggle):
"""Shuffles Children of Moonlight into the item pool."""
display_name = "Shuffle Children of Moonlight"
class LifeShuffle(DefaultOnToggle):
"""Shuffles life upgrades from the Lady of the Six Sorrows into the item pool."""
display_name = "Shuffle Life Upgrades"
class FervourShuffle(DefaultOnToggle):
"""Shuffles fervour upgrades from the Oil of the Pilgrims into the item pool."""
display_name = "Shuffle Fervour Upgrades"
class SwordShuffle(DefaultOnToggle):
"""Shuffles Mea Culpa upgrades from the Mea Culpa Altars into the item pool."""
display_name = "Shuffle Mea Culpa Upgrades"
class BlessingShuffle(DefaultOnToggle):
"""Shuffles blessings from the Lake of Silent Pilgrims into the item pool."""
display_name = "Shuffle Blessings"
class DungeonShuffle(DefaultOnToggle):
"""Shuffles rewards from completing Confessor Dungeons into the item pool."""
display_name = "Shuffle Dungeon Rewards"
class TirsoShuffle(DefaultOnToggle):
"""Shuffles rewards from delivering herbs to Tirso into the item pool."""
display_name = "Shuffle Tirso's Rewards"
class MiriamShuffle(DefaultOnToggle):
"""Shuffles the prayer given by Miriam into the item pool."""
display_name = "Shuffle Miriram's Reward"
class RedentoShuffle(DefaultOnToggle):
"""Shuffles rewards from assisting Redento into the item pool."""
display_name = "Shuffle Redento's Rewards"
class JocineroShuffle(DefaultOnToggle):
"""Shuffles rewards from rescuing 20 and 38 Children of Moonlight into the item pool."""
display_name = "Shuffle Jocinero's Rewards"
class AltasgraciasShuffle(DefaultOnToggle):
"""Shuffles the reward given by Altasgracias and the item left behind by them into the item pool."""
display_name = "Shuffle Altasgracias' Rewards"
class TentudiaShuffle(DefaultOnToggle):
"""Shuffles the rewards from delivering Tentudia's remains to Lvdovico into the item pool."""
display_name = "Shuffle Lvdovico's Rewards"
class GeminoShuffle(DefaultOnToggle):
"""Shuffles the rewards from Gemino's quest and the hidden tomb into the item pool."""
display_name = "Shuffle Gemino's Rewards"
class GuiltShuffle(DefaultOnToggle):
"""Shuffles the Weight of True Guilt into the item pool."""
display_name = "Shuffle Immaculate Bead"
class OssuaryShuffle(DefaultOnToggle):
"""Shuffles the rewards from delivering bones to the Ossuary into the item pool."""
display_name = "Shuffle Ossuary Rewards"
class BossShuffle(DefaultOnToggle):
"""Shuffles the Tears of Atonement from defeating bosses into the item pool."""
display_name = "Shuffle Boss Tears"
class WoundShuffle(DefaultOnToggle):
"""Shuffles the Holy Wounds required to pass the Bridge of the Three Cavalries into the item pool."""
display_name = "Shuffle Holy Wounds"
class MaskShuffle(DefaultOnToggle):
"""Shuffles the masks required to use the elevator in Archcathedral Rooftops into the item pool."""
display_name = "Shuffle Masks"
class EyeShuffle(DefaultOnToggle):
"""Shuffles the Eyes of the Traitor from defeating Isidora and Sierpes into the item pool."""
display_name = "Shuffle Traitor's Eyes"
class HerbShuffle(DefaultOnToggle):
"""Shuffles the herbs required for Tirso's quest into the item pool."""
display_name = "Shuffle Herbs"
class ChurchShuffle(DefaultOnToggle):
"""Shuffles the rewards from donating 5,000 and 50,000 Tears of Atonement to the Church in Albero into the item pool."""
display_name = "Shuffle Donation Rewards"
class ShopShuffle(DefaultOnToggle):
"""Shuffles the items sold in Candelaria's shops into the item pool."""
display_name = "Shuffle Shop Items"
class CandleShuffle(DefaultOnToggle):
"""Shuffles the Beads of Wax and their upgrades into the item pool."""
display_name = "Shuffle Candles"
class StartWheel(Toggle):
"""Changes the beginning gift to The Young Mason's Wheel."""
display_name = "Start with Wheel"
class SkillRando(Toggle):
"""Randomizes the abilities from the skill tree into the item pool."""
display_name = "Skill Randomizer"
class EnemyRando(Choice):
"""Randomizes the enemies that appear in each room.
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in a standard game.
Randomized: Every enemy is completely random, and can appear any number of times.
Some enemies will never be randomized."""
display_name = "Enemy Randomizer"
option_disabled = 0
option_shuffled = 1
option_randomized = 2
default = 0
class EnemyGroups(DefaultOnToggle):
"""Randomized enemies will chosen from sets of specific groups.
(Weak, normal, large, flying)
Has no effect if Enemy Randomizer is disabled."""
display_name = "Enemy Groups"
class EnemyScaling(DefaultOnToggle):
"""Randomized enemies will have their stats increased or decreased depending on the area they appear in.
Has no effect if Enemy Randomizer is disabled."""
display_name = "Enemy Scaling"
class BlasphemousDeathLink(DeathLink):
"""When you die, everyone dies. The reverse is also true.
Note that Guilt Fragments will not appear when killed by Death Link."""
blasphemous_options = {
"prie_dieu_warp": PrieDieuWarp,
"skip_cutscenes": SkipCutscenes,
"corpse_hints": CorpseHints,
"difficulty": Difficulty,
"penitence": Penitence,
"expert_logic": ExpertLogic,
"ending": Ending,
"thorn_shuffle" : ThornShuffle,
"reliquary_shuffle": ReliquaryShuffle,
"cherub_shuffle" : CherubShuffle,
"life_shuffle" : LifeShuffle,
"fervour_shuffle" : FervourShuffle,
"sword_shuffle" : SwordShuffle,
"blessing_shuffle" : BlessingShuffle,
"dungeon_shuffle" : DungeonShuffle,
"tirso_shuffle" : TirsoShuffle,
"miriam_shuffle" : MiriamShuffle,
"redento_shuffle" : RedentoShuffle,
"jocinero_shuffle" : JocineroShuffle,
"altasgracias_shuffle" : AltasgraciasShuffle,
"tentudia_shuffle" : TentudiaShuffle,
"gemino_shuffle" : GeminoShuffle,
"guilt_shuffle" : GuiltShuffle,
"ossuary_shuffle" : OssuaryShuffle,
"boss_shuffle" : BossShuffle,
"wound_shuffle" : WoundShuffle,
"mask_shuffle" : MaskShuffle,
"eye_shuffle": EyeShuffle,
"herb_shuffle" : HerbShuffle,
"church_shuffle" : ChurchShuffle,
"shop_shuffle" : ShopShuffle,
"candle_shuffle" : CandleShuffle,
"start_wheel": StartWheel,
"skill_randomizer": SkillRando,
"enemy_randomizer": EnemyRando,
"enemy_groups": EnemyGroups,
"enemy_scaling": EnemyScaling,
"death_link": BlasphemousDeathLink
}

1455
worlds/blasphemous/Rules.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,246 @@
from typing import Set, Dict
unrandomized_dict: Dict[str, str] = {
"CoOLotCV: Fountain of burning oil": "Golden Thimble Filled with Burning Oil",
"MotED: Egg hatching": "Hatched Egg of Deformity",
"BotSS: Crisanta's gift": "Holy Wound of Abnegation",
"DC: Chalice room": "Chalice of Inverted Verses"
}
cherub_set: Set[str] = [
"Albero: Child of Moonlight",
"AR: Upper west shaft Child of Moonlight",
"BotSS: Starting room Child of Moonlight",
"DC: Child of Moonlight, above water",
"DC: Upper east Child of Moonlight",
"DC: Child of Moonlight, miasma room",
"DC: Child of Moonlight, behind pillar",
"DC: Top of elevator Child of Moonlight",
"DC: Elevator shaft Child of Moonlight",
"GotP: Shop cave Child of Moonlight",
"GotP: Elevator shaft Child of Moonlight",
"GotP: West shaft Child of Moonlight",
"GotP: Center shaft Child of Moonlight",
"GA: Miasma room Child of Moonlight",
"GA: Blood bridge Child of Moonlight",
"GA: Lower east Child of Moonlight",
"Jondo: Upper east Child of Moonlight",
"Jondo: Spike tunnel Child of Moonlight",
"Jondo: Upper west Child of Moonlight",
"LotNW: Platform room Child of Moonlight",
"LotNW: Lowest west Child of Moonlight",
"LotNW: Elevator Child of Moonlight",
"MD: Second area Child of Moonlight",
"MD: Cave Child of Moonlight",
"MoM: Lower west Child of Moonlight",
"MoM: Upper center Child of Moonlight",
"MotED: Child of Moonlight, above chasm",
"PotSS: First area Child of Moonlight",
"PotSS: Third area Child of Moonlight",
"THL: Child of Moonlight",
"WotHP: Upper east room, top bronze cell",
"WotHP: Upper west room, top silver cell",
"WotHP: Lower east room, bottom silver cell",
"WotHP: Outside Child of Moonlight",
"WotBC: Outside Child of Moonlight",
"WotBC: Cliffside Child of Moonlight",
"WOTW: Underground Child of Moonlight",
"WOTW: Upper east Child of Moonlight",
]
life_set: Set[str] = [
"AR: Lady of the Six Sorrows",
"CoOLotCV: Lady of the Six Sorrows",
"DC: Lady of the Six Sorrows, from MD",
"DC: Lady of the Six Sorrows, elevator shaft",
"GotP: Lady of the Six Sorrows",
"LotNW: Lady of the Six Sorrows"
]
fervour_set: Set[str] = [
"DC: Oil of the Pilgrims",
"GotP: Oil of the Pilgrims",
"GA: Oil of the Pilgrims",
"LotNW: Oil of the Pilgrims",
"MoM: Oil of the Pilgrims",
"WotHP: Oil of the Pilgrims"
]
sword_set: Set[str] = [
"Albero: Mea Culpa altar",
"AR: Mea Culpa altar",
"BotSS: Mea Culpa altar",
"CoOLotCV: Mea Culpa altar",
"DC: Mea Culpa altar",
"LotNW: Mea Culpa altar",
"MoM: Mea Culpa altar"
]
blessing_dict: Dict[str, str] = {
"Albero: Bless Severed Hand": "Incorrupt Hand of the Fraternal Master",
"Albero: Bless Linen Cloth": "Shroud of Dreamt Sins",
"Albero: Bless Hatched Egg": "Three Gnarled Tongues"
}
dungeon_dict: Dict[str, str] = {
"Confessor Dungeon 1 extra": "Tears of Atonement (1000)",
"Confessor Dungeon 2 extra": "Heart of the Single Tone",
"Confessor Dungeon 3 extra": "Tears of Atonement (3000)",
"Confessor Dungeon 4 extra": "Embers of a Broken Star",
"Confessor Dungeon 5 extra": "Tears of Atonement (5000)",
"Confessor Dungeon 6 extra": "Scaly Coin",
"Confessor Dungeon 7 extra": "Seashell of the Inverted Spiral"
}
tirso_dict: Dict[str, str] = {
"Albero: Tirso's 1st reward": "Linen Cloth",
"Albero: Tirso's 2nd reward": "Tears of Atonement (500)",
"Albero: Tirso's 3rd reward": "Tears of Atonement (1000)",
"Albero: Tirso's 4th reward": "Tears of Atonement (2000)",
"Albero: Tirso's 5th reward": "Tears of Atonement (5000)",
"Albero: Tirso's 6th reward": "Tears of Atonement (10000)",
"Albero: Tirso's final reward": "Knot of Rosary Rope"
}
redento_dict: Dict[str, str] = {
"MoM: Redento's treasure": "Nail Uprooted from Dirt",
"MoM: Final meeting with Redento": "Knot of Rosary Rope",
"MotED: 1st meeting with Redento": "Fourth Toe made of Limestone",
"PotSS: 4th meeting with Redento": "Big Toe made of Limestone",
"WotBC: 3rd meeting with Redento": "Little Toe made of Limestone"
}
jocinero_dict: Dict[str, str] = {
"TSC: Jocinero's 1st reward": "Linen of Golden Thread",
"TSC: Jocinero's final reward": "Campanillero to the Sons of the Aurora"
}
altasgracias_dict: Dict[str, str] = {
"GA: Altasgracias' gift": "Egg of Deformity",
"GA: Empty giant egg": "Knot of Hair"
}
tentudia_dict: Dict[str, str] = {
"Albero: Lvdovico's 1st reward": "Tears of Atonement (500)",
"Albero: Lvdovico's 2nd reward": "Tears of Atonement (1000)",
"Albero: Lvdovico's 3rd reward": "Debla of the Lights"
}
gemino_dict: Dict[str, str] = {
"WOTW: Gift for the tomb": "Dried Flowers bathed in Tears",
"WOTW: Underground tomb": "Saeta Dolorosa",
"WOTW: Gemino's gift": "Empty Golden Thimble",
"WOTW: Gemino's reward": "Frozen Olive"
}
ossuary_dict: Dict[str, str] = {
"Ossuary: 1st reward": "Tears of Atonement (250)",
"Ossuary: 2nd reward": "Tears of Atonement (500)",
"Ossuary: 3rd reward": "Tears of Atonement (750)",
"Ossuary: 4th reward": "Tears of Atonement (1000)",
"Ossuary: 5th reward": "Tears of Atonement (1250)",
"Ossuary: 6th reward": "Tears of Atonement (1500)",
"Ossuary: 7th reward": "Tears of Atonement (1750)",
"Ossuary: 8th reward": "Tears of Atonement (2000)",
"Ossuary: 9th reward": "Tears of Atonement (2500)",
"Ossuary: 10th reward": "Tears of Atonement (3000)",
"Ossuary: 11th reward": "Tears of Atonement (5000)",
}
boss_dict: Dict[str, str] = {
"BotTC: Esdras, of the Anointed Legion": "Tears of Atonement (4300)",
"BotSS: Warden of the Silent Sorrow": "Tears of Atonement (300)",
"CoOLotCV: Our Lady of the Charred Visage": "Tears of Atonement (2600)",
"HotD: Laudes, the First of the Amanecidas": "Tears of Atonement (30000)",
"GotP: Amanecida of the Bejeweled Arrow": "Tears of Atonement (18000)",
"GA: Tres Angustias": "Tears of Atonement (2100)",
"MD: Ten Piedad": "Tears of Atonement (625)",
"MoM: Melquiades, The Exhumed Archbishop": "Tears of Atonement (5500)",
"MotED: Amanecida of the Golden Blades": "Tears of Atonement (18000)",
"MaH: Sierpes": "Tears of Atonement (5000)",
"PotSS: Amanecida of the Chiselled Steel": "Tears of Atonement (18000)",
"TSC: Exposito, Scion of Abjuration": "Tears of Atonement (9000)",
"WotHP: Quirce, Returned By The Flames": "Tears of Atonement (11250)",
"WotHP: Amanecida of the Molten Thorn": "Tears of Atonement (18000)"
}
wound_dict: Dict[str, str] = {
"CoOLotCV: Visage of Compunction": "Holy Wound of Compunction",
"GA: Visage of Contrition": "Holy Wound of Contrition",
"MD: Visage of Attrition": "Holy Wound of Attrition"
}
mask_dict: Dict[str, str] = {
"CoOLotCV: Mask room": "Mirrored Mask of Dolphos",
"LotNW: Mask room": "Embossed Mask of Crescente",
"MoM: Mask room": "Deformed Mask of Orestes"
}
eye_dict: Dict[str, str] = {
"Ossuary: Isidora, Voice of the Dead": "Severed Right Eye of the Traitor",
"MaH: Sierpes' eye": "Broken Left Eye of the Traitor"
}
herb_dict: Dict[str, str] = {
"Albero: Gate of Travel room": "Bouquet of Thyme",
"Jondo: Lower east bell trap": "Bouquet of Rosemary",
"MotED: Blood platform alcove": "Dried Clove",
"PotSS: Third area lower ledge": "Olive Seeds",
"TSC: Painting ladder ledge": "Sooty Garlic",
"WOTW: Entrance to tomb": "Incense Garlic"
}
church_dict: Dict[str, str] = {
"Albero: Donate 5000 Tears": "Token of Appreciation",
"Albero: Donate 50000 Tears": "Cloistered Ruby"
}
shop_dict: Dict[str, str] = {
"GotP: Shop item 1": "Torn Bridal Ribbon",
"GotP: Shop item 2": "Calcified Eye of Erudition",
"GotP: Shop item 3": "Ember of the Holy Cremation",
"MD: Shop item 1": "Key to the Chamber of the Eldest Brother",
"MD: Shop item 2": "Hollow Pearl",
"MD: Shop item 3": "Moss Preserved in Glass",
"TSC: Shop item 1": "Wicker Knot",
"TSC: Shop item 2": "Empty Bile Vessel",
"TSC: Shop item 3": "Key of the Inquisitor"
}
thorn_set: Set[str] = {
"THL: Deogracias' gift",
"Confessor Dungeon 1 main",
"Confessor Dungeon 2 main",
"Confessor Dungeon 3 main",
"Confessor Dungeon 4 main",
"Confessor Dungeon 5 main",
"Confessor Dungeon 6 main",
"Confessor Dungeon 7 main",
}
candle_dict: Dict[str, str] = {
"CoOLotCV: Red candle": "Bead of Red Wax",
"LotNW: Red candle": "Bead of Red Wax",
"MD: Red candle": "Bead of Red Wax",
"BotSS: Blue candle": "Bead of Blue Wax",
"CoOLotCV: Blue candle": "Bead of Blue Wax",
"MD: Blue candle": "Bead of Blue Wax"
}
skill_dict: Dict[str, str] = {
"Skill 1, Tier 1": "Combo Skill",
"Skill 1, Tier 2": "Combo Skill",
"Skill 1, Tier 3": "Combo Skill",
"Skill 2, Tier 1": "Charged Skill",
"Skill 2, Tier 2": "Charged Skill",
"Skill 2, Tier 3": "Charged Skill",
"Skill 3, Tier 1": "Ranged Skill",
"Skill 3, Tier 2": "Ranged Skill",
"Skill 3, Tier 3": "Ranged Skill",
"Skill 4, Tier 1": "Dive Skill",
"Skill 4, Tier 2": "Dive Skill",
"Skill 4, Tier 3": "Dive Skill",
"Skill 5, Tier 1": "Lunge Skill",
"Skill 5, Tier 2": "Lunge Skill",
"Skill 5, Tier 3": "Lunge Skill",
}

View File

@@ -0,0 +1,413 @@
from typing import Dict, Set, Any
from collections import Counter
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
from ..AutoWorld import World, WebWorld
from .Items import base_id, item_table, group_table, tears_set, reliquary_set, skill_set
from .Locations import location_table, shop_set
from .Exits import region_exit_table, exit_lookup_table
from .Rules import rules
from worlds.generic.Rules import set_rule
from .Options import blasphemous_options
from . import Vanilla
class BlasphemousWeb(WebWorld):
theme = "stone"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Blasphemous randomizer connected to an Archipelago Multiworld",
"English",
"setup_en.md",
"setup/en",
["TRPG"]
)]
class BlasphemousWorld(World):
"""
Blasphemous is a challenging Metroidvania set in the cursed land of Cvstodia. Play as the Penitent One, trapped
in an endless cycle of death and rebirth, and free the world from it's terrible fate in your quest to break
your eternal damnation!
"""
game: str = "Blasphemous"
web = BlasphemousWeb()
data_version: 1
item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)}
location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)}
location_name_to_game_id = {loc["name"]: loc["game_id"] for loc in location_table}
item_name_groups = group_table
option_definitions = blasphemous_options
def set_rules(self):
rules(self)
def create_item(self, name: str) -> "BlasphemousItem":
item_id: int = self.item_name_to_id[name]
id = item_id - base_id
return BlasphemousItem(name, item_table[id]["classification"], item_id, player=self.player)
def create_event(self, event: str):
return BlasphemousItem(event, ItemClassification.progression_skip_balancing, None, self.player)
def get_filler_item_name(self) -> str:
return self.multiworld.random.choice(tears_set)
def generate_basic(self):
placed_items = []
placed_items.extend(Vanilla.unrandomized_dict.values())
if not self.multiworld.reliquary_shuffle[self.player]:
placed_items.extend(reliquary_set)
elif self.multiworld.reliquary_shuffle[self.player]:
placed_items.append("Tears of Atonement (250)")
placed_items.append("Tears of Atonement (300)")
placed_items.append("Tears of Atonement (500)")
if not self.multiworld.cherub_shuffle[self.player]:
for i in range(38):
placed_items.append("Child of Moonlight")
if not self.multiworld.life_shuffle[self.player]:
for i in range(6):
placed_items.append("Life Upgrade")
if not self.multiworld.fervour_shuffle[self.player]:
for i in range(6):
placed_items.append("Fervour Upgrade")
if not self.multiworld.sword_shuffle[self.player]:
for i in range(7):
placed_items.append("Mea Culpa Upgrade")
if not self.multiworld.blessing_shuffle[self.player]:
placed_items.extend(Vanilla.blessing_dict.values())
if not self.multiworld.dungeon_shuffle[self.player]:
placed_items.extend(Vanilla.dungeon_dict.values())
if not self.multiworld.tirso_shuffle[self.player]:
placed_items.extend(Vanilla.tirso_dict.values())
if not self.multiworld.miriam_shuffle[self.player]:
placed_items.append("Cantina of the Blue Rose")
if not self.multiworld.redento_shuffle[self.player]:
placed_items.extend(Vanilla.redento_dict.values())
if not self.multiworld.jocinero_shuffle[self.player]:
placed_items.extend(Vanilla.jocinero_dict.values())
if not self.multiworld.altasgracias_shuffle[self.player]:
placed_items.extend(Vanilla.altasgracias_dict.values())
if not self.multiworld.tentudia_shuffle[self.player]:
placed_items.extend(Vanilla.tentudia_dict.values())
if not self.multiworld.gemino_shuffle[self.player]:
placed_items.extend(Vanilla.gemino_dict.values())
if not self.multiworld.guilt_shuffle[self.player]:
placed_items.append("Weight of True Guilt")
if not self.multiworld.ossuary_shuffle[self.player]:
placed_items.extend(Vanilla.ossuary_dict.values())
if not self.multiworld.boss_shuffle[self.player]:
placed_items.extend(Vanilla.boss_dict.values())
if not self.multiworld.wound_shuffle[self.player]:
placed_items.extend(Vanilla.wound_dict.values())
if not self.multiworld.mask_shuffle[self.player]:
placed_items.extend(Vanilla.mask_dict.values())
if not self.multiworld.eye_shuffle[self.player]:
placed_items.extend(Vanilla.eye_dict.values())
if not self.multiworld.herb_shuffle[self.player]:
placed_items.extend(Vanilla.herb_dict.values())
if not self.multiworld.church_shuffle[self.player]:
placed_items.extend(Vanilla.church_dict.values())
if not self.multiworld.shop_shuffle[self.player]:
placed_items.extend(Vanilla.shop_dict.values())
if self.multiworld.thorn_shuffle[self.player] == 2:
for i in range(8):
placed_items.append("Thorn Upgrade")
if not self.multiworld.candle_shuffle[self.player]:
placed_items.extend(Vanilla.candle_dict.values())
if self.multiworld.start_wheel[self.player]:
placed_items.append("The Young Mason's Wheel")
if not self.multiworld.skill_randomizer[self.player]:
placed_items.extend(Vanilla.skill_dict.values())
counter = Counter(placed_items)
pool = []
for item in item_table:
count = item["count"] - counter[item["name"]]
if count <= 0:
continue
else:
for i in range(count):
pool.append(self.create_item(item["name"]))
self.multiworld.itempool += pool
def pre_fill(self):
self.place_items_from_dict(Vanilla.unrandomized_dict)
if not self.multiworld.cherub_shuffle[self.player]:
self.place_items_from_set(Vanilla.cherub_set, "Child of Moonlight")
if not self.multiworld.life_shuffle[self.player]:
self.place_items_from_set(Vanilla.life_set, "Life Upgrade")
if not self.multiworld.fervour_shuffle[self.player]:
self.place_items_from_set(Vanilla.fervour_set, "Fervour Upgrade")
if not self.multiworld.sword_shuffle[self.player]:
self.place_items_from_set(Vanilla.sword_set, "Mea Culpa Upgrade")
if not self.multiworld.blessing_shuffle[self.player]:
self.place_items_from_dict(Vanilla.blessing_dict)
if not self.multiworld.dungeon_shuffle[self.player]:
self.place_items_from_dict(Vanilla.dungeon_dict)
if not self.multiworld.tirso_shuffle[self.player]:
self.place_items_from_dict(Vanilla.tirso_dict)
if not self.multiworld.miriam_shuffle[self.player]:
self.multiworld.get_location("AtTotS: Miriam's gift", self.player)\
.place_locked_item(self.create_item("Cantina of the Blue Rose"))
if not self.multiworld.redento_shuffle[self.player]:
self.place_items_from_dict(Vanilla.redento_dict)
if not self.multiworld.jocinero_shuffle[self.player]:
self.place_items_from_dict(Vanilla.jocinero_dict)
if not self.multiworld.altasgracias_shuffle[self.player]:
self.place_items_from_dict(Vanilla.altasgracias_dict)
if not self.multiworld.tentudia_shuffle[self.player]:
self.place_items_from_dict(Vanilla.tentudia_dict)
if not self.multiworld.gemino_shuffle[self.player]:
self.place_items_from_dict(Vanilla.gemino_dict)
if not self.multiworld.guilt_shuffle[self.player]:
self.multiworld.get_location("GotP: Confessor Dungeon room", self.player)\
.place_locked_item(self.create_item("Weight of True Guilt"))
if not self.multiworld.ossuary_shuffle[self.player]:
self.place_items_from_dict(Vanilla.ossuary_dict)
if not self.multiworld.boss_shuffle[self.player]:
self.place_items_from_dict(Vanilla.boss_dict)
if not self.multiworld.wound_shuffle[self.player]:
self.place_items_from_dict(Vanilla.wound_dict)
if not self.multiworld.mask_shuffle[self.player]:
self.place_items_from_dict(Vanilla.mask_dict)
if not self.multiworld.eye_shuffle[self.player]:
self.place_items_from_dict(Vanilla.eye_dict)
if not self.multiworld.herb_shuffle[self.player]:
self.place_items_from_dict(Vanilla.herb_dict)
if not self.multiworld.church_shuffle[self.player]:
self.place_items_from_dict(Vanilla.church_dict)
if not self.multiworld.shop_shuffle[self.player]:
self.place_items_from_dict(Vanilla.shop_dict)
if self.multiworld.thorn_shuffle[self.player] == 2:
self.place_items_from_set(Vanilla.thorn_set, "Thorn Upgrade")
if not self.multiworld.candle_shuffle[self.player]:
self.place_items_from_dict(Vanilla.candle_dict)
if self.multiworld.start_wheel[self.player]:
self.multiworld.get_location("BotSS: Beginning gift", self.player)\
.place_locked_item(self.create_item("The Young Mason's Wheel"))
if not self.multiworld.skill_randomizer[self.player]:
self.place_items_from_dict(Vanilla.skill_dict)
if self.multiworld.thorn_shuffle[self.player] == 1:
self.multiworld.local_items[self.player].value.add("Thorn Upgrade")
def place_items_from_set(self, location_set: Set[str], name: str):
for loc in location_set:
self.multiworld.get_location(loc, self.player)\
.place_locked_item(self.create_item(name))
def place_items_from_dict(self, option_dict: Dict[str, str]):
for loc, item in option_dict.items():
self.multiworld.get_location(loc, self.player)\
.place_locked_item(self.create_item(item))
def create_regions(self) -> None:
player = self.player
world = self.multiworld
region_table: Dict[str, Region] = {
"menu" : Region("Menu", player, world),
"albero" : Region("Albero", player, world),
"attots" : Region("All the Tears of the Sea", player, world),
"ar" : Region("Archcathedral Rooftops", player, world),
"bottc" : Region("Bridge of the Three Cavalries", player, world),
"botss" : Region("Brotherhood of the Silent Sorrow", player, world),
"coolotcv": Region("Convent of Our Lady of the Charred Visage", player, world),
"dohh" : Region("Deambulatory of His Holiness", player, world),
"dc" : Region("Desecrated Cistern", player, world),
"eos" : Region("Echoes of Salt", player, world),
"ft" : Region("Ferrous Tree", player, world),
"gotp" : Region("Graveyard of the Peaks", player, world),
"ga" : Region("Grievance Ascends", player, world),
"hotd" : Region("Hall of the Dawning", player, world),
"jondo" : Region("Jondo", player, world),
"kottw" : Region("Knot of the Three Words", player, world),
"lotnw" : Region("Library of the Negated Words", player, world),
"md" : Region("Mercy Dreams", player, world),
"mom" : Region("Mother of Mothers", player, world),
"moted" : Region("Mountains of the Endless Dusk", player, world),
"mah" : Region("Mourning and Havoc", player, world),
"potss" : Region("Patio of the Silent Steps", player, world),
"petrous" : Region("Petrous", player, world),
"thl" : Region("The Holy Line", player, world),
"trpots" : Region("The Resting Place of the Sister", player, world),
"tsc" : Region("The Sleeping Canvases", player, world),
"wothp" : Region("Wall of the Holy Prohibitions", player, world),
"wotbc" : Region("Wasteland of the Buried Churches", player, world),
"wotw" : Region("Where Olive Trees Wither", player, world),
"dungeon" : Region("Dungeons", player, world)
}
for rname, reg in region_table.items():
world.regions.append(reg)
for ename, exits in region_exit_table.items():
if ename == rname:
for i in exits:
ent = Entrance(player, i, reg)
reg.exits.append(ent)
for e, r in exit_lookup_table.items():
if i == e:
ent.connect(region_table[r])
for loc in location_table:
id = base_id + location_table.index(loc)
region_table[loc["region"]].locations\
.append(BlasphemousLocation(self.player, loc["name"], id, region_table[loc["region"]]))
victory = Location(self.player, "His Holiness Escribar", None, self.multiworld.get_region("Deambulatory of His Holiness", self.player))
victory.place_locked_item(self.create_event("Victory"))
self.multiworld.get_region("Deambulatory of His Holiness", self.player).locations.append(victory)
if self.multiworld.ending[self.player].value == 1:
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
elif self.multiworld.ending[self.player].value == 2:
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and \
state.has("Holy Wound of Abnegation", player))
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
def fill_slot_data(self) -> Dict[str, Any]:
slot_data: Dict[str, Any] = {}
locations = []
for loc in self.multiworld.get_filled_locations(self.player):
if loc.name == "His Holiness Escribar":
continue
else:
data = {
"id": self.location_name_to_game_id[loc.name],
"ap_id": loc.address,
"name": loc.item.name,
"player_name": self.multiworld.player_name[loc.item.player]
}
if loc.name in shop_set:
data["type"] = loc.item.classification.name
locations.append(data)
config = {
"versionCreated": "AP",
"general": {
"teleportationAlwaysUnlocked": bool(self.multiworld.prie_dieu_warp[self.player].value),
"skipCutscenes": bool(self.multiworld.skip_cutscenes[self.player].value),
"enablePenitence": bool(self.multiworld.penitence[self.player].value),
"hardMode": False,
"customSeed": 0,
"allowHints": bool(self.multiworld.corpse_hints[self.player].value)
},
"items": {
"type": 1,
"lungDamage": False,
"disableNPCDeath": True,
"startWithWheel": bool(self.multiworld.start_wheel[self.player].value),
"shuffleReliquaries": bool(self.multiworld.reliquary_shuffle[self.player].value)
},
"enemies": {
"type": self.multiworld.enemy_randomizer[self.player].value,
"maintainClass": bool(self.multiworld.enemy_groups[self.player].value),
"areaScaling": bool(self.multiworld.enemy_scaling[self.player].value)
},
"prayers": {
"type": 0,
"removeMirabis": False
},
"doors": {
"type": 0
},
"debug": {
"type": 0
}
}
slot_data = {
"locations": locations,
"cfg": config,
"ending": self.multiworld.ending[self.player].value,
"death_link": bool(self.multiworld.death_link[self.player].value)
}
return slot_data
class BlasphemousItem(Item):
game: str = "Blasphemous"
class BlasphemousLocation(Location):
game: str = "Blasphemous"

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