Compare commits

...

97 Commits

Author SHA1 Message Date
Fabian Dill
1a0ccf9685 MultiServer: no longer load local datapackage 2025-04-15 18:48:48 +02:00
Fabian Dill
f0ca56469f Core: always embed Archipelago 2025-04-15 18:46:54 +02:00
Seldom
1873c52aa6 Terraria: 1.4.4 and Calamity support (#3847)
* Terraria integration

* Precollected items for debugging

* Fix item classification

* Golem requires Plantera's Bulb

* Pumpkin Moon requires Dungeon

* Progressive Dungeon

* Reorg, Options.py work

* Items are boss flags

* Removed unused option

* Removed nothing

* Wall, Plantera, and Zenith goals

* Achievements and items

* Fixed The Cavalry and Completely Awesome achievements

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

* Some docs, Python 3.8 compat

* docs

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

* Requested changes

* Fix potential thread unsafety, replace Nothing with 50 Silver

* Remove a log

* Corrected heading

* Added incompatible mods list

* In-progress calamity integration

* Terraria events progress

* Rules use events

* Removed an intentional crash I accidentally left in

* Fixed infinite loop

* Moved rules to data file

* Moved item rewards to data file

* Generating from data file

* Fixed broken Mech Boss goal

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

* Added Deerclops, fixed Zenith goal

* Final detailed vanilla pass

* Disable calamity goals

* Typo

* Fixed some reward items not adding to item pool

* In-progress unit test fixes

* Unit test fixes

* `.apworld` compat

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

* Water Walking Boots and Titan Glove rewards

* Add goals to slot data

* Fixed Hammush logic in Post-Mech goal

* Fixed coin rewards

* Updated Terraria docs

* Formatted

* Deathlink in-progress

* Boots of the Hero is grindy

* Fixed zenith goal not placing an item

* Address review

* Gelatin World Tour is grindy

* Difficulty notice

* Switched some achievements' grindiness

* Added "Hey! Listen!" achievement

* Terarria Python 3.8 compat

* Fixed Terraria You and What Army logic

* Calamity minion accessories

* Typo

* Calamity integration

* `deathlink` -> `death_link`

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

* Missing `.`

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

* Incorrect type annotation

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

* `deathlink` -> `death_link` 2

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

* Style

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

* Markdown style

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

* Markdown style 2

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

* Address review

* Fix bad merge

* Terraria utility mod recommendations

* Calamity minion armor logic

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

* Fixed unplaced item

* Started on Terraria 1.4.4

* Crate logic

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

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

* Calamity fixes

* Calamity crate ore logic

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

* Early achievements, separate achievement category options

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

* The Frequent Flyer is impossible in Calamity getfixedboi

* Add Enchanted Sword and Starfury for starting inventories

* Don't Dread on Me is redundant in Calamity

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

* Can't use Gelatin Crystal outside Hallow

* You can't get the Terminus without flags

* Typo

* Options difficult warnings

* Robbing the Grave is Hardmode

* Don't reserve an ID for unused Victory item

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

* Unshuffled Life Crystal and Defender Medal items

* Comment about Midas' Blessing

* Update worlds/terraria/Options.py

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

* Remove stray expression

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

* Review suggestions

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

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

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

* Fix Acid Rain logic

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

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

* Mecha Mayhem is inaccessible in getfixedboi

* Update worlds/terraria/Rules.dsv

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

---------

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

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

* fix ut test

* self review

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

* my god can the tests plz pass

* code reviews

* code reviews

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

* Respect non_local_items for PC Item

* prefer exclude if also in priority locations

---------

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

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

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

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

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

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

* Fix comment typo

"be" was missing.

---------

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

* use get

* Update LinksAwakeningClient.py

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

* Update LinksAwakeningClient.py

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

---------

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

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

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

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

* add coops to content packs

* add building progression in game features

* add shippping bin to starting building; remove has_house

* replace config check with feature

* add other buildings in content packs

* not passing

* tests passes, unbelievable

* use newly create methods more

* use new assets to ease readability

* self review

* fix flake8 maybe

* properly split rule for mapping cave systems

* fix tractor garage name

* self review

* add upgrade_from to farm house buldings

* don't override building name variable in logic

* remove has_group from buildings

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

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

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

* disable shop source for mapping cave systems

* bunch of code review changes

* add petbowl and farmhouse to autobuilding

* set min easy items to 300

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

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

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

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

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

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

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

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

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

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

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

* Verbose af

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

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

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

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

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

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

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

* Clarify Panel Hunt

* Unnecessary line break

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

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

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

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

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

* spoiler-only exceptions

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

* Update EntranceLookup unit tests

* Add new dead-end test

* Add additional explanation to the new test

* minor formatting tweak

based on review feedback

---------

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

* Update worlds/dlcquest/Items.py

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

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

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

* DLC Quest Bug Fix
overcook failed test

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

---------

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

* Minor fixes.

* Update docs/adding games.md

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

* Code review updates.

* More updates.

* Client icon blurb.

* Update docs/adding games.md

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

* Revert one line.

* Filler item name blurb.

* Updates for Violet.

* Reorganize client expectations.

* Missed a line delete.

* Doctor's orders

---------

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

* Update worlds/tunic/docs/en_TUNIC.md

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

* Turn it into a bulleted list
2025-04-01 19:55:19 -04:00
Sanjay Govind
daee6d210f CommonClient: don't update ui hints if there is no ui (#4791) 2025-04-02 01:54:27 +02:00
Bryce Wilson
96be0071e6 Pokemon Emerald: Move recent change to new version (#4793) 2025-04-02 00:50:39 +02:00
threeandthreee
ff8e1dfb47 Launcher: Remove an unnecessary global (#4785) 2025-04-01 21:28:59 +02:00
LiquidCat64
d26db6f213 CV64: Fix some unrandomized locations containing unintended items on specific settings (#4728)
* Fix some unrandomized locations on specific settings.

* Remove now-unnecessary comment
2025-04-01 12:37:49 -04:00
Fabian Dill
bb6c753583 FFMQ: fix remote code execution (#4786) 2025-04-01 18:19:07 +02:00
Mysteryem
ca08e4b950 Super Metroid: Replace random module with world random in variaRandomizer (#4429) 2025-04-01 18:14:47 +02:00
Bryce Wilson
5a6b02dbd3 Pokemon Emerald: Fix pre-fill problems (#4686)
Co-authored-by: Mysteryem <Mysteryem@users.noreply.github.com>
2025-04-01 18:12:43 +02:00
jamesbrq
14416b1050 MLSS: Fix issue with door opening earlier than intended (#4737) 2025-04-01 18:10:51 +02:00
Carter Hesterman
da4e6fc532 Civ6: Sanitize player/item values before they go in the XML (#4755) 2025-04-01 18:09:59 +02:00
Justus Lind
57d8b69a6d Muse Dash: Update Song List to Muse Dash Legend. (#4775)
* Add Muse Dash Legend songs.

* Add a new SFX trap
2025-04-01 18:08:09 +02:00
Silvris
c9d8a8661c kvui: Fix hint tab formatting regression (#4778)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2025-04-01 18:06:49 +02:00
Fabian Dill
4a3d23e0e6 Core: update cx-Freeze to 8.0.0 & Worlds: fix packages missing __init__.py (#4773) 2025-04-01 16:29:32 +02:00
PoryGone
a3666f2ae5 SA2B: Fix critical typo #4779 2025-03-30 22:19:24 +02:00
Kaito Sinclaire
c3e000e574 id Tech 1 games: Logic updates (Feb '25) (#4677)
- Across Doom 1993 and Doom 2, any items that are accessible in Ultra-Violence from the start of the level without putting the player in any danger are now considered in logic when that level is first received, without needing any weapons available. This is intended to give generation more possible outs for bad placements.
  - This affects the following maps in Doom 1993:
    - Toxin Refinery (E1M3): 1 location.
    - Command Control (E1M4): 1 location.
    - Computer Station (E1M7): 1 location.
    - Deimos Lab (E2M4): 1 location.
    - Tower of Babel (E2M8): 1 location.
    - Unholy Cathedral (E3M5): 1 location.
  - This affects the following maps in Doom 2:
    - The Waste Tunnels (MAP05): 2 locations.
    - Dead Simple (MAP07): 2 locations.
    - The Pit (MAP09): 1 location.
    - Refueling Base (MAP10): 1 location.
    - Nirvana (MAP21): 1 location, except see below.
    - Icon of Sin (MAP30): 9 locations.
    - Grosse (MAP32): 2 locations.
- Doom 2 has had some more significant logical adjustments made.
  - The following Pro tricks have been added to Pro logic:
    - Circle of Death (MAP11): Lowering the exit wall without the Red key by hitting the switch to do so from the nukage. This makes three items previously locked behind the Red key available early, as well as the exit.
    - Suburbs (MAP16): Reaching the exit without any keys, as the gap between the pillar and the wall is large enough to let you through if you position yourself well. While multiple other squeeze glides exist (for example, you can skip the Yellow key in MAP21 by using one), this one is significantly easier than the rest; it does not require much precision, nor does it require vertical mouse movement.
    - Nirvana (MAP21): Skipping the Blue key, as there is a gigantic gap between the bars that attempt to block you.
    - The Chasm (MAP24): Skipping the Blue key by going extremely far through the nukage and finding one of a couple specific teleporters is now considered a Pro trick, and standard logic now expects the key to be obtained.
  - The following levels have had other logic adjustments:
    - The Waste Tunnels (MAP05): Requirements lowered to Shotgun + Super Shotgun + (Chaingun | Plasma gun).
    - The Crusher (MAP06): Requirements lowered to Shotgun + (Chaingun | Plasma gun) for areas immediately accessible. Going beyond the Blue key door also requires Super Shotgun.
    - The Factory (MAP12): The outdoors area, and the little room to the right of where you start, are accessible in sphere 1. These three items are all easily obtainable with only the pistol. The remaining items that are not in the central area are accessible with (Super Shotgun | Plasma gun), while the items in that area are accessible with Super Shotgun + Chaingun + (Plasma gun | BFG9000). This fixes Episode 2 not having an available sphere 1, and allows solo Episode 2 games.
    - Nirvana (MAP21): As above, the item in the starting room is accessible in sphere 1. Every other item that doesn't require a key is accessible with (Super Shotgun | Plasma gun). The room in which you use the Yellow key is accessible with Super Shotgun + Chaingun + (Plasma gun | BFG9000). This fixes Episode 3 not having an available sphere 1, and allows solo Episode 3 games.
    - The Catacombs (MAP22): The four items in the opening room only require (Shotgun | Super Shotgun | Plasma gun). The rest of the level is as before.
    - Bloodfalls (MAP25): Requirements lowered to Shotgun + Super Shotgun + Chaingun, as this level is unusually easy for its placement in the game. Progressing past the Blue key door additionally requires (Rocket launcher | Plasma gun | BFG9000) solely to deal with the Arch-vile at the end of the level.
    - Wolfenstein (MAP31): Requirements lowered to Chaingun + (Shotgun | Super Shotgun). This is closer to what the game expects from a non-secret hunting player from a pistol start.
- The following logic bugs in Heretic have been fixed:
  - Quay (E5M3): An item in a Blue key locked hallway was previously marked as being in the "Main" region, thus considered to be accessible without that key. It has been moved to the appropriate "Blue" region.
  - Courtyard (E5M4): Logic previously assumed you could reach the Wings of Wrath from the opening room, when that isn't actually possible. Changing this moved some items previously in the "Main" region into a new "Green" region, and items previously in the "Kakis" (Yellow OR Green) are now in a "Yellow" region instead. Fixes #4662.
- For known problematic solo episodes, some additional special cases have been added.
  - Doom 1993, Episode 3: One of either the Shotgun or Chaingun is placed early. Slough of Despair (E3M2) is given as an additional starting level.
  - Doom 2, Episode 3: One of either the Super Shotgun or Plasma gun is placed early.
  - Heretic, Episode 1: The Docks (E1M1) - Yellow key is placed early.
- The following levels (and thus, their items and locations) were renamed, due to typos or other oddities:
  - `Barrels o Fun (MAP23)` -> `Barrels o' Fun (MAP23)`
  - `Wolfenstein2 (MAP31)` -> `Wolfenstein (MAP31)`
  - `Grosse2 (MAP32)` -> `Grosse (MAP32)`
  - `D'Sparil'S Keep (E3M8)` -> `D'Sparil's Keep (E3M8)`
  - `The Aquifier (E3M9)` -> `The Aquifer (E3M9)`
2025-03-29 17:32:33 +01:00
Justus Lind
dd5481930a Muse Dash: Update docs to recommend MelonLoader 0.7.0 rather than 0.6.1 (#4776)
* Tiny version update.

* Update wording because there is no longer a latest button
2025-03-29 01:35:35 +01:00
Scipio Wright
842328c661 TUNIC: Update swamp and atoll fuse logic with weaponry (#4760)
* Update swamp and atoll fuse logic with weaponry

* Add it to the swamp and cath rules too
2025-03-28 21:12:16 +01:00
PoryGone
8f75384e2e SA2B - v2.4 Logic Fixes (#4770)
* Logic tweaks

* Docs updates

* Delete extra file

* One more logic tweak

* Add missing logic change
2025-03-28 21:11:31 +01:00
Fabian Dill
193faa00ce Factorio: fix energylink type back to int (#4768) 2025-03-28 00:28:10 +01:00
Star Rauchenberger
5e5383b399 Lingo: Add painting display names (#4707)
* Lingo: Add painting display names

* Reordered some paintings

* Update generated.dat
2025-03-27 01:32:39 +01:00
threeandthreee
cb6b29dbe3 LADX: fix for unconnected entrances in other worlds #4771 2025-03-25 22:30:25 +01:00
Fabian Dill
82b0819051 Core: ensure requirements files end on newline (#4761) 2025-03-24 22:26:30 +01:00
Jérémie Bolduc
e12ab4afa4 Stardew Valley: Move test option presets to their own file (#4349) 2025-03-24 03:32:34 +01:00
Justus Lind
1416f631cc Core: Add a test that checks all registered patches matches the name of a registered world (#4633)
Co-authored-by: qwint <qwint.42@gmail.com>
2025-03-24 03:30:44 +01:00
Fabian Dill
dbaac47d1e Core: update various requirements (#4731) 2025-03-23 17:24:50 +01:00
Jonathan Tan
cf0ae5e31b The Wind Waker: Implement New Game (#4458)
Adds The Legend of Zelda: The Wind Waker as a supported game in Archipelago. The game uses [LagoLunatic's randomizer](https://github.com/LagoLunatic/wwrando) as its base (regarding logic, options, etc.) and builds from there.
2025-03-23 00:42:17 +01:00
BadMagic100
8891f07362 Core: Allow and require user-provided target name when splitting 1-way entrances for GER (#4746)
* [Core][GER] Allow and require user-provided target name when splitting 1-way entrances

* Move target naming onto a parameter of disconnect_entrance_for_randomization
2025-03-22 20:58:35 +01:00
NewSoupVi
d78974ec59 The Witness: Bump Required Client Version to 0.6.0 (#4763)
The beta client releases already report this.
2025-03-22 20:57:22 +01:00
NewSoupVi
32be26c4d7 The Witness: Make sure the 2025 April Fools feature does not go live with RC3 (#4758) 2025-03-22 20:52:18 +01:00
Jérémie Bolduc
9de49aa419 Stardew Valley: Move all the goal logic into its own file (#4383) 2025-03-22 20:29:16 +01:00
PoryGone
294a67a4b4 SA2B: v2.4 - Minigame Madness (#4663)
Changelog:

Features:
- New Goal
  - Minigame Madness
    - Win a certain number of each type of Minigame Trap, then defeat the Finalhazard to win!
	- How many of each Minigame are required can be set by an Option
	- When the required amount of a Minigame has been received, that Minigame can be replayed in the Chao World Lobby
- New optional Location Checks
  - Bigsanity
    - Go fishing with Big in each stage for a Location Check
  - Itemboxsanity
    - Either Extra Life Boxes or All Item Boxes
- New Items
  - New Traps
    - Literature Trap
	- Controller Drift Trap
	- Poison Trap
	- Bee Trap
  - New Minigame Traps
    - Breakout Trap
	- Fishing Trap
	- Trivia Trap
	- Pokemon Trivia Trap
	- Pokemon Count Trap
	- Number Sequence Trap
	- Light Up Path Trap
	- Pinball Trap
	- Math Quiz Trap
	- Snake Trap
	- Input Sequence Trap
- Trap Link
  - When you receive a trap, you send a copy of it to every other player with Trap Link enabled
- Boss Gate Plando
- Expert Logic Difficulty
	- Use at your own risk. This difficulty requires complete mastery of SA2.
- Missions can now be enabled and disabled per-character, instead of just per-style
- Minigame Difficulty can now be set to "Chaos", which selects a new difficulty randomly per-trap received

Quality of Life:
- Gate Stages and Mission Orders are now displayed in the spoiler log
- Additional play stats are saved and displayed with the randomizer credits
- Stage Locations progress UI now displays in multiple pages when Itemboxsanity is enabled
- Current stage mission order and progress are now shown when paused in-level
- Chaos Emeralds are now shown when paused in-level
- Location Name Groups were created
- Moved SA2B to the new Options system
- Option Presets were created
- Error Messages are more obvious

Bug Fixes:
- Added missing `Dry Lagoon - 12 Animals` location
- Flying Dog boss should no longer crash when you have done at least 3 Intermediate Kart Races
- Invincibility can no longer be received in the King Boom Boo fight, preventing a crash
- Chaos Emeralds should no longer disproportionately end up in Cannon's Core or the final Level Gate
- Going into submenus from the pause menu should no longer reset traps
- `Sonic - Magic Gloves` are now plural
- Junk items will no longer cause a crash when in a falling state
- Chao Garden:
	- Prevent races from occasionally becoming uncompletable when using the "Prize Only" option
	- Properly allow Hero Chao to participate in Dark Races
	- Don't allow the Chao Garden to send locations when connected to an invalid server
	- Prevent the Chao Garden from resetting your life count
	- Fix Chao World Entrance Shuffle causing inaccessible Neutral Garden
	- Fix pressing the 'B' button to take you to the proper location in Chao World Entrance Shuffle
	- Prevent Chao Karate progress icon overflow
	- Prevent changing Chao Timescale while paused or while a Minigame is active
- Logic Fixes:
	- `Mission Street - Chao Key 1` (Hard Logic) now requires no upgrades
	- `Mission Street - Chao Key 2` (Hard Logic) now requires no upgrades
	- `Crazy Gadget - Hidden 1` (Standard Logic) now requires `Sonic - Bounce Bracelet` instead of `Sonic - Light Shoes`
	- `Lost Colony - Hidden 1` (Standard Logic) now requires `Eggman - Jet Engine`
	- `Mad Space - Gold Beetle` (Standard Logic) now only requires `Rouge - Iron Boots`
	- `Cosmic Wall - Gold Beetle` (Standard and Hard Logic) now only requires `Eggman - Jet Engine`
2025-03-22 13:00:07 +01:00
277 changed files with 20764 additions and 5333 deletions

View File

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

View File

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

View File

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

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@
*.apmc
*.apz5
*.aptloz
*.aptww
*.apemerald
*.pyc
*.pyd

View File

@@ -616,7 +616,7 @@ class MultiWorld():
locations: Set[Location] = set()
events: Set[Location] = set()
for location in self.get_filled_locations():
if type(location.item.code) is int:
if type(location.item.code) is int and type(location.address) is int:
locations.add(location)
else:
events.add(location)
@@ -1106,6 +1106,9 @@ class Region:
def __len__(self) -> int:
return self._list.__len__()
def __iter__(self):
return iter(self._list)
# This seems to not be needed, but that's a bit suspicious.
# def __del__(self):
# self.clear()
@@ -1310,9 +1313,6 @@ class Location:
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
def __hash__(self):
return hash((self.name, self.player))
def __lt__(self, other: Location):
return (self.player, self.name) < (other.player, other.name)
@@ -1416,6 +1416,10 @@ class Item:
def flags(self) -> int:
return self.classification.as_flag()
@property
def is_event(self) -> bool:
return self.code is None
def __eq__(self, other: object) -> bool:
if not isinstance(other, Item):
return NotImplemented

View File

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

21
Fill.py
View File

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

View File

@@ -54,12 +54,22 @@ def mystery_argparse():
parser.add_argument("--skip_output", action="store_true",
help="Skips generation assertion and output stages and skips multidata and spoiler output. "
"Intended for debugging and testing purposes.")
parser.add_argument("--spoiler_only", action="store_true",
help="Skips generation assertion and multidata, outputting only a spoiler log. "
"Intended for debugging and testing purposes.")
args = parser.parse_args()
if args.skip_output and args.spoiler_only:
parser.error("Cannot mix --skip_output and --spoiler_only")
elif args.spoiler == 0 and args.spoiler_only:
parser.error("Cannot use --spoiler_only when --spoiler=0. Use --skip_output or set --spoiler to a different value")
if not os.path.isabs(args.weights_file_path):
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
if not os.path.isabs(args.meta_file_path):
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
return args
@@ -108,6 +118,8 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
raise Exception("Cannot mix --sameoptions with --meta")
else:
meta_weights = None
player_id = 1
player_files = {}
for file in os.scandir(args.player_files_path):
@@ -164,6 +176,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
erargs.outputpath = args.outputpath
erargs.skip_prog_balancing = args.skip_prog_balancing
erargs.skip_output = args.skip_output
erargs.spoiler_only = args.spoiler_only
erargs.name = {}
erargs.csv_output = args.csv_output
@@ -279,22 +292,30 @@ def get_choice(option, root, value=None) -> Any:
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
class SafeDict(dict):
def __missing__(self, key):
return '{' + key + '}'
class SafeFormatter(string.Formatter):
def get_value(self, key, args, kwargs):
if isinstance(key, int):
if key < len(args):
return args[key]
else:
return "{" + str(key) + "}"
else:
return kwargs.get(key, "{" + key + "}")
def handle_name(name: str, player: int, name_counter: Counter):
name_counter[name.lower()] += 1
number = name_counter[name.lower()]
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=number,
NUMBER=(number if number > 1 else ''),
player=player,
PLAYER=(player if player > 1 else '')))
new_name = SafeFormatter().vformat(new_name, (), {"number": number,
"NUMBER": (number if number > 1 else ''),
"player": player,
"PLAYER": (player if player > 1 else '')})
# Run .strip twice for edge case where after the initial .slice new_name has a leading whitespace.
# Could cause issues for some clients that cannot handle the additional whitespace.
new_name = new_name.strip()[:16].strip()
if new_name == "Archipelago":
raise Exception(f"You cannot name yourself \"{new_name}\"")
return new_name

View File

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

View File

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

12
Main.py
View File

@@ -81,7 +81,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
del item_digits, location_digits, item_count, location_count
# This assertion method should not be necessary to run if we are not outputting any multidata.
if not args.skip_output:
if not args.skip_output and not args.spoiler_only:
AutoWorld.call_stage(multiworld, "assert_generate")
AutoWorld.call_all(multiworld, "generate_early")
@@ -224,6 +224,15 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.info(f'Beginning output...')
outfilebase = 'AP_' + multiworld.seed_name
if args.spoiler_only:
if args.spoiler > 1:
logger.info('Calculating playthrough.')
multiworld.spoiler.create_playthrough(create_paths=args.spoiler > 2)
multiworld.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
logger.info('Done. Skipped multidata modification. Total time: %s', time.perf_counter() - start)
return multiworld
output = tempfile.TemporaryDirectory()
with output as temp_dir:
output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__
@@ -306,6 +315,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
game_world.game: worlds.network_data_package["games"][game_world.game]
for game_world in multiworld.worlds.values()
}
data_package["Archipelago"] = worlds.network_data_package["games"]["Archipelago"]
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}

View File

@@ -66,9 +66,13 @@ def pop_from_container(container, value):
return container
def update_dict(dictionary, entries):
dictionary.update(entries)
return dictionary
def update_container_unique(container, entries):
if isinstance(container, list):
existing_container_as_set = set(container)
container.extend([entry for entry in entries if entry not in existing_container_as_set])
else:
container.update(entries)
return container
def queue_gc():
@@ -109,7 +113,7 @@ modify_functions = {
# lists/dicts:
"remove": remove_from_list,
"pop": pop_from_container,
"update": update_dict,
"update": update_container_unique,
}
@@ -277,25 +281,6 @@ class Context:
lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))
self.non_hintable_names = collections.defaultdict(frozenset)
self._load_game_data()
# Data package retrieval
def _load_game_data(self):
import worlds
self.gamespackage = worlds.network_data_package["games"]
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()}
self.location_name_groups = {world_name: world.location_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()}
for world_name, world in worlds.AutoWorldRegister.world_types.items():
self.non_hintable_names[world_name] = world.hint_blacklist
for game_package in self.gamespackage.values():
# remove groups from data sent to clients
del game_package["item_name_groups"]
del game_package["location_name_groups"]
def _init_game_data(self):
for game_name, game_package in self.gamespackage.items():
if "checksum" in game_package:
@@ -2037,7 +2022,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
value = func(value, operation["value"])
ctx.stored_data[args["key"]] = args["value"] = value
targets = set(ctx.stored_data_notification_clients[args["key"]])
if args.get("want_reply", True):
if args.get("want_reply", False):
targets.add(client)
if targets:
ctx.broadcast(targets, [args])

View File

@@ -81,6 +81,7 @@ Currently, the following games are supported:
* Castlevania: Circle of the Moon
* Inscryption
* Civilization VI
* The Legend of Zelda: The Wind Waker
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

View File

@@ -47,7 +47,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self)
__version__ = "0.6.0"
__version__ = "0.6.2"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ from typing import Dict, Union
from docutils.core import publish_parts
import yaml
from flask import redirect, render_template, request, Response
from flask import redirect, render_template, request, Response, abort
import Options
from Utils import local_path
@@ -142,7 +142,10 @@ def weighted_options_old():
@app.route("/games/<string:game>/weighted-options")
@cache.cached()
def weighted_options(game: str):
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
try:
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
except KeyError:
return abort(404)
@app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"])
@@ -197,7 +200,10 @@ def generate_weighted_yaml(game: str):
@app.route("/games/<string:game>/player-options")
@cache.cached()
def player_options(game: str):
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
try:
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
except KeyError:
return abort(404)
# YAML generator for player-options

View File

@@ -1,11 +1,12 @@
flask>=3.0.3
werkzeug>=3.0.6
flask>=3.1.0
werkzeug>=3.1.3
pony>=0.7.19
waitress>=3.0.0
waitress>=3.0.2
Flask-Caching>=2.3.0
Flask-Compress>=1.15
Flask-Limiter>=3.8.0
bokeh>=3.5.2
markupsafe>=2.1.5
Flask-Compress>=1.17
Flask-Limiter>=3.12
bokeh>=3.6.3
markupsafe>=3.0.2
Markdown>=3.7
mdx-breakless-lists>=1.0.1
setproctitle>=1.3.5

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,23 +14,51 @@
salmon: "FA8072" # typically trap item
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
orange: "FF7700" # Used for command echo
<Label>:
color: "FFFFFF"
<TabbedPanel>:
tab_width: root.width / app.tab_count
# KivyMD theming parameters
theme_style: "Dark" # Light/Dark
primary_palette: "Green" # Many options
dynamic_scheme_name: "TONAL_SPOT"
dynamic_scheme_contrast: 0.0
<MDLabel>:
color: self.theme_cls.primaryColor
<TooltipLabel>:
text_size: self.width, None
size_hint_y: None
height: self.texture_size[1]
adaptive_height: True
font_size: dp(20)
markup: True
halign: "left"
<SelectableLabel>:
size_hint: 1, None
canvas.before:
Color:
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1)
rgba: (.0, 0.9, .1, .3) if self.selected else self.theme_cls.surfaceContainerLowColor
Rectangle:
size: self.size
pos: self.pos
<MarkupDropdownItem>
orientation: "vertical"
MDLabel:
text: root.text
valign: "center"
padding_x: "12dp"
shorten: True
shorten_from: "right"
theme_text_color: "Custom"
markup: True
text_color:
app.theme_cls.onSurfaceVariantColor \
if not root.text_color else \
root.text_color
MDDivider:
md_bg_color:
( \
app.theme_cls.outlineVariantColor \
if not root.divider_color \
else root.divider_color \
) \
if root.divider else \
(0, 0, 0, 0)
<UILog>:
messages: 1000 # amount of messages stored in client logs.
cols: 1
@@ -49,7 +77,7 @@
<HintLabel>:
canvas.before:
Color:
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1) if self.striped else (0.18, 0.18, 0.18, 1)
rgba: (.0, 0.9, .1, .3) if self.selected else self.theme_cls.surfaceContainerHighColor if self.striped else self.theme_cls.surfaceContainerLowColor
Rectangle:
size: self.size
pos: self.pos
@@ -152,3 +180,16 @@
height: dp(30)
multiline: False
write_tab: False
<ScrollBox>:
layout: layout
bar_width: "12dp"
scroll_wheel_distance: 40
do_scroll_x: False
scroll_type: ['bars', 'content']
MDBoxLayout:
id: layout
orientation: "vertical"
spacing: 10
size_hint_y: None
height: self.minimum_height

142
data/launcher.kv Normal file
View File

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

View File

@@ -214,6 +214,9 @@
# Wargroove
/worlds/wargroove/ @FlySniper
# The Wind Waker
/worlds/tww/ @tanjo3
# The Witness
/worlds/witness/ @NewSoupVi @blastron

View File

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

View File

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

View File

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

View File

@@ -606,8 +606,8 @@ from .items import get_item_type
def set_rules(self) -> None:
# For some worlds this step can be omitted if either a Logic mixin
# (see below) is used, it's easier to apply the rules from data during
# location generation or everything is in generate_basic
# (see below) is used or it's easier to apply the rules from data during
# location generation
# set a simple rule for an region
set_rule(self.multiworld.get_entrance("Boss Door", self.player),

View File

@@ -50,13 +50,15 @@ class EntranceLookup:
_random: random.Random
_expands_graph_cache: dict[Entrance, bool]
_coupled: bool
_usable_exits: set[Entrance]
def __init__(self, rng: random.Random, coupled: bool):
def __init__(self, rng: random.Random, coupled: bool, usable_exits: set[Entrance]):
self.dead_ends = EntranceLookup.GroupLookup()
self.others = EntranceLookup.GroupLookup()
self._random = rng
self._expands_graph_cache = {}
self._coupled = coupled
self._usable_exits = usable_exits
def _can_expand_graph(self, entrance: Entrance) -> bool:
"""
@@ -95,7 +97,8 @@ class EntranceLookup:
# randomizable exits which are not reverse of the incoming entrance.
# uncoupled mode is an exception because in this case going back in the door you just came in could
# actually lead somewhere new
if not exit_.connected_region and (not self._coupled or exit_.name != entrance.name):
if (not exit_.connected_region and (not self._coupled or exit_.name != entrance.name)
and exit_ in self._usable_exits):
self._expands_graph_cache[entrance] = True
return True
elif exit_.connected_region and exit_.connected_region not in visited:
@@ -265,14 +268,19 @@ def bake_target_group_lookup(world: World, get_target_groups: Callable[[int], li
return { group: get_target_groups(group) for group in unique_groups }
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None) -> None:
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None,
one_way_target_name: str | None = None) -> None:
"""
Given an entrance in a "vanilla" region graph, splits that entrance to prepare it for randomization
in randomize_entrances. This should be done after setting the type and group of the entrance.
in randomize_entrances. This should be done after setting the type and group of the entrance. Because it attempts
to meet strict entrance naming requirements for coupled mode, this function may produce unintuitive results when
called only on a single entrance; it produces eventually-correct outputs only after calling it on all entrances.
:param entrance: The entrance which will be disconnected in preparation for randomization.
:param target_group: The group to assign to the created ER target. If not specified, the group from
the original entrance will be copied.
:param one_way_target_name: The name of the created ER target if `entrance` is one-way. This argument
is required for one-way entrances and is ignored otherwise.
"""
child_region = entrance.connected_region
parent_region = entrance.parent_region
@@ -287,8 +295,11 @@ def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int
# targets in the child region will be created when the other direction edge is disconnected
target = parent_region.create_er_target(entrance.name)
else:
# for 1-ways, the child region needs a target and coupling/naming is not a concern
target = child_region.create_er_target(child_region.name)
# for 1-ways, the child region needs a target. naming is not a concern for coupling so we
# allow it to be user provided (and require it, to prevent an unhelpful assumed name in pairings)
if not one_way_target_name:
raise ValueError("Cannot disconnect a one-way entrance without a target name specified")
target = child_region.create_er_target(one_way_target_name)
target.randomization_type = entrance.randomization_type
target.randomization_group = target_group or entrance.randomization_group
@@ -325,7 +336,6 @@ def randomize_entrances(
start_time = time.perf_counter()
er_state = ERPlacementState(world, coupled)
entrance_lookup = EntranceLookup(world.random, coupled)
# similar to fill, skip validity checks on entrances if the game is beatable on minimal accessibility
perform_validity_check = True
@@ -341,6 +351,7 @@ def randomize_entrances(
# used when membership checks are needed on the exit list, e.g. speculative sweep
exits_set = set(exits)
entrance_lookup = EntranceLookup(world.random, coupled, exits_set)
for entrance in er_targets:
entrance_lookup.add(entrance)

475
kvui.py
View File

@@ -35,8 +35,7 @@ from kivy.config import Config
Config.set("input", "mouse", "mouse,disable_multitouch")
Config.set("kivy", "exit_on_escape", "0")
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
from kivy.app import App
from kivymd.uix.divider import MDDivider
from kivy.core.window import Window
from kivy.core.clipboard import Clipboard
from kivy.core.text.markup import MarkupLabel
@@ -46,30 +45,32 @@ from kivy.clock import Clock
from kivy.factory import Factory
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
from kivy.metrics import dp
from kivy.effects.scroll import ScrollEffect
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.layout import Layout
from kivy.uix.textinput import TextInput
from kivy.uix.scrollview import ScrollView
from kivy.uix.recycleview import RecycleView
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.uix.progressbar import ProgressBar
from kivy.uix.dropdown import DropDown
from kivy.utils import escape_markup
from kivy.lang import Builder
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.behaviors import FocusBehavior, ToggleButtonBehavior
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.animation import Animation
from kivy.uix.popup import Popup
from kivy.uix.dropdown import DropDown
from kivy.uix.image import AsyncImage
from kivymd.app import MDApp
from kivymd.uix.gridlayout import MDGridLayout
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.tab.tab import MDTabsPrimary, MDTabsItem, MDTabsItemText, MDTabsCarousel
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.menu.menu import MDDropdownTextItem
from kivymd.uix.dropdownitem import MDDropDownItem, MDDropDownItemText
from kivymd.uix.button import MDButton, MDButtonText, MDButtonIcon, MDIconButton
from kivymd.uix.label import MDLabel, MDIcon
from kivymd.uix.recycleview import MDRecycleView
from kivymd.uix.textfield.textfield import MDTextField
from kivymd.uix.progressindicator import MDLinearProgressIndicator
from kivymd.uix.scrollview import MDScrollView
from kivymd.uix.tooltip import MDTooltip, MDTooltipPlain
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
@@ -86,6 +87,85 @@ else:
remove_between_brackets = re.compile(r"\[.*?]")
class ThemedApp(MDApp):
def set_colors(self):
text_colors = KivyJSONtoTextParser.TextColors()
self.theme_cls.theme_style = getattr(text_colors, "theme_style", "Dark")
self.theme_cls.primary_palette = getattr(text_colors, "primary_palette", "Green")
self.theme_cls.dynamic_scheme_name = getattr(text_colors, "dynamic_scheme_name", "TONAL_SPOT")
self.theme_cls.dynamic_scheme_contrast = getattr(text_colors, "dynamic_scheme_contrast", 0.0)
class ImageIcon(MDButtonIcon, AsyncImage):
def __init__(self, *args, **kwargs):
super().__init__(args, kwargs)
self.image = ApAsyncImage(**kwargs)
self.add_widget(self.image)
def add_widget(self, widget, index=0, canvas=None):
return super(MDIcon, self).add_widget(widget)
class ImageButton(MDIconButton):
def __init__(self, **kwargs):
image_args = dict()
for kwarg in ("fit_mode", "image_size", "color", "source", "texture"):
val = kwargs.pop(kwarg, "None")
if val != "None":
image_args[kwarg.replace("image_", "")] = val
super().__init__()
self.image = ApAsyncImage(**image_args)
def set_center(button, center):
self.image.center_x = self.center_x
self.image.center_y = self.center_y
self.bind(center=set_center)
self.add_widget(self.image)
def add_widget(self, widget, index=0, canvas=None):
return super(MDIcon, self).add_widget(widget)
class ScrollBox(MDScrollView):
layout: MDBoxLayout = ObjectProperty(None)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# thanks kivymd
class ToggleButton(MDButton, ToggleButtonBehavior):
def __init__(self, *args, **kwargs):
super(ToggleButton, self).__init__(*args, **kwargs)
self.bind(state=self._update_bg)
def _update_bg(self, _, state: str):
if self.disabled:
return
if self.theme_bg_color == "Primary":
self.theme_bg_color = "Custom"
if state == "down":
self.md_bg_color = self.theme_cls.primaryColor
for child in self.children:
if child.theme_text_color == "Primary":
child.theme_text_color = "Custom"
if child.theme_icon_color == "Primary":
child.theme_icon_color = "Custom"
child.text_color = self.theme_cls.onPrimaryColor
child.icon_color = self.theme_cls.onPrimaryColor
else:
self.md_bg_color = self.theme_cls.surfaceContainerLowestColor
for child in self.children:
if child.theme_text_color == "Primary":
child.theme_text_color = "Custom"
if child.theme_icon_color == "Primary":
child.theme_icon_color = "Custom"
child.text_color = self.theme_cls.primaryColor
child.icon_color = self.theme_cls.primaryColor
# I was surprised to find this didn't already exist in kivy :(
class HoverBehavior(object):
"""originally from https://stackoverflow.com/a/605348110"""
@@ -125,7 +205,7 @@ class HoverBehavior(object):
Factory.register("HoverBehavior", HoverBehavior)
class ToolTip(Label):
class ToolTip(MDTooltipPlain):
pass
@@ -133,49 +213,30 @@ class ServerToolTip(ToolTip):
pass
class ScrollBox(ScrollView):
layout: BoxLayout
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.layout = BoxLayout(size_hint_y=None)
self.layout.bind(minimum_height=self.layout.setter("height"))
self.add_widget(self.layout)
self.effect_cls = ScrollEffect
self.bar_width = dp(12)
self.scroll_type = ["content", "bars"]
class HovererableLabel(HoverBehavior, Label):
class HovererableLabel(HoverBehavior, MDLabel):
pass
class TooltipLabel(HovererableLabel):
tooltip = None
class TooltipLabel(HovererableLabel, MDTooltip):
tooltip_display_delay = 0.1
def create_tooltip(self, text, x, y):
text = text.replace("<br>", "\n").replace("&amp;", "&").replace("&bl;", "[").replace("&br;", "]")
if self.tooltip:
# update
self.tooltip.children[0].text = text
else:
self.tooltip = FloatLayout()
tooltip_label = ToolTip(text=text)
self.tooltip.add_widget(tooltip_label)
fade_in_animation.start(self.tooltip)
App.get_running_app().root.add_widget(self.tooltip)
# handle left-side boundary to not render off-screen
x = max(x, 3 + self.tooltip.children[0].texture_size[0] / 2)
# position float layout
self.tooltip.x = x - self.tooltip.width / 2
self.tooltip.y = y - self.tooltip.height / 2 + 48
center_x, center_y = self.to_window(self.center_x, self.center_y)
self.shift_y = y - center_y
shift_x = center_x - x
if shift_x > 0:
self.shift_left = shift_x
else:
self.shift_right = shift_x
def remove_tooltip(self):
if self.tooltip:
App.get_running_app().root.remove_widget(self.tooltip)
self.tooltip = None
if self._tooltip:
# update
self._tooltip.text = text
else:
self._tooltip = ToolTip(text=text, pos_hint={})
self.display_tooltip()
def on_mouse_pos(self, window, pos):
if not self.get_root_window():
@@ -202,26 +263,26 @@ class TooltipLabel(HovererableLabel):
def on_leave(self):
self.remove_tooltip()
self._tooltip = None
class ServerLabel(HovererableLabel):
class ServerLabel(HovererableLabel, MDTooltip):
tooltip_display_delay = 0.1
def __init__(self, *args, **kwargs):
super(HovererableLabel, self).__init__(*args, **kwargs)
self.layout = FloatLayout()
self.popuplabel = ServerToolTip(text="Test")
self.layout.add_widget(self.popuplabel)
self._tooltip = ServerToolTip(text="Test")
def on_enter(self):
self.popuplabel.text = self.get_text()
App.get_running_app().root.add_widget(self.layout)
fade_in_animation.start(self.layout)
self._tooltip.text = self.get_text()
self.display_tooltip()
def on_leave(self):
App.get_running_app().root.remove_widget(self.layout)
self.animation_tooltip_dismiss()
@property
def ctx(self) -> context_type:
return App.get_running_app().ctx
return MDApp.get_running_app().ctx
def get_text(self):
if self.ctx.server:
@@ -262,11 +323,11 @@ class ServerLabel(HovererableLabel):
return "No current server connection. \nPlease connect to an Archipelago server."
class MainLayout(GridLayout):
class MainLayout(MDGridLayout):
pass
class ContainerLayout(FloatLayout):
class ContainerLayout(MDFloatLayout):
pass
@@ -286,6 +347,11 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
return super(SelectableLabel, self).refresh_view_attrs(
rv, index, data)
def on_size(self, instance_label, size: list) -> None:
super().on_size(instance_label, size)
if self.parent:
self.width = self.parent.width
def on_touch_down(self, touch):
""" Add selection on touch down """
if super(SelectableLabel, self).on_touch_down(touch):
@@ -296,10 +362,10 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
else:
# Not a fan of the following few lines, but they work.
temp = MarkupLabel(text=self.text).markup
text = "".join(part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
cmdinput = App.get_running_app().textinput
text = "".join(part for part in temp if not part.startswith("["))
cmdinput = MDApp.get_running_app().textinput
if not cmdinput.text:
input_text = get_input_text_from_response(text, App.get_running_app().last_autofillable_command)
input_text = get_input_text_from_response(text, MDApp.get_running_app().last_autofillable_command)
if input_text is not None:
cmdinput.text = input_text
@@ -310,30 +376,115 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
""" Respond to the selection of items in the view. """
self.selected = is_selected
class AutocompleteHintInput(TextInput):
class MarkupDropdownTextItem(MDDropdownTextItem):
def __init__(self):
super().__init__()
for child in self.children:
if child.__class__ == MDLabel:
child.markup = True
print(self.text)
# Currently, this only lets us do markup on text that does not have any icons
# Create new TextItems as needed
class MarkupDropdown(MDDropdownMenu):
def on_items(self, instance, value: list) -> None:
"""
The method sets the class that will be used to create the menu item.
"""
items = []
viewclass = "MarkupDropdownTextItem"
for data in value:
if "viewclass" not in data:
if (
"leading_icon" not in data
and "trailing_icon" not in data
and "trailing_text" not in data
):
viewclass = "MarkupDropdownTextItem"
elif (
"leading_icon" in data
and "trailing_icon" not in data
and "trailing_text" not in data
):
viewclass = "MDDropdownLeadingIconItem"
elif (
"leading_icon" not in data
and "trailing_icon" in data
and "trailing_text" not in data
):
viewclass = "MDDropdownTrailingIconItem"
elif (
"leading_icon" not in data
and "trailing_icon" in data
and "trailing_text" in data
):
viewclass = "MDDropdownTrailingIconTextItem"
elif (
"leading_icon" in data
and "trailing_icon" in data
and "trailing_text" in data
):
viewclass = "MDDropdownLeadingTrailingIconTextItem"
elif (
"leading_icon" in data
and "trailing_icon" in data
and "trailing_text" not in data
):
viewclass = "MDDropdownLeadingTrailingIconItem"
elif (
"leading_icon" not in data
and "trailing_icon" not in data
and "trailing_text" in data
):
viewclass = "MDDropdownTrailingTextItem"
elif (
"leading_icon" in data
and "trailing_icon" not in data
and "trailing_text" in data
):
viewclass = "MDDropdownLeadingIconTrailingTextItem"
data["viewclass"] = viewclass
if "height" not in data:
data["height"] = dp(48)
items.append(data)
self._items = items
# Update items in view
if hasattr(self, "menu"):
self.menu.data = self._items
class AutocompleteHintInput(MDTextField):
min_chars = NumericProperty(3)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.dropdown = DropDown()
self.dropdown = MarkupDropdown(caller=self, position="bottom", border_margin=dp(24), width=self.width)
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
self.bind(on_text_validate=self.on_message)
self.bind(width=lambda instance, x: setattr(self.dropdown, "width", x))
def on_message(self, instance):
App.get_running_app().commandprocessor("!hint "+instance.text)
MDApp.get_running_app().commandprocessor("!hint "+instance.text)
def on_text(self, instance, value):
if len(value) >= self.min_chars:
self.dropdown.clear_widgets()
ctx: context_type = App.get_running_app().ctx
self.dropdown.items.clear()
ctx: context_type = MDApp.get_running_app().ctx
if not ctx.game:
return
item_names = ctx.item_names._game_store[ctx.game].values()
def on_press(button: Button):
split_text = MarkupLabel(text=button.text).markup
def on_press(text):
split_text = MarkupLabel(text=text).markup
return self.dropdown.select("".join(text_frag for text_frag in split_text
if not text_frag.startswith("[")))
lowered = value.lower()
@@ -345,20 +496,29 @@ class AutocompleteHintInput(TextInput):
else:
text = escape_markup(item_name)
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
btn = Button(text=text, size_hint_y=None, height=dp(30), markup=True)
btn.bind(on_release=on_press)
self.dropdown.add_widget(btn)
if not self.dropdown.attach_to:
self.dropdown.open(self)
self.dropdown.items.append({
"text": text,
"on_release": lambda: on_press(text),
"markup": True
})
if not self.dropdown.parent:
self.dropdown.open()
else:
self.dropdown.dismiss()
class HintLabel(RecycleDataViewBehavior, BoxLayout):
status_icons = {
HintStatus.HINT_NO_PRIORITY: "information",
HintStatus.HINT_PRIORITY: "exclamation-thick",
HintStatus.HINT_AVOID: "alert"
}
class HintLabel(RecycleDataViewBehavior, MDBoxLayout):
selected = BooleanProperty(False)
striped = BooleanProperty(False)
index = None
dropdown: DropDown
dropdown: MDDropdownMenu
def __init__(self):
super(HintLabel, self).__init__()
@@ -369,29 +529,28 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
self.entrance_text = ""
self.status_text = ""
self.hint = {}
for child in self.children:
child.bind(texture_size=self.set_height)
ctx = MDApp.get_running_app().ctx
menu_items = []
ctx = App.get_running_app().ctx
self.dropdown = DropDown()
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
name = status_names[status]
status_button = MDDropDownItem(MDDropDownItemText(text=name), size_hint_y=None, height=dp(50))
status_button.status = status
menu_items.append({
"text": name,
"leading_icon": status_icons[status],
"on_release": lambda x=status: select(self, x)
})
def set_value(button):
self.dropdown.select(button.status)
self.dropdown = MDDropdownMenu(caller=self.ids["status"], items=menu_items)
def select(instance, data):
ctx.update_hint(self.hint["location"],
self.hint["finding_player"],
data)
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
name = status_names[status]
status_button = Button(text=name, size_hint_y=None, height=dp(50))
status_button.status = status
status_button.bind(on_release=set_value)
self.dropdown.add_widget(status_button)
self.dropdown.bind(on_select=select)
self.dropdown.bind(on_release=self.dropdown.dismiss)
def set_height(self, instance, value):
self.height = max([child.texture_size[1] for child in self.children])
@@ -406,7 +565,6 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
self.entrance_text = data["entrance"]["text"]
self.status_text = data["status"]["text"]
self.hint = data["status"]["hint"]
self.height = self.minimum_height
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
def on_touch_down(self, touch):
@@ -419,10 +577,10 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
if status_label.collide_point(*touch.pos):
if self.hint["status"] == HintStatus.HINT_FOUND:
return
ctx = App.get_running_app().ctx
ctx = MDApp.get_running_app().ctx
if ctx.slot_concerns_self(self.hint["receiving_player"]): # If this player owns this hint
# open a dropdown
self.dropdown.open(self.ids["status"])
self.dropdown.open()
elif self.selected:
self.parent.clear_selection()
else:
@@ -431,8 +589,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
if self.entrance_text != "Vanilla"
else "", ". (", self.status_text.lower(), ")"))
temp = MarkupLabel(text).markup
text = "".join(
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
text = "".join(part for part in temp if not part.startswith("["))
Clipboard.copy(escape_markup(text).replace("&amp;", "&").replace("&bl;", "[").replace("&br;", "]"))
return self.parent.select_with_touch(self.index, touch)
else:
@@ -455,7 +612,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
else:
parent.sort_key = key
parent.reversed = False
App.get_running_app().update_hints()
MDApp.get_running_app().update_hints()
def apply_selection(self, rv, index, is_selected):
""" Respond to the selection of items in the view. """
@@ -463,7 +620,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
self.selected = is_selected
class ConnectBarTextInput(TextInput):
class ConnectBarTextInput(MDTextField):
def insert_text(self, substring, from_undo=False):
s = substring.replace("\n", "").replace("\r", "")
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
@@ -473,7 +630,7 @@ def is_command_input(string: str) -> bool:
return len(string) > 0 and string[0] in "/!"
class CommandPromptTextInput(TextInput):
class CommandPromptTextInput(MDTextField):
MAXIMUM_HISTORY_MESSAGES = 50
def __init__(self, **kwargs) -> None:
@@ -521,7 +678,7 @@ class CommandPromptTextInput(TextInput):
class MessageBox(Popup):
class MessageBoxLabel(Label):
class MessageBoxLabel(MDLabel):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._label.refresh()
@@ -539,14 +696,31 @@ class MessageBox(Popup):
self.height += max(0, label.height - 18)
class GameManager(App):
class ClientTabs(MDTabsPrimary):
carousel: MDTabsCarousel
lock_swiping = True
def __init__(self, *args, **kwargs):
self.carousel = MDTabsCarousel(lock_swiping=True)
super().__init__(*args, MDDivider(size_hint_y=None, height=dp(4)), self.carousel, **kwargs)
self.size_hint_y = 1
def remove_tab(self, tab, content=None):
if content is None:
content = tab.content
self.ids.container.remove_widget(tab)
self.carousel.remove_widget(content)
self.on_size(self, self.size)
class GameManager(ThemedApp):
logging_pairs = [
("Client", "Archipelago"),
]
base_title: str = "Archipelago Client"
last_autofillable_command: str
main_area_container: GridLayout
main_area_container: MDGridLayout
""" subclasses can add more columns beside the tabs """
def __init__(self, ctx: context_type):
@@ -581,18 +755,26 @@ class GameManager(App):
return max(1, len(self.tabs.tab_list))
return 1
def on_start(self):
def on_start(*args):
self.root.md_bg_color = self.theme_cls.backgroundColor
super().on_start()
Clock.schedule_once(on_start)
def build(self) -> Layout:
self.set_colors()
self.container = ContainerLayout()
self.grid = MainLayout()
self.grid.cols = 1
self.connect_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
self.connect_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70),
spacing=5, padding=(5, 10))
# top part
server_label = ServerLabel()
server_label = ServerLabel(halign="center")
self.connect_layout.add_widget(server_label)
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
size_hint_y=None,
height=dp(30), multiline=False, write_tab=False)
size_hint_y=None, role="medium",
height=dp(70), multiline=False, write_tab=False)
def connect_bar_validate(sender):
if not self.ctx.server:
@@ -600,26 +782,31 @@ class GameManager(App):
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
self.connect_layout.add_widget(self.server_connect_bar)
self.server_connect_button = Button(text="Connect", size=(dp(100), dp(30)), size_hint_y=None, size_hint_x=None)
self.server_connect_button = MDButton(MDButtonText(text="Connect"), style="filled", size=(dp(100), dp(70)),
size_hint_x=None, size_hint_y=None, radius=5, pos_hint={"center_y": 0.55})
self.server_connect_button.bind(on_press=self.connect_button_action)
self.server_connect_button.height = self.server_connect_bar.height
self.connect_layout.add_widget(self.server_connect_button)
self.grid.add_widget(self.connect_layout)
self.progressbar = ProgressBar(size_hint_y=None, height=3)
self.progressbar = MDLinearProgressIndicator(size_hint_y=None, height=3)
self.grid.add_widget(self.progressbar)
# middle part
self.tabs = TabbedPanel(size_hint_y=1)
self.tabs.default_tab_text = "All"
self.tabs = ClientTabs()
self.tabs.add_widget(MDTabsItem(MDTabsItemText(text="All" if len(self.logging_pairs) > 1 else "Archipelago")))
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
for logger_name, name in
self.logging_pairs))
for logger_name, name in
self.logging_pairs))
self.tabs.carousel.add_widget(self.tabs.default_tab_content)
for logger_name, display_name in self.logging_pairs:
bridge_logger = logging.getLogger(logger_name)
panel = TabbedPanelItem(text=display_name)
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
self.log_panels[display_name] = UILog(bridge_logger)
if len(self.logging_pairs) > 1:
panel = MDTabsItem(MDTabsItemText(text=display_name))
panel.content = self.log_panels[display_name]
# show Archipelago tab if other logging is present
self.tabs.carousel.add_widget(panel.content)
self.tabs.add_widget(panel)
hint_panel = self.add_client_tab("Hints", HintLayout())
@@ -627,21 +814,20 @@ class GameManager(App):
self.log_panels["Hints"] = hint_panel.content
hint_panel.content.add_widget(self.hint_log)
if len(self.logging_pairs) == 1:
self.tabs.default_tab_text = "Archipelago"
self.main_area_container = GridLayout(size_hint_y=1, rows=1)
self.main_area_container = MDGridLayout(size_hint_y=1, rows=1)
self.main_area_container.add_widget(self.tabs)
self.grid.add_widget(self.main_area_container)
# bottom part
bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None)
bottom_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70), spacing=5, padding=(5, 10))
info_button = MDButton(MDButtonText(text="Command:"), radius=5, style="filled", size=(dp(100), dp(70)),
size_hint_x=None, size_hint_y=None, pos_hint={"center_y": 0.575})
info_button.bind(on_release=self.command_button_action)
bottom_layout.add_widget(info_button)
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
self.textinput.bind(on_text_validate=self.on_message)
info_button.height = self.textinput.height
self.textinput.text_validate_unfocus = False
bottom_layout.add_widget(self.textinput)
self.grid.add_widget(bottom_layout)
@@ -662,24 +848,26 @@ class GameManager(App):
def add_client_tab(self, title: str, content: Widget) -> Widget:
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
Returns the new tab widget, with the provided content being placed on the tab as content."""
new_tab = TabbedPanelItem(text=title)
new_tab = MDTabsItem(MDTabsItemText(text=title))
new_tab.content = content
self.tabs.add_widget(new_tab)
self.tabs.carousel.add_widget(new_tab.content)
return new_tab
def update_texts(self, dt):
if hasattr(self.tabs.content.children[0], "fix_heights"):
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
for slide in self.tabs.carousel.slides:
if hasattr(slide, "fix_heights"):
slide.fix_heights() # TODO: remove this when Kivy fixes this upstream
if self.ctx.server:
self.title = self.base_title + " " + Utils.__version__ + \
f" | Connected to: {self.ctx.server_address} " \
f"{'.'.join(str(e) for e in self.ctx.server_version)}"
self.server_connect_button.text = "Disconnect"
self.server_connect_button._button_text.text = "Disconnect"
self.server_connect_bar.readonly = True
self.progressbar.max = len(self.ctx.checked_locations) + len(self.ctx.missing_locations)
self.progressbar.value = len(self.ctx.checked_locations)
else:
self.server_connect_button.text = "Connect"
self.server_connect_button._button_text.text = "Connect"
self.server_connect_bar.readonly = False
self.title = self.base_title + " " + Utils.__version__
self.progressbar.value = 0
@@ -742,8 +930,8 @@ class GameManager(App):
def enable_energy_link(self):
if not hasattr(self, "energy_link_label"):
self.energy_link_label = Label(text="Energy Link: Standby",
size_hint_x=None, width=150)
self.energy_link_label = MDLabel(text="Energy Link: Standby",
size_hint_x=None, width=150, halign="center")
self.connect_layout.add_widget(self.energy_link_label)
def set_new_energy_link_value(self):
@@ -779,8 +967,9 @@ class LogtoUI(logging.Handler):
self.on_log(self.format(record))
class UILog(RecycleView):
class UILog(MDRecycleView):
messages: typing.ClassVar[int] # comes from kv file
adaptive_height = True
def __init__(self, *loggers_to_handle, **kwargs):
super(UILog, self).__init__(**kwargs)
@@ -807,16 +996,22 @@ class UILog(RecycleView):
element.height = element.texture_size[1]
class HintLayout(BoxLayout):
class HintLayout(MDBoxLayout):
orientation = "vertical"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
boxlayout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
boxlayout.add_widget(Label(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(30)))
boxlayout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(55))
boxlayout.add_widget(MDLabel(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(55)))
boxlayout.add_widget(AutocompleteHintInput())
self.add_widget(boxlayout)
def fix_heights(self):
for child in self.children:
fix_func = getattr(child, "fix_heights", None)
if fix_func:
fix_func()
status_names: typing.Dict[HintStatus, str] = {
HintStatus.HINT_FOUND: "Found",
@@ -840,8 +1035,7 @@ status_sort_weights: dict[HintStatus, int] = {
HintStatus.HINT_PRIORITY: 4,
}
class HintLog(RecycleView):
class HintLog(MDRecycleView):
header = {
"receiving": {"text": "[u]Receiving Player[/u]"},
"item": {"text": "[u]Item[/u]"},
@@ -852,7 +1046,7 @@ class HintLog(RecycleView):
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
"striped": True,
}
data: list[typing.Any]
sort_key: str = ""
reversed: bool = True
@@ -865,7 +1059,7 @@ class HintLog(RecycleView):
if not hints: # Fix the scrolling looking visually wrong in some edge cases
self.scroll_y = 1.0
data = []
ctx = App.get_running_app().ctx
ctx = MDApp.get_running_app().ctx
for hint in hints:
if not hint.get("status"): # Allows connecting to old servers
hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED
@@ -929,7 +1123,8 @@ class ImageLoaderPkgutil(ImageLoaderBase):
data = pkgutil.get_data(module, path)
return self._bytes_to_data(data)
def _bytes_to_data(self, data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]:
@staticmethod
def _bytes_to_data(data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]:
loader = next(loader for loader in ImageLoader.loaders if loader.can_load_memory())
return loader.load(loader, io.BytesIO(data))

View File

@@ -1,14 +1,17 @@
colorama>=0.4.6
websockets>=13.0.1,<14
PyYAML>=6.0.2
jellyfish>=1.1.0
jinja2>=3.1.4
jellyfish>=1.1.3
jinja2>=3.1.6
schema>=0.7.7
kivy>=2.3.0
bsdiff4>=1.2.4
platformdirs>=4.2.2
certifi>=2024.12.14
cython>=3.0.11
cymem>=2.0.8
orjson>=3.10.7
kivy>=2.3.1
bsdiff4>=1.2.6
platformdirs>=4.3.6
certifi>=2025.1.31
cython>=3.0.12
cymem>=2.0.11
orjson>=3.10.15
typing_extensions>=4.12.2
pyshortcuts>=1.9.1
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
kivymd>=2.0.1.dev0

View File

@@ -19,7 +19,7 @@ from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
requirement = 'cx-Freeze==7.2.0'
requirement = 'cx-Freeze==8.0.0'
try:
import pkg_resources
try:
@@ -629,12 +629,13 @@ cx_Freeze.setup(
ext_modules=cythonize("_speedups.pyx"),
options={
"build_exe": {
"packages": ["worlds", "kivy", "cymem", "websockets"],
"packages": ["worlds", "kivy", "cymem", "websockets", "kivymd"],
"includes": [],
"excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas", "zstandard"],
"pandas"],
"zip_includes": [],
"zip_include_packages": ["*"],
"zip_exclude_packages": ["worlds", "sc2"],
"zip_exclude_packages": ["worlds", "sc2", "kivymd"],
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
"include_msvcr": False,
"replace_paths": ["*."],

View File

@@ -65,8 +65,10 @@ class TestEntranceLookup(unittest.TestCase):
"""tests that get_targets shuffles targets between groups when requested"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
exits_set = set([ex for region in multiworld.get_regions(1)
for ex in region.exits if not ex.connected_region])
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True)
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
er_targets = [entrance for region in multiworld.get_regions(1)
for entrance in region.entrances if not entrance.parent_region]
for entrance in er_targets:
@@ -86,8 +88,10 @@ class TestEntranceLookup(unittest.TestCase):
"""tests that get_targets does not shuffle targets between groups when requested"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
exits_set = set([ex for region in multiworld.get_regions(1)
for ex in region.exits if not ex.connected_region])
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True)
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
er_targets = [entrance for region in multiworld.get_regions(1)
for entrance in region.entrances if not entrance.parent_region]
for entrance in er_targets:
@@ -99,6 +103,30 @@ class TestEntranceLookup(unittest.TestCase):
group_order = [prev := group.randomization_group for group in retrieved_targets if prev != group.randomization_group]
self.assertEqual([ERTestGroups.TOP, ERTestGroups.BOTTOM], group_order)
def test_selective_dead_ends(self):
"""test that entrances that EntranceLookup has not been told to consider are ignored when finding dead-ends"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
exits_set = set([ex for region in multiworld.get_regions(1)
for ex in region.exits if not ex.connected_region
and ex.name != "region20_right" and ex.name != "region21_left"])
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
er_targets = [entrance for region in multiworld.get_regions(1)
for entrance in region.entrances if not entrance.parent_region and
entrance.name != "region20_right" and entrance.name != "region21_left"]
for entrance in er_targets:
lookup.add(entrance)
# region 20 is the bottom left corner of the grid, and therefore only has a right entrance from region 21
# and a top entrance from region 15; since we've told lookup to ignore the right entrance from region 21,
# the top entrance from region 15 should be considered a dead-end
dead_end_region = multiworld.get_region("region20", 1)
for dead_end in dead_end_region.entrances:
if dead_end.name == "region20_top":
break
# there should be only this one dead-end
self.assertTrue(dead_end in lookup.dead_ends)
self.assertEqual(len(lookup.dead_ends), 1)
class TestBakeTargetGroupLookup(unittest.TestCase):
def test_lookup_generation(self):
@@ -148,7 +176,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
e.randomization_group = 1
e.connect(r2)
disconnect_entrance_for_randomization(e)
disconnect_entrance_for_randomization(e, one_way_target_name="foo")
self.assertIsNone(e.connected_region)
self.assertEqual([], r1.entrances)
@@ -158,10 +186,22 @@ class TestDisconnectForRandomization(unittest.TestCase):
self.assertEqual(1, len(r2.entrances))
self.assertIsNone(r2.entrances[0].parent_region)
self.assertEqual("r2", r2.entrances[0].name)
self.assertEqual("foo", r2.entrances[0].name)
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
self.assertEqual(1, r2.entrances[0].randomization_group)
def test_disconnect_default_1way_no_vanilla_target_raises(self):
multiworld = generate_test_multiworld()
r1 = Region("r1", 1, multiworld)
r2 = Region("r2", 1, multiworld)
e = r1.create_exit("e")
e.randomization_type = EntranceType.ONE_WAY
e.randomization_group = 1
e.connect(r2)
with self.assertRaises(ValueError):
disconnect_entrance_for_randomization(e)
def test_disconnect_uses_alternate_group(self):
multiworld = generate_test_multiworld()
r1 = Region("r1", 1, multiworld)
@@ -171,7 +211,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
e.randomization_group = 1
e.connect(r2)
disconnect_entrance_for_randomization(e, 2)
disconnect_entrance_for_randomization(e, 2, "foo")
self.assertIsNone(e.connected_region)
self.assertEqual([], r1.entrances)
@@ -181,7 +221,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
self.assertEqual(1, len(r2.entrances))
self.assertIsNone(r2.entrances[0].parent_region)
self.assertEqual("r2", r2.entrances[0].name)
self.assertEqual("foo", r2.entrances[0].name)
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
self.assertEqual(2, r2.entrances[0].randomization_group)

View File

@@ -0,0 +1,14 @@
import unittest
import os
class TestPackages(unittest.TestCase):
def test_packages_have_init(self):
"""Test that all world folders containing .py files also have a __init__.py file,
to indicate full package rather than namespace package."""
import Utils
worlds_path = Utils.local_path("worlds")
for dirpath, dirnames, filenames in os.walk(worlds_path):
with self.subTest(directory=dirpath):
self.assertEqual("__init__.py" in filenames, any(file.endswith(".py") for file in filenames))

View File

@@ -0,0 +1,11 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister
from worlds.Files import AutoPatchRegister
class TestPatches(unittest.TestCase):
def test_patch_name_matches_game(self) -> None:
for game_name in AutoPatchRegister.patch_types:
with self.subTest(game=game_name):
self.assertIn(game_name, AutoWorldRegister.world_types.keys(),
f"Patch '{game_name}' does not match the name of any world.")

View File

@@ -0,0 +1,19 @@
import unittest
import os
class TestBase(unittest.TestCase):
def test_requirements_file_ends_on_newline(self):
"""Test that all requirements files end on a newline"""
import Utils
requirements_files = [Utils.local_path("requirements.txt"),
Utils.local_path("WebHostLib", "requirements.txt")]
worlds_path = Utils.local_path("worlds")
for entry in os.listdir(worlds_path):
requirements_path = os.path.join(worlds_path, entry, "requirements.txt")
if os.path.isfile(requirements_path):
requirements_files.append(requirements_path)
for requirements_file in requirements_files:
with self.subTest(path=requirements_file):
with open(requirements_file) as f:
self.assertEqual(f.read()[-1], "\n")

View File

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

View File

@@ -110,6 +110,16 @@ class AutoLogicRegister(type):
elif not item_name.startswith("__"):
if hasattr(CollectionState, item_name):
raise Exception(f"Name conflict on Logic Mixin {name} trying to overwrite {item_name}")
assert callable(function) or "init_mixin" in dct, (
f"{name} defined class variable {item_name} without also having init_mixin.\n\n"
"Explanation:\n"
"Class variables that will be mutated need to be inintialized as instance variables in init_mixin.\n"
"If your LogicMixin variables aren't actually mutable / you don't intend to mutate them, "
"there is no point in using LogixMixin.\n"
"LogicMixin exists to track custom state variables that change when items are collected/removed."
)
setattr(CollectionState, item_name, function)
return new_class

View File

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

View File

@@ -3,4 +3,4 @@ mpyq>=0.2.5
portpicker>=1.5.2
aiohttp>=3.8.4
loguru>=0.7.0
protobuf==3.20.3
protobuf==3.20.3

View File

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

View File

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

View File

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

View File

@@ -121,6 +121,7 @@ class ALTTPWeb(WebWorld):
)
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
game_info_languages = ["en", "fr"]
class ALTTPWorld(World):

View File

@@ -1,2 +1,2 @@
maseya-z3pr>=1.0.0rc1
xxtea>=3.0.0
xxtea>=3.0.0

View File

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

View File

@@ -48,6 +48,10 @@ class CivVIContainer(APContainer, metaclass=AutoPatchRegister):
opened_zipfile.writestr(filename, yml)
super().write_contents(opened_zipfile)
def sanitize_value(value: str) -> str:
"""Removes values that can cause issues in XML"""
return value.replace('"', "'").replace('&', 'and')
def get_cost(world: 'CivVIWorld', location: CivVILocationData) -> int:
"""
@@ -63,7 +67,7 @@ def get_formatted_player_name(world: 'CivVIWorld', player: int) -> str:
Returns the name of the player in the world
"""
if player != world.player:
return f"{world.multiworld.player_name[player]}{apo}s"
return sanitize_value(f"{world.multiworld.player_name[player]}{apo}s")
return "Your"
@@ -106,7 +110,7 @@ def generate_new_items(world: 'CivVIWorld') -> str:
<Row TechnologyType="TECH_BLOCKER" Name="TECH_BLOCKER" EraType="ERA_ANCIENT" UITreeRow="0" Cost="99999" AdvisorType="ADVISOR_GENERIC" Description="Archipelago Tech created to prevent players from researching their own tech. If you can read this, then congrats you have reached the end of your tree before beating the game!"/>
{"".join([f'{tab}<Row TechnologyType="{location.name}" '
f'Name="{get_formatted_player_name(world, location.item.player)} '
f'{location.item.name}" '
f'{sanitize_value(location.item.name)}" '
f'EraType="{world.location_table[location.name].era_type}" '
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
f'Cost="{get_cost(world, world.location_table[location.name])}" '
@@ -122,7 +126,7 @@ def generate_new_items(world: 'CivVIWorld') -> str:
<Row CivicType="CIVIC_BLOCKER" Name="CIVIC_BLOCKER" EraType="ERA_ANCIENT" UITreeRow="0" Cost="99999" AdvisorType="ADVISOR_GENERIC" Description="Archipelago Civic created to prevent players from researching their own civics. If you can read this, then congrats you have reached the end of your tree before beating the game!"/>
{"".join([f'{tab}<Row CivicType="{location.name}" '
f'Name="{get_formatted_player_name(world, location.item.player)} '
f'{location.item.name}" '
f'{sanitize_value(location.item.name)}" '
f'EraType="{world.location_table[location.name].era_type}" '
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
f'Cost="{get_cost(world, world.location_table[location.name])}" '

View File

View File

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

View File

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

View File

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

View File

View File

@@ -644,6 +644,9 @@ class CV64PatchExtensions(APPatchExtension):
# Replace the PowerUp in the Forest Special1 Bridge 3HB rock with an L jewel if 3HBs aren't randomized
if not options["multi_hit_breakables"]:
rom_data.write_byte(0x10C7A1, 0x03)
# Replace the PowerUp in one of the lizard lockers if the lizard locker items aren't randomized.
if not options["lizard_locker_items"]:
rom_data.write_byte(0xBFCA07, 0x03)
# Change the appearance of the Pot-Pourri to that of a larger PowerUp regardless of the above setting, so other
# game PermaUps are distinguishable.
rom_data.write_int32s(0xEE558, [0x06005F08, 0x3FB00000, 0xFFFFFF00])
@@ -714,7 +717,11 @@ class CV64PatchExtensions(APPatchExtension):
rom_data.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab
rom_data.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab
rom_data.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier
rom_data.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data
# Change the pointer to the Clock Tower final room 3HB door slab drops to not share its values with those of the
# 3HB slab near Renon at the top of the room.
if options["multi_hit_breakables"]:
rom_data.write_byte(0x10CF37, 0x04)
# Once-per-frame gameplay checks
rom_data.write_int32(0x6C848, 0x080FF40D) # J 0x803FD034
@@ -1000,6 +1007,7 @@ def write_patch(world: "CV64World", patch: CV64ProcedurePatch, offset_data: Dict
"multi_hit_breakables": world.options.multi_hit_breakables.value,
"drop_previous_sub_weapon": world.options.drop_previous_sub_weapon.value,
"countdown": world.options.countdown.value,
"lizard_locker_items": world.options.lizard_locker_items.value,
"shopsanity": world.options.shopsanity.value,
"panther_dash": world.options.panther_dash.value,
"big_toss": world.options.big_toss.value,

View File

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

View File

View File

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

View File

@@ -98,14 +98,14 @@ def create_trap_items(world, world_options: Options.DLCQuestOptions, trap_needed
return traps
def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, random: Random):
def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, excluded_items: list[str], random: Random):
created_items = []
if world_options.campaign == Options.Campaign.option_basic or world_options.campaign == Options.Campaign.option_both:
create_items_basic(world_options, created_items, world)
create_items_basic(world_options, created_items, world, excluded_items)
if (world_options.campaign == Options.Campaign.option_live_freemium_or_die or
world_options.campaign == Options.Campaign.option_both):
create_items_lfod(world_options, created_items, world)
create_items_lfod(world_options, created_items, world, excluded_items)
trap_items = create_trap_items(world, world_options, locations_count - len(created_items), random)
created_items += trap_items
@@ -113,8 +113,12 @@ def create_items(world, world_options: Options.DLCQuestOptions, locations_count:
return created_items
def create_items_lfod(world_options, created_items, world):
def create_items_lfod(world_options, created_items, world, excluded_items):
for item in items_by_group[Group.Freemium]:
if item.name in excluded_items:
excluded_items.remove(item)
continue
if item.has_any_group(Group.DLC):
created_items.append(world.create_item(item))
if item.has_any_group(Group.Item) and world_options.item_shuffle == Options.ItemShuffle.option_shuffled:
@@ -128,8 +132,12 @@ def create_items_lfod(world_options, created_items, world):
create_coin(world_options, created_items, world, 889, 200, Group.Freemium)
def create_items_basic(world_options, created_items, world):
def create_items_basic(world_options, created_items, world, excluded_items):
for item in items_by_group[Group.DLCQuest]:
if item.name in excluded_items:
excluded_items.remove(item.name)
continue
if item.has_any_group(Group.DLC):
created_items.append(world.create_item(item))
if item.has_any_group(Group.Item) and world_options.item_shuffle == Options.ItemShuffle.option_shuffled:

View File

@@ -34,6 +34,7 @@ class DLCqwebworld(WebWorld):
["Deoxis"]
)
tutorials = [setup_en, setup_fr]
game_info_languages = ["en", "fr"]
class DLCqworld(World):
@@ -65,10 +66,10 @@ class DLCqworld(World):
for location in self.multiworld.get_locations(self.player)
if not location.advancement])
items_to_exclude = [excluded_items
items_to_exclude = [excluded_items.name
for excluded_items in self.multiworld.precollected_items[self.player]]
created_items = create_items(self, self.options, locations_count + len(items_to_exclude), self.multiworld.random)
created_items = create_items(self, self.options, locations_count, items_to_exclude, self.multiworld.random)
self.multiworld.itempool += created_items
@@ -83,9 +84,7 @@ class DLCqworld(World):
else:
early_items[self.player]["Movement Pack"] = 1
for item in items_to_exclude:
if item in self.multiworld.itempool:
self.multiworld.itempool.remove(item)
def precollect_coinsanity(self):
if self.options.campaign == Options.Campaign.option_basic:

View File

@@ -1,10 +1,11 @@
import unittest
from typing import Dict
from BaseClasses import MultiWorld
from Options import NamedRange
from .option_names import options_to_include
from .checks.world_checks import assert_can_win, assert_same_number_items_locations
from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld
from .checks.world_checks import assert_can_win, assert_same_number_items_locations
from .option_names import options_to_include
def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld):
@@ -38,6 +39,8 @@ class TestGenerateDynamicOptions(DLCQuestTestBase):
basic_checks(self, multiworld)
def test_given_option_truple_when_generate_then_basic_checks(self):
if self.skip_long_tests:
raise unittest.SkipTest("Long tests disabled")
num_options = len(options_to_include)
for option1_index in range(0, num_options):
for option2_index in range(option1_index + 1, num_options):
@@ -59,6 +62,8 @@ class TestGenerateDynamicOptions(DLCQuestTestBase):
basic_checks(self, multiworld)
def test_given_option_quartet_when_generate_then_basic_checks(self):
if self.skip_long_tests:
raise unittest.SkipTest("Long tests disabled")
num_options = len(options_to_include)
for option1_index in range(0, num_options):
for option2_index in range(option1_index + 1, num_options):

View File

@@ -1,19 +1,26 @@
from typing import ClassVar
from typing import Dict, FrozenSet, Tuple, Any
import os
from argparse import Namespace
from typing import ClassVar
from typing import Dict, FrozenSet, Tuple, Any
from BaseClasses import MultiWorld
from test.bases import WorldTestBase
from .. import DLCqworld
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
from worlds.AutoWorld import call_all
from .. import DLCqworld
class DLCQuestTestBase(WorldTestBase):
game = "DLCQuest"
world: DLCqworld
player: ClassVar[int] = 1
# Set False to run tests that take long
skip_long_tests: bool = True
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.skip_long_tests = not bool(os.environ.get("long"))
def world_setup(self, *args, **kwargs):
super().world_setup(*args, **kwargs)

View File

@@ -126,7 +126,7 @@ location_table: Dict[int, LocationDict] = {
'map': 3,
'index': 64,
'doom_type': 2001,
'region': "Toxin Refinery (E1M3) Main"},
'region': "Toxin Refinery (E1M3) Start"},
351019: {'name': 'Toxin Refinery (E1M3) - Shotgun 2',
'episode': 1,
'map': 3,
@@ -234,7 +234,7 @@ location_table: Dict[int, LocationDict] = {
'map': 4,
'index': 107,
'doom_type': 8,
'region': "Command Control (E1M4) Main"},
'region': "Command Control (E1M4) Start"},
351037: {'name': 'Command Control (E1M4) - Shotgun',
'episode': 1,
'map': 4,
@@ -504,7 +504,7 @@ location_table: Dict[int, LocationDict] = {
'map': 7,
'index': 122,
'doom_type': 2001,
'region': "Computer Station (E1M7) Main"},
'region': "Computer Station (E1M7) Start"},
351082: {'name': 'Computer Station (E1M7) - Rocket launcher',
'episode': 1,
'map': 7,
@@ -912,7 +912,7 @@ location_table: Dict[int, LocationDict] = {
'map': 4,
'index': 109,
'doom_type': 2001,
'region': "Deimos Lab (E2M4) Main"},
'region': "Deimos Lab (E2M4) Start"},
351150: {'name': 'Deimos Lab (E2M4) - Mega Armor',
'episode': 2,
'map': 4,
@@ -1242,7 +1242,7 @@ location_table: Dict[int, LocationDict] = {
'map': 8,
'index': 36,
'doom_type': 2019,
'region': "Tower of Babel (E2M8) Main"},
'region': "Tower of Babel (E2M8) Start"},
351205: {'name': 'Fortress of Mystery (E2M9) - Supercharge',
'episode': 2,
'map': 9,
@@ -1638,7 +1638,7 @@ location_table: Dict[int, LocationDict] = {
'map': 5,
'index': 187,
'doom_type': 2001,
'region': "Unholy Cathedral (E3M5) Main"},
'region': "Unholy Cathedral (E3M5) Start"},
351271: {'name': 'Unholy Cathedral (E3M5) - Shotgun 2',
'episode': 3,
'map': 5,

View File

@@ -33,9 +33,11 @@ regions:List[RegionDict] = [
# Toxin Refinery (E1M3)
{"name":"Toxin Refinery (E1M3) Main",
"connects_to_hub":True,
"connects_to_hub":False,
"episode":1,
"connections":[{"target":"Toxin Refinery (E1M3) Blue","pro":False}]},
"connections":[
{"target":"Toxin Refinery (E1M3) Blue","pro":False},
{"target":"Toxin Refinery (E1M3) Start","pro":False}]},
{"name":"Toxin Refinery (E1M3) Blue",
"connects_to_hub":False,
"episode":1,
@@ -46,15 +48,20 @@ regions:List[RegionDict] = [
"connects_to_hub":False,
"episode":1,
"connections":[{"target":"Toxin Refinery (E1M3) Blue","pro":False}]},
{"name":"Toxin Refinery (E1M3) Start",
"connects_to_hub":True,
"episode":1,
"connections":[{"target":"Toxin Refinery (E1M3) Main","pro":False}]},
# Command Control (E1M4)
{"name":"Command Control (E1M4) Main",
"connects_to_hub":True,
"connects_to_hub":False,
"episode":1,
"connections":[
{"target":"Command Control (E1M4) Blue","pro":False},
{"target":"Command Control (E1M4) Yellow","pro":False},
{"target":"Command Control (E1M4) Ledge","pro":True}]},
{"target":"Command Control (E1M4) Ledge","pro":True},
{"target":"Command Control (E1M4) Start","pro":False}]},
{"name":"Command Control (E1M4) Blue",
"connects_to_hub":False,
"episode":1,
@@ -72,6 +79,10 @@ regions:List[RegionDict] = [
{"target":"Command Control (E1M4) Main","pro":False},
{"target":"Command Control (E1M4) Blue","pro":False},
{"target":"Command Control (E1M4) Yellow","pro":False}]},
{"name":"Command Control (E1M4) Start",
"connects_to_hub":True,
"episode":1,
"connections":[{"target":"Command Control (E1M4) Main","pro":False}]},
# Phobos Lab (E1M5)
{"name":"Phobos Lab (E1M5) Main",
@@ -126,11 +137,12 @@ regions:List[RegionDict] = [
# Computer Station (E1M7)
{"name":"Computer Station (E1M7) Main",
"connects_to_hub":True,
"connects_to_hub":False,
"episode":1,
"connections":[
{"target":"Computer Station (E1M7) Red","pro":False},
{"target":"Computer Station (E1M7) Yellow","pro":False}]},
{"target":"Computer Station (E1M7) Yellow","pro":False},
{"target":"Computer Station (E1M7) Start","pro":False}]},
{"name":"Computer Station (E1M7) Blue",
"connects_to_hub":False,
"episode":1,
@@ -150,6 +162,10 @@ regions:List[RegionDict] = [
"connects_to_hub":False,
"episode":1,
"connections":[{"target":"Computer Station (E1M7) Yellow","pro":False}]},
{"name":"Computer Station (E1M7) Start",
"connects_to_hub":True,
"episode":1,
"connections":[{"target":"Computer Station (E1M7) Main","pro":False}]},
# Phobos Anomaly (E1M8)
{"name":"Phobos Anomaly (E1M8) Main",
@@ -238,9 +254,11 @@ regions:List[RegionDict] = [
# Deimos Lab (E2M4)
{"name":"Deimos Lab (E2M4) Main",
"connects_to_hub":True,
"connects_to_hub":False,
"episode":2,
"connections":[{"target":"Deimos Lab (E2M4) Blue","pro":False}]},
"connections":[
{"target":"Deimos Lab (E2M4) Blue","pro":False},
{"target":"Deimos Lab (E2M4) Start","pro":False}]},
{"name":"Deimos Lab (E2M4) Blue",
"connects_to_hub":False,
"episode":2,
@@ -251,6 +269,10 @@ regions:List[RegionDict] = [
"connects_to_hub":False,
"episode":2,
"connections":[{"target":"Deimos Lab (E2M4) Blue","pro":False}]},
{"name":"Deimos Lab (E2M4) Start",
"connects_to_hub":True,
"episode":2,
"connections":[{"target":"Deimos Lab (E2M4) Main","pro":False}]},
# Command Center (E2M5)
{"name":"Command Center (E2M5) Main",
@@ -314,9 +336,13 @@ regions:List[RegionDict] = [
# Tower of Babel (E2M8)
{"name":"Tower of Babel (E2M8) Main",
"connects_to_hub":False,
"episode":2,
"connections":[{"target":"Tower of Babel (E2M8) Start","pro":False}]},
{"name":"Tower of Babel (E2M8) Start",
"connects_to_hub":True,
"episode":2,
"connections":[]},
"connections":[{"target":"Tower of Babel (E2M8) Main","pro":False}]},
# Fortress of Mystery (E2M9)
{"name":"Fortress of Mystery (E2M9) Main",
@@ -392,11 +418,12 @@ regions:List[RegionDict] = [
# Unholy Cathedral (E3M5)
{"name":"Unholy Cathedral (E3M5) Main",
"connects_to_hub":True,
"connects_to_hub":False,
"episode":3,
"connections":[
{"target":"Unholy Cathedral (E3M5) Yellow","pro":False},
{"target":"Unholy Cathedral (E3M5) Blue","pro":False}]},
{"target":"Unholy Cathedral (E3M5) Blue","pro":False},
{"target":"Unholy Cathedral (E3M5) Start","pro":False}]},
{"name":"Unholy Cathedral (E3M5) Blue",
"connects_to_hub":False,
"episode":3,
@@ -405,6 +432,10 @@ regions:List[RegionDict] = [
"connects_to_hub":False,
"episode":3,
"connections":[{"target":"Unholy Cathedral (E3M5) Main","pro":False}]},
{"name":"Unholy Cathedral (E3M5) Start",
"connects_to_hub":True,
"episode":3,
"connections":[{"target":"Unholy Cathedral (E3M5) Main","pro":False}]},
# Mt. Erebus (E3M6)
{"name":"Mt. Erebus (E3M6) Main",

View File

@@ -23,10 +23,6 @@ def set_episode1_rules(player, multiworld, pro):
state.has("Nuclear Plant (E1M2) - Red keycard", player, 1))
# Toxin Refinery (E1M3)
set_rule(multiworld.get_entrance("Hub -> Toxin Refinery (E1M3) Main", player), lambda state:
(state.has("Toxin Refinery (E1M3)", player, 1)) and
(state.has("Shotgun", player, 1) or
state.has("Chaingun", player, 1)))
set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Main -> Toxin Refinery (E1M3) Blue", player), lambda state:
state.has("Toxin Refinery (E1M3) - Blue keycard", player, 1))
set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Blue -> Toxin Refinery (E1M3) Yellow", player), lambda state:
@@ -35,12 +31,13 @@ def set_episode1_rules(player, multiworld, pro):
state.has("Toxin Refinery (E1M3) - Blue keycard", player, 1))
set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Yellow -> Toxin Refinery (E1M3) Blue", player), lambda state:
state.has("Toxin Refinery (E1M3) - Yellow keycard", player, 1))
set_rule(multiworld.get_entrance("Hub -> Toxin Refinery (E1M3) Start", player), lambda state:
state.has("Toxin Refinery (E1M3)", player, 1))
set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Start -> Toxin Refinery (E1M3) Main", player), lambda state:
state.has("Shotgun", player, 1) or
state.has("Chaingun", player, 1))
# Command Control (E1M4)
set_rule(multiworld.get_entrance("Hub -> Command Control (E1M4) Main", player), lambda state:
state.has("Command Control (E1M4)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1))
set_rule(multiworld.get_entrance("Command Control (E1M4) Main -> Command Control (E1M4) Blue", player), lambda state:
state.has("Command Control (E1M4) - Blue keycard", player, 1) or
state.has("Command Control (E1M4) - Yellow keycard", player, 1))
@@ -50,6 +47,11 @@ def set_episode1_rules(player, multiworld, pro):
set_rule(multiworld.get_entrance("Command Control (E1M4) Blue -> Command Control (E1M4) Main", player), lambda state:
state.has("Command Control (E1M4) - Yellow keycard", player, 1) or
state.has("Command Control (E1M4) - Blue keycard", player, 1))
set_rule(multiworld.get_entrance("Hub -> Command Control (E1M4) Start", player), lambda state:
state.has("Command Control (E1M4)", player, 1))
set_rule(multiworld.get_entrance("Command Control (E1M4) Start -> Command Control (E1M4) Main", player), lambda state:
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1))
# Phobos Lab (E1M5)
set_rule(multiworld.get_entrance("Hub -> Phobos Lab (E1M5) Main", player), lambda state:
@@ -83,11 +85,6 @@ def set_episode1_rules(player, multiworld, pro):
state.has("Central Processing (E1M6) - Yellow keycard", player, 1))
# Computer Station (E1M7)
set_rule(multiworld.get_entrance("Hub -> Computer Station (E1M7) Main", player), lambda state:
state.has("Computer Station (E1M7)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Rocket launcher", player, 1))
set_rule(multiworld.get_entrance("Computer Station (E1M7) Main -> Computer Station (E1M7) Red", player), lambda state:
state.has("Computer Station (E1M7) - Red keycard", player, 1))
set_rule(multiworld.get_entrance("Computer Station (E1M7) Main -> Computer Station (E1M7) Yellow", player), lambda state:
@@ -103,6 +100,12 @@ def set_episode1_rules(player, multiworld, pro):
state.has("Computer Station (E1M7) - Red keycard", player, 1))
set_rule(multiworld.get_entrance("Computer Station (E1M7) Courtyard -> Computer Station (E1M7) Yellow", player), lambda state:
state.has("Computer Station (E1M7) - Yellow keycard", player, 1))
set_rule(multiworld.get_entrance("Hub -> Computer Station (E1M7) Start", player), lambda state:
state.has("Computer Station (E1M7)", player, 1))
set_rule(multiworld.get_entrance("Computer Station (E1M7) Start -> Computer Station (E1M7) Main", player), lambda state:
state.has("Shotgun", player, 1) and
state.has("Rocket launcher", player, 1) and
state.has("Chaingun", player, 1))
# Phobos Anomaly (E1M8)
set_rule(multiworld.get_entrance("Hub -> Phobos Anomaly (E1M8) Start", player), lambda state:
@@ -172,15 +175,16 @@ def set_episode2_rules(player, multiworld, pro):
state.has("Refinery (E2M3) - Blue keycard", player, 1))
# Deimos Lab (E2M4)
set_rule(multiworld.get_entrance("Hub -> Deimos Lab (E2M4) Main", player), lambda state:
state.has("Deimos Lab (E2M4)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Plasma gun", player, 1))
set_rule(multiworld.get_entrance("Deimos Lab (E2M4) Main -> Deimos Lab (E2M4) Blue", player), lambda state:
state.has("Deimos Lab (E2M4) - Blue keycard", player, 1))
set_rule(multiworld.get_entrance("Deimos Lab (E2M4) Blue -> Deimos Lab (E2M4) Yellow", player), lambda state:
state.has("Deimos Lab (E2M4) - Yellow keycard", player, 1))
set_rule(multiworld.get_entrance("Hub -> Deimos Lab (E2M4) Start", player), lambda state:
state.has("Deimos Lab (E2M4)", player, 1))
set_rule(multiworld.get_entrance("Deimos Lab (E2M4) Start -> Deimos Lab (E2M4) Main", player), lambda state:
state.has("Shotgun", player, 1) and
state.has("Plasma gun", player, 1) and
state.has("Chaingun", player, 1))
# Command Center (E2M5)
set_rule(multiworld.get_entrance("Hub -> Command Center (E2M5) Main", player), lambda state:
@@ -238,11 +242,11 @@ def set_episode2_rules(player, multiworld, pro):
state.has("Spawning Vats (E2M7) - Red keycard", player, 1))
# Tower of Babel (E2M8)
set_rule(multiworld.get_entrance("Hub -> Tower of Babel (E2M8) Main", player), lambda state:
(state.has("Tower of Babel (E2M8)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
set_rule(multiworld.get_entrance("Hub -> Tower of Babel (E2M8) Start", player), lambda state:
state.has("Tower of Babel (E2M8)", player, 1))
set_rule(multiworld.get_entrance("Tower of Babel (E2M8) Start -> Tower of Babel (E2M8) Main", player), lambda state:
(state.has("Chaingun", player, 1) and
state.has("Shotgun", player, 1)) and (state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
@@ -321,13 +325,6 @@ def set_episode3_rules(player, multiworld, pro):
state.has("House of Pain (E3M4) - Yellow skull key", player, 1))
# Unholy Cathedral (E3M5)
set_rule(multiworld.get_entrance("Hub -> Unholy Cathedral (E3M5) Main", player), lambda state:
(state.has("Unholy Cathedral (E3M5)", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Shotgun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Main -> Unholy Cathedral (E3M5) Yellow", player), lambda state:
state.has("Unholy Cathedral (E3M5) - Yellow skull key", player, 1))
set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Main -> Unholy Cathedral (E3M5) Blue", player), lambda state:
@@ -336,6 +333,13 @@ def set_episode3_rules(player, multiworld, pro):
state.has("Unholy Cathedral (E3M5) - Blue skull key", player, 1))
set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Yellow -> Unholy Cathedral (E3M5) Main", player), lambda state:
state.has("Unholy Cathedral (E3M5) - Yellow skull key", player, 1))
set_rule(multiworld.get_entrance("Hub -> Unholy Cathedral (E3M5) Start", player), lambda state:
state.has("Unholy Cathedral (E3M5)", player, 1))
set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Start -> Unholy Cathedral (E3M5) Main", player), lambda state:
(state.has("Chaingun", player, 1) and
state.has("Shotgun", player, 1)) and (state.has("Plasma gun", player, 1) or
state.has("Rocket launcher", player, 1) or
state.has("BFG9000", player, 1)))
# Mt. Erebus (E3M6)
set_rule(multiworld.get_entrance("Hub -> Mt. Erebus (E3M6) Main", player), lambda state:

View File

@@ -50,14 +50,14 @@ class DOOM1993World(World):
location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
location_name_groups = Locations.location_name_groups
starting_level_for_episode: List[str] = [
"Hangar (E1M1)",
"Deimos Anomaly (E2M1)",
"Hell Keep (E3M1)",
"Hell Beneath (E4M1)"
]
starting_level_for_episode: Dict[int, str] = {
1: "Hangar (E1M1)",
2: "Deimos Anomaly (E2M1)",
3: "Hell Keep (E3M1)",
4: "Hell Beneath (E4M1)"
}
boss_level_for_espidoes: List[str] = [
all_boss_levels: List[str] = [
"Phobos Anomaly (E1M8)",
"Tower of Babel (E2M8)",
"Dis (E3M8)",
@@ -82,6 +82,7 @@ class DOOM1993World(World):
def __init__(self, multiworld: MultiWorld, player: int):
self.included_episodes = [1, 1, 1, 0]
self.location_count = 0
self.starting_levels = []
super().__init__(multiworld, player)
@@ -99,6 +100,16 @@ class DOOM1993World(World):
if self.get_episode_count() == 0:
self.included_episodes[0] = 1
self.starting_levels = [level_name for (episode, level_name) in self.starting_level_for_episode.items()
if self.included_episodes[episode - 1]]
# Solo Episode 3 presents a problem, because Hell Keep has only two locations.
# We have to give the player Slough of Despair (E3M2), and also mark a weapon early.
if self.get_episode_count() == 1 and self.included_episodes[2]:
early_weapon = self.random.choice(["Shotgun", "Chaingun"])
self.multiworld.early_items[self.player][early_weapon] = 1
self.starting_levels.append("Slough of Despair (E3M2)")
def create_regions(self):
pro = self.options.pro.value
@@ -152,7 +163,7 @@ class DOOM1993World(World):
def completion_rule(self, state: CollectionState):
goal_levels = Maps.map_names
if self.options.goal.value:
goal_levels = self.boss_level_for_espidoes
goal_levels = self.all_boss_levels
for map_name in goal_levels:
if map_name + " - Exit" not in self.location_name_to_id:
@@ -201,7 +212,7 @@ class DOOM1993World(World):
if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]:
continue
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
count = item["count"] if item["name"] not in self.starting_levels else item["count"] - 1
itempool += [self.create_item(item["name"]) for _ in range(count)]
# Backpack(s) based on options
@@ -232,9 +243,8 @@ class DOOM1993World(World):
self.location_count -= 1
# Give starting levels right away
for i in range(len(self.included_episodes)):
if self.included_episodes[i]:
self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i]))
for map_name in self.starting_levels:
self.multiworld.push_precollected(self.create_item(map_name))
# Give Computer area maps if option selected
if self.options.start_with_computer_area_maps.value:

View File

@@ -412,7 +412,7 @@ item_table: Dict[int, ItemDict] = {
'map': 2},
360246: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Barrels o Fun (MAP23) - Yellow skull key',
'name': "Barrels o' Fun (MAP23) - Yellow skull key",
'doom_type': 39,
'episode': 3,
'map': 3},
@@ -880,19 +880,19 @@ item_table: Dict[int, ItemDict] = {
'map': 2},
360466: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Barrels o Fun (MAP23)',
'name': "Barrels o' Fun (MAP23)",
'doom_type': -1,
'episode': 3,
'map': 3},
360467: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Barrels o Fun (MAP23) - Complete',
'name': "Barrels o' Fun (MAP23) - Complete",
'doom_type': -2,
'episode': 3,
'map': 3},
360468: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Barrels o Fun (MAP23) - Computer area map',
'name': "Barrels o' Fun (MAP23) - Computer area map",
'doom_type': 2026,
'episode': 3,
'map': 3},
@@ -1024,37 +1024,37 @@ item_table: Dict[int, ItemDict] = {
'map': 10},
360490: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Wolfenstein2 (MAP31)',
'name': 'Wolfenstein (MAP31)',
'doom_type': -1,
'episode': 4,
'map': 1},
360491: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Wolfenstein2 (MAP31) - Complete',
'name': 'Wolfenstein (MAP31) - Complete',
'doom_type': -2,
'episode': 4,
'map': 1},
360492: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Wolfenstein2 (MAP31) - Computer area map',
'name': 'Wolfenstein (MAP31) - Computer area map',
'doom_type': 2026,
'episode': 4,
'map': 1},
360493: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Grosse2 (MAP32)',
'name': 'Grosse (MAP32)',
'doom_type': -1,
'episode': 4,
'map': 2},
360494: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Grosse2 (MAP32) - Complete',
'name': 'Grosse (MAP32) - Complete',
'doom_type': -2,
'episode': 4,
'map': 2},
360495: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Grosse2 (MAP32) - Computer area map',
'name': 'Grosse (MAP32) - Computer area map',
'doom_type': 2026,
'episode': 4,
'map': 2},
@@ -1087,9 +1087,9 @@ item_table: Dict[int, ItemDict] = {
item_name_groups: Dict[str, Set[str]] = {
'Ammos': {'Box of bullets', 'Box of rockets', 'Box of shotgun shells', 'Energy cell pack', },
'Computer area maps': {'Barrels o Fun (MAP23) - Computer area map', 'Bloodfalls (MAP25) - Computer area map', 'Circle of Death (MAP11) - Computer area map', 'Dead Simple (MAP07) - Computer area map', 'Downtown (MAP13) - Computer area map', 'Entryway (MAP01) - Computer area map', 'Gotcha! (MAP20) - Computer area map', 'Grosse2 (MAP32) - Computer area map', 'Icon of Sin (MAP30) - Computer area map', 'Industrial Zone (MAP15) - Computer area map', 'Monster Condo (MAP27) - Computer area map', 'Nirvana (MAP21) - Computer area map', 'Refueling Base (MAP10) - Computer area map', 'Suburbs (MAP16) - Computer area map', 'Tenements (MAP17) - Computer area map', 'The Abandoned Mines (MAP26) - Computer area map', 'The Catacombs (MAP22) - Computer area map', 'The Chasm (MAP24) - Computer area map', 'The Citadel (MAP19) - Computer area map', 'The Courtyard (MAP18) - Computer area map', 'The Crusher (MAP06) - Computer area map', 'The Factory (MAP12) - Computer area map', 'The Focus (MAP04) - Computer area map', 'The Gantlet (MAP03) - Computer area map', 'The Inmost Dens (MAP14) - Computer area map', 'The Living End (MAP29) - Computer area map', 'The Pit (MAP09) - Computer area map', 'The Spirit World (MAP28) - Computer area map', 'The Waste Tunnels (MAP05) - Computer area map', 'Tricks and Traps (MAP08) - Computer area map', 'Underhalls (MAP02) - Computer area map', 'Wolfenstein2 (MAP31) - Computer area map', },
'Keys': {'Barrels o Fun (MAP23) - Yellow skull key', 'Bloodfalls (MAP25) - Blue skull key', 'Circle of Death (MAP11) - Blue keycard', 'Circle of Death (MAP11) - Red keycard', 'Downtown (MAP13) - Blue keycard', 'Downtown (MAP13) - Red keycard', 'Downtown (MAP13) - Yellow keycard', 'Industrial Zone (MAP15) - Blue keycard', 'Industrial Zone (MAP15) - Red keycard', 'Industrial Zone (MAP15) - Yellow keycard', 'Monster Condo (MAP27) - Blue skull key', 'Monster Condo (MAP27) - Red skull key', 'Monster Condo (MAP27) - Yellow skull key', 'Nirvana (MAP21) - Blue skull key', 'Nirvana (MAP21) - Red skull key', 'Nirvana (MAP21) - Yellow skull key', 'Refueling Base (MAP10) - Blue keycard', 'Refueling Base (MAP10) - Yellow keycard', 'Suburbs (MAP16) - Blue skull key', 'Suburbs (MAP16) - Red skull key', 'Tenements (MAP17) - Blue keycard', 'Tenements (MAP17) - Red keycard', 'Tenements (MAP17) - Yellow skull key', 'The Abandoned Mines (MAP26) - Blue keycard', 'The Abandoned Mines (MAP26) - Red keycard', 'The Abandoned Mines (MAP26) - Yellow keycard', 'The Catacombs (MAP22) - Blue skull key', 'The Catacombs (MAP22) - Red skull key', 'The Chasm (MAP24) - Blue keycard', 'The Chasm (MAP24) - Red keycard', 'The Citadel (MAP19) - Blue skull key', 'The Citadel (MAP19) - Red skull key', 'The Citadel (MAP19) - Yellow skull key', 'The Courtyard (MAP18) - Blue skull key', 'The Courtyard (MAP18) - Yellow skull key', 'The Crusher (MAP06) - Blue keycard', 'The Crusher (MAP06) - Red keycard', 'The Crusher (MAP06) - Yellow keycard', 'The Factory (MAP12) - Blue keycard', 'The Factory (MAP12) - Yellow keycard', 'The Focus (MAP04) - Blue keycard', 'The Focus (MAP04) - Red keycard', 'The Focus (MAP04) - Yellow keycard', 'The Gantlet (MAP03) - Blue keycard', 'The Gantlet (MAP03) - Red keycard', 'The Inmost Dens (MAP14) - Blue skull key', 'The Inmost Dens (MAP14) - Red skull key', 'The Pit (MAP09) - Blue keycard', 'The Pit (MAP09) - Yellow keycard', 'The Spirit World (MAP28) - Red skull key', 'The Spirit World (MAP28) - Yellow skull key', 'The Waste Tunnels (MAP05) - Blue keycard', 'The Waste Tunnels (MAP05) - Red keycard', 'The Waste Tunnels (MAP05) - Yellow keycard', 'Tricks and Traps (MAP08) - Red skull key', 'Tricks and Traps (MAP08) - Yellow skull key', 'Underhalls (MAP02) - Blue keycard', 'Underhalls (MAP02) - Red keycard', },
'Levels': {'Barrels o Fun (MAP23)', 'Bloodfalls (MAP25)', 'Circle of Death (MAP11)', 'Dead Simple (MAP07)', 'Downtown (MAP13)', 'Entryway (MAP01)', 'Gotcha! (MAP20)', 'Grosse2 (MAP32)', 'Icon of Sin (MAP30)', 'Industrial Zone (MAP15)', 'Monster Condo (MAP27)', 'Nirvana (MAP21)', 'Refueling Base (MAP10)', 'Suburbs (MAP16)', 'Tenements (MAP17)', 'The Abandoned Mines (MAP26)', 'The Catacombs (MAP22)', 'The Chasm (MAP24)', 'The Citadel (MAP19)', 'The Courtyard (MAP18)', 'The Crusher (MAP06)', 'The Factory (MAP12)', 'The Focus (MAP04)', 'The Gantlet (MAP03)', 'The Inmost Dens (MAP14)', 'The Living End (MAP29)', 'The Pit (MAP09)', 'The Spirit World (MAP28)', 'The Waste Tunnels (MAP05)', 'Tricks and Traps (MAP08)', 'Underhalls (MAP02)', 'Wolfenstein2 (MAP31)', },
'Computer area maps': {"Barrels o' Fun (MAP23) - Computer area map", 'Bloodfalls (MAP25) - Computer area map', 'Circle of Death (MAP11) - Computer area map', 'Dead Simple (MAP07) - Computer area map', 'Downtown (MAP13) - Computer area map', 'Entryway (MAP01) - Computer area map', 'Gotcha! (MAP20) - Computer area map', 'Grosse (MAP32) - Computer area map', 'Icon of Sin (MAP30) - Computer area map', 'Industrial Zone (MAP15) - Computer area map', 'Monster Condo (MAP27) - Computer area map', 'Nirvana (MAP21) - Computer area map', 'Refueling Base (MAP10) - Computer area map', 'Suburbs (MAP16) - Computer area map', 'Tenements (MAP17) - Computer area map', 'The Abandoned Mines (MAP26) - Computer area map', 'The Catacombs (MAP22) - Computer area map', 'The Chasm (MAP24) - Computer area map', 'The Citadel (MAP19) - Computer area map', 'The Courtyard (MAP18) - Computer area map', 'The Crusher (MAP06) - Computer area map', 'The Factory (MAP12) - Computer area map', 'The Focus (MAP04) - Computer area map', 'The Gantlet (MAP03) - Computer area map', 'The Inmost Dens (MAP14) - Computer area map', 'The Living End (MAP29) - Computer area map', 'The Pit (MAP09) - Computer area map', 'The Spirit World (MAP28) - Computer area map', 'The Waste Tunnels (MAP05) - Computer area map', 'Tricks and Traps (MAP08) - Computer area map', 'Underhalls (MAP02) - Computer area map', 'Wolfenstein (MAP31) - Computer area map', },
'Keys': {"Barrels o' Fun (MAP23) - Yellow skull key", 'Bloodfalls (MAP25) - Blue skull key', 'Circle of Death (MAP11) - Blue keycard', 'Circle of Death (MAP11) - Red keycard', 'Downtown (MAP13) - Blue keycard', 'Downtown (MAP13) - Red keycard', 'Downtown (MAP13) - Yellow keycard', 'Industrial Zone (MAP15) - Blue keycard', 'Industrial Zone (MAP15) - Red keycard', 'Industrial Zone (MAP15) - Yellow keycard', 'Monster Condo (MAP27) - Blue skull key', 'Monster Condo (MAP27) - Red skull key', 'Monster Condo (MAP27) - Yellow skull key', 'Nirvana (MAP21) - Blue skull key', 'Nirvana (MAP21) - Red skull key', 'Nirvana (MAP21) - Yellow skull key', 'Refueling Base (MAP10) - Blue keycard', 'Refueling Base (MAP10) - Yellow keycard', 'Suburbs (MAP16) - Blue skull key', 'Suburbs (MAP16) - Red skull key', 'Tenements (MAP17) - Blue keycard', 'Tenements (MAP17) - Red keycard', 'Tenements (MAP17) - Yellow skull key', 'The Abandoned Mines (MAP26) - Blue keycard', 'The Abandoned Mines (MAP26) - Red keycard', 'The Abandoned Mines (MAP26) - Yellow keycard', 'The Catacombs (MAP22) - Blue skull key', 'The Catacombs (MAP22) - Red skull key', 'The Chasm (MAP24) - Blue keycard', 'The Chasm (MAP24) - Red keycard', 'The Citadel (MAP19) - Blue skull key', 'The Citadel (MAP19) - Red skull key', 'The Citadel (MAP19) - Yellow skull key', 'The Courtyard (MAP18) - Blue skull key', 'The Courtyard (MAP18) - Yellow skull key', 'The Crusher (MAP06) - Blue keycard', 'The Crusher (MAP06) - Red keycard', 'The Crusher (MAP06) - Yellow keycard', 'The Factory (MAP12) - Blue keycard', 'The Factory (MAP12) - Yellow keycard', 'The Focus (MAP04) - Blue keycard', 'The Focus (MAP04) - Red keycard', 'The Focus (MAP04) - Yellow keycard', 'The Gantlet (MAP03) - Blue keycard', 'The Gantlet (MAP03) - Red keycard', 'The Inmost Dens (MAP14) - Blue skull key', 'The Inmost Dens (MAP14) - Red skull key', 'The Pit (MAP09) - Blue keycard', 'The Pit (MAP09) - Yellow keycard', 'The Spirit World (MAP28) - Red skull key', 'The Spirit World (MAP28) - Yellow skull key', 'The Waste Tunnels (MAP05) - Blue keycard', 'The Waste Tunnels (MAP05) - Red keycard', 'The Waste Tunnels (MAP05) - Yellow keycard', 'Tricks and Traps (MAP08) - Red skull key', 'Tricks and Traps (MAP08) - Yellow skull key', 'Underhalls (MAP02) - Blue keycard', 'Underhalls (MAP02) - Red keycard', },
'Levels': {"Barrels o' Fun (MAP23)", 'Bloodfalls (MAP25)', 'Circle of Death (MAP11)', 'Dead Simple (MAP07)', 'Downtown (MAP13)', 'Entryway (MAP01)', 'Gotcha! (MAP20)', 'Grosse (MAP32)', 'Icon of Sin (MAP30)', 'Industrial Zone (MAP15)', 'Monster Condo (MAP27)', 'Nirvana (MAP21)', 'Refueling Base (MAP10)', 'Suburbs (MAP16)', 'Tenements (MAP17)', 'The Abandoned Mines (MAP26)', 'The Catacombs (MAP22)', 'The Chasm (MAP24)', 'The Citadel (MAP19)', 'The Courtyard (MAP18)', 'The Crusher (MAP06)', 'The Factory (MAP12)', 'The Focus (MAP04)', 'The Gantlet (MAP03)', 'The Inmost Dens (MAP14)', 'The Living End (MAP29)', 'The Pit (MAP09)', 'The Spirit World (MAP28)', 'The Waste Tunnels (MAP05)', 'Tricks and Traps (MAP08)', 'Underhalls (MAP02)', 'Wolfenstein (MAP31)', },
'Powerups': {'Armor', 'Berserk', 'Invulnerability', 'Mega Armor', 'Megasphere', 'Partial invisibility', 'Supercharge', },
'Weapons': {'BFG9000', 'Chaingun', 'Chainsaw', 'Plasma gun', 'Rocket launcher', 'Shotgun', 'Super Shotgun', },
}

View File

@@ -180,7 +180,7 @@ location_table: Dict[int, LocationDict] = {
'map': 5,
'index': 46,
'doom_type': 82,
'region': "The Waste Tunnels (MAP05) Main"},
'region': "The Waste Tunnels (MAP05) Start"},
361028: {'name': 'The Waste Tunnels (MAP05) - Blue keycard',
'episode': 1,
'map': 5,
@@ -234,7 +234,7 @@ location_table: Dict[int, LocationDict] = {
'map': 5,
'index': 202,
'doom_type': 2001,
'region': "The Waste Tunnels (MAP05) Main"},
'region': "The Waste Tunnels (MAP05) Start"},
361037: {'name': 'The Waste Tunnels (MAP05) - Berserk',
'episode': 1,
'map': 5,
@@ -360,7 +360,7 @@ location_table: Dict[int, LocationDict] = {
'map': 7,
'index': 8,
'doom_type': 82,
'region': "Dead Simple (MAP07) Main"},
'region': "Dead Simple (MAP07) Start"},
361058: {'name': 'Dead Simple (MAP07) - Chaingun',
'episode': 1,
'map': 7,
@@ -378,7 +378,7 @@ location_table: Dict[int, LocationDict] = {
'map': 7,
'index': 43,
'doom_type': 8,
'region': "Dead Simple (MAP07) Main"},
'region': "Dead Simple (MAP07) Start"},
361061: {'name': 'Dead Simple (MAP07) - Berserk',
'episode': 1,
'map': 7,
@@ -570,7 +570,7 @@ location_table: Dict[int, LocationDict] = {
'map': 9,
'index': 26,
'doom_type': 2019,
'region': "The Pit (MAP09) Main"},
'region': "The Pit (MAP09) Start"},
361093: {'name': 'The Pit (MAP09) - Supercharge',
'episode': 1,
'map': 9,
@@ -678,7 +678,7 @@ location_table: Dict[int, LocationDict] = {
'map': 10,
'index': 99,
'doom_type': 2001,
'region': "Refueling Base (MAP10) Main"},
'region': "Refueling Base (MAP10) Start"},
361111: {'name': 'Refueling Base (MAP10) - Chaingun',
'episode': 1,
'map': 10,
@@ -846,31 +846,31 @@ location_table: Dict[int, LocationDict] = {
'map': 11,
'index': 88,
'doom_type': 8,
'region': "Circle of Death (MAP11) Red"},
'region': "Circle of Death (MAP11) Ending"},
361139: {'name': 'Circle of Death (MAP11) - Supercharge 2',
'episode': 1,
'map': 11,
'index': 108,
'doom_type': 2013,
'region': "Circle of Death (MAP11) Red"},
'region': "Circle of Death (MAP11) Ending"},
361140: {'name': 'Circle of Death (MAP11) - BFG9000',
'episode': 1,
'map': 11,
'index': 110,
'doom_type': 2006,
'region': "Circle of Death (MAP11) Red"},
'region': "Circle of Death (MAP11) Ending"},
361141: {'name': 'Circle of Death (MAP11) - Exit',
'episode': 1,
'map': 11,
'index': -1,
'doom_type': -1,
'region': "Circle of Death (MAP11) Red"},
'region': "Circle of Death (MAP11) Ending"},
361142: {'name': 'The Factory (MAP12) - Shotgun',
'episode': 2,
'map': 1,
'index': 14,
'doom_type': 2001,
'region': "The Factory (MAP12) Main"},
'region': "The Factory (MAP12) Outdoors"},
361143: {'name': 'The Factory (MAP12) - Berserk',
'episode': 2,
'map': 1,
@@ -888,13 +888,13 @@ location_table: Dict[int, LocationDict] = {
'map': 1,
'index': 52,
'doom_type': 2013,
'region': "The Factory (MAP12) Main"},
'region': "The Factory (MAP12) Indoors"},
361146: {'name': 'The Factory (MAP12) - Blue keycard',
'episode': 2,
'map': 1,
'index': 54,
'doom_type': 5,
'region': "The Factory (MAP12) Main"},
'region': "The Factory (MAP12) Indoors"},
361147: {'name': 'The Factory (MAP12) - Armor',
'episode': 2,
'map': 1,
@@ -912,31 +912,31 @@ location_table: Dict[int, LocationDict] = {
'map': 1,
'index': 83,
'doom_type': 2013,
'region': "The Factory (MAP12) Main"},
'region': "The Factory (MAP12) Indoors"},
361150: {'name': 'The Factory (MAP12) - Armor 2',
'episode': 2,
'map': 1,
'index': 92,
'doom_type': 2018,
'region': "The Factory (MAP12) Main"},
'region': "The Factory (MAP12) Outdoors"},
361151: {'name': 'The Factory (MAP12) - Partial invisibility',
'episode': 2,
'map': 1,
'index': 93,
'doom_type': 2024,
'region': "The Factory (MAP12) Main"},
'region': "The Factory (MAP12) Outdoors"},
361152: {'name': 'The Factory (MAP12) - Berserk 2',
'episode': 2,
'map': 1,
'index': 107,
'doom_type': 2023,
'region': "The Factory (MAP12) Main"},
'region': "The Factory (MAP12) Indoors"},
361153: {'name': 'The Factory (MAP12) - Yellow keycard',
'episode': 2,
'map': 1,
'index': 123,
'doom_type': 6,
'region': "The Factory (MAP12) Main"},
'region': "The Factory (MAP12) Indoors"},
361154: {'name': 'The Factory (MAP12) - BFG9000',
'episode': 2,
'map': 1,
@@ -954,7 +954,7 @@ location_table: Dict[int, LocationDict] = {
'map': 1,
'index': 192,
'doom_type': 82,
'region': "The Factory (MAP12) Main"},
'region': "The Factory (MAP12) Indoors"},
361157: {'name': 'The Factory (MAP12) - Exit',
'episode': 2,
'map': 1,
@@ -1812,7 +1812,7 @@ location_table: Dict[int, LocationDict] = {
'map': 1,
'index': 70,
'doom_type': 82,
'region': "Nirvana (MAP21) Main"},
'region': "Nirvana (MAP21) Start"},
361300: {'name': 'Nirvana (MAP21) - Rocket launcher',
'episode': 3,
'map': 1,
@@ -1884,7 +1884,7 @@ location_table: Dict[int, LocationDict] = {
'map': 2,
'index': 28,
'doom_type': 2001,
'region': "The Catacombs (MAP22) Main"},
'region': "The Catacombs (MAP22) Early"},
361312: {'name': 'The Catacombs (MAP22) - Berserk',
'episode': 3,
'map': 2,
@@ -1896,103 +1896,103 @@ location_table: Dict[int, LocationDict] = {
'map': 2,
'index': 83,
'doom_type': 2004,
'region': "The Catacombs (MAP22) Main"},
'region': "The Catacombs (MAP22) Early"},
361314: {'name': 'The Catacombs (MAP22) - Supercharge',
'episode': 3,
'map': 2,
'index': 118,
'doom_type': 2013,
'region': "The Catacombs (MAP22) Main"},
'region': "The Catacombs (MAP22) Early"},
361315: {'name': 'The Catacombs (MAP22) - Armor',
'episode': 3,
'map': 2,
'index': 119,
'doom_type': 2018,
'region': "The Catacombs (MAP22) Main"},
'region': "The Catacombs (MAP22) Early"},
361316: {'name': 'The Catacombs (MAP22) - Exit',
'episode': 3,
'map': 2,
'index': -1,
'doom_type': -1,
'region': "The Catacombs (MAP22) Red"},
361317: {'name': 'Barrels o Fun (MAP23) - Shotgun',
361317: {'name': "Barrels o' Fun (MAP23) - Shotgun",
'episode': 3,
'map': 3,
'index': 136,
'doom_type': 2001,
'region': "Barrels o Fun (MAP23) Main"},
361318: {'name': 'Barrels o Fun (MAP23) - Berserk',
'region': "Barrels o' Fun (MAP23) Main"},
361318: {'name': "Barrels o' Fun (MAP23) - Berserk",
'episode': 3,
'map': 3,
'index': 222,
'doom_type': 2023,
'region': "Barrels o Fun (MAP23) Main"},
361319: {'name': 'Barrels o Fun (MAP23) - Backpack',
'region': "Barrels o' Fun (MAP23) Main"},
361319: {'name': "Barrels o' Fun (MAP23) - Backpack",
'episode': 3,
'map': 3,
'index': 223,
'doom_type': 8,
'region': "Barrels o Fun (MAP23) Main"},
361320: {'name': 'Barrels o Fun (MAP23) - Computer area map',
'region': "Barrels o' Fun (MAP23) Main"},
361320: {'name': "Barrels o' Fun (MAP23) - Computer area map",
'episode': 3,
'map': 3,
'index': 224,
'doom_type': 2026,
'region': "Barrels o Fun (MAP23) Main"},
361321: {'name': 'Barrels o Fun (MAP23) - Armor',
'region': "Barrels o' Fun (MAP23) Main"},
361321: {'name': "Barrels o' Fun (MAP23) - Armor",
'episode': 3,
'map': 3,
'index': 249,
'doom_type': 2018,
'region': "Barrels o Fun (MAP23) Main"},
361322: {'name': 'Barrels o Fun (MAP23) - Rocket launcher',
'region': "Barrels o' Fun (MAP23) Main"},
361322: {'name': "Barrels o' Fun (MAP23) - Rocket launcher",
'episode': 3,
'map': 3,
'index': 264,
'doom_type': 2003,
'region': "Barrels o Fun (MAP23) Main"},
361323: {'name': 'Barrels o Fun (MAP23) - Megasphere',
'region': "Barrels o' Fun (MAP23) Main"},
361323: {'name': "Barrels o' Fun (MAP23) - Megasphere",
'episode': 3,
'map': 3,
'index': 266,
'doom_type': 83,
'region': "Barrels o Fun (MAP23) Main"},
361324: {'name': 'Barrels o Fun (MAP23) - Supercharge',
'region': "Barrels o' Fun (MAP23) Main"},
361324: {'name': "Barrels o' Fun (MAP23) - Supercharge",
'episode': 3,
'map': 3,
'index': 277,
'doom_type': 2013,
'region': "Barrels o Fun (MAP23) Main"},
361325: {'name': 'Barrels o Fun (MAP23) - Backpack 2',
'region': "Barrels o' Fun (MAP23) Main"},
361325: {'name': "Barrels o' Fun (MAP23) - Backpack 2",
'episode': 3,
'map': 3,
'index': 301,
'doom_type': 8,
'region': "Barrels o Fun (MAP23) Main"},
361326: {'name': 'Barrels o Fun (MAP23) - Yellow skull key',
'region': "Barrels o' Fun (MAP23) Main"},
361326: {'name': "Barrels o' Fun (MAP23) - Yellow skull key",
'episode': 3,
'map': 3,
'index': 307,
'doom_type': 39,
'region': "Barrels o Fun (MAP23) Main"},
361327: {'name': 'Barrels o Fun (MAP23) - BFG9000',
'region': "Barrels o' Fun (MAP23) Main"},
361327: {'name': "Barrels o' Fun (MAP23) - BFG9000",
'episode': 3,
'map': 3,
'index': 342,
'doom_type': 2006,
'region': "Barrels o Fun (MAP23) Main"},
361328: {'name': 'Barrels o Fun (MAP23) - Exit',
'region': "Barrels o' Fun (MAP23) Main"},
361328: {'name': "Barrels o' Fun (MAP23) - Exit",
'episode': 3,
'map': 3,
'index': -1,
'doom_type': -1,
'region': "Barrels o Fun (MAP23) Yellow"},
'region': "Barrels o' Fun (MAP23) Yellow"},
361329: {'name': 'The Chasm (MAP24) - Plasma gun',
'episode': 3,
'map': 4,
'index': 5,
'doom_type': 2004,
'region': "The Chasm (MAP24) Main"},
'region': "The Chasm (MAP24) Blue"},
361330: {'name': 'The Chasm (MAP24) - Shotgun',
'episode': 3,
'map': 4,
@@ -2004,7 +2004,7 @@ location_table: Dict[int, LocationDict] = {
'map': 4,
'index': 12,
'doom_type': 2022,
'region': "The Chasm (MAP24) Main"},
'region': "The Chasm (MAP24) Blue"},
361332: {'name': 'The Chasm (MAP24) - Rocket launcher',
'episode': 3,
'map': 4,
@@ -2022,7 +2022,7 @@ location_table: Dict[int, LocationDict] = {
'map': 4,
'index': 31,
'doom_type': 8,
'region': "The Chasm (MAP24) Main"},
'region': "The Chasm (MAP24) Blue"},
361335: {'name': 'The Chasm (MAP24) - Berserk',
'episode': 3,
'map': 4,
@@ -2034,19 +2034,19 @@ location_table: Dict[int, LocationDict] = {
'map': 4,
'index': 155,
'doom_type': 2023,
'region': "The Chasm (MAP24) Main"},
'region': "The Chasm (MAP24) Blue"},
361337: {'name': 'The Chasm (MAP24) - Armor',
'episode': 3,
'map': 4,
'index': 169,
'doom_type': 2018,
'region': "The Chasm (MAP24) Main"},
'region': "The Chasm (MAP24) Blue"},
361338: {'name': 'The Chasm (MAP24) - Red keycard',
'episode': 3,
'map': 4,
'index': 261,
'doom_type': 13,
'region': "The Chasm (MAP24) Main"},
'region': "The Chasm (MAP24) Blue"},
361339: {'name': 'The Chasm (MAP24) - BFG9000',
'episode': 3,
'map': 4,
@@ -2064,7 +2064,7 @@ location_table: Dict[int, LocationDict] = {
'map': 4,
'index': 355,
'doom_type': 83,
'region': "The Chasm (MAP24) Main"},
'region': "The Chasm (MAP24) Blue"},
361342: {'name': 'The Chasm (MAP24) - Megasphere 2',
'episode': 3,
'map': 4,
@@ -2082,7 +2082,7 @@ location_table: Dict[int, LocationDict] = {
'map': 5,
'index': 6,
'doom_type': 82,
'region': "Bloodfalls (MAP25) Main"},
'region': "Bloodfalls (MAP25) Start"},
361345: {'name': 'Bloodfalls (MAP25) - Partial invisibility',
'episode': 3,
'map': 5,
@@ -2664,55 +2664,55 @@ location_table: Dict[int, LocationDict] = {
'map': 10,
'index': 40,
'doom_type': 2006,
'region': "Icon of Sin (MAP30) Main"},
'region': "Icon of Sin (MAP30) Start"},
361442: {'name': 'Icon of Sin (MAP30) - Chaingun',
'episode': 3,
'map': 10,
'index': 41,
'doom_type': 2002,
'region': "Icon of Sin (MAP30) Main"},
'region': "Icon of Sin (MAP30) Start"},
361443: {'name': 'Icon of Sin (MAP30) - Chainsaw',
'episode': 3,
'map': 10,
'index': 42,
'doom_type': 2005,
'region': "Icon of Sin (MAP30) Main"},
'region': "Icon of Sin (MAP30) Start"},
361444: {'name': 'Icon of Sin (MAP30) - Plasma gun',
'episode': 3,
'map': 10,
'index': 43,
'doom_type': 2004,
'region': "Icon of Sin (MAP30) Main"},
'region': "Icon of Sin (MAP30) Start"},
361445: {'name': 'Icon of Sin (MAP30) - Rocket launcher',
'episode': 3,
'map': 10,
'index': 44,
'doom_type': 2003,
'region': "Icon of Sin (MAP30) Main"},
'region': "Icon of Sin (MAP30) Start"},
361446: {'name': 'Icon of Sin (MAP30) - Shotgun',
'episode': 3,
'map': 10,
'index': 45,
'doom_type': 2001,
'region': "Icon of Sin (MAP30) Main"},
'region': "Icon of Sin (MAP30) Start"},
361447: {'name': 'Icon of Sin (MAP30) - Super Shotgun',
'episode': 3,
'map': 10,
'index': 46,
'doom_type': 82,
'region': "Icon of Sin (MAP30) Main"},
'region': "Icon of Sin (MAP30) Start"},
361448: {'name': 'Icon of Sin (MAP30) - Backpack',
'episode': 3,
'map': 10,
'index': 47,
'doom_type': 8,
'region': "Icon of Sin (MAP30) Main"},
'region': "Icon of Sin (MAP30) Start"},
361449: {'name': 'Icon of Sin (MAP30) - Megasphere',
'episode': 3,
'map': 10,
'index': 64,
'doom_type': 83,
'region': "Icon of Sin (MAP30) Main"},
'region': "Icon of Sin (MAP30) Start"},
361450: {'name': 'Icon of Sin (MAP30) - Megasphere 2',
'episode': 3,
'map': 10,
@@ -2731,179 +2731,179 @@ location_table: Dict[int, LocationDict] = {
'index': -1,
'doom_type': -1,
'region': "Icon of Sin (MAP30) Main"},
361453: {'name': 'Wolfenstein2 (MAP31) - Rocket launcher',
361453: {'name': 'Wolfenstein (MAP31) - Rocket launcher',
'episode': 4,
'map': 1,
'index': 110,
'doom_type': 2003,
'region': "Wolfenstein2 (MAP31) Main"},
361454: {'name': 'Wolfenstein2 (MAP31) - Shotgun',
'region': "Wolfenstein (MAP31) Main"},
361454: {'name': 'Wolfenstein (MAP31) - Shotgun',
'episode': 4,
'map': 1,
'index': 139,
'doom_type': 2001,
'region': "Wolfenstein2 (MAP31) Main"},
361455: {'name': 'Wolfenstein2 (MAP31) - Berserk',
'region': "Wolfenstein (MAP31) Main"},
361455: {'name': 'Wolfenstein (MAP31) - Berserk',
'episode': 4,
'map': 1,
'index': 263,
'doom_type': 2023,
'region': "Wolfenstein2 (MAP31) Main"},
361456: {'name': 'Wolfenstein2 (MAP31) - Supercharge',
'region': "Wolfenstein (MAP31) Main"},
361456: {'name': 'Wolfenstein (MAP31) - Supercharge',
'episode': 4,
'map': 1,
'index': 278,
'doom_type': 2013,
'region': "Wolfenstein2 (MAP31) Main"},
361457: {'name': 'Wolfenstein2 (MAP31) - Chaingun',
'region': "Wolfenstein (MAP31) Main"},
361457: {'name': 'Wolfenstein (MAP31) - Chaingun',
'episode': 4,
'map': 1,
'index': 305,
'doom_type': 2002,
'region': "Wolfenstein2 (MAP31) Main"},
361458: {'name': 'Wolfenstein2 (MAP31) - Super Shotgun',
'region': "Wolfenstein (MAP31) Main"},
361458: {'name': 'Wolfenstein (MAP31) - Super Shotgun',
'episode': 4,
'map': 1,
'index': 308,
'doom_type': 82,
'region': "Wolfenstein2 (MAP31) Main"},
361459: {'name': 'Wolfenstein2 (MAP31) - Partial invisibility',
'region': "Wolfenstein (MAP31) Main"},
361459: {'name': 'Wolfenstein (MAP31) - Partial invisibility',
'episode': 4,
'map': 1,
'index': 309,
'doom_type': 2024,
'region': "Wolfenstein2 (MAP31) Main"},
361460: {'name': 'Wolfenstein2 (MAP31) - Megasphere',
'region': "Wolfenstein (MAP31) Main"},
361460: {'name': 'Wolfenstein (MAP31) - Megasphere',
'episode': 4,
'map': 1,
'index': 310,
'doom_type': 83,
'region': "Wolfenstein2 (MAP31) Main"},
361461: {'name': 'Wolfenstein2 (MAP31) - Backpack',
'region': "Wolfenstein (MAP31) Main"},
361461: {'name': 'Wolfenstein (MAP31) - Backpack',
'episode': 4,
'map': 1,
'index': 311,
'doom_type': 8,
'region': "Wolfenstein2 (MAP31) Main"},
361462: {'name': 'Wolfenstein2 (MAP31) - Backpack 2',
'region': "Wolfenstein (MAP31) Main"},
361462: {'name': 'Wolfenstein (MAP31) - Backpack 2',
'episode': 4,
'map': 1,
'index': 312,
'doom_type': 8,
'region': "Wolfenstein2 (MAP31) Main"},
361463: {'name': 'Wolfenstein2 (MAP31) - Backpack 3',
'region': "Wolfenstein (MAP31) Main"},
361463: {'name': 'Wolfenstein (MAP31) - Backpack 3',
'episode': 4,
'map': 1,
'index': 313,
'doom_type': 8,
'region': "Wolfenstein2 (MAP31) Main"},
361464: {'name': 'Wolfenstein2 (MAP31) - Backpack 4',
'region': "Wolfenstein (MAP31) Main"},
361464: {'name': 'Wolfenstein (MAP31) - Backpack 4',
'episode': 4,
'map': 1,
'index': 314,
'doom_type': 8,
'region': "Wolfenstein2 (MAP31) Main"},
361465: {'name': 'Wolfenstein2 (MAP31) - BFG9000',
'region': "Wolfenstein (MAP31) Main"},
361465: {'name': 'Wolfenstein (MAP31) - BFG9000',
'episode': 4,
'map': 1,
'index': 315,
'doom_type': 2006,
'region': "Wolfenstein2 (MAP31) Main"},
361466: {'name': 'Wolfenstein2 (MAP31) - Plasma gun',
'region': "Wolfenstein (MAP31) Main"},
361466: {'name': 'Wolfenstein (MAP31) - Plasma gun',
'episode': 4,
'map': 1,
'index': 316,
'doom_type': 2004,
'region': "Wolfenstein2 (MAP31) Main"},
361467: {'name': 'Wolfenstein2 (MAP31) - Exit',
'region': "Wolfenstein (MAP31) Main"},
361467: {'name': 'Wolfenstein (MAP31) - Exit',
'episode': 4,
'map': 1,
'index': -1,
'doom_type': -1,
'region': "Wolfenstein2 (MAP31) Main"},
361468: {'name': 'Grosse2 (MAP32) - Plasma gun',
'region': "Wolfenstein (MAP31) Main"},
361468: {'name': 'Grosse (MAP32) - Plasma gun',
'episode': 4,
'map': 2,
'index': 33,
'doom_type': 2004,
'region': "Grosse2 (MAP32) Main"},
361469: {'name': 'Grosse2 (MAP32) - Rocket launcher',
'region': "Grosse (MAP32) Main"},
361469: {'name': 'Grosse (MAP32) - Rocket launcher',
'episode': 4,
'map': 2,
'index': 57,
'doom_type': 2003,
'region': "Grosse2 (MAP32) Main"},
361470: {'name': 'Grosse2 (MAP32) - Invulnerability',
'region': "Grosse (MAP32) Start"},
361470: {'name': 'Grosse (MAP32) - Invulnerability',
'episode': 4,
'map': 2,
'index': 70,
'doom_type': 2022,
'region': "Grosse2 (MAP32) Main"},
361471: {'name': 'Grosse2 (MAP32) - Super Shotgun',
'region': "Grosse (MAP32) Main"},
361471: {'name': 'Grosse (MAP32) - Super Shotgun',
'episode': 4,
'map': 2,
'index': 74,
'doom_type': 82,
'region': "Grosse2 (MAP32) Main"},
361472: {'name': 'Grosse2 (MAP32) - BFG9000',
'region': "Grosse (MAP32) Main"},
361472: {'name': 'Grosse (MAP32) - BFG9000',
'episode': 4,
'map': 2,
'index': 75,
'doom_type': 2006,
'region': "Grosse2 (MAP32) Main"},
361473: {'name': 'Grosse2 (MAP32) - Megasphere',
'region': "Grosse (MAP32) Main"},
361473: {'name': 'Grosse (MAP32) - Megasphere',
'episode': 4,
'map': 2,
'index': 78,
'doom_type': 83,
'region': "Grosse2 (MAP32) Main"},
361474: {'name': 'Grosse2 (MAP32) - Chaingun',
'region': "Grosse (MAP32) Main"},
361474: {'name': 'Grosse (MAP32) - Chaingun',
'episode': 4,
'map': 2,
'index': 79,
'doom_type': 2002,
'region': "Grosse2 (MAP32) Main"},
361475: {'name': 'Grosse2 (MAP32) - Chaingun 2',
'region': "Grosse (MAP32) Main"},
361475: {'name': 'Grosse (MAP32) - Chaingun 2',
'episode': 4,
'map': 2,
'index': 80,
'doom_type': 2002,
'region': "Grosse2 (MAP32) Main"},
361476: {'name': 'Grosse2 (MAP32) - Chaingun 3',
'region': "Grosse (MAP32) Main"},
361476: {'name': 'Grosse (MAP32) - Chaingun 3',
'episode': 4,
'map': 2,
'index': 81,
'doom_type': 2002,
'region': "Grosse2 (MAP32) Main"},
361477: {'name': 'Grosse2 (MAP32) - Berserk',
'region': "Grosse (MAP32) Main"},
361477: {'name': 'Grosse (MAP32) - Berserk',
'episode': 4,
'map': 2,
'index': 82,
'doom_type': 2023,
'region': "Grosse2 (MAP32) Main"},
361478: {'name': 'Grosse2 (MAP32) - Exit',
'region': "Grosse (MAP32) Start"},
361478: {'name': 'Grosse (MAP32) - Exit',
'episode': 4,
'map': 2,
'index': -1,
'doom_type': -1,
'region': "Grosse2 (MAP32) Main"},
'region': "Grosse (MAP32) Main"},
}
location_name_groups: Dict[str, Set[str]] = {
'Barrels o Fun (MAP23)': {
'Barrels o Fun (MAP23) - Armor',
'Barrels o Fun (MAP23) - BFG9000',
'Barrels o Fun (MAP23) - Backpack',
'Barrels o Fun (MAP23) - Backpack 2',
'Barrels o Fun (MAP23) - Berserk',
'Barrels o Fun (MAP23) - Computer area map',
'Barrels o Fun (MAP23) - Exit',
'Barrels o Fun (MAP23) - Megasphere',
'Barrels o Fun (MAP23) - Rocket launcher',
'Barrels o Fun (MAP23) - Shotgun',
'Barrels o Fun (MAP23) - Supercharge',
'Barrels o Fun (MAP23) - Yellow skull key',
"Barrels o' Fun (MAP23)": {
"Barrels o' Fun (MAP23) - Armor",
"Barrels o' Fun (MAP23) - BFG9000",
"Barrels o' Fun (MAP23) - Backpack",
"Barrels o' Fun (MAP23) - Backpack 2",
"Barrels o' Fun (MAP23) - Berserk",
"Barrels o' Fun (MAP23) - Computer area map",
"Barrels o' Fun (MAP23) - Exit",
"Barrels o' Fun (MAP23) - Megasphere",
"Barrels o' Fun (MAP23) - Rocket launcher",
"Barrels o' Fun (MAP23) - Shotgun",
"Barrels o' Fun (MAP23) - Supercharge",
"Barrels o' Fun (MAP23) - Yellow skull key",
},
'Bloodfalls (MAP25)': {
'Bloodfalls (MAP25) - Armor',
@@ -2998,18 +2998,18 @@ location_name_groups: Dict[str, Set[str]] = {
'Gotcha! (MAP20) - Supercharge 3',
'Gotcha! (MAP20) - Supercharge 4',
},
'Grosse2 (MAP32)': {
'Grosse2 (MAP32) - BFG9000',
'Grosse2 (MAP32) - Berserk',
'Grosse2 (MAP32) - Chaingun',
'Grosse2 (MAP32) - Chaingun 2',
'Grosse2 (MAP32) - Chaingun 3',
'Grosse2 (MAP32) - Exit',
'Grosse2 (MAP32) - Invulnerability',
'Grosse2 (MAP32) - Megasphere',
'Grosse2 (MAP32) - Plasma gun',
'Grosse2 (MAP32) - Rocket launcher',
'Grosse2 (MAP32) - Super Shotgun',
'Grosse (MAP32)': {
'Grosse (MAP32) - BFG9000',
'Grosse (MAP32) - Berserk',
'Grosse (MAP32) - Chaingun',
'Grosse (MAP32) - Chaingun 2',
'Grosse (MAP32) - Chaingun 3',
'Grosse (MAP32) - Exit',
'Grosse (MAP32) - Invulnerability',
'Grosse (MAP32) - Megasphere',
'Grosse (MAP32) - Plasma gun',
'Grosse (MAP32) - Rocket launcher',
'Grosse (MAP32) - Super Shotgun',
},
'Icon of Sin (MAP30)': {
'Icon of Sin (MAP30) - BFG9000',
@@ -3417,22 +3417,22 @@ location_name_groups: Dict[str, Set[str]] = {
'Underhalls (MAP02) - Red keycard',
'Underhalls (MAP02) - Super Shotgun',
},
'Wolfenstein2 (MAP31)': {
'Wolfenstein2 (MAP31) - BFG9000',
'Wolfenstein2 (MAP31) - Backpack',
'Wolfenstein2 (MAP31) - Backpack 2',
'Wolfenstein2 (MAP31) - Backpack 3',
'Wolfenstein2 (MAP31) - Backpack 4',
'Wolfenstein2 (MAP31) - Berserk',
'Wolfenstein2 (MAP31) - Chaingun',
'Wolfenstein2 (MAP31) - Exit',
'Wolfenstein2 (MAP31) - Megasphere',
'Wolfenstein2 (MAP31) - Partial invisibility',
'Wolfenstein2 (MAP31) - Plasma gun',
'Wolfenstein2 (MAP31) - Rocket launcher',
'Wolfenstein2 (MAP31) - Shotgun',
'Wolfenstein2 (MAP31) - Super Shotgun',
'Wolfenstein2 (MAP31) - Supercharge',
'Wolfenstein (MAP31)': {
'Wolfenstein (MAP31) - BFG9000',
'Wolfenstein (MAP31) - Backpack',
'Wolfenstein (MAP31) - Backpack 2',
'Wolfenstein (MAP31) - Backpack 3',
'Wolfenstein (MAP31) - Backpack 4',
'Wolfenstein (MAP31) - Berserk',
'Wolfenstein (MAP31) - Chaingun',
'Wolfenstein (MAP31) - Exit',
'Wolfenstein (MAP31) - Megasphere',
'Wolfenstein (MAP31) - Partial invisibility',
'Wolfenstein (MAP31) - Plasma gun',
'Wolfenstein (MAP31) - Rocket launcher',
'Wolfenstein (MAP31) - Shotgun',
'Wolfenstein (MAP31) - Super Shotgun',
'Wolfenstein (MAP31) - Supercharge',
},
}

View File

@@ -26,7 +26,7 @@ map_names: List[str] = [
'Gotcha! (MAP20)',
'Nirvana (MAP21)',
'The Catacombs (MAP22)',
'Barrels o Fun (MAP23)',
"Barrels o' Fun (MAP23)",
'The Chasm (MAP24)',
'Bloodfalls (MAP25)',
'The Abandoned Mines (MAP26)',
@@ -34,6 +34,6 @@ map_names: List[str] = [
'The Spirit World (MAP28)',
'The Living End (MAP29)',
'Icon of Sin (MAP30)',
'Wolfenstein2 (MAP31)',
'Grosse2 (MAP32)',
'Wolfenstein (MAP31)',
'Grosse (MAP32)',
]

View File

@@ -84,11 +84,12 @@ regions:List[RegionDict] = [
# The Waste Tunnels (MAP05)
{"name":"The Waste Tunnels (MAP05) Main",
"connects_to_hub":True,
"connects_to_hub":False,
"episode":1,
"connections":[
{"target":"The Waste Tunnels (MAP05) Red","pro":False},
{"target":"The Waste Tunnels (MAP05) Blue","pro":False}]},
{"target":"The Waste Tunnels (MAP05) Blue","pro":False},
{"target":"The Waste Tunnels (MAP05) Start","pro":False}]},
{"name":"The Waste Tunnels (MAP05) Blue",
"connects_to_hub":False,
"episode":1,
@@ -103,6 +104,10 @@ regions:List[RegionDict] = [
"connects_to_hub":False,
"episode":1,
"connections":[{"target":"The Waste Tunnels (MAP05) Main","pro":False}]},
{"name":"The Waste Tunnels (MAP05) Start",
"connects_to_hub":True,
"episode":1,
"connections":[{"target":"The Waste Tunnels (MAP05) Main","pro":False}]},
# The Crusher (MAP06)
{"name":"The Crusher (MAP06) Main",
@@ -129,9 +134,13 @@ regions:List[RegionDict] = [
# Dead Simple (MAP07)
{"name":"Dead Simple (MAP07) Main",
"connects_to_hub":False,
"episode":1,
"connections":[{"target":"Dead Simple (MAP07) Start","pro":False}]},
{"name":"Dead Simple (MAP07) Start",
"connects_to_hub":True,
"episode":1,
"connections":[]},
"connections":[{"target":"Dead Simple (MAP07) Main","pro":False}]},
# Tricks and Traps (MAP08)
{"name":"Tricks and Traps (MAP08) Main",
@@ -151,11 +160,12 @@ regions:List[RegionDict] = [
# The Pit (MAP09)
{"name":"The Pit (MAP09) Main",
"connects_to_hub":True,
"connects_to_hub":False,
"episode":1,
"connections":[
{"target":"The Pit (MAP09) Yellow","pro":False},
{"target":"The Pit (MAP09) Blue","pro":False}]},
{"target":"The Pit (MAP09) Blue","pro":False},
{"target":"The Pit (MAP09) Start","pro":False}]},
{"name":"The Pit (MAP09) Blue",
"connects_to_hub":False,
"episode":1,
@@ -164,12 +174,18 @@ regions:List[RegionDict] = [
"connects_to_hub":False,
"episode":1,
"connections":[{"target":"The Pit (MAP09) Main","pro":False}]},
{"name":"The Pit (MAP09) Start",
"connects_to_hub":True,
"episode":1,
"connections":[{"target":"The Pit (MAP09) Main","pro":False}]},
# Refueling Base (MAP10)
{"name":"Refueling Base (MAP10) Main",
"connects_to_hub":True,
"connects_to_hub":False,
"episode":1,
"connections":[{"target":"Refueling Base (MAP10) Yellow","pro":False}]},
"connections":[
{"target":"Refueling Base (MAP10) Yellow","pro":False},
{"target":"Refueling Base (MAP10) Start","pro":False}]},
{"name":"Refueling Base (MAP10) Yellow",
"connects_to_hub":False,
"episode":1,
@@ -180,6 +196,10 @@ regions:List[RegionDict] = [
"connects_to_hub":False,
"episode":1,
"connections":[{"target":"Refueling Base (MAP10) Yellow","pro":False}]},
{"name":"Refueling Base (MAP10) Start",
"connects_to_hub":True,
"episode":1,
"connections":[{"target":"Refueling Base (MAP10) Main","pro":False}]},
# Circle of Death (MAP11)
{"name":"Circle of Death (MAP11) Main",
@@ -187,31 +207,49 @@ regions:List[RegionDict] = [
"episode":1,
"connections":[
{"target":"Circle of Death (MAP11) Blue","pro":False},
{"target":"Circle of Death (MAP11) Red","pro":False}]},
{"target":"Circle of Death (MAP11) Red","pro":False},
{"target":"Circle of Death (MAP11) Ending","pro":True}]},
{"name":"Circle of Death (MAP11) Blue",
"connects_to_hub":False,
"episode":1,
"connections":[{"target":"Circle of Death (MAP11) Main","pro":False}]},
{"name":"Circle of Death (MAP11) Red",
"connects_to_hub":False,
"episode":1,
"connections":[
{"target":"Circle of Death (MAP11) Main","pro":False},
{"target":"Circle of Death (MAP11) Ending","pro":False}]},
{"name":"Circle of Death (MAP11) Ending",
"connects_to_hub":False,
"episode":1,
"connections":[{"target":"Circle of Death (MAP11) Main","pro":False}]},
# The Factory (MAP12)
{"name":"The Factory (MAP12) Main",
"connects_to_hub":True,
{"name":"The Factory (MAP12) Indoors",
"connects_to_hub":False,
"episode":2,
"connections":[
{"target":"The Factory (MAP12) Yellow","pro":False},
{"target":"The Factory (MAP12) Blue","pro":False}]},
{"target":"The Factory (MAP12) Blue","pro":False},
{"target":"The Factory (MAP12) Main","pro":False}]},
{"name":"The Factory (MAP12) Blue",
"connects_to_hub":False,
"episode":2,
"connections":[{"target":"The Factory (MAP12) Main","pro":False}]},
"connections":[{"target":"The Factory (MAP12) Indoors","pro":False}]},
{"name":"The Factory (MAP12) Yellow",
"connects_to_hub":False,
"episode":2,
"connections":[]},
{"name":"The Factory (MAP12) Outdoors",
"connects_to_hub":True,
"episode":2,
"connections":[{"target":"The Factory (MAP12) Main","pro":False}]},
{"name":"The Factory (MAP12) Main",
"connects_to_hub":False,
"episode":2,
"connections":[
{"target":"The Factory (MAP12) Indoors","pro":False},
{"target":"The Factory (MAP12) Outdoors","pro":False}]},
# Downtown (MAP13)
{"name":"Downtown (MAP13) Main",
@@ -291,7 +329,8 @@ regions:List[RegionDict] = [
"episode":2,
"connections":[
{"target":"Suburbs (MAP16) Red","pro":False},
{"target":"Suburbs (MAP16) Blue","pro":False}]},
{"target":"Suburbs (MAP16) Blue","pro":False},
{"target":"Suburbs (MAP16) Pro Exit","pro":True}]},
{"name":"Suburbs (MAP16) Blue",
"connects_to_hub":False,
"episode":2,
@@ -299,7 +338,13 @@ regions:List[RegionDict] = [
{"name":"Suburbs (MAP16) Red",
"connects_to_hub":False,
"episode":2,
"connections":[{"target":"Suburbs (MAP16) Main","pro":False}]},
"connections":[
{"target":"Suburbs (MAP16) Main","pro":False},
{"target":"Suburbs (MAP16) Pro Exit","pro":False}]},
{"name":"Suburbs (MAP16) Pro Exit",
"connects_to_hub":False,
"episode":2,
"connections":[{"target":"Suburbs (MAP16) Red","pro":False}]},
# Tenements (MAP17)
{"name":"Tenements (MAP17) Main",
@@ -358,7 +403,7 @@ regions:List[RegionDict] = [
# Nirvana (MAP21)
{"name":"Nirvana (MAP21) Main",
"connects_to_hub":True,
"connects_to_hub":False,
"episode":3,
"connections":[{"target":"Nirvana (MAP21) Yellow","pro":False}]},
{"name":"Nirvana (MAP21) Yellow",
@@ -366,19 +411,31 @@ regions:List[RegionDict] = [
"episode":3,
"connections":[
{"target":"Nirvana (MAP21) Main","pro":False},
{"target":"Nirvana (MAP21) Magenta","pro":False}]},
{"target":"Nirvana (MAP21) Magenta","pro":False},
{"target":"Nirvana (MAP21) Pro Magenta","pro":True}]},
{"name":"Nirvana (MAP21) Magenta",
"connects_to_hub":False,
"episode":3,
"connections":[{"target":"Nirvana (MAP21) Yellow","pro":False}]},
"connections":[
{"target":"Nirvana (MAP21) Yellow","pro":False},
{"target":"Nirvana (MAP21) Pro Magenta","pro":False}]},
{"name":"Nirvana (MAP21) Start",
"connects_to_hub":True,
"episode":3,
"connections":[{"target":"Nirvana (MAP21) Main","pro":False}]},
{"name":"Nirvana (MAP21) Pro Magenta",
"connects_to_hub":False,
"episode":3,
"connections":[{"target":"Nirvana (MAP21) Magenta","pro":False}]},
# The Catacombs (MAP22)
{"name":"The Catacombs (MAP22) Main",
"connects_to_hub":True,
"connects_to_hub":False,
"episode":3,
"connections":[
{"target":"The Catacombs (MAP22) Blue","pro":False},
{"target":"The Catacombs (MAP22) Red","pro":False}]},
{"target":"The Catacombs (MAP22) Red","pro":False},
{"target":"The Catacombs (MAP22) Early","pro":False}]},
{"name":"The Catacombs (MAP22) Blue",
"connects_to_hub":False,
"episode":3,
@@ -387,36 +444,59 @@ regions:List[RegionDict] = [
"connects_to_hub":False,
"episode":3,
"connections":[{"target":"The Catacombs (MAP22) Main","pro":False}]},
# Barrels o Fun (MAP23)
{"name":"Barrels o Fun (MAP23) Main",
{"name":"The Catacombs (MAP22) Early",
"connects_to_hub":True,
"episode":3,
"connections":[{"target":"Barrels o Fun (MAP23) Yellow","pro":False}]},
{"name":"Barrels o Fun (MAP23) Yellow",
"connections":[{"target":"The Catacombs (MAP22) Main","pro":False}]},
# Barrels o' Fun (MAP23)
{"name":"Barrels o' Fun (MAP23) Main",
"connects_to_hub":True,
"episode":3,
"connections":[{"target":"Barrels o' Fun (MAP23) Yellow","pro":False}]},
{"name":"Barrels o' Fun (MAP23) Yellow",
"connects_to_hub":False,
"episode":3,
"connections":[{"target":"Barrels o Fun (MAP23) Main","pro":False}]},
"connections":[{"target":"Barrels o' Fun (MAP23) Main","pro":False}]},
# The Chasm (MAP24)
{"name":"The Chasm (MAP24) Main",
"connects_to_hub":True,
"episode":3,
"connections":[{"target":"The Chasm (MAP24) Red","pro":False}]},
"connections":[
{"target":"The Chasm (MAP24) Blue","pro":False},
{"target":"The Chasm (MAP24) Blue Pro","pro":True}]},
{"name":"The Chasm (MAP24) Red",
"connects_to_hub":False,
"episode":3,
"connections":[{"target":"The Chasm (MAP24) Main","pro":False}]},
"connections":[{"target":"The Chasm (MAP24) Blue","pro":False}]},
{"name":"The Chasm (MAP24) Blue",
"connects_to_hub":False,
"episode":3,
"connections":[
{"target":"The Chasm (MAP24) Red","pro":False},
{"target":"The Chasm (MAP24) Main","pro":False},
{"target":"The Chasm (MAP24) Blue Pro","pro":False}]},
{"name":"The Chasm (MAP24) Blue Pro",
"connects_to_hub":False,
"episode":3,
"connections":[{"target":"The Chasm (MAP24) Blue","pro":False}]},
# Bloodfalls (MAP25)
{"name":"Bloodfalls (MAP25) Main",
"connects_to_hub":True,
"connects_to_hub":False,
"episode":3,
"connections":[{"target":"Bloodfalls (MAP25) Blue","pro":False}]},
"connections":[
{"target":"Bloodfalls (MAP25) Blue","pro":False},
{"target":"Bloodfalls (MAP25) Start","pro":False}]},
{"name":"Bloodfalls (MAP25) Blue",
"connects_to_hub":False,
"episode":3,
"connections":[{"target":"Bloodfalls (MAP25) Main","pro":False}]},
{"name":"Bloodfalls (MAP25) Start",
"connects_to_hub":True,
"episode":3,
"connections":[{"target":"Bloodfalls (MAP25) Main","pro":False}]},
# The Abandoned Mines (MAP26)
{"name":"The Abandoned Mines (MAP26) Main",
@@ -484,19 +564,27 @@ regions:List[RegionDict] = [
# Icon of Sin (MAP30)
{"name":"Icon of Sin (MAP30) Main",
"connects_to_hub":False,
"episode":3,
"connections":[{"target":"Icon of Sin (MAP30) Start","pro":False}]},
{"name":"Icon of Sin (MAP30) Start",
"connects_to_hub":True,
"episode":3,
"connections":[]},
"connections":[{"target":"Icon of Sin (MAP30) Main","pro":False}]},
# Wolfenstein2 (MAP31)
{"name":"Wolfenstein2 (MAP31) Main",
# Wolfenstein (MAP31)
{"name":"Wolfenstein (MAP31) Main",
"connects_to_hub":True,
"episode":4,
"connections":[]},
# Grosse2 (MAP32)
{"name":"Grosse2 (MAP32) Main",
# Grosse (MAP32)
{"name":"Grosse (MAP32) Main",
"connects_to_hub":False,
"episode":4,
"connections":[{"target":"Grosse (MAP32) Start","pro":False}]},
{"name":"Grosse (MAP32) Start",
"connects_to_hub":True,
"episode":4,
"connections":[]},
"connections":[{"target":"Grosse (MAP32) Main","pro":False}]},
]

View File

@@ -53,14 +53,6 @@ def set_episode1_rules(player, multiworld, pro):
state.has("The Focus (MAP04) - Red keycard", player, 1))
# The Waste Tunnels (MAP05)
set_rule(multiworld.get_entrance("Hub -> The Waste Tunnels (MAP05) Main", player), lambda state:
(state.has("The Waste Tunnels (MAP05)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Super Shotgun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Main -> The Waste Tunnels (MAP05) Red", player), lambda state:
state.has("The Waste Tunnels (MAP05) - Red keycard", player, 1))
set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Main -> The Waste Tunnels (MAP05) Blue", player), lambda state:
@@ -71,18 +63,22 @@ def set_episode1_rules(player, multiworld, pro):
state.has("The Waste Tunnels (MAP05) - Blue keycard", player, 1))
set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Yellow -> The Waste Tunnels (MAP05) Blue", player), lambda state:
state.has("The Waste Tunnels (MAP05) - Yellow keycard", player, 1))
set_rule(multiworld.get_entrance("Hub -> The Waste Tunnels (MAP05) Start", player), lambda state:
state.has("The Waste Tunnels (MAP05)", player, 1))
set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Start -> The Waste Tunnels (MAP05) Main", player), lambda state:
(state.has("Shotgun", player, 1) and
state.has("Super Shotgun", player, 1)) and (state.has("Chaingun", player, 1) or
state.has("Plasma gun", player, 1)))
# The Crusher (MAP06)
set_rule(multiworld.get_entrance("Hub -> The Crusher (MAP06) Main", player), lambda state:
(state.has("The Crusher (MAP06)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Super Shotgun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
state.has("Shotgun", player, 1)) and
(state.has("Plasma gun", player, 1) or
state.has("Chaingun", player, 1)))
set_rule(multiworld.get_entrance("The Crusher (MAP06) Main -> The Crusher (MAP06) Blue", player), lambda state:
state.has("The Crusher (MAP06) - Blue keycard", player, 1))
state.has("The Crusher (MAP06) - Blue keycard", player, 1) and
state.has("Super Shotgun", player, 1))
set_rule(multiworld.get_entrance("The Crusher (MAP06) Blue -> The Crusher (MAP06) Red", player), lambda state:
state.has("The Crusher (MAP06) - Red keycard", player, 1))
set_rule(multiworld.get_entrance("The Crusher (MAP06) Blue -> The Crusher (MAP06) Main", player), lambda state:
@@ -95,14 +91,14 @@ def set_episode1_rules(player, multiworld, pro):
state.has("The Crusher (MAP06) - Red keycard", player, 1))
# Dead Simple (MAP07)
set_rule(multiworld.get_entrance("Hub -> Dead Simple (MAP07) Main", player), lambda state:
(state.has("Dead Simple (MAP07)", player, 1) and
state.has("Shotgun", player, 1) and
set_rule(multiworld.get_entrance("Hub -> Dead Simple (MAP07) Start", player), lambda state:
state.has("Dead Simple (MAP07)", player, 1))
set_rule(multiworld.get_entrance("Dead Simple (MAP07) Start -> Dead Simple (MAP07) Main", player), lambda state:
(state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Super Shotgun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
state.has("Rocket launcher", player, 1)))
# Tricks and Traps (MAP08)
set_rule(multiworld.get_entrance("Hub -> Tricks and Traps (MAP08) Main", player), lambda state:
@@ -119,34 +115,34 @@ def set_episode1_rules(player, multiworld, pro):
state.has("Tricks and Traps (MAP08) - Yellow skull key", player, 1))
# The Pit (MAP09)
set_rule(multiworld.get_entrance("Hub -> The Pit (MAP09) Main", player), lambda state:
(state.has("The Pit (MAP09)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Super Shotgun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
set_rule(multiworld.get_entrance("The Pit (MAP09) Main -> The Pit (MAP09) Yellow", player), lambda state:
state.has("The Pit (MAP09) - Yellow keycard", player, 1))
set_rule(multiworld.get_entrance("The Pit (MAP09) Main -> The Pit (MAP09) Blue", player), lambda state:
state.has("The Pit (MAP09) - Blue keycard", player, 1))
set_rule(multiworld.get_entrance("The Pit (MAP09) Yellow -> The Pit (MAP09) Main", player), lambda state:
state.has("The Pit (MAP09) - Yellow keycard", player, 1))
set_rule(multiworld.get_entrance("Hub -> The Pit (MAP09) Start", player), lambda state:
state.has("The Pit (MAP09)", player, 1))
set_rule(multiworld.get_entrance("The Pit (MAP09) Start -> The Pit (MAP09) Main", player), lambda state:
(state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("Rocket launcher", player, 1)))
# Refueling Base (MAP10)
set_rule(multiworld.get_entrance("Hub -> Refueling Base (MAP10) Main", player), lambda state:
(state.has("Refueling Base (MAP10)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Super Shotgun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
set_rule(multiworld.get_entrance("Refueling Base (MAP10) Main -> Refueling Base (MAP10) Yellow", player), lambda state:
state.has("Refueling Base (MAP10) - Yellow keycard", player, 1))
set_rule(multiworld.get_entrance("Refueling Base (MAP10) Yellow -> Refueling Base (MAP10) Yellow Blue", player), lambda state:
state.has("Refueling Base (MAP10) - Blue keycard", player, 1))
set_rule(multiworld.get_entrance("Hub -> Refueling Base (MAP10) Start", player), lambda state:
state.has("Refueling Base (MAP10)", player, 1))
set_rule(multiworld.get_entrance("Refueling Base (MAP10) Start -> Refueling Base (MAP10) Main", player), lambda state:
(state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("Rocket launcher", player, 1)))
# Circle of Death (MAP11)
set_rule(multiworld.get_entrance("Hub -> Circle of Death (MAP11) Main", player), lambda state:
@@ -165,18 +161,19 @@ def set_episode1_rules(player, multiworld, pro):
def set_episode2_rules(player, multiworld, pro):
# The Factory (MAP12)
set_rule(multiworld.get_entrance("Hub -> The Factory (MAP12) Main", player), lambda state:
(state.has("The Factory (MAP12)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Super Shotgun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
set_rule(multiworld.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Yellow", player), lambda state:
set_rule(multiworld.get_entrance("The Factory (MAP12) Indoors -> The Factory (MAP12) Yellow", player), lambda state:
state.has("The Factory (MAP12) - Yellow keycard", player, 1))
set_rule(multiworld.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Blue", player), lambda state:
set_rule(multiworld.get_entrance("The Factory (MAP12) Indoors -> The Factory (MAP12) Blue", player), lambda state:
state.has("The Factory (MAP12) - Blue keycard", player, 1))
set_rule(multiworld.get_entrance("Hub -> The Factory (MAP12) Outdoors", player), lambda state:
state.has("The Factory (MAP12)", player, 1))
set_rule(multiworld.get_entrance("The Factory (MAP12) Outdoors -> The Factory (MAP12) Main", player), lambda state:
state.has("Super Shotgun", player, 1) or
state.has("Plasma gun", player, 1))
set_rule(multiworld.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Indoors", player), lambda state:
(state.has("Super Shotgun", player, 1) and
state.has("Chaingun", player, 1)) and (state.has("BFG9000", player, 1) or
state.has("Plasma gun", player, 1)))
# Downtown (MAP13)
set_rule(multiworld.get_entrance("Hub -> Downtown (MAP13) Main", player), lambda state:
@@ -307,54 +304,56 @@ def set_episode2_rules(player, multiworld, pro):
def set_episode3_rules(player, multiworld, pro):
# Nirvana (MAP21)
set_rule(multiworld.get_entrance("Hub -> Nirvana (MAP21) Main", player), lambda state:
(state.has("Nirvana (MAP21)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Super Shotgun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
set_rule(multiworld.get_entrance("Nirvana (MAP21) Main -> Nirvana (MAP21) Yellow", player), lambda state:
state.has("Nirvana (MAP21) - Yellow skull key", player, 1))
(state.has("Super Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Nirvana (MAP21) - Yellow skull key", player, 1)) and (state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
set_rule(multiworld.get_entrance("Nirvana (MAP21) Yellow -> Nirvana (MAP21) Main", player), lambda state:
state.has("Nirvana (MAP21) - Yellow skull key", player, 1))
set_rule(multiworld.get_entrance("Nirvana (MAP21) Yellow -> Nirvana (MAP21) Magenta", player), lambda state:
state.has("Nirvana (MAP21) - Red skull key", player, 1) and
state.has("Nirvana (MAP21) - Blue skull key", player, 1))
set_rule(multiworld.get_entrance("Nirvana (MAP21) Magenta -> Nirvana (MAP21) Yellow", player), lambda state:
state.has("Nirvana (MAP21) - Red skull key", player, 1) and
state.has("Nirvana (MAP21) - Blue skull key", player, 1))
set_rule(multiworld.get_entrance("Hub -> Nirvana (MAP21) Start", player), lambda state:
state.has("Nirvana (MAP21)", player, 1))
set_rule(multiworld.get_entrance("Nirvana (MAP21) Start -> Nirvana (MAP21) Main", player), lambda state:
state.has("Super Shotgun", player, 1) or
state.has("Plasma gun", player, 1))
set_rule(multiworld.get_entrance("Nirvana (MAP21) Pro Magenta -> Nirvana (MAP21) Magenta", player), lambda state:
state.has("Nirvana (MAP21) - Red skull key", player, 1))
# The Catacombs (MAP22)
set_rule(multiworld.get_entrance("Hub -> The Catacombs (MAP22) Main", player), lambda state:
(state.has("The Catacombs (MAP22)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Super Shotgun", player, 1)) and
(state.has("BFG9000", player, 1) or
state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1)))
set_rule(multiworld.get_entrance("The Catacombs (MAP22) Main -> The Catacombs (MAP22) Blue", player), lambda state:
state.has("The Catacombs (MAP22) - Blue skull key", player, 1))
set_rule(multiworld.get_entrance("The Catacombs (MAP22) Main -> The Catacombs (MAP22) Red", player), lambda state:
state.has("The Catacombs (MAP22) - Red skull key", player, 1))
set_rule(multiworld.get_entrance("The Catacombs (MAP22) Red -> The Catacombs (MAP22) Main", player), lambda state:
state.has("The Catacombs (MAP22) - Red skull key", player, 1))
set_rule(multiworld.get_entrance("Hub -> The Catacombs (MAP22) Early", player), lambda state:
(state.has("The Catacombs (MAP22)", player, 1)) and
(state.has("Shotgun", player, 1) or
state.has("Super Shotgun", player, 1) or
state.has("Plasma gun", player, 1)))
set_rule(multiworld.get_entrance("The Catacombs (MAP22) Early -> The Catacombs (MAP22) Main", player), lambda state:
(state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("Rocket launcher", player, 1)))
# Barrels o Fun (MAP23)
set_rule(multiworld.get_entrance("Hub -> Barrels o Fun (MAP23) Main", player), lambda state:
(state.has("Barrels o Fun (MAP23)", player, 1) and
# Barrels o' Fun (MAP23)
set_rule(multiworld.get_entrance("Hub -> Barrels o' Fun (MAP23) Main", player), lambda state:
(state.has("Barrels o' Fun (MAP23)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Super Shotgun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
set_rule(multiworld.get_entrance("Barrels o Fun (MAP23) Main -> Barrels o Fun (MAP23) Yellow", player), lambda state:
state.has("Barrels o Fun (MAP23) - Yellow skull key", player, 1))
set_rule(multiworld.get_entrance("Barrels o Fun (MAP23) Yellow -> Barrels o Fun (MAP23) Main", player), lambda state:
state.has("Barrels o Fun (MAP23) - Yellow skull key", player, 1))
set_rule(multiworld.get_entrance("Barrels o' Fun (MAP23) Main -> Barrels o' Fun (MAP23) Yellow", player), lambda state:
state.has("Barrels o' Fun (MAP23) - Yellow skull key", player, 1))
set_rule(multiworld.get_entrance("Barrels o' Fun (MAP23) Yellow -> Barrels o' Fun (MAP23) Main", player), lambda state:
state.has("Barrels o' Fun (MAP23) - Yellow skull key", player, 1))
# The Chasm (MAP24)
set_rule(multiworld.get_entrance("Hub -> The Chasm (MAP24) Main", player), lambda state:
@@ -365,24 +364,26 @@ def set_episode3_rules(player, multiworld, pro):
state.has("Plasma gun", player, 1) and
state.has("BFG9000", player, 1) and
state.has("Super Shotgun", player, 1))
set_rule(multiworld.get_entrance("The Chasm (MAP24) Main -> The Chasm (MAP24) Red", player), lambda state:
set_rule(multiworld.get_entrance("The Chasm (MAP24) Main -> The Chasm (MAP24) Blue", player), lambda state:
state.has("The Chasm (MAP24) - Blue keycard", player, 1))
set_rule(multiworld.get_entrance("The Chasm (MAP24) Red -> The Chasm (MAP24) Blue", player), lambda state:
state.has("The Chasm (MAP24) - Red keycard", player, 1))
set_rule(multiworld.get_entrance("The Chasm (MAP24) Red -> The Chasm (MAP24) Main", player), lambda state:
set_rule(multiworld.get_entrance("The Chasm (MAP24) Blue -> The Chasm (MAP24) Red", player), lambda state:
state.has("The Chasm (MAP24) - Red keycard", player, 1))
# Bloodfalls (MAP25)
set_rule(multiworld.get_entrance("Hub -> Bloodfalls (MAP25) Main", player), lambda state:
state.has("Bloodfalls (MAP25)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Rocket launcher", player, 1) and
state.has("Plasma gun", player, 1) and
state.has("BFG9000", player, 1) and
state.has("Super Shotgun", player, 1))
set_rule(multiworld.get_entrance("Bloodfalls (MAP25) Main -> Bloodfalls (MAP25) Blue", player), lambda state:
state.has("Bloodfalls (MAP25) - Blue skull key", player, 1))
(state.has("Bloodfalls (MAP25) - Blue skull key", player, 1)) and (state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
set_rule(multiworld.get_entrance("Bloodfalls (MAP25) Blue -> Bloodfalls (MAP25) Main", player), lambda state:
state.has("Bloodfalls (MAP25) - Blue skull key", player, 1))
set_rule(multiworld.get_entrance("Hub -> Bloodfalls (MAP25) Start", player), lambda state:
state.has("Bloodfalls (MAP25)", player, 1))
set_rule(multiworld.get_entrance("Bloodfalls (MAP25) Start -> Bloodfalls (MAP25) Main", player), lambda state:
state.has("Super Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Shotgun", player, 1))
# The Abandoned Mines (MAP26)
set_rule(multiworld.get_entrance("Hub -> The Abandoned Mines (MAP26) Main", player), lambda state:
@@ -451,36 +452,34 @@ def set_episode3_rules(player, multiworld, pro):
state.has("Super Shotgun", player, 1))
# Icon of Sin (MAP30)
set_rule(multiworld.get_entrance("Hub -> Icon of Sin (MAP30) Main", player), lambda state:
state.has("Icon of Sin (MAP30)", player, 1) and
state.has("Rocket launcher", player, 1) and
set_rule(multiworld.get_entrance("Hub -> Icon of Sin (MAP30) Start", player), lambda state:
state.has("Icon of Sin (MAP30)", player, 1))
set_rule(multiworld.get_entrance("Icon of Sin (MAP30) Start -> Icon of Sin (MAP30) Main", player), lambda state:
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Rocket launcher", player, 1) and
state.has("Plasma gun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("BFG9000", player, 1) and
state.has("Super Shotgun", player, 1))
def set_episode4_rules(player, multiworld, pro):
# Wolfenstein2 (MAP31)
set_rule(multiworld.get_entrance("Hub -> Wolfenstein2 (MAP31) Main", player), lambda state:
(state.has("Wolfenstein2 (MAP31)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Super Shotgun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
# Wolfenstein (MAP31)
set_rule(multiworld.get_entrance("Hub -> Wolfenstein (MAP31) Main", player), lambda state:
(state.has("Wolfenstein (MAP31)", player, 1) and
state.has("Chaingun", player, 1)) and
(state.has("Shotgun", player, 1) or
state.has("Super Shotgun", player, 1)))
# Grosse2 (MAP32)
set_rule(multiworld.get_entrance("Hub -> Grosse2 (MAP32) Main", player), lambda state:
(state.has("Grosse2 (MAP32)", player, 1) and
state.has("Shotgun", player, 1) and
# Grosse (MAP32)
set_rule(multiworld.get_entrance("Hub -> Grosse (MAP32) Start", player), lambda state:
state.has("Grosse (MAP32)", player, 1))
set_rule(multiworld.get_entrance("Grosse (MAP32) Start -> Grosse (MAP32) Main", player), lambda state:
(state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Super Shotgun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
state.has("Rocket launcher", player, 1)))
def set_rules(doom_ii_world: "DOOM2World", included_episodes, pro):

View File

@@ -51,11 +51,11 @@ class DOOM2World(World):
location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
location_name_groups = Locations.location_name_groups
starting_level_for_episode: List[str] = [
"Entryway (MAP01)",
"The Factory (MAP12)",
"Nirvana (MAP21)"
]
starting_level_for_episode: Dict[int, str] = {
1: "Entryway (MAP01)",
2: "The Factory (MAP12)",
3: "Nirvana (MAP21)"
}
# Item ratio that scales depending on episode count. These are the ratio for 3 episode. In DOOM1.
# The ratio have been tweaked seem, and feel good.
@@ -77,6 +77,7 @@ class DOOM2World(World):
def __init__(self, multiworld: MultiWorld, player: int):
self.included_episodes = [1, 1, 1, 0]
self.location_count = 0
self.starting_levels = []
super().__init__(multiworld, player)
@@ -95,6 +96,14 @@ class DOOM2World(World):
if self.get_episode_count() == 0:
self.included_episodes[0] = 1
self.starting_levels = [level_name for (episode, level_name) in self.starting_level_for_episode.items()
if self.included_episodes[episode - 1]]
# If soloing MAP21-MAP30, we need to mark a weapon as early to help generation succeed
if self.get_episode_count() == 1 and self.included_episodes[2]:
early_weapon = self.random.choice(["Super Shotgun", "Plasma gun"])
self.multiworld.early_items[self.player][early_weapon] = 1
def create_regions(self):
pro = self.options.pro.value
@@ -193,7 +202,7 @@ class DOOM2World(World):
if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]:
continue
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
count = item["count"] if item["name"] not in self.starting_levels else item["count"] - 1
itempool += [self.create_item(item["name"]) for _ in range(count)]
# Backpack(s) based on options
@@ -224,9 +233,8 @@ class DOOM2World(World):
self.location_count -= 1
# Give starting levels right away
for i in range(len(self.starting_level_for_episode)):
if self.included_episodes[i]:
self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i]))
for map_name in self.starting_levels:
self.multiworld.push_precollected(self.create_item(map_name))
# Give Computer area maps if option selected
if start_with_computer_area_maps:

View File

@@ -255,7 +255,8 @@ async def game_watcher(ctx: FactorioContext):
if "DeathLink" in ctx.tags:
async_start(ctx.send_death())
if ctx.energy_link_increment:
in_world_bridges = data["energy_bridges"]
# 1 + quality * 0.3 for each bridge
in_world_bridges: float = data["energy_bridges"]
if in_world_bridges:
in_world_energy = data["energy"]
if in_world_energy < (ctx.energy_link_increment * in_world_bridges):
@@ -263,14 +264,14 @@ async def game_watcher(ctx: FactorioContext):
ctx.last_deplete = time.time()
async_start(ctx.send_msgs([{
"cmd": "Set", "key": ctx.energylink_key, "operations":
[{"operation": "add", "value": -ctx.energy_link_increment * in_world_bridges},
[{"operation": "add", "value": int(-ctx.energy_link_increment * in_world_bridges)},
{"operation": "max", "value": 0}],
"last_deplete": ctx.last_deplete
}]))
# Above Capacity - (len(Bridges) * ENERGY_INCREMENT)
elif in_world_energy > (in_world_bridges * ctx.energy_link_increment * 5) - \
ctx.energy_link_increment * in_world_bridges:
value = ctx.energy_link_increment * in_world_bridges
value = int(ctx.energy_link_increment * in_world_bridges)
async_start(ctx.send_msgs([{
"cmd": "Set", "key": ctx.energylink_key, "operations":
[{"operation": "add", "value": value}]
@@ -406,7 +407,7 @@ async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient):
ctx.auth = info["slot_name"]
ctx.seed_name = info["seed_name"]
death_link = info["death_link"]
ctx.energy_link_increment = info.get("energy_link", 0)
ctx.energy_link_increment = int(info.get("energy_link", 0))
logger.debug(f"Energy Link Increment: {ctx.energy_link_increment}")
if ctx.energy_link_increment and ctx.ui:
ctx.ui.enable_energy_link()

View File

@@ -8,17 +8,20 @@ from schema import Schema, Optional, And, Or, SchemaError
from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \
StartInventoryPool, PerGameCommonOptions, OptionGroup
# schema helpers
class FloatRange:
def __init__(self, low, high):
self._low = low
self._high = high
def validate(self, value):
def validate(self, value) -> float:
if not isinstance(value, (float, int)):
raise SchemaError(f"should be instance of float or int, but was {value!r}")
if not self._low <= value <= self._high:
raise SchemaError(f"{value} is not between {self._low} and {self._high}")
return float(value)
LuaBool = Or(bool, And(int, lambda n: n in (0, 1)))

View File

@@ -102,7 +102,7 @@ class Factorio(World):
item_name_groups = {
"Progressive": set(progressive_tech_table.keys()),
}
required_client_version = (0, 5, 1)
required_client_version = (0, 6, 0)
if Utils.version_tuple < required_client_version:
raise Exception(f"Update Archipelago to use this world ({game}).")
ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs()

View File

@@ -3,7 +3,6 @@ import settings
import base64
import threading
import requests
import yaml
from worlds.AutoWorld import World, WebWorld
from BaseClasses import Tutorial
from .Regions import create_regions, location_table, set_rules, stage_set_rules, rooms, non_dead_end_crest_rooms,\
@@ -44,6 +43,7 @@ class FFMQWebWorld(WebWorld):
)
tutorials = [setup_en, setup_fr]
game_info_languages = ["en", "fr"]
class FFMQWorld(World):
@@ -134,7 +134,7 @@ class FFMQWorld(World):
errors.append([api_url, err])
else:
if response.ok:
world.rooms = rooms_data[query] = yaml.load(response.text, yaml.Loader)
world.rooms = rooms_data[query] = Utils.parse_yaml(response.text)
break
else:
api_urls.remove(api_url)

View File

View File

@@ -514,19 +514,19 @@ item_table: Dict[int, ItemDict] = {
'map': 7},
370259: {'classification': ItemClassification.progression,
'count': 1,
'name': 'The Aquifier (E3M9) - Blue key',
'name': 'The Aquifer (E3M9) - Blue key',
'doom_type': 79,
'episode': 3,
'map': 9},
370260: {'classification': ItemClassification.progression,
'count': 1,
'name': 'The Aquifier (E3M9) - Green key',
'name': 'The Aquifer (E3M9) - Green key',
'doom_type': 73,
'episode': 3,
'map': 9},
370261: {'classification': ItemClassification.progression,
'count': 1,
'name': 'The Aquifier (E3M9) - Yellow key',
'name': 'The Aquifer (E3M9) - Yellow key',
'doom_type': 80,
'episode': 3,
'map': 9},
@@ -1234,37 +1234,37 @@ item_table: Dict[int, ItemDict] = {
'map': 7},
370475: {'classification': ItemClassification.progression,
'count': 1,
'name': "D'Sparil'S Keep (E3M8)",
'name': "D'Sparil's Keep (E3M8)",
'doom_type': -1,
'episode': 3,
'map': 8},
370476: {'classification': ItemClassification.progression,
'count': 1,
'name': "D'Sparil'S Keep (E3M8) - Complete",
'name': "D'Sparil's Keep (E3M8) - Complete",
'doom_type': -2,
'episode': 3,
'map': 8},
370477: {'classification': ItemClassification.filler,
'count': 1,
'name': "D'Sparil'S Keep (E3M8) - Map Scroll",
'name': "D'Sparil's Keep (E3M8) - Map Scroll",
'doom_type': 35,
'episode': 3,
'map': 8},
370478: {'classification': ItemClassification.progression,
'count': 1,
'name': 'The Aquifier (E3M9)',
'name': 'The Aquifer (E3M9)',
'doom_type': -1,
'episode': 3,
'map': 9},
370479: {'classification': ItemClassification.progression,
'count': 1,
'name': 'The Aquifier (E3M9) - Complete',
'name': 'The Aquifer (E3M9) - Complete',
'doom_type': -2,
'episode': 3,
'map': 9},
370480: {'classification': ItemClassification.filler,
'count': 1,
'name': 'The Aquifier (E3M9) - Map Scroll',
'name': 'The Aquifer (E3M9) - Map Scroll',
'doom_type': 35,
'episode': 3,
'map': 9},
@@ -1635,8 +1635,8 @@ item_name_groups: Dict[str, Set[str]] = {
'Ammos': {'Crystal Geode', 'Energy Orb', 'Greater Runes', 'Inferno Orb', 'Pile of Mace Spheres', 'Quiver of Ethereal Arrows', },
'Armors': {'Enchanted Shield', 'Silver Shield', },
'Artifacts': {'Chaos Device', 'Morph Ovum', 'Mystic Urn', 'Quartz Flask', 'Ring of Invincibility', 'Shadowsphere', 'Timebomb of the Ancients', 'Tome of Power', 'Torch', },
'Keys': {'Ambulatory (E4M3) - Blue key', 'Ambulatory (E4M3) - Green key', 'Ambulatory (E4M3) - Yellow key', 'Blockhouse (E4M2) - Blue key', 'Blockhouse (E4M2) - Green key', 'Blockhouse (E4M2) - Yellow key', 'Catafalque (E4M1) - Green key', 'Catafalque (E4M1) - Yellow key', 'Colonnade (E5M6) - Blue key', 'Colonnade (E5M6) - Green key', 'Colonnade (E5M6) - Yellow key', 'Courtyard (E5M4) - Blue key', 'Courtyard (E5M4) - Green key', 'Courtyard (E5M4) - Yellow key', 'Foetid Manse (E5M7) - Blue key', 'Foetid Manse (E5M7) - Green key', 'Foetid Manse (E5M7) - Yellow key', 'Great Stair (E4M5) - Blue key', 'Great Stair (E4M5) - Green key', 'Great Stair (E4M5) - Yellow key', 'Halls of the Apostate (E4M6) - Blue key', 'Halls of the Apostate (E4M6) - Green key', 'Halls of the Apostate (E4M6) - Yellow key', 'Hydratyr (E5M5) - Blue key', 'Hydratyr (E5M5) - Green key', 'Hydratyr (E5M5) - Yellow key', 'Mausoleum (E4M9) - Yellow key', 'Ochre Cliffs (E5M1) - Blue key', 'Ochre Cliffs (E5M1) - Green key', 'Ochre Cliffs (E5M1) - Yellow key', 'Quay (E5M3) - Blue key', 'Quay (E5M3) - Green key', 'Quay (E5M3) - Yellow key', 'Ramparts of Perdition (E4M7) - Blue key', 'Ramparts of Perdition (E4M7) - Green key', 'Ramparts of Perdition (E4M7) - Yellow key', 'Rapids (E5M2) - Green key', 'Rapids (E5M2) - Yellow key', 'Shattered Bridge (E4M8) - Yellow key', "Skein of D'Sparil (E5M9) - Blue key", "Skein of D'Sparil (E5M9) - Green key", "Skein of D'Sparil (E5M9) - Yellow key", 'The Aquifier (E3M9) - Blue key', 'The Aquifier (E3M9) - Green key', 'The Aquifier (E3M9) - Yellow key', 'The Azure Fortress (E3M4) - Green key', 'The Azure Fortress (E3M4) - Yellow key', 'The Catacombs (E2M5) - Blue key', 'The Catacombs (E2M5) - Green key', 'The Catacombs (E2M5) - Yellow key', 'The Cathedral (E1M6) - Green key', 'The Cathedral (E1M6) - Yellow key', 'The Cesspool (E3M2) - Blue key', 'The Cesspool (E3M2) - Green key', 'The Cesspool (E3M2) - Yellow key', 'The Chasm (E3M7) - Blue key', 'The Chasm (E3M7) - Green key', 'The Chasm (E3M7) - Yellow key', 'The Citadel (E1M5) - Blue key', 'The Citadel (E1M5) - Green key', 'The Citadel (E1M5) - Yellow key', 'The Confluence (E3M3) - Blue key', 'The Confluence (E3M3) - Green key', 'The Confluence (E3M3) - Yellow key', 'The Crater (E2M1) - Green key', 'The Crater (E2M1) - Yellow key', 'The Crypts (E1M7) - Blue key', 'The Crypts (E1M7) - Green key', 'The Crypts (E1M7) - Yellow key', 'The Docks (E1M1) - Yellow key', 'The Dungeons (E1M2) - Blue key', 'The Dungeons (E1M2) - Green key', 'The Dungeons (E1M2) - Yellow key', 'The Gatehouse (E1M3) - Green key', 'The Gatehouse (E1M3) - Yellow key', 'The Glacier (E2M9) - Blue key', 'The Glacier (E2M9) - Green key', 'The Glacier (E2M9) - Yellow key', 'The Graveyard (E1M9) - Blue key', 'The Graveyard (E1M9) - Green key', 'The Graveyard (E1M9) - Yellow key', 'The Great Hall (E2M7) - Blue key', 'The Great Hall (E2M7) - Green key', 'The Great Hall (E2M7) - Yellow key', 'The Guard Tower (E1M4) - Green key', 'The Guard Tower (E1M4) - Yellow key', 'The Halls of Fear (E3M6) - Blue key', 'The Halls of Fear (E3M6) - Green key', 'The Halls of Fear (E3M6) - Yellow key', 'The Ice Grotto (E2M4) - Blue key', 'The Ice Grotto (E2M4) - Green key', 'The Ice Grotto (E2M4) - Yellow key', 'The Labyrinth (E2M6) - Blue key', 'The Labyrinth (E2M6) - Green key', 'The Labyrinth (E2M6) - Yellow key', 'The Lava Pits (E2M2) - Green key', 'The Lava Pits (E2M2) - Yellow key', 'The Ophidian Lair (E3M5) - Green key', 'The Ophidian Lair (E3M5) - Yellow key', 'The River of Fire (E2M3) - Blue key', 'The River of Fire (E2M3) - Green key', 'The River of Fire (E2M3) - Yellow key', 'The Storehouse (E3M1) - Green key', 'The Storehouse (E3M1) - Yellow key', },
'Levels': {'Ambulatory (E4M3)', 'Blockhouse (E4M2)', 'Catafalque (E4M1)', 'Colonnade (E5M6)', 'Courtyard (E5M4)', "D'Sparil'S Keep (E3M8)", 'Field of Judgement (E5M8)', 'Foetid Manse (E5M7)', 'Great Stair (E4M5)', 'Halls of the Apostate (E4M6)', "Hell's Maw (E1M8)", 'Hydratyr (E5M5)', 'Mausoleum (E4M9)', 'Ochre Cliffs (E5M1)', 'Quay (E5M3)', 'Ramparts of Perdition (E4M7)', 'Rapids (E5M2)', 'Sepulcher (E4M4)', 'Shattered Bridge (E4M8)', "Skein of D'Sparil (E5M9)", 'The Aquifier (E3M9)', 'The Azure Fortress (E3M4)', 'The Catacombs (E2M5)', 'The Cathedral (E1M6)', 'The Cesspool (E3M2)', 'The Chasm (E3M7)', 'The Citadel (E1M5)', 'The Confluence (E3M3)', 'The Crater (E2M1)', 'The Crypts (E1M7)', 'The Docks (E1M1)', 'The Dungeons (E1M2)', 'The Gatehouse (E1M3)', 'The Glacier (E2M9)', 'The Graveyard (E1M9)', 'The Great Hall (E2M7)', 'The Guard Tower (E1M4)', 'The Halls of Fear (E3M6)', 'The Ice Grotto (E2M4)', 'The Labyrinth (E2M6)', 'The Lava Pits (E2M2)', 'The Ophidian Lair (E3M5)', 'The Portals of Chaos (E2M8)', 'The River of Fire (E2M3)', 'The Storehouse (E3M1)', },
'Map Scrolls': {'Ambulatory (E4M3) - Map Scroll', 'Blockhouse (E4M2) - Map Scroll', 'Catafalque (E4M1) - Map Scroll', 'Colonnade (E5M6) - Map Scroll', 'Courtyard (E5M4) - Map Scroll', "D'Sparil'S Keep (E3M8) - Map Scroll", 'Field of Judgement (E5M8) - Map Scroll', 'Foetid Manse (E5M7) - Map Scroll', 'Great Stair (E4M5) - Map Scroll', 'Halls of the Apostate (E4M6) - Map Scroll', "Hell's Maw (E1M8) - Map Scroll", 'Hydratyr (E5M5) - Map Scroll', 'Mausoleum (E4M9) - Map Scroll', 'Ochre Cliffs (E5M1) - Map Scroll', 'Quay (E5M3) - Map Scroll', 'Ramparts of Perdition (E4M7) - Map Scroll', 'Rapids (E5M2) - Map Scroll', 'Sepulcher (E4M4) - Map Scroll', 'Shattered Bridge (E4M8) - Map Scroll', "Skein of D'Sparil (E5M9) - Map Scroll", 'The Aquifier (E3M9) - Map Scroll', 'The Azure Fortress (E3M4) - Map Scroll', 'The Catacombs (E2M5) - Map Scroll', 'The Cathedral (E1M6) - Map Scroll', 'The Cesspool (E3M2) - Map Scroll', 'The Chasm (E3M7) - Map Scroll', 'The Citadel (E1M5) - Map Scroll', 'The Confluence (E3M3) - Map Scroll', 'The Crater (E2M1) - Map Scroll', 'The Crypts (E1M7) - Map Scroll', 'The Docks (E1M1) - Map Scroll', 'The Dungeons (E1M2) - Map Scroll', 'The Gatehouse (E1M3) - Map Scroll', 'The Glacier (E2M9) - Map Scroll', 'The Graveyard (E1M9) - Map Scroll', 'The Great Hall (E2M7) - Map Scroll', 'The Guard Tower (E1M4) - Map Scroll', 'The Halls of Fear (E3M6) - Map Scroll', 'The Ice Grotto (E2M4) - Map Scroll', 'The Labyrinth (E2M6) - Map Scroll', 'The Lava Pits (E2M2) - Map Scroll', 'The Ophidian Lair (E3M5) - Map Scroll', 'The Portals of Chaos (E2M8) - Map Scroll', 'The River of Fire (E2M3) - Map Scroll', 'The Storehouse (E3M1) - Map Scroll', },
'Keys': {'Ambulatory (E4M3) - Blue key', 'Ambulatory (E4M3) - Green key', 'Ambulatory (E4M3) - Yellow key', 'Blockhouse (E4M2) - Blue key', 'Blockhouse (E4M2) - Green key', 'Blockhouse (E4M2) - Yellow key', 'Catafalque (E4M1) - Green key', 'Catafalque (E4M1) - Yellow key', 'Colonnade (E5M6) - Blue key', 'Colonnade (E5M6) - Green key', 'Colonnade (E5M6) - Yellow key', 'Courtyard (E5M4) - Blue key', 'Courtyard (E5M4) - Green key', 'Courtyard (E5M4) - Yellow key', 'Foetid Manse (E5M7) - Blue key', 'Foetid Manse (E5M7) - Green key', 'Foetid Manse (E5M7) - Yellow key', 'Great Stair (E4M5) - Blue key', 'Great Stair (E4M5) - Green key', 'Great Stair (E4M5) - Yellow key', 'Halls of the Apostate (E4M6) - Blue key', 'Halls of the Apostate (E4M6) - Green key', 'Halls of the Apostate (E4M6) - Yellow key', 'Hydratyr (E5M5) - Blue key', 'Hydratyr (E5M5) - Green key', 'Hydratyr (E5M5) - Yellow key', 'Mausoleum (E4M9) - Yellow key', 'Ochre Cliffs (E5M1) - Blue key', 'Ochre Cliffs (E5M1) - Green key', 'Ochre Cliffs (E5M1) - Yellow key', 'Quay (E5M3) - Blue key', 'Quay (E5M3) - Green key', 'Quay (E5M3) - Yellow key', 'Ramparts of Perdition (E4M7) - Blue key', 'Ramparts of Perdition (E4M7) - Green key', 'Ramparts of Perdition (E4M7) - Yellow key', 'Rapids (E5M2) - Green key', 'Rapids (E5M2) - Yellow key', 'Shattered Bridge (E4M8) - Yellow key', "Skein of D'Sparil (E5M9) - Blue key", "Skein of D'Sparil (E5M9) - Green key", "Skein of D'Sparil (E5M9) - Yellow key", 'The Aquifer (E3M9) - Blue key', 'The Aquifer (E3M9) - Green key', 'The Aquifer (E3M9) - Yellow key', 'The Azure Fortress (E3M4) - Green key', 'The Azure Fortress (E3M4) - Yellow key', 'The Catacombs (E2M5) - Blue key', 'The Catacombs (E2M5) - Green key', 'The Catacombs (E2M5) - Yellow key', 'The Cathedral (E1M6) - Green key', 'The Cathedral (E1M6) - Yellow key', 'The Cesspool (E3M2) - Blue key', 'The Cesspool (E3M2) - Green key', 'The Cesspool (E3M2) - Yellow key', 'The Chasm (E3M7) - Blue key', 'The Chasm (E3M7) - Green key', 'The Chasm (E3M7) - Yellow key', 'The Citadel (E1M5) - Blue key', 'The Citadel (E1M5) - Green key', 'The Citadel (E1M5) - Yellow key', 'The Confluence (E3M3) - Blue key', 'The Confluence (E3M3) - Green key', 'The Confluence (E3M3) - Yellow key', 'The Crater (E2M1) - Green key', 'The Crater (E2M1) - Yellow key', 'The Crypts (E1M7) - Blue key', 'The Crypts (E1M7) - Green key', 'The Crypts (E1M7) - Yellow key', 'The Docks (E1M1) - Yellow key', 'The Dungeons (E1M2) - Blue key', 'The Dungeons (E1M2) - Green key', 'The Dungeons (E1M2) - Yellow key', 'The Gatehouse (E1M3) - Green key', 'The Gatehouse (E1M3) - Yellow key', 'The Glacier (E2M9) - Blue key', 'The Glacier (E2M9) - Green key', 'The Glacier (E2M9) - Yellow key', 'The Graveyard (E1M9) - Blue key', 'The Graveyard (E1M9) - Green key', 'The Graveyard (E1M9) - Yellow key', 'The Great Hall (E2M7) - Blue key', 'The Great Hall (E2M7) - Green key', 'The Great Hall (E2M7) - Yellow key', 'The Guard Tower (E1M4) - Green key', 'The Guard Tower (E1M4) - Yellow key', 'The Halls of Fear (E3M6) - Blue key', 'The Halls of Fear (E3M6) - Green key', 'The Halls of Fear (E3M6) - Yellow key', 'The Ice Grotto (E2M4) - Blue key', 'The Ice Grotto (E2M4) - Green key', 'The Ice Grotto (E2M4) - Yellow key', 'The Labyrinth (E2M6) - Blue key', 'The Labyrinth (E2M6) - Green key', 'The Labyrinth (E2M6) - Yellow key', 'The Lava Pits (E2M2) - Green key', 'The Lava Pits (E2M2) - Yellow key', 'The Ophidian Lair (E3M5) - Green key', 'The Ophidian Lair (E3M5) - Yellow key', 'The River of Fire (E2M3) - Blue key', 'The River of Fire (E2M3) - Green key', 'The River of Fire (E2M3) - Yellow key', 'The Storehouse (E3M1) - Green key', 'The Storehouse (E3M1) - Yellow key', },
'Levels': {'Ambulatory (E4M3)', 'Blockhouse (E4M2)', 'Catafalque (E4M1)', 'Colonnade (E5M6)', 'Courtyard (E5M4)', "D'Sparil's Keep (E3M8)", 'Field of Judgement (E5M8)', 'Foetid Manse (E5M7)', 'Great Stair (E4M5)', 'Halls of the Apostate (E4M6)', "Hell's Maw (E1M8)", 'Hydratyr (E5M5)', 'Mausoleum (E4M9)', 'Ochre Cliffs (E5M1)', 'Quay (E5M3)', 'Ramparts of Perdition (E4M7)', 'Rapids (E5M2)', 'Sepulcher (E4M4)', 'Shattered Bridge (E4M8)', "Skein of D'Sparil (E5M9)", 'The Aquifer (E3M9)', 'The Azure Fortress (E3M4)', 'The Catacombs (E2M5)', 'The Cathedral (E1M6)', 'The Cesspool (E3M2)', 'The Chasm (E3M7)', 'The Citadel (E1M5)', 'The Confluence (E3M3)', 'The Crater (E2M1)', 'The Crypts (E1M7)', 'The Docks (E1M1)', 'The Dungeons (E1M2)', 'The Gatehouse (E1M3)', 'The Glacier (E2M9)', 'The Graveyard (E1M9)', 'The Great Hall (E2M7)', 'The Guard Tower (E1M4)', 'The Halls of Fear (E3M6)', 'The Ice Grotto (E2M4)', 'The Labyrinth (E2M6)', 'The Lava Pits (E2M2)', 'The Ophidian Lair (E3M5)', 'The Portals of Chaos (E2M8)', 'The River of Fire (E2M3)', 'The Storehouse (E3M1)', },
'Map Scrolls': {'Ambulatory (E4M3) - Map Scroll', 'Blockhouse (E4M2) - Map Scroll', 'Catafalque (E4M1) - Map Scroll', 'Colonnade (E5M6) - Map Scroll', 'Courtyard (E5M4) - Map Scroll', "D'Sparil's Keep (E3M8) - Map Scroll", 'Field of Judgement (E5M8) - Map Scroll', 'Foetid Manse (E5M7) - Map Scroll', 'Great Stair (E4M5) - Map Scroll', 'Halls of the Apostate (E4M6) - Map Scroll', "Hell's Maw (E1M8) - Map Scroll", 'Hydratyr (E5M5) - Map Scroll', 'Mausoleum (E4M9) - Map Scroll', 'Ochre Cliffs (E5M1) - Map Scroll', 'Quay (E5M3) - Map Scroll', 'Ramparts of Perdition (E4M7) - Map Scroll', 'Rapids (E5M2) - Map Scroll', 'Sepulcher (E4M4) - Map Scroll', 'Shattered Bridge (E4M8) - Map Scroll', "Skein of D'Sparil (E5M9) - Map Scroll", 'The Aquifer (E3M9) - Map Scroll', 'The Azure Fortress (E3M4) - Map Scroll', 'The Catacombs (E2M5) - Map Scroll', 'The Cathedral (E1M6) - Map Scroll', 'The Cesspool (E3M2) - Map Scroll', 'The Chasm (E3M7) - Map Scroll', 'The Citadel (E1M5) - Map Scroll', 'The Confluence (E3M3) - Map Scroll', 'The Crater (E2M1) - Map Scroll', 'The Crypts (E1M7) - Map Scroll', 'The Docks (E1M1) - Map Scroll', 'The Dungeons (E1M2) - Map Scroll', 'The Gatehouse (E1M3) - Map Scroll', 'The Glacier (E2M9) - Map Scroll', 'The Graveyard (E1M9) - Map Scroll', 'The Great Hall (E2M7) - Map Scroll', 'The Guard Tower (E1M4) - Map Scroll', 'The Halls of Fear (E3M6) - Map Scroll', 'The Ice Grotto (E2M4) - Map Scroll', 'The Labyrinth (E2M6) - Map Scroll', 'The Lava Pits (E2M2) - Map Scroll', 'The Ophidian Lair (E3M5) - Map Scroll', 'The Portals of Chaos (E2M8) - Map Scroll', 'The River of Fire (E2M3) - Map Scroll', 'The Storehouse (E3M1) - Map Scroll', },
'Weapons': {'Dragon Claw', 'Ethereal Crossbow', 'Firemace', 'Gauntlets of the Necromancer', 'Hellstaff', 'Phoenix Rod', },
}

View File

@@ -3633,300 +3633,300 @@ location_table: Dict[int, LocationDict] = {
'index': -1,
'doom_type': -1,
'region': "The Chasm (E3M7) Blue"},
371517: {'name': "D'Sparil'S Keep (E3M8) - Phoenix Rod",
371517: {'name': "D'Sparil's Keep (E3M8) - Phoenix Rod",
'episode': 3,
'check_sanity': False,
'map': 8,
'index': 55,
'doom_type': 2003,
'region': "D'Sparil'S Keep (E3M8) Main"},
371518: {'name': "D'Sparil'S Keep (E3M8) - Ethereal Crossbow",
'region': "D'Sparil's Keep (E3M8) Main"},
371518: {'name': "D'Sparil's Keep (E3M8) - Ethereal Crossbow",
'episode': 3,
'check_sanity': True,
'map': 8,
'index': 56,
'doom_type': 2001,
'region': "D'Sparil'S Keep (E3M8) Main"},
371519: {'name': "D'Sparil'S Keep (E3M8) - Dragon Claw",
'region': "D'Sparil's Keep (E3M8) Main"},
371519: {'name': "D'Sparil's Keep (E3M8) - Dragon Claw",
'episode': 3,
'check_sanity': False,
'map': 8,
'index': 57,
'doom_type': 53,
'region': "D'Sparil'S Keep (E3M8) Main"},
371520: {'name': "D'Sparil'S Keep (E3M8) - Gauntlets of the Necromancer",
'region': "D'Sparil's Keep (E3M8) Main"},
371520: {'name': "D'Sparil's Keep (E3M8) - Gauntlets of the Necromancer",
'episode': 3,
'check_sanity': False,
'map': 8,
'index': 58,
'doom_type': 2005,
'region': "D'Sparil'S Keep (E3M8) Main"},
371521: {'name': "D'Sparil'S Keep (E3M8) - Hellstaff",
'region': "D'Sparil's Keep (E3M8) Main"},
371521: {'name': "D'Sparil's Keep (E3M8) - Hellstaff",
'episode': 3,
'check_sanity': False,
'map': 8,
'index': 59,
'doom_type': 2004,
'region': "D'Sparil'S Keep (E3M8) Main"},
371522: {'name': "D'Sparil'S Keep (E3M8) - Bag of Holding",
'region': "D'Sparil's Keep (E3M8) Main"},
371522: {'name': "D'Sparil's Keep (E3M8) - Bag of Holding",
'episode': 3,
'check_sanity': False,
'map': 8,
'index': 63,
'doom_type': 8,
'region': "D'Sparil'S Keep (E3M8) Main"},
371523: {'name': "D'Sparil'S Keep (E3M8) - Mystic Urn",
'region': "D'Sparil's Keep (E3M8) Main"},
371523: {'name': "D'Sparil's Keep (E3M8) - Mystic Urn",
'episode': 3,
'check_sanity': False,
'map': 8,
'index': 64,
'doom_type': 32,
'region': "D'Sparil'S Keep (E3M8) Main"},
371524: {'name': "D'Sparil'S Keep (E3M8) - Ring of Invincibility",
'region': "D'Sparil's Keep (E3M8) Main"},
371524: {'name': "D'Sparil's Keep (E3M8) - Ring of Invincibility",
'episode': 3,
'check_sanity': False,
'map': 8,
'index': 65,
'doom_type': 84,
'region': "D'Sparil'S Keep (E3M8) Main"},
371525: {'name': "D'Sparil'S Keep (E3M8) - Shadowsphere",
'region': "D'Sparil's Keep (E3M8) Main"},
371525: {'name': "D'Sparil's Keep (E3M8) - Shadowsphere",
'episode': 3,
'check_sanity': False,
'map': 8,
'index': 66,
'doom_type': 75,
'region': "D'Sparil'S Keep (E3M8) Main"},
371526: {'name': "D'Sparil'S Keep (E3M8) - Silver Shield",
'region': "D'Sparil's Keep (E3M8) Main"},
371526: {'name': "D'Sparil's Keep (E3M8) - Silver Shield",
'episode': 3,
'check_sanity': False,
'map': 8,
'index': 67,
'doom_type': 85,
'region': "D'Sparil'S Keep (E3M8) Main"},
371527: {'name': "D'Sparil'S Keep (E3M8) - Enchanted Shield",
'region': "D'Sparil's Keep (E3M8) Main"},
371527: {'name': "D'Sparil's Keep (E3M8) - Enchanted Shield",
'episode': 3,
'check_sanity': False,
'map': 8,
'index': 68,
'doom_type': 31,
'region': "D'Sparil'S Keep (E3M8) Main"},
371528: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power",
'region': "D'Sparil's Keep (E3M8) Main"},
371528: {'name': "D'Sparil's Keep (E3M8) - Tome of Power",
'episode': 3,
'check_sanity': False,
'map': 8,
'index': 69,
'doom_type': 86,
'region': "D'Sparil'S Keep (E3M8) Main"},
371529: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power 2",
'region': "D'Sparil's Keep (E3M8) Main"},
371529: {'name': "D'Sparil's Keep (E3M8) - Tome of Power 2",
'episode': 3,
'check_sanity': True,
'map': 8,
'index': 70,
'doom_type': 86,
'region': "D'Sparil'S Keep (E3M8) Main"},
371530: {'name': "D'Sparil'S Keep (E3M8) - Chaos Device",
'region': "D'Sparil's Keep (E3M8) Main"},
371530: {'name': "D'Sparil's Keep (E3M8) - Chaos Device",
'episode': 3,
'check_sanity': True,
'map': 8,
'index': 71,
'doom_type': 36,
'region': "D'Sparil'S Keep (E3M8) Main"},
371531: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power 3",
'region': "D'Sparil's Keep (E3M8) Main"},
371531: {'name': "D'Sparil's Keep (E3M8) - Tome of Power 3",
'episode': 3,
'check_sanity': True,
'map': 8,
'index': 245,
'doom_type': 86,
'region': "D'Sparil'S Keep (E3M8) Main"},
371532: {'name': "D'Sparil'S Keep (E3M8) - Exit",
'region': "D'Sparil's Keep (E3M8) Main"},
371532: {'name': "D'Sparil's Keep (E3M8) - Exit",
'episode': 3,
'check_sanity': False,
'map': 8,
'index': -1,
'doom_type': -1,
'region': "D'Sparil'S Keep (E3M8) Main"},
371533: {'name': 'The Aquifier (E3M9) - Blue key',
'region': "D'Sparil's Keep (E3M8) Main"},
371533: {'name': 'The Aquifer (E3M9) - Blue key',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 12,
'doom_type': 79,
'region': "The Aquifier (E3M9) Green"},
371534: {'name': 'The Aquifier (E3M9) - Green key',
'region': "The Aquifer (E3M9) Green"},
371534: {'name': 'The Aquifer (E3M9) - Green key',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 13,
'doom_type': 73,
'region': "The Aquifier (E3M9) Yellow"},
371535: {'name': 'The Aquifier (E3M9) - Yellow key',
'region': "The Aquifer (E3M9) Yellow"},
371535: {'name': 'The Aquifer (E3M9) - Yellow key',
'episode': 3,
'check_sanity': True,
'map': 9,
'index': 14,
'doom_type': 80,
'region': "The Aquifier (E3M9) Main"},
371536: {'name': 'The Aquifier (E3M9) - Ethereal Crossbow',
'region': "The Aquifer (E3M9) Main"},
371536: {'name': 'The Aquifer (E3M9) - Ethereal Crossbow',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 141,
'doom_type': 2001,
'region': "The Aquifier (E3M9) Main"},
371537: {'name': 'The Aquifier (E3M9) - Phoenix Rod',
'region': "The Aquifer (E3M9) Main"},
371537: {'name': 'The Aquifer (E3M9) - Phoenix Rod',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 142,
'doom_type': 2003,
'region': "The Aquifier (E3M9) Yellow"},
371538: {'name': 'The Aquifier (E3M9) - Dragon Claw',
'region': "The Aquifer (E3M9) Yellow"},
371538: {'name': 'The Aquifer (E3M9) - Dragon Claw',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 143,
'doom_type': 53,
'region': "The Aquifier (E3M9) Green"},
371539: {'name': 'The Aquifier (E3M9) - Hellstaff',
'region': "The Aquifer (E3M9) Green"},
371539: {'name': 'The Aquifer (E3M9) - Hellstaff',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 144,
'doom_type': 2004,
'region': "The Aquifier (E3M9) Green"},
371540: {'name': 'The Aquifier (E3M9) - Gauntlets of the Necromancer',
'region': "The Aquifer (E3M9) Green"},
371540: {'name': 'The Aquifer (E3M9) - Gauntlets of the Necromancer',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 145,
'doom_type': 2005,
'region': "The Aquifier (E3M9) Green"},
371541: {'name': 'The Aquifier (E3M9) - Ring of Invincibility',
'region': "The Aquifer (E3M9) Green"},
371541: {'name': 'The Aquifer (E3M9) - Ring of Invincibility',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 148,
'doom_type': 84,
'region': "The Aquifier (E3M9) Yellow"},
371542: {'name': 'The Aquifier (E3M9) - Mystic Urn',
'region': "The Aquifer (E3M9) Yellow"},
371542: {'name': 'The Aquifer (E3M9) - Mystic Urn',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 149,
'doom_type': 32,
'region': "The Aquifier (E3M9) Green"},
371543: {'name': 'The Aquifier (E3M9) - Silver Shield',
'region': "The Aquifer (E3M9) Green"},
371543: {'name': 'The Aquifer (E3M9) - Silver Shield',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 151,
'doom_type': 85,
'region': "The Aquifier (E3M9) Main"},
371544: {'name': 'The Aquifier (E3M9) - Tome of Power',
'region': "The Aquifer (E3M9) Main"},
371544: {'name': 'The Aquifer (E3M9) - Tome of Power',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 152,
'doom_type': 86,
'region': "The Aquifier (E3M9) Main"},
371545: {'name': 'The Aquifier (E3M9) - Bag of Holding',
'region': "The Aquifer (E3M9) Main"},
371545: {'name': 'The Aquifer (E3M9) - Bag of Holding',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 153,
'doom_type': 8,
'region': "The Aquifier (E3M9) Yellow"},
371546: {'name': 'The Aquifier (E3M9) - Morph Ovum',
'region': "The Aquifer (E3M9) Yellow"},
371546: {'name': 'The Aquifer (E3M9) - Morph Ovum',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 154,
'doom_type': 30,
'region': "The Aquifier (E3M9) Green"},
371547: {'name': 'The Aquifier (E3M9) - Map Scroll',
'region': "The Aquifer (E3M9) Green"},
371547: {'name': 'The Aquifer (E3M9) - Map Scroll',
'episode': 3,
'check_sanity': True,
'map': 9,
'index': 155,
'doom_type': 35,
'region': "The Aquifier (E3M9) Green"},
371548: {'name': 'The Aquifier (E3M9) - Chaos Device',
'region': "The Aquifer (E3M9) Green"},
371548: {'name': 'The Aquifer (E3M9) - Chaos Device',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 156,
'doom_type': 36,
'region': "The Aquifier (E3M9) Yellow"},
371549: {'name': 'The Aquifier (E3M9) - Enchanted Shield',
'region': "The Aquifer (E3M9) Yellow"},
371549: {'name': 'The Aquifer (E3M9) - Enchanted Shield',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 157,
'doom_type': 31,
'region': "The Aquifier (E3M9) Green"},
371550: {'name': 'The Aquifier (E3M9) - Tome of Power 2',
'region': "The Aquifer (E3M9) Green"},
371550: {'name': 'The Aquifer (E3M9) - Tome of Power 2',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 158,
'doom_type': 86,
'region': "The Aquifier (E3M9) Green"},
371551: {'name': 'The Aquifier (E3M9) - Torch',
'region': "The Aquifer (E3M9) Green"},
371551: {'name': 'The Aquifer (E3M9) - Torch',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 159,
'doom_type': 33,
'region': "The Aquifier (E3M9) Main"},
371552: {'name': 'The Aquifier (E3M9) - Shadowsphere',
'region': "The Aquifer (E3M9) Main"},
371552: {'name': 'The Aquifer (E3M9) - Shadowsphere',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 160,
'doom_type': 75,
'region': "The Aquifier (E3M9) Green"},
371553: {'name': 'The Aquifier (E3M9) - Silver Shield 2',
'region': "The Aquifer (E3M9) Green"},
371553: {'name': 'The Aquifer (E3M9) - Silver Shield 2',
'episode': 3,
'check_sanity': True,
'map': 9,
'index': 374,
'doom_type': 85,
'region': "The Aquifier (E3M9) Green"},
371554: {'name': 'The Aquifier (E3M9) - Firemace',
'region': "The Aquifer (E3M9) Green"},
371554: {'name': 'The Aquifer (E3M9) - Firemace',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 478,
'doom_type': 2002,
'region': "The Aquifier (E3M9) Green"},
371555: {'name': 'The Aquifier (E3M9) - Firemace 2',
'region': "The Aquifer (E3M9) Green"},
371555: {'name': 'The Aquifer (E3M9) - Firemace 2',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 526,
'doom_type': 2002,
'region': "The Aquifier (E3M9) Green"},
371556: {'name': 'The Aquifier (E3M9) - Firemace 3',
'region': "The Aquifer (E3M9) Green"},
371556: {'name': 'The Aquifer (E3M9) - Firemace 3',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': 527,
'doom_type': 2002,
'region': "The Aquifier (E3M9) Green"},
371557: {'name': 'The Aquifier (E3M9) - Firemace 4',
'region': "The Aquifer (E3M9) Green"},
371557: {'name': 'The Aquifer (E3M9) - Firemace 4',
'episode': 3,
'check_sanity': True,
'map': 9,
'index': 528,
'doom_type': 2002,
'region': "The Aquifier (E3M9) Yellow"},
371558: {'name': 'The Aquifier (E3M9) - Exit',
'region': "The Aquifer (E3M9) Yellow"},
371558: {'name': 'The Aquifer (E3M9) - Exit',
'episode': 3,
'check_sanity': False,
'map': 9,
'index': -1,
'doom_type': -1,
'region': "The Aquifier (E3M9) Blue"},
'region': "The Aquifer (E3M9) Blue"},
371559: {'name': 'Catafalque (E4M1) - Yellow key',
'episode': 4,
'check_sanity': False,
@@ -5963,7 +5963,7 @@ location_table: Dict[int, LocationDict] = {
'map': 3,
'index': 213,
'doom_type': 2005,
'region': "Quay (E5M3) Main"},
'region': "Quay (E5M3) Blue"},
371850: {'name': 'Quay (E5M3) - Dragon Claw',
'episode': 5,
'check_sanity': False,
@@ -6145,7 +6145,7 @@ location_table: Dict[int, LocationDict] = {
'map': 4,
'index': 3,
'doom_type': 79,
'region': "Courtyard (E5M4) Main"},
'region': "Courtyard (E5M4) Green"},
371876: {'name': 'Courtyard (E5M4) - Yellow key',
'episode': 5,
'check_sanity': False,
@@ -6159,7 +6159,7 @@ location_table: Dict[int, LocationDict] = {
'map': 4,
'index': 21,
'doom_type': 73,
'region': "Courtyard (E5M4) Kakis"},
'region': "Courtyard (E5M4) Yellow"},
371878: {'name': 'Courtyard (E5M4) - Gauntlets of the Necromancer',
'episode': 5,
'check_sanity': False,
@@ -6187,14 +6187,14 @@ location_table: Dict[int, LocationDict] = {
'map': 4,
'index': 87,
'doom_type': 2004,
'region': "Courtyard (E5M4) Kakis"},
'region': "Courtyard (E5M4) Yellow"},
371882: {'name': 'Courtyard (E5M4) - Phoenix Rod',
'episode': 5,
'check_sanity': False,
'map': 4,
'index': 88,
'doom_type': 2003,
'region': "Courtyard (E5M4) Main"},
'region': "Courtyard (E5M4) Green"},
371883: {'name': 'Courtyard (E5M4) - Morph Ovum',
'episode': 5,
'check_sanity': False,
@@ -6229,7 +6229,7 @@ location_table: Dict[int, LocationDict] = {
'map': 4,
'index': 104,
'doom_type': 84,
'region': "Courtyard (E5M4) Kakis"},
'region': "Courtyard (E5M4) Yellow"},
371888: {'name': 'Courtyard (E5M4) - Shadowsphere',
'episode': 5,
'check_sanity': False,
@@ -6250,14 +6250,14 @@ location_table: Dict[int, LocationDict] = {
'map': 4,
'index': 107,
'doom_type': 35,
'region': "Courtyard (E5M4) Kakis"},
'region': "Courtyard (E5M4) Yellow"},
371891: {'name': 'Courtyard (E5M4) - Chaos Device',
'episode': 5,
'check_sanity': False,
'map': 4,
'index': 108,
'doom_type': 36,
'region': "Courtyard (E5M4) Main"},
'region': "Courtyard (E5M4) Green"},
371892: {'name': 'Courtyard (E5M4) - Tome of Power',
'episode': 5,
'check_sanity': False,
@@ -6278,7 +6278,7 @@ location_table: Dict[int, LocationDict] = {
'map': 4,
'index': 111,
'doom_type': 86,
'region': "Courtyard (E5M4) Kakis"},
'region': "Courtyard (E5M4) Yellow"},
371895: {'name': 'Courtyard (E5M4) - Torch',
'episode': 5,
'check_sanity': False,
@@ -6299,7 +6299,7 @@ location_table: Dict[int, LocationDict] = {
'map': 4,
'index': 219,
'doom_type': 85,
'region': "Courtyard (E5M4) Kakis"},
'region': "Courtyard (E5M4) Yellow"},
371898: {'name': 'Courtyard (E5M4) - Bag of Holding 3',
'episode': 5,
'check_sanity': False,
@@ -7247,23 +7247,23 @@ location_name_groups: Dict[str, Set[str]] = {
'Courtyard (E5M4) - Torch',
'Courtyard (E5M4) - Yellow key',
},
"D'Sparil'S Keep (E3M8)": {
"D'Sparil'S Keep (E3M8) - Bag of Holding",
"D'Sparil'S Keep (E3M8) - Chaos Device",
"D'Sparil'S Keep (E3M8) - Dragon Claw",
"D'Sparil'S Keep (E3M8) - Enchanted Shield",
"D'Sparil'S Keep (E3M8) - Ethereal Crossbow",
"D'Sparil'S Keep (E3M8) - Exit",
"D'Sparil'S Keep (E3M8) - Gauntlets of the Necromancer",
"D'Sparil'S Keep (E3M8) - Hellstaff",
"D'Sparil'S Keep (E3M8) - Mystic Urn",
"D'Sparil'S Keep (E3M8) - Phoenix Rod",
"D'Sparil'S Keep (E3M8) - Ring of Invincibility",
"D'Sparil'S Keep (E3M8) - Shadowsphere",
"D'Sparil'S Keep (E3M8) - Silver Shield",
"D'Sparil'S Keep (E3M8) - Tome of Power",
"D'Sparil'S Keep (E3M8) - Tome of Power 2",
"D'Sparil'S Keep (E3M8) - Tome of Power 3",
"D'Sparil's Keep (E3M8)": {
"D'Sparil's Keep (E3M8) - Bag of Holding",
"D'Sparil's Keep (E3M8) - Chaos Device",
"D'Sparil's Keep (E3M8) - Dragon Claw",
"D'Sparil's Keep (E3M8) - Enchanted Shield",
"D'Sparil's Keep (E3M8) - Ethereal Crossbow",
"D'Sparil's Keep (E3M8) - Exit",
"D'Sparil's Keep (E3M8) - Gauntlets of the Necromancer",
"D'Sparil's Keep (E3M8) - Hellstaff",
"D'Sparil's Keep (E3M8) - Mystic Urn",
"D'Sparil's Keep (E3M8) - Phoenix Rod",
"D'Sparil's Keep (E3M8) - Ring of Invincibility",
"D'Sparil's Keep (E3M8) - Shadowsphere",
"D'Sparil's Keep (E3M8) - Silver Shield",
"D'Sparil's Keep (E3M8) - Tome of Power",
"D'Sparil's Keep (E3M8) - Tome of Power 2",
"D'Sparil's Keep (E3M8) - Tome of Power 3",
},
'Field of Judgement (E5M8)': {
'Field of Judgement (E5M8) - Bag of Holding',
@@ -7641,33 +7641,33 @@ location_name_groups: Dict[str, Set[str]] = {
"Skein of D'Sparil (E5M9) - Torch",
"Skein of D'Sparil (E5M9) - Yellow key",
},
'The Aquifier (E3M9)': {
'The Aquifier (E3M9) - Bag of Holding',
'The Aquifier (E3M9) - Blue key',
'The Aquifier (E3M9) - Chaos Device',
'The Aquifier (E3M9) - Dragon Claw',
'The Aquifier (E3M9) - Enchanted Shield',
'The Aquifier (E3M9) - Ethereal Crossbow',
'The Aquifier (E3M9) - Exit',
'The Aquifier (E3M9) - Firemace',
'The Aquifier (E3M9) - Firemace 2',
'The Aquifier (E3M9) - Firemace 3',
'The Aquifier (E3M9) - Firemace 4',
'The Aquifier (E3M9) - Gauntlets of the Necromancer',
'The Aquifier (E3M9) - Green key',
'The Aquifier (E3M9) - Hellstaff',
'The Aquifier (E3M9) - Map Scroll',
'The Aquifier (E3M9) - Morph Ovum',
'The Aquifier (E3M9) - Mystic Urn',
'The Aquifier (E3M9) - Phoenix Rod',
'The Aquifier (E3M9) - Ring of Invincibility',
'The Aquifier (E3M9) - Shadowsphere',
'The Aquifier (E3M9) - Silver Shield',
'The Aquifier (E3M9) - Silver Shield 2',
'The Aquifier (E3M9) - Tome of Power',
'The Aquifier (E3M9) - Tome of Power 2',
'The Aquifier (E3M9) - Torch',
'The Aquifier (E3M9) - Yellow key',
'The Aquifer (E3M9)': {
'The Aquifer (E3M9) - Bag of Holding',
'The Aquifer (E3M9) - Blue key',
'The Aquifer (E3M9) - Chaos Device',
'The Aquifer (E3M9) - Dragon Claw',
'The Aquifer (E3M9) - Enchanted Shield',
'The Aquifer (E3M9) - Ethereal Crossbow',
'The Aquifer (E3M9) - Exit',
'The Aquifer (E3M9) - Firemace',
'The Aquifer (E3M9) - Firemace 2',
'The Aquifer (E3M9) - Firemace 3',
'The Aquifer (E3M9) - Firemace 4',
'The Aquifer (E3M9) - Gauntlets of the Necromancer',
'The Aquifer (E3M9) - Green key',
'The Aquifer (E3M9) - Hellstaff',
'The Aquifer (E3M9) - Map Scroll',
'The Aquifer (E3M9) - Morph Ovum',
'The Aquifer (E3M9) - Mystic Urn',
'The Aquifer (E3M9) - Phoenix Rod',
'The Aquifer (E3M9) - Ring of Invincibility',
'The Aquifer (E3M9) - Shadowsphere',
'The Aquifer (E3M9) - Silver Shield',
'The Aquifer (E3M9) - Silver Shield 2',
'The Aquifer (E3M9) - Tome of Power',
'The Aquifer (E3M9) - Tome of Power 2',
'The Aquifer (E3M9) - Torch',
'The Aquifer (E3M9) - Yellow key',
},
'The Azure Fortress (E3M4)': {
'The Azure Fortress (E3M4) - Bag of Holding',

View File

@@ -29,8 +29,8 @@ map_names: List[str] = [
'The Ophidian Lair (E3M5)',
'The Halls of Fear (E3M6)',
'The Chasm (E3M7)',
"D'Sparil'S Keep (E3M8)",
'The Aquifier (E3M9)',
"D'Sparil's Keep (E3M8)",
'The Aquifer (E3M9)',
'Catafalque (E4M1)',
'Blockhouse (E4M2)',
'Ambulatory (E4M3)',

View File

@@ -520,34 +520,34 @@ regions:List[RegionDict] = [
"episode":3,
"connections":[{"target":"The Chasm (E3M7) Yellow","pro":False}]},
# D'Sparil'S Keep (E3M8)
{"name":"D'Sparil'S Keep (E3M8) Main",
# D'Sparil's Keep (E3M8)
{"name":"D'Sparil's Keep (E3M8) Main",
"connects_to_hub":True,
"episode":3,
"connections":[]},
# The Aquifier (E3M9)
{"name":"The Aquifier (E3M9) Main",
# The Aquifer (E3M9)
{"name":"The Aquifer (E3M9) Main",
"connects_to_hub":True,
"episode":3,
"connections":[{"target":"The Aquifier (E3M9) Yellow","pro":False}]},
{"name":"The Aquifier (E3M9) Blue",
"connections":[{"target":"The Aquifer (E3M9) Yellow","pro":False}]},
{"name":"The Aquifer (E3M9) Blue",
"connects_to_hub":False,
"episode":3,
"connections":[]},
{"name":"The Aquifier (E3M9) Yellow",
{"name":"The Aquifer (E3M9) Yellow",
"connects_to_hub":False,
"episode":3,
"connections":[
{"target":"The Aquifier (E3M9) Green","pro":False},
{"target":"The Aquifier (E3M9) Main","pro":False}]},
{"name":"The Aquifier (E3M9) Green",
{"target":"The Aquifer (E3M9) Green","pro":False},
{"target":"The Aquifer (E3M9) Main","pro":False}]},
{"name":"The Aquifer (E3M9) Green",
"connects_to_hub":False,
"episode":3,
"connections":[
{"target":"The Aquifier (E3M9) Yellow","pro":False},
{"target":"The Aquifier (E3M9) Main","pro":False},
{"target":"The Aquifier (E3M9) Blue","pro":False}]},
{"target":"The Aquifer (E3M9) Yellow","pro":False},
{"target":"The Aquifer (E3M9) Main","pro":False},
{"target":"The Aquifer (E3M9) Blue","pro":False}]},
# Catafalque (E4M1)
{"name":"Catafalque (E4M1) Main",
@@ -795,16 +795,22 @@ regions:List[RegionDict] = [
"connects_to_hub":True,
"episode":5,
"connections":[
{"target":"Courtyard (E5M4) Kakis","pro":False},
{"target":"Courtyard (E5M4) Yellow","pro":False},
{"target":"Courtyard (E5M4) Blue","pro":False}]},
{"name":"Courtyard (E5M4) Blue",
"connects_to_hub":False,
"episode":5,
"connections":[{"target":"Courtyard (E5M4) Main","pro":False}]},
{"name":"Courtyard (E5M4) Kakis",
{"name":"Courtyard (E5M4) Yellow",
"connects_to_hub":False,
"episode":5,
"connections":[{"target":"Courtyard (E5M4) Main","pro":False}]},
"connections":[
{"target":"Courtyard (E5M4) Main","pro":False},
{"target":"Courtyard (E5M4) Green","pro":False}]},
{"name":"Courtyard (E5M4) Green",
"connects_to_hub":False,
"episode":5,
"connections":[{"target":"Courtyard (E5M4) Yellow","pro":False}]},
# Hydratyr (E5M5)
{"name":"Hydratyr (E5M5) Main",

View File

@@ -388,9 +388,9 @@ def set_episode3_rules(player, multiworld, pro):
set_rule(multiworld.get_entrance("The Chasm (E3M7) Green -> The Chasm (E3M7) Yellow", player), lambda state:
state.has("The Chasm (E3M7) - Green key", player, 1))
# D'Sparil'S Keep (E3M8)
set_rule(multiworld.get_entrance("Hub -> D'Sparil'S Keep (E3M8) Main", player), lambda state:
state.has("D'Sparil'S Keep (E3M8)", player, 1) and
# D'Sparil's Keep (E3M8)
set_rule(multiworld.get_entrance("Hub -> D'Sparil's Keep (E3M8) Main", player), lambda state:
state.has("D'Sparil's Keep (E3M8)", player, 1) and
state.has("Gauntlets of the Necromancer", player, 1) and
state.has("Ethereal Crossbow", player, 1) and
state.has("Dragon Claw", player, 1) and
@@ -398,23 +398,23 @@ def set_episode3_rules(player, multiworld, pro):
state.has("Firemace", player, 1) and
state.has("Hellstaff", player, 1))
# The Aquifier (E3M9)
set_rule(multiworld.get_entrance("Hub -> The Aquifier (E3M9) Main", player), lambda state:
state.has("The Aquifier (E3M9)", player, 1) and
# The Aquifer (E3M9)
set_rule(multiworld.get_entrance("Hub -> The Aquifer (E3M9) Main", player), lambda state:
state.has("The Aquifer (E3M9)", player, 1) and
state.has("Gauntlets of the Necromancer", player, 1) and
state.has("Ethereal Crossbow", player, 1) and
state.has("Dragon Claw", player, 1) and
state.has("Phoenix Rod", player, 1) and
state.has("Firemace", player, 1) and
state.has("Hellstaff", player, 1))
set_rule(multiworld.get_entrance("The Aquifier (E3M9) Main -> The Aquifier (E3M9) Yellow", player), lambda state:
state.has("The Aquifier (E3M9) - Yellow key", player, 1))
set_rule(multiworld.get_entrance("The Aquifier (E3M9) Yellow -> The Aquifier (E3M9) Green", player), lambda state:
state.has("The Aquifier (E3M9) - Green key", player, 1))
set_rule(multiworld.get_entrance("The Aquifier (E3M9) Yellow -> The Aquifier (E3M9) Main", player), lambda state:
state.has("The Aquifier (E3M9) - Yellow key", player, 1))
set_rule(multiworld.get_entrance("The Aquifier (E3M9) Green -> The Aquifier (E3M9) Yellow", player), lambda state:
state.has("The Aquifier (E3M9) - Green key", player, 1))
set_rule(multiworld.get_entrance("The Aquifer (E3M9) Main -> The Aquifer (E3M9) Yellow", player), lambda state:
state.has("The Aquifer (E3M9) - Yellow key", player, 1))
set_rule(multiworld.get_entrance("The Aquifer (E3M9) Yellow -> The Aquifer (E3M9) Green", player), lambda state:
state.has("The Aquifer (E3M9) - Green key", player, 1))
set_rule(multiworld.get_entrance("The Aquifer (E3M9) Yellow -> The Aquifer (E3M9) Main", player), lambda state:
state.has("The Aquifer (E3M9) - Yellow key", player, 1))
set_rule(multiworld.get_entrance("The Aquifer (E3M9) Green -> The Aquifer (E3M9) Yellow", player), lambda state:
state.has("The Aquifer (E3M9) - Green key", player, 1))
def set_episode4_rules(player, multiworld, pro):
@@ -623,15 +623,17 @@ def set_episode5_rules(player, multiworld, pro):
(state.has("Phoenix Rod", player, 1) or
state.has("Firemace", player, 1) or
state.has("Hellstaff", player, 1)))
set_rule(multiworld.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Kakis", player), lambda state:
state.has("Courtyard (E5M4) - Yellow key", player, 1) or
state.has("Courtyard (E5M4) - Green key", player, 1))
set_rule(multiworld.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Yellow", player), lambda state:
state.has("Courtyard (E5M4) - Yellow key", player, 1))
set_rule(multiworld.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Blue", player), lambda state:
state.has("Courtyard (E5M4) - Blue key", player, 1))
set_rule(multiworld.get_entrance("Courtyard (E5M4) Blue -> Courtyard (E5M4) Main", player), lambda state:
state.has("Courtyard (E5M4) - Blue key", player, 1))
set_rule(multiworld.get_entrance("Courtyard (E5M4) Kakis -> Courtyard (E5M4) Main", player), lambda state:
state.has("Courtyard (E5M4) - Yellow key", player, 1) or
set_rule(multiworld.get_entrance("Courtyard (E5M4) Yellow -> Courtyard (E5M4) Main", player), lambda state:
state.has("Courtyard (E5M4) - Yellow key", player, 1))
set_rule(multiworld.get_entrance("Courtyard (E5M4) Yellow -> Courtyard (E5M4) Green", player), lambda state:
state.has("Courtyard (E5M4) - Green key", player, 1))
set_rule(multiworld.get_entrance("Courtyard (E5M4) Green -> Courtyard (E5M4) Yellow", player), lambda state:
state.has("Courtyard (E5M4) - Green key", player, 1))
# Hydratyr (E5M5)

View File

@@ -49,18 +49,18 @@ class HereticWorld(World):
location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
location_name_groups = Locations.location_name_groups
starting_level_for_episode: List[str] = [
"The Docks (E1M1)",
"The Crater (E2M1)",
"The Storehouse (E3M1)",
"Catafalque (E4M1)",
"Ochre Cliffs (E5M1)"
]
starting_level_for_episode: Dict[int, str] = {
1: "The Docks (E1M1)",
2: "The Crater (E2M1)",
3: "The Storehouse (E3M1)",
4: "Catafalque (E4M1)",
5: "Ochre Cliffs (E5M1)"
}
boss_level_for_episode: List[str] = [
all_boss_levels: List[str] = [
"Hell's Maw (E1M8)",
"The Portals of Chaos (E2M8)",
"D'Sparil'S Keep (E3M8)",
"D'Sparil's Keep (E3M8)",
"Shattered Bridge (E4M8)",
"Field of Judgement (E5M8)"
]
@@ -82,6 +82,7 @@ class HereticWorld(World):
def __init__(self, multiworld: MultiWorld, player: int):
self.included_episodes = [1, 1, 1, 0, 0]
self.location_count = 0
self.starting_levels = []
super().__init__(multiworld, player)
@@ -100,6 +101,14 @@ class HereticWorld(World):
if self.get_episode_count() == 0:
self.included_episodes[0] = 1
self.starting_levels = [level_name for (episode, level_name) in self.starting_level_for_episode.items()
if self.included_episodes[episode - 1]]
# For Solo Episode 1, place the Yellow Key for E1M1 early.
# Gives the generator five potential placements (plus the forced key) instead of only two.
if self.get_episode_count() == 1 and self.included_episodes[0]:
self.multiworld.early_items[self.player]["The Docks (E1M1) - Yellow key"] = 1
def create_regions(self):
pro = self.options.pro.value
check_sanity = self.options.check_sanity.value
@@ -154,7 +163,7 @@ class HereticWorld(World):
def completion_rule(self, state: CollectionState):
goal_levels = Maps.map_names
if self.options.goal.value:
goal_levels = self.boss_level_for_episode
goal_levels = self.all_boss_levels
for map_name in goal_levels:
if map_name + " - Exit" not in self.location_name_to_id:
@@ -203,7 +212,7 @@ class HereticWorld(World):
if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]:
continue
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
count = item["count"] if item["name"] not in self.starting_levels else item["count"] - 1
itempool += [self.create_item(item["name"]) for _ in range(count)]
# Bag(s) of Holding based on options
@@ -236,9 +245,8 @@ class HereticWorld(World):
self.location_count -= 1
# Give starting levels right away
for i in range(len(self.included_episodes)):
if self.included_episodes[i]:
self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i]))
for map_name in self.starting_levels:
self.multiworld.push_precollected(self.create_item(map_name))
# Give Computer area maps if option selected
if self.options.start_with_map_scrolls.value:

View File

@@ -450,7 +450,7 @@ class GrubHuntGoal(NamedRange):
display_name = "Grub Hunt Goal"
range_start = 1
range_end = 46
special_range_names = {"all": -1}
special_range_names = {"all": -1, "forty_six": 46}
default = 46

View File

View File

@@ -1 +1 @@
Pymem>=1.10.0
Pymem>=1.10.0

View File

View File

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