Compare commits

...

120 Commits

Author SHA1 Message Date
Exempt-Medic
b70a5b8dbd Account for multiclass items in progression balancing 2025-04-26 07:58:20 -04:00
KonoTyran
2624a0a7ea Remove Slay the Spire (#4673)
* Remove Slay the Spire

* remove slay the spire
2025-04-25 20:54:53 +02:00
Nicholas Brochu
8755d5cbc0 Remove Game: Zork Grand Inquisitor (#4884)
* remove zork grand inquisitor

* add apworld to inno setup installdelete
2025-04-25 01:42:42 +02:00
Jérémie Bolduc
abb6d7fbdb Stardew Valley: Replace all add_rule by set_rule #4909 2025-04-24 23:36:25 +02:00
Star Rauchenberger
fc04192c99 Lingo: Use OptionCounter for trap_weights (#4920) 2025-04-24 23:14:42 +02:00
Fabian Dill
d4110d3b2a LttP: make progression health optional (#4918) 2025-04-24 23:10:58 +02:00
NewSoupVi
05c1751d29 Core: Add "OptionCounter", use it for generic "StartInventory" and Witness "TrapWeights" (#3756)
* CounterOption

* bring back the negative exception for ItemDict

* Backwards compatibility

* ruff on witness

* fix in calls

* move the contains

* comment

* comment

* Add option min and max values for CounterOption

* Use min 0 for TrapWeights

* This is safe now

* ruff

* This fits on one line again now

* OptionCounter

* Update Options.py

* Couple more typing things

* Update Options.py

* Make StartInventory work again, also make LocationCounter theoretically work

* Docs

* more forceful wording

* forced line break

* Fix unit test (that wasn't breaking?)

* Add trapweights to witness option presets to 'prove' that the unit test passes

* Make it so you can order stuff

* Update macros.html
2025-04-24 22:06:41 +02:00
NewSoupVi
6ad042b349 Core: Add Region.add_event (#2965)
* region.add_event function

* Make it return the location bc why not

* Actually item bc that seems more useful

* Update BaseClasses.py

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

* Update BaseClasses.py

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

* add all the requested features from code review

* oop

* roughly sort args in order of importance (imo)

* Fix typing

---------

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2025-04-24 21:56:52 +02:00
NewSoupVi
e52d8b4dbd The Witness: Remove first-stage requirements of progressive items from the logic files (#4257)
* Remove extraneous symbol requirements

* Some missed Full Dots cases

* Bruh

* merge error

* merge error 2
2025-04-24 21:56:05 +02:00
NewSoupVi
f288e3469c Core: Add a function docstring to roll_settings to hopefully prevent the weights fiasco from being repeated (#3388)
* Add an option docstring to roll_settings to hopefully prevent the weights fiasco from being repeated

* Update Generate.py

* Update Generate.py
2025-04-24 21:55:48 +02:00
Jarno
5bb87c6da5 Tests: Make overlapping test actually print out the overlaps (#4431) 2025-04-24 15:33:30 -04:00
Aaron Wagener
03768a5f90 Tests: Test that a world can generate with item links (#2081)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-04-24 15:23:51 -04:00
Scipio Wright
a84366368f Docs: Update comment for create_item (#4919) 2025-04-24 09:38:30 -04:00
Fabian Dill
29e6a10e42 Setup: offer the default-on option to clean /lib folder on update (#4890)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2025-04-24 08:50:34 +02:00
Fabian Dill
febd280fba Setup: use sha256 for timestamp server (#4892) 2025-04-23 20:30:15 +02:00
black-sliver
73964b374c MultiServer: import get_settings from the correct module (#4914)
* MultiServer: import get_settings from the correct module

* MultiServer: settings: use attr inbstead of dict access
2025-04-23 15:40:36 +00:00
Jérémie Bolduc
bad6a4b211 Stardew Valley: remove BaseLogic generic so importing mixins is no longer needed (#4916)
* remove BaseLogic generic so importing mixins is no longer needed

* self review
2025-04-23 17:31:08 +02:00
Scipio Wright
57d3c52df9 TUNIC: More varied reserved locations for local_fill option (#4653)
* Make reserved locations more varied

* Use CollectionState(self.multiworld) instead of whatever it used to be
2025-04-21 23:41:20 +02:00
Star Rauchenberger
d309de2557 Lingo: Rework Early Good Items (#4910) 2025-04-21 16:06:24 -04:00
Scipio Wright
d5d56ede8b TUNIC: Remove Outdated Plando Code (#4908) 2025-04-21 15:20:22 -04:00
Fabian Dill
6613c29652 Core: print both world source paths in case of conflict (#4751) 2025-04-21 00:53:40 +02:00
NewSoupVi
1a6de25ab6 Core, all worlds: Hard-deprecate old options API (by August 10th 2024) (#3284)
* Core: deprecate old options API

* also deprecate assigning options via option_definitions

---------

Co-authored-by: alwaysintreble <mmmcheese158@gmail.com>
2025-04-21 00:43:31 +02:00
NewSoupVi
b62c1364a9 MultiServer.py: Another Hint Priority + Item Links bug oh boy (#4874)
Basically, hints for itemlink worlds' locations get stored in ctx.hints under
1. the location's player
2. **every individual player** that is participating in the itemlink.

Right now, the updatehint code tries to replace and resend the hint under the itemlinked player, which doesn't work.
2025-04-21 00:43:05 +02:00
Fabian Dill
b59162737d LttP: increase gen rate of pedestal goal with limited rupee pool (#4905)
* LttP: increase gen rate of pedestal goal with limited rupee pool

* improve chance further if retro bow is involved
2025-04-20 23:04:40 +02:00
Jérémie Bolduc
543dcb27d8 Stardew Valley: Exclude maximum one resource packs from pool when in start inventory (#4839)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-04-20 10:51:03 -04:00
Jérémie Bolduc
22941168cd Stardew Valley: Refactor Animals to use Content Packs (#4320) 2025-04-20 10:17:22 -04:00
Scipio Wright
33dc845de8 TUNIC: Fix UT Issue with Fewer Shops Option (#4873) 2025-04-20 09:48:09 -04:00
LiquidCat64
be0f23beb3 CV64: Some DeathLink Adjustments (#4727) 2025-04-20 09:46:57 -04:00
Silvris
b76f2163a4 MM2: Fix invalid weakness failsafe and refactor weakness tests (#4899) 2025-04-20 09:08:30 -04:00
Omnises Nihilis
04aa471526 KH2: Update Docs (#4871) 2025-04-20 08:43:52 -04:00
Trevor L
b756a67c2a BRC: Update Setup Guide (#4861)
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-04-20 08:31:58 -04:00
Jérémie Bolduc
a76ee010eb Stardew Valley: Make Bus and Boat Require Money (#4833) 2025-04-20 08:21:02 -04:00
shananas
eb1fef1f92 KH2: Update Docs (#4869) 2025-04-20 08:20:23 -04:00
Doug Hoskisson
e498cc7d48 Tests: Don't use type as Callable (#4866) 2025-04-20 07:21:40 -04:00
Doug Hoskisson
a26abe079e Zillion: Some Code Cleaning (#4780) 2025-04-20 07:07:17 -04:00
qwint
199b6bdabb Launcher: Update header docstring (#4777) 2025-04-20 07:04:56 -04:00
SunCat
e4bc7bd1cd Checksfinder: Fix the last remnant of outdated game description (#4893)
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-04-20 00:16:46 -04:00
Silvris
20651df307 kvui: fix kwargs on ResizableTextField and ImageButton (#4903) 2025-04-20 01:21:11 +02:00
massimilianodelliubaldini
f857933748 Launcher: Add search box (#4863)
* Add fuzzy search box to Launcher.

* move func bind to the kv and prefer substring matching (#79)

* move the func bind to the kv

* prefer substr matching

* Remove fuzzy results, rely on substring only.

* Use early return instead of else.

* Add type hint to filter_clients_by_type.

* Activate search on keyboard input.

* Clear search box when filtering by type.

* Update Launcher.py

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

---------

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2025-04-19 23:27:03 +02:00
Jérémie Bolduc
efe2b7c539 Core: Support default value with cache_self1 (#4667)
* add cache_self1_default and tests

* merge the two decorators

* just change the defaults of the wrap lol

* add test for default and default
2025-04-19 17:55:02 +02:00
Fabian Dill
e090153d93 LttP: fix generation if other games are involved (#4901) 2025-04-19 15:44:55 +02:00
Silvris
5088b02bfe Unittests: fix world unittests with unittest module (#4895) 2025-04-19 15:42:20 +02:00
Nicholas Saylor
57a716b57a LTTP: Update to options API (#4134)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-04-18 23:41:38 +02:00
Aaron Wagener
1b51714f3b LTTP: Rip Lttp specific entrance code out of core and use Region helpers (#1960) 2025-04-18 23:34:34 +02:00
ScootyPuffJr1
cb3d35faf9 LttP: Add keydrop locations to location groups (#4465) 2025-04-18 20:50:51 +02:00
Fabian Dill
a0c83b4854 Core: no longer log ID ranges on generate (#4013)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-04-18 20:49:08 +02:00
Fabian Dill
1b3ee0e94f Core: require clients to support overlapping IDs (#4451) 2025-04-18 20:41:09 +02:00
Mysteryem
552a6e7f1c Stardew Valley: Precollect building items in deterministic order (#4883)
#4239 refactored buildings, but introduced iteration of a set when precollecting the building items into start inventory.

The iteration order of sets varies between separate Python processes due to set order being partially based on the hashes of the objects in the set and because Python processes each have a random hash seed by default.
2025-04-18 18:41:46 +02:00
qwint
38bfb1087b Webhost: fix get_seeds api endpoint (#4889) 2025-04-18 18:15:59 +02:00
qwint
2dc55873f0 Webhost: add link to new session page (#4857)
Co-authored-by: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com>
2025-04-18 04:57:41 +02:00
qwint
4b1898bfaf HK: fix docs whitespace (#4885) 2025-04-18 00:57:17 +02:00
Silvris
125bf6f270 Core: Post-KivyMD cleanup 2 and enhancements (#4876)
* Adds a new class allowing TextFields to be resized
* Resizes most CommonClient components to be more in-line with pre-KivyMD
* Change the color of SelectableLabels and TooltipLabels to white
* Fixed ClientTabs not correctly showing the current tab indicator
* The server label now features a (i) icon to indicate that it can be hovered over.
* Changed the default `primary_palette` to `Lightsteelblue` and the default `dynamic_scheme_name` to `VIBRANT`
* Properly set attributes on `KivyJSONToTextParser.TextColors` so that proper typing can be utilized if an individual value is needed
* Fixed some buttons being discolored permanently once pressed
* Sped up the animations of button ripples and tab switching
* Added the ability to insert a new tab to `GameManager.add_client_tab`
* Hovering over the "Command" button in CommonClient will now display the contents of `/help` as a popup (note: this popup can be too large on default height for adequately large /help (SC2 Client), but should always fit fine on fullscreen).
* Fixed invalid sizing of MessageBox errors, and changed their text color to white
2025-04-16 00:09:27 +02:00
Seldom
1873c52aa6 Terraria: 1.4.4 and Calamity support (#3847)
* Terraria integration

* Precollected items for debugging

* Fix item classification

* Golem requires Plantera's Bulb

* Pumpkin Moon requires Dungeon

* Progressive Dungeon

* Reorg, Options.py work

* Items are boss flags

* Removed unused option

* Removed nothing

* Wall, Plantera, and Zenith goals

* Achievements and items

* Fixed The Cavalry and Completely Awesome achievements

* Made "Dead Men Tell No Tales" a grindy achievement

* Some docs, Python 3.8 compat

* docs

* Fix extra item and "Head in the Clouds" being included when achievements are disabled

* Requested changes

* Fix potential thread unsafety, replace Nothing with 50 Silver

* Remove a log

* Corrected heading

* Added incompatible mods list

* In-progress calamity integration

* Terraria events progress

* Rules use events

* Removed an intentional crash I accidentally left in

* Fixed infinite loop

* Moved rules to data file

* Moved item rewards to data file

* Generating from data file

* Fixed broken Mech Boss goal

* Changes Calamity makes to vanilla rules, Calamity final bosses goal

* Added Deerclops, fixed Zenith goal

* Final detailed vanilla pass

* Disable calamity goals

* Typo

* Fixed some reward items not adding to item pool

* In-progress unit test fixes

* Unit test fixes

* `.apworld` compat

* Organized rewards file, made Frog Leg and Fllpper available in vanilla

* Water Walking Boots and Titan Glove rewards

* Add goals to slot data

* Fixed Hammush logic in Post-Mech goal

* Fixed coin rewards

* Updated Terraria docs

* Formatted

* Deathlink in-progress

* Boots of the Hero is grindy

* Fixed zenith goal not placing an item

* Address review

* Gelatin World Tour is grindy

* Difficulty notice

* Switched some achievements' grindiness

* Added "Hey! Listen!" achievement

* Terarria Python 3.8 compat

* Fixed Terraria You and What Army logic

* Calamity minion accessories

* Typo

* Calamity integration

* `deathlink` -> `death_link`

Co-authored-by: Zach Parks <zach@alliware.com>

* Missing `.`

Co-authored-by: Zach Parks <zach@alliware.com>

* Incorrect type annotation

Co-authored-by: Zach Parks <zach@alliware.com>

* `deathlink` -> `death_link` 2

Co-authored-by: Zach Parks <zach@alliware.com>

* Style

Co-authored-by: Zach Parks <zach@alliware.com>

* Markdown style

Co-authored-by: Zach Parks <zach@alliware.com>

* Markdown style 2

Co-authored-by: Zach Parks <zach@alliware.com>

* Address review

* Fix bad merge

* Terraria utility mod recommendations

* Calamity minion armor logic

* ArmorMinions -> Armor Minions, boss rush goal, fixed unplaced item

* Fixed unplaced item

* Started on Terraria 1.4.4

* Crate logic

* getfixedboi, 1.4.4 achievements, shimmer, town slimes, `Rule`, `Condition`, etc

* More clam getfixedboi logic, bar decraft logic, `NotGetfixedboi` -> `Not Getfixedboi`

* Calamity fixes

* Calamity crate ore logic

* Fixed item accessibility not generating in getfixedboi, fixed not generating with incompatible options, fixed grindy function

* Early achievements, separate achievement category options

* Infinity +1 Sword achievement can be location in later goals

* The Frequent Flyer is impossible in Calamity getfixedboi

* Add Enchanted Sword and Starfury for starting inventories

* Don't Dread on Me is redundant in Calamity

* In Calamity getfixedboi, Queen Bee summons enemies who drop Plague Cell Canisters

* Can't use Gelatin Crystal outside Hallow

* You can't get the Terminus without flags

* Typo

* Options difficult warnings

* Robbing the Grave is Hardmode

* Don't reserve an ID for unused Victory item

* Plantera is accessible early in Calamity via Giant Plantera's Bulbs

* Unshuffled Life Crystal and Defender Medal items

* Comment about Midas' Blessing

* Update worlds/terraria/Options.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Remove stray expression

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Review suggestions

* Option naming caps consistency, add Laser Drill, Lunatic Cultist alt reqs, fix Eldritch Soul Artifact, Ceaseless Void reqs Dungeon

* Cal Clone doesn't drop Broken Hero Sword anymore, Laser Drill is weaker in Calamity

Co-authored-by: Seatori <92278897+Seatori@users.noreply.github.com>

* Fix Acid Rain logic

* Fix XB-∞ Hekate failing accessibility checks (by commenting it out bc it doesn't affect logic)

* Hardmode ores being fishable early in Calamity is not a bug anymore

* Mecha Mayhem is inaccessible in getfixedboi

* Update worlds/terraria/Rules.dsv

Co-authored-by: Seafo <92278897+Seatori@users.noreply.github.com>

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: Zach Parks <zach@alliware.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Seatori <92278897+Seatori@users.noreply.github.com>
2025-04-15 15:51:05 +02:00
black-sliver
ec1e113b4c Doc: fix parse_yaml in adding games.md (#4872) 2025-04-13 13:10:36 +02:00
agilbert1412
347efac0cd DLC Quest - Skip two long tests in the main pipeline (#4862)
* - Set up the two long tests to only run when the specific config is active

* Apply Black Sliver's suggestion
2025-04-12 02:41:08 +02:00
Jérémie Bolduc
b7b5bf58aa Stardew Valley: Use classvar_matrix to split tests (#4762)
* Unroll tests for better parallelization

* fix ut test

* self review

* bro it's the second time today I have to commit some garbage to have a github action rerun because messenger fails what is this

* my god can the tests plz pass

* code reviews

* code reviews

* move TestRandomWorlds out of long module
2025-04-12 02:19:17 +02:00
Fabian Dill
a324c97815 Factorio: fix FloatRanges writing effectively nil into the mod (#4846) 2025-04-11 20:52:20 +02:00
Natalie Weizenbaum
f263a0bc91 DS3: Mark a lizard location that was previously not annotated (#4860) 2025-04-10 21:18:49 -04:00
Mysteryem
6a9299018c MLSS: Fix generation error with emblem hunt and no digspots (#4859) 2025-04-10 21:17:28 -04:00
Jérémie Bolduc
ee471a48bd Stardew Valley: Fix some determinism issues with entrance rando when playing with mods (#4812) 2025-04-10 14:34:21 -04:00
qwint
879d7c23b7 HK: Workaround for NamedRange webhost bug (#4819) 2025-04-10 14:18:43 -04:00
massimilianodelliubaldini
934b09238e Docs: Update to adding games.md (#4816) 2025-04-10 13:21:33 -04:00
Carter Hesterman
1fd8e4435e Civ 6: Update setup documentation to account for common pitfalls (#4797) 2025-04-10 13:19:03 -04:00
Aaron Wagener
50fd42d0c2 The Messenger: Add a plando guide (#4719) 2025-04-10 13:13:38 -04:00
Aaron Wagener
399958c881 The Messenger: Add an FAQ (#4718) 2025-04-10 13:03:05 -04:00
qwint
78c93d7e39 Docs: Add FAQ section for corrupted metadata debugging (#4705)
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-04-10 13:00:48 -04:00
qwint
e3b8a60584 Webhost: Fix Sphere Tracker crashing on item links (#4855) 2025-04-10 03:29:11 +02:00
Star Rauchenberger
b7263edfd0 Lingo: Removed unnecessary "global" keywords (#4854) 2025-04-10 01:41:07 +02:00
Ziktofel
1ee749b352 SC2 Client: Fix missing mission tooltip after KivyMD switch (#4827) 2025-04-09 22:21:16 +02:00
Alchav
f93734f9e3 Pokemon Red and Blue: PC Item Fix (#4835)
* Pokemon Red and Blue PC Item fix

* Respect non_local_items for PC Item

* prefer exclude if also in priority locations

---------

Co-authored-by: alchav <alchav@jalchavware.com>
2025-04-09 13:20:56 -04:00
Fabian Dill
e211dfa1c2 WebHost: use JS to refresh waitSeed if scripting is enabled (#4843) 2025-04-09 07:43:28 +02:00
Zach 'Phar' Parks
0f7deb1d2a WebHost: Remove styleController.js and replace functionality with HTML/CSS. (#4852)
* ensure footer stays at bottom of page without JS

* Remove some other usages.
2025-04-08 23:46:46 -05:00
black-sliver
f2cb16a5be CI: update action ubuntu build runners to 22.04 (#4847) 2025-04-09 01:38:46 +02:00
Mysteryem
98477e27aa Core: Speed up fill_restrictive item_pool pop loop (#4536)
* Core: Speed up fill_restrictive item_pool pop loop

Items from `reachable_items` are placed in last-in-first-out order, so
items being placed will be towards the end of `item_pool`, but the
iteration to find the item was iterating from the start of `item_pool`.

Now also uses `del` instead of `.pop()` for an additional, tiny,
performance increase.

It is unlikely for there to be a noticeable difference in most cases.
Only generating with many worlds with a high percentage of progression
items and fast access rules is likely to see a difference with this
change.

--skip_output generation of 400 template A Hat in Time yamls with
progression balancing disabled goes from 76s to 43s (43% reduction) for
me with this patch. This placed 43200 progression items out of 89974
items total (48% progression items).

* Fix comment typo

"be" was missing.

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-04-08 23:57:31 +02:00
threeandthreee
4149db1a01 LADX: Stop using Utils.get_options (#4818)
* init

* use get

* Update LinksAwakeningClient.py

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

* Update LinksAwakeningClient.py

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2025-04-08 23:54:50 +02:00
Jérémie Bolduc
9ac921380f Stardew Valley: Refactor buildings to use content packs (#4239)
* create building data object and rename ItemSource to Source to be more generic

# Conflicts:
#	worlds/stardew_valley/content/game_content.py

# Conflicts:
#	worlds/stardew_valley/data/artisan.py
#	worlds/stardew_valley/data/game_item.py
#	worlds/stardew_valley/data/harvest.py
#	worlds/stardew_valley/data/shop.py

* remove compound sources, replace by other requirements which already handle this usecase

* add coops to content packs

* add building progression in game features

* add shippping bin to starting building; remove has_house

* replace config check with feature

* add other buildings in content packs

* not passing

* tests passes, unbelievable

* use newly create methods more

* use new assets to ease readability

* self review

* fix flake8 maybe

* properly split rule for mapping cave systems

* fix tractor garage name

* self review

* add upgrade_from to farm house buldings

* don't override building name variable in logic

* remove has_group from buildings

* mark some items easy in grinding logic so blueprints buildings can be in more early spheres

* move stuff around to maybe avoid future conflicts cuz I have like 10 PRs opened right now

* remove price_multiplier, turns out it's unused during generation

* disable shop source for mapping cave systems

* bunch of code review changes

* add petbowl and farmhouse to autobuilding

* set min easy items to 300

* fix farm type
2025-04-08 12:37:45 -04:00
CookieCat
286e24629f AHIT: Add start_inventory_from_pool and get_filler_item_name (#4798)
* Update __init__.py

* Update Options.py
2025-04-08 12:26:30 -04:00
Emily
ab2efc0c5c kvui: actually fix [u] and [/u] appearing in copied hints (#4842) 2025-04-08 18:06:19 +02:00
NewSoupVi
60d6078e1f Wind Waker: Don't collect nonprogression #4826 2025-04-07 23:17:25 +02:00
black-sliver
f94492b2d3 CI: ignore F824 (#4790)
This is an added check in flake8 that does not really fit the goal
of the github action and currently throws a lot of errors.
2025-04-07 00:39:25 +02:00
Star Rauchenberger
f03bb61747 Lingo: Add "shuffle_postgame" flag to slot data (#4825)
This allows the tracker to see whether postgame is shuffled in the player's world, and if it's not, allows it to hide locations/paintings accordingly.
2025-04-07 00:02:34 +02:00
Silvris
dc4e8bae98 Core: post-KivyMD cleanup (#4815)
* Removed now unused imports from Launcher
* Moved ImageIcon and ImageButton to use ApAsyncImage for compatibility with apworlds
* Adjusted image size in the Launcher from 40x40 to 48x48. This is already larger than the size in previous versions, and a docs update is soon to follow.
* Expose `dynamic_scheme_contrast` to user.kv, allowing users to set high contrast.
* ScrollBox's default scroll_type was set to only content, so the scrollbar in Launcher was nonfunctional.
* Adjusted the spacing of the title of a component when a description is present to be closer to the center.
* Launcher now scrolls to the top automatically when changing between filters
2025-04-06 20:11:16 +02:00
Star Rauchenberger
ac26f8be8b Lingo: Mark some items as ProgUseful (#4822) 2025-04-06 19:44:33 +02:00
black-sliver
8c79499573 SoE: remove use of deprecated Utils.get_options() (#4821) 2025-04-06 17:00:14 +02:00
black-sliver
63fbcc5fc8 WebHost: custom proc title for Generator and MultiHoster (#4310)
* WebHost: custom proctitle for Generator and MultiHoster

* Update setproctitle to 1.3.5
2025-04-06 13:50:24 +02:00
Fabian Dill
cad217af19 Core: update cert file daily in customserver.py (#4454) 2025-04-06 05:31:14 +02:00
Exempt-Medic
a6ad4a8293 Docs: Remove false claim that rules can be done in generate_basic (#4809) 2025-04-05 13:51:22 -04:00
Silvris
503999cb32 Core: KivyMD and Launcher overhaul (#3934)
Shifts the contents of `kvui.py`, and thus all CommonClient-based clients as well as Launcher, to using KivyMD. KivyMD is an extension for Kivy that is almost fully compatible with pre-existing Kivy components, while providing Material Design support for theming and overall visual design as well as useful pre-existing built in components such as Snackbars, Tooltips, and a built-in File Manager (not currently being used).

As a part of this shift, the launcher was completely overhauled, adding the ability to filter the list of components down to each type of component, the ability to define favorite components and filter to them, and add shortcuts for launcher components to the desktop. An optional description field was added to Component for display within the new launcher.

The theme (Light/Dark) and primary palette have also been exposed to users via client/user.kv.
2025-04-05 18:46:24 +02:00
threeandthreee
c2d8f2443e LADX: more tracker support (#4355)
* init

* oops
2025-04-05 18:39:31 +02:00
Ishigh1
4571ed7e2f Core: Made want_reply follow the specs in the docs #4750 2025-04-05 18:35:00 +02:00
Exempt-Medic
ef5cbd3ba3 Adventure: Set Victory Condition Earlier (#4810) 2025-04-05 12:30:08 -04:00
Aaron Wagener
5c162bd7ce Core: add an is_event property to Item to match the one on Location (#3401) 2025-04-05 18:07:06 +02:00
NewSoupVi
7bdaaa25c1 Core: Prevent worlds from using LogicMixin incorrectly (having class variables without an init_mixin) (#3974)
* Core: Prevent people from using LogicMixin incorrectly

There's a world that ran into some issues because it defined its custom LogicMixin variables at the class level.

This caused "instance bleed" when new CollectionState objects were created.

I don't think there is ever a reason to have a non-function class variable on LogicMixin without also having `init_mixin`, so this asserts that this is the case.

Tested:
Doesn't fail any current worlds
Correctly fails the world in question

Also, not gonna call out that world because it was literally my fault for explaining it to them wrong :D

* Verbose af

* Update AutoWorld.py
2025-04-05 18:06:30 +02:00
NewSoupVi
9a5a02b654 MultiServer Extend datastore "update" operation to work on lists as well, acting as a pseudo "set union". #4666 2025-04-05 18:05:58 +02:00
Mysteryem
4fea6b6e9b Core: Remove Location.__hash__ (#4274)
`Location` does not override `__eq__` so should not override `__hash__`.

With this patch, this makes operations on sets of locations slightly
faster because they will use `object.__hash__` rather than
`Location.__hash__`.

`object.__hash__` is about 4 to 5 times faster than `Location.__hash__`
for me. Generation often uses sets of locations, so this slightly speeds
up generation.

The only place I could find that was hashing locations directly was
`WitnessLocationHint.__hash__`, but it has implemented a matching
`__eq__`, so is fine.

For security reasons, Python randomizes its hash seed each time it is
started, so the result of the `hash()` function is nondeterministic and
can't have been used by worlds for anything that needed to be
deterministic and can't have been used to compare information hashed at
generation time to information hashed by a client.
2025-04-05 17:53:59 +02:00
Mysteryem
bd8b8822ac Core: Pass maximum exploration states in distribute_items_restrictive (#4535)
The base state passed to fill_restrictive should be as maximal as
possible otherwise fill_restrictive has to repeatedly re-sweep and
collect from advancement locations that were reachable from before
fill_restrictive has placed a single item.

This is not added within fill_restrictive itself because it is common
for fills to be performed using a partial 'all_state', which is already
a maximum exploration state.

With --skip_output generation of every template yaml, except FF, KH
and Shivers, this prevented repeatedly re-sweeping 576 advancement
locations in every sweep within progression fill, reducing the
generation time from 124s to 113s for me (8.8% reduction, averaged over
5 generations each).
2025-04-05 17:50:19 +02:00
NewSoupVi
0a44c3ec49 The Witness: Move the Easter Egg Hunt option group lower so that the tooltip isn't cut off (#4789) 2025-04-05 17:48:18 +02:00
NewSoupVi
3262984386 The Witness: Option tooltip clarifications (#4807)
* Missing colon

* Clarify Panel Hunt

* Unnecessary line break

* that wasn't meant to be in here
2025-04-05 17:47:16 +02:00
LiquidCat64
180265c8f4 CVCotM: Fix DeathLinks sent by a different instance of the same slot not being received. (#4726)
* Fix same-slot-different-player DeathLinks not being received.

* A few more comments.
2025-04-05 10:27:51 -04:00
PinkSwitch
a9b4d33cd2 Yoshi's Island: Fix Piece of Luigi not goaling until reset (#4709) 2025-04-05 16:07:37 +02:00
Mysteryem
5dfb9b28f7 Core: Improve iteration speed of Region.Register objects (#4583)
Without implementing __iter__ directly, calling iter() on a
Region.Register on Python 3.12 would return a new generator implemented
as follows:
```py
        def __iter__(self) -> int:
            i = 0
            try:
                while True:
                    v = self[i]
                    yield v
                    i += 1
            except IndexError:
                return None
```
This was determined by disassembling the returned generator with
dis.dis() and then constructing a function that disassembles into the
same bytecode.

The iterator returned by `iter(self._list)` is faster than this
generator, so using it slightly improves generation performance on
average.

Iteration of Region.Register objects is used a lot in
`CollectionState.update_reachable_regions` in both of the private
_update methods that get called. The performance gain here will vary
depending on how many regions a world has and how many exits those
regions have on average.

For a game like Blasphemous, with a lot of regions and exits, generation
of 10 template Blasphemous yamls with `--skip_output --seed 1` and
progression balancing disabled went from 19.0s to 16.4s (14.2% reduction
in generation duration).
2025-04-05 15:59:39 +02:00
Benjamin S Wolf
ec75793ac3 Core: Add spoiler-only output mode (#4059)
* Core: Add spoiler-only output mode

* spoiler-only exceptions

* Move new errors to mystery_argparse
2025-04-05 09:50:52 -04:00
CodeGorilla
cd4da36863 GER: Only consider usable exits when calculating dead-ends (#4701)
* Only consider usable exits when calculating whether or not a region is a dead-end

* Update EntranceLookup unit tests

* Add new dead-end test

* Add additional explanation to the new test

* minor formatting tweak

based on review feedback

---------

Co-authored-by: CodeGorilla <3672561+Ars-Ignis@users.noreply.github.com>
2025-04-05 09:21:38 -04:00
Nocallia
1749e22569 Stardew: Fix minor grammar issues in Options (#4800) 2025-04-05 08:20:51 -04:00
axe-y
0cce88cfbc DLC Quest: Fix more items than location with non existing start inventory (#4735)
* DLC Quest Bug Fix
Start inventory item that do not exist in the present world do not make more trap item to appear anymore

* Update worlds/dlcquest/Items.py

Co-authored-by: Mysteryem <Mysteryem@users.noreply.github.com>

* DLC Quest Bug Fix
did the recommendation of Mysteryem and made the item not exist in the pool of item created

* DLC Quest Bug Fix
did the recommendation of agilbert1412 and made a check by name instead of item to itemData

* DLC Quest Bug Fix
overcook failed test

* DLC Quest Bug Fix
re-type correctly a type hint

---------

Co-authored-by: Mysteryem <Mysteryem@users.noreply.github.com>
2025-04-05 08:19:54 -04:00
black-sliver
61e83a300b Clients: stop updating datapackage in persistent_storage (#4799)
Still uses things that are in there but stops writing to it.
2025-04-05 11:51:01 +02:00
Exempt-Medic
136a13aac7 Docs: Include that DeathLink cause can be an empty string (#4729) 2025-04-04 22:39:18 -04:00
massimilianodelliubaldini
2c90db9ae7 Docs: Additional detail and organization to adding games.md (#4805)
* Additional detail and organization to adding games.md

* Minor fixes.

* Update docs/adding games.md

Co-authored-by: qwint <qwint.42@gmail.com>

* Code review updates.

* More updates.

* Client icon blurb.

* Update docs/adding games.md

Co-authored-by: qwint <qwint.42@gmail.com>

* Revert one line.

* Filler item name blurb.

* Updates for Violet.

* Reorganize client expectations.

* Missed a line delete.

* Doctor's orders

---------

Co-authored-by: qwint <qwint.42@gmail.com>
2025-04-05 04:18:47 +02:00
Richard Snider
507e051a5a Core: Handle integer arguments in player names gracefully (#4151) 2025-04-05 03:36:20 +02:00
Scipio Wright
b5bf9ed1d7 TUNIC: Error message in the spot that UT errors at if you have an old APWorld #4788
Schnice and Shrimple
2025-04-05 00:53:13 +02:00
Fabian Dill
215eb7e473 core: increment version (#4808) 2025-04-04 23:25:37 +02:00
qwint
f42233699a Core: make accessibility_corrections only state.remove if the location was collected 2025-04-04 23:20:45 +02:00
massimilianodelliubaldini
1bec68df4d WebHost: Standardize some 404 redirects (#4642) 2025-04-04 23:11:45 +02:00
CodeGorilla
d8576e72eb Pokemon Red/Blue: Set allow_partial_entrances to true when building a state for ER #4802
Co-authored-by: CodeGorilla <3672561+Ars-Ignis@users.noreply.github.com>
2025-04-04 10:48:47 +02:00
Fabian Dill
7265468e8d kvui: fix [u] and [/u] appearing in copied hints (#4794) 2025-04-03 09:22:02 +02:00
Fabian Dill
d07f36dedd Core: increment version (#4787) 2025-04-02 05:35:39 +02:00
Scipio Wright
364a1b71ec TUNIC: Note Death Link and Trap Link in-game toggles on Game Info page (#4741)
* Note death link and trap link in game info page

* Update worlds/tunic/docs/en_TUNIC.md

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Turn it into a bulleted list
2025-04-01 19:55:19 -04:00
Sanjay Govind
daee6d210f CommonClient: don't update ui hints if there is no ui (#4791) 2025-04-02 01:54:27 +02:00
Bryce Wilson
96be0071e6 Pokemon Emerald: Move recent change to new version (#4793) 2025-04-02 00:50:39 +02:00
threeandthreee
ff8e1dfb47 Launcher: Remove an unnecessary global (#4785) 2025-04-01 21:28:59 +02:00
309 changed files with 5520 additions and 13616 deletions

View File

@@ -65,7 +65,7 @@ jobs:
continue-on-error: false continue-on-error: false
if: env.diff != '' && matrix.task == 'flake8' if: env.diff != '' && matrix.task == 'flake8'
run: | run: |
flake8 --count --select=E9,F63,F7,F82 --show-source --statistics ${{ env.diff }} flake8 --count --select=E9,F63,F7,F82 --ignore F824 --show-source --statistics ${{ env.diff }}
- name: "flake8: Lint modified files" - name: "flake8: Lint modified files"
continue-on-error: true continue-on-error: true

View File

@@ -99,8 +99,8 @@ jobs:
if-no-files-found: error if-no-files-found: error
retention-days: 7 # keep for 7 days, should be enough retention-days: 7 # keep for 7 days, should be enough
build-ubuntu2004: build-ubuntu2204:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
steps: steps:
# - copy code below to release.yml - # - copy code below to release.yml -
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@@ -29,8 +29,8 @@ jobs:
# build-release-windows: # this is done by hand because of signing # build-release-windows: # this is done by hand because of signing
# build-release-macos: # LF volunteer # build-release-macos: # LF volunteer
build-release-ubuntu2004: build-release-ubuntu2204:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
steps: steps:
- name: Set env - name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV

View File

@@ -223,7 +223,7 @@ class MultiWorld():
AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints} AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
for option_key in all_keys: for option_key in all_keys:
option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. " option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. "
f"Please use `self.options.{option_key}` instead.") f"Please use `self.options.{option_key}` instead.", True)
option.update(getattr(args, option_key, {})) option.update(getattr(args, option_key, {}))
setattr(self, option_key, option) setattr(self, option_key, option)
@@ -616,7 +616,7 @@ class MultiWorld():
locations: Set[Location] = set() locations: Set[Location] = set()
events: Set[Location] = set() events: Set[Location] = set()
for location in self.get_filled_locations(): for location in self.get_filled_locations():
if type(location.item.code) is int: if type(location.item.code) is int and type(location.address) is int:
locations.add(location) locations.add(location)
else: else:
events.add(location) events.add(location)
@@ -1022,9 +1022,6 @@ class Entrance:
connected_region: Optional[Region] = None connected_region: Optional[Region] = None
randomization_group: int randomization_group: int
randomization_type: EntranceType randomization_type: EntranceType
# LttP specific, TODO: should make a LttPEntrance
addresses = None
target = None
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None, def __init__(self, player: int, name: str = "", parent: Optional[Region] = None,
randomization_group: int = 0, randomization_type: EntranceType = EntranceType.ONE_WAY) -> None: randomization_group: int = 0, randomization_type: EntranceType = EntranceType.ONE_WAY) -> None:
@@ -1043,10 +1040,8 @@ class Entrance:
return False return False
def connect(self, region: Region, addresses: Any = None, target: Any = None) -> None: def connect(self, region: Region) -> None:
self.connected_region = region self.connected_region = region
self.target = target
self.addresses = addresses
region.entrances.append(self) region.entrances.append(self)
def is_valid_source_transition(self, er_state: "ERPlacementState") -> bool: def is_valid_source_transition(self, er_state: "ERPlacementState") -> bool:
@@ -1106,6 +1101,9 @@ class Region:
def __len__(self) -> int: def __len__(self) -> int:
return self._list.__len__() return self._list.__len__()
def __iter__(self):
return iter(self._list)
# This seems to not be needed, but that's a bit suspicious. # This seems to not be needed, but that's a bit suspicious.
# def __del__(self): # def __del__(self):
# self.clear() # self.clear()
@@ -1200,6 +1198,48 @@ class Region:
for location, address in locations.items(): for location, address in locations.items():
self.locations.append(location_type(self.player, location, address, self)) self.locations.append(location_type(self.player, location, address, self))
def add_event(
self,
location_name: str,
item_name: str | None = None,
rule: Callable[[CollectionState], bool] | None = None,
location_type: type[Location] | None = None,
item_type: type[Item] | None = None,
show_in_spoiler: bool = True,
) -> Item:
"""
Adds an event location/item pair to the region.
:param location_name: Name for the event location.
:param item_name: Name for the event item. If not provided, defaults to location_name.
:param rule: Callable to determine access for this event location within its region.
:param location_type: Location class to create the event location with. Defaults to BaseClasses.Location.
:param item_type: Item class to create the event item with. Defaults to BaseClasses.Item.
:param show_in_spoiler: Will be passed along to the created event Location's show_in_spoiler attribute.
:return: The created Event Item
"""
if location_type is None:
location_type = Location
if item_name is None:
item_name = location_name
if item_type is None:
item_type = Item
event_location = location_type(self.player, location_name, None, self)
event_location.show_in_spoiler = show_in_spoiler
if rule is not None:
event_location.access_rule = rule
event_item = item_type(item_name, ItemClassification.progression, None, self.player)
event_location.place_locked_item(event_item)
self.locations.append(event_location)
return event_item
def connect(self, connecting_region: Region, name: Optional[str] = None, def connect(self, connecting_region: Region, name: Optional[str] = None,
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance: rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
""" """
@@ -1310,9 +1350,6 @@ class Location:
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})' return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
def __hash__(self):
return hash((self.name, self.player))
def __lt__(self, other: Location): def __lt__(self, other: Location):
return (self.player, self.name) < (other.player, other.name) return (self.player, self.name) < (other.player, other.name)
@@ -1416,6 +1453,10 @@ class Item:
def flags(self) -> int: def flags(self) -> int:
return self.classification.as_flag() return self.classification.as_flag()
@property
def is_event(self) -> bool:
return self.code is None
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
if not isinstance(other, Item): if not isinstance(other, Item):
return NotImplemented return NotImplemented

View File

@@ -413,7 +413,8 @@ class CommonContext:
await self.server.socket.close() await self.server.socket.close()
if self.server_task is not None: if self.server_task is not None:
await self.server_task await self.server_task
self.ui.update_hints() if self.ui:
self.ui.update_hints()
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None: async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
""" `msgs` JSON serializable """ """ `msgs` JSON serializable """
@@ -624,9 +625,6 @@ class CommonContext:
def consume_network_data_package(self, data_package: dict): def consume_network_data_package(self, data_package: dict):
self.update_data_package(data_package) self.update_data_package(data_package)
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
current_cache.update(data_package["games"])
Utils.persistent_store("datapackage", "games", current_cache)
logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}") logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}")
for game, game_data in data_package["games"].items(): for game, game_data in data_package["games"].items():
Utils.store_data_package_for_checksum(game, game_data) Utils.store_data_package_for_checksum(game, game_data)

21
Fill.py
View File

@@ -75,9 +75,11 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
items_to_place.append(reachable_items[next_player].pop()) items_to_place.append(reachable_items[next_player].pop())
for item in items_to_place: for item in items_to_place:
for p, pool_item in enumerate(item_pool): # The items added into `reachable_items` are placed starting from the end of each deque in
# `reachable_items`, so the items being placed are more likely to be found towards the end of `item_pool`.
for p, pool_item in enumerate(reversed(item_pool), start=1):
if pool_item is item: if pool_item is item:
item_pool.pop(p) del item_pool[-p]
break break
maximum_exploration_state = sweep_from_pool( maximum_exploration_state = sweep_from_pool(
@@ -348,10 +350,10 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
if (location.item is not None and location.item.advancement and location.address is not None and not if (location.item is not None and location.item.advancement and location.address is not None and not
location.locked and location.item.player not in minimal_players): location.locked and location.item.player not in minimal_players):
pool.append(location.item) pool.append(location.item)
state.remove(location.item)
location.item = None location.item = None
if location in state.advancements: if location in state.advancements:
state.advancements.remove(location) state.advancements.remove(location)
state.remove(location.item)
locations.append(location) locations.append(location)
if pool and locations: if pool and locations:
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY) locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
@@ -500,13 +502,15 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if prioritylocations: if prioritylocations:
# "priority fill" # "priority fill"
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, maximum_exploration_state = sweep_from_pool(multiworld.state)
fill_restrictive(multiworld, maximum_exploration_state, prioritylocations, progitempool,
single_player_placement=single_player, swap=False, on_place=mark_for_locking, single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority", one_item_per_player=True, allow_partial=True) name="Priority", one_item_per_player=True, allow_partial=True)
if prioritylocations: if prioritylocations:
# retry with one_item_per_player off because some priority fills can fail to fill with that optimization # retry with one_item_per_player off because some priority fills can fail to fill with that optimization
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, maximum_exploration_state = sweep_from_pool(multiworld.state)
fill_restrictive(multiworld, maximum_exploration_state, prioritylocations, progitempool,
single_player_placement=single_player, swap=False, on_place=mark_for_locking, single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority Retry", one_item_per_player=False) name="Priority Retry", one_item_per_player=False)
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
@@ -514,14 +518,15 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if progitempool: if progitempool:
# "advancement/progression fill" # "advancement/progression fill"
maximum_exploration_state = sweep_from_pool(multiworld.state)
if panic_method == "swap": if panic_method == "swap":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True, fill_restrictive(multiworld, maximum_exploration_state, defaultlocations, progitempool, swap=True,
name="Progression", single_player_placement=single_player) name="Progression", single_player_placement=single_player)
elif panic_method == "raise": elif panic_method == "raise":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False, fill_restrictive(multiworld, maximum_exploration_state, defaultlocations, progitempool, swap=False,
name="Progression", single_player_placement=single_player) name="Progression", single_player_placement=single_player)
elif panic_method == "start_inventory": elif panic_method == "start_inventory":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False, fill_restrictive(multiworld, maximum_exploration_state, defaultlocations, progitempool, swap=False,
allow_partial=True, name="Progression", single_player_placement=single_player) allow_partial=True, name="Progression", single_player_placement=single_player)
if progitempool: if progitempool:
for item in progitempool: for item in progitempool:

View File

@@ -54,12 +54,22 @@ def mystery_argparse():
parser.add_argument("--skip_output", action="store_true", parser.add_argument("--skip_output", action="store_true",
help="Skips generation assertion and output stages and skips multidata and spoiler output. " help="Skips generation assertion and output stages and skips multidata and spoiler output. "
"Intended for debugging and testing purposes.") "Intended for debugging and testing purposes.")
parser.add_argument("--spoiler_only", action="store_true",
help="Skips generation assertion and multidata, outputting only a spoiler log. "
"Intended for debugging and testing purposes.")
args = parser.parse_args() args = parser.parse_args()
if args.skip_output and args.spoiler_only:
parser.error("Cannot mix --skip_output and --spoiler_only")
elif args.spoiler == 0 and args.spoiler_only:
parser.error("Cannot use --spoiler_only when --spoiler=0. Use --skip_output or set --spoiler to a different value")
if not os.path.isabs(args.weights_file_path): if not os.path.isabs(args.weights_file_path):
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path) args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
if not os.path.isabs(args.meta_file_path): if not os.path.isabs(args.meta_file_path):
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path) args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando) args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
return args return args
@@ -108,6 +118,8 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
raise Exception("Cannot mix --sameoptions with --meta") raise Exception("Cannot mix --sameoptions with --meta")
else: else:
meta_weights = None meta_weights = None
player_id = 1 player_id = 1
player_files = {} player_files = {}
for file in os.scandir(args.player_files_path): for file in os.scandir(args.player_files_path):
@@ -164,6 +176,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
erargs.outputpath = args.outputpath erargs.outputpath = args.outputpath
erargs.skip_prog_balancing = args.skip_prog_balancing erargs.skip_prog_balancing = args.skip_prog_balancing
erargs.skip_output = args.skip_output erargs.skip_output = args.skip_output
erargs.spoiler_only = args.spoiler_only
erargs.name = {} erargs.name = {}
erargs.csv_output = args.csv_output erargs.csv_output = args.csv_output
@@ -279,22 +292,30 @@ def get_choice(option, root, value=None) -> Any:
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.") raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
class SafeDict(dict): class SafeFormatter(string.Formatter):
def __missing__(self, key): def get_value(self, key, args, kwargs):
return '{' + key + '}' if isinstance(key, int):
if key < len(args):
return args[key]
else:
return "{" + str(key) + "}"
else:
return kwargs.get(key, "{" + key + "}")
def handle_name(name: str, player: int, name_counter: Counter): def handle_name(name: str, player: int, name_counter: Counter):
name_counter[name.lower()] += 1 name_counter[name.lower()] += 1
number = name_counter[name.lower()] number = name_counter[name.lower()]
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")]) new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=number,
NUMBER=(number if number > 1 else ''), new_name = SafeFormatter().vformat(new_name, (), {"number": number,
player=player, "NUMBER": (number if number > 1 else ''),
PLAYER=(player if player > 1 else ''))) "player": player,
"PLAYER": (player if player > 1 else '')})
# Run .strip twice for edge case where after the initial .slice new_name has a leading whitespace. # Run .strip twice for edge case where after the initial .slice new_name has a leading whitespace.
# Could cause issues for some clients that cannot handle the additional whitespace. # Could cause issues for some clients that cannot handle the additional whitespace.
new_name = new_name.strip()[:16].strip() new_name = new_name.strip()[:16].strip()
if new_name == "Archipelago": if new_name == "Archipelago":
raise Exception(f"You cannot name yourself \"{new_name}\"") raise Exception(f"You cannot name yourself \"{new_name}\"")
return new_name return new_name
@@ -435,6 +456,14 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses): def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
"""
Roll options from specified weights, usually originating from a .yaml options file.
Important note:
The same weights dict is shared between all slots using the same yaml (e.g. generic weights file for filler slots).
This means it should never be modified without making a deepcopy first.
"""
from worlds import AutoWorldRegister from worlds import AutoWorldRegister
if "linked_options" in weights: if "linked_options" in weights:

View File

@@ -1,16 +1,14 @@
""" """
Archipelago launcher for bundled app. Archipelago Launcher
* if run with APBP as argument, launch corresponding client. * If run with a patch file as argument, launch corresponding client with the patch file as an argument.
* if run with executable as argument, run it passing argv[2:] as arguments * If run with component name as argument, run it passing argv[2:] as arguments.
* if run without arguments, open launcher GUI * If run without arguments or unknown arguments, open launcher GUI.
Scroll down to components= to add components to the launcher as well as setup.py Additional components can be added to worlds.LauncherComponents.components.
""" """
import argparse import argparse
import itertools
import logging import logging
import multiprocessing import multiprocessing
import shlex import shlex
@@ -20,10 +18,11 @@ import urllib.parse
import webbrowser import webbrowser
from os.path import isfile from os.path import isfile
from shutil import which from shutil import which
from typing import Callable, Optional, Sequence, Tuple, Union from typing import Callable, Optional, Sequence, Tuple, Union, Any
if __name__ == "__main__": if __name__ == "__main__":
import ModuleUpdate import ModuleUpdate
ModuleUpdate.update() ModuleUpdate.update()
import settings import settings
@@ -105,7 +104,8 @@ components.extend([
Component("Generate Template Options", func=generate_yamls), Component("Generate Template Options", func=generate_yamls),
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")), Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")), Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
Component("Unrated/18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")), Component("Unrated/18+ Discord Server", icon="discord",
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
Component("Browse Files", func=browse_files), Component("Browse Files", func=browse_files),
]) ])
@@ -114,7 +114,7 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
url = urllib.parse.urlparse(path) url = urllib.parse.urlparse(path)
queries = urllib.parse.parse_qs(url.query) queries = urllib.parse.parse_qs(url.query)
launch_args = (path, *launch_args) launch_args = (path, *launch_args)
client_component = None client_component = []
text_client_component = None text_client_component = None
if "game" in queries: if "game" in queries:
game = queries["game"][0] game = queries["game"][0]
@@ -122,49 +122,40 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
game = "Archipelago" game = "Archipelago"
for component in components: for component in components:
if component.supports_uri and component.game_name == game: if component.supports_uri and component.game_name == game:
client_component = component client_component.append(component)
elif component.display_name == "Text Client": elif component.display_name == "Text Client":
text_client_component = component text_client_component = component
if client_component is None: from kvui import MDButton, MDButtonText
from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogContentContainer, MDDialogSupportingText
from kivymd.uix.divider import MDDivider
if not client_component:
run_component(text_client_component, *launch_args) run_component(text_client_component, *launch_args)
return return
else:
popup_text = MDDialogSupportingText(text="Select client to open and connect with.")
component_buttons = [MDDivider()]
for component in [text_client_component, *client_component]:
component_buttons.append(MDButton(
MDButtonText(text=component.display_name),
on_release=lambda *args, comp=component: run_component(comp, *launch_args),
style="text"
))
component_buttons.append(MDDivider())
from kvui import App, Button, BoxLayout, Label, Window MDDialog(
# Headline
MDDialogHeadlineText(text="Connect to Multiworld"),
# Text
popup_text,
# Content
MDDialogContentContainer(
*component_buttons,
orientation="vertical"
),
class Popup(App): ).open()
def __init__(self):
self.title = "Connect to Multiworld"
self.icon = r"data/icon.png"
super().__init__()
def build(self):
layout = BoxLayout(orientation="vertical")
layout.add_widget(Label(text="Select client to open and connect with."))
button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
text_client_button = Button(
text=text_client_component.display_name,
on_release=lambda *args: run_component(text_client_component, *launch_args)
)
button_row.add_widget(text_client_button)
game_client_button = Button(
text=client_component.display_name,
on_release=lambda *args: run_component(client_component, *launch_args)
)
button_row.add_widget(game_client_button)
layout.add_widget(button_row)
return layout
def _stop(self, *largs):
# see run_gui Launcher _stop comment for details
self.root_window.close()
super()._stop(*largs)
Popup().run()
def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]: def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
@@ -220,100 +211,189 @@ def launch(exe, in_terminal=False):
subprocess.Popen(exe) subprocess.Popen(exe)
def create_shortcut(button: Any, component: Component) -> None:
from pyshortcuts import make_shortcut
script = sys.argv[0]
wkdir = Utils.local_path()
script = f"{script} \"{component.display_name}\""
make_shortcut(script, name=f"Archipelago {component.display_name}", icon=local_path("data", "icon.ico"),
startmenu=False, terminal=False, working_dir=wkdir)
button.menu.dismiss()
refresh_components: Optional[Callable[[], None]] = None refresh_components: Optional[Callable[[], None]] = None
def run_gui(): def run_gui(path: str, args: Any) -> None:
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, ScrollBox)
from kivy.properties import ObjectProperty
from kivy.core.window import Window from kivy.core.window import Window
from kivy.uix.relativelayout import RelativeLayout from kivy.metrics import dp
from kivymd.uix.button import MDIconButton, MDButton
from kivymd.uix.card import MDCard
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText
from kivymd.uix.textfield import MDTextField
class Launcher(App): from kivy.lang.builder import Builder
class LauncherCard(MDCard):
component: Component | None
image: str
context_button: MDIconButton = ObjectProperty(None)
def __init__(self, *args, component: Component | None = None, image_path: str = "", **kwargs):
self.component = component
self.image = image_path
super().__init__(args, kwargs)
class Launcher(ThemedApp):
base_title: str = "Archipelago Launcher" base_title: str = "Archipelago Launcher"
container: ContainerLayout top_screen: MDFloatLayout = ObjectProperty(None)
grid: GridLayout navigation: MDGridLayout = ObjectProperty(None)
_tool_layout: Optional[ScrollBox] = None grid: MDGridLayout = ObjectProperty(None)
_client_layout: Optional[ScrollBox] = None button_layout: ScrollBox = ObjectProperty(None)
search_box: MDTextField = ObjectProperty(None)
cards: list[LauncherCard]
current_filter: Sequence[str | Type] | None
def __init__(self, ctx=None): def __init__(self, ctx=None, path=None, args=None):
self.title = self.base_title + " " + Utils.__version__ self.title = self.base_title + " " + Utils.__version__
self.ctx = ctx self.ctx = ctx
self.icon = r"data/icon.png" self.icon = r"data/icon.png"
self.favorites = []
self.launch_uri = path
self.launch_args = args
self.cards = []
self.current_filter = (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
persistent = Utils.persistent_load()
if "launcher" in persistent:
if "favorites" in persistent["launcher"]:
self.favorites.extend(persistent["launcher"]["favorites"])
if "filter" in persistent["launcher"]:
if persistent["launcher"]["filter"]:
filters = []
for filter in persistent["launcher"]["filter"].split(", "):
if filter == "favorites":
filters.append(filter)
else:
filters.append(Type[filter])
self.current_filter = filters
super().__init__() super().__init__()
def _refresh_components(self) -> None: def set_favorite(self, caller):
if caller.component.display_name in self.favorites:
self.favorites.remove(caller.component.display_name)
caller.icon = "star-outline"
else:
self.favorites.append(caller.component.display_name)
caller.icon = "star"
def build_button(component: Component) -> Widget: def build_card(self, component: Component) -> LauncherCard:
"""
Builds a card widget for a given component.
:param component: The component associated with the button.
:return: The created Card Widget.
""" """
Builds a button widget for a given component. button_card = LauncherCard(component=component,
image_path=icon_paths[component.icon])
Args: def open_menu(caller):
component (Component): The component associated with the button. caller.menu.open()
Returns: menu_items = [
None. The button is added to the parent grid layout. {
"text": "Add shortcut on desktop",
"leading_icon": "laptop",
"on_release": lambda: create_shortcut(button_card.context_button, component)
}
]
button_card.context_button.menu = MDDropdownMenu(caller=button_card.context_button, items=menu_items)
button_card.context_button.bind(on_release=open_menu)
""" return button_card
button = Button(text=component.display_name, size_hint_y=None, height=40)
button.component = component def _refresh_components(self, type_filter: Sequence[str | Type] | None = None) -> None:
button.bind(on_release=self.component_action) if not type_filter:
if component.icon != "icon": type_filter = [Type.CLIENT, Type.ADJUSTER, Type.TOOL, Type.MISC]
image = ApAsyncImage(source=icon_paths[component.icon], favorites = "favorites" in type_filter
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
box_layout = RelativeLayout(size_hint_y=None, height=40)
box_layout.add_widget(button)
box_layout.add_widget(image)
return box_layout
return button
# clear before repopulating # clear before repopulating
assert self._tool_layout and self._client_layout, "must call `build` first" assert self.button_layout, "must call `build` first"
tool_children = reversed(self._tool_layout.layout.children) tool_children = reversed(self.button_layout.layout.children)
for child in tool_children: for child in tool_children:
self._tool_layout.layout.remove_widget(child) self.button_layout.layout.remove_widget(child)
client_children = reversed(self._client_layout.layout.children)
for child in client_children:
self._client_layout.layout.remove_widget(child)
_tools = {c.display_name: c for c in components if c.type == Type.TOOL} cards = [card for card in self.cards if card.component.type in type_filter
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT} or favorites and card.component.display_name in self.favorites]
_adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER}
_miscs = {c.display_name: c for c in components if c.type == Type.MISC}
for (tool, client) in itertools.zip_longest(itertools.chain( self.current_filter = type_filter
_tools.items(), _miscs.items(), _adjusters.items()
), _clients.items()): for card in cards:
# column 1 self.button_layout.layout.add_widget(card)
if tool:
self._tool_layout.layout.add_widget(build_button(tool[1])) top = self.button_layout.children[0].y + self.button_layout.children[0].height \
# column 2 - self.button_layout.height
if client: scroll_percent = self.button_layout.convert_distance_to_scroll(0, top)
self._client_layout.layout.add_widget(build_button(client[1])) self.button_layout.scroll_y = max(0, min(1, scroll_percent[1]))
def filter_clients_by_type(self, caller: MDButton):
self._refresh_components(caller.type)
self.search_box.text = ""
def filter_clients_by_name(self, caller: MDTextField, name: str) -> None:
if len(name) == 0:
self._refresh_components(self.current_filter)
return
sub_matches = [
card for card in self.cards
if name.lower() in card.component.display_name.lower() and card.component.type != Type.HIDDEN
]
self.button_layout.layout.clear_widgets()
for card in sub_matches:
self.button_layout.layout.add_widget(card)
def build(self): def build(self):
self.container = ContainerLayout() self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv"))
self.grid = GridLayout(cols=2) self.grid = self.top_screen.ids.grid
self.container.add_widget(self.grid) self.navigation = self.top_screen.ids.navigation
self.grid.add_widget(Label(text="General", size_hint_y=None, height=40)) self.button_layout = self.top_screen.ids.button_layout
self.grid.add_widget(Label(text="Clients", size_hint_y=None, height=40)) self.search_box = self.top_screen.ids.search_box
self._tool_layout = ScrollBox() self.set_colors()
self._tool_layout.layout.orientation = "vertical" self.top_screen.md_bg_color = self.theme_cls.backgroundColor
self.grid.add_widget(self._tool_layout)
self._client_layout = ScrollBox()
self._client_layout.layout.orientation = "vertical"
self.grid.add_widget(self._client_layout)
self._refresh_components()
global refresh_components global refresh_components
refresh_components = self._refresh_components refresh_components = self._refresh_components
Window.bind(on_drop_file=self._on_drop_file) Window.bind(on_drop_file=self._on_drop_file)
Window.bind(on_keyboard=self._on_keyboard)
return self.container for component in components:
self.cards.append(self.build_card(component))
self._refresh_components(self.current_filter)
# Uncomment to re-enable the Kivy console/live editor
# Ctrl-E to enable it, make sure numlock/capslock is disabled
# from kivy.modules.console import create_console
# create_console(Window, self.top_screen)
return self.top_screen
def on_start(self):
if self.launch_uri:
handle_uri(self.launch_uri, self.launch_args)
self.launch_uri = None
self.launch_args = None
@staticmethod @staticmethod
def component_action(button): def component_action(button):
MDSnackbar(MDSnackbarText(text="Opening in a new window..."), y=dp(24), pos_hint={"center_x": 0.5},
size_hint_x=0.5).open()
if button.component.func: if button.component.func:
button.component.func() button.component.func()
else: else:
@@ -327,13 +407,28 @@ def run_gui():
else: else:
logging.warning(f"unable to identify component for {file}") logging.warning(f"unable to identify component for {file}")
def _on_keyboard(self, window: Window, key: int, scancode: int, codepoint: str, modifier: list[str]):
# Activate search as soon as we start typing, no matter if we are focused on the search box or not.
# Focus first, then capture the first character we type, otherwise it gets swallowed and lost.
# Limit text input to ASCII non-control characters (space bar to tilde).
if not self.search_box.focus:
self.search_box.focus = True
if key in range(32, 126):
self.search_box.text += codepoint
def _stop(self, *largs): def _stop(self, *largs):
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm. # ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
# Closing the window explicitly cleans it up. # Closing the window explicitly cleans it up.
self.root_window.close() self.root_window.close()
super()._stop(*largs) super()._stop(*largs)
Launcher().run() def on_stop(self):
Utils.persistent_store("launcher", "favorites", self.favorites)
Utils.persistent_store("launcher", "filter", ", ".join(filter.name if isinstance(filter, Type) else filter
for filter in self.current_filter))
super().on_stop()
Launcher(path=path, args=args).run()
# avoiding Launcher reference leak # avoiding Launcher reference leak
# and don't try to do something with widgets after window closed # and don't try to do something with widgets after window closed
@@ -360,16 +455,14 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
path = args.get("Patch|Game|Component|url", None) path = args.get("Patch|Game|Component|url", None)
if path is not None: if path is not None:
if path.startswith("archipelago://"): if not path.startswith("archipelago://"):
handle_uri(path, args.get("args", ())) file, component = identify(path)
return if file:
file, component = identify(path) args['file'] = file
if file: if component:
args['file'] = file args['component'] = component
if component: if not component:
args['component'] = component logging.warning(f"Could not identify Component responsible for {path}")
if not component:
logging.warning(f"Could not identify Component responsible for {path}")
if args["update_settings"]: if args["update_settings"]:
update_settings() update_settings()
@@ -378,7 +471,7 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
elif "component" in args: elif "component" in args:
run_component(args["component"], *args["args"]) run_component(args["component"], *args["args"])
elif not args["update_settings"]: elif not args["update_settings"]:
run_gui() run_gui(path, args.get("args", ()))
if __name__ == '__main__': if __name__ == '__main__':
@@ -400,6 +493,7 @@ if __name__ == '__main__':
main(parser.parse_args()) main(parser.parse_args())
from worlds.LauncherComponents import processes from worlds.LauncherComponents import processes
for process in processes: for process in processes:
# we await all child processes to close before we tear down the process host # we await all child processes to close before we tear down the process host
# this makes it feel like each one is its own program, as the Launcher is closed now # this makes it feel like each one is its own program, as the Launcher is closed now

View File

@@ -26,6 +26,7 @@ import typing
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger, from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
server_loop) server_loop)
from NetUtils import ClientStatus from NetUtils import ClientStatus
from worlds.ladx import LinksAwakeningWorld
from worlds.ladx.Common import BASE_ID as LABaseID from worlds.ladx.Common import BASE_ID as LABaseID
from worlds.ladx.GpsTracker import GpsTracker from worlds.ladx.GpsTracker import GpsTracker
from worlds.ladx.TrackerConsts import storage_key from worlds.ladx.TrackerConsts import storage_key
@@ -139,7 +140,7 @@ class RAGameboy():
def set_checks_range(self, checks_start, checks_size): def set_checks_range(self, checks_start, checks_size):
self.checks_start = checks_start self.checks_start = checks_start
self.checks_size = checks_size self.checks_size = checks_size
def set_location_range(self, location_start, location_size, critical_addresses): def set_location_range(self, location_start, location_size, critical_addresses):
self.location_start = location_start self.location_start = location_start
self.location_size = location_size self.location_size = location_size
@@ -237,7 +238,7 @@ class RAGameboy():
self.cache[start:start + len(hram_block)] = hram_block self.cache[start:start + len(hram_block)] = hram_block
self.last_cache_read = time.time() self.last_cache_read = time.time()
async def read_memory_block(self, address: int, size: int): async def read_memory_block(self, address: int, size: int):
block = bytearray() block = bytearray()
remaining_size = size remaining_size = size
@@ -245,7 +246,7 @@ class RAGameboy():
chunk = await self.async_read_memory(address + len(block), remaining_size) chunk = await self.async_read_memory(address + len(block), remaining_size)
remaining_size -= len(chunk) remaining_size -= len(chunk)
block += chunk block += chunk
return block return block
async def read_memory_cache(self, addresses): async def read_memory_cache(self, addresses):
@@ -514,8 +515,8 @@ class LinksAwakeningContext(CommonContext):
magpie_task = None magpie_task = None
won = False won = False
@property @property
def slot_storage_key(self): def slot_storage_key(self):
return f"{self.slot_info[self.slot].name}_{storage_key}" return f"{self.slot_info[self.slot].name}_{storage_key}"
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None: def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
@@ -529,9 +530,7 @@ class LinksAwakeningContext(CommonContext):
def run_gui(self) -> None: def run_gui(self) -> None:
import webbrowser import webbrowser
import kvui from kvui import GameManager, ImageButton
from kvui import Button, GameManager
from kivy.uix.image import Image
class LADXManager(GameManager): class LADXManager(GameManager):
logging_pairs = [ logging_pairs = [
@@ -544,21 +543,15 @@ class LinksAwakeningContext(CommonContext):
b = super().build() b = super().build()
if self.ctx.magpie_enabled: if self.ctx.magpie_enabled:
button = Button(text="", size=(30, 30), size_hint_x=None, button = ImageButton(texture=magpie_logo(), fit_mode="cover", image_size=(32, 32), size_hint_x=None,
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1')) on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
image = Image(size=(16, 16), texture=magpie_logo())
button.add_widget(image)
def set_center(_, center):
image.center = center
button.bind(center=set_center)
self.connect_layout.add_widget(button) self.connect_layout.add_widget(button)
return b return b
self.ui = LADXManager(self) self.ui = LADXManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def send_new_entrances(self, entrances: typing.Dict[str, str]): async def send_new_entrances(self, entrances: typing.Dict[str, str]):
# Store the entrances we find on the server for future sessions # Store the entrances we find on the server for future sessions
message = [{ message = [{
@@ -597,12 +590,12 @@ class LinksAwakeningContext(CommonContext):
logger.info("victory!") logger.info("victory!")
await self.send_msgs(message) await self.send_msgs(message)
self.won = True self.won = True
async def request_found_entrances(self): async def request_found_entrances(self):
await self.send_msgs([{"cmd": "Get", "keys": [self.slot_storage_key]}]) await self.send_msgs([{"cmd": "Get", "keys": [self.slot_storage_key]}])
# Ask for updates so that players can co-op entrances in a seed # Ask for updates so that players can co-op entrances in a seed
await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}]) await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}])
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None: async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
if self.ENABLE_DEATHLINK: if self.ENABLE_DEATHLINK:
@@ -638,12 +631,18 @@ class LinksAwakeningContext(CommonContext):
if cmd == "Connected": if cmd == "Connected":
self.game = self.slot_info[self.slot].game self.game = self.slot_info[self.slot].game
self.slot_data = args.get("slot_data", {}) self.slot_data = args.get("slot_data", {})
# This is sent to magpie over local websocket to make its own connection
self.slot_data.update({
"server_address": self.server_address,
"slot_name": self.player_names[self.slot],
"password": self.password,
})
# TODO - use watcher_event # TODO - use watcher_event
if cmd == "ReceivedItems": if cmd == "ReceivedItems":
for index, item in enumerate(args["items"], start=args["index"]): for index, item in enumerate(args["items"], start=args["index"]):
self.client.recvd_checks[index] = item self.client.recvd_checks[index] = item
if cmd == "Retrieved" and self.magpie_enabled and self.slot_storage_key in args["keys"]: if cmd == "Retrieved" and self.magpie_enabled and self.slot_storage_key in args["keys"]:
self.client.gps_tracker.receive_found_entrances(args["keys"][self.slot_storage_key]) self.client.gps_tracker.receive_found_entrances(args["keys"][self.slot_storage_key])
@@ -722,8 +721,10 @@ class LinksAwakeningContext(CommonContext):
try: try:
self.magpie.set_checks(self.client.tracker.all_checks) self.magpie.set_checks(self.client.tracker.all_checks)
await self.magpie.set_item_tracker(self.client.item_tracker) await self.magpie.set_item_tracker(self.client.item_tracker)
self.magpie.slot_data = self.slot_data if self.slot_data and "slot_data" in self.magpie.features and not self.magpie.has_sent_slot_data:
self.magpie.slot_data = self.slot_data
await self.magpie.send_slot_data()
if self.client.gps_tracker.needs_found_entrances: if self.client.gps_tracker.needs_found_entrances:
await self.request_found_entrances() await self.request_found_entrances()
self.client.gps_tracker.needs_found_entrances = False self.client.gps_tracker.needs_found_entrances = False
@@ -741,8 +742,8 @@ class LinksAwakeningContext(CommonContext):
await asyncio.sleep(1.0) await asyncio.sleep(1.0)
def run_game(romfile: str) -> None: def run_game(romfile: str) -> None:
auto_start = typing.cast(typing.Union[bool, str], auto_start = LinksAwakeningWorld.settings.rom_start
Utils.get_options()["ladx_options"].get("rom_start", True))
if auto_start is True: if auto_start is True:
import webbrowser import webbrowser
webbrowser.open(romfile) webbrowser.open(romfile)

31
Main.py
View File

@@ -56,32 +56,18 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} 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_location = 0
for cls in AutoWorld.AutoWorldRegister.world_types.values():
if cls.item_id_to_name:
max_item = max(max_item, max(cls.item_id_to_name))
max_location = max(max_location, max(cls.location_id_to_name))
item_digits = len(str(max_item))
location_digits = len(str(max_location))
item_count = len(str(max(len(cls.item_names) for cls in AutoWorld.AutoWorldRegister.world_types.values()))) item_count = len(str(max(len(cls.item_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
location_count = len(str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values()))) location_count = len(str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
del max_item, max_location
for name, cls in AutoWorld.AutoWorldRegister.world_types.items(): for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
if not cls.hidden and len(cls.item_names) > 0: if not cls.hidden and len(cls.item_names) > 0:
logger.info(f" {name:{longest_name}}: {len(cls.item_names):{item_count}} " logger.info(f" {name:{longest_name}}: Items: {len(cls.item_names):{item_count}} | "
f"Items (IDs: {min(cls.item_id_to_name):{item_digits}} - " f"Locations: {len(cls.location_names):{location_count}}")
f"{max(cls.item_id_to_name):{item_digits}}) | "
f"{len(cls.location_names):{location_count}} "
f"Locations (IDs: {min(cls.location_id_to_name):{location_digits}} - "
f"{max(cls.location_id_to_name):{location_digits}})")
del item_digits, location_digits, item_count, location_count del item_count, location_count
# This assertion method should not be necessary to run if we are not outputting any multidata. # This assertion method should not be necessary to run if we are not outputting any multidata.
if not args.skip_output: if not args.skip_output and not args.spoiler_only:
AutoWorld.call_stage(multiworld, "assert_generate") AutoWorld.call_stage(multiworld, "assert_generate")
AutoWorld.call_all(multiworld, "generate_early") AutoWorld.call_all(multiworld, "generate_early")
@@ -224,6 +210,15 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.info(f'Beginning output...') logger.info(f'Beginning output...')
outfilebase = 'AP_' + multiworld.seed_name outfilebase = 'AP_' + multiworld.seed_name
if args.spoiler_only:
if args.spoiler > 1:
logger.info('Calculating playthrough.')
multiworld.spoiler.create_playthrough(create_paths=args.spoiler > 2)
multiworld.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
logger.info('Done. Skipped multidata modification. Total time: %s', time.perf_counter() - start)
return multiworld
output = tempfile.TemporaryDirectory() output = tempfile.TemporaryDirectory()
with output as temp_dir: with output as temp_dir:
output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__ output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__

View File

@@ -46,7 +46,8 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ
SlotType, LocationStore, Hint, HintStatus SlotType, LocationStore, Hint, HintStatus
from BaseClasses import ItemClassification from BaseClasses import ItemClassification
min_client_version = Version(0, 1, 6)
min_client_version = Version(0, 5, 0)
colorama.just_fix_windows_console() colorama.just_fix_windows_console()
@@ -66,9 +67,13 @@ def pop_from_container(container, value):
return container return container
def update_dict(dictionary, entries): def update_container_unique(container, entries):
dictionary.update(entries) if isinstance(container, list):
return dictionary existing_container_as_set = set(container)
container.extend([entry for entry in entries if entry not in existing_container_as_set])
else:
container.update(entries)
return container
def queue_gc(): def queue_gc():
@@ -109,7 +114,7 @@ modify_functions = {
# lists/dicts: # lists/dicts:
"remove": remove_from_list, "remove": remove_from_list,
"pop": pop_from_container, "pop": pop_from_container,
"update": update_dict, "update": update_container_unique,
} }
@@ -1978,11 +1983,13 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
new_hint = new_hint.re_prioritize(ctx, status) new_hint = new_hint.re_prioritize(ctx, status)
if hint == new_hint: if hint == new_hint:
return return
ctx.replace_hint(client.team, hint.finding_player, hint, new_hint)
ctx.replace_hint(client.team, hint.receiving_player, hint, new_hint) concerning_slots = ctx.slot_set(hint.receiving_player) | {hint.finding_player}
for slot in concerning_slots:
ctx.replace_hint(client.team, slot, hint, new_hint)
ctx.save() ctx.save()
ctx.on_changed_hints(client.team, hint.finding_player) for slot in concerning_slots:
ctx.on_changed_hints(client.team, hint.receiving_player) ctx.on_changed_hints(client.team, slot)
elif cmd == 'StatusUpdate': elif cmd == 'StatusUpdate':
update_client_status(ctx, client, args["status"]) update_client_status(ctx, client, args["status"])
@@ -2037,7 +2044,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
value = func(value, operation["value"]) value = func(value, operation["value"])
ctx.stored_data[args["key"]] = args["value"] = value ctx.stored_data[args["key"]] = args["value"] = value
targets = set(ctx.stored_data_notification_clients[args["key"]]) targets = set(ctx.stored_data_notification_clients[args["key"]])
if args.get("want_reply", True): if args.get("want_reply", False):
targets.add(client) targets.add(client)
if targets: if targets:
ctx.broadcast(targets, [args]) ctx.broadcast(targets, [args])
@@ -2412,8 +2419,10 @@ async def console(ctx: Context):
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
from settings import get_settings
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
defaults = Utils.get_settings()["server_options"].as_dict() defaults = get_settings().server_options.as_dict()
parser.add_argument('multidata', nargs="?", default=defaults["multidata"]) parser.add_argument('multidata', nargs="?", default=defaults["multidata"])
parser.add_argument('--host', default=defaults["host"]) parser.add_argument('--host', default=defaults["host"])
parser.add_argument('--port', default=defaults["port"], type=int) parser.add_argument('--port', default=defaults["port"], type=int)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import abc import abc
import collections
import functools import functools
import logging import logging
import math import math
@@ -866,15 +867,49 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
def __len__(self) -> int: def __len__(self) -> int:
return self.value.__len__() return self.value.__len__()
# __getitem__ fallback fails for Counters, so we define this explicitly
def __contains__(self, item) -> bool:
return item in self.value
class ItemDict(OptionDict):
class OptionCounter(OptionDict):
min: int | None = None
max: int | None = None
def __init__(self, value: dict[str, int]) -> None:
super(OptionCounter, self).__init__(collections.Counter(value))
def verify(self, world: type[World], player_name: str, plando_options: PlandoOptions) -> None:
super(OptionCounter, self).verify(world, player_name, plando_options)
range_errors = []
if self.max is not None:
range_errors += [
f"\"{key}: {value}\" is higher than maximum allowed value {self.max}."
for key, value in self.value.items() if value > self.max
]
if self.min is not None:
range_errors += [
f"\"{key}: {value}\" is lower than minimum allowed value {self.min}."
for key, value in self.value.items() if value < self.min
]
if range_errors:
range_errors = [f"For option {getattr(self, 'display_name', self)}:"] + range_errors
raise OptionError("\n".join(range_errors))
class ItemDict(OptionCounter):
verify_item_name = True verify_item_name = True
def __init__(self, value: typing.Dict[str, int]): min = 0
if any(item_count is None for item_count in value.values()):
raise Exception("Items must have counts associated with them. Please provide positive integer values in the format \"item\": count .") def __init__(self, value: dict[str, int]) -> None:
if any(item_count < 1 for item_count in value.values()): # Backwards compatibility: Cull 0s to make "in" checks behave the same as when this wasn't a OptionCounter
raise Exception("Cannot have non-positive item counts.") value = {item_name: amount for item_name, amount in value.items() if amount != 0}
super(ItemDict, self).__init__(value) super(ItemDict, self).__init__(value)

View File

@@ -9,7 +9,6 @@ Currently, the following games are supported:
* Factorio * Factorio
* Minecraft * Minecraft
* Subnautica * Subnautica
* Slay the Spire
* Risk of Rain 2 * Risk of Rain 2
* The Legend of Zelda: Ocarina of Time * The Legend of Zelda: Ocarina of Time
* Timespinner * Timespinner
@@ -63,7 +62,6 @@ Currently, the following games are supported:
* TUNIC * TUNIC
* Kirby's Dream Land 3 * Kirby's Dream Land 3
* Celeste 64 * Celeste 64
* Zork Grand Inquisitor
* Castlevania 64 * Castlevania 64
* A Short Hike * A Short Hike
* Yoshi's Island * Yoshi's Island

View File

@@ -47,7 +47,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self) return ".".join(str(item) for item in self)
__version__ = "0.6.0" __version__ = "0.6.2"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux") is_linux = sys.platform.startswith("linux")
@@ -114,6 +114,8 @@ def cache_self1(function: typing.Callable[[S, T], RetType]) -> typing.Callable[[
cache[arg] = res cache[arg] = res
return res return res
wrap.__defaults__ = function.__defaults__
return wrap return wrap
@@ -427,6 +429,9 @@ class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module: str, name: str) -> type: def find_class(self, module: str, name: str) -> type:
if module == "builtins" and name in safe_builtins: if module == "builtins" and name in safe_builtins:
return getattr(builtins, name) return getattr(builtins, name)
# used by OptionCounter
if module == "collections" and name == "Counter":
return collections.Counter
# used by MultiServer -> savegame/multidata # used by MultiServer -> savegame/multidata
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint",
"SlotType", "NetworkSlot", "HintStatus"}: "SlotType", "NetworkSlot", "HintStatus"}:

View File

@@ -214,17 +214,11 @@ class WargrooveContext(CommonContext):
def run_gui(self): def run_gui(self):
"""Import kivy UI system and start running it as self.ui_task.""" """Import kivy UI system and start running it as self.ui_task."""
from kvui import GameManager, HoverBehavior, ServerToolTip from kvui import GameManager, HoverBehavior, ServerToolTip
from kivy.uix.tabbedpanel import TabbedPanelItem from kivymd.uix.tab import MDTabsItem, MDTabsItemText
from kivy.lang import Builder from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.togglebutton import ToggleButton from kivy.uix.togglebutton import ToggleButton
from kivy.uix.boxlayout import BoxLayout 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.uix.label import Label
from kivy.properties import ColorProperty
from kivy.uix.image import Image
import pkgutil import pkgutil
class TrackerLayout(BoxLayout): class TrackerLayout(BoxLayout):

View File

@@ -28,6 +28,6 @@ def get_seeds():
response.append({ response.append({
"seed_id": seed.id, "seed_id": seed.id,
"creation_time": seed.creation_time, "creation_time": seed.creation_time,
"players": get_players(seed.slots), "players": get_players(seed),
}) })
return jsonify(response) return jsonify(response)

View File

@@ -9,7 +9,7 @@ from threading import Event, Thread
from typing import Any from typing import Any
from uuid import UUID from uuid import UUID
from pony.orm import db_session, select, commit from pony.orm import db_session, select, commit, PrimaryKey
from Utils import restricted_loads from Utils import restricted_loads
from .locker import Locker, AlreadyRunningException from .locker import Locker, AlreadyRunningException
@@ -36,12 +36,21 @@ def handle_generation_failure(result: BaseException):
logging.exception(e) logging.exception(e)
def _mp_gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None) -> PrimaryKey | None:
from setproctitle import setproctitle
setproctitle(f"Generator ({sid})")
res = gen_game(gen_options, meta=meta, owner=owner, sid=sid)
setproctitle(f"Generator (idle)")
return res
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation): def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
try: try:
meta = json.loads(generation.meta) meta = json.loads(generation.meta)
options = restricted_loads(generation.options) options = restricted_loads(generation.options)
logging.info(f"Generating {generation.id} for {len(options)} players") logging.info(f"Generating {generation.id} for {len(options)} players")
pool.apply_async(gen_game, (options,), pool.apply_async(_mp_gen_game, (options,),
{"meta": meta, {"meta": meta,
"sid": generation.id, "sid": generation.id,
"owner": generation.owner}, "owner": generation.owner},
@@ -55,6 +64,10 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
def init_generator(config: dict[str, Any]) -> None: def init_generator(config: dict[str, Any]) -> None:
from setproctitle import setproctitle
setproctitle("Generator (idle)")
try: try:
import resource import resource
except ModuleNotFoundError: except ModuleNotFoundError:

View File

@@ -227,6 +227,9 @@ def set_up_logging(room_id) -> logging.Logger:
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, def run_server_process(name: str, 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, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue): host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
from setproctitle import setproctitle
setproctitle(name)
Utils.init_logging(name) Utils.init_logging(name)
try: try:
import resource import resource
@@ -247,8 +250,23 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
raise Exception("Worlds system should not be loaded in the custom server.") raise Exception("Worlds system should not be loaded in the custom server.")
import gc import gc
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
del cert_file, cert_key_file, ponyconfig if not cert_file:
def get_ssl_context():
return None
else:
load_date = None
ssl_context = load_server_cert(cert_file, cert_key_file)
def get_ssl_context():
nonlocal load_date, ssl_context
today = datetime.date.today()
if load_date != today:
ssl_context = load_server_cert(cert_file, cert_key_file)
load_date = today
return ssl_context
del ponyconfig
gc.collect() # free intermediate objects used during setup gc.collect() # free intermediate objects used during setup
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
@@ -263,12 +281,12 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
assert ctx.server is None assert ctx.server is None
try: try:
ctx.server = websockets.serve( ctx.server = websockets.serve(
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context) functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=get_ssl_context())
await ctx.server await ctx.server
except OSError: # likely port in use except OSError: # likely port in use
ctx.server = websockets.serve( ctx.server = websockets.serve(
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context) functools.partial(server, ctx=ctx), ctx.host, 0, ssl=get_ssl_context())
await ctx.server await ctx.server
port = 0 port = 0

View File

@@ -135,6 +135,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
{"bosses", "items", "connections", "texts"})) {"bosses", "items", "connections", "texts"}))
erargs.skip_prog_balancing = False erargs.skip_prog_balancing = False
erargs.skip_output = False erargs.skip_output = False
erargs.spoiler_only = False
erargs.csv_output = False erargs.csv_output = False
name_counter = Counter() name_counter = Counter()

View File

@@ -35,6 +35,12 @@ def start_playing():
@app.route('/games/<string:game>/info/<string:lang>') @app.route('/games/<string:game>/info/<string:lang>')
@cache.cached() @cache.cached()
def game_info(game, lang): def game_info(game, lang):
try:
world = AutoWorldRegister.world_types[game]
if lang not in world.web.game_info_languages:
raise KeyError("Sorry, this game's info page is not available in that language yet.")
except KeyError:
return abort(404)
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game)) return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
@@ -52,6 +58,12 @@ def games():
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>') @app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
@cache.cached() @cache.cached()
def tutorial(game, file, lang): def tutorial(game, file, lang):
try:
world = AutoWorldRegister.world_types[game]
if lang not in [tut.link.split("/")[1] for tut in world.web.tutorials]:
raise KeyError("Sorry, the tutorial is not available in that language yet.")
except KeyError:
return abort(404)
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game)) return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))

View File

@@ -6,7 +6,7 @@ from typing import Dict, Union
from docutils.core import publish_parts from docutils.core import publish_parts
import yaml import yaml
from flask import redirect, render_template, request, Response from flask import redirect, render_template, request, Response, abort
import Options import Options
from Utils import local_path from Utils import local_path
@@ -108,7 +108,7 @@ def option_presets(game: str) -> Response:
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}." f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
presets[preset_name][preset_option_name] = option.value presets[preset_name][preset_option_name] = option.value
elif isinstance(option, (Options.Range, Options.OptionSet, Options.OptionList, Options.ItemDict)): elif isinstance(option, (Options.Range, Options.OptionSet, Options.OptionList, Options.OptionCounter)):
presets[preset_name][preset_option_name] = option.value presets[preset_name][preset_option_name] = option.value
elif isinstance(preset_option, str): elif isinstance(preset_option, str):
# Ensure the option value is valid for Choice and Toggle options # Ensure the option value is valid for Choice and Toggle options
@@ -142,7 +142,10 @@ def weighted_options_old():
@app.route("/games/<string:game>/weighted-options") @app.route("/games/<string:game>/weighted-options")
@cache.cached() @cache.cached()
def weighted_options(game: str): def weighted_options(game: str):
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True) try:
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
except KeyError:
return abort(404)
@app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"]) @app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"])
@@ -197,7 +200,10 @@ def generate_weighted_yaml(game: str):
@app.route("/games/<string:game>/player-options") @app.route("/games/<string:game>/player-options")
@cache.cached() @cache.cached()
def player_options(game: str): def player_options(game: str):
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False) try:
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
except KeyError:
return abort(404)
# YAML generator for player-options # YAML generator for player-options
@@ -216,7 +222,7 @@ def generate_yaml(game: str):
for key, val in options.copy().items(): for key, val in options.copy().items():
key_parts = key.rsplit("||", 2) key_parts = key.rsplit("||", 2)
# Detect and build ItemDict options from their name pattern # Detect and build OptionCounter options from their name pattern
if key_parts[-1] == "qty": if key_parts[-1] == "qty":
if key_parts[0] not in options: if key_parts[0] not in options:
options[key_parts[0]] = {} options[key_parts[0]] = {}

View File

@@ -9,3 +9,4 @@ bokeh>=3.6.3
markupsafe>=3.0.2 markupsafe>=3.0.2
Markdown>=3.7 Markdown>=3.7
mdx-breakless-lists>=1.0.1 mdx-breakless-lists>=1.0.1
setproctitle>=1.3.5

View File

@@ -23,7 +23,6 @@ window.addEventListener('load', () => {
showdown.setOption('strikethrough', true); showdown.setOption('strikethrough', true);
showdown.setOption('literalMidWordUnderscores', true); showdown.setOption('literalMidWordUnderscores', true);
gameInfo.innerHTML += (new showdown.Converter()).makeHtml(results); gameInfo.innerHTML += (new showdown.Converter()).makeHtml(results);
adjustHeaderWidth();
// Reset the id of all header divs to something nicer // Reset the id of all header divs to something nicer
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
@@ -42,10 +41,5 @@ window.addEventListener('load', () => {
scrollTarget?.scrollIntoView(); scrollTarget?.scrollIntoView();
} }
}); });
}).catch((error) => {
console.error(error);
gameInfo.innerHTML =
`<h2>This page is out of logic!</h2>
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
}); });
}); });

View File

@@ -6,6 +6,4 @@ window.addEventListener('load', () => {
document.getElementById('file-input').addEventListener('change', () => { document.getElementById('file-input').addEventListener('change', () => {
document.getElementById('host-game-form').submit(); document.getElementById('host-game-form').submit();
}); });
adjustFooterHeight();
}); });

View File

@@ -1,47 +0,0 @@
const adjustFooterHeight = () => {
// If there is no footer on this page, do nothing
const footer = document.getElementById('island-footer');
if (!footer) { return; }
// If the body is taller than the window, also do nothing
if (document.body.offsetHeight > window.innerHeight) {
footer.style.marginTop = '0';
return;
}
// Add a margin-top to the footer to position it at the bottom of the screen
const sibling = footer.previousElementSibling;
const margin = (window.innerHeight - sibling.offsetTop - sibling.offsetHeight - footer.offsetHeight);
if (margin < 1) {
footer.style.marginTop = '0';
return;
}
footer.style.marginTop = `${margin}px`;
};
const adjustHeaderWidth = () => {
// If there is no header, do nothing
const header = document.getElementById('base-header');
if (!header) { return; }
const tempDiv = document.createElement('div');
tempDiv.style.width = '100px';
tempDiv.style.height = '100px';
tempDiv.style.overflow = 'scroll';
tempDiv.style.position = 'absolute';
tempDiv.style.top = '-500px';
document.body.appendChild(tempDiv);
const scrollbarWidth = tempDiv.offsetWidth - tempDiv.clientWidth;
document.body.removeChild(tempDiv);
const documentRoot = document.compatMode === 'BackCompat' ? document.body : document.documentElement;
const margin = (documentRoot.scrollHeight > documentRoot.clientHeight) ? 0-scrollbarWidth : 0;
document.getElementById('base-header-right').style.marginRight = `${margin}px`;
};
window.addEventListener('load', () => {
window.addEventListener('resize', adjustFooterHeight);
window.addEventListener('resize', adjustHeaderWidth);
adjustFooterHeight();
adjustHeaderWidth();
});

View File

@@ -25,7 +25,6 @@ window.addEventListener('load', () => {
showdown.setOption('literalMidWordUnderscores', true); showdown.setOption('literalMidWordUnderscores', true);
showdown.setOption('disableForced4SpacesIndentedSublists', true); showdown.setOption('disableForced4SpacesIndentedSublists', true);
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results); tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
adjustHeaderWidth();
const title = document.querySelector('h1') const title = document.querySelector('h1')
if (title) { if (title) {
@@ -49,10 +48,5 @@ window.addEventListener('load', () => {
scrollTarget?.scrollIntoView(); scrollTarget?.scrollIntoView();
} }
}); });
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =
`<h2>This page is out of logic!</h2>
<h3>Click <a href="${window.location.origin}/tutorial">here</a> to return to safety.</h3>`;
}); });
}); });

View File

@@ -36,6 +36,13 @@ html{
body{ body{
margin: 0; margin: 0;
display: flex;
flex-direction: column;
min-height: calc(100vh - 110px);
}
main {
flex-grow: 1;
} }
a{ a{

View File

@@ -1,5 +1,6 @@
{% extends 'pageWrapper.html' %} {% extends 'pageWrapper.html' %}
{% import "macros.html" as macros %} {% import "macros.html" as macros %}
{% set show_footer = True %}
{% block head %} {% block head %}
<title>Page Not Found (404)</title> <title>Page Not Found (404)</title>
@@ -13,5 +14,4 @@
The page you're looking for doesn&apos;t exist.<br /> The page you're looking for doesn&apos;t exist.<br />
<a href="/">Click here to return to safety.</a> <a href="/">Click here to return to safety.</a>
</div> </div>
{% include 'islandFooter.html' %}
{% endblock %} {% endblock %}

View File

@@ -1,4 +1,5 @@
{% extends 'pageWrapper.html' %} {% extends 'pageWrapper.html' %}
{% set show_footer = True %}
{% block head %} {% block head %}
<title>Upload Multidata</title> <title>Upload Multidata</title>
@@ -27,6 +28,4 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'islandFooter.html' %}
{% endblock %} {% endblock %}

View File

@@ -1,4 +1,5 @@
{% extends 'pageWrapper.html' %} {% extends 'pageWrapper.html' %}
{% set show_footer = True %}
{% block head %} {% block head %}
<title>Archipelago</title> <title>Archipelago</title>
@@ -57,5 +58,4 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'islandFooter.html' %}
{% endblock %} {% endblock %}

View File

@@ -5,26 +5,29 @@
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tooltip.css") }}" /> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tooltip.css") }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/cookieNotice.css") }}" /> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/cookieNotice.css") }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/globalStyles.css") }}" /> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/globalStyles.css") }}" />
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/styleController.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/cookieNotice.js") }}"></script> <script type="application/ecmascript" src="{{ url_for('static', filename="assets/cookieNotice.js") }}"></script>
{% block head %} {% block head %}
<title>Archipelago</title> <title>Archipelago</title>
{% endblock %} {% endblock %}
</head> </head>
<body> <body>
<main>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div>
{% for message in messages | unique %}
<div class="user-message">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% with messages = get_flashed_messages() %} {% block body %}
{% if messages %} {% endblock %}
<div> </main>
{% for message in messages | unique %}
<div class="user-message">{{ message }}</div> {% if show_footer %}
{% endfor %} {% include "islandFooter.html" %}
</div>
{% endif %} {% endif %}
{% endwith %}
{% block body %}
{% endblock %}
</body> </body>
</html> </html>

View File

@@ -111,10 +111,19 @@
</div> </div>
{% endmacro %} {% endmacro %}
{% macro ItemDict(option_name, option) %} {% macro OptionCounter(option_name, option) %}
{% set relevant_keys = option.valid_keys %}
{% if not relevant_keys %}
{% if option.verify_item_name %}
{% set relevant_keys = world.item_names %}
{% elif option.verify_location_name %}
{% set relevant_keys = world.location_names %}
{% endif %}
{% endif %}
{{ OptionTitle(option_name, option) }} {{ OptionTitle(option_name, option) }}
<div class="option-container"> <div class="option-container">
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %} {% for item_name in (relevant_keys if relevant_keys is ordered else relevant_keys|sort) %}
<div class="option-entry"> <div class="option-entry">
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label> <label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
<input type="number" id="{{ option_name }}-{{ item_name }}-qty" name="{{ option_name }}||{{ item_name }}||qty" value="{{ option.default[item_name]|default("0") }}" data-option-name="{{ option_name }}" data-item-name="{{ item_name }}" /> <input type="number" id="{{ option_name }}-{{ item_name }}-qty" name="{{ option_name }}||{{ item_name }}||qty" value="{{ option.default[item_name]|default("0") }}" data-option-name="{{ option_name }}" data-item-name="{{ item_name }}" />

View File

@@ -93,8 +93,10 @@
{% elif issubclass(option, Options.FreeText) %} {% elif issubclass(option, Options.FreeText) %}
{{ inputs.FreeText(option_name, option) }} {{ inputs.FreeText(option_name, option) }}
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %} {% elif issubclass(option, Options.OptionCounter) and (
{{ inputs.ItemDict(option_name, option) }} option.valid_keys or option.verify_item_name or option.verify_location_name
) %}
{{ inputs.OptionCounter(option_name, option) }}
{% elif issubclass(option, Options.OptionList) and option.valid_keys %} {% elif issubclass(option, Options.OptionList) and option.valid_keys %}
{{ inputs.OptionList(option_name, option) }} {{ inputs.OptionList(option_name, option) }}
@@ -133,8 +135,10 @@
{% elif issubclass(option, Options.FreeText) %} {% elif issubclass(option, Options.FreeText) %}
{{ inputs.FreeText(option_name, option) }} {{ inputs.FreeText(option_name, option) }}
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %} {% elif issubclass(option, Options.OptionCounter) and (
{{ inputs.ItemDict(option_name, option) }} option.valid_keys or option.verify_item_name or option.verify_location_name
) %}
{{ inputs.OptionCounter(option_name, option) }}
{% elif issubclass(option, Options.OptionList) and option.valid_keys %} {% elif issubclass(option, Options.OptionList) and option.valid_keys %}
{{ inputs.OptionList(option_name, option) }} {{ inputs.OptionList(option_name, option) }}

View File

@@ -1,5 +1,6 @@
{% extends 'pageWrapper.html' %} {% extends 'pageWrapper.html' %}
{% import "macros.html" as macros %} {% import "macros.html" as macros %}
{% set show_footer = True %}
{% block head %} {% block head %}
<title>Generation failed, please retry.</title> <title>Generation failed, please retry.</title>
@@ -15,5 +16,4 @@
{{ seed_error }} {{ seed_error }}
</div> </div>
</div> </div>
{% include 'islandFooter.html' %}
{% endblock %} {% endblock %}

View File

@@ -1,4 +1,5 @@
{% extends 'pageWrapper.html' %} {% extends 'pageWrapper.html' %}
{% set show_footer = True %}
{% block head %} {% block head %}
<title>Start Playing</title> <title>Start Playing</title>
@@ -26,6 +27,4 @@
</p> </p>
</div> </div>
</div> </div>
{% include 'islandFooter.html' %}
{% endblock %} {% endblock %}

View File

@@ -29,7 +29,8 @@
<div id="user-content-wrapper" class="markdown"> <div id="user-content-wrapper" class="markdown">
<div id="user-content" class="grass-island"> <div id="user-content" class="grass-island">
<h1>User Content</h1> <h1>User Content</h1>
Below is a list of all the content you have generated on this site. Rooms and seeds are listed separately. Below is a list of all the content you have generated on this site. Rooms and seeds are listed separately.<br/>
Sessions can be saved or synced across devices using the <a href="{{url_for('show_session')}}">Sessions Page.</a>
<h2>Your Rooms</h2> <h2>Your Rooms</h2>
{% if rooms %} {% if rooms %}

View File

@@ -1,5 +1,6 @@
{% extends 'pageWrapper.html' %} {% extends 'pageWrapper.html' %}
{% import "macros.html" as macros %} {% import "macros.html" as macros %}
{% set show_footer = True %}
{% block head %} {% block head %}
<title>View Seed {{ seed.id|suuid }}</title> <title>View Seed {{ seed.id|suuid }}</title>
@@ -50,5 +51,4 @@
</table> </table>
</div> </div>
</div> </div>
{% include 'islandFooter.html' %}
{% endblock %} {% endblock %}

View File

@@ -1,9 +1,12 @@
{% extends 'pageWrapper.html' %} {% extends 'pageWrapper.html' %}
{% import "macros.html" as macros %} {% import "macros.html" as macros %}
{% set show_footer = True %}
{% block head %} {% block head %}
<title>Generation in Progress</title> <title>Generation in Progress</title>
<meta http-equiv="refresh" content="1"> <noscript>
<meta http-equiv="refresh" content="1">
</noscript>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/waitSeed.css") }}"/> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/waitSeed.css") }}"/>
{% endblock %} {% endblock %}
@@ -15,5 +18,34 @@
Waiting for game to generate, this page auto-refreshes to check. Waiting for game to generate, this page auto-refreshes to check.
</div> </div>
</div> </div>
{% include 'islandFooter.html' %} <script>
const waitSeedDiv = document.getElementById("wait-seed");
async function checkStatus() {
try {
const response = await fetch("{{ url_for('api.wait_seed_api', seed=seed_id) }}");
if (response.status !== 202) {
// Seed is ready; reload page to load seed page.
location.reload();
return;
}
const data = await response.json();
waitSeedDiv.innerHTML = `
<h1>Generation in Progress</h1>
<p>${data.text}</p>
`;
setTimeout(checkStatus, 1000); // Continue polling.
} catch (error) {
waitSeedDiv.innerHTML = `
<h1>Progress Unknown</h1>
<p>${error.message}<br />(Last checked: ${new Date().toLocaleTimeString()})</p>
`;
setTimeout(checkStatus, 1000);
}
}
setTimeout(checkStatus, 1000);
</script>
{% endblock %} {% endblock %}

View File

@@ -113,9 +113,18 @@
{{ TextChoice(option_name, option) }} {{ TextChoice(option_name, option) }}
{% endmacro %} {% endmacro %}
{% macro ItemDict(option_name, option, world) %} {% macro OptionCounter(option_name, option, world) %}
{% set relevant_keys = option.valid_keys %}
{% if not relevant_keys %}
{% if option.verify_item_name %}
{% set relevant_keys = world.item_names %}
{% elif option.verify_location_name %}
{% set relevant_keys = world.location_names %}
{% endif %}
{% endif %}
<div class="dict-container"> <div class="dict-container">
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %} {% for item_name in (relevant_keys if relevant_keys is ordered else relevant_keys|sort) %}
<div class="dict-entry"> <div class="dict-entry">
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label> <label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
<input <input

View File

@@ -83,8 +83,10 @@
{% elif issubclass(option, Options.FreeText) %} {% elif issubclass(option, Options.FreeText) %}
{{ inputs.FreeText(option_name, option) }} {{ inputs.FreeText(option_name, option) }}
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %} {% elif issubclass(option, Options.OptionCounter) and (
{{ inputs.ItemDict(option_name, option, world) }} option.valid_keys or option.verify_item_name or option.verify_location_name
) %}
{{ inputs.OptionCounter(option_name, option, world) }}
{% elif issubclass(option, Options.OptionList) and option.valid_keys %} {% elif issubclass(option, Options.OptionList) and option.valid_keys %}
{{ inputs.OptionList(option_name, option) }} {{ inputs.OptionList(option_name, option) }}

View File

@@ -14,23 +14,60 @@
salmon: "FA8072" # typically trap item salmon: "FA8072" # typically trap item
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
orange: "FF7700" # Used for command echo orange: "FF7700" # Used for command echo
<Label>: # KivyMD theming parameters
color: "FFFFFF" theme_style: "Dark" # Light/Dark
<TabbedPanel>: primary_palette: "Lightsteelblue" # Many options
tab_width: root.width / app.tab_count dynamic_scheme_name: "VIBRANT"
dynamic_scheme_contrast: 0.0
<MDLabel>:
color: self.theme_cls.primaryColor
<BaseButton>:
ripple_color: app.theme_cls.primaryColor
ripple_duration_in_fast: 0.2
<MDTabsItemBase>:
ripple_color: app.theme_cls.primaryColor
ripple_duration_in_fast: 0.2
<TooltipLabel>: <TooltipLabel>:
text_size: self.width, None adaptive_height: True
size_hint_y: None theme_font_size: "Custom"
height: self.texture_size[1] font_size: "20dp"
font_size: dp(20)
markup: True markup: True
halign: "left"
<SelectableLabel>: <SelectableLabel>:
size_hint: 1, None
theme_text_color: "Custom"
text_color: 1, 1, 1, 1
canvas.before: canvas.before:
Color: Color:
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1) rgba: (self.theme_cls.primaryColor[0], self.theme_cls.primaryColor[1], self.theme_cls.primaryColor[2], .3) if self.selected else self.theme_cls.surfaceContainerLowestColor
Rectangle: Rectangle:
size: self.size size: self.size
pos: self.pos pos: self.pos
<MarkupDropdownItem>
orientation: "vertical"
MDLabel:
text: root.text
valign: "center"
padding_x: "12dp"
shorten: True
shorten_from: "right"
theme_text_color: "Custom"
markup: True
text_color:
app.theme_cls.onSurfaceVariantColor \
if not root.text_color else \
root.text_color
MDDivider:
md_bg_color:
( \
app.theme_cls.outlineVariantColor \
if not root.divider_color \
else root.divider_color \
) \
if root.divider else \
(0, 0, 0, 0)
<UILog>: <UILog>:
messages: 1000 # amount of messages stored in client logs. messages: 1000 # amount of messages stored in client logs.
cols: 1 cols: 1
@@ -49,7 +86,7 @@
<HintLabel>: <HintLabel>:
canvas.before: canvas.before:
Color: Color:
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1) if self.striped else (0.18, 0.18, 0.18, 1) rgba: (.0, 0.9, .1, .3) if self.selected else self.theme_cls.surfaceContainerHighColor if self.striped else self.theme_cls.surfaceContainerLowColor
Rectangle: Rectangle:
size: self.size size: self.size
pos: self.pos pos: self.pos
@@ -126,9 +163,12 @@
<ToolTip>: <ToolTip>:
size: self.texture_size size: self.texture_size
size_hint: None, None size_hint: None, None
theme_font_size: "Custom"
font_size: dp(18) font_size: dp(18)
pos_hint: {'center_y': 0.5, 'center_x': 0.5} pos_hint: {'center_y': 0.5, 'center_x': 0.5}
halign: "left" halign: "left"
theme_text_color: "Custom"
text_color: (1, 1, 1, 1)
canvas.before: canvas.before:
Color: Color:
rgba: 0.2, 0.2, 0.2, 1 rgba: 0.2, 0.2, 0.2, 1
@@ -147,8 +187,38 @@
rectangle: self.x-2, self.y-2, self.width+4, self.height+4 rectangle: self.x-2, self.y-2, self.width+4, self.height+4
<ServerToolTip>: <ServerToolTip>:
pos_hint: {'center_y': 0.5, 'center_x': 0.5} pos_hint: {'center_y': 0.5, 'center_x': 0.5}
<AutocompleteHintInput> <AutocompleteHintInput>:
size_hint_y: None size_hint_y: None
height: dp(30) height: "30dp"
multiline: False multiline: False
write_tab: False write_tab: False
pos_hint: {"center_x": 0.5, "center_y": 0.5}
<ConnectBarTextInput>:
height: "30dp"
multiline: False
write_tab: False
role: "medium"
size_hint_y: None
pos_hint: {"center_x": 0.5, "center_y": 0.5}
<CommandPromptTextInput>:
size_hint_y: None
height: "30dp"
multiline: False
write_tab: False
pos_hint: {"center_x": 0.5, "center_y": 0.5}
<MessageBoxLabel>:
theme_text_color: "Custom"
text_color: 1, 1, 1, 1
<ScrollBox>:
layout: layout
bar_width: "12dp"
scroll_wheel_distance: 40
do_scroll_x: False
scroll_type: ['bars', 'content']
MDBoxLayout:
id: layout
orientation: "vertical"
spacing: 10
size_hint_y: None
height: self.minimum_height

161
data/launcher.kv Normal file
View File

@@ -0,0 +1,161 @@
<LauncherCard>:
id: main
style: "filled"
padding: "4dp"
size_hint: 1, None
height: "75dp"
context_button: context
focus_behavior: False
MDRelativeLayout:
ApAsyncImage:
source: main.image
size: (48, 48)
size_hint: None, None
pos_hint: {"center_x": 0.1, "center_y": 0.5}
MDLabel:
text: main.component.display_name
pos_hint:{"center_x": 0.5, "center_y": 0.75 if main.component.description else 0.65}
halign: "center"
font_style: "Title"
role: "medium"
theme_text_color: "Custom"
text_color: app.theme_cls.primaryColor
MDLabel:
text: main.component.description
pos_hint: {"center_x": 0.5, "center_y": 0.35}
halign: "center"
role: "small"
theme_text_color: "Custom"
text_color: app.theme_cls.primaryColor
MDIconButton:
component: main.component
icon: "star" if self.component.display_name in app.favorites else "star-outline"
style: "standard"
pos_hint:{"center_x": 0.85, "center_y": 0.8}
theme_text_color: "Custom"
text_color: app.theme_cls.primaryColor
detect_visible: False
on_release: app.set_favorite(self)
MDIconButton:
id: context
icon: "menu"
style: "standard"
pos_hint:{"center_x": 0.95, "center_y": 0.8}
theme_text_color: "Custom"
text_color: app.theme_cls.primaryColor
detect_visible: False
MDButton:
pos_hint:{"center_x": 0.9, "center_y": 0.25}
size_hint_y: None
height: "25dp"
component: main.component
on_release: app.component_action(self)
detect_visible: False
MDButtonText:
text: "Open"
#:import Type worlds.LauncherComponents.Type
MDFloatLayout:
id: top_screen
MDGridLayout:
id: grid
cols: 2
spacing: "5dp"
padding: "10dp"
MDGridLayout:
id: navigation
cols: 1
size_hint_x: 0.25
MDButton:
id: all
style: "text"
type: (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "asterisk"
MDButtonText:
text: "All"
MDButton:
id: client
style: "text"
type: (Type.CLIENT, )
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "controller"
MDButtonText:
text: "Client"
MDButton:
id: Tool
style: "text"
type: (Type.TOOL, )
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "desktop-classic"
MDButtonText:
text: "Tool"
MDButton:
id: adjuster
style: "text"
type: (Type.ADJUSTER, )
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "wrench"
MDButtonText:
text: "Adjuster"
MDButton:
id: misc
style: "text"
type: (Type.MISC, )
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "dots-horizontal-circle-outline"
MDButtonText:
text: "Misc"
MDButton:
id: favorites
style: "text"
type: ("favorites", )
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "star"
MDButtonText:
text: "Favorites"
MDNavigationDrawerDivider:
MDGridLayout:
id: main_layout
cols: 1
spacing: "10dp"
MDTextField:
id: search_box
mode: "outlined"
set_text: app.filter_clients_by_name
MDTextFieldLeadingIcon:
icon: "magnify"
MDTextFieldHintText:
text: "Search"
ScrollBox:
id: button_layout

View File

@@ -184,9 +184,6 @@
# Secret of Evermore # Secret of Evermore
/worlds/soe/ @black-sliver /worlds/soe/ @black-sliver
# Slay the Spire
/worlds/spire/ @KonoTyran
# Stardew Valley # Stardew Valley
/worlds/stardew_valley/ @agilbert1412 /worlds/stardew_valley/ @agilbert1412
@@ -232,10 +229,6 @@
# Zillion # Zillion
/worlds/zillion/ @beauxq /worlds/zillion/ @beauxq
# Zork Grand Inquisitor
/worlds/zork_grand_inquisitor/ @nbrochu
## Active Unmaintained Worlds ## Active Unmaintained Worlds
# The following worlds in this repo are currently unmaintained, but currently still work in core. If any update breaks # The following worlds in this repo are currently unmaintained, but currently still work in core. If any update breaks

View File

@@ -1,5 +1,8 @@
# Adding Games # Adding Games
Like all contributions to Archipelago, New Game implementations should follow the [Contributing](/docs/contributing.md)
guide.
Adding a new game to Archipelago has two major parts: Adding a new game to Archipelago has two major parts:
* Game Modification to communicate with Archipelago server (hereafter referred to as "client") * Game Modification to communicate with Archipelago server (hereafter referred to as "client")
@@ -13,30 +16,51 @@ it will not be detailed here.
The client is an intermediary program between the game and the Archipelago server. This can either be a direct The client is an intermediary program between the game and the Archipelago server. This can either be a direct
modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
must fulfill a few requirements in order to function as expected. The specific requirements the game client must follow must fulfill a few requirements in order to function as expected. Libraries for most modern languages and the spec for
to behave as expected are: various packets can be found in the [network protocol](/docs/network%20protocol.md) API reference document.
### Hard Requirements
In order for the game client to behave as expected, it must be able to perform these functions:
* Handle both secure and unsecure websocket connections * Handle both secure and unsecure websocket connections
* Detect and react when a location has been "checked" by the player by sending a network packet to the server * Reconnect if the connection is unstable and lost while playing
* Receive and parse network packets when the player receives an item from the server, and reward it to the player on
demand
* **Any** of your items can be received any number of times, up to and far surpassing those that the game might
normally expect from features such as starting inventory, item link replacement, or item cheating
* Players and the admin can cheat items to the player at any time with a server command, and these items may not have
a player or location attributed to them
* Be able to change the port for saved connection info * Be able to change the port for saved connection info
* Rooms hosted on the website attempt to reserve their port, but since there are a limited number of ports, this * Rooms hosted on the website attempt to reserve their port, but since there are a limited number of ports, this
privilege can be lost, requiring the room to be moved to a new port privilege can be lost, requiring the room to be moved to a new port
* Reconnect if the connection is unstable and lost while playing
* Keep an index for items received in order to resync. The ItemsReceived Packets are a single list with guaranteed
order.
* Receive items that were sent to the player while they were not connected to the server
* The player being able to complete checks while offline and sending them when reconnecting is a good bonus, but not
strictly required
* Send a status update packet alerting the server that the player has completed their goal * Send a status update packet alerting the server that the player has completed their goal
Libraries for most modern languages and the spec for various packets can be found in the Regarding items and locations, the game client must be able to handle these tasks:
[network protocol](/docs/network%20protocol.md) API reference document.
#### Location Handling
Send a network packet to the server when it detects a location has been "checked" by the player in-game.
* If actions were taken in game that would usually trigger a location check, and those actions can only ever be taken
once, but the client was not connected when they happened: The client must send those location checks on connection
so that they are not permanently lost, e.g. by reading flags in the game state or save file.
#### Item Handling
Receive and parse network packets from the server when the player receives an item.
* It must reward items to the player on demand, as items can come from other players at any time.
* It must be able to reward copies of an item, up to and beyond the number the game normally expects. This may happen
due to features such as starting inventory, item link replacement, admin commands, or item cheating. **Any** of
your items can be received **any** number of times.
* Admins and players may use server commands to create items without a player or location attributed to them. The
client must be able to handle these items.
* It must keep an index for items received in order to resync. The ItemsReceived Packets are a single list with a
guaranteed order.
* It must be able to receive items that were sent to the player while they were not connected to the server.
### Encouraged Features
These are "nice to have" features for a client, but they are not strictly required. It is encouraged to add them
if possible.
* If your client appears in the Archipelago Launcher, you may define an icon for it that differentiates it from
other clients. The icon size is 48x48 pixels, but smaller or larger images will scale to that size.
## World ## World
@@ -44,35 +68,94 @@ The world is your game integration for the Archipelago generator, webhost, and m
information necessary for creating the items and locations to be randomized, the logic for item placement, the information necessary for creating the items and locations to be randomized, the logic for item placement, the
datapackage information so other game clients can recognize your game data, and documentation. Your world must be datapackage information so other game clients can recognize your game data, and documentation. Your world must be
written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
repository and creating a new world package in `/worlds/`. A bare minimum world implementation must satisfy the repository and creating a new world package in `/worlds/`.
following requirements:
* A folder within `/worlds/` that contains an `__init__.py` The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call
* A `World` subclass where you create your world and define all of its rules during generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
* A unique game name regarding the API can be found in the [world api doc](/docs/world%20api.md). Before publishing, make sure to also
* For webhost documentation and behaviors, a `WebWorld` subclass that must be instantiated in the `World` class check out [world maintainer.md](/docs/world%20maintainer.md).
definition
* The game_info doc must follow the format `{language_code}_{game_name}.md` ### Hard Requirements
A bare minimum world implementation must satisfy the following requirements:
* It has a folder with the name of your game (or an abbreviation) under `/worlds/`
* The `/worlds/{game}` folder contains an `__init__.py`
* Any subfolders within `/worlds/{game}` that contain `*.py` files also contain an `__init__.py` for frozen build
packaging
* The game folder has at least one game_info doc named with follow the format `{language_code}_{game_name}.md`
* The game folder has at least one setup doc
* There must be a `World` subclass in your game folder (typically in `/worlds/{game}/__init__.py`) where you create
your world and define all of its rules and features
Within the `World` subclass you should also have:
* A [unique game name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L260)
* An [instance](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L295) of a `WebWorld`
subclass for webhost documentation and behaviors
* In your `WebWorld`, if you wrote a game_info doc in more than one language, override the list of
[game info languages](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L210) with the
ones you include.
* In your `WebWorld`, override the list of
[tutorials](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L213) with each tutorial
or setup doc you included in the game folder.
* A mapping for items and locations defining their names and ids for clients to be able to identify them. These are * A mapping for items and locations defining their names and ids for clients to be able to identify them. These are
`item_name_to_id` and `location_name_to_id`, respectively. `item_name_to_id` and `location_name_to_id`, respectively.
* Create an item when `create_item` is called both by your code and externally * An implementation of `create_item` that can create an item when called by either your code or by another process
* An `options_dataclass` defining the options players have available to them within Archipelago
* A `Region` for your player with the name "Menu" to start from * At least one `Region` for your player to start from (i.e. the Origin Region)
* Create a non-zero number of locations and add them to your regions * The default name of this region is "Menu" but you may configure a different name with
* Create a non-zero number of items **equal** to the number of locations and add them to the multiworld itempool [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)
* All items submitted to the multiworld itempool must not be manually placed by the World. If you need to place specific * A non-zero number of locations, added to your regions
items, there are multiple ways to do so, but they should not be added to the multiworld itempool. * A non-zero number of items **equal** to the number of locations, added to the multiworld itempool
* In rare cases, there may be 0-location-0-item games, but this is extremely atypical.
* A set
[completion condition](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#L77) (aka "goal") for
the player.
* Use your player as the index (`multiworld.completion_condition[player]`) for your world's completion goal.
Notable caveats: ### Encouraged Features
* The "Menu" region will always be considered the "start" for the player
* The "Menu" region is *always* considered accessible; i.e. the player is expected to always be able to return to the These are "nice to have" features for a world, but they are not strictly required. It is encouraged to add them
if possible.
* An implementation of
[get_filler_item_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L473)
* By default, this function chooses any item name from `item_name_to_id`, so you want to limit it to only the true
filler items.
* An `options_dataclass` defining the options players have available to them
* This should be accompanied by a type hint for `options` with the same class name
* A [bug report page](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L220)
* A list of [option groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L226)
for better organization on the webhost
* A dictionary of [options presets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L223)
for player convenience
* A dictionary of [item name groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L273)
for player convenience
* A dictionary of
[location name groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L276)
for player convenience
* Other games may also benefit from your name group dictionaries for hints, features, etc.
### Discouraged or Prohibited Behavior
These are behaviors or implementations that are known to cause various issues. Some of these points have notable
workarounds or preferred methods which should be used instead:
* All items submitted to the multiworld itempool must not be manually placed by the World.
* If you need to place specific items, there are multiple ways to do so, but they should not be added to the
multiworld itempool.
* It is not allowed to use `eval` for most reasons, chiefly due to security concerns.
* It is discouraged to use PyYAML (i.e. `yaml.load`) directly due to security concerns.
* When possible, use `Utils.parse_yaml` instead, as this defaults to the safe loader and the faster C parser.
* When submitting regions or items to the multiworld (`multiworld.regions` and `multiworld.itempool` respectively),
do **not** use `=` as this will overwrite all elements for all games in the seed.
* Instead, use `append`, `extend`, or `+=`.
### Notable Caveats
* The Origin Region will always be considered the "start" for the player
* The Origin Region is *always* considered accessible; i.e. the player is expected to always be able to return to the
start of the game from anywhere start of the game from anywhere
* When submitting regions or items to the multiworld (multiworld.regions and multiworld.itempool respectively), use
`append`, `extend`, or `+=`. **Do not use `=`**
* Regions are simply containers for locations that share similar access rules. They do not have to map to * Regions are simply containers for locations that share similar access rules. They do not have to map to
concrete, physical areas within your game and can be more abstract like tech trees or a questline. concrete, physical areas within your game and can be more abstract like tech trees or a questline.
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call during
generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
regarding the API can be found in the [world api doc](/docs/world%20api.md).
Before publishing, make sure to also check out [world maintainer.md](/docs/world%20maintainer.md).

View File

@@ -66,3 +66,22 @@ The reason entrance access rules using `location.can_reach` and `entrance.can_re
We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition, and that some games have very complex access rules. We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition, and that some games have very complex access rules.
As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682) being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of checking each entrance whenever a region has been reached, although this does come with a performance cost. As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682) being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of checking each entrance whenever a region has been reached, although this does come with a performance cost.
Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should be reasonable to know all entrance &rarr; region dependencies, making indirect conditions preferred because they are much faster. Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should be reasonable to know all entrance &rarr; region dependencies, making indirect conditions preferred because they are much faster.
---
### I uploaded the generated output of my world to the webhost and webhost is erroring on corrupted multidata
The error `Could not load multidata. File may be corrupted or incompatible.` occurs when uploading a locally generated
file where there is an issue with the multidata contained within it. It may come with a description like
`(No module named 'worlds.myworld')` or `(global 'worlds.myworld.names.ItemNames' is forbidden)`
Pickling is a way to compress python objects such that they can be decompressed and be used to rebuild the
python objects. This means that if one of your custom class instances ends up in the multidata, the server would not
be able to load that custom class to decompress the data, which can fail either because the custom class is unknown
(because it cannot load your world module) or the class it's attempting to import to decompress is deemed unsafe.
Common situations where this can happen include:
* Using Option instances directly in slot_data. Ex: using `options.option_name` instead of `options.option_name.value`.
Also, consider using the `options.as_dict("option_name", "option_two")` helper.
* Using enums as Location/Item names in the datapackage. When building out `location_name_to_id` and `item_name_to_id`,
make sure that you are not using your enum class for either the names or ids in these mappings.

View File

@@ -117,8 +117,6 @@ flowchart LR
%% Java Based Games %% Java Based Games
subgraph Java subgraph Java
JM[Mod with Archipelago.MultiClient.Java] JM[Mod with Archipelago.MultiClient.Java]
STS[Slay the Spire]
JM <-- Mod the Spire --> STS
subgraph Minecraft subgraph Minecraft
MCS[Minecraft Forge Server] MCS[Minecraft Forge Server]
JMC[Any Java Minecraft Clients] JMC[Any Java Minecraft Clients]

View File

@@ -470,7 +470,7 @@ The following operations can be applied to a datastorage key
| right_shift | Applies a bitwise right-shift to the current value of the key by `value`. | | right_shift | Applies a bitwise right-shift to the current value of the key by `value`. |
| remove | List only: removes the first instance of `value` found in the list. | | remove | List only: removes the first instance of `value` found in the list. |
| pop | List or Dict: for lists it will remove the index of the `value` given. for dicts it removes the element with the specified key of `value`. | | pop | List or Dict: for lists it will remove the index of the `value` given. for dicts it removes the element with the specified key of `value`. |
| update | Dict only: Updates the dictionary with the specified elements given in `value` creating new keys, or updating old ones if they previously existed. | | update | List or Dict: Adds the elements of `value` to the container if they weren't already present. In the case of a Dict, already present keys will have their corresponding values updated. |
### SetNotify ### SetNotify
Used to register your current session for receiving all [SetReply](#SetReply) packages of certain keys to allow your client to keep track of changes. Used to register your current session for receiving all [SetReply](#SetReply) packages of certain keys to allow your client to keep track of changes.
@@ -756,8 +756,8 @@ Tags are represented as a list of strings, the common client tags follow:
### DeathLink ### DeathLink
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data: A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
| Name | Type | Notes | | Name | Type | Notes |
|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------| |--------|-------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| time | float | Unix Time Stamp of time of death. | | time | float | Unix Time Stamp of time of death. |
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, this should contain the player name, ex. "Berserker was run over by a train." | | cause | str | Optional. Text to explain the cause of death. When provided, or checked, if the string is non-empty, it should contain the player name, ex. "Berserker was run over by a train." |
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. | | source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |

View File

@@ -352,8 +352,15 @@ template. If you set a [Schema](https://pypi.org/project/schema/) on the class w
options system will automatically validate the user supplied data against the schema to ensure it's in the correct options system will automatically validate the user supplied data against the schema to ensure it's in the correct
format. format.
### OptionCounter
This is a special case of OptionDict where the dictionary values can only be integers.
It returns a [collections.Counter](https://docs.python.org/3/library/collections.html#collections.Counter).
This means that if you access a key that isn't present, its value will be 0.
The upside of using an OptionCounter (instead of an OptionDict with integer values) is that an OptionCounter can be
displayed on the Options page on WebHost.
### ItemDict ### ItemDict
Like OptionDict, except this will verify that every key in the dictionary is a valid name for an item for your world. An OptionCounter that will verify that every key in the dictionary is a valid name for an item for your world.
### OptionList ### OptionList
This option defines a List, where the user can add any number of strings to said list, allowing duplicate values. You This option defines a List, where the user can add any number of strings to said list, allowing duplicate values. You

View File

@@ -561,7 +561,7 @@ from .items import is_progression # this is just a dummy
def create_item(self, item: str) -> MyGameItem: def create_item(self, item: str) -> MyGameItem:
# this is called when AP wants to create an item by name (for plando) or when you call it from your own code # this is called when AP wants to create an item by name (for plando, start inventory, item links) or when you call it from your own code
classification = ItemClassification.progression if is_progression(item) else ItemClassification.filler classification = ItemClassification.progression if is_progression(item) else ItemClassification.filler
return MyGameItem(item, classification, self.item_name_to_id[item], self.player) return MyGameItem(item, classification, self.item_name_to_id[item], self.player)
@@ -606,8 +606,8 @@ from .items import get_item_type
def set_rules(self) -> None: def set_rules(self) -> None:
# For some worlds this step can be omitted if either a Logic mixin # For some worlds this step can be omitted if either a Logic mixin
# (see below) is used, it's easier to apply the rules from data during # (see below) is used or it's easier to apply the rules from data during
# location generation or everything is in generate_basic # location generation
# set a simple rule for an region # set a simple rule for an region
set_rule(self.multiworld.get_entrance("Boss Door", self.player), set_rule(self.multiworld.get_entrance("Boss Door", self.player),

View File

@@ -50,13 +50,15 @@ class EntranceLookup:
_random: random.Random _random: random.Random
_expands_graph_cache: dict[Entrance, bool] _expands_graph_cache: dict[Entrance, bool]
_coupled: bool _coupled: bool
_usable_exits: set[Entrance]
def __init__(self, rng: random.Random, coupled: bool): def __init__(self, rng: random.Random, coupled: bool, usable_exits: set[Entrance]):
self.dead_ends = EntranceLookup.GroupLookup() self.dead_ends = EntranceLookup.GroupLookup()
self.others = EntranceLookup.GroupLookup() self.others = EntranceLookup.GroupLookup()
self._random = rng self._random = rng
self._expands_graph_cache = {} self._expands_graph_cache = {}
self._coupled = coupled self._coupled = coupled
self._usable_exits = usable_exits
def _can_expand_graph(self, entrance: Entrance) -> bool: def _can_expand_graph(self, entrance: Entrance) -> bool:
""" """
@@ -95,7 +97,8 @@ class EntranceLookup:
# randomizable exits which are not reverse of the incoming entrance. # randomizable exits which are not reverse of the incoming entrance.
# uncoupled mode is an exception because in this case going back in the door you just came in could # uncoupled mode is an exception because in this case going back in the door you just came in could
# actually lead somewhere new # actually lead somewhere new
if not exit_.connected_region and (not self._coupled or exit_.name != entrance.name): if (not exit_.connected_region and (not self._coupled or exit_.name != entrance.name)
and exit_ in self._usable_exits):
self._expands_graph_cache[entrance] = True self._expands_graph_cache[entrance] = True
return True return True
elif exit_.connected_region and exit_.connected_region not in visited: elif exit_.connected_region and exit_.connected_region not in visited:
@@ -333,7 +336,6 @@ def randomize_entrances(
start_time = time.perf_counter() start_time = time.perf_counter()
er_state = ERPlacementState(world, coupled) er_state = ERPlacementState(world, coupled)
entrance_lookup = EntranceLookup(world.random, coupled)
# similar to fill, skip validity checks on entrances if the game is beatable on minimal accessibility # similar to fill, skip validity checks on entrances if the game is beatable on minimal accessibility
perform_validity_check = True perform_validity_check = True
@@ -349,6 +351,7 @@ def randomize_entrances(
# used when membership checks are needed on the exit list, e.g. speculative sweep # used when membership checks are needed on the exit list, e.g. speculative sweep
exits_set = set(exits) exits_set = set(exits)
entrance_lookup = EntranceLookup(world.random, coupled, exits_set)
for entrance in er_targets: for entrance in er_targets:
entrance_lookup.add(entrance) entrance_lookup.add(entrance)

View File

@@ -45,7 +45,8 @@ MinVersion={#min_windows}
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks] [Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}";
Name: "deletelib"; Description: "Clean existing /lib folder and subfolders including /worlds (leave checked if unsure)"; Check: ShouldShowDeleteLibTask
[Types] [Types]
Name: "full"; Description: "Full installation" Name: "full"; Description: "Full installation"
@@ -83,18 +84,8 @@ Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringC
Type: dirifempty; Name: "{app}" Type: dirifempty; Name: "{app}"
[InstallDelete] [InstallDelete]
Type: files; Name: "{app}\lib\worlds\_bizhawk.apworld" Type: files; Name: "{app}\*.exe"
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
Type: files; Name: "{app}\ArchipelagoPokemonClient.exe"
Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua" Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy"
Type: dirifempty; Name: "{app}\lib\worlds\rogue-legacy"
Type: files; Name: "{app}\lib\worlds\sc2wol.apworld"
Type: filesandordirs; Name: "{app}\lib\worlds\sc2wol"
Type: dirifempty; Name: "{app}\lib\worlds\sc2wol"
Type: filesandordirs; Name: "{app}\lib\worlds\bk_sudoku"
Type: dirifempty; Name: "{app}\lib\worlds\bk_sudoku"
Type: files; Name: "{app}\ArchipelagoLauncher(DEBUG).exe"
Type: filesandordirs; Name: "{app}\SNI\lua*" Type: filesandordirs; Name: "{app}\SNI\lua*"
Type: filesandordirs; Name: "{app}\EnemizerCLI*" Type: filesandordirs; Name: "{app}\EnemizerCLI*"
#include "installdelete.iss" #include "installdelete.iss"
@@ -261,3 +252,17 @@ begin
Result := True; Result := True;
end; end;
end; end;
function ShouldShowDeleteLibTask: Boolean;
begin
Result := DirExists(ExpandConstant('{app}\lib'));
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssInstall then
begin
if WizardIsTaskSelected('deletelib') then
DelTree(ExpandConstant('{app}\lib'), True, True, True);
end;
end;

618
kvui.py
View File

@@ -35,8 +35,7 @@ from kivy.config import Config
Config.set("input", "mouse", "mouse,disable_multitouch") Config.set("input", "mouse", "mouse,disable_multitouch")
Config.set("kivy", "exit_on_escape", "0") Config.set("kivy", "exit_on_escape", "0")
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
from kivymd.uix.divider import MDDivider
from kivy.app import App
from kivy.core.window import Window from kivy.core.window import Window
from kivy.core.clipboard import Clipboard from kivy.core.clipboard import Clipboard
from kivy.core.text.markup import MarkupLabel from kivy.core.text.markup import MarkupLabel
@@ -44,32 +43,34 @@ from kivy.core.image import ImageLoader, ImageLoaderBase, ImageData
from kivy.base import ExceptionHandler, ExceptionManager 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, NumericProperty from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty, StringProperty
from kivy.metrics import dp from kivy.metrics import dp, sp
from kivy.effects.scroll import ScrollEffect
from kivy.uix.widget import Widget from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.layout import Layout from kivy.uix.layout import Layout
from kivy.uix.textinput import TextInput
from kivy.uix.scrollview import ScrollView
from kivy.uix.recycleview import RecycleView
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.uix.progressbar import ProgressBar
from kivy.uix.dropdown import DropDown
from kivy.utils import escape_markup from kivy.utils import escape_markup
from kivy.lang import Builder from kivy.lang import Builder
from kivy.uix.recycleview.views import RecycleDataViewBehavior from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.behaviors import FocusBehavior from kivy.uix.behaviors import FocusBehavior, ToggleButtonBehavior
from kivy.uix.recycleboxlayout import RecycleBoxLayout from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.recycleview.layout import LayoutSelectionBehavior from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.animation import Animation from kivy.animation import Animation
from kivy.uix.popup import Popup from kivy.uix.popup import Popup
from kivy.uix.dropdown import DropDown
from kivy.uix.image import AsyncImage from kivy.uix.image import AsyncImage
from kivymd.app import MDApp
from kivymd.uix.gridlayout import MDGridLayout
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.tab.tab import MDTabsSecondary, MDTabsItem, MDTabsItemText, MDTabsCarousel
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.menu.menu import MDDropdownTextItem
from kivymd.uix.dropdownitem import MDDropDownItem, MDDropDownItemText
from kivymd.uix.button import MDButton, MDButtonText, MDButtonIcon, MDIconButton
from kivymd.uix.label import MDLabel, MDIcon
from kivymd.uix.recycleview import MDRecycleView
from kivymd.uix.textfield.textfield import MDTextField
from kivymd.uix.progressindicator import MDLinearProgressIndicator
from kivymd.uix.scrollview import MDScrollView
from kivymd.uix.tooltip import MDTooltip, MDTooltipPlain
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25) fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
@@ -86,6 +87,113 @@ else:
remove_between_brackets = re.compile(r"\[.*?]") remove_between_brackets = re.compile(r"\[.*?]")
class ThemedApp(MDApp):
def set_colors(self):
text_colors = KivyJSONtoTextParser.TextColors()
self.theme_cls.theme_style = text_colors.theme_style
self.theme_cls.primary_palette = text_colors.primary_palette
self.theme_cls.dynamic_scheme_name = text_colors.dynamic_scheme_name
self.theme_cls.dynamic_scheme_contrast = text_colors.dynamic_scheme_contrast
class ImageIcon(MDButtonIcon, AsyncImage):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.image = ApAsyncImage(**kwargs)
self.add_widget(self.image)
def add_widget(self, widget, index=0, canvas=None):
return super(MDIcon, self).add_widget(widget)
class ImageButton(MDIconButton):
def __init__(self, **kwargs):
image_args = dict()
for kwarg in ("fit_mode", "image_size", "color", "source", "texture"):
val = kwargs.pop(kwarg, "None")
if val != "None":
image_args[kwarg.replace("image_", "")] = val
super().__init__()
self.image = ApAsyncImage(**image_args)
def set_center(button, center):
self.image.center_x = self.center_x
self.image.center_y = self.center_y
self.bind(center=set_center)
self.add_widget(self.image)
def add_widget(self, widget, index=0, canvas=None):
return super(MDIcon, self).add_widget(widget)
class ScrollBox(MDScrollView):
layout: MDBoxLayout = ObjectProperty(None)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# thanks kivymd
class ToggleButton(MDButton, ToggleButtonBehavior):
def __init__(self, *args, **kwargs):
super(ToggleButton, self).__init__(*args, **kwargs)
self.bind(state=self._update_bg)
def _update_bg(self, _, state: str):
if self.disabled:
return
if self.theme_bg_color == "Primary":
self.theme_bg_color = "Custom"
if state == "down":
self.md_bg_color = self.theme_cls.primaryColor
for child in self.children:
if child.theme_text_color == "Primary":
child.theme_text_color = "Custom"
if child.theme_icon_color == "Primary":
child.theme_icon_color = "Custom"
child.text_color = self.theme_cls.onPrimaryColor
child.icon_color = self.theme_cls.onPrimaryColor
else:
self.md_bg_color = self.theme_cls.surfaceContainerLowestColor
for child in self.children:
if child.theme_text_color == "Primary":
child.theme_text_color = "Custom"
if child.theme_icon_color == "Primary":
child.theme_icon_color = "Custom"
child.text_color = self.theme_cls.primaryColor
child.icon_color = self.theme_cls.primaryColor
# thanks kivymd
class ResizableTextField(MDTextField):
"""
Resizable MDTextField that manually overrides the builtin sizing.
Note that in order to use this, the sizing must be specified from within a .kv rule.
"""
def __init__(self, *args, **kwargs):
# cursed rules override
rules = Builder.match(self)
textfield = next((rule for rule in rules if rule.name == f"<MDTextField>"), None)
if textfield:
subclasses = rules[rules.index(textfield) + 1:]
for subclass in subclasses:
height_rule = subclass.properties.get("height", None)
if height_rule:
height_rule.ignore_prev = True
super().__init__(*args, **kwargs)
def on_release(self: MDButton, *args):
super(MDButton, self).on_release(args)
self.on_leave()
MDButton.on_release = on_release
# I was surprised to find this didn't already exist in kivy :( # I was surprised to find this didn't already exist in kivy :(
class HoverBehavior(object): class HoverBehavior(object):
"""originally from https://stackoverflow.com/a/605348110""" """originally from https://stackoverflow.com/a/605348110"""
@@ -125,7 +233,7 @@ class HoverBehavior(object):
Factory.register("HoverBehavior", HoverBehavior) Factory.register("HoverBehavior", HoverBehavior)
class ToolTip(Label): class ToolTip(MDTooltipPlain):
pass pass
@@ -133,49 +241,30 @@ class ServerToolTip(ToolTip):
pass pass
class ScrollBox(ScrollView): class HovererableLabel(HoverBehavior, MDLabel):
layout: BoxLayout
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.layout = BoxLayout(size_hint_y=None)
self.layout.bind(minimum_height=self.layout.setter("height"))
self.add_widget(self.layout)
self.effect_cls = ScrollEffect
self.bar_width = dp(12)
self.scroll_type = ["content", "bars"]
class HovererableLabel(HoverBehavior, Label):
pass pass
class TooltipLabel(HovererableLabel): class TooltipLabel(HovererableLabel, MDTooltip):
tooltip = None tooltip_display_delay = 0.1
def create_tooltip(self, text, x, y): def create_tooltip(self, text, x, y):
text = text.replace("<br>", "\n").replace("&amp;", "&").replace("&bl;", "[").replace("&br;", "]") text = text.replace("<br>", "\n").replace("&amp;", "&").replace("&bl;", "[").replace("&br;", "]")
if self.tooltip:
# update
self.tooltip.children[0].text = text
else:
self.tooltip = FloatLayout()
tooltip_label = ToolTip(text=text)
self.tooltip.add_widget(tooltip_label)
fade_in_animation.start(self.tooltip)
App.get_running_app().root.add_widget(self.tooltip)
# handle left-side boundary to not render off-screen
x = max(x, 3 + self.tooltip.children[0].texture_size[0] / 2)
# position float layout # position float layout
self.tooltip.x = x - self.tooltip.width / 2 center_x, center_y = self.to_window(self.center_x, self.center_y)
self.tooltip.y = y - self.tooltip.height / 2 + 48 self.shift_y = y - center_y
shift_x = center_x - x
if shift_x > 0:
self.shift_left = shift_x
else:
self.shift_right = shift_x
def remove_tooltip(self): if self._tooltip:
if self.tooltip: # update
App.get_running_app().root.remove_widget(self.tooltip) self._tooltip.text = text
self.tooltip = None else:
self._tooltip = ToolTip(text=text, pos_hint={})
self.display_tooltip()
def on_mouse_pos(self, window, pos): def on_mouse_pos(self, window, pos):
if not self.get_root_window(): if not self.get_root_window():
@@ -202,26 +291,30 @@ class TooltipLabel(HovererableLabel):
def on_leave(self): def on_leave(self):
self.remove_tooltip() self.remove_tooltip()
self._tooltip = None
class ServerLabel(HovererableLabel): class ServerLabel(HoverBehavior, MDTooltip, MDBoxLayout):
tooltip_display_delay = 0.1
text: str = StringProperty("Server:")
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(HovererableLabel, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.layout = FloatLayout() self.add_widget(MDIcon(icon="information", font_size=sp(15)))
self.popuplabel = ServerToolTip(text="Test") self.add_widget(TooltipLabel(text=self.text, pos_hint={"center_x": 0.5, "center_y": 0.5},
self.layout.add_widget(self.popuplabel) font_size=sp(15)))
self._tooltip = ServerToolTip(text="Test")
def on_enter(self): def on_enter(self):
self.popuplabel.text = self.get_text() self._tooltip.text = self.get_text()
App.get_running_app().root.add_widget(self.layout) self.display_tooltip()
fade_in_animation.start(self.layout)
def on_leave(self): def on_leave(self):
App.get_running_app().root.remove_widget(self.layout) self.animation_tooltip_dismiss()
@property @property
def ctx(self) -> context_type: def ctx(self) -> context_type:
return App.get_running_app().ctx return MDApp.get_running_app().ctx
def get_text(self): def get_text(self):
if self.ctx.server: if self.ctx.server:
@@ -262,11 +355,11 @@ class ServerLabel(HovererableLabel):
return "No current server connection. \nPlease connect to an Archipelago server." return "No current server connection. \nPlease connect to an Archipelago server."
class MainLayout(GridLayout): class MainLayout(MDGridLayout):
pass pass
class ContainerLayout(FloatLayout): class ContainerLayout(MDFloatLayout):
pass pass
@@ -286,6 +379,11 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
return super(SelectableLabel, self).refresh_view_attrs( return super(SelectableLabel, self).refresh_view_attrs(
rv, index, data) rv, index, data)
def on_size(self, instance_label, size: list) -> None:
super().on_size(instance_label, size)
if self.parent:
self.width = self.parent.width
def on_touch_down(self, touch): def on_touch_down(self, touch):
""" Add selection on touch down """ """ Add selection on touch down """
if super(SelectableLabel, self).on_touch_down(touch): if super(SelectableLabel, self).on_touch_down(touch):
@@ -296,10 +394,10 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
else: else:
# Not a fan of the following few lines, but they work. # Not a fan of the following few lines, but they work.
temp = MarkupLabel(text=self.text).markup temp = MarkupLabel(text=self.text).markup
text = "".join(part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]"))) text = "".join(part for part in temp if not part.startswith("["))
cmdinput = App.get_running_app().textinput cmdinput = MDApp.get_running_app().textinput
if not cmdinput.text: if not cmdinput.text:
input_text = get_input_text_from_response(text, App.get_running_app().last_autofillable_command) input_text = get_input_text_from_response(text, MDApp.get_running_app().last_autofillable_command)
if input_text is not None: if input_text is not None:
cmdinput.text = input_text cmdinput.text = input_text
@@ -310,32 +408,118 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
""" Respond to the selection of items in the view. """ """ Respond to the selection of items in the view. """
self.selected = is_selected self.selected = is_selected
class AutocompleteHintInput(TextInput): class MarkupDropdownTextItem(MDDropdownTextItem):
def __init__(self):
super().__init__()
for child in self.children:
if child.__class__ == MDLabel:
child.markup = True
# Currently, this only lets us do markup on text that does not have any icons
# Create new TextItems as needed
class MarkupDropdown(MDDropdownMenu):
def on_items(self, instance, value: list) -> None:
"""
The method sets the class that will be used to create the menu item.
"""
items = []
viewclass = "MarkupDropdownTextItem"
for data in value:
if "viewclass" not in data:
if (
"leading_icon" not in data
and "trailing_icon" not in data
and "trailing_text" not in data
):
viewclass = "MarkupDropdownTextItem"
elif (
"leading_icon" in data
and "trailing_icon" not in data
and "trailing_text" not in data
):
viewclass = "MDDropdownLeadingIconItem"
elif (
"leading_icon" not in data
and "trailing_icon" in data
and "trailing_text" not in data
):
viewclass = "MDDropdownTrailingIconItem"
elif (
"leading_icon" not in data
and "trailing_icon" in data
and "trailing_text" in data
):
viewclass = "MDDropdownTrailingIconTextItem"
elif (
"leading_icon" in data
and "trailing_icon" in data
and "trailing_text" in data
):
viewclass = "MDDropdownLeadingTrailingIconTextItem"
elif (
"leading_icon" in data
and "trailing_icon" in data
and "trailing_text" not in data
):
viewclass = "MDDropdownLeadingTrailingIconItem"
elif (
"leading_icon" not in data
and "trailing_icon" not in data
and "trailing_text" in data
):
viewclass = "MDDropdownTrailingTextItem"
elif (
"leading_icon" in data
and "trailing_icon" not in data
and "trailing_text" in data
):
viewclass = "MDDropdownLeadingIconTrailingTextItem"
data["viewclass"] = viewclass
if "height" not in data:
data["height"] = dp(48)
items.append(data)
self._items = items
# Update items in view
if hasattr(self, "menu"):
self.menu.data = self._items
class AutocompleteHintInput(ResizableTextField):
min_chars = NumericProperty(3) min_chars = NumericProperty(3)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.dropdown = DropDown() self.dropdown = MarkupDropdown(caller=self, position="bottom", border_margin=dp(2), width=self.width)
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
self.bind(on_text_validate=self.on_message) self.bind(on_text_validate=self.on_message)
self.bind(width=lambda instance, x: setattr(self.dropdown, "width", x))
def on_message(self, instance): def on_message(self, instance):
App.get_running_app().commandprocessor("!hint "+instance.text) MDApp.get_running_app().commandprocessor("!hint "+instance.text)
def on_text(self, instance, value): def on_text(self, instance, value):
if len(value) >= self.min_chars: if len(value) >= self.min_chars:
self.dropdown.clear_widgets() self.dropdown.items.clear()
ctx: context_type = App.get_running_app().ctx ctx: context_type = MDApp.get_running_app().ctx
if not ctx.game: if not ctx.game:
return return
item_names = ctx.item_names._game_store[ctx.game].values() item_names = ctx.item_names._game_store[ctx.game].values()
def on_press(button: Button): def on_press(text):
split_text = MarkupLabel(text=button.text).markup split_text = MarkupLabel(text=text).markup
return self.dropdown.select("".join(text_frag for text_frag in split_text self.set_text(self, "".join(text_frag for text_frag in split_text
if not text_frag.startswith("["))) if not text_frag.startswith("[")))
self.dropdown.dismiss()
self.focus = True
lowered = value.lower() lowered = value.lower()
for item_name in item_names: for item_name in item_names:
try: try:
@@ -345,20 +529,29 @@ class AutocompleteHintInput(TextInput):
else: else:
text = escape_markup(item_name) text = escape_markup(item_name)
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):] text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
btn = Button(text=text, size_hint_y=None, height=dp(30), markup=True) self.dropdown.items.append({
btn.bind(on_release=on_press) "text": text,
self.dropdown.add_widget(btn) "on_release": lambda txt=text: on_press(txt),
if not self.dropdown.attach_to: "markup": True
self.dropdown.open(self) })
if not self.dropdown.parent:
self.dropdown.open()
else: else:
self.dropdown.dismiss() self.dropdown.dismiss()
class HintLabel(RecycleDataViewBehavior, BoxLayout): status_icons = {
HintStatus.HINT_NO_PRIORITY: "information",
HintStatus.HINT_PRIORITY: "exclamation-thick",
HintStatus.HINT_AVOID: "alert"
}
class HintLabel(RecycleDataViewBehavior, MDBoxLayout):
selected = BooleanProperty(False) selected = BooleanProperty(False)
striped = BooleanProperty(False) striped = BooleanProperty(False)
index = None index = None
dropdown: DropDown dropdown: MDDropdownMenu
def __init__(self): def __init__(self):
super(HintLabel, self).__init__() super(HintLabel, self).__init__()
@@ -369,29 +562,28 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
self.entrance_text = "" self.entrance_text = ""
self.status_text = "" self.status_text = ""
self.hint = {} self.hint = {}
for child in self.children:
child.bind(texture_size=self.set_height)
ctx = MDApp.get_running_app().ctx
menu_items = []
ctx = App.get_running_app().ctx for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
self.dropdown = DropDown() name = status_names[status]
status_button = MDDropDownItem(MDDropDownItemText(text=name), size_hint_y=None, height=dp(50))
status_button.status = status
menu_items.append({
"text": name,
"leading_icon": status_icons[status],
"on_release": lambda x=status: select(self, x)
})
def set_value(button): self.dropdown = MDDropdownMenu(caller=self.ids["status"], items=menu_items)
self.dropdown.select(button.status)
def select(instance, data): def select(instance, data):
ctx.update_hint(self.hint["location"], ctx.update_hint(self.hint["location"],
self.hint["finding_player"], self.hint["finding_player"],
data) data)
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID): self.dropdown.bind(on_release=self.dropdown.dismiss)
name = status_names[status]
status_button = Button(text=name, size_hint_y=None, height=dp(50))
status_button.status = status
status_button.bind(on_release=set_value)
self.dropdown.add_widget(status_button)
self.dropdown.bind(on_select=select)
def set_height(self, instance, value): def set_height(self, instance, value):
self.height = max([child.texture_size[1] for child in self.children]) self.height = max([child.texture_size[1] for child in self.children])
@@ -406,7 +598,6 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
self.entrance_text = data["entrance"]["text"] self.entrance_text = data["entrance"]["text"]
self.status_text = data["status"]["text"] self.status_text = data["status"]["text"]
self.hint = data["status"]["hint"] self.hint = data["status"]["hint"]
self.height = self.minimum_height
return super(HintLabel, self).refresh_view_attrs(rv, index, data) return super(HintLabel, self).refresh_view_attrs(rv, index, data)
def on_touch_down(self, touch): def on_touch_down(self, touch):
@@ -419,10 +610,10 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
if status_label.collide_point(*touch.pos): if status_label.collide_point(*touch.pos):
if self.hint["status"] == HintStatus.HINT_FOUND: if self.hint["status"] == HintStatus.HINT_FOUND:
return return
ctx = App.get_running_app().ctx ctx = MDApp.get_running_app().ctx
if ctx.slot_concerns_self(self.hint["receiving_player"]): # If this player owns this hint if ctx.slot_concerns_self(self.hint["receiving_player"]): # If this player owns this hint
# open a dropdown # open a dropdown
self.dropdown.open(self.ids["status"]) self.dropdown.open()
elif self.selected: elif self.selected:
self.parent.clear_selection() self.parent.clear_selection()
else: else:
@@ -431,8 +622,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
if self.entrance_text != "Vanilla" if self.entrance_text != "Vanilla"
else "", ". (", self.status_text.lower(), ")")) else "", ". (", self.status_text.lower(), ")"))
temp = MarkupLabel(text).markup temp = MarkupLabel(text).markup
text = "".join( text = "".join(part for part in temp if not part.startswith("["))
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
Clipboard.copy(escape_markup(text).replace("&amp;", "&").replace("&bl;", "[").replace("&br;", "]")) Clipboard.copy(escape_markup(text).replace("&amp;", "&").replace("&bl;", "[").replace("&br;", "]"))
return self.parent.select_with_touch(self.index, touch) return self.parent.select_with_touch(self.index, touch)
else: else:
@@ -455,7 +645,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
else: else:
parent.sort_key = key parent.sort_key = key
parent.reversed = False parent.reversed = False
App.get_running_app().update_hints() MDApp.get_running_app().update_hints()
def apply_selection(self, rv, index, is_selected): def apply_selection(self, rv, index, is_selected):
""" Respond to the selection of items in the view. """ """ Respond to the selection of items in the view. """
@@ -463,7 +653,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
self.selected = is_selected self.selected = is_selected
class ConnectBarTextInput(TextInput): class ConnectBarTextInput(ResizableTextField):
def insert_text(self, substring, from_undo=False): def insert_text(self, substring, from_undo=False):
s = substring.replace("\n", "").replace("\r", "") s = substring.replace("\n", "").replace("\r", "")
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo) return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
@@ -473,14 +663,14 @@ def is_command_input(string: str) -> bool:
return len(string) > 0 and string[0] in "/!" return len(string) > 0 and string[0] in "/!"
class CommandPromptTextInput(TextInput): class CommandPromptTextInput(ResizableTextField):
MAXIMUM_HISTORY_MESSAGES = 50 MAXIMUM_HISTORY_MESSAGES = 50
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self._command_history_index = -1 self._command_history_index = -1
self._command_history: typing.Deque[str] = deque(maxlen=CommandPromptTextInput.MAXIMUM_HISTORY_MESSAGES) self._command_history: typing.Deque[str] = deque(maxlen=CommandPromptTextInput.MAXIMUM_HISTORY_MESSAGES)
def update_history(self, new_entry: str) -> None: def update_history(self, new_entry: str) -> None:
self._command_history_index = -1 self._command_history_index = -1
if is_command_input(new_entry): if is_command_input(new_entry):
@@ -507,7 +697,7 @@ class CommandPromptTextInput(TextInput):
self._change_to_history_text_if_available(self._command_history_index - 1) self._change_to_history_text_if_available(self._command_history_index - 1)
return True return True
return super().keyboard_on_key_down(window, keycode, text, modifiers) return super().keyboard_on_key_down(window, keycode, text, modifiers)
def _change_to_history_text_if_available(self, new_index: int) -> None: def _change_to_history_text_if_available(self, new_index: int) -> None:
if new_index < -1: if new_index < -1:
return return
@@ -521,32 +711,96 @@ class CommandPromptTextInput(TextInput):
class MessageBox(Popup): class MessageBox(Popup):
class MessageBoxLabel(Label): class MessageBoxLabel(MDLabel):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._label.refresh() self._label.refresh()
self.size = self._label.texture.size
if self.width + 50 > Window.width:
self.text_size[0] = Window.width - 50
self._label.refresh()
self.size = self._label.texture.size
def __init__(self, title, text, error=False, **kwargs): def __init__(self, title, text, error=False, **kwargs):
label = MessageBox.MessageBoxLabel(text=text) label = MessageBox.MessageBoxLabel(text=text)
separator_color = [217 / 255, 129 / 255, 122 / 255, 1.] if error else [47 / 255., 167 / 255., 212 / 255, 1.] separator_color = [217 / 255, 129 / 255, 122 / 255, 1.] if error else [47 / 255., 167 / 255., 212 / 255, 1.]
super().__init__(title=title, content=label, size_hint=(None, None), width=max(100, int(label.width) + 40), super().__init__(title=title, content=label, size_hint=(0.5, None), width=max(100, int(label.width) + 40),
separator_color=separator_color, **kwargs) separator_color=separator_color, **kwargs)
self.height += max(0, label.height - 18) self.height += max(0, label.height - 18)
class GameManager(App): class ClientTabs(MDTabsSecondary):
carousel: MDTabsCarousel
lock_swiping = True
def __init__(self, *args, **kwargs):
self.carousel = MDTabsCarousel(lock_swiping=True, anim_move_duration=0.2)
super().__init__(*args, MDDivider(size_hint_y=None, height=dp(1)), self.carousel, **kwargs)
self.size_hint_y = 1
def _check_panel_height(self, *args):
self.ids.tab_scroll.height = dp(38)
def update_indicator(
self, x: float = 0.0, w: float = 0.0, instance: MDTabsItem = None
) -> None:
def update_indicator(*args):
indicator_pos = (0, 0)
indicator_size = (0, 0)
item_text_object = self._get_tab_item_text_icon_object()
if item_text_object:
indicator_pos = (
instance.x + dp(12),
self.indicator.pos[1]
if not self._tabs_carousel
else self._tabs_carousel.height,
)
indicator_size = (
instance.width - dp(24),
self.indicator_height,
)
Animation(
pos=indicator_pos,
size=indicator_size,
d=0 if not self.indicator_anim else self.indicator_duration,
t=self.indicator_transition,
).start(self.indicator)
if not instance:
self.indicator.pos = (x, self.indicator.pos[1])
self.indicator.size = (w, self.indicator_height)
else:
Clock.schedule_once(update_indicator)
def remove_tab(self, tab, content=None):
if content is None:
content = tab.content
self.ids.container.remove_widget(tab)
self.carousel.remove_widget(content)
self.on_size(self, self.size)
class CommandButton(MDButton, MDTooltip):
def __init__(self, *args, manager: "GameManager", **kwargs):
super().__init__(*args, **kwargs)
self.manager = manager
self._tooltip = ToolTip(text="Test")
def on_enter(self):
self._tooltip.text = self.manager.commandprocessor.get_help_text()
self._tooltip.font_size = dp(20 - (len(self._tooltip.text) // 400)) # mostly guessing on the numbers here
self.display_tooltip()
def on_leave(self):
self.animation_tooltip_dismiss()
class GameManager(ThemedApp):
logging_pairs = [ logging_pairs = [
("Client", "Archipelago"), ("Client", "Archipelago"),
] ]
base_title: str = "Archipelago Client" base_title: str = "Archipelago Client"
last_autofillable_command: str last_autofillable_command: str
main_area_container: GridLayout main_area_container: MDGridLayout
""" subclasses can add more columns beside the tabs """ """ subclasses can add more columns beside the tabs """
def __init__(self, ctx: context_type): def __init__(self, ctx: context_type):
@@ -581,45 +835,58 @@ class GameManager(App):
return max(1, len(self.tabs.tab_list)) return max(1, len(self.tabs.tab_list))
return 1 return 1
def on_start(self):
def on_start(*args):
self.root.md_bg_color = self.theme_cls.backgroundColor
super().on_start()
Clock.schedule_once(on_start)
def build(self) -> Layout: def build(self) -> Layout:
self.set_colors()
self.container = ContainerLayout() self.container = ContainerLayout()
self.grid = MainLayout() self.grid = MainLayout()
self.grid.cols = 1 self.grid.cols = 1
self.connect_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30)) self.connect_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(40),
spacing=5, padding=(5, 10))
# top part # top part
server_label = ServerLabel() server_label = ServerLabel(width=dp(75))
self.connect_layout.add_widget(server_label) self.connect_layout.add_widget(server_label)
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:", self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
size_hint_y=None, pos_hint={"center_x": 0.5, "center_y": 0.5})
height=dp(30), multiline=False, write_tab=False)
def connect_bar_validate(sender): def connect_bar_validate(sender):
if not self.ctx.server: if not self.ctx.server:
self.connect_button_action(sender) self.connect_button_action(sender)
self.server_connect_bar.height = dp(30)
self.server_connect_bar.bind(on_text_validate=connect_bar_validate) self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
self.connect_layout.add_widget(self.server_connect_bar) self.connect_layout.add_widget(self.server_connect_bar)
self.server_connect_button = Button(text="Connect", size=(dp(100), dp(30)), size_hint_y=None, size_hint_x=None) self.server_connect_button = MDButton(MDButtonText(text="Connect"), style="filled", size=(dp(100), dp(70)),
size_hint_x=None, size_hint_y=None, radius=5, pos_hint={"center_y": 0.55})
self.server_connect_button.bind(on_press=self.connect_button_action) self.server_connect_button.bind(on_press=self.connect_button_action)
self.server_connect_button.height = self.server_connect_bar.height
self.connect_layout.add_widget(self.server_connect_button) self.connect_layout.add_widget(self.server_connect_button)
self.grid.add_widget(self.connect_layout) self.grid.add_widget(self.connect_layout)
self.progressbar = ProgressBar(size_hint_y=None, height=3) self.progressbar = MDLinearProgressIndicator(size_hint_y=None, height=3)
self.grid.add_widget(self.progressbar) self.grid.add_widget(self.progressbar)
# middle part # middle part
self.tabs = TabbedPanel(size_hint_y=1) self.tabs = ClientTabs(pos_hint={"center_x": 0.5, "center_y": 0.5})
self.tabs.default_tab_text = "All" self.tabs.add_widget(MDTabsItem(MDTabsItemText(text="All" if len(self.logging_pairs) > 1 else "Archipelago")))
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name) self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
for logger_name, name in for logger_name, name in
self.logging_pairs)) self.logging_pairs))
self.tabs.carousel.add_widget(self.tabs.default_tab_content)
for logger_name, display_name in self.logging_pairs: for logger_name, display_name in self.logging_pairs:
bridge_logger = logging.getLogger(logger_name) bridge_logger = logging.getLogger(logger_name)
panel = TabbedPanelItem(text=display_name) self.log_panels[display_name] = UILog(bridge_logger)
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
if len(self.logging_pairs) > 1: if len(self.logging_pairs) > 1:
panel = MDTabsItem(MDTabsItemText(text=display_name))
panel.content = self.log_panels[display_name]
# show Archipelago tab if other logging is present # show Archipelago tab if other logging is present
self.tabs.carousel.add_widget(panel.content)
self.tabs.add_widget(panel) self.tabs.add_widget(panel)
hint_panel = self.add_client_tab("Hints", HintLayout()) hint_panel = self.add_client_tab("Hints", HintLayout())
@@ -627,21 +894,21 @@ class GameManager(App):
self.log_panels["Hints"] = hint_panel.content self.log_panels["Hints"] = hint_panel.content
hint_panel.content.add_widget(self.hint_log) hint_panel.content.add_widget(self.hint_log)
if len(self.logging_pairs) == 1: self.main_area_container = MDGridLayout(size_hint_y=1, rows=1)
self.tabs.default_tab_text = "Archipelago"
self.main_area_container = GridLayout(size_hint_y=1, rows=1)
self.main_area_container.add_widget(self.tabs) self.main_area_container.add_widget(self.tabs)
self.grid.add_widget(self.main_area_container) self.grid.add_widget(self.main_area_container)
# bottom part # bottom part
bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30)) bottom_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(40), spacing=5, padding=(5, 10))
info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None) info_button = CommandButton(MDButtonText(text="Command:", halign="left"), manager=self, radius=5,
style="filled", size=(dp(100), dp(70)), size_hint_x=None, size_hint_y=None,
pos_hint={"center_y": 0.575})
info_button.bind(on_release=self.command_button_action) info_button.bind(on_release=self.command_button_action)
bottom_layout.add_widget(info_button) bottom_layout.add_widget(info_button)
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False) self.textinput = CommandPromptTextInput(size_hint_y=None, multiline=False, write_tab=False)
self.textinput.bind(on_text_validate=self.on_message) self.textinput.bind(on_text_validate=self.on_message)
info_button.height = self.textinput.height
self.textinput.text_validate_unfocus = False self.textinput.text_validate_unfocus = False
bottom_layout.add_widget(self.textinput) bottom_layout.add_widget(self.textinput)
self.grid.add_widget(bottom_layout) self.grid.add_widget(bottom_layout)
@@ -657,29 +924,43 @@ class GameManager(App):
self.server_connect_bar.focus = True self.server_connect_bar.focus = True
self.server_connect_bar.select_text(port_start if port_start > 0 else host_start, len(s)) self.server_connect_bar.select_text(port_start if port_start > 0 else host_start, len(s))
# Uncomment to enable the kivy live editor console
# Press Ctrl-E (with numlock/capslock) disabled to open
# from kivy.core.window import Window
# from kivy.modules import console
# console.create_console(Window, self.container)
return self.container return self.container
def add_client_tab(self, title: str, content: Widget) -> Widget: def add_client_tab(self, title: str, content: Widget, index: int = -1) -> Widget:
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content. """Adds a new tab to the client window with a given title, and provides a given Widget as its content.
Returns the new tab widget, with the provided content being placed on the tab as content.""" Returns the new tab widget, with the provided content being placed on the tab as content."""
new_tab = TabbedPanelItem(text=title) new_tab = MDTabsItem(MDTabsItemText(text=title))
new_tab.content = content new_tab.content = content
self.tabs.add_widget(new_tab) if -1 < index <= len(self.tabs.carousel.slides):
new_tab.bind(on_release=self.tabs.set_active_item)
new_tab._tabs = self.tabs
self.tabs.ids.container.add_widget(new_tab, index=index)
self.tabs.carousel.add_widget(new_tab.content, index=len(self.tabs.carousel.slides) - index)
else:
self.tabs.add_widget(new_tab)
self.tabs.carousel.add_widget(new_tab.content)
return new_tab return new_tab
def update_texts(self, dt): def update_texts(self, dt):
if hasattr(self.tabs.content.children[0], "fix_heights"): for slide in self.tabs.carousel.slides:
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream if hasattr(slide, "fix_heights"):
slide.fix_heights() # TODO: remove this when Kivy fixes this upstream
if self.ctx.server: if self.ctx.server:
self.title = self.base_title + " " + Utils.__version__ + \ self.title = self.base_title + " " + Utils.__version__ + \
f" | Connected to: {self.ctx.server_address} " \ f" | Connected to: {self.ctx.server_address} " \
f"{'.'.join(str(e) for e in self.ctx.server_version)}" f"{'.'.join(str(e) for e in self.ctx.server_version)}"
self.server_connect_button.text = "Disconnect" self.server_connect_button._button_text.text = "Disconnect"
self.server_connect_bar.readonly = True self.server_connect_bar.readonly = True
self.progressbar.max = len(self.ctx.checked_locations) + len(self.ctx.missing_locations) self.progressbar.max = len(self.ctx.checked_locations) + len(self.ctx.missing_locations)
self.progressbar.value = len(self.ctx.checked_locations) self.progressbar.value = len(self.ctx.checked_locations)
else: else:
self.server_connect_button.text = "Connect" self.server_connect_button._button_text.text = "Connect"
self.server_connect_bar.readonly = False self.server_connect_bar.readonly = False
self.title = self.base_title + " " + Utils.__version__ self.title = self.base_title + " " + Utils.__version__
self.progressbar.value = 0 self.progressbar.value = 0
@@ -742,8 +1023,8 @@ class GameManager(App):
def enable_energy_link(self): def enable_energy_link(self):
if not hasattr(self, "energy_link_label"): if not hasattr(self, "energy_link_label"):
self.energy_link_label = Label(text="Energy Link: Standby", self.energy_link_label = MDLabel(text="Energy Link: Standby",
size_hint_x=None, width=150) size_hint_x=None, width=150, halign="center")
self.connect_layout.add_widget(self.energy_link_label) self.connect_layout.add_widget(self.energy_link_label)
def set_new_energy_link_value(self): def set_new_energy_link_value(self):
@@ -779,8 +1060,9 @@ class LogtoUI(logging.Handler):
self.on_log(self.format(record)) self.on_log(self.format(record))
class UILog(RecycleView): class UILog(MDRecycleView):
messages: typing.ClassVar[int] # comes from kv file messages: typing.ClassVar[int] # comes from kv file
adaptive_height = True
def __init__(self, *loggers_to_handle, **kwargs): def __init__(self, *loggers_to_handle, **kwargs):
super(UILog, self).__init__(**kwargs) super(UILog, self).__init__(**kwargs)
@@ -807,13 +1089,14 @@ class UILog(RecycleView):
element.height = element.texture_size[1] element.height = element.texture_size[1]
class HintLayout(BoxLayout): class HintLayout(MDBoxLayout):
orientation = "vertical" orientation = "vertical"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
boxlayout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30)) boxlayout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(40))
boxlayout.add_widget(Label(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(30))) boxlayout.add_widget(MDLabel(text="New Hint:", size_hint_x=None, size_hint_y=None,
height=dp(40), width=dp(75), halign="center", valign="center"))
boxlayout.add_widget(AutocompleteHintInput()) boxlayout.add_widget(AutocompleteHintInput())
self.add_widget(boxlayout) self.add_widget(boxlayout)
@@ -823,7 +1106,7 @@ class HintLayout(BoxLayout):
if fix_func: if fix_func:
fix_func() fix_func()
status_names: typing.Dict[HintStatus, str] = { status_names: typing.Dict[HintStatus, str] = {
HintStatus.HINT_FOUND: "Found", HintStatus.HINT_FOUND: "Found",
HintStatus.HINT_UNSPECIFIED: "Unspecified", HintStatus.HINT_UNSPECIFIED: "Unspecified",
@@ -846,8 +1129,7 @@ status_sort_weights: dict[HintStatus, int] = {
HintStatus.HINT_PRIORITY: 4, HintStatus.HINT_PRIORITY: 4,
} }
class HintLog(MDRecycleView):
class HintLog(RecycleView):
header = { header = {
"receiving": {"text": "[u]Receiving Player[/u]"}, "receiving": {"text": "[u]Receiving Player[/u]"},
"item": {"text": "[u]Item[/u]"}, "item": {"text": "[u]Item[/u]"},
@@ -858,7 +1140,7 @@ class HintLog(RecycleView):
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}}, "hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
"striped": True, "striped": True,
} }
data: list[typing.Any]
sort_key: str = "" sort_key: str = ""
reversed: bool = True reversed: bool = True
@@ -871,7 +1153,7 @@ class HintLog(RecycleView):
if not hints: # Fix the scrolling looking visually wrong in some edge cases if not hints: # Fix the scrolling looking visually wrong in some edge cases
self.scroll_y = 1.0 self.scroll_y = 1.0
data = [] data = []
ctx = App.get_running_app().ctx ctx = MDApp.get_running_app().ctx
for hint in hints: for hint in hints:
if not hint.get("status"): # Allows connecting to old servers if not hint.get("status"): # Allows connecting to old servers
hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED
@@ -921,6 +1203,7 @@ class HintLog(RecycleView):
class ApAsyncImage(AsyncImage): class ApAsyncImage(AsyncImage):
def is_uri(self, filename: str) -> bool: def is_uri(self, filename: str) -> bool:
if filename.startswith("ap:"): if filename.startswith("ap:"):
return True return True
@@ -935,7 +1218,8 @@ class ImageLoaderPkgutil(ImageLoaderBase):
data = pkgutil.get_data(module, path) data = pkgutil.get_data(module, path)
return self._bytes_to_data(data) return self._bytes_to_data(data)
def _bytes_to_data(self, data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]: @staticmethod
def _bytes_to_data(data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]:
loader = next(loader for loader in ImageLoader.loaders if loader.can_load_memory()) loader = next(loader for loader in ImageLoader.loaders if loader.can_load_memory())
return loader.load(loader, io.BytesIO(data)) return loader.load(loader, io.BytesIO(data))
@@ -965,7 +1249,23 @@ class E(ExceptionHandler):
class KivyJSONtoTextParser(JSONtoTextParser): class KivyJSONtoTextParser(JSONtoTextParser):
# dummy class to absorb kvlang definitions # dummy class to absorb kvlang definitions
class TextColors(Widget): class TextColors(Widget):
pass white: str = StringProperty("FFFFFF")
black: str = StringProperty("000000")
red: str = StringProperty("EE0000")
green: str = StringProperty("00FF7F")
yellow: str = StringProperty("FAFAD2")
blue: str = StringProperty("6495ED")
magenta: str = StringProperty("EE00EE")
cyan: str = StringProperty("00EEEE")
slateblue: str = StringProperty("6D8BE8")
plum: str = StringProperty("AF99EF")
salmon: str = StringProperty("FA8072")
orange: str = StringProperty("FF7700")
# KivyMD parameters
theme_style: str = StringProperty("Dark")
primary_palette: str = StringProperty("Lightsteelblue")
dynamic_scheme_name: str = StringProperty("VIBRANT")
dynamic_scheme_contrast: int = NumericProperty(0)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# we grab the color definitions from the .kv file, then overwrite the JSONtoTextParser default entries # we grab the color definitions from the .kv file, then overwrite the JSONtoTextParser default entries

View File

@@ -12,3 +12,6 @@ cython>=3.0.12
cymem>=2.0.11 cymem>=2.0.11
orjson>=3.10.15 orjson>=3.10.15
typing_extensions>=4.12.2 typing_extensions>=4.12.2
pyshortcuts>=1.9.1
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
kivymd>=2.0.1.dev0

View File

@@ -72,7 +72,6 @@ non_apworlds: Set[str] = {
"Ocarina of Time", "Ocarina of Time",
"Overcooked! 2", "Overcooked! 2",
"Raft", "Raft",
"Slay the Spire",
"Sudoku", "Sudoku",
"Super Mario 64", "Super Mario 64",
"VVVVVV", "VVVVVV",
@@ -154,7 +153,7 @@ if os.path.exists("X:/pw.txt"):
with open("X:/pw.txt", encoding="utf-8-sig") as f: with open("X:/pw.txt", encoding="utf-8-sig") as f:
pw = f.read() pw = f.read()
signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + \ signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + \
r'" /fd sha256 /tr http://timestamp.digicert.com/ ' r'" /fd sha256 /td sha256 /tr http://timestamp.digicert.com/ '
else: else:
signtool = None signtool = None
@@ -629,12 +628,13 @@ cx_Freeze.setup(
ext_modules=cythonize("_speedups.pyx"), ext_modules=cythonize("_speedups.pyx"),
options={ options={
"build_exe": { "build_exe": {
"packages": ["worlds", "kivy", "cymem", "websockets"], "packages": ["worlds", "kivy", "cymem", "websockets", "kivymd"],
"includes": [], "includes": [],
"excludes": ["numpy", "Cython", "PySide2", "PIL", "excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas", "zstandard"], "pandas"],
"zip_includes": [],
"zip_include_packages": ["*"], "zip_include_packages": ["*"],
"zip_exclude_packages": ["worlds", "sc2"], "zip_exclude_packages": ["worlds", "sc2", "kivymd"],
"include_files": [], # broken in cx 6.14.0, we use more special sauce now "include_files": [], # broken in cx 6.14.0, we use more special sauce now
"include_msvcr": False, "include_msvcr": False,
"replace_paths": ["*."], "replace_paths": ["*."],

View File

@@ -1,3 +1,4 @@
from typing import Callable
import unittest import unittest
from enum import IntEnum from enum import IntEnum
@@ -34,7 +35,7 @@ def generate_entrance_pair(region: Region, name_suffix: str, group: int):
def generate_disconnected_region_grid(multiworld: MultiWorld, grid_side_length: int, region_size: int = 0, def generate_disconnected_region_grid(multiworld: MultiWorld, grid_side_length: int, region_size: int = 0,
region_type: type[Region] = Region): region_creator: Callable[[str, int, MultiWorld], Region] = Region):
""" """
Generates a grid-like region structure for ER testing, where menu is connected to the top-left region, and each Generates a grid-like region structure for ER testing, where menu is connected to the top-left region, and each
region "in vanilla" has 2 2-way exits going either down or to the right, until reaching the goal region in the region "in vanilla" has 2 2-way exits going either down or to the right, until reaching the goal region in the
@@ -44,7 +45,7 @@ def generate_disconnected_region_grid(multiworld: MultiWorld, grid_side_length:
for col in range(grid_side_length): for col in range(grid_side_length):
index = row * grid_side_length + col index = row * grid_side_length + col
name = f"region{index}" name = f"region{index}"
region = region_type(name, 1, multiworld) region = region_creator(name, 1, multiworld)
multiworld.regions.append(region) multiworld.regions.append(region)
generate_locations(region_size, 1, region=region, tag=f"_{name}") generate_locations(region_size, 1, region=region, tag=f"_{name}")
@@ -65,8 +66,10 @@ class TestEntranceLookup(unittest.TestCase):
"""tests that get_targets shuffles targets between groups when requested""" """tests that get_targets shuffles targets between groups when requested"""
multiworld = generate_test_multiworld() multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5) generate_disconnected_region_grid(multiworld, 5)
exits_set = set([ex for region in multiworld.get_regions(1)
for ex in region.exits if not ex.connected_region])
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True) lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
er_targets = [entrance for region in multiworld.get_regions(1) er_targets = [entrance for region in multiworld.get_regions(1)
for entrance in region.entrances if not entrance.parent_region] for entrance in region.entrances if not entrance.parent_region]
for entrance in er_targets: for entrance in er_targets:
@@ -86,8 +89,10 @@ class TestEntranceLookup(unittest.TestCase):
"""tests that get_targets does not shuffle targets between groups when requested""" """tests that get_targets does not shuffle targets between groups when requested"""
multiworld = generate_test_multiworld() multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5) generate_disconnected_region_grid(multiworld, 5)
exits_set = set([ex for region in multiworld.get_regions(1)
for ex in region.exits if not ex.connected_region])
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True) lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
er_targets = [entrance for region in multiworld.get_regions(1) er_targets = [entrance for region in multiworld.get_regions(1)
for entrance in region.entrances if not entrance.parent_region] for entrance in region.entrances if not entrance.parent_region]
for entrance in er_targets: for entrance in er_targets:
@@ -99,6 +104,30 @@ class TestEntranceLookup(unittest.TestCase):
group_order = [prev := group.randomization_group for group in retrieved_targets if prev != group.randomization_group] group_order = [prev := group.randomization_group for group in retrieved_targets if prev != group.randomization_group]
self.assertEqual([ERTestGroups.TOP, ERTestGroups.BOTTOM], group_order) self.assertEqual([ERTestGroups.TOP, ERTestGroups.BOTTOM], group_order)
def test_selective_dead_ends(self):
"""test that entrances that EntranceLookup has not been told to consider are ignored when finding dead-ends"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
exits_set = set([ex for region in multiworld.get_regions(1)
for ex in region.exits if not ex.connected_region
and ex.name != "region20_right" and ex.name != "region21_left"])
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
er_targets = [entrance for region in multiworld.get_regions(1)
for entrance in region.entrances if not entrance.parent_region and
entrance.name != "region20_right" and entrance.name != "region21_left"]
for entrance in er_targets:
lookup.add(entrance)
# region 20 is the bottom left corner of the grid, and therefore only has a right entrance from region 21
# and a top entrance from region 15; since we've told lookup to ignore the right entrance from region 21,
# the top entrance from region 15 should be considered a dead-end
dead_end_region = multiworld.get_region("region20", 1)
for dead_end in dead_end_region.entrances:
if dead_end.name == "region20_top":
break
# there should be only this one dead-end
self.assertTrue(dead_end in lookup.dead_ends)
self.assertEqual(len(lookup.dead_ends), 1)
class TestBakeTargetGroupLookup(unittest.TestCase): class TestBakeTargetGroupLookup(unittest.TestCase):
def test_lookup_generation(self): def test_lookup_generation(self):
@@ -437,7 +466,7 @@ class TestRandomizeEntrances(unittest.TestCase):
entrance_type = CustomEntrance entrance_type = CustomEntrance
multiworld = generate_test_multiworld() multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5, region_type=CustomRegion) generate_disconnected_region_grid(multiworld, 5, region_creator=CustomRegion)
self.assertRaises(EntranceRandomizationError, randomize_entrances, multiworld.worlds[1], False, self.assertRaises(EntranceRandomizationError, randomize_entrances, multiworld.worlds[1], False,
directionally_matched_group_lookup) directionally_matched_group_lookup)

View File

@@ -47,13 +47,39 @@ class TestIDs(unittest.TestCase):
"""Test that a game doesn't have item id overlap within its own datapackage""" """Test that a game doesn't have item id overlap within its own datapackage"""
for gamename, world_type in AutoWorldRegister.world_types.items(): for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename): with self.subTest(game=gamename):
self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id)) len_item_id_to_name = len(world_type.item_id_to_name)
len_item_name_to_id = len(world_type.item_name_to_id)
if len_item_id_to_name != len_item_name_to_id:
self.assertCountEqual(
world_type.item_id_to_name.values(),
world_type.item_name_to_id.keys(),
"\nThese items have overlapping ids with other items in its own world")
self.assertCountEqual(
world_type.item_id_to_name.keys(),
world_type.item_name_to_id.values(),
"\nThese items have overlapping names with other items in its own world")
self.assertEqual(len_item_id_to_name, len_item_name_to_id)
def test_duplicate_location_ids(self): def test_duplicate_location_ids(self):
"""Test that a game doesn't have location id overlap within its own datapackage""" """Test that a game doesn't have location id overlap within its own datapackage"""
for gamename, world_type in AutoWorldRegister.world_types.items(): for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename): with self.subTest(game=gamename):
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id)) len_location_id_to_name = len(world_type.location_id_to_name)
len_location_name_to_id = len(world_type.location_name_to_id)
if len_location_id_to_name != len_location_name_to_id:
self.assertCountEqual(
world_type.location_id_to_name.values(),
world_type.location_name_to_id.keys(),
"\nThese locations have overlapping ids with other locations in its own world")
self.assertCountEqual(
world_type.location_id_to_name.keys(),
world_type.location_name_to_id.values(),
"\nThese locations have overlapping names with other locations in its own world")
self.assertEqual(len_location_id_to_name, len_location_name_to_id)
def test_postgen_datapackage(self): def test_postgen_datapackage(self):
"""Generates a solo multiworld and checks that the datapackage is still valid""" """Generates a solo multiworld and checks that the datapackage is still valid"""

View File

@@ -1,7 +1,11 @@
import unittest import unittest
from argparse import Namespace
from typing import Type
from BaseClasses import CollectionState from BaseClasses import CollectionState, MultiWorld
from worlds.AutoWorld import AutoWorldRegister, call_all from Fill import distribute_items_restrictive
from Options import ItemLinks
from worlds.AutoWorld import AutoWorldRegister, World, call_all
from . import setup_solo_multiworld from . import setup_solo_multiworld
@@ -83,6 +87,47 @@ class TestBase(unittest.TestCase):
multiworld = setup_solo_multiworld(world_type) multiworld = setup_solo_multiworld(world_type)
for item in multiworld.itempool: for item in multiworld.itempool:
self.assertIn(item.name, world_type.item_name_to_id) self.assertIn(item.name, world_type.item_name_to_id)
def test_item_links(self) -> None:
"""
Tests item link creation by creating a multiworld of 2 worlds for every game and linking their items together.
"""
def setup_link_multiworld(world: Type[World], link_replace: bool) -> None:
multiworld = MultiWorld(2)
multiworld.game = {1: world.game, 2: world.game}
multiworld.player_name = {1: "Linker 1", 2: "Linker 2"}
multiworld.set_seed()
item_link_group = [{
"name": "ItemLinkTest",
"item_pool": ["Everything"],
"link_replacement": link_replace,
"replacement_item": None,
}]
args = Namespace()
for name, option in world.options_dataclass.type_hints.items():
setattr(args, name, {1: option.from_any(option.default), 2: option.from_any(option.default)})
setattr(args, "item_links",
{1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)})
multiworld.set_options(args)
multiworld.set_item_links()
# groups get added to state during its constructor so this has to be after item links are set
multiworld.state = CollectionState(multiworld)
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances", "generate_basic")
for step in gen_steps:
call_all(multiworld, step)
# link the items together and attempt to fill
multiworld.link_items()
multiworld._all_state = None
call_all(multiworld, "pre_fill")
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
self.assertTrue(multiworld.can_beat_game(CollectionState(multiworld)), f"seed = {multiworld.seed}")
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Can generate with link replacement", game=game_name):
setup_link_multiworld(world_type, True)
with self.subTest("Can generate without link replacement", game=game_name):
setup_link_multiworld(world_type, False)
def test_itempool_not_modified(self): def test_itempool_not_modified(self):
"""Test that worlds don't modify the itempool after `create_items`""" """Test that worlds don't modify the itempool after `create_items`"""

View File

@@ -80,8 +80,8 @@ class Client:
"version": { "version": {
"class": "Version", "class": "Version",
"major": 0, "major": 0,
"minor": 4, "minor": 6,
"build": 6, "build": 0,
}, },
"items_handling": 0, "items_handling": 0,
"tags": [], "tags": [],

View File

@@ -35,6 +35,19 @@ class TestCacheSelf1(unittest.TestCase):
self.assertFalse(o1.func(1) is o1.func(2)) self.assertFalse(o1.func(1) is o1.func(2))
self.assertFalse(o1.func(1) is o2.func(1)) self.assertFalse(o1.func(1) is o2.func(1))
def test_cache_default(self) -> None:
class Cls:
@cache_self1
def func(self, _: Any = 1) -> object:
return object()
o1 = Cls()
o2 = Cls()
self.assertIs(o1.func(), o1.func())
self.assertIs(o1.func(1), o1.func())
self.assertIsNot(o1.func(2), o1.func())
self.assertIsNot(o1.func(), o2.func())
def test_gc(self) -> None: def test_gc(self) -> None:
# verify that we don't keep a global reference # verify that we don't keep a global reference
import gc import gc

View File

@@ -2,7 +2,7 @@ import unittest
from BaseClasses import PlandoOptions from BaseClasses import PlandoOptions
from worlds import AutoWorldRegister from worlds import AutoWorldRegister
from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet
class TestOptionPresets(unittest.TestCase): class TestOptionPresets(unittest.TestCase):
@@ -19,7 +19,7 @@ class TestOptionPresets(unittest.TestCase):
# pass in all plando options in case a preset wants to require certain plando options # pass in all plando options in case a preset wants to require certain plando options
# for some reason # for some reason
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions))) option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
supported_types = [NumericOption, OptionSet, OptionList, ItemDict] supported_types = [NumericOption, OptionSet, OptionList, OptionCounter]
if not any([issubclass(option.__class__, t) for t in supported_types]): if not any([issubclass(option.__class__, t) for t in supported_types]):
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' " self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "
f"is not a supported type for webhost. " f"is not a supported type for webhost. "

View File

@@ -12,7 +12,7 @@ def load_tests(loader, standard_tests, pattern):
all_tests = [ all_tests = [
test_case for folder in folders if os.path.exists(folder) test_case for folder in folders if os.path.exists(folder)
for test_collection in loader.discover(folder, top_level_dir=file_path) for test_collection in loader.discover(folder, top_level_dir=file_path)
for test_suite in test_collection for test_suite in test_collection if isinstance(test_suite, unittest.suite.TestSuite)
for test_case in test_suite for test_case in test_suite
] ]

View File

@@ -9,7 +9,8 @@ from worlds.LauncherComponents import Component, SuffixIdentifier, Type, compone
if TYPE_CHECKING: if TYPE_CHECKING:
from SNIClient import SNIContext from SNIClient import SNIContext
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe")) component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"),
description="A client for connecting to SNES consoles via Super Nintendo Interface.")
components.append(component) components.append(component)

View File

@@ -12,6 +12,7 @@ from typing import (Any, Callable, ClassVar, Dict, FrozenSet, Iterable, List, Ma
from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions
from BaseClasses import CollectionState from BaseClasses import CollectionState
from Utils import deprecate
if TYPE_CHECKING: if TYPE_CHECKING:
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
@@ -75,19 +76,20 @@ class AutoWorldRegister(type):
# TODO - remove this once all worlds use options dataclasses # TODO - remove this once all worlds use options dataclasses
if "options_dataclass" not in dct and "option_definitions" in dct: if "options_dataclass" not in dct and "option_definitions" in dct:
# TODO - switch to deprecate after a version # TODO - switch to deprecate after a version
if __debug__: deprecate(f"{name} Assigned options through option_definitions which is now deprecated. "
logging.warning(f"{name} Assigned options through option_definitions which is now deprecated. " "Please use options_dataclass instead.")
"Please use options_dataclass instead.")
dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(), dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
bases=(PerGameCommonOptions,)) bases=(PerGameCommonOptions,))
# construct class # construct class
new_class = super().__new__(mcs, name, bases, dct) new_class = super().__new__(mcs, name, bases, dct)
new_class.__file__ = sys.modules[new_class.__module__].__file__
if "game" in dct: if "game" in dct:
if dct["game"] in AutoWorldRegister.world_types: if dct["game"] in AutoWorldRegister.world_types:
raise RuntimeError(f"""Game {dct["game"]} already registered.""") raise RuntimeError(f"""Game {dct["game"]} already registered in
{AutoWorldRegister.world_types[dct["game"]].__file__} when attempting to register from
{new_class.__file__}.""")
AutoWorldRegister.world_types[dct["game"]] = new_class AutoWorldRegister.world_types[dct["game"]] = new_class
new_class.__file__ = sys.modules[new_class.__module__].__file__
if ".apworld" in new_class.__file__: if ".apworld" in new_class.__file__:
new_class.zip_path = pathlib.Path(new_class.__file__).parents[1] new_class.zip_path = pathlib.Path(new_class.__file__).parents[1]
if "settings_key" not in dct: if "settings_key" not in dct:
@@ -110,6 +112,16 @@ class AutoLogicRegister(type):
elif not item_name.startswith("__"): elif not item_name.startswith("__"):
if hasattr(CollectionState, item_name): if hasattr(CollectionState, item_name):
raise Exception(f"Name conflict on Logic Mixin {name} trying to overwrite {item_name}") raise Exception(f"Name conflict on Logic Mixin {name} trying to overwrite {item_name}")
assert callable(function) or "init_mixin" in dct, (
f"{name} defined class variable {item_name} without also having init_mixin.\n\n"
"Explanation:\n"
"Class variables that will be mutated need to be inintialized as instance variables in init_mixin.\n"
"If your LogicMixin variables aren't actually mutable / you don't intend to mutate them, "
"there is no point in using LogixMixin.\n"
"LogicMixin exists to track custom state variables that change when items are collected/removed."
)
setattr(CollectionState, item_name, function) setattr(CollectionState, item_name, function)
return new_class return new_class

View File

@@ -27,6 +27,8 @@ class Component:
""" """
display_name: str display_name: str
"""Used as the GUI button label and the component name in the CLI args""" """Used as the GUI button label and the component name in the CLI args"""
description: str
"""Optional description displayed on the GUI underneath the display name"""
type: Type type: Type
""" """
Enum "Type" classification of component intent, for filtering in the Launcher GUI Enum "Type" classification of component intent, for filtering in the Launcher GUI
@@ -58,8 +60,9 @@ class Component:
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None, def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None, cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None, func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None,
game_name: Optional[str] = None, supports_uri: Optional[bool] = False): game_name: Optional[str] = None, supports_uri: Optional[bool] = False, description: str = "") -> None:
self.display_name = display_name self.display_name = display_name
self.description = description
self.script_name = script_name self.script_name = script_name
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
self.icon = icon self.icon = icon
@@ -88,7 +91,6 @@ processes = weakref.WeakSet()
def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None: def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
global processes
import multiprocessing import multiprocessing
process = multiprocessing.Process(target=func, name=name, args=args) process = multiprocessing.Process(target=func, name=name, args=args)
process.start() process.start()

View File

@@ -238,14 +238,12 @@ class AdventureWorld(World):
def create_regions(self) -> None: def create_regions(self) -> None:
create_regions(self.options, self.multiworld, self.player, self.dragon_rooms) create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
set_rules = set_rules
def generate_basic(self) -> None:
self.multiworld.get_location("Chalice Home", self.player).place_locked_item( self.multiworld.get_location("Chalice Home", self.player).place_locked_item(
self.create_event("Victory", ItemClassification.progression)) self.create_event("Victory", ItemClassification.progression))
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
set_rules = set_rules
def pre_fill(self): def pre_fill(self):
# Place empty items in filler locations here, to limit # Place empty items in filler locations here, to limit
# the number of exported empty items and the density of stuff in overworld. # the number of exported empty items and the density of stuff in overworld.

View File

@@ -2,7 +2,7 @@ from typing import List, TYPE_CHECKING, Dict, Any
from schema import Schema, Optional from schema import Schema, Optional
from dataclasses import dataclass from dataclasses import dataclass
from worlds.AutoWorld import PerGameCommonOptions from worlds.AutoWorld import PerGameCommonOptions
from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle, OptionGroup from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle, OptionGroup, StartInventoryPool
if TYPE_CHECKING: if TYPE_CHECKING:
from . import HatInTimeWorld from . import HatInTimeWorld
@@ -625,6 +625,8 @@ class ParadeTrapWeight(Range):
@dataclass @dataclass
class AHITOptions(PerGameCommonOptions): class AHITOptions(PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
EndGoal: EndGoal EndGoal: EndGoal
ActRandomizer: ActRandomizer ActRandomizer: ActRandomizer
ActPlando: ActPlando ActPlando: ActPlando

View File

@@ -1,6 +1,6 @@
from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld
from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \ from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \
calculate_yarn_costs, alps_hooks calculate_yarn_costs, alps_hooks, junk_weights
from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \ from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
get_total_locations get_total_locations
@@ -78,6 +78,9 @@ class HatInTimeWorld(World):
self.nyakuza_thug_items: Dict[str, int] = {} self.nyakuza_thug_items: Dict[str, int] = {}
self.badge_seller_count: int = 0 self.badge_seller_count: int = 0
def get_filler_item_name(self) -> str:
return self.random.choices(list(junk_weights.keys()), weights=junk_weights.values(), k=1)[0]
def generate_early(self): def generate_early(self):
adjust_options(self) adjust_options(self)

View File

@@ -102,7 +102,7 @@ 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
(has_sword(state, player) or state.multiworld.swordless[player]) (has_sword(state, player) or state.multiworld.worlds[player].options.swordless)
) )
) and ) and
( (
@@ -111,7 +111,7 @@ def KholdstareDefeatRule(state, player: int) -> bool:
( (
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.worlds[player].options.swordless and
can_extend_magic(state, player, 16) can_extend_magic(state, player, 16)
) )
) )
@@ -137,7 +137,7 @@ def AgahnimDefeatRule(state, player: int) -> bool:
def GanonDefeatRule(state, player: int) -> bool: def GanonDefeatRule(state, player: int) -> bool:
if state.multiworld.swordless[player]: if state.multiworld.worlds[player].options.swordless:
return state.has('Hammer', player) and \ return state.has('Hammer', player) and \
has_fire_source(state, player) and \ has_fire_source(state, player) and \
state.has('Silver Bow', player) and \ state.has('Silver Bow', player) and \
@@ -146,7 +146,7 @@ def GanonDefeatRule(state, player: int) -> bool:
can_hurt = has_beam_sword(state, player) can_hurt = has_beam_sword(state, player)
common = can_hurt and has_fire_source(state, 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.glitches_required[player] != 'no_glitches': if state.multiworld.worlds[player].options.glitches_required != 'no_glitches':
# 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 can_shoot_arrows(state, player)) or state.has('Silver Bow', player) and can_shoot_arrows(state, player)) or
@@ -248,7 +248,7 @@ for location in boss_location_table:
def place_boss(world: "ALTTPWorld", boss: str, location: str, level: Optional[str]) -> None: def place_boss(world: "ALTTPWorld", boss: str, location: str, level: Optional[str]) -> None:
player = world.player player = world.player
if location == 'Ganons Tower' and world.multiworld.mode[player] == 'inverted': if location == 'Ganons Tower' and world.options.mode == 'inverted':
location = 'Inverted Ganons Tower' location = 'Inverted Ganons Tower'
logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else '')) logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else ''))
world.dungeons[location].bosses[level] = BossFactory(boss, player) world.dungeons[location].bosses[level] = BossFactory(boss, player)
@@ -260,9 +260,8 @@ def format_boss_location(location_name: str, level: str) -> str:
def place_bosses(world: "ALTTPWorld") -> None: def place_bosses(world: "ALTTPWorld") -> None:
multiworld = world.multiworld multiworld = world.multiworld
player = world.player
# will either be an int or a lower case string with ';' between options # will either be an int or a lower case string with ';' between options
boss_shuffle: Union[str, int] = multiworld.boss_shuffle[player].value boss_shuffle: Union[str, int] = world.options.boss_shuffle.value
already_placed_bosses: List[str] = [] already_placed_bosses: List[str] = []
remaining_locations: List[Tuple[str, str]] = [] remaining_locations: List[Tuple[str, str]] = []
# handle plando # handle plando

View File

@@ -66,7 +66,7 @@ def create_dungeons(world: "ALTTPWorld"):
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
dungeon = Dungeon(name, dungeon_regions, big_key, dungeon = Dungeon(name, dungeon_regions, big_key,
[] if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal else small_keys, [] if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal else small_keys,
dungeon_items, player) dungeon_items, player)
for item in dungeon.all_items: for item in dungeon.all_items:
item.dungeon = dungeon item.dungeon = dungeon
@@ -143,7 +143,7 @@ def create_dungeons(world: "ALTTPWorld"):
item_factory(['Small Key (Turtle Rock)'] * 6, world), item_factory(['Small Key (Turtle Rock)'] * 6, world),
item_factory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], world)) item_factory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], world))
if multiworld.mode[player] != 'inverted': if multiworld.worlds[player].options.mode != 'inverted':
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None, AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None,
item_factory(['Small Key (Agahnims Tower)'] * 4, world), []) item_factory(['Small Key (Agahnims Tower)'] * 4, world), [])
GT = make_dungeon('Ganons Tower', 'Agahnim2', GT = make_dungeon('Ganons Tower', 'Agahnim2',

View File

@@ -23,17 +23,17 @@ def link_entrances(world, player):
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
# if we do not shuffle, set default connections # if we do not shuffle, set default connections
if world.entrance_shuffle[player] == 'vanilla': if world.worlds[player].options.entrance_shuffle == 'vanilla':
for exitname, regionname in default_connections: for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
for exitname, regionname in default_dungeon_connections: for exitname, regionname in default_dungeon_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
elif world.entrance_shuffle[player] == 'dungeons_simple': elif world.worlds[player].options.entrance_shuffle == 'dungeons_simple':
for exitname, regionname in default_connections: for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
simple_shuffle_dungeons(world, player) simple_shuffle_dungeons(world, player)
elif world.entrance_shuffle[player] == 'dungeons_full': elif world.worlds[player].options.entrance_shuffle == 'dungeons_full':
for exitname, regionname in default_connections: for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
@@ -43,7 +43,7 @@ def link_entrances(world, player):
lw_entrances = list(LW_Dungeon_Entrances) lw_entrances = list(LW_Dungeon_Entrances)
dw_entrances = list(DW_Dungeon_Entrances) dw_entrances = list(DW_Dungeon_Entrances)
if world.mode[player] == 'standard': if world.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape # must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else: else:
@@ -56,7 +56,7 @@ def link_entrances(world, player):
dw_entrances.append('Ganons Tower') dw_entrances.append('Ganons Tower')
dungeon_exits.append('Ganons Tower Exit') dungeon_exits.append('Ganons Tower Exit')
if world.mode[player] == 'standard': if world.worlds[player].options.mode == 'standard':
# rest of hyrule castle must be in light world, so it has to be the one connected to east exit of desert # rest of hyrule castle must be in light world, so it has to be the one connected to east exit of desert
hyrule_castle_exits = [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')] hyrule_castle_exits = [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')]
connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, list(LW_Dungeon_Entrances_Must_Exit), player) connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
@@ -65,9 +65,9 @@ def link_entrances(world, player):
connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player) connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player) connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player)
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
elif world.entrance_shuffle[player] == 'dungeons_crossed': elif world.worlds[player].options.entrance_shuffle == 'dungeons_crossed':
crossed_shuffle_dungeons(world, player) crossed_shuffle_dungeons(world, player)
elif world.entrance_shuffle[player] == 'simple': elif world.worlds[player].options.entrance_shuffle == 'simple':
simple_shuffle_dungeons(world, player) simple_shuffle_dungeons(world, player)
old_man_entrances = list(Old_Man_Entrances) old_man_entrances = list(Old_Man_Entrances)
@@ -138,7 +138,7 @@ def link_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, single_doors, door_targets, player) connect_doors(world, single_doors, door_targets, player)
elif world.entrance_shuffle[player] == 'restricted': elif world.worlds[player].options.entrance_shuffle == 'restricted':
simple_shuffle_dungeons(world, player) simple_shuffle_dungeons(world, player)
lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances) lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
@@ -210,7 +210,7 @@ def link_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, doors, door_targets, player) connect_doors(world, doors, door_targets, player)
elif world.entrance_shuffle[player] == 'full': elif world.worlds[player].options.entrance_shuffle == 'full':
skull_woods_shuffle(world, player) skull_woods_shuffle(world, player)
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances) lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
@@ -227,7 +227,7 @@ def link_entrances(world, player):
# tavern back door cannot be shuffled yet # tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player) connect_doors(world, ['Tavern North'], ['Tavern'], player)
if world.mode[player] == 'standard': if world.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape # must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else: else:
@@ -264,7 +264,7 @@ def link_entrances(world, player):
pass pass
else: #if the cave wasn't placed we get here else: #if the cave wasn't placed we get here
connect_caves(world, lw_entrances, [], old_man_house, player) connect_caves(world, lw_entrances, [], old_man_house, player)
if world.mode[player] == 'standard': if world.worlds[player].options.mode == 'standard':
# rest of hyrule castle must be in light world # rest of hyrule castle must be in light world
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
@@ -316,7 +316,7 @@ def link_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, doors, door_targets, player) connect_doors(world, doors, door_targets, player)
elif world.entrance_shuffle[player] == 'crossed': elif world.worlds[player].options.entrance_shuffle == 'crossed':
skull_woods_shuffle(world, player) skull_woods_shuffle(world, player)
entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors) entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors)
@@ -331,7 +331,7 @@ def link_entrances(world, player):
# tavern back door cannot be shuffled yet # tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player) connect_doors(world, ['Tavern North'], ['Tavern'], player)
if world.mode[player] == 'standard': if world.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape # must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else: else:
@@ -348,7 +348,7 @@ def link_entrances(world, player):
#place must-exit caves #place must-exit caves
connect_mandatory_exits(world, entrances, caves, must_exits, player) connect_mandatory_exits(world, entrances, caves, must_exits, player)
if world.mode[player] == 'standard': if world.worlds[player].options.mode == 'standard':
# rest of hyrule castle must be dealt with # rest of hyrule castle must be dealt with
connect_caves(world, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) connect_caves(world, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
@@ -394,7 +394,7 @@ def link_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, entrances, door_targets, player) connect_doors(world, entrances, door_targets, player)
elif world.entrance_shuffle[player] == 'insanity': elif world.worlds[player].options.entrance_shuffle == 'insanity':
# beware ye who enter here # beware ye who enter here
entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']
@@ -431,7 +431,7 @@ def link_entrances(world, player):
# tavern back door cannot be shuffled yet # tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player) connect_doors(world, ['Tavern North'], ['Tavern'], player)
if world.mode[player] == 'standard': if world.worlds[player].options.mode == 'standard':
# cannot move uncle cave # cannot move uncle cave
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
@@ -464,7 +464,7 @@ def link_entrances(world, player):
connect_entrance(world, hole, hole_targets.pop(), player) connect_entrance(world, hole, hole_targets.pop(), player)
# hyrule castle handling # hyrule castle handling
if world.mode[player] == 'standard': if world.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape # must connect front of hyrule castle to do escape
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
@@ -544,12 +544,12 @@ def link_entrances(world, player):
else: else:
raise NotImplementedError( raise NotImplementedError(
f'{world.entrance_shuffle[player]} Shuffling not supported yet. Player {world.get_player_name(player)}') f'{world.worlds[player].options.entrance_shuffle} Shuffling not supported yet. Player {world.get_player_name(player)}')
if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
overworld_glitch_connections(world, player) overworld_glitch_connections(world, player)
# mandatory hybrid major glitches connections # mandatory hybrid major glitches connections
if world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']: if world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
underworld_glitch_connections(world, player) underworld_glitch_connections(world, player)
# check for swamp palace fix # check for swamp palace fix
@@ -584,17 +584,17 @@ def link_inverted_entrances(world, player):
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
# if we do not shuffle, set default connections # if we do not shuffle, set default connections
if world.entrance_shuffle[player] == 'vanilla': if world.worlds[player].options.entrance_shuffle == 'vanilla':
for exitname, regionname in inverted_default_connections: for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
for exitname, regionname in inverted_default_dungeon_connections: for exitname, regionname in inverted_default_dungeon_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
elif world.entrance_shuffle[player] == 'dungeons_simple': elif world.worlds[player].options.entrance_shuffle == 'dungeons_simple':
for exitname, regionname in inverted_default_connections: for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
simple_shuffle_dungeons(world, player) simple_shuffle_dungeons(world, player)
elif world.entrance_shuffle[player] == 'dungeons_full': elif world.worlds[player].options.entrance_shuffle == 'dungeons_full':
for exitname, regionname in inverted_default_connections: for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
@@ -649,9 +649,9 @@ def link_inverted_entrances(world, player):
connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player) connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player)
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
elif world.entrance_shuffle[player] == 'dungeons_crossed': elif world.worlds[player].options.entrance_shuffle == 'dungeons_crossed':
inverted_crossed_shuffle_dungeons(world, player) inverted_crossed_shuffle_dungeons(world, player)
elif world.entrance_shuffle[player] == 'simple': elif world.worlds[player].options.entrance_shuffle == 'simple':
simple_shuffle_dungeons(world, player) simple_shuffle_dungeons(world, player)
old_man_entrances = list(Inverted_Old_Man_Entrances) old_man_entrances = list(Inverted_Old_Man_Entrances)
@@ -748,7 +748,7 @@ def link_inverted_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, single_doors, door_targets, player) connect_doors(world, single_doors, door_targets, player)
elif world.entrance_shuffle[player] == 'restricted': elif world.worlds[player].options.entrance_shuffle == 'restricted':
simple_shuffle_dungeons(world, player) simple_shuffle_dungeons(world, player)
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Single_Cave_Doors) lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Single_Cave_Doors)
@@ -833,7 +833,7 @@ def link_inverted_entrances(world, player):
doors = lw_entrances + dw_entrances doors = lw_entrances + dw_entrances
# place remaining doors # place remaining doors
connect_doors(world, doors, door_targets, player) connect_doors(world, doors, door_targets, player)
elif world.entrance_shuffle[player] == 'full': elif world.worlds[player].options.entrance_shuffle == 'full':
skull_woods_shuffle(world, player) skull_woods_shuffle(world, player)
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors) lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors)
@@ -984,7 +984,7 @@ def link_inverted_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, doors, door_targets, player) connect_doors(world, doors, door_targets, player)
elif world.entrance_shuffle[player] == 'crossed': elif world.worlds[player].options.entrance_shuffle == 'crossed':
skull_woods_shuffle(world, player) skull_woods_shuffle(world, player)
entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors) entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors)
@@ -1095,7 +1095,7 @@ def link_inverted_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, entrances, door_targets, player) connect_doors(world, entrances, door_targets, player)
elif world.entrance_shuffle[player] == 'insanity': elif world.worlds[player].options.entrance_shuffle == 'insanity':
# beware ye who enter here # beware ye who enter here
entrances = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Entrance (South)'] entrances = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Entrance (South)']
@@ -1254,10 +1254,10 @@ def link_inverted_entrances(world, player):
else: else:
raise NotImplementedError('Shuffling not supported yet') raise NotImplementedError('Shuffling not supported yet')
if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
overworld_glitch_connections(world, player) overworld_glitch_connections(world, player)
# mandatory hybrid major glitches connections # mandatory hybrid major glitches connections
if world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']: if world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
underworld_glitch_connections(world, player) underworld_glitch_connections(world, player)
# patch swamp drain # patch swamp drain
@@ -1349,7 +1349,7 @@ def scramble_holes(world, player):
else: else:
hole_targets.append(('Pyramid Exit', 'Pyramid')) hole_targets.append(('Pyramid Exit', 'Pyramid'))
if world.mode[player] == 'standard': if world.worlds[player].options.mode == 'standard':
# cannot move uncle cave # cannot move uncle cave
connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
@@ -1358,14 +1358,14 @@ def scramble_holes(world, player):
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed # do not shuffle sanctuary into pyramid hole unless shuffle is crossed
if world.entrance_shuffle[player] == 'crossed': if world.worlds[player].options.entrance_shuffle == 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
if world.shuffle_ganon: if world.shuffle_ganon:
world.random.shuffle(hole_targets) world.random.shuffle(hole_targets)
exit, target = hole_targets.pop() exit, target = hole_targets.pop()
connect_two_way(world, 'Pyramid Entrance', exit, player) connect_two_way(world, 'Pyramid Entrance', exit, player)
connect_entrance(world, 'Pyramid Hole', target, player) connect_entrance(world, 'Pyramid Hole', target, player)
if world.entrance_shuffle[player] != 'crossed': if world.worlds[player].options.entrance_shuffle != 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
world.random.shuffle(hole_targets) world.random.shuffle(hole_targets)
@@ -1400,14 +1400,14 @@ def scramble_inverted_holes(world, player):
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed # do not shuffle sanctuary into pyramid hole unless shuffle is crossed
if world.entrance_shuffle[player] == 'crossed': if world.worlds[player].options.entrance_shuffle == 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
if world.shuffle_ganon: if world.shuffle_ganon:
world.random.shuffle(hole_targets) world.random.shuffle(hole_targets)
exit, target = hole_targets.pop() exit, target = hole_targets.pop()
connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) connect_two_way(world, 'Inverted Pyramid Entrance', exit, player)
connect_entrance(world, 'Inverted Pyramid Hole', target, player) connect_entrance(world, 'Inverted Pyramid Hole', target, player)
if world.entrance_shuffle[player] != 'crossed': if world.worlds[player].options.entrance_shuffle != 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
world.random.shuffle(hole_targets) world.random.shuffle(hole_targets)
@@ -1430,15 +1430,15 @@ def connect_random(world, exitlist, targetlist, player, two_way=False):
def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
# Keeps track of entrances that cannot be used to access each exit / cave # Keeps track of entrances that cannot be used to access each exit / cave
if world.mode[player] == 'inverted': if world.worlds[player].options.mode == 'inverted':
invalid_connections = Inverted_Must_Exit_Invalid_Connections.copy() invalid_connections = Inverted_Must_Exit_Invalid_Connections.copy()
else: else:
invalid_connections = Must_Exit_Invalid_Connections.copy() invalid_connections = Must_Exit_Invalid_Connections.copy()
invalid_cave_connections = defaultdict(set) invalid_cave_connections = defaultdict(set)
if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
from . import OverworldGlitchRules from . import OverworldGlitchRules
for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'): for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.worlds[player].options.mode == 'inverted'):
invalid_connections[entrance] = set() invalid_connections[entrance] = set()
if entrance in must_be_exits: if entrance in must_be_exits:
must_be_exits.remove(entrance) must_be_exits.remove(entrance)
@@ -1449,7 +1449,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
world.random.shuffle(caves) world.random.shuffle(caves)
# Handle inverted Aga Tower - if it depends on connections, then so does Hyrule Castle Ledge # Handle inverted Aga Tower - if it depends on connections, then so does Hyrule Castle Ledge
if world.mode[player] == 'inverted': if world.worlds[player].options.mode == 'inverted':
for entrance in invalid_connections: for entrance in invalid_connections:
if world.get_entrance(entrance, player).connected_region == world.get_region('Inverted Agahnims Tower', if world.get_entrance(entrance, player).connected_region == world.get_region('Inverted Agahnims Tower',
player): player):
@@ -1490,7 +1490,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit]) entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit])
cave_entrances.append(entrance) cave_entrances.append(entrance)
entrances.remove(entrance) entrances.remove(entrance)
connect_two_way(world,entrance,cave_exit, player) connect_two_way(world, entrance, cave_exit, player)
if entrance not in invalid_connections: if entrance not in invalid_connections:
invalid_connections[exit] = set() invalid_connections[exit] = set()
if all(entrance in invalid_connections for entrance in cave_entrances): if all(entrance in invalid_connections for entrance in cave_entrances):
@@ -1564,7 +1564,7 @@ def simple_shuffle_dungeons(world, player):
dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace'] dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace']
dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit'] dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit']
if world.mode[player] != 'inverted': if world.worlds[player].options.mode != 'inverted':
if not world.shuffle_ganon: if not world.shuffle_ganon:
connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
else: else:
@@ -1579,13 +1579,13 @@ def simple_shuffle_dungeons(world, player):
# mix up 4 door dungeons # mix up 4 door dungeons
multi_dungeons = ['Desert', 'Turtle Rock'] multi_dungeons = ['Desert', 'Turtle Rock']
if world.mode[player] == 'open' or (world.mode[player] == 'inverted' and world.shuffle_ganon): if world.worlds[player].options.mode == 'open' or (world.worlds[player].options.mode == 'inverted' and world.shuffle_ganon):
multi_dungeons.append('Hyrule Castle') multi_dungeons.append('Hyrule Castle')
world.random.shuffle(multi_dungeons) world.random.shuffle(multi_dungeons)
dp_target = multi_dungeons[0] dp_target = multi_dungeons[0]
tr_target = multi_dungeons[1] tr_target = multi_dungeons[1]
if world.mode[player] not in ['open', 'inverted'] or (world.mode[player] == 'inverted' and world.shuffle_ganon is False): if world.worlds[player].options.mode not in ['open', 'inverted'] or (world.worlds[player].options.mode == 'inverted' and world.shuffle_ganon is False):
# place hyrule castle as intended # place hyrule castle as intended
hc_target = 'Hyrule Castle' hc_target = 'Hyrule Castle'
else: else:
@@ -1593,7 +1593,7 @@ def simple_shuffle_dungeons(world, player):
# ToDo improve this? # ToDo improve this?
if world.mode[player] != 'inverted': if world.worlds[player].options.mode != 'inverted':
if hc_target == 'Hyrule Castle': if hc_target == 'Hyrule Castle':
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player)
@@ -1708,7 +1708,7 @@ def crossed_shuffle_dungeons(world, player: int):
dungeon_entrances.append('Ganons Tower') dungeon_entrances.append('Ganons Tower')
dungeon_exits.append('Ganons Tower Exit') dungeon_exits.append('Ganons Tower Exit')
if world.mode[player] == 'standard': if world.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape # must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else: else:
@@ -1718,7 +1718,7 @@ def crossed_shuffle_dungeons(world, player: int):
connect_mandatory_exits(world, dungeon_entrances, dungeon_exits, connect_mandatory_exits(world, dungeon_entrances, dungeon_exits,
LW_Dungeon_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit, player) LW_Dungeon_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit, player)
if world.mode[player] == 'standard': if world.worlds[player].options.mode == 'standard':
connect_caves(world, dungeon_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) connect_caves(world, dungeon_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
connect_caves(world, dungeon_entrances, [], dungeon_exits, player) connect_caves(world, dungeon_entrances, [], dungeon_exits, player)
@@ -1823,14 +1823,14 @@ lookup = {
def plando_connect(world, player: int): def plando_connect(world, player: int):
if world.plando_connections[player]: if world.worlds[player].options.plando_connections:
for connection in world.plando_connections[player]: for connection in world.worlds[player].options.plando_connections:
func = lookup[connection.direction] func = lookup[connection.direction]
try: try:
func(world, connection.entrance, connection.exit, player) func(world, connection.entrance, connection.exit, player)
except Exception as e: except Exception as e:
raise Exception(f"Could not connect using {connection}") from e raise Exception(f"Could not connect using {connection}") from e
if world.mode[player] != 'inverted': if world.worlds[player].options.mode != 'inverted':
mark_light_world_regions(world, player) mark_light_world_regions(world, player)
else: else:
mark_dark_world_regions(world, player) mark_dark_world_regions(world, player)

View File

@@ -226,25 +226,25 @@ def generate_itempool(world):
player = world.player player = world.player
multiworld = world.multiworld multiworld = world.multiworld
if multiworld.item_pool[player].current_key not in difficulties: if world.options.item_pool.current_key not in difficulties:
raise NotImplementedError(f"Diffulty {multiworld.item_pool[player]}") raise NotImplementedError(f"Diffulty {world.options.item_pool}")
if multiworld.goal[player] not in ('ganon', 'pedestal', 'bosses', 'triforce_hunt', 'local_triforce_hunt', if world.options.goal not in ('ganon', 'pedestal', 'bosses', 'triforce_hunt', 'local_triforce_hunt',
'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'crystals',
'ganon_pedestal'): 'ganon_pedestal'):
raise NotImplementedError(f"Goal {multiworld.goal[player]} for player {player}") raise NotImplementedError(f"Goal {world.options.goal} for player {player}")
if multiworld.mode[player] not in ('open', 'standard', 'inverted'): if world.options.mode not in ('open', 'standard', 'inverted'):
raise NotImplementedError(f"Mode {multiworld.mode[player]} for player {player}") raise NotImplementedError(f"Mode {world.options.mode} for player {player}")
if multiworld.timer[player] not in (False, 'display', 'timed', 'timed_ohko', 'ohko', 'timed_countdown'): if world.options.timer not in (False, 'display', 'timed', 'timed_ohko', 'ohko', 'timed_countdown'):
raise NotImplementedError(f"Timer {multiworld.timer[player]} for player {player}") raise NotImplementedError(f"Timer {world.options.timer} for player {player}")
if multiworld.timer[player] in ['ohko', 'timed_ohko']: if world.options.timer in ['ohko', 'timed_ohko']:
world.can_take_damage = False world.can_take_damage = False
if multiworld.goal[player] in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']: if world.options.goal in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']:
multiworld.push_item(multiworld.get_location('Ganon', player), item_factory('Nothing', world), False) multiworld.push_item(multiworld.get_location('Ganon', player), item_factory('Nothing', world), False)
else: else:
multiworld.push_item(multiworld.get_location('Ganon', player), item_factory('Triforce', world), False) multiworld.push_item(multiworld.get_location('Ganon', player), item_factory('Triforce', world), False)
if multiworld.goal[player] in ['triforce_hunt', 'local_triforce_hunt']: if world.options.goal in ['triforce_hunt', 'local_triforce_hunt']:
region = multiworld.get_region('Light World', player) region = multiworld.get_region('Light World', player)
loc = ALttPLocation(player, "Murahdahla", parent=region) loc = ALttPLocation(player, "Murahdahla", parent=region)
@@ -288,7 +288,7 @@ def generate_itempool(world):
for item in precollected_items: for item in precollected_items:
multiworld.push_precollected(item_factory(item, world)) multiworld.push_precollected(item_factory(item, world))
if multiworld.mode[player] == 'standard' and not has_melee_weapon(multiworld.state, player): if world.options.mode == 'standard' and not has_melee_weapon(multiworld.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
@@ -304,10 +304,10 @@ def generate_itempool(world):
elif item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']: elif item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
if item not in possible_weapons: if item not in possible_weapons:
possible_weapons.append(item) possible_weapons.append(item)
elif (item == 'Bombs (10)' and (not multiworld.bombless_start[player]) and item not in elif (item == 'Bombs (10)' and (not world.options.bombless_start) and item not in
possible_weapons): possible_weapons):
possible_weapons.append(item) possible_weapons.append(item)
elif (item in ['Bomb Upgrade (+10)', 'Bomb Upgrade (50)'] and multiworld.bombless_start[player] and item elif (item in ['Bomb Upgrade (+10)', 'Bomb Upgrade (50)'] and world.options.bombless_start and item
not in possible_weapons): not in possible_weapons):
possible_weapons.append(item) possible_weapons.append(item)
@@ -315,21 +315,21 @@ def generate_itempool(world):
placed_items["Link's Uncle"] = starting_weapon placed_items["Link's Uncle"] = starting_weapon
pool.remove(starting_weapon) pool.remove(starting_weapon)
if (placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Bomb Upgrade (+10)', if (placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Bomb Upgrade (+10)',
'Bomb Upgrade (50)', 'Cane of Somaria', 'Cane of Byrna'] and multiworld.enemy_health[player] not in ['default', 'easy']): 'Bomb Upgrade (50)', 'Cane of Somaria', 'Cane of Byrna'] and world.options.enemy_health not in ['default', 'easy']):
if multiworld.bombless_start[player] and "Bomb Upgrade" not in placed_items["Link's Uncle"]: if world.options.bombless_start and "Bomb Upgrade" not in placed_items["Link's Uncle"]:
if 'Bow' in placed_items["Link's Uncle"]: if 'Bow' in placed_items["Link's Uncle"]:
multiworld.worlds[player].escape_assist.append('arrows') world.escape_assist.append('arrows')
elif 'Cane' in placed_items["Link's Uncle"]: elif 'Cane' in placed_items["Link's Uncle"]:
multiworld.worlds[player].escape_assist.append('magic') world.escape_assist.append('magic')
else: else:
multiworld.worlds[player].escape_assist.append('bombs') world.escape_assist.append('bombs')
for (location, item) in placed_items.items(): for (location, item) in placed_items.items():
multiworld.get_location(location, player).place_locked_item(item_factory(item, world)) multiworld.get_location(location, player).place_locked_item(item_factory(item, world))
items = item_factory(pool, world) items = item_factory(pool, world)
# convert one Progressive Bow into Progressive Bow (Alt), in ID only, for ganon silvers hint text # convert one Progressive Bow into Progressive Bow (Alt), in ID only, for ganon silvers hint text
if multiworld.worlds[player].has_progressive_bows: if world.has_progressive_bows:
for item in items: for item in items:
if item.code == 0x64: # Progressive Bow if item.code == 0x64: # Progressive Bow
item.code = 0x65 # Progressive Bow (Alt) item.code = 0x65 # Progressive Bow (Alt)
@@ -338,21 +338,21 @@ def generate_itempool(world):
if clock_mode: if clock_mode:
world.clock_mode = clock_mode world.clock_mode = clock_mode
multiworld.worlds[player].treasure_hunt_required = treasure_hunt_required % 999 world.treasure_hunt_required = treasure_hunt_required % 999
multiworld.worlds[player].treasure_hunt_total = treasure_hunt_total world.treasure_hunt_total = treasure_hunt_total
dungeon_items = [item for item in get_dungeon_item_pool_player(world) dungeon_items = [item for item in get_dungeon_item_pool_player(world)
if item.name not in multiworld.worlds[player].dungeon_local_item_names] if item.name not in world.dungeon_local_item_names]
for key_loc in key_drop_data: for key_loc in key_drop_data:
key_data = key_drop_data[key_loc] key_data = key_drop_data[key_loc]
drop_item = item_factory(key_data[3], world) drop_item = item_factory(key_data[3], world)
if not multiworld.key_drop_shuffle[player]: if not world.options.key_drop_shuffle:
if drop_item in dungeon_items: if drop_item in dungeon_items:
dungeon_items.remove(drop_item) dungeon_items.remove(drop_item)
else: else:
dungeon = drop_item.name.split("(")[1].split(")")[0] dungeon = drop_item.name.split("(")[1].split(")")[0]
if multiworld.mode[player] == 'inverted': if world.options.mode == 'inverted':
if dungeon == "Agahnims Tower": if dungeon == "Agahnims Tower":
dungeon = "Inverted Agahnims Tower" dungeon = "Inverted Agahnims Tower"
if dungeon == "Ganons Tower": if dungeon == "Ganons Tower":
@@ -365,7 +365,7 @@ def generate_itempool(world):
loc = multiworld.get_location(key_loc, player) loc = multiworld.get_location(key_loc, player)
loc.place_locked_item(drop_item) loc.place_locked_item(drop_item)
loc.address = None loc.address = None
elif "Small" in key_data[3] and multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal: elif "Small" in key_data[3] and world.options.small_key_shuffle == small_key_shuffle.option_universal:
# key drop shuffle and universal keys are on. Add universal keys in place of key drop keys. # key drop shuffle and universal keys are on. Add universal keys in place of key drop keys.
multiworld.itempool.append(item_factory(GetBeemizerItem(multiworld, player, 'Small Key (Universal)'), world)) multiworld.itempool.append(item_factory(GetBeemizerItem(multiworld, player, 'Small Key (Universal)'), world))
dungeon_item_replacements = sum(difficulties[world.options.item_pool.current_key].extras, []) * 2 dungeon_item_replacements = sum(difficulties[world.options.item_pool.current_key].extras, []) * 2
@@ -373,10 +373,10 @@ def generate_itempool(world):
for x in range(len(dungeon_items)-1, -1, -1): for x in range(len(dungeon_items)-1, -1, -1):
item = dungeon_items[x] item = dungeon_items[x]
if ((multiworld.small_key_shuffle[player] == small_key_shuffle.option_start_with and item.type == 'SmallKey') if ((world.options.small_key_shuffle == small_key_shuffle.option_start_with and item.type == 'SmallKey')
or (multiworld.big_key_shuffle[player] == big_key_shuffle.option_start_with and item.type == 'BigKey') or (world.options.big_key_shuffle == big_key_shuffle.option_start_with and item.type == 'BigKey')
or (multiworld.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass') or (world.options.compass_shuffle == compass_shuffle.option_start_with and item.type == 'Compass')
or (multiworld.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')): or (world.options.map_shuffle == map_shuffle.option_start_with and item.type == 'Map')):
dungeon_items.pop(x) dungeon_items.pop(x)
multiworld.push_precollected(item) multiworld.push_precollected(item)
multiworld.itempool.append(item_factory(dungeon_item_replacements.pop(), world)) multiworld.itempool.append(item_factory(dungeon_item_replacements.pop(), world))
@@ -384,7 +384,7 @@ def generate_itempool(world):
set_up_shops(multiworld, player) set_up_shops(multiworld, player)
if multiworld.retro_bow[player]: if world.options.retro_bow:
shop_items = 0 shop_items = 0
shop_locations = [location for shop_locations in (shop.region.locations for shop in multiworld.shops if shop_locations = [location for shop_locations in (shop.region.locations for shop in multiworld.shops if
shop.type == ShopType.Shop and shop.region.player == player) for location in shop_locations if shop.type == ShopType.Shop and shop.region.player == player) for location in shop_locations if
@@ -395,12 +395,12 @@ def generate_itempool(world):
else: else:
shop_items += 1 shop_items += 1
else: else:
shop_items = min(multiworld.shop_item_slots[player], 30 if multiworld.include_witch_hut[player] else 27) shop_items = min(world.options.shop_item_slots, 30 if world.options.include_witch_hut else 27)
if multiworld.shuffle_capacity_upgrades[player]: if world.options.shuffle_capacity_upgrades:
shop_items += 2 shop_items += 2
chance_100 = int(multiworld.retro_bow[player]) * 0.25 + int( chance_100 = int(world.options.retro_bow) * 0.25 + int(
multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal) * 0.5 world.options.small_key_shuffle == small_key_shuffle.option_universal) * 0.5
for _ in range(shop_items): for _ in range(shop_items):
if multiworld.random.random() < chance_100: if multiworld.random.random() < chance_100:
items.append(item_factory(GetBeemizerItem(multiworld, player, "Rupees (100)"), world)) items.append(item_factory(GetBeemizerItem(multiworld, player, "Rupees (100)"), world))
@@ -410,19 +410,19 @@ def generate_itempool(world):
multiworld.random.shuffle(items) multiworld.random.shuffle(items)
pool_count = len(items) pool_count = len(items)
new_items = ["Triforce Piece" for _ in range(additional_triforce_pieces)] new_items = ["Triforce Piece" for _ in range(additional_triforce_pieces)]
if multiworld.shuffle_capacity_upgrades[player] or multiworld.bombless_start[player]: if world.options.shuffle_capacity_upgrades or world.options.bombless_start:
progressive = multiworld.progressive[player] progressive = world.options.progressive
progressive = multiworld.random.choice([True, False]) if progressive == 'grouped_random' else progressive == 'on' progressive = multiworld.random.choice([True, False]) if progressive == 'grouped_random' else progressive == 'on'
if multiworld.shuffle_capacity_upgrades[player] == "on_combined": if world.options.shuffle_capacity_upgrades == "on_combined":
new_items.append("Bomb Upgrade (50)") new_items.append("Bomb Upgrade (50)")
elif multiworld.shuffle_capacity_upgrades[player] == "on": elif world.options.shuffle_capacity_upgrades == "on":
new_items += ["Bomb Upgrade (+5)"] * 6 new_items += ["Bomb Upgrade (+5)"] * 6
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)") new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
if multiworld.shuffle_capacity_upgrades[player] != "on_combined" and multiworld.bombless_start[player]: if world.options.shuffle_capacity_upgrades != "on_combined" and world.options.bombless_start:
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)") new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
if multiworld.shuffle_capacity_upgrades[player] and not multiworld.retro_bow[player]: if world.options.shuffle_capacity_upgrades and not world.options.retro_bow:
if multiworld.shuffle_capacity_upgrades[player] == "on_combined": if world.options.shuffle_capacity_upgrades == "on_combined":
new_items += ["Arrow Upgrade (70)"] new_items += ["Arrow Upgrade (70)"]
else: else:
new_items += ["Arrow Upgrade (+5)"] * 6 new_items += ["Arrow Upgrade (+5)"] * 6
@@ -481,7 +481,7 @@ def generate_itempool(world):
if len(items) < pool_count: if len(items) < pool_count:
items += removed_filler[len(items) - pool_count:] items += removed_filler[len(items) - pool_count:]
if multiworld.randomize_cost_types[player]: if world.options.randomize_cost_types:
# Heart and Arrow costs require all Heart Container/Pieces and Arrow Upgrades to be advancement items for logic # Heart and Arrow costs require all Heart Container/Pieces and Arrow Upgrades to be advancement items for logic
for item in items: for item in items:
if item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart"): if item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart"):
@@ -490,21 +490,25 @@ def generate_itempool(world):
# Otherwise, logic has some branches where having 4 hearts is one possible requirement (of several alternatives) # Otherwise, logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
# rather than making all hearts/heart pieces progression items (which slows down generation considerably) # rather than making all hearts/heart pieces progression items (which slows down generation considerably)
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode) # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
if multiworld.item_pool[player] in ['easy', 'normal', 'hard'] and not (multiworld.custom and multiworld.customitemarray[30] == 0): try:
next(item for item in items if item.name == 'Boss Heart Container').classification = ItemClassification.progression next(item for item in items if item.name == 'Boss Heart Container').classification \
elif multiworld.item_pool[player] in ['expert'] and not (multiworld.custom and multiworld.customitemarray[29] < 4): |= ItemClassification.progression
except StopIteration:
adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart') adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
for i in range(4): for i in range(4):
next(adv_heart_pieces).classification = ItemClassification.progression try:
next(adv_heart_pieces).classification |= ItemClassification.progression
except StopIteration:
break # logically health tanking is an option, so rules should still resolve to something beatable
world.required_medallions = (multiworld.misery_mire_medallion[player].current_key.title(), world.required_medallions = (world.options.misery_mire_medallion.current_key.title(),
multiworld.turtle_rock_medallion[player].current_key.title()) world.options.turtle_rock_medallion.current_key.title())
place_bosses(world) place_bosses(world)
multiworld.itempool += items multiworld.itempool += items
if multiworld.retro_caves[player]: if world.options.retro_caves:
set_up_take_anys(multiworld, world, player) # depends on world.itempool to be set set_up_take_anys(multiworld, world, player) # depends on world.itempool to be set
@@ -527,7 +531,7 @@ take_any_locations.sort()
def set_up_take_anys(multiworld, world, player): def set_up_take_anys(multiworld, world, player):
# these are references, do not modify these lists in-place # these are references, do not modify these lists in-place
if multiworld.mode[player] == 'inverted': if world.options.mode == 'inverted':
take_any_locs = take_any_locations_inverted take_any_locs = take_any_locations_inverted
else: else:
take_any_locs = take_any_locations take_any_locs = take_any_locations
@@ -578,14 +582,14 @@ def set_up_take_anys(multiworld, world, player):
def get_pool_core(world, player: int): def get_pool_core(world, player: int):
shuffle = world.entrance_shuffle[player].current_key shuffle = world.worlds[player].options.entrance_shuffle.current_key
difficulty = world.item_pool[player].current_key difficulty = world.worlds[player].options.item_pool.current_key
timer = world.timer[player].current_key timer = world.worlds[player].options.timer.current_key
goal = world.goal[player].current_key goal = world.worlds[player].options.goal.current_key
mode = world.mode[player].current_key mode = world.worlds[player].options.mode.current_key
swordless = world.swordless[player] swordless = world.worlds[player].options.swordless
retro_bow = world.retro_bow[player] retro_bow = world.worlds[player].options.retro_bow
logic = world.glitches_required[player] logic = world.worlds[player].options.glitches_required
pool = [] pool = []
placed_items = {} placed_items = {}
@@ -602,11 +606,11 @@ def get_pool_core(world, player: int):
placed_items[loc] = item placed_items[loc] = item
# provide boots to major glitch dependent seeds # provide boots to major glitch dependent seeds
if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.glitch_boots[player]: if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.glitch_boots:
precollected_items.append('Pegasus Boots') precollected_items.append('Pegasus Boots')
pool.remove('Pegasus Boots') pool.remove('Pegasus Boots')
pool.append('Rupees (20)') pool.append('Rupees (20)')
want_progressives = world.progressive[player].want_progressives want_progressives = world.worlds[player].options.progressive.want_progressives
if want_progressives(world.random): if want_progressives(world.random):
pool.extend(diff.progressiveglove) pool.extend(diff.progressiveglove)
@@ -680,22 +684,22 @@ def get_pool_core(world, player: int):
additional_pieces_to_place = 0 additional_pieces_to_place = 0
if 'triforce_hunt' in goal: if 'triforce_hunt' in goal:
if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra: if world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_extra:
treasure_hunt_total = (world.triforce_pieces_required[player].value treasure_hunt_total = (world.worlds[player].options.triforce_pieces_required.value
+ world.triforce_pieces_extra[player].value) + world.worlds[player].options.triforce_pieces_extra.value)
elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage: elif world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_percentage:
percentage = float(world.triforce_pieces_percentage[player].value) / 100 percentage = float(world.worlds[player].options.triforce_pieces_percentage.value) / 100
treasure_hunt_total = int(round(world.triforce_pieces_required[player].value * percentage, 0)) treasure_hunt_total = int(round(world.worlds[player].options.triforce_pieces_required.value * percentage, 0))
else: # available else: # available
treasure_hunt_total = world.triforce_pieces_available[player].value treasure_hunt_total = world.worlds[player].options.triforce_pieces_available.value
triforce_pieces = min(90, max(treasure_hunt_total, world.triforce_pieces_required[player].value)) triforce_pieces = min(90, max(treasure_hunt_total, world.worlds[player].options.triforce_pieces_required.value))
pieces_in_core = min(extraitems, triforce_pieces) pieces_in_core = min(extraitems, triforce_pieces)
additional_pieces_to_place = triforce_pieces - pieces_in_core additional_pieces_to_place = triforce_pieces - pieces_in_core
pool.extend(["Triforce Piece"] * pieces_in_core) pool.extend(["Triforce Piece"] * pieces_in_core)
extraitems -= pieces_in_core extraitems -= pieces_in_core
treasure_hunt_required = world.triforce_pieces_required[player].value treasure_hunt_required = world.worlds[player].options.triforce_pieces_required.value
for extra in diff.extras: for extra in diff.extras:
if extraitems >= len(extra): if extraitems >= len(extra):
@@ -707,17 +711,24 @@ def get_pool_core(world, player: int):
else: else:
break break
if goal == 'pedestal':
place_item('Master Sword Pedestal', 'Triforce')
pool.remove("Rupees (20)")
if retro_bow: if retro_bow:
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (70)'} replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (70)'}
pool = ['Rupees (5)' if item in replace else item for item in pool] pool = ['Rupees (5)' if item in replace else item for item in pool]
if world.small_key_shuffle[player] == small_key_shuffle.option_universal:
if goal == 'pedestal':
place_item('Master Sword Pedestal', 'Triforce')
for rupee_name in ("Rupees (5)", "Rupees (20)", "Rupees (50)", "Rupees (100)", "Rupees (300)"):
try:
pool.remove(rupee_name)
except ValueError:
pass
else:
break
if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
pool.extend(diff.universal_keys) pool.extend(diff.universal_keys)
if mode == 'standard': if mode == 'standard':
if world.key_drop_shuffle[player]: if world.worlds[player].options.key_drop_shuffle:
key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop'] key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop']
key_location = world.random.choice(key_locations) key_location = world.random.choice(key_locations)
key_locations.remove(key_location) key_locations.remove(key_location)
@@ -741,11 +752,11 @@ def get_pool_core(world, player: int):
def make_custom_item_pool(world, player): def make_custom_item_pool(world, player):
shuffle = world.entrance_shuffle[player] shuffle = world.worlds[player].options.entrance_shuffle
difficulty = world.item_pool[player] difficulty = world.worlds[player].options.item_pool
timer = world.timer[player] timer = world.worlds[player].options.timer
goal = world.goal[player] goal = world.worlds[player].options.goal
mode = world.mode[player] mode = world.worlds[player].options.mode
customitemarray = world.customitemarray customitemarray = world.customitemarray
pool = [] pool = []
@@ -845,10 +856,10 @@ def make_custom_item_pool(world, player):
thisbottle = world.random.choice(diff.bottles) thisbottle = world.random.choice(diff.bottles)
pool.append(thisbottle) pool.append(thisbottle)
if "triforce" in world.goal[player]: if "triforce" in world.worlds[player].options.goal:
pool.extend(["Triforce Piece"] * world.triforce_pieces_available[player]) pool.extend(["Triforce Piece"] * world.worlds[player].options.triforce_pieces_available)
itemtotal += world.triforce_pieces_available[player] itemtotal += world.worlds[player].options.triforce_pieces_available
treasure_hunt_required = world.triforce_pieces_required[player] treasure_hunt_required = world.worlds[player].options.triforce_pieces_required
if timer in ['display', 'timed', 'timed_countdown']: if timer in ['display', 'timed', 'timed_countdown']:
clock_mode = 'countdown' if timer == 'timed_countdown' else 'stopwatch' clock_mode = 'countdown' if timer == 'timed_countdown' else 'stopwatch'
@@ -862,7 +873,7 @@ def make_custom_item_pool(world, player):
itemtotal = itemtotal + 1 itemtotal = itemtotal + 1
if mode == 'standard': if mode == 'standard':
if world.small_key_shuffle[player] == small_key_shuffle.option_universal: if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
key_location = world.random.choice( key_location = world.random.choice(
['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', ['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']) 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
@@ -885,9 +896,9 @@ def make_custom_item_pool(world, player):
pool.extend(['Magic Mirror'] * customitemarray[22]) pool.extend(['Magic Mirror'] * customitemarray[22])
pool.extend(['Moon Pearl'] * customitemarray[28]) pool.extend(['Moon Pearl'] * customitemarray[28])
if world.small_key_shuffle[player] == small_key_shuffle.option_universal: if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in universal Mode itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in universal Mode
if world.key_drop_shuffle[player]: if world.worlds[player].options.key_drop_shuffle:
itemtotal = itemtotal - (len(key_drop_data) - 1) itemtotal = itemtotal - (len(key_drop_data) - 1)
if itemtotal < total_items_to_place: if itemtotal < total_items_to_place:
pool.extend(['Nothing'] * (total_items_to_place - itemtotal)) pool.extend(['Nothing'] * (total_items_to_place - itemtotal))

View File

@@ -11,11 +11,11 @@ def GetBeemizerItem(world, player: int, item):
return item return item
# first roll - replaceable item should be replaced, within beemizer_total_chance # first roll - replaceable item should be replaced, within beemizer_total_chance
if not world.beemizer_total_chance[player] or world.random.random() > (world.beemizer_total_chance[player] / 100): if not world.worlds[player].options.beemizer_total_chance or world.random.random() > (world.worlds[player].options.beemizer_total_chance / 100):
return item return item
# second roll - bee replacement should be trap, within beemizer_trap_chance # second roll - bee replacement should be trap, within beemizer_trap_chance
if not world.beemizer_trap_chance[player] or world.random.random() > (world.beemizer_trap_chance[player] / 100): if not world.worlds[player].options.beemizer_trap_chance or world.random.random() > (world.worlds[player].options.beemizer_trap_chance / 100):
return "Bee" if isinstance(item, str) else world.create_item("Bee", player) return "Bee" if isinstance(item, str) else world.create_item("Bee", player)
else: else:
return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player) return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player)

View File

@@ -156,10 +156,10 @@ class OpenPyramid(Choice):
def to_bool(self, world: MultiWorld, player: int) -> bool: def to_bool(self, world: MultiWorld, player: int) -> bool:
if self.value == self.option_goal: if self.value == self.option_goal:
return world.goal[player].current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
elif self.value == self.option_auto: elif self.value == self.option_auto:
return world.goal[player].current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \ return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
and (world.entrance_shuffle[player].current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not and (world.worlds[player].options.entrance_shuffle.current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
world.shuffle_ganon) world.shuffle_ganon)
elif self.value == self.option_open: elif self.value == self.option_open:
return True return True

View File

@@ -2,8 +2,6 @@
Helper functions to deliver entrance/exit/region sets to OWG rules. Helper functions to deliver entrance/exit/region sets to OWG rules.
""" """
from BaseClasses import Entrance
from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw
@@ -222,14 +220,14 @@ def get_invalid_bunny_revival_dungeons():
def overworld_glitch_connections(world, player): def overworld_glitch_connections(world, player):
# Boots-accessible locations. # Boots-accessible locations.
create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted')) create_owg_connections(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted'))
create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player)) create_owg_connections(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player))
# Glitched speed drops. # Glitched speed drops.
create_owg_connections(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted')) create_owg_connections(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted'))
# Mirror clip spots. # Mirror clip spots.
if world.mode[player] != 'inverted': if world.worlds[player].options.mode != 'inverted':
create_owg_connections(player, world, get_mirror_clip_spots_dw()) create_owg_connections(player, world, get_mirror_clip_spots_dw())
create_owg_connections(player, world, get_mirror_offset_spots_dw()) create_owg_connections(player, world, get_mirror_offset_spots_dw())
else: else:
@@ -239,24 +237,24 @@ 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: can_boots_clip_lw(state, player)) set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == '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: can_boots_clip_dw(state, player)) set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == '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: can_get_glitched_speed_dw(state, player)) set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == '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.worlds[player].options.mode != '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))
# Mirror clip spots. # Mirror clip spots.
if world.mode[player] != 'inverted': if world.worlds[player].options.mode != '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 can_boots_clip_lw(state, 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 can_boots_clip_dw(state, 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.worlds[player].options.mode != 'inverted':
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) 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:
@@ -279,18 +277,14 @@ def create_no_logic_connections(player, world, connections):
for entrance, parent_region, target_region, *rule_override in connections: for entrance, parent_region, target_region, *rule_override in connections:
parent = world.get_region(parent_region, player) parent = world.get_region(parent_region, player)
target = world.get_region(target_region, player) target = world.get_region(target_region, player)
connection = Entrance(player, entrance, parent) parent.connect(target, entrance)
parent.exits.append(connection)
connection.connect(target)
def create_owg_connections(player, world, connections): def create_owg_connections(player, world, connections):
for entrance, parent_region, target_region, *rule_override in connections: for entrance, parent_region, target_region, *rule_override in connections:
parent = world.get_region(parent_region, player) parent = world.get_region(parent_region, player)
target = world.get_region(target_region, player) target = world.get_region(target_region, player)
connection = Entrance(player, entrance, parent) parent.connect(target, entrance)
parent.exits.append(connection)
connection.connect(target)
def set_owg_connection_rules(player, world, connections, default_rule): def set_owg_connection_rules(player, world, connections, default_rule):

View File

@@ -1,11 +1,11 @@
import collections import collections
import typing import typing
from BaseClasses import Entrance, MultiWorld from BaseClasses import MultiWorld
from .SubClasses import LTTPRegion, LTTPRegionType from .SubClasses import LTTPEntrance, LTTPRegion, LTTPRegionType
def is_main_entrance(entrance: Entrance) -> bool: def is_main_entrance(entrance: LTTPEntrance) -> bool:
return entrance.parent_region.type in {LTTPRegionType.DarkWorld, LTTPRegionType.LightWorld} if entrance.parent_region.type else True return entrance.parent_region.type in {LTTPRegionType.DarkWorld, LTTPRegionType.LightWorld} if entrance.parent_region.type else True
@@ -410,7 +410,7 @@ def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionTy
ret = LTTPRegion(name, type, hint, player, world) ret = LTTPRegion(name, type, hint, player, world)
if exits: if exits:
for exit in exits: for exit in exits:
ret.exits.append(Entrance(player, exit, ret)) ret.create_exit(exit)
if locations: if locations:
for location in locations: for location in locations:
if location in key_drop_data: if location in key_drop_data:

View File

@@ -92,7 +92,7 @@ class LocalRom:
# cause crash to provide traceback # cause crash to provide traceback
import xxtea import xxtea
local_random = world.per_slot_randoms[player] local_random = world.worlds[player].random
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)
@@ -281,7 +281,6 @@ def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_rand
def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory): def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
player = world.player player = world.player
multiworld = world.multiworld
check_enemizer(enemizercli) check_enemizer(enemizercli)
randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc')) randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc'))
options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json')) options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json'))
@@ -289,18 +288,18 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
# write options file for enemizer # write options file for enemizer
options = { options = {
'RandomizeEnemies': multiworld.enemy_shuffle[player].value, 'RandomizeEnemies': world.options.enemy_shuffle.value,
'RandomizeEnemiesType': 3, 'RandomizeEnemiesType': 3,
'RandomizeBushEnemyChance': multiworld.bush_shuffle[player].value, 'RandomizeBushEnemyChance': world.options.bush_shuffle.value,
'RandomizeEnemyHealthRange': multiworld.enemy_health[player] != 'default', 'RandomizeEnemyHealthRange': world.options.enemy_health != 'default',
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[ 'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[
multiworld.enemy_health[player].current_key], world.options.enemy_health.current_key],
'OHKO': False, 'OHKO': False,
'RandomizeEnemyDamage': multiworld.enemy_damage[player] != 'default', 'RandomizeEnemyDamage': world.options.enemy_damage != 'default',
'AllowEnemyZeroDamage': True, 'AllowEnemyZeroDamage': True,
'ShuffleEnemyDamageGroups': multiworld.enemy_damage[player] != 'default', 'ShuffleEnemyDamageGroups': world.options.enemy_damage != 'default',
'EnemyDamageChaosMode': multiworld.enemy_damage[player] == 'chaos', 'EnemyDamageChaosMode': world.options.enemy_damage == 'chaos',
'EasyModeEscape': multiworld.mode[player] == "standard", 'EasyModeEscape': world.options.mode == "standard",
'EnemiesAbsorbable': False, 'EnemiesAbsorbable': False,
'AbsorbableSpawnRate': 10, 'AbsorbableSpawnRate': 10,
'AbsorbableTypes': { 'AbsorbableTypes': {
@@ -329,7 +328,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
'GrayscaleMode': False, 'GrayscaleMode': False,
'GenerateSpoilers': False, 'GenerateSpoilers': False,
'RandomizeLinkSpritePalette': False, 'RandomizeLinkSpritePalette': False,
'RandomizePots': multiworld.pot_shuffle[player].value, 'RandomizePots': world.options.pot_shuffle.value,
'ShuffleMusic': False, 'ShuffleMusic': False,
'BootlegMagic': True, 'BootlegMagic': True,
'CustomBosses': False, 'CustomBosses': False,
@@ -342,7 +341,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
'BeesLevel': 0, 'BeesLevel': 0,
'RandomizeTileTrapPattern': False, 'RandomizeTileTrapPattern': False,
'RandomizeTileTrapFloorTile': False, 'RandomizeTileTrapFloorTile': False,
'AllowKillableThief': multiworld.killable_thieves[player].value, 'AllowKillableThief': world.options.killable_thieves.value,
'RandomizeSpriteOnHit': False, 'RandomizeSpriteOnHit': False,
'DebugMode': False, 'DebugMode': False,
'DebugForceEnemy': False, 'DebugForceEnemy': False,
@@ -366,13 +365,13 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
'MiseryMire': world.dungeons["Misery Mire"].boss.enemizer_name, 'MiseryMire': world.dungeons["Misery Mire"].boss.enemizer_name,
'TurtleRock': world.dungeons["Turtle Rock"].boss.enemizer_name, 'TurtleRock': world.dungeons["Turtle Rock"].boss.enemizer_name,
'GanonsTower1': 'GanonsTower1':
world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
"Inverted Ganons Tower"].bosses['bottom'].enemizer_name, "Inverted Ganons Tower"].bosses['bottom'].enemizer_name,
'GanonsTower2': 'GanonsTower2':
world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
"Inverted Ganons Tower"].bosses['middle'].enemizer_name, "Inverted Ganons Tower"].bosses['middle'].enemizer_name,
'GanonsTower3': 'GanonsTower3':
world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
"Inverted Ganons Tower"].bosses['top'].enemizer_name, "Inverted Ganons Tower"].bosses['top'].enemizer_name,
'GanonsTower4': 'Agahnim2', 'GanonsTower4': 'Agahnim2',
'Ganon': 'Ganon', 'Ganon': 'Ganon',
@@ -386,7 +385,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
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(multiworld.per_slot_randoms[player].randint(0, 999999999)) enemizer_seed = str(world.random.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,
@@ -416,7 +415,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
continue continue
for j in range(i + 1, max_enemizer_tries): for j in range(i + 1, max_enemizer_tries):
multiworld.per_slot_randoms[player].randint(0, 999999999) world.random.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
@@ -430,7 +429,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
# Moblins attached to "key drop" locations crash the game when dropping their item when Key Drop Shuffle is on. # Moblins attached to "key drop" locations crash the game when dropping their item when Key Drop Shuffle is on.
# Replace them with a Slime enemy if they are placed. # Replace them with a Slime enemy if they are placed.
if multiworld.key_drop_shuffle[player]: if world.options.key_drop_shuffle:
key_drop_enemies = { key_drop_enemies = {
0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201, 0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201,
0x4E20A, 0x4E326, 0x4E4F7, 0x4E687, 0x4E70C, 0x4E7C8, 0x4E7FA 0x4E20A, 0x4E326, 0x4E4F7, 0x4E687, 0x4E70C, 0x4E7C8, 0x4E7FA
@@ -792,8 +791,8 @@ def get_nonnative_item_sprite(code: int) -> int:
def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
local_random = world.worlds[player].random
local_world = world.worlds[player] local_world = world.worlds[player]
local_random = local_world.random
# patch items # patch items
@@ -840,20 +839,20 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# patch music # patch music
music_addresses = dungeon_music_addresses[location.name] music_addresses = dungeon_music_addresses[location.name]
if world.map_shuffle[player]: if local_world.options.map_shuffle:
music = local_random.choice([0x11, 0x16]) music = local_random.choice([0x11, 0x16])
else: else:
music = 0x11 if 'Pendant' in location.item.name else 0x16 music = 0x11 if 'Pendant' in location.item.name else 0x16
for music_address in music_addresses: for music_address in music_addresses:
rom.write_byte(music_address, music) rom.write_byte(music_address, music)
if world.map_shuffle[player]: if local_world.options.map_shuffle:
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
# patch entrance/exits/holes # patch entrance/exits/holes
for region in world.regions: for region in world.get_regions(player):
for exit in region.exits: for exit in region.exits:
if exit.target is not None and exit.player == player: if exit.target is not None:
if isinstance(exit.addresses, tuple): if isinstance(exit.addresses, tuple):
offset = exit.target offset = exit.target
room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = exit.addresses room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = exit.addresses
@@ -868,15 +867,15 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# Thanks to Zarby89 for originally finding these values # Thanks to Zarby89 for originally finding these values
# todo fix screen scrolling # todo fix screen scrolling
if world.entrance_shuffle[player] != 'insanity' and \ if local_world.options.entrance_shuffle != 'insanity' and \
exit.name in {'Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', exit.name in {'Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit',
'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit',
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit', 'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit',
'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)', 'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)',
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)'} and \ 'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)'} and \
(world.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic'] or (local_world.options.glitches_required not in ['hybrid_major_glitches', 'no_logic'] or
exit.name not in {'Palace of Darkness Exit', 'Tower of Hera Exit', 'Swamp Palace Exit'}): exit.name not in {'Palace of Darkness Exit', 'Tower of Hera Exit', 'Swamp Palace Exit'}):
# For exits that connot be reached from another, no need to apply offset fixes. # For exits that cannot be reached from another, no need to apply offset fixes.
rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else
elif room_id == 0x0059 and local_world.fix_skullwoods_exit: elif room_id == 0x0059 and local_world.fix_skullwoods_exit:
rom.write_int16(0x15DB5 + 2 * offset, 0x00F8) rom.write_int16(0x15DB5 + 2 * offset, 0x00F8)
@@ -903,7 +902,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
else: else:
# patch door table # patch door table
rom.write_byte(0xDBB73 + exit.addresses, exit.target) rom.write_byte(0xDBB73 + exit.addresses, exit.target)
if world.mode[player] == 'inverted': if local_world.options.mode == 'inverted':
patch_shuffled_dark_sanc(world, rom, player) patch_shuffled_dark_sanc(world, rom, player)
write_custom_shops(rom, world, player) write_custom_shops(rom, world, player)
@@ -914,16 +913,16 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
return 0x53 + int(num), 0x79 + int(num) return 0x53 + int(num), 0x79 + int(num)
credits_total = 216 credits_total = 216
if world.retro_caves[player]: # Old man cave and Take any caves will count towards collection rate. if local_world.options.retro_caves: # Old man cave and Take any caves will count towards collection rate.
credits_total += 5 credits_total += 5
if world.shop_item_slots[player]: # Potion shop only counts towards collection rate if included in the shuffle. if local_world.options.shop_item_slots: # Potion shop only counts towards collection rate if included in the shuffle.
credits_total += 30 if world.include_witch_hut[player] else 27 credits_total += 30 if local_world.options.include_witch_hut else 27
if world.shuffle_capacity_upgrades[player]: if local_world.options.shuffle_capacity_upgrades:
credits_total += 2 credits_total += 2
rom.write_byte(0x187010, credits_total) # dynamic credits rom.write_byte(0x187010, credits_total) # dynamic credits
if world.key_drop_shuffle[player]: if local_world.options.key_drop_shuffle:
rom.write_byte(0x140000, 1) # enable key drop shuffle rom.write_byte(0x140000, 1) # enable key drop shuffle
credits_total += len(key_drop_data) credits_total += len(key_drop_data)
# update dungeon counters # update dungeon counters
@@ -977,11 +976,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x51DE, 0x00) rom.write_byte(0x51DE, 0x00)
# set open mode: # set open mode:
if world.mode[player] in ['open', 'inverted']: if local_world.options.mode in ['open', 'inverted']:
rom.write_byte(0x180032, 0x01) # open mode rom.write_byte(0x180032, 0x01) # open mode
if world.mode[player] == 'inverted': if local_world.options.mode == 'inverted':
set_inverted_mode(world, player, rom) set_inverted_mode(world, player, rom)
elif world.mode[player] == 'standard': elif local_world.options.mode == 'standard':
rom.write_byte(0x180032, 0x00) # standard mode rom.write_byte(0x180032, 0x00) # standard mode
uncle_location = world.get_location('Link\'s Uncle', player) uncle_location = world.get_location('Link\'s Uncle', player)
@@ -1001,7 +1000,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_bytes(0x6D323, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) rom.write_bytes(0x6D323, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
# set light cones # set light cones
rom.write_byte(0x180038, 0x01 if world.mode[player] == "standard" else 0x00) rom.write_byte(0x180038, 0x01 if local_world.options.mode == "standard" else 0x00)
rom.write_byte(0x180039, 0x01 if world.light_world_light_cone else 0x00) rom.write_byte(0x180039, 0x01 if world.light_world_light_cone else 0x00)
rom.write_byte(0x18003A, 0x01 if world.dark_world_light_cone else 0x00) rom.write_byte(0x18003A, 0x01 if world.dark_world_light_cone else 0x00)
@@ -1011,7 +1010,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x18004F, 0x01) # Byrna Invulnerability: on rom.write_byte(0x18004F, 0x01) # Byrna Invulnerability: on
# handle item_functionality # handle item_functionality
if world.item_functionality[player] == 'hard': if local_world.options.item_functionality == 'hard':
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
# Powdered Fairies Prize # Powdered Fairies Prize
@@ -1031,7 +1030,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_int16(0x180036, world.rupoor_cost) rom.write_int16(0x180036, world.rupoor_cost)
# Set stun items # Set stun items
rom.write_byte(0x180180, 0x02) # Hookshot only rom.write_byte(0x180180, 0x02) # Hookshot only
elif world.item_functionality[player] == 'expert': elif local_world.options.item_functionality == 'expert':
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
# Powdered Fairies Prize # Powdered Fairies Prize
@@ -1071,7 +1070,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# Set stun items # Set stun items
rom.write_byte(0x180180, 0x03) # All standard items rom.write_byte(0x180180, 0x03) # All standard items
# Set overflow items for progressive equipment # Set overflow items for progressive equipment
if world.timer[player] in ['timed', 'timed_countdown', 'timed_ohko']: if local_world.options.timer in ['timed', 'timed_countdown', 'timed_ohko']:
overflow_replacement = GREEN_CLOCK overflow_replacement = GREEN_CLOCK
else: else:
overflow_replacement = GREEN_TWENTY_RUPEES overflow_replacement = GREEN_TWENTY_RUPEES
@@ -1083,7 +1082,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# Set overflow items for progressive equipment # Set overflow items for progressive equipment
rom.write_bytes(0x180090, rom.write_bytes(0x180090,
[difficulty.progressive_sword_limit if not world.swordless[player] else 0, [difficulty.progressive_sword_limit if not local_world.options.swordless else 0,
item_table[difficulty.basicsword[-1]].item_code, item_table[difficulty.basicsword[-1]].item_code,
difficulty.progressive_shield_limit, item_table[difficulty.basicshield[-1]].item_code, difficulty.progressive_shield_limit, item_table[difficulty.basicshield[-1]].item_code,
difficulty.progressive_armor_limit, item_table[difficulty.basicarmor[-1]].item_code, difficulty.progressive_armor_limit, item_table[difficulty.basicarmor[-1]].item_code,
@@ -1091,7 +1090,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
difficulty.progressive_bow_limit, item_table[difficulty.basicbow[-1]].item_code]) difficulty.progressive_bow_limit, item_table[difficulty.basicbow[-1]].item_code])
if difficulty.progressive_bow_limit < 2 and ( if difficulty.progressive_bow_limit < 2 and (
world.swordless[player] or world.glitches_required[player] == 'no_glitches'): local_world.options.swordless or local_world.options.glitches_required == 'no_glitches'):
rom.write_bytes(0x180098, [2, item_table["Silver Bow"].item_code]) rom.write_bytes(0x180098, [2, item_table["Silver Bow"].item_code])
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
@@ -1099,15 +1098,15 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# set up game internal RNG seed # set up game internal RNG seed
rom.write_bytes(0x178000, local_random.getrandbits(8 * 1024).to_bytes(1024, 'big')) rom.write_bytes(0x178000, local_random.getrandbits(8 * 1024).to_bytes(1024, 'big'))
prize_replacements = {} prize_replacements = {}
if world.item_functionality[player] in ['hard', 'expert']: if local_world.options.item_functionality in ['hard', 'expert']:
prize_replacements[0xE0] = 0xDF # Fairy -> heart prize_replacements[0xE0] = 0xDF # Fairy -> heart
prize_replacements[0xE3] = 0xD8 # Big magic -> small magic prize_replacements[0xE3] = 0xD8 # Big magic -> small magic
if world.retro_bow[player]: if local_world.options.retro_bow:
prize_replacements[0xE1] = 0xDA # 5 Arrows -> Blue Rupee prize_replacements[0xE1] = 0xDA # 5 Arrows -> Blue Rupee
prize_replacements[0xE2] = 0xDB # 10 Arrows -> Red Rupee prize_replacements[0xE2] = 0xDB # 10 Arrows -> Red Rupee
if world.shuffle_prizes[player] in ("general", "both"): if local_world.options.shuffle_prizes in ("general", "both"):
# shuffle prize packs # shuffle prize packs
prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0, prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0,
0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF,
@@ -1169,7 +1168,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
byte = int(rom.read_byte(address)) byte = int(rom.read_byte(address))
rom.write_byte(address, prize_replacements.get(byte, byte)) rom.write_byte(address, prize_replacements.get(byte, byte))
if world.shuffle_prizes[player] in ("bonk", "both"): if local_world.options.shuffle_prizes in ("bonk", "both"):
# set bonk prizes # set bonk prizes
bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC, bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC,
0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79, 0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79,
@@ -1196,7 +1195,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees 0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees
0x51, 0x06, 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade 0x51, 0x06, 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade
0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade 0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade
0x58, 0x01, 0x36 if world.retro_bow[player] else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode) 0x58, 0x01, 0x36 if local_world.options.retro_bow else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode)
0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20 0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20
0x17, difficulty.heart_piece_limit, 0x47, 0xff, # piece of heart -> green 20 0x17, difficulty.heart_piece_limit, 0x47, 0xff, # piece of heart -> green 20
0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel 0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel
@@ -1238,13 +1237,13 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x180029, 0x01) # Smithy quick item give rom.write_byte(0x180029, 0x01) # Smithy quick item give
# set swordless mode settings # set swordless mode settings
rom.write_byte(0x18003F, 0x01 if world.swordless[player] else 0x00) # hammer can harm ganon rom.write_byte(0x18003F, 0x01 if local_world.options.swordless else 0x00) # hammer can harm ganon
rom.write_byte(0x180040, 0x01 if world.swordless[player] else 0x00) # open curtains rom.write_byte(0x180040, 0x01 if local_world.options.swordless else 0x00) # open curtains
rom.write_byte(0x180041, 0x01 if world.swordless[player] else 0x00) # swordless medallions rom.write_byte(0x180041, 0x01 if local_world.options.swordless else 0x00) # swordless medallions
rom.write_byte(0x180043, 0xFF if world.swordless[player] else 0x00) # starting sword for link rom.write_byte(0x180043, 0xFF if local_world.options.swordless else 0x00) # starting sword for link
rom.write_byte(0x180044, 0x01 if world.swordless[player] else 0x00) # hammer activates tablets rom.write_byte(0x180044, 0x01 if local_world.options.swordless else 0x00) # hammer activates tablets
if world.item_functionality[player] == 'easy': if local_world.options.item_functionality == 'easy':
rom.write_byte(0x18003F, 0x01) # hammer can harm ganon rom.write_byte(0x18003F, 0x01) # hammer can harm ganon
rom.write_byte(0x180041, 0x02) # Allow swordless medallion use EVERYWHERE. rom.write_byte(0x180041, 0x02) # Allow swordless medallion use EVERYWHERE.
rom.write_byte(0x180044, 0x01) # hammer activates tablets rom.write_byte(0x180044, 0x01) # hammer activates tablets
@@ -1262,11 +1261,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# Set up requested clock settings # Set up requested clock settings
if local_world.clock_mode in ['countdown-ohko', 'stopwatch', 'countdown']: if local_world.clock_mode in ['countdown-ohko', 'stopwatch', 'countdown']:
rom.write_int32(0x180200, rom.write_int32(0x180200,
world.red_clock_time[player] * 60 * 60) # red clock adjustment time (in frames, sint32) local_world.options.red_clock_time * 60 * 60) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, rom.write_int32(0x180204,
world.blue_clock_time[player] * 60 * 60) # blue clock adjustment time (in frames, sint32) local_world.options.blue_clock_time * 60 * 60) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208, rom.write_int32(0x180208,
world.green_clock_time[player] * 60 * 60) # green clock adjustment time (in frames, sint32) local_world.options.green_clock_time * 60 * 60) # green clock adjustment time (in frames, sint32)
else: else:
rom.write_int32(0x180200, 0) # red clock adjustment time (in frames, sint32) rom.write_int32(0x180200, 0) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, 0) # blue clock adjustment time (in frames, sint32) rom.write_int32(0x180204, 0) # blue clock adjustment time (in frames, sint32)
@@ -1274,20 +1273,20 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# Set up requested start time for countdown modes # Set up requested start time for countdown modes
if local_world.clock_mode in ['countdown-ohko', 'countdown']: if local_world.clock_mode in ['countdown-ohko', 'countdown']:
rom.write_int32(0x18020C, world.countdown_start_time[player] * 60 * 60) # starting time (in frames, sint32) rom.write_int32(0x18020C, local_world.options.countdown_start_time * 60 * 60) # starting time (in frames, sint32)
else: else:
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32) rom.write_int32(0x18020C, 0) # starting time (in frames, sint32)
# set up goals for treasure hunt # set up goals for treasure hunt
rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required - rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required -
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))) sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")))
rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled) rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed
gametype = 0x04 # item gametype = 0x04 # item
if world.entrance_shuffle[player] != 'vanilla': if local_world.options.entrance_shuffle != 'vanilla':
gametype |= 0x02 # entrance gametype |= 0x02 # entrance
if enemized: if enemized:
gametype |= 0x01 # enemizer gametype |= 0x01 # enemizer
@@ -1298,7 +1297,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x1800A2, 0x01 if local_world.fix_fake_world else 0x00) rom.write_byte(0x1800A2, 0x01 if local_world.fix_fake_world else 0x00)
# Lock or unlock aga tower door during escape sequence. # Lock or unlock aga tower door during escape sequence.
rom.write_byte(0x180169, 0x00) rom.write_byte(0x180169, 0x00)
if world.mode[player] == 'inverted': if local_world.options.mode == 'inverted':
rom.write_byte(0x180169, 0x02) # lock aga/ganon tower door with crystals in inverted rom.write_byte(0x180169, 0x02) # lock aga/ganon tower door with crystals in inverted
rom.write_byte(0x180171, rom.write_byte(0x180171,
0x01 if local_world.ganon_at_pyramid else 0x00) # Enable respawning on pyramid after ganon death 0x01 if local_world.ganon_at_pyramid else 0x00) # Enable respawning on pyramid after ganon death
@@ -1309,9 +1308,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest
rom.write_byte(0x50599, 0x00) # disable below ganon chest rom.write_byte(0x50599, 0x00) # disable below ganon chest
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player].to_bool(world, player) else 0x00) # pre-open Pyramid Hole rom.write_byte(0x18008B, 0x01 if local_world.options.open_pyramid.to_bool(world, player) else 0x00) # pre-open Pyramid Hole
rom.write_byte(0x18008C, 0x01 if world.crystals_needed_for_gt[ rom.write_byte(0x18008C, 0x01 if local_world.options.crystals_needed_for_gt == 0 else 0x00) # GT pre-opened if crystal requirement is 0
player] == 0 else 0x00) # GT pre-opened if crystal requirement is 0
rom.write_byte(0xF5D73, 0xF0) # bees are catchable rom.write_byte(0xF5D73, 0xF0) # bees are catchable
rom.write_byte(0xF5F10, 0xF0) # bees are catchable rom.write_byte(0xF5F10, 0xF0) # bees are catchable
rom.write_byte(0x180086, 0x00 if world.aga_randomness else 0x01) # set blue ball and ganon warp randomness rom.write_byte(0x180086, 0x00 if world.aga_randomness else 0x01) # set blue ball and ganon warp randomness
@@ -1325,7 +1323,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
equip[0x36C] = 0x18 equip[0x36C] = 0x18
equip[0x36D] = 0x18 equip[0x36D] = 0x18
equip[0x379] = 0x68 equip[0x379] = 0x68
starting_max_bombs = 0 if world.bombless_start[player] else 10 starting_max_bombs = 0 if local_world.options.bombless_start else 10
starting_max_arrows = 30 starting_max_arrows = 30
startingstate = CollectionState(world) startingstate = CollectionState(world)
@@ -1333,12 +1331,12 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
if startingstate.has('Silver Bow', player): if startingstate.has('Silver Bow', player):
equip[0x340] = 1 equip[0x340] = 1
equip[0x38E] |= 0x60 equip[0x38E] |= 0x60
if not world.retro_bow[player]: if not local_world.options.retro_bow:
equip[0x38E] |= 0x80 equip[0x38E] |= 0x80
elif startingstate.has('Bow', player): elif startingstate.has('Bow', player):
equip[0x340] = 1 equip[0x340] = 1
equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases
if not world.retro_bow[player]: if not local_world.options.retro_bow:
equip[0x38E] |= 0x80 equip[0x38E] |= 0x80
if startingstate.has('Silver Arrows', player): if startingstate.has('Silver Arrows', player):
equip[0x38E] |= 0x40 equip[0x38E] |= 0x40
@@ -1476,7 +1474,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
elif item.name in bombs: elif item.name in bombs:
equip[0x343] += bombs[item.name] equip[0x343] += bombs[item.name]
elif item.name in arrows: elif item.name in arrows:
if world.retro_bow[player]: if local_world.options.retro_bow:
equip[0x38E] |= 0x80 equip[0x38E] |= 0x80
equip[0x377] = 1 equip[0x377] = 1
else: else:
@@ -1502,16 +1500,13 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_bytes(0x183000, equip[0x340:]) rom.write_bytes(0x183000, equip[0x340:])
rom.write_bytes(0x271A6, equip[0x340:0x340 + 60]) rom.write_bytes(0x271A6, equip[0x340:0x340 + 60])
rom.write_byte(0x18004A, 0x00 if world.mode[player] != 'inverted' else 0x01) # Inverted mode rom.write_byte(0x18004A, 0x00 if local_world.options.mode != 'inverted' else 0x01) # Inverted mode
rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier
rom.write_byte(0x2AF79, 0xD0 if world.mode[ rom.write_byte(0x2AF79, 0xD0 if local_world.options.mode != 'inverted' else 0xF0) # vortexes: Normal (D0=light to dark, F0=dark to light, 42 = both)
player] != 'inverted' else 0xF0) # vortexes: Normal (D0=light to dark, F0=dark to light, 42 = both) rom.write_byte(0x3A943, 0xD0 if local_world.options.mode != 'inverted' else 0xF0) # Mirror: Normal (D0=Dark to Light, F0=light to dark, 42 = both)
rom.write_byte(0x3A943, 0xD0 if world.mode[ rom.write_byte(0x3A96D, 0xF0 if local_world.options.mode != 'inverted' else 0xD0) # Residual Portal: Normal (F0= Light Side, D0=Dark Side, 42 = both (Darth Vader))
player] != 'inverted' else 0xF0) # Mirror: Normal (D0=Dark to Light, F0=light to dark, 42 = both)
rom.write_byte(0x3A96D, 0xF0 if world.mode[
player] != 'inverted' else 0xD0) # Residual Portal: Normal (F0= Light Side, D0=Dark Side, 42 = both (Darth Vader))
rom.write_byte(0x3A9A7, 0xD0) # Residual Portal: Normal (D0= Light Side, F0=Dark Side, 42 = both (Darth Vader)) rom.write_byte(0x3A9A7, 0xD0) # Residual Portal: Normal (D0= Light Side, F0=Dark Side, 42 = both (Darth Vader))
if world.shuffle_capacity_upgrades[player]: if local_world.options.shuffle_capacity_upgrades:
rom.write_bytes(0x180080, rom.write_bytes(0x180080,
[5, 10, 5, 10]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10) [5, 10, 5, 10]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10)
else: else:
@@ -1522,21 +1517,21 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
(0x02 if 'bombs' in local_world.escape_assist else 0x00) | (0x02 if 'bombs' in local_world.escape_assist else 0x00) |
(0x04 if 'magic' in local_world.escape_assist else 0x00))) # Escape assist (0x04 if 'magic' in local_world.escape_assist else 0x00))) # Escape assist
if world.goal[player] in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']: if local_world.options.goal in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']:
rom.write_byte(0x18003E, 0x01) # make ganon invincible rom.write_byte(0x18003E, 0x01) # make ganon invincible
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: elif local_world.options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
rom.write_byte(0x18003E, 0x05) # make ganon invincible until enough triforce pieces are collected rom.write_byte(0x18003E, 0x05) # make ganon invincible until enough triforce pieces are collected
elif world.goal[player] in ['ganon_pedestal']: elif local_world.options.goal in ['ganon_pedestal']:
rom.write_byte(0x18003E, 0x06) rom.write_byte(0x18003E, 0x06)
elif world.goal[player] in ['bosses']: elif local_world.options.goal in ['bosses']:
rom.write_byte(0x18003E, 0x02) # make ganon invincible until all bosses are beat rom.write_byte(0x18003E, 0x02) # make ganon invincible until all bosses are beat
elif world.goal[player] in ['crystals']: elif local_world.options.goal in ['crystals']:
rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals
else: else:
rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected
rom.write_byte(0x18005E, world.crystals_needed_for_gt[player]) rom.write_byte(0x18005E, local_world.options.crystals_needed_for_gt)
rom.write_byte(0x18005F, world.crystals_needed_for_ganon[player]) rom.write_byte(0x18005F, local_world.options.crystals_needed_for_ganon)
# Bitfield - enable text box to show with free roaming items # Bitfield - enable text box to show with free roaming items
# #
@@ -1547,21 +1542,20 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# c - enabled for inside compasses # c - enabled for inside compasses
# s - enabled for inside small keys # s - enabled for inside small keys
# block HC upstairs doors in rain state in standard mode # block HC upstairs doors in rain state in standard mode
rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.entrance_shuffle[player] != 'vanilla' else 0x00) rom.write_byte(0x18008A, 0x01 if local_world.options.mode == "standard" and local_world.options.entrance_shuffle != 'vanilla' else 0x00)
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.small_key_shuffle[player] else 0x00) rom.write_byte(0x18016A, 0x10 | ((0x01 if local_world.options.small_key_shuffle else 0x00)
| (0x02 if world.compass_shuffle[player] else 0x00) | (0x02 if local_world.options.compass_shuffle else 0x00)
| (0x04 if world.map_shuffle[player] else 0x00) | (0x04 if local_world.options.map_shuffle else 0x00)
| (0x08 if world.big_key_shuffle[ | (0x08 if local_world.options.big_key_shuffle else 0x00))) # free roaming item text boxes
player] else 0x00))) # free roaming item text boxes rom.write_byte(0x18003B, 0x01 if local_world.options.map_shuffle else 0x00) # maps showing crystals on overworld
rom.write_byte(0x18003B, 0x01 if world.map_shuffle[player] else 0x00) # maps showing crystals on overworld
# compasses showing dungeon count # compasses showing dungeon count
if local_world.clock_mode or world.dungeon_counters[player] == 'off': if local_world.clock_mode or local_world.options.dungeon_counters == 'off':
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
elif world.dungeon_counters[player] == 'on': elif local_world.options.dungeon_counters == 'on':
rom.write_byte(0x18003C, 0x02) # always on rom.write_byte(0x18003C, 0x02) # always on
elif world.compass_shuffle[player] or world.dungeon_counters[player] == 'pickup': elif local_world.options.compass_shuffle or local_world.options.dungeon_counters == 'pickup':
rom.write_byte(0x18003C, 0x01) # show on pickup rom.write_byte(0x18003C, 0x01) # show on pickup
else: else:
rom.write_byte(0x18003C, 0x00) rom.write_byte(0x18003C, 0x00)
@@ -1574,11 +1568,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# b - Big Key # b - Big Key
# a - Small Key # a - Small Key
# #
rom.write_byte(0x180045, ((0x00 if (world.small_key_shuffle[player] == small_key_shuffle.option_original_dungeon or rom.write_byte(0x180045, ((0x00 if (local_world.options.small_key_shuffle == small_key_shuffle.option_original_dungeon or
world.small_key_shuffle[player] == small_key_shuffle.option_universal) else 0x01) local_world.options.small_key_shuffle == small_key_shuffle.option_universal) else 0x01)
| (0x02 if world.big_key_shuffle[player] else 0x00) | (0x02 if local_world.options.big_key_shuffle else 0x00)
| (0x04 if world.map_shuffle[player] else 0x00) | (0x04 if local_world.options.map_shuffle else 0x00)
| (0x08 if world.compass_shuffle[player] else 0x00))) # free roaming items in menu | (0x08 if local_world.options.compass_shuffle else 0x00))) # free roaming items in menu
# Map reveals # Map reveals
reveal_bytes = { reveal_bytes = {
@@ -1604,31 +1598,25 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
return 0x0000 return 0x0000
rom.write_int16(0x18017A, rom.write_int16(0x18017A,
get_reveal_bytes('Green Pendant') if world.map_shuffle[player] else 0x0000) # Sahasrahla reveal get_reveal_bytes('Green Pendant') if local_world.options.map_shuffle else 0x0000) # Sahasrahla reveal
rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if world.map_shuffle[ rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if local_world.options.map_shuffle else 0x0000) # Bomb Shop Reveal
player] else 0x0000) # Bomb Shop Reveal
rom.write_byte(0x180172, 0x01 if world.small_key_shuffle[ rom.write_byte(0x180172, 0x01 if local_world.options.small_key_shuffle == small_key_shuffle.option_universal else 0x00) # universal keys
player] == small_key_shuffle.option_universal else 0x00) # universal keys rom.write_byte(0x18637E, 0x01 if local_world.options.retro_bow else 0x00) # Skip quiver in item shops once bought
rom.write_byte(0x18637E, 0x01 if world.retro_bow[player] else 0x00) # Skip quiver in item shops once bought rom.write_byte(0x180175, 0x01 if local_world.options.retro_bow else 0x00) # rupee bow
rom.write_byte(0x180175, 0x01 if world.retro_bow[player] else 0x00) # rupee bow rom.write_byte(0x180176, 0x0A if local_world.options.retro_bow else 0x00) # wood arrow cost
rom.write_byte(0x180176, 0x0A if world.retro_bow[player] else 0x00) # wood arrow cost rom.write_byte(0x180178, 0x32 if local_world.options.retro_bow else 0x00) # silver arrow cost
rom.write_byte(0x180178, 0x32 if world.retro_bow[player] else 0x00) # silver arrow cost rom.write_byte(0x301FC, 0xDA if local_world.options.retro_bow else 0xE1) # rupees replace arrows under pots
rom.write_byte(0x301FC, 0xDA if world.retro_bow[player] else 0xE1) # rupees replace arrows under pots rom.write_byte(0x30052, 0xDB if local_world.options.retro_bow else 0xE2) # replace arrows in fish prize from bottle merchant
rom.write_byte(0x30052, 0xDB if world.retro_bow[player] else 0xE2) # replace arrows in fish prize from bottle merchant rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if local_world.options.retro_bow else [0xAF, 0x77, 0xF3, 0x7E]) # Thief steals rupees instead of arrows
rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if world.retro_bow[player] else [0xAF, 0x77, 0xF3, rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if local_world.options.retro_bow else [0xAF, 0x77, 0xF3, 0x7E]) # Pikit steals rupees instead of arrows
0x7E]) # Thief steals rupees instead of arrows rom.write_bytes(0xEDA5, [0x35, 0x41] if local_world.options.retro_bow else [0x43, 0x44]) # Chest game gives rupees instead of arrows
rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if world.retro_bow[player] else [0xAF, 0x77, 0xF3,
0x7E]) # Pikit steals rupees instead of arrows
rom.write_bytes(0xEDA5,
[0x35, 0x41] if world.retro_bow[player] else [0x43, 0x44]) # Chest game gives rupees instead of arrows
digging_game_rng = local_random.randint(1, 30) # set rng for digging game digging_game_rng = local_random.randint(1, 30) # set rng for digging game
rom.write_byte(0x180020, digging_game_rng) rom.write_byte(0x180020, digging_game_rng)
rom.write_byte(0xEFD95, digging_game_rng) rom.write_byte(0xEFD95, digging_game_rng)
rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills
rom.write_byte(0x1800A4, 0x01 if world.glitches_required[player] != 'no_logic' else 0x00) # enable POD EG fix rom.write_byte(0x1800A4, 0x01 if local_world.options.glitches_required != 'no_logic' else 0x00) # enable POD EG fix
rom.write_byte(0x186383, 0x01 if world.glitches_required[ rom.write_byte(0x186383, 0x01 if local_world.options.glitches_required == 'no_logic' else 0x00) # disable glitching to Triforce from Ganons Room
player] == 'no_logic' else 0x00) # disable glitching to Triforce from Ganons Room
rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
# remove shield from uncle # remove shield from uncle
@@ -1645,7 +1633,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_bytes(0x180185, [0, 0, 0]) # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180185, [0, 0, 0]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [0, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x180188, [0, 0, 0]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
if world.mode[player] == 'standard' and uncle_location.item and uncle_location.item.player == player: if local_world.options.mode == 'standard' and uncle_location.item and uncle_location.item.player == player:
if uncle_location.item.name in {'Bow', 'Progressive Bow'}: if uncle_location.item.name in {'Bow', 'Progressive Bow'}:
rom.write_byte(0x18004E, 1) # Escape Fill (arrows) rom.write_byte(0x18004E, 1) # Escape Fill (arrows)
rom.write_int16(0x180183, 300) # Escape fill rupee bow rom.write_int16(0x180183, 300) # Escape fill rupee bow
@@ -1673,8 +1661,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
0xAD, 0xBF, 0x0A, 0xF0, 0x4F]) 0xAD, 0xBF, 0x0A, 0xF0, 0x4F])
# allow smith into multi-entrance caves in appropriate shuffles # allow smith into multi-entrance caves in appropriate shuffles
if world.entrance_shuffle[player] in ['restricted', 'full', 'crossed', 'insanity'] or ( if local_world.options.entrance_shuffle in ['restricted', 'full', 'crossed', 'insanity'] or (
world.entrance_shuffle[player] == 'simple' and world.mode[player] == 'inverted'): local_world.options.entrance_shuffle == 'simple' and local_world.options.mode == 'inverted'):
rom.write_byte(0x18004C, 0x01) rom.write_byte(0x18004C, 0x01)
# set correct flag for hera basement item # set correct flag for hera basement item
@@ -1694,8 +1682,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0xFED31, 0x2A) # bombable exit rom.write_byte(0xFED31, 0x2A) # bombable exit
rom.write_byte(0xFEE41, 0x2A) # bombable exit rom.write_byte(0xFEE41, 0x2A) # bombable exit
if world.tile_shuffle[player]: if local_world.options.tile_shuffle:
tile_set = TileSet.get_random_tile_set(world.per_slot_randoms[player]) tile_set = TileSet.get_random_tile_set(world.worlds[player].random)
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())
@@ -1770,9 +1758,9 @@ def write_custom_shops(rom, world, player):
slot = 0 if shop.type == ShopType.TakeAny else index slot = 0 if shop.type == ShopType.TakeAny else index
if item is None: if item is None:
break break
if world.shop_item_slots[player] or shop.type == ShopType.TakeAny: if world.worlds[player].options.shop_item_slots or shop.type == ShopType.TakeAny:
count_shop = (shop.region.name != 'Potion Shop' or world.include_witch_hut[player]) and \ count_shop = (shop.region.name != 'Potion Shop' or world.worlds[player].options.include_witch_hut) and \
(shop.region.name != 'Capacity Upgrade' or world.shuffle_capacity_upgrades[player]) (shop.region.name != 'Capacity Upgrade' or world.worlds[player].options.shuffle_capacity_upgrades)
rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0) rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0)
if item['item'] == 'Single Arrow' and item['player'] == 0: if item['item'] == 'Single Arrow' and item['player'] == 0:
arrow_mask |= 1 << index arrow_mask |= 1 << index
@@ -1789,7 +1777,7 @@ def write_custom_shops(rom, world, player):
item_code = get_nonnative_item_sprite(world.worlds[item['player']].item_name_to_id[item['item']]) item_code = get_nonnative_item_sprite(world.worlds[item['player']].item_name_to_id[item['item']])
else: else:
item_code = item_table[item["item"]].item_code item_code = item_table[item["item"]].item_code
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro_bow[player]: if item['item'] == 'Single Arrow' and item['player'] == 0 and world.worlds[player].options.retro_bow:
rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask) rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask)
item_data = [shop_id, item_code] + price_data + \ item_data = [shop_id, item_code] + price_data + \
@@ -1802,7 +1790,7 @@ def write_custom_shops(rom, world, player):
items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
rom.write_bytes(0x184900, items_data) rom.write_bytes(0x184900, items_data)
if world.retro_bow[player]: if world.worlds[player].options.retro_bow:
retro_shop_slots.append(0xFF) retro_shop_slots.append(0xFF)
rom.write_bytes(0x186540, retro_shop_slots) rom.write_bytes(0x186540, retro_shop_slots)
@@ -2207,19 +2195,18 @@ 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.worlds[player].random
w: ALTTPWorld = world.worlds[player] w: ALTTPWorld = world.worlds[player]
local_random = w.random
tt = TextTable() tt = TextTable()
tt.removeUnwantedText() tt.removeUnwantedText()
# Let's keep this guy's text accurate to the shuffle setting. # Let's keep this guy's text accurate to the shuffle setting.
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']: if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.' tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.' tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
if world.mode[player] == 'inverted': if world.worlds[player].options.mode == 'inverted':
tt['sign_village_of_outcasts'] = 'attention\nferal ducks sighted\nhiding in statues\n\nflute players beware\n' tt['sign_village_of_outcasts'] = 'attention\nferal ducks sighted\nhiding in statues\n\nflute players beware\n'
def hint_text(dest, ped_hint=False): def hint_text(dest, ped_hint=False):
@@ -2238,21 +2225,21 @@ def write_strings(rom, world, player):
hint += f" for {world.player_name[dest.player]}" hint += f" for {world.player_name[dest.player]}"
return hint return hint
if world.scams[player].gives_king_zora_hint: if world.worlds[player].options.scams.gives_king_zora_hint:
# Zora hint # Zora hint
zora_location = world.get_location("King Zora", player) zora_location = world.get_location("King Zora", player)
tt['zora_tells_cost'] = f"You got 500 rupees to buy {hint_text(zora_location.item)}" \ tt['zora_tells_cost'] = f"You got 500 rupees to buy {hint_text(zora_location.item)}" \
f"\n ≥ Duh\n Oh carp\n{{CHOICE}}" f"\n ≥ Duh\n Oh carp\n{{CHOICE}}"
if world.scams[player].gives_bottle_merchant_hint: if world.worlds[player].options.scams.gives_bottle_merchant_hint:
# Bottle Vendor hint # Bottle Vendor hint
vendor_location = world.get_location("Bottle Merchant", player) vendor_location = world.get_location("Bottle Merchant", player)
tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?" \ tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?" \
f"\n ≥ I want\n no way!\n{{CHOICE}}" f"\n ≥ I want\n no way!\n{{CHOICE}}"
# First we write hints about entrances, some from the inconvenient list others from all reasonable entrances. # First we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
if world.hints[player]: if world.worlds[player].options.hints:
if world.hints[player].value >= 2: if world.worlds[player].options.hints.value >= 2:
if world.hints[player] == "full": if world.worlds[player].options.hints == "full":
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles have hints!' tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles have hints!'
else: else:
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!' tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
@@ -2265,11 +2252,11 @@ def write_strings(rom, world, player):
entrances_to_hint = {} entrances_to_hint = {}
entrances_to_hint.update(InconvenientDungeonEntrances) entrances_to_hint.update(InconvenientDungeonEntrances)
if world.shuffle_ganon: if world.shuffle_ganon:
if world.mode[player] == 'inverted': if world.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'}) entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'})
else: else:
entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'}) entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'})
if world.entrance_shuffle[player] in ['simple', 'restricted']: if world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
for entrance in all_entrances: for entrance in all_entrances:
if entrance.name in entrances_to_hint: if entrance.name in entrances_to_hint:
this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text( this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(
@@ -2279,9 +2266,9 @@ def write_strings(rom, world, player):
break break
# Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones. # Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones.
entrances_to_hint.update(InconvenientOtherEntrances) entrances_to_hint.update(InconvenientOtherEntrances)
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
hint_count = 0 hint_count = 0
elif world.entrance_shuffle[player] in ['simple', 'restricted']: elif world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
hint_count = 2 hint_count = 2
else: else:
hint_count = 4 hint_count = 4
@@ -2298,31 +2285,31 @@ def write_strings(rom, world, player):
# Next we handle hints for randomly selected other entrances, # Next we handle hints for randomly selected other entrances,
# curating the selection intelligently based on shuffle. # curating the selection intelligently based on shuffle.
if world.entrance_shuffle[player] not in ['simple', 'restricted']: if world.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']:
entrances_to_hint.update(ConnectorEntrances) entrances_to_hint.update(ConnectorEntrances)
entrances_to_hint.update(DungeonEntrances) entrances_to_hint.update(DungeonEntrances)
if world.mode[player] == 'inverted': if world.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Agahnims Tower': 'The dark mountain tower'}) entrances_to_hint.update({'Inverted Agahnims Tower': 'The dark mountain tower'})
else: else:
entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'}) entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'})
elif world.entrance_shuffle[player] == 'restricted': elif world.worlds[player].options.entrance_shuffle == 'restricted':
entrances_to_hint.update(ConnectorEntrances) entrances_to_hint.update(ConnectorEntrances)
entrances_to_hint.update(OtherEntrances) entrances_to_hint.update(OtherEntrances)
if world.mode[player] == 'inverted': if world.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Dark Sanctuary': 'The dark sanctuary cave'}) entrances_to_hint.update({'Inverted Dark Sanctuary': 'The dark sanctuary cave'})
entrances_to_hint.update({'Inverted Big Bomb Shop': 'The old hero\'s dark home'}) entrances_to_hint.update({'Inverted Big Bomb Shop': 'The old hero\'s dark home'})
entrances_to_hint.update({'Inverted Links House': 'The old hero\'s light home'}) entrances_to_hint.update({'Inverted Links House': 'The old hero\'s light home'})
else: else:
entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'}) entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'})
entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'}) entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'})
if world.entrance_shuffle[player] != 'insanity': if world.worlds[player].options.entrance_shuffle != 'insanity':
entrances_to_hint.update(InsanityEntrances) entrances_to_hint.update(InsanityEntrances)
if world.shuffle_ganon: if world.shuffle_ganon:
if world.mode[player] == 'inverted': if world.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'}) entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'})
else: else:
entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'}) entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'})
hint_count = 4 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full', hint_count = 4 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
'dungeons_crossed'] else 0 'dungeons_crossed'] else 0
for entrance in all_entrances: for entrance in all_entrances:
if entrance.name in entrances_to_hint: if entrance.name in entrances_to_hint:
@@ -2337,10 +2324,10 @@ def write_strings(rom, world, player):
# Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable. # Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable.
locations_to_hint = InconvenientLocations.copy() locations_to_hint = InconvenientLocations.copy()
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
locations_to_hint.extend(InconvenientVanillaLocations) locations_to_hint.extend(InconvenientVanillaLocations)
local_random.shuffle(locations_to_hint) local_random.shuffle(locations_to_hint)
hint_count = 3 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full', hint_count = 3 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
'dungeons_crossed'] else 5 'dungeons_crossed'] else 5
for location in locations_to_hint[:hint_count]: for location in locations_to_hint[:hint_count]:
if location == 'Swamp Left': if location == 'Swamp Left':
@@ -2395,15 +2382,15 @@ def write_strings(rom, world, player):
# Lastly we write hints to show where certain interesting items are. # Lastly we write hints to show where certain interesting items are.
items_to_hint = RelevantItems.copy() items_to_hint = RelevantItems.copy()
if world.small_key_shuffle[player].hints_useful: if world.worlds[player].options.small_key_shuffle.hints_useful:
items_to_hint |= item_name_groups["Small Keys"] items_to_hint |= item_name_groups["Small Keys"]
if world.big_key_shuffle[player].hints_useful: if world.worlds[player].options.big_key_shuffle.hints_useful:
items_to_hint |= item_name_groups["Big Keys"] items_to_hint |= item_name_groups["Big Keys"]
if world.hints[player] == "full": if world.worlds[player].options.hints == "full":
hint_count = len(hint_locations) # fill all remaining hint locations with Item hints. hint_count = len(hint_locations) # fill all remaining hint locations with Item hints.
else: else:
hint_count = 5 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full', hint_count = 5 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
'dungeons_crossed'] else 8 'dungeons_crossed'] else 8
hint_count = min(hint_count, len(items_to_hint), len(hint_locations)) hint_count = min(hint_count, len(items_to_hint), len(hint_locations))
if hint_count: if hint_count:
@@ -2434,7 +2421,7 @@ def write_strings(rom, world, player):
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint
if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or ( if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
world.swordless[player] or world.glitches_required[player] == 'no_glitches')): world.worlds[player].options.swordless or world.worlds[player].options.glitches_required == 'no_glitches')):
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True) prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
local_random.shuffle(prog_bow_locs) local_random.shuffle(prog_bow_locs)
found_bow = False found_bow = False
@@ -2458,26 +2445,26 @@ def write_strings(rom, world, player):
greenpendant = world.find_item('Green Pendant', player) greenpendant = world.find_item('Green Pendant', player)
tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text
if world.crystals_needed_for_gt[player] == 1: if world.worlds[player].options.crystals_needed_for_gt == 1:
tt['sign_ganons_tower'] = 'You need a crystal to enter.' tt['sign_ganons_tower'] = 'You need a crystal to enter.'
else: else:
tt['sign_ganons_tower'] = f'You need {world.crystals_needed_for_gt[player]} crystals to enter.' tt['sign_ganons_tower'] = f'You need {world.worlds[player].options.crystals_needed_for_gt} crystals to enter.'
if world.goal[player] == 'bosses': if world.worlds[player].options.goal == 'bosses':
tt['sign_ganon'] = 'You need to kill all bosses, Ganon last.' tt['sign_ganon'] = 'You need to kill all bosses, Ganon last.'
elif world.goal[player] == 'ganon_pedestal': elif world.worlds[player].options.goal == 'ganon_pedestal':
tt['sign_ganon'] = 'You need to pull the pedestal to defeat Ganon.' tt['sign_ganon'] = 'You need to pull the pedestal to defeat Ganon.'
elif world.goal[player] == "ganon": elif world.worlds[player].options.goal == "ganon":
if world.crystals_needed_for_ganon[player] == 1: if world.worlds[player].options.crystals_needed_for_ganon == 1:
tt['sign_ganon'] = 'You need a crystal to beat Ganon and have beaten Agahnim atop Ganons Tower.' tt['sign_ganon'] = 'You need a crystal to beat Ganon and have beaten Agahnim atop Ganons Tower.'
else: else:
tt['sign_ganon'] = f'You need {world.crystals_needed_for_ganon[player]} crystals to beat Ganon and ' \ tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \
f'have beaten Agahnim atop Ganons Tower' f'have beaten Agahnim atop Ganons Tower'
else: else:
if world.crystals_needed_for_ganon[player] == 1: if world.worlds[player].options.crystals_needed_for_ganon == 1:
tt['sign_ganon'] = 'You need a crystal to beat Ganon.' tt['sign_ganon'] = 'You need a crystal to beat Ganon.'
else: else:
tt['sign_ganon'] = f'You need {world.crystals_needed_for_ganon[player]} crystals to beat Ganon.' tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon.'
tt['uncle_leaving_text'] = Uncle_texts[local_random.randint(0, len(Uncle_texts) - 1)] tt['uncle_leaving_text'] = Uncle_texts[local_random.randint(0, len(Uncle_texts) - 1)]
tt['end_triforce'] = "{NOBORDER}\n" + Triforce_texts[local_random.randint(0, len(Triforce_texts) - 1)] tt['end_triforce'] = "{NOBORDER}\n" + Triforce_texts[local_random.randint(0, len(Triforce_texts) - 1)]
@@ -2490,10 +2477,10 @@ def write_strings(rom, world, player):
triforce_pieces_required = max(0, w.treasure_hunt_required - triforce_pieces_required = max(0, w.treasure_hunt_required -
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")) sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))
if world.goal[player] in ['triforce_hunt', 'local_triforce_hunt']: if world.worlds[player].options.goal in ['triforce_hunt', 'local_triforce_hunt']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.' tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
if world.goal[player] == 'triforce_hunt' and world.players > 1: if world.worlds[player].options.goal == 'triforce_hunt' and world.players > 1:
tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!' tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!'
else: else:
tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!' tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!'
@@ -2507,7 +2494,7 @@ def write_strings(rom, world, player):
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
"hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \ "hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \
(triforce_pieces_required, w.treasure_hunt_total) (triforce_pieces_required, w.treasure_hunt_total)
elif world.goal[player] in ['pedestal']: elif world.worlds[player].options.goal in ['pedestal']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.' tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
tt['sign_ganon'] = 'You need to get to the pedestal... Ganon is invincible!' tt['sign_ganon'] = 'You need to get to the pedestal... Ganon is invincible!'
@@ -2516,17 +2503,17 @@ def write_strings(rom, world, player):
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!' tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!' tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
if triforce_pieces_required > 1: if triforce_pieces_required > 1:
if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1: if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1:
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \ tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total) (triforce_pieces_required, w.treasure_hunt_total)
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \ tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total) (triforce_pieces_required, w.treasure_hunt_total)
else: else:
if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1: if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1:
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \ tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total) (triforce_pieces_required, w.treasure_hunt_total)
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \ tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total) (triforce_pieces_required, w.treasure_hunt_total)
@@ -2549,11 +2536,11 @@ def write_strings(rom, world, player):
tt['tablet_bombos_book'] = bombos_text tt['tablet_bombos_book'] = bombos_text
# inverted spawn menu changes # inverted spawn menu changes
if world.mode[player] == 'inverted': if world.worlds[player].options.mode == 'inverted':
tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}" tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}"
tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}" tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}"
for at, text, _ in world.plando_texts[player]: for at, text, _ in world.worlds[player].options.plando_texts:
if at not in tt: if at not in tt:
raise Exception(f"No text target \"{at}\" found.") raise Exception(f"No text target \"{at}\" found.")
@@ -2626,12 +2613,12 @@ def set_inverted_mode(world, player, rom):
rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof
# the following bytes should only be written in vanilla # the following bytes should only be written in vanilla
# or they'll overwrite the randomizer's shuffles # or they'll overwrite the randomizer's shuffles
if world.entrance_shuffle[player] == 'vanilla': if world.worlds[player].options.entrance_shuffle == 'vanilla':
rom.write_byte(0xDBB73 + 0x23, 0x37) # switch AT and GT rom.write_byte(0xDBB73 + 0x23, 0x37) # switch AT and GT
rom.write_byte(0xDBB73 + 0x36, 0x24) rom.write_byte(0xDBB73 + 0x36, 0x24)
rom.write_int16(0x15AEE + 2 * 0x38, 0x00E0) rom.write_int16(0x15AEE + 2 * 0x38, 0x00E0)
rom.write_int16(0x15AEE + 2 * 0x25, 0x000C) rom.write_int16(0x15AEE + 2 * 0x25, 0x000C)
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(0x15B8C, 0x6C) rom.write_byte(0x15B8C, 0x6C)
rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house
rom.write_byte(0xDBB73 + 0x52, 0x01) rom.write_byte(0xDBB73 + 0x52, 0x01)
@@ -2689,7 +2676,7 @@ def set_inverted_mode(world, player, rom):
rom.write_int16(snes_to_pc(0x02D9A6), 0x005A) rom.write_int16(snes_to_pc(0x02D9A6), 0x005A)
rom.write_byte(snes_to_pc(0x02D9B3), 0x12) rom.write_byte(snes_to_pc(0x02D9B3), 0x12)
# keep the old man spawn point at old man house unless shuffle is vanilla # keep the old man spawn point at old man house unless shuffle is vanilla
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']: if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01]) rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01])
rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1) rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1)
rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03]) rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03])
@@ -2752,7 +2739,7 @@ def set_inverted_mode(world, player, rom):
rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B]) rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B])
rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance
rom.write_int16(snes_to_pc(0x308320), 0x001B) rom.write_int16(snes_to_pc(0x308320), 0x001B)
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(snes_to_pc(0x308340), 0x7B) rom.write_byte(snes_to_pc(0x308340), 0x7B)
rom.write_int16(snes_to_pc(0x1af504), 0x148B) rom.write_int16(snes_to_pc(0x1af504), 0x148B)
rom.write_int16(snes_to_pc(0x1af50c), 0x149B) rom.write_int16(snes_to_pc(0x1af50c), 0x149B)
@@ -2789,10 +2776,10 @@ def set_inverted_mode(world, player, rom):
rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82]) rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82])
rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door
rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4) rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4)
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(0xDBB73 + 0x35, 0x36) rom.write_byte(0xDBB73 + 0x35, 0x36)
rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area
rom.write_byte(0x15B8C + 0x37, 0x1B) rom.write_byte(0x15B8C + 0x37, 0x1B)
rom.write_int16(0x15BDB + 2 * 0x37, 0x0418) rom.write_int16(0x15BDB + 2 * 0x37, 0x0418)

View File

@@ -3,7 +3,7 @@ import logging
from typing import Iterator, Set from typing import Iterator, Set
from Options import ItemsAccessibility from Options import ItemsAccessibility
from BaseClasses import Entrance, MultiWorld from BaseClasses import MultiWorld
from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item, from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item,
item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items) item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items)
@@ -27,9 +27,9 @@ from .UnderworldGlitchRules import underworld_glitches_rules
def set_rules(world): def set_rules(world):
player = world.player player = world.player
world = world.multiworld world = world.multiworld
if world.glitches_required[player] == 'no_logic': if world.worlds[player].options.glitches_required == 'no_logic':
if player == next(player_id for player_id in world.get_game_players("A Link to the Past") if player == next(player_id for player_id in world.get_game_players("A Link to the Past")
if world.glitches_required[player_id] == 'no_logic'): # only warn one time if world.worlds[player_id].options.glitches_required == 'no_logic'): # only warn one time
logging.info( logging.info(
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!') 'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
@@ -40,8 +40,8 @@ def set_rules(world):
else: else:
# Set access rules according to max glitches for multiworld progression. # Set access rules according to max glitches for multiworld progression.
# Set accessibility to none, and shuffle assuming the no logic players can always win # Set accessibility to none, and shuffle assuming the no logic players can always win
world.accessibility[player].value = ItemsAccessibility.option_minimal world.worlds[player].options.accessibility.value = ItemsAccessibility.option_minimal
world.progression_balancing[player].value = 0 world.worlds[player].options.progression_balancing.value = 0
else: else:
world.completion_condition[player] = lambda state: state.has('Triforce', player) world.completion_condition[player] = lambda state: state.has('Triforce', player)
@@ -49,52 +49,52 @@ def set_rules(world):
dungeon_boss_rules(world, player) dungeon_boss_rules(world, player)
global_rules(world, player) global_rules(world, player)
if world.mode[player] != 'inverted': if world.worlds[player].options.mode != 'inverted':
default_rules(world, player) default_rules(world, player)
if world.mode[player] == 'open': if world.worlds[player].options.mode == 'open':
open_rules(world, player) open_rules(world, player)
elif world.mode[player] == 'standard': elif world.worlds[player].options.mode == 'standard':
standard_rules(world, player) standard_rules(world, player)
elif world.mode[player] == 'inverted': elif world.worlds[player].options.mode == 'inverted':
open_rules(world, player) open_rules(world, player)
inverted_rules(world, player) inverted_rules(world, player)
else: else:
raise NotImplementedError(f'World state {world.mode[player]} is not implemented yet') raise NotImplementedError(f'World state {world.worlds[player].options.mode} is not implemented yet')
if world.glitches_required[player] == 'no_glitches': if world.worlds[player].options.glitches_required == 'no_glitches':
no_glitches_rules(world, player) no_glitches_rules(world, player)
forbid_bomb_jump_requirements(world, player) forbid_bomb_jump_requirements(world, player)
elif world.glitches_required[player] == 'overworld_glitches': elif world.worlds[player].options.glitches_required == 'overworld_glitches':
# Initially setting no_glitches_rules to set the baseline rules for some # Initially setting no_glitches_rules to set the baseline rules for some
# entrances. The overworld_glitches_rules set is primarily additive. # entrances. The overworld_glitches_rules set is primarily additive.
no_glitches_rules(world, player) no_glitches_rules(world, player)
fake_flipper_rules(world, player) fake_flipper_rules(world, player)
overworld_glitches_rules(world, player) overworld_glitches_rules(world, player)
forbid_bomb_jump_requirements(world, player) forbid_bomb_jump_requirements(world, player)
elif world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']: elif world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
no_glitches_rules(world, player) no_glitches_rules(world, player)
fake_flipper_rules(world, player) fake_flipper_rules(world, player)
overworld_glitches_rules(world, player) overworld_glitches_rules(world, player)
underworld_glitches_rules(world, player) underworld_glitches_rules(world, player)
bomb_jump_requirements(world, player) bomb_jump_requirements(world, player)
elif world.glitches_required[player] == 'minor_glitches': elif world.worlds[player].options.glitches_required == 'minor_glitches':
no_glitches_rules(world, player) no_glitches_rules(world, player)
fake_flipper_rules(world, player) fake_flipper_rules(world, player)
forbid_bomb_jump_requirements(world, player) forbid_bomb_jump_requirements(world, player)
else: else:
raise NotImplementedError(f'Not implemented yet: Logic - {world.glitches_required[player]}') raise NotImplementedError(f'Not implemented yet: Logic - {world.worlds[player].options.glitches_required}')
if world.goal[player] == 'bosses': if world.worlds[player].options.goal == '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 has_crystals(state, 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.worlds[player].options.goal == '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))
if world.mode[player] != 'inverted': if world.worlds[player].options.mode != 'inverted':
set_big_bomb_rules(world, player) set_big_bomb_rules(world, player)
if world.glitches_required[player].current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.entrance_shuffle[player].current_key not in {'insanity', 'insanity_legacy', 'madness'}: if world.worlds[player].options.glitches_required.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.entrance_shuffle.current_key not in {'insanity', 'insanity_legacy', 'madness'}:
path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player) path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player)
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.multiworld.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or') add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.multiworld.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or')
else: else:
@@ -102,21 +102,24 @@ def set_rules(world):
# if swamp and dam have not been moved we require mirror for swamp palace # if swamp and dam have not been moved we require mirror for swamp palace
# however there is mirrorless swamp in hybrid MG, so we don't necessarily want this. HMG handles this requirement itself. # however there is mirrorless swamp in hybrid MG, so we don't necessarily want this. HMG handles this requirement itself.
if not world.worlds[player].swamp_patch_required and world.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']: if not world.worlds[player].swamp_patch_required and world.worlds[player].options.glitches_required not in ['hybrid_major_glitches', 'no_logic']:
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
# GT Entrance may be required for Turtle Rock for OWG and < 7 required # GT Entrance may be required for Turtle Rock for OWG and < 7 required
ganons_tower = world.get_entrance('Inverted Ganons Tower' if world.mode[player] == 'inverted' else 'Ganons Tower', player) ganons_tower = world.get_entrance('Inverted Ganons Tower' if world.worlds[player].options.mode == 'inverted' else 'Ganons Tower', player)
if world.crystals_needed_for_gt[player] == 7 and not (world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and world.mode[player] != 'inverted'): if (world.worlds[player].options.crystals_needed_for_gt == 7
and not (world.worlds[player].options.glitches_required
in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']
and world.worlds[player].options.mode != 'inverted')):
set_rule(ganons_tower, lambda state: False) set_rule(ganons_tower, lambda state: False)
set_trock_key_rules(world, player) set_trock_key_rules(world, player)
set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_gt[player], player)) set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.worlds[player].options.crystals_needed_for_gt, player))
if world.mode[player] != 'inverted' and world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: if world.worlds[player].options.mode != 'inverted' and world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
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')
set_bunny_rules(world, player, world.mode[player] == 'inverted') set_bunny_rules(world, player, world.worlds[player].options.mode == 'inverted')
def mirrorless_path_to_castle_courtyard(world, player): def mirrorless_path_to_castle_courtyard(world, player):
@@ -150,17 +153,17 @@ def set_always_allow(spot, rule):
def add_lamp_requirement(world: MultiWorld, spot, player: int, has_accessible_torch: bool = False): def add_lamp_requirement(world: MultiWorld, spot, player: int, has_accessible_torch: bool = False):
if world.dark_room_logic[player] == "lamp": if world.worlds[player].options.dark_room_logic == "lamp":
add_rule(spot, lambda state: state.has('Lamp', player)) add_rule(spot, lambda state: state.has('Lamp', player))
elif world.dark_room_logic[player] == "torches": # implicitly lamp as well elif world.worlds[player].options.dark_room_logic == "torches": # implicitly lamp as well
if has_accessible_torch: if has_accessible_torch:
add_rule(spot, lambda state: state.has('Lamp', player) or state.has('Fire Rod', player)) add_rule(spot, lambda state: state.has('Lamp', player) or state.has('Fire Rod', player))
else: else:
add_rule(spot, lambda state: state.has('Lamp', player)) add_rule(spot, lambda state: state.has('Lamp', player))
elif world.dark_room_logic[player] == "none": elif world.worlds[player].options.dark_room_logic == "none":
pass pass
else: else:
raise ValueError(f"Unknown Dark Room Logic: {world.dark_room_logic[player]}") raise ValueError(f"Unknown Dark Room Logic: {world.worlds[player].options.dark_room_logic}")
non_crossover_items = (item_name_groups["Small Keys"] | item_name_groups["Big Keys"] | progression_items) - { non_crossover_items = (item_name_groups["Small Keys"] | item_name_groups["Big Keys"] | progression_items) - {
@@ -227,12 +230,13 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player)) set_rule(multiworld.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player))
set_rule(multiworld.get_location('Library', player), lambda state: state.has('Pegasus Boots', player)) set_rule(multiworld.get_location('Library', player), lambda state: state.has('Pegasus Boots', player))
if multiworld.enemy_shuffle[player]: if world.options.enemy_shuffle:
set_rule(multiworld.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player) and set_rule(multiworld.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player) and
can_kill_most_things(state, player, 4)) can_kill_most_things(state, player, 4))
else: else:
set_rule(multiworld.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player) set_rule(multiworld.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player)
and ((state.multiworld.enemy_health[player] in ("easy", "default") and can_use_bombs(state, player, 4)) and ((state.multiworld.worlds[player].options.enemy_health in ("easy", "default")
and can_use_bombs(state, player, 4))
or can_shoot_arrows(state, player) or state.has("Cane of Somaria", player) or can_shoot_arrows(state, player) or state.has("Cane of Somaria", player)
or has_beam_sword(state, player))) or has_beam_sword(state, player)))
@@ -299,8 +303,7 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_entrance('Sewers Door', player), set_rule(multiworld.get_entrance('Sewers Door', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) or ( lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) or (
multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal and multiworld.mode[ world.options.small_key_shuffle == small_key_shuffle.option_universal and world.options.mode == 'standard')) # standard universal small keys cannot access the shop
player] == 'standard')) # standard universal small keys cannot access the shop
set_rule(multiworld.get_entrance('Sewers Back Door', player), set_rule(multiworld.get_entrance('Sewers Back Door', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4)) lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4))
set_rule(multiworld.get_entrance('Sewers Secret Room', player), lambda state: can_bomb_or_bonk(state, player)) set_rule(multiworld.get_entrance('Sewers Secret Room', player), lambda state: can_bomb_or_bonk(state, player))
@@ -339,12 +342,12 @@ def global_rules(multiworld: MultiWorld, player: int):
add_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and add_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and
state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and
ep_prize.parent_region.dungeon.boss.can_defeat(state)) ep_prize.parent_region.dungeon.boss.can_defeat(state))
if not multiworld.enemy_shuffle[player]: if not world.options.enemy_shuffle:
add_rule(ep_boss, lambda state: can_shoot_arrows(state, player)) add_rule(ep_boss, lambda state: can_shoot_arrows(state, player))
add_rule(ep_prize, lambda state: can_shoot_arrows(state, player)) add_rule(ep_prize, lambda state: can_shoot_arrows(state, player))
# You can always kill the Stalfos' with the pots on easy/normal # You can always kill the Stalfos' with the pots on easy/normal
if multiworld.enemy_health[player] in ("hard", "expert") or multiworld.enemy_shuffle[player]: if world.options.enemy_health in ("hard", "expert") or world.options.enemy_shuffle:
stalfos_rule = lambda state: can_kill_most_things(state, player, 4) stalfos_rule = lambda state: can_kill_most_things(state, player, 4)
for location in ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', for location in ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop',
@@ -362,14 +365,14 @@ def global_rules(multiworld: MultiWorld, player: int):
add_rule(multiworld.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) 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)) add_rule(multiworld.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) 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 (multiworld.small_key_shuffle[player] and multiworld.big_key_shuffle[player]): if not (world.options.small_key_shuffle and world.options.big_key_shuffle):
add_rule(multiworld.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state)) add_rule(multiworld.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state))
set_rule(multiworld.get_location('Tower of Hera - Basement Cage', player), lambda state: can_activate_crystal_switch(state, player)) set_rule(multiworld.get_location('Tower of Hera - Basement Cage', player), lambda state: can_activate_crystal_switch(state, player))
set_rule(multiworld.get_location('Tower of Hera - Map Chest', player), lambda state: can_activate_crystal_switch(state, player)) set_rule(multiworld.get_location('Tower of Hera - Map Chest', player), lambda state: can_activate_crystal_switch(state, player))
set_rule(multiworld.get_entrance('Tower of Hera Small Key Door', player), lambda state: can_activate_crystal_switch(state, player) and (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(multiworld.get_entrance('Tower of Hera Small Key Door', player), lambda state: can_activate_crystal_switch(state, player) and (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(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_activate_crystal_switch(state, player) and state.has('Big Key (Tower of Hera)', player)) set_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_activate_crystal_switch(state, player) and state.has('Big Key (Tower of Hera)', player))
if multiworld.enemy_shuffle[player]: if world.options.enemy_shuffle:
add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_kill_most_things(state, player, 3)) add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_kill_most_things(state, player, 3))
else: else:
add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player),
@@ -378,7 +381,7 @@ def global_rules(multiworld: MultiWorld, player: int):
or state.has("Cane of Somaria", player))) or state.has("Cane of Somaria", player)))
set_rule(multiworld.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(multiworld.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
set_rule(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player)) set_rule(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player))
if multiworld.accessibility[player] != 'full': if world.options.accessibility != 'full':
set_always_allow(multiworld.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(multiworld.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(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player)) set_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
@@ -387,32 +390,32 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_location('Swamp Palace - Trench 1 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 2)) set_rule(multiworld.get_location('Swamp Palace - Trench 1 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 2))
set_rule(multiworld.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 3)) set_rule(multiworld.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 3))
set_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: state.has('Hookshot', player)) set_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: state.has('Hookshot', player))
if multiworld.pot_shuffle[player]: if world.options.pot_shuffle:
# it could move the key to the top right platform which can only be reached with bombs # it could move the key to the top right platform which can only be reached with bombs
add_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: can_use_bombs(state, player)) add_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Swamp Palace (West)', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6) set_rule(multiworld.get_entrance('Swamp Palace (West)', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)
if state.has('Hookshot', player) if state.has('Hookshot', player)
else state._lttp_has_key('Small Key (Swamp Palace)', player, 4)) else state._lttp_has_key('Small Key (Swamp Palace)', player, 4))
set_rule(multiworld.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player)) set_rule(multiworld.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
if multiworld.accessibility[player] != 'full': if world.options.accessibility != 'full':
allow_self_locking_items(multiworld.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)') allow_self_locking_items(multiworld.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5)) set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5))
if not multiworld.small_key_shuffle[player] and multiworld.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']: if not world.options.small_key_shuffle and world.options.glitches_required not in ['hybrid_major_glitches', 'no_logic']:
forbid_item(multiworld.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player) forbid_item(multiworld.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
add_rule(multiworld.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)) add_rule(multiworld.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
add_rule(multiworld.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)) add_rule(multiworld.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
if multiworld.pot_shuffle[player]: if world.options.pot_shuffle:
# key can (and probably will) be moved behind bombable wall # key can (and probably will) be moved behind bombable wall
set_rule(multiworld.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player)) set_rule(multiworld.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player)) set_rule(multiworld.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
if multiworld.worlds[player].dungeons["Thieves Town"].boss.enemizer_name == "Blind": if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind":
set_rule(multiworld.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3) and can_use_bombs(state, player)) set_rule(multiworld.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3) and can_use_bombs(state, player))
set_rule(multiworld.get_location('Thieves\' Town - Big Chest', player), set_rule(multiworld.get_location('Thieves\' Town - Big Chest', player),
lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player)) lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player), set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player),
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player)) lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
if multiworld.accessibility[player] != 'full' and not multiworld.key_drop_shuffle[player]: if world.options.accessibility != 'full' and not world.options.key_drop_shuffle:
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player) set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3)) set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player), set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
@@ -424,7 +427,7 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) set_rule(multiworld.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
set_rule(multiworld.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) set_rule(multiworld.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
set_rule(multiworld.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player)) set_rule(multiworld.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player))
if multiworld.accessibility[player] != 'full': if world.options.accessibility != 'full':
allow_self_locking_items(multiworld.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)') allow_self_locking_items(multiworld.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
add_rule(multiworld.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) add_rule(multiworld.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
@@ -501,13 +504,13 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player)) set_rule(multiworld.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
set_rule(multiworld.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_kill_most_things(state, player, 10)) set_rule(multiworld.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_kill_most_things(state, player, 10))
if not multiworld.worlds[player].fix_trock_doors: if not world.fix_trock_doors:
add_rule(multiworld.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_use_bombs(state, player)) add_rule(multiworld.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Turtle Rock Second Section from Bomb Wall', player), lambda state: can_use_bombs(state, player)) set_rule(multiworld.get_entrance('Turtle Rock Second Section from Bomb Wall', player), lambda state: can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Turtle Rock Eye Bridge from Bomb Wall', player), lambda state: can_use_bombs(state, player)) set_rule(multiworld.get_entrance('Turtle Rock Eye Bridge from Bomb Wall', player), lambda state: can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Turtle Rock Eye Bridge Bomb Wall', player), lambda state: can_use_bombs(state, player)) set_rule(multiworld.get_entrance('Turtle Rock Eye Bridge Bomb Wall', player), lambda state: can_use_bombs(state, player))
if multiworld.enemy_shuffle[player]: if world.options.enemy_shuffle:
set_rule(multiworld.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_kill_most_things(state, player, 3)) set_rule(multiworld.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_kill_most_things(state, player, 3))
else: else:
set_rule(multiworld.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_shoot_arrows(state, player)) set_rule(multiworld.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_shoot_arrows(state, player))
@@ -517,18 +520,18 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)) set_rule(multiworld.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))
set_rule(multiworld.get_location('Palace of Darkness - Big Chest', player), lambda state: can_use_bombs(state, player) and state.has('Big Key (Palace of Darkness)', player)) set_rule(multiworld.get_location('Palace of Darkness - Big Chest', player), lambda state: can_use_bombs(state, player) and state.has('Big Key (Palace of Darkness)', player))
set_rule(multiworld.get_location('Palace of Darkness - The Arena - Ledge', player), lambda state: can_use_bombs(state, player)) set_rule(multiworld.get_location('Palace of Darkness - The Arena - Ledge', player), lambda state: can_use_bombs(state, player))
if multiworld.pot_shuffle[player]: if world.options.pot_shuffle:
# chest switch may be up on ledge where bombs are required # chest switch may be up on ledge where bombs are required
set_rule(multiworld.get_location('Palace of Darkness - Stalfos Basement', player), lambda state: can_use_bombs(state, player)) set_rule(multiworld.get_location('Palace of Darkness - Stalfos Basement', player), lambda state: can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or ( set_rule(multiworld.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
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)))) 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 multiworld.accessibility[player] != 'full': if world.options.accessibility != 'full':
set_always_allow(multiworld.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(multiworld.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(multiworld.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(multiworld.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
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))) 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 multiworld.accessibility[player] != 'full': if world.options.accessibility != 'full':
set_always_allow(multiworld.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(multiworld.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_rule(multiworld.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6)) set_rule(multiworld.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
@@ -541,13 +544,13 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player)) set_rule(multiworld.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player))
set_rule(multiworld.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(multiworld.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
set_rule(multiworld.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(multiworld.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
if multiworld.pot_shuffle[player]: if world.options.pot_shuffle:
set_rule(multiworld.get_location('Ganons Tower - Conveyor Cross Pot Key', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player))) set_rule(multiworld.get_location('Ganons Tower - Conveyor Cross Pot Key', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
set_rule(multiworld.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or ( set_rule(multiworld.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6))) location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
# this seemed to be causing generation failure, disable for now # this seemed to be causing generation failure, disable for now
# if world.accessibility[player] != 'full': # if world.worlds[player].options.accessibility != 'full':
# 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, 7) 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, 7) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
# It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements. # It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
@@ -582,7 +585,7 @@ def global_rules(multiworld: MultiWorld, player: int):
lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Chest', player).parent_region.dungeon.bosses['bottom'].can_defeat(state)) lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Chest', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
set_rule(multiworld.get_location('Ganons Tower - Big Key Room - Right', player), set_rule(multiworld.get_location('Ganons Tower - Big Key Room - Right', player),
lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Room - Right', player).parent_region.dungeon.bosses['bottom'].can_defeat(state)) lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Room - Right', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
if multiworld.enemy_shuffle[player]: if world.options.enemy_shuffle:
set_rule(multiworld.get_entrance('Ganons Tower Big Key Door', player), set_rule(multiworld.get_entrance('Ganons Tower Big Key Door', player),
lambda state: state.has('Big Key (Ganons Tower)', player)) lambda state: state.has('Big Key (Ganons Tower)', player))
else: else:
@@ -600,12 +603,12 @@ def global_rules(multiworld: MultiWorld, player: int):
set_defeat_dungeon_boss_rule(multiworld.get_location('Agahnim 2', player)) set_defeat_dungeon_boss_rule(multiworld.get_location('Agahnim 2', player))
ganon = multiworld.get_location('Ganon', player) ganon = multiworld.get_location('Ganon', player)
set_rule(ganon, lambda state: GanonDefeatRule(state, player)) set_rule(ganon, lambda state: GanonDefeatRule(state, player))
if multiworld.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: if world.options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
add_rule(ganon, lambda state: has_triforce_pieces(state, player)) add_rule(ganon, lambda state: has_triforce_pieces(state, player))
elif multiworld.goal[player] == 'ganon_pedestal': elif world.options.goal == 'ganon_pedestal':
add_rule(multiworld.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player)) add_rule(multiworld.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player))
else: else:
add_rule(ganon, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_ganon[player], player)) add_rule(ganon, lambda state: has_crystals(state, state.multiworld.worlds[player].options.crystals_needed_for_ganon, player))
set_rule(multiworld.get_entrance('Ganon Drop', player), lambda state: has_beam_sword(state, player)) # need to damage ganon to get tiles to drop set_rule(multiworld.get_entrance('Ganon Drop', player), lambda state: has_beam_sword(state, player)) # need to damage ganon to get tiles to drop
set_rule(multiworld.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player)) set_rule(multiworld.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player))
@@ -722,9 +725,9 @@ def default_rules(world, 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('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('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.worlds[player].options.open_pyramid.to_bool(world, player))
if world.swordless[player]: if world.worlds[player].options.swordless:
swordless_rules(world, player) swordless_rules(world, player)
@@ -879,14 +882,14 @@ def inverted_rules(world, player):
set_rule(world.get_entrance('Dark Grassy Lawn Flute', player), lambda state: state.has('Activated Flute', player)) set_rule(world.get_entrance('Dark Grassy Lawn Flute', player), lambda state: state.has('Activated Flute', player))
set_rule(world.get_entrance('Hammer Peg Area Flute', player), lambda state: state.has('Activated Flute', player)) set_rule(world.get_entrance('Hammer Peg Area Flute', player), lambda state: state.has('Activated Flute', player))
set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player]) set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.worlds[player].options.open_pyramid)
if world.swordless[player]: if world.worlds[player].options.swordless:
swordless_rules(world, player) swordless_rules(world, player)
def no_glitches_rules(world, player): def no_glitches_rules(world, player):
"""""" """"""
if world.mode[player] == 'inverted': if world.worlds[player].options.mode == 'inverted':
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('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
@@ -910,7 +913,7 @@ def no_glitches_rules(world, player):
add_conditional_lamps(world, player) add_conditional_lamps(world, player)
def fake_flipper_rules(world, player): def fake_flipper_rules(world, player):
if world.mode[player] == 'inverted': if world.worlds[player].options.mode == 'inverted':
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player))
@@ -996,7 +999,7 @@ def add_conditional_lamps(world, player):
'Location', True) 'Location', True)
add_conditional_lamp('Palace of Darkness - Dark Basement - Right', 'Palace of Darkness (Entrance)', add_conditional_lamp('Palace of Darkness - Dark Basement - Right', 'Palace of Darkness (Entrance)',
'Location', True) 'Location', True)
if world.mode[player] != 'inverted': if world.worlds[player].options.mode != 'inverted':
add_conditional_lamp('Agahnim 1', 'Agahnims Tower', 'Entrance') add_conditional_lamp('Agahnim 1', 'Agahnims Tower', 'Entrance')
add_conditional_lamp('Castle Tower - Dark Maze', 'Agahnims Tower') add_conditional_lamp('Castle Tower - Dark Maze', 'Agahnims Tower')
add_conditional_lamp('Castle Tower - Dark Archer Key Drop', 'Agahnims Tower') add_conditional_lamp('Castle Tower - Dark Archer Key Drop', 'Agahnims Tower')
@@ -1018,7 +1021,7 @@ def add_conditional_lamps(world, player):
add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location', True) add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location', True)
add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location', True) add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location', True)
if not world.mode[player] == "standard": if not world.worlds[player].options.mode == "standard":
add_lamp_requirement(world, world.get_location('Sewers - Dark Cross', player), player) add_lamp_requirement(world, world.get_location('Sewers - Dark Cross', player), player)
add_lamp_requirement(world, world.get_entrance('Sewers Back Door', player), player) add_lamp_requirement(world, world.get_entrance('Sewers Back Door', player), player)
add_lamp_requirement(world, world.get_entrance('Throne Room', player), player) add_lamp_requirement(world, world.get_entrance('Throne Room', player), player)
@@ -1044,7 +1047,7 @@ def open_rules(world, player):
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4)
and state.has('Big Key (Hyrule Castle)', player) and state.has('Big Key (Hyrule Castle)', player)
and (world.enemy_health[player] in ("easy", "default") and (world.worlds[player].options.enemy_health in ("easy", "default")
or can_kill_most_things(state, player, 1))) or can_kill_most_things(state, player, 1)))
@@ -1058,7 +1061,7 @@ def swordless_rules(world, player):
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.worlds[player].options.mode != '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 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('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 has_misery_mire_medallion(state, 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 (!)
@@ -1071,9 +1074,8 @@ def swordless_rules(world, player):
def add_connection(parent_name, target_name, entrance_name, world, player): def add_connection(parent_name, target_name, entrance_name, world, player):
parent = world.get_region(parent_name, player) parent = world.get_region(parent_name, player)
target = world.get_region(target_name, player) target = world.get_region(target_name, player)
connection = Entrance(player, entrance_name, parent) parent.connect(target, entrance_name)
parent.exits.append(connection)
connection.connect(target)
def standard_rules(world, player): def standard_rules(world, player):
@@ -1085,7 +1087,7 @@ def standard_rules(world, player):
set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
if world.small_key_shuffle[player] != small_key_shuffle.option_universal: if world.worlds[player].options.small_key_shuffle != small_key_shuffle.option_universal:
set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1) lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)
and can_kill_most_things(state, player, 2)) and can_kill_most_things(state, player, 2))
@@ -1098,7 +1100,7 @@ def standard_rules(world, player):
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2) lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2)
and state.has('Big Key (Hyrule Castle)', player) and state.has('Big Key (Hyrule Castle)', player)
and (world.enemy_health[player] in ("easy", "default") and (world.worlds[player].options.enemy_health in ("easy", "default")
or can_kill_most_things(state, player, 1))) or can_kill_most_things(state, player, 1)))
set_rule(world.get_location('Sewers - Key Rat Key Drop', player), set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
@@ -1108,6 +1110,7 @@ def standard_rules(world, player):
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
lambda state: state.has('Big Key (Hyrule Castle)', player)) lambda state: state.has('Big Key (Hyrule Castle)', player))
def toss_junk_item(world, player): def toss_junk_item(world, player):
items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)', items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)',
'Single Arrow', 'Rupees (50)', 'Rupees (100)', 'Single Bomb', 'Bee', 'Bee Trap', 'Single Arrow', 'Rupees (50)', 'Rupees (100)', 'Single Bomb', 'Bee', 'Bee Trap',
@@ -1195,15 +1198,15 @@ def set_trock_key_rules(multiworld, player):
return 6 return 6
# If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential # If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential
if not can_reach_front and not multiworld.small_key_shuffle[player]: if not can_reach_front and not multiworld.worlds[player].options.small_key_shuffle:
# Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests # Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests
forbid_item(multiworld.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player) forbid_item(multiworld.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
if not can_reach_big_chest: if not can_reach_big_chest:
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests # Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
forbid_item(multiworld.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player) forbid_item(multiworld.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
forbid_item(multiworld.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player) forbid_item(multiworld.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
if multiworld.accessibility[player] == 'full': if multiworld.worlds[player].options.accessibility == 'full':
if multiworld.big_key_shuffle[player] and can_reach_big_chest: if multiworld.worlds[player].options.big_key_shuffle and can_reach_big_chest:
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first # Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest', for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop', 'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop',
@@ -1216,9 +1219,9 @@ def set_trock_key_rules(multiworld, player):
location.place_locked_item(item) location.place_locked_item(item)
toss_junk_item(multiworld, player) toss_junk_item(multiworld, player)
if multiworld.accessibility[player] != 'full': if multiworld.worlds[player].options.accessibility != 'full':
set_always_allow(multiworld.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player set_always_allow(multiworld.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player))) and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
def set_big_bomb_rules(world, player): def set_big_bomb_rules(world, player):
@@ -1683,7 +1686,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
def get_rule_to_add(region, location = None, connecting_entrance = None): def get_rule_to_add(region, location = None, connecting_entrance = None):
# In OWG, a location can potentially be superbunny-mirror accessible or # In OWG, a location can potentially be superbunny-mirror accessible or
# bunny revival accessible. # bunny revival accessible.
if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic']: if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
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
@@ -1723,7 +1726,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
seen.add(new_region) seen.add(new_region)
if not is_link(new_region): if not is_link(new_region):
# For glitch rulesets, establish superbunny and revival rules. # For glitch rulesets, establish superbunny and revival rules.
if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] 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 has_sword(state, 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()
@@ -1760,7 +1763,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
# Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival # Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival
for entrance in world.get_entrances(player): for entrance in world.get_entrances(player):
if is_bunny(entrance.connected_region): if is_bunny(entrance.connected_region):
if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] : if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] :
if entrance.connected_region.type == LTTPRegionType.Dungeon: if entrance.connected_region.type == LTTPRegionType.Dungeon:
if entrance.parent_region.type != LTTPRegionType.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))
@@ -1768,7 +1771,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
if entrance.connected_region.name == 'Turtle Rock (Entrance)': if entrance.connected_region.name == 'Turtle Rock (Entrance)':
add_rule(world.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance)) add_rule(world.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance))
for location in entrance.connected_region.locations: for location in entrance.connected_region.locations:
if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances(): if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances():
continue continue
if location.name in bunny_accessible_locations: if location.name in bunny_accessible_locations:
continue continue

View File

@@ -168,7 +168,7 @@ def push_shop_inventories(multiworld):
for location in shop_slots: for location in shop_slots:
item_name = location.item.name item_name = location.item.name
# Retro Bow arrows will already have been pushed # Retro Bow arrows will already have been pushed
if (not multiworld.retro_bow[location.player]) or ((item_name, location.item.player) if (not multiworld.worlds[location.player].options.retro_bow) or ((item_name, location.item.player)
!= ("Single Arrow", location.player)): != ("Single Arrow", location.player)):
location.shop.push_inventory(location.shop_slot, item_name, location.shop.push_inventory(location.shop_slot, item_name,
round(location.shop_price * get_price_modifier(location.item)), round(location.shop_price * get_price_modifier(location.item)),
@@ -185,36 +185,36 @@ def push_shop_inventories(multiworld):
def create_shops(multiworld, player: int): def create_shops(multiworld, player: int):
from .Options import RandomizeShopInventories from .Options import RandomizeShopInventories
player_shop_table = shop_table.copy() player_shop_table = shop_table.copy()
if multiworld.include_witch_hut[player]: if multiworld.worlds[player].options.include_witch_hut:
player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False) player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False)
dynamic_shop_slots = total_dynamic_shop_slots + 3 dynamic_shop_slots = total_dynamic_shop_slots + 3
else: else:
dynamic_shop_slots = total_dynamic_shop_slots dynamic_shop_slots = total_dynamic_shop_slots
if multiworld.shuffle_capacity_upgrades[player]: if multiworld.worlds[player].options.shuffle_capacity_upgrades:
player_shop_table["Capacity Upgrade"] = player_shop_table["Capacity Upgrade"]._replace(locked=False) player_shop_table["Capacity Upgrade"] = player_shop_table["Capacity Upgrade"]._replace(locked=False)
num_slots = min(dynamic_shop_slots, multiworld.shop_item_slots[player]) num_slots = min(dynamic_shop_slots, multiworld.worlds[player].options.shop_item_slots)
single_purchase_slots: List[bool] = [True] * num_slots + [False] * (dynamic_shop_slots - num_slots) single_purchase_slots: List[bool] = [True] * num_slots + [False] * (dynamic_shop_slots - num_slots)
multiworld.random.shuffle(single_purchase_slots) multiworld.random.shuffle(single_purchase_slots)
if multiworld.randomize_shop_inventories[player]: if multiworld.worlds[player].options.randomize_shop_inventories:
default_shop_table = [i for l in default_shop_table = [i for l in
[shop_generation_types[x] for x in ['arrows', 'bombs', 'potions', 'shields', 'bottle'] if [shop_generation_types[x] for x in ['arrows', 'bombs', 'potions', 'shields', 'bottle'] if
not multiworld.retro_bow[player] or x != 'arrows'] for i in l] not multiworld.worlds[player].options.retro_bow or x != 'arrows'] for i in l]
new_basic_shop = multiworld.random.sample(default_shop_table, k=3) new_basic_shop = multiworld.random.sample(default_shop_table, k=3)
new_dark_shop = multiworld.random.sample(default_shop_table, k=3) new_dark_shop = multiworld.random.sample(default_shop_table, k=3)
for name, shop in player_shop_table.items(): for name, shop in player_shop_table.items():
typ, shop_id, keeper, custom, locked, items, sram_offset = shop typ, shop_id, keeper, custom, locked, items, sram_offset = shop
if not locked: if not locked:
new_items = multiworld.random.sample(default_shop_table, k=len(items)) new_items = multiworld.random.sample(default_shop_table, k=len(items))
if multiworld.randomize_shop_inventories[player] == RandomizeShopInventories.option_randomize_by_shop_type: if multiworld.worlds[player].options.randomize_shop_inventories == RandomizeShopInventories.option_randomize_by_shop_type:
if items == _basic_shop_defaults: if items == _basic_shop_defaults:
new_items = new_basic_shop new_items = new_basic_shop
elif items == _dark_world_shop_defaults: elif items == _dark_world_shop_defaults:
new_items = new_dark_shop new_items = new_dark_shop
keeper = multiworld.random.choice([0xA0, 0xC1, 0xFF]) keeper = multiworld.random.choice([0xA0, 0xC1, 0xFF])
player_shop_table[name] = ShopData(typ, shop_id, keeper, custom, locked, new_items, sram_offset) player_shop_table[name] = ShopData(typ, shop_id, keeper, custom, locked, new_items, sram_offset)
if multiworld.mode[player] == "inverted": if multiworld.worlds[player].options.mode == "inverted":
# make sure that blue potion is available in inverted, special case locked = None; lock when done. # make sure that blue potion is available in inverted, special case locked = None; lock when done.
player_shop_table["Dark Lake Hylia Shop"] = \ player_shop_table["Dark Lake Hylia Shop"] = \
player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None) player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None)
@@ -237,7 +237,7 @@ def create_shops(multiworld, player: int):
add_rule(loc, lambda state, spot=loc: shop_price_rules(state, player, spot)) add_rule(loc, lambda state, spot=loc: shop_price_rules(state, player, spot))
loc.shop = shop loc.shop = shop
loc.shop_slot = index loc.shop_slot = index
if ((not (multiworld.shuffle_capacity_upgrades[player] and type == ShopType.UpgradeShop)) if ((not (multiworld.worlds[player].options.shuffle_capacity_upgrades and type == ShopType.UpgradeShop))
and not single_purchase_slots.pop()): and not single_purchase_slots.pop()):
loc.shop_slot_disabled = True loc.shop_slot_disabled = True
loc.locked = True loc.locked = True
@@ -309,18 +309,18 @@ def set_up_shops(multiworld, player: int):
from .Options import small_key_shuffle from .Options import small_key_shuffle
# TODO: move hard+ mode changes for shields here, utilizing the new shops # TODO: move hard+ mode changes for shields here, utilizing the new shops
if multiworld.retro_bow[player]: if multiworld.worlds[player].options.retro_bow:
rss = multiworld.get_region('Red Shield Shop', player).shop rss = multiworld.get_region('Red Shield Shop', player).shop
replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50], replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50],
['Blue Shield', 50], ['Small Heart', ['Blue Shield', 50], ['Small Heart',
10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them. 10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them.
if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal: if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
replacement_items.append(['Small Key (Universal)', 100]) replacement_items.append(['Small Key (Universal)', 100])
replacement_item = multiworld.random.choice(replacement_items) replacement_item = multiworld.random.choice(replacement_items)
rss.add_inventory(2, 'Single Arrow', 80, 1, replacement_item[0], replacement_item[1]) rss.add_inventory(2, 'Single Arrow', 80, 1, replacement_item[0], replacement_item[1])
rss.locked = True rss.locked = True
if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal or multiworld.retro_bow[player]: if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal or multiworld.worlds[player].options.retro_bow:
for shop in multiworld.random.sample([s for s in multiworld.shops if for shop in multiworld.random.sample([s for s in multiworld.shops if
s.custom and not s.locked and s.type == ShopType.Shop s.custom and not s.locked and s.type == ShopType.Shop
and s.region.player == player], 5): and s.region.player == player], 5):
@@ -328,19 +328,19 @@ def set_up_shops(multiworld, player: int):
slots = [0, 1, 2] slots = [0, 1, 2]
multiworld.random.shuffle(slots) multiworld.random.shuffle(slots)
slots = iter(slots) slots = iter(slots)
if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal: if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
shop.add_inventory(next(slots), 'Small Key (Universal)', 100) shop.add_inventory(next(slots), 'Small Key (Universal)', 100)
if multiworld.retro_bow[player]: if multiworld.worlds[player].options.retro_bow:
shop.push_inventory(next(slots), 'Single Arrow', 80) shop.push_inventory(next(slots), 'Single Arrow', 80)
if multiworld.shuffle_capacity_upgrades[player]: if multiworld.worlds[player].options.shuffle_capacity_upgrades:
for shop in multiworld.shops: for shop in multiworld.shops:
if shop.type == ShopType.UpgradeShop and shop.region.player == player and \ if shop.type == ShopType.UpgradeShop and shop.region.player == player and \
shop.region.name == "Capacity Upgrade": shop.region.name == "Capacity Upgrade":
shop.clear_inventory() shop.clear_inventory()
if (multiworld.shuffle_shop_inventories[player] or multiworld.randomize_shop_prices[player] if (multiworld.worlds[player].options.shuffle_shop_inventories or multiworld.worlds[player].options.randomize_shop_prices
or multiworld.randomize_cost_types[player]): or multiworld.worlds[player].options.randomize_cost_types):
shops = [] shops = []
total_inventory = [] total_inventory = []
for shop in multiworld.shops: for shop in multiworld.shops:
@@ -352,7 +352,7 @@ def set_up_shops(multiworld, player: int):
for item in total_inventory: for item in total_inventory:
item["price_type"], item["price"] = get_price(multiworld, item, player) item["price_type"], item["price"] = get_price(multiworld, item, player)
if multiworld.shuffle_shop_inventories[player]: if multiworld.worlds[player].options.shuffle_shop_inventories:
multiworld.random.shuffle(total_inventory) multiworld.random.shuffle(total_inventory)
i = 0 i = 0
@@ -434,39 +434,39 @@ def get_price(multiworld, item, player: int, price_type=None):
price_types = [price_type] price_types = [price_type]
else: else:
price_types = [ShopPriceType.Rupees] # included as a chance to not change price price_types = [ShopPriceType.Rupees] # included as a chance to not change price
if multiworld.randomize_cost_types[player]: if multiworld.worlds[player].options.randomize_cost_types:
price_types += [ price_types += [
ShopPriceType.Hearts, ShopPriceType.Hearts,
ShopPriceType.Bombs, ShopPriceType.Bombs,
ShopPriceType.Magic, ShopPriceType.Magic,
] ]
if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal: if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
if item and item["item"] == "Small Key (Universal)": if item and item["item"] == "Small Key (Universal)":
price_types = [ShopPriceType.Rupees, ShopPriceType.Magic] # no logical requirements for repeatable keys price_types = [ShopPriceType.Rupees, ShopPriceType.Magic] # no logical requirements for repeatable keys
else: else:
price_types.append(ShopPriceType.Keys) price_types.append(ShopPriceType.Keys)
if multiworld.retro_bow[player]: if multiworld.worlds[player].options.retro_bow:
if item and item["item"] == "Single Arrow": if item and item["item"] == "Single Arrow":
price_types = [ShopPriceType.Rupees, ShopPriceType.Magic] # no logical requirements for arrows price_types = [ShopPriceType.Rupees, ShopPriceType.Magic] # no logical requirements for arrows
else: else:
price_types.append(ShopPriceType.Arrows) price_types.append(ShopPriceType.Arrows)
diff = multiworld.item_pool[player].value diff = multiworld.worlds[player].options.item_pool.value
if item: if item:
# This is for a shop's regular inventory, the item is already determined, and we will decide the price here # This is for a shop's regular inventory, the item is already determined, and we will decide the price here
price = item["price"] price = item["price"]
if multiworld.randomize_shop_prices[player]: if multiworld.worlds[player].options.randomize_shop_prices:
adjust = 2 if price < 100 else 5 adjust = 2 if price < 100 else 5
price = int((price / adjust) * (0.5 + multiworld.per_slot_randoms[player].random() * 1.5)) * adjust price = int((price / adjust) * (0.5 + multiworld.worlds[player].random.random() * 1.5)) * adjust
multiworld.per_slot_randoms[player].shuffle(price_types) multiworld.worlds[player].random.shuffle(price_types)
for p_type in price_types: for p_type in price_types:
if any(x in item['item'] for x in price_blacklist[p_type]): if any(x in item['item'] for x in price_blacklist[p_type]):
continue continue
return p_type, price_chart[p_type](price, diff) return p_type, price_chart[p_type](price, diff)
else: else:
# This is an AP location and the price will be adjusted after an item is shuffled into it # This is an AP location and the price will be adjusted after an item is shuffled into it
p_type = multiworld.per_slot_randoms[player].choice(price_types) p_type = multiworld.worlds[player].random.choice(price_types)
return p_type, price_chart[p_type](min(int(multiworld.per_slot_randoms[player].randint(8, 56) return p_type, price_chart[p_type](min(int(multiworld.worlds[player].random.randint(8, 56)
* multiworld.shop_price_modifier[player] / 100) * 5, 9999), diff) * multiworld.worlds[player].options.shop_price_modifier / 100) * 5, 9999), diff)
def shop_price_rules(state: CollectionState, player: int, location: ALttPLocation): def shop_price_rules(state: CollectionState, player: int, location: ALttPLocation):

View File

@@ -6,7 +6,7 @@ def is_not_bunny(state: CollectionState, region: LTTPRegion, player: int) -> boo
if state.has('Moon Pearl', player): if state.has('Moon Pearl', player):
return True return True
return region.is_light_world if state.multiworld.mode[player] != 'inverted' else region.is_dark_world return region.is_light_world if state.multiworld.worlds[player].options.mode != 'inverted' else region.is_dark_world
def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bool: def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bool:
@@ -24,7 +24,7 @@ def can_buy(state: CollectionState, item: str, player: int) -> bool:
def can_shoot_arrows(state: CollectionState, player: int, count: int = 0) -> bool: def can_shoot_arrows(state: CollectionState, player: int, count: int = 0) -> bool:
if state.multiworld.retro_bow[player]: if state.multiworld.worlds[player].options.retro_bow:
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)) and can_buy(state, 'Single Arrow', player)
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_hold_arrows(state, player, count) return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_hold_arrows(state, player, count)
@@ -74,9 +74,9 @@ def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16,
elif state.has('Magic Upgrade (1/2)', player): elif state.has('Magic Upgrade (1/2)', player):
basemagic = 16 basemagic = 16
if can_buy_unlimited(state, 'Green Potion', player) or can_buy_unlimited(state, 'Blue Potion', player): 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: if state.multiworld.worlds[player].options.item_functionality == 'hard' and not fullrefill:
basemagic = basemagic + int(basemagic * 0.5 * bottle_count(state, player)) basemagic = basemagic + int(basemagic * 0.5 * bottle_count(state, player))
elif state.multiworld.item_functionality[player] == 'expert' and not fullrefill: elif state.multiworld.worlds[player].options.item_functionality == 'expert' and not fullrefill:
basemagic = basemagic + int(basemagic * 0.25 * bottle_count(state, player)) basemagic = basemagic + int(basemagic * 0.25 * bottle_count(state, player))
else: else:
basemagic = basemagic + basemagic * bottle_count(state, player) basemagic = basemagic + basemagic * bottle_count(state, player)
@@ -99,12 +99,12 @@ def can_hold_arrows(state: CollectionState, player: int, quantity: int):
def can_use_bombs(state: CollectionState, player: int, quantity: int = 1) -> bool: def can_use_bombs(state: CollectionState, player: int, quantity: int = 1) -> bool:
bombs = 0 if state.multiworld.bombless_start[player] else 10 bombs = 0 if state.multiworld.worlds[player].options.bombless_start else 10
bombs += ((state.count("Bomb Upgrade (+5)", player) * 5) + (state.count("Bomb Upgrade (+10)", player) * 10) bombs += ((state.count("Bomb Upgrade (+5)", player) * 5) + (state.count("Bomb Upgrade (+10)", player) * 10)
+ (state.count("Bomb Upgrade (50)", player) * 50)) + (state.count("Bomb Upgrade (50)", player) * 50))
# Bomb Upgrade (+5) beyond the 6th gives +10 # Bomb Upgrade (+5) beyond the 6th gives +10
bombs += max(0, ((state.count("Bomb Upgrade (+5)", player) - 6) * 10)) bombs += max(0, ((state.count("Bomb Upgrade (+5)", player) - 6) * 10))
if (not state.multiworld.shuffle_capacity_upgrades[player]) and state.has("Capacity Upgrade Shop", player): if (not state.multiworld.worlds[player].options.shuffle_capacity_upgrades) and state.has("Capacity Upgrade Shop", player):
bombs += 40 bombs += 40
return bombs >= min(quantity, 50) return bombs >= min(quantity, 50)
@@ -120,7 +120,7 @@ def can_activate_crystal_switch(state: CollectionState, player: int) -> bool:
def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) -> bool: def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) -> bool:
if state.multiworld.enemy_shuffle[player]: if state.multiworld.worlds[player].options.enemy_shuffle:
# I don't fully understand Enemizer's logic for placing enemies in spots where they need to be killable, if any. # I don't fully understand Enemizer's logic for placing enemies in spots where they need to be killable, if any.
# Just go with maximal requirements for now. # Just go with maximal requirements for now.
return (has_melee_weapon(state, player) return (has_melee_weapon(state, player)
@@ -135,7 +135,7 @@ def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5)
or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player))) or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player)))
or can_shoot_arrows(state, player) or can_shoot_arrows(state, player)
or state.has('Fire Rod', player) or state.has('Fire Rod', player)
or (state.multiworld.enemy_health[player] in ("easy", "default") or (state.multiworld.worlds[player].options.enemy_health in ("easy", "default")
and can_use_bombs(state, player, enemies * 4))) and can_use_bombs(state, player, enemies * 4)))
@@ -152,7 +152,7 @@ def can_get_good_bee(state: CollectionState, player: int) -> bool:
def can_retrieve_tablet(state: CollectionState, player: int) -> bool: def can_retrieve_tablet(state: CollectionState, player: int) -> bool:
return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or
(state.multiworld.swordless[player] and (state.multiworld.worlds[player].options.swordless and
state.has("Hammer", player))) state.has("Hammer", player)))
@@ -179,7 +179,7 @@ def has_fire_source(state: CollectionState, player: int) -> bool:
def can_melt_things(state: CollectionState, player: int) -> bool: def can_melt_things(state: CollectionState, player: int) -> bool:
return state.has('Fire Rod', player) or \ return state.has('Fire Rod', player) or \
(state.has('Bombos', player) and (state.has('Bombos', player) and
(state.multiworld.swordless[player] or (state.multiworld.worlds[player].options.swordless or
has_sword(state, player))) has_sword(state, player)))
@@ -192,19 +192,19 @@ def has_turtle_rock_medallion(state: CollectionState, player: int) -> bool:
def can_boots_clip_lw(state: CollectionState, player: int) -> bool: def can_boots_clip_lw(state: CollectionState, player: int) -> bool:
if state.multiworld.mode[player] == 'inverted': if state.multiworld.worlds[player].options.mode == 'inverted':
return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)
return state.has('Pegasus Boots', player) return state.has('Pegasus Boots', player)
def can_boots_clip_dw(state: CollectionState, player: int) -> bool: def can_boots_clip_dw(state: CollectionState, player: int) -> bool:
if state.multiworld.mode[player] != 'inverted': if state.multiworld.worlds[player].options.mode != 'inverted':
return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)
return state.has('Pegasus Boots', player) return state.has('Pegasus Boots', player)
def can_get_glitched_speed_dw(state: CollectionState, player: int) -> bool: 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)])] rules = [state.has('Pegasus Boots', player), any([state.has('Hookshot', player), has_sword(state, player)])]
if state.multiworld.mode[player] != 'inverted': if state.multiworld.worlds[player].options.mode != 'inverted':
rules.append(state.has('Moon Pearl', player)) rules.append(state.has('Moon Pearl', player))
return all(rules) return all(rules)

View File

@@ -2,11 +2,10 @@
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from enum import IntEnum from enum import IntEnum
from BaseClasses import Location, Item, ItemClassification, Region, MultiWorld from BaseClasses import Entrance, Location, Item, ItemClassification, Region, MultiWorld
if TYPE_CHECKING: if TYPE_CHECKING:
from .Dungeons import Dungeon from .Dungeons import Dungeon
from .Regions import LTTPRegion
class ALttPLocation(Location): class ALttPLocation(Location):
@@ -77,6 +76,19 @@ class ALttPItem(Item):
return self.type return self.type
Addresses = int | list[int] | tuple[int, int, int, int, int, int, int, int, int, int, int, int, int]
class LTTPEntrance(Entrance):
addresses: Addresses | None = None
target: int | None = None
def connect(self, region: Region, addresses: Addresses | None = None, target: int | None = None) -> None:
super().connect(region)
self.addresses = addresses
self.target = target
class LTTPRegionType(IntEnum): class LTTPRegionType(IntEnum):
LightWorld = 1 LightWorld = 1
DarkWorld = 2 DarkWorld = 2
@@ -90,6 +102,7 @@ class LTTPRegionType(IntEnum):
class LTTPRegion(Region): class LTTPRegion(Region):
entrance_type = LTTPEntrance
type: LTTPRegionType type: LTTPRegionType
# will be set after making connections. # will be set after making connections.

View File

@@ -1,6 +1,6 @@
from BaseClasses import Entrance
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 from .StateHelpers import can_bomb_clip, has_sword, has_beam_sword, has_fire_source, can_melt_things, has_misery_mire_medallion
from .SubClasses import LTTPEntrance
# 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.
@@ -9,17 +9,15 @@ def underworld_glitch_connections(world, player):
specrock = world.get_region('Spectacle Rock Cave (Bottom)', player) specrock = world.get_region('Spectacle Rock Cave (Bottom)', player)
mire = world.get_region('Misery Mire (West)', player) mire = world.get_region('Misery Mire (West)', player)
kikiskip = Entrance(player, 'Kiki Skip', specrock) kikiskip = specrock.create_exit('Kiki Skip')
mire_to_hera = Entrance(player, 'Mire to Hera Clip', mire) mire_to_hera = mire.create_exit('Mire to Hera Clip')
mire_to_swamp = Entrance(player, 'Hera to Swamp Clip', mire) mire_to_swamp = mire.create_exit('Hera to Swamp Clip')
specrock.exits.append(kikiskip)
mire.exits.extend([mire_to_hera, mire_to_swamp])
if world.worlds[player].fix_fake_world: if world.worlds[player].fix_fake_world:
kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region) kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region)
mire_to_hera.connect(world.get_entrance('Tower of Hera Exit', player).connected_region) mire_to_hera.connect(world.get_entrance('Tower of Hera Exit', player).connected_region)
mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit', player).connected_region) mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit', player).connected_region)
else: else:
kikiskip.connect(world.get_region('Palace of Darkness (Entrance)', player)) kikiskip.connect(world.get_region('Palace of Darkness (Entrance)', player))
mire_to_hera.connect(world.get_region('Tower of Hera (Bottom)', player)) mire_to_hera.connect(world.get_region('Tower of Hera (Bottom)', player))
mire_to_swamp.connect(world.get_region('Swamp Palace (Entrance)', player)) mire_to_swamp.connect(world.get_region('Swamp Palace (Entrance)', player))
@@ -37,7 +35,7 @@ def fake_pearl_state(state, player):
# Sets the rules on where we can actually go using this clip. # Sets the rules on where we can actually go using this clip.
# Behavior differs based on what type of ER shuffle we're playing. # Behavior differs based on what type of ER shuffle we're playing.
def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, dungeon_exit: str): def dungeon_reentry_rules(world, player, clip: LTTPEntrance, dungeon_region: str, dungeon_exit: str):
fix_dungeon_exits = world.worlds[player].fix_palaceofdarkness_exit fix_dungeon_exits = world.worlds[player].fix_palaceofdarkness_exit
fix_fake_worlds = world.worlds[player].fix_fake_world fix_fake_worlds = world.worlds[player].fix_fake_world
@@ -61,7 +59,7 @@ def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, du
# since the clip links directly to the exterior region. # since the clip links directly to the exterior region.
def underworld_glitches_rules(world, player): 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. # This is the easiest one since it's a simple internal clip.
# Need to also add melting to freezor chest since it's otherwise assumed. # Need to also add melting to freezor chest since it's otherwise assumed.
@@ -90,12 +88,12 @@ def underworld_glitches_rules(world, player):
# We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT. # We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT.
# First we require a certain type of entrance shuffle, then build the rule from its pieces. # First we require a certain type of entrance shuffle, then build the rule from its pieces.
if not world.worlds[player].swamp_patch_required: if not world.worlds[player].swamp_patch_required:
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rule_map = { rule_map = {
'Misery Mire (Entrance)': (lambda state: True), 'Misery Mire (Entrance)': (lambda state: True),
'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player)) 'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player))
} }
inverted = world.mode[player] == 'inverted' inverted = world.worlds[player].options.mode == 'inverted'
hera_rule = lambda state: (state.has('Moon Pearl', player) or not inverted) and \ hera_rule = lambda state: (state.has('Moon Pearl', player) or not inverted) and \
rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state) rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state)
gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \ gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \

View File

@@ -121,6 +121,7 @@ class ALTTPWeb(WebWorld):
) )
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound] tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
game_info_languages = ["en", "fr"]
class ALTTPWorld(World): class ALTTPWorld(World):
@@ -140,7 +141,7 @@ class ALTTPWorld(World):
item_name_groups = item_name_groups item_name_groups = item_name_groups
location_name_groups = { location_name_groups = {
"Blind's Hideout": {"Blind's Hideout - Top", "Blind's Hideout - Left", "Blind's Hideout - Right", "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"}, "Blind's Hideout - Far Left", "Blind's Hideout - Far Right"},
"Kakariko Well": {"Kakariko Well - Top", "Kakariko Well - Left", "Kakariko Well - Middle", "Kakariko Well": {"Kakariko Well - Top", "Kakariko Well - Left", "Kakariko Well - Middle",
"Kakariko Well - Right", "Kakariko Well - Bottom"}, "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": {"Mini Moldorm Cave - Far Left", "Mini Moldorm Cave - Left", "Mini Moldorm Cave - Right",
@@ -153,15 +154,23 @@ class ALTTPWorld(World):
"Hookshot Cave": {"Hookshot Cave - Top Right", "Hookshot Cave - Top Left", "Hookshot Cave - Bottom Right", "Hookshot Cave": {"Hookshot Cave - Top Right", "Hookshot Cave - Top Left", "Hookshot Cave - Bottom Right",
"Hookshot Cave - Bottom Left"}, "Hookshot Cave - Bottom Left"},
"Hyrule Castle": {"Hyrule Castle - Boomerang Chest", "Hyrule Castle - Map Chest", "Hyrule Castle": {"Hyrule Castle - Boomerang Chest", "Hyrule Castle - Map Chest",
"Hyrule Castle - Zelda's Chest", "Sewers - Dark Cross", "Sewers - Secret Room - Left", "Hyrule Castle - Zelda's Chest", "Hyrule Castle - Big Key Drop",
"Sewers - Secret Room - Middle", "Sewers - Secret Room - Right"}, "Hyrule Castle - Boomerang Guard Key Drop", "Hyrule Castle - Map Guard Key Drop",
"Sewers - Dark Cross", "Sewers - Secret Room - Left",
"Sewers - Secret Room - Middle", "Sewers - Secret Room - Right",
"Sewers - Key Rat Key Drop"},
"Eastern Palace": {"Eastern Palace - Compass Chest", "Eastern Palace - Big Chest", "Eastern Palace": {"Eastern Palace - Compass Chest", "Eastern Palace - Big Chest",
"Eastern Palace - Cannonball Chest", "Eastern Palace - Big Key Chest", "Eastern Palace - Cannonball Chest", "Eastern Palace - Big Key Chest",
"Eastern Palace - Dark Eyegore Key Drop", "Eastern Palace - Dark Square Pot Key",
"Eastern Palace - Map Chest", "Eastern Palace - Boss"}, "Eastern Palace - Map Chest", "Eastern Palace - Boss"},
"Desert Palace": {"Desert Palace - Big Chest", "Desert Palace - Torch", "Desert Palace - Map Chest", "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"}, "Desert Palace - Beamos Hall Pot Key", "Desert Palace - Desert Tiles 1 Pot Key",
"Desert Palace - Desert Tiles 2 Pot Key", "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": {"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"}, "Tower of Hera - Compass Chest", "Tower of Hera - Big Chest", "Tower of Hera - Boss"},
"Castle Tower": {"Castle Tower - Room 03", "Castle Tower - Dark Maze",
"Castle Tower - Dark Archer Key Drop", "Castle Tower - Circle of Pots Key Drop"},
"Palace of Darkness": {"Palace of Darkness - Shooter Room", "Palace of Darkness - The Arena - Bridge", "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 - Stalfos Basement", "Palace of Darkness - Big Key Chest",
"Palace of Darkness - The Arena - Ledge", "Palace of Darkness - Map Chest", "Palace of Darkness - The Arena - Ledge", "Palace of Darkness - Map Chest",
@@ -172,25 +181,33 @@ class ALTTPWorld(World):
"Swamp Palace": {"Swamp Palace - Entrance", "Swamp Palace - Map Chest", "Swamp Palace - Big Chest", "Swamp Palace": {"Swamp Palace - Entrance", "Swamp Palace - Map Chest", "Swamp Palace - Big Chest",
"Swamp Palace - Compass Chest", "Swamp Palace - Big Key Chest", "Swamp Palace - West 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 - Flooded Room - Left", "Swamp Palace - Flooded Room - Right",
"Swamp Palace - Waterfall Room", "Swamp Palace - Boss"}, "Swamp Palace - Hookshot Pot Key", "Swamp Palace - Pot Row Pot Key",
"Swamp Palace - Trench 1 Pot Key", "Swamp Palace - Trench 2 Pot Key",
"Swamp Palace - Waterway Pot Key", "Swamp Palace - Waterfall Room", "Swamp Palace - Boss"},
"Thieves' Town": {"Thieves' Town - Big Key Chest", "Thieves' Town - Map Chest", "Thieves' Town - Compass Chest", "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 - Ambush Chest", "Thieves' Town - Attic", "Thieves' Town - Big Chest",
"Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key",
"Thieves' Town - Blind's Cell", "Thieves' Town - Boss"}, "Thieves' Town - Blind's Cell", "Thieves' Town - Boss"},
"Skull Woods": {"Skull Woods - Map Chest", "Skull Woods - Pinball Room", "Skull Woods - Compass Chest", "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 - Pot Prison", "Skull Woods - Big Chest", "Skull Woods - Big Key Chest",
"Skull Woods - Spike Corner Key Drop", "Skull Woods - West Lobby Pot Key",
"Skull Woods - Bridge Room", "Skull Woods - Boss"}, "Skull Woods - Bridge Room", "Skull Woods - Boss"},
"Ice Palace": {"Ice Palace - Compass Chest", "Ice Palace - Freezor Chest", "Ice Palace - Big Chest", "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 - 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 - Spike Room", "Ice Palace - Big Key Chest", "Ice Palace - Map Chest",
"Ice Palace - Conveyor Key Drop", "Ice Palace - Hammer Block Key Drop",
"Ice Palace - Jelly Key Drop", "Ice Palace - Many Pots Pot Key",
"Ice Palace - Boss"}, "Ice Palace - Boss"},
"Misery Mire": {"Misery Mire - Big Chest", "Misery Mire - Map Chest", "Misery Mire - Main Lobby", "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 - Bridge Chest", "Misery Mire - Spike Chest", "Misery Mire - Compass Chest",
"Misery Mire - Big Key Chest", "Misery Mire - Boss"}, "Misery Mire - Conveyor Crystal Key Drop", "Misery Mire - Fishbone Pot Key",
"Misery Mire - Spikes Pot Key", "Misery Mire - Big Key Chest", "Misery Mire - Boss"},
"Turtle Rock": {"Turtle Rock - Compass Chest", "Turtle Rock - Roller Room - Left", "Turtle Rock": {"Turtle Rock - Compass Chest", "Turtle Rock - Roller Room - Left",
"Turtle Rock - Roller Room - Right", "Turtle Rock - Chain Chomps", "Turtle Rock - Big Key Chest", "Turtle Rock - Roller Room - Right", "Turtle Rock - Chain Chomps", "Turtle Rock - Big Key Chest",
"Turtle Rock - Big Chest", "Turtle Rock - Crystaroller Room", "Turtle Rock - Big Chest", "Turtle Rock - Crystaroller Room",
"Turtle Rock - Eye Bridge - Bottom Left", "Turtle Rock - Eye Bridge - Bottom Right", "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 - Eye Bridge - Top Left", "Turtle Rock - Eye Bridge - Top Right",
"Turtle Rock - Pokey 1 Key Drop", "Turtle Rock - Pokey 2 Key Drop",
"Turtle Rock - Boss"}, "Turtle Rock - Boss"},
"Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganons Tower - Hope Room - Left", "Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganons Tower - Hope Room - Left",
"Ganons Tower - Hope Room - Right", "Ganons Tower - Tile Room", "Ganons Tower - Hope Room - Right", "Ganons Tower - Tile Room",
@@ -203,10 +220,13 @@ class ALTTPWorld(World):
"Ganons Tower - Randomizer Room - Bottom Left", "Ganons Tower - Randomizer Room - Bottom 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 - 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 - Big Key Room - Right", "Ganons Tower - Big Key Chest",
"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right", "Ganons Tower - Conveyor Cross Pot Key", "Ganons Tower - Conveyor Star Pits Pot Key",
"Ganons Tower - Pre-Moldorm Chest", "Ganons Tower - Validation Chest"}, "Ganons Tower - Double Switch Pot Key", "Ganons Tower - Mini Helmasaur Room - Left",
"Ganons Tower - Mini Helmasaur Room - Right", "Ganons Tower - Pre-Moldorm Chest",
"Ganons Tower - Mini Helmasaur Key Drop", "Ganons Tower - Validation Chest"},
"Ganons Tower Climb": {"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right", "Ganons Tower Climb": {"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right",
"Ganons Tower - Pre-Moldorm Chest", "Ganons Tower - Validation Chest"}, "Ganons Tower - Mini Helmasaur Key Drop", "Ganons Tower - Pre-Moldorm Chest",
"Ganons Tower - Validation Chest"},
} }
hint_blacklist = {"Triforce"} hint_blacklist = {"Triforce"}
@@ -293,74 +313,62 @@ class ALTTPWorld(World):
break break
def generate_early(self): def generate_early(self):
# write old options
import dataclasses
is_first = self.player == min(self.multiworld.get_game_players(self.game))
for field in dataclasses.fields(self.options_dataclass):
if is_first:
setattr(self.multiworld, field.name, {})
getattr(self.multiworld, field.name)[self.player] = getattr(self.options, field.name)
# end of old options re-establisher
player = self.player
multiworld = self.multiworld multiworld = self.multiworld
self.fix_trock_doors = (multiworld.entrance_shuffle[player] != 'vanilla' self.fix_trock_doors = (self.options.entrance_shuffle != 'vanilla' or self.options.mode == 'inverted')
or multiworld.mode[player] == 'inverted') self.fix_skullwoods_exit = self.options.entrance_shuffle not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']
self.fix_skullwoods_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted', self.fix_palaceofdarkness_exit = self.options.entrance_shuffle not in ['dungeons_simple', 'vanilla', 'simple', 'restricted']
'dungeons_simple'] self.fix_trock_exit = self.options.entrance_shuffle not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']
self.fix_palaceofdarkness_exit = multiworld.entrance_shuffle[player] not in ['dungeons_simple', 'vanilla',
'simple', 'restricted']
self.fix_trock_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted',
'dungeons_simple']
# fairy bottle fills # fairy bottle fills
bottle_options = [ bottle_options = [
"Bottle (Red Potion)", "Bottle (Green Potion)", "Bottle (Blue Potion)", "Bottle (Red Potion)", "Bottle (Green Potion)", "Bottle (Blue Potion)",
"Bottle (Bee)", "Bottle (Good Bee)" "Bottle (Bee)", "Bottle (Good Bee)"
] ]
if multiworld.item_pool[player] not in ["hard", "expert"]: if self.options.item_pool not in ["hard", "expert"]:
bottle_options.append("Bottle (Fairy)") bottle_options.append("Bottle (Fairy)")
self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options) self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options) self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
if multiworld.mode[player] == 'standard': if self.options.mode == 'standard':
if multiworld.small_key_shuffle[player]: if self.options.small_key_shuffle:
if (multiworld.small_key_shuffle[player] not in if (self.options.small_key_shuffle not in
(small_key_shuffle.option_universal, small_key_shuffle.option_own_dungeons, (small_key_shuffle.option_universal, small_key_shuffle.option_own_dungeons,
small_key_shuffle.option_start_with)): small_key_shuffle.option_start_with)):
self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1 self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
self.multiworld.local_items[self.player].value.add("Small Key (Hyrule Castle)") self.options.local_items.value.add("Small Key (Hyrule Castle)")
self.multiworld.non_local_items[self.player].value.discard("Small Key (Hyrule Castle)") self.options.non_local_items.value.discard("Small Key (Hyrule Castle)")
if multiworld.big_key_shuffle[player]: if self.options.big_key_shuffle:
self.multiworld.local_items[self.player].value.add("Big Key (Hyrule Castle)") self.options.local_items.value.add("Big Key (Hyrule Castle)")
self.multiworld.non_local_items[self.player].value.discard("Big Key (Hyrule Castle)") self.options.non_local_items.value.discard("Big Key (Hyrule Castle)")
# system for sharing ER layouts # system for sharing ER layouts
self.er_seed = str(multiworld.random.randint(0, 2 ** 64)) self.er_seed = str(multiworld.random.randint(0, 2 ** 64))
if multiworld.entrance_shuffle[player] != "vanilla" and multiworld.entrance_shuffle_seed[player] != "random": if self.options.entrance_shuffle != "vanilla" and self.options.entrance_shuffle_seed != "random":
shuffle = multiworld.entrance_shuffle[player].current_key shuffle = self.options.entrance_shuffle.current_key
if shuffle == "vanilla": if shuffle == "vanilla":
self.er_seed = "vanilla" self.er_seed = "vanilla"
elif (not multiworld.entrance_shuffle_seed[player].value.isdigit()) or multiworld.is_race: elif (not self.options.entrance_shuffle_seed.value.isdigit()) or multiworld.is_race:
self.er_seed = get_same_seed(multiworld, ( self.er_seed = get_same_seed(multiworld, (
shuffle, multiworld.entrance_shuffle_seed[player].value, multiworld.retro_caves[player], multiworld.mode[player], shuffle, self.options.entrance_shuffle_seed.value,
multiworld.glitches_required[player])) self.options.retro_caves,
self.options.mode,
self.options.glitches_required
))
else: # not a race or group seed, use set seed as is. else: # not a race or group seed, use set seed as is.
self.er_seed = int(multiworld.entrance_shuffle_seed[player].value) self.er_seed = int(self.options.entrance_shuffle_seed.value)
elif multiworld.entrance_shuffle[player] == "vanilla": elif self.options.entrance_shuffle == "vanilla":
self.er_seed = "vanilla" self.er_seed = "vanilla"
for dungeon_item in ["small_key_shuffle", "big_key_shuffle", "compass_shuffle", "map_shuffle"]: for dungeon_item in ["small_key_shuffle", "big_key_shuffle", "compass_shuffle", "map_shuffle"]:
option = getattr(multiworld, dungeon_item)[player] option = getattr(self.options, dungeon_item)
if option == "own_world": if option == "own_world":
multiworld.local_items[player].value |= self.item_name_groups[option.item_name_group] self.options.local_items.value |= self.item_name_groups[option.item_name_group]
elif option == "different_world": elif option == "different_world":
multiworld.non_local_items[player].value |= self.item_name_groups[option.item_name_group] self.options.non_local_items.value |= self.item_name_groups[option.item_name_group]
if multiworld.mode[player] == "standard": if self.options.mode == "standard":
multiworld.non_local_items[player].value -= {"Small Key (Hyrule Castle)"} self.options.non_local_items.value -= {"Small Key (Hyrule Castle)"}
elif option.in_dungeon: elif option.in_dungeon:
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group] self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
if option == "original_dungeon": if option == "original_dungeon":
@@ -368,15 +376,15 @@ class ALTTPWorld(World):
else: else:
self.options.local_items.value |= self.dungeon_local_item_names self.options.local_items.value |= self.dungeon_local_item_names
self.difficulty_requirements = difficulties[multiworld.item_pool[player].current_key] self.difficulty_requirements = difficulties[self.options.item_pool.current_key]
# enforce pre-defined local items. # enforce pre-defined local items.
if multiworld.goal[player] in ["local_triforce_hunt", "local_ganon_triforce_hunt"]: if self.options.goal in ["local_triforce_hunt", "local_ganon_triforce_hunt"]:
multiworld.local_items[player].value.add('Triforce Piece') self.options.local_items.value.add('Triforce Piece')
# Not possible to place crystals outside boss prizes yet (might as well make it consistent with pendants too). # Not possible to place crystals outside boss prizes yet (might as well make it consistent with pendants too).
multiworld.non_local_items[player].value -= item_name_groups['Pendants'] self.options.non_local_items.value -= item_name_groups['Pendants']
multiworld.non_local_items[player].value -= item_name_groups['Crystals'] self.options.non_local_items.value -= item_name_groups['Crystals']
create_dungeons = create_dungeons create_dungeons = create_dungeons
@@ -384,15 +392,15 @@ class ALTTPWorld(World):
player = self.player player = self.player
multiworld = self.multiworld multiworld = self.multiworld
if multiworld.mode[player] != 'inverted': if self.options.mode != 'inverted':
create_regions(multiworld, player) create_regions(multiworld, player)
else: else:
create_inverted_regions(multiworld, player) create_inverted_regions(multiworld, player)
create_shops(multiworld, player) create_shops(multiworld, player)
self.create_dungeons() self.create_dungeons()
if (multiworld.glitches_required[player] not in ["no_glitches", "minor_glitches"] and if (self.options.glitches_required not in ["no_glitches", "minor_glitches"] and
multiworld.entrance_shuffle[player] in [ self.options.entrance_shuffle in [
"vanilla", "dungeons_simple", "dungeons_full", "simple", "restricted", "full"]): "vanilla", "dungeons_simple", "dungeons_full", "simple", "restricted", "full"]):
self.fix_fake_world = False self.fix_fake_world = False
@@ -400,7 +408,7 @@ class ALTTPWorld(World):
old_random = multiworld.random old_random = multiworld.random
multiworld.random = random.Random(self.er_seed) multiworld.random = random.Random(self.er_seed)
if multiworld.mode[player] != 'inverted': if self.options.mode != 'inverted':
link_entrances(multiworld, player) link_entrances(multiworld, player)
mark_light_world_regions(multiworld, player) mark_light_world_regions(multiworld, player)
else: else:
@@ -485,8 +493,9 @@ class ALTTPWorld(World):
if state.has('Silver Bow', item.player): if state.has('Silver Bow', item.player):
return return
elif state.has('Bow', item.player) and (self.difficulty_requirements.progressive_bow_limit >= 2 elif state.has('Bow', item.player) and (self.difficulty_requirements.progressive_bow_limit >= 2
or self.multiworld.glitches_required[self.player] == 'no_glitches' or self.options.glitches_required == 'no_glitches'
or self.multiworld.swordless[self.player]): # modes where silver bow is always required for ganon or self.options.swordless):
# modes where silver bow is always required for ganon
return 'Silver Bow' return 'Silver Bow'
elif self.difficulty_requirements.progressive_bow_limit >= 1: elif self.difficulty_requirements.progressive_bow_limit >= 1:
return 'Bow' return 'Bow'
@@ -529,9 +538,9 @@ class ALTTPWorld(World):
break break
else: else:
raise FillError('Unable to place dungeon prizes') raise FillError('Unable to place dungeon prizes')
if world.mode[player] == 'standard' and world.small_key_shuffle[player] \ if self.options.mode == 'standard' and self.options.small_key_shuffle \
and world.small_key_shuffle[player] != small_key_shuffle.option_universal and \ and self.options.small_key_shuffle != small_key_shuffle.option_universal and \
world.small_key_shuffle[player] != small_key_shuffle.option_own_dungeons: self.options.small_key_shuffle != small_key_shuffle.option_own_dungeons:
world.local_early_items[player]["Small Key (Hyrule Castle)"] = 1 world.local_early_items[player]["Small Key (Hyrule Castle)"] = 1
@classmethod @classmethod
@@ -572,27 +581,27 @@ class ALTTPWorld(World):
multiworld.spoiler.hashes[player] = get_hash_string(rom.hash) multiworld.spoiler.hashes[player] = get_hash_string(rom.hash)
palettes_options = { palettes_options = {
'dungeon': multiworld.uw_palettes[player], 'dungeon': self.options.uw_palettes,
'overworld': multiworld.ow_palettes[player], 'overworld': self.options.ow_palettes,
'hud': multiworld.hud_palettes[player], 'hud': self.options.hud_palettes,
'sword': multiworld.sword_palettes[player], 'sword': self.options.sword_palettes,
'shield': multiworld.shield_palettes[player], 'shield': self.options.shield_palettes,
# 'link': world.link_palettes[player] # 'link': world.link_palettes[player]
} }
palettes_options = {key: option.current_key for key, option in palettes_options.items()} palettes_options = {key: option.current_key for key, option in palettes_options.items()}
apply_rom_settings(rom, multiworld.heartbeep[player].current_key, apply_rom_settings(rom, self.options.heartbeep.current_key,
multiworld.heartcolor[player].current_key, self.options.heartcolor.current_key,
multiworld.quickswap[player], self.options.quickswap,
multiworld.menuspeed[player].current_key, self.options.menuspeed.current_key,
multiworld.music[player], self.options.music,
multiworld.sprite[player], multiworld.sprite[player],
None, None,
palettes_options, multiworld, player, True, palettes_options, multiworld, player, True,
reduceflashing=multiworld.reduceflashing[player] or multiworld.is_race, reduceflashing=self.options.reduceflashing or multiworld.is_race,
triforcehud=multiworld.triforcehud[player].current_key, triforcehud=self.options.triforcehud.current_key,
deathlink=multiworld.death_link[player], deathlink=self.options.death_link,
allowcollect=multiworld.allow_collect[player]) allowcollect=self.options.allow_collect)
rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc") rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")
rom.write_to_file(rompath) rom.write_to_file(rompath)
@@ -609,7 +618,7 @@ class ALTTPWorld(World):
@classmethod @classmethod
def stage_extend_hint_information(cls, world, hint_data: typing.Dict[int, typing.Dict[int, str]]): def stage_extend_hint_information(cls, world, hint_data: typing.Dict[int, typing.Dict[int, str]]):
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
world.entrance_shuffle[player] != "vanilla" or world.retro_caves[player]} world.worlds[player].options.entrance_shuffle != "vanilla" or world.worlds[player].options.retro_caves}
for region in world.regions: for region in world.regions:
if region.player in er_hint_data and region.locations: if region.player in er_hint_data and region.locations:
@@ -725,7 +734,7 @@ class ALTTPWorld(World):
f" {self.pyramid_fairy_bottle_fill}") f" {self.pyramid_fairy_bottle_fill}")
spoiler_handle.write(f"\nWaterfall Fairy ({player_name}):" spoiler_handle.write(f"\nWaterfall Fairy ({player_name}):"
f" {self.waterfall_fairy_bottle_fill}") f" {self.waterfall_fairy_bottle_fill}")
if self.multiworld.boss_shuffle[self.player] != "none": if self.options.boss_shuffle != "none":
def create_boss_map() -> typing.Dict: def create_boss_map() -> typing.Dict:
boss_map = { boss_map = {
"Eastern Palace": self.dungeons["Eastern Palace"].boss.name, "Eastern Palace": self.dungeons["Eastern Palace"].boss.name,
@@ -742,7 +751,7 @@ class ALTTPWorld(World):
"Ganons Tower": "Agahnim 2", "Ganons Tower": "Agahnim 2",
"Ganon": "Ganon" "Ganon": "Ganon"
} }
if self.multiworld.mode[self.player] != 'inverted': if self.options.mode != 'inverted':
boss_map.update({ boss_map.update({
"Ganons Tower Basement": "Ganons Tower Basement":
self.dungeons["Ganons Tower"].bosses["bottom"].name, self.dungeons["Ganons Tower"].bosses["bottom"].name,
@@ -827,7 +836,7 @@ class ALTTPWorld(World):
"triforce_pieces_available", "triforce_pieces_extra", "triforce_pieces_available", "triforce_pieces_extra",
] ]
slot_data = {option_name: getattr(self.multiworld, option_name)[self.player].value for option_name in slot_options} slot_data = {option_name: getattr(self.options, option_name).value for option_name in slot_options}
slot_data.update({ slot_data.update({
'mm_medalion': self.required_medallions[0], 'mm_medalion': self.required_medallions[0],
@@ -848,8 +857,8 @@ def get_same_seed(world, seed_def: tuple) -> str:
class ALttPLogic(LogicMixin): class ALttPLogic(LogicMixin):
def _lttp_has_key(self, item, player, count: int = 1): def _lttp_has_key(self, item, player, count: int = 1):
if self.multiworld.glitches_required[player] == 'no_logic': if self.multiworld.worlds[player].options.glitches_required == 'no_logic':
return True return True
if self.multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal: if self.multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
return can_buy_unlimited(self, 'Small Key (Universal)', player) return can_buy_unlimited(self, 'Small Key (Universal)', player)
return self.prog_items[player][item] >= count return self.prog_items[player][item] >= count

View File

@@ -14,8 +14,8 @@ class TestDungeon(LTTPTestBase):
self.starting_regions = [] # Where to start exploring self.starting_regions = [] # Where to start exploring
self.remove_exits = [] # Block dungeon exits self.remove_exits = [] # Block dungeon exits
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal'] self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.multiworld.bombless_start[1].value = True self.multiworld.worlds[1].options.bombless_start.value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2 self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
create_regions(self.multiworld, 1) create_regions(self.multiworld, 1)
self.multiworld.worlds[1].create_dungeons() self.multiworld.worlds[1].create_dungeons()
create_shops(self.multiworld, 1) create_shops(self.multiworld, 1)

View File

@@ -14,9 +14,9 @@ class TestInverted(TestBase, LTTPTestBase):
def setUp(self): def setUp(self):
self.world_setup() self.world_setup()
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal'] self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.multiworld.mode[1].value = 2 self.multiworld.worlds[1].options.mode.value = 2
self.multiworld.bombless_start[1].value = True self.multiworld.worlds[1].options.bombless_start.value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2 self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
create_inverted_regions(self.multiworld, 1) create_inverted_regions(self.multiworld, 1)
self.world.create_dungeons() self.world.create_dungeons()
create_shops(self.multiworld, 1) create_shops(self.multiworld, 1)

View File

@@ -12,7 +12,7 @@ class TestInvertedBombRules(LTTPTestBase):
def setUp(self): def setUp(self):
self.world_setup() self.world_setup()
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal'] self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.multiworld.mode[1].value = 2 self.multiworld.worlds[1].options.mode.value = 2
create_inverted_regions(self.multiworld, 1) create_inverted_regions(self.multiworld, 1)
self.multiworld.worlds[1].create_dungeons() self.multiworld.worlds[1].create_dungeons()

View File

@@ -14,10 +14,10 @@ from worlds.alttp.test import LTTPTestBase
class TestInvertedMinor(TestBase, LTTPTestBase): class TestInvertedMinor(TestBase, LTTPTestBase):
def setUp(self): def setUp(self):
self.world_setup() self.world_setup()
self.multiworld.mode[1].value = 2 self.multiworld.worlds[1].options.mode.value = 2
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("minor_glitches") self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("minor_glitches")
self.multiworld.bombless_start[1].value = True self.multiworld.worlds[1].options.bombless_start.value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2 self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal'] self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
create_inverted_regions(self.multiworld, 1) create_inverted_regions(self.multiworld, 1)
self.world.create_dungeons() self.world.create_dungeons()

View File

@@ -14,10 +14,10 @@ from worlds.alttp.test import LTTPTestBase
class TestInvertedOWG(TestBase, LTTPTestBase): class TestInvertedOWG(TestBase, LTTPTestBase):
def setUp(self): def setUp(self):
self.world_setup() self.world_setup()
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("overworld_glitches") self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("overworld_glitches")
self.multiworld.mode[1].value = 2 self.multiworld.worlds[1].options.mode.value = 2
self.multiworld.bombless_start[1].value = True self.multiworld.worlds[1].options.bombless_start.value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2 self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal'] self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
create_inverted_regions(self.multiworld, 1) create_inverted_regions(self.multiworld, 1)
self.world.create_dungeons() self.world.create_dungeons()

View File

@@ -11,9 +11,9 @@ from worlds.alttp.test import LTTPTestBase
class TestMinor(TestBase, LTTPTestBase): class TestMinor(TestBase, LTTPTestBase):
def setUp(self): def setUp(self):
self.world_setup() self.world_setup()
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("minor_glitches") self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("minor_glitches")
self.multiworld.bombless_start[1].value = True self.multiworld.worlds[1].options.bombless_start.value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2 self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal'] self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.world.er_seed = 0 self.world.er_seed = 0
self.world.create_regions() self.world.create_regions()

View File

@@ -23,7 +23,7 @@ class GoalPyramidTest(PyramidTestBase):
} }
def testCrystalsGoalAccess(self): def testCrystalsGoalAccess(self):
self.multiworld.goal[1].value = 1 # crystals self.multiworld.worlds[1].options.goal.value = 1 # crystals
self.assertFalse(self.can_reach_entrance("Pyramid Hole")) self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
self.collect_by_name(["Hammer", "Progressive Glove", "Moon Pearl"]) self.collect_by_name(["Hammer", "Progressive Glove", "Moon Pearl"])
self.assertTrue(self.can_reach_entrance("Pyramid Hole")) self.assertTrue(self.can_reach_entrance("Pyramid Hole"))

View File

@@ -12,9 +12,9 @@ class TestVanillaOWG(TestBase, LTTPTestBase):
def setUp(self): def setUp(self):
self.world_setup() self.world_setup()
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal'] self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("overworld_glitches") self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("overworld_glitches")
self.multiworld.bombless_start[1].value = True self.multiworld.worlds[1].options.bombless_start.value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2 self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
self.multiworld.worlds[1].er_seed = 0 self.multiworld.worlds[1].er_seed = 0
self.multiworld.worlds[1].create_regions() self.multiworld.worlds[1].create_regions()
self.multiworld.worlds[1].create_items() self.multiworld.worlds[1].create_items()

View File

@@ -10,10 +10,10 @@ from worlds.alttp.test import LTTPTestBase
class TestVanilla(TestBase, LTTPTestBase): class TestVanilla(TestBase, LTTPTestBase):
def setUp(self): def setUp(self):
self.world_setup() self.world_setup()
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("no_glitches") self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("no_glitches")
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal'] self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.multiworld.bombless_start[1].value = True self.multiworld.worlds[1].options.bombless_start.value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2 self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
self.multiworld.worlds[1].er_seed = 0 self.multiworld.worlds[1].er_seed = 0
self.multiworld.worlds[1].create_regions() self.multiworld.worlds[1].create_regions()
self.multiworld.worlds[1].create_items() self.multiworld.worlds[1].create_items()

View File

@@ -41,6 +41,7 @@ class AquariaWeb(WebWorld):
) )
tutorials = [setup, setup_fr] tutorials = [setup, setup_fr]
game_info_languages = ["en", "fr"]
class AquariaWorld(World): class AquariaWorld(World):

View File

@@ -16,8 +16,11 @@ Cyberfunk root folder. *Do not use any pre-release versions of BepInEx 6.*
2. Start Bomb Rush Cyberfunk once so that BepInEx can create its required configuration files. 2. Start Bomb Rush Cyberfunk once so that BepInEx can create its required configuration files.
3. Download the zip archive from the [releases](https://github.com/TRPG0/BRC-Archipelago/releases) page, and extract its 3. Download `ModLocalizer.dll` from its [releases](https://github.com/TRPG0/BRC-ModLocalizer/releases) page, and put it
contents into `BepInEx\plugins`. in `BepInEx\plugins`.
4. Download the zip archive for the Archipelago plugin from its [releases](https://github.com/TRPG0/BRC-Archipelago/releases)
page, and extract the contents into `BepInEx\plugins`.
After installing Archipelago, there are some additional mods that can also be installed for a better experience: After installing Archipelago, there are some additional mods that can also be installed for a better experience:

View File

@@ -7,9 +7,9 @@ config file.
## What is considered a location check in ChecksFinder? ## What is considered a location check in ChecksFinder?
Location checks in are completed when the player finds a spot on a board that has the archipelago logo. The bottom of Location checks get cleared when you open all non-bomb cells in a board. The bottom
the screen has a number next to the archipelago logo, that number is how many you can find so far. You can only get as of the screen has a number next to the Archipelago logo that displays how many location checks are left to be sent with
many checks as you have gained items, plus five to start with being available. your current inventory. You can only get as many checks as you have gained items plus five checks to start with.
## When the player receives an item, what happens? ## When the player receives an item, what happens?

View File

@@ -51,7 +51,7 @@ Boosts have logic associated with them in order to verify you can always reach t
- I need to kill a unit with a slinger/archer/musketman or some other obsolete unit I can't build anymore, how can I do this? - I need to kill a unit with a slinger/archer/musketman or some other obsolete unit I can't build anymore, how can I do this?
- Don't forget you can go into the Tech Tree and click on a Vanilla tech you've received in order to toggle it on/off. This is necessary in order to pursue some of the boosts if you receive techs in certain orders. - Don't forget you can go into the Tech Tree and click on a Vanilla tech you've received in order to toggle it on/off. This is necessary in order to pursue some of the boosts if you receive techs in certain orders.
- Something happened, and I'm not able to unlock the boost due to game rules! - Something happened, and I'm not able to unlock the boost due to game rules!
- A few scenarios you may worry about: "Found a religion", "Make an alliance with another player", "Develop an alliance to level 2", "Build a wonder from X Era", to name a few. Any boost that is "miss-able" has been flagged as an "Excluded" location and will not ever receive a progression item. For a list of how each boost is flagged, take a look [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/boosts.json). - A few scenarios you may worry about: "Found a religion", "Make an alliance with another player", "Develop an alliance to level 2", "Build a wonder from X Era", to name a few. Any boost that is "miss-able" has been flagged as an "Excluded" location and will not ever receive a progression item. For a list of how each boost is flagged, take a look [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/boosts.py).
- I'm worried that my `PROGRESSIVE_ERA` item is going to be stuck in a boost I won't have time to complete before my maximum unlocked era ends! - I'm worried that my `PROGRESSIVE_ERA` item is going to be stuck in a boost I won't have time to complete before my maximum unlocked era ends!
- The unpredictable timing of boosts and unlocking them can occasionally lead to scenarios where you'll have to first encounter a locked era defeat and then load a previous save. To help reduce the frequency of this, local `PROGRESSIVE_ERA` items will never be located at a boost check. - The unpredictable timing of boosts and unlocking them can occasionally lead to scenarios where you'll have to first encounter a locked era defeat and then load a previous save. To help reduce the frequency of this, local `PROGRESSIVE_ERA` items will never be located at a boost check.
- There's too many boosts, how will I know which one's I should focus on?! - There's too many boosts, how will I know which one's I should focus on?!

View File

@@ -14,22 +14,17 @@ The following are required in order to play Civ VI in Archipelago:
## Enabling the tuner ## Enabling the tuner
Depending on how you installed Civ 6 you will have to navigate to one of the following: In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
- `YOUR_USER/Documents/My Games/Sid Meier's Civilization VI/AppOptions.txt`
- `YOUR_USER/AppData/Local/Firaxis Games/Sid Meier's Civilization VI/AppOptions.txt`
Once you have located your `AppOptions.txt`, do a search for `Enable FireTuner`. Set `EnableTuner` to `1` instead of `0`. **NOTE**: While this is active, achievements will be disabled.
## Mod Installation ## Mod Installation
1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest). 1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. 2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure.
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. Associate the file with the Archipelago Launcher and double click it. 3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. Associate the file with the Archipelago Launcher and double click it.
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. 4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can just rename it to a file ending with `.zip` and extract its contents to a new folder. To do this, right click the `.apcivvi` file and click "Rename", make sure it ends in `.zip`, then right click it again and select "Extract All".
5. Your finished mod folder should look something like this: 5. Your finished mod folder should look something like this:

View File

@@ -31,6 +31,7 @@ class CliqueWebWorld(WebWorld):
) )
tutorials = [setup_en, setup_de] tutorials = [setup_en, setup_de]
game_info_languages = ["en", "de"]
class CliqueWorld(World): class CliqueWorld(World):

View File

@@ -10,12 +10,20 @@ from worlds._bizhawk.client import BizHawkClient
if TYPE_CHECKING: if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext from worlds._bizhawk.context import BizHawkClientContext
DEATHLINK_AREA_NUMBERS = [0, 1, 1, 2, 2, 2, 2, 3, 4, 5, 5, 5, 5, 5, 5, 5,
7, 9, 8, 6, 12, 12, 13, 11, 12, 5, 2, 10, 13, 13]
DEATHLINK_AREA_NAMES = ["Forest of Silence", "Castle Wall", "Villa", "Tunnel", "Underground Waterway", "Castle Center",
"Duel Tower", "Tower of Execution", "Tower of Science", "Tower of Sorcery", "Room of Clocks",
"Clock Tower", "Castle Keep", "Level: You Cheated"]
class Castlevania64Client(BizHawkClient): class Castlevania64Client(BizHawkClient):
game = "Castlevania 64" game = "Castlevania 64"
system = "N64" system = "N64"
patch_suffix = ".apcv64" patch_suffix = ".apcv64"
self_induced_death = False self_induced_death = False
time_of_sent_death = None
received_deathlinks = 0 received_deathlinks = 0
death_causes = [] death_causes = []
currently_shopping = False currently_shopping = False
@@ -62,15 +70,19 @@ class Castlevania64Client(BizHawkClient):
return return
if "tags" not in args: if "tags" not in args:
return return
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name: if "DeathLink" in args["tags"] and args["data"]["time"] != self.time_of_sent_death:
self.received_deathlinks += 1 self.received_deathlinks += 1
if "cause" in args["data"]: if "cause" in args["data"]:
cause = args["data"]["cause"] cause = args["data"]["cause"]
# If the other game sent a death with a blank string for the cause, use the default death message.
if cause == "":
cause = f"{args['data']['source']} killed you without a word!"
# Truncate the death cause message at 120 characters. # Truncate the death cause message at 120 characters.
if len(cause) > 120: if len(cause) > 120:
cause = cause[0:120] cause = cause[0:120]
else: else:
cause = f"{args['data']['source']} killed you!" # If the other game sent a death with no cause at all, use the default death message.
cause = f"{args['data']['source']} killed you without a word!"
self.death_causes.append(cause) self.death_causes.append(cause)
async def game_watcher(self, ctx: "BizHawkClientContext") -> None: async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
@@ -115,11 +127,30 @@ class Castlevania64Client(BizHawkClient):
if "DeathLink" in ctx.tags and save_struct[0xA4] & 0x80 and not self.self_induced_death and not \ if "DeathLink" in ctx.tags and save_struct[0xA4] & 0x80 and not self.self_induced_death and not \
deathlink_induced_death: deathlink_induced_death:
self.self_induced_death = True self.self_induced_death = True
if save_struct[0xA4] & 0x08:
# Special death message for dying while having the Vamp status. # If the player died at the Castle Keep exterior map on one of the Room of Clocks boss towers
await ctx.send_death(f"{ctx.player_names[ctx.slot]} became a vampire and drank your blood!") # (determinable by checking the entrance value as well as the map value), consider Room of Clocks the
# actual area of death.
if save_struct[0xAD] == 0x14 and save_struct[0xAF] in [0, 1]:
area_of_death = DEATHLINK_AREA_NAMES[10]
# Otherwise, determine what area the player perished in from the current map ID.
else: else:
await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished. Dracula has won!") area_of_death = DEATHLINK_AREA_NAMES[DEATHLINK_AREA_NUMBERS[save_struct[0xAD]]]
# If we had the Vamp status while dying, use a special message.
if save_struct[0xA4] & 0x08:
death_message = (f"{ctx.player_names[ctx.slot]} became a vampire at {area_of_death} and drank your "
f"blood!")
# Otherwise, use the generic one.
else:
death_message = f"{ctx.player_names[ctx.slot]} perished in {area_of_death}. Dracula has won!"
# Send the death.
await ctx.send_death(death_message)
# Record the time in which the death was sent so when we receive the packet we can tell it wasn't our
# own death. ctx.on_deathlink overwrites it later, so it MUST be grabbed now.
self.time_of_sent_death = ctx.last_death_link
# Write any DeathLinks received along with the corresponding death cause starting with the oldest. # Write any DeathLinks received along with the corresponding death cause starting with the oldest.
# To minimize Bizhawk Write jank, the DeathLink write will be prioritized over the item received one. # To minimize Bizhawk Write jank, the DeathLink write will be prioritized over the item received one.
@@ -208,6 +239,7 @@ class Castlevania64Client(BizHawkClient):
# Send game clear if we're in either any ending cutscene or the credits state. # Send game clear if we're in either any ending cutscene or the credits state.
if not ctx.finished_game and (0x26 <= int(cutscene_value) <= 0x2E or game_state == 0x0000000B): if not ctx.finished_game and (0x26 <= int(cutscene_value) <= 0x2E or game_state == 0x0000000B):
ctx.finished_game = True
await ctx.send_msgs([{ await ctx.send_msgs([{
"cmd": "StatusUpdate", "cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL "status": ClientStatus.CLIENT_GOAL

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Set from typing import TYPE_CHECKING, Set, Optional
from .locations import BASE_ID, get_location_names_to_ids from .locations import BASE_ID, get_location_names_to_ids
from .items import cvcotm_item_info, MAJORS_CLASSIFICATIONS from .items import cvcotm_item_info, MAJORS_CLASSIFICATIONS
from .locations import cvcotm_location_info from .locations import cvcotm_location_info
@@ -91,6 +91,7 @@ class CastlevaniaCotMClient(BizHawkClient):
patch_suffix = ".apcvcotm" patch_suffix = ".apcvcotm"
sent_initial_packets: bool sent_initial_packets: bool
self_induced_death: bool self_induced_death: bool
time_of_sent_death: Optional[float]
local_checked_locations: Set[int] local_checked_locations: Set[int]
client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()} client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
killed_dracula_2: bool killed_dracula_2: bool
@@ -139,6 +140,7 @@ class CastlevaniaCotMClient(BizHawkClient):
self.sent_initial_packets = False self.sent_initial_packets = False
self.local_checked_locations = set() self.local_checked_locations = set()
self.self_induced_death = False self.self_induced_death = False
self.time_of_sent_death = None
self.client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()} self.client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
self.killed_dracula_2 = False self.killed_dracula_2 = False
self.won_battle_arena = False self.won_battle_arena = False
@@ -156,14 +158,16 @@ class CastlevaniaCotMClient(BizHawkClient):
return return
if ctx.slot is None: if ctx.slot is None:
return return
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name: if "DeathLink" in args["tags"] and args["data"]["time"] != self.time_of_sent_death:
if "cause" in args["data"]: if "cause" in args["data"]:
cause = args["data"]["cause"] cause = args["data"]["cause"]
# If the other game sent a death with a blank string for the cause, use the default death message.
if cause == "": if cause == "":
cause = f"{args['data']['source']} killed you without a word!" cause = f"{args['data']['source']} killed you without a word!"
if len(cause) > ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT: if len(cause) > ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT:
cause = cause[:ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT] cause = cause[:ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT]
else: else:
# If the other game sent a death with no cause at all, use the default death message.
cause = f"{args['data']['source']} killed you without a word!" cause = f"{args['data']['source']} killed you without a word!"
# Highlight the player that killed us in the game's orange text. # Highlight the player that killed us in the game's orange text.
@@ -259,8 +263,13 @@ class CastlevaniaCotMClient(BizHawkClient):
else: else:
area_of_death = DEATHLINK_AREA_NAMES[area] area_of_death = DEATHLINK_AREA_NAMES[area]
# Send the death.
await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished in {area_of_death}. Dracula has won!") await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished in {area_of_death}. Dracula has won!")
# Record the time in which the death was sent so when we receive the packet we can tell it wasn't our
# own death. ctx.on_deathlink overwrites it later, so it MUST be grabbed now.
self.time_of_sent_death = ctx.last_death_link
# Update the Dracula II and Battle Arena events already being done on past separate sessions for if the # Update the Dracula II and Battle Arena events already being done on past separate sessions for if the
# player is running the Battle Arena and Dracula goal. # player is running the Battle Arena and Dracula goal.
if f"castlevania_cotm_events_{ctx.team}_{ctx.slot}" in ctx.stored_data: if f"castlevania_cotm_events_{ctx.team}_{ctx.slot}" in ctx.stored_data:

View File

@@ -930,7 +930,7 @@ location_tables: Dict[str, List[DS3LocationData]] = {
"Great Swamp Ring", miniboss=True), # Giant Crab drop "Great Swamp Ring", miniboss=True), # Giant Crab drop
DS3LocationData("RS: Blue Sentinels - Horace", "Blue Sentinels", DS3LocationData("RS: Blue Sentinels - Horace", "Blue Sentinels",
missable=True, npc=True), # Horace quest missable=True, npc=True), # Horace quest
DS3LocationData("RS: Crystal Gem - stronghold, lizard", "Crystal Gem"), DS3LocationData("RS: Crystal Gem - stronghold, lizard", "Crystal Gem", lizard=True),
DS3LocationData("RS: Fading Soul - woods by Crucifixion Woods bonfire", "Fading Soul", DS3LocationData("RS: Fading Soul - woods by Crucifixion Woods bonfire", "Fading Soul",
static='03,0:53300210::'), static='03,0:53300210::'),

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