Compare commits

...

72 Commits

Author SHA1 Message Date
jamesbrq f110a7efbc Gauntlet Legends: Implement New Game (#3499)
* Update GauntletLegendsClient.py

* Continuation of obelisk update

* APPP Implementation Groundwork.

* Continuation of APPP Implementation

* Create Pain.asm

* ROM verification modified since crc32 hash can no longer be calculated.

* Initial upload of barrel/chest locations.

* Finished adding and naming all chests/barrels.

* Continued locations.

* Added new yaml options

* Work on mirror shards

* Python 3.8 compatibility

* Finalized location names. Fixed lambda bug.

* v1.0

* Removed excess Docs and Changed item counts.

* Update Archipelago Unittests.run.xml

* Started working on Docs.

* Updated item arrays for client

* Fix client bugs

* Flake8 fixes + Yaml fix

* Update speed boots

* v1.1

* Update GauntletLegendsClient.py

* Update GauntletLegendsClient.py

* Update GauntletLegendsClient.py

* Update GauntletLegendsClient.py

* v1.0.4

* Initial Ruff format

* Formatting part-2

* More formatting + Doc changes

* Updated Docs

* Move client to root

* Update Client Name

* Make Imports Relative

* Add new yaml options + rename obelisks

* v1.1 Initial Upload

* v1.1.0 + some additions

* Client Update

* Client Updates

* pre-v1.1.1

* Update GauntletLegendsClient.py

* pre-1.2

* 1.2.0

* v1.2.1

* Client fixes

* v1.3.0

* v1.4.0

* v1.4.1

* Merged Changes

* Experimental

* pre-v1.5.0

* Changes

* v1.5.0

* Update GauntletLegendsClient.py

* Death Update

* pre v1.5.1

* Obelisk rule changes

* pre-v1.5.1

* Update GauntletLegendsClient.py

* v1.5.1

* v1.5.1

* Update GauntletLegendsClient.py

* Small fix

* Update GauntletLegendsClient.py

* v1.5.2

* pre-v1.6.0

* pre-v1.6.0

* v1.6.0

* Update

* Update GauntletLegendsClient.py

* Test

* pre-v1.6.0

* Client Updtaes

* Update GauntletLegendsClient.py

* Update GauntletLegendsClient.py

* Update GauntletLegendsClient.py

* Update Rom.py

* Update GauntletLegendsClient.py

* Update Rom.py

* Added Logging on stage enter

* v1.6.2

* v1.6.2

* Update Rom.py

* Update GauntletLegendsClient.py

* v1.6.3

* Small Update

* Update GauntletLegendsClient.py

* Update GauntletLegendsClient.py

* Update GauntletLegendsClient.py

* Changes ready for review

* Update Rom.py

* Remove root client

* Update inno_setup.iss

* Update client

* Initial Review Refactor

* Update Arrays.py

* Review refactor

* Update __init__.py

* Small tweaks after test generations

* Update GauntletLegendsClient.py

* Implement new check_locations() function

* Add name for Skorn to level_names Dict

* Fix for #4671

* Update __init__.py

* Adjust item frequency + Place 50% of filler as local

* Oops I modified the itempool after create_items teehee

* Update __init__.py

* Check MD5 hash before patching ROM

* Update __init__.py

* Fix get_filler_item_name()

* Update MD5 check to built in method in FilePath

* Update Rom.py

* Update worlds/gl/Rom.py

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

* WIP Generation fixes

* v1.7.1

* Update Archipelago Unittests.run.xml

* 0.7.2

* Update GauntletLegendsClient.py

* Review Edits

* Update World description

* Fuzz fixes

* Update GauntletLegendsClient.py

* Make static data immutable

* Update option comparisons since no longer a Choice

* Update worlds/gl/Rom.py

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* Update __init__.py

* Initial Update commit

* Progress

* Remove un-used import + Add manifest

* Add missing docstring for client command

* Progress

* pre-v2.0.0-BETA

* Updates

* Progress

* Working Generations

* Working IN-Game

* v2.0.0-BETA

* Create archipelago.json

* v2.0.1

* v2.0.1 FINAL

* pre v2.0.2

* Delete report.json

* Cleanup

* pre v2.0.2

* v2.0.2

* PR Ready

* Update Items.py

* Delete fuzz.py

* Update Items.py

* Update GauntletLegendsClient.py

* Fix scouting disabled locations

* Client Updates

* v2.0.3

* Increment World Version

* Docstring + slot_data updates

* Option name update + Remove redundant option

* Code Cleanup

* Trenches is level 24

* Frequencies

* More cleanup and removal of stale code. MISSING BASEPATCH

* v2.1.0

* Minor enemy changes

* Small fix

* Update Rom.py

* v2.1.1

* Small fixes

* Update basepatch.bsdiff4

* Update GauntletLegendsClient.py

* Socket error clarity

* Delete settings.local.json

* Update GauntletLegendsClient.py

* v2.1.2 fixes

* Boss portal fix

* Small client fix + Item Frequencies

* Update archipelago.json

* Fix for games playing with max difficulty lower than 4 not sending locations

* Update archipelago.json

* v2.1.5

* Fix for Chimera's lair mirror shard

* Update archipelago.json

* Mod Re-Base + Queue limits + Item Receiving fixes

* Options cleanup and newlines for some files

* Include v2.x features in docs

* Make sure players has at least 1 item

* v2.1.6

* Update archipelago.json

* Review Edits + Client Tweaks

---------

Co-authored-by: qwint <qwint.42@gmail.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2026-07-11 19:26:27 +02:00
qwint f9c5868f7b ModuleUpdate: Add core requirements.txt as a constraint file to pip calls (#6309)
* Add core requirements.txt as a constraint file to pip calls

* remove --upgrade as requested

* Update ModuleUpdate.py
2026-07-11 18:27:58 +02:00
Doug Hoskisson fde71f66f3 Core: typing and cleaning, mostly in Fill.py (#4459)
* Core: typing and cleaning, mostly in `Fill.py`

* fix typing of things we get out of plando blocks

* clean getting multiworld from kwargs
add a list type
clean indentation

* fix issues with merge conflict resolution

* some things found after merge
2026-07-11 13:26:19 +02:00
Uriel e6e0bc3042 WebHost: Config option for custom port ranges v2 (#6009)
* Added ability to define custom port ranges the WebHost will use for game servers, instead of pure random.

* - Added better fallback to default port range when a custom range fails
- Updated config to be clearer

* Added ability to define custom port ranges the WebHost will use for game servers, instead of pure random.

* - Added better fallback to default port range when a custom range fails
- Updated config to be clearer

* Updated soft-fail message

* Removed dead import from customserver.py

* Update requirements.txt

Settings requirements to main core branch

* fix what reviewers said and add some improvements

* remove unused argument

* try fixing test with try

* use yaml lists instead of string for config

* fix value type bug on ephemeral type

* reuse sockets with websockets api instead of opening and closing them

* add used ports cache and filter used ports when looking for ports

* fix port randomizer

* Apply suggestions from code review

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* fix some reviews

* use weights for random port and remove more-itertools

* fix net_connections not working on macOS

* rename variables and functions

* lazy init `get_used_ports`

* change `game_ports` to be `tuple`

* fix last_used_ports not being updated locally

* fix random choices and move game_port conversion into tuple

* Apply suggestions from code review

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* use a named tuple on parse_game_ports

* only use ranges

* do it the duck way

* this should check all usable ports before failing

* fix while loop

* add return type to weighted random

* Update WebHostLib/customserver.py

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

* simplify tuple conversion check

* add tests

* reformat file and change `create_random_port_socket` test

* add more test cases for parse_game_ports

* try to prevent busy-looping on create random port socket when doing test

* simplify parse game port tests to one assertListEqual

* make the range lesser for port test

* reduce range on macOS

* Apply suggestions from code review

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

* Update WebHostLib/customserver.py

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

* Apply suggestions from code review

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* remove unused import

* Update WebHostLib/customserver.py

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* use generator expressions

* check for 0-tuple

* use some kind of shuffled queue

* update tests

* refactor new port handling into a class (#1)

* change time to monotonic

* Update docs/webhost configuration sample.yaml

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

* add psutil 7.2.2 as requirement

* Update WebHostLib/requirements.txt

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

---------

Co-authored-by: Lexipherous <jasonnlelong@gmail.com>
Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2026-06-20 17:36:18 +02:00
Emily 5ccef9802a WebHost: fix Authors on supported game page (#6218) 2026-06-07 18:21:30 +02:00
CosmicWolf 09aba95c03 Factorio: Fixed a needlessly small bug that does not affect gameplay (#6147) 2026-06-03 17:53:37 +02:00
Ishigh1 2a8ebe2399 Core+RB: Make has_from_list, has_from_list_unique, has_group and has_group_unique return True if count <= 0 and list is empty (#6240) 2026-06-03 08:20:04 +02:00
Ishigh1 ad0af18f51 Rule Builder: Resolve empty and() to True_() instead of False_()(#6239) 2026-06-02 09:10:43 +02:00
Justus Lind 65277a50ce Muse Dash: Update to Touhou Mugakudan -V- (#6234) 2026-06-01 22:38:23 +02:00
dependabot[bot] d241829585 Bump mistune from 3.2.0 to 3.2.1 in /WebHostLib (#6185)
Bumps [mistune](https://github.com/lepture/mistune) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/lepture/mistune/releases)
- [Changelog](https://github.com/lepture/mistune/blob/main/docs/changes.rst)
- [Commits](https://github.com/lepture/mistune/compare/v3.2.0...v3.2.1)

---
updated-dependencies:
- dependency-name: mistune
  dependency-version: 3.2.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 23:55:57 +02:00
CosmicWolf b24b3d10b6 Factorio: Locale changes (#5908)
---------

Co-authored-by: Joris Santen <joris.j.vansanten@student.hu.nl>
Co-authored-by: lepideble <147614625+lepideble@users.noreply.github.com>
2026-05-31 01:49:39 +02:00
Fabian Dill 787ddc400c Core: Bump version from 0.6.7 to 0.6.8 (#6114) 2026-05-27 03:26:30 +02:00
qwint 545171f3f4 Core: reset log level when using pyximport (#6212) 2026-05-20 09:12:54 +02:00
Jérémie Bolduc d0abfeb88b The Messenger: Fix portal shuffle logic not using unlocked portals (#6194) 2026-05-18 13:53:48 +02:00
Will Morrow b575983599 SM64: Change power star item classification to be deprioritized (#6191) 2026-05-17 23:44:40 +02:00
Jérémie Bolduc d8d148ac13 Stardew Valley: Bypass CollectionState overhead by just calling prog_items (#6065) 2026-05-17 23:41:34 +02:00
Silvris 24f75ba072 Settings: validate FilePath hash on use (#5854) 2026-05-17 23:40:08 +02:00
Katelyn Gigante 38e77e1b46 Factorio: Remove the need to have two separate installs (#4221) 2026-05-16 18:29:30 +02:00
JaredWeakStrike 799e0b7b0f KH2: fix ice cream double counting for logic (#6068) 2026-05-11 02:16:20 +02:00
Justus Lind 4ef1fb7630 Muse Dash: Convert to Rule Builder (#6166)
---------

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
2026-05-09 21:25:34 +02:00
Alchav 0cd81ff500 A Link to the Past: We Have Enemizer at Home (#6153) 2026-05-09 17:07:46 +02:00
Duck 55a1b12cb7 CommonClient: Make hint cost (shown in server label) update (#6149) 2026-05-09 17:01:22 +02:00
Duck 3fcd337f65 WebHost: Add authors to supported games page (#6123) 2026-05-09 17:00:00 +02:00
Duck 7a5acfeceb Core: Make manifest accessible as a world attribute (#6122) 2026-05-09 16:59:05 +02:00
Bryce Wilson 06dc1b897c Pokemon Emerald: Switch to rule builder (#5923) 2026-05-09 16:58:01 +02:00
Ishigh1 4a28888a66 Rule Builder: Implement AtLeast (#6085)
---------

Co-authored-by: Ian Robinson <ianronrobinson@gmail.com>
2026-05-09 16:56:10 +02:00
Christina d0407a74d6 Docs: Added new documentation for DS3's enemy randomization (random_enemy_preset) (#5738)
---------

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2026-05-09 16:54:20 +02:00
josephwhite e0810022f8 WebHost: Fix adding weighted text options (#5116) 2026-05-09 16:51:56 +02:00
Mysteryem 29a6f40c2b Blasphemous: Increase logic performance (#4585)
---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2026-05-09 16:50:34 +02:00
josephwhite 2bd572c23d Super Mario 64: Basic testing (#4335)
---------

Co-authored-by: Yussur Mustafa Oraji <N00byKing@hotmail.de>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2026-05-09 16:46:53 +02:00
Scipio Wright b59e52a103 Options: Remove the "" from the start inventory examples (#6165)
---------

Co-authored-by: Emily <35015090+EmilyV99@users.noreply.github.com>
2026-05-05 16:14:19 +02:00
Silvris 6d9d340c71 Launcher: display window for failed world loads if any (#6058) 2026-05-05 07:30:47 +02:00
qwint 4486510fbc Options: make template generation log the failing world if one fails (#6176) 2026-05-02 17:53:37 +02:00
Fabian Dill a68109f5a7 WebHost: Add deletion of old content to cleanup function (#6119)
---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2026-05-01 22:38:46 +02:00
Alchav 234b0581c9 ALttP: Fix starting mail and shield equipment from start inventory (#6154) 2026-05-01 11:30:35 +02:00
Justus Lind 798eeaad91 Muse Dash: Update to DASH AND SHOOT (#6163) 2026-05-01 11:29:59 +02:00
qwint 1875062099 Undertale: Handle zero arg /auto_patch (#5130) 2026-04-29 22:29:41 +02:00
Seldom 9da3c29990 Terraria: fix failing to generate with Calamity enabled and full accessibility on Mechanical Bosses goal #5787 2026-04-29 21:25:38 +02:00
palex00 90b72c0fa5 Docs: Add missing space to plando doc #6060 2026-04-29 21:25:14 +02:00
Mysteryem fd8f3fcd8b The Messenger: Add missing indirect conditions (#6101) 2026-04-29 21:24:41 +02:00
Jérémie Bolduc d36ba62243 The Messenger: Add missing rules for Key of Strength#6124 2026-04-29 21:24:10 +02:00
Ixrec 8ac335f92d Docs: Rewrite get_filler_item_name's docstring to avoid implying it's for filling all unfilled locations #5956 2026-04-29 21:22:35 +02:00
qwint 74a0cd6022 MultiServer: update help doc on auto_shutdown#5917 2026-04-29 21:21:42 +02:00
Sophia Caspe 31f5a6c4ea Docs: Rule Builder Filtered Example Fix (#6152) 2026-04-29 21:19:32 +02:00
River Buizel be9c7b1728 KH2: Update setup_en.md (#6129) 2026-04-29 21:18:59 +02:00
massimilianodelliubaldini a0236b2d74 Jak1: yamlless UT support (#6066) 2026-04-29 21:18:21 +02:00
threeandthreee af39d7926a LADX: doc update (#6050)
* update gfxmod doc

* romless generation

* Update setup_en.md

Co-authored-by: RoobyRoo <thegreenrobby@gmail.com>

---------

Co-authored-by: RoobyRoo <thegreenrobby@gmail.com>
2026-04-29 21:15:57 +02:00
Ian Robinson a48bf0195f Docs: Make RB OptionFilter docs clearer (#6028)
* add more docs for option filters

* reword the whole things

* style guide
2026-04-29 21:14:34 +02:00
Sophira 73856f63c8 Docs: Mention APQuest as a reference (#6000)
The APQuest implementation of an APWorld is incredibly useful and should
really have a mention here.
2026-04-29 21:12:04 +02:00
CookieCat d5cc201138 AHIT: Update FAQ in setup guide (#5975) 2026-04-29 21:11:36 +02:00
Ixrec 4346191c55 Docs: Add a warning to world api.md#events that you want StatusUpdate, not events, for actually sending the goal (#5957) 2026-04-29 21:10:26 +02:00
Mysteryem 0601494e39 Pokemon RB: Fix mutating global item_groups in get_filler_item_name() (#5947)
The `banned_items` variable was the `"Unique"`list within the
`item_groups` global. `get_filler_item_name()` could then mutate
`banned_items` through `banned_items.append("Poke Doll")` and
`banned_items += item_groups["Vending Machine Drinks"]`, causing
the contents of the `item_groups` global to be mutated.

This has been fixed by making `banned_items` a shallow copy of
`item_groups["Unique"]`.
2026-04-29 21:08:16 +02:00
Yussur Mustafa Oraji 78937054de sm64ex: Recommend Flatpak for Linux users (#5897) 2026-04-29 21:07:52 +02:00
UCSA 59674926ed Docs: added an italian translation of the setup guide for Muse Dash (#5834)
* Create setup_it.md

Created an italian translation of the setup for muse dash. I could not find direct references to translating a setup so I am assuming this is the correct way to submit a translation.
While I am a native in italian and I would consider myself fluent in english, I am not a translator so mistakes could have been made.

* Update __init__.py
2026-04-29 21:07:32 +02:00
Oshroth b716096a4b SML2: Send logic affecting randomiser options as slot_data (#5751)
* Send logic affecting randomiser options as slot_data

* SML2 - Universal Tracker support

* Remove debug logs

* Only check generation_is_fake once

---------

Co-authored-by: alchav <alchav@jalchavware.com>
2026-04-29 21:01:23 +02:00
el-u c851eff521 lufia2ac/docs: fix some errors (#5618)
Co-authored-by: Ludwig <80899010+wordfcuk@users.noreply.github.com>
2026-04-29 20:56:11 +02:00
Duck 01033940d6 MMBN3: Add spaces in concatenated string#5689 2026-04-29 20:55:32 +02:00
Mrks 15561e1e2d Generate: Added "quantity" option for player yamls to use multiple times a single yaml (#4948)
* Added  option for player yamls.

* Extended documentation

* Minimized value extraction.

* Added allow_quantity option to host.yaml

* Added  option for player yamls.

* Extended documentation

* Minimized value extraction.

* Added allow_quantity option to host.yaml

* Update settings.py

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

* Update Generate.py

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

* Added allow_quantity as application argument.

* Quantity > 1, allow_quantity = false -> error instead of silent 1

* Added check for quantity <= 0; reverted settings import change

* Update Generate.py

---------

Co-authored-by: qwint <qwint.42@gmail.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2026-04-29 20:49:19 +02:00
Ian Robinson 9c78edc764 Rule Builder: Fix rule hash collisions (#6169)
* fix rule hash collisions

* just let the dict do the work
2026-04-29 08:21:09 +02:00
lepideble d2395de9fa Factorio: move communication savegame to user dir (#5646) 2026-04-27 14:06:19 +02:00
black-sliver d41cec6494 CI: update softprops/action-gh-release to v3.0.0 (#6162)
The update will be required once GH drops support for Node 20.

Also enables new options the action has now.
2026-04-26 00:44:48 +02:00
NewSoupVi dc911399fb The Witness: More control over progressive symbols (#3961)
* More control over progressive symbols!

* Cleanup crew

* lol

* Make it configurable how second stage items act on their own

* only let independent symbols work if they're not progressive

* revert defaults

* Better description

* comment for reviewers

* A complicated docstring for a complicated function

* More accurate

* This probably works more generically

* Actually, this would cause other issues anyway, so no need to make this generic yet

* :/

* oops

* Change the system to use collect/remove override so that plando and start inventory work correctly

* Vi stop doing that thing challenge

* Make SecondStateSymbolsActIndependently an OptionSet

* jank

* oop

* this is why we make unit tests I guess

* More unit tests

* More unit tests

* Add note about the absence of Rotated Shapers from the independent symbols optionset

* More verbose description

* More verbose description

* slight reword

* Ruff ruff :3 I am a good puppy <3

* Remove invis dots

* oops

* Remove some unused symbols

* display name

* linecounting -> discard

* Make all progressive chains always work

* Unfortunately, this optimisation is now unsafe with plando :(

* oops

* This is now a possible optimisation

* optimise optimise optimise

* optimise optimise optimise

* fix

* ruff

* oh

* fixed frfr

* mypy

* Clean up the tests a bit

* oop

* I actually like this better now, so I'm doing it as default

* Put stuff on the actual item class for faster collect/remove

* giga oops

* Make proguseful work

* None item left beef

* unnecessary change

* formatting

* add proguseful test to progressive symbols tests

* add proguseful test to progressive symbols tests

* clean up collect/remove a bit more

* giga lmfao

* Put stuff in option groups

* Add the new symbol items to hint system

* bump req client version

* fix soft conflict in unit tests

* fix more merge errors
2026-04-18 18:07:23 +02:00
NewSoupVi 0b38065123 APQuest: Fix Easy Mode boss having too much health to beat (#6146)
* Change the boss' health to 2 in easy mode, adjust boss graphics to reflect this behavior

* graphics formatting
2026-04-18 17:08:21 +02:00
NewSoupVi a6c1347102 The Witness: Make "Early Good Item" an OptionSet allowing Symbols, Doors and Obelisk Keys (#3804)
* New solution to that plando issue

* better

* Another warning

* better comment

* Best of both worlds I guess?

* oops

* Smarter code reuse

* better comment

* oop

* lint

* mypy

* player_name

* add unit test

* oh

* Rebrand time baby

* This fits on one line

* oop

* Update worlds/witness/__init__.py

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

* mypy

* Reorganize some doors according to medic's suggestions

* This should make it much faster (Thanks Medic)

* Town Doors works because of Church being a check always, Church Entry feels really bad tho

* Only add Desert Entry if there are no control panels stopping you

* No overlap here, so why is it a set

* Idk everything's kinda good without symbol shuffle

* Just make sure, I guess

* This makes way more sense doesn't it

* Oh, this is probably important

* oop

* loc

* oops 2

* ruff

* that was already in there

* Change the door picking a bit further

* some renaming

* slight wording change

* Fix

* Move it all to a new file

* ruff

* mypy

* .

* Make sure we're only adding as many tutorial checks as necessary

* ruff

* These checks aren't necessary, as the final list gets culled to only existing items anyway. It saves CPU cycles, but this is nicer for future compatibility

* Special handling for caves shortcuts

* 120 chars

* Update worlds/witness/place_early_item.py

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

* Clean up Windmill & Theater cases

* Make early_symbol_item removed instead

* Add early_good_item to presets

* replace double None checks with casts

* That doesn't exist anymore

* Mypy thing

* Update the doors again a bit

* Pycharm pls

* ruff

* forgot one

* oop

* Is it finally right?

* Update options.py

* Fix with new Panel Keys

* Hopefully fix crash when one of the types runs out when the others haven't yet

* oops

* Medic suggestion

* unused import

* Update place_early_item.py

* Update __init__.py

* Update __init__.py

* Update options.py

* Add possible types to option desc

* Make that include all Tutorial (Inside) checks

* Update __init__.py

* Update test_early_good_item.py

* Update test_early_good_item.py

* Slight cleanup

* fix no tutorial locations being picked up by tutorial location size check

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2026-04-18 13:52:38 +02:00
NewSoupVi 0a742b6c98 Options: Add more Option unit tests, add generic Option.__eq__, make cull_zeroes available to all OptionCounters #5905 2026-04-18 13:49:19 +02:00
NewSoupVi 66712bbd87 APQuest: Switch to Rule Builder (#5906)
* CachedRuleBuilderWorld

* CachedRuleBuilderWorld

* APQuest Rule Builder finished

* Added comment

Add a rule to check if the player has a Sword to destroy bushes.

* Bump version + typo fix

* Update worlds/apquest/rules.py

Co-authored-by: Ian Robinson <drtchops@users.noreply.github.com>

* Address Tchops' review comments

---------

Co-authored-by: Ian Robinson <drtchops@users.noreply.github.com>
2026-04-18 13:48:32 +02:00
black-sliver 5f9e38b783 Test: add test for unpickling NetUtils enums (#5326)
* Test: add test for unpickling NetUtils enums

This verifies that Utils.ByValue either works or is not required,
and once we drop ByValue, this validates that future Python
versions do not break our Enums again.

* Test: NetUtils enum switch to more direct pickle.dumps

It's probably better to use the direct interface in case
restricted_dumps does some funky stuff in the future.

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Test: NetUtils enum fix import for change

---------

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>
2026-04-18 02:15:37 +02:00
Duck bdde2140b3 Core: Remove worlds.generic.PlandoItem (#6035) 2026-04-18 01:24:25 +02:00
qwint 42d24b4869 HK: add manifest (#6057) 2026-04-18 01:23:19 +02:00
black-sliver 14aa6571de WebHost: Fix: strict suuid converter (#6130)
* WebHost: disallow '=' and whitespace in SUUID and append the correct number of '='

This makes Room IDs unambiguous and avoids breaking if b64decode ever becomes strict.

* Tests, WebHost: add SUUID tests

* Tests, WebHost: add edge cases to SUUID tests
2026-04-13 20:17:27 +00:00
sudo-nano 37d32a10b1 Factorio: Improve English instructions (#5306) 2026-04-13 11:19:23 +02:00
Silvris 0ef2ed5b81 Docs: fix misinformation in tests.md (#6125) 2026-04-09 21:27:39 +02:00
173 changed files with 26697 additions and 2847 deletions
-1
View File
@@ -46,7 +46,6 @@ dist
/prof/
README.html
.vs/
EnemizerCLI/
/Players/
/SNI/
/sni-*/
+1
View File
@@ -19,6 +19,7 @@
"../test/programs/test_multi_server.py",
"../test/utils/__init__.py",
"../test/webhost/test_descriptions.py",
"../test/webhost/test_suuid.py",
"../worlds/AutoSNIClient.py",
"type_check.py"
],
+7 -3
View File
@@ -29,7 +29,7 @@ jobs:
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV # tag x.y.z will become "Archipelago x.y.z"
- name: Create Release
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
draft: true # don't publish right away, especially since windows build is added by hand
prerelease: false
@@ -97,13 +97,15 @@ jobs:
build/exe.*/ArchipelagoServer.exe
setups/*
- name: Add to Release
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
draft: true # see above
prerelease: false
name: Archipelago ${{ env.RELEASE_VERSION }}
files: |
setups/*
fail_on_unmatched_files: true
overwrite_files: false # Windows release is usually built by hand
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -165,12 +167,14 @@ jobs:
build/exe.*/ArchipelagoServer
dist/*
- name: Add to Release
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
draft: true # see above
prerelease: false
name: Archipelago ${{ env.RELEASE_VERSION }}
files: |
dist/*
fail_on_unmatched_files: true
overwrite_files: false # should never happen; avoids accidentally changing a release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+9 -1
View File
@@ -514,7 +514,7 @@ class MultiWorld():
state.can_reach(Region) in the Entrance's traversal condition, as opposed to pure transition logic."""
self.indirect_connections.setdefault(region, set()).add(entrance)
def get_locations(self, player: Optional[int] = None) -> Iterable[Location]:
def get_locations(self, player: int | None = None) -> Collection[Location]:
if player is not None:
return self.regions.location_cache[player].values()
return Utils.RepeatableChain(tuple(self.regions.location_cache[player].values()
@@ -1038,6 +1038,8 @@ class CollectionState():
def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list."""
if count <= 0:
return True
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in items:
@@ -1049,6 +1051,8 @@ class CollectionState():
def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
Ignores duplicates of the same item."""
if count <= 0:
return True
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in items:
@@ -1077,6 +1081,8 @@ class CollectionState():
# item name group related
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
"""Returns True if the state contains at least `count` items present in a specified item group."""
if count <= 0:
return True
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
@@ -1089,6 +1095,8 @@ class CollectionState():
"""Returns True if the state contains at least `count` items present in a specified item group.
Ignores duplicates of the same item.
"""
if count <= 0:
return True
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
+5 -1
View File
@@ -1069,7 +1069,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
if "players" in args:
ctx.consume_players_package(args["players"])
if "hint_points" in args:
ctx.hint_points = args['hint_points']
ctx.hint_points = args["hint_points"]
if "checked_locations" in args:
checked = set(args["checked_locations"])
ctx.checked_locations |= checked
@@ -1077,6 +1077,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
if "permissions" in args:
ctx.update_permissions(args["permissions"])
# Update hint info for local display
if "hint_cost" in args:
ctx.hint_cost = int(args["hint_cost"])
elif cmd == 'Print':
ctx.on_print(args)
-27
View File
@@ -1,23 +1,5 @@
# hadolint global ignore=SC1090,SC1091
# Source
FROM scratch AS release
WORKDIR /release
ADD https://github.com/Ijwu/Enemizer/releases/latest/download/ubuntu.16.04-x64.zip Enemizer.zip
# Enemizer
FROM alpine:3.21 AS enemizer
ARG TARGETARCH
WORKDIR /release
COPY --from=release /release/Enemizer.zip .
# No release for arm architecture. Skip.
RUN if [ "$TARGETARCH" = "amd64" ]; then \
apk add unzip=6.0-r15 --no-cache && \
unzip -u Enemizer.zip -d EnemizerCLI && \
chmod -R 777 EnemizerCLI; \
else touch EnemizerCLI; fi
# Cython builder stage
FROM python:3.12 AS cython-builder
@@ -81,15 +63,6 @@ RUN apt-get purge -y \
g++ && \
apt-get autoremove -y
# Copy necessary components
COPY --from=enemizer /release/EnemizerCLI /tmp/EnemizerCLI
# No release for arm architecture. Skip.
RUN if [ "$TARGETARCH" = "amd64" ]; then \
cp -r /tmp/EnemizerCLI EnemizerCLI; \
fi; \
rm -rf /tmp/EnemizerCLI
# Define health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:${PORT:-80} || exit 1
+108 -87
View File
@@ -1,8 +1,8 @@
import collections
from collections import Counter, defaultdict, deque
from collections.abc import Iterable, Sequence
import itertools
import logging
import typing
from collections import Counter, deque
from typing import Callable, Literal
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, PlandoItemBlock
from Options import Accessibility
@@ -12,10 +12,11 @@ from worlds.generic.Rules import add_item_rule
class FillError(RuntimeError):
def __init__(self, *args: typing.Union[str, typing.Any], **kwargs) -> None:
if "multiworld" in kwargs and isinstance(args[0], str):
placements = (args[0] + f"\nAll Placements:\n" +
f"{[(loc, loc.item) for loc in kwargs['multiworld'].get_filled_locations()]}")
def __init__(self, *args: str | object, **kwargs: object) -> None:
multiworld = kwargs.get("multiworld")
if isinstance(multiworld, MultiWorld) and isinstance(args[0], str):
placements = (args[0] + "\nAll Placements:\n" +
f"{[(loc, loc.item) for loc in multiworld.get_filled_locations()]}")
args = (placements, *args[1:])
super().__init__(*args)
@@ -24,8 +25,9 @@ def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
logging.info(f"Current fill step ({name}) at {placed}/{total_items} items placed.")
def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple(),
locations: typing.Optional[typing.List[Location]] = None) -> CollectionState:
def sweep_from_pool(base_state: CollectionState,
itempool: Sequence[Item] = (),
locations: Iterable[Location] | None = None) -> CollectionState:
new_state = base_state.copy()
for item in itempool:
new_state.collect(item, True)
@@ -33,9 +35,9 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
return new_state
def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: list[Location],
item_pool: list[Item], single_player_placement: bool = False, lock: bool = False,
swap: bool = True, on_place: Callable[[Location], None] | None = None,
allow_partial: bool = False, allow_excluded: bool = False, one_item_per_player: bool = True,
name: str = "Unknown") -> None:
"""
@@ -51,11 +53,11 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
:param allow_excluded: if true and placement fails, it is re-attempted while ignoring excluded on Locations
:param name: name of this fill step for progress logging purposes
"""
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
unplaced_items: list[Item] = []
placements: list[Location] = []
cleanup_required = False
swapped_items: typing.Counter[typing.Tuple[int, str, bool]] = Counter()
reachable_items: typing.Dict[int, typing.Deque[Item]] = {}
swapped_items: Counter[tuple[int, str, bool]] = Counter()
reachable_items: dict[int, deque[Item]] = {}
for item in item_pool:
reachable_items.setdefault(item.player, deque()).append(item)
@@ -64,6 +66,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
placed = 0
while any(reachable_items.values()) and locations:
items_to_place: list[Item]
if one_item_per_player:
# grab one item per player
items_to_place = [items.pop()
@@ -95,13 +98,15 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
break
item_to_place = items_to_place.pop(0)
spot_to_fill: typing.Optional[Location] = None
spot_to_fill: Location | None = None
# if minimal accessibility, only check whether location is reachable if game not beatable
if multiworld.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
perform_access_check = not multiworld.has_beaten_game(maximum_exploration_state,
item_to_place.player) \
if single_player_placement else not has_beaten_game
perform_access_check = (
(not multiworld.has_beaten_game(maximum_exploration_state, item_to_place.player))
if single_player_placement
else not has_beaten_game
)
else:
perform_access_check = True
@@ -118,7 +123,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
if swap:
# Keep a cache of previous safe swap states that might be usable to sweep from to produce the next
# swap state, instead of sweeping from `base_state` each time.
previous_safe_swap_state_cache: typing.Deque[CollectionState] = deque()
previous_safe_swap_state_cache: deque[CollectionState] = deque()
# Almost never are more than 2 states needed. The rare cases that do are usually highly restrictive
# single_player_placement=True pre-fills which can go through more than 10 states in some seeds.
max_swap_base_state_cache_length = 3
@@ -215,7 +220,10 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
base_state, [], multiworld.get_filled_locations(item.player)
if single_player_placement else None)
for placement in placements:
if multiworld.worlds[placement.item.player].options.accessibility != "minimal" and not placement.can_reach(state):
if (
multiworld.worlds[placement.item.player].options.accessibility != "minimal" and
not placement.can_reach(state)
):
placement.item.location = None
unplaced_items.append(placement.item)
placement.item = None
@@ -255,14 +263,14 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
def remaining_fill(multiworld: MultiWorld,
locations: typing.List[Location],
itempool: typing.List[Item],
name: str = "Remaining",
locations: list[Location],
itempool: list[Item],
name: str = "Remaining",
move_unplaceable_to_start_inventory: bool = False,
check_location_can_fill: bool = False) -> None:
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
unplaced_items: list[Item] = []
placements: list[Location] = []
swapped_items: Counter[tuple[int, str]] = Counter()
total = min(len(itempool), len(locations))
placed = 0
@@ -270,15 +278,15 @@ def remaining_fill(multiworld: MultiWorld,
if check_location_can_fill:
state = CollectionState(multiworld)
def location_can_fill_item(location_to_fill: Location, item_to_fill: Item):
def location_can_fill_item(location_to_fill: Location, item_to_fill: Item) -> bool:
return location_to_fill.can_fill(state, item_to_fill, check_access=False)
else:
def location_can_fill_item(location_to_fill: Location, item_to_fill: Item):
def location_can_fill_item(location_to_fill: Location, item_to_fill: Item) -> bool:
return location_to_fill.item_rule(item_to_fill)
while locations and itempool:
item_to_place = itempool.pop()
spot_to_fill: typing.Optional[Location] = None
spot_to_fill: Location | None = None
# going through locations in the same order as the provided `locations` argument
for i, location in enumerate(locations):
@@ -336,7 +344,7 @@ def remaining_fill(multiworld: MultiWorld,
if unplaced_items and locations:
# There are leftover unplaceable items and locations that won't accept them
if move_unplaceable_to_start_inventory:
last_batch = []
last_batch: list[Item] = []
for item in unplaced_items:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
multiworld.push_precollected(item)
@@ -355,8 +363,8 @@ def remaining_fill(multiworld: MultiWorld,
def fast_fill(multiworld: MultiWorld,
item_pool: typing.List[Item],
fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]:
item_pool: list[Item],
fill_locations: list[Location]) -> tuple[list[Item], list[Location]]:
placing = min(len(item_pool), len(fill_locations))
for item, location in zip(item_pool, fill_locations):
multiworld.push_item(location, item, False)
@@ -370,11 +378,14 @@ def accessibility_corrections(multiworld: MultiWorld,
if pool is None:
pool = []
maximum_exploration_state = sweep_from_pool(state, pool)
minimal_players = {player for player in multiworld.player_ids if
multiworld.worlds[player].options.accessibility == "minimal"}
unreachable_locations = [location for location in multiworld.get_locations() if
location.player in minimal_players and
not location.can_reach(maximum_exploration_state)]
minimal_players = {player
for player in multiworld.player_ids
if multiworld.worlds[player].options.accessibility == "minimal"}
unreachable_locations = [
location
for location in multiworld.get_locations()
if location.player in minimal_players and not location.can_reach(maximum_exploration_state)
]
for location in unreachable_locations:
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):
@@ -389,33 +400,36 @@ def accessibility_corrections(multiworld: MultiWorld,
fill_restrictive(multiworld, state, locations, pool, name="Accessibility Corrections")
def inaccessible_location_rules(multiworld: MultiWorld, state: CollectionState, locations):
def inaccessible_location_rules(multiworld: MultiWorld, state: CollectionState, locations: Iterable[Location]) -> None:
maximum_exploration_state = sweep_from_pool(state)
unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)]
if unreachable_locations:
def forbid_important_item_rule(item: Item):
return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != "minimal")
def forbid_important_item_rule(item: Item) -> bool:
return not ((item.classification & 0b0011) and
multiworld.worlds[item.player].options.accessibility != "minimal")
for location in unreachable_locations:
add_item_rule(location, forbid_important_item_rule)
def distribute_early_items(multiworld: MultiWorld,
fill_locations: typing.List[Location],
itempool: typing.List[Item]) -> typing.Tuple[typing.List[Location], typing.List[Item]]:
fill_locations: list[Location],
itempool: list[Item]) -> tuple[list[Location], list[Item]]:
""" returns new fill_locations and itempool """
early_items_count: typing.Dict[typing.Tuple[str, int], typing.List[int]] = {}
early_items_count: dict[tuple[str, int], list[int]] = {}
for player in multiworld.player_ids:
items = itertools.chain(multiworld.early_items[player], multiworld.local_early_items[player])
for item in items:
early_items_count[item, player] = [multiworld.early_items[player].get(item, 0),
multiworld.local_early_items[player].get(item, 0)]
if early_items_count:
early_locations: typing.List[Location] = []
early_priority_locations: typing.List[Location] = []
loc_indexes_to_remove: typing.Set[int] = set()
early_locations: list[Location] = []
early_priority_locations: list[Location] = []
loc_indexes_to_remove: set[int] = set()
base_state = multiworld.state.copy()
base_state.sweep_for_advancements(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
base_state.sweep_for_advancements(locations=(loc
for loc in multiworld.get_filled_locations()
if loc.address is None))
for i, loc in enumerate(fill_locations):
if loc.can_reach(base_state):
if loc.progress_type == LocationProgressType.PRIORITY:
@@ -425,11 +439,11 @@ def distribute_early_items(multiworld: MultiWorld,
loc_indexes_to_remove.add(i)
fill_locations = [loc for i, loc in enumerate(fill_locations) if i not in loc_indexes_to_remove]
early_prog_items: typing.List[Item] = []
early_rest_items: typing.List[Item] = []
early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids}
early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids}
item_indexes_to_remove: typing.Set[int] = set()
early_prog_items: list[Item] = []
early_rest_items: list[Item] = []
early_local_prog_items: dict[int, list[Item]] = {player: [] for player in multiworld.player_ids}
early_local_rest_items: dict[int, list[Item]] = {player: [] for player in multiworld.player_ids}
item_indexes_to_remove: set[int] = set()
for i, item in enumerate(itempool):
if (item.name, item.player) in early_items_count:
if item.advancement:
@@ -487,7 +501,7 @@ def distribute_early_items(multiworld: MultiWorld,
def distribute_items_restrictive(multiworld: MultiWorld,
panic_method: typing.Literal["swap", "raise", "start_inventory"] = "swap") -> None:
panic_method: Literal["swap", "raise", "start_inventory"] = "swap") -> None:
assert all(item.location is None for item in multiworld.itempool), (
"At the start of distribute_items_restrictive, "
"there are items in the multiworld itempool that are already placed on locations:\n"
@@ -502,9 +516,9 @@ def distribute_items_restrictive(multiworld: MultiWorld,
fill_locations, itempool = distribute_early_items(multiworld, fill_locations, itempool)
progitempool: typing.List[Item] = []
usefulitempool: typing.List[Item] = []
filleritempool: typing.List[Item] = []
progitempool: list[Item] = []
usefulitempool: list[Item] = []
filleritempool: list[Item] = []
for item in itempool:
if item.advancement:
@@ -516,7 +530,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
call_all(multiworld, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
locations: dict[LocationProgressType, list[Location]] = {
loc_type: [] for loc_type in LocationProgressType}
for loc in fill_locations:
@@ -527,17 +541,17 @@ def distribute_items_restrictive(multiworld: MultiWorld,
excludedlocations = locations[LocationProgressType.EXCLUDED]
# can't lock due to accessibility corrections touching things, so we remember which ones got placed and lock later
lock_later = []
lock_later: list[Location] = []
def mark_for_locking(location: Location):
def mark_for_locking(location: Location) -> None:
nonlocal lock_later
lock_later.append(location)
single_player = multiworld.players == 1 and not multiworld.groups
if prioritylocations:
regular_progression = []
deprioritized_progression = []
regular_progression: list[Item] = []
deprioritized_progression: list[Item] = []
for item in progitempool:
if item.deprioritized:
deprioritized_progression.append(item)
@@ -623,7 +637,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
inaccessible_location_rules(multiworld, multiworld.state, defaultlocations)
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded",
move_unplaceable_to_start_inventory=panic_method=="start_inventory")
move_unplaceable_to_start_inventory=(panic_method == "start_inventory"))
if excludedlocations:
raise FillError(
@@ -635,7 +649,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
restitempool = filleritempool + usefulitempool
remaining_fill(multiworld, defaultlocations, restitempool,
move_unplaceable_to_start_inventory=panic_method=="start_inventory")
move_unplaceable_to_start_inventory=(panic_method == "start_inventory"))
unplaced = restitempool
unfilled = defaultlocations
@@ -654,18 +668,19 @@ def distribute_items_restrictive(multiworld: MultiWorld,
for player in multiworld.player_ids:
if more_locations[player]:
logging.error(
f"Player {multiworld.get_player_name(player)} had {more_locations[player]} more locations than items.")
f"Player {multiworld.get_player_name(player)} had "
f"{more_locations[player]} more locations than items.")
elif more_items[player]:
logging.warning(
f"Player {multiworld.get_player_name(player)} had {more_items[player]} more items than locations.")
if unfilled:
raise FillError(
f"Unable to fill all locations.\n" +
"Unable to fill all locations.\n"
f"Unfilled locations({len(unfilled)}): {unfilled}"
)
else:
logging.warning(
f"Unable to place all items.\n" +
"Unable to place all items.\n"
f"Unplaced items({len(unplaced)}): {unplaced}"
)
@@ -715,7 +730,7 @@ def flood_items(multiworld: MultiWorld) -> None:
if candidate_item_to_place is not None:
item_to_place = candidate_item_to_place
else:
raise FillError('No more progress items left to place.', multiworld=multiworld)
raise FillError("No more progress items left to place.", multiworld=multiworld)
# find item to replace with progress item
location_list = multiworld.get_reachable_locations()
@@ -738,7 +753,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
# Define a threshold value based on the player with the most available locations.
# If other players are below the threshold value, swap progression in this sphere into earlier spheres,
# which gives more locations available by this sphere.
balanceable_players: typing.Dict[int, float] = {
balanceable_players: dict[int, float] = {
player: multiworld.worlds[player].options.progression_balancing / 100
for player in multiworld.player_ids
if multiworld.worlds[player].options.progression_balancing > 0
@@ -749,15 +764,15 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
logging.info(f"Balancing multiworld progression for {len(balanceable_players)} Players.")
logging.debug(balanceable_players)
state: CollectionState = CollectionState(multiworld)
checked_locations: typing.Set[Location] = set()
unchecked_locations: typing.Set[Location] = set(multiworld.get_locations())
checked_locations: set[Location] = set()
unchecked_locations: set[Location] = set(multiworld.get_locations())
total_locations_count: typing.Counter[int] = Counter(
total_locations_count: Counter[int] = Counter(
location.player
for location in multiworld.get_locations()
if not location.locked
)
reachable_locations_count: typing.Dict[int, int] = {
reachable_locations_count: dict[int, int] = {
player: 0
for player in multiworld.player_ids
if total_locations_count[player] and len(multiworld.get_filled_locations(player)) != 0
@@ -771,7 +786,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
moved_item_count: int = 0
def get_sphere_locations(sphere_state: CollectionState,
locations: typing.Set[Location]) -> typing.Set[Location]:
locations: set[Location]) -> set[Location]:
return {loc for loc in locations if sphere_state.can_reach(loc)}
def item_percentage(player: int, num: int) -> float:
@@ -819,7 +834,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
balancing_unchecked_locations = unchecked_locations.copy()
balancing_reachables = reachable_locations_count.copy()
balancing_sphere = sphere_locations.copy()
candidate_items: typing.Dict[int, typing.Set[Location]] = collections.defaultdict(set)
candidate_items: dict[int, set[Location]] = defaultdict(set)
while True:
# Check locations in the current sphere and gather progression items to swap earlier
for location in balancing_sphere:
@@ -846,11 +861,11 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
elif not balancing_sphere:
raise RuntimeError("Not all required items reachable. Something went terribly wrong here.")
# Gather a set of locations which we can swap items into
unlocked_locations: typing.Dict[int, typing.Set[Location]] = collections.defaultdict(set)
unlocked_locations: dict[int, set[Location]] = defaultdict(set)
for l in unchecked_locations:
if l not in balancing_unchecked_locations:
unlocked_locations[l.player].add(l)
items_to_replace: typing.List[Location] = []
items_to_replace: list[Location] = []
for player in balancing_players:
locations_to_test = unlocked_locations[player]
items_to_test = list(candidate_items[player])
@@ -956,6 +971,12 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
for block in multiworld.worlds[player].options.plando_items:
new_block: PlandoItemBlock = PlandoItemBlock(player, block.from_pool, block.force)
target_world = block.world
# TODO: This doesn't handle the plando API correctly
# It says `world` can be other containers other than list,
# but this is checking specifically for list.
# (But it's not simple to just change the check to Iterable,
# because that will catch the str case too early.)
# (And it will be broken without failing unit tests. So also TODO: unit test this.)
if target_world is False or multiworld.players == 1: # target own world
worlds: set[int] = {player}
elif target_world is True: # target any worlds besides own
@@ -985,7 +1006,7 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
worlds = {world_name_lookup[target_world]}
new_block.worlds = worlds
items: list[str] | dict[str, typing.Any] = block.items
items = block.items
if isinstance(items, dict):
item_list: list[str] = []
for key, value in items.items():
@@ -1046,8 +1067,8 @@ def resolve_early_locations_for_planned(multiworld: MultiWorld):
swept_state = multiworld.state.copy()
swept_state.sweep_for_advancements()
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
early_locations: dict[int, list[Location]] = collections.defaultdict(list)
non_early_locations: dict[int, list[Location]] = collections.defaultdict(list)
early_locations: dict[int, list[Location]] = defaultdict(list)
non_early_locations: dict[int, list[Location]] = defaultdict(list)
for loc in multiworld.get_unfilled_locations():
if loc in reachable:
early_locations[loc.player].append(loc)
@@ -1055,7 +1076,7 @@ def resolve_early_locations_for_planned(multiworld: MultiWorld):
non_early_locations[loc.player].append(loc)
for player in multiworld.plando_item_blocks:
removed = []
removed: list[PlandoItemBlock] = []
for block in multiworld.plando_item_blocks[player]:
locations = block.locations
resolved_locations = block.resolved_locations
@@ -1079,8 +1100,7 @@ def resolve_early_locations_for_planned(multiworld: MultiWorld):
block.count["max"] = len(block.resolved_locations)
if block.count["min"] > len(block.resolved_locations):
block.count["min"] = len(block.resolved_locations)
block.count["target"] = multiworld.random.randint(block.count["min"],
block.count["max"])
block.count["target"] = multiworld.random.randint(block.count["min"], block.count["max"])
if not block.count["target"]:
removed.append(block)
@@ -1118,7 +1138,7 @@ def distribute_planned_blocks(multiworld: MultiWorld, plando_blocks: list[Plando
maxcount = placement.count["target"]
from_pool = placement.from_pool
item_candidates = []
item_candidates: list[Item] = []
if from_pool:
instances = [item for item in multiworld.itempool if item.player == player and item.name in items]
for item in multiworld.random.sample(items, maxcount):
@@ -1144,8 +1164,9 @@ def distribute_planned_blocks(multiworld: MultiWorld, plando_blocks: list[Plando
continue
else:
is_real = item_candidates[0].code is not None
candidates = [candidate for candidate in locations if candidate.item is None
and bool(candidate.address) == is_real]
candidates = [candidate
for candidate in locations
if candidate.item is None and bool(candidate.address) == is_real]
multiworld.random.shuffle(candidates)
allstate = multiworld.get_all_state(False)
mincount = placement.count["min"]
+13 -2
View File
@@ -40,6 +40,8 @@ def mystery_argparse(argv: list[str] | None = None) -> argparse.Namespace:
parser.add_argument('--spoiler', type=int, default=defaults.spoiler)
parser.add_argument('--outputpath', default=settings.general_options.output_path,
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
parser.add_argument('--allow_quantity', action="store_true", default=defaults.allow_quantity,
help='Allows the use of the quantity option in yamls. Default is the set value in the host.yaml.')
parser.add_argument('--race', action='store_true', default=defaults.race)
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
parser.add_argument('--log_level', default=defaults.loglevel, help='Sets log level')
@@ -123,6 +125,7 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
player_id: int = 1
player_files: dict[int, str] = {}
player_errors: list[str] = []
allow_quantity = args.allow_quantity
for file in os.scandir(args.player_files_path):
fname = file.name
if file.is_file() and not fname.startswith(".") and not fname.lower().endswith(".ini") and \
@@ -134,7 +137,14 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
if yaml is None:
logging.warning(f"Ignoring empty yaml document #{doc_idx + 1} in {fname}")
else:
weights_for_file.append(yaml)
quantity = yaml.get("quantity", 1)
if quantity <= 0:
raise ValueError("A quantity of 0 or less is invalid. Please change it to at least 1.")
if not allow_quantity and quantity > 1:
raise ValueError("Quantity greater than 1 is deactivated by host settings.")
for _ in range(quantity):
weights_for_file.append(yaml)
weights_cache[fname] = tuple(weights_for_file)
except Exception as e:
@@ -575,7 +585,8 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
raise Exception(f"Invalid game: {ret.game}")
if ret.game not in AutoWorldRegister.world_types:
from worlds import failed_world_loads
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + list(failed_world_loads.keys()),
limit=1)[0]
if picks[0] in failed_world_loads:
raise Exception(f"No functional world found to handle game {ret.game}. "
f"Did you mean '{picks[0]}' ({picks[1]}% sure)? "
+35
View File
@@ -36,6 +36,7 @@ if __name__ == "__main__":
init_logging('Launcher')
from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type
from worlds import failed_world_loads
def open_host_yaml():
@@ -275,6 +276,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
search_box: MDTextField = ObjectProperty(None)
cards: list[LauncherCard]
current_filter: Sequence[str | Type] | None
failed_worlds: bool = bool(failed_world_loads)
def __init__(self, ctx=None, components=None, args=None):
self.title = self.base_title + " " + Utils.__version__
@@ -422,6 +424,39 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
MDSnackbar(MDSnackbarText(text=open_text), y=dp(24), pos_hint={"center_x": 0.5},
size_hint_x=0.5).open()
@staticmethod
def copy_to_clipboard(text):
from kivy.core.clipboard import Clipboard
Clipboard.copy(text)
MDSnackbar(MDSnackbarText(text="Copied to clipboard."), y=dp(24), pos_hint={"center_x": 0.5},
size_hint_x=0.5).open()
def display_failed(self):
"""Display a dialog showing the exceptions produced by any world that failed to load during
initialization."""
if not self.failed_worlds:
return
from kivymd.uix.dialog import MDDialog, MDDialogIcon, MDDialogHeadlineText, MDDialogContentContainer
from kivymd.uix.divider import MDDivider
from kivymd.uix.list import MDListItem, MDListItemHeadlineText, MDListItemSupportingText
entries = []
for world, reason in failed_world_loads.items():
entries.append(MDListItem(
MDListItemHeadlineText(text=world),
MDListItemSupportingText(text=reason),
on_release=lambda x, r=reason: self.copy_to_clipboard(r)
))
dialog = MDDialog(
MDDialogIcon(icon="alert"),
MDDialogHeadlineText(text="Failed World Loads"),
MDDialogContentContainer(
MDDivider(),
*entries,
orientation="vertical",
)
)
dialog.open()
def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None:
""" When a patch file is dropped into the window, run the associated component. """
file, component = identify(filename.decode())
+2 -2
View File
@@ -241,8 +241,8 @@ async def gba_sync_task(ctx: MMBN3Context):
await ctx.server_auth(False)
else:
if not ctx.version_warning:
logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}."
"Please update to the latest version."
logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}. "
"Please update to the latest version. "
"Your connection to the Archipelago server will not be accepted.")
ctx.version_warning = True
except asyncio.TimeoutError:
+4 -3
View File
@@ -38,7 +38,8 @@ class RequirementsSet(set):
local_dir = os.path.dirname(__file__)
requirements_files = RequirementsSet((os.path.join(local_dir, 'requirements.txt'),))
core_constraints = os.path.join(local_dir, 'requirements.txt')
requirements_files = RequirementsSet((core_constraints,))
if not update_ran:
for entry in os.scandir(os.path.join(local_dir, "worlds")):
@@ -69,7 +70,7 @@ def confirm(msg: str):
def update_command():
check_pip()
for file in requirements_files:
subprocess.call([sys.executable, "-m", "pip", "install", "-r", file, "--upgrade"])
subprocess.call([sys.executable, "-m", "pip", "install", "-r", file, "--constraint", core_constraints])
def install_pkg_resources(yes=False):
@@ -79,7 +80,7 @@ def install_pkg_resources(yes=False):
check_pip()
if not yes:
confirm("pkg_resources not found, press enter to install it")
subprocess.call([sys.executable, "-m", "pip", "install", "--upgrade", "setuptools>=75,<81"])
subprocess.call([sys.executable, "-m", "pip", "install", "setuptools>=75,<81"])
def update(yes: bool = False, force: bool = False) -> None:
+2 -2
View File
@@ -2633,8 +2633,8 @@ def parse_args() -> argparse.Namespace:
goal: !remaining can be used after goal completion
''')
parser.add_argument('--auto_shutdown', default=defaults["auto_shutdown"], type=int,
help="automatically shut down the server after this many minutes without new location checks. "
"0 to keep running. Not yet implemented.")
help="automatically shut down the server after this many seconds without new location checks. "
"0 to keep running.")
parser.add_argument('--use_embedded_options', action="store_true",
help='retrieve release, remaining and hint options from the multidata file,'
' instead of host.yaml')
+4
View File
@@ -527,7 +527,11 @@ else:
except ImportError:
pyximport = None
try:
import logging
logger = logging.getLogger()
old_level = logger.level
from _speedups import LocationStore
logger.setLevel(old_level)
except ImportError:
warnings.warn("_speedups not available. Falling back to pure python LocationStore. "
"Install a matching C++ compiler for your platform to compile _speedups.")
+56 -30
View File
@@ -212,6 +212,13 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
else:
return cls.name_lookup[value]
def __eq__(self, other: typing.Any) -> bool:
if isinstance(other, self.__class__):
return self.value == other.value
if isinstance(other, Option):
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
return self.value == other
def __int__(self) -> T:
return self.value
@@ -930,13 +937,34 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
class OptionCounter(OptionDict):
min: int | None = None
max: int | None = None
cull_zeroes: bool = False
def __init__(self, value: dict[str, int]) -> None:
super(OptionCounter, self).__init__(collections.Counter(value))
cleaned_dict = {}
invalid_value_errors = []
for key, value in value.items():
if not isinstance(value, (int, float)) or int(value) != value:
invalid_value_errors += [f"Invalid value {value} for key {key}, must be an integer."]
continue
if self.cull_zeroes and value == 0:
continue
cleaned_dict[key] = int(value)
if invalid_value_errors:
type_errors = [f"For option {self.__class__.__name__}:"] + invalid_value_errors
raise TypeError("\n".join(invalid_value_errors))
super(OptionCounter, self).__init__(collections.Counter(cleaned_dict))
def verify(self, world: type[World], player_name: str, plando_options: PlandoOptions) -> None:
super(OptionCounter, self).verify(world, player_name, plando_options)
self.verify_values()
def verify_values(self):
range_errors = []
if self.max is not None:
@@ -959,13 +987,8 @@ class OptionCounter(OptionDict):
class ItemDict(OptionCounter):
verify_item_name = True
min = 0
def __init__(self, value: dict[str, int]) -> None:
# Backwards compatibility: Cull 0s to make "in" checks behave the same as when this wasn't a OptionCounter
value = {item_name: amount for item_name, amount in value.items() if amount != 0}
super(ItemDict, self).__init__(value)
# Backwards compatibility: Cull 0s to make "in" checks behave the same as when this wasn't a OptionCounter
cull_zeroes = True
class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
@@ -1446,7 +1469,7 @@ class NonLocalItems(ItemSet):
class StartInventory(ItemDict):
"""Start with the specified amount of these items. Example: "Bomb: 1" """
"""Start with the specified amount of these items. Example: {Bomb: 1, Arrow: 3} """
verify_item_name = True
display_name = "Start Inventory"
rich_text_doc = True
@@ -1454,7 +1477,7 @@ class StartInventory(ItemDict):
class StartInventoryPool(StartInventory):
"""Start with the specified amount of these items and don't place them in the world. Example: "Bomb: 1"
"""Start with the specified amount of these items and don't place them in the world. Example: {Bomb: 1, Arrow: 3}
The game decides what the replacement items will be.
"""
@@ -1833,27 +1856,30 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
for game_name, world in AutoWorldRegister.world_types.items():
if not world.hidden or generate_hidden:
presets = world.web.options_presets.copy()
presets.update({"": {}})
try:
presets = world.web.options_presets.copy()
presets.update({"": {}})
option_groups = get_option_groups(world)
for name, preset in presets.items():
res = template.render(
option_groups=option_groups,
__version__=__version__,
game=game_name,
world_version=world.world_version.as_simple_string(),
yaml_dump=yaml_dump_scalar,
dictify_range=dictify_range,
cleandoc=cleandoc,
preset_name=name,
preset=preset,
)
preset_name = f" - {name}" if name else ""
with open(os.path.join(preset_folder if name else target_folder,
get_file_safe_name(game_name + preset_name) + ".yaml"),
"w", encoding="utf-8-sig") as f:
f.write(res)
option_groups = get_option_groups(world)
for name, preset in presets.items():
res = template.render(
option_groups=option_groups,
__version__=__version__,
game=game_name,
world_version=world.world_version.as_simple_string(),
yaml_dump=yaml_dump_scalar,
dictify_range=dictify_range,
cleandoc=cleandoc,
preset_name=name,
preset=preset,
)
preset_name = f" - {name}" if name else ""
with open(os.path.join(preset_folder if name else target_folder,
get_file_safe_name(game_name + preset_name) + ".yaml"),
"w", encoding="utf-8-sig") as f:
f.write(res)
except Exception as ex:
raise Exception(f"Template generation failed for world {game_name}") from ex
def dump_player_options(multiworld: MultiWorld) -> None:
+1
View File
@@ -85,6 +85,7 @@ Currently, the following games are supported:
* Satisfactory
* EarthBound
* Mega Man 3
* Gauntlet Legends
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
+1 -1
View File
@@ -49,7 +49,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
if isinstance(self.ctx, UndertaleContext):
os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True)
tempInstall = steaminstall
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
if tempInstall and not os.path.isfile(os.path.join(tempInstall, "data.win")):
tempInstall = None
if tempInstall is None:
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
+11 -11
View File
@@ -18,11 +18,12 @@ import logging
import warnings
from argparse import Namespace
from collections.abc import Collection, Iterable
from datetime import datetime, timezone
from settings import Settings, get_settings
from time import sleep
from typing import BinaryIO, Coroutine, Mapping, Optional, Set, Dict, Any, Union, TypeGuard
from typing import BinaryIO, Coroutine, Generic, Mapping, Optional, Set, Dict, Any, TypeVar, Union, TypeGuard
from yaml import load, load_all, dump
from pathspec import PathSpec, GitIgnoreSpec
from typing_extensions import deprecated
@@ -52,7 +53,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self)
__version__ = "0.6.7"
__version__ = "0.6.8"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
@@ -450,13 +451,10 @@ safe_builtins = frozenset((
class RestrictedUnpickler(pickle.Unpickler):
generic_properties_module: Optional[object]
def __init__(self, *args: Any, **kwargs: Any) -> None:
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
self.options_module = importlib.import_module("Options")
self.net_utils_module = importlib.import_module("NetUtils")
self.generic_properties_module = None
def find_class(self, module: str, name: str) -> type:
if module == "builtins" and name in safe_builtins:
@@ -470,10 +468,6 @@ class RestrictedUnpickler(pickle.Unpickler):
"SlotType", "NetworkSlot", "HintStatus"}:
return getattr(self.net_utils_module, name)
# Options and Plando are unpickled by WebHost -> Generate
if module == "worlds.generic" and name == "PlandoItem":
if not self.generic_properties_module:
self.generic_properties_module = importlib.import_module("worlds.generic")
return getattr(self.generic_properties_module, name)
# pep 8 specifies that modules should have "all-lowercase names" (options, not Options)
if module.lower().endswith("options"):
if module == "Options":
@@ -1277,8 +1271,11 @@ def visualize_regions(
f.write("\n".join(uml))
class RepeatableChain:
def __init__(self, iterable: typing.Iterable):
_T_co = TypeVar("_T_co", covariant=True)
class RepeatableChain(Generic[_T_co]):
def __init__(self, iterable: Iterable[Collection[_T_co]]):
self.iterable = iterable
def __iter__(self):
@@ -1290,6 +1287,9 @@ class RepeatableChain:
def __len__(self):
return sum(len(iterable) for iterable in self.iterable)
def __contains__(self, o: object) -> bool:
return any(o in sub_iterable for sub_iterable in self.iterable)
def is_iterable_except_str(obj: object) -> TypeGuard[typing.Iterable[typing.Any]]:
""" `str` is `Iterable`, but that's not what we want """
+6 -1
View File
@@ -42,12 +42,15 @@ app.config["SELFLAUNCH"] = True # application process is in charge of launching
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
app.config["GAME_PORTS"] = ["49152-65535", 0]
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
app.config["JOB_THRESHOLD"] = 1
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
app.config["JOB_TIME"] = 600
# maximum time in seconds since last activity for a room to be hosted
app.config["MAX_ROOM_TIMEOUT"] = 259200
# minimum time in days since last activity for a room to be deleted. 0 to disable.
app.config["ROOM_AUTO_DELETE"] = 0
# memory limit for generator processes in bytes
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
@@ -71,7 +74,9 @@ CLI(app)
def to_python(value: str) -> uuid.UUID:
return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '=='))
if "=" in value or any(c.isspace() for c in value):
raise ValueError("Invalid UUID format")
return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '=' * (-len(value) % 4)))
def to_url(value: uuid.UUID) -> str:
+10 -4
View File
@@ -100,13 +100,18 @@ def init_generator(config: dict[str, Any]) -> None:
db.generate_mapping()
def cleanup():
"""delete unowned user-content"""
def cleanup(config: dict[str, Any]):
"""delete unowned or old user-content"""
auto_delete: int = config.get("ROOM_AUTO_DELETE", 0)
with db_session:
# >>> bool(uuid.UUID(int=0))
# True
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
if auto_delete > 0:
cutoff = utcnow() - timedelta(days=auto_delete)
rooms += Room.select(lambda room: room.last_activity < cutoff).delete(bulk=True)
seeds += Seed.select(lambda seed: not seed.rooms and seed.creation_time < cutoff).delete(bulk=True)
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
if rooms or seeds or slots:
@@ -118,7 +123,7 @@ def autohost(config: dict):
stop_event = _stop_event
try:
with Locker("autohost"):
cleanup()
cleanup(config)
hosters = []
for x in range(config["HOSTERS"]):
hoster = MultiworldInstance(config, x)
@@ -188,6 +193,7 @@ class MultiworldInstance():
self.cert = config["SELFLAUNCHCERT"]
self.key = config["SELFLAUNCHKEY"]
self.host = config["HOST_ADDRESS"]
self.game_ports = config["GAME_PORTS"]
self.rooms_to_start = multiprocessing.Queue()
self.rooms_shutting_down = multiprocessing.Queue()
self.name = f"MultiHoster{id}"
@@ -198,7 +204,7 @@ class MultiworldInstance():
process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.name, self.ponyconfig, get_static_server_data(),
self.cert, self.key, self.host,
self.cert, self.key, self.host, self.game_ports,
self.rooms_to_start, self.rooms_shutting_down),
name=self.name)
process.start()
+116 -19
View File
@@ -4,6 +4,7 @@ import asyncio
import collections
import datetime
import functools
import itertools
import logging
import multiprocessing
import pickle
@@ -13,7 +14,9 @@ import threading
import time
import typing
import sys
from collections.abc import Iterable
import psutil
import websockets
from pony.orm import commit, db_session, select
@@ -24,6 +27,7 @@ from MultiServer import (
server_per_message_deflate_factory,
)
from Utils import restricted_loads, cache_argsless
from .locker import Locker
from .models import Command, GameDataPackage, Room, db
@@ -76,12 +80,8 @@ class WebHostContext(Context):
self.tags = ["AP", "WebHost"]
def __del__(self):
try:
import psutil
from Utils import format_SI_prefix
self.logger.debug(f"Context destroyed, Mem: {format_SI_prefix(psutil.Process().memory_info().rss, 1024)}iB")
except ImportError:
self.logger.debug("Context destroyed")
from Utils import format_SI_prefix
self.logger.debug(f"Context destroyed, Mem: {format_SI_prefix(psutil.Process().memory_info().rss, 1024)}iB")
def _load_game_data(self):
for key, value in self.static_server_data.items():
@@ -115,7 +115,7 @@ class WebHostContext(Context):
if room.last_port:
self.port = room.last_port
else:
self.port = get_random_port()
self.port = 0
multidata = self.decompress(room.seed.multidata)
game_data_packages = {}
@@ -181,8 +181,97 @@ class WebHostContext(Context):
return d
def get_random_port():
return random.randint(49152, 65535)
class GameRangePorts(typing.NamedTuple):
valid_ports: list[int]
ephemeral_allowed: bool
class RandomPortSocketCreator:
""" Creates server sockets on random available ports from a configured range. """
_next_port_index: int
_used_ports_cache: tuple[frozenset[int], int] | None
_parsed_ports: GameRangePorts
def __init__(self, game_ports: Iterable[str | int]) -> None:
self._next_port_index = 0
self._used_ports_cache = None
self._parsed_ports = self._parse_game_ports(game_ports)
@staticmethod
def _parse_game_ports(game_ports: Iterable[str | int]) -> GameRangePorts:
""" Parse the game ports configuration into a structured format. """
valid_ports: list[int] = []
ephemeral_allowed = False
for item in game_ports:
if isinstance(item, str) and "-" in item:
start, end = map(int, item.split("-"))
x = range(start, end + 1)
valid_ports.extend(x)
elif int(item) == 0:
ephemeral_allowed = True
else:
valid_ports.append(int(item))
random.shuffle(valid_ports)
return GameRangePorts(valid_ports, ephemeral_allowed)
@staticmethod
def _try_conns_per_process(p: psutil.Process) -> Iterable[int]:
""" Get ports from a single process's connections. """
try:
return (c.laddr.port for c in p.net_connections("tcp4") if c.laddr)
except psutil.AccessDenied:
return ()
@staticmethod
def _get_active_net_connections() -> Iterable[int]:
""" Get all active TCP4 connections on the system. """
# Don't even try to check if system using AIX
if psutil.AIX:
return ()
try:
return (c.laddr.port for c in psutil.net_connections("tcp4") if c.laddr)
# raises AccessDenied when done on macOS
except psutil.AccessDenied:
# flatten the list of iterables
return itertools.chain.from_iterable(map(
RandomPortSocketCreator._try_conns_per_process,
psutil.process_iter(["net_connections"])
))
def _get_used_ports(self) -> frozenset[int]:
""" Get currently used ports with 90-second caching. """
t_hash = round(time.monotonic() / 90)
if self._used_ports_cache is None or self._used_ports_cache[1] != t_hash:
self._used_ports_cache = (frozenset(self._get_active_net_connections()), t_hash)
return self._used_ports_cache[0]
def create(self, host: str) -> socket.socket:
""" Create a server socket on an available port. """
valid_ports, ephemeral_allowed = self._parsed_ports
used_ports = self._get_used_ports()
next_index = self._next_port_index
for i, port in enumerate(itertools.chain(valid_ports[next_index:], valid_ports[:next_index])):
if port in used_ports:
continue
try:
res = socket.create_server((host, port))
next_index = (next_index + i + 1) % len(valid_ports)
self._next_port_index = next_index
return res
except OSError:
pass
if ephemeral_allowed:
return socket.create_server((host, 0))
raise OSError(98, "No available ports")
@cache_argsless
@@ -247,7 +336,8 @@ def tear_down_logging(room_id):
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
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, game_ports: Iterable[str | int],
rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
from setproctitle import setproctitle
setproctitle(name)
@@ -291,6 +381,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
gc.collect() # free intermediate objects used during setup
loop = asyncio.get_event_loop()
socket_creator = RandomPortSocketCreator(game_ports)
async def start_room(room_id):
with Locker(f"RoomLocker {room_id}"):
@@ -300,20 +391,26 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
ctx.load(room_id)
ctx.init_save()
assert ctx.server is None
try:
if ctx.port != 0:
try:
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx),
ctx.host,
ctx.port,
ssl=get_ssl_context(),
extensions=[server_per_message_deflate_factory],
)
await ctx.server
except OSError:
ctx.port = 0
if ctx.port == 0:
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx),
ctx.host,
ctx.port,
sock=socket_creator.create(ctx.host),
ssl=get_ssl_context(),
extensions=[server_per_message_deflate_factory],
)
await ctx.server
except OSError: # likely port in use
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=get_ssl_context())
await ctx.server
port = 0
for wssocket in ctx.server.ws_server.sockets:
socketname = wssocket.getsockname()
@@ -388,7 +485,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
def run(self):
while 1:
next_room = rooms_to_run.get(block=True, timeout=None)
next_room = rooms_to_run.get(block=True, timeout=None)
gc.collect()
task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
self._tasks.append(task)
+2 -1
View File
@@ -10,5 +10,6 @@ Flask-Cors==6.0.2
bokeh==3.8.2
markupsafe==3.0.3
setproctitle==1.3.7
mistune==3.2.0
mistune==3.2.1
docutils==0.22.4
psutil==7.2.2
+18 -4
View File
@@ -123,12 +123,26 @@ window.addEventListener('load', () => {
});
const addRangeRow = (optionName) => {
const inputQuery = `input[type=number][data-option="${optionName}"].range-option-value`;
const inputQuery = `input[data-option="${optionName}"]`;
const inputTarget = document.querySelector(inputQuery);
const newValue = inputTarget.value;
if (!/^-?\d+$/.test(newValue)) {
alert('Range values must be a positive or negative integer!');
return;
switch (inputTarget.type) {
case 'number':
if (!/^-?\d+$/.test(newValue)) {
alert('Range values must be a positive or negative integer!');
return;
}
break;
case 'text':
if (newValue === "") {
alert('Range values for text must be a non-empty string!');
return;
}
break;
default:
console.error(`Found unsupported input type: ${inputTarget.type}`);
return;
break;
}
inputTarget.value = '';
const tBody = document.querySelector(`table[data-option="${optionName}"].range-rows tbody`);
@@ -42,3 +42,7 @@
#games .page-controls button{
margin-left: 0.5rem;
}
#games .author-label{
margin-top: 0.5rem;
}
+7
View File
@@ -49,6 +49,13 @@
<details data-game="{{ game_name }}">
<summary class="h2">{{ game_name }}</summary>
{{ world.__doc__ | default("No description provided.", true) }}<br />
{% if "authors" in world.manifest %}
{% if world.manifest["authors"]|length == 1 %}
<p class="author-label">Author: {{ world.manifest["authors"][0] }}</p>
{% else %}
<p class="author-label">Authors: {{ world.manifest["authors"] | join(", ") }}</p>
{% endif %}
{% endif %}
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
{% if world.web.tutorials %}
<span class="link-spacer">|</span>
@@ -71,10 +71,10 @@
<div class="hint-text">
This option allows custom values only. Please enter your desired values below.
<div class="custom-value-wrapper">
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
<button type="button" data-option="{{ option_name }}">Add</button>
<input type="text" class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
<button type="button" class="add-range-option-button" data-option="{{ option_name }}">Add</button>
</div>
<table>
<table class="range-rows" data-option="{{ option_name }}">
<tbody>
{% if option.default %}
{{ RangeRow(option_name, option, option.default, option.default) }}
@@ -88,11 +88,11 @@
<div class="hint-text">
Custom values are also allowed for this option. To create one, enter it into the input box below.
<div class="custom-value-wrapper">
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
<button type="button" data-option="{{ option_name }}">Add</button>
<input type="text" class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
<button type="button" class="add-range-option-button" data-option="{{ option_name }}">Add</button>
</div>
</div>
<table>
<table class="range-rows" data-option="{{ option_name }}">
<tbody>
{% for id, name in option.name_lookup.items() %}
{% if name != 'random' %}
+9
View File
@@ -140,6 +140,15 @@ MDFloatLayout:
MDNavigationDrawerDivider:
MDBoxLayout:
orientation: "horizontal"
MDIconButton:
icon: "alert" if app.failed_worlds else ""
theme_text_color: "Custom"
text_color: "D23C42"
disabled: not app.failed_worlds
on_release: app.display_failed()
MDGridLayout:
id: main_layout
+3
View File
@@ -80,6 +80,9 @@
# Final Fantasy Mystic Quest
/worlds/ffmq/ @Alchav @wildham0
# Gauntlet Legends
/worlds/gl/ @jamesbrq
# Heretic
/worlds/heretic/ @Daivuk @KScl
+3 -2
View File
@@ -92,8 +92,9 @@ for setup).
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).
regarding the API can be found in the [world api doc](/docs/world%20api.md), and the [APQuest](/worlds/apquest/) world
is a complete world implementation that functions as an introduction to world development. Before publishing, make sure
to also check out [world maintainer.md](/docs/world%20maintainer.md).
### Hard Requirements
+2 -2
View File
@@ -35,8 +35,8 @@ There are also the following optional fields:
* `world_version` - an arbitrary version for that world in order to only load the newest valid world.
An APWorld without a world_version is always treated as older than one with a version
(**Must** use exactly the format `"major.minor.build"`, e.g. `1.0.0`)
* `authors` - a list of authors, to eventually be displayed in various user-facing places such as WebHost and
package managers. Should always be a list of strings.
* `authors` - a list of authors of the world. Displayed in user-facing places like the Supported Games page
on WebHost. Should always be a list of strings.
If the APWorld is packaged as an `.apworld` zip file, it also needs to have `version` and `compatible_version`,
which refer to the version of the APContainer packaging scheme defined in [Files.py](../worlds/Files.py).
-9
View File
@@ -77,15 +77,6 @@ Changes made to `docker-compose.yaml` can be applied by running `docker compose
It is possible to carry out these deployment steps on Windows under [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/install).
## Optional: A Link to the Past Enemizer
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
error if it is required.
Enemizer can be enabled on `x86_64` platform architecture, and is included in the image build process. Enemizer requires a version 1.0 Japanese "Zelda no Densetsu" `.sfc` rom file to be placed in the application directory:
`docker run archipelago -v "/path/to/zelda.sfc:/app/Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"`.
Enemizer is not currently available for `aarch64`.
## Optional: Git
Building the image requires a local copy of the ArchipelagoMW source code.
+162 -61
View File
@@ -1,6 +1,7 @@
# Rule Builder
This document describes the API provided for the rule builder. Using this API provides you with with a simple interface to define rules and the following advantages:
This document describes the API provided for the rule builder. Using this API provides you with with a simple interface
to define rules and the following advantages:
- Rule classes that avoid all the common pitfalls
- Logic optimization
@@ -12,13 +13,21 @@ This document describes the API provided for the rule builder. Using this API pr
The rule builder consists of 3 main parts:
1. The rules, which are classes that inherit from `rule_builder.rules.Rule`. These are what you write for your logic. They can be combined and take into account your world's options. There are a number of default rules listed below, and you can create as many custom rules for your world as needed. When assigning the rules to a location or entrance they must be resolved.
1. Resolved rules, which are classes that inherit from `rule_builder.rules.Rule.Resolved`. These are the optimized rules specific to one player that are set as a location or entrance's access rule. You generally shouldn't be directly creating these but they'll be created when assigning rules to locations or entrances. These are what power the human-readable logic explanations.
1. The optional rule builder world subclass `CachedRuleBuilderWorld`, which is a class your world can inherit from instead of `World`. It adds a caching system to the rules that will lazy evaluate and cache the result.
1. The rules, which are classes that inherit from `rule_builder.rules.Rule`. These are what you write for your logic.
They can be combined and take into account your world's options. There are a number of default rules listed below,
and you can create as many custom rules for your world as needed. When assigning the rules to a location or entrance
they must be resolved.
2. Resolved rules, which are classes that inherit from `rule_builder.rules.Rule.Resolved`. These are the optimized rules
specific to one player that are set as a location or entrance's access rule. You generally shouldn't be directly
creating these but they'll be created when assigning rules to locations or entrances. These are what power the
human-readable logic explanations.
3. The optional rule builder world subclass `CachedRuleBuilderWorld`, which is a class your world can inherit from
instead of `World`. It adds a caching system to the rules that will lazy evaluate and cache the result.
## Usage
For the most part the only difference in usage is instead of writing lambdas for your logic, you write static Rule objects. You then must use `world.set_rule` to assign the rule to a location or entrance.
For the most part the only difference in usage is instead of writing lambdas for your logic, you write static Rule
objects. You then must use `world.set_rule` to assign the rule to a location or entrance.
```python
# In your world's create_regions method
@@ -32,6 +41,7 @@ The rule builder comes with a number of rules by default:
- `False_`: Always returns false
- `And`: Checks that all child rules are true (also provided by `&` operator)
- `Or`: Checks that at least one child rule is true (also provided by `|` operator)
- `AtLeast`: Checks that at least some count of rules is true
- `Has`: Checks that the player has the given item with the given count (default 1)
- `HasAll`: Checks that the player has all given items
- `HasAny`: Checks that the player has at least one of the given items
@@ -40,18 +50,22 @@ The rule builder comes with a number of rules by default:
- `HasFromList`: Checks that the player has some number of given items
- `HasFromListUnique`: Checks that the player has some number of given items, ignoring duplicates of the same item
- `HasGroup`: Checks that the player has some number of items from a given item group
- `HasGroupUnique`: Checks that the player has some number of items from a given item group, ignoring duplicates of the same item
- `HasGroupUnique`: Checks that the player has some number of items from a given item group, ignoring duplicates of the
same item
- `CanReachLocation`: Checks that the player can logically reach the given location
- `CanReachRegion`: Checks that the player can logically reach the given region
- `CanReachEntrance`: Checks that the player can logically reach the given entrance
You can combine these rules together to describe the logic required for something. For example, to check if a player either has `Movement ability` or they have both `Key 1` and `Key 2`, you can do:
You can combine these rules together to describe the logic required for something. For example, to check if a player
either has `Movement ability` or they have both `Key 1` and `Key 2`, you can do:
```python
rule = Has("Movement ability") | HasAll("Key 1", "Key 2")
```
> ⚠️ Composing rules with the `and` and `or` keywords will not work. You must use the bitwise `&` and `|` operators. In order to catch mistakes, the rule builder will not let you do boolean operations. As a consequence, in order to check if a rule is defined you must use `if rule is not None`.
> ⚠️ Composing rules with the `and` and `or` keywords will not work. You must use the bitwise `&` and `|` operators. In
> order to catch mistakes, the rule builder will not let you do boolean operations. As a consequence, in order to check
> if a rule is defined you must use `if rule is not None`.
### Assigning rules
@@ -61,13 +75,16 @@ When assigning the rule you must use the `set_rule` helper to correctly resolve
self.set_rule(location_or_entrance, rule)
```
There is also a `create_entrance` helper that will resolve the rule, check if it's `False`, and if not create the entrance and set the rule. This allows you to skip creating entrances that will never be valid. You can also specify `force_creation=True` if you would like to create the entrance even if the rule is `False`.
There is also a `create_entrance` helper that will resolve the rule, check if it's `False`, and if not create the
entrance and set the rule. This allows you to skip creating entrances that will never be valid. You can also specify
`force_creation=True` if you would like to create the entrance even if the rule is `False`.
```python
self.create_entrance(from_region, to_region, rule)
```
> ⚠️ If you use a `CanReachLocation` rule on an entrance, you will either have to create the locations first, or specify the location's parent region name with the `parent_region_name` argument of `CanReachLocation`.
> ⚠️ If you use a `CanReachLocation` rule on an entrance, you will either have to create the locations first, or specify
> the location's parent region name with the `parent_region_name` argument of `CanReachLocation`.
You can also set a rule for your world's completion condition:
@@ -77,21 +94,42 @@ self.set_completion_rule(rule)
### Restricting options
Every rule allows you to specify which options it's applicable for. You can provide the argument `options` which is an iterable of `OptionFilter` instances. Rules that pass the options check will be resolved as normal, and those that fail will be resolved as `False`.
Every rule allows you to specify which options it's applicable for. You can provide the argument `options` which is an
iterable of `OptionFilter` instances. When resolved, if no filters are provided or all of them pass then the rule will
resolve as normal. Otherwise, the rule will be replaced with `True` or `False` depending on what `filtered_resolution`
is set to, which defaults to `False`.
If you want a comparison that isn't equals, you can specify with the `operator` argument. The following operators are allowed:
```python
rule1 = Has(
"Fast Travel Spell",
options=[OptionFilter(RandoFastTravel, RandoFastTravel.option_true)],
)
rule2 = Has(
"Starting Party Member",
options=[OptionFilter(RandoParty, 1)], # option attributes are suggested but any value works
filtered_resolution=True,
)
```
- `eq`: `==`
- `ne`: `!=`
- `gt`: `>`
- `lt`: `<`
- `ge`: `>=`
- `le`: `<=`
- `contains`: `in`
If you want a comparison that isn't equals, you can specify with the `operator` argument. The following operators are
allowed:
By default rules that are excluded by their options will default to `False`. If you want to default to `True` instead, you can specify `filtered_resolution=True` on your rule.
- `eq`: `option_value == filter_value`
- `ne`: `option_value != filter_value`
- `gt`: `option_value > filter_value`
- `lt`: `option_value < filter_value`
- `ge`: `option_value >= filter_value`
- `le`: `option_value <= filter_value`
- `in`: `option_value in filter_value`
- `contains`: `filter_value in option_value` (note reversed operands)
To check if the player can reach a switch, or if they've received the switch item if switches are randomized:
```python
rule1 = Has("Movement Ability", options=[OptionFilter(SkipsLevel, SkipsLevel.option_hard, operator="lt")])
rule2 = Has("Item", options=[OptionFilter(ChoiceOption, [1, 5], operator="in")])
```
To check if the player has received the switch item if switches are randomized, or if they can reach the switch when not
randomized:
```python
rule = (
@@ -115,12 +153,12 @@ If you would like to provide option filters when reusing or composing rules, you
common_rule = Has("A") | HasAny("B", "C")
...
rule = (
Filtered(common_rule, options=[OptionFilter(Opt, 0)]),
| Filtered(Has("X") | CanReachRegion("Y"), options=[OptionFilter(Opt, 1)]),
Filtered(common_rule, options=[OptionFilter(Opt, 0)])
| Filtered(Has("X") | CanReachRegion("Y"), options=[OptionFilter(Opt, 1)])
)
```
You can also use the & and | operators to apply options to rules:
For convenience, you can also use the `&` and `|` operators to apply options to rules:
```python
common_rule = Has("A")
@@ -129,14 +167,22 @@ common_rule_only_on_easy = common_rule & easy_filter
common_rule_skipped_on_easy = common_rule | easy_filter
```
Combining the above, you can easily bypass a requirement based on option choices:
```python
rule = Has("Some Upgrade") | OptionFilter(CombatDifficulty, CombatDifficulty.option_medium, operator="ge")
```
### Field resolvers
When creating rules you may sometimes need to set a field to a value that depends on the world instance. You can use a `FieldResolver` to define how to populate that field when the rule is being resolved.
When creating rules you may sometimes need to set a field to a value that depends on the world instance. You can use a
`FieldResolver` to define how to populate that field when the rule is being resolved.
There are two build-in field resolvers:
- `FromOption`: Resolves to the value of the given option
- `FromWorldAttr`: Resolves to the value of the given world instance attribute, can specify a dotted path `a.b.c` to get a nested attribute or dict item
- `FromWorldAttr`: Resolves to the value of the given world instance attribute, can specify a dotted path `a.b.c` to get
a nested attribute or dict item
```python
world.options.mcguffin_count = 5
@@ -148,7 +194,8 @@ rule = (
# Results in Has("A", count=5) | HasGroup("Important items", count=99)
```
You can define your own resolvers by creating a class that inherits from `FieldResolver`, provides your game name, and implements a `resolve` function:
You can define your own resolvers by creating a class that inherits from `FieldResolver`, provides your game name, and
implements a `resolve` function:
```python
@dataclasses.dataclass(frozen=True)
@@ -163,24 +210,30 @@ class FromCustomResolution(FieldResolver, game="MyGame"):
rule = Has("Combat Level", count=FromCustomResolution("combat"))
```
If you want to support rule serialization and your resolver contains non-serializable properties you may need to override `to_dict` or `from_dict`.
If you want to support rule serialization and your resolver contains non-serializable properties you may need to
override `to_dict` or `from_dict`.
## Enabling caching
The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your rules.
The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your
rules.
```python
class MyWorld(CachedRuleBuilderWorld):
game = "My Game"
```
If your world's logic is very simple and you don't have many nested rules, the caching system may have more overhead cost than time it saves. You'll have to benchmark your own world to see if it should be enabled or not.
If your world's logic is very simple and you don't have many nested rules, the caching system may have more overhead
cost than time it saves. You'll have to benchmark your own world to see if it should be enabled or not.
### Item name mapping
If you have multiple real items that map to a single logic item, add a `item_mapping` class dict to your world that maps actual item names to real item names so the cache system knows what to invalidate.
If you have multiple real items that map to a single logic item, add a `item_mapping` class dict to your world that maps
actual item names to real item names so the cache system knows what to invalidate.
For example, if you have multiple `Currency x<num>` items on locations, but your rules only check a singular logical `Currency` item, eg `Has("Currency", 1000)`, you'll want to map each numerical currency item to the single logical `Currency`.
For example, if you have multiple `Currency x<num>` items on locations, but your rules only check a singular logical
`Currency` item, eg `Has("Currency", 1000)`, you'll want to map each numerical currency item to the single logical
`Currency`.
```python
class MyWorld(CachedRuleBuilderWorld):
@@ -194,9 +247,13 @@ class MyWorld(CachedRuleBuilderWorld):
## Defining custom rules
You can create a custom rule by creating a class that inherits from `Rule` or any of the default rules. You must provide the game name as an argument to the class. It's recommended to use the `@dataclass` decorator to reduce boilerplate, and to also provide your world as a type argument to add correct type checking to the `_instantiate` method.
You can create a custom rule by creating a class that inherits from `Rule` or any of the default rules. You must provide
the game name as an argument to the class. It's recommended to use the `@dataclass` decorator to reduce boilerplate, and
to also provide your world as a type argument to add correct type checking to the `_instantiate` method.
You must provide or inherit a `Resolved` child class that defines an `_evaluate` method. This class will automatically be converted into a frozen `dataclass`. If your world has caching enabled you may need to define one or more dependencies functions as outlined below.
You must provide or inherit a `Resolved` child class that defines an `_evaluate` method. This class will automatically
be converted into a frozen `dataclass`. If your world has caching enabled you may need to define one or more
dependencies functions as outlined below.
To add a rule that checks if the user has enough mcguffins to goal, with a randomized requirement:
@@ -245,7 +302,10 @@ class ComplicatedFilter(Rule["MyWorld"], game="My Game"):
### Item dependencies
If your world inherits from `CachedRuleBuilderWorld` and there are items that when collected will affect the result of your rule evaluation, it must define an `item_dependencies` function that returns a mapping of the item name to the id of your rule. These dependencies will be combined to inform the caching system. It may be worthwhile to define this function even when caching is disabled as more things may use it in the future.
If your world inherits from `CachedRuleBuilderWorld` and there are items that when collected will affect the result of
your rule evaluation, it must define an `item_dependencies` function that returns a mapping of the item name to the id
of your rule. These dependencies will be combined to inform the caching system. It may be worthwhile to define this
function even when caching is disabled as more things may use it in the future.
```python
@dataclasses.dataclass()
@@ -262,7 +322,10 @@ All of the default `Has*` rules define this function already.
### Region dependencies
If your custom rule references other regions, it must define a `region_dependencies` function that returns a mapping of region names to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
If your custom rule references other regions, it must define a `region_dependencies` function that returns a mapping of
region names to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These
dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the
caching system if applicable.
```python
@dataclasses.dataclass()
@@ -279,7 +342,10 @@ The default `CanReachLocation`, `CanReachRegion`, and `CanReachEntrance` rules d
### Location dependencies
If your custom rule references other locations, it must define a `location_dependencies` function that returns a mapping of the location name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
If your custom rule references other locations, it must define a `location_dependencies` function that returns a mapping
of the location name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These
dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the
caching system if applicable.
```python
@dataclasses.dataclass()
@@ -296,7 +362,10 @@ The default `CanReachLocation` rule defines this function already.
### Entrance dependencies
If your custom rule references other entrances, it must define a `entrance_dependencies` function that returns a mapping of the entrance name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
If your custom rule references other entrances, it must define a `entrance_dependencies` function that returns a mapping
of the entrance name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These
dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the
caching system if applicable.
```python
@dataclasses.dataclass()
@@ -313,9 +382,13 @@ The default `CanReachEntrance` rule defines this function already.
### Rule explanations
Resolved rules have a default implementation for `explain_json` and `explain_str` functions. The former optionally accepts a `CollectionState` and returns a list of `JSONMessagePart` appropriate for `print_json` in a client. It will display a human-readable message that explains what the rule requires. The latter is similar but returns a string. It is useful when debugging. There is also a `__str__` method defined to check what a rule is without a state.
Resolved rules have a default implementation for `explain_json` and `explain_str` functions. The former optionally
accepts a `CollectionState` and returns a list of `JSONMessagePart` appropriate for `print_json` in a client. It will
display a human-readable message that explains what the rule requires. The latter is similar but returns a string. It is
useful when debugging. There is also a `__str__` method defined to check what a rule is without a state.
To implement a custom message with a custom rule, override the `explain_json` and/or `explain_str` method on your `Resolved` class:
To implement a custom message with a custom rule, override the `explain_json` and/or `explain_str` method on your
`Resolved` class:
```python
class MyRule(Rule, game="My Game"):
@@ -352,22 +425,35 @@ class MyRule(Rule, game="My Game"):
### Cache control
By default your custom rule will work through the cache system as any other rule if caching is enabled. There are two class attributes on the `Resolved` class you can override to change this behavior.
By default your custom rule will work through the cache system as any other rule if caching is enabled. There are two
class attributes on the `Resolved` class you can override to change this behavior.
- `force_recalculate`: Setting this to `True` will cause your custom rule to skip going through the caching system and always recalculate when being evaluated. When a rule with this flag enabled is composed with `And` or `Or` it will cause any parent rules to always force recalculate as well. Use this flag when it's difficult to determine when your rule should be marked as stale.
- `skip_cache`: Setting this to `True` will also cause your custom rule to skip going through the caching system when being evaluated. However, it will **not** affect any other rules when composed with `And` or `Or`, so it must still define its `*_dependencies` functions as required. Use this flag when the evaluation of this rule is trivial and the overhead of the caching system will slow it down.
- `force_recalculate`: Setting this to `True` will cause your custom rule to skip going through the caching system and
always recalculate when being evaluated. When a rule with this flag enabled is composed with `And` or `Or` it will
cause any parent rules to always force recalculate as well. Use this flag when it's difficult to determine when your
rule should be marked as stale.
- `skip_cache`: Setting this to `True` will also cause your custom rule to skip going through the caching system when
being evaluated. However, it will **not** affect any other rules when composed with `And` or `Or`, so it must still
define its `*_dependencies` functions as required. Use this flag when the evaluation of this rule is trivial and the
overhead of the caching system will slow it down.
### Caveats
- Ensure you are passing `caching_enabled=True` in your `_instantiate` function when creating resolved rule instances if your world has opted into caching.
- Ensure you are passing `caching_enabled=True` in your `_instantiate` function when creating resolved rule instances if
your world has opted into caching.
- Resolved rules are forced to be frozen dataclasses. They and all their attributes must be immutable and hashable.
- If your rule creates child rules ensure they are being resolved through the world rather than creating `Resolved` instances directly.
- If your rule creates child rules ensure they are being resolved through the world rather than creating `Resolved`
instances directly.
## Serialization
The rule builder is intended to be written first in Python for optimization and type safety. To facilitate exporting the rules to a client or tracker, rules have a `to_dict` method that returns a JSON-compatible dict. Since the location and entrance logic structure varies greatly from world to world, the actual JSON dumping is left up to the world dev.
The rule builder is intended to be written first in Python for optimization and type safety. To facilitate exporting the
rules to a client or tracker, rules have a `to_dict` method that returns a JSON-compatible dict. Since the location and
entrance logic structure varies greatly from world to world, the actual JSON dumping is left up to the world dev.
The dict contains a `rule` key with the name of the rule, an `options` key with the rule's list of option filters, and an `args` key that contains any other arguments the individual rule has. For example, this is what a simple `Has` rule would look like:
The dict contains a `rule` key with the name of the rule, an `options` key with the rule's list of option filters, and
an `args` key that contains any other arguments the individual rule has. For example, this is what a simple `Has` rule
would look like:
```python
{
@@ -380,7 +466,8 @@ The dict contains a `rule` key with the name of the rule, an `options` key with
}
```
For `And` and `Or` rules, instead of an `args` key, they have a `children` key containing a list of their child rules in the same serializable format:
For `And` and `Or` rules, instead of an `args` key, they have a `children` key containing a list of their child rules in
the same serializable format:
```python
{
@@ -464,7 +551,8 @@ class BasicLogicRule(Rule, game="My Game"):
}
```
If your logic has been done in custom JSON first, you can define a `from_dict` class method on your rules to parse it correctly:
If your logic has been done in custom JSON first, you can define a `from_dict` class method on your rules to parse it
correctly:
```python
class BasicLogicRule(Rule, game="My Game"):
@@ -485,10 +573,14 @@ These are properties and helpers that are available to you in your world.
#### Methods
- `rule_from_dict(data)`: Create a rule instance from a deserialized dict representation
- `register_rule_builder_dependencies()`: Register all rules that depend on location or entrance access with the inherited dependencies, gets called automatically after set_rules
- `set_rule(spot: Location | Entrance, rule: Rule)`: Resolve a rule, register its dependencies, and set it on the given location or entrance
- `register_rule_builder_dependencies()`: Register all rules that depend on location or entrance access with the
inherited dependencies, gets called automatically after set_rules
- `set_rule(spot: Location | Entrance, rule: Rule)`: Resolve a rule, register its dependencies, and set it on the given
location or entrance
- `set_completion_rule(rule: Rule)`: Sets the completion condition for this world
- `create_entrance(from_region: Region, to_region: Region, rule: Rule | None, name: str | None = None, force_creation: bool = False)`: Attempt to create an entrance from `from_region` to `to_region`, skipping creation if `rule` is defined and evaluates to `False_()` unless force_creation is `True`
- `create_entrance(from_region: Region, to_region: Region, rule: Rule | None, name: str | None = None, force_creation: bool = False)`:
Attempt to create an entrance from `from_region` to `to_region`, skipping creation if `rule` is defined and evaluates
to `False_()` unless force_creation is `True`
#### CachedRuleBuilderWorld Properties
@@ -501,18 +593,27 @@ The following property is only available when inheriting from `CachedRuleBuilder
These are properties and helpers that you can use or override for custom rules.
- `_instantiate(world: World)`: Create a new resolved rule instance, override for custom rules as required
- `to_dict()`: Create a JSON-compatible dict representation of this rule, override if you want to customize your rule's serialization
- `from_dict(data, world_cls: type[World])`: Return a new rule instance from a deserialized representation, override if you've overridden `to_dict`
- `to_dict()`: Create a JSON-compatible dict representation of this rule, override if you want to customize your rule's
serialization
- `from_dict(data, world_cls: type[World])`: Return a new rule instance from a deserialized representation, override if
you've overridden `to_dict`
- `__str__()`: Basic string representation of a rule, useful for debugging
#### Resolved rule API
- `player: int`: The slot this rule is resolved for
- `_evaluate(state: CollectionState)`: Evaluate this rule against the given state, override this to define the logic for this rule
- `item_dependencies()`: A mapping of item name to set of ids, override this if your custom rule depends on item collection
- `region_dependencies()`: A mapping of region name to set of ids, override this if your custom rule depends on reaching regions
- `location_dependencies()`: A mapping of location name to set of ids, override this if your custom rule depends on reaching locations
- `entrance_dependencies()`: A mapping of entrance name to set of ids, override this if your custom rule depends on reaching entrances
- `explain_json(state: CollectionState | None = None)`: Return a list of printJSON messages describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules
- `explain_str(state: CollectionState | None = None)`: Return a string describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules, more useful for debugging
- `_evaluate(state: CollectionState)`: Evaluate this rule against the given state, override this to define the logic for
this rule
- `item_dependencies()`: A mapping of item name to set of ids, override this if your custom rule depends on item
collection
- `region_dependencies()`: A mapping of region name to set of ids, override this if your custom rule depends on reaching
regions
- `location_dependencies()`: A mapping of location name to set of ids, override this if your custom rule depends on
reaching locations
- `entrance_dependencies()`: A mapping of entrance name to set of ids, override this if your custom rule depends on
reaching entrances
- `explain_json(state: CollectionState | None = None)`: Return a list of printJSON messages describing this rule's logic
(and if state is defined its evaluation) in a human readable way, override to explain custom rules
- `explain_str(state: CollectionState | None = None)`: Return a string describing this rule's logic (and if state is
defined its evaluation) in a human readable way, override to explain custom rules, more useful for debugging
- `__str__()`: A string describing this rule's logic without its evaluation, override to explain custom rules
-10
View File
@@ -78,16 +78,6 @@ first generate the binary distribution and then run `python setup.py bdist_appim
put an `appimagetool` into the directory you run the command from, rename it to `appimagetool` and make it executable.
## Optional: A Link to the Past Enemizer
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
error if it is required.
You can get the latest Enemizer release at [Enemizer Github releases](https://github.com/Ijwu/Enemizer/releases).
It should be dropped as "EnemizerCLI" into the root folder of the project. Alternatively, you can point the Enemizer
setting in host.yaml at your Enemizer executable.
## Optional: SNI
[SNI](https://github.com/alttpo/sni/blob/main/README.md) is required to use SNIClient. If not integrated into the project, it has to be started manually.
+1 -1
View File
@@ -131,7 +131,7 @@ Unless you configured PyCharm to use pytest as a test runner, you may get import
edit the run configuration, and set the working directory to the Archipelago directory which contains all the project files.
If you only want to run your world's defined tests, repeat the steps for the test directory within your world.
Your working directory should be the directory of your world in the worlds directory and the script should be the
Your working directory should be the root Archipelago directory and the script should be the
tests folder within your world.
You can also find the 'Archipelago Unittests' as an option in the dropdown at the top of the window
+6
View File
@@ -17,6 +17,12 @@
# Web hosting port
#PORT: 80
# Ports used for game hosting. Values can be specific ports, port ranges or both. Default is: [49152-65535, 0]
# Zero means it will use a random free port if there is no free port in the ranges specified
# Examples of valid values: [40000-41000, 49152-65535]
# If ports within the range(s) are already in use, the WebHost will fallback to the default [49152-65535, 0] range.
#GAME_PORTS: [49152-65535, 0]
# Place where uploads go.
#UPLOAD_FOLDER: uploads
+5
View File
@@ -327,6 +327,11 @@ reject the placement of an item there.
### Events (or "generation-only items/locations")
> **Warning:** If you're trying to tell the Archipelago server that the player has achieved their goal, you want to send
a [StatusUpdate packet](network%20protocol.md#statusupdate), or however [your client library](network%20protocol.md)
wraps it. Despite the popularity of "victory events" during generation, events have nothing to do with how goals are
triggered during gameplay.
An event item or location is one that only exists during multiworld generation; the server is never made aware of them.
Event locations can never be checked by the player, and event items cannot be received during play.
+7 -3
View File
@@ -57,9 +57,8 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
[Files]
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs;
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs;
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
[Icons]
@@ -83,7 +82,6 @@ Type: files; Name: "{app}\*.exe"
Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
Type: files; Name: "{app}\data\lua\connector_ff1.lua"
Type: filesandordirs; Name: "{app}\SNI\lua*"
Type: filesandordirs; Name: "{app}\EnemizerCLI*"
#include "installdelete.iss"
[Registry]
@@ -208,11 +206,17 @@ Root: HKCR; Subkey: "{#MyAppName}ebpatch"; ValueData: "Archi
Root: HKCR; Subkey: "{#MyAppName}ebpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ebpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apmm3"; ValueData: "{#MyAppName}mm3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mm3patch"; ValueData: "Archipelago Mega Man 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mm3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mm3patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apgl"; ValueData: "{#MyAppName}glpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}glpatch"; ValueData: "Archipelago Gauntlet Legends Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}glpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}glpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
+10 -9
View File
@@ -363,15 +363,16 @@ class ServerLabel(HoverBehavior, MDTooltip, MDBoxLayout):
text += "\nPermissions:"
for permission_name, permission_data in ctx.permissions.items():
text += f"\n {permission_name}: {permission_data}"
if ctx.hint_cost is not None and ctx.total_locations:
min_cost = int(ctx.server_version >= (0, 3, 9))
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
f"For you this means every " \
f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
"location checks." \
f"\nYou currently have {ctx.hint_points} points."
elif ctx.hint_cost == 0:
text += "\n!hint is free to use."
if ctx.total_locations and ctx.hint_cost is not None:
if ctx.hint_cost == 0:
text += "\n!hint is free to use."
else:
min_cost = int(ctx.server_version >= (0, 3, 9))
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
f"For you this means every " \
f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
"location checks." \
f"\nYou currently have {ctx.hint_points} points."
if ctx.stored_data and "_read_race_mode" in ctx.stored_data:
text += "\nRace mode is enabled." \
if ctx.stored_data["_read_race_mode"] else "\nRace mode is disabled."
+156 -14
View File
@@ -36,7 +36,7 @@ def _create_hash_fn(resolved_rule_cls: "CustomRuleRegister") -> Callable[..., in
class CustomRuleRegister(type):
"""A metaclass to contain world custom rules and automatically convert resolved rules to frozen dataclasses"""
resolved_rules: ClassVar[dict[int, "Rule.Resolved"]] = {}
resolved_rules: ClassVar[dict["Rule.Resolved", "Rule.Resolved"]] = {}
"""A cached of resolved rules to turn each unique one into a singleton"""
custom_rules: ClassVar[dict[str, dict[str, type["Rule[Any]"]]]] = {}
@@ -64,10 +64,9 @@ class CustomRuleRegister(type):
@override
def __call__(cls, *args: Any, **kwds: Any) -> Any:
rule = super().__call__(*args, **kwds)
rule_hash = hash(rule)
if rule_hash in cls.resolved_rules:
return cls.resolved_rules[rule_hash]
cls.resolved_rules[rule_hash] = rule
if rule in cls.resolved_rules:
return cls.resolved_rules[rule]
cls.resolved_rules[rule] = rule
return rule
@classmethod
@@ -426,13 +425,142 @@ class NestedRule(Rule[TWorld], game="Archipelago"):
return combined_deps
class AtLeast(NestedRule[TWorld], game="Archipelago"):
"""A rule that returns true when at least N child rules evaluate as true"""
count: int | FieldResolver
def __init__(
self,
count: int | FieldResolver,
*children: Rule[TWorld],
options: Iterable[OptionFilter] = (),
filtered_resolution: bool = False,
) -> None:
super().__init__(*children, options=options, filtered_resolution=filtered_resolution)
self.count = count
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
count = resolve_field(self.count, world, int)
if count == 0:
return True_().resolve(world)
children_to_process = [c.resolve(world) for c in self.children]
return AtLeast.from_resolved(count, world, children_to_process)
@classmethod
def from_resolved(cls, count: int, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
clauses: list[Rule.Resolved] = []
while children_to_process:
child = children_to_process.pop(0)
if child.always_true:
if count == 1:
return child
count -= 1
continue
if child.always_false:
# falses can be ignored
continue
clauses.append(child)
if len(clauses) < count:
return False_().resolve(world)
if count == 1:
# Switch to Or which has more optimized handling
return Or.from_resolved(world, clauses)
if count == len(clauses):
# Switch to And which has more optimized handling
return And.from_resolved(world, clauses)
return AtLeast.Resolved(
tuple(clauses),
count=count,
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
@override
def to_dict(self) -> dict[str, Any]:
output = super().to_dict()
count = self.count
output["count"] = count.to_dict() if isinstance(count, FieldResolver) else count
return output
@override
@classmethod
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
args = cls._parse_field_resolvers(data, world_cls.game)
options = OptionFilter.multiple_from_dict(data.get("options", ()))
children = [world_cls.rule_from_dict(c) for c in data.get("children", ())]
return cls(
args.pop("count"),
*children,
options=options,
filtered_resolution=data.get("filtered_resolution", False),
)
class Resolved(NestedRule.Resolved):
count: int
@override
def _evaluate(self, state: CollectionState) -> bool:
count = self.count
for rule in self.children:
if rule(state):
if count == 1:
return True
count -= 1
return False
@override
def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]:
messages: list[JSONMessagePart] = []
if state is None:
messages = [
{"type": "text", "text": "At least "},
{"type": "color", "color": "cyan", "text": str(self.count)},
{"type": "text", "text": " of ("},
]
else:
satisfied_count = sum(1 if child(state) else 0 for child in self.children)
messages = [
{"type": "text", "text": "At least "},
{"type": "color", "color": "cyan", "text": f"{satisfied_count}/{self.count}"},
{"type": "text", "text": " of ("},
]
for i, child in enumerate(self.children):
if i > 0:
messages.append({"type": "text", "text": ", "})
messages.extend(child.explain_json(state))
messages.append({"type": "text", "text": ")"})
return messages
@override
def explain_str(self, state: CollectionState | None = None) -> str:
clauses = ", ".join([c.explain_str(state) for c in self.children])
if state is None:
return f"At least {self.count} of ({clauses})"
satisfied_count = sum(1 if child(state) else 0 for child in self.children)
return f"At least {satisfied_count}/{self.count} of ({clauses})"
@override
def __str__(self) -> str:
clauses = ", ".join([str(c) for c in self.children])
return f"At least {self.count} of ({clauses})"
@dataclasses.dataclass(init=False)
class And(NestedRule[TWorld], game="Archipelago"):
"""A rule that only returns true when all child rules evaluate as true"""
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
children_to_process = [c.resolve(world) for c in self.children]
return And.from_resolved(world, [c.resolve(world) for c in self.children])
@classmethod
def from_resolved(cls, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
clauses: list[Rule.Resolved] = []
items: dict[str, int] = {}
true_rule: Rule.Resolved | None = None
@@ -465,7 +593,7 @@ class And(NestedRule[TWorld], game="Archipelago"):
clauses.append(child)
if not clauses and not items:
return true_rule or False_().resolve(world)
return true_rule or True_().resolve(world)
if len(items) == 1:
item, count = next(iter(items.items()))
@@ -519,7 +647,10 @@ class Or(NestedRule[TWorld], game="Archipelago"):
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
children_to_process = [c.resolve(world) for c in self.children]
return Or.from_resolved(world, [c.resolve(world) for c in self.children])
@classmethod
def from_resolved(cls, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
clauses: list[Rule.Resolved] = []
items: dict[str, int] = {}
@@ -1272,14 +1403,16 @@ class HasFromList(Rule[TWorld], game="Archipelago"):
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
count = resolve_field(self.count, world, int)
if count <= 0:
return True_().resolve(world)
if len(self.item_names) == 0:
# match state.has_from_list
return False_().resolve(world)
if len(self.item_names) == 1:
return Has(self.item_names[0], self.count).resolve(world)
return self.Resolved(
self.item_names,
count=resolve_field(self.count, world, int),
count=count,
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
@@ -1407,8 +1540,9 @@ class HasFromListUnique(Rule[TWorld], game="Archipelago"):
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
count = resolve_field(self.count, world, int)
if len(self.item_names) == 0 or len(self.item_names) < count:
# match state.has_from_list_unique
if count <= 0:
return True_().resolve(world)
if len(self.item_names) < count:
return False_().resolve(world)
if len(self.item_names) == 1:
return Has(self.item_names[0]).resolve(world)
@@ -1526,11 +1660,14 @@ class HasGroup(Rule[TWorld], game="Archipelago"):
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
count = resolve_field(self.count, world, int)
if count <= 0:
return True_().resolve(world)
item_names = tuple(sorted(world.item_name_groups[self.item_name_group]))
return self.Resolved(
self.item_name_group,
item_names,
count=resolve_field(self.count, world, int),
count=count,
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
@@ -1600,11 +1737,16 @@ class HasGroupUnique(Rule[TWorld], game="Archipelago"):
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
count = resolve_field(self.count, world, int)
if count <= 0:
return True_().resolve(world)
item_names = tuple(sorted(world.item_name_groups[self.item_name_group]))
if len(item_names) < count:
return False_().resolve(world)
return self.Resolved(
self.item_name_group,
item_names,
count=resolve_field(self.count, world, int),
count=count,
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
+9 -5
View File
@@ -98,6 +98,8 @@ class Group:
self._changed = True
attr = new
# resolve the path immediately when accessing it
if attr.exists():
attr.__class__.validate(attr.resolve())
return attr.__class__(attr.resolve())
return attr
@@ -633,10 +635,6 @@ class ServerOptions(Group):
class GeneratorOptions(Group):
"""Options for Generation"""
class EnemizerPath(LocalFilePath):
"""Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases"""
is_exe = True
class PlayerFilesPath(OptionalUserFolderPath):
"""Folder from which the player yaml files are pulled from"""
# created on demand, so marked as optional
@@ -644,6 +642,12 @@ class GeneratorOptions(Group):
class Players(int):
"""amount of players, 0 to infer from player files"""
class AllowQuantity(Bool):
"""
allow players to set an individual quantity for their yaml settings
with 'false' any amounts from the players will be ignored and set to 1
"""
class WeightsFilePath(str):
"""
general weights file, within the stated player_files_path location
@@ -687,9 +691,9 @@ class GeneratorOptions(Group):
start_inventory -> Move remaining items to start_inventory, generate additional filler items to fill locations.
"""
enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows
player_files_path: PlayerFilesPath = PlayerFilesPath("Players")
players: Players = Players(0)
allow_quantity: AllowQuantity | bool = False
weights_file_path: WeightsFilePath = WeightsFilePath("weights.yaml")
meta_file_path: MetaFilePath = MetaFilePath("meta.yaml")
spoiler: Spoiler = Spoiler(3)
+2 -3
View File
@@ -201,7 +201,7 @@ if is_windows:
icon=resolve_icon(c.icon),
))
extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
extra_data = ["LICENSE", "data", "SNI"]
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
@@ -456,9 +456,8 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
for world_directory in folders_to_remove)
else:
# make sure extra programs are executable
enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core'
sni_exe = self.buildfolder / 'SNI/sni'
extra_exes = (enemizer_exe, sni_exe)
extra_exes = (sni_exe,)
for extra_exe in extra_exes:
if extra_exe.is_file():
extra_exe.chmod(0o755)
+1 -1
View File
@@ -54,7 +54,7 @@ class TestImplemented(unittest.TestCase):
def test_no_failed_world_loads(self):
if failed_world_loads:
self.fail(f"The following worlds failed to load: {failed_world_loads}")
self.fail(f"The following worlds failed to load: {failed_world_loads.keys()}")
def test_prefill_items(self):
"""Test that every world can reach every location from allstate before pre_fill."""
+145 -29
View File
@@ -12,6 +12,7 @@ from rule_builder.field_resolvers import FieldResolver, FromOption, FromWorldAtt
from rule_builder.options import Operator, OptionFilter
from rule_builder.rules import (
And,
AtLeast,
CanReachEntrance,
CanReachLocation,
CanReachRegion,
@@ -158,6 +159,14 @@ class CachedRuleBuilderTestCase(RuleBuilderTestCase):
@classvar_matrix(
rules=(
(
And(),
True_.Resolved(player=1)
),
(
Or(),
False_.Resolved(player=1)
),
(
And(Has("A", 1), Has("A", 2)),
Has.Resolved("A", 2, player=1),
@@ -250,6 +259,40 @@ class CachedRuleBuilderTestCase(RuleBuilderTestCase):
Or(HasAnyCount({"A": 1, "B": 2}), HasAnyCount({"A": 2, "B": 2})),
HasAnyCount.Resolved((("A", 1), ("B", 2)), player=1),
),
(
AtLeast(0, Has("A")),
True_.Resolved(player=1),
),
(
AtLeast(3, True_(), Has("A"), Has("B"), Has("C")),
AtLeast.Resolved(
(Has.Resolved("A", player=1), Has.Resolved("B", player=1), Has.Resolved("C", player=1)), 2, player=1
),
),
(
AtLeast(2, False_(), Has("A"), Has("B"), Has("C")),
AtLeast.Resolved(
(Has.Resolved("A", player=1), Has.Resolved("B", player=1), Has.Resolved("C", player=1)), 2, player=1
),
),
(
AtLeast(2, True_(), True_(), Has("A")),
True_.Resolved(player=1),
),
(
AtLeast(3, Has("A"), Has("B")),
False_.Resolved(player=1),
),
(
# This test will fail when Or(Rule, Rule) will be optimized to Rule
AtLeast(1, Rule(), Rule()),
Or.Resolved((Rule.Resolved(player=1), Rule.Resolved(player=1)), player=1),
),
(
# This test will fail when And(Rule, Rule) will be optimized to Rule
AtLeast(2, Rule(), Rule()),
And.Resolved((Rule.Resolved(player=1), Rule.Resolved(player=1)), player=1),
),
)
)
class TestSimplify(RuleBuilderTestCase):
@@ -416,6 +459,15 @@ class TestHashes(RuleBuilderTestCase):
rule2 = HasAll("2", "2", "2", "1")
self.assertEqual(hash(rule1.resolve(world)), hash(rule2.resolve(world)))
def test_hash_collision(self) -> None:
multiworld = setup_solo_multiworld(self.world_cls, steps=("generate_early",), seed=0)
world = multiworld.worlds[1]
rule1 = Has("A", count=1).resolve(world)
rule2 = Has("A", count=1 << 61).resolve(world)
self.assertEqual(hash(rule1), hash(rule2))
self.assertNotEqual(rule1, rule2)
self.assertNotEqual(id(rule1), id(rule2))
class TestCaching(CachedRuleBuilderTestCase):
multiworld: MultiWorld # pyright: ignore[reportUninitializedInstanceVariable]
@@ -622,6 +674,24 @@ class TestRules(RuleBuilderTestCase):
self.state.remove(item)
self.assertFalse(resolved_rule(self.state))
def test_at_least(self) -> None:
# Has has to be relied on as True_ and False_ would be optimized out
rule = AtLeast(2, Has("Item 1"), Has("Item 1"), Has("Item 2"), Has("Item 3"))
resolved_rule = rule.resolve(self.world)
self.world.register_rule_dependencies(resolved_rule)
item1 = self.world.create_item("Item 1")
item2 = self.world.create_item("Item 2")
item3 = self.world.create_item("Item 3")
self.assertFalse(resolved_rule(self.state))
self.state.collect(item1)
self.assertTrue(resolved_rule(self.state))
self.state.collect(item2)
self.assertTrue(resolved_rule(self.state))
self.state.remove(item1)
self.assertFalse(resolved_rule(self.state))
self.state.collect(item3)
self.assertTrue(resolved_rule(self.state))
def test_has_all(self) -> None:
rule = HasAll("Item 1", "Item 2")
resolved_rule = rule.resolve(self.world)
@@ -797,8 +867,13 @@ class TestSerialization(RuleBuilderTestCase):
OptionFilter(ChoiceOption, ChoiceOption.option_second, "ge"),
],
),
AtLeast(
FromWorldAttr("instance_data.at_least_requirement"),
Has("i15", count=2),
HasGroup("g2", count=3),
),
CanReachEntrance("e1"),
HasGroupUnique("g2", count=5),
HasGroupUnique("g3", count=5),
)
rule_dict: ClassVar[dict[str, Any]] = {
@@ -922,6 +997,29 @@ class TestSerialization(RuleBuilderTestCase):
},
],
},
{
"rule": "AtLeast",
"options": [],
"filtered_resolution": False,
"count": {"resolver": "FromWorldAttr", "name": "instance_data.at_least_requirement"},
"children": [
{
"rule": "Has",
"options": [],
"filtered_resolution": False,
"args": {
"item_name": "i15",
"count": 2,
},
},
{
"rule": "HasGroup",
"options": [],
"filtered_resolution": False,
"args": {"item_name_group": "g2", "count": 3},
},
],
},
{
"rule": "CanReachEntrance",
"options": [],
@@ -932,7 +1030,7 @@ class TestSerialization(RuleBuilderTestCase):
"rule": "HasGroupUnique",
"options": [],
"filtered_resolution": False,
"args": {"item_name_group": "g2", "count": 5},
"args": {"item_name_group": "g3", "count": 5},
},
],
}
@@ -964,9 +1062,15 @@ class TestExplain(RuleBuilderTestCase):
),
player=1,
),
HasAllCounts.Resolved((("Item 6", 1), ("Item 7", 5)), player=1),
HasAnyCount.Resolved((("Item 8", 2), ("Item 9", 3)), player=1),
HasFromList.Resolved(("Item 10", "Item 11", "Item 12"), count=2, player=1),
AtLeast.Resolved(
children=(
HasAllCounts.Resolved((("Item 6", 1), ("Item 7", 5)), player=1),
HasAnyCount.Resolved((("Item 8", 2), ("Item 9", 3)), player=1),
HasFromList.Resolved(("Item 10", "Item 11", "Item 12"), count=2, player=1),
),
count=2,
player=1,
),
HasFromListUnique.Resolved(("Item 13", "Item 14"), player=1),
HasGroup.Resolved("Group 1", ("Item 15", "Item 16", "Item 17"), player=1),
HasGroupUnique.Resolved("Group 2", ("Item 18", "Item 19"), count=2, player=1),
@@ -1031,6 +1135,9 @@ class TestExplain(RuleBuilderTestCase):
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "At least "},
{"type": "color", "color": "cyan", "text": "0/2"},
{"type": "text", "text": " of ("},
{"type": "text", "text": "Missing "},
{"type": "color", "color": "cyan", "text": "some"},
{"type": "text", "text": " of ("},
@@ -1041,7 +1148,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "color", "color": "salmon", "text": "Item 7"},
{"type": "text", "text": " x5"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": ", "},
{"type": "text", "text": "Missing "},
{"type": "color", "color": "cyan", "text": "all"},
{"type": "text", "text": " of ("},
@@ -1052,7 +1159,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "color", "color": "salmon", "text": "Item 9"},
{"type": "text", "text": " x3"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "salmon", "text": "0/2"},
{"type": "text", "text": " items from ("},
@@ -1063,6 +1170,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "text", "text": ", "},
{"type": "color", "color": "salmon", "text": "Item 12"},
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "salmon", "text": "0/1"},
@@ -1129,6 +1237,9 @@ class TestExplain(RuleBuilderTestCase):
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "At least "},
{"type": "color", "color": "cyan", "text": "3/2"},
{"type": "text", "text": " of ("},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "all"},
{"type": "text", "text": " of ("},
@@ -1139,7 +1250,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "color", "color": "green", "text": "Item 7"},
{"type": "text", "text": " x5"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "some"},
{"type": "text", "text": " of ("},
@@ -1150,7 +1261,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "color", "color": "green", "text": "Item 9"},
{"type": "text", "text": " x3"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "green", "text": "30/2"},
{"type": "text", "text": " items from ("},
@@ -1161,6 +1272,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "text", "text": ", "},
{"type": "color", "color": "green", "text": "Item 12"},
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "green", "text": "2/1"},
@@ -1195,7 +1307,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "color", "color": "salmon", "text": "False"},
{"type": "text", "text": ")"},
]
assert self.resolved_rule.explain_json(self.state) == expected
self.assertEqual(self.resolved_rule.explain_json(self.state), expected)
def test_explain_json_without_state(self) -> None:
expected: list[JSONMessagePart] = [
@@ -1223,6 +1335,9 @@ class TestExplain(RuleBuilderTestCase):
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "At least "},
{"type": "color", "color": "cyan", "text": "2"},
{"type": "text", "text": " of ("},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "all"},
{"type": "text", "text": " of ("},
@@ -1232,7 +1347,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "item_name", "flags": 1, "text": "Item 7", "player": 1},
{"type": "text", "text": " x5"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "any"},
{"type": "text", "text": " of ("},
@@ -1242,7 +1357,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "item_name", "flags": 1, "text": "Item 9", "player": 1},
{"type": "text", "text": " x3"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": ", "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "2"},
{"type": "text", "text": "x items from ("},
@@ -1252,6 +1367,7 @@ class TestExplain(RuleBuilderTestCase):
{"type": "text", "text": ", "},
{"type": "item_name", "flags": 1, "text": "Item 12", "player": 1},
{"type": "text", "text": ")"},
{"type": "text", "text": ")"},
{"type": "text", "text": " & "},
{"type": "text", "text": "Has "},
{"type": "color", "color": "cyan", "text": "1"},
@@ -1285,16 +1401,16 @@ class TestExplain(RuleBuilderTestCase):
{"type": "color", "color": "salmon", "text": "False"},
{"type": "text", "text": ")"},
]
assert self.resolved_rule.explain_json() == expected
self.assertEqual(self.resolved_rule.explain_json(), expected)
def test_explain_str_with_state_no_items(self) -> None:
expected = (
"((Missing 4x Item 1",
"| Missing some of (Missing: Item 2, Item 3)",
"| Missing all of (Missing: Item 4, Item 5))",
"& Missing some of (Missing: Item 6 x1, Item 7 x5)",
"& Missing all of (Missing: Item 8 x2, Item 9 x3)",
"& Has 0/2 items from (Missing: Item 10, Item 11, Item 12)",
"& At least 0/2 of (Missing some of (Missing: Item 6 x1, Item 7 x5),",
"Missing all of (Missing: Item 8 x2, Item 9 x3),",
"Has 0/2 items from (Missing: Item 10, Item 11, Item 12))",
"& Has 0/1 unique items from (Missing: Item 13, Item 14)",
"& Has 0/1 items from Group 1",
"& Has 0/2 unique items from Group 2",
@@ -1304,7 +1420,7 @@ class TestExplain(RuleBuilderTestCase):
"& True",
"& False)",
)
assert self.resolved_rule.explain_str(self.state) == " ".join(expected)
self.assertEqual(self.resolved_rule.explain_str(self.state), " ".join(expected))
def test_explain_str_with_state_all_items(self) -> None:
self._collect_all()
@@ -1313,9 +1429,9 @@ class TestExplain(RuleBuilderTestCase):
"((Has 4x Item 1",
"| Has all of (Found: Item 2, Item 3)",
"| Has some of (Found: Item 4, Item 5))",
"& Has all of (Found: Item 6 x1, Item 7 x5)",
"& Has some of (Found: Item 8 x2, Item 9 x3)",
"& Has 30/2 items from (Found: Item 10, Item 11, Item 12)",
"& At least 3/2 of (Has all of (Found: Item 6 x1, Item 7 x5),",
"Has some of (Found: Item 8 x2, Item 9 x3),",
"Has 30/2 items from (Found: Item 10, Item 11, Item 12))",
"& Has 2/1 unique items from (Found: Item 13, Item 14)",
"& Has 30/1 items from Group 1",
"& Has 2/2 unique items from Group 2",
@@ -1325,16 +1441,16 @@ class TestExplain(RuleBuilderTestCase):
"& True",
"& False)",
)
assert self.resolved_rule.explain_str(self.state) == " ".join(expected)
self.assertEqual(self.resolved_rule.explain_str(self.state), " ".join(expected))
def test_explain_str_without_state(self) -> None:
expected = (
"((Has 4x Item 1",
"| Has all of (Item 2, Item 3)",
"| Has any of (Item 4, Item 5))",
"& Has all of (Item 6 x1, Item 7 x5)",
"& Has any of (Item 8 x2, Item 9 x3)",
"& Has 2x items from (Item 10, Item 11, Item 12)",
"& At least 2 of (Has all of (Item 6 x1, Item 7 x5),",
"Has any of (Item 8 x2, Item 9 x3),",
"Has 2x items from (Item 10, Item 11, Item 12))",
"& Has a unique item from (Item 13, Item 14)",
"& Has an item from Group 1",
"& Has 2x unique items from Group 2",
@@ -1344,16 +1460,16 @@ class TestExplain(RuleBuilderTestCase):
"& True",
"& False)",
)
assert self.resolved_rule.explain_str() == " ".join(expected)
self.assertEqual(self.resolved_rule.explain_str(), " ".join(expected))
def test_str(self) -> None:
expected = (
"((Has 4x Item 1",
"| Has all of (Item 2, Item 3)",
"| Has any of (Item 4, Item 5))",
"& Has all of (Item 6 x1, Item 7 x5)",
"& Has any of (Item 8 x2, Item 9 x3)",
"& Has 2x items from (Item 10, Item 11, Item 12)",
"& At least 2 of (Has all of (Item 6 x1, Item 7 x5),",
"Has any of (Item 8 x2, Item 9 x3),",
"Has 2x items from (Item 10, Item 11, Item 12))",
"& Has a unique item from (Item 13, Item 14)",
"& Has an item from Group 1",
"& Has 2x unique items from Group 2",
@@ -1363,7 +1479,7 @@ class TestExplain(RuleBuilderTestCase):
"& True",
"& False)",
)
assert str(self.resolved_rule) == " ".join(expected)
self.assertEqual(str(self.resolved_rule), " ".join(expected))
@classvar_matrix(
+37
View File
@@ -0,0 +1,37 @@
"""Verify that NetUtils' enums work correctly with all supported Python versions."""
import pickle
import unittest
from enum import Enum
from typing import Type
from NetUtils import ClientStatus, HintStatus, SlotType
from Utils import restricted_loads
class Base:
class DataEnumTest(unittest.TestCase):
type: Type[Enum]
value: Enum
def test_unpickle(self) -> None:
"""Tests that enums used in multidata or multisave can be pickled and unpickled."""
pickled = pickle.dumps(self.value)
unpickled = restricted_loads(pickled)
self.assertEqual(unpickled, self.value)
self.assertIsInstance(unpickled, self.type)
class HintStatusTest(Base.DataEnumTest):
type = HintStatus
value = HintStatus.HINT_AVOID
class ClientStatusTest(Base.DataEnumTest):
type = ClientStatus
value = ClientStatus.CLIENT_GOAL
class SlotTypeTest(Base.DataEnumTest):
type = SlotType
value = SlotType.player
+97 -1
View File
@@ -1,6 +1,8 @@
import unittest
from Options import Choice, DefaultOnToggle, Toggle
from collections import Counter
from Options import Choice, DefaultOnToggle, Toggle, OptionDict, OptionError, OptionSet, OptionList, OptionCounter
class TestNumericOptions(unittest.TestCase):
@@ -74,3 +76,97 @@ class TestNumericOptions(unittest.TestCase):
self.assertTrue(toggle_string)
self.assertTrue(toggle_int)
self.assertTrue(toggle_alias)
class TestContainerOptions(unittest.TestCase):
def test_option_dict(self):
class TestOptionDict(OptionDict):
valid_keys = frozenset({"A", "B", "C"})
unknown_key_init_dict = {"D": "Foo"}
test_option_dict = TestOptionDict(unknown_key_init_dict)
self.assertRaises(OptionError, test_option_dict.verify_keys)
init_dict = {"A": "foo", "B": "bar"}
test_option_dict = TestOptionDict(init_dict)
self.assertEqual(test_option_dict, init_dict) # Implicit value comparison
self.assertEqual(test_option_dict["A"], "foo")
self.assertIn("B", test_option_dict)
self.assertNotIn("C", test_option_dict)
self.assertRaises(KeyError, lambda: test_option_dict["C"])
def test_option_set(self):
class TestOptionSet(OptionSet):
valid_keys = frozenset({"A", "B", "C"})
unknown_key_init_set = {"D"}
test_option_set = TestOptionSet(unknown_key_init_set)
self.assertRaises(OptionError, test_option_set.verify_keys)
init_set = {"A", "B"}
test_option_set = TestOptionSet(init_set)
self.assertEqual(test_option_set, init_set) # Implicit value comparison
self.assertIn("B", test_option_set)
self.assertNotIn("C", test_option_set)
def test_option_list(self):
class TestOptionList(OptionList):
valid_keys = frozenset({"A", "B", "C"})
unknown_key_init_list = ["D"]
test_option_list = TestOptionList(unknown_key_init_list)
self.assertRaises(OptionError, test_option_list.verify_keys)
init_list = ["A", "B"]
test_option_list = TestOptionList(init_list)
self.assertEqual(test_option_list, init_list)
self.assertIn("B", test_option_list)
self.assertNotIn("C", test_option_list)
def test_option_counter(self):
class TestOptionCounter(OptionCounter):
valid_keys = frozenset({"A", "B", "C"})
max = 10
min = 0
unknown_key_init_dict = {"D": 5}
test_option_counter = TestOptionCounter(unknown_key_init_dict)
self.assertRaises(OptionError, test_option_counter.verify_keys)
wrong_value_type_init_dict = {"A": "B"}
self.assertRaises(TypeError, TestOptionCounter, wrong_value_type_init_dict)
violates_max_init_dict = {"A": 5, "B": 11}
test_option_counter = TestOptionCounter(violates_max_init_dict)
self.assertRaises(OptionError, test_option_counter.verify_values)
violates_min_init_dict = {"A": -1, "B": 5}
test_option_counter = TestOptionCounter(violates_min_init_dict)
self.assertRaises(OptionError, test_option_counter.verify_values)
init_dict = {"A": 0, "B": 10}
test_option_counter = TestOptionCounter(init_dict)
self.assertEqual(test_option_counter, Counter(init_dict))
self.assertIn("A", test_option_counter)
self.assertNotIn("C", test_option_counter)
self.assertEqual(test_option_counter["A"], 0)
self.assertEqual(test_option_counter["B"], 10)
self.assertEqual(test_option_counter["C"], 0)
def test_culling_option_counter(self):
class TestCullingCounter(OptionCounter):
valid_keys = frozenset({"A", "B", "C"})
cull_zeroes = True
init_dict = {"A": 0, "B": 10}
test_option_counter = TestCullingCounter(init_dict)
self.assertNotIn("A", test_option_counter)
self.assertIn("B", test_option_counter)
self.assertNotIn("C", test_option_counter)
self.assertEqual(test_option_counter["A"], 0) # It's still a Counter! cull_zeroes is about "in" checks.
self.assertEqual(test_option_counter, Counter({"B": 10}))
+5
View File
@@ -33,4 +33,9 @@ class TestBase(unittest.TestCase):
cls.app = raw_app
def setUp(self) -> None:
from WebHostLib.models import db
from pony.orm import db_session
with db_session:
for entity in db.entities.values():
entity.select().delete(bulk=True)
self.client = self.app.test_client()
+107
View File
@@ -0,0 +1,107 @@
from datetime import timedelta
from uuid import UUID, uuid4
from pony.orm import db_session, commit
from Utils import utcnow
from WebHostLib.autolauncher import cleanup
from WebHostLib.models import Room, Seed, Slot
from . import TestBase
class TestCleanup(TestBase):
def test_cleanup_unowned(self) -> None:
with db_session:
s1 = Seed(id=uuid4(), multidata=b"", owner=UUID(int=0))
Room(id=uuid4(), owner=UUID(int=0), seed=s1)
s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4()) # Owned
Room(id=uuid4(), owner=UUID(int=0), seed=s2) # Unowned room of owned seed
Seed(id=uuid4(), multidata=b"", owner=UUID(int=0)) # Unowned seed with no rooms
commit()
cleanup({"ROOM_AUTO_DELETE": 0})
with db_session:
self.assertEqual(Room.select().count(), 0) # Both rooms were unowned
self.assertEqual(Seed.select().count(), 1) # s2 is owned
self.assertIsNotNone(Seed.get(id=s2.id))
def test_cleanup_auto_delete(self) -> None:
now = utcnow()
old_time = now - timedelta(days=10)
recent_time = now - timedelta(days=2)
with db_session:
# Case 1: Old room, owned
s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
r1 = Room(id=uuid4(), owner=uuid4(), seed=s1, last_activity=old_time)
# Case 2: Recent room, owned
s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
r2 = Room(id=uuid4(), owner=uuid4(), seed=s2, last_activity=recent_time)
# Case 3: Old seed, no rooms, owned
s3 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
# Case 4: Recent seed, no rooms, owned
s4 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=recent_time)
# Case 5: Old seed with recent room (should not be deleted)
s5 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
r5 = Room(id=uuid4(), owner=uuid4(), seed=s5, last_activity=recent_time)
commit()
# Delete items older than 5 days
cleanup({"ROOM_AUTO_DELETE": 5})
with db_session:
self.assertIsNone(Room.get(id=r1.id), "Old room should be deleted")
self.assertIsNotNone(Room.get(id=r2.id), "Recent room should NOT be deleted")
self.assertIsNone(Seed.get(id=s3.id), "Old seed without rooms should be deleted")
self.assertIsNotNone(Seed.get(id=s4.id), "Recent seed without rooms should NOT be deleted")
self.assertIsNotNone(Seed.get(id=s5.id), "Old seed with recent room should NOT be deleted")
self.assertIsNotNone(Room.get(id=r5.id), "Recent room for old seed should NOT be deleted")
# Seeds are deleted if they have NO rooms AND are old.
# After r1 is deleted, s1 has no rooms. Since it's old, it should be deleted.
self.assertIsNone(Seed.get(id=s1.id), "Old seed whose only room was deleted should be deleted")
def test_cleanup_disabled(self) -> None:
now = utcnow()
old_time = now - timedelta(days=10)
with db_session:
s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
r1 = Room(id=uuid4(), owner=uuid4(), seed=s1, last_activity=old_time)
commit()
cleanup({"ROOM_AUTO_DELETE": 0})
with db_session:
self.assertIsNotNone(Room.get(id=r1.id), "Room should NOT be deleted when auto-delete is 0")
self.assertIsNotNone(Seed.get(id=s1.id), "Seed should NOT be deleted when auto-delete is 0")
def test_cleanup_slots(self) -> None:
now = utcnow()
old_time = now - timedelta(days=10)
with db_session:
s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
slot1 = Slot(player_id=1, player_name="P1", seed=s1, game="TestGame")
s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=now)
slot2 = Slot(player_id=2, player_name="P2", seed=s2, game="TestGame")
commit()
# Delete items older than 5 days
cleanup({"ROOM_AUTO_DELETE": 5})
with db_session:
self.assertIsNone(Seed.get(id=s1.id), "Old seed should be deleted")
self.assertIsNone(Slot.get(id=slot1.id), "Slot of deleted seed should be deleted")
self.assertIsNotNone(Seed.get(id=s2.id), "Recent seed should NOT be deleted")
self.assertIsNotNone(Slot.get(id=slot2.id), "Slot of recent seed should NOT be deleted")
+89
View File
@@ -0,0 +1,89 @@
import os
import unittest
from socket import socket as Socket # noqa: N812
from Utils import is_macos
from WebHostLib.customserver import RandomPortSocketCreator
ci = bool(os.environ.get("CI"))
class TestPortAllocating(unittest.TestCase):
def test_parse_game_ports(self) -> None:
"""Ensure that game ports with ranges are parsed correctly"""
val = RandomPortSocketCreator._parse_game_ports(("1000-2000", "2000-5000", "1000-2000", 20, 40, "20", "0"))
self.assertCountEqual(val.valid_ports,
[*range(1000, 2001), *range(2000, 5001), *range(1000, 2001), 20, 40, 20],
"The parsed game ports are not the expected length")
self.assertTrue(val.ephemeral_allowed, "The ephemeral allowed flag is not set even though it was passed")
val = RandomPortSocketCreator._parse_game_ports(())
self.assertListEqual(val.valid_ports, [], "Empty list of game port returned something")
self.assertFalse(val.ephemeral_allowed, "Empty list returned that ephemeral is allowed")
val = RandomPortSocketCreator._parse_game_ports((0,))
self.assertListEqual(val.valid_ports, [], "Empty list of ranges returned something")
self.assertTrue(val.ephemeral_allowed, "List with just 0 is not allowing ephemeral ports")
val = RandomPortSocketCreator._parse_game_ports((1,))
self.assertListEqual(val.valid_ports, [1], "Valid ports doesn't contain the expected values")
self.assertFalse(val.ephemeral_allowed, "List with just single port returned that ephemeral is allowed")
def test_parse_game_port_errors(self) -> None:
"""Ensure that game ports with incorrect values raise the expected error"""
with self.assertRaises(ValueError, msg="Negative numbers didn't get interpreted as an invalid range"):
RandomPortSocketCreator._parse_game_ports(tuple("-50215"))
with self.assertRaises(ValueError, msg="Text got interpreted as a valid number"):
RandomPortSocketCreator._parse_game_ports(tuple("dwafawg"))
with self.assertRaises(
ValueError,
msg="A range with an extra dash at the end didn't get interpreted as an invalid number because of it's end dash"
):
RandomPortSocketCreator._parse_game_ports(tuple("20-21215-"))
with self.assertRaises(ValueError, msg="Text got interpreted as a valid number for the start of a range"):
RandomPortSocketCreator._parse_game_ports(tuple("f-21215"))
def test_random_port_socket_edge_cases(self) -> None:
"""Verify if edge cases on creation of random port socket is working fine"""
# Try giving an empty tuple and fail over it
creator = RandomPortSocketCreator(())
with self.assertRaises(OSError) as err:
creator.create("127.0.0.1")
self.assertEqual(err.exception.errno, 98, "Raised an unexpected error code")
self.assertEqual(err.exception.strerror, "No available ports", "Raised an unexpected error string")
# Try only having ephemeral ports enabled
creator = RandomPortSocketCreator(("0",))
try:
creator.create("127.0.0.1").close()
except OSError as err:
self.assertEqual(err.errno, 98, "Raised an unexpected error code")
# If it returns our error string that means something is wrong with our code
self.assertNotEqual(err.strerror, "No available ports",
"Raised an unexpected error string")
@unittest.skipUnless(ci, "can't guarantee free ports outside of CI")
def test_random_port_socket(self) -> None:
"""Verify if returned sockets use the correct port ranges"""
creator = RandomPortSocketCreator(("8080-8085",))
sockets: list[Socket] = []
for _ in range(6):
socket = creator.create("127.0.0.1")
sockets.append(socket)
_, port = socket.getsockname()
self.assertIn(port, range(8080, 8086), "Port of socket was not inside the expected range")
for s in sockets:
s.close()
sockets.clear()
creator = RandomPortSocketCreator(("30000-65535",))
length = 5_000 if is_macos else (30_000 - len(creator._get_used_ports()))
for _ in range(length):
socket = creator.create("127.0.0.1")
sockets.append(socket)
_, port = socket.getsockname()
self.assertIn(port, range(30_000, 65536), "Port of socket was not inside the expected range")
for s in sockets:
s.close()
+76
View File
@@ -0,0 +1,76 @@
import math
from typing import Any, Callable
from typing_extensions import override
from uuid import uuid4
from werkzeug.routing import BaseConverter
from . import TestBase
class TestSUUID(TestBase):
converter: BaseConverter
filter: Callable[[Any], str]
@override
def setUp(self) -> None:
from werkzeug.routing import Map
super().setUp()
self.converter = self.app.url_map.converters["suuid"](Map())
self.filter = self.app.jinja_env.filters["suuid"] # type: ignore # defines how we use it, not what it can be
def test_is_reversible(self) -> None:
u = uuid4()
self.assertEqual(u, self.converter.to_python(self.converter.to_url(u)))
s = "A" * 22 # uuid with all zeros
self.assertEqual(s, self.converter.to_url(self.converter.to_python(s)))
def test_uuid_length(self) -> None:
with self.assertRaises(ValueError):
self.converter.to_python("AAAA")
def test_padding(self) -> None:
self.converter.to_python("A" * 22) # check that the correct value works
with self.assertRaises(ValueError):
self.converter.to_python("A" * 22 + "==") # converter should not allow padding
def test_empty(self) -> None:
with self.assertRaises(ValueError):
self.converter.to_python("")
def test_stray_equal_signs(self) -> None:
self.converter.to_python("A" * 22) # check that the correct value works
with self.assertRaises(ValueError):
self.converter.to_python("A" * 22 + "==" + "AA") # the "==AA" should not be ignored, but error out
with self.assertRaises(ValueError):
self.converter.to_python("A" * 20 + "==" + "AA") # the final "A"s should not be appended to the first "A"s
def test_stray_whitespace(self) -> None:
s = "A" * 22
self.converter.to_python(s) # check that the correct value works
for char in " \t\r\n\v":
for pos in (0, 11, 22):
with self.subTest(char=char, pos=pos):
s_with_whitespace = s[0:pos] + char * 4 + s[pos:] # insert 4 to make padding correct
# check that the constructed s_with_whitespace is correct
self.assertEqual(len(s_with_whitespace), len(s) + 4)
self.assertEqual(s_with_whitespace[pos], char)
# s_with_whitespace should be invalid as SUUID
with self.assertRaises(ValueError):
self.converter.to_python(s_with_whitespace)
def test_filter_returns_valid_string(self) -> None:
u = uuid4()
s = self.filter(u)
self.assertIsInstance(s, str)
self.assertNotIn("=", s)
self.assertEqual(len(s), math.ceil(len(u.bytes) * 4 / 3))
def test_filter_is_same_as_converter(self) -> None:
u = uuid4()
self.assertEqual(self.filter(u), self.converter.to_url(u))
def test_filter_bad_type(self) -> None:
with self.assertRaises(Exception): # currently the type is not checked directly, so any exception is valid
self.filter(None)
+5 -1
View File
@@ -353,6 +353,8 @@ class World(metaclass=AutoWorldRegister):
"""path it was loaded from"""
world_version: ClassVar[Version] = Version(0, 0, 0)
"""Optional world version loaded from archipelago.json"""
manifest: ClassVar[dict[str, Any]] = {}
"""Mapping of the world's archipelago.json manifest. Use game and world_version attrs instead for those values."""
def __init__(self, multiworld: "MultiWorld", player: int):
assert multiworld is not None
@@ -512,7 +514,9 @@ class World(metaclass=AutoWorldRegister):
def get_filler_item_name(self) -> str:
"""
Called when the item pool needs to be filled with additional items to match location count.
If core AP removes an item from your item pool, this method is called to choose a replacement item
so item count and location count remain equal.
For example: plando, item_links and start_inventory_from_pool are features that may cause this.
Any returned item name must be for a "repeatable" item, i.e. one that it's okay to generate arbitrarily many of.
For most worlds this will be one or more of your filler items, but the classification of these items
+16 -5
View File
@@ -11,7 +11,7 @@ import json
from pathlib import Path
from types import ModuleType
from typing import List, Sequence
from zipfile import BadZipFile
from zipfile import ZipFile, BadZipFile
from NetUtils import DataPackage
from Utils import local_path, user_path, Version, version_tuple, tuplize_version, messagebox
@@ -33,7 +33,7 @@ __all__ = [
]
failed_world_loads: List[str] = []
failed_world_loads: dict[str, str] = {}
@dataclasses.dataclass(order=True)
@@ -68,8 +68,9 @@ class WorldSource:
print(f"Could not load world {self}:", file=file_like)
traceback.print_exc(file=file_like)
file_like.seek(0)
logging.exception(file_like.read())
failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0])
reason = file_like.read()
logging.exception(reason)
failed_world_loads[os.path.basename(self.path).rsplit(".", 1)[0]] = reason
return False
@@ -118,6 +119,7 @@ for world_source in world_sources:
game = manifest.get("game")
if game in AutoWorldRegister.world_types:
AutoWorldRegister.world_types[game].world_version = tuplize_version(manifest.get("world_version", "0.0.0"))
AutoWorldRegister.world_types[game].manifest = manifest
if apworlds:
# encapsulation for namespace / gc purposes
@@ -128,7 +130,7 @@ if apworlds:
def fail_world(game_name: str, reason: str, add_as_failed_to_load: bool = True) -> None:
if add_as_failed_to_load:
failed_world_loads.append(game_name)
failed_world_loads[game_name] = reason
logging.warning(reason)
for apworld_source in apworlds:
@@ -199,6 +201,15 @@ if apworlds:
# world could fail to load at this point
if apworld.world_version:
AutoWorldRegister.world_types[apworld.game].world_version = apworld.world_version
assert apworld.path
with ZipFile(apworld.path, "r") as zf:
manifest = apworld.read_contents(zf)
# version/compatible_version shouldn't be needed by world, makes it consistent with folder world
manifest.pop("version", None)
manifest.pop("compatible_version", None)
AutoWorldRegister.world_types[apworld.game].manifest = manifest
load_apworlds()
del load_apworlds
+8 -7
View File
@@ -50,16 +50,17 @@ make sure ***Enable Developer Console*** is checked in Game Settings and press t
## FAQ/Common Issues
### The game is not connecting when starting a new save!
For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu
(rocket icon) in-game, and re-enable the mod.
### The game is crashing on startup repeatedly!
This is a common issue on older versions of the game, caused by the game failing to interface with the Steam Workshop.
To fix it you can try the following (from least to most effort required)
- Subscribe to any random workshop mod, then unsubscribe from it
- Restart Steam
- Restart your computer
- Delete the game's config directory from the files `steamapps/common/HatinTime/HatinTimeGame/Config` then verify the game files
- Reinstall the game
### Why do relics disappear from the stands in the Spaceship after they're completed?
This is intentional behaviour. Because of how randomizer logic works, there is no way to predict the order that
a player will place their relics. Since there are a limited amount of relic stands in the Spaceship, relics are removed
after being completed to allow for the placement of more relics without being potentially locked out.
The level that the relic set unlocked will stay unlocked.
### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work!
There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly
if you have too many save files. Delete them and it should fix the problem.
+478
View File
@@ -0,0 +1,478 @@
from __future__ import annotations
from dataclasses import dataclass, field
from functools import lru_cache
import hashlib
import random
from typing import TYPE_CHECKING, Optional
from Utils import pc_to_snes, snes_to_pc
from .enemizer_data.base_patch_data import ENEMIZER_BASE_PATCHES
from .enemizer_data.symbols import ENEMIZER_SYMBOLS
if TYPE_CHECKING:
from . import ALTTPWorld
from .Rom import LocalRom
@dataclass(frozen=True)
class BossPatchData:
pointer: tuple[int, int]
graphics: int
sprite_array: tuple[int, ...]
@dataclass(frozen=True)
class DungeonBossPatchData:
room_id: int
sprite_pointer_address: int
shell_x: int
shell_y: int
clear_layer2: bool = False
extra_sprites: tuple[int, ...] = ()
gt_sprite_write_address: Optional[int] = None
@dataclass
class RoomObjectTable:
header_byte_0: int
header_byte_1: int
layer_1_objects: list[bytes] = field(default_factory=list)
layer_1_doors: list[bytes] = field(default_factory=list)
layer_2_objects: list[bytes] = field(default_factory=list)
layer_2_doors: list[bytes] = field(default_factory=list)
layer_3_objects: list[bytes] = field(default_factory=list)
layer_3_doors: list[bytes] = field(default_factory=list)
@classmethod
def from_rom(cls, rom: "LocalRom", start_address: int) -> "RoomObjectTable":
table = cls(rom.read_byte(start_address), rom.read_byte(start_address + 1))
layers = (
(table.layer_1_objects, table.layer_1_doors),
(table.layer_2_objects, table.layer_2_doors),
(table.layer_3_objects, table.layer_3_doors),
)
index = start_address + 2
for objects, doors in layers:
is_door = False
while True:
if rom.read_bytes(index, 2) == bytearray((0xF0, 0xFF)):
is_door = True
index += 2
continue
if rom.read_bytes(index, 2) == bytearray((0xFF, 0xFF)):
index += 2
break
if is_door:
doors.append(bytes(rom.read_bytes(index, 2)))
index += 2
else:
objects.append(bytes(rom.read_bytes(index, 3)))
index += 3
return table
def add_shell(self, x: int, y: int, clear_layer_2: bool, shell_id: int) -> None:
self.header_byte_0 = 0xF0
if clear_layer_2:
self.layer_2_objects.clear()
self.layer_2_objects.append(_build_subtype_3_object(x, y, shell_id))
def remove_shell(self, shell_id: int) -> None:
self.layer_2_objects = [obj for obj in self.layer_2_objects if _object_id(obj) != shell_id]
def to_bytes(self) -> bytes:
output = bytearray((self.header_byte_0, self.header_byte_1))
output.extend(self._serialize_layer(self.layer_1_objects, self.layer_1_doors, is_last_layer=False))
output.extend(self._serialize_layer(self.layer_2_objects, self.layer_2_doors, is_last_layer=False))
output.extend(self._serialize_layer(self.layer_3_objects, self.layer_3_doors, is_last_layer=True))
return bytes(output)
@staticmethod
def _serialize_layer(objects: list[bytes], doors: list[bytes], is_last_layer: bool) -> bytes:
output = bytearray()
for obj in objects:
output.extend(obj)
if is_last_layer or doors:
output.extend((0xF0, 0xFF))
for door in doors:
output.extend(door)
output.extend((0xFF, 0xFF))
return bytes(output)
BOSS_PATCH_DATA: dict[str, BossPatchData] = {
"Armos": BossPatchData((0x87, 0xE8), 9, (0x05, 0x04, 0x53, 0x05, 0x07, 0x53, 0x05, 0x0A, 0x53,
0x08, 0x0A, 0x53, 0x08, 0x07, 0x53, 0x08, 0x04, 0x53,
0x08, 0xE7, 0x19)),
"Arrghus": BossPatchData((0x97, 0xD9), 20, (0x07, 0x07, 0x8C, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D)),
"Blind": BossPatchData((0x54, 0xE6), 32, (0x05, 0x09, 0xCE)),
"Helmasaur": BossPatchData((0x49, 0xE0), 21, (0x06, 0x07, 0x92)),
"Kholdstare": BossPatchData((0x01, 0xEA), 22, (0x05, 0x07, 0xA3, 0x05, 0x07, 0xA4, 0x05, 0x07, 0xA2)),
"Lanmola": BossPatchData((0xCB, 0xDC), 11, (0x07, 0x06, 0x54, 0x07, 0x09, 0x54, 0x09, 0x07, 0x54)),
"Moldorm": BossPatchData((0xC3, 0xD9), 12, (0x09, 0x09, 0x09)),
"Mothula": BossPatchData((0x31, 0xDC), 26, (0x06, 0x08, 0x88)),
"Trinexx": BossPatchData((0xBA, 0xE5), 23, (0x05, 0x07, 0xCB, 0x05, 0x07, 0xCC, 0x05, 0x07, 0xCD)),
"Vitreous": BossPatchData((0x57, 0xE4), 22, (0x05, 0x07, 0xBD)),
}
DUNGEON_BOSS_PATCH_DATA: dict[tuple[str, Optional[str]], DungeonBossPatchData] = {
("Eastern Palace", None): DungeonBossPatchData(200, 0x04D7BE, 0x2B, 0x28),
("Desert Palace", None): DungeonBossPatchData(51, 0x04D694, 0x0B, 0x28),
("Tower of Hera", None): DungeonBossPatchData(7, 0x04D63C, 0x18, 0x16),
("Palace of Darkness", None): DungeonBossPatchData(90, 0x04D6E2, 0x2B, 0x28),
("Swamp Palace", None): DungeonBossPatchData(6, 0x04D63A, 0x0B, 0x28),
("Skull Woods", None): DungeonBossPatchData(41, 0x04D680, 0x2B, 0x28),
("Thieves Town", None): DungeonBossPatchData(172, 0x04D786, 0x2B, 0x28, clear_layer2=True),
("Ice Palace", None): DungeonBossPatchData(222, 0x04D7EA, 0x2B, 0x08, clear_layer2=True),
("Misery Mire", None): DungeonBossPatchData(144, 0x04D74E, 0x0B, 0x28, clear_layer2=True),
("Turtle Rock", None): DungeonBossPatchData(164, 0x04D776, 0x0B, 0x28, clear_layer2=True),
("Ganons Tower", "bottom"): DungeonBossPatchData(
28, 0x04D666, 0x2B, 0x28, extra_sprites=(0x07, 0x07, 0xE3, 0x07, 0x08, 0xE3, 0x08, 0x07, 0xE3, 0x08, 0x08, 0xE3),
gt_sprite_write_address=0x04D87E,
),
("Ganons Tower", "middle"): DungeonBossPatchData(
108, 0x04D706, 0x0B, 0x28, extra_sprites=(0x18, 0x17, 0xD1, 0x1C, 0x03, 0xC5), gt_sprite_write_address=0x04D8B6,
),
("Ganons Tower", "top"): DungeonBossPatchData(77, 0x04D6C8, 0x18, 0x16),
}
TRINEXX_SHELL_OBJECT_ID = 0xFF2
KHOLDSTARE_SHELL_OBJECT_ID = 0xF95
TRINEXX_VANILLA_ROOM_ID = 164
KHOLDSTARE_VANILLA_ROOM_ID = 222
ENEMY_HP_TABLE_ADDRESS = 0x6B173
ENEMY_DAMAGE_TABLE_ADDRESS = 0x6B266
HIDDEN_ENEMY_CHANCE_POOL_ADDRESS = 0xD7BBB
DAMAGE_GROUP_TABLE_ADDRESS = 0x3742D
RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS = 0x301FC
RETRO_RUPEE_REPLACEMENT_SPRITE_ID = 0xDA
ARROW_REFILL_5_SPRITE_ID = 0xE1
THIEF_SPRITE_ID = 0xC4
THIEF_DEFAULT_HP = 4
VANILLA_HIDDEN_ENEMY_CHANCE_POOL = (
0x01, 0x01, 0x01, 0x01, 0x0F, 0x01, 0x01, 0x12,
0x10, 0x01, 0x01, 0x01, 0x11, 0x01, 0x01, 0x03,
)
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL = (
0x01, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x12,
0x0F, 0x01, 0x0F, 0x0F, 0x11, 0x0F, 0x0F, 0x03,
)
EXCLUDED_ENEMY_TABLE_SPRITE_IDS = frozenset({
0x09, 0x53, 0x54, 0x70, 0x7A, 0x7B, 0x88, 0x89, 0x8C, 0x8D, 0x92,
0xA2, 0xA3, 0xA4, 0xBD, 0xBE, 0xBF, 0xCB, 0xCC, 0xCD, 0xCE, 0xD6, 0xD7,
})
ENEMY_HEALTH_RANGE_BY_KEY = {
"easy": (1, 4),
"normal": (2, 15),
"hard": (2, 25),
"expert": (4, 50),
}
_ENEMIZER_SYMBOLS: Optional[dict[str, int]] = None
BOSS_GFX_SHEET_INDEXES = {
"Agahnim1": 0x8D,
"Agahnim2": 0xB5,
"Agahnim3": 0xC8,
"Agahnim4": 0xB6,
"ArmosKnight1": 0x90,
"Ganon1": 0x94,
"Ganon2": 0xA6,
"Ganon3": 0xB4,
"Ganon4": 0xB8,
"Moldorm1": 0xA3,
"Lanmola1": 0xA4,
"Arrghus1": 0xAC,
"Mothula1": 0xAB,
"Helmasaure1": 0xAD,
"Helmasaure2": 0xB1,
"Blind1": 0xAE,
"Kholdstare1": 0xAF,
"Vitreous1": 0xB0,
"Trinexx1": 0xB2,
"Trinexx2": 0xB3,
}
BOSS_GFX_TABLE = {
"Agahnim1": (21, 190, 228),
"Agahnim2": (22, 255, 135),
"Agahnim3": (23, 220, 101),
"Agahnim4": (23, 132, 92),
"ArmosKnight1": (21, 206, 27),
"Ganon1": (21, 227, 160),
"Ganon2": (22, 186, 55),
"Ganon3": (22, 250, 199),
"Ganon4": (23, 142, 33),
"Moldorm1": (22, 175, 152),
"Lanmola1": (22, 180, 23),
"Arrghus1": (22, 214, 147),
"Mothula1": (22, 210, 84),
"Helmasaure1": (22, 219, 114),
"Helmasaure2": (22, 239, 177),
"Blind1": (22, 224, 90),
"Kholdstare1": (22, 230, 31),
"Vitreous1": (22, 235, 9),
"Trinexx1": (22, 243, 89),
"Trinexx2": (22, 246, 35),
}
TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS = 0x04B37E
TRINEXX_ICE_PROJECTILE_TILE_ADDRESS = 0xE7A5
TILE_TRAP_FLOOR_TILE_ADDRESS = 0xF3BED
def apply_enemizer_base_patch(rom: "LocalRom") -> None:
for address, patch_data in _load_enemizer_base_patches():
rom.write_bytes(address, patch_data)
_apply_trinexx_room_fixes(rom)
def patch_bosses(world: "ALTTPWorld", rom: "LocalRom") -> None:
dungeon_header_base = _get_enemizer_symbol("room_header_table")
moved_room_object_base = _get_enemizer_symbol("modified_room_object_table")
gt_dungeon_name = "Ganons Tower" if world.options.mode != "inverted" else "Inverted Ganons Tower"
gt_dungeon = world.dungeons[gt_dungeon_name]
placements = (
(world.dungeons["Eastern Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Eastern Palace", None)]),
(world.dungeons["Desert Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Desert Palace", None)]),
(world.dungeons["Tower of Hera"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Tower of Hera", None)]),
(world.dungeons["Palace of Darkness"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Palace of Darkness", None)]),
(world.dungeons["Swamp Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Swamp Palace", None)]),
(world.dungeons["Skull Woods"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Skull Woods", None)]),
(world.dungeons["Thieves Town"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Thieves Town", None)]),
(world.dungeons["Ice Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ice Palace", None)]),
(world.dungeons["Misery Mire"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Misery Mire", None)]),
(world.dungeons["Turtle Rock"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Turtle Rock", None)]),
(gt_dungeon.bosses["bottom"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "bottom")]),
(gt_dungeon.bosses["middle"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "middle")]),
(gt_dungeon.bosses["top"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "top")]),
)
modified_room_tables: dict[int, RoomObjectTable] = {}
for boss_name, dungeon_data in placements:
boss_data = BOSS_PATCH_DATA[boss_name]
rom.write_bytes(dungeon_data.sprite_pointer_address, boss_data.pointer)
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 3, boss_data.graphics)
if boss_name == "Trinexx" and dungeon_data.room_id != TRINEXX_VANILLA_ROOM_ID:
room_table = _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id)
room_table.add_shell(
dungeon_data.shell_x,
dungeon_data.shell_y - 2,
dungeon_data.clear_layer2,
TRINEXX_SHELL_OBJECT_ID,
)
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14), 0x60)
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 4, 0x04)
if boss_name == "Kholdstare" and dungeon_data.room_id != KHOLDSTARE_VANILLA_ROOM_ID:
room_table = _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id)
room_table.add_shell(
dungeon_data.shell_x,
dungeon_data.shell_y,
dungeon_data.clear_layer2,
KHOLDSTARE_SHELL_OBJECT_ID,
)
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14), 0xE0)
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 4, 0x01)
if boss_name != "Trinexx" and dungeon_data.room_id == TRINEXX_VANILLA_ROOM_ID:
_get_room_object_table(rom, modified_room_tables, dungeon_data.room_id).remove_shell(TRINEXX_SHELL_OBJECT_ID)
if boss_name != "Kholdstare" and dungeon_data.room_id == KHOLDSTARE_VANILLA_ROOM_ID:
_get_room_object_table(rom, modified_room_tables, dungeon_data.room_id).remove_shell(KHOLDSTARE_SHELL_OBJECT_ID)
if dungeon_data.gt_sprite_write_address is not None:
_write_gt_boss_sprite_block(rom, dungeon_data, boss_data)
write_address = moved_room_object_base
for room_id in sorted(modified_room_tables):
table_bytes = modified_room_tables[room_id].to_bytes()
_write_room_object_pointer(rom, room_id, write_address)
rom.write_bytes(write_address, table_bytes)
write_address += len(table_bytes)
rom.write_byte(0x1B0101, 0x01)
rom.write_byte(0x04DE81, 0x00)
if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind":
rom.write_byte(0x04DE81, 0x06)
rom.write_byte(0x1B0101, 0x00)
def _get_room_object_table(rom: "LocalRom", cache: dict[int, RoomObjectTable], room_id: int) -> RoomObjectTable:
room_table = cache.get(room_id)
if room_table is not None:
return room_table
pointer_address = 0xF8000 + (room_id * 3)
snes_address_bytes = rom.read_bytes(pointer_address, 3)
snes_address = (snes_address_bytes[2] << 16) | (snes_address_bytes[1] << 8) | snes_address_bytes[0]
room_table = RoomObjectTable.from_rom(rom, snes_to_pc(snes_address))
cache[room_id] = room_table
return room_table
def _write_gt_boss_sprite_block(rom: "LocalRom", dungeon_data: DungeonBossPatchData, boss_data: BossPatchData) -> None:
assert dungeon_data.gt_sprite_write_address is not None
rom.write_int16(dungeon_data.sprite_pointer_address, dungeon_data.gt_sprite_write_address)
sprite_block = bytearray((0x00,))
sprite_block.extend(boss_data.sprite_array)
if dungeon_data.room_id == 28 and boss_data.pointer == BOSS_PATCH_DATA["Arrghus"].pointer:
sprite_block.extend(dungeon_data.extra_sprites[:6])
else:
sprite_block.extend(dungeon_data.extra_sprites)
sprite_block.append(0xFF)
rom.write_bytes(dungeon_data.gt_sprite_write_address, sprite_block)
def _write_room_object_pointer(rom: "LocalRom", room_id: int, pc_address: int) -> None:
snes_address = pc_to_snes(pc_address)
pointer_address = 0xF8000 + (room_id * 3)
rom.write_bytes(pointer_address, (
snes_address & 0xFF,
(snes_address >> 8) & 0xFF,
(snes_address >> 16) & 0xFF,
))
def _build_subtype_3_object(x: int, y: int, object_id: int) -> bytes:
return bytes((
((x << 2) & 0xFC) | (object_id & 0x03),
((y << 2) & 0xFC) | ((object_id >> 2) & 0x03),
0xF0 | ((object_id >> 4) & 0x0F),
))
def _object_id(object_bytes: bytes) -> Optional[int]:
if len(object_bytes) != 3:
return None
if object_bytes[0] >= 0xFC:
return (object_bytes[2] & 0x3F) + 0x100
if object_bytes[2] >= 0xF8:
return 0xF00 | ((object_bytes[2] & 0x0F) << 4) | ((object_bytes[1] & 0x03) << 2) | (object_bytes[0] & 0x03)
return object_bytes[2]
def _set_enemizer_flag(rom: "LocalRom", symbol_name: str, enabled: bool) -> None:
rom.write_byte(_get_enemizer_symbol(symbol_name), 0x01 if enabled else 0x00)
def _apply_killable_thief(rom: "LocalRom") -> None:
rom.write_byte(_get_enemizer_symbol("notItemSprite_Mimic") + 4, THIEF_SPRITE_ID)
thief_hp_address = ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID
if rom.read_byte(thief_hp_address) != 0xFF:
rom.write_byte(thief_hp_address, THIEF_DEFAULT_HP)
def _randomize_enemy_health(rom: "LocalRom", rng: random.Random, enemy_health_key: str) -> None:
min_hp, max_hp = ENEMY_HEALTH_RANGE_BY_KEY[enemy_health_key]
for sprite_id in range(0xF3):
hp_address = ENEMY_HP_TABLE_ADDRESS + sprite_id
if rom.read_byte(hp_address) == 0xFF or sprite_id in EXCLUDED_ENEMY_TABLE_SPRITE_IDS:
continue
rom.write_byte(hp_address, rng.randrange(min_hp, max_hp))
def _randomize_enemy_damage(rom: "LocalRom", rng: random.Random, allow_zero_damage: bool) -> None:
for sprite_id in range(0xF3):
if sprite_id in EXCLUDED_ENEMY_TABLE_SPRITE_IDS:
continue
new_damage = rng.randrange(8)
if not allow_zero_damage and new_damage == 2:
continue
rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + sprite_id, new_damage)
def _shuffle_damage_groups(
rom: "LocalRom",
rng: random.Random,
*,
chaos_mode: bool,
allow_zero_damage: bool,
) -> None:
min_damage = 0 if allow_zero_damage else 4
max_damage = 64 if chaos_mode else 32
for group_id in range(10):
green_mail_damage = rng.randrange(min_damage, max_damage)
if chaos_mode:
blue_mail_damage = rng.randrange(min_damage, max_damage)
red_mail_damage = rng.randrange(min_damage, max_damage)
else:
blue_mail_damage = green_mail_damage * 3 // 4
red_mail_damage = green_mail_damage * 3 // 8
group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
rom.write_bytes(group_address, (green_mail_damage, blue_mail_damage, red_mail_damage))
def _update_hidden_enemy_item_table_for_retro_mode(rom: "LocalRom") -> None:
if rom.read_byte(RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS) != RETRO_RUPEE_REPLACEMENT_SPRITE_ID:
return
item_table_address = _get_enemizer_symbol("sprite_bush_spawn_item_table")
for index in range(22):
if rom.read_byte(item_table_address + index) == ARROW_REFILL_5_SPRITE_ID:
rom.write_byte(item_table_address + index, RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
def _apply_trinexx_room_fixes(rom: "LocalRom") -> None:
# Match original Enemizer's unconditional Trinexx ice-floor removal so
# blue-head projectiles do not create solid walls in non-vanilla rooms.
rom.write_bytes(TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS, (0xEA, 0xEA, 0xEA, 0xEA))
def _apply_randomized_tile_trap_floor_tile(rom: "LocalRom") -> None:
# Original Enemizer's RandomizeTileTrapFloorTile option changes the tile
# left behind by flying floor tile traps. AP does not currently expose or
# call this option, so keep the implementation isolated and unused.
rom.write_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, (0x88, 0x01))
rom.write_byte(TILE_TRAP_FLOOR_TILE_ADDRESS, 0x12)
def _make_native_enemizer_rng(world: "ALTTPWorld") -> random.Random:
seed_material = "|".join((
str(world.multiworld.seed),
world.multiworld.seed_name,
str(world.player),
_option_key(world.options.enemy_health),
_option_key(world.options.enemy_damage),
str(int(bool(world.options.enemy_shuffle))),
str(int(bool(world.options.bush_shuffle))),
str(int(bool(world.options.killable_thieves))),
))
seed = int.from_bytes(hashlib.sha256(seed_material.encode("utf-8")).digest()[:8], "big")
return random.Random(seed)
@lru_cache(maxsize=1)
def _load_enemizer_base_patches() -> tuple[tuple[int, bytes], ...]:
return tuple(
(entry.address, entry.patch_data)
for entry in ENEMIZER_BASE_PATCHES
)
def _option_key(option: object) -> str:
return str(getattr(option, "current_key", option))
def _get_enemizer_symbol(symbol_name: str) -> int:
global _ENEMIZER_SYMBOLS
if _ENEMIZER_SYMBOLS is None:
_ENEMIZER_SYMBOLS = _load_enemizer_symbols()
return _ENEMIZER_SYMBOLS[symbol_name]
def _load_enemizer_symbols() -> dict[str, int]:
return {
name: snes_to_pc(snes_address)
for name, snes_address in ENEMIZER_SYMBOLS.items()
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -9,6 +9,7 @@ from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType
from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType
from .Bosses import place_bosses
from .Dungeons import get_dungeon_item_pool_player
from .EnemyShuffle import generate_enemy_shuffle_state
from .EntranceShuffle import connect_entrance
from .Items import item_factory, GetBeemizerItem, trap_replaceable, item_name_groups
from .Options import small_key_shuffle, compass_shuffle, big_key_shuffle, map_shuffle, TriforcePiecesMode, LTTPBosses
@@ -511,6 +512,8 @@ def generate_itempool(world: "ALTTPWorld"):
world.options.turtle_rock_medallion.current_key.title())
place_bosses(world)
if world.options.enemy_shuffle:
world.enemy_shuffle_state = generate_enemy_shuffle_state(world)
multiworld.itempool += items
+125
View File
@@ -0,0 +1,125 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from Utils import snes_to_pc
from .enemizer_data.pot_shuffle_data import POT_ROOMS
if TYPE_CHECKING:
from . import ALTTPWorld
from .Rom import LocalRom
POT_ITEM_POINTER_TABLE = 0xDB67
POT_KEY = 0x08
POT_ARROW = 0x09
POT_BLUE_RUPEE = 0x07
POT_SWITCH = 0x88
POT_HOLE = 0x80
@dataclass(frozen=True)
class PotData:
x: int
y: int
reserved: int
@dataclass(frozen=True)
class PotRoomData:
room_id: int
pots: tuple[PotData, ...]
items: tuple[int, ...]
@dataclass(frozen=True)
class FilledPot:
x: int
y: int
item: int
def generate_pot_shuffle(world: "ALTTPWorld") -> dict[int, tuple[FilledPot, ...]]:
room_data = _load_pot_room_data()
shuffled_pots: dict[int, tuple[FilledPot, ...]] = {}
for room in room_data:
room_items = [item for item in room.items if item != POT_HOLE]
if world.options.retro_bow:
room_items = [POT_BLUE_RUPEE if item == POT_ARROW else item for item in room_items]
empty_pots: list[PotData] = []
filled_pots: list[FilledPot] = []
for pot in room.pots:
if pot.reserved == 3:
filled_pots.append(FilledPot(pot.x, pot.y, POT_HOLE))
else:
empty_pots.append(pot)
while POT_KEY in room_items:
candidate_indices = [index for index, pot in enumerate(empty_pots) if pot.reserved == 1]
if not candidate_indices:
break
pot_index = world.random.choice(candidate_indices)
pot = empty_pots.pop(pot_index)
room_items.remove(POT_KEY)
filled_pots.append(FilledPot(pot.x, pot.y, POT_KEY))
while POT_SWITCH in room_items:
candidate_indices = [index for index, pot in enumerate(empty_pots) if pot.reserved == 2]
if not candidate_indices:
break
pot_index = world.random.choice(candidate_indices)
pot = empty_pots.pop(pot_index)
room_items.remove(POT_SWITCH)
filled_pots.append(FilledPot(pot.x, pot.y, POT_SWITCH))
while room_items and empty_pots:
pot_index = world.random.randrange(len(empty_pots))
item_index = world.random.randrange(len(room_items))
pot = empty_pots.pop(pot_index)
item = room_items.pop(item_index)
filled_pots.append(FilledPot(pot.x, pot.y, item))
shuffled_pots[room.room_id] = tuple(filled_pots)
return shuffled_pots
def apply_pot_shuffle(rom: "LocalRom", shuffled_pots: dict[int, tuple[FilledPot, ...]]) -> None:
for room_id, pots in shuffled_pots.items():
pointer_address = POT_ITEM_POINTER_TABLE + (room_id * 2)
snes_address = rom.read_byte(pointer_address) | (rom.read_byte(pointer_address + 1) << 8) | (0x01 << 16)
address = snes_to_pc(snes_address)
for index, pot in enumerate(pots):
rom.write_bytes(address + (index * 3), (pot.x, pot.y, pot.item))
def get_unique_pot_item_position(
shuffled_pots: dict[int, tuple[FilledPot, ...]],
room_id: int,
item: int,
) -> tuple[int, int]:
positions = [
(pot.x, pot.y)
for pot in shuffled_pots.get(room_id, ())
if pot.item == item
]
if len(positions) != 1:
raise ValueError(
f"Expected exactly one pot item {hex(item)} in room {hex(room_id)}, found {len(positions)}"
)
return positions[0]
def _load_pot_room_data() -> tuple[PotRoomData, ...]:
return tuple(
PotRoomData(
room_id=room.room_id,
pots=tuple(PotData(x=pot.x, y=pot.y, reserved=pot.reserved) for pot in room.pots),
items=room.items,
)
for room in POT_ROOMS
)
+132 -225
View File
@@ -15,7 +15,6 @@ import logging
import os
import random
import struct
import subprocess
import threading
import concurrent.futures
import bsdiff4
@@ -53,9 +52,6 @@ try:
except:
xxtea = None
enemizer_logger = logging.getLogger("Enemizer")
class LocalRom:
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
@@ -179,43 +175,6 @@ class LocalRom:
self.write_int32(startaddress + (i * 4), value)
check_lock = threading.Lock()
def check_enemizer(enemizercli):
if getattr(check_enemizer, "done", None):
return
if not os.path.exists(enemizercli) and not os.path.exists(enemizercli + ".exe"):
raise Exception(f"Enemizer not found at {enemizercli}, please install it. "
f"Such as https://github.com/Ijwu/Enemizer/releases")
with check_lock:
# some time may have passed since the lock was acquired, as such a quick re-check doesn't hurt
if getattr(check_enemizer, "done", None):
return
wanted_version = (7, 1, 0)
# version info is saved on the lib, for some reason
library_info = os.path.join(os.path.dirname(enemizercli), "EnemizerCLI.Core.deps.json")
with open(library_info) as f:
info = json.load(f)
for lib in info["libraries"]:
if lib.startswith("EnemizerLibrary/"):
version = lib.split("/")[-1]
version = tuple(int(element) for element in version.split("."))
enemizer_logger.debug(f"Found Enemizer version {version}")
if version < wanted_version:
raise Exception(
f"Enemizer found at {enemizercli} is outdated ({version}) < ({wanted_version}), "
f"please update your Enemizer. "
f"Such as from https://github.com/Ijwu/Enemizer/releases")
break
else:
raise Exception(f"Could not find Enemizer library version information in {library_info}")
check_enemizer.done = True
def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_random_on_event, sprite_pool):
userandomsprites = False
if sprite and not isinstance(sprite, Sprite):
@@ -282,174 +241,6 @@ def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_rand
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
player = world.player
check_enemizer(enemizercli)
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'))
enemizer_output_path = os.path.abspath(os.path.join(output_directory, f'enemizer_output_{player}.sfc'))
# write options file for enemizer
options = {
'RandomizeEnemies': world.options.enemy_shuffle.value,
'RandomizeEnemiesType': 3,
'RandomizeBushEnemyChance': world.options.bush_shuffle.value,
'RandomizeEnemyHealthRange': world.options.enemy_health != 'default',
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[
world.options.enemy_health.current_key],
'OHKO': False,
'RandomizeEnemyDamage': world.options.enemy_damage != 'default',
'AllowEnemyZeroDamage': True,
'ShuffleEnemyDamageGroups': world.options.enemy_damage != 'default',
'EnemyDamageChaosMode': world.options.enemy_damage == 'chaos',
'EasyModeEscape': world.options.mode == "standard",
'EnemiesAbsorbable': False,
'AbsorbableSpawnRate': 10,
'AbsorbableTypes': {
'FullMagic': True, 'SmallMagic': True, 'Bomb_1': True, 'BlueRupee': True, 'Heart': True, 'BigKey': True,
'Key': True,
'Fairy': True, 'Arrow_10': True, 'Arrow_5': True, 'Bomb_8': True, 'Bomb_4': True, 'GreenRupee': True,
'RedRupee': True
},
'BossMadness': False,
'RandomizeBosses': True,
'RandomizeBossesType': 0,
'RandomizeBossHealth': False,
'RandomizeBossHealthMinAmount': 0,
'RandomizeBossHealthMaxAmount': 300,
'RandomizeBossDamage': False,
'RandomizeBossDamageMinAmount': 0,
'RandomizeBossDamageMaxAmount': 200,
'RandomizeBossBehavior': False,
'RandomizeDungeonPalettes': False,
'SetBlackoutMode': False,
'RandomizeOverworldPalettes': False,
'RandomizeSpritePalettes': False,
'SetAdvancedSpritePalettes': False,
'PukeMode': False,
'NegativeMode': False,
'GrayscaleMode': False,
'GenerateSpoilers': False,
'RandomizeLinkSpritePalette': False,
'RandomizePots': world.options.pot_shuffle.value,
'ShuffleMusic': False,
'BootlegMagic': True,
'CustomBosses': False,
'AndyMode': False,
'HeartBeepSpeed': 0,
'AlternateGfx': False,
'ShieldGraphics': "shield_gfx/normal.gfx",
'SwordGraphics': "sword_gfx/normal.gfx",
'BeeMizer': False,
'BeesLevel': 0,
'RandomizeTileTrapPattern': False,
'RandomizeTileTrapFloorTile': False,
'AllowKillableThief': world.options.killable_thieves.value,
'RandomizeSpriteOnHit': False,
'DebugMode': False,
'DebugForceEnemy': False,
'DebugForceEnemyId': 0,
'DebugForceBoss': False,
'DebugForceBossId': 0,
'DebugOpenShutterDoors': False,
'DebugForceEnemyDamageZero': False,
'DebugShowRoomIdInRupeeCounter': False,
'UseManualBosses': True,
'ManualBosses': {
'EasternPalace': world.dungeons["Eastern Palace"].boss.enemizer_name,
'DesertPalace': world.dungeons["Desert Palace"].boss.enemizer_name,
'TowerOfHera': world.dungeons["Tower of Hera"].boss.enemizer_name,
'AgahnimsTower': 'Agahnim',
'PalaceOfDarkness': world.dungeons["Palace of Darkness"].boss.enemizer_name,
'SwampPalace': world.dungeons["Swamp Palace"].boss.enemizer_name,
'SkullWoods': world.dungeons["Skull Woods"].boss.enemizer_name,
'ThievesTown': world.dungeons["Thieves Town"].boss.enemizer_name,
'IcePalace': world.dungeons["Ice Palace"].boss.enemizer_name,
'MiseryMire': world.dungeons["Misery Mire"].boss.enemizer_name,
'TurtleRock': world.dungeons["Turtle Rock"].boss.enemizer_name,
'GanonsTower1':
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
"Inverted Ganons Tower"].bosses['bottom'].enemizer_name,
'GanonsTower2':
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
"Inverted Ganons Tower"].bosses['middle'].enemizer_name,
'GanonsTower3':
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
"Inverted Ganons Tower"].bosses['top'].enemizer_name,
'GanonsTower4': 'Agahnim2',
'Ganon': 'Ganon',
}
}
rom.write_to_file(randopatch_path)
with open(options_path, 'w') as f:
json.dump(options, f)
max_enemizer_tries = 5
for i in range(max_enemizer_tries):
enemizer_seed = str(world.random.randint(0, 999999999))
enemizer_command = [os.path.abspath(enemizercli),
'--rom', randopatch_path,
'--seed', enemizer_seed,
'--binary',
'--enemizer', options_path,
'--output', enemizer_output_path]
p_open = subprocess.Popen(enemizer_command,
cwd=os.path.dirname(enemizercli),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True)
enemizer_logger.debug(
f"Enemizer attempt {i + 1} of {max_enemizer_tries} for player {player} using enemizer seed {enemizer_seed}")
for stdout_line in iter(p_open.stdout.readline, ""):
if i == max_enemizer_tries - 1:
enemizer_logger.warning(stdout_line.rstrip())
else:
enemizer_logger.debug(stdout_line.rstrip())
p_open.stdout.close()
return_code = p_open.wait()
if return_code:
if i == max_enemizer_tries - 1:
raise subprocess.CalledProcessError(return_code, enemizer_command)
continue
for j in range(i + 1, max_enemizer_tries):
world.random.randint(0, 999999999)
# 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
break
rom.read_from_file(enemizer_output_path)
os.remove(enemizer_output_path)
if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind":
rom.write_byte(0x04DE81, 6)
rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry.
# 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.
if world.options.key_drop_shuffle:
key_drop_enemies = {
0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201,
0x4E20A, 0x4E326, 0x4E4F7, 0x4E687, 0x4E70C, 0x4E7C8, 0x4E7FA
}
for enemy in key_drop_enemies:
if rom.read_byte(enemy) == 0x12:
logging.debug(f"Moblin found and replaced at {enemy} in world {player}")
rom.write_byte(enemy, 0x8F)
for used in (randopatch_path, options_path):
try:
os.remove(used)
except OSError:
pass
tile_list_lock = threading.Lock()
_tile_collection_table = []
@@ -795,9 +586,13 @@ def get_nonnative_item_sprite(code: int) -> int:
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool):
def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int):
local_random = multiworld.worlds[player].random
local_world = multiworld.worlds[player]
enemized = bool(local_world.options.boss_shuffle or local_world.options.enemy_shuffle
or local_world.options.enemy_health != 'default' or local_world.options.enemy_damage != 'default'
or local_world.options.pot_shuffle or local_world.options.bush_shuffle
or local_world.options.killable_thieves)
# patch items
@@ -1331,6 +1126,13 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
starting_max_arrows = 30
startingstate = CollectionState(multiworld)
has_blue_shield = False
has_red_shield = False
has_mirror_shield = False
progressive_shields = 0
has_blue_mail = False
has_red_mail = False
progressive_mail = 0
if startingstate.has('Silver Bow', player):
equip[0x340] = 1
@@ -1359,18 +1161,6 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
elif startingstate.has('Fighter Sword', player):
equip[0x359] = 1
if startingstate.has('Mirror Shield', player):
equip[0x35A] = 3
elif startingstate.has('Red Shield', player):
equip[0x35A] = 2
elif startingstate.has('Blue Shield', player):
equip[0x35A] = 1
if startingstate.has('Red Mail', player):
equip[0x35B] = 2
elif startingstate.has('Blue Mail', player):
equip[0x35B] = 1
if startingstate.has('Magic Upgrade (1/4)', player):
equip[0x37B] = 2
equip[0x36E] = 0x80
@@ -1383,8 +1173,6 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
'Titans Mitts', 'Power Glove', 'Progressive Glove',
'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword',
'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield',
'Red Mail', 'Blue Mail', 'Progressive Mail',
'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)', 'Triforce Piece'}:
continue
@@ -1489,9 +1277,63 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
if item.name != 'Piece of Heart' or equip[0x36B] == 0:
equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0)
equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0)
elif item.name == 'Blue Shield':
has_blue_shield = True
continue
elif item.name == 'Red Shield':
has_red_shield = True
continue
elif item.name == 'Mirror Shield':
has_mirror_shield = True
continue
elif item.name == 'Progressive Shield':
progressive_shields += 1
continue
elif item.name == 'Blue Mail':
has_blue_mail = True
continue
elif item.name == 'Red Mail':
has_red_mail = True
continue
elif item.name == 'Progressive Mail':
progressive_mail += 1
continue
else:
raise RuntimeError(f'Unsupported item in starting equipment: {item.name}')
for _ in range(progressive_shields):
if has_mirror_shield:
continue
if has_red_shield and local_world.difficulty_requirements.progressive_shield_limit >= 3:
has_mirror_shield = True
continue
if has_blue_shield and local_world.difficulty_requirements.progressive_shield_limit >= 2:
has_red_shield = True
continue
if local_world.difficulty_requirements.progressive_shield_limit >= 1:
has_blue_shield = True
for _ in range(progressive_mail):
if has_red_mail:
continue
if has_blue_mail and local_world.difficulty_requirements.progressive_armor_limit >= 2:
has_red_mail = True
continue
if local_world.difficulty_requirements.progressive_armor_limit >= 1:
has_blue_mail = True
if has_mirror_shield:
equip[0x35A] = 3
elif has_red_shield:
equip[0x35A] = 2
elif has_blue_shield:
equip[0x35A] = 1
if has_red_mail:
equip[0x35B] = 2
elif has_blue_mail:
equip[0x35B] = 1
equip[0x343] = min(equip[0x343], starting_max_bombs)
rom.write_byte(0x180034, starting_max_bombs)
equip[0x377] = min(equip[0x377], starting_max_arrows)
@@ -1710,6 +1552,71 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
if encoded_players > ROM_PLAYER_LIMIT:
rom.write_bytes(0x195FFC + ((ROM_PLAYER_LIMIT - 1) * 32), hud_format_text("Archipelago"))
if enemized:
from . import EnemizerPatches as enemizer_patches
from .EnemyShuffle import apply_enemy_shuffle
from .PotShuffle import apply_pot_shuffle
enemizer_patches.apply_enemizer_base_patch(rom)
enemy_shuffle_enabled = bool(local_world.options.enemy_shuffle)
bush_shuffle_enabled = bool(local_world.options.bush_shuffle)
enemy_health_key = enemizer_patches._option_key(local_world.options.enemy_health)
enemy_damage_key = enemizer_patches._option_key(local_world.options.enemy_damage)
if enemy_shuffle_enabled or bush_shuffle_enabled:
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_randomize_bushes", True)
hidden_enemy_chance_pool = (
enemizer_patches.RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL
if bush_shuffle_enabled
else enemizer_patches.VANILLA_HIDDEN_ENEMY_CHANCE_POOL
)
rom.write_bytes(enemizer_patches.HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, hidden_enemy_chance_pool)
enemizer_patches._update_hidden_enemy_item_table_for_retro_mode(rom)
if enemy_shuffle_enabled:
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_randomize_sprites", True)
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_enable_mimic_override", True)
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_enable_terrorpin_ai_fix", True)
rom.write_bytes(0x1F2D5, (0x54, 0x9C))
rom.write_byte(0x1F2E5, 0xB0)
rom.write_byte(0x1F2EB, 0xD0)
if local_world.options.killable_thieves:
enemizer_patches._apply_killable_thief(rom)
if enemy_health_key != "default" or enemy_damage_key != "default":
rng = enemizer_patches._make_native_enemizer_rng(local_world)
else:
rng = None
if enemy_health_key != "default":
assert rng is not None
enemizer_patches._randomize_enemy_health(rom, rng, enemy_health_key)
if enemy_damage_key != "default":
assert rng is not None
enemizer_patches._randomize_enemy_damage(rom, rng, allow_zero_damage=True)
enemizer_patches._shuffle_damage_groups(
rom,
rng,
chaos_mode=enemy_damage_key == "chaos",
allow_zero_damage=True,
)
enemy_shuffle_state = getattr(local_world, "enemy_shuffle_state", None)
if local_world.options.enemy_shuffle and enemy_shuffle_state is not None:
apply_enemy_shuffle(rom, enemy_shuffle_state)
if local_world.options.boss_shuffle:
# Boss shuffle must run after enemy shuffle so boss room sprite pointers
# and graphics block IDs are not restored to the enemy-shuffled room values.
enemizer_patches.patch_bosses(local_world, rom)
pot_shuffle_state = getattr(local_world, "pot_shuffle_state", None)
if local_world.options.pot_shuffle and pot_shuffle_state is not None:
apply_pot_shuffle(rom, pot_shuffle_state)
# Write title screen Code
hashint = int(rom.get_hash(), 16)
code = [
@@ -1860,7 +1767,7 @@ def apply_oof_sfx(rom: LocalRom, oof: str):
rom.write_bytes(0x12803A, oof_bytes)
rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB])
# Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
# Preserve SPC $3188 instead of writing the unused "WHAT" sound effect there.
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
+8 -29
View File
@@ -14,9 +14,10 @@ from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
from .ItemPool import generate_itempool, difficulties
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
from .Options import ALTTPOptions, small_key_shuffle
from .PotShuffle import generate_pot_shuffle
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \
is_main_entrance, key_drop_data
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
from .Rom import LocalRom, patch_rom, patch_race_rom, apply_rom_settings, \
get_hash_string, get_base_rom_path, LttPDeltaPatch
from .Rules import set_rules
from .Shops import create_shops, Shop, push_shop_inventories, ShopType, price_rate_display, price_type_display_name
@@ -253,17 +254,6 @@ class ALTTPWorld(World):
create_items = generate_itempool
_enemizer_path: typing.ClassVar[typing.Optional[str]] = None
@property
def enemizer_path(self) -> str:
# TODO: directly use settings
cls = self.__class__
if cls._enemizer_path is None:
cls._enemizer_path = settings.get_settings().generator.enemizer_path
assert isinstance(cls._enemizer_path, str)
return cls._enemizer_path
# custom instance vars
dungeon_local_item_names: typing.Set[str]
dungeon_specific_item_names: typing.Set[str]
@@ -305,6 +295,8 @@ class ALTTPWorld(World):
self.required_medallions = ["Ether", "Quake"]
self.escape_assist = []
self.shops = []
self.enemy_shuffle_state = None
self.pot_shuffle_state = None
self.logical_heart_containers = 10
self.logical_heart_pieces = 24
super(ALTTPWorld, self).__init__(*args, **kwargs)
@@ -316,10 +308,6 @@ class ALTTPWorld(World):
raise FileNotFoundError(rom_file)
if multiworld.is_race:
import xxtea # noqa
for player in multiworld.get_game_players(cls.game):
if multiworld.worlds[player].use_enemizer:
check_enemizer(multiworld.worlds[player].enemizer_path)
break
def generate_early(self):
multiworld = self.multiworld
@@ -339,6 +327,9 @@ class ALTTPWorld(World):
self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
if self.options.pot_shuffle:
self.pot_shuffle_state = generate_pot_shuffle(self)
if self.options.mode == 'standard':
if self.options.small_key_shuffle:
if (self.options.small_key_shuffle not in
@@ -564,13 +555,6 @@ class ALTTPWorld(World):
def stage_generate_output(cls, multiworld, output_directory):
push_shop_inventories(multiworld)
@property
def use_enemizer(self) -> bool:
return bool(self.options.boss_shuffle or self.options.enemy_shuffle
or self.options.enemy_health != 'default' or self.options.enemy_damage != 'default'
or self.options.pot_shuffle or self.options.bush_shuffle
or self.options.killable_thieves)
def generate_output(self, output_directory: str):
multiworld = self.multiworld
player = self.player
@@ -578,14 +562,9 @@ class ALTTPWorld(World):
self.pushed_shop_inventories.wait()
try:
use_enemizer = self.use_enemizer
rom = LocalRom(get_base_rom_path())
patch_rom(multiworld, rom, player, use_enemizer)
if use_enemizer:
patch_enemizer(self, rom, self.enemizer_path, output_directory)
patch_rom(multiworld, rom, player)
if multiworld.is_race:
patch_race_rom(rom, multiworld, player)
+27
View File
@@ -0,0 +1,27 @@
These modules are vendored/generated from the upstream Enemizer compiled release and source that were already present
locally in `/home/alchav/PycharmProjects/Archipelago/EnemizerCLI` and `/home/alchav/PycharmProjects/Archipelago/Enemizer`.
Source details:
- Upstream project: `Ijwu/Enemizer`
- Release family: `7.1`
- Library version from `EnemizerCLI/EnemizerCLI.Core.deps.json`: `EnemizerLibrary/7.1.0`
Vendored data modules:
- `base_patch_data.py`
- `symbols.py`
- `enemy_room_metadata.py`
- `enemy_sprite_requirements.py`
- `overworld_enemy_metadata.py`
- `dungeon_sprite_addresses.py`
- `pot_shuffle_data.py`
Purpose:
- `base_patch_data.py` contains the generated base patch Enemizer applies before feature-specific randomization.
- `symbols.py` contains the assembled symbol map consumed by Enemizer's runtime code for ROM addresses.
- `enemy_room_metadata.py` and `overworld_enemy_metadata.py` contain room and area grouping/randomization constraints.
- `enemy_sprite_requirements.py` contains the sprite metadata used by the native enemy shuffle implementation.
- `dungeon_sprite_addresses.py` contains dungeon sprite slot metadata derived from Enemizer's source tables and keyed-enemy address list.
- `pot_shuffle_data.py` contains the native pot shuffle room/item source data.
+1
View File
@@ -0,0 +1 @@
"""Native ALTTP Enemizer data modules."""
File diff suppressed because one or more lines are too long
@@ -0,0 +1,202 @@
from __future__ import annotations
from typing import NamedTuple
class DungeonSpriteAddressData(NamedTuple):
room_id: int
sprite_id_addresses: tuple[int, ...]
DUNGEON_SPRITE_ADDRESSES = (
DungeonSpriteAddressData(room_id=2, sprite_id_addresses=(317750, 317753, 317756, 317759, 317762, 317792, 317795)),
DungeonSpriteAddressData(room_id=4, sprite_id_addresses=(317803, 317806, 317809, 317812, 317827, 317839, 317842, 317845)),
DungeonSpriteAddressData(room_id=9, sprite_id_addresses=(317904, 317907, 317910)),
DungeonSpriteAddressData(room_id=10, sprite_id_addresses=(317915, 317918, 317921, 317924, 317930, 317933)),
DungeonSpriteAddressData(room_id=11, sprite_id_addresses=(317941, 317944, 317947, 317950, 317953, 317956, 317959, 317962, 317965)),
DungeonSpriteAddressData(room_id=14, sprite_id_addresses=(317978, 317981, 317984)),
DungeonSpriteAddressData(room_id=17, sprite_id_addresses=(317992, 317995, 317998, 318001, 318004, 318007, 318010, 318013)),
DungeonSpriteAddressData(room_id=19, sprite_id_addresses=(318029, 318032, 318035, 318038, 318044, 318056, 318053, 318041)),
DungeonSpriteAddressData(room_id=21, sprite_id_addresses=(318105, 318108, 318111, 318114, 318117, 318120)),
DungeonSpriteAddressData(room_id=22, sprite_id_addresses=(318125, 318128, 318131, 318134, 318137, 318140, 318143)),
DungeonSpriteAddressData(room_id=23, sprite_id_addresses=(318157, 318160, 318163, 318166, 318169, 318172)),
DungeonSpriteAddressData(room_id=25, sprite_id_addresses=(318177, 318180, 318183, 318186)),
DungeonSpriteAddressData(room_id=26, sprite_id_addresses=(318191, 318194, 318197, 318200, 318203, 318206, 318209, 318212, 318218)),
DungeonSpriteAddressData(room_id=27, sprite_id_addresses=(318232, 318235, 318238, 318241)),
DungeonSpriteAddressData(room_id=30, sprite_id_addresses=(318284, 318287, 318290, 318293, 318296, 318299)),
DungeonSpriteAddressData(room_id=31, sprite_id_addresses=(318304, 318307, 318310, 318313, 318316, 318319, 318322, 318325)),
DungeonSpriteAddressData(room_id=33, sprite_id_addresses=(318335, 318341, 318344, 318347, 318350, 318353, 318356, 318359, 318362, 318365, 318368)),
DungeonSpriteAddressData(room_id=34, sprite_id_addresses=(318373, 318376, 318379, 318382, 318385, 318388, 318391)),
DungeonSpriteAddressData(room_id=36, sprite_id_addresses=(318413, 318416, 318419, 318422, 318425, 318428, 318431)),
DungeonSpriteAddressData(room_id=38, sprite_id_addresses=(318471, 318438, 318441, 318444, 318447, 318450, 318453, 318459, 318462, 318465, 318468)),
DungeonSpriteAddressData(room_id=39, sprite_id_addresses=(318476, 318479, 318482, 318485, 318488, 318491, 318494)),
DungeonSpriteAddressData(room_id=40, sprite_id_addresses=(318511,)),
DungeonSpriteAddressData(room_id=42, sprite_id_addresses=(318530, 318533, 318536, 318539, 318542, 318545)),
DungeonSpriteAddressData(room_id=43, sprite_id_addresses=(318556, 318559, 318562, 318565, 318568, 318571)),
DungeonSpriteAddressData(room_id=46, sprite_id_addresses=(318590, 318593, 318596, 318599, 318602, 318605)),
DungeonSpriteAddressData(room_id=49, sprite_id_addresses=(318621, 318624, 318627, 318630, 318633, 318636, 318639, 318642, 318645, 318648)),
DungeonSpriteAddressData(room_id=50, sprite_id_addresses=(318653, 318656, 318659, 318662, 318665)),
DungeonSpriteAddressData(room_id=52, sprite_id_addresses=(318681, 318684, 318687, 318693, 318696, 318699, 318690)),
DungeonSpriteAddressData(room_id=53, sprite_id_addresses=(318710, 318713, 318716, 318719, 318722, 318728, 318731, 318734, 318725)),
DungeonSpriteAddressData(room_id=54, sprite_id_addresses=(318742, 318745, 318754, 318760, 318763)),
DungeonSpriteAddressData(room_id=55, sprite_id_addresses=(318777, 318780, 318783, 318786, 318792, 318795, 318798, 318801, 318789)),
DungeonSpriteAddressData(room_id=56, sprite_id_addresses=(318806, 318809, 318812, 318815, 318818, 318821, 318824)),
DungeonSpriteAddressData(room_id=57, sprite_id_addresses=(318829, 318835, 318841, 318844, 318847, 318850)),
DungeonSpriteAddressData(room_id=58, sprite_id_addresses=(318855, 318858, 318861, 318864, 318867, 318870)),
DungeonSpriteAddressData(room_id=59, sprite_id_addresses=(318875, 318878, 318881, 318884, 318887, 318890, 318893)),
DungeonSpriteAddressData(room_id=60, sprite_id_addresses=(318898, 318901, 318904)),
DungeonSpriteAddressData(room_id=61, sprite_id_addresses=(318915, 318921, 318924, 318927, 318930, 318933, 318939, 318942, 318945, 318948, 318951)),
DungeonSpriteAddressData(room_id=62, sprite_id_addresses=(318959, 318962, 318980, 318983, 318989, 318992)),
DungeonSpriteAddressData(room_id=63, sprite_id_addresses=(319000, 319006, 319009)),
DungeonSpriteAddressData(room_id=64, sprite_id_addresses=(319014, 319017, 319023, 319026, 319029)),
DungeonSpriteAddressData(room_id=65, sprite_id_addresses=(319036, 319039, 319042, 319045)),
DungeonSpriteAddressData(room_id=66, sprite_id_addresses=(319050, 319053, 319056, 319059, 319062, 319065)),
DungeonSpriteAddressData(room_id=67, sprite_id_addresses=(319070, 319073)),
DungeonSpriteAddressData(room_id=68, sprite_id_addresses=(319084, 319087, 319090, 319093, 319096, 319102)),
DungeonSpriteAddressData(room_id=69, sprite_id_addresses=(319110, 319116, 319119, 319131, 319134, 319137, 319113, 319122, 319125, 319128)),
DungeonSpriteAddressData(room_id=70, sprite_id_addresses=(319142, 319148, 319154)),
DungeonSpriteAddressData(room_id=73, sprite_id_addresses=(319161, 319164, 319167, 319170, 319173, 319176, 319182, 319185, 319188, 319191, 319194, 319197)),
DungeonSpriteAddressData(room_id=74, sprite_id_addresses=(319205, 319208)),
DungeonSpriteAddressData(room_id=75, sprite_id_addresses=(319213, 319216, 319219, 319222, 319225, 319228, 319231, 319234)),
DungeonSpriteAddressData(room_id=76, sprite_id_addresses=(319245, 319248, 319251, 319254, 319257, 319260)),
DungeonSpriteAddressData(room_id=78, sprite_id_addresses=(319270, 319273, 319276, 319279)),
DungeonSpriteAddressData(room_id=80, sprite_id_addresses=(319295, 319298, 319301)),
DungeonSpriteAddressData(room_id=81, sprite_id_addresses=(319309, 319312)),
DungeonSpriteAddressData(room_id=82, sprite_id_addresses=(319317, 319320, 319323)),
DungeonSpriteAddressData(room_id=83, sprite_id_addresses=(319328, 319331, 319334, 319337, 319340, 319343, 319346, 319349, 319352, 319355, 319358, 319361, 319364)),
DungeonSpriteAddressData(room_id=84, sprite_id_addresses=(319369, 319372, 319375, 319378, 319381, 319384, 319387, 319390)),
DungeonSpriteAddressData(room_id=85, sprite_id_addresses=(319398, 319401)),
DungeonSpriteAddressData(room_id=86, sprite_id_addresses=(319415, 319418, 319421, 319424, 319430, 319433, 319436, 319442, 319439)),
DungeonSpriteAddressData(room_id=87, sprite_id_addresses=(319447, 319450, 319453, 319456, 319459, 319462, 319468, 319471, 319474, 319477, 319480, 319483, 319486, 319489)),
DungeonSpriteAddressData(room_id=88, sprite_id_addresses=(319497, 319500, 319506, 319509, 319515, 319518, 319521)),
DungeonSpriteAddressData(room_id=89, sprite_id_addresses=(319526, 319529, 319538, 319544, 319547, 319550, 319553, 319556, 319559, 319541)),
DungeonSpriteAddressData(room_id=91, sprite_id_addresses=(319575, 319578, 319581, 319584)),
DungeonSpriteAddressData(room_id=93, sprite_id_addresses=(319615, 319618, 319621, 319624, 319627, 319633, 319636, 319639, 319651, 319642, 319645, 319648, 319630)),
DungeonSpriteAddressData(room_id=94, sprite_id_addresses=(319659, 319662, 319665, 319668)),
DungeonSpriteAddressData(room_id=95, sprite_id_addresses=(319673, 319676, 319679)),
DungeonSpriteAddressData(room_id=96, sprite_id_addresses=(319684,)),
DungeonSpriteAddressData(room_id=97, sprite_id_addresses=(319689, 319692, 319695)),
DungeonSpriteAddressData(room_id=98, sprite_id_addresses=(319700, 319703, 319706)),
DungeonSpriteAddressData(room_id=99, sprite_id_addresses=(319714, 319711)),
DungeonSpriteAddressData(room_id=100, sprite_id_addresses=(319719, 319725, 319728, 319731, 319734, 319737)),
DungeonSpriteAddressData(room_id=101, sprite_id_addresses=(319760, 319763, 319766, 319769, 319772)),
DungeonSpriteAddressData(room_id=102, sprite_id_addresses=(319777, 319783, 319786, 319795, 319798, 319801, 319804, 319810)),
DungeonSpriteAddressData(room_id=103, sprite_id_addresses=(319818, 319821, 319824, 319827, 319830, 319833, 319836, 319839, 319842)),
DungeonSpriteAddressData(room_id=104, sprite_id_addresses=(319859, 319865, 319868)),
DungeonSpriteAddressData(room_id=106, sprite_id_addresses=(319873, 319876, 319879, 319882, 319885, 319888)),
DungeonSpriteAddressData(room_id=107, sprite_id_addresses=(319899, 319902, 319905, 319911, 319914, 319917, 319920, 319923, 319926, 319929, 319932)),
DungeonSpriteAddressData(room_id=109, sprite_id_addresses=(319954, 319957, 319960, 319963, 319966, 319969, 319972, 319975, 319978)),
DungeonSpriteAddressData(room_id=110, sprite_id_addresses=(319983, 319986, 319989, 319992, 319995)),
DungeonSpriteAddressData(room_id=113, sprite_id_addresses=(320000, 320003)),
DungeonSpriteAddressData(room_id=114, sprite_id_addresses=(320011, 320017)),
DungeonSpriteAddressData(room_id=115, sprite_id_addresses=(320022, 320025, 320028, 320031, 320034, 320037)),
DungeonSpriteAddressData(room_id=116, sprite_id_addresses=(320045, 320048, 320051, 320054, 320057, 320060, 320063, 320066)),
DungeonSpriteAddressData(room_id=117, sprite_id_addresses=(320071, 320074, 320077, 320080, 320083, 320086, 320095, 320098)),
DungeonSpriteAddressData(room_id=118, sprite_id_addresses=(320106, 320109, 320112, 320115, 320121)),
DungeonSpriteAddressData(room_id=119, sprite_id_addresses=(320126, 320138, 320141)),
DungeonSpriteAddressData(room_id=123, sprite_id_addresses=(320146, 320149, 320152, 320155, 320158, 320161, 320167, 320170, 320173, 320176)),
DungeonSpriteAddressData(room_id=124, sprite_id_addresses=(320181, 320184, 320187, 320190, 320193, 320196)),
DungeonSpriteAddressData(room_id=125, sprite_id_addresses=(320216, 320219, 320225, 320228, 320234, 320222, 320231, 320204, 320207, 320210, 320213, 320222, 320231)),
DungeonSpriteAddressData(room_id=126, sprite_id_addresses=(320242, 320245, 320254, 320257)),
DungeonSpriteAddressData(room_id=128, sprite_id_addresses=(320291, 320294)),
DungeonSpriteAddressData(room_id=129, sprite_id_addresses=(320302, 320305)),
DungeonSpriteAddressData(room_id=130, sprite_id_addresses=(320310, 320313, 320316)),
DungeonSpriteAddressData(room_id=131, sprite_id_addresses=(320321, 320324, 320327, 320330, 320333, 320336, 320339, 320342, 320345, 320348)),
DungeonSpriteAddressData(room_id=132, sprite_id_addresses=(320353, 320356, 320359, 320362, 320365, 320368, 320371)),
DungeonSpriteAddressData(room_id=133, sprite_id_addresses=(320376, 320379, 320382, 320385, 320388, 320391, 320394, 320397, 320400, 320403)),
DungeonSpriteAddressData(room_id=135, sprite_id_addresses=(320410, 320413, 320416, 320419, 320434, 320437, 320440, 320446, 320422)),
DungeonSpriteAddressData(room_id=139, sprite_id_addresses=(320468, 320471, 320474, 320477, 320480)),
DungeonSpriteAddressData(room_id=140, sprite_id_addresses=(320503, 320506, 320509, 320512, 320518, 320521, 320527, 320524, 320515)),
DungeonSpriteAddressData(room_id=141, sprite_id_addresses=(320538, 320541, 320544, 320547, 320550, 320556, 320559, 320562, 320565, 320568, 320571, 320535)),
DungeonSpriteAddressData(room_id=142, sprite_id_addresses=(320579, 320582, 320585, 320588, 320591, 320594, 320597)),
DungeonSpriteAddressData(room_id=145, sprite_id_addresses=(320610, 320616, 320619, 320622, 320625, 320613)),
DungeonSpriteAddressData(room_id=146, sprite_id_addresses=(320636, 320639, 320642, 320645, 320648, 320654, 320657, 320660, 320663)),
DungeonSpriteAddressData(room_id=147, sprite_id_addresses=(320668, 320671, 320674, 320677, 320680, 320683, 320686, 320689)),
DungeonSpriteAddressData(room_id=149, sprite_id_addresses=(320694, 320697, 320700, 320703)),
DungeonSpriteAddressData(room_id=151, sprite_id_addresses=(320728,)),
DungeonSpriteAddressData(room_id=152, sprite_id_addresses=(320733, 320736, 320739, 320742, 320745)),
DungeonSpriteAddressData(room_id=153, sprite_id_addresses=(320750, 320753, 320756, 320759, 320765, 320768, 320771, 320774, 320777, 320780)),
DungeonSpriteAddressData(room_id=155, sprite_id_addresses=(320794, 320797, 320800, 320803, 320806, 320809, 320812, 320815, 320818, 320821)),
DungeonSpriteAddressData(room_id=156, sprite_id_addresses=(320826, 320829, 320832, 320835, 320838, 320841)),
DungeonSpriteAddressData(room_id=157, sprite_id_addresses=(320852, 320855, 320858, 320861, 320864, 320867, 320870, 320873)),
DungeonSpriteAddressData(room_id=158, sprite_id_addresses=(320878, 320881, 320884, 320887)),
DungeonSpriteAddressData(room_id=159, sprite_id_addresses=(320907, 320910)),
DungeonSpriteAddressData(room_id=160, sprite_id_addresses=(320915, 320918, 320921)),
DungeonSpriteAddressData(room_id=161, sprite_id_addresses=(320929, 320932, 320935, 320938, 320941, 320944, 320947, 320950)),
DungeonSpriteAddressData(room_id=165, sprite_id_addresses=(320968, 320971, 320974, 320977, 320980, 320983, 320986, 320989, 320998, 321001)),
DungeonSpriteAddressData(room_id=167, sprite_id_addresses=(321014, 321017)),
DungeonSpriteAddressData(room_id=168, sprite_id_addresses=(321022, 321025, 321028, 321031, 321034)),
DungeonSpriteAddressData(room_id=169, sprite_id_addresses=(321039, 321042, 321057, 321060, 321045, 321048, 321051, 321054)),
DungeonSpriteAddressData(room_id=170, sprite_id_addresses=(321065, 321068, 321071, 321074, 321077, 321080)),
DungeonSpriteAddressData(room_id=171, sprite_id_addresses=(321088, 321091, 321094, 321097, 321100, 321103, 321106)),
DungeonSpriteAddressData(room_id=174, sprite_id_addresses=(321116, 321119)),
DungeonSpriteAddressData(room_id=176, sprite_id_addresses=(321129, 321132, 321135, 321138, 321141, 321144, 321147, 321150, 321153, 321156, 321159, 321165, 321168)),
DungeonSpriteAddressData(room_id=177, sprite_id_addresses=(321173, 321176, 321179, 321182, 321185, 321188, 321191, 321194, 321197, 321200)),
DungeonSpriteAddressData(room_id=178, sprite_id_addresses=(321205, 321208, 321211, 321214, 321217, 321220, 321223, 321226, 321229, 321232, 321235, 321238, 321241, 321244)),
DungeonSpriteAddressData(room_id=179, sprite_id_addresses=(321249, 321252, 321255, 321258, 321261)),
DungeonSpriteAddressData(room_id=182, sprite_id_addresses=(321277, 321280, 321289, 321292, 321301, 321304)),
DungeonSpriteAddressData(room_id=183, sprite_id_addresses=(321309, 321312)),
DungeonSpriteAddressData(room_id=184, sprite_id_addresses=(321317, 321320, 321323, 321326, 321329, 321332)),
DungeonSpriteAddressData(room_id=186, sprite_id_addresses=(321342, 321345, 321348, 321351, 321354, 321357, 321360)),
DungeonSpriteAddressData(room_id=187, sprite_id_addresses=(321365, 321368, 321371, 321374, 321377, 321380, 321386, 321389, 321392, 321395, 321383)),
DungeonSpriteAddressData(room_id=188, sprite_id_addresses=(321403, 321406, 321409, 321412, 321418, 321421, 321424, 321433, 321400, 321415, 321427, 321430)),
DungeonSpriteAddressData(room_id=190, sprite_id_addresses=(321440, 321446, 321449, 321452, 321455, 321458)),
DungeonSpriteAddressData(room_id=192, sprite_id_addresses=(321471, 321474, 321477, 321480, 321486, 321489, 321492, 321495)),
DungeonSpriteAddressData(room_id=193, sprite_id_addresses=(321503, 321506, 321509, 321512, 321518, 321524, 321527, 321530, 321521, 321515, 321536)),
DungeonSpriteAddressData(room_id=194, sprite_id_addresses=(321547, 321550, 321553, 321556, 321562, 321559, 321544, 321541)),
DungeonSpriteAddressData(room_id=195, sprite_id_addresses=(321567, 321585, 321588)),
DungeonSpriteAddressData(room_id=196, sprite_id_addresses=(321605, 321608, 321611, 321614, 321617, 321620)),
DungeonSpriteAddressData(room_id=201, sprite_id_addresses=(321697, 321700, 321703)),
DungeonSpriteAddressData(room_id=203, sprite_id_addresses=(321708, 321717, 321720, 321723, 321726, 321729, 321732, 321735, 321738, 321741, 321714, 321711)),
DungeonSpriteAddressData(room_id=204, sprite_id_addresses=(321746, 321749, 321755, 321758, 321761, 321770, 321773, 321776, 321779, 321782, 321785, 321752, 321764, 321767)),
DungeonSpriteAddressData(room_id=206, sprite_id_addresses=(321790, 321793, 321799, 321802, 321805, 321808, 321811)),
DungeonSpriteAddressData(room_id=208, sprite_id_addresses=(321816, 321819, 321822, 321825, 321828, 321831, 321834, 321837, 321840, 321843, 321846)),
DungeonSpriteAddressData(room_id=209, sprite_id_addresses=(321851, 321854, 321857, 321860, 321863, 321866, 321869, 321872)),
DungeonSpriteAddressData(room_id=210, sprite_id_addresses=(321877, 321880, 321883, 321886, 321889, 321892, 321895, 321898, 321901, 321904)),
DungeonSpriteAddressData(room_id=216, sprite_id_addresses=(321937, 321940, 321943, 321946, 321949, 321952, 321955, 321958, 321961, 321964, 321967)),
DungeonSpriteAddressData(room_id=217, sprite_id_addresses=(321975, 321978, 321981, 321972)),
DungeonSpriteAddressData(room_id=218, sprite_id_addresses=(321986, 321989)),
DungeonSpriteAddressData(room_id=219, sprite_id_addresses=(321994, 321997, 322000, 322006, 322003, 322012, 322009)),
DungeonSpriteAddressData(room_id=220, sprite_id_addresses=(322020, 322023, 322026, 322029, 322032, 322035, 322047, 322017, 322038, 322041, 322044)),
DungeonSpriteAddressData(room_id=223, sprite_id_addresses=(322063, 322066)),
DungeonSpriteAddressData(room_id=224, sprite_id_addresses=(322071, 322074, 322077, 322080)),
DungeonSpriteAddressData(room_id=232, sprite_id_addresses=(322189, 322192, 322195, 322198)),
DungeonSpriteAddressData(room_id=238, sprite_id_addresses=(322213, 322216, 322219, 322222, 322225)),
DungeonSpriteAddressData(room_id=239, sprite_id_addresses=(322230, 322233, 322236)),
DungeonSpriteAddressData(room_id=249, sprite_id_addresses=(322323, 322326, 322329, 322332)),
DungeonSpriteAddressData(room_id=254, sprite_id_addresses=(322378, 322381, 322384, 322387, 322390)),
DungeonSpriteAddressData(room_id=263, sprite_id_addresses=(322444, 322447)),
DungeonSpriteAddressData(room_id=264, sprite_id_addresses=(322452, 322455, 322458, 322461)),
DungeonSpriteAddressData(room_id=267, sprite_id_addresses=(322494,)),
DungeonSpriteAddressData(room_id=269, sprite_id_addresses=(322525, 322528)),
DungeonSpriteAddressData(room_id=291, sprite_id_addresses=(322671, 322674, 322677, 322680)),
)
KEYED_SPRITE_ID_ADDRESSES = frozenset((317984,
318044,
318335,
318835,
318915,
318983,
320003,
320011,
320294,
320759,
321292,
321480,
321530,
320000,
321159,
321937,
321940,
321943,
321946,
321949,
321952,
321955,
321958,
321961,
321964,
321967,
321424,
321421,
321418))
@@ -0,0 +1,106 @@
from __future__ import annotations
from typing import NamedTuple, Optional
class RoomGroupRequirementData(NamedTuple):
group_id: Optional[int]
subgroup_0: Optional[int]
subgroup_1: Optional[int]
subgroup_2: Optional[int]
subgroup_3: Optional[int]
rooms: tuple[int, ...]
SHUTTER_ROOM_IDS = frozenset((184,
11,
27,
75,
4,
36,
182,
40,
14,
46,
62,
110,
49,
135,
68,
69,
83,
117,
133,
61,
93,
107,
109,
123,
125,
141,
150,
165,
113,
168,
216,
176,
192,
224,
178,
210,
239,
268,
291))
WATER_ROOM_IDS = frozenset((22, 40, 52, 54, 56, 70, 102))
DONT_RANDOMIZE_ROOM_IDS = frozenset((0, 1, 3, 13, 20, 32, 48, 127))
NO_SPECIAL_ENEMIES_STANDARD_ROOM_IDS = frozenset((1, 2, 17, 33, 34, 50, 65, 66, 80, 81, 82, 85, 96, 97, 98, 112, 113, 114, 128, 129, 130))
BOSS_ROOM_IDS = frozenset((200, 51, 108, 7, 77, 90, 6, 41, 172, 222, 144, 164, 32, 13, 0))
ROOM_GROUP_REQUIREMENTS = (
RoomGroupRequirementData(group_id=1, subgroup_0=70, subgroup_1=73, subgroup_2=28, subgroup_3=82, rooms=(228, 240)),
RoomGroupRequirementData(group_id=5, subgroup_0=75, subgroup_1=77, subgroup_2=74, subgroup_3=90, rooms=(243, 265, 270, 271, 272, 273, 282, 284, 290)),
RoomGroupRequirementData(group_id=None, subgroup_0=75, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(255, 274, 287)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=77, subgroup_2=None, subgroup_3=21, rooms=(289,)),
RoomGroupRequirementData(group_id=7, subgroup_0=75, subgroup_1=77, subgroup_2=57, subgroup_3=54, rooms=(8, 44, 276, 277)),
RoomGroupRequirementData(group_id=13, subgroup_0=81, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(85, 258, 260)),
RoomGroupRequirementData(group_id=14, subgroup_0=71, subgroup_1=73, subgroup_2=76, subgroup_3=80, rooms=(18, 261, 266)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=80, rooms=(264,)),
RoomGroupRequirementData(group_id=15, subgroup_0=79, subgroup_1=77, subgroup_2=74, subgroup_3=80, rooms=(244, 245, 257, 259, 262, 280, 281)),
RoomGroupRequirementData(group_id=18, subgroup_0=85, subgroup_1=61, subgroup_2=66, subgroup_3=67, rooms=(32, 48)),
RoomGroupRequirementData(group_id=24, subgroup_0=85, subgroup_1=26, subgroup_2=66, subgroup_3=67, rooms=(13,)),
RoomGroupRequirementData(group_id=34, subgroup_0=33, subgroup_1=65, subgroup_2=69, subgroup_3=51, rooms=(0,)),
RoomGroupRequirementData(group_id=40, subgroup_0=14, subgroup_1=None, subgroup_2=74, subgroup_3=80, rooms=(225, 256, 293, 292, 294)),
RoomGroupRequirementData(group_id=None, subgroup_0=14, subgroup_1=30, subgroup_2=None, subgroup_3=None, rooms=(291,)),
RoomGroupRequirementData(group_id=23, subgroup_0=64, subgroup_1=None, subgroup_2=None, subgroup_3=63, rooms=()),
RoomGroupRequirementData(group_id=9, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=29, rooms=(227,)),
RoomGroupRequirementData(group_id=11, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=61, rooms=()),
RoomGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=49, rooms=()),
RoomGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=60, subgroup_3=None, rooms=()),
RoomGroupRequirementData(group_id=21, subgroup_0=None, subgroup_1=None, subgroup_2=58, subgroup_3=62, rooms=()),
RoomGroupRequirementData(group_id=28, subgroup_0=None, subgroup_1=None, subgroup_2=38, subgroup_3=82, rooms=(14, 126, 142, 158, 190)),
RoomGroupRequirementData(group_id=12, subgroup_0=None, subgroup_1=None, subgroup_2=48, subgroup_3=None, rooms=()),
RoomGroupRequirementData(group_id=26, subgroup_0=None, subgroup_1=None, subgroup_2=56, subgroup_3=None, rooms=()),
RoomGroupRequirementData(group_id=20, subgroup_0=None, subgroup_1=None, subgroup_2=57, subgroup_3=None, rooms=()),
RoomGroupRequirementData(group_id=32, subgroup_0=None, subgroup_1=44, subgroup_2=59, subgroup_3=None, rooms=()),
RoomGroupRequirementData(group_id=3, subgroup_0=93, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(81,)),
RoomGroupRequirementData(group_id=42, subgroup_0=21, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(286,)),
RoomGroupRequirementData(group_id=10, subgroup_0=47, subgroup_1=None, subgroup_2=46, subgroup_3=None, rooms=(92, 117, 185, 217)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=34, subgroup_3=None, rooms=(54, 70, 102, 118)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=32, subgroup_2=None, subgroup_3=None, rooms=(62, 159)),
RoomGroupRequirementData(group_id=None, subgroup_0=31, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(127,)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=35, subgroup_3=None, rooms=(57, 73, 86, 87, 104, 141)),
RoomGroupRequirementData(group_id=37, subgroup_0=31, subgroup_1=None, subgroup_2=39, subgroup_3=82, rooms=(36, 180, 181, 198, 199, 214)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(23, 42, 68, 76, 86, 88, 89, 103, 104, 126, 139, 235, 251)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(23, 42, 76, 89, 103, 104, 126, 139, 235, 251)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(11, 19, 27, 30, 42, 43, 49, 61, 62, 91, 107, 119, 135, 139, 145, 146, 155, 157, 161, 171, 182, 191, 193, 196, 239)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(11, 19, 27, 30, 42, 43, 49, 53, 62, 91, 107, 119, 135, 139, 145, 146, 155, 157, 161, 171, 182, 191, 193, 196, 239)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(19, 35, 150, 165, 195, 197, 213)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(19, 35, 150, 165, 197, 213)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(26, 38, 43, 64, 74, 87, 107, 123)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(38, 43, 64, 74, 87, 107, 123, 206)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(2, 88, 100, 140, 267)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(26, 61, 68, 86, 94, 124, 149, 195)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(4, 63, 206)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(53, 55, 118)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=34, subgroup_3=None, rooms=(40,)),
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=37, subgroup_3=None, rooms=(151,)),
)
@@ -0,0 +1,295 @@
from __future__ import annotations
from typing import NamedTuple, Optional
class EnemySpriteRequirementData(NamedTuple):
sprite_name: str
sprite_id: int
boss: bool
overlord: bool
do_not_randomize: bool
killable: bool
npc: bool
never_use_dungeon: bool
never_use_overworld: bool
cannot_have_key: bool
is_object: bool
absorbable: bool
is_water_sprite: bool
is_enemy_sprite: bool
group_ids: tuple[int, ...]
subgroup_0: tuple[int, ...]
subgroup_1: tuple[int, ...]
subgroup_2: tuple[int, ...]
subgroup_3: tuple[int, ...]
parameters: Optional[int]
special_glitched: bool
excluded_rooms: tuple[int, ...]
dont_randomize_rooms: tuple[int, ...]
spawnable_rooms: tuple[int, ...]
ENEMY_SPRITE_REQUIREMENTS = (
EnemySpriteRequirementData(sprite_name='RavenSprite', sprite_id=0, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17, 25), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='VultureSprite', sprite_id=1, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='EmptySprite', sprite_id=3, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PullSwitch_GoodSprite', sprite_id=4, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PullSwitch_TrapSprite', sprite_id=6, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Octorok_OneWaySprite', sprite_id=8, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12, 24), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MoldormSprite', sprite_id=9, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(48,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Octorok_FourWaySprite', sprite_id=10, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ChickenSprite', sprite_id=11, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(21, 80), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BuzzblobSprite', sprite_id=13, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SnapdragonSprite', sprite_id=14, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(23,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OctoballoonSprite', sprite_id=15, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OctoballoonHatchlingsSprite', sprite_id=16, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HinoxSprite', sprite_id=17, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MoblinSprite', sprite_id=18, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(23,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MiniHelmasaurSprite', sprite_id=19, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GargoylesDomainGateSprite', sprite_id=20, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AntifairySprite', sprite_id=21, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(64, 210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SahasrahlaAginahSprite', sprite_id=22, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(76,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BushHoarderSprite', sprite_id=23, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MiniMoldormSprite', sprite_id=24, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PoeSprite', sprite_id=25, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(14, 21), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='DwarvesSprite', sprite_id=26, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(), subgroup_3=(21,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArrowInWall_MaybeSprite', sprite_id=27, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='StatueSprite', sprite_id=28, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268, 63), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WeathervaneSprite', sprite_id=29, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CrystalSwitchSprite', sprite_id=30, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BugCatchingKidSprite', sprite_id=31, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(81,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SluggulaSprite', sprite_id=32, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PushSwitchSprite', sprite_id=33, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(83,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RopaSprite', sprite_id=34, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedBariSprite', sprite_id=35, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(127,), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BlueBariSprite', sprite_id=36, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(127,), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='TalkingTreeSprite', sprite_id=37, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(21,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HardhatBeetleSprite', sprite_id=38, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='DeadrockSprite', sprite_id=39, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(127, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='StorytellersSprite', sprite_id=40, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BlindHideoutAttendantSprite', sprite_id=41, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14, 79), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SweepingLadySprite', sprite_id=42, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MultipurposeSpriteSprite', sprite_id=43, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LumberjacksSprite', sprite_id=44, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(74,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='TelepathicStones_NoIdeaWhatThisActuallyIsLikelyUnusedSprite', sprite_id=45, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FluteBoysNotesSprite', sprite_id=46, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RaceHPNPCsSprite', sprite_id=47, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Person_MaybeSprite', sprite_id=48, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FortuneTellerSprite', sprite_id=49, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AngryBrothersSprite', sprite_id=50, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PullForRupeesSpriteSprite', sprite_id=51, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ScaredGirl2Sprite', sprite_id=52, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='InnkeeperSprite', sprite_id=53, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WitchSprite', sprite_id=54, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(76,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WaterfallSprite', sprite_id=55, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArrowTargetSprite', sprite_id=56, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AverageMiddleAgedManSprite', sprite_id=57, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HalfMagicBatSprite', sprite_id=58, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(29,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='DashItemSprite', sprite_id=59, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='VillageKidSprite', sprite_id=60, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Signs_ChickenLadyAlsoShowedUp_ScaredLadiesOutsideHousesSprite', sprite_id=61, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RockHoarderSprite', sprite_id=62, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='TutorialSoldierSprite', sprite_id=63, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LightningLockSprite', sprite_id=64, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BlueSwordSoldier_DetectPlayerSprite', sprite_id=65, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GreenSwordSoldierSprite', sprite_id=66, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedSpearSoldierSprite', sprite_id=67, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AssaultSwordSoldierSprite', sprite_id=68, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GreenSpearSoldierSprite', sprite_id=69, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BlueArcherSprite', sprite_id=70, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(72,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GreenArcherSprite', sprite_id=71, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(72,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedJavelinSoldierSprite', sprite_id=72, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedJavelinSoldier2Sprite', sprite_id=73, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedBombSoldiersSprite', sprite_id=74, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GreenSoldierRecruits_HMKnightSprite', sprite_id=75, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(73,), subgroup_2=(19,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GeldmanSprite', sprite_id=76, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RabbitSprite', sprite_id=77, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PopoSprite', sprite_id=78, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Popo2Sprite', sprite_id=79, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CannonBallsSprite', sprite_id=80, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArmosSprite', sprite_id=81, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GiantZoraSprite', sprite_id=82, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(68,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArmosKnightsSprite', sprite_id=83, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(29,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LanmolasSprite', sprite_id=84, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(49,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FireballZoraSprite', sprite_id=85, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12, 24), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WalkingZoraSprite', sprite_id=86, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(68,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='DesertPalaceBarriersSprite', sprite_id=87, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CrabSprite', sprite_id=88, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BirdSprite', sprite_id=89, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SquirrelSprite', sprite_id=90, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Spark_LeftToRightSprite', sprite_id=91, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Spark_RightToLeftSprite', sprite_id=92, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Roller_VerticalMovingSprite', sprite_id=93, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Roller_VerticalMoving2Sprite', sprite_id=94, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RollerSprite', sprite_id=95, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Roller_HorizontalMovingSprite', sprite_id=96, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BeamosSprite', sprite_id=97, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MasterSwordSprite', sprite_id=98, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Devalant_NonShooterSprite', sprite_id=99, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Devalant_ShooterSprite', sprite_id=100, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ShootingGalleryProprietorSprite', sprite_id=101, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_RightSprite', sprite_id=102, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_LeftSprite', sprite_id=103, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_DownSprite', sprite_id=104, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_UpSprite', sprite_id=105, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BallNChainTrooperSprite', sprite_id=106, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CannonSoldierSprite', sprite_id=107, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MirrorPortalSprite', sprite_id=108, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RatSprite', sprite_id=109, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RopeSprite', sprite_id=110, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='KeeseSprite', sprite_id=111, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LeeverSprite', sprite_id=113, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ActivatoForThePonds_WhereYouThrowInItemsSprite', sprite_id=114, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='UnclePriestSprite', sprite_id=115, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(71, 81), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RunningManSprite', sprite_id=116, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BottleSalesmanSprite', sprite_id=117, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PrincessZeldaSprite', sprite_id=118, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='VillageElderSprite', sprite_id=120, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AgahnimSprite', sprite_id=122, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(85,), subgroup_1=(26, 61), subgroup_2=(66,), subgroup_3=(67,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AgahnimEnergyBallSprite', sprite_id=123, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FloatingStalfosHeadSprite', sprite_id=124, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BigSpikeTrapSprite', sprite_id=125, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GuruguruBar_ClockwiseSprite', sprite_id=126, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(181, 150), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GuruguruBar_CounterClockwiseSprite', sprite_id=127, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(181, 150), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WinderSprite', sprite_id=128, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WaterTektiteSprite', sprite_id=129, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(40,), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AntifairyCircleSprite', sprite_id=130, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GreenEyegoreSprite', sprite_id=131, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedEyegoreSprite', sprite_id=132, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='KodongosSprite', sprite_id=134, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(42,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MothulaSprite', sprite_id=136, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(56,), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MothulasBeamSprite', sprite_id=137, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(56,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SpikeTrapSprite', sprite_id=138, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(40, 11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GibdoSprite', sprite_id=139, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArrghusSprite', sprite_id=140, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArrghusSpawnSprite', sprite_id=141, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='TerrorpinSprite', sprite_id=142, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(42,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SlimeSprite_JumpsOutOfTheFloor', sprite_id=143, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WallmasterSprite', sprite_id=144, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(1, 2, 17, 33, 34, 50, 65, 66, 80, 81, 82, 96, 97, 98, 112, 113, 114, 128, 129, 130, 137, 153, 168, 169, 170, 184, 185, 186, 200, 201, 216, 217, 218, 51, 67, 83, 99, 115, 116, 117, 131, 132, 133, 7, 23, 39, 49, 119, 135, 167, 32, 48, 64, 176, 192, 208, 224, 9, 10, 11, 25, 26, 27, 42, 43, 58, 59, 74, 75, 90, 106, 6, 22, 38, 40, 52, 53, 54, 55, 56, 70, 84, 102, 118, 41, 57, 73, 86, 87, 88, 89, 103, 104, 68, 69, 100, 101, 171, 172, 187, 188, 203, 204, 219, 220, 14, 30, 31, 46, 62, 63, 78, 79, 94, 95, 110, 126, 127, 142, 158, 159, 174, 175, 190, 191, 206, 222, 144, 145, 146, 147, 151, 152, 160, 161, 162, 163, 177, 178, 179, 193, 194, 195, 209, 210, 4, 19, 20, 21, 35, 36, 164, 180, 181, 182, 183, 196, 197, 198, 199, 213, 214, 12, 13, 28, 29, 61, 76, 77, 91, 92, 93, 107, 108, 109, 123, 124, 125, 139, 140, 141, 149, 150, 155, 156, 157, 165, 166)),
EnemySpriteRequirementData(sprite_name='StalfosKnightSprite', sprite_id=145, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HelmasaurKingSprite', sprite_id=146, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(58,), subgroup_3=(62,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BumperSprite', sprite_id=147, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SwimmersEvilSprite', sprite_id=148, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='EyeLaser_RightSprite', sprite_id=149, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='EyeLaser_LeftSprite', sprite_id=150, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='EyeLaser_DownSprite', sprite_id=151, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='EyeLaser_UpSprite', sprite_id=152, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PengatorSprite', sprite_id=153, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(38,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='KyameronWaterSplashSprite', sprite_id=154, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(40,), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WizzrobeSprite', sprite_id=155, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37, 41), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='VerminHorizontalSprite', sprite_id=156, boss=False, overlord=False, do_not_randomize=True, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='VerminVerticalSprite', sprite_id=157, boss=False, overlord=False, do_not_randomize=True, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Ostrich_HauntedGroveSprite', sprite_id=158, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(78,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FluteSprite', sprite_id=159, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Birds_HauntedGroveSprite', sprite_id=160, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(78,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FreezorSprite', sprite_id=161, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(38,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='KholdstareSprite', sprite_id=162, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(60,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='KholdstaresShellSprite', sprite_id=163, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FallingIceSprite', sprite_id=164, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(60,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BlueZazakSprite', sprite_id=165, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedZazakSprite', sprite_id=166, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='StalfosSprite', sprite_id=167, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BomberFlyingCreaturesFromDarkworldSprite', sprite_id=168, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BomberFlyingCreaturesFromDarkworld2Sprite', sprite_id=169, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='PikitSprite', sprite_id=170, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MaidenSprite', sprite_id=171, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AppleSprite', sprite_id=172, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LostOldManSprite', sprite_id=173, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(28,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='DownPipeSprite', sprite_id=174, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='UpPipeSprite', sprite_id=175, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RightPipeSprite', sprite_id=176, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LeftPipeSprite', sprite_id=177, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GoodBee_AgainMaybeSprite', sprite_id=178, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HylianInscriptionSprite', sprite_id=179, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ThiefsChestSprite', sprite_id=180, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(21,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BombSalesmanSprite', sprite_id=181, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='KikiSprite', sprite_id=182, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(25,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MaidenInBlindDungeonSprite', sprite_id=183, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MimicSprite', sprite_id=184, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FeudingFriendsOnDeathMountainSprite', sprite_id=185, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(20,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='WhirlpoolSprite', sprite_id=186, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(255, 274, 287)),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(271, 272)),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(272,)),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(280,)),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(291, 292)),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(291, 292)),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(80,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(293,)),
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(21,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(286,)),
EnemySpriteRequirementData(sprite_name='DrunkInTheInnSprite', sprite_id=188, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(80,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Vitreous_LargeEyeballSprite', sprite_id=189, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Vitreous_SmallEyeballSprite', sprite_id=190, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='VitreousLightningSprite', sprite_id=191, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CatFish_QuakeMedallionSprite', sprite_id=192, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(24,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AgahnimTeleportingZeldaToDarkworldSprite', sprite_id=193, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(85,), subgroup_1=(61,), subgroup_2=(66,), subgroup_3=(67,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BouldersSprite', sprite_id=194, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='Gibo_FloatingBlobSprite', sprite_id=195, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ThiefSprite', sprite_id=196, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(14, 21), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MedusaSprite', sprite_id=197, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FourWayFireballSpittersSprite', sprite_id=198, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HokkuBokkuSprite', sprite_id=199, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BigFairyWhoHealsYouSprite', sprite_id=200, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='TektiteSprite', sprite_id=201, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ChainChompSprite', sprite_id=202, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='TrinexxSprite', sprite_id=203, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='AnotherPartOfTrinexxSprite', sprite_id=204, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='YetAnotherPartOfTrinexxSprite', sprite_id=205, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BlindTheThiefSprite', sprite_id=206, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(59,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SwamolaSprite', sprite_id=207, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(25,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LynelSprite', sprite_id=208, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(20,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BunnyBeamSprite', sprite_id=209, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FloppingFishSprite', sprite_id=210, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='StalSprite', sprite_id=211, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='LandmineSprite', sprite_id=212, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='DiggingGameProprietorSprite', sprite_id=213, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(42,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GanonSprite', sprite_id=214, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(33,), subgroup_1=(65,), subgroup_2=(69,), subgroup_3=(51,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CopyOfGanon_ExceptInvincibleSprite', sprite_id=215, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HeartSprite', sprite_id=216, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='GreenRupeeSprite', sprite_id=217, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BlueRupeeSprite', sprite_id=218, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='RedRupeeSprite', sprite_id=219, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BombRefill1Sprite', sprite_id=220, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BombRefill4Sprite', sprite_id=221, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BombRefill8Sprite', sprite_id=222, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='SmallMagicRefillSprite', sprite_id=223, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FullMagicRefillSprite', sprite_id=224, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArrowRefill5Sprite', sprite_id=225, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ArrowRefill10Sprite', sprite_id=226, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FairySprite', sprite_id=227, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='KeySprite', sprite_id=228, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BigKeySprite', sprite_id=229, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='ShieldEaterSprite', sprite_id=230, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MushroomSprite', sprite_id=231, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='FakeMasterSwordSprite', sprite_id=232, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MagicShopDude_HisItemsIncludingTheMagicPowderSprite', sprite_id=233, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HeartContainerSprite', sprite_id=234, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='HeartPieceSprite', sprite_id=235, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='BushesSprite', sprite_id=236, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatformSprite', sprite_id=237, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MantleSprite', sprite_id=238, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(93,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused1Sprite', sprite_id=239, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused2Sprite', sprite_id=240, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused3Sprite', sprite_id=241, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='MedallionTabletSprite', sprite_id=242, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OW_OL_FallingRocks', sprite_id=244, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_CanonBalls_EP4Walls', sprite_id=258, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_CanonBalls_EPEntrance', sprite_id=259, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_StalfosHeadTrap', sprite_id=261, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_BombDrop_RopeTrap', sprite_id=262, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_MovingFloor', sprite_id=263, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_SlimeDropper', sprite_id=264, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_Wallmaster', sprite_id=265, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_FloorDrop_Square', sprite_id=266, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_FloorDrop_Path', sprite_id=267, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_RightEvil_PirogusuSpawner', sprite_id=272, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_LeftEvil_PirogusuSpawner', sprite_id=273, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_DownEvil_PirogusuSpawner', sprite_id=274, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_UpEvil_PirogusuSpawner', sprite_id=275, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_FlyingFloorTileTrap', sprite_id=276, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_WizzrobeSpawner', sprite_id=277, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37, 41), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_BlackSpawn_Zoro_BombHole', sprite_id=278, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_4Skull_Trap_Pot', sprite_id=279, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_Stalfos_Spawn_Trap_EP', sprite_id=280, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_ArmosKnight_Trigger', sprite_id=281, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
EnemySpriteRequirementData(sprite_name='OL_BombDrop_BombTrap', sprite_id=282, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
)
@@ -0,0 +1,131 @@
from __future__ import annotations
from typing import NamedTuple, Optional
class OverworldGroupRequirementData(NamedTuple):
group_id: Optional[int]
subgroup_0: Optional[int]
subgroup_1: Optional[int]
subgroup_2: Optional[int]
subgroup_3: Optional[int]
areas: tuple[int, ...]
AREA_IDS = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207)
DO_NOT_RANDOMIZE_AREA_IDS = frozenset((1,
4,
6,
8,
9,
11,
12,
13,
14,
25,
28,
31,
32,
33,
35,
36,
38,
39,
49,
54,
56,
57,
61,
62,
65,
68,
70,
72,
73,
75,
76,
77,
78,
89,
92,
95,
96,
97,
99,
100,
102,
103,
113,
118,
120,
121,
125,
126,
42,
106,
130,
131,
132,
133,
134,
135,
136,
137,
138,
139,
140,
141,
142,
143,
186,
250,
145,
148,
150,
152,
153,
155,
156,
158,
169,
172,
175,
176,
177,
179,
180,
182,
183,
193,
198,
200,
274,
275,
276,
277,
278,
279,
281,
288))
FORCED_GROUP_REQUIREMENTS = (
OverworldGroupRequirementData(group_id=7, subgroup_0=None, subgroup_1=None, subgroup_2=74, subgroup_3=None, areas=(2,)),
OverworldGroupRequirementData(group_id=16, subgroup_0=None, subgroup_1=None, subgroup_2=18, subgroup_3=16, areas=(3, 147)),
OverworldGroupRequirementData(group_id=7, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=17, areas=(10, 154)),
OverworldGroupRequirementData(group_id=4, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(15, 159)),
OverworldGroupRequirementData(group_id=3, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=14, areas=(20, 164)),
OverworldGroupRequirementData(group_id=1, subgroup_0=None, subgroup_1=None, subgroup_2=76, subgroup_3=63, areas=(27, 171)),
OverworldGroupRequirementData(group_id=6, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(34, 40, 178, 184)),
OverworldGroupRequirementData(group_id=8, subgroup_0=None, subgroup_1=None, subgroup_2=18, subgroup_3=None, areas=(48, 192)),
OverworldGroupRequirementData(group_id=10, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(58, 202)),
OverworldGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=24, subgroup_3=None, areas=(79, 223)),
OverworldGroupRequirementData(group_id=21, subgroup_0=21, subgroup_1=None, subgroup_2=None, subgroup_3=21, areas=(98, 242)),
OverworldGroupRequirementData(group_id=27, subgroup_0=None, subgroup_1=42, subgroup_2=None, subgroup_3=None, areas=(104, 248)),
OverworldGroupRequirementData(group_id=13, subgroup_0=None, subgroup_1=None, subgroup_2=76, subgroup_3=None, areas=(22, 166)),
OverworldGroupRequirementData(group_id=29, subgroup_0=None, subgroup_1=77, subgroup_2=None, subgroup_3=21, areas=(105, 249)),
OverworldGroupRequirementData(group_id=15, subgroup_0=None, subgroup_1=None, subgroup_2=78, subgroup_3=None, areas=(42, 186)),
OverworldGroupRequirementData(group_id=17, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=76, areas=(106, 250)),
OverworldGroupRequirementData(group_id=12, subgroup_0=None, subgroup_1=None, subgroup_2=55, subgroup_3=54, areas=(128, 272)),
OverworldGroupRequirementData(group_id=14, subgroup_0=None, subgroup_1=None, subgroup_2=12, subgroup_3=68, areas=(129, 273)),
OverworldGroupRequirementData(group_id=26, subgroup_0=15, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(146,)),
OverworldGroupRequirementData(group_id=23, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=25, areas=(94, 238)),
)
@@ -0,0 +1,107 @@
from __future__ import annotations
from typing import NamedTuple
class PotDataRecord(NamedTuple):
x: int
y: int
reserved: int
class PotRoomDataRecord(NamedTuple):
room_id: int
pots: tuple[PotDataRecord, ...]
items: tuple[int, ...]
POT_ROOMS = (
PotRoomDataRecord(room_id=4, pots=(PotDataRecord(x=162, y=25, reserved=0), PotDataRecord(x=152, y=25, reserved=0), PotDataRecord(x=152, y=22, reserved=0), PotDataRecord(x=162, y=22, reserved=0), PotDataRecord(x=240, y=19, reserved=0), PotDataRecord(x=204, y=19, reserved=0),), items=(10, 10)),
PotRoomDataRecord(room_id=9, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=12, reserved=0),), items=(1, 11, 136)),
PotRoomDataRecord(room_id=10, pots=(PotDataRecord(x=204, y=11, reserved=0), PotDataRecord(x=156, y=17, reserved=0), PotDataRecord(x=96, y=8, reserved=0), PotDataRecord(x=100, y=7, reserved=0), PotDataRecord(x=160, y=17, reserved=0), PotDataRecord(x=104, y=8, reserved=0), PotDataRecord(x=100, y=9, reserved=0),), items=(11, 11, 136)),
PotRoomDataRecord(room_id=17, pots=(PotDataRecord(x=152, y=19, reserved=0), PotDataRecord(x=152, y=15, reserved=0), PotDataRecord(x=144, y=15, reserved=0), PotDataRecord(x=10, y=15, reserved=0), PotDataRecord(x=144, y=19, reserved=0), PotDataRecord(x=160, y=19, reserved=0),), items=(11, 11, 11, 11)),
PotRoomDataRecord(room_id=21, pots=(PotDataRecord(x=96, y=4, reserved=0), PotDataRecord(x=100, y=4, reserved=0), PotDataRecord(x=104, y=4, reserved=0), PotDataRecord(x=108, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=12, y=6, reserved=0), PotDataRecord(x=16, y=6, reserved=0), PotDataRecord(x=20, y=6, reserved=0), PotDataRecord(x=70, y=11, reserved=0),), items=(1, 7, 9, 9, 10, 11, 12, 12, 13)),
PotRoomDataRecord(room_id=22, pots=(PotDataRecord(x=188, y=3, reserved=0), PotDataRecord(x=192, y=3, reserved=0), PotDataRecord(x=188, y=4, reserved=0), PotDataRecord(x=192, y=4, reserved=0), PotDataRecord(x=188, y=5, reserved=0), PotDataRecord(x=192, y=5, reserved=0), PotDataRecord(x=188, y=6, reserved=0), PotDataRecord(x=192, y=6, reserved=0), PotDataRecord(x=240, y=19, reserved=0),), items=(8, 9, 9, 10, 10, 11, 11, 12, 12)),
PotRoomDataRecord(room_id=26, pots=(PotDataRecord(x=232, y=19, reserved=0), PotDataRecord(x=212, y=19, reserved=0), PotDataRecord(x=28, y=5, reserved=0), PotDataRecord(x=32, y=5, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0),), items=(10, 10, 10, 10)),
PotRoomDataRecord(room_id=33, pots=(PotDataRecord(x=100, y=28, reserved=0), PotDataRecord(x=168, y=24, reserved=0), PotDataRecord(x=48, y=28, reserved=0), PotDataRecord(x=82, y=28, reserved=0), PotDataRecord(x=160, y=20, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(11, 12, 12)),
PotRoomDataRecord(room_id=35, pots=(PotDataRecord(x=86, y=26, reserved=0), PotDataRecord(x=90, y=26, reserved=0), PotDataRecord(x=94, y=26, reserved=0), PotDataRecord(x=98, y=26, reserved=0), PotDataRecord(x=102, y=26, reserved=0),), items=(1, 10, 11)),
PotRoomDataRecord(room_id=36, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=12, reserved=0), PotDataRecord(x=48, y=12, reserved=0),), items=(1, 11, 12, 7)),
PotRoomDataRecord(room_id=38, pots=(PotDataRecord(x=28, y=4, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=150, y=19, reserved=2), PotDataRecord(x=22, y=26, reserved=2), PotDataRecord(x=220, y=26, reserved=0),), items=(7, 9, 10, 12, 136)),
PotRoomDataRecord(room_id=39, pots=(PotDataRecord(x=214, y=19, reserved=0), PotDataRecord(x=214, y=20, reserved=0), PotDataRecord(x=166, y=20, reserved=0), PotDataRecord(x=214, y=21, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=80, y=28, reserved=0), PotDataRecord(x=84, y=28, reserved=0), PotDataRecord(x=102, y=17, reserved=0), PotDataRecord(x=98, y=17, reserved=0), PotDataRecord(x=106, y=17, reserved=0), PotDataRecord(x=166, y=21, reserved=0), PotDataRecord(x=166, y=19, reserved=0), PotDataRecord(x=92, y=12, reserved=0), PotDataRecord(x=160, y=12, reserved=0),), items=(1, 1, 10, 11, 7, 7)),
PotRoomDataRecord(room_id=43, pots=(PotDataRecord(x=16, y=5, reserved=2), PotDataRecord(x=44, y=5, reserved=2), PotDataRecord(x=16, y=6, reserved=2), PotDataRecord(x=44, y=6, reserved=2), PotDataRecord(x=16, y=7, reserved=2), PotDataRecord(x=44, y=7, reserved=2), PotDataRecord(x=146, y=21, reserved=0), PotDataRecord(x=170, y=21, reserved=0), PotDataRecord(x=146, y=22, reserved=0), PotDataRecord(x=170, y=22, reserved=0),), items=(9, 9, 10, 10, 10, 10, 11, 11, 11, 136)),
PotRoomDataRecord(room_id=47, pots=(PotDataRecord(x=28, y=7, reserved=0), PotDataRecord(x=32, y=7, reserved=0), PotDataRecord(x=28, y=9, reserved=0), PotDataRecord(x=32, y=9, reserved=0), PotDataRecord(x=172, y=19, reserved=0), PotDataRecord(x=180, y=19, reserved=0), PotDataRecord(x=104, y=27, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(7, 7, 7, 7, 11, 11, 11, 11)),
PotRoomDataRecord(room_id=53, pots=(PotDataRecord(x=60, y=6, reserved=1), PotDataRecord(x=20, y=8, reserved=0), PotDataRecord(x=24, y=8, reserved=0), PotDataRecord(x=28, y=8, reserved=0), PotDataRecord(x=32, y=8, reserved=0), PotDataRecord(x=36, y=8, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=76, y=23, reserved=1), PotDataRecord(x=88, y=23, reserved=1), PotDataRecord(x=100, y=27, reserved=1), PotDataRecord(x=242, y=28, reserved=1), PotDataRecord(x=240, y=22, reserved=1), PotDataRecord(x=76, y=28, reserved=1),), items=(7, 7, 7, 7, 7, 8, 11)),
PotRoomDataRecord(room_id=54, pots=(PotDataRecord(x=108, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=10, y=16, reserved=0), PotDataRecord(x=114, y=16, reserved=0),), items=(8, 10, 11)),
PotRoomDataRecord(room_id=55, pots=(PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=60, y=6, reserved=0),), items=(8,)),
PotRoomDataRecord(room_id=56, pots=(PotDataRecord(x=164, y=12, reserved=0), PotDataRecord(x=164, y=13, reserved=0), PotDataRecord(x=164, y=18, reserved=0), PotDataRecord(x=164, y=19, reserved=0),), items=(8, 7, 10, 10)),
PotRoomDataRecord(room_id=57, pots=(PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=100, y=22, reserved=0), PotDataRecord(x=100, y=26, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(9, 9, 11, 12)),
PotRoomDataRecord(room_id=60, pots=(PotDataRecord(x=24, y=8, reserved=0), PotDataRecord(x=64, y=12, reserved=0), PotDataRecord(x=20, y=14, reserved=0), PotDataRecord(x=68, y=18, reserved=0), PotDataRecord(x=96, y=19, reserved=0), PotDataRecord(x=64, y=20, reserved=0), PotDataRecord(x=64, y=26, reserved=0),), items=(1, 7, 7, 7, 7, 11, 12)),
PotRoomDataRecord(room_id=61, pots=(PotDataRecord(x=76, y=12, reserved=0), PotDataRecord(x=112, y=12, reserved=0), PotDataRecord(x=24, y=22, reserved=0), PotDataRecord(x=40, y=22, reserved=0), PotDataRecord(x=32, y=24, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=36, y=26, reserved=0),), items=(9, 7, 10, 10, 11, 11, 13)),
PotRoomDataRecord(room_id=62, pots=(PotDataRecord(x=96, y=6, reserved=0), PotDataRecord(x=100, y=6, reserved=0), PotDataRecord(x=88, y=10, reserved=0), PotDataRecord(x=92, y=10, reserved=0),), items=(10, 11, 12, 12)),
PotRoomDataRecord(room_id=63, pots=(PotDataRecord(x=12, y=25, reserved=0), PotDataRecord(x=20, y=25, reserved=0), PotDataRecord(x=12, y=26, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=12, y=27, reserved=0), PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=28, y=23, reserved=0),), items=(1, 1, 8, 10, 10, 11, 136)),
PotRoomDataRecord(room_id=65, pots=(PotDataRecord(x=100, y=10, reserved=0), PotDataRecord(x=52, y=15, reserved=0), PotDataRecord(x=52, y=16, reserved=0), PotDataRecord(x=148, y=22, reserved=0),), items=(1, 11, 12, 12)),
PotRoomDataRecord(room_id=67, pots=(PotDataRecord(x=112, y=28, reserved=1), PotDataRecord(x=76, y=28, reserved=1), PotDataRecord(x=76, y=20, reserved=1), PotDataRecord(x=66, y=4, reserved=0), PotDataRecord(x=78, y=4, reserved=0), PotDataRecord(x=66, y=9, reserved=0), PotDataRecord(x=78, y=9, reserved=0), PotDataRecord(x=112, y=20, reserved=1),), items=(8, 9, 11, 11, 12)),
PotRoomDataRecord(room_id=69, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=108, y=11, reserved=0), PotDataRecord(x=48, y=12, reserved=0), PotDataRecord(x=220, y=16, reserved=0), PotDataRecord(x=236, y=16, reserved=0),), items=(9, 9, 11, 11, 12)),
PotRoomDataRecord(room_id=73, pots=(PotDataRecord(x=156, y=27, reserved=0), PotDataRecord(x=172, y=24, reserved=0), PotDataRecord(x=172, y=23, reserved=0), PotDataRecord(x=144, y=20, reserved=0), PotDataRecord(x=104, y=15, reserved=0), PotDataRecord(x=104, y=16, reserved=0), PotDataRecord(x=144, y=19, reserved=0), PotDataRecord(x=172, y=20, reserved=0), PotDataRecord(x=144, y=27, reserved=0), PotDataRecord(x=172, y=28, reserved=0), PotDataRecord(x=160, y=27, reserved=0),), items=(11, 11, 12, 12, 12, 12)),
PotRoomDataRecord(room_id=78, pots=(PotDataRecord(x=48, y=10, reserved=2), PotDataRecord(x=140, y=11, reserved=2), PotDataRecord(x=28, y=12, reserved=2), PotDataRecord(x=112, y=12, reserved=0),), items=(136, 11, 12)),
PotRoomDataRecord(room_id=83, pots=(PotDataRecord(x=92, y=11, reserved=0), PotDataRecord(x=96, y=11, reserved=0), PotDataRecord(x=100, y=11, reserved=0), PotDataRecord(x=104, y=11, reserved=0),), items=(8, 11, 11, 12)),
PotRoomDataRecord(room_id=84, pots=(PotDataRecord(x=186, y=25, reserved=0), PotDataRecord(x=186, y=26, reserved=0), PotDataRecord(x=186, y=27, reserved=0), PotDataRecord(x=186, y=28, reserved=0),), items=(7, 11, 11, 11)),
PotRoomDataRecord(room_id=86, pots=(PotDataRecord(x=100, y=6, reserved=1), PotDataRecord(x=96, y=10, reserved=1), PotDataRecord(x=92, y=10, reserved=1), PotDataRecord(x=48, y=20, reserved=1), PotDataRecord(x=20, y=6, reserved=0), PotDataRecord(x=40, y=6, reserved=0), PotDataRecord(x=24, y=7, reserved=0), PotDataRecord(x=36, y=7, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=48, y=8, reserved=0), PotDataRecord(x=24, y=9, reserved=0), PotDataRecord(x=36, y=9, reserved=0), PotDataRecord(x=20, y=10, reserved=0), PotDataRecord(x=40, y=10, reserved=0), PotDataRecord(x=12, y=20, reserved=1),), items=(7, 7, 11, 11, 8, 12, 12, 12, 12, 12, 12)),
PotRoomDataRecord(room_id=87, pots=(PotDataRecord(x=92, y=7, reserved=0), PotDataRecord(x=12, y=20, reserved=2), PotDataRecord(x=92, y=23, reserved=0), PotDataRecord(x=100, y=23, reserved=0), PotDataRecord(x=84, y=25, reserved=0), PotDataRecord(x=76, y=27, reserved=0), PotDataRecord(x=48, y=20, reserved=2), PotDataRecord(x=30, y=22, reserved=2),), items=(7, 10, 11, 12, 12, 12, 13, 136)),
PotRoomDataRecord(room_id=88, pots=(PotDataRecord(x=96, y=9, reserved=0), PotDataRecord(x=92, y=8, reserved=0), PotDataRecord(x=108, y=8, reserved=0), PotDataRecord(x=108, y=6, reserved=0), PotDataRecord(x=104, y=5, reserved=0), PotDataRecord(x=92, y=6, reserved=0), PotDataRecord(x=12, y=12, reserved=0), PotDataRecord(x=16, y=7, reserved=0), PotDataRecord(x=96, y=5, reserved=0), PotDataRecord(x=100, y=5, reserved=0), PotDataRecord(x=12, y=7, reserved=0), PotDataRecord(x=92, y=7, reserved=0), PotDataRecord(x=108, y=7, reserved=0), PotDataRecord(x=16, y=8, reserved=0), PotDataRecord(x=100, y=9, reserved=0), PotDataRecord(x=104, y=9, reserved=0),), items=(10, 10, 11, 11, 12, 12, 12, 12)),
PotRoomDataRecord(room_id=91, pots=(PotDataRecord(x=218, y=37, reserved=0), PotDataRecord(x=222, y=37, reserved=0), PotDataRecord(x=226, y=37, reserved=0),), items=(136,)),
PotRoomDataRecord(room_id=92, pots=(PotDataRecord(x=228, y=25, reserved=0), PotDataRecord(x=104, y=24, reserved=0), PotDataRecord(x=228, y=22, reserved=0), PotDataRecord(x=216, y=25, reserved=0), PotDataRecord(x=84, y=24, reserved=0), PotDataRecord(x=216, y=22, reserved=0), PotDataRecord(x=94, y=22, reserved=0), PotDataRecord(x=94, y=26, reserved=0),), items=(10, 13)),
PotRoomDataRecord(room_id=93, pots=(PotDataRecord(x=16, y=5, reserved=0), PotDataRecord(x=44, y=5, reserved=0), PotDataRecord(x=16, y=11, reserved=0), PotDataRecord(x=44, y=11, reserved=0), PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=12, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(1, 7, 9, 9, 10, 10, 10, 12)),
PotRoomDataRecord(room_id=94, pots=(PotDataRecord(x=92, y=4, reserved=0), PotDataRecord(x=96, y=4, reserved=0), PotDataRecord(x=76, y=8, reserved=0), PotDataRecord(x=112, y=8, reserved=0),), items=(11, 11, 12, 12)),
PotRoomDataRecord(room_id=99, pots=(PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=48, y=12, reserved=0), PotDataRecord(x=48, y=8, reserved=0), PotDataRecord(x=12, y=12, reserved=0),), items=(8, 11)),
PotRoomDataRecord(room_id=100, pots=(PotDataRecord(x=12, y=22, reserved=0), PotDataRecord(x=16, y=22, reserved=0), PotDataRecord(x=20, y=22, reserved=0), PotDataRecord(x=36, y=28, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(10, 10, 10, 10, 12, 12, 136)),
PotRoomDataRecord(room_id=102, pots=(PotDataRecord(x=48, y=37, reserved=0), PotDataRecord(x=52, y=37, reserved=0), PotDataRecord(x=56, y=37, reserved=0), PotDataRecord(x=84, y=5, reserved=0), PotDataRecord(x=104, y=5, reserved=0), PotDataRecord(x=48, y=38, reserved=0), PotDataRecord(x=52, y=38, reserved=0), PotDataRecord(x=56, y=38, reserved=0), PotDataRecord(x=84, y=6, reserved=0), PotDataRecord(x=104, y=6, reserved=0),), items=(7, 7, 9, 9, 9, 10, 10, 10, 11, 11)),
PotRoomDataRecord(room_id=103, pots=(PotDataRecord(x=22, y=26, reserved=0), PotDataRecord(x=18, y=22, reserved=0), PotDataRecord(x=92, y=9, reserved=0), PotDataRecord(x=84, y=28, reserved=0), PotDataRecord(x=12, y=7, reserved=0), PotDataRecord(x=48, y=7, reserved=0), PotDataRecord(x=96, y=19, reserved=0), PotDataRecord(x=74, y=20, reserved=0), PotDataRecord(x=18, y=23, reserved=0), PotDataRecord(x=18, y=26, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(9, 11, 11, 11, 12, 12, 12)),
PotRoomDataRecord(room_id=104, pots=(PotDataRecord(x=84, y=14, reserved=0), PotDataRecord(x=84, y=13, reserved=0), PotDataRecord(x=88, y=12, reserved=0), PotDataRecord(x=88, y=6, reserved=0), PotDataRecord(x=88, y=5, reserved=0), PotDataRecord(x=88, y=4, reserved=0), PotDataRecord(x=64, y=17, reserved=0), PotDataRecord(x=64, y=15, reserved=0), PotDataRecord(x=64, y=7, reserved=0), PotDataRecord(x=88, y=7, reserved=0), PotDataRecord(x=64, y=16, reserved=0), PotDataRecord(x=64, y=24, reserved=0), PotDataRecord(x=64, y=25, reserved=0),), items=(11, 11, 11, 12, 12)),
PotRoomDataRecord(room_id=115, pots=(PotDataRecord(x=154, y=21, reserved=0), PotDataRecord(x=158, y=21, reserved=0), PotDataRecord(x=20, y=23, reserved=0), PotDataRecord(x=36, y=23, reserved=0), PotDataRecord(x=144, y=24, reserved=0), PotDataRecord(x=168, y=24, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=36, y=26, reserved=0), PotDataRecord(x=154, y=27, reserved=0), PotDataRecord(x=158, y=27, reserved=0),), items=(1, 1, 11, 11, 7, 7, 9, 9, 12, 136)),
PotRoomDataRecord(room_id=116, pots=(PotDataRecord(x=30, y=5, reserved=0), PotDataRecord(x=62, y=5, reserved=0), PotDataRecord(x=94, y=5, reserved=0), PotDataRecord(x=14, y=11, reserved=0), PotDataRecord(x=46, y=11, reserved=0), PotDataRecord(x=78, y=11, reserved=0), PotDataRecord(x=110, y=11, reserved=0),), items=(9, 9, 11, 11, 12, 12, 136)),
PotRoomDataRecord(room_id=117, pots=(PotDataRecord(x=148, y=22, reserved=0), PotDataRecord(x=160, y=22, reserved=0), PotDataRecord(x=172, y=22, reserved=0),), items=(9, 11, 12)),
PotRoomDataRecord(room_id=123, pots=(PotDataRecord(x=48, y=10, reserved=0), PotDataRecord(x=88, y=10, reserved=0), PotDataRecord(x=76, y=7, reserved=0), PotDataRecord(x=60, y=4, reserved=0), PotDataRecord(x=64, y=4, reserved=0),), items=(11, 8)),
PotRoomDataRecord(room_id=124, pots=(PotDataRecord(x=36, y=21, reserved=0), PotDataRecord(x=24, y=11, reserved=0), PotDataRecord(x=28, y=4, reserved=0), PotDataRecord(x=32, y=4, reserved=0),), items=(11, 11)),
PotRoomDataRecord(room_id=125, pots=(PotDataRecord(x=44, y=12, reserved=0), PotDataRecord(x=44, y=6, reserved=0), PotDataRecord(x=112, y=6, reserved=0), PotDataRecord(x=108, y=20, reserved=0), PotDataRecord(x=114, y=20, reserved=0), PotDataRecord(x=76, y=28, reserved=0),), items=(9, 10, 10, 11)),
PotRoomDataRecord(room_id=126, pots=(PotDataRecord(x=86, y=15, reserved=0), PotDataRecord(x=82, y=26, reserved=0), PotDataRecord(x=100, y=26, reserved=0), PotDataRecord(x=104, y=26, reserved=0),), items=(11, 12, 136)),
PotRoomDataRecord(room_id=130, pots=(PotDataRecord(x=50, y=5, reserved=0), PotDataRecord(x=50, y=10, reserved=0), PotDataRecord(x=76, y=50, reserved=0),), items=(11,)),
PotRoomDataRecord(room_id=131, pots=(PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=80, y=4, reserved=0), PotDataRecord(x=76, y=28, reserved=0), PotDataRecord(x=80, y=28, reserved=0),), items=(1, 7, 9, 9)),
PotRoomDataRecord(room_id=132, pots=(PotDataRecord(x=64, y=17, reserved=0), PotDataRecord(x=60, y=17, reserved=0), PotDataRecord(x=80, y=14, reserved=0), PotDataRecord(x=44, y=14, reserved=0), PotDataRecord(x=100, y=6, reserved=0), PotDataRecord(x=24, y=6, reserved=0), PotDataRecord(x=24, y=7, reserved=0), PotDataRecord(x=100, y=7, reserved=0),), items=(9, 9)),
PotRoomDataRecord(room_id=135, pots=(PotDataRecord(x=12, y=11, reserved=0), PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=112, y=20, reserved=0), PotDataRecord(x=16, y=12, reserved=0), PotDataRecord(x=40, y=12, reserved=0), PotDataRecord(x=32, y=12, reserved=0), PotDataRecord(x=24, y=12, reserved=0), PotDataRecord(x=16, y=11, reserved=0),), items=(12, 13)),
PotRoomDataRecord(room_id=139, pots=(PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=76, y=12, reserved=1), PotDataRecord(x=32, y=23, reserved=1), PotDataRecord(x=28, y=23, reserved=1), PotDataRecord(x=112, y=12, reserved=1), PotDataRecord(x=32, y=9, reserved=1), PotDataRecord(x=76, y=28, reserved=0),), items=(8, 11, 12)),
PotRoomDataRecord(room_id=140, pots=(PotDataRecord(x=76, y=12, reserved=2), PotDataRecord(x=112, y=12, reserved=2), PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=92, y=20, reserved=0), PotDataRecord(x=100, y=21, reserved=0), PotDataRecord(x=104, y=26, reserved=0), PotDataRecord(x=88, y=27, reserved=0),), items=(9, 10, 10, 10, 10, 12, 136)),
PotRoomDataRecord(room_id=145, pots=(PotDataRecord(x=84, y=4, reserved=0), PotDataRecord(x=104, y=4, reserved=0),), items=(11, 12)),
PotRoomDataRecord(room_id=150, pots=(PotDataRecord(x=14, y=18, reserved=0), PotDataRecord(x=32, y=5, reserved=0), PotDataRecord(x=32, y=17, reserved=0), PotDataRecord(x=32, y=24, reserved=0), PotDataRecord(x=76, y=21, reserved=0), PotDataRecord(x=112, y=21, reserved=0), PotDataRecord(x=14, y=24, reserved=0),), items=(11, 12, 12, 13)),
PotRoomDataRecord(room_id=155, pots=(PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=48, y=12, reserved=0),), items=(8, 12)),
PotRoomDataRecord(room_id=157, pots=(PotDataRecord(x=32, y=7, reserved=0), PotDataRecord(x=40, y=9, reserved=0), PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=84, y=4, reserved=0),), items=(10, 12)),
PotRoomDataRecord(room_id=159, pots=(PotDataRecord(x=138, y=20, reserved=0), PotDataRecord(x=138, y=19, reserved=0), PotDataRecord(x=178, y=19, reserved=0), PotDataRecord(x=40, y=21, reserved=0), PotDataRecord(x=138, y=21, reserved=0), PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=138, y=27, reserved=0), PotDataRecord(x=178, y=28, reserved=0), PotDataRecord(x=178, y=21, reserved=0), PotDataRecord(x=178, y=20, reserved=0), PotDataRecord(x=40, y=27, reserved=0), PotDataRecord(x=178, y=27, reserved=0), PotDataRecord(x=178, y=26, reserved=0), PotDataRecord(x=138, y=28, reserved=0), PotDataRecord(x=138, y=26, reserved=0), PotDataRecord(x=20, y=21, reserved=0),), items=(8, 11, 11, 11, 11, 11, 136)),
PotRoomDataRecord(room_id=161, pots=(PotDataRecord(x=96, y=27, reserved=0), PotDataRecord(x=92, y=21, reserved=0), PotDataRecord(x=150, y=6, reserved=0), PotDataRecord(x=100, y=11, reserved=0), PotDataRecord(x=104, y=12, reserved=0), PotDataRecord(x=108, y=13, reserved=0), PotDataRecord(x=112, y=14, reserved=0), PotDataRecord(x=96, y=23, reserved=0), PotDataRecord(x=76, y=28, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(8, 11, 11, 11, 12, 12)),
PotRoomDataRecord(room_id=168, pots=(PotDataRecord(x=138, y=28, reserved=0), PotDataRecord(x=178, y=28, reserved=0), PotDataRecord(x=178, y=19, reserved=0), PotDataRecord(x=138, y=19, reserved=0), PotDataRecord(x=30, y=24, reserved=0),), items=(1, 11)),
PotRoomDataRecord(room_id=169, pots=(PotDataRecord(x=12, y=19, reserved=0), PotDataRecord(x=112, y=19, reserved=0), PotDataRecord(x=144, y=43, reserved=0), PotDataRecord(x=236, y=43, reserved=0), PotDataRecord(x=144, y=44, reserved=0), PotDataRecord(x=236, y=44, reserved=0), PotDataRecord(x=16, y=20, reserved=0), PotDataRecord(x=108, y=20, reserved=0),), items=(11, 11, 11, 9, 9, 9)),
PotRoomDataRecord(room_id=170, pots=(PotDataRecord(x=212, y=10, reserved=2), PotDataRecord(x=232, y=10, reserved=2), PotDataRecord(x=232, y=5, reserved=2), PotDataRecord(x=212, y=5, reserved=2), PotDataRecord(x=94, y=8, reserved=2), PotDataRecord(x=108, y=55, reserved=0), PotDataRecord(x=108, y=56, reserved=0), PotDataRecord(x=108, y=57, reserved=0),), items=(11, 11, 11, 11, 136)),
PotRoomDataRecord(room_id=176, pots=(PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=24, y=24, reserved=0), PotDataRecord(x=44, y=25, reserved=0), PotDataRecord(x=20, y=21, reserved=0), PotDataRecord(x=28, y=21, reserved=0), PotDataRecord(x=32, y=21, reserved=0), PotDataRecord(x=40, y=21, reserved=0), PotDataRecord(x=16, y=23, reserved=0), PotDataRecord(x=44, y=23, reserved=0), PotDataRecord(x=36, y=24, reserved=0), PotDataRecord(x=16, y=25, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=40, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0),), items=(1, 1, 7, 7, 9, 9, 10, 10, 11, 11)),
PotRoomDataRecord(room_id=179, pots=(PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(8, 12, 136)),
PotRoomDataRecord(room_id=180, pots=(PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(11, 13)),
PotRoomDataRecord(room_id=181, pots=(PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=112, y=15, reserved=0), PotDataRecord(x=76, y=16, reserved=0), PotDataRecord(x=112, y=16, reserved=0), PotDataRecord(x=112, y=17, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(7, 10, 11, 11, 13, 136)),
PotRoomDataRecord(room_id=184, pots=(PotDataRecord(x=96, y=13, reserved=0), PotDataRecord(x=88, y=16, reserved=0), PotDataRecord(x=104, y=16, reserved=0),), items=(11, 11, 136)),
PotRoomDataRecord(room_id=185, pots=(PotDataRecord(x=92, y=18, reserved=0), PotDataRecord(x=96, y=18, reserved=0), PotDataRecord(x=104, y=18, reserved=0), PotDataRecord(x=108, y=18, reserved=0),), items=(1, 1, 7, 7)),
PotRoomDataRecord(room_id=186, pots=(PotDataRecord(x=100, y=8, reserved=0), PotDataRecord(x=88, y=8, reserved=0), PotDataRecord(x=94, y=4, reserved=0), PotDataRecord(x=76, y=6, reserved=0), PotDataRecord(x=112, y=6, reserved=0), PotDataRecord(x=76, y=10, reserved=0), PotDataRecord(x=112, y=10, reserved=0), PotDataRecord(x=94, y=12, reserved=0),), items=(1, 1, 8, 11, 11, 12)),
PotRoomDataRecord(room_id=188, pots=(PotDataRecord(x=138, y=3, reserved=2), PotDataRecord(x=178, y=3, reserved=2), PotDataRecord(x=86, y=4, reserved=1), PotDataRecord(x=102, y=4, reserved=1), PotDataRecord(x=138, y=12, reserved=2), PotDataRecord(x=178, y=12, reserved=2), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=28, y=21, reserved=0), PotDataRecord(x=32, y=21, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0), PotDataRecord(x=12, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(7, 7, 7, 7, 8, 10, 10, 10, 10, 10, 11, 11, 136)),
PotRoomDataRecord(room_id=191, pots=(PotDataRecord(x=40, y=20, reserved=0), PotDataRecord(x=44, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(9, 10, 11, 12, 12, 12)),
PotRoomDataRecord(room_id=192, pots=(PotDataRecord(x=48, y=10, reserved=0), PotDataRecord(x=12, y=14, reserved=0), PotDataRecord(x=12, y=26, reserved=0), PotDataRecord(x=28, y=27, reserved=0),), items=(1, 7, 10, 11)),
PotRoomDataRecord(room_id=194, pots=(PotDataRecord(x=180, y=7, reserved=0), PotDataRecord(x=100, y=46, reserved=0), PotDataRecord(x=68, y=48, reserved=0), PotDataRecord(x=64, y=52, reserved=0),), items=(1, 9, 12, 136)),
PotRoomDataRecord(room_id=196, pots=(PotDataRecord(x=84, y=9, reserved=0), PotDataRecord(x=24, y=14, reserved=0), PotDataRecord(x=56, y=17, reserved=0), PotDataRecord(x=84, y=17, reserved=0), PotDataRecord(x=12, y=21, reserved=0), PotDataRecord(x=76, y=23, reserved=0), PotDataRecord(x=48, y=25, reserved=0), PotDataRecord(x=12, y=26, reserved=0),), items=(1, 9, 12, 7, 10, 10, 11, 11)),
PotRoomDataRecord(room_id=199, pots=(PotDataRecord(x=12, y=10, reserved=0), PotDataRecord(x=12, y=11, reserved=0), PotDataRecord(x=12, y=22, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(9, 12, 11, 13)),
PotRoomDataRecord(room_id=201, pots=(PotDataRecord(x=30, y=22, reserved=0), PotDataRecord(x=94, y=22, reserved=0), PotDataRecord(x=60, y=22, reserved=0),), items=(1, 1, 136)),
PotRoomDataRecord(room_id=203, pots=(PotDataRecord(x=88, y=16, reserved=0), PotDataRecord(x=88, y=28, reserved=0),), items=(7, 11)),
PotRoomDataRecord(room_id=204, pots=(PotDataRecord(x=36, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=36, y=28, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(7, 11, 7, 10)),
PotRoomDataRecord(room_id=206, pots=(PotDataRecord(x=76, y=8, reserved=0), PotDataRecord(x=80, y=8, reserved=0), PotDataRecord(x=108, y=12, reserved=0), PotDataRecord(x=112, y=12, reserved=0), PotDataRecord(x=204, y=11, reserved=3),), items=(9, 12, 12, 10, 128)),
PotRoomDataRecord(room_id=208, pots=(PotDataRecord(x=158, y=5, reserved=0), PotDataRecord(x=140, y=11, reserved=0), PotDataRecord(x=42, y=13, reserved=0), PotDataRecord(x=48, y=16, reserved=0), PotDataRecord(x=176, y=20, reserved=0), PotDataRecord(x=146, y=23, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(1, 1, 7, 11, 11, 12, 12)),
PotRoomDataRecord(room_id=209, pots=(PotDataRecord(x=76, y=12, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=168, y=7, reserved=0), PotDataRecord(x=112, y=12, reserved=0),), items=(9, 1, 1, 1, 13)),
PotRoomDataRecord(room_id=214, pots=(PotDataRecord(x=92, y=22, reserved=0), PotDataRecord(x=96, y=22, reserved=0),), items=(10, 13)),
PotRoomDataRecord(room_id=216, pots=(PotDataRecord(x=202, y=8, reserved=0), PotDataRecord(x=242, y=8, reserved=0), PotDataRecord(x=202, y=10, reserved=0), PotDataRecord(x=242, y=10, reserved=0), PotDataRecord(x=202, y=12, reserved=0), PotDataRecord(x=242, y=12, reserved=0), PotDataRecord(x=92, y=24, reserved=0), PotDataRecord(x=96, y=24, reserved=0),), items=(9, 9, 9, 9, 9, 11, 11, 11)),
PotRoomDataRecord(room_id=218, pots=(PotDataRecord(x=24, y=23, reserved=0), PotDataRecord(x=36, y=23, reserved=0), PotDataRecord(x=24, y=25, reserved=0), PotDataRecord(x=36, y=25, reserved=0),), items=(9, 9, 11, 136)),
PotRoomDataRecord(room_id=219, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=62, y=19, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=88, y=16, reserved=0),), items=(7, 11)),
PotRoomDataRecord(room_id=220, pots=(PotDataRecord(x=56, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=68, y=16, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(7, 9, 10, 11)),
PotRoomDataRecord(room_id=235, pots=(PotDataRecord(x=206, y=8, reserved=0), PotDataRecord(x=210, y=8, reserved=0), PotDataRecord(x=88, y=14, reserved=0), PotDataRecord(x=92, y=14, reserved=0), PotDataRecord(x=96, y=14, reserved=0),), items=(7, 7, 11, 12, 12)),
)
+171
View File
@@ -0,0 +1,171 @@
from __future__ import annotations
ENEMIZER_SYMBOLS = {
':pos_1_0': 0x36975E,
':pos_1_1': 0x36976D,
':pos_1_10': 0x369819,
':pos_1_11': 0x369828,
':pos_1_12': 0x369837,
':pos_1_13': 0x369845,
':pos_1_14': 0x36984B,
':pos_1_15': 0x369851,
':pos_1_16': 0x369873,
':pos_1_17': 0x369898,
':pos_1_18': 0x36989E,
':pos_1_19': 0x3698A4,
':pos_1_2': 0x369787,
':pos_1_20': 0x3698C6,
':pos_1_21': 0x3698EB,
':pos_1_22': 0x3698F1,
':pos_1_23': 0x3698F7,
':pos_1_24': 0x369919,
':pos_1_25': 0x36993E,
':pos_1_26': 0x369944,
':pos_1_27': 0x36994A,
':pos_1_28': 0x36996C,
':pos_1_29': 0x369AA5,
':pos_1_3': 0x369796,
':pos_1_30': 0x36B796,
':pos_1_4': 0x3697A5,
':pos_1_5': 0x3697B4,
':pos_1_6': 0x3697D5,
':pos_1_7': 0x3697E8,
':pos_1_8': 0x3697F7,
':pos_1_9': 0x369806,
'CheckIfLinkShouldDie': 0x3699B2,
'CheckIfLinkShouldDie_dead': 0x3699BB,
'CheckIfLinkShouldDie_done': 0x3699BD,
'Check_for_Blind_Fight': 0x1DA085,
'CopyShield': 0x36B713,
'CopyShield_loop_copy': 0x36B729,
'CopyShield_shield_positon_gfx': 0x36B74B,
'CopySword': 0x36B6D1,
'CopySword_loop_copy': 0x36B6E7,
'CopySword_sword_positon_gfx': 0x36B709,
'DMAKholdstare': 0x3695A5,
'DMATrinexx': 0x369617,
'Dungeon_ResetSprites': 0x09C114,
'EnemizerCodeStart': 0x3694F5,
'EnemizerFlags': 0x368100,
'EnemizerFlags_agahnim_fun_balls': 0x368104,
'EnemizerFlags_close_blind_door': 0x368101,
'EnemizerFlags_enable_mimic_override': 0x368105,
'EnemizerFlags_enable_terrorpin_ai_fix': 0x368106,
'EnemizerFlags_moldorm_eye_count': 0x368102,
'EnemizerFlags_randomize_bushes': 0x368100,
'EnemizerFlags_randomize_sprites': 0x368103,
'EnemizerTablesStart': 0x368000,
'Ext_OnBossDeath': 0x29803C,
'Ext_OnDungeonCompleted': 0x29804B,
'Ext_OnDungeonEnter': 0x298041,
'Ext_OnDungeonExit': 0x298046,
'Ext_OnFairyRevive': 0x298019,
'Ext_OnFileCreate': 0x298000,
'Ext_OnFileLoad': 0x298005,
'Ext_OnFileSave': 0x29800A,
'Ext_OnIemMenuOpen': 0x298023,
'Ext_OnItemChange': 0x29802D,
'Ext_OnItemMenuClose': 0x298028,
'Ext_OnMapUse': 0x298014,
'Ext_OnPlayerAttack': 0x298037,
'Ext_OnPlayerDamaged': 0x298032,
'Ext_OnPlayerDeath': 0x29800F,
'Ext_OnYItemUse': 0x29801E,
'Ext_OnZeldaRescued': 0x298050,
'FixTerrorpin': 0x36971A,
'FixTerrorpin_new': 0x369728,
'GFX_Kholdstare_Shell': 0x36C79A,
'GFX_Trinexx_Shell': 0x36D79A,
'GFX_Trinexx_Shell2': 0x36DF9A,
'GetRandomInt': 0x0DBA71,
'Initialize_Blind_Fight': 0x1DA090,
'Kholdstare_Draw': 0x0DD97F,
'LoadFile': 0x369A87,
'LoadNewSoundFx': 0x369A9E,
'LoadOverworldSprites': 0x36B753,
'Module_LoadFile_indoors': 0x028118,
'Moldorm_UpdateOamPosition': 0x3699E7,
'Moldorm_UpdateOamPosition_more_eyes': 0x3699ED,
'NMIHookAction': 0x36956C,
'NMIHookAction_loadKholdstare': 0x369584,
'NMIHookAction_loadTrinexx': 0x369590,
'NMIHookAction_return': 0x36959A,
'NMIHookReturn': 0x0080D5,
'NewLoadSoundBank': 0x369A98,
'NewLoadSoundBank_Intro': 0x369A92,
'OnInitFileSelect': 0x3696FA,
'OnInitFileSelect_continue': 0x36970F,
'Player_Main': 0x078000,
'Sound_LoadSongBank': 0x008888,
'Sound_SetSfx3PanLong': 0x0DBB8A,
'Sound_SetSfxPanWithPlayerCoords': 0x0DBB67,
'Spawn_Bees': 0x36B75D,
'Spawn_Bees_done': 0x36B779,
'SpritePrep_Eyegore': 0x1EC6FA,
'SpritePrep_EyegoreNew': 0x369A1A,
'SpritePrep_EyegoreNew_mimic': 0x369A31,
'SpritePrep_EyegoreNew_new': 0x369A25,
'Sprite_ResetAll': 0x09C44E,
'Sprite_SpawnDynamically': 0x1DF65D,
'VitreousKeyReset': 0x36B77A,
'boss_move': 0x369743,
'boss_move_loop_bottom_left': 0x369935,
'boss_move_loop_bottom_left2': 0x369963,
'boss_move_loop_bottom_right': 0x3698E2,
'boss_move_loop_bottom_right2': 0x369910,
'boss_move_loop_middle': 0x36983C,
'boss_move_loop_middle2': 0x36986A,
'boss_move_loop_top_right': 0x36988F,
'boss_move_loop_top_right2': 0x3698BD,
'boss_move_move_to_bottom_left': 0x369933,
'boss_move_move_to_bottom_right': 0x3698E0,
'boss_move_move_to_middle': 0x36983A,
'boss_move_move_to_top_right': 0x36988D,
'boss_move_no_blind_door': 0x3697D2,
'boss_move_no_change': 0x369863,
'boss_move_no_change2': 0x3698B6,
'boss_move_no_change3': 0x369909,
'boss_move_no_change4': 0x36995C,
'boss_move_no_change_ov': 0x369885,
'boss_move_no_change_ov2': 0x3698D8,
'boss_move_no_change_ov3': 0x36992B,
'boss_move_no_change_ov4': 0x36997E,
'boss_move_return': 0x369986,
'change_heartcontainer_position': 0x3699BE,
'change_heartcontainer_position_not_moldorm_room': 0x3699E1,
'check_blind_boss_room': 0x36B782,
'check_special_action': 0x369731,
'check_special_action_no_special_action': 0x36973E,
'enemizer_info_table': 0x368000,
'linkIsDead': 0x0780D5,
'linkNotDead': 0x0780F7,
'modified_room_object_table': 0x36B79A,
'moved_room_header_bank_value_address': 0x368374,
'newKodongoCollision': 0x369A02,
'newKodongoCollision_continue': 0x369A19,
'new_kholdstare_code': 0x369987,
'new_kholdstare_code_already_iced': 0x369997,
'new_trinexx_code': 0x36999C,
'new_trinexx_code_already_rocked': 0x3699AC,
'notItemSprite_Mimic': 0x369A66,
'notItemSprite_Mimic_changeSpriteId': 0x369A7A,
'notItemSprite_Mimic_continue': 0x369A82,
'notItemSprite_Mimic_reloadSpriteIdAndSkipMimic': 0x369A7F,
'resetSprite_Mimic': 0x369A4E,
'resetSprite_Mimic_notMimic': 0x369A60,
'room_header_table': 0x368375,
'shieldgfx': 0x36AAD1,
'sprite_bush_spawn': 0x3694F5,
'sprite_bush_spawn_continue': 0x36950F,
'sprite_bush_spawn_dontGoPhase2': 0x369565,
'sprite_bush_spawn_item_table': 0x369525,
'sprite_bush_spawn_newSpriteSpawn': 0x36954C,
'sprite_bush_spawn_not_random': 0x36953B,
'sprite_bush_spawn_not_random_old': 0x36950B,
'sprite_bush_spawn_return': 0x369568,
'sprite_bush_spawn_table': 0x368120,
'sprite_bush_spawn_table_dungeons': 0x368248,
'sprite_bush_spawn_table_overworld': 0x368120,
'sprite_bush_spawn_table_random_sprites': 0x368370,
'swordgfx': 0x369AD1,
}
+308
View File
@@ -0,0 +1,308 @@
import unittest
from types import SimpleNamespace
from worlds.alttp.EnemizerPatches import (
ARROW_REFILL_5_SPRITE_ID,
BOSS_GFX_SHEET_INDEXES,
BOSS_PATCH_DATA,
DAMAGE_GROUP_TABLE_ADDRESS,
DUNGEON_BOSS_PATCH_DATA,
ENEMY_DAMAGE_TABLE_ADDRESS,
ENEMY_HP_TABLE_ADDRESS,
EXCLUDED_ENEMY_TABLE_SPRITE_IDS,
HIDDEN_ENEMY_CHANCE_POOL_ADDRESS,
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL,
RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS,
RETRO_RUPEE_REPLACEMENT_SPRITE_ID,
THIEF_DEFAULT_HP,
THIEF_SPRITE_ID,
TILE_TRAP_FLOOR_TILE_ADDRESS,
TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS,
TRINEXX_ICE_PROJECTILE_TILE_ADDRESS,
VANILLA_HIDDEN_ENEMY_CHANCE_POOL,
_apply_killable_thief,
_apply_randomized_tile_trap_floor_tile,
_get_enemizer_symbol,
_make_native_enemizer_rng,
_option_key,
patch_bosses,
_randomize_enemy_damage,
_randomize_enemy_health,
_set_enemizer_flag,
_shuffle_damage_groups,
_update_hidden_enemy_item_table_for_retro_mode,
apply_enemizer_base_patch,
)
class FakeRom:
def __init__(self, size: int = 0x400000) -> None:
self.buffer = bytearray(size)
def read_byte(self, address: int) -> int:
return self.buffer[address]
def read_bytes(self, startaddress: int, length: int) -> bytearray:
return self.buffer[startaddress:startaddress + length]
def write_byte(self, address: int, value: int) -> None:
self.buffer[address] = value
def write_bytes(self, startaddress: int, values) -> None:
self.buffer[startaddress:startaddress + len(values)] = values
def write_int16(self, address: int, value: int) -> None:
self.write_bytes(address, (value & 0xFF, (value >> 8) & 0xFF))
class TestEnemizerPatches(unittest.TestCase):
def test_enemizer_base_patch_applies_mimic_hooks(self) -> None:
rom = FakeRom()
apply_enemizer_base_patch(rom)
self.assertEqual(tuple(rom.read_bytes(0x307CB, 2)), (0xB6, 0x91))
self.assertEqual(tuple(rom.read_bytes(0x311B6, 4)), (0x22, 0x1A, 0x9A, 0x36))
self.assertEqual(tuple(rom.read_bytes(0x36C08, 5)), (0x22, 0x4E, 0x9A, 0x36, 0xEA))
self.assertEqual(tuple(rom.read_bytes(0x36DA6, 4)), (0x22, 0x66, 0x9A, 0x36))
self.assertEqual(tuple(rom.read_bytes(0xF0BB1, 2)), (0x95, 0xC7))
self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS, 4)), (0xEA, 0xEA, 0xEA, 0xEA))
self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, 2)), (0x00, 0x00))
self.assertEqual(rom.read_byte(TILE_TRAP_FLOOR_TILE_ADDRESS), 0x00)
def test_randomized_tile_trap_floor_tile_patch_is_separate(self) -> None:
rom = FakeRom()
_apply_randomized_tile_trap_floor_tile(rom)
self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, 2)), (0x88, 0x01))
self.assertEqual(rom.read_byte(TILE_TRAP_FLOOR_TILE_ADDRESS), 0x12)
def test_enemy_shuffle_enables_hidden_enemy_and_mimic_support(self) -> None:
rom = FakeRom()
world = self._build_world(enemy_shuffle=True, bush_shuffle=False)
self._apply_native_enemizer_features(world, rom)
self.assertEqual(
tuple(rom.read_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, len(VANILLA_HIDDEN_ENEMY_CHANCE_POOL))),
VANILLA_HIDDEN_ENEMY_CHANCE_POOL,
)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_bushes")), 0x01)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_sprites")), 0x01)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_mimic_override")), 0x01)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_terrorpin_ai_fix")), 0x01)
self.assertEqual(tuple(rom.read_bytes(0x1F2D5, 2)), (0x54, 0x9C))
self.assertEqual(rom.read_byte(0x1F2E5), 0xB0)
self.assertEqual(rom.read_byte(0x1F2EB), 0xD0)
def test_bush_shuffle_and_remaining_tables_are_patched_natively(self) -> None:
rom = FakeRom()
item_table_address = _get_enemizer_symbol("sprite_bush_spawn_item_table")
not_item_sprite_address = _get_enemizer_symbol("notItemSprite_Mimic")
rom.write_byte(RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS, RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
rom.write_byte(item_table_address + 5, ARROW_REFILL_5_SPRITE_ID)
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID, 0x08)
included_hp_sprite_id = 0x01
included_damage_sprite_id = 0x02
excluded_sprite_id = min(EXCLUDED_ENEMY_TABLE_SPRITE_IDS)
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id, 0x06)
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + excluded_sprite_id, 0x07)
rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + included_damage_sprite_id, 0x06)
rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + excluded_sprite_id, 0x05)
world = self._build_world(
bush_shuffle=True,
killable_thieves=True,
enemy_health="hard",
enemy_damage="chaos",
)
self._apply_native_enemizer_features(world, rom)
self.assertEqual(
tuple(rom.read_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, len(RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL))),
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL,
)
self.assertEqual(rom.read_byte(item_table_address + 5), RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
self.assertEqual(rom.read_byte(not_item_sprite_address + 4), THIEF_SPRITE_ID)
self.assertNotEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 0x08)
self.assertGreaterEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 2)
self.assertLess(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 25)
self.assertGreaterEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id), 2)
self.assertLess(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id), 25)
self.assertEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + excluded_sprite_id), 0x07)
self.assertIn(rom.read_byte(ENEMY_DAMAGE_TABLE_ADDRESS + included_damage_sprite_id), range(8))
self.assertEqual(rom.read_byte(ENEMY_DAMAGE_TABLE_ADDRESS + excluded_sprite_id), 0x05)
for group_id in range(10):
group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
green_mail, blue_mail, red_mail = rom.read_bytes(group_address, 3)
self.assertIn(green_mail, range(64))
self.assertIn(blue_mail, range(64))
self.assertIn(red_mail, range(64))
def test_killable_thief_sets_default_hp_without_enemy_health_shuffle(self) -> None:
rom = FakeRom()
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID, 0x08)
world = self._build_world(killable_thieves=True)
self._apply_native_enemizer_features(world, rom)
self.assertEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), THIEF_DEFAULT_HP)
def test_bush_shuffle_without_enemy_shuffle_does_not_enable_sprite_randomization_flags(self) -> None:
rom = FakeRom()
self._apply_native_enemizer_features(self._build_world(bush_shuffle=True), rom)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_bushes")), 0x01)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_sprites")), 0x00)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_mimic_override")), 0x00)
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_terrorpin_ai_fix")), 0x00)
self.assertEqual(tuple(rom.read_bytes(0x1F2D5, 2)), (0x00, 0x00))
self.assertEqual(rom.read_byte(0x1F2E5), 0x00)
self.assertEqual(rom.read_byte(0x1F2EB), 0x00)
def test_non_chaos_enemy_damage_uses_expected_mail_scaling(self) -> None:
rom = FakeRom()
self._apply_native_enemizer_features(self._build_world(enemy_damage="hard"), rom)
for group_id in range(10):
group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
green_mail, blue_mail, red_mail = rom.read_bytes(group_address, 3)
self.assertEqual(blue_mail, green_mail * 3 // 4)
self.assertEqual(red_mail, green_mail * 3 // 8)
def test_patch_bosses_overwrites_enemy_shuffle_boss_room_graphics(self) -> None:
rom = FakeRom()
dungeon_header_base = _get_enemizer_symbol("room_header_table")
eastern_dungeon_data = DUNGEON_BOSS_PATCH_DATA[("Eastern Palace", None)]
rom.write_byte(dungeon_header_base + (eastern_dungeon_data.room_id * 14) + 3, BOSS_PATCH_DATA["Armos"].graphics)
for table_index in BOSS_GFX_SHEET_INDEXES.values():
rom.write_byte(0x4FC0 + table_index, 0xAA)
rom.write_byte(0x509F + table_index, 0xBB)
rom.write_byte(0x517E + table_index, 0xCC)
patch_bosses(self._build_boss_world({"Eastern Palace": "Vitreous"}), rom)
eastern_boss_data = BOSS_PATCH_DATA["Vitreous"]
self.assertEqual(
tuple(rom.read_bytes(eastern_dungeon_data.sprite_pointer_address, 2)),
eastern_boss_data.pointer,
)
self.assertEqual(
rom.read_byte(dungeon_header_base + (eastern_dungeon_data.room_id * 14) + 3),
eastern_boss_data.graphics,
)
for table_index in BOSS_GFX_SHEET_INDEXES.values():
self.assertEqual(rom.read_byte(0x4FC0 + table_index), 0xAA)
self.assertEqual(rom.read_byte(0x509F + table_index), 0xBB)
self.assertEqual(rom.read_byte(0x517E + table_index), 0xCC)
def test_native_enemizer_rng_is_deterministic_for_same_world_settings(self) -> None:
world = self._build_world(enemy_health="hard", enemy_damage="chaos", bush_shuffle=True)
rng_a = _make_native_enemizer_rng(world)
rng_b = _make_native_enemizer_rng(world)
self.assertEqual([rng_a.randrange(256) for _ in range(8)], [rng_b.randrange(256) for _ in range(8)])
@staticmethod
def _apply_native_enemizer_features(world: SimpleNamespace, rom: FakeRom) -> None:
enemy_shuffle_enabled = bool(world.options.enemy_shuffle)
bush_shuffle_enabled = bool(world.options.bush_shuffle)
enemy_health_key = _option_key(world.options.enemy_health)
enemy_damage_key = _option_key(world.options.enemy_damage)
if enemy_shuffle_enabled or bush_shuffle_enabled:
_set_enemizer_flag(rom, "EnemizerFlags_randomize_bushes", True)
hidden_enemy_chance_pool = (
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL if bush_shuffle_enabled else VANILLA_HIDDEN_ENEMY_CHANCE_POOL
)
rom.write_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, hidden_enemy_chance_pool)
_update_hidden_enemy_item_table_for_retro_mode(rom)
if enemy_shuffle_enabled:
_set_enemizer_flag(rom, "EnemizerFlags_randomize_sprites", True)
_set_enemizer_flag(rom, "EnemizerFlags_enable_mimic_override", True)
_set_enemizer_flag(rom, "EnemizerFlags_enable_terrorpin_ai_fix", True)
rom.write_bytes(0x1F2D5, (0x54, 0x9C))
rom.write_byte(0x1F2E5, 0xB0)
rom.write_byte(0x1F2EB, 0xD0)
if world.options.killable_thieves:
_apply_killable_thief(rom)
if enemy_health_key != "default" or enemy_damage_key != "default":
rng = _make_native_enemizer_rng(world)
else:
rng = None
if enemy_health_key != "default":
assert rng is not None
_randomize_enemy_health(rom, rng, enemy_health_key)
if enemy_damage_key != "default":
assert rng is not None
_randomize_enemy_damage(rom, rng, allow_zero_damage=True)
_shuffle_damage_groups(rom, rng, chaos_mode=enemy_damage_key == "chaos", allow_zero_damage=True)
@staticmethod
def _build_world(
*,
enemy_shuffle: bool = False,
bush_shuffle: bool = False,
killable_thieves: bool = False,
enemy_health: str = "default",
enemy_damage: str = "default",
) -> SimpleNamespace:
return SimpleNamespace(
player=1,
multiworld=SimpleNamespace(seed=12345, seed_name="native-enemizer-test"),
options=SimpleNamespace(
enemy_shuffle=enemy_shuffle,
bush_shuffle=bush_shuffle,
killable_thieves=killable_thieves,
enemy_health=SimpleNamespace(current_key=enemy_health),
enemy_damage=SimpleNamespace(current_key=enemy_damage),
),
)
@staticmethod
def _build_boss_world(boss_overrides: dict[str, str] | None = None) -> SimpleNamespace:
boss_overrides = boss_overrides or {}
def boss(name: str) -> SimpleNamespace:
return SimpleNamespace(enemizer_name=name)
return SimpleNamespace(
options=SimpleNamespace(mode="open"),
dungeons={
"Eastern Palace": SimpleNamespace(boss=boss(boss_overrides.get("Eastern Palace", "Armos"))),
"Desert Palace": SimpleNamespace(boss=boss(boss_overrides.get("Desert Palace", "Lanmola"))),
"Tower of Hera": SimpleNamespace(boss=boss(boss_overrides.get("Tower of Hera", "Moldorm"))),
"Palace of Darkness": SimpleNamespace(boss=boss(boss_overrides.get("Palace of Darkness", "Helmasaur"))),
"Swamp Palace": SimpleNamespace(boss=boss(boss_overrides.get("Swamp Palace", "Arrghus"))),
"Skull Woods": SimpleNamespace(boss=boss(boss_overrides.get("Skull Woods", "Mothula"))),
"Thieves Town": SimpleNamespace(boss=boss(boss_overrides.get("Thieves Town", "Blind"))),
"Ice Palace": SimpleNamespace(boss=boss(boss_overrides.get("Ice Palace", "Kholdstare"))),
"Misery Mire": SimpleNamespace(boss=boss(boss_overrides.get("Misery Mire", "Vitreous"))),
"Turtle Rock": SimpleNamespace(boss=boss(boss_overrides.get("Turtle Rock", "Trinexx"))),
"Ganons Tower": SimpleNamespace(
bosses={
"bottom": boss(boss_overrides.get("Ganons Tower Bottom", "Armos")),
"middle": boss(boss_overrides.get("Ganons Tower Middle", "Lanmola")),
"top": boss(boss_overrides.get("Ganons Tower Top", "Moldorm")),
}
),
},
)
if __name__ == "__main__":
unittest.main()
+834
View File
@@ -0,0 +1,834 @@
import unittest
from types import SimpleNamespace
import random
from worlds.alttp.EnemyShuffle import (
DungeonEnemyRoom,
DungeonEnemySprite,
DungeonSpriteGroup,
EnemyShuffleState,
EnemySpriteRequirement,
OverworldEnemyArea,
OverworldEnemySprite,
RandomizedDungeonEnemyRoom,
RandomizedDungeonEnemySprite,
RandomizedOverworldEnemyArea,
RandomizedOverworldEnemySprite,
WALLMASTER_SPRITE_ID,
_load_dungeon_sprite_metadata,
_read_room_sprites,
get_possible_dungeon_sprite_groups,
_get_requirements_for_usable_dungeon_enemies,
_get_requirements_for_usable_overworld_enemies,
_get_randomizable_sprites_in_room,
_apply_selected_boss_group_requirements,
_randomize_overworld_groups,
_randomize_room_sprites,
_setup_required_overworld_groups,
can_spawn_in_room,
validate_enemy_shuffle_state,
)
class TestEnemyShuffleValidation(unittest.TestCase):
def test_curated_room_sprite_addresses_exclude_hera_basement_key_slot(self) -> None:
room_id = 135
sprite_table_address = 0x4E397
rom_bytes = bytearray(0x4E3C0)
rom_bytes[sprite_table_address] = 0
room_135_sprite_records = (
(0x4E398, 0x05, 0x14, 0x18),
(0x4E39B, 0x07, 0x1A, 0x18),
(0x4E39E, 0x0B, 0x13, 0x18),
(0x4E3A1, 0x19, 0x06, 0x18),
(0x4E3A4, 0x08, 0xE7, 0x14),
(0x4E3A7, 0x04, 0x17, 0x1E),
(0x4E3AA, 0x0C, 0x03, 0x1E),
(0x4E3AD, 0x15, 0x04, 0x1E),
(0x4E3B0, 0x17, 0x0B, 0xA7),
(0x4E3B3, 0x18, 0x19, 0xA7),
(0x4E3B6, 0x19, 0x04, 0xA7),
(0x4E3B9, 0x1A, 0x08, 0xE4),
(0x4E3BC, 0x1C, 0x15, 0xA7),
)
for address, byte_0, byte_1, sprite_id in room_135_sprite_records:
rom_bytes[address] = byte_0
rom_bytes[address + 1] = byte_1
rom_bytes[address + 2] = sprite_id
rom_bytes[0x4E3BF] = 0xFF
sprites = _read_room_sprites(rom_bytes, room_id, sprite_table_address, _load_dungeon_sprite_metadata())
sprite_addresses = {sprite.address for sprite in sprites}
self.assertNotIn(0x4E3B9, sprite_addresses)
self.assertIn(0x4E3B6, sprite_addresses)
self.assertFalse(any(sprite.has_key for sprite in sprites))
def test_curated_room_sprite_addresses_deduplicate_duplicate_slots(self) -> None:
room_id = 125
sprite_table_address = 0x4E2CA
metadata = _load_dungeon_sprite_metadata()
max_sprite_id_address = max(metadata["room_sprite_id_addresses"][room_id])
rom_bytes = bytearray(max_sprite_id_address + 2)
rom_bytes[sprite_table_address] = 0
for offset, sprite_id_address in enumerate(metadata["room_sprite_id_addresses"][room_id]):
address = sprite_id_address - 2
sprite_id = 0x80 if offset % 2 == 0 else 0x81
rom_bytes[address] = 0
rom_bytes[address + 1] = 0
rom_bytes[address + 2] = sprite_id
sprites = _read_room_sprites(rom_bytes, room_id, sprite_table_address, metadata)
sprite_addresses = [sprite.address for sprite in sprites]
self.assertEqual(len(sprite_addresses), len(set(sprite_addresses)))
def test_rejects_non_killable_shutter_room(self) -> None:
room = DungeonEnemyRoom(
room_id=1,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x10, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=True,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={1: room},
randomized_dungeon_rooms={
1: RandomizedDungeonEnemyRoom(
room_id=1,
room_header_address=0,
sprite_table_address=0,
original_graphics_block_id=1,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
RandomizedDungeonEnemySprite(
address=0x1000,
byte_0=0,
byte_1=0,
original_sprite_id=0x10,
sprite_id=0x11,
is_overlord=False,
has_key=False,
),
),
skipped_randomization=False,
)
},
sprite_requirements=(
self._requirement(0x10, killable=True, subgroup_0=(1,)),
self._requirement(0x11, killable=False, subgroup_0=(1,)),
),
)
with self.assertRaises(ValueError):
validate_enemy_shuffle_state(state, is_standard_mode=False)
def test_rejects_water_enemy_in_non_water_room(self) -> None:
room = DungeonEnemyRoom(
room_id=165,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=True,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={165: room},
randomized_dungeon_rooms={
165: RandomizedDungeonEnemyRoom(
room_id=165,
room_header_address=0,
sprite_table_address=0,
original_graphics_block_id=1,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
RandomizedDungeonEnemySprite(
address=0x1000,
byte_0=0,
byte_1=0,
original_sprite_id=0x20,
sprite_id=0x81,
is_overlord=False,
has_key=False,
),
),
skipped_randomization=False,
)
},
sprite_requirements=(
self._requirement(0x20, killable=True, subgroup_0=(1,)),
self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
),
)
with self.assertRaisesRegex(ValueError, "water enemy"):
validate_enemy_shuffle_state(state, is_standard_mode=False)
def test_rejects_multiple_flopping_fish(self) -> None:
area = OverworldEnemyArea(
area_id=0x10,
sprite_table_address=0,
graphics_block_address=0,
graphics_block_id=1,
bush_sprite_id=0x20,
sprites=(
OverworldEnemySprite(address=0x2000, y_coord=0, x_coord=0, sprite_id=0x20),
OverworldEnemySprite(address=0x2003, y_coord=0, x_coord=0, sprite_id=0x21),
),
do_not_randomize=False,
)
state = self._build_state(
overworld_areas={0x10: area},
randomized_overworld_areas={
0x10: RandomizedOverworldEnemyArea(
area_id=0x10,
sprite_table_address=0,
graphics_block_address=0,
original_graphics_block_id=1,
graphics_block_id=1,
original_bush_sprite_id=0x20,
bush_sprite_id=0xD2,
sprites=(
RandomizedOverworldEnemySprite(
address=0x2000,
y_coord=0,
x_coord=0,
original_sprite_id=0x20,
sprite_id=0xD2,
),
RandomizedOverworldEnemySprite(
address=0x2003,
y_coord=0,
x_coord=0,
original_sprite_id=0x21,
sprite_id=0xD2,
),
),
skipped_randomization=False,
)
},
sprite_requirements=(
self._requirement(0x20, group_ids=(1,)),
self._requirement(0x21, group_ids=(1,)),
self._requirement(0x22, group_ids=(1,)),
self._requirement(0xD2, group_ids=(1,)),
),
)
with self.assertRaises(ValueError):
validate_enemy_shuffle_state(state, is_standard_mode=False)
def test_allows_multiple_flopping_fish_when_no_other_sprite_is_possible(self) -> None:
area = OverworldEnemyArea(
area_id=0x10,
sprite_table_address=0,
graphics_block_address=0,
graphics_block_id=1,
bush_sprite_id=0x20,
sprites=(
OverworldEnemySprite(address=0x2000, y_coord=0, x_coord=0, sprite_id=0x20),
OverworldEnemySprite(address=0x2003, y_coord=0, x_coord=0, sprite_id=0x21),
),
do_not_randomize=False,
)
state = self._build_state(
overworld_areas={0x10: area},
randomized_overworld_areas={
0x10: RandomizedOverworldEnemyArea(
area_id=0x10,
sprite_table_address=0,
graphics_block_address=0,
original_graphics_block_id=1,
graphics_block_id=1,
original_bush_sprite_id=0x20,
bush_sprite_id=0xD2,
sprites=(
RandomizedOverworldEnemySprite(
address=0x2000,
y_coord=0,
x_coord=0,
original_sprite_id=0x20,
sprite_id=0xD2,
),
RandomizedOverworldEnemySprite(
address=0x2003,
y_coord=0,
x_coord=0,
original_sprite_id=0x21,
sprite_id=0xD2,
),
),
skipped_randomization=False,
)
},
sprite_requirements=(
self._requirement(0x20, group_ids=(2,)),
self._requirement(0x21, group_ids=(2,)),
self._requirement(0xD2, group_ids=(1,)),
),
)
validate_enemy_shuffle_state(state, is_standard_mode=False)
def test_excludes_absorbables_from_usable_enemy_pools(self) -> None:
state = self._build_state(
sprite_requirements=(
self._requirement(0x10, subgroup_0=(1,)),
self._requirement(0xE3, subgroup_0=(1,), absorbable=True),
self._requirement(0x20, subgroup_0=(1,), never_use_dungeon=True),
self._requirement(0x21, subgroup_0=(1,), never_use_overworld=True),
),
)
self.assertEqual(
[requirement.sprite_id for requirement in _get_requirements_for_usable_dungeon_enemies(state)],
[0x10, 0x21],
)
self.assertEqual(
[requirement.sprite_id for requirement in _get_requirements_for_usable_overworld_enemies(state)],
[0x10, 0x20],
)
def test_key_enemy_replacements_exclude_moblins(self) -> None:
room = DungeonEnemyRoom(
room_id=1,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x12, is_overlord=False, has_key=True),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=False,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={1: room},
sprite_requirements=(
self._requirement(0x12, killable=True, subgroup_0=(1,), cannot_have_key=True),
self._requirement(0x13, killable=True, subgroup_0=(1,)),
),
)
selected_group = state.sprite_groups[0x41]
randomized_room = _randomize_room_sprites(
SimpleNamespace(random=random.Random(0)),
state,
room,
selected_group,
False,
)
self.assertEqual(randomized_room.sprites[0].sprite_id, 0x13)
def test_shutter_water_room_prefers_killable_water_enemy(self) -> None:
room = DungeonEnemyRoom(
room_id=40,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x8A, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=True,
is_water_room=True,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={40: room},
sprite_requirements=(
self._requirement(0x8A, killable=False, subgroup_2=(34,)),
self._requirement(0x81, killable=True, subgroup_2=(34,), is_water_sprite=True),
self._requirement(0x9A, killable=False, subgroup_2=(34,), is_water_sprite=True),
),
)
selected_group = state.sprite_groups[0x41]
selected_group.subgroup_2 = 34
randomized_room = _randomize_room_sprites(
SimpleNamespace(random=random.Random(0)),
state,
room,
selected_group,
False,
)
self.assertEqual(randomized_room.sprites[0].sprite_id, 0x81)
def test_non_water_shutter_room_replacements_exclude_water_enemies(self) -> None:
room = DungeonEnemyRoom(
room_id=165,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=True,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={165: room},
sprite_requirements=(
self._requirement(0x20, killable=False, subgroup_0=(1,)),
self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
self._requirement(0x22, killable=True, subgroup_0=(1,)),
),
)
randomized_room = _randomize_room_sprites(
SimpleNamespace(random=random.Random(1)),
state,
room,
state.sprite_groups[0x41],
False,
)
self.assertEqual(randomized_room.sprites[0].sprite_id, 0x22)
def test_non_water_shutter_group_selection_requires_non_water_killable_enemy(self) -> None:
room = DungeonEnemyRoom(
room_id=165,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=True,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={165: room},
sprite_requirements=(
self._requirement(0x20, killable=False, subgroup_0=(1,)),
self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
),
)
self.assertEqual(get_possible_dungeon_sprite_groups(state, room), tuple())
def test_wallmaster_cannot_spawn_in_high_room_ids(self) -> None:
room = DungeonEnemyRoom(
room_id=0x100,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=tuple(),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=False,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
self.assertFalse(can_spawn_in_room(self._requirement(WALLMASTER_SPRITE_ID), room))
def test_room_specific_do_not_randomize_sprites_are_not_updated(self) -> None:
room = DungeonEnemyRoom(
room_id=7,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x30, is_overlord=False, has_key=False),
DungeonEnemySprite(address=0x1003, byte_0=0, byte_1=0, sprite_id=0x31, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=False,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={7: room},
sprite_requirements=(
self._requirement(0x30, subgroup_0=(1,), dont_randomize_rooms=(7,)),
self._requirement(0x31, subgroup_0=(1,)),
),
)
self.assertEqual(
[sprite.sprite_id for sprite in _get_randomizable_sprites_in_room(state, room)],
[0x31],
)
def test_water_rooms_only_use_water_enemies(self) -> None:
room = DungeonEnemyRoom(
room_id=1,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=False,
is_water_room=True,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={1: room},
sprite_requirements=(
self._requirement(0x20, subgroup_0=(1,)),
self._requirement(0x21, subgroup_0=(1,), is_water_sprite=True),
self._requirement(0x22, subgroup_0=(1,), is_water_sprite=True),
),
)
randomized_room = _randomize_room_sprites(
SimpleNamespace(random=random.Random(0)),
state,
room,
state.sprite_groups[0x41],
False,
)
self.assertIn(randomized_room.sprites[0].sprite_id, {0x21, 0x22})
def test_dungeon_group_selection_excludes_groups_without_enemy_requirements(self) -> None:
room = DungeonEnemyRoom(
room_id=1,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=False,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={1: room},
sprite_requirements=(self._requirement(0x20, subgroup_0=(1,)),),
)
state.sprite_groups[0x42] = DungeonSpriteGroup(
group_id=0x42,
dungeon_group_id=2,
subgroup_0=0,
subgroup_1=0,
subgroup_2=0,
subgroup_3=0,
)
possible_groups = get_possible_dungeon_sprite_groups(state, room)
self.assertEqual([group.group_id for group in possible_groups], [0x41])
def test_key_room_group_selection_excludes_groups_without_room_spawnable_key_enemies(self) -> None:
room = DungeonEnemyRoom(
room_id=61,
room_header_address=0,
sprite_table_address=0,
graphics_block_id=1,
tag_1=0,
tag_2=0,
sort_sprites_value=0,
sprites=(
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=True),
),
required_group_id=None,
required_subgroup_0=tuple(),
required_subgroup_1=tuple(),
required_subgroup_2=tuple(),
required_subgroup_3=tuple(),
is_shutter_room=False,
is_water_room=False,
do_not_randomize=False,
no_special_enemies_standard=False,
)
state = self._build_state(
dungeon_rooms={61: room},
sprite_requirements=(
self._requirement(0x20, subgroup_0=(1,)),
self._requirement(0x50, killable=True, subgroup_1=(32,), excluded_rooms=(61,)),
self._requirement(0x9C, killable=True, subgroup_1=(32,), cannot_have_key=True),
self._requirement(0x51, killable=True, subgroup_1=(33,)),
),
)
state.sprite_groups[0x41] = DungeonSpriteGroup(
group_id=0x41,
dungeon_group_id=1,
subgroup_0=1,
subgroup_1=32,
subgroup_2=1,
subgroup_3=1,
)
state.sprite_groups[0x42] = DungeonSpriteGroup(
group_id=0x42,
dungeon_group_id=2,
subgroup_0=1,
subgroup_1=33,
subgroup_2=1,
subgroup_3=1,
)
possible_groups = get_possible_dungeon_sprite_groups(state, room)
self.assertEqual([group.group_id for group in possible_groups], [0x42])
def test_overworld_group_randomization_preserves_forced_subgroups(self) -> None:
sprite_groups = {
7: DungeonSpriteGroup(group_id=7, dungeon_group_id=-57, subgroup_0=1, subgroup_1=2, subgroup_2=3, subgroup_3=4),
}
_setup_required_overworld_groups(
sprite_groups,
(
SimpleNamespace(
group_id=7,
subgroup_0=None,
subgroup_1=None,
subgroup_2=None,
subgroup_3=17,
areas=(0x02,),
),
),
)
_randomize_overworld_groups(SimpleNamespace(random=random.Random(0)), sprite_groups)
group = sprite_groups[7]
self.assertEqual(group.subgroup_3, 17)
self.assertIn(group.subgroup_0, {22, 31, 47, 14})
self.assertIn(group.subgroup_1, {44, 30, 32})
self.assertIn(group.subgroup_2, {12, 18, 23, 24, 28, 46, 34, 35, 39, 40, 38, 41, 36, 37, 42})
def test_selected_boss_group_requirements_override_shared_boss_graphics_group(self) -> None:
sprite_groups = {
0x56: DungeonSpriteGroup(
group_id=0x56,
dungeon_group_id=22,
subgroup_0=1,
subgroup_1=1,
subgroup_2=60,
subgroup_3=49,
),
}
sprite_requirements = (
self._requirement(162, subgroup_2=(60,)),
self._requirement(189, subgroup_3=(61,)),
)
_apply_selected_boss_group_requirements(
self._build_boss_world({"Eastern Palace": "Vitreous"}),
sprite_groups,
sprite_requirements,
)
group = sprite_groups[0x56]
self.assertEqual(group.subgroup_2, 60)
self.assertEqual(group.subgroup_3, 61)
self.assertTrue(group.preserve_subgroup_2)
self.assertTrue(group.preserve_subgroup_3)
@staticmethod
def _requirement(
sprite_id: int,
*,
killable: bool = False,
subgroup_0: tuple[int, ...] = tuple(),
subgroup_1: tuple[int, ...] = tuple(),
subgroup_2: tuple[int, ...] = tuple(),
subgroup_3: tuple[int, ...] = tuple(),
group_ids: tuple[int, ...] = tuple(),
absorbable: bool = False,
never_use_dungeon: bool = False,
never_use_overworld: bool = False,
cannot_have_key: bool = False,
is_water_sprite: bool = False,
excluded_rooms: tuple[int, ...] = tuple(),
dont_randomize_rooms: tuple[int, ...] = tuple(),
) -> EnemySpriteRequirement:
return EnemySpriteRequirement(
sprite_name=f"sprite_{sprite_id:02x}",
sprite_id=sprite_id,
boss=False,
overlord=False,
do_not_randomize=False,
killable=killable,
npc=False,
never_use_dungeon=never_use_dungeon,
never_use_overworld=never_use_overworld,
cannot_have_key=cannot_have_key,
is_object=False,
absorbable=absorbable,
is_water_sprite=is_water_sprite,
is_enemy_sprite=True,
group_ids=group_ids,
subgroup_0=subgroup_0,
subgroup_1=subgroup_1,
subgroup_2=subgroup_2,
subgroup_3=subgroup_3,
parameters=None,
special_glitched=False,
excluded_rooms=excluded_rooms,
dont_randomize_rooms=dont_randomize_rooms,
spawnable_rooms=tuple(),
)
@staticmethod
def _build_state(
*,
dungeon_rooms=None,
overworld_areas=None,
randomized_dungeon_rooms=None,
randomized_overworld_areas=None,
sprite_requirements=tuple(),
) -> EnemyShuffleState:
sprite_groups = {
1: DungeonSpriteGroup(group_id=1, dungeon_group_id=-63, subgroup_0=1, subgroup_1=1, subgroup_2=1, subgroup_3=1),
0x41: DungeonSpriteGroup(group_id=0x41, dungeon_group_id=1, subgroup_0=1, subgroup_1=1, subgroup_2=1, subgroup_3=1),
}
return EnemyShuffleState(
dungeon_rooms=dungeon_rooms or {},
overworld_areas=overworld_areas or {},
sprite_groups=sprite_groups,
sprite_requirements=sprite_requirements,
room_group_requirements=tuple(),
overworld_group_requirements=tuple(),
shutter_room_ids=frozenset(),
water_room_ids=frozenset(),
dont_randomize_room_ids=frozenset(),
no_special_enemies_standard_room_ids=frozenset(),
boss_room_ids=frozenset(),
dont_randomize_overworld_area_ids=frozenset(),
randomized_dungeon_rooms=randomized_dungeon_rooms or {},
randomized_overworld_areas=randomized_overworld_areas or {},
)
@staticmethod
def _build_boss_world(boss_overrides: dict[str, str] | None = None) -> SimpleNamespace:
boss_overrides = boss_overrides or {}
def boss(name: str) -> SimpleNamespace:
return SimpleNamespace(enemizer_name=name)
return SimpleNamespace(
options=SimpleNamespace(mode="open"),
dungeons={
"Eastern Palace": SimpleNamespace(boss=boss(boss_overrides.get("Eastern Palace", "Armos"))),
"Desert Palace": SimpleNamespace(boss=boss(boss_overrides.get("Desert Palace", "Lanmola"))),
"Tower of Hera": SimpleNamespace(boss=boss(boss_overrides.get("Tower of Hera", "Moldorm"))),
"Palace of Darkness": SimpleNamespace(boss=boss(boss_overrides.get("Palace of Darkness", "Helmasaur"))),
"Swamp Palace": SimpleNamespace(boss=boss(boss_overrides.get("Swamp Palace", "Arrghus"))),
"Skull Woods": SimpleNamespace(boss=boss(boss_overrides.get("Skull Woods", "Mothula"))),
"Thieves Town": SimpleNamespace(boss=boss(boss_overrides.get("Thieves Town", "Blind"))),
"Ice Palace": SimpleNamespace(boss=boss(boss_overrides.get("Ice Palace", "Kholdstare"))),
"Misery Mire": SimpleNamespace(boss=boss(boss_overrides.get("Misery Mire", "Vitreous"))),
"Turtle Rock": SimpleNamespace(boss=boss(boss_overrides.get("Turtle Rock", "Trinexx"))),
"Ganons Tower": SimpleNamespace(
bosses={
"bottom": boss(boss_overrides.get("Ganons Tower Bottom", "Armos")),
"middle": boss(boss_overrides.get("Ganons Tower Middle", "Lanmola")),
"top": boss(boss_overrides.get("Ganons Tower Top", "Moldorm")),
}
),
},
)
if __name__ == "__main__":
unittest.main()
+56
View File
@@ -0,0 +1,56 @@
import random
import unittest
from types import SimpleNamespace
from worlds.alttp.PotShuffle import (
POT_KEY,
POT_HOLE,
generate_pot_shuffle,
get_unique_pot_item_position,
)
class TestPotShuffle(unittest.TestCase):
def test_reserved_key_rooms_only_place_actual_keys(self) -> None:
for seed in range(10):
world = SimpleNamespace(
random=random.Random(seed),
options=SimpleNamespace(retro_bow=False),
)
shuffled_pots = generate_pot_shuffle(world)
conveyor_cross_keys = [
pot for pot in shuffled_pots[0x8B]
if pot.item == POT_KEY
]
self.assertEqual(len(conveyor_cross_keys), 1)
def test_get_unique_pot_item_position_returns_single_match(self) -> None:
world = SimpleNamespace(
random=random.Random(0),
options=SimpleNamespace(retro_bow=False),
)
shuffled_pots = generate_pot_shuffle(world)
self.assertEqual(
get_unique_pot_item_position(shuffled_pots, 0x36, POT_KEY),
(114, 16),
)
def test_reserved_hole_room_keeps_hole_fixed(self) -> None:
for seed in range(25):
world = SimpleNamespace(
random=random.Random(seed),
options=SimpleNamespace(retro_bow=False),
)
shuffled_pots = generate_pot_shuffle(world)
hole_positions = [
(pot.x, pot.y)
for pot in shuffled_pots[206]
if pot.item == POT_HOLE
]
self.assertEqual(hole_positions, [(204, 11)])
if __name__ == "__main__":
unittest.main()
+2 -2
View File
@@ -1,6 +1,6 @@
{
"game": "APQuest",
"minimum_ap_version": "0.6.4",
"world_version": "1.0.1",
"minimum_ap_version": "0.6.7",
"world_version": "2.0.0",
"authors": ["NewSoupVi"]
}
+1 -1
View File
@@ -198,7 +198,7 @@ class APQuestContext(CommonContext):
if self.ap_quest_game is None:
raise RuntimeError("Tried to render before self.ap_quest_game was initialized.")
self.ui.render(self.ap_quest_game, self.player_sprite)
self.ui.render(self.ap_quest_game, self.player_sprite, self.hard_mode)
self.handle_game_events()
def location_checked_side_effects(self, location: int) -> None:
+4 -4
View File
@@ -88,23 +88,23 @@ class APQuestManager(GameManager):
self.game_view.force_focus()
self.sound_manager.game_started = True
def render(self, game: Game, player_sprite: PlayerSprite) -> None:
def render(self, game: Game, player_sprite: PlayerSprite, hard_mode: bool) -> None:
self.setup_game_grid_if_not_setup(game)
# This calls game.render(), which needs to happen to update the state of math traps
self.render_gameboard(game, player_sprite)
self.render_gameboard(game, player_sprite, hard_mode)
# Only now can we check whether a math problem is active
self.render_background_game_grid(game.gameboard.size, game.active_math_problem is None)
self.sound_manager.math_trap_active = game.active_math_problem is not None
self.render_item_column(game)
def render_gameboard(self, game: Game, player_sprite: PlayerSprite) -> None:
def render_gameboard(self, game: Game, player_sprite: PlayerSprite, hard_mode: bool) -> None:
rendered_gameboard = game.render()
for gameboard_row, image_row in zip(rendered_gameboard, self.top_image_grid, strict=False):
for graphic, image in zip(gameboard_row, image_row[:11], strict=False):
texture = get_texture(graphic, player_sprite)
texture = get_texture(graphic, player_sprite, hard_mode)
if texture is None:
image.opacity = 0
+16 -11
View File
@@ -29,6 +29,7 @@ class RelatedTexture(NamedTuple):
IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = {
# Inanimates
Graphic.WALL: RelatedTexture("inanimates.png", 16, 32, 16, 16),
Graphic.BREAKABLE_BLOCK: RelatedTexture("inanimates.png", 32, 32, 16, 16),
Graphic.CHEST: RelatedTexture("inanimates.png", 0, 16, 16, 16),
@@ -37,29 +38,25 @@ IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = {
Graphic.BUTTON_NOT_ACTIVATED: RelatedTexture("inanimates.png", 0, 0, 16, 16),
Graphic.BUTTON_ACTIVATED: RelatedTexture("inanimates.png", 16, 0, 16, 16),
Graphic.BUTTON_DOOR: RelatedTexture("inanimates.png", 32, 0, 16, 16),
# Enemies
Graphic.NORMAL_ENEMY_1_HEALTH: RelatedTexture("normal_enemy.png", 0, 0, 16, 16),
Graphic.NORMAL_ENEMY_2_HEALTH: RelatedTexture("normal_enemy.png", 16, 0, 16, 16),
Graphic.BOSS_5_HEALTH: RelatedTexture("boss.png", 16, 16, 16, 16),
Graphic.BOSS_4_HEALTH: RelatedTexture("boss.png", 0, 16, 16, 16),
Graphic.BOSS_3_HEALTH: RelatedTexture("boss.png", 32, 32, 16, 16),
Graphic.BOSS_2_HEALTH: RelatedTexture("boss.png", 16, 32, 16, 16),
Graphic.BOSS_1_HEALTH: RelatedTexture("boss.png", 0, 32, 16, 16),
# Items
Graphic.EMPTY_HEART: RelatedTexture("hearts.png", 0, 0, 16, 16),
Graphic.HEART: RelatedTexture("hearts.png", 16, 0, 16, 16),
Graphic.HALF_HEART: RelatedTexture("hearts.png", 32, 0, 16, 16),
Graphic.REMOTE_ITEM: RelatedTexture("items.png", 0, 16, 16, 16),
Graphic.CONFETTI_CANNON: RelatedTexture("items.png", 16, 16, 16, 16),
Graphic.HAMMER: RelatedTexture("items.png", 32, 16, 16, 16),
Graphic.KEY: RelatedTexture("items.png", 0, 0, 16, 16),
Graphic.SHIELD: RelatedTexture("items.png", 16, 0, 16, 16),
Graphic.SWORD: RelatedTexture("items.png", 32, 0, 16, 16),
Graphic.ITEMS_TEXT: "items_text.png",
# Numbers
Graphic.ZERO: RelatedTexture("numbers.png", 0, 16, 16, 16),
Graphic.ONE: RelatedTexture("numbers.png", 16, 16, 16, 16),
Graphic.TWO: RelatedTexture("numbers.png", 32, 16, 16, 16),
@@ -70,26 +67,29 @@ IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = {
Graphic.SEVEN: RelatedTexture("numbers.png", 32, 0, 16, 16),
Graphic.EIGHT: RelatedTexture("numbers.png", 48, 0, 16, 16),
Graphic.NINE: RelatedTexture("numbers.png", 64, 0, 16, 16),
# Letters
Graphic.LETTER_A: RelatedTexture("letters.png", 0, 16, 16, 16),
Graphic.LETTER_E: RelatedTexture("letters.png", 16, 16, 16, 16),
Graphic.LETTER_H: RelatedTexture("letters.png", 32, 16, 16, 16),
Graphic.LETTER_I: RelatedTexture("letters.png", 0, 0, 16, 16),
Graphic.LETTER_M: RelatedTexture("letters.png", 16, 0, 16, 16),
Graphic.LETTER_T: RelatedTexture("letters.png", 32, 0, 16, 16),
# Mathematical symbols
Graphic.DIVIDE: RelatedTexture("symbols.png", 0, 16, 16, 16),
Graphic.EQUALS: RelatedTexture("symbols.png", 16, 16, 16, 16),
Graphic.MINUS: RelatedTexture("symbols.png", 32, 16, 16, 16),
Graphic.PLUS: RelatedTexture("symbols.png", 0, 0, 16, 16),
Graphic.TIMES: RelatedTexture("symbols.png", 16, 0, 16, 16),
# Other visual-only elements
Graphic.ITEMS_TEXT: "items_text.png",
Graphic.NO: RelatedTexture("symbols.png", 32, 0, 16, 16),
Graphic.UNKNOWN: RelatedTexture("symbols.png", 32, 0, 16, 16), # Same as "No"
}
BACKGROUND_TILE = RelatedTexture("inanimates.png", 0, 32, 16, 16)
EASY_MODE_BOSS_2_HEALTH = RelatedTexture("boss.png", 16, 0, 16, 16)
class PlayerSprite(Enum):
HUMAN = 0
@@ -160,13 +160,18 @@ def get_texture_by_identifier(texture_identifier: str | RelatedTexture) -> Textu
return sub_texture
def get_texture(graphic: Graphic | Literal["Grass"], player_sprite: PlayerSprite | None = None) -> Texture | None:
def get_texture(
graphic: Graphic | Literal["Grass"], player_sprite: PlayerSprite | None = None, hard_mode: bool = False
) -> Texture | None:
if graphic == Graphic.EMPTY:
return None
if graphic == "Grass":
return get_texture_by_identifier(BACKGROUND_TILE)
if graphic == Graphic.BOSS_2_HEALTH and not hard_mode:
return get_texture_by_identifier(EASY_MODE_BOSS_2_HEALTH)
if graphic in IMAGE_GRAPHICS:
return get_texture_by_identifier(IMAGE_GRAPHICS[graphic])
+1 -1
View File
@@ -246,7 +246,7 @@ def create_gameboard(hard_mode: bool, hammer_exists: bool, extra_chest: bool) ->
breakable_block = BreakableBlock() if hammer_exists else Empty()
normal_enemy = EnemyWithLoot(2 if hard_mode else 1, Location.ENEMY_DROP)
boss = FinalBoss(5 if hard_mode else 3)
boss = FinalBoss(5 if hard_mode else 2)
gameboard = (
(Empty(), Empty(), Empty(), Wall(), Empty(), Empty(), Empty(), Wall(), Empty(), Empty(), Empty()),
Binary file not shown.

Before

Width:  |  Height:  |  Size: 580 B

After

Width:  |  Height:  |  Size: 754 B

+92 -71
View File
@@ -2,12 +2,16 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from BaseClasses import CollectionState
from worlds.generic.Rules import add_rule, set_rule
from rule_builder.options import OptionFilter
from rule_builder.rules import Has, HasAll, Rule
from .options import HardMode
if TYPE_CHECKING:
from .world import APQuestWorld
HAS_KEY = Has("Key") # Hmm, what could this be? A little foreshadowing perhaps? :) You'll find out if you keep reading!
def set_all_rules(world: APQuestWorld) -> None:
# In order for AP to generate an item layout that is actually possible for the player to complete,
@@ -26,36 +30,46 @@ def set_all_entrance_rules(world: APQuestWorld) -> None:
overworld_to_top_left_room = world.get_entrance("Overworld to Top Left Room")
right_room_to_final_boss_room = world.get_entrance("Right Room to Final Boss Room")
# An access rule is a function. We can define this function like any other function.
# This function must accept exactly one parameter: A "CollectionState".
# A CollectionState describes the current progress of the players in the multiworld, i.e. what items they have,
# which regions they've reached, etc.
# In an access rule, we can ask whether the player has a collected a certain item.
# We can do this via the state.has(...) function.
# This function takes an item name, a player number, and an optional count parameter (more on that below)
# Since a rule only takes a CollectionState parameter, but we also need the player number in the state.has call,
# our function needs to be locally defined so that it has access to the player number from the outer scope.
# In our case, we are inside a function that has access to the "world" parameter, so we can use world.player.
def can_destroy_bush(state: CollectionState) -> bool:
return state.has("Sword", world.player)
# Now, let's make some rules!
# First, let's handle the transition from the overworld to the bottom right room,
# which requires slashing a bush with the Sword.
# For this, we need a rule that says "player has a Sword".
# We can use a "Has"-type rule from the rule_builder module for this.
can_destroy_bush = Has("Sword")
# Now we can set our "can_destroy_bush" rule to our entrance which requires slashing a bush to clear the path.
# One way to set rules is via the set_rule() function, which works on both Entrances and Locations.
set_rule(overworld_to_bottom_right_room, can_destroy_bush)
# Now we can set our "can_destroy_bush" rule to the entrance which requires slashing a bush to clear the path.
# The easiest way to do this is by calling world.set_rule, which works for both Locations and Entrances.
world.set_rule(overworld_to_bottom_right_room, can_destroy_bush)
# Because the function has to be defined locally, most worlds prefer the lambda syntax.
set_rule(overworld_to_top_left_room, lambda state: state.has("Key", world.player))
# Conditions can depend on event items.
set_rule(right_room_to_final_boss_room, lambda state: state.has("Top Left Room Button Pressed", world.player))
# Conditions can also depend on event items.
button_pressed = Has("Top Left Room Button Pressed")
world.set_rule(right_room_to_final_boss_room, button_pressed)
# Some entrance rules may only apply if the player enabled certain options.
# In our case, if the hammer option is enabled, we need to add the Hammer requirement to the Entrance from
# Overworld to the Top Middle Room.
if world.options.hammer:
overworld_to_top_middle_room = world.get_entrance("Overworld to Top Middle Room")
set_rule(overworld_to_top_middle_room, lambda state: state.has("Hammer", world.player))
can_smash_brick = Has("Hammer")
world.set_rule(overworld_to_top_middle_room, can_smash_brick)
# So far, we've been using "Has" from the Rule Builder to make our rules.
# There is another way to make rules that you will see in a lot of older worlds.
# A rule can just be a function that takes a "state" argument and returns a bool.
# As a demonstration of what that looks like, let's do it with our final Entrance rule:
world.set_rule(overworld_to_top_left_room, lambda state: state.has("Key", world.player))
# This style is not really recommended anymore, though.
# Notice how you have to explicitly capture world.player here so that the rule applies to the correct player?
# Well, Rule Builder does this part for you, inside of world.set_rule.
# This doesn't just result in shorter code, it also means you can define rules statically (at the module level).
# APQuest opts to create its Rule objects locally, but just to show what this would look like,
# we'll re-set the "Overworld to Top Left Room" rule to a constant defined at the top of this file:
world.set_rule(overworld_to_top_left_room, HAS_KEY)
# Beyond these structural advantages,
# Rule Builder also allows the core AP code to do a lot of under-the-hood optimizations.
# Rule Builder is quite comprehensive, and even if you have really esoteric rules,
# you can make custom rules by subclassing CustomRule.
def set_all_location_rules(world: APQuestWorld) -> None:
# Location rules work no differently from Entrance rules.
@@ -67,65 +81,72 @@ def set_all_location_rules(world: APQuestWorld) -> None:
# So, we need to set requirements on the Locations themselves.
# Since combat is a bit more complicated, we'll use this chance to cover some advanced access rule concepts.
# Sometimes, you may want to have different rules depending on the player's chosen options.
# There is a wrong way to do this, and a right way to do this. Let's do the wrong way first.
# In "set_all_entrance_rules", we had a rule for a location that doesn't always exist.
# In this case, we had to check for its existence (by checking the player's chosen options) before setting the rule.
# Other times, you may have a situation where a location can have two different rules depending on the options.
# In our case, the enemy in the right room has more health if hard mode is selected,
# so ontop of the Sword, the player will either need one more health or a Shield in hard mode.
# First, let's make our sword condition.
can_defeat_basic_enemy: Rule = Has("Sword")
# Next, we'll check whether hard mode has been chosen in the player options.
if world.options.hard_mode:
# We'll make the condition for "Has a Shield or a Health Upgrade".
# We can chain two "Has" conditions together with the | operator to make "Has Shield or has Health Upgrade".
can_withstand_a_hit = Has("Shield") | Has("Health Upgrade")
# Now, we chain this rule to our Sword rule.
# Since we want both conditions to be true, in this case, we have to chain them in an "and" way.
# For this, we can use the & operator.
can_defeat_basic_enemy = can_defeat_basic_enemy & can_withstand_a_hit
# Finally, we set our rule onto the Right Room Eney Drop location.
right_room_enemy = world.get_location("Right Room Enemy Drop")
world.set_rule(right_room_enemy, can_defeat_basic_enemy)
# DON'T DO THIS!!!!
set_rule(
right_room_enemy,
lambda state: (
state.has("Sword", world.player)
and (not world.options.hard_mode or state.has_any(("Shield", "Health Upgrade"), world.player))
),
)
# DON'T DO THIS!!!!
# For the final boss, we also need to chain multiple conditions.
# First of all, you always need a Sword and a Shield.
# So far, we used the | and & operators to chain "Has" rules.
# Instead, we can also use HasAny for an or-chain of items, or HasAll for an and-chain of items.
has_sword_and_shield: Rule = HasAll("Sword", "Shield")
# Now, what's actually wrong with this? It works perfectly fine, right?
# If hard mode disabled, Sword is enough. If hard mode is enabled, we also need a Shield or a Health Upgrade.
# The access rule we just wrote does this correctly, so what's the problem?
# The problem is performance.
# Most of your world code doesn't need to be perfectly performant, since it just runs once per slot.
# However, access rules in particular are by far the hottest code path in Archipelago.
# An access rule will potentially be called thousands or even millions of times over the course of one generation.
# As a result, access rules are the one place where it's really worth putting in some effort to optimize.
# What's the performance problem here?
# Every time our access rule is called, it has to evaluate whether world.options.hard_mode is True or False.
# Wouldn't it be better if in easy mode, the access rule only checked for Sword to begin with?
# Wouldn't it also be better if in hard mode, it already knew it had to check Shield and Health Upgrade as well?
# Well, we can achieve this by doing the "if world.options.hard_mode" check outside the set_rule call,
# and instead having two *different* set_rule calls depending on which case we're in.
# In hard mode, the player also needs both Health Upgrades to survive long enough to defeat the boss.
# For this, we can use the optional "count" parameter for "Has".
has_both_health_upgrades = Has("Health Upgrade", count=2)
if world.options.hard_mode:
# If you have multiple conditions, you can obviously chain them via "or" or "and".
# However, there are also the nice helper functions "state.has_any" and "state.has_all".
set_rule(
right_room_enemy,
lambda state: (
state.has("Sword", world.player) and state.has_any(("Shield", "Health Upgrade"), world.player)
),
)
else:
set_rule(right_room_enemy, lambda state: state.has("Sword", world.player))
# Previously, we used an "if world.options.hard_mode" condition to check if we should apply the extra requirement.
# However, if you're comfortable with boolean logic, there is another way.
# OptionFilter is a rule component which isn't a "Rule" on its own, but when used in a boolean expression with
# rules, it acts like True if the option has the specified value, and acts like False otherwise.
hard_mode_is_off = OptionFilter(HardMode, False)
# Another way to chain multiple conditions is via the add_rule function.
# This makes the access rules a bit slower though, so it should only be used if your structure justifies it.
# In our case, it's pretty useful because hard mode and easy mode have different requirements.
# So with this option-checking rule component in hand, we can write our boss condition like this:
can_defeat_final_boss = has_sword_and_shield & (hard_mode_is_off | has_both_health_upgrades)
# If you're not as comfortable with boolean logic, it might be somewhat confusing why this is correct.
# There is nothing wrong with using "if" conditions to check for options, if you find that easier to understand.
# Finally, we apply the rule to our "Final Boss Defeated" event location.
final_boss = world.get_location("Final Boss Defeated")
# For the "known" requirements, it's still better to chain them using a normal "and" condition.
add_rule(final_boss, lambda state: state.has_all(("Sword", "Shield"), world.player))
if world.options.hard_mode:
# You can check for multiple copies of an item by using the optional count parameter of state.has().
add_rule(final_boss, lambda state: state.has("Health Upgrade", world.player, 2))
world.set_rule(final_boss, can_defeat_final_boss)
def set_completion_condition(world: APQuestWorld) -> None:
# Finally, we need to set a completion condition for our world, defining what the player needs to win the game.
# For this, we can use world.set_completion_rule.
# You can just set a completion condition directly like any other condition, referencing items the player receives:
world.multiworld.completion_condition[world.player] = lambda state: state.has_all(("Sword", "Shield"), world.player)
world.set_completion_rule(HasAll("Sword", "Shield"))
# In our case, we went for the Victory event design pattern (see create_events() in locations.py).
# So lets undo what we just did, and instead set the completion condition to:
world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
world.set_completion_rule(Has("Victory"))
# One final comment about rules:
# If your world exclusively uses Rule Builder rules (like APQuest), it's worth trying CachedRuleBuilderWorld.
# CachedRuleBuilderWorld is a subclass of World that has a bunch of caching magic to make rules faster.
# Just have your world class subclass CachedRuleBuilderWorld instead of World:
# class APQuestWorld(CachedRuleBuilderWorld): ...
# This may speed up your world, or it may make it slower.
# The exact factors are complex and not well understood, but there is no harm in trying it.
# Generate a few seeds and see if there is a noticeable difference!
# If you're wondering, author has checked: APQuest is too simple to see any benefits, so we'll stick with "World".
+3
View File
@@ -739,6 +739,9 @@ group_table: Dict[str, Set[str]] = {
"Broken Left Eye of the Traitor"}
}
# Because each item is only in a single group, a reverse lookup table from each item to its group can be created.
group_table_reverse: Dict[str, str] = {item: group for group, items in group_table.items() for item in items}
tears_list: List[str] = [
"Tears of Atonement (500)",
"Tears of Atonement (625)",
+377 -262
View File
@@ -1,5 +1,6 @@
from typing import Dict, List, Tuple, Any, Callable, TYPE_CHECKING
from typing import Dict, List, Tuple, Any, Callable, TYPE_CHECKING, Mapping
from BaseClasses import CollectionState
from worlds.generic.Rules import CollectionRule
if TYPE_CHECKING:
from . import BlasphemousWorld
@@ -7,30 +8,145 @@ else:
BlasphemousWorld = object
# Depending on a player's options, some logic can either always be True, or always be False.
# When combining rules together in load_rule(), optimizations can be made by checking whether a rule being combined is
# _always or _never.
def _always(state: CollectionState):
return True
def _never(state: CollectionState):
return False
def _bool_rule(b) -> CollectionRule:
"""Small helper to return the appropriate rule function for a rule that can be pre-calculated"""
if b:
return _always
else:
return _never
# Player strengths required to logically beat bosses.
# Mapping is an immutable type, so type hints should warn if attempts are made to modify it.
BOSS_STRENGTHS: Mapping[str, float] = {
"warden": -0.10,
"ten-piedad": 0.05,
"charred-visage": 0.20,
"tres-angustias": 0.15,
"esdras": 0.25,
"melquiades": 0.25,
"exposito": 0.30,
"quirce": 0.35,
"crisanta": 0.50,
"isidora": 0.70,
"sierpes": 0.70,
"amanecida": 0.60,
"laudes": 0.60,
"perpetua": -0.05,
"legionary": 0.20
}
class BlasRules:
player: int
world: BlasphemousWorld
string_rules: Dict[str, Callable[[CollectionState], bool]]
upwarp_skips_allowed: bool
mourning_skip_allowed: bool
enemy_skips_allowed: bool
obscure_skips_allowed: bool
precise_skips_allowed: bool
can_enemy_bounce: bool
# Player strengths required to logically beat bosses, adjusted by the player's difficulty option.
boss_strengths: Mapping[str, float]
can_enemy_upslash: CollectionRule
can_air_stall: CollectionRule
can_dawn_jump: CollectionRule
can_dive_laser: CollectionRule
can_survive_poison_1: CollectionRule
can_survive_poison_2: CollectionRule
can_survive_poison_3: CollectionRule
def __init__(self, world: "BlasphemousWorld") -> None:
self.player = world.player
self.world = world
self.multiworld = world.multiworld
self.indirect_conditions: List[Tuple[str, str]] = []
difficulty = world.options.difficulty.value
# Rules that can be fully or partially pre-calculated based on world.options.
# Special Skips
self.upwarp_skips_allowed = difficulty >= 2
self.mourning_skip_allowed = difficulty >= 2
self.enemy_skips_allowed = difficulty >= 2 and not world.options.enemy_randomizer.value
self.obscure_skips_allowed = difficulty >= 2
self.precise_skips_allowed = difficulty >= 2
if difficulty >= 2:
# Beating bosses ends up in logic earlier.
self.boss_strengths = {boss: strength - 0.1 for boss, strength in BOSS_STRENGTHS.items()}
elif difficulty >= 1:
self.boss_strengths = BOSS_STRENGTHS
else:
# Beating bosses ends up in logic later.
self.boss_strengths = {boss: strength + 0.1 for boss, strength in BOSS_STRENGTHS.items()}
# Enemy tech
if self.enemy_skips_allowed:
self.can_enemy_bounce = True
self.can_enemy_upslash = lambda state: self.combo(state) >= 2
else:
self.can_enemy_bounce = False
self.can_enemy_upslash = _never
# Movement tech
if difficulty >= 1:
self.can_air_stall = self.ranged
self.can_dawn_jump = lambda state: self.dawn_heart(state) and self.dash(state)
else:
self.can_air_stall = _never
self.can_dawn_jump = _never
# Breakable tech
if difficulty >= 2:
self.can_dive_laser = lambda state: self.dive(state) >= 3
else:
self.can_dive_laser = _never
# Lung tech
if difficulty >= 2:
self.can_survive_poison_1 = _always
self.can_survive_poison_2 = lambda state: self.lung(state) or self.tiento(state)
self.can_survive_poison_3 = lambda state: self.lung(state) or (self.tiento(state)
and self.total_fervour(state) >= 120)
elif difficulty >= 1:
self.can_survive_poison_1 = lambda state: self.lung(state) or self.tiento(state)
self.can_survive_poison_2 = lambda state: self.lung(state) or self.tiento(state)
self.can_survive_poison_3 = self.lung
else:
self.can_survive_poison_1 = self.lung
self.can_survive_poison_2 = self.lung
self.can_survive_poison_3 = self.lung
# BrandenEK/Blasphemous.Randomizer/ItemRando/BlasphemousInventory.cs
self.string_rules = {
self.string_rules: dict[str, CollectionRule] = {
# Visibility flags
"DoubleJump": lambda state: bool(self.world.options.purified_hand),
"NormalLogic": lambda state: self.world.options.difficulty >= 1,
"NormalLogicAndDoubleJump": lambda state: self.world.options.difficulty >= 1 \
and bool(self.world.options.purified_hand),
"HardLogic": lambda state: self.world.options.difficulty >= 2,
"HardLogicAndDoubleJump": lambda state: self.world.options.difficulty >= 2 \
and bool(self.world.options.purified_hand),
"EnemySkips": self.enemy_skips_allowed,
"EnemySkipsAndDoubleJump": lambda state: self.enemy_skips_allowed(state) \
and bool(self.world.options.purified_hand),
"DoubleJump": _bool_rule(self.world.options.purified_hand.value),
"NormalLogic": _bool_rule(self.world.options.difficulty.value >= 1),
"NormalLogicAndDoubleJump": _bool_rule(self.world.options.difficulty.value >= 1
and bool(self.world.options.purified_hand.value)),
"HardLogic": _bool_rule(self.world.options.difficulty.value >= 2),
"HardLogicAndDoubleJump": _bool_rule(self.world.options.difficulty.value >= 2
and bool(self.world.options.purified_hand.value)),
"EnemySkips": _bool_rule(self.enemy_skips_allowed),
"EnemySkipsAndDoubleJump": _bool_rule(self.enemy_skips_allowed and self.world.options.purified_hand.value),
# Relics
"blood": self.blood,
@@ -52,20 +168,20 @@ class BlasRules:
"cherubs20": lambda state: self.cherubs(state) >= 20,
"cherubs38": lambda state: self.cherubs(state) >= 38,
"bones4": lambda state: self.bones(state) >= 4,
"bones8": lambda state: self.bones(state) >= 8,
"bones12": lambda state: self.bones(state) >= 12,
"bones16": lambda state: self.bones(state) >= 16,
"bones20": lambda state: self.bones(state) >= 20,
"bones24": lambda state: self.bones(state) >= 24,
"bones28": lambda state: self.bones(state) >= 28,
"bones30": lambda state: self.bones(state) >= 30,
"bones32": lambda state: self.bones(state) >= 32,
"bones36": lambda state: self.bones(state) >= 36,
"bones40": lambda state: self.bones(state) >= 40,
"bones44": lambda state: self.bones(state) >= 44,
"bones4": lambda state: self.bones(state, 4),
"bones8": lambda state: self.bones(state, 8),
"bones12": lambda state: self.bones(state, 12),
"bones16": lambda state: self.bones(state, 16),
"bones20": lambda state: self.bones(state, 20),
"bones24": lambda state: self.bones(state, 24),
"bones28": lambda state: self.bones(state, 28),
"bones30": lambda state: self.bones(state, 30),
"bones32": lambda state: self.bones(state, 32),
"bones36": lambda state: self.bones(state, 36),
"bones40": lambda state: self.bones(state, 40),
"bones44": lambda state: self.bones(state, 44),
"tears0": lambda state: True,
"tears0": _always,
# Special items
"dash": self.dash,
@@ -118,13 +234,13 @@ class BlasRules:
# skip "dive"
# skip "lunge"
"chargeBeam": self.charge_beam,
"rangedAttack": lambda state: self.ranged(state) > 0,
"rangedAttack": self.ranged,
# Main quest
"holyWounds3": lambda state: self.holy_wounds(state) >= 3,
"masks1": lambda state: self.masks(state) >= 1,
"masks2": lambda state: self.masks(state) >= 2,
"masks3": lambda state: self.masks(state) >= 3,
"holyWounds3": lambda state: self.holy_wounds(state, 3),
"masks1": lambda state: self.masks(state, 1),
"masks2": lambda state: self.masks(state, 2),
"masks3": lambda state: self.masks(state, 3),
"guiltBead": self.guilt_bead,
# LOTL quest
@@ -133,17 +249,17 @@ class BlasRules:
"hatchedEgg": self.hatched_egg,
# Tirso quest
"herbs1": lambda state: self.herbs(state) >= 1,
"herbs2": lambda state: self.herbs(state) >= 2,
"herbs3": lambda state: self.herbs(state) >= 3,
"herbs4": lambda state: self.herbs(state) >= 4,
"herbs5": lambda state: self.herbs(state) >= 5,
"herbs6": lambda state: self.herbs(state) >= 6,
"herbs1": lambda state: self.herbs(state, 1),
"herbs2": lambda state: self.herbs(state, 2),
"herbs3": lambda state: self.herbs(state, 3),
"herbs4": lambda state: self.herbs(state, 4),
"herbs5": lambda state: self.herbs(state, 5),
"herbs6": lambda state: self.herbs(state, 6),
# Tentudia quest
"tentudiaRemains1": lambda state: self.tentudia_remains(state) >= 1,
"tentudiaRemains2": lambda state: self.tentudia_remains(state) >= 2,
"tentudiaRemains3": lambda state: self.tentudia_remains(state) >= 3,
"tentudiaRemains1": lambda state: self.tentudia_remains(state, 1),
"tentudiaRemains2": lambda state: self.tentudia_remains(state, 2),
"tentudiaRemains3": lambda state: self.tentudia_remains(state, 3),
# Gemino quest
"emptyThimble": self.empty_thimble,
@@ -151,7 +267,7 @@ class BlasRules:
"driedFlowers": self.dried_flowers,
# Altasgracias quest
"ceremonyItems3": lambda state: self.ceremony_items(state) >= 3,
"ceremonyItems3": lambda state: self.ceremony_items(state, 3),
"egg": self.egg,
# Redento quest
@@ -159,13 +275,13 @@ class BlasRules:
# skip "knots", not actually used
# Cleofas quest
"marksOfRefuge3": lambda state: self.marks_of_refuge(state) >= 3,
"marksOfRefuge3": lambda state: self.marks_of_refuge(state, 3),
"cord": self.cord,
# Crisanta quest
"scapular": self.scapular,
"trueHeart": self.true_heart,
"traitorEyes2": lambda state: self.traitor_eyes(state) >= 2,
"traitorEyes2": lambda state: self.traitor_eyes(state, 2),
# Jibrael quest
"bell": self.bell,
@@ -190,32 +306,32 @@ class BlasRules:
"canSurvivePoison3": self.can_survive_poison_3,
# Enemy tech
"canEnemyBounce": self.can_enemy_bounce,
"canEnemyBounce": _bool_rule(self.can_enemy_bounce),
"canEnemyUpslash": self.can_enemy_upslash,
# Reaching rooms
"guiltRooms1": lambda state: self.guilt_rooms(state) >= 1,
"guiltRooms2": lambda state: self.guilt_rooms(state) >= 2,
"guiltRooms3": lambda state: self.guilt_rooms(state) >= 3,
"guiltRooms4": lambda state: self.guilt_rooms(state) >= 4,
"guiltRooms5": lambda state: self.guilt_rooms(state) >= 5,
"guiltRooms6": lambda state: self.guilt_rooms(state) >= 6,
"guiltRooms7": lambda state: self.guilt_rooms(state) >= 7,
"guiltRooms1": lambda state: self.guilt_rooms(state, 1),
"guiltRooms2": lambda state: self.guilt_rooms(state, 2),
"guiltRooms3": lambda state: self.guilt_rooms(state, 3),
"guiltRooms4": lambda state: self.guilt_rooms(state, 4),
"guiltRooms5": lambda state: self.guilt_rooms(state, 5),
"guiltRooms6": lambda state: self.guilt_rooms(state, 6),
"guiltRooms7": lambda state: self.guilt_rooms(state, 7),
"swordRooms1": lambda state: self.sword_rooms(state) >= 1,
"swordRooms2": lambda state: self.sword_rooms(state) >= 2,
"swordRooms3": lambda state: self.sword_rooms(state) >= 3,
"swordRooms4": lambda state: self.sword_rooms(state) >= 4,
"swordRooms5": lambda state: self.sword_rooms(state) >= 5,
"swordRooms6": lambda state: self.sword_rooms(state) >= 6,
"swordRooms7": lambda state: self.sword_rooms(state) >= 7,
"swordRooms1": lambda state: self.sword_rooms(state, 1),
"swordRooms2": lambda state: self.sword_rooms(state, 2),
"swordRooms3": lambda state: self.sword_rooms(state, 3),
"swordRooms4": lambda state: self.sword_rooms(state, 4),
"swordRooms5": lambda state: self.sword_rooms(state, 5),
"swordRooms6": lambda state: self.sword_rooms(state, 6),
"swordRooms7": lambda state: self.sword_rooms(state, 7),
"redentoRooms2": lambda state: self.redento_rooms(state) >= 2,
"redentoRooms3": lambda state: self.redento_rooms(state) >= 3,
"redentoRooms4": lambda state: self.redento_rooms(state) >= 4,
"redentoRooms5": lambda state: self.redento_rooms(state) >= 5,
"redentoRooms2": lambda state: self.redento_rooms(state, 2),
"redentoRooms3": lambda state: self.redento_rooms(state, 3),
"redentoRooms4": lambda state: self.redento_rooms(state, 4),
"redentoRooms5": lambda state: self.redento_rooms(state, 5),
"miriamRooms5": lambda state: self.miriam_rooms(state) >= 5,
"miriamRooms5": self.all_miriam_rooms,
"amanecidaRooms1": lambda state: self.amanecida_rooms(state) >= 1,
"amanecidaRooms2": lambda state: self.amanecida_rooms(state) >= 2,
@@ -254,11 +370,11 @@ class BlasRules:
"openedBotSSLadder": self.opened_botss_ladder,
# Special skips
"upwarpSkipsAllowed": self.upwarp_skips_allowed,
"mourningSkipAllowed": self.mourning_skip_allowed,
"enemySkipsAllowed": self.enemy_skips_allowed,
"obscureSkipsAllowed": self.obscure_skips_allowed,
"preciseSkipsAllowed": self.precise_skips_allowed,
"upwarpSkipsAllowed": _bool_rule(self.upwarp_skips_allowed),
"mourningSkipAllowed": _bool_rule(self.mourning_skip_allowed),
"enemySkipsAllowed": _bool_rule(self.enemy_skips_allowed),
"obscureSkipsAllowed": _bool_rule(self.obscure_skips_allowed),
"preciseSkipsAllowed": _bool_rule(self.precise_skips_allowed),
# Bosses
"canBeatBrotherhoodBoss": self.can_beat_brotherhood_boss,
@@ -498,30 +614,74 @@ class BlasRules:
def load_rule(self, obj_is_region: bool, name: str, obj: Dict[str, Any]) -> Callable[[CollectionState], bool]:
clauses = []
clauses_are_impossible_if_empty = False
rule_indirect_conditions = []
for clause in obj["logic"]:
reqs = []
clause_indirect_conditions = []
clause_is_impossible = False
for req in clause["item_requirements"]:
if self.req_is_region(req):
if obj_is_region:
# add to indirect conditions if object and requirement are doors
self.indirect_conditions.append((req, f"{name} -> {obj['target']}"))
clause_indirect_conditions.append((req, f"{name} -> {obj['target']}"))
reqs.append(lambda state, req=req: state.can_reach_region(req, self.player))
else:
string_rule = self.string_rules[req]
if string_rule is _never:
# This clause is not possible with the options this player has chosen.
clause_is_impossible = True
break
elif string_rule is _always:
# Don't need to add a rule that is always True with the options this player has chosen.
# Continue to the next requirement.
continue
if obj_is_region and req in self.indirect_regions:
# add to indirect conditions if object is door and requirement has list of regions
for region in self.indirect_regions[req]:
self.indirect_conditions.append((region, f"{name} -> {obj['target']}"))
clause_indirect_conditions.append((region, f"{name} -> {obj['target']}"))
reqs.append(self.string_rules[req])
if clause_is_impossible:
# At least one clause was impossible, so if all clauses were impossible, the entire rule is impossible.
clauses_are_impossible_if_empty = True
# Continue to the next clause.
continue
rule_indirect_conditions.extend(clause_indirect_conditions)
# Combine the requirements if there are multiple.
# Requirements are AND-ed together.
if len(reqs) == 1:
clauses.append(reqs[0])
else:
clauses.append(lambda state, reqs=reqs: all(req(state) for req in reqs))
def req_func(state, reqs=reqs):
for req in reqs:
if not req(state):
return False
return True
clauses.append(req_func)
# Combine the clauses if there are multiple.
# Clauses are OR-ed together.
if not clauses:
return lambda state: True
# There is no need to register the indirect conditions if it turns out the rule is impossible or always
# possible.
rule_indirect_conditions.clear()
if clauses_are_impossible_if_empty:
to_return = _never
else:
to_return = _always
elif len(clauses) == 1:
return clauses[0]
to_return = clauses[0]
else:
return lambda state: any(clause(state) for clause in clauses)
def clause_func(state, clauses=clauses):
for clause in clauses:
if clause(state):
return True
return False
to_return = clause_func
# Update the list of indirect conditions to add.
self.indirect_conditions.extend(rule_indirect_conditions)
return to_return
# Relics
def blood(self, state: CollectionState) -> bool:
@@ -565,8 +725,10 @@ class BlasRules:
def cherubs(self, state: CollectionState) -> int:
return state.count("Child of Moonlight", self.player)
def bones(self, state: CollectionState) -> int:
return state.count_group_unique("bones", self.player)
def bones(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "bones" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("bones", self.player, count)
# def tears():
@@ -594,7 +756,7 @@ class BlasRules:
# Health boosts
def flasks(self, state: CollectionState) -> int:
doors = {
doors = (
"D01Z05S05[SW]",
"D02Z02S04[W]",
"D03Z02S08[W]",
@@ -602,10 +764,11 @@ class BlasRules:
"D04Z02S13[W]",
"D05Z01S08[NW]",
"D20Z01S07[NE]"
}
return state.count("Empty Bile Vessel", self.player) \
if sum(state.can_reach_region(door, self.player) for door in doors) >= 1 else 0
)
for door in doors:
if state.can_reach_region(door, self.player):
return state.count("Empty Bile Vessel", self.player)
return 0
def quicksilver(self, state: CollectionState) -> int:
return state.count("Quicksilver", self.player) if state.can_reach_region("D01Z05S01[W]", self.player) else 0
@@ -613,7 +776,7 @@ class BlasRules:
# Puzzles
def red_wax(self, state: CollectionState) -> int:
return state.count("Bead of Red Wax", self.player)
def blue_wax(self, state: CollectionState) -> int:
return state.count("Bead of Blue Wax", self.player)
@@ -670,7 +833,7 @@ class BlasRules:
or self.cante(state)
or self.cantina(state)
or self.tiento(state)
or state.has_any({
or state.has_any((
"Campanillero to the Sons of the Aurora",
"Mirabras of the Return to Port",
"Romance to the Crimson Mist",
@@ -678,7 +841,7 @@ class BlasRules:
"Seguiriya to your Eyes like Stars",
"Verdiales of the Forsaken Hamlet",
"Zambra to the Resplendent Crown"
}, self.player)
), self.player)
)
def pillar(self, state: CollectionState) -> bool:
@@ -710,8 +873,8 @@ class BlasRules:
def charged(self, state: CollectionState) -> int:
return state.count("Charged Skill", self.player)
def ranged(self, state: CollectionState) -> int:
return state.count("Ranged Skill", self.player)
def ranged(self, state: CollectionState) -> bool:
return state.has("Ranged Skill", self.player)
def dive(self, state: CollectionState) -> int:
return state.count("Dive Skill", self.player)
@@ -723,11 +886,15 @@ class BlasRules:
return self.charged(state) >= 3
# Main quest
def holy_wounds(self, state: CollectionState) -> int:
return state.count_group_unique("wounds", self.player)
def holy_wounds(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "wounds" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("wounds", self.player, count)
def masks(self, state: CollectionState) -> int:
return state.count_group_unique("masks", self.player)
def masks(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "masks" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("masks", self.player, count)
def guilt_bead(self, state: CollectionState) -> bool:
return state.has("Weight of True Guilt", self.player)
@@ -743,12 +910,16 @@ class BlasRules:
return state.has("Hatched Egg of Deformity", self.player)
# Tirso quest
def herbs(self, state: CollectionState) -> int:
return state.count_group_unique("tirso", self.player)
def herbs(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "tirso" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("tirso", self.player, count)
# Tentudia quest
def tentudia_remains(self, state: CollectionState) -> int:
return state.count_group_unique("tentudia", self.player)
def tentudia_remains(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "tentudia" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("tentudia", self.player, count)
# Gemino quest
def empty_thimble(self, state: CollectionState) -> bool:
@@ -761,23 +932,29 @@ class BlasRules:
return state.has("Dried Flowers bathed in Tears", self.player)
# Altasgracias quest
def ceremony_items(self, state: CollectionState) -> int:
return state.count_group_unique("egg", self.player)
def ceremony_items(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "egg" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("egg", self.player, count)
def egg(self, state: CollectionState) -> bool:
return state.has("Egg of Deformity", self.player)
# Redento quest
def limestones(self, state: CollectionState) -> int:
return state.count_group_unique("toe", self.player)
def limestones(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "toe" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("toe", self.player, count)
def knots(self, state: CollectionState) -> int:
return state.count("Knot of Rosary Rope", self.player) if state.can_reach_region("D17Z01S07[NW]", self.player)\
else 0
# Cleofas quest
def marks_of_refuge(self, state: CollectionState) -> int:
return state.count_group_unique("marks", self.player)
def marks_of_refuge(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "marks" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("marks", self.player, count)
def cord(self, state: CollectionState) -> bool:
return state.has("Cord of the True Burying", self.player)
@@ -789,8 +966,10 @@ class BlasRules:
def true_heart(self, state: CollectionState) -> bool:
return state.has("Apodictic Heart of Mea Culpa", self.player)
def traitor_eyes(self, state: CollectionState) -> int:
return state.count_group_unique("eye", self.player)
def traitor_eyes(self, state: CollectionState, count: int) -> bool:
# Count of unique items in the "eye" item group that have been collected into state.
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
return state.has("eye", self.player, count)
# Jibrael quest
def bell(self, state: CollectionState) -> bool:
@@ -800,19 +979,6 @@ class BlasRules:
return state.count("Verses Spun from Gold", self.player)
# Movement tech
def can_air_stall(self, state: CollectionState) -> bool:
return (
self.ranged(state) > 0
and self.world.options.difficulty >= 1
)
def can_dawn_jump(self, state: CollectionState) -> bool:
return (
self.dawn_heart(state)
and self.dash(state)
and self.world.options.difficulty >= 1
)
def can_water_jump(self, state: CollectionState) -> bool:
return (
self.nail(state)
@@ -828,12 +994,6 @@ class BlasRules:
or self.can_use_any_prayer(state)
)
def can_dive_laser(self, state: CollectionState) -> bool:
return (
self.dive(state) >= 3
and self.world.options.difficulty >= 2
)
# Root tech
def can_walk_on_root(self, state: CollectionState) -> bool:
return self.root(state)
@@ -844,40 +1004,6 @@ class BlasRules:
and self.wall_climb(state)
)
# Lung tech
def can_survive_poison_1(self, state: CollectionState) -> bool:
return (
self.lung(state)
or self.world.options.difficulty >= 1
and self.tiento(state)
or self.world.options.difficulty >= 2
)
def can_survive_poison_2(self, state: CollectionState) -> bool:
return (
self.lung(state)
or self.world.options.difficulty >= 1
and self.tiento(state)
)
def can_survive_poison_3(self, state: CollectionState) -> bool:
return (
self.lung(state)
or self.world.options.difficulty >= 2
and self.tiento(state)
and self.total_fervour(state) >= 120
)
# Enemy tech
def can_enemy_bounce(self, state: CollectionState) -> bool:
return self.enemy_skips_allowed(state)
def can_enemy_upslash(self, state: CollectionState) -> bool:
return (
self.combo(state) >= 2
and self.enemy_skips_allowed(state)
)
# Crossing gaps
def can_cross_gap_1(self, state: CollectionState) -> bool:
return (
@@ -1021,7 +1147,7 @@ class BlasRules:
or state.can_reach_region("D03Z02S03[E]", self.player)
and (
self.can_cross_gap_5(state)
or self.can_enemy_bounce(state)
or self.can_enemy_bounce
and self.can_cross_gap_3(state)
)
)
@@ -1067,25 +1193,6 @@ class BlasRules:
or state.can_reach_region("D17BZ02S01[FrontR]", self.player)
)
# Special skips
def upwarp_skips_allowed(self, state: CollectionState) -> bool:
return self.world.options.difficulty >= 2
def mourning_skip_allowed(self, state: CollectionState) -> bool:
return self.world.options.difficulty >= 2
def enemy_skips_allowed(self, state: CollectionState) -> bool:
return (
self.world.options.difficulty >= 2
and not self.world.options.enemy_randomizer
)
def obscure_skips_allowed(self, state: CollectionState) -> bool:
return self.world.options.difficulty >= 2
def precise_skips_allowed(self, state: CollectionState) -> bool:
return self.world.options.difficulty >= 2
# Bosses
def can_beat_brotherhood_boss(self, state: CollectionState) -> bool:
return (
@@ -1183,18 +1290,18 @@ class BlasRules:
and state.can_reach_region("D20Z02S07[W]", self.player)
)
def can_beat_graveyard_boss(self, state: CollectionState) -> bool:
def can_beat_graveyard_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
self.has_boss_strength(state, "amanecida")
self.has_boss_strength(state, "amanecida", player_strength)
and self.wall_climb(state)
and state.can_reach_region("D01Z06S01[Santos]", self.player)
and state.can_reach_region("D02Z03S18[NW]", self.player)
and state.can_reach_region("D02Z02S03[NE]", self.player)
)
def can_beat_jondo_boss(self, state: CollectionState) -> bool:
def can_beat_jondo_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
self.has_boss_strength(state, "amanecida")
self.has_boss_strength(state, "amanecida", player_strength)
and state.can_reach_region("D01Z06S01[Santos]", self.player)
and (
state.can_reach_region("D20Z01S06[NE]", self.player)
@@ -1206,9 +1313,9 @@ class BlasRules:
)
)
def can_beat_patio_boss(self, state: CollectionState) -> bool:
def can_beat_patio_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
self.has_boss_strength(state, "amanecida")
self.has_boss_strength(state, "amanecida", player_strength)
and state.can_reach_region("D01Z06S01[Santos]", self.player)
and state.can_reach_region("D06Z01S02[W]", self.player)
and (
@@ -1218,9 +1325,9 @@ class BlasRules:
)
)
def can_beat_wall_boss(self, state: CollectionState) -> bool:
def can_beat_wall_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
self.has_boss_strength(state, "amanecida")
self.has_boss_strength(state, "amanecida", player_strength)
and state.can_reach_region("D01Z06S01[Santos]", self.player)
and state.can_reach_region("D09Z01S09[Cell24]", self.player)
and (
@@ -1244,8 +1351,7 @@ class BlasRules:
def can_beat_legionary(self, state: CollectionState) -> bool:
return self.has_boss_strength(state, "legionary")
def has_boss_strength(self, state: CollectionState, boss: str) -> bool:
def get_player_strength(self, state: CollectionState) -> float:
life: int = state.count("Life Upgrade", self.player)
sword: int = state.count("Mea Culpa Upgrade", self.player)
fervour: int = state.count("Fervour Upgrade", self.player)
@@ -1259,30 +1365,16 @@ class BlasRules:
+ min(8, flasks) * 0.15 / 8
+ min(5, quicksilver) * 0.15 / 5
)
return player_strength
bosses: Dict[str, float] = {
"warden": -0.10,
"ten-piedad": 0.05,
"charred-visage": 0.20,
"tres-angustias": 0.15,
"esdras": 0.25,
"melquiades": 0.25,
"exposito": 0.30,
"quirce": 0.35,
"crisanta": 0.50,
"isidora": 0.70,
"sierpes": 0.70,
"amanecida": 0.60,
"laudes": 0.60,
"perpetua": -0.05,
"legionary": 0.20
}
boss_strength: float = bosses[boss]
return player_strength >= (boss_strength - 0.10 if self.world.options.difficulty >= 2 else
(boss_strength if self.world.options.difficulty >= 1 else boss_strength + 0.10))
def has_boss_strength(self, state: CollectionState, boss: str, player_strength: float | None = None) -> bool:
if player_strength is None:
return self.get_player_strength(state) >= self.boss_strengths[boss]
else:
return player_strength >= self.boss_strengths[boss]
def guilt_rooms(self, state: CollectionState) -> int:
doors = [
def guilt_rooms(self, state: CollectionState, count: int) -> bool:
doors = (
"D01Z04S01[NE]",
"D02Z02S11[W]",
"D03Z03S02[NE]",
@@ -1290,20 +1382,25 @@ class BlasRules:
"D05Z01S05[NE]",
"D09Z01S05[W]",
"D17Z01S04[W]",
]
)
return sum(state.can_reach_region(door, self.player) for door in doors)
total: int = 0
for door in doors:
total += state.can_reach_region(door, self.player)
if total >= count:
return True
return False
def sword_rooms(self, state: CollectionState) -> int:
doors = [
["D01Z02S07[E]", "D01Z02S02[SW]"],
["D20Z01S04[E]", "D01Z05S23[W]"],
["D02Z03S02[NE]"],
["D04Z02S21[NE]"],
["D05Z01S21[NW]"],
["D06Z01S15[NE]"],
["D17Z01S07[SW]"]
]
def sword_rooms(self, state: CollectionState, count: int) -> bool:
doors = (
("D01Z02S07[E]", "D01Z02S02[SW]"),
("D20Z01S04[E]", "D01Z05S23[W]"),
("D02Z03S02[NE]",),
("D04Z02S21[NE]",),
("D05Z01S21[NW]",),
("D06Z01S15[NE]",),
("D17Z01S07[SW]",)
)
total: int = 0
for subdoors in doors:
@@ -1311,72 +1408,90 @@ class BlasRules:
if state.can_reach_region(door, self.player):
total += 1
break
if total >= count:
return True
return total
return False
def redento_rooms(self, state: CollectionState) -> int:
if (
state.can_reach_region("D03Z01S04[E]", self.player)
or state.can_reach_region("D03Z02S10[N]", self.player)
def redento_rooms(self, state: CollectionState, count: int) -> bool:
if not (
state.can_reach_region("D03Z01S04[E]", self.player)
or state.can_reach_region("D03Z02S10[N]", self.player)
):
if (
# Realistically, count should never be zero or negative.
return count < 1
if count == 1:
return True
if not (
state.can_reach_region("D17Z01S05[S]", self.player)
or state.can_reach_region("D17BZ02S01[FrontR]", self.player)
):
if (
state.can_reach_region("D01Z03S04[E]", self.player)
or state.can_reach_region("D08Z01S01[W]", self.player)
):
if (
state.can_reach_region("D04Z01S03[E]", self.player)
or state.can_reach_region("D04Z02S01[W]", self.player)
or state.can_reach_region("D06Z01S18[-Cherubs]", self.player)
):
if (
self.knots(state) >= 1
and self.limestones(state) >= 3
and (
state.can_reach_region("D04Z02S08[E]", self.player)
or state.can_reach_region("D04BZ02S01[Redento]", self.player)
)
):
return 5
return 4
return 3
return 2
return 1
return 0
def miriam_rooms(self, state: CollectionState) -> int:
doors = [
):
return False
if count == 2:
return True
if not (state.can_reach_region("D01Z03S04[E]", self.player)
or state.can_reach_region("D08Z01S01[W]", self.player)):
return False
if count == 3:
return True
if not (state.can_reach_region("D04Z01S03[E]", self.player)
or state.can_reach_region("D04Z02S01[W]", self.player)
or state.can_reach_region("D06Z01S18[-Cherubs]", self.player)):
return False
if count == 4:
return True
if not (
self.knots(state) >= 1
and self.limestones(state, 3)
and (state.can_reach_region("D04Z02S08[E]", self.player)
or state.can_reach_region("D04BZ02S01[Redento]", self.player))
):
return False
return count == 5
def all_miriam_rooms(self, state: CollectionState) -> bool:
doors = (
"D02Z03S07[NWW]",
"D03Z03S07[NW]",
"D04Z04S01[E]",
"D05Z01S06[W]",
"D06Z01S17[E]"
]
)
return sum(state.can_reach_region(door, self.player) for door in doors)
for door in doors:
if not state.can_reach_region(door, self.player):
return False
return True
def amanecida_rooms(self, state: CollectionState) -> int:
player_strength = self.get_player_strength(state)
total: int = 0
if self.can_beat_graveyard_boss(state):
if self.can_beat_graveyard_boss(state, player_strength):
total += 1
if self.can_beat_jondo_boss(state):
if self.can_beat_jondo_boss(state, player_strength):
total += 1
if self.can_beat_patio_boss(state):
if self.can_beat_patio_boss(state, player_strength):
total += 1
if self.can_beat_wall_boss(state):
if self.can_beat_wall_boss(state, player_strength):
total += 1
return total
def chalice_rooms(self, state: CollectionState) -> int:
doors = [
["D03Z01S02[E]", "D01Z05S02[W]", "D20Z01S03[N]"],
["D05Z01S11[SE]", "D05Z02S02[NW]"],
["D09Z01S09[E]", "D09Z01S10[W]", "D09Z01S08[SE]", "D09Z01S02[SW]"]
]
doors = (
("D03Z01S02[E]", "D01Z05S02[W]", "D20Z01S03[N]"),
("D05Z01S11[SE]", "D05Z02S02[NW]"),
("D09Z01S09[E]", "D09Z01S10[W]", "D09Z01S08[SE]", "D09Z01S02[SW]")
)
total: int = 0
for subdoors in doors:
+23 -2
View File
@@ -1,9 +1,9 @@
from typing import Dict, List, Set, Any
from collections import Counter
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, CollectionState
from Options import OptionError
from worlds.AutoWorld import World, WebWorld
from .Items import base_id, item_table, group_table, tears_list, reliquary_set
from .Items import base_id, item_table, group_table, tears_list, reliquary_set, group_table_reverse
from .Locations import location_names
from .Rules import BlasRules
from worlds.generic.Rules import set_rule
@@ -216,6 +216,27 @@ class BlasphemousWorld(World):
for loc, item in option_dict.items():
self.get_location(loc).place_locked_item(self.create_item(item))
def collect(self, state: CollectionState, item: Item) -> bool:
changed = super().collect(state, item)
if changed:
name = item.name
if name in group_table_reverse and state.count(name, self.player) == 1:
# Count was 0 before super().collect().
group_name = group_table_reverse[name]
# Increase unique count for items in this group.
state.prog_items[self.player][group_name] += 1
return changed
def remove(self, state: CollectionState, item: Item) -> bool:
changed = super().remove(state, item)
if changed:
name = item.name
if name in group_table_reverse and state.count(name, self.player) == 0:
# Count was 1 before super().remove().
group_name = group_table_reverse[name]
# Decrease unique count for items in this group.
state.prog_items[self.player][group_name] -= 1
return changed
def create_regions(self) -> None:
multiworld = self.multiworld
+1 -3
View File
@@ -296,11 +296,9 @@ class ImpatientMimicsOption(Toggle):
class RandomEnemyPresetOption(OptionDict):
"""The YAML preset for the static enemy randomizer.
See the static randomizer documentation in `randomizer\\presets\\README.txt` for details.
See the online enemy randomization documentation for all available options.
Include this as nested YAML. For example:
.. code-block:: YAML
random_enemy_preset:
RemoveSource: Ancient Wyvern; Darkeater Midir
DontRandomize: Iudex Gundyr
+11 -1
View File
@@ -1,9 +1,11 @@
# Dark Souls III
Game Page | [Items] | [Locations]
Game Page | [Setup] | [Items] | [Locations] | [Enemy Randomization]
[Setup]: /tutorial/Dark%20Souls%20III/setup/en
[Items]: /tutorial/Dark%20Souls%20III/items/en
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
## What do I need to do to randomize DS3?
@@ -138,6 +140,14 @@ Check out the [item guide], which explains the named groups available for items.
[item guide]: /tutorial/Dark%20Souls%20III/items/en
## How can I change what enemies get randomized?
The [enemy randomization guide] explains how to further customize enemy randomization
for challenge runs or convenience. You can target specific enemies or entire
categories and even remove annoying enemy types outright.
[enemy randomization guide]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
## What's new from 2.x.x?
Version 3.0.0 of the Dark Souls III Archipelago client has a number of
@@ -0,0 +1,411 @@
# Dark Souls III Enemy Randomization
[Game Page] | [Setup] | [Items] | [Locations] | Enemy Randomization
[Game Page]: /games/Dark%20Souls%20III/info/en
[Setup]: /tutorial/Dark%20Souls%20III/setup/en
[Items]: /tutorial/Dark%20Souls%20III/items/en
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
If `randomize_enemies` in your Dark Souls 3 player config YAML is enabled, bosses, minibosses and basic enemies will
be shuffled with themselves respectively.
To further customize enemy randomization beyond that, there is a section called `random_enemy_preset`.
This tutorial will show all the ways how to configure that preset.
## Table of Contents
- [The Basics](#the-basics)
- [Individual Assignments](#individual-assignments)
- [Pools](#pools)
* [Pool Groups](#pool-groups)
+ [RandomByType](#randombytype)
* [Weights](#weights)
- [Settings](#settings)
* [Boss](#boss)
* [Miniboss](#miniboss)
* [Basic](#basic)
+ [BuffBasicEnemiesAsBosses](#buffbasicenemiesasbosses)
* [Enemies](#enemies)
* [DontRandomize](#dontrandomize)
* [RemoveSource](#removesource)
* [OopsAll](#oopsall)
- [Enemy Categories](#enemy-categories)
## The Basics
There are two main ways to assign an enemy to be randomized: [individual enemy assignments](#individual-assignments)
to target a singular enemy placement and setting up [Pools](#pools) to target a category of enemies.
Custom pools are recommended unless you specifically want to single out one enemy placement.
All bosses also have their own category, so individual assignment is not necessary in those cases.
Be aware of correct indentation of your YAML file. Every example in this document will need to be nested under the
`random_enemy_preset:` section.
Disable the preset by leaving just empty brackets `{}`. Like usual with YAML, you can add comments by using `#`.
For further examples, check out the "presets" folder of the standalone randomizer.
## Individual Assignments
Individual enemy assignment allows you to target individual enemies, rather than a category as under pools.
This overrides pools and any other configuration, will usually ignore progression, and can possibly cause you to have to
fight Yhorm the Giant without Storm Ruler.
You use it in the [`Enemies`](#enemies) section by selecting a specific enemy using its unique
ID, or its specific name followed by its ID.
See the '/randomizer/preset/Template.txt' file of the static randomizer for all available IDs.
There are also some special target names available for individual assignments:
- `any`: This is the default and allows any enemy in the pool to appear there.
- `norandom`: Assigns an enemy to itself. This has the same effect as adding the enemy name to [`DontRandomize`](#dontrandomize).
## Pools
A pool is a collection of enemies. A pool can both be a randomization target and an eligible group of random enemies to
be drawn from for randomization. See [Enemy Categories](#enemy-categories) for all available pools.
Pool assignment generally respects progression, like requiring Storm Ruler to be accessible before Yhorm the Giant.
By default, using a boss as another boss, or a miniboss as another miniboss, takes the source enemy out of the default
pool for that category, so each enemy will still be used once if possible. However, the enemy can still appear more
than once if used in a custom pool.
### Pool Groups
Pools can be joined into a pool group by joining several names, separated by a semicolon.
```yaml
# All basic enemies are just different hollows now
Basic:
- Weight: 100
Pool: Hollow Soldiers; Large Hollow Soldiers
```
#### RandomByType
By default, selection will be random across all eligible enemies. In our example above it would select from:
- Hollow Soldier
- Road of Sacrifices Hollow Soldier
- Cathedral Hollow Soldier
- Lothric Castle Hollow Soldier
- Grand Archives Hollow Soldier
and
- Large Hollow Soldier
- Cathedral Large Hollow Soldier
- Lothric Castle Large Hollow Soldier
However, this would make it more likely to select a regular soldier instead of a large one (5 out of 8), just because
there are fewer entries in the latter category.
You can specify `RandomByType: true` to select randomly from the list itself (Hollow Soldiers, Large Hollow Soldiers)
and make our previous example a true 50/50 split.
```yaml
# All basic enemies are just different hollows now
Basic:
- Weight: 100
Pool: Hollow Soldiers; Large Hollow Soldiers
RandomByType: true # To make it truly 50/50 between the categories
```
### Weights
Weights can be used to select multiple different outcomes within a pool, weighted to give different probabilities each.
Weights don't necessarily have to add up to 100, but doing it that way makes estimating probabilities very intuitive.
```yaml
Boss:
- Weight: 79 # 79% of bosses will still be bosses
Pool: default
- Weight: 20 # Replace 20% of all bosses with minibosses
Pool: Miniboss
- Weight: 1 # Replace 1% of all bosses with regular enemies. It's always funny
Pool: Basic
```
Be aware that weights will not work in the [`Enemies`](#enemies) section.
## Settings
### Boss
This setting indicates which enemies can be used as replacements for bosses.
By default, this is the pool of all 29 bosses.
```yaml
Boss:
- Weight: 80
Pool: default
- Weight: 20 # Replace 20% of all bosses with minibosses
Pool: Miniboss
```
### Miniboss
This setting indicates which enemies can be used as replacements for minibosses.
By default, this is the pool of all 32 minibosses (including duplicates).
```yaml
Miniboss:
- Weight: 80
Pool: default
- Weight: 20 # Replace 20% of all minibosses with bosses
Pool: Boss
```
### Basic
This setting indicates which enemies can be used as replacements for all other enemies, so non-bosses and non-minibosses.
By default, this is the pool of all ~2000 basic enemies (including duplicates).
```yaml
Basic:
- Weight: 94
Pool: default
- Weight: 5 # Replace 5% of all basic enemies with minibosses
Pool: Miniboss
- Weight: 1 # Replace 1% of all basic enemies with bosses
Pool: Boss
```
#### BuffBasicEnemiesAsBosses
If enabled, this causes basic enemies to become a lot stronger when randomized into the slot of a boss.
```yaml
Boss:
- Weight: 100 # All bosses are just basic enemies...
Pool: Basic
BuffBasicEnemiesAsBosses: true # ...but they are strong
```
### Enemies
Under the `Enemies:` setting you can add more nuanced replacements of random enemies.
There are two ways you can adjust enemies:
- Assign to a group of enemies using their category pool (see [Enemy Categories](#enemy-categories))
- Assign to one specifc enemy by using its number (see [Individual Assignments](#individual-assignments))
```yaml
Enemies:
# Replace only the very first Ravenous Crystal Lizard with the final boss
Ravenous Crystal Lizard 4000380: Lords of Cinder
# Replace all regular soldiers with skeletons or small crabs
Hollow Soldiers: Skeletons; Lesser Crab
# Knights remain knights, but variants (i.e. weapons) are still shuffled within the category
High Wall Lothric Knight: High Wall Lothric Knight
```
### DontRandomize
A semicolon-separated list of enemies or enemy types to not randomize (assign to themselves).
It is taken out of its default pool and also custom pools in this case, but it can still be assigned to
[individual enemies](#individual-assignments).
```yaml
DontRandomize: Iudex Gundyr # Iudex Gundyr will be at his vanilla location
Boss:
- Weight: 100
Pool: default # Boss slots other than Iudex Gundyr will never become him
```
### RemoveSource
A semicolon-separated list of enemies or enemy types to remove from all pools.
It can still be assigned to individual enemies.
This is overridden by [`DontRandomize`](#dontrandomize) directives.
```yaml
# Remove the most annoying enemies from all pools
RemoveSource: Bridge Darkeater Midir; Ancient Wyvern Mob; Curse-rotted Greatwood; High Lord Wolnir; Carthus Sandworm
```
### OopsAll
Assigning an enemy or a pool to `OopsAll` sets all pools to that specific enemy or category of enemy. This can still be
overridden using [individual enemy assginments](#individual-assignments), but otherwise every enemy is replaced by
this setting.
```yaml
# This run suddenly got very spooky
OopsAll: Skeletons
```
---
## Enemy Categories
The following enemy category pools are available:
- Any
- Bosses
- Minibosses
- Bosses and Minibosses
- Basic
- Abyss Watchers
- Aldrich, Devourer of Gods
- Ancient Wyvern
- Ancient Wyvern Mob
- Angel Pilgrim
- Basilisk
- Black Knight
- Blackflame Friede
- Boreal Outrider Knight
- Bridge Darkeater Midir
- Cage Spider
- Carthus Sandworm
- Cathedral Evangelist
- Cathedral Knight
- Cemetery Hollow
- Champion Gundyr
- Champion's Gravetender and Gravetender Greatwolf
- Consumed King Oceiros
- Corpse-grub
- Corvian
- Corvian Knight
- Corvian Settler
- Crabs
- Lesser Crab
- Greater Crab
- Ariandel Greater Crab
- Crystal Lizard
- Crystal Sage
- Crystal Sage in Archives
- Curse-rotted Greatwood
- Dancer of the Boreal Valley
- Darkeater Midir
- Darkwraith
- Deacon
- Cathedral Deacon
- Wide Deacon
- Irirthyll Deacon
- Irirthyll Tall Deacon
- Deacons of the Deep
- Deep Accursed
- Demon
- Demon Cleric
- Demon Prince
- Demonic Statue
- Dragonslayer Armour
- Dreg Heap Thrall
- Elder Ghru
- Father Ariandel
- Farron Follower
- Fire Witch
- Gargoyles
- Profaned Capital Gargoyle
- Archives Gargoyle
- Ghru Grunt
- Giant Fly
- Giant Slave
- Grand Archives Scholar
- Grave Warden
- Halflight, Spear of the Church
- Harald Legion Knight
- High Lord Wolnir
- Hobbled Cleric
- Hollow Manservant
- Hollow Soldiers
- Hollow Soldier
- Road of Sacrifices Hollow Soldier
- Cathedral Hollow Soldier
- Lothric Castle Hollow Soldier
- Grand Archives Hollow Soldier
- Hound Rat
- Infested Corpse
- Irirthyll Dungeon Peasant Hollow
- Irithyll Giant Slave
- Irithyll Starved Hound
- Irithyllian Slave
- Iudex Gundyr
- Jailer
- Judicator
- King of the Storm
- Large Hollow Soldiers
- Large Hollow Soldier
- Cathedral Large Hollow Soldier
- Lothric Castle Large Hollow Soldier
- Large Hound Rat
- Large Serpent-Man
- Large Starved Hound
- Locust Preacher
- Lords of Cinder (actually called "Soul of Cinder" ingame)
- Lorian, Elder Prince
- Lothric Knights
- High Wall Lothric Knight
- Lothric Castle Lothric Knight
- Dreg Heap Lothric Knight
- Red-Eyed Lothric Knight
- Lothric Priest
- Lothric, Younger Prince
- Lycanthrope
- Lycanthrope Hunter
- Maggot Belly Starved Hound
- Millwood Knight
- Mimic Chest
- Monstrosity of Sin
- Murkman
- Murkman Summoner
- Nameless King
- Old Demon King
- Passive Locust Preacher
- Peasant Hollow
- Poisonhorn Bug
- Pontiff Knight
- Pontiff Sulyvahn
- Pus of Man
- Ravenous Crystal Lizard
- Reanimated Corpse
- Ringed City Cleric
- Ringed Knight
- Road of Sacrifices Sorcerer
- Rock Lizard
- Rotten Slug
- Serpent-Man
- Serpent-Man Summoner
- Sewer Centipede
- Silver Knight
- Sister Friede
- Skeletons
- Skeleton
- Bonewheel Skeleton
- Carthus Curved Sword Skeleton
- Carthus Shotel Skeleton
- Ringed City Skeleton
- Slave Knight Gael
- Slave Knight Gael 1
- Slave Knight Gael 2
- Small Locust Preacher
- Smouldering Ghru Grunt
- Starved Hound
- Stray Demon
- Sulyvahn's Beast
- Thrall
- Tree Woman
- Vordt of the Boreal Valley
- Winged Knight
- Wolves
- Smaller Wolf
- Larger Wolf
- Greatwolf
- Wretch
- Writhing Flesh
- Catacombs Writhing Flesh
- Smouldering Writhing Flesh
- Anor Londo Writhing Flesh
- Yhorm the Giant
+3 -1
View File
@@ -1,9 +1,11 @@
# Dark Souls III Items
[Game Page] | Items | [Locations]
[Game Page] | [Setup] | Items | [Locations] | [Enemy Randomization]
[Game Page]: /games/Dark%20Souls%20III/info/en
[Setup]: /tutorial/Dark%20Souls%20III/setup/en
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
## Item Groups
+3 -1
View File
@@ -1,9 +1,11 @@
# Dark Souls III Locations
[Game Page] | [Items] | Locations
[Game Page] | [Setup] | [Items] | Locations | [Enemy Randomization]
[Game Page]: /games/Dark%20Souls%20III/info/en
[Setup]: /tutorial/Dark%20Souls%20III/setup/en
[Items]: /tutorial/Dark%20Souls%20III/items/en
[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
## Table of Contents
+7
View File
@@ -1,5 +1,12 @@
# Dark Souls III Randomizer Setup Guide
[Game Page] | Setup | [Items] | [Locations] | [Enemy Randomization]
[Game Page]: /games/Dark%20Souls%20III/info/en
[Items]: /tutorial/Dark%20Souls%20III/items/en
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
## Required Software
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
Executable → Regular
+19 -7
View File
@@ -19,7 +19,7 @@ import factorio_rcon
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled, get_base_parser
from MultiServer import mark_raw
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart
from Utils import async_start, get_file_safe_name, is_windows, Version, format_SI_prefix, get_text_between
from Utils import async_start, get_file_safe_name, is_windows, Version, format_SI_prefix, get_text_between, user_path
from .settings import FactorioSettings
from settings import get_settings
@@ -63,7 +63,7 @@ class FactorioCommandProcessor(ClientCommandProcessor):
def _cmd_toggle_chat(self):
"""Toggle sending of chat messages from players on the Factorio server to Archipelago."""
self.ctx.toggle_bridge_chat_out()
def _cmd_rcon_reconnect(self) -> bool:
"""Reconnect the RCON client if its disconnected."""
try:
@@ -88,7 +88,7 @@ class FactorioContext(CommonContext):
def __init__(self, server_address, password, filter_connection_changes: bool, filter_item_sends: bool, bridge_chat_out: bool,
rcon_port: int, rcon_password: str, server_settings_path: str | None,
factorio_server_args: tuple[str, ...]):
config_file: str, factorio_server_args: tuple[str, ...] | list[str]):
super(FactorioContext, self).__init__(server_address, password)
self.send_index: int = 0
self.rcon_client = None
@@ -105,6 +105,7 @@ class FactorioContext(CommonContext):
self.rcon_port: int = rcon_port
self.rcon_password: str = rcon_password
self.server_settings_path: str = server_settings_path
self.config_file: str = config_file
self.additional_factorio_server_args = factorio_server_args
@property
@@ -158,9 +159,11 @@ class FactorioContext(CommonContext):
"--rcon-port", str(self.rcon_port),
"--rcon-password", self.rcon_password,
"--server-settings", self.server_settings_path,
"--config", self.config_file,
*self.additional_factorio_server_args)
else:
return ("--rcon-port", str(self.rcon_port), "--rcon-password", self.rcon_password,
"--config", self.config_file,
*self.additional_factorio_server_args)
@property
@@ -364,7 +367,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
subprocess.run((
executable, "--create", savegame_name, "--preset", "archipelago"
executable, "--create", savegame_name, "--preset", "archipelago", "--config", ctx.config_file
))
factorio_process = subprocess.Popen((executable, "--start-server", savegame_name,
*ctx.server_args),
@@ -474,11 +477,11 @@ async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient):
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
savegame_name = os.path.abspath("Archipelago.zip")
savegame_name = user_path("factorio", "saves", "Archipelago.zip")
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
subprocess.run((
executable, "--create", savegame_name
executable, "--create", savegame_name, "--config", ctx.config_file
))
factorio_process = subprocess.Popen(
(executable, "--start-server", savegame_name, *ctx.server_args),
@@ -609,6 +612,9 @@ def launch(*new_args: str):
if not os.path.exists(os.path.dirname(executable)):
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
if os.path.isdir(executable) and os.path.exists(os.path.join(executable, "Contents", "MacOS", "factorio")):
# user entered the .App bundle, let's find the executable
executable = os.path.join(executable, "Contents", "MacOS", "factorio")
if os.path.isdir(executable): # user entered a path to a directory, let's find the executable therein
executable = os.path.join(executable, "factorio")
if not os.path.isfile(executable):
@@ -617,9 +623,15 @@ def launch(*new_args: str):
else:
raise FileNotFoundError(f"Path {executable} is not an executable file.")
config_file = user_path('factorio', 'config', 'apconfig.ini')
if not os.path.exists(config_file):
os.makedirs(os.path.dirname(config_file), exist_ok=True)
with open(config_file, 'w') as f:
f.write(f"[path]\nread-data=__PATH__system-read-data__\nwrite-data={user_path('factorio')}")
asyncio.run(main(lambda: FactorioContext(
args.connect, args.password,
initial_filter_connection_changes, initial_filter_item_sends, initial_bridge_chat_out,
rcon_port, rcon_password, server_settings, rest
rcon_port, rcon_password, server_settings, config_file, rest
)))
colorama.deinit()
+2 -6
View File
@@ -23,7 +23,6 @@ template_env: Optional[jinja2.Environment] = None
data_template: Optional[jinja2.Template] = None
data_final_template: Optional[jinja2.Template] = None
locale_template: Optional[jinja2.Template] = None
control_template: Optional[jinja2.Template] = None
settings_template: Optional[jinja2.Template] = None
@@ -94,7 +93,7 @@ def generate_mod(world: "Factorio", output_directory: str):
multiworld = world.multiworld
random = world.random
global data_final_template, locale_template, control_template, data_template, settings_template
global data_final_template, control_template, data_template, settings_template
with template_load_lock:
if not data_final_template:
def load_template(name: str):
@@ -107,7 +106,6 @@ def generate_mod(world: "Factorio", output_directory: str):
data_template = template_env.get_template("data.lua")
data_final_template = template_env.get_template("data-final-fixes.lua")
locale_template = template_env.get_template(r"locale/en/locale.cfg")
control_template = template_env.get_template("control.lua")
settings_template = template_env.get_template("settings.lua")
# get data for templates
@@ -195,9 +193,7 @@ def generate_mod(world: "Factorio", output_directory: str):
control_template.render(**template_data)))
mod.writing_tasks.append(lambda: (versioned_mod_name + "/settings.lua",
settings_template.render(**template_data)))
mod.writing_tasks.append(lambda: (versioned_mod_name + "/locale/en/locale.cfg",
locale_template.render(**template_data)))
info = base_info.copy()
info["name"] = mod_name
mod.writing_tasks.append(lambda: (versioned_mod_name + "/info.json",
@@ -0,0 +1,36 @@
[map-gen-preset-name]
archipelago=Archipelago
[entity-name]
ap-energy-bridge=Archipelago EnergyLink Bridge
[map-gen-preset-description]
archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos.
[technology-name]
ap-technology-full=__1__'s __2__ (__3__)
ap-technology-hidden=__1__
[technology-description]
ap-technology-full=Researching this technology sends __1__ to __2____3__.
ap-technology-item-advancement=, which is considered a logical advancement
ap-technology-item-useful=, which is considered useful
ap-technology-item-trap=, which is considered fun
ap-technology-hidden=Researching this technology sends something to someone__1__.
[mod-setting-name]
archipelago-death-link=Death Link
[mod-setting-description]
archipelago-death-link=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die.
[archipelago]
receive-ap-item=Received __1__ from __2__.
receive-ap-catchup=Received __1__ as it is already checked.
receive-sample-item=Received __1__x __2__
sample-inventory-full=Additional items will be sent when inventory space is available.
sample-error=Unable to receive __1__x [item=__2__] as this item does not exist.
fail-to-place=Failed to place __1__ in __2__
[traps]
new-evolution-factor=New evolution factor: __1__
+10 -10
View File
@@ -420,12 +420,12 @@ function update_player(index)
sent = 0
end
if sent > 0 then
player.print("Received " .. sent .. "x [item=" .. name .. ",quality={{ free_sample_quality_name }}]")
player.print({"archipelago.receive-sample-item", sent, "[item=" .. name .. ",quality="..stack.quality.."]"})
data.suppress_full_inventory_message = false
end
if sent ~= count then -- Couldn't full send.
if not data.suppress_full_inventory_message then
player.print("Additional items will be sent when inventory space is available.", {r=1, g=1, b=0.25})
player.print({"archipelago.sample-inventory-full"}, {r=1, g=1, b=0.25})
end
data.suppress_full_inventory_message = true -- Avoid spamming them with repeated full inventory messages.
samples[name] = count - sent -- Buffer the remaining items
@@ -434,7 +434,7 @@ function update_player(index)
samples[name] = nil -- Remove from the list
end
else
player.print("Unable to receive " .. count .. "x [item=" .. name .. "] as this item does not exist.")
player.print({"archipelago.sample-inventory-full", count, name})
samples[name] = nil
end
end
@@ -665,7 +665,7 @@ function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores)
end
end
if new_entity == nil then
force.print("Failed to place " .. args.name .. " in " .. serpent.line({x = x, y = y, radius = radius}))
force.print({"archipelago.fail-to-place", args.name, serpent.line({x = x, y = y, radius = radius})})
end
end
@@ -725,7 +725,7 @@ end,
local new_factor = game.forces["enemy"].get_evolution_factor("nauvis") +
(TRAP_EVO_FACTOR * (1 - game.forces["enemy"].get_evolution_factor("nauvis")))
game.forces["enemy"].set_evolution_factor(new_factor, "nauvis")
game.print({"", "New evolution factor:", new_factor})
game.print({"traps.new-evolution-factor", new_factor})
end,
["Teleport Trap"] = function()
for _, player in ipairs(game.forces["player"].players) do
@@ -777,10 +777,10 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
if index == nil then
game.print("ap-get-technology is only to be used by the Archipelago Factorio Client")
return
elseif index == -1 then -- for coop sync and restoring from an older savegame
elseif index == "-1" then -- for coop sync and restoring from an older savegame
tech = force.technologies[item_name]
if tech.researched ~= true then
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
game.print({"archipelago.receive-ap-catchup", "[technology=" .. tech.name .. "]"})
game.play_sound({path="utility/research_completed"})
tech.researched = true
end
@@ -792,7 +792,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
for _, item_name in ipairs(tech_stack) do
tech = force.technologies[item_name]
if tech.researched ~= true then
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
game.print({"archipelago.receive-ap-item", "[technology=" .. tech.name .. "]", source})
game.play_sound({path="utility/research_completed"})
tech.researched = true
return
@@ -804,7 +804,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
if tech ~= nil then
storage.index_sync[index] = tech
if tech.researched ~= true then
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
game.print({"archipelago.receive-ap-item", "[technology=" .. tech.name .. "]", source})
game.play_sound({path="utility/research_completed"})
tech.researched = true
end
@@ -812,7 +812,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
elseif TRAP_TABLE[item_name] ~= nil then
if storage.index_sync[index] ~= item_name then -- not yet received trap
storage.index_sync[index] = item_name
game.print({"", "Received ", item_name, " from ", source})
game.print({"archipelago.receive-ap-item", item_name, source})
TRAP_TABLE[item_name]()
end
else
@@ -154,6 +154,13 @@ technologies["{{ original_tech_name }}"].hidden_in_factoriopedia = true
{#- the tech researched by the local player #}
new_tree_copy = table.deepcopy(template_tech)
new_tree_copy.name = "ap-{{ location.address }}-"{# use AP ID #}
{%- if location.revealed %}
new_tree_copy.localised_name = {"technology-name.ap-technology-full", "{{ player_names[item.player] }}", "{{ item.name }}", "{{ location.name }}"}
new_tree_copy.localised_description = {"technology-description.ap-technology-full", "{{ item.name }}", "{{ player_names[item.player] }}", {% if item.advancement %}{"technology-description.ap-technology-item-advancement"}{% elif item.useful %}{"technology-description.ap-technology-item-useful"}{% elif item.trap %}{"technology-description.ap-technology-item-trap"}{% else %}""{% endif %}}
{%- else %}
new_tree_copy.localised_name = {"technology-name.ap-technology-hidden", "{{location.name}}"}
new_tree_copy.localised_description = {"technology-description.ap-technology-hidden", {% if tech_tree_information == 1 and item.advancement %}{"technology-description.ap-technology-item-advancement"}{% else %}""{% endif %}}
{% endif -%}
{% if location.crafted_item is not none %}
new_tree_copy.research_trigger = {
type = "{{ 'craft-fluid' if location.crafted_item in liquids else 'craft-item' }}",
@@ -13,7 +13,6 @@ end
local energy_bridge = table.deepcopy(data.raw["accumulator"]["accumulator"])
energy_bridge.name = "ap-energy-bridge"
energy_bridge.minable.result = "ap-energy-bridge"
energy_bridge.localised_name = "Archipelago EnergyLink Bridge"
energy_bridge.energy_source.buffer_capacity = "50MJ"
energy_bridge.energy_source.input_flow_limit = "10MW"
energy_bridge.energy_source.output_flow_limit = "10MW"
@@ -25,7 +24,6 @@ data.raw["accumulator"]["ap-energy-bridge"] = energy_bridge
local energy_bridge_item = table.deepcopy(data.raw["item"]["accumulator"])
energy_bridge_item.name = "ap-energy-bridge"
energy_bridge_item.localised_name = "Archipelago EnergyLink Bridge"
energy_bridge_item.place_result = energy_bridge.name
tint_icon(energy_bridge_item, energy_bridge_tint())
data.raw["item"]["ap-energy-bridge"] = energy_bridge_item
@@ -35,7 +33,6 @@ energy_bridge_recipe.name = "ap-energy-bridge"
energy_bridge_recipe.results = { {type = "item", name = energy_bridge_item.name, amount = 1} }
energy_bridge_recipe.energy_required = 1
energy_bridge_recipe.enabled = {% if energy_link %}true{% else %}false{% endif %}
energy_bridge_recipe.localised_name = "Archipelago EnergyLink Bridge"
data.raw["recipe"]["ap-energy-bridge"] = energy_bridge_recipe
data.raw["map-gen-presets"].default["archipelago"] = {{ dict_to_lua({"default": False, "order": "a", "basic_settings": world_gen["basic"], "advanced_settings": world_gen["advanced"]}) }}
@@ -1,31 +0,0 @@
[map-gen-preset-name]
archipelago=Archipelago
[map-gen-preset-description]
archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos.
[technology-name]
{% for location, item in locations %}
{%- if location.revealed %}
ap-{{ location.address }}-={{ player_names[item.player] }}'s {{ item.name }} ({{ location.name }})
{%- else %}
ap-{{ location.address }}-= {{location.name}}
{%- endif -%}
{% endfor %}
[technology-description]
{% for location, item in locations %}
{%- if location.revealed %}
ap-{{ location.address }}-=Researching this technology sends {{ item.name }} to {{ player_names[item.player] }}{% if item.advancement %}, which is considered a logical advancement{% elif item.useful %}, which is considered useful{% elif item.trap %}, which is considered fun{% endif %}.
{%- elif tech_tree_information == 1 and item.advancement %}
ap-{{ location.address }}-=Researching this technology sends something to someone, which is considered a logical advancement.
{%- else %}
ap-{{ location.address }}-=Researching this technology sends something to someone.
{%- endif -%}
{% endfor %}
[mod-setting-name]
archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Death Link
[mod-setting-description]
archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die.
@@ -21,6 +21,8 @@ data:extend({
type = "bool-setting",
name = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}",
setting_type = "runtime-global",
localised_name = {"mod-setting-name.archipelago-death-link"},
localised_description = {"mod-setting-description.archipelago-death-link"},
{% if death_link %}
default_value = true
{% else %}
+25 -20
View File
@@ -6,6 +6,7 @@
- Factorio: [Factorio Official Website](https://factorio.com)
- Needed by Players and Hosts
- Players will need to download an additional copy of Factorio for Archipelago to use. If you already own the game, you can download another copy for free by logging into the website.
##### Server Hosts
@@ -38,9 +39,13 @@ Validator page: [Yaml Validation Page](/check)
Connecting to someone else's game is the simplest way to play Factorio with Archipelago. It allows multiple people to
play in a single world, all contributing to the completion of the seed.
1. Acquire the Archipelago mod for this seed. It should be named `AP_*.zip`, where `*` is the seed number.
2. Copy the mod file into your Factorio `mods` folder, which by default is located at:
`C:\Users\<YourUserName>\AppData\Roaming\Factorio\mods`
**Note:** If nobody else on the multiworld is joining your Factorio game, then you are
hosting your own. This includes cases where more than one person is playing Factorio, but
all on separate worlds.
1. Acquire the Archipelago mod for this seed. Once you create the multiworld on the Archipelago website, there will be a link next to your name in the "Download Link" column for your Archipelago room. It will be called `AP-<seed>-<playernumber>-<playername>_<archipelago_version>.zip`.
2. Copy the mod file into your Factorio `mods` folder, which by default is located at:
`C:\Users\<YourUserName>\AppData\Roaming\Factorio\mods`. If you're on Linux, it's located at `~/.factorio/mods`.
3. Get the server address from the person hosting the game you are joining.
4. Launch Factorio
5. Click on "Multiplayer" in the main menu
@@ -66,8 +71,8 @@ This guide uses the following terms to refer to the software:
### What a Playable State Looks Like
- An Archipelago Server
- The generated Factorio Mod, created as a result of running `ArchipelagoGenerate.exe`
- One running instance of `ArchipelagoFactorioClient.exe` (the Archipelago Client) per Factorio world
- The generated Factorio Mod, created as a result of running `ArchipelagoGenerate.exe` or downloaded from
- One running instance of the Archipelago Client per Factorio world
- A running modded Factorio Server, which should have been started by the Archipelago Client automatically
- A running modded Factorio Client
@@ -75,7 +80,7 @@ This guide uses the following terms to refer to the software:
To play Factorio with Archipelago, a dedicated server setup is required. This dedicated Factorio Server must be
installed separately from your main Factorio Client installation. The recommended way to install two instances of
Factorio on your computer is to download the Factorio installer file directly from
Factorio on your computer is to download the standalone Factorio file directly from
factorio.com: [Factorio Official Website Download Page](https://factorio.com/download).
#### If you purchased Factorio on Steam, GOG, etc.
@@ -88,7 +93,7 @@ Factorio product code. This will allow you to download the game directly from th
It is recommended to download the standalone version of Factorio for use as a dedicated server. Doing so prevents any
potential conflicts with your currently-installed version of Factorio. Download the file by clicking on the button
appropriate to your operating system, and extract the folder to a convenient location. The best place to do this for
appropriate to your operating system, and extract the folder to a convenient location. The best place to do this for
Archipelago is to place the extracted game folder into the `Archipelago` directory and rename it to just be "Factorio".
@@ -100,9 +105,9 @@ have logged in, you may close the game.
#### Configure your Archipelago Installation
If you did not place the Factorio standalone in your Archipelago installation, you must modify your `host.yaml` file
inside your Archipelago installation directory so that it points to your standalone Factorio executable. Here is an
example of the appropriate setup, note the double `\\` are required:
If you did not place the Factorio standalone in your Archipelago installation, you must modify your `host.yaml` file
inside your Archipelago installation directory so that it points to your standalone Factorio executable. Here is an
example of the appropriate setup, note the double `\\` are required if you are on Windows:
```yaml
factorio_options:
@@ -115,12 +120,11 @@ This allows you to host your own Factorio game.
1. Obtain the Factorio mod for this Archipelago seed. It should be named `AP_*.zip`, where `*` is the seed number.
2. Install the mod into your Factorio Server by copying the zip file into the `mods` folder.
3. Install the mod into your Factorio Client by copying the zip file into the `mods` folder, which is likely located
at `C:\Users\YourName\AppData\Roaming\Factorio\mods`.
4. Obtain the Archipelago Server address from the website's host room, or from the server host.
5. Run your Archipelago Client, which is named `ArchipelagoFactorioClient.exe`. This was installed along with
Archipelago if you chose to include it during the installation process.
6. Enter `/connect [server-address]` into the input box at the bottom of the Archipelago Client and press "Enter"
3. Disable Space Age on your server by editing `mods-list.json` in the server's `mods` folder, going to the Space Age section, and changing `enabled: true` to `enabled: false`.
4. Install the mod into your Factorio Client by copying the zip file into the `mods` folder, which is likely located
at `C:\Users\YourName\AppData\Roaming\Factorio\mods`. If you're on Linux, it will be located at `~/.factorio/mods`.
5. Obtain the Archipelago Server address from the website's host room, or from the server host.
6. Open your Archipelago client. Type `/connect <server-address>` into the input box at the bottom of the client and press enter.
![Factorio Client for Archipelago Connection Command](connect-to-ap-server.png)
@@ -138,9 +142,10 @@ For more information about the commands you can use, see the [Commands Guide](/t
## Allowing Other People to Join Your Game
1. Ensure your Archipelago Client is running.
2. Ensure port `34197` is forwarded to the computer running the Archipelago Client.
3. Obtain your IP address by visiting whatismyip.com: [WhatIsMyIP Website](https://whatismyip.com/).
4. Provide your IP address to anyone you want to join your game, and have them follow the steps for
2. Ensure port `34197` is open on your computer's firewall.
3. Ensure port `34197` is forwarded to the computer running the Archipelago Client. This can be done in your router's settings.
4. Obtain your IP address by visiting whatismyip.com: [WhatIsMyIP Website](https://whatismyip.com/).
5. Provide your IP address to anyone you want to join your game, and have them follow the steps for
"Connecting to Someone Else's Factorio Game" above.
## Enabling Peaceful Mode
@@ -210,8 +215,8 @@ contents of this file may help you troubleshoot an issue on your own and is vita
in Archipelago.
## Additional Resources
- Alternate Tutorial by
Umenen: [Factorio (Steam) Archipelago Setup Guide for Windows](https://docs.google.com/document/d/1yZPAaXB-QcetD8FJsmsFrenAHO5V6Y2ctMAyIoT9jS4)
- Factorio Speedrun Guide: [Factorio Speedrun Guide by Nefrums](https://www.youtube.com/watch?v=ExLrmK1c7tA)
- Factorio Wiki: [Factorio Official Wiki](https://wiki.factorio.com/)
- [Archipelago Factorio Cheat Sheet](https://docs.google.com/spreadsheets/d/1317PZU957IVWCG9jUUklyHAl82BhrKIriynSZJ0AGNg/edit?gid=0#gid=0)
-21
View File
@@ -52,24 +52,3 @@ class GenericWorld(World):
if name == "Nothing":
return Item(name, ItemClassification.filler, -1, self.player)
raise InvalidItemError(name)
@deprecated("worlds.generic.PlandoItem is deprecated and will be removed in the next version. "
"Use Options.PlandoItem(s) instead.")
class PlandoItem(NamedTuple):
item: str
location: str
world: Union[bool, str] = False # False -> own world, True -> not own world
from_pool: bool = True # if item should be removed from item pool
force: str = 'silent' # false -> warns if item not successfully placed. true -> errors out on failure to place item.
def warn(self, warning: str):
if self.force in ['true', 'fail', 'failure', 'none', 'false', 'warn', 'warning']:
logging.warning(f'{warning}')
else:
logging.debug(f'{warning}')
def failed(self, warning: str, exception=Exception):
if self.force in ['true', 'fail', 'failure']:
raise exception(warning)
else:
self.warn(warning)
+4 -1
View File
@@ -60,7 +60,7 @@ adding more randomness and "mystery" to your options. Every configurable setting
Currently, there are only a few options that are root options. Everything else should be nested within one of these root
options or in some cases nested within other nested options. The only options that should exist in root
are `description`, `name`, `game`, `requires`, and the name of the games you want options for.
are `description`, `name`, `game`, `quantity`, `requires`, and the name of the games you want options for.
* `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files
using this to detail the intention of the file.
@@ -78,6 +78,9 @@ are `description`, `name`, `game`, `requires`, and the name of the games you wan
* `game` is where either your chosen game goes or, if you would like, can be filled with multiple games each with
different weights.
* `quantity` is the amount of times this yaml should be used when generating. This option is optional, the default value is 1.
To ensure that the name is unique with a value of at least two, the keywords from above must be used.
* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this
is good for detailing the version of Archipelago this YAML was prepared for. If it is rolled on an older version,
options may be missing and as such it will not work as expected. If any plando is used in the file then requiring it
+1 -1
View File
@@ -99,7 +99,7 @@ case-sensitive. You can also use item groups and location groups that are define
## Item Plando Examples
```yaml
plando_items:
plando_items:
# Example block - Pokémon Red and Blue
- items:
Potion: 3
+459
View File
@@ -0,0 +1,459 @@
from collections.abc import Mapping, Sequence
from types import MappingProxyType
from typing import Final
from .Locations import LocationData, get_locations_by_tags
# Item name to ram value conversion
item_ids: Final[Mapping[str, int]] = MappingProxyType({
"Strength": 0x000002,
"Speed": 0x000003,
"Magic": 0x000004,
"Armour": 0x000005,
"Gold": 0x000006,
"Key": 0x000007,
"XP": 0x000009,
"Level": 0x00000A,
"Class": 0x00000B,
"Max": 0x00000D,
"Compass": 0x000270,
"Lightning Potion": 0x001008,
"Light Potion": 0x002008,
"Acid Potion": 0x003008,
"Fire Potion": 0x004008,
"Acid Breath": 0x800480,
"Lightning Breath": 0x8004F0,
"Fire Breath": 0x800460,
"Light Amulet": 0x400870,
"Acid Amulet": 0x400880,
"Lightning Amulet": 0x4008F0,
"Fire Amulet": 0x400860,
"Lightning Shield": 0x400CF0,
"Fire Shield": 0x400C60,
"Invisibility": 0x4000E0,
"Levitate": 0x400030,
"Speed Boots": 0x400173,
"3-Way Shot": 0x400020,
"5-Way Shot": 0x400160,
"Rapid Fire": 0x400050,
"Reflective Shot": 0x400840,
"Reflective Shield": 0x400C40,
"Super Shot": 0x8008B0,
"Timestop": 0x4000D0,
"Phoenix Familiar": 0x400110,
"Growth": 0x400120,
"Shrink": 0x400130,
"Thunder Hammer": 0x800940,
"Anti-Death Halo": 0x7000A0,
"Invulnerability": 0x7000C0,
"Health": 0x000001,
"Runestone": 0x008210,
"Mirror Shard": 0x008260,
"Ice Axe of Untar": 0x8201C0,
"Flame of Tarkana": 0x8201D0,
"Scimitar of Decapitation": 0x8201E0,
"Marker's Javelin": 0x8201F0,
"Soul Savior": 0x820200,
"Mountain": 0x00804A,
"Castle": 0x00801A,
"Hell": 0x00806A,
"Ice": 0x00807A,
"Town": 0x00808A,
"Temple": 0x00809A,
"Battlefield": 0x0080DA,
"Skorne": 0x0080EA,
"Secret": 0x0080FA,
"Obelisk": 0x00800C,
"Minotaur": 0x0001A1,
"Falconess": 0x0001A2,
"Jackal": 0x0001A3,
"Tigress": 0x0001A4,
"Sumner": 0x0001A5,
"Skorne's Mask": 0x840620,
"Skorne's Horns": 0x840630,
"Skorne's Right Gauntlet": 0x840A40,
"Skorne's Left Gauntlet": 0x840A50,
"Death": 0x0000F0,
"Crossbow Shooter": 0x0000F1,
"Bomb Thrower": 0x0000F2,
"Bomb Runner": 0x0000F3,
"Golem": 0x0000F4,
})
# Character names used for slot data
characters: Final[tuple[str, ...]] = ("Minotaur", "Falconess", "Tigress", "Jackal", "Sumner")
# Base item charge count per pickup
# Some items are bitwise
base_count: Final[Mapping[str, int]] = MappingProxyType({
"Key": 1,
"Lightning Potion": 1,
"Light Potion": 1,
"Acid Potion": 1,
"Fire Potion": 1,
"Acid Breath": 5,
"Lightning Breath": 5,
"Fire Breath": 5,
"Light Amulet": 30,
"Acid Amulet": 30,
"Lightning Amulet": 30,
"Fire Amulet": 30,
"Lightning Shield": 10,
"Fire Shield": 10,
"Invisibility": 30,
"Levitate": 30,
"Speed Boots": 30,
"3-Way Shot": 30,
"5-Way Shot": 30,
"Rapid Fire": 30,
"Reflective Shot": 30,
"Reflective Shield": 30,
"Super Shot": 3,
"Timestop": 15,
"Phoenix Familiar": 30,
"Growth": 30,
"Shrink": 30,
"Thunder Hammer": 3,
"Anti-Death Halo": 30,
"Invulnerability": 30,
"Fruit": 50,
"Meat": 100,
"Gold": 100,
"Runestone 1": 1,
"Runestone 2": 2,
"Runestone 3": 3,
"Runestone 4": 4,
"Runestone 5": 5,
"Runestone 6": 6,
"Runestone 7": 7,
"Runestone 8": 8,
"Runestone 9": 9,
"Runestone 10": 10,
"Runestone 11": 11,
"Runestone 12": 12,
"Runestone 13": 13,
"Dragon Mirror Shard": 1,
"Chimera Mirror Shard": 3,
"Plague Fiend Mirror Shard": 4,
"Yeti Mirror Shard": 2,
"Valley of Fire Obelisk": 1,
"Dagger Peak Obelisk": 2,
"Cliffs of Desolation Obelisk": 3,
"Poisoned Fields Obelisk": 4,
"Haunted Cemetery Obelisk": 5,
"Castle Courtyard Obelisk": 6,
"Dungeon of Torment Obelisk": 7,
"Ice Axe of Untar": 1,
"Flame of Tarkana": 1,
"Scimitar of Decapitation": 1,
"Marker's Javelin": 1,
"Soul Savior": 1,
"Minotaur": 1,
"Falconess": 1,
"Tigress": 1,
"Jackal": 1,
"Sumner": 1,
"Poison Fruit": -50,
"Skorne's Mask": 50,
"Skorne's Horns": 50,
"Skorne's Left Gauntlet": 100,
"Skorne's Right Gauntlet": 100,
"Portal to Dagger Peak": 1,
"Portal to Cliffs of Desolation": 2,
"Portal to Lost Cave": 3,
"Portal to Volcanic Caverns": 4,
"Portal to Dragon's Lair": 5,
"Portal to Dungeon of Torment": 1,
"Portal to Tower Armory": 2,
"Portal to Castle Treasury": 3,
"Portal to Chimera's Keep": 4,
"Portal to Haunted Cemetery": 1,
"Portal to Venomous Spire": 2,
"Portal to Toxic Air Ship": 3,
"Portal to Vat of the Plague Fiend": 4,
"Portal to Frozen Camp": 1,
"Portal to Crystal Mine": 2,
"Portal to Erupting Fissure": 3,
"Portal to Yeti's Cavern": 4,
"Portal to Fortified Towers": 1,
"Portal to Infernal Fortress": 2,
"Death": 1,
"Crossbow Shooter": 1,
"Bomb Thrower": 1,
"Bomb Runner": 1,
"Golem": 1
})
# (zone_config << 4) + room_config -> location list
level_locations: Final[Mapping[int, Sequence[LocationData]]] = MappingProxyType({
# Zone 0x00 - Castle
0x00: get_locations_by_tags("castle_courtyard"),
0x01: get_locations_by_tags("dungeon_of_torment"),
0x02: get_locations_by_tags("tower_armory"),
0x03: get_locations_by_tags("castle_treasury"),
0x08: get_locations_by_tags("chimeras_keep"),
# Zone 0x03 - Mountain
0x30: get_locations_by_tags("valley_of_fire"),
0x31: get_locations_by_tags("dagger_peak"),
0x32: get_locations_by_tags("cliffs_of_desolation"),
0x33: get_locations_by_tags("lost_cave"),
0x34: get_locations_by_tags("volcanic_cavern"),
0x38: get_locations_by_tags("dragons_lair"),
# Zone 0x07 - Town
0x70: get_locations_by_tags("poisoned_fields"),
0x71: get_locations_by_tags("haunted_cemetery"),
0x72: get_locations_by_tags("venomous_spire"),
0x73: get_locations_by_tags("toxic_air_ship"),
0x78: get_locations_by_tags("plague_fiend"),
# Zone 0x05 - Underworld
0x50: get_locations_by_tags("gates_of_the_underworld"),
# Zone 0x06 - Ice
0x60: get_locations_by_tags("arctic_docks"),
0x61: get_locations_by_tags("frozen_camp"),
0x62: get_locations_by_tags("crystal_mine"),
0x63: get_locations_by_tags("erupting_fissure"),
0x68: get_locations_by_tags("yeti"),
# Zone 0x0D - Temple
0xD0: get_locations_by_tags("desecrated_temple"),
0xD8: get_locations_by_tags("altar_of_skorne"),
# Zone 0x0C - Battlefield
0xC0: get_locations_by_tags("battle_trenches"),
0xC1: get_locations_by_tags("fortified_towers"),
0xC2: get_locations_by_tags("infernal_fortress"),
})
# Compressed level size in ROM
level_size: Final[tuple[int, ...]] = (
0x9E0,
0x5E0,
0x740,
0x8A0,
0x90,
0x3B0,
0x5A0,
0x890,
0x670,
0x7D0,
0x90,
0xCE0,
0xA50,
0xA30,
0x8E0,
0x20,
0x760,
0xE90,
0xE40,
0xE00,
0xCD0,
0x20,
0x3F0,
0x0,
0xB00,
0xA30,
0xB30
)
# Level address in ROM
level_address: Final[tuple[int, ...]] = (
0xF939B0,
0xF958B0,
0xF945B0,
0xF94EE0,
0xF84CC0,
0xF910B0,
0xF915B0,
0xF91D00,
0xF92710,
0xF92F40,
0xF84B70,
0xF84FA0,
0xF85EB0,
0xF86B60,
0xF877C0,
0xF880E0,
0xF901C0,
0xF884E0,
0xF89370,
0xF8A5F0,
0xF8B760,
0xF8C7C0,
0xF90B50,
0x0,
0xF8D960,
0xF8E6E0,
0xF8F110
)
# Level header address in ROM
level_header: Final[tuple[int, ...]] = (
0xF9DD9C,
0xF9E07C,
0xF9DE54,
0xF9DF0C,
0xF9DFC4,
0xF9E6E0,
0xF9E798,
0xF9E850,
0xF9E908,
0xF9E9C0,
0xF9EA78,
0xF9F004,
0xF9F0BC,
0xF9F174,
0xF9F22C,
0xF9F2E4,
0xF9E1C0,
0xF9E330,
0xF9E3E4,
0xF9E498,
0xF9E54C,
0xF9E600,
0xF9DC2C,
0x0,
0xF9DA04,
0xF9DABC,
0xF9DB74
)
# Runestones required to access difficulties
# Used in Rules.py for access calculation
difficulty_lambda: Final[Mapping[int, Sequence[int]]] = MappingProxyType({
0x3: (0, 2, 3, 4),
0x0: (0, 5, 6, 7),
0x7: (0, 8, 9, 10),
0x6: (0, 11, 12, 13),
0xD: (0, 14, 15, 16),
0xC: (0, 17, 18, 19),
0x5: (0, 20, 21, 22)
})
# Runestones required to access difficulties
# Used in Rules.py for access calculation
difficulty_lambda_no_portal: Final[Mapping[int, Sequence[int]]] = MappingProxyType({
0x3: (0, 0, 1, 2),
0x0: (0, 2, 3, 4),
0x7: (0, 5, 6, 7),
0x6: (0, 8, 9, 10),
0xD: (0, 11, 12, 13),
0xC: (0, 14, 15, 16),
0x5: (0, 17, 18, 19)
})
# ID's for names said by announcer
sounds: Final[Mapping[int, int]] = MappingProxyType({
0: 0x9D,
1: 0x9E,
2: 0xA7,
3: 0xA5,
5: 0x9F,
6: 0xA1,
7: 0xA0,
8: 0xA2,
9: 0xA7
})
# ID's for colors said by announcer
colors: Final[Mapping[int, int]] = MappingProxyType({
0: 0xA3,
1: 0xA6,
2: 0x9C,
3: 0xA4
})
obelisks: Final[tuple[str, ...]] = (
"Valley of Fire Obelisk",
"Dagger Peak Obelisk",
"Cliffs of Desolation Obelisk",
"Poisoned Fields Obelisk",
"Haunted Cemetery Obelisk",
"Castle Courtyard Obelisk",
"Dungeon of Torment Obelisk"
)
mirror_shards: Final[tuple[str, ...]] = (
"Dragon Mirror Shard",
"Chimera Mirror Shard",
"Yeti Mirror Shard",
"Plague Fiend Mirror Shard"
)
portals: Final[Mapping[str, str]] = MappingProxyType({
"Portal to Dagger Peak": "Mountain",
"Portal to Cliffs of Desolation": "Mountain",
"Portal to Lost Cave": "Mountain",
"Portal to Volcanic Caverns": "Mountain",
"Portal to Dragon's Lair": "Mountain",
"Portal to Dungeon of Torment": "Castle",
"Portal to Tower Armory": "Castle",
"Portal to Castle Treasury": "Castle",
"Portal to Chimera's Keep": "Castle",
"Portal to Frozen Camp": "Ice",
"Portal to Crystal Mine": "Ice",
"Portal to Erupting Fissure": "Ice",
"Portal to Yeti's Cavern": "Ice",
"Portal to Haunted Cemetery": "Town",
"Portal to Venomous Spire": "Town",
"Portal to Toxic Air Ship": "Town",
"Portal to Vat of the Plague Fiend": "Town",
"Portal to Fortified Towers": "Battlefield",
"Portal to Infernal Fortress": "Battlefield"
})
excluded_portals: Final[Mapping[str, list[str]]] = MappingProxyType({
"Castle" : ["Portal to Dungeon of Torment", "Portal to Tower Armory", "Portal to Castle Treasury", "Portal to Chimera's Keep"],
"Town" : ["Portal to Haunted Cemetery", "Portal to Venomous Spire", "Portal to Toxic Air Ship", "Portal to Vat of the Plague Fiend"],
"Ice" : ["Portal to Frozen Camp", "Portal to Crystal Mine", "Portal to Erupting Fissure", "Portal to Yeti's Cavern"],
"Battlefield" : ["Portal to Fortified Towers", "Portal to Infernal Fortress"]
})
excluded_levels: Final[Mapping[str, list[str]]] = MappingProxyType({
"Mountain" : ["Valley of Fire", "Dagger Peak", "Cliffs of Desolation", "Lost Cave", "Volcanic Caverns", "Dragon's Lair"],
"Castle" : ["Castle Courtyard" ,"Dungeon of Torment", "Tower Armory", "Castle Treasury", "Chimera's Keep"],
"Ice" : ["Arctic Docks", "Frozen Camp", "Crystal Mine", "Erupting Fissure", "Yeti's Cavern"],
"Town" : ["Poisoned Fields", "Haunted Cemetery", "Venomous Spire", "Toxic Air Ship", "Vat of the Plague Fiend"],
"Battlefield" : ["Battle Trenches", "Fortified Towers", "Infernal Fortress"]
})
excluded_obelisks: Final[Mapping[str, list[str]]] = MappingProxyType({
"Castle" : ["Valley of Fire Obelisk", "Dagger Peak Obelisk", "Cliffs of Desolation Obelisk"],
"Town" : ["Castle Courtyard Obelisk", "Dungeon of Torment Obelisk"],
"Ice" : ["Poisoned Fields Obelisk", "Haunted Cemetery Obelisk"],
})
# Map location names to offsets in decompressed boss.bin
boss_location_offsets: Final[Mapping[str, int]] = MappingProxyType({
"Dragon's Lair - Dragon Mirror Shard": 0x9C08,
"Yeti's Cavern - Yeti Mirror Shard": 0x9C18,
"Chimera's Keep - Chimera Mirror Shard": 0x9C28,
"Vat of the Plague Fiend - Plague Fiend Mirror Shard": 0x9C38,
"Altar of Skorne - Skorne's Mask": 0x9C48,
"Altar of Skorne - Skorne's Horns": 0x9C58,
"Altar of Skorne - Skorne's Left Gauntlet": 0x9C68,
"Altar of Skorne - Skorne's Right Gauntlet": 0x9C78
})
boss_regions: Final[tuple[str, ...]] = (
"Dragon's Lair",
"Yeti's Cavern",
"Chimera's Keep",
"Vat of the Plague Fiend",
"Altar of Skorne",
"Gates of the Underworld",
)
# Item IDs for spawner traps (77780087-77780090)
spawner_trap_ids: Final[tuple[int, ...]] = (77780087, 77780088, 77780089, 77780090)
player_compass_index: Final[Mapping[int, int]] = MappingProxyType({
1: 1,
2: 0,
3: 3,
4: 2
})
+728
View File
@@ -0,0 +1,728 @@
import asyncio
import Utils
import settings
import os
import re
import socket
import traceback
import subprocess
import Patch
from typing import Optional
from BaseClasses import ItemClassification
from CommonClient import ClientCommandProcessor, CommonContext, get_base_parser, gui_enabled, logger, server_loop
from NetUtils import ClientStatus, NetworkItem
from .Data import (
base_count,
item_ids,
level_locations,
sounds,
colors,
portals,
spawner_trap_ids,
player_compass_index
)
from .Items import ItemData, items_by_id
READ = "READ_CORE_RAM"
WRITE = "WRITE_CORE_RAM"
PLAYER_CLASS = 0xFD30F
PLAYER_COLOR = 0xFD30E
SOUND_ADDRESS = 0xAE740
SOUND_START = 0xEEFC
PLAYER_KILL = 0xFD300
PLAYER_MENU = 0x9D3C
BOSS_GOAL = 0x45D34
BOSS_GOAL_BACKUP = 0x45D3C
LOCATIONS_BASE_ADDRESS = 0x64A68
ZONE_ID = 0x6CA58
LEVEL_ID = 0x6CA5C
MOD_ITEM_ID = 0x3FC800
MOD_QUANTITY = 0x3FC804
MOD_PLAYER_ID = 0x3FC808
MOD_OBELISK_QUANTITY = 0x3FC7E4
MOD_BOSS_GOAL = 0x3FC7E5
MOD_PLAYERS_LIST = 0x3FC7D0
MOD_COMPASS_COUNT = 0x3FC7D4
GOAL_POLLS = 5
class RetroSocket:
def __init__(self):
self.host = "localhost"
self.port = 55355
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.socket.setblocking(False)
def send(self, message: str):
try:
self.socket.sendto(message.encode(), (self.host, self.port))
except Exception as e:
raise Exception("An error occurred while sending a message.")
async def read(self, message: str) -> Optional[bytes]:
self.send(message)
try:
response = await asyncio.wait_for(asyncio.get_event_loop().sock_recv(self.socket, 30000), 1.0)
data = response.decode().strip("\n").split(" ")
b = b""
for s in data[2:]:
if "-1" in s:
raise Exception("Client tried to read from an invalid address or ROM is not open...")
b += bytes.fromhex(s)
return b
except asyncio.TimeoutError:
logger.error("Timeout while waiting for socket response...")
await asyncio.sleep(2)
return None
except ConnectionResetError:
logger.error("The connection was reset...")
await asyncio.sleep(2)
return None
except OSError as e:
logger.error(f"Socket error during read: {e}")
await asyncio.sleep(2)
return None
async def status(self) -> str:
message = "GET_STATUS"
self.send(message)
try:
data = await asyncio.wait_for(asyncio.get_event_loop().sock_recv(self.socket, 4096), 1.0)
return data.decode()
except (asyncio.TimeoutError, ConnectionResetError, OSError):
pass
def message_format(arg: str, params: str) -> str:
return f"{arg} {params}"
def param_format(adr: int, arr: bytes) -> str:
return " ".join([hex(adr)] + [f"0x{byte:02X}" for byte in arr])
class GauntletLegendsCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: CommonContext):
super().__init__(ctx)
def _cmd_deathlink_toggle(self):
"""Toggle Deathlink on or off"""
self.ctx.deathlink_enabled = not self.ctx.deathlink_enabled
self.ctx.update_death_link(self.ctx.deathlink_enabled)
logger.info(f"Deathlink {('Enabled.' if self.ctx.deathlink_enabled else 'Disabled.')}")
class GauntletLegendsContext(CommonContext):
command_processor = GauntletLegendsCommandProcessor
game = "Gauntlet Legends"
items_handling = 0b101
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.useful: list[NetworkItem] = []
self.deathlink_pending: bool = False
self.deathlink_enabled: bool = False
self.deathlink_triggered: bool = False
self.gl_sync_task = None
self.glslotdata = None
self.socket = RetroSocket()
self.rom_loaded: bool = False
self.locations_checked: list[int] = []
self.retro_connected: bool = False
self.scouted: bool = False
self.obelisks: list[NetworkItem] = []
self.item_locations: list[int] = []
self.obelisk_locations: list[int] = []
self.chest_locations: list[int] = []
self.spawner_locations: list[int] = []
self.item_address: int = 0
self.chest_address: int = 0
self.spawner_address: int = 0
self.vanilla_spawner_count: int = 0
self.zone: int = 0
self.level: int = 0
self.current_zone: int = -1
self.current_level: int = -1
self.level_id: int = 0
self.location_scouts: list[NetworkItem] = []
self.players: list[int] = []
self.queued_traps: list[tuple[str, int, bool]] = []
self.spawned_traps: int = 0
self.item_ram_indices: dict[int, int] = {}
self.chest_ram_indices: dict[int, int] = {}
self.goal_streak: int = 0
def on_deathlink(self, data: dict):
super().on_deathlink(data)
self.deathlink_pending = True
async def update_stage(self):
self.zone = await self._read_ram_int(ZONE_ID, 1)
self.level = await self._read_ram_int(LEVEL_ID, 1)
async def check_goal(self) -> bool:
if self.glslotdata is None:
return False
if self.glslotdata["goal"] == 1:
goal = await self._read_ram_int(BOSS_GOAL, 4)
backup = await self._read_ram_int(BOSS_GOAL_BACKUP, 4)
return goal == 0xA or backup == 0xA
elif self.glslotdata["goal"] == 2:
goal = await self._read_ram_int(MOD_BOSS_GOAL, 1, True)
if goal >= self.glslotdata["boss_goal_count"]:
self.goal_streak += 1
else:
self.goal_streak = 0
return self.goal_streak >= GOAL_POLLS
def _normalize_item_name(self, name: str) -> str:
if "Runestone" in name:
return "Runestone"
if "Fruit" in name or "Meat" in name:
return "Health"
if "Obelisk" in name:
return "Obelisk"
if "Mirror" in name:
return "Mirror Shard"
if portals.get(name, False):
return portals[name]
return name
async def update_item(self, name: str, count: int, player: int = None, infinite_count: bool = False):
name = self._normalize_item_name(name)
await self._write_ram(MOD_ITEM_ID,
int.to_bytes((item_ids[name] if not infinite_count else item_ids[name] & 0xFFFF), 4,
"little"))
await self._write_ram(MOD_QUANTITY, int.to_bytes(count, 4, "little", signed=True))
await self._write_ram(MOD_PLAYER_ID, int.to_bytes(player, 4, "little"))
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(GauntletLegendsContext, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected"}:
self.glslotdata = args["slot_data"]
self.deathlink_enabled = bool(self.glslotdata["death_link"])
if self.deathlink_enabled:
Utils.async_start(self.update_death_link(self.deathlink_enabled))
elif cmd == "LocationInfo":
self.location_scouts = args["locations"]
elif cmd == "RoomInfo":
self.seed_name = args["seed_name"]
# Update inventory based on items received from server
# Also adds starting items based on a few yaml options
async def handle_items(self):
self.players = list(await self._read_ram(MOD_PLAYERS_LIST, 4))
self.players = [player for player in self.players if player != 0]
if not self.players:
return
for player in self.players:
active = await self._read_ram_int(PLAYER_KILL + (0x1F0 * (player - 1)), 1)
if active != 0x4:
continue
compass = await self._read_ram_int(MOD_COMPASS_COUNT + (2 * player_compass_index[player]), 2)
if compass - 1 < len(self.items_received):
for index in range(compass - 1, len(self.items_received)):
active = await self._read_ram_int(PLAYER_KILL + (0x1F0 * (player - 1)), 1)
if active != 0x4:
break
item = self.items_received[index].item
if player != self.players[0] and item in spawner_trap_ids:
continue
item_name = items_by_id[item].item_name
if self.current_zone in (0x8, 0xE) and item in spawner_trap_ids:
if len([trap for trap in self.queued_traps if trap[1] == index]) < 1:
self.queued_traps.append((item_name, index, False))
await self.give_item(item_name, player)
await self.update_item("Compass", 1, player)
async def give_item(self, item_name: str, player: int):
await self.wait_for_mod_clear()
await asyncio.sleep(0.02)
await self.update_item(item_name, base_count[item_name], player)
await self.wait_for_mod_clear()
await asyncio.sleep(0.02)
async def wait_for_mod_clear(self, poll_interval: float = 0.05):
while True:
mod_data = await self.socket.read(message_format(READ, f"0x{MOD_ITEM_ID:x} 12"))
if mod_data and all(byte == 0 for byte in mod_data):
return True
await asyncio.sleep(poll_interval)
async def _read_ram(self, address: int, size: int) -> bytes:
return await self.socket.read(message_format(READ, f"0x{address:x} {size}"))
async def _read_ram_int(self, address: int, size: int, signed: bool = False) -> int:
data = await self._read_ram(address, size)
return int.from_bytes(data, "little", signed=signed) if data else 0
async def _write_ram(self, address: int, data: bytes):
self.socket.send(message_format(WRITE, param_format(address, data)))
async def dead(self) -> bool:
val = await self._read_ram_int(PLAYER_KILL + (0x1F0 * (self.players[0] - 1)), 1)
return (val & 0xF) == 0x8
async def dead_or_menu(self) -> bool:
val = await self._read_ram_int(PLAYER_KILL + (0x1F0 * (self.players[0] - 1)), 1)
return (val & 0xF) == 0x8 or (val & 0xF) == 0x1
async def get_seed_name(self) -> str:
seed_name = await self._read_ram(0x3FC7F0, 0x10)
return seed_name.decode("utf-8").strip()
async def scout_locations(self, ctx: "GauntletLegendsContext") -> None:
try:
self.location_scouts = []
self.obelisk_locations = []
self.item_locations = []
self.chest_locations = []
self.spawner_locations = []
self.useful = []
self.obelisks = []
self.vanilla_spawner_count = 0
self.item_ram_indices = {}
self.chest_ram_indices = {}
raw_locations = [location for location in level_locations.get(self.level_id, []) if
"Mirror" not in location.name and "Skorne" not in location.name]
scoutable_location_ids = [location.id for location in raw_locations if
location.id in ctx.checked_locations or location.id in self.missing_locations]
# Scout locations if any exist
if raw_locations:
await ctx.send_msgs([{
"cmd": "LocationScouts",
"locations": scoutable_location_ids,
"create_as_hint": 0,
}])
while not self.location_scouts:
await asyncio.sleep(0.1)
# Build lookup for scouted items by location
scouted_by_location = {item.location: item for item in self.location_scouts}
item_slots: list[int] = []
chest_slots: list[int] = []
appended_items: list[int] = []
for loc in raw_locations:
is_obelisk_loc = "Obelisk" in loc.name
is_chest_loc = (not is_obelisk_loc) and (
"Chest" in loc.name
or ("Barrel" in loc.name and "Barrel of Gold" not in loc.name)
)
scouted = scouted_by_location.get(loc.id)
if not scouted:
if is_obelisk_loc:
pass
elif is_chest_loc:
chest_slots.append(loc.id)
else:
item_slots.append(loc.id)
continue
item_id = scouted.item
item_player = scouted.player
item_data = items_by_id.get(item_id, ItemData())
if is_obelisk_loc:
if "Obelisk" in item_data.item_name and item_player == self.slot:
pass
else:
appended_items.append(loc.id)
continue
if item_player == self.slot and item_id in spawner_trap_ids:
continue
if item_player != self.slot:
if is_chest_loc:
chest_slots.append(loc.id)
else:
item_slots.append(loc.id)
continue
if "Obelisk" in item_data.item_name:
continue
if (item_data.progression in (ItemClassification.useful, ItemClassification.progression)
and is_chest_loc):
appended_items.append(loc.id)
continue
if is_chest_loc:
chest_slots.append(loc.id)
else:
item_slots.append(loc.id)
final_item_order = item_slots + appended_items
self.item_ram_indices = {
loc_id: idx for idx, loc_id in enumerate(final_item_order)
}
self.chest_ram_indices = {
loc_id: idx for idx, loc_id in enumerate(chest_slots)
}
# Categorize scouted locations - mirror ROM's patch_items logic exactly
for loc in raw_locations:
scouted_item = scouted_by_location.get(loc.id)
if not scouted_item:
continue
item_id = scouted_item.item
item_player = scouted_item.player
item_data = items_by_id.get(item_id, ItemData())
is_chest = "Chest" in loc.name or ("Barrel" in loc.name and "Barrel of Gold" not in loc.name)
# Check obelisk locations
if "Obelisk" in loc.name:
if "Obelisk" in item_data.item_name and item_player == self.slot:
self.obelisk_locations.append(loc.id)
self.obelisks.append(scouted_item)
else:
self.item_locations.append(loc.id)
continue
# Check spawner (ROM: item[1] == player and item[0] in SPAWNER_TRAP_IDS)
if item_player == self.slot and item_id in spawner_trap_ids:
self.spawner_locations.append(loc.id)
continue
# Check non-local player (ROM: item[1] != player) - stays as item/chest
if item_player != self.slot:
if is_chest:
self.chest_locations.append(loc.id)
else:
self.item_locations.append(loc.id)
continue
# Check obelisk item at non-obelisk location (ROM: "Obelisk" in item_name)
if "Obelisk" in item_data.item_name:
self.obelisk_locations.append(loc.id)
self.obelisks.append(scouted_item)
continue
# Check useful/progression chest -> item conversion
if item_data.progression in (ItemClassification.useful, ItemClassification.progression) and is_chest:
self.item_locations.append(loc.id)
self.useful.append(scouted_item)
continue
# Regular item/chest
if is_chest:
self.chest_locations.append(loc.id)
else:
self.item_locations.append(loc.id)
self.scouted = True
except Exception:
logger.error(traceback.format_exc())
async def location_loop(self) -> list[int]:
if self.current_zone == -1:
self.current_zone = self.zone
self.current_level = self.level
self.level_id = (self.current_zone << 4) + self.current_level
zone_or_level_changed = self.zone != self.current_zone or self.level != self.current_level
if zone_or_level_changed:
if self.current_level & 0x8 == 0x8 and self.level_id != 0x58 and not await self.dead_or_menu():
await self.check_locations([loc.id for loc in level_locations[self.level_id]
if "Mirror Shard" in loc.name or "Skorne" in loc.name])
self.current_zone = self.zone
self.current_level = self.level
self.level_id = (self.current_zone << 4) + self.current_level
self.scouted = False
self.spawned_traps = 0
self.goal_streak = 0
await asyncio.sleep(2)
if self.current_zone in (0x8, 0xE):
return []
if not self.scouted:
await self.scout_locations(self)
active = await self._read_ram_int(PLAYER_KILL + (0x1F0 * (self.players[0] - 1)), 1)
if active != 0x4:
return []
if len(self.queued_traps) > 0 and self.spawned_traps == 0:
for i, trap in enumerate(self.queued_traps):
trap_name, index, given = trap
if not given:
self.spawned_traps += 1
await self.give_item(trap_name, self.players[0])
self.queued_traps[i] = (trap_name, index, True)
if self.spawned_traps >= 5:
break
locations_address = await self._read_ram_int(LOCATIONS_BASE_ADDRESS, 4) & 0xFFFFFF
self.item_address = await self._read_ram_int(locations_address + 0x14, 4)
self.spawner_address = await self._read_ram_int(locations_address + 0x1C, 4)
self.chest_address = await self._read_ram_int(locations_address + 0x30, 4)
# Read vanilla spawner count from level header (2 bytes at offset 0x28)
spawner_count_data = await self._read_ram(locations_address + 0x6, 2)
if spawner_count_data and not self.vanilla_spawner_count:
total_spawner_count = int.from_bytes(spawner_count_data, "little")
# Vanilla count is total minus the ones we added
self.vanilla_spawner_count = total_spawner_count - len(self.spawner_locations)
if 0x7FFF0BAD in (self.item_address, self.chest_address, self.spawner_address):
return []
self.item_address &= 0xFFFFFF
self.spawner_address &= 0xFFFFFF
self.chest_address &= 0xFFFFFF
acquired = []
max_item_idx = max((self.item_ram_indices[loc_id] for loc_id in self.item_locations), default=-1)
item_section = await self._read_ram(self.item_address, (max_item_idx + 1) * 0x18) if max_item_idx >= 0 else b""
for loc_id in self.item_locations:
offset = self.item_ram_indices[loc_id] * 0x18
active_byte, state = item_section[offset + 0x2], item_section[offset + 0x3]
if state < 0x7F and active_byte == 1 and state == 0:
acquired.append(loc_id)
obelisk = await self._read_ram_int(MOD_OBELISK_QUANTITY, 1)
for j, loc_id in enumerate(self.obelisk_locations):
bit = base_count[items_by_id[self.obelisks[j].item].item_name] - 1
if obelisk & (1 << bit):
acquired.append(loc_id)
max_chest_idx = max((self.chest_ram_indices[loc_id] for loc_id in self.chest_locations), default=-1)
chest_section = await self._read_ram(self.chest_address, (max_chest_idx + 1) * 0x18) if max_chest_idx >= 0 else b""
for loc_id in self.chest_locations:
offset = self.chest_ram_indices[loc_id] * 0x18
active_byte, state = chest_section[offset + 0x2], chest_section[offset + 0x3]
if state < 0x7F and active_byte == 1 and state != 1:
acquired.append(loc_id)
# Check spawner locations - these are added after vanilla spawners
# Read spawners starting after vanilla_spawner_count
if self.spawner_locations:
spawner_start = self.spawner_address + (self.vanilla_spawner_count * 0x1C)
spawner_section = await self._read_ram(spawner_start, len(self.spawner_locations) * 0x1C)
for i, loc_id in enumerate(self.spawner_locations):
offset = i * 0x1C
active_byte, hit = spawner_section[offset + 0x2], spawner_section[offset + 0x1A]
if active_byte == 1 and hit == 1:
acquired.append(loc_id)
await self.update_stage()
if self.zone != self.current_zone or self.level != self.current_level:
return []
return [loc_id for loc_id in acquired if loc_id not in self.checked_locations]
async def die(self):
"""Trigger deathlink death with character-specific death sound."""
self.deathlink_triggered = True
for player in self.players:
char = await self._read_ram_int(PLAYER_CLASS + (0x1F0 * (player - 1)), 1)
color = await self._read_ram_int(PLAYER_COLOR + (0x1F0 * (player - 1)), 1)
# Play death sound
sound_data = (int.to_bytes(colors[color], 4, "little") +
int.to_bytes(sounds[char], 4, "little") +
int.to_bytes(0xBB, 4, "little"))
await self._write_ram(SOUND_ADDRESS, sound_data)
await self._write_ram(SOUND_START, int.to_bytes(0xE00AE718, 4, "little"))
await asyncio.sleep(2)
# Stop sound and kill player
await self._write_ram(SOUND_START, int.to_bytes(0x0, 4, "little"))
await self._write_ram(PLAYER_KILL + (0x1F0 * (player - 1)), int.to_bytes(0x7, 1, "little"))
def make_gui(self):
ui = super().make_gui()
ui.base_title = "Archipelago Gauntlet Legends Client"
return ui
async def gl_sync_task(ctx: GauntletLegendsContext):
logger.info("Starting N64 connector...")
while not ctx.exit_event.is_set():
try:
if not ctx.retro_connected:
logger.info("Attempting to connect to Retroarch...")
status = await ctx.socket.status()
ctx.retro_connected = True
ctx.rom_loaded = "CONTENTLESS" not in status
logger.info("Connected to Retroarch")
continue
if not ctx.rom_loaded:
status = await ctx.socket.status()
if "CONTENTLESS" in status:
logger.info("No ROM loaded, waiting...")
await asyncio.sleep(3)
continue
logger.info("ROM Loaded")
ctx.rom_loaded = True
if not ctx.auth:
await asyncio.sleep(1)
continue
seed_name = await ctx.get_seed_name()
if seed_name != ctx.seed_name[0:16]:
logger.info(f"ROM seed does not match room seed ({seed_name} != {ctx.seed_name}), "
f"please load the correct ROM.")
await ctx.disconnect()
continue
await ctx.update_stage()
if ctx.zone == 0x10:
continue
await ctx.handle_items()
checking = await ctx.location_loop()
menu = await ctx._read_ram_int(PLAYER_MENU, 4)
alive = False
if ctx.deathlink_pending and ctx.deathlink_enabled and menu == 1:
ctx.deathlink_pending = False
ctx.deathlink_triggered = True
await ctx.die()
elif len(ctx.players) > 0:
player_state = await ctx._read_ram_int(PLAYER_KILL + (0x1F0 * (ctx.players[0] - 1)), 1)
dead = (player_state & 0xF) == 0x8
alive = (player_state & 0xF) == 0x4
if dead and ctx.deathlink_enabled and not ctx.deathlink_triggered:
await ctx.send_death(f"{ctx.player_names[ctx.slot]} ran out of food.")
ctx.deathlink_triggered = True
if alive:
ctx.deathlink_triggered = False
if checking:
ctx.locations_checked += checking
await ctx.check_locations(checking)
# Only trust the goal check when the player is alive
if alive:
goal = await ctx.check_goal()
if not ctx.finished_game and goal:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
else:
ctx.goal_streak = 0
except Exception as e:
logger.error(f"Error: {e}\n{traceback.format_exc()}")
ctx.socket = RetroSocket()
ctx.retro_connected = False
await asyncio.sleep(2)
_original_opt_content: dict[str, str | None] = {}
async def _patch_opt():
"""Create RetroArch core options override for CountPerOp=1."""
retroarch_path = settings.get_settings().gl_options.retroarch_path
override_dir = os.path.join(retroarch_path, "config", "Mupen64Plus-Next")
os.makedirs(override_dir, exist_ok=True)
override_path = os.path.join(override_dir, "Mupen64Plus-Next.opt")
target_setting = 'mupen64plus-CountPerOp = "1"'
if override_path not in _original_opt_content:
_original_opt_content[override_path] = open(override_path).read() if os.path.exists(override_path) else None
content = _original_opt_content[override_path] or ""
if target_setting in content:
return
if "mupen64plus-CountPerOp" in content:
content = re.sub(r'mupen64plus-CountPerOp\s*=\s*"[^"]*"', target_setting, content)
else:
content = content.rstrip("\n") + f"\n{target_setting}\n" if content else f"{target_setting}\n"
with open(override_path, "w") as f:
f.write(content)
def _restore_opt_files():
for path, original in _original_opt_content.items():
try:
if original is None and os.path.exists(path):
os.remove(path)
elif original is not None:
with open(path, "w") as f:
f.write(original)
except Exception as e:
logger.error(f"Failed to restore {path}: {e}")
_original_opt_content.clear()
async def _launch_retroarch(rom_path: str):
retroarch_path = settings.get_settings().gl_options.retroarch_path
retroarch_exe = os.path.join(retroarch_path, "retroarch.exe")
core_path = os.path.join(retroarch_path, "cores", "mupen64plus_next_libretro.dll")
if not os.path.exists(retroarch_exe):
logger.error(f"RetroArch not found at: {retroarch_exe}")
return
if not os.path.exists(core_path):
logger.error(f"Mupen64Plus core not found at: {core_path}")
return
subprocess.Popen([retroarch_exe, "-L", core_path, rom_path])
logger.info(f"Launched RetroArch with ROM: {rom_path}")
# Wait for RetroArch to start up
await asyncio.sleep(2)
async def _patch_and_launch_game(patch_file: str):
metadata, output_file = Patch.create_rom_file(patch_file)
await _patch_opt()
await _launch_retroarch(output_file)
def launch(*args):
async def main(args):
if args.patch_file:
await asyncio.create_task(_patch_and_launch_game(args.patch_file))
ctx = GauntletLegendsContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.gl_sync_task = asyncio.create_task(gl_sync_task(ctx), name="Gauntlet Legends Sync Task")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
_restore_opt_files()
parser = get_base_parser()
parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an APGL file")
args = parser.parse_args(args)
import colorama
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()
+43
View File
@@ -0,0 +1,43 @@
from collections.abc import Mapping
from types import MappingProxyType
from typing import Final
from BaseClasses import Item, ItemClassification
from .Data import portals, obelisks, mirror_shards
class ItemData:
id: int
item_name: str
progression: ItemClassification
rom_id: int
frequency: int
def __init__(self, id: int | None = 0, item_name: str = "", progression: int = 0, rom_id: str = "0x0", frequency: int = 1):
self.id = id
self.item_name = item_name
self.progression = ItemClassification(progression)
self.rom_id = int(rom_id, 16)
self.frequency = frequency
class GLItem(Item):
game: str = "Gauntlet Legends"
def import_items() -> tuple[ItemData, ...]:
import orjson
import pkgutil
return tuple(ItemData(**item) for item in orjson.loads(pkgutil.get_data(__name__, "json/items.json").decode("utf-8")))
item_list: Final[tuple[ItemData, ...]] = import_items()
item_table: Final[Mapping[str, ItemData]] = MappingProxyType({item.item_name: item for item in item_list})
items_by_id: Final[Mapping[int, ItemData]] = MappingProxyType({item.id: item for item in item_list})
gauntlet_item_name_groups = {
"Runestone": [f"Runestone {i}" for i in range(1, 14)],
"Portal": portals.keys(),
"Obelisk": obelisks,
"Mirror Shard": mirror_shards
}

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