diff --git a/WebHostLib/templates/player-settings.html b/WebHostLib/templates/player-options.html
similarity index 75%
rename from WebHostLib/templates/player-settings.html
rename to WebHostLib/templates/player-options.html
index 50b9e3cbb1a2..701b4e5861c0 100644
--- a/WebHostLib/templates/player-settings.html
+++ b/WebHostLib/templates/player-options.html
@@ -1,26 +1,26 @@
{% extends 'pageWrapper.html' %}
{% block head %}
- {{ game }} Settings
+ {{ game }} Options
-
+
-
+
{% endblock %}
{% block body %}
{% include 'header/'+theme+'Header.html' %}
-
+
-
Player Settings
+
Player Options
Choose the options you would like to play with! You may generate a single-player game from this page,
- or download a settings file you can use to participate in a MultiWorld.
+ or download an options file you can use to participate in a MultiWorld.
- A more advanced settings configuration for all games can be found on the
- Weighted Settings page.
+ A more advanced options configuration for all games can be found on the
+ Weighted options page.
A list of all games you have generated can be found on the User Content Page.
@@ -39,8 +39,8 @@
diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html
index 63b70216d705..3252b16ad4e7 100644
--- a/WebHostLib/templates/supportedGames.html
+++ b/WebHostLib/templates/supportedGames.html
@@ -5,15 +5,35 @@
+
{% endblock %}
{% block body %}
{% include 'header/oceanHeader.html' %}
Currently Supported Games
-
+
-
+
@@ -22,21 +42,21 @@
Currently Supported Games
{% for game_name in worlds | title_sorted %}
{% set world = worlds[game_name] %}
- ▶ {{ game_name }}
+ ▶{{ game_name }}
-
+
{{ world.__doc__ | default("No description provided.", true) }} Game Page
{% if world.web.tutorials %}
|Setup Guides
{% endif %}
- {% if world.web.settings_page is string %}
+ {% if world.web.options_page is string %}
|
- Settings Page
- {% elif world.web.settings_page %}
+ Options Page
+ {% elif world.web.options_page %}
|
- Settings Page
+ Options Page
{% endif %}
{% if world.web.bug_report_page %}
|
diff --git a/WebHostLib/templates/weighted-settings.html b/WebHostLib/templates/weighted-options.html
similarity index 82%
rename from WebHostLib/templates/weighted-settings.html
rename to WebHostLib/templates/weighted-options.html
index 9ce097c37fb5..032a4eeb905c 100644
--- a/WebHostLib/templates/weighted-settings.html
+++ b/WebHostLib/templates/weighted-options.html
@@ -1,26 +1,26 @@
{% extends 'pageWrapper.html' %}
{% block head %}
-
{{ game }} Settings
+ {{ game }} Options
-
+
-
+
{% endblock %}
{% block body %}
{% include 'header/grassHeader.html' %}
-
Weighted Settings
-
Weighted Settings allows you to choose how likely a particular option is to be used in game generation.
+
Weighted Options
+
Weighted options allow you to choose how likely a particular option is to be used in game generation.
The higher an option is weighted, the more likely the option will be chosen. Think of them like
entries in a raffle.
Choose the games and options you would like to play with! You may generate a single-player game from
- this page, or download a settings file you can use to participate in a MultiWorld.
+ this page, or download an options file you can use to participate in a MultiWorld.
A list of all games you have generated can be found on the User Content
page.
@@ -40,7 +40,7 @@
Weighted Settings
-
+
diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py
index 0d9ead795161..55b98df59e42 100644
--- a/WebHostLib/tracker.py
+++ b/WebHostLib/tracker.py
@@ -1532,9 +1532,11 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s
continue
player_locations = locations[player]
checks_done[team][player]["Total"] = len(locations_checked)
- percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] /
- len(player_locations) * 100) \
- if player_locations else 100
+ percent_total_checks_done[team][player] = (
+ checks_done[team][player]["Total"] / len(player_locations) * 100
+ if player_locations
+ else 100
+ )
activity_timers = {}
now = datetime.datetime.utcnow()
@@ -1690,10 +1692,13 @@ def attribute_item(team: int, recipient: int, item: int):
for recipient in recipients:
attribute_item(team, recipient, item)
checks_done[team][player][player_location_to_area[player][location]] += 1
- checks_done[team][player]["Total"] += 1
- percent_total_checks_done[team][player] = int(
- checks_done[team][player]["Total"] / len(player_locations) * 100) if \
- player_locations else 100
+ checks_done[team][player]["Total"] = len(locations_checked)
+
+ percent_total_checks_done[team][player] = (
+ checks_done[team][player]["Total"] / len(player_locations) * 100
+ if player_locations
+ else 100
+ )
for (team, player), game_state in multisave.get("client_game_state", {}).items():
if player in groups:
diff --git a/data/client.kv b/data/client.kv
index f0e36169002a..3b48d216ddb3 100644
--- a/data/client.kv
+++ b/data/client.kv
@@ -17,6 +17,12 @@
color: "FFFFFF"
:
tab_width: root.width / app.tab_count
+:
+ text_size: self.width, None
+ size_hint_y: None
+ height: self.texture_size[1]
+ font_size: dp(20)
+ markup: True
:
canvas.before:
Color:
@@ -24,11 +30,6 @@
Rectangle:
size: self.size
pos: self.pos
- text_size: self.width, None
- size_hint_y: None
- height: self.texture_size[1]
- font_size: dp(20)
- markup: True
:
messages: 1000 # amount of messages stored in client logs.
cols: 1
@@ -44,6 +45,70 @@
height: self.minimum_height
orientation: 'vertical'
spacing: dp(3)
+:
+ 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)
+ Rectangle:
+ size: self.size
+ pos: self.pos
+ height: self.minimum_height
+ receiving_text: "Receiving Player"
+ item_text: "Item"
+ finding_text: "Finding Player"
+ location_text: "Location"
+ entrance_text: "Entrance"
+ found_text: "Found?"
+ TooltipLabel:
+ id: receiving
+ text: root.receiving_text
+ halign: 'center'
+ valign: 'center'
+ pos_hint: {"center_y": 0.5}
+ TooltipLabel:
+ id: item
+ text: root.item_text
+ halign: 'center'
+ valign: 'center'
+ pos_hint: {"center_y": 0.5}
+ TooltipLabel:
+ id: finding
+ text: root.finding_text
+ halign: 'center'
+ valign: 'center'
+ pos_hint: {"center_y": 0.5}
+ TooltipLabel:
+ id: location
+ text: root.location_text
+ halign: 'center'
+ valign: 'center'
+ pos_hint: {"center_y": 0.5}
+ TooltipLabel:
+ id: entrance
+ text: root.entrance_text
+ halign: 'center'
+ valign: 'center'
+ pos_hint: {"center_y": 0.5}
+ TooltipLabel:
+ id: found
+ text: root.found_text
+ halign: 'center'
+ valign: 'center'
+ pos_hint: {"center_y": 0.5}
+:
+ cols: 1
+ viewclass: 'HintLabel'
+ scroll_y: self.height
+ scroll_type: ["content", "bars"]
+ bar_width: dp(12)
+ effect_cls: "ScrollEffect"
+ SelectableRecycleBoxLayout:
+ default_size: None, dp(20)
+ default_size_hint: 1, None
+ size_hint_y: None
+ height: self.minimum_height
+ orientation: 'vertical'
+ spacing: dp(3)
:
text: "Server:"
size_hint_x: None
diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS
index e92bfa42b628..0afc565280f1 100644
--- a/docs/CODEOWNERS
+++ b/docs/CODEOWNERS
@@ -61,6 +61,9 @@
# Kingdom Hearts 2
/worlds/kh2/ @JaredWeakStrike
+# Lingo
+/worlds/lingo/ @hatkirby
+
# Links Awakening DX
/worlds/ladx/ @zig-for
diff --git a/docs/adding games.md b/docs/adding games.md
index 24d9e499cd98..e9f7860fc650 100644
--- a/docs/adding games.md
+++ b/docs/adding games.md
@@ -1,214 +1,206 @@
+# How do I add a game to Archipelago?
-
-# How do I add a game to Archipelago?
This guide is going to try and be a broad summary of how you can do just that.
-There are two key steps to incorporating a game into Archipelago:
-- Game Modification
+There are two key steps to incorporating a game into Archipelago:
+
+- Game Modification
- Archipelago Server Integration
Refer to the following documents as well:
-- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md) for network communication between client and server.
-- [world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md) for documentation on server side code and creating a world package.
+- [network protocol.md](/docs/network%20protocol.md) for network communication between client and server.
+- [world api.md](/docs/world%20api.md) for documentation on server side code and creating a world package.
-# Game Modification
-One half of the work required to integrate a game into Archipelago is the development of the game client. This is
+# Game Modification
+
+One half of the work required to integrate a game into Archipelago is the development of the game client. This is
typically done through a modding API or other modification process, described further down.
As an example, modifications to a game typically include (more on this later):
+
- Hooking into when a 'location check' is completed.
- Networking with the Archipelago server.
- Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection.
In order to determine how to modify a game, refer to the following sections.
-
-## Engine Identification
-This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
+
+## Engine Identification
+
+This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is
+critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s
+important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
Examples are provided below.
-
+
### Creepy Castle
-![Creepy Castle Root Directory in Window's Explorer](./img/creepy-castle-directory.png)
-
+
+![Creepy Castle Root Directory in Windows Explorer](/docs/img/creepy-castle-directory.png)
+
This is the delightful title Creepy Castle, which is a fantastic game that I highly recommend. It’s also your worst-case
-scenario as a modder. All that’s present here is an executable file and some meta-information that Steam uses. You have
-basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty nasty
-disassembly and reverse engineering work, which is outside the scope of this tutorial. Let’s look at some other examples
-of game releases.
+scenario as a modder. All that’s present here is an executable file and some meta-information that Steam uses. You have
+basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty
+nasty disassembly and reverse engineering work, which is outside the scope of this tutorial. Let’s look at some other
+examples of game releases.
### Heavy Bullets
-![Heavy Bullets Root Directory in Window's Explorer](./img/heavy-bullets-directory.png)
-
-Here’s the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files.
-“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually
-with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing
-information, credits, and general info about the game. You usually won’t find anything too helpful here, but it never
-hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important.
-“steam_api.dll” is a file you can safely ignore, it’s just some code used to interface with Steam.
-The directory “HEAVY_BULLETS_Data”, however, has some good news.
-
-![Heavy Bullets Data Directory in Window's Explorer](./img/heavy-bullets-data-directory.png)
-
-Jackpot! It might not be obvious what you’re looking at here, but I can instantly tell from this folder’s contents that
-what we have is a game made in the Unity Engine. If you look in the sub-folders, you’ll seem some .dll files which affirm
-our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered, extension-less
-level files and the sharedassets files. We’ll tell you a bit about why seeing a Unity game is such good news later,
-but for now, this is what one looks like. Also keep your eyes out for an executable with a name like UnityCrashHandler,
-that’s another dead giveaway.
+
+![Heavy Bullets Root Directory in Window's Explorer](/docs/img/heavy-bullets-directory.png)
+
+Here’s the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files.
+“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually
+with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing
+information, credits, and general info about the game. You usually won’t find anything too helpful here, but it never
+hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important.
+“steam_api.dll” is a file you can safely ignore, it’s just some code used to interface with Steam.
+The directory “HEAVY_BULLETS_Data”, however, has some good news.
+
+![Heavy Bullets Data Directory in Window's Explorer](/docs/img/heavy-bullets-data-directory.png)
+
+Jackpot! It might not be obvious what you’re looking at here, but I can instantly tell from this folder’s contents that
+what we have is a game made in the Unity Engine. If you look in the sub-folders, you’ll seem some .dll files which
+affirm our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered,
+extension-less level files and the sharedassets files. If you've identified the game as a Unity game, some useful tools
+and information to help you on your journey can be found at this
+[Unity Game Hacking guide.](https://github.com/imadr/Unity-game-hacking)
### Stardew Valley
-![Stardew Valley Root Directory in Window's Explorer](./img/stardew-valley-directory.png)
-
-This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways.
-Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good news.
-More on that later.
+
+![Stardew Valley Root Directory in Window's Explorer](/docs/img/stardew-valley-directory.png)
+
+This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways.
+Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good
+news. Many games made in C# can be modified using the same tools found in our Unity game hacking toolset; namely BepInEx
+and MonoMod.
### Gato Roboto
-![Gato Roboto Root Directory in Window's Explorer](./img/gato-roboto-directory.png)
-
-Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for.
-The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker.
-
-This isn't all you'll ever see looking at game files, but it's a good place to start.
-As a general rule, the more files a game has out in plain sight, the more you'll be able to change.
-This especially applies in the case of code or script files - always keep a lookout for anything you can use to your
-advantage!
-
+
+![Gato Roboto Root Directory in Window's Explorer](/docs/img/gato-roboto-directory.png)
+
+Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for.
+The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker. For
+modifying GameMaker games the [Undertale Mod Tool](https://github.com/krzys-h/UndertaleModTool) is incredibly helpful.
+
+This isn't all you'll ever see looking at game files, but it's a good place to start.
+As a general rule, the more files a game has out in plain sight, the more you'll be able to change.
+This especially applies in the case of code or script files - always keep a lookout for anything you can use to your
+advantage!
+
## Open or Leaked Source Games
-As a side note, many games have either been made open source, or have had source files leaked at some point.
-This can be a boon to any would-be modder, for obvious reasons.
-Always be sure to check - a quick internet search for "(Game) Source Code" might not give results often, but when it
-does you're going to have a much better time.
-
+
+As a side note, many games have either been made open source, or have had source files leaked at some point.
+This can be a boon to any would-be modder, for obvious reasons. Always be sure to check - a quick internet search for
+"(Game) Source Code" might not give results often, but when it does, you're going to have a much better time.
+
Be sure never to distribute source code for games that you decompile or find if you do not have express permission to do
so, or to redistribute any materials obtained through similar methods, as this is illegal and unethical.
-
-## Modifying Release Versions of Games
-However, for now we'll assume you haven't been so lucky, and have to work with only what’s sitting in your install directory.
-Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools,
-but these are often not geared to the kind of work you'll be doing and may not help much.
-
-As a general rule, any modding tool that lets you write actual code is something worth using.
-
+
+## Modifying Release Versions of Games
+
+However, for now we'll assume you haven't been so lucky, and have to work with only what’s sitting in your install
+directory. Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools,
+but these are often not geared to the kind of work you'll be doing and may not help much.
+
+As a general rule, any modding tool that lets you write actual code is something worth using.
+
### Research
-The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification,
-it's possible other motivated parties have concocted useful tools for your game already.
-Always be sure to search the Internet for the efforts of other modders.
-
-### Analysis Tools
-Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to existing game tools.
-
-#### [dnSpy](https://github.com/dnSpy/dnSpy/releases)
-The first tool in your toolbox is dnSpy.
-dnSpy is useful for opening and modifying code files, like .exe and .dll files, that were made in C#.
-This won't work for executable files made by other means, and obfuscated code (code which was deliberately made
-difficult to reverse engineer) will thwart it, but 9 times out of 10 this is exactly what you need.
-You'll want to avoid opening common library files in dnSpy, as these are unlikely to contain the data you're looking to
-modify.
-
-For Unity games, the file you’ll want to open will be the file (Data Folder)/Managed/Assembly-CSharp.dll, as pictured below:
-
-![Heavy Bullets Managed Directory in Window's Explorer](./img/heavy-bullets-managed-directory.png)
-
-This file will contain the data of the actual game.
-For other C# games, the file you want is usually just the executable itself.
-
-With dnSpy, you can view the game’s C# code, but the tool isn’t perfect.
-Although the names of classes, methods, variables, and more will be preserved, code structures may not remain entirely intact. This is because compilers will often subtly rewrite code to be more optimal, so that it works the same as the original code but uses fewer resources. Compiled C# files also lose comments and other documentation.
-
-#### [UndertaleModTool](https://github.com/krzys-h/UndertaleModTool/releases)
-This is currently the best tool for modifying games made in GameMaker, and supports games made in both GMS 1 and 2.
-It allows you to modify code in GML, if the game wasn't made with the wrong compiler (usually something you don't have
-to worry about).
-
-You'll want to open the data.win file, as this is where all the goods are kept.
-Like dnSpy, you won’t be able to see comments.
-In addition, you will be able to see and modify many hidden fields on items that GameMaker itself will often hide from
-creators.
-
-Fonts in particular are notoriously complex, and to add new sprites you may need to modify existing sprite sheets.
-
-#### [CheatEngine](https://cheatengine.org/)
-CheatEngine is a tool with a very long and storied history.
-Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as
-malware (because this behavior is most commonly found in malware and rarely used by other programs).
-If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level,
-including binary data formats, addressing, and assembly language programming.
-
-The tool itself is highly complex and even I have not yet charted its expanses.
+
+The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification,
+it's possible other motivated parties have concocted useful tools for your game already.
+Always be sure to search the Internet for the efforts of other modders.
+
+### Other helpful tools
+
+Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to
+existing game tools.
+
+#### [CheatEngine](https://cheatengine.org/)
+
+CheatEngine is a tool with a very long and storied history.
+Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as
+malware (because this behavior is most commonly found in malware and rarely used by other programs).
+If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level,
+including binary data formats, addressing, and assembly language programming.
+
+The tool itself is highly complex and even I have not yet charted its expanses.
However, it can also be a very powerful tool in the right hands, allowing you to query and modify gamestate without ever
-modifying the actual game itself.
-In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do
-anything with it.
-
+modifying the actual game itself.
+In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do
+anything with it.
+
### What Modifications You Should Make to the Game
+
We talked about this briefly in [Game Modification](#game-modification) section.
-The next step is to know what you need to make the game do now that you can modify it. Here are your key goals:
-- Modify the game so that checks are shuffled
-- Know when the player has completed a check, and react accordingly
-- Listen for messages from the Archipelago server
-- Modify the game to display messages from the Archipelago server
-- Add interface for connecting to the Archipelago server with passwords and sessions
-- Add commands for manually rewarding, re-syncing, releasing, and other actions
-
-To elaborate, you need to be able to inform the server whenever you check locations, print out messages that you receive
-from the server in-game so players can read them, award items when the server tells you to, sync and re-sync when necessary,
-avoid double-awarding items while still maintaining game file integrity, and allow players to manually enter commands in
-case the client or server make mistakes.
-
-Refer to the [Network Protocol documentation](./network%20protocol.md) for how to communicate with Archipelago's servers.
-
-## But my Game is a console game. Can I still add it?
-That depends – what console?
-
-### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc
+The next step is to know what you need to make the game do now that you can modify it. Here are your key goals:
+
+- Know when the player has checked a location, and react accordingly
+- Be able to receive items from the server on the fly
+- Keep an index for items received in order to resync from disconnections
+- Add interface for connecting to the Archipelago server with passwords and sessions
+- Add commands for manually rewarding, re-syncing, releasing, and other actions
+
+Refer to the [Network Protocol documentation](/docs/network%20protocol.md) for how to communicate with Archipelago's
+servers.
+
+## But my Game is a console game. Can I still add it?
+
+That depends – what console?
+
+### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc
+
Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised
that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright
-holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console games.
-
-### My Game isn’t that old, it’s for the Wii/PS2/360/etc
-This is very complex, but doable.
-If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it.
+holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console
+games.
+
+### My Game isn’t that old, it’s for the Wii/PS2/360/etc
+
+This is very complex, but doable.
+If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it.
There exist many disassembly and debugging tools, but more recent content may have lackluster support.
-
-### My Game is a classic for the SNES/Sega Genesis/etc
-That’s a lot more feasible.
-There are many good tools available for understanding and modifying games on these older consoles, and the emulation
-community will have figured out the bulk of the console’s secrets.
-Look for debugging tools, but be ready to learn assembly.
-Old consoles usually have their own unique dialects of ASM you’ll need to get used to.
+
+### My Game is a classic for the SNES/Sega Genesis/etc
+
+That’s a lot more feasible.
+There are many good tools available for understanding and modifying games on these older consoles, and the emulation
+community will have figured out the bulk of the console’s secrets.
+Look for debugging tools, but be ready to learn assembly.
+Old consoles usually have their own unique dialects of ASM you’ll need to get used to.
Also make sure there’s a good way to interface with a running emulator, since that’s the only way you can connect these
older consoles to the Internet.
-There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a computer,
-but these will require the same sort of interface software to be written in order to work properly - from your perspective
-the two won't really look any different.
-
-### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that-
-Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no.
+There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a
+computer, but these will require the same sort of interface software to be written in order to work properly; from your
+perspective the two won't really look any different.
+
+### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that-
+
+Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no.
Obscurity is your enemy – there will likely be little to no emulator or modding information, and you’d essentially be
-working from scratch.
-
+working from scratch.
+
## How to Distribute Game Modifications
+
**NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!**
This is a good way to get any project you're working on sued out from under you.
The right way to distribute modified versions of a game's binaries, assuming that the licensing terms do not allow you
-to copy them wholesale, is as patches.
+to copy them wholesale, is as patches.
There are many patch formats, which I'll cover in brief. The common theme is that you can’t distribute anything that
wasn't made by you. Patches are files that describe how your modified file differs from the original one, thus avoiding
the issue of distributing someone else’s original work.
-Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play.
+Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play.
### Patches
#### IPS
+
IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode
moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's
fine.
#### UPS, BPS, VCDIFF (xdelta), bsdiff
+
Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is
possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes
compression, so this format is used by APBP.
@@ -217,6 +209,7 @@ Only a bsdiff module is integrated into AP. If the final patch requires or is ba
bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp".
#### APBP Archipelago Binary Patch
+
Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional
files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the
bsdiff between the original and the randomized ROM.
@@ -224,121 +217,53 @@ bsdiff between the original and the randomized ROM.
To make using APBP easy, they can be generated by inheriting from `worlds.Files.APDeltaPatch`.
### Mod files
+
Games which support modding will usually just let you drag and drop the mod’s files into a folder somewhere.
Mod files come in many forms, but the rules about not distributing other people's content remain the same.
They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be
-generated per seed.
+generated per seed. If at all possible, it's generally best practice to collect your world information from `slot_data`
+so that the users don't have to move files around in order to play.
If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy
integration into the Webhost by inheriting from `worlds.Files.APContainer`.
-
## Archipelago Integration
-Integrating a randomizer into Archipelago involves a few steps.
-There are several things that may need to be done, but the most important is to create an implementation of the
-`World` class specific to your game. This implementation should exist as a Python module within the `worlds` folder
-in the Archipelago file structure.
-
-This encompasses most of the data for your game – the items available, what checks you have, the logic for reaching those
-checks, what options to offer for the player’s yaml file, and the code to initialize all this data.
-
-Here’s an example of what your world module can look like:
-
-![Example world module directory open in Window's Explorer](./img/archipelago-world-directory-example.png)
-
-The minimum requirements for a new archipelago world are the package itself (the world folder containing a file named `__init__.py`),
-which must define a `World` class object for the game with a game name, create an equal number of items and locations with rules,
-a win condition, and at least one `Region` object.
-
-Let's give a quick breakdown of what the contents for these files look like.
-This is just one example of an Archipelago world - the way things are done below is not an immutable property of Archipelago.
-
-### Items.py
-This file is used to define the items which exist in a given game.
-
-![Example Items.py file open in Notepad++](./img/example-items-py-file.png)
-
-Some important things to note here. The center of our Items.py file is the item_table, which individually lists every
-item in the game and associates them with an ItemData.
-
-This file is rather skeletal - most of the actual data has been stripped out for simplicity.
-Each ItemData gives a numeric ID to associate with the item and a boolean telling us whether the item might allow the
-player to do more than they would have been able to before.
-
-Next there's the item_frequencies. This simply tells Archipelago how many times each item appears in the pool.
-Items that appear exactly once need not be listed - Archipelago will interpret absence from this dictionary as meaning
-that the item appears once.
-
-Lastly, note the `lookup_id_to_name` dictionary, which is typically imported and used in your Archipelago `World`
-implementation. This is how Archipelago is told about the items in your world.
-
-### Locations.py
-This file lists all locations in the game.
-
-![Example Locations.py file open in Notepad++](./img/example-locations-py-file.png)
-
-First is the achievement_table. It lists each location, the region that it can be found in (more on regions later),
-and a numeric ID to associate with each location.
-
-The exclusion table is a series of dictionaries which are used to exclude certain checks from the pool of progression
-locations based on user settings, and the events table associates certain specific checks with specific items.
-
-`lookup_id_to_name` is also present for locations, though this is a separate dictionary, to be clear.
-
-### Options.py
-This file details options to be searched for in a player's YAML settings file.
-
-![Example Options.py file open in Notepad++](./img/example-options-py-file.png)
-
-There are several types of option Archipelago has support for.
-In our case, we have three separate choices a player can toggle, either On or Off.
-You can also have players choose between a number of predefined values, or have them provide a numeric value within a
-specified range.
-
-### Regions.py
-This file contains data which defines the world's topology.
-In other words, it details how different regions of the game connect to each other.
-
-![Example Regions.py file open in Notepad++](./img/example-regions-py-file.png)
-
-`terraria_regions` contains a list of tuples.
-The first element of the tuple is the name of the region, and the second is a list of connections that lead out of the region.
-
-`mandatory_connections` describe where the connection leads.
-
-Above this data is a function called `link_terraria_structures` which uses our defined regions and connections to create
-something more usable for Archipelago, but this has been left out for clarity.
-
-### Rules.py
-This is the file that details rules for what players can and cannot logically be required to do, based on items and settings.
-
-![Example Rules.py file open in Notepad++](./img/example-rules-py-file.png)
-
-This is the most complicated part of the job, and is one part of Archipelago that is likely to see some changes in the future.
-The first class, called `TerrariaLogic`, is an extension of the `LogicMixin` class.
-This is where you would want to define methods for evaluating certain conditions, which would then return a boolean to
-indicate whether conditions have been met. Your rule definitions should start with some sort of identifier to delineate it
-from other games, as all rules are mixed together due to `LogicMixin`. In our case, `_terraria_rule` would be a better name.
-
-The method below, `set_rules()`, is where you would assign these functions as "rules", using lambdas to associate these
-functions or combinations of them (or any other code that evaluates to a boolean, in my case just the placeholder `True`)
-to certain tasks, like checking locations or using entrances.
-
-### \_\_init\_\_.py
-This is the file that actually extends the `World` class, and is where you expose functionality and data to Archipelago.
-
-![Example \_\_init\_\_.py file open in Notepad++](./img/example-init-py-file.png)
-
-This is the most important file for the implementation, and technically the only one you need, but it's best to keep this
-file as short as possible and use other script files to do most of the heavy lifting.
-If you've done things well, this will just be where you assign everything you set up in the other files to their associated
-fields in the class being extended.
-
-This is also a good place to put game-specific quirky behavior that needs to be managed, as it tends to make things a bit
-cluttered if you put these things elsewhere.
-
-The various methods and attributes are documented in `/worlds/AutoWorld.py[World]` and
-[world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md),
-though it is also recommended to look at existing implementations to see how all this works first-hand.
-Once you get all that, all that remains to do is test the game and publish your work.
-Make sure to check out [world maintainer.md](./world%20maintainer.md) before publishing.
+
+In order for your game to communicate with the Archipelago server and generate the necessary randomized information,
+you must create a world package in the main Archipelago repo. This section will cover the requisites and expectations
+and show the basics of a world. More in depth documentation on the available API can be read in
+the [world api doc.](/docs/world%20api.md)
+For setting up your working environment with Archipelago refer
+to [running from source](/docs/running%20from%20source.md) and the [style guide](/docs/style.md).
+
+### Requirements
+
+A world implementation requires a few key things from its implementation
+
+- A folder within `worlds` that contains an `__init__.py`
+ - This is what defines it as a Python package and how it's able to be imported
+ into Archipelago's generation system. During generation time only code that is
+ defined within this file will be run. It's suggested to split up your information
+ into more files to improve readability, but all of that information can be
+ imported at its base level within your world.
+- A `World` subclass where you create your world and define all of its rules
+ and the following requirements:
+ - Your items and locations need a `item_name_to_id` and `location_name_to_id`,
+ respectively, mapping.
+ - An `option_definitions` mapping of your game options with the format
+ `{name: Class}`, where `name` uses Python snake_case.
+ - You must define your world's `create_item` method, because this may be called
+ by the generator in certain circumstances
+ - When creating your world you submit items and regions to the Multiworld.
+ - These are lists of said objects which you can access at
+ `self.multiworld.itempool` and `self.multiworld.regions`. Best practice for
+ adding to these lists is with either `append` or `extend`, where `append` is a
+ single object and `extend` is a list.
+ - Do not use `=` as this will delete other worlds' items and regions.
+ - Regions are containers for holding your world's Locations.
+ - Locations are where players will "check" for items and must exist within
+ a region. It's also important for your world's submitted items to be the same as
+ its submitted locations count.
+ - You must always have a "Menu" Region from which the generation algorithm
+ uses to enter the game and access locations.
+- Make sure to check out [world maintainer.md](/docs/world%20maintainer.md) before publishing.
\ No newline at end of file
diff --git a/docs/img/archipelago-world-directory-example.png b/docs/img/archipelago-world-directory-example.png
deleted file mode 100644
index ba720f3319b9..000000000000
Binary files a/docs/img/archipelago-world-directory-example.png and /dev/null differ
diff --git a/docs/img/example-init-py-file.png b/docs/img/example-init-py-file.png
deleted file mode 100644
index 6dd5c3c9380b..000000000000
Binary files a/docs/img/example-init-py-file.png and /dev/null differ
diff --git a/docs/img/example-items-py-file.png b/docs/img/example-items-py-file.png
deleted file mode 100644
index e114a78d2110..000000000000
Binary files a/docs/img/example-items-py-file.png and /dev/null differ
diff --git a/docs/img/example-locations-py-file.png b/docs/img/example-locations-py-file.png
deleted file mode 100644
index 53c4bc1e2905..000000000000
Binary files a/docs/img/example-locations-py-file.png and /dev/null differ
diff --git a/docs/img/example-options-py-file.png b/docs/img/example-options-py-file.png
deleted file mode 100644
index 5811f5400060..000000000000
Binary files a/docs/img/example-options-py-file.png and /dev/null differ
diff --git a/docs/img/example-regions-py-file.png b/docs/img/example-regions-py-file.png
deleted file mode 100644
index a9d05c53fcbf..000000000000
Binary files a/docs/img/example-regions-py-file.png and /dev/null differ
diff --git a/docs/img/example-rules-py-file.png b/docs/img/example-rules-py-file.png
deleted file mode 100644
index b76e78b40622..000000000000
Binary files a/docs/img/example-rules-py-file.png and /dev/null differ
diff --git a/docs/img/heavy-bullets-managed-directory.png b/docs/img/heavy-bullets-managed-directory.png
deleted file mode 100644
index 73017f6dc96d..000000000000
Binary files a/docs/img/heavy-bullets-managed-directory.png and /dev/null differ
diff --git a/docs/triage role expectations.md b/docs/triage role expectations.md
new file mode 100644
index 000000000000..5b4cab227532
--- /dev/null
+++ b/docs/triage role expectations.md
@@ -0,0 +1,100 @@
+# Triage Role Expectations
+
+Users with Triage-level access are selected contributors who can and wish to proactively label/triage issues and pull
+requests without being granted write access to the Archipelago repository.
+
+Triage users are not necessarily official members of the Archipelago organization, for the list of core maintainers,
+please reference [ArchipelagoMW Members](https://github.com/orgs/ArchipelagoMW/people) page.
+
+## Access Permissions
+
+Triage users have the following permissions:
+
+* Apply/dismiss labels on all issues and pull requests.
+* Close, reopen, and assign all issues and pull requests.
+* Mark issues and pull requests as duplicate.
+* Request pull request reviews from repository members.
+* Hide comments in issues or pull requests from public view.
+ * Hidden comments are not deleted and can be reversed by another triage user or repository member with write access.
+* And all other standard permissions granted to regular GitHub users.
+
+For more details on permissions granted by the Triage role, see
+[GitHub's Role Documentation](https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/repository-roles-for-an-organization).
+
+## Expectations
+
+Users with triage-level permissions have no expectation to review code, but, if desired, to review pull requests/issues
+and apply the relevant labels and ping/request reviews from any relevant [code owners](./CODEOWNERS) for review. Triage
+users are also expected not to close others' issues or pull requests without strong reason to do so (with exception of
+`meta: invalid` or `meta: duplicate` scenarios, which are listed below). When in doubt, defer to a core maintainer.
+
+Triage users are not "moderators" for others' issues or pull requests. However, they may voice their opinions/feedback
+on issues or pull requests, just the same as any other GitHub user contributing to Archipelago.
+
+## Labeling
+
+As of the time of writing this document, there are 15 distinct labels that can be applied to issues and pull requests.
+
+### Affects
+
+These labels notate if certain issues or pull requests affect critical aspects of Archipelago that may require specific
+review. More than one of these labels can be used on a issue or pull request, if relevant.
+
+* `affects: core` is to be applied to issues/PRs that may affect core Archipelago functionality and should be reviewed
+with additional scrutiny.
+ * Core is defined as any files not contained in the `WebHostLib` directory or individual world implementations
+ directories inside the `worlds` directory, not including `worlds/generic`.
+* `affects: webhost` is to be applied to issues/PRs that may affect the core WebHost portion of Archipelago. In
+general, this is anything being modified inside the `WebHostLib` directory or `WebHost.py` file.
+* `affects: release/blocker` is to be applied for any issues/PRs that may either negatively impact (issues) or propose
+to resolve critical issues (pull requests) that affect the current or next official release of Archipelago and should be
+given top priority for review.
+
+### Is
+
+These labels notate what kinds of changes are being made or proposed in issues or pull requests. More than one of these
+labels can be used on a issue or pull request, if relevant, but at least one of these labels should be applied to every
+pull request and issue.
+
+* `is: bug/fix` is to be applied to issues/PRs that report or resolve an issue in core, web, or individual world
+implementations.
+* `is: documentation` is to be applied to issues/PRs that relate to adding, updating, or removing documentation in
+core, web, or individual world implementations without modifying actual code.
+* `is: enhancement` is to be applied to issues/PRs that relate to adding, modifying, or removing functionality in
+core, web, or individual world implementations.
+* `is: refactor/cleanup` is to be applied to issues/PRs that relate to reorganizing existing code to improve
+readability or performance without adding, modifying, or removing functionality or fixing known regressions.
+* `is: maintenance` is to be applied to issues/PRs that don't modify logic, refactor existing code, change features.
+This is typically reserved for pull requests that need to update dependencies or increment version numbers without
+resolving existing issues.
+* `is: new game` is to be applied to any pull requests that introduce a new game for the first time to the `worlds`
+directory.
+ * Issues should not be opened and classified with `is: new game`, and instead should be directed to the
+ #future-game-design channel in Archipelago for opening suggestions. If they are opened, they should be labeled
+ with `meta: invalid` and closed.
+ * Pull requests for new games should only have this label, as enhancement, documentation, bug/fix, refactor, and
+ possibly maintenance is implied.
+
+### Meta
+
+These labels allow additional quick meta information for contributors or reviewers for issues and pull requests. They
+have specific situations where they should be applied.
+
+* `meta: duplicate` is to be applied to any issues/PRs that are duplicate of another issue/PR that was already opened.
+ * These should be immediately closed after leaving a comment, directing to the original issue or pull request.
+* `meta: invalid` is to be applied to any issues/PRs that do not relate to Archipelago or are inappropriate for
+discussion on GitHub.
+ * These should be immediately closed afterwards.
+* `meta: help wanted` is to be applied to any issues/PRs that require additional attention for whatever reason.
+ * These should include a comment describing what kind of help is requested when the label is added.
+ * Some common reasons include, but are not limited to: Breaking API changes that require developer input/testing or
+ pull requests with large line changes that need additional reviewers to be reviewed effectively.
+ * This label may require some programming experience and familiarity with Archipelago source to determine if
+ requesting additional attention for help is warranted.
+* `meta: good first issue` is to be applied to any issues that may be a good starting ground for new contributors to try
+and tackle.
+ * This label may require some programming experience and familiarity with Archipelago source to determine if an
+ issue is a "good first issue".
+* `meta: wontfix` is to be applied for any issues/PRs that are opened that will not be actioned because it's out of
+scope or determined to not be an issue.
+ * This should be reserved for use by a world's code owner(s) on their relevant world or by core maintainers.
diff --git a/docs/world api.md b/docs/world api.md
index 6fb5b3ac9c6d..b128e2b146b4 100644
--- a/docs/world api.md
+++ b/docs/world api.md
@@ -759,8 +759,9 @@ multiworld for each test written using it. Within subsequent modules, classes sh
TestBase, and can then define options to test in the class body, and run tests in each test method.
Example `__init__.py`
+
```python
-from test.TestBase import WorldTestBase
+from test.test_base import WorldTestBase
class MyGameTestBase(WorldTestBase):
diff --git a/inno_setup.iss b/inno_setup.iss
index 3c1bdc4571e0..b6f40f770110 100644
--- a/inno_setup.iss
+++ b/inno_setup.iss
@@ -46,151 +46,33 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
[Types]
Name: "full"; Description: "Full installation"
-Name: "hosting"; Description: "Installation for hosting purposes"
-Name: "playing"; Description: "Installation for playing purposes"
+Name: "minimal"; Description: "Minimal installation"
Name: "custom"; Description: "Custom installation"; Flags: iscustom
[Components]
-Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
-Name: "generator"; Description: "Generator"; Types: full hosting
-Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
-Name: "generator/dkc3"; Description: "Donkey Kong Country 3 ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
-Name: "generator/smw"; Description: "Super Mario World ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
-Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
-Name: "generator/l2ac"; Description: "Lufia II Ancient Cave ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 2621440; Flags: disablenouninstallwarning
-Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
-Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
-Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning
-Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting
-Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting
-Name: "generator/mmbn3"; Description: "MegaMan Battle Network 3"; Types: full hosting; ExtraDiskSpaceRequired: 8388608; Flags: disablenouninstallwarning
-Name: "generator/ladx"; Description: "Link's Awakening DX ROM Setup"; Types: full hosting
-Name: "generator/tloz"; Description: "The Legend of Zelda ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 135168; Flags: disablenouninstallwarning
-Name: "server"; Description: "Server"; Types: full hosting
-Name: "client"; Description: "Clients"; Types: full playing
-Name: "client/sni"; Description: "SNI Client"; Types: full playing
-Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
-Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
-Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
-Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
-Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
-Name: "client/bizhawk"; Description: "BizHawk Client"; Types: full playing
-Name: "client/factorio"; Description: "Factorio"; Types: full playing
-Name: "client/kh2"; Description: "Kingdom Hearts 2"; Types: full playing
-Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
-Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
-Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
-Name: "client/pkmn"; Description: "Pokemon Client"
-Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
-Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
-Name: "client/mmbn3"; Description: "MegaMan Battle Network 3 Client"; Types: full playing;
-Name: "client/ladx"; Description: "Link's Awakening Client"; Types: full playing; ExtraDiskSpaceRequired: 1048576
-Name: "client/cf"; Description: "ChecksFinder"; Types: full playing
-Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing
-Name: "client/wargroove"; Description: "Wargroove"; Types: full playing
-Name: "client/zl"; Description: "Zillion"; Types: full playing
-Name: "client/tloz"; Description: "The Legend of Zelda"; Types: full playing
-Name: "client/advn"; Description: "Adventure"; Types: full playing
-Name: "client/ut"; Description: "Undertale"; Types: full playing
-Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
+Name: "core"; Description: "Archipelago"; Types: full minimal custom; Flags: fixed
+Name: "lttp_sprites"; Description: "Download ""A Link to the Past"" player sprites"; Types: full;
[Dirs]
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
[Files]
-Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp
-Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm
-Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"; Flags: external; Components: client/sni/dkc3 or generator/dkc3
-Source: "{code:GetSMWROMPath}"; DestDir: "{app}"; DestName: "Super Mario World (USA).sfc"; Flags: external; Components: client/sni/smw or generator/smw
-Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe
-Source: "{code:GetL2ACROMPath}"; DestDir: "{app}"; DestName: "Lufia II - Rise of the Sinistrals (USA).sfc"; Flags: external; Components: generator/l2ac
-Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot
-Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl
-Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r
-Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b
-Source: "{code:GetBN3ROMPath}"; DestDir: "{app}"; DestName: "Mega Man Battle Network 3 - Blue Version (USA).gba"; Flags: external; Components: client/mmbn3
-Source: "{code:GetLADXROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"; Flags: external; Components: client/ladx or generator/ladx
-Source: "{code:GetTLoZROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The (U) (PRG0) [!].nes"; Flags: external; Components: client/tloz or generator/tloz
-Source: "{code:GetAdvnROMPath}"; DestDir: "{app}"; DestName: "ADVNTURE.BIN"; Flags: external; Components: client/advn
-Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
-Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
-Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
-
-Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion;
-Source: "{#source_path}\ArchipelagoLauncher(DEBUG).exe"; DestDir: "{app}"; Flags: ignoreversion;
-Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
-Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
-Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
-Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
-Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
-Source: "{#source_path}\ArchipelagoBizHawkClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/bizhawk
-Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ladx
-Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
-Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
-Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
-Source: "{#source_path}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
-Source: "{#source_path}\ArchipelagoZillionClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/zl
-Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1
-Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn
-Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf
-Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2
-Source: "{#source_path}\ArchipelagoMMBN3Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/mmbn3
-Source: "{#source_path}\ArchipelagoZelda1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/tloz
-Source: "{#source_path}\ArchipelagoWargrooveClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/wargroove
-Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2
-Source: "{#source_path}\ArchipelagoAdventureClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/advn
-Source: "{#source_path}\ArchipelagoUndertaleClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ut
+Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
+Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs;
+Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs;
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
[Icons]
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
Name: "{group}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"
-Name: "{group}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Components: server
-Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text
-Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni
-Name: "{group}\{#MyAppName} BizHawk Client"; Filename: "{app}\ArchipelagoBizHawkClient.exe"; Components: client/bizhawk
-Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
-Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
-Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot
-Name: "{group}\{#MyAppName} Zillion Client"; Filename: "{app}\ArchipelagoZillionClient.exe"; Components: client/zl
-Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1
-Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn
-Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf
-Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2
-Name: "{group}\{#MyAppName} MegaMan Battle Network 3 Client"; Filename: "{app}\ArchipelagoMMBN3Client.exe"; Components: client/mmbn3
-Name: "{group}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Components: client/tloz
-Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2
-Name: "{group}\{#MyAppName} Link's Awakening Client"; Filename: "{app}\ArchipelagoLinksAwakeningClient.exe"; Components: client/ladx
-Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn
-Name: "{group}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Components: client/wargroove
-Name: "{group}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Components: client/ut
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"; Tasks: desktopicon
-Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Tasks: desktopicon; Components: server
-Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni
-Name: "{commondesktop}\{#MyAppName} BizHawk Client"; Filename: "{app}\ArchipelagoBizHawkClient.exe"; Tasks: desktopicon; Components: client/bizhawk
-Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio
-Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft
-Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot
-Name: "{commondesktop}\{#MyAppName} Zillion Client"; Filename: "{app}\ArchipelagoZillionClient.exe"; Tasks: desktopicon; Components: client/zl
-Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/ff1
-Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn
-Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf
-Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2
-Name: "{commondesktop}\{#MyAppName} MegaMan Battle Network 3 Client"; Filename: "{app}\ArchipelagoMMBN3Client.exe"; Tasks: desktopicon; Components: client/mmbn3
-Name: "{commondesktop}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Tasks: desktopicon; Components: client/tloz
-Name: "{commondesktop}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Tasks: desktopicon; Components: client/wargroove
-Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2
-Name: "{commondesktop}\{#MyAppName} Link's Awakening Client"; Filename: "{app}\ArchipelagoLinksAwakeningClient.exe"; Tasks: desktopicon; Components: client/ladx
-Name: "{commondesktop}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Tasks: desktopicon; Components: client/advn
-Name: "{commondesktop}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Tasks: desktopicon; Components: client/ut
[Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
-Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/sni/lttp or generator/lttp
-Filename: "{app}\ArchipelagoMinecraftClient.exe"; Parameters: "--install"; StatusMsg: "Installing Forge Server..."; Components: client/minecraft
+Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Flags: nowait; Components: lttp_sprites
Filename: "{app}\ArchipelagoLauncher"; Parameters: "--update_settings"; StatusMsg: "Updating host.yaml..."; Flags: runasoriginaluser runhidden
Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringChange('Launcher', '&', '&&')}}"; Flags: nowait postinstall skipifsilent
@@ -206,101 +88,97 @@ Type: filesandordirs; Name: "{app}\EnemizerCLI*"
[Registry]
-Root: HKCR; Subkey: ".aplttp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
-
-Root: HKCR; Subkey: ".apsm"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
-
-Root: HKCR; Subkey: ".apdkc3"; ValueData: "{#MyAppName}dkc3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Archipelago Donkey Kong Country 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}dkc3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}dkc3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
-
-Root: HKCR; Subkey: ".apsmw"; ValueData: "{#MyAppName}smwpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smwpatch"; ValueData: "Archipelago Super Mario World Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smwpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smwpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
-
-Root: HKCR; Subkey: ".apzl"; ValueData: "{#MyAppName}zlpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/zl
-Root: HKCR; Subkey: "{#MyAppName}zlpatch"; ValueData: "Archipelago Zillion Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/zl
-Root: HKCR; Subkey: "{#MyAppName}zlpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZillionClient.exe,0"; ValueType: string; ValueName: ""; Components: client/zl
-Root: HKCR; Subkey: "{#MyAppName}zlpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZillionClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/zl
-
-Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smz3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
-
-Root: HKCR; Subkey: ".apsoe"; ValueData: "{#MyAppName}soepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Archipelago Secret of Evermore Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
-
-Root: HKCR; Subkey: ".apl2ac"; ValueData: "{#MyAppName}l2acpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}l2acpatch"; ValueData: "Archipelago Lufia II Ancient Cave Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}l2acpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}l2acpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
-
-Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft
-Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft
-Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: ""; Components: client/minecraft
-Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/minecraft
-
-Root: HKCR; Subkey: ".apz5"; ValueData: "{#MyAppName}n64zpf"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/oot
-Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archipelago Ocarina of Time Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/oot
-Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: ""; Components: client/oot
-Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{app}\ArchipelagoOoTClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/oot
-
-Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn
-Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn
-Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn
-Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn
-
-Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn
-Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn
-Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn
-Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn
-
-Root: HKCR; Subkey: ".apbn3"; ValueData: "{#MyAppName}bn3bpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/mmbn3
-Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Archipelago MegaMan Battle Network 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/mmbn3
-Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoMMBN3Client.exe,0"; ValueType: string; ValueName: ""; Components: client/mmbn3
-Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\shell\open\command"; ValueData: """{app}\ArchipelagoMMBN3Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/mmbn3
-
-Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/ladx
-Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/ladx
-Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; Components: client/ladx
-Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/ladx
-
-Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/tloz
-Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/tloz
-Root: HKCR; Subkey: "{#MyAppName}tlozpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZelda1Client.exe,0"; ValueType: string; ValueName: ""; Components: client/tloz
-Root: HKCR; Subkey: "{#MyAppName}tlozpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZelda1Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/tloz
-
-Root: HKCR; Subkey: ".apadvn"; ValueData: "{#MyAppName}advnpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/advn
-Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Archipelago Adventure Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/advn
-Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: ""; Components: client/advn
-Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/advn
-
-Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
-Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
-Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server
-Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: server
-
-Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey; Components: client/text
-Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: ""; Components: client/text
-Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0"; Components: client/text
-Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1"""; Components: client/text
+Root: HKCR; Subkey: ".aplttp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apsm"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apdkc3"; ValueData: "{#MyAppName}dkc3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Archipelago Donkey Kong Country 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}dkc3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}dkc3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apsmw"; ValueData: "{#MyAppName}smwpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smwpatch"; ValueData: "Archipelago Super Mario World Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smwpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smwpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apzl"; ValueData: "{#MyAppName}zlpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}zlpatch"; ValueData: "Archipelago Zillion Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}zlpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZillionClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}zlpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZillionClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smz3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apsoe"; ValueData: "{#MyAppName}soepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Archipelago Secret of Evermore Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apl2ac"; ValueData: "{#MyAppName}l2acpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}l2acpatch"; ValueData: "Archipelago Lufia II Ancient Cave Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}l2acpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}l2acpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apz5"; ValueData: "{#MyAppName}n64zpf"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archipelago Ocarina of Time Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{app}\ArchipelagoOoTClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apbn3"; ValueData: "{#MyAppName}bn3bpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Archipelago MegaMan Battle Network 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoMMBN3Client.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\shell\open\command"; ValueData: """{app}\ArchipelagoMMBN3Client.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}tlozpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZelda1Client.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}tlozpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZelda1Client.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apadvn"; ValueData: "{#MyAppName}advnpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Archipelago Adventure Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
+Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
+Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0";
+Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1""";
[Code]
-const
- SHCONTCH_NOPROGRESSBOX = 4;
- SHCONTCH_RESPONDYESTOALL = 16;
-
// See: https://stackoverflow.com/a/51614652/2287576
function IsVCRedist64BitNeeded(): boolean;
var
@@ -320,594 +198,3 @@ begin
Result := True;
end;
end;
-
-var R : longint;
-
-var lttprom: string;
-var LttPROMFilePage: TInputFileWizardPage;
-
-var smrom: string;
-var SMRomFilePage: TInputFileWizardPage;
-
-var dkc3rom: string;
-var DKC3RomFilePage: TInputFileWizardPage;
-
-var smwrom: string;
-var SMWRomFilePage: TInputFileWizardPage;
-
-var soerom: string;
-var SoERomFilePage: TInputFileWizardPage;
-
-var l2acrom: string;
-var L2ACROMFilePage: TInputFileWizardPage;
-
-var ootrom: string;
-var OoTROMFilePage: TInputFileWizardPage;
-
-var zlrom: string;
-var ZlROMFilePage: TInputFileWizardPage;
-
-var redrom: string;
-var RedROMFilePage: TInputFileWizardPage;
-
-var bluerom: string;
-var BlueROMFilePage: TInputFileWizardPage;
-
-var bn3rom: string;
-var BN3ROMFilePage: TInputFileWizardPage;
-
-var ladxrom: string;
-var LADXROMFilePage: TInputFileWizardPage;
-
-var tlozrom: string;
-var TLoZROMFilePage: TInputFileWizardPage;
-
-var advnrom: string;
-var AdvnROMFilePage: TInputFileWizardPage;
-
-function GetSNESMD5OfFile(const rom: string): string;
-var data: AnsiString;
-begin
- if LoadStringFromFile(rom, data) then
- begin
- if Length(data) mod 1024 = 512 then
- begin
- data := copy(data, 513, Length(data)-512);
- end;
- Result := GetMD5OfString(data);
- end;
-end;
-
-function GetSMSMD5OfFile(const rom: string): string;
-var data: AnsiString;
-begin
- if LoadStringFromFile(rom, data) then
- begin
- Result := GetMD5OfString(data);
- end;
-end;
-
-function CheckRom(name: string; hash: string): string;
-var rom: string;
-begin
- log('Handling ' + name)
- rom := FileSearch(name, WizardDirValue());
- if Length(rom) > 0 then
- begin
- log('existing ROM found');
- log(IntToStr(CompareStr(GetSNESMD5OfFile(rom), hash)));
- if CompareStr(GetSNESMD5OfFile(rom), hash) = 0 then
- begin
- log('existing ROM verified');
- Result := rom;
- exit;
- end;
- log('existing ROM failed verification');
- end;
-end;
-
-function CheckSMSRom(name: string; hash: string): string;
-var rom: string;
-begin
- log('Handling ' + name)
- rom := FileSearch(name, WizardDirValue());
- if Length(rom) > 0 then
- begin
- log('existing ROM found');
- log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash)));
- if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then
- begin
- log('existing ROM verified');
- Result := rom;
- exit;
- end;
- log('existing ROM failed verification');
- end;
-end;
-
-function CheckNESRom(name: string; hash: string): string;
-var rom: string;
-begin
- log('Handling ' + name)
- rom := FileSearch(name, WizardDirValue());
- if Length(rom) > 0 then
- begin
- log('existing ROM found');
- log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash)));
- if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then
- begin
- log('existing ROM verified');
- Result := rom;
- exit;
- end;
- log('existing ROM failed verification');
- end;
-end;
-
-function AddRomPage(name: string): TInputFileWizardPage;
-begin
- Result :=
- CreateInputFilePage(
- wpSelectComponents,
- 'Select ROM File',
- 'Where is your ' + name + ' located?',
- 'Select the file, then click Next.');
-
- Result.Add(
- 'Location of ROM file:',
- 'SNES ROM files|*.sfc;*.smc|All files|*.*',
- '.sfc');
-end;
-
-
-function AddGBRomPage(name: string): TInputFileWizardPage;
-begin
- Result :=
- CreateInputFilePage(
- wpSelectComponents,
- 'Select ROM File',
- 'Where is your ' + name + ' located?',
- 'Select the file, then click Next.');
-
- Result.Add(
- 'Location of ROM file:',
- 'GB ROM files|*.gb;*.gbc|All files|*.*',
- '.gb');
-end;
-
-function AddGBARomPage(name: string): TInputFileWizardPage;
-begin
- Result :=
- CreateInputFilePage(
- wpSelectComponents,
- 'Select ROM File',
- 'Where is your ' + name + ' located?',
- 'Select the file, then click Next.');
- Result.Add(
- 'Location of ROM file:',
- 'GBA ROM files|*.gba|All files|*.*',
- '.gba');
-end;
-
-function AddSMSRomPage(name: string): TInputFileWizardPage;
-begin
- Result :=
- CreateInputFilePage(
- wpSelectComponents,
- 'Select ROM File',
- 'Where is your ' + name + ' located?',
- 'Select the file, then click Next.');
- Result.Add(
- 'Location of ROM file:',
- 'SMS ROM files|*.sms|All files|*.*',
- '.sms');
-end;
-
-function AddNESRomPage(name: string): TInputFileWizardPage;
-begin
- Result :=
- CreateInputFilePage(
- wpSelectComponents,
- 'Select ROM File',
- 'Where is your ' + name + ' located?',
- 'Select the file, then click Next.');
-
- Result.Add(
- 'Location of ROM file:',
- 'NES ROM files|*.nes|All files|*.*',
- '.nes');
-end;
-
-procedure AddOoTRomPage();
-begin
- ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue());
- if Length(ootrom) > 0 then
- begin
- log('existing ROM found');
- log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '5bd1fe107bf8106b2ab6650abecd54d6'))); // normal
- log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '6697768a7a7df2dd27a692a2638ea90b'))); // byteswapped
- log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '05f0f3ebacbc8df9243b6148ffe4792f'))); // decompressed
- if (CompareStr(GetMD5OfFile(ootrom), '5bd1fe107bf8106b2ab6650abecd54d6') = 0) or (CompareStr(GetMD5OfFile(ootrom), '6697768a7a7df2dd27a692a2638ea90b') = 0) or (CompareStr(GetMD5OfFile(ootrom), '05f0f3ebacbc8df9243b6148ffe4792f') = 0) then
- begin
- log('existing ROM verified');
- exit;
- end;
- log('existing ROM failed verification');
- end;
- ootrom := ''
- OoTROMFilePage :=
- CreateInputFilePage(
- wpSelectComponents,
- 'Select ROM File',
- 'Where is your OoT 1.0 ROM located?',
- 'Select the file, then click Next.');
-
- OoTROMFilePage.Add(
- 'Location of ROM file:',
- 'N64 ROM files (*.z64, *.n64)|*.z64;*.n64|All files|*.*',
- '.z64');
-end;
-
-function AddA26Page(name: string): TInputFileWizardPage;
-begin
- Result :=
- CreateInputFilePage(
- wpSelectComponents,
- 'Select ROM File',
- 'Where is your ' + name + ' located?',
- 'Select the file, then click Next.');
-
- Result.Add(
- 'Location of ROM file:',
- 'A2600 ROM files|*.BIN;*.a26|All files|*.*',
- '.BIN');
-end;
-
-function NextButtonClick(CurPageID: Integer): Boolean;
-begin
- if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
- Result := not (LttPROMFilePage.Values[0] = '')
- else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then
- Result := not (SMROMFilePage.Values[0] = '')
- else if (assigned(DKC3ROMFilePage)) and (CurPageID = DKC3ROMFilePage.ID) then
- Result := not (DKC3ROMFilePage.Values[0] = '')
- else if (assigned(SMWROMFilePage)) and (CurPageID = SMWROMFilePage.ID) then
- Result := not (SMWROMFilePage.Values[0] = '')
- else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then
- Result := not (SoEROMFilePage.Values[0] = '')
- else if (assigned(L2ACROMFilePage)) and (CurPageID = L2ACROMFilePage.ID) then
- Result := not (L2ACROMFilePage.Values[0] = '')
- else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
- Result := not (OoTROMFilePage.Values[0] = '')
- else if (assigned(BN3ROMFilePage)) and (CurPageID = BN3ROMFilePage.ID) then
- Result := not (BN3ROMFilePage.Values[0] = '')
- else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then
- Result := not (ZlROMFilePage.Values[0] = '')
- else if (assigned(RedROMFilePage)) and (CurPageID = RedROMFilePage.ID) then
- Result := not (RedROMFilePage.Values[0] = '')
- else if (assigned(BlueROMFilePage)) and (CurPageID = BlueROMFilePage.ID) then
- Result := not (BlueROMFilePage.Values[0] = '')
- else if (assigned(LADXROMFilePage)) and (CurPageID = LADXROMFilePage.ID) then
- Result := not (LADXROMFilePage.Values[0] = '')
- else if (assigned(TLoZROMFilePage)) and (CurPageID = TLoZROMFilePage.ID) then
- Result := not (TLoZROMFilePage.Values[0] = '')
- else if (assigned(AdvnROMFilePage)) and (CurPageID = AdvnROMFilePage.ID) then
- Result := not (AdvnROMFilePage.Values[0] = '')
- else
- Result := True;
-end;
-
-function GetROMPath(Param: string): string;
-begin
- if Length(lttprom) > 0 then
- Result := lttprom
- else if Assigned(LttPRomFilePage) then
- begin
- R := CompareStr(GetSNESMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
- if R <> 0 then
- MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := LttPROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetSMROMPath(Param: string): string;
-begin
- if Length(smrom) > 0 then
- Result := smrom
- else if Assigned(SMRomFilePage) then
- begin
- R := CompareStr(GetSNESMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
- if R <> 0 then
- MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := SMROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetDKC3ROMPath(Param: string): string;
-begin
- if Length(dkc3rom) > 0 then
- Result := dkc3rom
- else if Assigned(DKC3RomFilePage) then
- begin
- R := CompareStr(GetSNESMD5OfFile(DKC3ROMFilePage.Values[0]), '120abf304f0c40fe059f6a192ed4f947')
- if R <> 0 then
- MsgBox('Donkey Kong Country 3 ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := DKC3ROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetSMWROMPath(Param: string): string;
-begin
- if Length(smwrom) > 0 then
- Result := smwrom
- else if Assigned(SMWRomFilePage) then
- begin
- R := CompareStr(GetSNESMD5OfFile(SMWROMFilePage.Values[0]), 'cdd3c8c37322978ca8669b34bc89c804')
- if R <> 0 then
- MsgBox('Super Mario World ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := SMWROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetSoEROMPath(Param: string): string;
-begin
- if Length(soerom) > 0 then
- Result := soerom
- else if Assigned(SoERomFilePage) then
- begin
- R := CompareStr(GetSNESMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a')
- if R <> 0 then
- MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := SoEROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetOoTROMPath(Param: string): string;
-begin
- if Length(ootrom) > 0 then
- Result := ootrom
- else if Assigned(OoTROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '5bd1fe107bf8106b2ab6650abecd54d6') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '6697768a7a7df2dd27a692a2638ea90b') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '05f0f3ebacbc8df9243b6148ffe4792f');
- if R <> 0 then
- MsgBox('OoT ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := OoTROMFilePage.Values[0]
- end
- else
- Result := '';
-end;
-
-function GetL2ACROMPath(Param: string): string;
-begin
- if Length(l2acrom) > 0 then
- Result := l2acrom
- else if Assigned(L2ACROMFilePage) then
- begin
- R := CompareStr(GetSNESMD5OfFile(L2ACROMFilePage.Values[0]), '6efc477d6203ed2b3b9133c1cd9e9c5d')
- if R <> 0 then
- MsgBox('Lufia II ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := L2ACROMFilePage.Values[0]
- end
- else
- Result := '';
-end;
-
-function GetZlROMPath(Param: string): string;
-begin
- if Length(zlrom) > 0 then
- Result := zlrom
- else if Assigned(ZlROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(ZlROMFilePage.Values[0]), 'd4bf9e7bcf9a48da53785d2ae7bc4270');
- if R <> 0 then
- MsgBox('Zillion ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := ZlROMFilePage.Values[0]
- end
- else
- Result := '';
-end;
-
-function GetRedROMPath(Param: string): string;
-begin
- if Length(redrom) > 0 then
- Result := redrom
- else if Assigned(RedROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(RedROMFilePage.Values[0]), '3d45c1ee9abd5738df46d2bdda8b57dc')
- if R <> 0 then
- MsgBox('Pokemon Red ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := RedROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetBlueROMPath(Param: string): string;
-begin
- if Length(bluerom) > 0 then
- Result := bluerom
- else if Assigned(BlueROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(BlueROMFilePage.Values[0]), '50927e843568814f7ed45ec4f944bd8b')
- if R <> 0 then
- MsgBox('Pokemon Blue ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := BlueROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetTLoZROMPath(Param: string): string;
-begin
- if Length(tlozrom) > 0 then
- Result := tlozrom
- else if Assigned(TLoZROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(TLoZROMFilePage.Values[0]), '337bd6f1a1163df31bf2633665589ab0');
- if R <> 0 then
- MsgBox('The Legend of Zelda ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := TLoZROMFilePage.Values[0]
- end
- else
- Result := '';
-end;
-
-function GetLADXROMPath(Param: string): string;
-begin
- if Length(ladxrom) > 0 then
- Result := ladxrom
- else if Assigned(LADXROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(LADXROMFilePage.Values[0]), '07c211479386825042efb4ad31bb525f')
- if R <> 0 then
- MsgBox('Link''s Awakening DX ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := LADXROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetAdvnROMPath(Param: string): string;
-begin
- if Length(advnrom) > 0 then
- Result := advnrom
- else if Assigned(AdvnROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(AdvnROMFilePage.Values[0]), '157bddb7192754a45372be196797f284');
- if R <> 0 then
- MsgBox('Adventure ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := AdvnROMFilePage.Values[0]
- end
- else
- Result := '';
-end;
-
-function GetBN3ROMPath(Param: string): string;
-begin
- if Length(bn3rom) > 0 then
- Result := bn3rom
- else if Assigned(BN3ROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(BN3ROMFilePage.Values[0]), '6fe31df0144759b34ad666badaacc442')
- if R <> 0 then
- MsgBox('MegaMan Battle Network 3 Blue ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := BN3ROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-procedure InitializeWizard();
-begin
- AddOoTRomPage();
-
- lttprom := CheckRom('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', '03a63945398191337e896e5771f77173');
- if Length(lttprom) = 0 then
- LttPROMFilePage:= AddRomPage('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc');
-
- smrom := CheckRom('Super Metroid (JU).sfc', '21f3e98df4780ee1c667b84e57d88675');
- if Length(smrom) = 0 then
- SMRomFilePage:= AddRomPage('Super Metroid (JU).sfc');
-
- dkc3rom := CheckRom('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc', '120abf304f0c40fe059f6a192ed4f947');
- if Length(dkc3rom) = 0 then
- DKC3RomFilePage:= AddRomPage('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc');
-
- smwrom := CheckRom('Super Mario World (USA).sfc', 'cdd3c8c37322978ca8669b34bc89c804');
- if Length(smwrom) = 0 then
- SMWRomFilePage:= AddRomPage('Super Mario World (USA).sfc');
-
- soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a');
- if Length(soerom) = 0 then
- SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc');
-
- zlrom := CheckSMSRom('Zillion (UE) [!].sms', 'd4bf9e7bcf9a48da53785d2ae7bc4270');
- if Length(zlrom) = 0 then
- ZlROMFilePage:= AddSMSRomPage('Zillion (UE) [!].sms');
-
- redrom := CheckRom('Pokemon Red (UE) [S][!].gb','3d45c1ee9abd5738df46d2bdda8b57dc');
- if Length(redrom) = 0 then
- RedROMFilePage:= AddGBRomPage('Pokemon Red (UE) [S][!].gb');
-
- bluerom := CheckRom('Pokemon Blue (UE) [S][!].gb','50927e843568814f7ed45ec4f944bd8b');
- if Length(bluerom) = 0 then
- BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb');
-
- bn3rom := CheckRom('Mega Man Battle Network 3 - Blue Version (USA).gba','6fe31df0144759b34ad666badaacc442');
- if Length(bn3rom) = 0 then
- BN3ROMFilePage:= AddGBARomPage('Mega Man Battle Network 3 - Blue Version (USA).gba');
-
- ladxrom := CheckRom('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc','07c211479386825042efb4ad31bb525f');
- if Length(ladxrom) = 0 then
- LADXROMFilePage:= AddGBRomPage('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc');
-
- l2acrom := CheckRom('Lufia II - Rise of the Sinistrals (USA).sfc', '6efc477d6203ed2b3b9133c1cd9e9c5d');
- if Length(l2acrom) = 0 then
- L2ACROMFilePage:= AddRomPage('Lufia II - Rise of the Sinistrals (USA).sfc');
-
- tlozrom := CheckNESROM('Legend of Zelda, The (U) (PRG0) [!].nes', '337bd6f1a1163df31bf2633665589ab0');
- if Length(tlozrom) = 0 then
- TLoZROMFilePage:= AddNESRomPage('Legend of Zelda, The (U) (PRG0) [!].nes');
-
- advnrom := CheckSMSRom('ADVNTURE.BIN', '157bddb7192754a45372be196797f284');
- if Length(advnrom) = 0 then
- AdvnROMFilePage:= AddA26Page('ADVNTURE.BIN');
-end;
-
-
-function ShouldSkipPage(PageID: Integer): Boolean;
-begin
- Result := False;
- if (assigned(LttPROMFilePage)) and (PageID = LttPROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('client/sni/lttp') or WizardIsComponentSelected('generator/lttp'));
- if (assigned(SMROMFilePage)) and (PageID = SMROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm'));
- if (assigned(DKC3ROMFilePage)) and (PageID = DKC3ROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('client/sni/dkc3') or WizardIsComponentSelected('generator/dkc3'));
- if (assigned(SMWROMFilePage)) and (PageID = SMWROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('client/sni/smw') or WizardIsComponentSelected('generator/smw'));
- if (assigned(L2ACROMFilePage)) and (PageID = L2ACROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('client/sni/l2ac') or WizardIsComponentSelected('generator/l2ac'));
- if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/soe'));
- if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/oot') or WizardIsComponentSelected('client/oot'));
- if (assigned(ZlROMFilePage)) and (PageID = ZlROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/zl') or WizardIsComponentSelected('client/zl'));
- if (assigned(RedROMFilePage)) and (PageID = RedROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red'));
- if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue'));
- if (assigned(BN3ROMFilePage)) and (PageID = BN3ROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/mmbn3') or WizardIsComponentSelected('client/mmbn3'));
- if (assigned(LADXROMFilePage)) and (PageID = LADXROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/ladx') or WizardIsComponentSelected('client/ladx'));
- if (assigned(TLoZROMFilePage)) and (PageID = TLoZROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/tloz') or WizardIsComponentSelected('client/tloz'));
- if (assigned(AdvnROMFilePage)) and (PageID = AdvnROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('client/advn'));
-end;
diff --git a/kvui.py b/kvui.py
index 71bf80c86d9b..22e179d5be94 100644
--- a/kvui.py
+++ b/kvui.py
@@ -5,12 +5,13 @@
if sys.platform == "win32":
import ctypes
+
# kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout
# by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's
try:
ctypes.windll.shcore.SetProcessDpiAwareness(0)
except FileNotFoundError: # shcore may not be found on <= Windows 7
- pass # TODO: remove silent except when Python 3.8 is phased out.
+ pass # TODO: remove silent except when Python 3.8 is phased out.
os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1"
@@ -18,14 +19,15 @@
os.environ["KIVY_LOG_ENABLE"] = "0"
import Utils
+
if Utils.is_frozen():
os.environ["KIVY_DATA_DIR"] = Utils.local_path("data")
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
+Config.set("kivy", "exit_on_escape", "0")
+Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
from kivy.app import App
from kivy.core.window import Window
@@ -58,7 +60,6 @@
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
-
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType
from Utils import async_start
@@ -77,8 +78,8 @@ class HoverBehavior(object):
border_point = ObjectProperty(None)
def __init__(self, **kwargs):
- self.register_event_type('on_enter')
- self.register_event_type('on_leave')
+ self.register_event_type("on_enter")
+ self.register_event_type("on_leave")
Window.bind(mouse_pos=self.on_mouse_pos)
Window.bind(on_cursor_leave=self.on_cursor_leave)
super(HoverBehavior, self).__init__(**kwargs)
@@ -106,7 +107,7 @@ def on_cursor_leave(self, *args):
self.dispatch("on_leave")
-Factory.register('HoverBehavior', HoverBehavior)
+Factory.register("HoverBehavior", HoverBehavior)
class ToolTip(Label):
@@ -121,6 +122,60 @@ class HovererableLabel(HoverBehavior, Label):
pass
+class TooltipLabel(HovererableLabel):
+ tooltip = None
+
+ def create_tooltip(self, text, x, y):
+ text = text.replace(" ", "\n").replace("&", "&").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
+
+ def remove_tooltip(self):
+ if self.tooltip:
+ App.get_running_app().root.remove_widget(self.tooltip)
+ self.tooltip = None
+
+ def on_mouse_pos(self, window, pos):
+ if not self.get_root_window():
+ return # Abort if not displayed
+ super().on_mouse_pos(window, pos)
+ if self.refs and self.hovered:
+
+ tx, ty = self.to_widget(*pos, relative=True)
+ # Why TF is Y flipped *within* the texture?
+ ty = self.texture_size[1] - ty
+ hit = False
+ for uid, zones in self.refs.items():
+ for zone in zones:
+ x, y, w, h = zone
+ if x <= tx <= w and y <= ty <= h:
+ self.create_tooltip(uid.split("|", 1)[1], *pos)
+ hit = True
+ break
+ if not hit:
+ self.remove_tooltip()
+
+ def on_enter(self):
+ pass
+
+ def on_leave(self):
+ self.remove_tooltip()
+
+
class ServerLabel(HovererableLabel):
def __init__(self, *args, **kwargs):
super(HovererableLabel, self).__init__(*args, **kwargs)
@@ -189,11 +244,10 @@ class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
""" Adds selection and focus behaviour to the view. """
-class SelectableLabel(RecycleDataViewBehavior, HovererableLabel):
+class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
""" Add selection support to the Label """
index = None
selected = BooleanProperty(False)
- tooltip = None
def refresh_view_attrs(self, rv, index, data):
""" Catch and handle the view changes """
@@ -201,56 +255,6 @@ def refresh_view_attrs(self, rv, index, data):
return super(SelectableLabel, self).refresh_view_attrs(
rv, index, data)
- def create_tooltip(self, text, x, y):
- text = text.replace(" ", "\n").replace('&', '&').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
-
- def remove_tooltip(self):
- if self.tooltip:
- App.get_running_app().root.remove_widget(self.tooltip)
- self.tooltip = None
-
- def on_mouse_pos(self, window, pos):
- if not self.get_root_window():
- return # Abort if not displayed
- super().on_mouse_pos(window, pos)
- if self.refs and self.hovered:
-
- tx, ty = self.to_widget(*pos, relative=True)
- # Why TF is Y flipped *within* the texture?
- ty = self.texture_size[1] - ty
- hit = False
- for uid, zones in self.refs.items():
- for zone in zones:
- x, y, w, h = zone
- if x <= tx <= w and y <= ty <= h:
- self.create_tooltip(uid.split("|", 1)[1], *pos)
- hit = True
- break
- if not hit:
- self.remove_tooltip()
-
- def on_enter(self):
- pass
-
- def on_leave(self):
- self.remove_tooltip()
-
def on_touch_down(self, touch):
""" Add selection on touch down """
if super(SelectableLabel, self).on_touch_down(touch):
@@ -274,7 +278,7 @@ def on_touch_down(self, touch):
elif not cmdinput.text and text.startswith("Missing: "):
cmdinput.text = text.replace("Missing: ", "!hint_location ")
- Clipboard.copy(text.replace('&', '&').replace('&bl;', '[').replace('&br;', ']'))
+ Clipboard.copy(text.replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
return self.parent.select_with_touch(self.index, touch)
def apply_selection(self, rv, index, is_selected):
@@ -282,9 +286,68 @@ def apply_selection(self, rv, index, is_selected):
self.selected = is_selected
+class HintLabel(RecycleDataViewBehavior, BoxLayout):
+ selected = BooleanProperty(False)
+ striped = BooleanProperty(False)
+ index = None
+ no_select = []
+
+ def __init__(self):
+ super(HintLabel, self).__init__()
+ self.receiving_text = ""
+ self.item_text = ""
+ self.finding_text = ""
+ self.location_text = ""
+ self.entrance_text = ""
+ self.found_text = ""
+ for child in self.children:
+ child.bind(texture_size=self.set_height)
+
+ def set_height(self, instance, value):
+ self.height = max([child.texture_size[1] for child in self.children])
+
+ def refresh_view_attrs(self, rv, index, data):
+ self.index = index
+ if "select" in data and not data["select"] and index not in self.no_select:
+ self.no_select.append(index)
+ self.striped = data["striped"]
+ self.receiving_text = data["receiving"]["text"]
+ self.item_text = data["item"]["text"]
+ self.finding_text = data["finding"]["text"]
+ self.location_text = data["location"]["text"]
+ self.entrance_text = data["entrance"]["text"]
+ self.found_text = data["found"]["text"]
+ self.height = self.minimum_height
+ return super(HintLabel, self).refresh_view_attrs(rv, index, data)
+
+ def on_touch_down(self, touch):
+ """ Add selection on touch down """
+ if super(HintLabel, self).on_touch_down(touch):
+ return True
+ if self.index not in self.no_select:
+ if self.collide_point(*touch.pos):
+ if self.selected:
+ self.parent.clear_selection()
+ else:
+ text = "".join([self.receiving_text, "\'s ", self.item_text, " is at ", self.location_text, " in ",
+ self.finding_text, "\'s World", (" at " + self.entrance_text)
+ if self.entrance_text != "Vanilla"
+ else "", ". (", self.found_text.lower(), ")"])
+ temp = MarkupLabel(text).markup
+ text = "".join(
+ part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
+ Clipboard.copy(escape_markup(text).replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
+ return self.parent.select_with_touch(self.index, touch)
+
+ def apply_selection(self, rv, index, is_selected):
+ """ Respond to the selection of items in the view. """
+ if self.index not in self.no_select:
+ self.selected = is_selected
+
+
class ConnectBarTextInput(TextInput):
def insert_text(self, substring, from_undo=False):
- s = substring.replace('\n', '').replace('\r', '')
+ s = substring.replace("\n", "").replace("\r", "")
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
@@ -302,7 +365,7 @@ def __init__(self, **kwargs):
def __init__(self, title, text, error=False, **kwargs):
label = MessageBox.MessageBoxLabel(text=text)
separator_color = [217 / 255, 129 / 255, 122 / 255, 1.] if error else [47 / 255., 167 / 255., 212 / 255, 1.]
- super().__init__(title=title, content=label, size_hint=(None, None), width=max(100, int(label.width)+40),
+ super().__init__(title=title, content=label, size_hint=(None, None), width=max(100, int(label.width) + 40),
separator_color=separator_color, **kwargs)
self.height += max(0, label.height - 18)
@@ -358,11 +421,14 @@ def build(self) -> Layout:
# top part
server_label = ServerLabel()
self.connect_layout.add_widget(server_label)
- self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:", size_hint_y=None,
+ 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)
+
def connect_bar_validate(sender):
if not self.ctx.server:
self.connect_button_action(sender)
+
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)
@@ -383,20 +449,22 @@ def connect_bar_validate(sender):
bridge_logger = logging.getLogger(logger_name)
panel = TabbedPanelItem(text=display_name)
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
- self.tabs.add_widget(panel)
+ if len(self.logging_pairs) > 1:
+ # show Archipelago tab if other logging is present
+ self.tabs.add_widget(panel)
+
+ hint_panel = TabbedPanelItem(text="Hints")
+ self.log_panels["Hints"] = hint_panel.content = HintLog(self.json_to_kivy_parser)
+ self.tabs.add_widget(hint_panel)
+
+ 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.add_widget(self.tabs)
self.grid.add_widget(self.main_area_container)
- if len(self.logging_pairs) == 1:
- # Hide Tab selection if only one tab
- self.tabs.clear_tabs()
- self.tabs.do_default_tab = False
- self.tabs.current_tab.height = 0
- self.tabs.tab_height = 0
-
# 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)
@@ -422,7 +490,7 @@ def connect_bar_validate(sender):
return self.container
def update_texts(self, dt):
- if hasattr(self.tabs.content.children[0], 'fix_heights'):
+ if hasattr(self.tabs.content.children[0], "fix_heights"):
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
if self.ctx.server:
self.title = self.base_title + " " + Utils.__version__ + \
@@ -499,6 +567,10 @@ def set_new_energy_link_value(self):
if hasattr(self, "energy_link_label"):
self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J"
+ def update_hints(self):
+ hints = self.ctx.stored_data[f"_read_hints_{self.ctx.team}_{self.ctx.slot}"]
+ self.log_panels["Hints"].refresh_hints(hints)
+
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
def open_settings(self, *largs):
pass
@@ -513,12 +585,12 @@ def __init__(self, on_log):
def format_compact(record: logging.LogRecord) -> str:
if isinstance(record.msg, Exception):
return str(record.msg)
- return (f'{record.exc_info[1]}\n' if record.exc_info else '') + str(record.msg).split("\n")[0]
+ return (f"{record.exc_info[1]}\n" if record.exc_info else "") + str(record.msg).split("\n")[0]
def handle(self, record: logging.LogRecord) -> None:
- if getattr(record, 'skip_gui', False):
+ if getattr(record, "skip_gui", False):
pass # skip output
- elif getattr(record, 'compact_gui', False):
+ elif getattr(record, "compact_gui", False):
self.on_log(self.format_compact(record))
else:
self.on_log(self.format(record))
@@ -552,6 +624,44 @@ def fix_heights(self):
element.height = element.texture_size[1]
+class HintLog(RecycleView):
+ header = {
+ "receiving": {"text": "[u]Receiving Player[/u]"},
+ "item": {"text": "[u]Item[/u]"},
+ "finding": {"text": "[u]Finding Player[/u]"},
+ "location": {"text": "[u]Location[/u]"},
+ "entrance": {"text": "[u]Entrance[/u]"},
+ "found": {"text": "[u]Status[/u]"},
+ "striped": True,
+ "select": False,
+ }
+
+ def __init__(self, parser):
+ super(HintLog, self).__init__()
+ self.data = [self.header]
+ self.parser = parser
+
+ def refresh_hints(self, hints):
+ self.data = [self.header]
+ striped = False
+ for hint in hints:
+ self.data.append({
+ "striped": striped,
+ "receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})},
+ "item": {"text": self.parser.handle_node(
+ {"type": "item_id", "text": hint["item"], "flags": hint["item_flags"]})},
+ "finding": {"text": self.parser.handle_node({"type": "player_id", "text": hint["finding_player"]})},
+ "location": {"text": self.parser.handle_node({"type": "location_id", "text": hint["location"]})},
+ "entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text",
+ "color": "blue", "text": hint["entrance"]
+ if hint["entrance"] else "Vanilla"})},
+ "found": {
+ "text": self.parser.handle_node({"type": "color", "color": "green" if hint["found"] else "red",
+ "text": "Found" if hint["found"] else "Not Found"})},
+ })
+ striped = not striped
+
+
class E(ExceptionHandler):
logger = logging.getLogger("Client")
@@ -599,7 +709,7 @@ def _handle_player_id(self, node: JSONMessagePart):
f"Type: {SlotType(slot_info.type).name}"
if slot_info.group_members:
text += f" Members: " + \
- ' '.join(self.ctx.player_names[player] for player in slot_info.group_members)
+ " ".join(self.ctx.player_names[player] for player in slot_info.group_members)
node.setdefault("refs", []).append(text)
return super(KivyJSONtoTextParser, self)._handle_player_id(node)
@@ -627,4 +737,3 @@ def _handle_text(self, node: JSONMessagePart):
if os.path.exists(user_file):
logging.info("Loading user.kv into builder.")
Builder.load_file(user_file)
-
diff --git a/pytest.ini b/pytest.ini
index 5599a3c90f3a..33e0bab8a98f 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,4 +1,4 @@
[pytest]
-python_files = Test*.py
+python_files = test_*.py Test*.py # TODO: remove Test* once all worlds have been ported
python_classes = Test
-python_functions = test
\ No newline at end of file
+python_functions = test
diff --git a/settings.py b/settings.py
index a7dcbbf8ddbf..acae86095cda 100644
--- a/settings.py
+++ b/settings.py
@@ -694,6 +694,25 @@ class SnesRomStart(str):
snes_rom_start: Union[SnesRomStart, bool] = True
+class BizHawkClientOptions(Group):
+ class EmuHawkPath(UserFilePath):
+ """
+ The location of the EmuHawk you want to auto launch patched ROMs with
+ """
+ is_exe = True
+ description = "EmuHawk Executable"
+
+ class RomStart(str):
+ """
+ Set this to true to autostart a patched ROM in BizHawk with the connector script,
+ to false to never open the patched rom automatically,
+ or to a path to an external program to open the ROM file with that instead.
+ """
+
+ emuhawk_path: EmuHawkPath = EmuHawkPath(None)
+ rom_start: Union[RomStart, bool] = True
+
+
# Top-level group with lazy loading of worlds
class Settings(Group):
@@ -701,6 +720,7 @@ class Settings(Group):
server_options: ServerOptions = ServerOptions()
generator: GeneratorOptions = GeneratorOptions()
sni_options: SNIOptions = SNIOptions()
+ bizhawkclient_options: BizHawkClientOptions = BizHawkClientOptions()
_filename: Optional[str] = None
diff --git a/setup.py b/setup.py
index 6d4d947dbd1f..cea60dab8320 100644
--- a/setup.py
+++ b/setup.py
@@ -71,7 +71,6 @@
"Clique",
"DLCQuest",
"Final Fantasy",
- "Hylics 2",
"Kingdom Hearts 2",
"Lufia II Ancient Cave",
"Meritous",
diff --git a/test/TestBase.py b/test/TestBase.py
index e6fbafd95aa0..bfd92346d301 100644
--- a/test/TestBase.py
+++ b/test/TestBase.py
@@ -1,311 +1,3 @@
-import typing
-import unittest
-from argparse import Namespace
-
-from test.general import gen_steps
-from worlds import AutoWorld
-from worlds.AutoWorld import call_all
-
-from BaseClasses import Location, MultiWorld, CollectionState, ItemClassification, Item
-from worlds.alttp.Items import ItemFactory
-
-
-class TestBase(unittest.TestCase):
- multiworld: MultiWorld
- _state_cache = {}
-
- def get_state(self, items):
- if (self.multiworld, tuple(items)) in self._state_cache:
- return self._state_cache[self.multiworld, tuple(items)]
- state = CollectionState(self.multiworld)
- for item in items:
- item.classification = ItemClassification.progression
- state.collect(item, event=True)
- state.sweep_for_events()
- state.update_reachable_regions(1)
- self._state_cache[self.multiworld, tuple(items)] = state
- return state
-
- def get_path(self, state, region):
- def flist_to_iter(node):
- while node:
- value, node = node
- yield value
-
- from itertools import zip_longest
- reversed_path_as_flist = state.path.get(region, (region, None))
- string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
- # Now we combine the flat string list into (region, exit) pairs
- pathsiter = iter(string_path_flat)
- pathpairs = zip_longest(pathsiter, pathsiter)
- return list(pathpairs)
-
- def run_location_tests(self, access_pool):
- for i, (location, access, *item_pool) in enumerate(access_pool):
- items = item_pool[0]
- all_except = item_pool[1] if len(item_pool) > 1 else None
- state = self._get_items(item_pool, all_except)
- path = self.get_path(state, self.multiworld.get_location(location, 1).parent_region)
- with self.subTest(msg="Reach Location", location=location, access=access, items=items,
- all_except=all_except, path=path, entry=i):
-
- self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access,
- f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
-
- # check for partial solution
- if not all_except and access: # we are not supposed to be able to reach location with partial inventory
- for missing_item in item_pool[0]:
- with self.subTest(msg="Location reachable without required item", location=location,
- items=item_pool[0], missing_item=missing_item, entry=i):
- state = self._get_items_partial(item_pool, missing_item)
-
- self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False,
- f"failed {self.multiworld.get_location(location, 1)}: succeeded with "
- f"{missing_item} removed from: {item_pool}")
-
- def run_entrance_tests(self, access_pool):
- for i, (entrance, access, *item_pool) in enumerate(access_pool):
- items = item_pool[0]
- all_except = item_pool[1] if len(item_pool) > 1 else None
- state = self._get_items(item_pool, all_except)
- path = self.get_path(state, self.multiworld.get_entrance(entrance, 1).parent_region)
- with self.subTest(msg="Reach Entrance", entrance=entrance, access=access, items=items,
- all_except=all_except, path=path, entry=i):
-
- self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), access)
-
- # check for partial solution
- if not all_except and access: # we are not supposed to be able to reach location with partial inventory
- for missing_item in item_pool[0]:
- with self.subTest(msg="Entrance reachable without required item", entrance=entrance,
- items=item_pool[0], missing_item=missing_item, entry=i):
- state = self._get_items_partial(item_pool, missing_item)
- self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False,
- f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}")
-
- def _get_items(self, item_pool, all_except):
- if all_except and len(all_except) > 0:
- items = self.multiworld.itempool[:]
- items = [item for item in items if
- item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
- items.extend(ItemFactory(item_pool[0], 1))
- else:
- items = ItemFactory(item_pool[0], 1)
- return self.get_state(items)
-
- def _get_items_partial(self, item_pool, missing_item):
- new_items = item_pool[0].copy()
- new_items.remove(missing_item)
- items = ItemFactory(new_items, 1)
- return self.get_state(items)
-
-
-class WorldTestBase(unittest.TestCase):
- options: typing.Dict[str, typing.Any] = {}
- multiworld: MultiWorld
-
- game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore"
- auto_construct: typing.ClassVar[bool] = True
- """ automatically set up a world for each test in this class """
-
- def setUp(self) -> None:
- if self.auto_construct:
- self.world_setup()
-
- def world_setup(self, seed: typing.Optional[int] = None) -> None:
- if type(self) is WorldTestBase or \
- (hasattr(WorldTestBase, self._testMethodName)
- and not self.run_default_tests and
- getattr(self, self._testMethodName).__code__ is
- getattr(WorldTestBase, self._testMethodName, None).__code__):
- return # setUp gets called for tests defined in the base class. We skip world_setup here.
- if not hasattr(self, "game"):
- raise NotImplementedError("didn't define game name")
- self.multiworld = MultiWorld(1)
- self.multiworld.game[1] = self.game
- self.multiworld.player_name = {1: "Tester"}
- self.multiworld.set_seed(seed)
- self.multiworld.state = CollectionState(self.multiworld)
- args = Namespace()
- for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
- setattr(args, name, {
- 1: option.from_any(self.options.get(name, getattr(option, "default")))
- })
- self.multiworld.set_options(args)
- for step in gen_steps:
- call_all(self.multiworld, step)
-
- # methods that can be called within tests
- def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]],
- state: typing.Optional[CollectionState] = None) -> None:
- """Collects all pre-placed items and items in the multiworld itempool except those provided"""
- if isinstance(item_names, str):
- item_names = (item_names,)
- if not state:
- state = self.multiworld.state
- for item in self.multiworld.get_items():
- if item.name not in item_names:
- state.collect(item)
-
- def get_item_by_name(self, item_name: str) -> Item:
- """Returns the first item found in placed items, or in the itempool with the matching name"""
- for item in self.multiworld.get_items():
- if item.name == item_name:
- return item
- raise ValueError("No such item")
-
- def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
- """Returns actual items from the itempool that match the provided name(s)"""
- if isinstance(item_names, str):
- item_names = (item_names,)
- return [item for item in self.multiworld.itempool if item.name in item_names]
-
- def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
- """ collect all of the items in the item pool that have the given names """
- items = self.get_items_by_name(item_names)
- self.collect(items)
- return items
-
- def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
- """Collects the provided item(s) into state"""
- if isinstance(items, Item):
- items = (items,)
- for item in items:
- self.multiworld.state.collect(item)
-
- def remove_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
- """Remove all of the items in the item pool with the given names from state"""
- items = self.get_items_by_name(item_names)
- self.remove(items)
- return items
-
- def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
- """Removes the provided item(s) from state"""
- if isinstance(items, Item):
- items = (items,)
- for item in items:
- if item.location and item.location.event and item.location in self.multiworld.state.events:
- self.multiworld.state.events.remove(item.location)
- self.multiworld.state.remove(item)
-
- def can_reach_location(self, location: str) -> bool:
- """Determines if the current state can reach the provided location name"""
- return self.multiworld.state.can_reach(location, "Location", 1)
-
- def can_reach_entrance(self, entrance: str) -> bool:
- """Determines if the current state can reach the provided entrance name"""
- return self.multiworld.state.can_reach(entrance, "Entrance", 1)
-
- def can_reach_region(self, region: str) -> bool:
- """Determines if the current state can reach the provided region name"""
- return self.multiworld.state.can_reach(region, "Region", 1)
-
- def count(self, item_name: str) -> int:
- """Returns the amount of an item currently in state"""
- return self.multiworld.state.count(item_name, 1)
-
- def assertAccessDependency(self,
- locations: typing.List[str],
- possible_items: typing.Iterable[typing.Iterable[str]],
- only_check_listed: bool = False) -> None:
- """Asserts that the provided locations can't be reached without the listed items but can be reached with any
- one of the provided combinations"""
- all_items = [item_name for item_names in possible_items for item_name in item_names]
-
- state = CollectionState(self.multiworld)
- self.collect_all_but(all_items, state)
- if only_check_listed:
- for location in locations:
- self.assertFalse(state.can_reach(location, "Location", 1), f"{location} is reachable without {all_items}")
- else:
- for location in self.multiworld.get_locations():
- loc_reachable = state.can_reach(location, "Location", 1)
- self.assertEqual(loc_reachable, location.name not in locations,
- f"{location.name} is reachable without {all_items}" if loc_reachable
- else f"{location.name} is not reachable without {all_items}")
- for item_names in possible_items:
- items = self.get_items_by_name(item_names)
- for item in items:
- state.collect(item)
- for location in locations:
- self.assertTrue(state.can_reach(location, "Location", 1),
- f"{location} not reachable with {item_names}")
- for item in items:
- state.remove(item)
-
- def assertBeatable(self, beatable: bool):
- """Asserts that the game can be beaten with the current state"""
- self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable)
-
- # following tests are automatically run
- @property
- def run_default_tests(self) -> bool:
- """Not possible or identical to the base test that's always being run already"""
- return (self.options
- or self.setUp.__code__ is not WorldTestBase.setUp.__code__
- or self.world_setup.__code__ is not WorldTestBase.world_setup.__code__)
-
- @property
- def constructed(self) -> bool:
- """A multiworld has been constructed by this point"""
- return hasattr(self, "game") and hasattr(self, "multiworld")
-
- def testAllStateCanReachEverything(self):
- """Ensure all state can reach everything and complete the game with the defined options"""
- if not (self.run_default_tests and self.constructed):
- return
- with self.subTest("Game", game=self.game):
- excluded = self.multiworld.exclude_locations[1].value
- state = self.multiworld.get_all_state(False)
- for location in self.multiworld.get_locations():
- if location.name not in excluded:
- with self.subTest("Location should be reached", location=location):
- reachable = location.can_reach(state)
- self.assertTrue(reachable, f"{location.name} unreachable")
- with self.subTest("Beatable"):
- self.multiworld.state = state
- self.assertBeatable(True)
-
- def testEmptyStateCanReachSomething(self):
- """Ensure empty state can reach at least one location with the defined options"""
- if not (self.run_default_tests and self.constructed):
- return
- with self.subTest("Game", game=self.game):
- state = CollectionState(self.multiworld)
- locations = self.multiworld.get_reachable_locations(state, 1)
- self.assertGreater(len(locations), 0,
- "Need to be able to reach at least one location to get started.")
-
- def testFill(self):
- """Generates a multiworld and validates placements with the defined options"""
- # don't run this test if accessibility is set manually
- if not (self.run_default_tests and self.constructed):
- return
- from Fill import distribute_items_restrictive
-
- # basically a shortened reimplementation of this method from core, in order to force the check is done
- def fulfills_accessibility():
- locations = self.multiworld.get_locations(1).copy()
- state = CollectionState(self.multiworld)
- while locations:
- sphere: typing.List[Location] = []
- for n in range(len(locations) - 1, -1, -1):
- if locations[n].can_reach(state):
- sphere.append(locations.pop(n))
- self.assertTrue(sphere or self.multiworld.accessibility[1] == "minimal",
- f"Unreachable locations: {locations}")
- if not sphere:
- break
- for location in sphere:
- if location.item:
- state.collect(location.item, True, location)
-
- return self.multiworld.has_beaten_game(state, 1)
-
- with self.subTest("Game", game=self.game):
- distribute_items_restrictive(self.multiworld)
- call_all(self.multiworld, "post_fill")
- self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.")
- placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code]
- self.assertLessEqual(len(self.multiworld.itempool), len(placed_items),
- "Unplaced Items remaining in itempool")
+from .bases import TestBase, WorldTestBase
+from warnings import warn
+warn("TestBase was renamed to bases", DeprecationWarning)
diff --git a/test/bases.py b/test/bases.py
new file mode 100644
index 000000000000..2054c2d18725
--- /dev/null
+++ b/test/bases.py
@@ -0,0 +1,335 @@
+import sys
+import typing
+import unittest
+from argparse import Namespace
+
+from test.general import gen_steps
+from worlds import AutoWorld
+from worlds.AutoWorld import call_all
+
+from BaseClasses import Location, MultiWorld, CollectionState, ItemClassification, Item
+from worlds.alttp.Items import ItemFactory
+
+
+class TestBase(unittest.TestCase):
+ multiworld: MultiWorld
+ _state_cache = {}
+
+ def get_state(self, items):
+ if (self.multiworld, tuple(items)) in self._state_cache:
+ return self._state_cache[self.multiworld, tuple(items)]
+ state = CollectionState(self.multiworld)
+ for item in items:
+ item.classification = ItemClassification.progression
+ state.collect(item, event=True)
+ state.sweep_for_events()
+ state.update_reachable_regions(1)
+ self._state_cache[self.multiworld, tuple(items)] = state
+ return state
+
+ def get_path(self, state, region):
+ def flist_to_iter(node):
+ while node:
+ value, node = node
+ yield value
+
+ from itertools import zip_longest
+ reversed_path_as_flist = state.path.get(region, (region, None))
+ string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
+ # Now we combine the flat string list into (region, exit) pairs
+ pathsiter = iter(string_path_flat)
+ pathpairs = zip_longest(pathsiter, pathsiter)
+ return list(pathpairs)
+
+ def run_location_tests(self, access_pool):
+ for i, (location, access, *item_pool) in enumerate(access_pool):
+ items = item_pool[0]
+ all_except = item_pool[1] if len(item_pool) > 1 else None
+ state = self._get_items(item_pool, all_except)
+ path = self.get_path(state, self.multiworld.get_location(location, 1).parent_region)
+ with self.subTest(msg="Reach Location", location=location, access=access, items=items,
+ all_except=all_except, path=path, entry=i):
+
+ self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access,
+ f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
+
+ # check for partial solution
+ if not all_except and access: # we are not supposed to be able to reach location with partial inventory
+ for missing_item in item_pool[0]:
+ with self.subTest(msg="Location reachable without required item", location=location,
+ items=item_pool[0], missing_item=missing_item, entry=i):
+ state = self._get_items_partial(item_pool, missing_item)
+
+ self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False,
+ f"failed {self.multiworld.get_location(location, 1)}: succeeded with "
+ f"{missing_item} removed from: {item_pool}")
+
+ def run_entrance_tests(self, access_pool):
+ for i, (entrance, access, *item_pool) in enumerate(access_pool):
+ items = item_pool[0]
+ all_except = item_pool[1] if len(item_pool) > 1 else None
+ state = self._get_items(item_pool, all_except)
+ path = self.get_path(state, self.multiworld.get_entrance(entrance, 1).parent_region)
+ with self.subTest(msg="Reach Entrance", entrance=entrance, access=access, items=items,
+ all_except=all_except, path=path, entry=i):
+
+ self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), access)
+
+ # check for partial solution
+ if not all_except and access: # we are not supposed to be able to reach location with partial inventory
+ for missing_item in item_pool[0]:
+ with self.subTest(msg="Entrance reachable without required item", entrance=entrance,
+ items=item_pool[0], missing_item=missing_item, entry=i):
+ state = self._get_items_partial(item_pool, missing_item)
+ self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False,
+ f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}")
+
+ def _get_items(self, item_pool, all_except):
+ if all_except and len(all_except) > 0:
+ items = self.multiworld.itempool[:]
+ items = [item for item in items if
+ item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
+ items.extend(ItemFactory(item_pool[0], 1))
+ else:
+ items = ItemFactory(item_pool[0], 1)
+ return self.get_state(items)
+
+ def _get_items_partial(self, item_pool, missing_item):
+ new_items = item_pool[0].copy()
+ new_items.remove(missing_item)
+ items = ItemFactory(new_items, 1)
+ return self.get_state(items)
+
+
+class WorldTestBase(unittest.TestCase):
+ options: typing.Dict[str, typing.Any] = {}
+ multiworld: MultiWorld
+
+ game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore"
+ auto_construct: typing.ClassVar[bool] = True
+ """ automatically set up a world for each test in this class """
+ memory_leak_tested: typing.ClassVar[bool] = False
+ """ remember if memory leak test was already done for this class """
+
+ def setUp(self) -> None:
+ if self.auto_construct:
+ self.world_setup()
+
+ def tearDown(self) -> None:
+ if self.__class__.memory_leak_tested or not self.options or not self.constructed or \
+ sys.version_info < (3, 11, 0): # the leak check in tearDown fails in py<3.11 for an unknown reason
+ # only run memory leak test once per class, only for constructed with non-default options
+ # default options will be tested in test/general
+ super().tearDown()
+ return
+
+ import gc
+ import weakref
+ weak = weakref.ref(self.multiworld)
+ for attr_name in dir(self): # delete all direct references to MultiWorld and World
+ attr: object = typing.cast(object, getattr(self, attr_name))
+ if type(attr) is MultiWorld or isinstance(attr, AutoWorld.World):
+ delattr(self, attr_name)
+ state_cache: typing.Optional[typing.Dict[typing.Any, typing.Any]] = getattr(self, "_state_cache", None)
+ if state_cache is not None: # in case of multiple inheritance with TestBase, we need to clear its cache
+ state_cache.clear()
+ gc.collect()
+ self.__class__.memory_leak_tested = True
+ self.assertFalse(weak(), f"World {getattr(self, 'game', '')} leaked MultiWorld object")
+ super().tearDown()
+
+ def world_setup(self, seed: typing.Optional[int] = None) -> None:
+ if type(self) is WorldTestBase or \
+ (hasattr(WorldTestBase, self._testMethodName)
+ and not self.run_default_tests and
+ getattr(self, self._testMethodName).__code__ is
+ getattr(WorldTestBase, self._testMethodName, None).__code__):
+ return # setUp gets called for tests defined in the base class. We skip world_setup here.
+ if not hasattr(self, "game"):
+ raise NotImplementedError("didn't define game name")
+ self.multiworld = MultiWorld(1)
+ self.multiworld.game[1] = self.game
+ self.multiworld.player_name = {1: "Tester"}
+ self.multiworld.set_seed(seed)
+ self.multiworld.state = CollectionState(self.multiworld)
+ args = Namespace()
+ for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
+ setattr(args, name, {
+ 1: option.from_any(self.options.get(name, getattr(option, "default")))
+ })
+ self.multiworld.set_options(args)
+ for step in gen_steps:
+ call_all(self.multiworld, step)
+
+ # methods that can be called within tests
+ def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]],
+ state: typing.Optional[CollectionState] = None) -> None:
+ """Collects all pre-placed items and items in the multiworld itempool except those provided"""
+ if isinstance(item_names, str):
+ item_names = (item_names,)
+ if not state:
+ state = self.multiworld.state
+ for item in self.multiworld.get_items():
+ if item.name not in item_names:
+ state.collect(item)
+
+ def get_item_by_name(self, item_name: str) -> Item:
+ """Returns the first item found in placed items, or in the itempool with the matching name"""
+ for item in self.multiworld.get_items():
+ if item.name == item_name:
+ return item
+ raise ValueError("No such item")
+
+ def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
+ """Returns actual items from the itempool that match the provided name(s)"""
+ if isinstance(item_names, str):
+ item_names = (item_names,)
+ return [item for item in self.multiworld.itempool if item.name in item_names]
+
+ def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
+ """ collect all of the items in the item pool that have the given names """
+ items = self.get_items_by_name(item_names)
+ self.collect(items)
+ return items
+
+ def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
+ """Collects the provided item(s) into state"""
+ if isinstance(items, Item):
+ items = (items,)
+ for item in items:
+ self.multiworld.state.collect(item)
+
+ def remove_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
+ """Remove all of the items in the item pool with the given names from state"""
+ items = self.get_items_by_name(item_names)
+ self.remove(items)
+ return items
+
+ def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
+ """Removes the provided item(s) from state"""
+ if isinstance(items, Item):
+ items = (items,)
+ for item in items:
+ if item.location and item.location.event and item.location in self.multiworld.state.events:
+ self.multiworld.state.events.remove(item.location)
+ self.multiworld.state.remove(item)
+
+ def can_reach_location(self, location: str) -> bool:
+ """Determines if the current state can reach the provided location name"""
+ return self.multiworld.state.can_reach(location, "Location", 1)
+
+ def can_reach_entrance(self, entrance: str) -> bool:
+ """Determines if the current state can reach the provided entrance name"""
+ return self.multiworld.state.can_reach(entrance, "Entrance", 1)
+
+ def can_reach_region(self, region: str) -> bool:
+ """Determines if the current state can reach the provided region name"""
+ return self.multiworld.state.can_reach(region, "Region", 1)
+
+ def count(self, item_name: str) -> int:
+ """Returns the amount of an item currently in state"""
+ return self.multiworld.state.count(item_name, 1)
+
+ def assertAccessDependency(self,
+ locations: typing.List[str],
+ possible_items: typing.Iterable[typing.Iterable[str]],
+ only_check_listed: bool = False) -> None:
+ """Asserts that the provided locations can't be reached without the listed items but can be reached with any
+ one of the provided combinations"""
+ all_items = [item_name for item_names in possible_items for item_name in item_names]
+
+ state = CollectionState(self.multiworld)
+ self.collect_all_but(all_items, state)
+ if only_check_listed:
+ for location in locations:
+ self.assertFalse(state.can_reach(location, "Location", 1), f"{location} is reachable without {all_items}")
+ else:
+ for location in self.multiworld.get_locations():
+ loc_reachable = state.can_reach(location, "Location", 1)
+ self.assertEqual(loc_reachable, location.name not in locations,
+ f"{location.name} is reachable without {all_items}" if loc_reachable
+ else f"{location.name} is not reachable without {all_items}")
+ for item_names in possible_items:
+ items = self.get_items_by_name(item_names)
+ for item in items:
+ state.collect(item)
+ for location in locations:
+ self.assertTrue(state.can_reach(location, "Location", 1),
+ f"{location} not reachable with {item_names}")
+ for item in items:
+ state.remove(item)
+
+ def assertBeatable(self, beatable: bool):
+ """Asserts that the game can be beaten with the current state"""
+ self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable)
+
+ # following tests are automatically run
+ @property
+ def run_default_tests(self) -> bool:
+ """Not possible or identical to the base test that's always being run already"""
+ return (self.options
+ or self.setUp.__code__ is not WorldTestBase.setUp.__code__
+ or self.world_setup.__code__ is not WorldTestBase.world_setup.__code__)
+
+ @property
+ def constructed(self) -> bool:
+ """A multiworld has been constructed by this point"""
+ return hasattr(self, "game") and hasattr(self, "multiworld")
+
+ def test_all_state_can_reach_everything(self):
+ """Ensure all state can reach everything and complete the game with the defined options"""
+ if not (self.run_default_tests and self.constructed):
+ return
+ with self.subTest("Game", game=self.game):
+ excluded = self.multiworld.exclude_locations[1].value
+ state = self.multiworld.get_all_state(False)
+ for location in self.multiworld.get_locations():
+ if location.name not in excluded:
+ with self.subTest("Location should be reached", location=location):
+ reachable = location.can_reach(state)
+ self.assertTrue(reachable, f"{location.name} unreachable")
+ with self.subTest("Beatable"):
+ self.multiworld.state = state
+ self.assertBeatable(True)
+
+ def test_empty_state_can_reach_something(self):
+ """Ensure empty state can reach at least one location with the defined options"""
+ if not (self.run_default_tests and self.constructed):
+ return
+ with self.subTest("Game", game=self.game):
+ state = CollectionState(self.multiworld)
+ locations = self.multiworld.get_reachable_locations(state, 1)
+ self.assertGreater(len(locations), 0,
+ "Need to be able to reach at least one location to get started.")
+
+ def test_fill(self):
+ """Generates a multiworld and validates placements with the defined options"""
+ if not (self.run_default_tests and self.constructed):
+ return
+ from Fill import distribute_items_restrictive
+
+ # basically a shortened reimplementation of this method from core, in order to force the check is done
+ def fulfills_accessibility() -> bool:
+ locations = list(self.multiworld.get_locations(1))
+ state = CollectionState(self.multiworld)
+ while locations:
+ sphere: typing.List[Location] = []
+ for n in range(len(locations) - 1, -1, -1):
+ if locations[n].can_reach(state):
+ sphere.append(locations.pop(n))
+ self.assertTrue(sphere or self.multiworld.accessibility[1] == "minimal",
+ f"Unreachable locations: {locations}")
+ if not sphere:
+ break
+ for location in sphere:
+ if location.item:
+ state.collect(location.item, True, location)
+ return self.multiworld.has_beaten_game(state, 1)
+
+ with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
+ distribute_items_restrictive(self.multiworld)
+ call_all(self.multiworld, "post_fill")
+ self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.")
+ placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code]
+ self.assertLessEqual(len(self.multiworld.itempool), len(placed_items),
+ "Unplaced Items remaining in itempool")
diff --git a/test/general/__init__.py b/test/general/__init__.py
index d7ecc9574930..5e0f22f4ecfa 100644
--- a/test/general/__init__.py
+++ b/test/general/__init__.py
@@ -8,6 +8,13 @@
def setup_solo_multiworld(world_type: Type[World], steps: Tuple[str, ...] = gen_steps) -> MultiWorld:
+ """
+ Creates a multiworld with a single player of `world_type`, sets default options, and calls provided gen steps.
+
+ :param world_type: Type of the world to generate a multiworld for
+ :param steps: The gen steps that should be called on the generated multiworld before returning. Default calls
+ steps through pre_fill
+ """
multiworld = MultiWorld(1)
multiworld.game[1] = world_type.game
multiworld.player_name = {1: "Tester"}
diff --git a/test/general/TestFill.py b/test/general/test_fill.py
similarity index 91%
rename from test/general/TestFill.py
rename to test/general/test_fill.py
index 0933603dfdd0..1e469ef04d0d 100644
--- a/test/general/TestFill.py
+++ b/test/general/test_fill.py
@@ -72,7 +72,7 @@ def generate_region(self, parent: Region, size: int, access_rule: CollectionRule
return region
-def fillRegion(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]:
+def fill_region(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]:
items = items.copy()
while len(items) > 0:
location = region.locations.pop(0)
@@ -86,7 +86,7 @@ def fillRegion(world: MultiWorld, region: Region, items: List[Item]) -> List[Ite
return items
-def regionContains(region: Region, item: Item) -> bool:
+def region_contains(region: Region, item: Item) -> bool:
for location in region.locations:
if location.item == item:
return True
@@ -133,6 +133,7 @@ def names(objs: list) -> Iterable[str]:
class TestFillRestrictive(unittest.TestCase):
def test_basic_fill(self):
+ """Tests `fill_restrictive` fills and removes the locations and items from their respective lists"""
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2)
@@ -150,6 +151,7 @@ def test_basic_fill(self):
self.assertEqual([], player1.prog_items)
def test_ordered_fill(self):
+ """Tests `fill_restrictive` fulfills set rules"""
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2)
items = player1.prog_items
@@ -166,6 +168,7 @@ def test_ordered_fill(self):
self.assertEqual(locations[1].item, items[1])
def test_partial_fill(self):
+ """Tests that `fill_restrictive` returns unfilled locations"""
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 3, 2)
@@ -191,6 +194,7 @@ def test_partial_fill(self):
self.assertEqual(player1.locations[0], loc2)
def test_minimal_fill(self):
+ """Test that fill for minimal player can have unreachable items"""
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2)
@@ -246,6 +250,7 @@ def test_minimal_mixed_fill(self):
f'{item} is unreachable in {item.location}')
def test_reversed_fill(self):
+ """Test a different set of rules can be satisfied"""
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2)
@@ -264,6 +269,7 @@ def test_reversed_fill(self):
self.assertEqual(loc1.item, item0)
def test_multi_step_fill(self):
+ """Test that fill is able to satisfy multiple spheres"""
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 4, 4)
@@ -288,6 +294,7 @@ def test_multi_step_fill(self):
self.assertEqual(locations[3].item, items[3])
def test_impossible_fill(self):
+ """Test that fill raises an error when it can't place any items"""
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2)
items = player1.prog_items
@@ -304,6 +311,7 @@ def test_impossible_fill(self):
player1.locations.copy(), player1.prog_items.copy())
def test_circular_fill(self):
+ """Test that fill raises an error when it can't place all items"""
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 3, 3)
@@ -324,6 +332,7 @@ def test_circular_fill(self):
player1.locations.copy(), player1.prog_items.copy())
def test_competing_fill(self):
+ """Test that fill raises an error when it can't place items in a way to satisfy the conditions"""
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2)
@@ -340,6 +349,7 @@ def test_competing_fill(self):
player1.locations.copy(), player1.prog_items.copy())
def test_multiplayer_fill(self):
+ """Test that items can be placed across worlds"""
multi_world = generate_multi_world(2)
player1 = generate_player_data(multi_world, 1, 2, 2)
player2 = generate_player_data(multi_world, 2, 2, 2)
@@ -360,6 +370,7 @@ def test_multiplayer_fill(self):
self.assertEqual(player2.locations[1].item, player2.prog_items[0])
def test_multiplayer_rules_fill(self):
+ """Test that fill across worlds satisfies the rules"""
multi_world = generate_multi_world(2)
player1 = generate_player_data(multi_world, 1, 2, 2)
player2 = generate_player_data(multi_world, 2, 2, 2)
@@ -383,6 +394,7 @@ def test_multiplayer_rules_fill(self):
self.assertEqual(player2.locations[1].item, player1.prog_items[1])
def test_restrictive_progress(self):
+ """Test that various spheres with different requirements can be filled"""
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, prog_item_count=25)
items = player1.prog_items.copy()
@@ -405,6 +417,7 @@ def test_restrictive_progress(self):
locations, player1.prog_items)
def test_swap_to_earlier_location_with_item_rule(self):
+ """Test that item swap happens and works as intended"""
# test for PR#1109
multi_world = generate_multi_world(1)
player1 = generate_player_data(multi_world, 1, 4, 4)
@@ -430,6 +443,7 @@ def test_swap_to_earlier_location_with_item_rule(self):
self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1")
def test_double_sweep(self):
+ """Test that sweep doesn't duplicate Event items when sweeping"""
# test for PR1114
multi_world = generate_multi_world(1)
player1 = generate_player_data(multi_world, 1, 1, 1)
@@ -441,10 +455,11 @@ def test_double_sweep(self):
location.place_locked_item(item)
multi_world.state.sweep_for_events()
multi_world.state.sweep_for_events()
- self.assertTrue(multi_world.state.prog_items[item.name, item.player], "Sweep did not collect - Test flawed")
- self.assertEqual(multi_world.state.prog_items[item.name, item.player], 1, "Sweep collected multiple times")
+ self.assertTrue(multi_world.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed")
+ self.assertEqual(multi_world.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times")
def test_correct_item_instance_removed_from_pool(self):
+ """Test that a placed item gets removed from the submitted pool"""
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2)
@@ -461,6 +476,7 @@ def test_correct_item_instance_removed_from_pool(self):
class TestDistributeItemsRestrictive(unittest.TestCase):
def test_basic_distribute(self):
+ """Test that distribute_items_restrictive is deterministic"""
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
@@ -480,6 +496,7 @@ def test_basic_distribute(self):
self.assertFalse(locations[3].event)
def test_excluded_distribute(self):
+ """Test that distribute_items_restrictive doesn't put advancement items on excluded locations"""
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
@@ -494,6 +511,7 @@ def test_excluded_distribute(self):
self.assertFalse(locations[2].item.advancement)
def test_non_excluded_item_distribute(self):
+ """Test that useful items aren't placed on excluded locations"""
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
@@ -508,6 +526,7 @@ def test_non_excluded_item_distribute(self):
self.assertEqual(locations[1].item, basic_items[0])
def test_too_many_excluded_distribute(self):
+ """Test that fill fails if it can't place all progression items due to too many excluded locations"""
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
@@ -520,6 +539,7 @@ def test_too_many_excluded_distribute(self):
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
def test_non_excluded_item_must_distribute(self):
+ """Test that fill fails if it can't place useful items due to too many excluded locations"""
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
@@ -534,6 +554,7 @@ def test_non_excluded_item_must_distribute(self):
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
def test_priority_distribute(self):
+ """Test that priority locations receive advancement items"""
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
@@ -548,6 +569,7 @@ def test_priority_distribute(self):
self.assertTrue(locations[3].item.advancement)
def test_excess_priority_distribute(self):
+ """Test that if there's more priority locations than advancement items, they can still fill"""
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
@@ -562,6 +584,7 @@ def test_excess_priority_distribute(self):
self.assertFalse(locations[3].item.advancement)
def test_multiple_world_priority_distribute(self):
+ """Test that priority fill can be satisfied for multiple worlds"""
multi_world = generate_multi_world(3)
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
@@ -591,7 +614,7 @@ def test_multiple_world_priority_distribute(self):
self.assertTrue(player3.locations[3].item.advancement)
def test_can_remove_locations_in_fill_hook(self):
-
+ """Test that distribute_items_restrictive calls the fill hook and allows for item and location removal"""
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
@@ -611,6 +634,7 @@ def fill_hook(progitempool, usefulitempool, filleritempool, fill_locations):
self.assertIsNone(removed_location[0].item)
def test_seed_robust_to_item_order(self):
+ """Test deterministic fill"""
mw1 = generate_multi_world()
gen1 = generate_player_data(
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
@@ -628,6 +652,7 @@ def test_seed_robust_to_item_order(self):
self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
def test_seed_robust_to_location_order(self):
+ """Test deterministic fill even if locations in a region are reordered"""
mw1 = generate_multi_world()
gen1 = generate_player_data(
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
@@ -646,6 +671,7 @@ def test_seed_robust_to_location_order(self):
self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
def test_can_reserve_advancement_items_for_general_fill(self):
+ """Test that priority locations fill still satisfies item rules"""
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, location_count=5, prog_item_count=5)
@@ -655,14 +681,14 @@ def test_can_reserve_advancement_items_for_general_fill(self):
location = player1.locations[0]
location.progress_type = LocationProgressType.PRIORITY
- location.item_rule = lambda item: item != items[
- 0] and item != items[1] and item != items[2] and item != items[3]
+ location.item_rule = lambda item: item not in items[:4]
distribute_items_restrictive(multi_world)
self.assertEqual(location.item, items[4])
def test_non_excluded_local_items(self):
+ """Test that local items get placed locally in a multiworld"""
multi_world = generate_multi_world(2)
player1 = generate_player_data(
multi_world, 1, location_count=5, basic_item_count=5)
@@ -683,6 +709,7 @@ def test_non_excluded_local_items(self):
self.assertFalse(item.location.event, False)
def test_early_items(self) -> None:
+ """Test that the early items API successfully places items early"""
mw = generate_multi_world(2)
player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5)
player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5)
@@ -762,21 +789,22 @@ def setUp(self) -> None:
# Sphere 1
region = player1.generate_region(player1.menu, 20)
- items = fillRegion(multi_world, region, [
+ items = fill_region(multi_world, region, [
player1.prog_items[0]] + items)
# Sphere 2
region = player1.generate_region(
player1.regions[1], 20, lambda state: state.has(player1.prog_items[0].name, player1.id))
- items = fillRegion(
+ items = fill_region(
multi_world, region, [player1.prog_items[1], player2.prog_items[0]] + items)
# Sphere 3
region = player2.generate_region(
player2.menu, 20, lambda state: state.has(player2.prog_items[0].name, player2.id))
- fillRegion(multi_world, region, [player2.prog_items[1]] + items)
+ fill_region(multi_world, region, [player2.prog_items[1]] + items)
def test_balances_progression(self) -> None:
+ """Tests that progression balancing moves progression items earlier"""
self.multi_world.progression_balancing[self.player1.id].value = 50
self.multi_world.progression_balancing[self.player2.id].value = 50
@@ -789,6 +817,7 @@ def test_balances_progression(self) -> None:
self.player1.regions[1], self.player2.prog_items[0])
def test_balances_progression_light(self) -> None:
+ """Test that progression balancing still moves items earlier on minimum value"""
self.multi_world.progression_balancing[self.player1.id].value = 1
self.multi_world.progression_balancing[self.player2.id].value = 1
@@ -802,6 +831,7 @@ def test_balances_progression_light(self) -> None:
self.player1.regions[1], self.player2.prog_items[0])
def test_balances_progression_heavy(self) -> None:
+ """Test that progression balancing moves items earlier on maximum value"""
self.multi_world.progression_balancing[self.player1.id].value = 99
self.multi_world.progression_balancing[self.player2.id].value = 99
@@ -815,6 +845,7 @@ def test_balances_progression_heavy(self) -> None:
self.player1.regions[1], self.player2.prog_items[0])
def test_skips_balancing_progression(self) -> None:
+ """Test that progression balancing is skipped when players have it disabled"""
self.multi_world.progression_balancing[self.player1.id].value = 0
self.multi_world.progression_balancing[self.player2.id].value = 0
@@ -827,6 +858,7 @@ def test_skips_balancing_progression(self) -> None:
self.player1.regions[2], self.player2.prog_items[0])
def test_ignores_priority_locations(self) -> None:
+ """Test that progression items on priority locations don't get moved by balancing"""
self.multi_world.progression_balancing[self.player1.id].value = 50
self.multi_world.progression_balancing[self.player2.id].value = 50
diff --git a/test/general/TestHelpers.py b/test/general/test_helpers.py
similarity index 90%
rename from test/general/TestHelpers.py
rename to test/general/test_helpers.py
index 17fdce653c8c..83b56b34386b 100644
--- a/test/general/TestHelpers.py
+++ b/test/general/test_helpers.py
@@ -1,8 +1,7 @@
-from argparse import Namespace
-from typing import Dict, Optional, Callable
-
-from BaseClasses import MultiWorld, CollectionState, Region
import unittest
+from typing import Callable, Dict, Optional
+
+from BaseClasses import CollectionState, MultiWorld, Region
class TestHelpers(unittest.TestCase):
@@ -15,7 +14,8 @@ def setUp(self) -> None:
self.multiworld.player_name = {1: "Tester"}
self.multiworld.set_seed()
- def testRegionHelpers(self) -> None:
+ def test_region_helpers(self) -> None:
+ """Tests `Region.add_locations()` and `Region.add_exits()` have correct behavior"""
regions: Dict[str, str] = {
"TestRegion1": "I'm an apple",
"TestRegion2": "I'm a banana",
@@ -79,4 +79,5 @@ def testRegionHelpers(self) -> None:
current_region.add_exits(reg_exit_set[region])
exit_names = {_exit.name for _exit in current_region.exits}
for reg_exit in reg_exit_set[region]:
- self.assertTrue(f"{region} -> {reg_exit}" in exit_names, f"{region} -> {reg_exit} not in {exit_names}")
+ self.assertTrue(f"{region} -> {reg_exit}" in exit_names,
+ f"{region} -> {reg_exit} not in {exit_names}")
diff --git a/test/general/TestHostYAML.py b/test/general/test_host_yaml.py
similarity index 78%
rename from test/general/TestHostYAML.py
rename to test/general/test_host_yaml.py
index f5fd406cac84..79285d3a633a 100644
--- a/test/general/TestHostYAML.py
+++ b/test/general/test_host_yaml.py
@@ -15,14 +15,16 @@ def setUpClass(cls) -> None:
cls.yaml_options = Utils.parse_yaml(f.read())
def test_utils_in_yaml(self) -> None:
- for option_key, option_set in Utils.get_default_options().items():
+ """Tests that the auto generated host.yaml has default settings in it"""
+ for option_key, option_set in Settings(None).items():
with self.subTest(option_key):
self.assertIn(option_key, self.yaml_options)
for sub_option_key in option_set:
self.assertIn(sub_option_key, self.yaml_options[option_key])
def test_yaml_in_utils(self) -> None:
- utils_options = Utils.get_default_options()
+ """Tests that the auto generated host.yaml shows up in reference calls"""
+ utils_options = Settings(None)
for option_key, option_set in self.yaml_options.items():
with self.subTest(option_key):
self.assertIn(option_key, utils_options)
diff --git a/test/general/TestIDs.py b/test/general/test_ids.py
similarity index 82%
rename from test/general/TestIDs.py
rename to test/general/test_ids.py
index db1c9461b91a..4edfb8d994ef 100644
--- a/test/general/TestIDs.py
+++ b/test/general/test_ids.py
@@ -3,35 +3,37 @@
class TestIDs(unittest.TestCase):
- def testUniqueItems(self):
+ def test_unique_items(self):
+ """Tests that every game has a unique ID per item in the datapackage"""
known_item_ids = set()
for gamename, world_type in AutoWorldRegister.world_types.items():
current = len(known_item_ids)
known_item_ids |= set(world_type.item_id_to_name)
self.assertEqual(len(known_item_ids) - len(world_type.item_id_to_name), current)
- def testUniqueLocations(self):
+ def test_unique_locations(self):
+ """Tests that every game has a unique ID per location in the datapackage"""
known_location_ids = set()
for gamename, world_type in AutoWorldRegister.world_types.items():
current = len(known_location_ids)
known_location_ids |= set(world_type.location_id_to_name)
self.assertEqual(len(known_location_ids) - len(world_type.location_id_to_name), current)
- def testRangeItems(self):
+ def test_range_items(self):
"""There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
for item_id in world_type.item_id_to_name:
self.assertLess(item_id, 2**53)
- def testRangeLocations(self):
+ def test_range_locations(self):
"""There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
for location_id in world_type.location_id_to_name:
self.assertLess(location_id, 2**53)
- def testReservedItems(self):
+ def test_reserved_items(self):
"""negative item IDs are reserved to the special "Archipelago" world."""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
@@ -42,7 +44,7 @@ def testReservedItems(self):
for item_id in world_type.item_id_to_name:
self.assertGreater(item_id, 0)
- def testReservedLocations(self):
+ def test_reserved_locations(self):
"""negative location IDs are reserved to the special "Archipelago" world."""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
@@ -53,12 +55,14 @@ def testReservedLocations(self):
for location_id in world_type.location_id_to_name:
self.assertGreater(location_id, 0)
- def testDuplicateItemIDs(self):
+ def test_duplicate_item_ids(self):
+ """Test that a game doesn't have item id overlap within its own datapackage"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id))
- def testDuplicateLocationIDs(self):
+ def test_duplicate_location_ids(self):
+ """Test that a game doesn't have location id overlap within its own datapackage"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))
diff --git a/test/general/TestImplemented.py b/test/general/test_implemented.py
similarity index 57%
rename from test/general/TestImplemented.py
rename to test/general/test_implemented.py
index 22c546eff18b..624be710185d 100644
--- a/test/general/TestImplemented.py
+++ b/test/general/test_implemented.py
@@ -1,11 +1,13 @@
import unittest
-from worlds.AutoWorld import AutoWorldRegister
+from Fill import distribute_items_restrictive
+from NetUtils import encode
+from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_solo_multiworld
class TestImplemented(unittest.TestCase):
- def testCompletionCondition(self):
+ def test_completion_condition(self):
"""Ensure a completion condition is set that has requirements."""
for game_name, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden and game_name not in {"Sudoku"}:
@@ -13,7 +15,7 @@ def testCompletionCondition(self):
multiworld = setup_solo_multiworld(world_type)
self.assertFalse(multiworld.completion_condition[1](multiworld.state))
- def testEntranceParents(self):
+ def test_entrance_parents(self):
"""Tests that the parents of created Entrances match the exiting Region."""
for game_name, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden:
@@ -23,7 +25,7 @@ def testEntranceParents(self):
for exit in region.exits:
self.assertEqual(exit.parent_region, region)
- def testStageMethods(self):
+ def test_stage_methods(self):
"""Tests that worlds don't try to implement certain steps that are only ever called as stage."""
for game_name, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden:
@@ -31,3 +33,17 @@ def testStageMethods(self):
for method in ("assert_generate",):
self.assertFalse(hasattr(world_type, method),
f"{method} must be implemented as a @classmethod named stage_{method}.")
+
+ def test_slot_data(self):
+ """Tests that if a world creates slot data, it's json serializable."""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ # has an await for generate_output which isn't being called
+ if game_name in {"Ocarina of Time", "Zillion"}:
+ continue
+ multiworld = setup_solo_multiworld(world_type)
+ with self.subTest(game=game_name, seed=multiworld.seed):
+ distribute_items_restrictive(multiworld)
+ call_all(multiworld, "post_fill")
+ for key, data in multiworld.worlds[1].fill_slot_data().items():
+ self.assertIsInstance(key, str, "keys in slot data must be a string")
+ self.assertIsInstance(encode(data), str, f"object {type(data).__name__} not serializable.")
diff --git a/test/general/TestItems.py b/test/general/test_items.py
similarity index 88%
rename from test/general/TestItems.py
rename to test/general/test_items.py
index 95eb8d28d9af..464d246e1fa3 100644
--- a/test/general/TestItems.py
+++ b/test/general/test_items.py
@@ -4,7 +4,8 @@
class TestBase(unittest.TestCase):
- def testCreateItem(self):
+ def test_create_item(self):
+ """Test that a world can successfully create all items in its datapackage"""
for game_name, world_type in AutoWorldRegister.world_types.items():
proxy_world = world_type(None, 0) # this is identical to MultiServer.py creating worlds
for item_name in world_type.item_name_to_id:
@@ -12,7 +13,7 @@ def testCreateItem(self):
item = proxy_world.create_item(item_name)
self.assertEqual(item.name, item_name)
- def testItemNameGroupHasValidItem(self):
+ def test_item_name_group_has_valid_item(self):
"""Test that all item name groups contain valid items. """
# This cannot test for Event names that you may have declared for logic, only sendable Items.
# In such a case, you can add your entries to this Exclusion dict. Game Name -> Group Names
@@ -33,7 +34,7 @@ def testItemNameGroupHasValidItem(self):
for item in items:
self.assertIn(item, world_type.item_name_to_id)
- def testItemNameGroupConflict(self):
+ def test_item_name_group_conflict(self):
"""Test that all item name groups aren't also item names."""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game_name, game_name=game_name):
@@ -41,7 +42,8 @@ def testItemNameGroupConflict(self):
with self.subTest(group_name, group_name=group_name):
self.assertNotIn(group_name, world_type.item_name_to_id)
- def testItemCountGreaterEqualLocations(self):
+ def test_item_count_greater_equal_locations(self):
+ """Test that by the pre_fill step under default settings, each game submits items >= locations"""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type)
diff --git a/test/general/TestLocations.py b/test/general/test_locations.py
similarity index 93%
rename from test/general/TestLocations.py
rename to test/general/test_locations.py
index e77e7a6332bb..63b3b0f3640a 100644
--- a/test/general/TestLocations.py
+++ b/test/general/test_locations.py
@@ -5,7 +5,7 @@
class TestBase(unittest.TestCase):
- def testCreateDuplicateLocations(self):
+ def test_create_duplicate_locations(self):
"""Tests that no two Locations share a name or ID."""
for game_name, world_type in AutoWorldRegister.world_types.items():
multiworld = setup_solo_multiworld(world_type)
@@ -20,7 +20,7 @@ def testCreateDuplicateLocations(self):
self.assertLessEqual(locations.most_common(1)[0][1], 1,
f"{world_type.game} has duplicate of location ID {locations.most_common(1)}")
- def testLocationsInDatapackage(self):
+ def test_locations_in_datapackage(self):
"""Tests that created locations not filled before fill starts exist in the datapackage."""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
@@ -30,13 +30,12 @@ def testLocationsInDatapackage(self):
self.assertIn(location.name, world_type.location_name_to_id)
self.assertEqual(location.address, world_type.location_name_to_id[location.name])
- def testLocationCreationSteps(self):
+ def test_location_creation_steps(self):
"""Tests that Regions and Locations aren't created after `create_items`."""
gen_steps = ("generate_early", "create_regions", "create_items")
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
- multiworld._recache()
region_count = len(multiworld.get_regions())
location_count = len(multiworld.get_locations())
@@ -46,21 +45,19 @@ def testLocationCreationSteps(self):
self.assertEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during rule creation")
- multiworld._recache()
call_all(multiworld, "generate_basic")
self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during generate_basic")
self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during generate_basic")
- multiworld._recache()
call_all(multiworld, "pre_fill")
self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during pre_fill")
self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during pre_fill")
- def testLocationGroup(self):
+ def test_location_group(self):
"""Test that all location name groups contain valid locations and don't share names."""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game_name, game_name=game_name):
diff --git a/test/general/test_memory.py b/test/general/test_memory.py
new file mode 100644
index 000000000000..e352b9e8751a
--- /dev/null
+++ b/test/general/test_memory.py
@@ -0,0 +1,16 @@
+import unittest
+
+from worlds.AutoWorld import AutoWorldRegister
+from . import setup_solo_multiworld
+
+
+class TestWorldMemory(unittest.TestCase):
+ def test_leak(self):
+ """Tests that worlds don't leak references to MultiWorld or themselves with default options."""
+ import gc
+ import weakref
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest("Game", game_name=game_name):
+ weak = weakref.ref(setup_solo_multiworld(world_type))
+ gc.collect()
+ self.assertFalse(weak(), "World leaked a reference")
diff --git a/test/general/TestNames.py b/test/general/test_names.py
similarity index 92%
rename from test/general/TestNames.py
rename to test/general/test_names.py
index 6dae53240d10..7be76eed4ba9 100644
--- a/test/general/TestNames.py
+++ b/test/general/test_names.py
@@ -3,7 +3,7 @@
class TestNames(unittest.TestCase):
- def testItemNamesFormat(self):
+ def test_item_names_format(self):
"""Item names must not be all numeric in order to differentiate between ID and name in !hint"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
@@ -11,7 +11,7 @@ def testItemNamesFormat(self):
self.assertFalse(item_name.isnumeric(),
f"Item name \"{item_name}\" is invalid. It must not be numeric.")
- def testLocationNameFormat(self):
+ def test_location_name_format(self):
"""Location names must not be all numeric in order to differentiate between ID and name in !hint_location"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
diff --git a/test/general/TestOptions.py b/test/general/test_options.py
similarity index 78%
rename from test/general/TestOptions.py
rename to test/general/test_options.py
index 4a3bd0b02a0a..e1136f93c96f 100644
--- a/test/general/TestOptions.py
+++ b/test/general/test_options.py
@@ -3,7 +3,8 @@
class TestOptions(unittest.TestCase):
- def testOptionsHaveDocString(self):
+ def test_options_have_doc_string(self):
+ """Test that submitted options have their own specified docstring"""
for gamename, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden:
for option_key, option in world_type.options_dataclass.type_hints.items():
diff --git a/test/general/TestReachability.py b/test/general/test_reachability.py
similarity index 91%
rename from test/general/TestReachability.py
rename to test/general/test_reachability.py
index dd786b8352f5..828912ee35a3 100644
--- a/test/general/TestReachability.py
+++ b/test/general/test_reachability.py
@@ -31,7 +31,8 @@ class TestBase(unittest.TestCase):
}
}
- def testDefaultAllStateCanReachEverything(self):
+ def test_default_all_state_can_reach_everything(self):
+ """Ensure all state can reach everything and complete the game with the defined options"""
for game_name, world_type in AutoWorldRegister.world_types.items():
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
with self.subTest("Game", game=game_name):
@@ -54,7 +55,8 @@ def testDefaultAllStateCanReachEverything(self):
with self.subTest("Completion Condition"):
self.assertTrue(world.can_beat_game(state))
- def testDefaultEmptyStateCanReachSomething(self):
+ def test_default_empty_state_can_reach_something(self):
+ """Ensure empty state can reach at least one location with the defined options"""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game=game_name):
world = setup_solo_multiworld(world_type)
diff --git a/test/netutils/TestLocationStore.py b/test/netutils/test_location_store.py
similarity index 100%
rename from test/netutils/TestLocationStore.py
rename to test/netutils/test_location_store.py
diff --git a/test/programs/data/OnePlayer/test.yaml b/test/programs/data/one_player/test.yaml
similarity index 100%
rename from test/programs/data/OnePlayer/test.yaml
rename to test/programs/data/one_player/test.yaml
diff --git a/test/programs/TestGenerate.py b/test/programs/test_generate.py
similarity index 98%
rename from test/programs/TestGenerate.py
rename to test/programs/test_generate.py
index 73e1d3b8348c..887a417ec9f9 100644
--- a/test/programs/TestGenerate.py
+++ b/test/programs/test_generate.py
@@ -16,7 +16,7 @@ class TestGenerateMain(unittest.TestCase):
generate_dir = Path(Generate.__file__).parent
run_dir = generate_dir / "test" # reproducible cwd that's neither __file__ nor Generate.__file__
- abs_input_dir = Path(__file__).parent / 'data' / 'OnePlayer'
+ abs_input_dir = Path(__file__).parent / 'data' / 'one_player'
rel_input_dir = abs_input_dir.relative_to(run_dir) # directly supplied relative paths are relative to cwd
yaml_input_dir = abs_input_dir.relative_to(generate_dir) # yaml paths are relative to user_path
diff --git a/test/programs/TestMultiServer.py b/test/programs/test_multi_server.py
similarity index 100%
rename from test/programs/TestMultiServer.py
rename to test/programs/test_multi_server.py
diff --git a/test/utils/test_caches.py b/test/utils/test_caches.py
new file mode 100644
index 000000000000..fc681611f0cf
--- /dev/null
+++ b/test/utils/test_caches.py
@@ -0,0 +1,66 @@
+# Tests for caches in Utils.py
+
+import unittest
+from typing import Any
+
+from Utils import cache_argsless, cache_self1
+
+
+class TestCacheArgless(unittest.TestCase):
+ def test_cache(self) -> None:
+ @cache_argsless
+ def func_argless() -> object:
+ return object()
+
+ self.assertTrue(func_argless() is func_argless())
+
+ if __debug__: # assert only available with __debug__
+ def test_invalid_decorator(self) -> None:
+ with self.assertRaises(Exception):
+ @cache_argsless # type: ignore[arg-type]
+ def func_with_arg(_: Any) -> None:
+ pass
+
+
+class TestCacheSelf1(unittest.TestCase):
+ def test_cache(self) -> None:
+ class Cls:
+ @cache_self1
+ def func(self, _: Any) -> object:
+ return object()
+
+ o1 = Cls()
+ o2 = Cls()
+ self.assertTrue(o1.func(1) is o1.func(1))
+ self.assertFalse(o1.func(1) is o1.func(2))
+ self.assertFalse(o1.func(1) is o2.func(1))
+
+ def test_gc(self) -> None:
+ # verify that we don't keep a global reference
+ import gc
+ import weakref
+
+ class Cls:
+ @cache_self1
+ def func(self, _: Any) -> object:
+ return object()
+
+ o = Cls()
+ _ = o.func(o) # keep a hard ref to the result
+ r = weakref.ref(o) # keep weak ref to the cache
+ del o # remove hard ref to the cache
+ gc.collect()
+ self.assertFalse(r()) # weak ref should be dead now
+
+ if __debug__: # assert only available with __debug__
+ def test_no_self(self) -> None:
+ with self.assertRaises(Exception):
+ @cache_self1 # type: ignore[arg-type]
+ def func() -> Any:
+ pass
+
+ def test_too_many_args(self) -> None:
+ with self.assertRaises(Exception):
+ @cache_self1 # type: ignore[arg-type]
+ def func(_1: Any, _2: Any, _3: Any) -> Any:
+ pass
diff --git a/test/utils/TestSIPrefix.py b/test/utils/test_si_prefix.py
similarity index 100%
rename from test/utils/TestSIPrefix.py
rename to test/utils/test_si_prefix.py
diff --git a/test/webhost/TestAPIGenerate.py b/test/webhost/test_api_generate.py
similarity index 93%
rename from test/webhost/TestAPIGenerate.py
rename to test/webhost/test_api_generate.py
index 8ea78f27f93a..b8bdcb38c764 100644
--- a/test/webhost/TestAPIGenerate.py
+++ b/test/webhost/test_api_generate.py
@@ -19,11 +19,11 @@ def setUpClass(cls) -> None:
cls.client = app.test_client()
- def testCorrectErrorEmptyRequest(self):
+ def test_correct_error_empty_request(self):
response = self.client.post("/api/generate")
self.assertIn("No options found. Expected file attachment or json weights.", response.text)
- def testGenerationQueued(self):
+ def test_generation_queued(self):
options = {
"Tester1":
{
diff --git a/test/webhost/TestDocs.py b/test/webhost/test_docs.py
similarity index 96%
rename from test/webhost/TestDocs.py
rename to test/webhost/test_docs.py
index f6ede1543e26..68aba05f9dcc 100644
--- a/test/webhost/TestDocs.py
+++ b/test/webhost/test_docs.py
@@ -11,7 +11,7 @@ class TestDocs(unittest.TestCase):
def setUpClass(cls) -> None:
cls.tutorials_data = WebHost.create_ordered_tutorials_file()
- def testHasTutorial(self):
+ def test_has_tutorial(self):
games_with_tutorial = set(entry["gameTitle"] for entry in self.tutorials_data)
for game_name, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden:
@@ -27,7 +27,7 @@ def testHasTutorial(self):
self.fail(f"{game_name} has no setup tutorial. "
f"Games with Tutorial: {games_with_tutorial}")
- def testHasGameInfo(self):
+ def test_has_game_info(self):
for game_name, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden:
target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", game_name)
diff --git a/test/webhost/TestFileGeneration.py b/test/webhost/test_file_generation.py
similarity index 96%
rename from test/webhost/TestFileGeneration.py
rename to test/webhost/test_file_generation.py
index f01b70e14f90..059f6b49a1fd 100644
--- a/test/webhost/TestFileGeneration.py
+++ b/test/webhost/test_file_generation.py
@@ -13,7 +13,7 @@ def setUpClass(cls) -> None:
# should not create the folder *here*
cls.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
- def testOptions(self):
+ def test_options(self):
from WebHostLib.options import create as create_options_files
create_options_files()
target = os.path.join(self.correct_path, "static", "generated", "configs")
@@ -30,7 +30,7 @@ def testOptions(self):
for value in roll_options({file.name: f.read()})[0].values():
self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.")
- def testTutorial(self):
+ def test_tutorial(self):
WebHost.create_ordered_tutorials_file()
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json")))
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "tutorials.json")))
diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py
index 9a8b6a56ef36..d05797cf9e12 100644
--- a/worlds/AutoWorld.py
+++ b/worlds/AutoWorld.py
@@ -4,6 +4,7 @@
import logging
import pathlib
import sys
+import time
from dataclasses import make_dataclass
from typing import Any, Callable, ClassVar, Dict, Set, Tuple, FrozenSet, List, Optional, TYPE_CHECKING, TextIO, Type, \
Union
@@ -17,6 +18,8 @@
from . import GamesPackage
from settings import Group
+perf_logger = logging.getLogger("performance")
+
class AutoWorldRegister(type):
world_types: Dict[str, Type[World]] = {}
@@ -103,10 +106,24 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut
return new_class
+def _timed_call(method: Callable[..., Any], *args: Any,
+ multiworld: Optional["MultiWorld"] = None, player: Optional[int] = None) -> Any:
+ start = time.perf_counter()
+ ret = method(*args)
+ taken = time.perf_counter() - start
+ if taken > 1.0:
+ if player and multiworld:
+ perf_logger.info(f"Took {taken} seconds in {method.__qualname__} for player {player}, "
+ f"named {multiworld.player_name[player]}.")
+ else:
+ perf_logger.info(f"Took {taken} seconds in {method.__qualname__}.")
+ return ret
+
+
def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
method = getattr(multiworld.worlds[player], method_name)
try:
- ret = method(*args)
+ ret = _timed_call(method, *args, multiworld=multiworld, player=player)
except Exception as e:
message = f"Exception in {method} for player {player}, named {multiworld.player_name[player]}."
if sys.version_info >= (3, 11, 0):
@@ -132,24 +149,21 @@ def call_all(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
f"Duplicate item reference of \"{item.name}\" in \"{multiworld.worlds[player].game}\" "
f"of player \"{multiworld.player_name[player]}\". Please make a copy instead.")
- for world_type in sorted(world_types, key=lambda world: world.__name__):
- stage_callable = getattr(world_type, f"stage_{method_name}", None)
- if stage_callable:
- stage_callable(multiworld, *args)
+ call_stage(multiworld, method_name, *args)
def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
world_types = {multiworld.worlds[player].__class__ for player in multiworld.player_ids}
- for world_type in world_types:
+ for world_type in sorted(world_types, key=lambda world: world.__name__):
stage_callable = getattr(world_type, f"stage_{method_name}", None)
if stage_callable:
- stage_callable(multiworld, *args)
+ _timed_call(stage_callable, multiworld, *args)
class WebWorld:
"""Webhost integration"""
- settings_page: Union[bool, str] = True
+ options_page: Union[bool, str] = True
"""display a settings page. Can be a link to a specific page or external tool."""
game_info_languages: List[str] = ['en']
@@ -400,16 +414,16 @@ def get_pre_fill_items(self) -> List["Item"]:
def collect(self, state: "CollectionState", item: "Item") -> bool:
name = self.collect_item(state, item)
if name:
- state.prog_items[name, self.player] += 1
+ state.prog_items[self.player][name] += 1
return True
return False
def remove(self, state: "CollectionState", item: "Item") -> bool:
name = self.collect_item(state, item, True)
if name:
- state.prog_items[name, self.player] -= 1
- if state.prog_items[name, self.player] < 1:
- del (state.prog_items[name, self.player])
+ state.prog_items[self.player][name] -= 1
+ if state.prog_items[self.player][name] < 1:
+ del (state.prog_items[self.player][name])
return True
return False
diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py
index 2d445a77b8e0..c3ae2b0495b0 100644
--- a/worlds/LauncherComponents.py
+++ b/worlds/LauncherComponents.py
@@ -89,9 +89,6 @@ def launch_textclient():
Component('SNI Client', 'SNIClient',
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3',
'.apsmw', '.apl2ac')),
- # BizHawk
- Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT,
- file_identifier=SuffixIdentifier()),
Component('Links Awakening DX Client', 'LinksAwakeningClient',
file_identifier=SuffixIdentifier('.apladx')),
Component('LttP Adjuster', 'LttPAdjuster'),
diff --git a/worlds/__init__.py b/worlds/__init__.py
index c6208fa9a159..40e0b20f1974 100644
--- a/worlds/__init__.py
+++ b/worlds/__init__.py
@@ -5,19 +5,20 @@
import warnings
import zipimport
-folder = os.path.dirname(__file__)
+from Utils import user_path, local_path
-__all__ = {
+local_folder = os.path.dirname(__file__)
+user_folder = user_path("worlds") if user_path() != local_path() else None
+
+__all__ = (
"lookup_any_item_id_to_name",
"lookup_any_location_id_to_name",
"network_data_package",
"AutoWorldRegister",
"world_sources",
- "folder",
-}
-
-if typing.TYPE_CHECKING:
- from .AutoWorld import World
+ "local_folder",
+ "user_folder",
+)
class GamesData(typing.TypedDict):
@@ -41,13 +42,13 @@ class WorldSource(typing.NamedTuple):
is_zip: bool = False
relative: bool = True # relative to regular world import folder
- def __repr__(self):
+ def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip}, relative={self.relative})"
@property
def resolved_path(self) -> str:
if self.relative:
- return os.path.join(folder, self.path)
+ return os.path.join(local_folder, self.path)
return self.path
def load(self) -> bool:
@@ -56,6 +57,7 @@ def load(self) -> bool:
importer = zipimport.zipimporter(self.resolved_path)
if hasattr(importer, "find_spec"): # new in Python 3.10
spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0])
+ assert spec, f"{self.path} is not a loadable module"
mod = importlib.util.module_from_spec(spec)
else: # TODO: remove with 3.8 support
mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0])
@@ -72,7 +74,7 @@ def load(self) -> bool:
importlib.import_module(f".{self.path}", "worlds")
return True
- except Exception as e:
+ except Exception:
# A single world failing can still mean enough is working for the user, log and carry on
import traceback
import io
@@ -87,14 +89,16 @@ def load(self) -> bool:
# find potential world containers, currently folders and zip-importable .apworld's
world_sources: typing.List[WorldSource] = []
-file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly
-for file in os.scandir(folder):
- # prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "."
- if not file.name.startswith(("_", ".")):
- if file.is_dir():
- world_sources.append(WorldSource(file.name))
- elif file.is_file() and file.name.endswith(".apworld"):
- world_sources.append(WorldSource(file.name, is_zip=True))
+for folder in (folder for folder in (user_folder, local_folder) if folder):
+ relative = folder == local_folder
+ for entry in os.scandir(folder):
+ # prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "."
+ if not entry.name.startswith(("_", ".")):
+ file_name = entry.name if relative else os.path.join(folder, entry.name)
+ if entry.is_dir():
+ world_sources.append(WorldSource(file_name, relative=relative))
+ elif entry.is_file() and entry.name.endswith(".apworld"):
+ world_sources.append(WorldSource(file_name, is_zip=True, relative=relative))
# import all submodules to trigger AutoWorldRegister
world_sources.sort()
@@ -105,7 +109,7 @@ def load(self) -> bool:
lookup_any_location_id_to_name = {}
games: typing.Dict[str, GamesPackage] = {}
-from .AutoWorld import AutoWorldRegister
+from .AutoWorld import AutoWorldRegister # noqa: E402
# Build the data package for each game.
for world_name, world in AutoWorldRegister.world_types.items():
diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py
index cdf227ec7bdc..340399083217 100644
--- a/worlds/_bizhawk/__init__.py
+++ b/worlds/_bizhawk/__init__.py
@@ -13,7 +13,6 @@
BIZHAWK_SOCKET_PORT = 43055
-EXPECTED_SCRIPT_VERSION = 1
class ConnectionStatus(enum.IntEnum):
@@ -22,15 +21,6 @@ class ConnectionStatus(enum.IntEnum):
CONNECTED = 3
-class BizHawkContext:
- streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]]
- connection_status: ConnectionStatus
-
- def __init__(self) -> None:
- self.streams = None
- self.connection_status = ConnectionStatus.NOT_CONNECTED
-
-
class NotConnectedError(Exception):
"""Raised when something tries to make a request to the connector script before a connection has been established"""
pass
@@ -51,6 +41,50 @@ class SyncError(Exception):
pass
+class BizHawkContext:
+ streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]]
+ connection_status: ConnectionStatus
+ _lock: asyncio.Lock
+
+ def __init__(self) -> None:
+ self.streams = None
+ self.connection_status = ConnectionStatus.NOT_CONNECTED
+ self._lock = asyncio.Lock()
+
+ async def _send_message(self, message: str):
+ async with self._lock:
+ if self.streams is None:
+ raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
+
+ try:
+ reader, writer = self.streams
+ writer.write(message.encode("utf-8") + b"\n")
+ await asyncio.wait_for(writer.drain(), timeout=5)
+
+ res = await asyncio.wait_for(reader.readline(), timeout=5)
+
+ if res == b"":
+ writer.close()
+ self.streams = None
+ self.connection_status = ConnectionStatus.NOT_CONNECTED
+ raise RequestFailedError("Connection closed")
+
+ if self.connection_status == ConnectionStatus.TENTATIVE:
+ self.connection_status = ConnectionStatus.CONNECTED
+
+ return res.decode("utf-8")
+ except asyncio.TimeoutError as exc:
+ writer.close()
+ self.streams = None
+ self.connection_status = ConnectionStatus.NOT_CONNECTED
+ raise RequestFailedError("Connection timed out") from exc
+ except ConnectionResetError as exc:
+ writer.close()
+ self.streams = None
+ self.connection_status = ConnectionStatus.NOT_CONNECTED
+ raise RequestFailedError("Connection reset") from exc
+
+
async def connect(ctx: BizHawkContext) -> bool:
"""Attempts to establish a connection with the connector script. Returns True if successful."""
try:
@@ -72,74 +106,14 @@ def disconnect(ctx: BizHawkContext) -> None:
async def get_script_version(ctx: BizHawkContext) -> int:
- if ctx.streams is None:
- raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
-
- try:
- reader, writer = ctx.streams
- writer.write("VERSION".encode("ascii") + b"\n")
- await asyncio.wait_for(writer.drain(), timeout=5)
-
- version = await asyncio.wait_for(reader.readline(), timeout=5)
-
- if version == b"":
- writer.close()
- ctx.streams = None
- ctx.connection_status = ConnectionStatus.NOT_CONNECTED
- raise RequestFailedError("Connection closed")
-
- return int(version.decode("ascii"))
- except asyncio.TimeoutError as exc:
- writer.close()
- ctx.streams = None
- ctx.connection_status = ConnectionStatus.NOT_CONNECTED
- raise RequestFailedError("Connection timed out") from exc
- except ConnectionResetError as exc:
- writer.close()
- ctx.streams = None
- ctx.connection_status = ConnectionStatus.NOT_CONNECTED
- raise RequestFailedError("Connection reset") from exc
+ return int(await ctx._send_message("VERSION"))
async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]:
"""Sends a list of requests to the BizHawk connector and returns their responses.
It's likely you want to use the wrapper functions instead of this."""
- if ctx.streams is None:
- raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
-
- try:
- reader, writer = ctx.streams
- writer.write(json.dumps(req_list).encode("utf-8") + b"\n")
- await asyncio.wait_for(writer.drain(), timeout=5)
-
- res = await asyncio.wait_for(reader.readline(), timeout=5)
-
- if res == b"":
- writer.close()
- ctx.streams = None
- ctx.connection_status = ConnectionStatus.NOT_CONNECTED
- raise RequestFailedError("Connection closed")
-
- if ctx.connection_status == ConnectionStatus.TENTATIVE:
- ctx.connection_status = ConnectionStatus.CONNECTED
-
- ret = json.loads(res.decode("utf-8"))
- for response in ret:
- if response["type"] == "ERROR":
- raise ConnectorError(response["err"])
-
- return ret
- except asyncio.TimeoutError as exc:
- writer.close()
- ctx.streams = None
- ctx.connection_status = ConnectionStatus.NOT_CONNECTED
- raise RequestFailedError("Connection timed out") from exc
- except ConnectionResetError as exc:
- writer.close()
- ctx.streams = None
- ctx.connection_status = ConnectionStatus.NOT_CONNECTED
- raise RequestFailedError("Connection reset") from exc
+ return json.loads(await ctx._send_message(json.dumps(req_list)))
async def ping(ctx: BizHawkContext) -> None:
diff --git a/worlds/_bizhawk/client.py b/worlds/_bizhawk/client.py
index b614c083ba4e..32a6e3704e1e 100644
--- a/worlds/_bizhawk/client.py
+++ b/worlds/_bizhawk/client.py
@@ -16,12 +16,22 @@
BizHawkClientContext = object
+def launch_client(*args) -> None:
+ from .context import launch
+ launch_subprocess(launch, name="BizHawkClient")
+
+component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
+ file_identifier=SuffixIdentifier())
+components.append(component)
+
+
class AutoBizHawkClientRegister(abc.ABCMeta):
game_handlers: ClassVar[Dict[Tuple[str, ...], Dict[str, BizHawkClient]]] = {}
def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> AutoBizHawkClientRegister:
new_class = super().__new__(cls, name, bases, namespace)
+ # Register handler
if "system" in namespace:
systems = (namespace["system"],) if type(namespace["system"]) is str else tuple(sorted(namespace["system"]))
if systems not in AutoBizHawkClientRegister.game_handlers:
@@ -30,6 +40,19 @@ def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any])
if "game" in namespace:
AutoBizHawkClientRegister.game_handlers[systems][namespace["game"]] = new_class()
+ # Update launcher component's suffixes
+ if "patch_suffix" in namespace:
+ if namespace["patch_suffix"] is not None:
+ existing_identifier: SuffixIdentifier = component.file_identifier
+ new_suffixes = [*existing_identifier.suffixes]
+
+ if type(namespace["patch_suffix"]) is str:
+ new_suffixes.append(namespace["patch_suffix"])
+ else:
+ new_suffixes.extend(namespace["patch_suffix"])
+
+ component.file_identifier = SuffixIdentifier(*new_suffixes)
+
return new_class
@staticmethod
@@ -45,11 +68,14 @@ async def get_handler(ctx: BizHawkClientContext, system: str) -> Optional[BizHaw
class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
system: ClassVar[Union[str, Tuple[str, ...]]]
- """The system that the game this client is for runs on"""
+ """The system(s) that the game this client is for runs on"""
game: ClassVar[str]
"""The game this client is for"""
+ patch_suffix: ClassVar[Optional[Union[str, Tuple[str, ...]]]]
+ """The file extension(s) this client is meant to open and patch (e.g. ".apz3")"""
+
@abc.abstractmethod
async def validate_rom(self, ctx: BizHawkClientContext) -> bool:
"""Should return whether the currently loaded ROM should be handled by this client. You might read the game name
@@ -75,13 +101,3 @@ async def game_watcher(self, ctx: BizHawkClientContext) -> None:
def on_package(self, ctx: BizHawkClientContext, cmd: str, args: dict) -> None:
"""For handling packages from the server. Called from `BizHawkClientContext.on_package`."""
pass
-
-
-def launch_client(*args) -> None:
- from .context import launch
- launch_subprocess(launch, name="BizHawkClient")
-
-
-if not any(component.script_name == "BizHawkClient" for component in components):
- components.append(Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
- file_identifier=SuffixIdentifier()))
diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py
index 6e53b370af1c..ccf747f15afe 100644
--- a/worlds/_bizhawk/context.py
+++ b/worlds/_bizhawk/context.py
@@ -5,6 +5,8 @@
import asyncio
+import enum
+import subprocess
import traceback
from typing import Any, Dict, Optional
@@ -12,14 +14,21 @@
import Patch
import Utils
-from . import BizHawkContext, ConnectionStatus, RequestFailedError, connect, disconnect, get_hash, get_script_version, \
- get_system, ping
+from . import BizHawkContext, ConnectionStatus, NotConnectedError, RequestFailedError, connect, disconnect, get_hash, \
+ get_script_version, get_system, ping
from .client import BizHawkClient, AutoBizHawkClientRegister
EXPECTED_SCRIPT_VERSION = 1
+class AuthStatus(enum.IntEnum):
+ NOT_AUTHENTICATED = 0
+ NEED_INFO = 1
+ PENDING = 2
+ AUTHENTICATED = 3
+
+
class BizHawkClientCommandProcessor(ClientCommandProcessor):
def _cmd_bh(self):
"""Shows the current status of the client's connection to BizHawk"""
@@ -34,6 +43,8 @@ def _cmd_bh(self):
class BizHawkClientContext(CommonContext):
command_processor = BizHawkClientCommandProcessor
+ auth_status: AuthStatus
+ password_requested: bool
client_handler: Optional[BizHawkClient]
slot_data: Optional[Dict[str, Any]] = None
rom_hash: Optional[str] = None
@@ -44,6 +55,8 @@ class BizHawkClientContext(CommonContext):
def __init__(self, server_address: Optional[str], password: Optional[str]):
super().__init__(server_address, password)
+ self.auth_status = AuthStatus.NOT_AUTHENTICATED
+ self.password_requested = False
self.client_handler = None
self.bizhawk_ctx = BizHawkContext()
self.watcher_timeout = 0.5
@@ -60,10 +73,41 @@ class BizHawkManager(GameManager):
def on_package(self, cmd, args):
if cmd == "Connected":
self.slot_data = args.get("slot_data", None)
+ self.auth_status = AuthStatus.AUTHENTICATED
if self.client_handler is not None:
self.client_handler.on_package(self, cmd, args)
+ async def server_auth(self, password_requested: bool = False):
+ self.password_requested = password_requested
+
+ if self.bizhawk_ctx.connection_status != ConnectionStatus.CONNECTED:
+ logger.info("Awaiting connection to BizHawk before authenticating")
+ return
+
+ if self.client_handler is None:
+ return
+
+ # Ask handler to set auth
+ if self.auth is None:
+ self.auth_status = AuthStatus.NEED_INFO
+ await self.client_handler.set_auth(self)
+
+ # Handler didn't set auth, ask user for slot name
+ if self.auth is None:
+ await self.get_username()
+
+ if password_requested and not self.password:
+ self.auth_status = AuthStatus.NEED_INFO
+ await super(BizHawkClientContext, self).server_auth(password_requested)
+
+ await self.send_connect()
+ self.auth_status = AuthStatus.PENDING
+
+ async def disconnect(self, allow_autoreconnect: bool = False):
+ self.auth_status = AuthStatus.NOT_AUTHENTICATED
+ await super().disconnect(allow_autoreconnect)
+
async def _game_watcher(ctx: BizHawkClientContext):
showed_connecting_message = False
@@ -108,12 +152,13 @@ async def _game_watcher(ctx: BizHawkClientContext):
rom_hash = await get_hash(ctx.bizhawk_ctx)
if ctx.rom_hash is not None and ctx.rom_hash != rom_hash:
- if ctx.server is not None:
+ if ctx.server is not None and not ctx.server.socket.closed:
logger.info(f"ROM changed. Disconnecting from server.")
- await ctx.disconnect(True)
ctx.auth = None
ctx.username = None
+ ctx.client_handler = None
+ await ctx.disconnect(False)
ctx.rom_hash = rom_hash
if ctx.client_handler is None:
@@ -132,22 +177,39 @@ async def _game_watcher(ctx: BizHawkClientContext):
except RequestFailedError as exc:
logger.info(f"Lost connection to BizHawk: {exc.args[0]}")
continue
+ except NotConnectedError:
+ continue
- # Get slot name and send `Connect`
- if ctx.server is not None and ctx.username is None:
- await ctx.client_handler.set_auth(ctx)
-
- if ctx.auth is None:
- await ctx.get_username()
-
- await ctx.send_connect()
+ # Server auth
+ if ctx.server is not None and not ctx.server.socket.closed:
+ if ctx.auth_status == AuthStatus.NOT_AUTHENTICATED:
+ Utils.async_start(ctx.server_auth(ctx.password_requested))
+ else:
+ ctx.auth_status = AuthStatus.NOT_AUTHENTICATED
+ # Call the handler's game watcher
await ctx.client_handler.game_watcher(ctx)
async def _run_game(rom: str):
- import webbrowser
- webbrowser.open(rom)
+ import os
+ auto_start = Utils.get_settings().bizhawkclient_options.rom_start
+
+ if auto_start is True:
+ emuhawk_path = Utils.get_settings().bizhawkclient_options.emuhawk_path
+ subprocess.Popen([emuhawk_path, "--lua=data/lua/connector_bizhawk_generic.lua", os.path.realpath(rom)],
+ cwd=Utils.local_path("."),
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
+ elif isinstance(auto_start, str):
+ import shlex
+
+ subprocess.Popen([*shlex.split(auto_start), os.path.realpath(rom)],
+ cwd=Utils.local_path("."),
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
async def _patch_and_run_game(patch_file: str):
diff --git a/worlds/adventure/Rom.py b/worlds/adventure/Rom.py
index 62c401971895..9f1ca3fe5eba 100644
--- a/worlds/adventure/Rom.py
+++ b/worlds/adventure/Rom.py
@@ -6,9 +6,8 @@
import Utils
from .Locations import AdventureLocation, LocationData
-from Utils import OptionsType
+from settings import get_settings
from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer
-from itertools import chain
import bsdiff4
@@ -313,9 +312,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
def get_base_rom_path(file_name: str = "") -> str:
- options: OptionsType = Utils.get_options()
if not file_name:
- file_name = options["adventure_options"]["rom_file"]
+ file_name = get_settings()["adventure_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.user_path(file_name)
return file_name
diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py
index 22ef2a39a81a..edc68473b93f 100644
--- a/worlds/alttp/Client.py
+++ b/worlds/alttp/Client.py
@@ -520,7 +520,8 @@ async def game_watcher(self, ctx):
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
currently_dead = gamemode[0] in DEATH_MODES
- await ctx.handle_deathlink_state(currently_dead)
+ await ctx.handle_deathlink_state(currently_dead,
+ ctx.player_names[ctx.slot] + " ran out of hearts." if ctx.slot else "")
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py
index 630d61e01959..a68acf7288cf 100644
--- a/worlds/alttp/Dungeons.py
+++ b/worlds/alttp/Dungeons.py
@@ -264,7 +264,8 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
if loc in all_state_base.events:
all_state_base.events.remove(loc)
- fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True)
+ fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True,
+ name="LttP Dungeon Items")
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py
index ffa23881d3d9..f89eebec3339 100644
--- a/worlds/alttp/InvertedRegions.py
+++ b/worlds/alttp/InvertedRegions.py
@@ -477,8 +477,6 @@ def create_inverted_regions(world, player):
create_lw_region(world, player, 'Death Mountain Bunny Descent Area')
]
- world.initialize_regions()
-
def mark_dark_world_regions(world, player):
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py
index f8fdd55ef657..88a2d899fc60 100644
--- a/worlds/alttp/ItemPool.py
+++ b/worlds/alttp/ItemPool.py
@@ -293,7 +293,6 @@ def generate_itempool(world):
loc.access_rule = lambda state: has_triforce_pieces(state, player)
region.locations.append(loc)
- multiworld.clear_location_cache()
multiworld.push_item(loc, ItemFactory('Triforce', player), False)
loc.event = True
@@ -535,8 +534,6 @@ def set_up_take_anys(world, player):
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0, create_location=True)
- world.initialize_regions()
-
def get_pool_core(world, player: int):
shuffle = world.shuffle[player]
diff --git a/worlds/alttp/Items.py b/worlds/alttp/Items.py
index 40634de8daa3..18f96b2ddb81 100644
--- a/worlds/alttp/Items.py
+++ b/worlds/alttp/Items.py
@@ -102,7 +102,7 @@ def as_init_dict(self) -> typing.Dict[str, typing.Any]:
'Red Pendant': ItemData(IC.progression, 'Crystal', (0x01, 0x32, 0x60, 0x00, 0x69, 0x03), None, None, None, None, None, None, "the red pendant"),
'Triforce': ItemData(IC.progression, None, 0x6A, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'),
'Power Star': ItemData(IC.progression, None, 0x6B, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'),
- 'Triforce Piece': ItemData(IC.progression, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'),
+ 'Triforce Piece': ItemData(IC.progression_skip_balancing, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'),
'Crystal 1': ItemData(IC.progression, 'Crystal', (0x02, 0x34, 0x64, 0x40, 0x7F, 0x06), None, None, None, None, None, None, "a blue crystal"),
'Crystal 2': ItemData(IC.progression, 'Crystal', (0x10, 0x34, 0x64, 0x40, 0x79, 0x06), None, None, None, None, None, None, "a blue crystal"),
'Crystal 3': ItemData(IC.progression, 'Crystal', (0x40, 0x34, 0x64, 0x40, 0x6C, 0x06), None, None, None, None, None, None, "a blue crystal"),
diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py
index 8311bc32694e..0cc8a3d6a71f 100644
--- a/worlds/alttp/Regions.py
+++ b/worlds/alttp/Regions.py
@@ -382,8 +382,6 @@ def create_regions(world, player):
create_dw_region(world, player, 'Dark Death Mountain Bunny Descent Area')
]
- world.initialize_regions()
-
def create_lw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
return _create_region(world, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits)
diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py
index 47cea8c20ea4..e1ae0cc6e6c3 100644
--- a/worlds/alttp/Rom.py
+++ b/worlds/alttp/Rom.py
@@ -786,8 +786,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# patch items
- for location in world.get_locations():
- if location.player != player or location.address is None or location.shop_slot is not None:
+ for location in world.get_locations(player):
+ if location.address is None or location.shop_slot is not None:
continue
itemid = location.item.code if location.item is not None else 0x5A
@@ -2247,7 +2247,7 @@ def hint_text(dest, ped_hint=False):
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
hint_locations = HintLocations.copy()
local_random.shuffle(hint_locations)
- all_entrances = [entrance for entrance in world.get_entrances() if entrance.player == player]
+ all_entrances = list(world.get_entrances(player))
local_random.shuffle(all_entrances)
# First we take care of the one inconvenient dungeon in the appropriately simple shuffles.
diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py
index 1fddecd8f4f4..469f4f82eefd 100644
--- a/worlds/alttp/Rules.py
+++ b/worlds/alttp/Rules.py
@@ -197,8 +197,13 @@ def global_rules(world, player):
# determines which S&Q locations are available - hide from paths since it isn't an in-game location
for exit in world.get_region('Menu', player).exits:
exit.hide_path = True
-
- set_rule(world.get_entrance('Old Man S&Q', player), lambda state: state.can_reach('Old Man', 'Location', player))
+ try:
+ old_man_sq = world.get_entrance('Old Man S&Q', player)
+ except KeyError:
+ pass # it doesn't exist, should be dungeon-only unittests
+ else:
+ old_man = world.get_location("Old Man", player)
+ set_rule(old_man_sq, lambda state: old_man.can_reach(state))
set_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player))
set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player))
@@ -1526,16 +1531,16 @@ def options_to_access_rule(options):
# Helper functions to determine if the moon pearl is required
if inverted:
def is_bunny(region):
- return region.is_light_world
+ return region and region.is_light_world
def is_link(region):
- return region.is_dark_world
+ return region and region.is_dark_world
else:
def is_bunny(region):
- return region.is_dark_world
+ return region and region.is_dark_world
def is_link(region):
- return region.is_light_world
+ return region and region.is_light_world
def get_rule_to_add(region, location = None, connecting_entrance = None):
# In OWG, a location can potentially be superbunny-mirror accessible or
@@ -1603,21 +1608,20 @@ def get_rule_to_add(region, location = None, connecting_entrance = None):
return options_to_access_rule(possible_options)
# Add requirements for bunny-impassible caves if link is a bunny in them
- for region in [world.get_region(name, player) for name in bunny_impassable_caves]:
-
+ for region in (world.get_region(name, player) for name in bunny_impassable_caves):
if not is_bunny(region):
continue
rule = get_rule_to_add(region)
- for exit in region.exits:
- add_rule(exit, rule)
+ for region_exit in region.exits:
+ add_rule(region_exit, rule)
paradox_shop = world.get_region('Light World Death Mountain Shop', player)
if is_bunny(paradox_shop):
add_rule(paradox_shop.entrances[0], get_rule_to_add(paradox_shop))
# Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival
- for entrance in world.get_entrances():
- if entrance.player == player and is_bunny(entrance.connected_region):
+ for entrance in world.get_entrances(player):
+ if is_bunny(entrance.connected_region):
if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] :
if entrance.connected_region.type == LTTPRegionType.Dungeon:
if entrance.parent_region.type != LTTPRegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py
index f17eb1eadbca..c0f2e2236e69 100644
--- a/worlds/alttp/Shops.py
+++ b/worlds/alttp/Shops.py
@@ -348,7 +348,6 @@ def create_shops(world, player: int):
loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)
loc.shop_slot_disabled = True
shop.region.locations.append(loc)
- world.clear_location_cache()
class ShopData(NamedTuple):
@@ -619,6 +618,4 @@ def create_dynamic_shop_locations(world, player):
if shop.type == ShopType.TakeAny:
loc.shop_slot_disabled = True
shop.region.locations.append(loc)
- world.clear_location_cache()
-
loc.shop_slot = i
diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py
index 4b6bc54111e6..a6aefc74129a 100644
--- a/worlds/alttp/UnderworldGlitchRules.py
+++ b/worlds/alttp/UnderworldGlitchRules.py
@@ -31,7 +31,7 @@ def fake_pearl_state(state, player):
if state.has('Moon Pearl', player):
return state
fake_state = state.copy()
- fake_state.prog_items['Moon Pearl', player] += 1
+ fake_state.prog_items[player]['Moon Pearl'] += 1
return fake_state
diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py
index 65e36da3bd6a..d89e65c59d89 100644
--- a/worlds/alttp/__init__.py
+++ b/worlds/alttp/__init__.py
@@ -470,7 +470,8 @@ def pre_fill(self):
prizepool = unplaced_prizes.copy()
prize_locs = empty_crystal_locations.copy()
world.random.shuffle(prize_locs)
- fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True)
+ fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True,
+ name="LttP Dungeon Prizes")
except FillError as e:
lttp_logger.exception("Failed to place dungeon prizes (%s). Will retry %s more times", e,
attempts - attempt)
@@ -585,27 +586,26 @@ def stage_modify_multidata(cls, multiworld, multidata: dict):
for player in checks_in_area:
checks_in_area[player]["Total"] = 0
-
- for location in multiworld.get_locations():
- if location.game == cls.game and type(location.address) is int:
- main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
- if location.parent_region.dungeon:
- dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
- 'Inverted Ganons Tower': 'Ganons Tower'} \
- .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
- checks_in_area[location.player][dungeonname].append(location.address)
- elif location.parent_region.type == LTTPRegionType.LightWorld:
- checks_in_area[location.player]["Light World"].append(location.address)
- elif location.parent_region.type == LTTPRegionType.DarkWorld:
- checks_in_area[location.player]["Dark World"].append(location.address)
- elif main_entrance.parent_region.type == LTTPRegionType.LightWorld:
- checks_in_area[location.player]["Light World"].append(location.address)
- elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld:
- checks_in_area[location.player]["Dark World"].append(location.address)
- else:
- assert False, "Unknown Location area."
- # TODO: remove Total as it's duplicated data and breaks consistent typing
- checks_in_area[location.player]["Total"] += 1
+ for location in multiworld.get_locations(player):
+ if location.game == cls.game and type(location.address) is int:
+ main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
+ if location.parent_region.dungeon:
+ dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
+ 'Inverted Ganons Tower': 'Ganons Tower'} \
+ .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
+ checks_in_area[location.player][dungeonname].append(location.address)
+ elif location.parent_region.type == LTTPRegionType.LightWorld:
+ checks_in_area[location.player]["Light World"].append(location.address)
+ elif location.parent_region.type == LTTPRegionType.DarkWorld:
+ checks_in_area[location.player]["Dark World"].append(location.address)
+ elif main_entrance.parent_region.type == LTTPRegionType.LightWorld:
+ checks_in_area[location.player]["Light World"].append(location.address)
+ elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld:
+ checks_in_area[location.player]["Dark World"].append(location.address)
+ else:
+ assert False, "Unknown Location area."
+ # TODO: remove Total as it's duplicated data and breaks consistent typing
+ checks_in_area[location.player]["Total"] += 1
multidata["checks_in_area"].update(checks_in_area)
@@ -830,4 +830,4 @@ def _lttp_has_key(self, item, player, count: int = 1):
return True
if self.multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
return can_buy_unlimited(self, 'Small Key (Universal)', player)
- return self.prog_items[item, player] >= count
+ return self.prog_items[player][item] >= count
diff --git a/worlds/alttp/docs/multiworld_de.md b/worlds/alttp/docs/multiworld_de.md
index 38009fb58ed3..8ccd1a87a6b7 100644
--- a/worlds/alttp/docs/multiworld_de.md
+++ b/worlds/alttp/docs/multiworld_de.md
@@ -67,7 +67,7 @@ Wenn du eine Option nicht gewählt haben möchtest, setze ihren Wert einfach auf
### Überprüfung deiner YAML-Datei
-Wenn man sichergehen will, ob die YAML-Datei funktioniert, kann man dies bei der [YAML Validator](/mysterycheck) Seite
+Wenn man sichergehen will, ob die YAML-Datei funktioniert, kann man dies bei der [YAML Validator](/check) Seite
tun.
## ein Einzelspielerspiel erstellen
diff --git a/worlds/alttp/docs/multiworld_es.md b/worlds/alttp/docs/multiworld_es.md
index 8576318bb997..37aeda2a63e5 100644
--- a/worlds/alttp/docs/multiworld_es.md
+++ b/worlds/alttp/docs/multiworld_es.md
@@ -82,7 +82,7 @@ debe tener al menos un valor mayor que cero, si no la generación fallará.
### Verificando tu archivo YAML
Si quieres validar que tu fichero YAML para asegurarte que funciona correctamente, puedes hacerlo en la pagina
-[YAML Validator](/mysterycheck).
+[YAML Validator](/check).
## Generar una partida para un jugador
diff --git a/worlds/alttp/docs/multiworld_fr.md b/worlds/alttp/docs/multiworld_fr.md
index 329ca6537573..078a270f08b9 100644
--- a/worlds/alttp/docs/multiworld_fr.md
+++ b/worlds/alttp/docs/multiworld_fr.md
@@ -83,7 +83,7 @@ chaque paramètre il faut au moins une option qui soit paramétrée sur un nombr
### Vérifier son fichier YAML
Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du
-[Validateur de YAML](/mysterycheck).
+[Validateur de YAML](/check).
## Générer une partie pour un joueur
diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py
index 94c30c349398..8ca2791dcfe4 100644
--- a/worlds/alttp/test/dungeons/TestDungeon.py
+++ b/worlds/alttp/test/dungeons/TestDungeon.py
@@ -1,5 +1,5 @@
from BaseClasses import CollectionState, ItemClassification
-from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
+from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import mandatory_connections, connect_simple
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory
diff --git a/worlds/archipidle/Rules.py b/worlds/archipidle/Rules.py
index cdd48e760445..3bf4bad475e1 100644
--- a/worlds/archipidle/Rules.py
+++ b/worlds/archipidle/Rules.py
@@ -5,12 +5,7 @@
class ArchipIDLELogic(LogicMixin):
def _archipidle_location_is_accessible(self, player_id, items_required):
- items_received = 0
- for item in self.prog_items:
- if item[1] == player_id:
- items_received += 1
-
- return items_received >= items_required
+ return sum(self.prog_items[player_id].values()) >= items_required
def set_rules(world: MultiWorld, player: int):
diff --git a/worlds/bk_sudoku/__init__.py b/worlds/bk_sudoku/__init__.py
index f914baf066aa..36d863bb4475 100644
--- a/worlds/bk_sudoku/__init__.py
+++ b/worlds/bk_sudoku/__init__.py
@@ -5,7 +5,7 @@
class Bk_SudokuWebWorld(WebWorld):
- settings_page = "games/Sudoku/info/en"
+ options_page = "games/Sudoku/info/en"
theme = 'partyTime'
tutorials = [
Tutorial(
diff --git a/worlds/blasphemous/Options.py b/worlds/blasphemous/Options.py
index ea304d22ed66..127a1dc77669 100644
--- a/worlds/blasphemous/Options.py
+++ b/worlds/blasphemous/Options.py
@@ -67,6 +67,7 @@ class StartingLocation(ChoiceIsRandom):
class Ending(Choice):
"""Choose which ending is required to complete the game.
+ Talking to Tirso in Albero will tell you the selected ending for the current game.
Ending A: Collect all thorn upgrades.
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation."""
display_name = "Ending"
diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py
index 4218fa94cf64..5d8829213163 100644
--- a/worlds/blasphemous/Rules.py
+++ b/worlds/blasphemous/Rules.py
@@ -578,11 +578,12 @@ def rules(blasphemousworld):
or state.has("Purified Hand of the Nun", player)
or state.has("D01Z02S03[NW]", player)
and (
- can_cross_gap(state, logic, player, 1)
+ can_cross_gap(state, logic, player, 2)
or state.has("Lorquiana", player)
or aubade(state, player)
or state.has("Cantina of the Blue Rose", player)
or charge_beam(state, player)
+ or state.has("Ranged Skill", player)
)
))
set_rule(world.get_location("Albero: Lvdovico's 1st reward", player),
@@ -702,10 +703,11 @@ def rules(blasphemousworld):
# Items
set_rule(world.get_location("WotBC: Cliffside Child of Moonlight", player),
lambda state: (
- can_cross_gap(state, logic, player, 1)
+ can_cross_gap(state, logic, player, 2)
or aubade(state, player)
or charge_beam(state, player)
- or state.has_any({"Lorquiana", "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", "Cloistered Ruby"}, player)
+ or state.has_any({"Lorquiana", "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", \
+ "Cloistered Ruby", "Ranged Skill"}, player)
or precise_skips_allowed(logic)
))
# Doors
diff --git a/worlds/blasphemous/docs/en_Blasphemous.md b/worlds/blasphemous/docs/en_Blasphemous.md
index 15223213ac67..1ff7f5a9035c 100644
--- a/worlds/blasphemous/docs/en_Blasphemous.md
+++ b/worlds/blasphemous/docs/en_Blasphemous.md
@@ -19,6 +19,7 @@ In addition, there are other changes to the game that make it better optimized f
- The Apodictic Heart of Mea Culpa can be unequipped.
- Dying with the Immaculate Bead is unnecessary, it is automatically upgraded to the Weight of True Guilt.
- If the option is enabled, the 34 corpses in game will have their messages changed to give hints about certain items and locations. The Shroud of Dreamt Sins is not required to hear them.
+- Talking to Tirso in Albero will tell you the selected ending for the current game.
## What has been changed about the side quests?
diff --git a/worlds/bumpstik/Regions.py b/worlds/bumpstik/Regions.py
index 247d6d61a34b..6cddde882a08 100644
--- a/worlds/bumpstik/Regions.py
+++ b/worlds/bumpstik/Regions.py
@@ -23,13 +23,13 @@ def create_regions(world: MultiWorld, player: int):
entrance_map = {
"Level 1": lambda state:
- state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 9),
+ state.has("Booster Bumper", player, 1) and state.has("Treasure Bumper", player, 8),
"Level 2": lambda state:
- state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 17),
+ state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 16),
"Level 3": lambda state:
- state.has("Booster Bumper", player, 4) and state.has("Treasure Bumper", player, 25),
+ state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 24),
"Level 4": lambda state:
- state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 33)
+ state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 32)
}
for x, region_name in enumerate(region_map):
diff --git a/worlds/bumpstik/__init__.py b/worlds/bumpstik/__init__.py
index 9eeb3325e38f..c4e65d07b6a9 100644
--- a/worlds/bumpstik/__init__.py
+++ b/worlds/bumpstik/__init__.py
@@ -108,7 +108,7 @@ def create_items(self):
item_pool += self._create_item_in_quantities(
name, frequencies[i])
- item_delta = len(location_table) - len(item_pool) - 1
+ item_delta = len(location_table) - len(item_pool)
if item_delta > 0:
item_pool += self._create_item_in_quantities(
"Score Bonus", item_delta)
@@ -116,13 +116,16 @@ def create_items(self):
self.multiworld.itempool += item_pool
def set_rules(self):
- forbid_item(self.multiworld.get_location("Bonus Booster 5", self.player),
- "Booster Bumper", self.player)
-
- def generate_basic(self):
- self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).place_locked_item(
- self.create_item(self.get_filler_item_name()))
-
+ for x in range(1, 32):
+ self.multiworld.get_location(f"Treasure Bumper {x + 1}", self.player).access_rule = \
+ lambda state, x = x: state.has("Treasure Bumper", self.player, x)
+ for x in range(1, 5):
+ self.multiworld.get_location(f"Bonus Booster {x + 1}", self.player).access_rule = \
+ lambda state, x = x: state.has("Booster Bumper", self.player, x)
+ self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).access_rule = \
+ lambda state: state.has("Hazard Bumper", self.player, 25)
+
self.multiworld.completion_condition[self.player] = \
lambda state: state.has("Booster Bumper", self.player, 5) and \
state.has("Treasure Bumper", self.player, 32)
+
diff --git a/worlds/bumpstik/test/TestLogic.py b/worlds/bumpstik/test/TestLogic.py
new file mode 100644
index 000000000000..e374b7b1e999
--- /dev/null
+++ b/worlds/bumpstik/test/TestLogic.py
@@ -0,0 +1,39 @@
+from . import BumpStikTestBase
+
+
+class TestRuleLogic(BumpStikTestBase):
+ def testLogic(self):
+ for x in range(1, 33):
+ if x == 32:
+ self.assertFalse(self.can_reach_location("Level 5 - Cleared all Hazards"))
+
+ self.collect(self.get_item_by_name("Treasure Bumper"))
+ if x % 8 == 0:
+ bb_count = round(x / 8)
+
+ if bb_count < 4:
+ self.assertFalse(self.can_reach_location(f"Treasure Bumper {x + 1}"))
+ elif bb_count == 4:
+ bb_count += 1
+
+ for y in range(self.count("Booster Bumper"), bb_count):
+ self.assertTrue(self.can_reach_location(f"Bonus Booster {y + 1}"),
+ f"BB {y + 1} check not reachable with {self.count('Booster Bumper')} BBs")
+ if y < 4:
+ self.assertFalse(self.can_reach_location(f"Bonus Booster {y + 2}"),
+ f"BB {y + 2} check reachable with {self.count('Treasure Bumper')} TBs")
+ self.collect(self.get_item_by_name("Booster Bumper"))
+
+ if x < 31:
+ self.assertFalse(self.can_reach_location(f"Treasure Bumper {x + 2}"))
+ elif x == 31:
+ self.assertFalse(self.can_reach_location("Level 5 - 50,000+ Total Points"))
+
+ if x < 32:
+ self.assertTrue(self.can_reach_location(f"Treasure Bumper {x + 1}"),
+ f"TB {x + 1} check not reachable with {self.count('Treasure Bumper')} TBs")
+ elif x == 32:
+ self.assertTrue(self.can_reach_location("Level 5 - 50,000+ Total Points"))
+ self.assertFalse(self.can_reach_location("Level 5 - Cleared all Hazards"))
+ self.collect(self.get_items_by_name("Hazard Bumper"))
+ self.assertTrue(self.can_reach_location("Level 5 - Cleared all Hazards"))
diff --git a/worlds/bumpstik/test/__init__.py b/worlds/bumpstik/test/__init__.py
new file mode 100644
index 000000000000..1199d7b8e506
--- /dev/null
+++ b/worlds/bumpstik/test/__init__.py
@@ -0,0 +1,5 @@
+from test.TestBase import WorldTestBase
+
+
+class BumpStikTestBase(WorldTestBase):
+ game = "Bumper Stickers"
diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py
index feff1486514a..621e8f5c37b2 100644
--- a/worlds/checksfinder/__init__.py
+++ b/worlds/checksfinder/__init__.py
@@ -14,8 +14,8 @@ class ChecksFinderWeb(WebWorld):
"A guide to setting up the Archipelago ChecksFinder software on your computer. This guide covers "
"single-player, multiworld, and related software.",
"English",
- "checksfinder_en.md",
- "checksfinder/en",
+ "setup_en.md",
+ "setup/en",
["Mewlif"]
)]
@@ -69,8 +69,8 @@ def set_rules(self):
def create_regions(self):
menu = Region("Menu", self.player, self.multiworld)
board = Region("Board", self.player, self.multiworld)
- board.locations = [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board)
- for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name]
+ board.locations += [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board)
+ for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name]
connection = Entrance(self.player, "New Board", menu)
menu.exits.append(connection)
diff --git a/worlds/checksfinder/docs/en_ChecksFinder.md b/worlds/checksfinder/docs/en_ChecksFinder.md
index bd82660b09ba..96fb0529df64 100644
--- a/worlds/checksfinder/docs/en_ChecksFinder.md
+++ b/worlds/checksfinder/docs/en_ChecksFinder.md
@@ -14,11 +14,18 @@ many checks as you have gained items, plus five to start with being available.
## When the player receives an item, what happens?
When the player receives an item in ChecksFinder, it either can make the future boards they play be bigger in width or
-height, or add a new bomb to the future boards, with a limit to having up to one fifth of the _current_ board being
-bombs. The items you have gained _before_ the current board was made will be said at the bottom of the screen as a number
+height, or add a new bomb to the future boards, with a limit to having up to one fifth of the _current_ board being
+bombs. The items you have gained _before_ the current board was made will be said at the bottom of the screen as a
+number
next to an icon, the number is how many you have gotten and the icon represents which item it is.
## What is the victory condition?
Victory is achieved when the player wins a board they were given after they have received all of their Map Width, Map
-Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received.
\ No newline at end of file
+Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received.
+
+## Unique Local Commands
+
+The following command is only available when using the ChecksFinderClient to play with Archipelago.
+
+- `/resync` Manually trigger a resync.
diff --git a/worlds/checksfinder/docs/checksfinder_en.md b/worlds/checksfinder/docs/setup_en.md
similarity index 100%
rename from worlds/checksfinder/docs/checksfinder_en.md
rename to worlds/checksfinder/docs/setup_en.md
diff --git a/worlds/dkc3/docs/setup_en.md b/worlds/dkc3/docs/setup_en.md
index bb1075630016..9c4197286eb9 100644
--- a/worlds/dkc3/docs/setup_en.md
+++ b/worlds/dkc3/docs/setup_en.md
@@ -50,7 +50,7 @@ them. Player settings page: [Donkey Kong Country 3 Player Settings Page](/games/
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
-validator page: [YAML Validation page](/mysterycheck)
+validator page: [YAML Validation page](/check)
## Generating a Single-Player Game
diff --git a/worlds/dlcquest/Items.py b/worlds/dlcquest/Items.py
index 61d1be54cbd4..e7008f7b1284 100644
--- a/worlds/dlcquest/Items.py
+++ b/worlds/dlcquest/Items.py
@@ -11,6 +11,8 @@
class DLCQuestItem(Item):
game: str = "DLCQuest"
+ coins: int = 0
+ coin_suffix: str = ""
offset = 120_000
diff --git a/worlds/dlcquest/Regions.py b/worlds/dlcquest/Regions.py
index dfb5f6c021be..6dad9fc10c81 100644
--- a/worlds/dlcquest/Regions.py
+++ b/worlds/dlcquest/Regions.py
@@ -1,4 +1,5 @@
import math
+from typing import List
from BaseClasses import Entrance, MultiWorld, Region
from . import Options
@@ -9,318 +10,181 @@
"Double Jump Behind the Tree", "The Forest", "Final Room"]
-def add_coin_freemium(region: Region, Coin: int, player: int):
- number_coin = f"{Coin} coins freemium"
- location_coin = f"{region.name} coins freemium"
- location = DLCQuestLocation(player, location_coin, None, region)
- region.locations.append(location)
- location.place_locked_item(create_event(player, number_coin))
+def add_coin_lfod(region: Region, coin: int, player: int):
+ add_coin(region, coin, player, " coins freemium")
+
+def add_coin_dlcquest(region: Region, coin: int, player: int):
+ add_coin(region, coin, player, " coins")
-def add_coin_dlcquest(region: Region, Coin: int, player: int):
- number_coin = f"{Coin} coins"
- location_coin = f"{region.name} coins"
+
+def add_coin(region: Region, coin: int, player: int, suffix: str):
+ number_coin = f"{coin}{suffix}"
+ location_coin = f"{region.name}{suffix}"
location = DLCQuestLocation(player, location_coin, None, region)
region.locations.append(location)
- location.place_locked_item(create_event(player, number_coin))
-
-
-def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQuestOptions):
- Regmenu = Region("Menu", player, world)
- if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign
- == Options.Campaign.option_both):
- Regmenu.exits += [Entrance(player, "DLC Quest Basic", Regmenu)]
- if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign
- == Options.Campaign.option_both):
- Regmenu.exits += [Entrance(player, "Live Freemium or Die", Regmenu)]
- world.regions.append(Regmenu)
-
- if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign
- == Options.Campaign.option_both):
-
- Regmoveright = Region("Move Right", player, world, "Start of the basic game")
- Locmoveright_name = ["Movement Pack", "Animation Pack", "Audio Pack", "Pause Menu Pack"]
- Regmoveright.exits = [Entrance(player, "Moving", Regmoveright)]
- Regmoveright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmoveright) for
- loc_name in Locmoveright_name]
- add_coin_dlcquest(Regmoveright, 4, player)
- if World_Options.coinsanity == Options.CoinSanity.option_coin:
- coin_bundle_needed = math.floor(825 / World_Options.coinbundlequantity)
- for i in range(coin_bundle_needed):
- item_coin = f"DLC Quest: {World_Options.coinbundlequantity * (i + 1)} Coin"
- Regmoveright.locations += [
- DLCQuestLocation(player, item_coin, location_table[item_coin], Regmoveright)]
- if 825 % World_Options.coinbundlequantity != 0:
- Regmoveright.locations += [
- DLCQuestLocation(player, "DLC Quest: 825 Coin", location_table["DLC Quest: 825 Coin"],
- Regmoveright)]
- world.regions.append(Regmoveright)
-
- Regmovpack = Region("Movement Pack", player, world)
- Locmovpack_name = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack",
- "Shepherd Sheep"]
- if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
- Locmovpack_name += ["Sword"]
- Regmovpack.exits = [Entrance(player, "Tree", Regmovpack), Entrance(player, "Cloud", Regmovpack)]
- Regmovpack.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmovpack) for loc_name
- in Locmovpack_name]
- add_coin_dlcquest(Regmovpack, 46, player)
- world.regions.append(Regmovpack)
-
- Regbtree = Region("Behind Tree", player, world)
- Locbtree_name = ["Double Jump Pack", "Map Pack", "Between Trees Sheep", "Hole in the Wall Sheep"]
- if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
- Locbtree_name += ["Gun"]
- Regbtree.exits = [Entrance(player, "Behind Tree Double Jump", Regbtree),
- Entrance(player, "Forest Entrance", Regbtree)]
- Regbtree.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbtree) for loc_name in
- Locbtree_name]
- add_coin_dlcquest(Regbtree, 60, player)
- world.regions.append(Regbtree)
-
- Regpsywarfare = Region("Psychological Warfare", player, world)
- Locpsywarfare_name = ["West Cave Sheep"]
- Regpsywarfare.exits = [Entrance(player, "Cloud Double Jump", Regpsywarfare)]
- Regpsywarfare.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regpsywarfare) for
- loc_name in Locpsywarfare_name]
- add_coin_dlcquest(Regpsywarfare, 100, player)
- world.regions.append(Regpsywarfare)
-
- Regdoubleleft = Region("Double Jump Total Left", player, world)
- Locdoubleleft_name = ["Pet Pack", "Top Hat Pack", "North West Alcove Sheep"]
- Regdoubleleft.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleft) for
- loc_name in
- Locdoubleleft_name]
- Regdoubleleft.exits = [Entrance(player, "Cave Tree", Regdoubleleft),
- Entrance(player, "Cave Roof", Regdoubleleft)]
- add_coin_dlcquest(Regdoubleleft, 50, player)
- world.regions.append(Regdoubleleft)
-
- Regdoubleleftcave = Region("Double Jump Total Left Cave", player, world)
- Locdoubleleftcave_name = ["Top Hat Sheep"]
- Regdoubleleftcave.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleftcave)
- for loc_name in Locdoubleleftcave_name]
- add_coin_dlcquest(Regdoubleleftcave, 9, player)
- world.regions.append(Regdoubleleftcave)
-
- Regdoubleleftroof = Region("Double Jump Total Left Roof", player, world)
- Locdoubleleftroof_name = ["North West Ceiling Sheep"]
- Regdoubleleftroof.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleftroof)
- for loc_name in Locdoubleleftroof_name]
- add_coin_dlcquest(Regdoubleleftroof, 10, player)
- world.regions.append(Regdoubleleftroof)
-
- Regdoubletree = Region("Double Jump Behind Tree", player, world)
- Locdoubletree_name = ["Sexy Outfits Pack", "Double Jump Alcove Sheep", "Sexy Outfits Sheep"]
- Regdoubletree.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubletree) for
- loc_name in
- Locdoubletree_name]
- Regdoubletree.exits = [Entrance(player, "True Double Jump", Regdoubletree)]
- add_coin_dlcquest(Regdoubletree, 89, player)
- world.regions.append(Regdoubletree)
-
- Regtruedoublejump = Region("True Double Jump Behind Tree", player, world)
- Loctruedoublejump_name = ["Double Jump Floating Sheep", "Cutscene Sheep"]
- Regtruedoublejump.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regtruedoublejump)
- for loc_name in Loctruedoublejump_name]
- add_coin_dlcquest(Regtruedoublejump, 7, player)
- world.regions.append(Regtruedoublejump)
-
- Regforest = Region("The Forest", player, world)
- Locforest_name = ["Gun Pack", "Night Map Pack"]
- Regforest.exits = [Entrance(player, "Behind Ogre", Regforest),
- Entrance(player, "Forest Double Jump", Regforest)]
- Regforest.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regforest) for loc_name in
- Locforest_name]
- add_coin_dlcquest(Regforest, 171, player)
- world.regions.append(Regforest)
-
- Regforestdoublejump = Region("The Forest whit double Jump", player, world)
- Locforestdoublejump_name = ["The Zombie Pack", "Forest Low Sheep"]
- Regforestdoublejump.exits = [Entrance(player, "Forest True Double Jump", Regforestdoublejump)]
- Regforestdoublejump.locations += [
- DLCQuestLocation(player, loc_name, location_table[loc_name], Regforestdoublejump) for loc_name in
- Locforestdoublejump_name]
- add_coin_dlcquest(Regforestdoublejump, 76, player)
- world.regions.append(Regforestdoublejump)
-
- Regforesttruedoublejump = Region("The Forest whit double Jump Part 2", player, world)
- Locforesttruedoublejump_name = ["Forest High Sheep"]
- Regforesttruedoublejump.locations += [
- DLCQuestLocation(player, loc_name, location_table[loc_name], Regforesttruedoublejump)
- for loc_name in Locforesttruedoublejump_name]
- add_coin_dlcquest(Regforesttruedoublejump, 203, player)
- world.regions.append(Regforesttruedoublejump)
-
- Regfinalroom = Region("The Final Boss Room", player, world)
- Locfinalroom_name = ["Finish the Fight Pack"]
- Regfinalroom.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfinalroom) for
- loc_name in
- Locfinalroom_name]
- world.regions.append(Regfinalroom)
-
- loc_win = DLCQuestLocation(player, "Winning Basic", None, world.get_region("The Final Boss Room", player))
- world.get_region("The Final Boss Room", player).locations.append(loc_win)
- loc_win.place_locked_item(create_event(player, "Victory Basic"))
-
- world.get_entrance("DLC Quest Basic", player).connect(world.get_region("Move Right", player))
-
- world.get_entrance("Moving", player).connect(world.get_region("Movement Pack", player))
-
- world.get_entrance("Tree", player).connect(world.get_region("Behind Tree", player))
-
- world.get_entrance("Cloud", player).connect(world.get_region("Psychological Warfare", player))
-
- world.get_entrance("Cloud Double Jump", player).connect(world.get_region("Double Jump Total Left", player))
-
- world.get_entrance("Cave Tree", player).connect(world.get_region("Double Jump Total Left Cave", player))
-
- world.get_entrance("Cave Roof", player).connect(world.get_region("Double Jump Total Left Roof", player))
-
- world.get_entrance("Forest Entrance", player).connect(world.get_region("The Forest", player))
-
- world.get_entrance("Behind Tree Double Jump", player).connect(
- world.get_region("Double Jump Behind Tree", player))
-
- world.get_entrance("Behind Ogre", player).connect(world.get_region("The Final Boss Room", player))
-
- world.get_entrance("Forest Double Jump", player).connect(
- world.get_region("The Forest whit double Jump", player))
-
- world.get_entrance("Forest True Double Jump", player).connect(
- world.get_region("The Forest whit double Jump Part 2", player))
-
- world.get_entrance("True Double Jump", player).connect(world.get_region("True Double Jump Behind Tree", player))
-
- if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign
- == Options.Campaign.option_both):
-
- Regfreemiumstart = Region("Freemium Start", player, world)
- Locfreemiumstart_name = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack",
- "Nice Try", "Story is Important", "I Get That Reference!"]
- if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
- Locfreemiumstart_name += ["Wooden Sword"]
- Regfreemiumstart.exits = [Entrance(player, "Vines", Regfreemiumstart)]
- Regfreemiumstart.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfreemiumstart)
- for loc_name in
- Locfreemiumstart_name]
- add_coin_freemium(Regfreemiumstart, 50, player)
- if World_Options.coinsanity == Options.CoinSanity.option_coin:
- coin_bundle_needed = math.floor(889 / World_Options.coinbundlequantity)
- for i in range(coin_bundle_needed):
- item_coin_freemium = f"Live Freemium or Die: {World_Options.coinbundlequantity * (i + 1)} Coin"
- Regfreemiumstart.locations += [
- DLCQuestLocation(player, item_coin_freemium, location_table[item_coin_freemium],
- Regfreemiumstart)]
- if 889 % World_Options.coinbundlequantity != 0:
- Regfreemiumstart.locations += [
- DLCQuestLocation(player, "Live Freemium or Die: 889 Coin",
- location_table["Live Freemium or Die: 889 Coin"],
- Regfreemiumstart)]
- world.regions.append(Regfreemiumstart)
-
- Regbehindvine = Region("Behind the Vines", player, world)
- Locbehindvine_name = ["Wall Jump Pack", "Health Bar Pack", "Parallax Pack"]
- if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
- Locbehindvine_name += ["Pickaxe"]
- Regbehindvine.exits = [Entrance(player, "Wall Jump Entrance", Regbehindvine)]
- Regbehindvine.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbehindvine) for
- loc_name in Locbehindvine_name]
- add_coin_freemium(Regbehindvine, 95, player)
- world.regions.append(Regbehindvine)
-
- Regwalljump = Region("Wall Jump", player, world)
- Locwalljump_name = ["Harmless Plants Pack", "Death of Comedy Pack", "Canadian Dialog Pack", "DLC NPC Pack"]
- Regwalljump.exits = [Entrance(player, "Harmless Plants", Regwalljump),
- Entrance(player, "Pickaxe Hard Cave", Regwalljump)]
- Regwalljump.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regwalljump) for
- loc_name in Locwalljump_name]
- add_coin_freemium(Regwalljump, 150, player)
- world.regions.append(Regwalljump)
-
- Regfakeending = Region("Fake Ending", player, world)
- Locfakeending_name = ["Cut Content Pack", "Name Change Pack"]
- Regfakeending.exits = [Entrance(player, "Name Change Entrance", Regfakeending),
- Entrance(player, "Cut Content Entrance", Regfakeending)]
- Regfakeending.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfakeending) for
- loc_name in Locfakeending_name]
- world.regions.append(Regfakeending)
-
- Reghardcave = Region("Hard Cave", player, world)
- add_coin_freemium(Reghardcave, 20, player)
- Reghardcave.exits = [Entrance(player, "Hard Cave Wall Jump", Reghardcave)]
- world.regions.append(Reghardcave)
-
- Reghardcavewalljump = Region("Hard Cave Wall Jump", player, world)
- Lochardcavewalljump_name = ["Increased HP Pack"]
- Reghardcavewalljump.locations += [
- DLCQuestLocation(player, loc_name, location_table[loc_name], Reghardcavewalljump) for
- loc_name in Lochardcavewalljump_name]
- add_coin_freemium(Reghardcavewalljump, 130, player)
- world.regions.append(Reghardcavewalljump)
-
- Regcutcontent = Region("Cut Content", player, world)
- Loccutcontent_name = []
- if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
- Loccutcontent_name += ["Humble Indie Bindle"]
- Regcutcontent.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regcutcontent) for
- loc_name in Loccutcontent_name]
- add_coin_freemium(Regcutcontent, 200, player)
- world.regions.append(Regcutcontent)
-
- Regnamechange = Region("Name Change", player, world)
- Locnamechange_name = []
- if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
- Locnamechange_name += ["Box of Various Supplies"]
- Regnamechange.exits = [Entrance(player, "Behind Rocks", Regnamechange)]
- Regnamechange.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regnamechange) for
- loc_name in Locnamechange_name]
- world.regions.append(Regnamechange)
-
- Regtopright = Region("Top Right", player, world)
- Loctopright_name = ["Season Pass", "High Definition Next Gen Pack"]
- Regtopright.exits = [Entrance(player, "Blizzard", Regtopright)]
- Regtopright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regtopright) for
- loc_name in Loctopright_name]
- add_coin_freemium(Regtopright, 90, player)
- world.regions.append(Regtopright)
-
- Regseason = Region("Season", player, world)
- Locseason_name = ["Remove Ads Pack", "Not Exactly Noble"]
- Regseason.exits = [Entrance(player, "Boss Door", Regseason)]
- Regseason.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regseason) for
- loc_name in Locseason_name]
- add_coin_freemium(Regseason, 154, player)
- world.regions.append(Regseason)
-
- Regfinalboss = Region("Final Boss", player, world)
- Locfinalboss_name = ["Big Sword Pack", "Really Big Sword Pack", "Unfathomable Sword Pack"]
- Regfinalboss.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfinalboss) for
- loc_name in Locfinalboss_name]
- world.regions.append(Regfinalboss)
-
- loc_wining = DLCQuestLocation(player, "Winning Freemium", None, world.get_region("Final Boss", player))
- world.get_region("Final Boss", player).locations.append(loc_wining)
- loc_wining.place_locked_item(create_event(player, "Victory Freemium"))
-
- world.get_entrance("Live Freemium or Die", player).connect(world.get_region("Freemium Start", player))
-
- world.get_entrance("Vines", player).connect(world.get_region("Behind the Vines", player))
-
- world.get_entrance("Wall Jump Entrance", player).connect(world.get_region("Wall Jump", player))
-
- world.get_entrance("Harmless Plants", player).connect(world.get_region("Fake Ending", player))
-
- world.get_entrance("Pickaxe Hard Cave", player).connect(world.get_region("Hard Cave", player))
-
- world.get_entrance("Hard Cave Wall Jump", player).connect(world.get_region("Hard Cave Wall Jump", player))
-
- world.get_entrance("Name Change Entrance", player).connect(world.get_region("Name Change", player))
-
- world.get_entrance("Cut Content Entrance", player).connect(world.get_region("Cut Content", player))
-
- world.get_entrance("Behind Rocks", player).connect(world.get_region("Top Right", player))
-
- world.get_entrance("Blizzard", player).connect(world.get_region("Season", player))
-
- world.get_entrance("Boss Door", player).connect(world.get_region("Final Boss", player))
+ event = create_event(player, number_coin)
+ event.coins = coin
+ event.coin_suffix = suffix
+ location.place_locked_item(event)
+
+
+def create_regions(multiworld: MultiWorld, player: int, world_options: Options.DLCQuestOptions):
+ region_menu = Region("Menu", player, multiworld)
+ has_campaign_basic = world_options.campaign == Options.Campaign.option_basic or world_options.campaign == Options.Campaign.option_both
+ has_campaign_lfod = world_options.campaign == Options.Campaign.option_live_freemium_or_die or world_options.campaign == Options.Campaign.option_both
+ has_coinsanity = world_options.coinsanity == Options.CoinSanity.option_coin
+ coin_bundle_size = world_options.coinbundlequantity.value
+ has_item_shuffle = world_options.item_shuffle == Options.ItemShuffle.option_shuffled
+
+ multiworld.regions.append(region_menu)
+
+ create_regions_basic_campaign(has_campaign_basic, region_menu, has_item_shuffle, has_coinsanity, coin_bundle_size, player, multiworld)
+
+ create_regions_lfod_campaign(coin_bundle_size, has_campaign_lfod, has_coinsanity, has_item_shuffle, multiworld, player, region_menu)
+
+
+def create_regions_basic_campaign(has_campaign_basic: bool, region_menu: Region, has_item_shuffle: bool, has_coinsanity: bool,
+ coin_bundle_size: int, player: int, world: MultiWorld):
+ if not has_campaign_basic:
+ return
+
+ region_menu.exits += [Entrance(player, "DLC Quest Basic", region_menu)]
+ locations_move_right = ["Movement Pack", "Animation Pack", "Audio Pack", "Pause Menu Pack"]
+ region_move_right = create_region_and_locations_basic("Move Right", locations_move_right, ["Moving"], player, world, 4)
+ create_coinsanity_locations_dlc_quest(has_coinsanity, coin_bundle_size, player, region_move_right)
+ locations_movement_pack = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack", "Shepherd Sheep"]
+ locations_movement_pack += conditional_location(has_item_shuffle, "Sword")
+ create_region_and_locations_basic("Movement Pack", locations_movement_pack, ["Tree", "Cloud"], player, world, 46)
+ locations_behind_tree = ["Double Jump Pack", "Map Pack", "Between Trees Sheep", "Hole in the Wall Sheep"] + conditional_location(has_item_shuffle, "Gun")
+ create_region_and_locations_basic("Behind Tree", locations_behind_tree, ["Behind Tree Double Jump", "Forest Entrance"], player, world, 60)
+ create_region_and_locations_basic("Psychological Warfare", ["West Cave Sheep"], ["Cloud Double Jump"], player, world, 100)
+ locations_double_jump_left = ["Pet Pack", "Top Hat Pack", "North West Alcove Sheep"]
+ create_region_and_locations_basic("Double Jump Total Left", locations_double_jump_left, ["Cave Tree", "Cave Roof"], player, world, 50)
+ create_region_and_locations_basic("Double Jump Total Left Cave", ["Top Hat Sheep"], [], player, world, 9)
+ create_region_and_locations_basic("Double Jump Total Left Roof", ["North West Ceiling Sheep"], [], player, world, 10)
+ locations_double_jump_left_ceiling = ["Sexy Outfits Pack", "Double Jump Alcove Sheep", "Sexy Outfits Sheep"]
+ create_region_and_locations_basic("Double Jump Behind Tree", locations_double_jump_left_ceiling, ["True Double Jump"], player, world, 89)
+ create_region_and_locations_basic("True Double Jump Behind Tree", ["Double Jump Floating Sheep", "Cutscene Sheep"], [], player, world, 7)
+ create_region_and_locations_basic("The Forest", ["Gun Pack", "Night Map Pack"], ["Behind Ogre", "Forest Double Jump"], player, world, 171)
+ create_region_and_locations_basic("The Forest with double Jump", ["The Zombie Pack", "Forest Low Sheep"], ["Forest True Double Jump"], player, world, 76)
+ create_region_and_locations_basic("The Forest with double Jump Part 2", ["Forest High Sheep"], [], player, world, 203)
+ region_final_boss_room = create_region_and_locations_basic("The Final Boss Room", ["Finish the Fight Pack"], [], player, world)
+
+ create_victory_event(region_final_boss_room, "Winning Basic", "Victory Basic", player)
+
+ connect_entrances_basic(player, world)
+
+
+def create_regions_lfod_campaign(coin_bundle_size, has_campaign_lfod, has_coinsanity, has_item_shuffle, multiworld, player, region_menu):
+ if not has_campaign_lfod:
+ return
+
+ region_menu.exits += [Entrance(player, "Live Freemium or Die", region_menu)]
+ locations_lfod_start = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack",
+ "Nice Try", "Story is Important", "I Get That Reference!"] + conditional_location(has_item_shuffle, "Wooden Sword")
+ region_lfod_start = create_region_and_locations_lfod("Freemium Start", locations_lfod_start, ["Vines"], player, multiworld, 50)
+ create_coinsanity_locations_lfod(has_coinsanity, coin_bundle_size, player, region_lfod_start)
+ locations_behind_vines = ["Wall Jump Pack", "Health Bar Pack", "Parallax Pack"] + conditional_location(has_item_shuffle, "Pickaxe")
+ create_region_and_locations_lfod("Behind the Vines", locations_behind_vines, ["Wall Jump Entrance"], player, multiworld, 95)
+ locations_wall_jump = ["Harmless Plants Pack", "Death of Comedy Pack", "Canadian Dialog Pack", "DLC NPC Pack"]
+ create_region_and_locations_lfod("Wall Jump", locations_wall_jump, ["Harmless Plants", "Pickaxe Hard Cave"], player, multiworld, 150)
+ create_region_and_locations_lfod("Fake Ending", ["Cut Content Pack", "Name Change Pack"], ["Name Change Entrance", "Cut Content Entrance"], player,
+ multiworld)
+ create_region_and_locations_lfod("Hard Cave", [], ["Hard Cave Wall Jump"], player, multiworld, 20)
+ create_region_and_locations_lfod("Hard Cave Wall Jump", ["Increased HP Pack"], [], player, multiworld, 130)
+ create_region_and_locations_lfod("Cut Content", conditional_location(has_item_shuffle, "Humble Indie Bindle"), [], player, multiworld, 200)
+ create_region_and_locations_lfod("Name Change", conditional_location(has_item_shuffle, "Box of Various Supplies"), ["Behind Rocks"], player, multiworld)
+ create_region_and_locations_lfod("Top Right", ["Season Pass", "High Definition Next Gen Pack"], ["Blizzard"], player, multiworld, 90)
+ create_region_and_locations_lfod("Season", ["Remove Ads Pack", "Not Exactly Noble"], ["Boss Door"], player, multiworld, 154)
+ region_final_boss = create_region_and_locations_lfod("Final Boss", ["Big Sword Pack", "Really Big Sword Pack", "Unfathomable Sword Pack"], [], player, multiworld)
+
+ create_victory_event(region_final_boss, "Winning Freemium", "Victory Freemium", player)
+
+ connect_entrances_lfod(multiworld, player)
+
+
+def conditional_location(condition: bool, location: str) -> List[str]:
+ return conditional_locations(condition, [location])
+
+
+def conditional_locations(condition: bool, locations: List[str]) -> List[str]:
+ return locations if condition else []
+
+
+def create_region_and_locations_basic(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld,
+ number_coins: int = 0) -> Region:
+ return create_region_and_locations(region_name, locations, exits, player, multiworld, number_coins, 0)
+
+
+def create_region_and_locations_lfod(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld,
+ number_coins: int = 0) -> Region:
+ return create_region_and_locations(region_name, locations, exits, player, multiworld, 0, number_coins)
+
+
+def create_region_and_locations(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld,
+ number_coins_basic: int, number_coins_lfod: int) -> Region:
+ region = Region(region_name, player, multiworld)
+ region.exits = [Entrance(player, exit_name, region) for exit_name in exits]
+ region.locations += [DLCQuestLocation(player, name, location_table[name], region) for name in locations]
+ if number_coins_basic > 0:
+ add_coin_dlcquest(region, number_coins_basic, player)
+ if number_coins_lfod > 0:
+ add_coin_lfod(region, number_coins_lfod, player)
+ multiworld.regions.append(region)
+ return region
+
+
+def create_victory_event(region_victory: Region, event_name: str, item_name: str, player: int):
+ location_victory = DLCQuestLocation(player, event_name, None, region_victory)
+ region_victory.locations.append(location_victory)
+ location_victory.place_locked_item(create_event(player, item_name))
+
+
+def connect_entrances_basic(player, world):
+ world.get_entrance("DLC Quest Basic", player).connect(world.get_region("Move Right", player))
+ world.get_entrance("Moving", player).connect(world.get_region("Movement Pack", player))
+ world.get_entrance("Tree", player).connect(world.get_region("Behind Tree", player))
+ world.get_entrance("Cloud", player).connect(world.get_region("Psychological Warfare", player))
+ world.get_entrance("Cloud Double Jump", player).connect(world.get_region("Double Jump Total Left", player))
+ world.get_entrance("Cave Tree", player).connect(world.get_region("Double Jump Total Left Cave", player))
+ world.get_entrance("Cave Roof", player).connect(world.get_region("Double Jump Total Left Roof", player))
+ world.get_entrance("Forest Entrance", player).connect(world.get_region("The Forest", player))
+ world.get_entrance("Behind Tree Double Jump", player).connect(world.get_region("Double Jump Behind Tree", player))
+ world.get_entrance("Behind Ogre", player).connect(world.get_region("The Final Boss Room", player))
+ world.get_entrance("Forest Double Jump", player).connect(world.get_region("The Forest with double Jump", player))
+ world.get_entrance("Forest True Double Jump", player).connect(world.get_region("The Forest with double Jump Part 2", player))
+ world.get_entrance("True Double Jump", player).connect(world.get_region("True Double Jump Behind Tree", player))
+
+
+def connect_entrances_lfod(multiworld, player):
+ multiworld.get_entrance("Live Freemium or Die", player).connect(multiworld.get_region("Freemium Start", player))
+ multiworld.get_entrance("Vines", player).connect(multiworld.get_region("Behind the Vines", player))
+ multiworld.get_entrance("Wall Jump Entrance", player).connect(multiworld.get_region("Wall Jump", player))
+ multiworld.get_entrance("Harmless Plants", player).connect(multiworld.get_region("Fake Ending", player))
+ multiworld.get_entrance("Pickaxe Hard Cave", player).connect(multiworld.get_region("Hard Cave", player))
+ multiworld.get_entrance("Hard Cave Wall Jump", player).connect(multiworld.get_region("Hard Cave Wall Jump", player))
+ multiworld.get_entrance("Name Change Entrance", player).connect(multiworld.get_region("Name Change", player))
+ multiworld.get_entrance("Cut Content Entrance", player).connect(multiworld.get_region("Cut Content", player))
+ multiworld.get_entrance("Behind Rocks", player).connect(multiworld.get_region("Top Right", player))
+ multiworld.get_entrance("Blizzard", player).connect(multiworld.get_region("Season", player))
+ multiworld.get_entrance("Boss Door", player).connect(multiworld.get_region("Final Boss", player))
+
+
+def create_coinsanity_locations_dlc_quest(has_coinsanity: bool, coin_bundle_size: int, player: int, region_move_right: Region):
+ create_coinsanity_locations(has_coinsanity, coin_bundle_size, player, region_move_right, 825, "DLC Quest")
+
+
+def create_coinsanity_locations_lfod(has_coinsanity: bool, coin_bundle_size: int, player: int, region_lfod_start: Region):
+ create_coinsanity_locations(has_coinsanity, coin_bundle_size, player, region_lfod_start, 889, "Live Freemium or Die")
+
+
+def create_coinsanity_locations(has_coinsanity: bool, coin_bundle_size: int, player: int, region: Region, last_coin_number: int, campaign_prefix: str):
+ if not has_coinsanity:
+ return
+
+ coin_bundle_needed = math.ceil(last_coin_number / coin_bundle_size)
+ for i in range(1, coin_bundle_needed + 1):
+ number_coins = min(last_coin_number, coin_bundle_size * i)
+ item_coin = f"{campaign_prefix}: {number_coins} Coin"
+ region.locations += [DLCQuestLocation(player, item_coin, location_table[item_coin], region)]
diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py
index c5fdfe8282c4..5792d9c3ab66 100644
--- a/worlds/dlcquest/Rules.py
+++ b/worlds/dlcquest/Rules.py
@@ -7,41 +7,25 @@
from .Items import DLCQuestItem
-def create_event(player, event: str):
+def create_event(player, event: str) -> DLCQuestItem:
return DLCQuestItem(event, ItemClassification.progression, None, player)
-def set_rules(world, player, World_Options: Options.DLCQuestOptions):
- def has_enough_coin(player: int, coin: int):
- def has_coin(state, player: int, coins: int):
- coin_possessed = 0
- for i in [4, 7, 9, 10, 46, 50, 60, 76, 89, 100, 171, 203]:
- name_coin = f"{i} coins"
- if state.has(name_coin, player):
- coin_possessed += i
-
- return coin_possessed >= coins
+def has_enough_coin(player: int, coin: int):
+ return lambda state: state.prog_items[player][" coins"] >= coin
- return lambda state: has_coin(state, player, coin)
- def has_enough_coin_freemium(player: int, coin: int):
- def has_coin(state, player: int, coins: int):
- coin_possessed = 0
- for i in [20, 50, 90, 95, 130, 150, 154, 200]:
- name_coin = f"{i} coins freemium"
- if state.has(name_coin, player):
- coin_possessed += i
+def has_enough_coin_freemium(player: int, coin: int):
+ return lambda state: state.prog_items[player][" coins freemium"] >= coin
- return coin_possessed >= coins
- return lambda state: has_coin(state, player, coin)
-
- set_basic_rules(World_Options, has_enough_coin, player, world)
- set_lfod_rules(World_Options, has_enough_coin_freemium, player, world)
+def set_rules(world, player, World_Options: Options.DLCQuestOptions):
+ set_basic_rules(World_Options, player, world)
+ set_lfod_rules(World_Options, player, world)
set_completion_condition(World_Options, player, world)
-def set_basic_rules(World_Options, has_enough_coin, player, world):
+def set_basic_rules(World_Options, player, world):
if World_Options.campaign == Options.Campaign.option_live_freemium_or_die:
return
set_basic_entrance_rules(player, world)
@@ -49,8 +33,8 @@ def set_basic_rules(World_Options, has_enough_coin, player, world):
set_basic_shuffled_items_rules(World_Options, player, world)
set_double_jump_glitchless_rules(World_Options, player, world)
set_easy_double_jump_glitch_rules(World_Options, player, world)
- self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world)
- set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world)
+ self_basic_coinsanity_funded_purchase_rules(World_Options, player, world)
+ set_basic_self_funded_purchase_rules(World_Options, player, world)
self_basic_win_condition(World_Options, player, world)
@@ -131,7 +115,7 @@ def set_easy_double_jump_glitch_rules(World_Options, player, world):
lambda state: state.has("Double Jump Pack", player))
-def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world):
+def self_basic_coinsanity_funded_purchase_rules(World_Options, player, world):
if World_Options.coinsanity != Options.CoinSanity.option_coin:
return
number_of_bundle = math.floor(825 / World_Options.coinbundlequantity)
@@ -194,7 +178,7 @@ def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin,
math.ceil(5 / World_Options.coinbundlequantity)))
-def set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world):
+def set_basic_self_funded_purchase_rules(World_Options, player, world):
if World_Options.coinsanity != Options.CoinSanity.option_none:
return
set_rule(world.get_location("Movement Pack", player),
@@ -241,14 +225,14 @@ def self_basic_win_condition(World_Options, player, world):
player))
-def set_lfod_rules(World_Options, has_enough_coin_freemium, player, world):
+def set_lfod_rules(World_Options, player, world):
if World_Options.campaign == Options.Campaign.option_basic:
return
set_lfod_entrance_rules(player, world)
set_boss_door_requirements_rules(player, world)
set_lfod_self_obtained_items_rules(World_Options, player, world)
set_lfod_shuffled_items_rules(World_Options, player, world)
- self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world)
+ self_lfod_coinsanity_funded_purchase_rules(World_Options, player, world)
set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world)
@@ -327,7 +311,7 @@ def set_lfod_shuffled_items_rules(World_Options, player, world):
lambda state: state.can_reach("Cut Content", 'region', player))
-def self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world):
+def self_lfod_coinsanity_funded_purchase_rules(World_Options, player, world):
if World_Options.coinsanity != Options.CoinSanity.option_coin:
return
number_of_bundle = math.floor(889 / World_Options.coinbundlequantity)
diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py
index 392eac7796fb..c22b7cd9847b 100644
--- a/worlds/dlcquest/__init__.py
+++ b/worlds/dlcquest/__init__.py
@@ -1,9 +1,9 @@
from typing import Union
-from BaseClasses import Tutorial
+from BaseClasses import Tutorial, CollectionState
from worlds.AutoWorld import WebWorld, World
from . import Options
-from .Items import DLCQuestItem, ItemData, create_items, item_table
+from .Items import DLCQuestItem, ItemData, create_items, item_table, items_by_group, Group
from .Locations import DLCQuestLocation, location_table
from .Options import DLCQuestOptions
from .Regions import create_regions
@@ -60,7 +60,9 @@ def create_items(self):
created_items = create_items(self, self.options, locations_count + len(items_to_exclude), self.multiworld.random)
self.multiworld.itempool += created_items
- self.multiworld.early_items[self.player]["Movement Pack"] = 1
+
+ if self.options.campaign == Options.Campaign.option_basic or self.options.campaign == Options.Campaign.option_both:
+ self.multiworld.early_items[self.player]["Movement Pack"] = 1
for item in items_to_exclude:
if item in self.multiworld.itempool:
@@ -71,13 +73,16 @@ def precollect_coinsanity(self):
if self.options.coinsanity == Options.CoinSanity.option_coin and self.options.coinbundlequantity >= 5:
self.multiworld.push_precollected(self.create_item("Movement Pack"))
-
def create_item(self, item: Union[str, ItemData]) -> DLCQuestItem:
if isinstance(item, str):
item = item_table[item]
return DLCQuestItem(item.name, item.classification, item.code, self.player)
+ def get_filler_item_name(self) -> str:
+ trap = self.multiworld.random.choice(items_by_group[Group.Trap])
+ return trap.name
+
def fill_slot_data(self):
options_dict = self.options.as_dict(
"death_link", "ending_choice", "campaign", "coinsanity", "item_shuffle"
@@ -87,3 +92,19 @@ def fill_slot_data(self):
"seed": self.random.randrange(99999999)
})
return options_dict
+
+ def collect(self, state: CollectionState, item: DLCQuestItem) -> bool:
+ change = super().collect(state, item)
+ if change:
+ suffix = item.coin_suffix
+ if suffix:
+ state.prog_items[self.player][suffix] += item.coins
+ return change
+
+ def remove(self, state: CollectionState, item: DLCQuestItem) -> bool:
+ change = super().remove(state, item)
+ if change:
+ suffix = item.coin_suffix
+ if suffix:
+ state.prog_items[self.player][suffix] -= item.coins
+ return change
diff --git a/worlds/dlcquest/test/TestItemShuffle.py b/worlds/dlcquest/test/TestItemShuffle.py
new file mode 100644
index 000000000000..bfe999246a50
--- /dev/null
+++ b/worlds/dlcquest/test/TestItemShuffle.py
@@ -0,0 +1,130 @@
+from . import DLCQuestTestBase
+from .. import Options
+
+sword = "Sword"
+gun = "Gun"
+wooden_sword = "Wooden Sword"
+pickaxe = "Pickaxe"
+humble_bindle = "Humble Indie Bindle"
+box_supplies = "Box of Various Supplies"
+items = [sword, gun, wooden_sword, pickaxe, humble_bindle, box_supplies]
+
+important_pack = "Incredibly Important Pack"
+
+
+class TestItemShuffle(DLCQuestTestBase):
+ options = {Options.ItemShuffle.internal_name: Options.ItemShuffle.option_shuffled,
+ Options.Campaign.internal_name: Options.Campaign.option_both}
+
+ def test_items_in_pool(self):
+ item_names = {item.name for item in self.multiworld.get_items()}
+ for item in items:
+ with self.subTest(f"{item}"):
+ self.assertIn(item, item_names)
+
+ def test_item_locations_in_pool(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for item_location in items:
+ with self.subTest(f"{item_location}"):
+ self.assertIn(item_location, location_names)
+
+ def test_sword_location_has_correct_rules(self):
+ self.assertFalse(self.can_reach_location(sword))
+ movement_pack = self.multiworld.create_item("Movement Pack", self.player)
+ self.collect(movement_pack)
+ self.assertFalse(self.can_reach_location(sword))
+ time_pack = self.multiworld.create_item("Time is Money Pack", self.player)
+ self.collect(time_pack)
+ self.assertTrue(self.can_reach_location(sword))
+
+ def test_gun_location_has_correct_rules(self):
+ self.assertFalse(self.can_reach_location(gun))
+ movement_pack = self.multiworld.create_item("Movement Pack", self.player)
+ self.collect(movement_pack)
+ self.assertFalse(self.can_reach_location(gun))
+ sword_item = self.multiworld.create_item(sword, self.player)
+ self.collect(sword_item)
+ self.assertFalse(self.can_reach_location(gun))
+ gun_pack = self.multiworld.create_item("Gun Pack", self.player)
+ self.collect(gun_pack)
+ self.assertTrue(self.can_reach_location(gun))
+
+ def test_wooden_sword_location_has_correct_rules(self):
+ self.assertFalse(self.can_reach_location(wooden_sword))
+ important_pack_item = self.multiworld.create_item(important_pack, self.player)
+ self.collect(important_pack_item)
+ self.assertTrue(self.can_reach_location(wooden_sword))
+
+ def test_bindle_location_has_correct_rules(self):
+ self.assertFalse(self.can_reach_location(humble_bindle))
+ wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player)
+ self.collect(wooden_sword_item)
+ self.assertFalse(self.can_reach_location(humble_bindle))
+ plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player)
+ self.collect(plants_pack)
+ self.assertFalse(self.can_reach_location(humble_bindle))
+ wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player)
+ self.collect(wall_jump_pack)
+ self.assertFalse(self.can_reach_location(humble_bindle))
+ name_change_pack = self.multiworld.create_item("Name Change Pack", self.player)
+ self.collect(name_change_pack)
+ self.assertFalse(self.can_reach_location(humble_bindle))
+ cut_content_pack = self.multiworld.create_item("Cut Content Pack", self.player)
+ self.collect(cut_content_pack)
+ self.assertFalse(self.can_reach_location(humble_bindle))
+ box_supplies_item = self.multiworld.create_item(box_supplies, self.player)
+ self.collect(box_supplies_item)
+ self.assertTrue(self.can_reach_location(humble_bindle))
+
+ def test_box_supplies_location_has_correct_rules(self):
+ self.assertFalse(self.can_reach_location(box_supplies))
+ wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player)
+ self.collect(wooden_sword_item)
+ self.assertFalse(self.can_reach_location(box_supplies))
+ plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player)
+ self.collect(plants_pack)
+ self.assertFalse(self.can_reach_location(box_supplies))
+ wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player)
+ self.collect(wall_jump_pack)
+ self.assertFalse(self.can_reach_location(box_supplies))
+ name_change_pack = self.multiworld.create_item("Name Change Pack", self.player)
+ self.collect(name_change_pack)
+ self.assertFalse(self.can_reach_location(box_supplies))
+ cut_content_pack = self.multiworld.create_item("Cut Content Pack", self.player)
+ self.collect(cut_content_pack)
+ self.assertTrue(self.can_reach_location(box_supplies))
+
+ def test_pickaxe_location_has_correct_rules(self):
+ self.assertFalse(self.can_reach_location(pickaxe))
+ wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player)
+ self.collect(wooden_sword_item)
+ self.assertFalse(self.can_reach_location(pickaxe))
+ plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player)
+ self.collect(plants_pack)
+ self.assertFalse(self.can_reach_location(pickaxe))
+ wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player)
+ self.collect(wall_jump_pack)
+ self.assertFalse(self.can_reach_location(pickaxe))
+ name_change_pack = self.multiworld.create_item("Name Change Pack", self.player)
+ self.collect(name_change_pack)
+ self.assertFalse(self.can_reach_location(pickaxe))
+ bindle_item = self.multiworld.create_item("Humble Indie Bindle", self.player)
+ self.collect(bindle_item)
+ self.assertTrue(self.can_reach_location(pickaxe))
+
+
+class TestNoItemShuffle(DLCQuestTestBase):
+ options = {Options.ItemShuffle.internal_name: Options.ItemShuffle.option_disabled,
+ Options.Campaign.internal_name: Options.Campaign.option_both}
+
+ def test_items_not_in_pool(self):
+ item_names = {item.name for item in self.multiworld.get_items()}
+ for item in items:
+ with self.subTest(f"{item}"):
+ self.assertNotIn(item, item_names)
+
+ def test_item_locations_not_in_pool(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for item_location in items:
+ with self.subTest(f"{item_location}"):
+ self.assertNotIn(item_location, location_names)
\ No newline at end of file
diff --git a/worlds/dlcquest/test/TestOptionsLong.py b/worlds/dlcquest/test/TestOptionsLong.py
new file mode 100644
index 000000000000..d0a5c0ed7dfb
--- /dev/null
+++ b/worlds/dlcquest/test/TestOptionsLong.py
@@ -0,0 +1,87 @@
+from typing import Dict
+
+from BaseClasses import MultiWorld
+from Options import SpecialRange
+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 ... import AutoWorldRegister
+
+
+def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld):
+ assert_can_win(tester, multiworld)
+ assert_same_number_items_locations(tester, multiworld)
+
+
+def get_option_choices(option) -> Dict[str, int]:
+ if issubclass(option, SpecialRange):
+ return option.special_range_names
+ elif option.options:
+ return option.options
+ return {}
+
+
+class TestGenerateDynamicOptions(DLCQuestTestBase):
+ def test_given_option_pair_when_generate_then_basic_checks(self):
+ num_options = len(options_to_include)
+ for option1_index in range(0, num_options):
+ for option2_index in range(option1_index + 1, num_options):
+ option1 = options_to_include[option1_index]
+ option2 = options_to_include[option2_index]
+ option1_choices = get_option_choices(option1)
+ option2_choices = get_option_choices(option2)
+ for key1 in option1_choices:
+ for key2 in option2_choices:
+ with self.subTest(f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}"):
+ choices = {option1.internal_name: option1_choices[key1],
+ option2.internal_name: option2_choices[key2]}
+ multiworld = setup_dlc_quest_solo_multiworld(choices)
+ basic_checks(self, multiworld)
+
+ def test_given_option_truple_when_generate_then_basic_checks(self):
+ num_options = len(options_to_include)
+ for option1_index in range(0, num_options):
+ for option2_index in range(option1_index + 1, num_options):
+ for option3_index in range(option2_index + 1, num_options):
+ option1 = options_to_include[option1_index]
+ option2 = options_to_include[option2_index]
+ option3 = options_to_include[option3_index]
+ option1_choices = get_option_choices(option1)
+ option2_choices = get_option_choices(option2)
+ option3_choices = get_option_choices(option3)
+ for key1 in option1_choices:
+ for key2 in option2_choices:
+ for key3 in option3_choices:
+ with self.subTest(f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}, {option3.internal_name}: {key3}"):
+ choices = {option1.internal_name: option1_choices[key1],
+ option2.internal_name: option2_choices[key2],
+ option3.internal_name: option3_choices[key3]}
+ multiworld = setup_dlc_quest_solo_multiworld(choices)
+ basic_checks(self, multiworld)
+
+ def test_given_option_quartet_when_generate_then_basic_checks(self):
+ num_options = len(options_to_include)
+ for option1_index in range(0, num_options):
+ for option2_index in range(option1_index + 1, num_options):
+ for option3_index in range(option2_index + 1, num_options):
+ for option4_index in range(option3_index + 1, num_options):
+ option1 = options_to_include[option1_index]
+ option2 = options_to_include[option2_index]
+ option3 = options_to_include[option3_index]
+ option4 = options_to_include[option4_index]
+ option1_choices = get_option_choices(option1)
+ option2_choices = get_option_choices(option2)
+ option3_choices = get_option_choices(option3)
+ option4_choices = get_option_choices(option4)
+ for key1 in option1_choices:
+ for key2 in option2_choices:
+ for key3 in option3_choices:
+ for key4 in option4_choices:
+ with self.subTest(
+ f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}, {option3.internal_name}: {key3}, {option4.internal_name}: {key4}"):
+ choices = {option1.internal_name: option1_choices[key1],
+ option2.internal_name: option2_choices[key2],
+ option3.internal_name: option3_choices[key3],
+ option4.internal_name: option4_choices[key4]}
+ multiworld = setup_dlc_quest_solo_multiworld(choices)
+ basic_checks(self, multiworld)
diff --git a/worlds/dlcquest/test/__init__.py b/worlds/dlcquest/test/__init__.py
new file mode 100644
index 000000000000..e998bd8a5e8b
--- /dev/null
+++ b/worlds/dlcquest/test/__init__.py
@@ -0,0 +1,53 @@
+from typing import ClassVar
+
+from typing import Dict, FrozenSet, Tuple, Any
+from argparse import Namespace
+
+from BaseClasses import MultiWorld
+from test.TestBase 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
+
+
+class DLCQuestTestBase(WorldTestBase):
+ game = "DLCQuest"
+ world: DLCqworld
+ player: ClassVar[int] = 1
+
+ def world_setup(self, *args, **kwargs):
+ super().world_setup(*args, **kwargs)
+ if self.constructed:
+ self.world = self.multiworld.worlds[self.player] # noqa
+
+ @property
+ def run_default_tests(self) -> bool:
+ # world_setup is overridden, so it'd always run default tests when importing DLCQuestTestBase
+ is_not_dlc_test = type(self) is not DLCQuestTestBase
+ should_run_default_tests = is_not_dlc_test and super().run_default_tests
+ return should_run_default_tests
+
+
+def setup_dlc_quest_solo_multiworld(test_options=None, seed=None, _cache: Dict[FrozenSet[Tuple[str, Any]], MultiWorld] = {}) -> MultiWorld: #noqa
+ if test_options is None:
+ test_options = {}
+
+ # Yes I reuse the worlds generated between tests, its speeds the execution by a couple seconds
+ frozen_options = frozenset(test_options.items()).union({seed})
+ if frozen_options in _cache:
+ return _cache[frozen_options]
+
+ multiworld = setup_base_solo_multiworld(DLCqworld, ())
+ multiworld.set_seed(seed)
+ # print(f"Seed: {multiworld.seed}") # Uncomment to print the seed for every test
+ args = Namespace()
+ for name, option in DLCqworld.options_dataclass.type_hints.items():
+ value = option(test_options[name]) if name in test_options else option.from_any(option.default)
+ setattr(args, name, {1: value})
+ multiworld.set_options(args)
+ for step in gen_steps:
+ call_all(multiworld, step)
+
+ _cache[frozen_options] = multiworld
+
+ return multiworld
diff --git a/worlds/dlcquest/test/checks/__init__.py b/worlds/dlcquest/test/checks/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/dlcquest/test/checks/world_checks.py b/worlds/dlcquest/test/checks/world_checks.py
new file mode 100644
index 000000000000..a97093d62036
--- /dev/null
+++ b/worlds/dlcquest/test/checks/world_checks.py
@@ -0,0 +1,42 @@
+from typing import List
+
+from BaseClasses import MultiWorld, ItemClassification
+from .. import DLCQuestTestBase
+from ... import Options
+
+
+def get_all_item_names(multiworld: MultiWorld) -> List[str]:
+ return [item.name for item in multiworld.itempool]
+
+
+def get_all_location_names(multiworld: MultiWorld) -> List[str]:
+ return [location.name for location in multiworld.get_locations() if not location.event]
+
+
+def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld):
+ campaign = multiworld.campaign[1]
+ all_items = [item.name for item in multiworld.get_items()]
+ if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both:
+ tester.assertIn("Victory Basic", all_items)
+ if campaign == Options.Campaign.option_live_freemium_or_die or campaign == Options.Campaign.option_both:
+ tester.assertIn("Victory Freemium", all_items)
+
+
+def collect_all_then_assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld):
+ for item in multiworld.get_items():
+ multiworld.state.collect(item)
+ campaign = multiworld.campaign[1]
+ if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both:
+ tester.assertTrue(multiworld.find_item("Victory Basic", 1).can_reach(multiworld.state))
+ if campaign == Options.Campaign.option_live_freemium_or_die or campaign == Options.Campaign.option_both:
+ tester.assertTrue(multiworld.find_item("Victory Freemium", 1).can_reach(multiworld.state))
+
+
+def assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld):
+ assert_victory_exists(tester, multiworld)
+ collect_all_then_assert_can_win(tester, multiworld)
+
+
+def assert_same_number_items_locations(tester: DLCQuestTestBase, multiworld: MultiWorld):
+ non_event_locations = [location for location in multiworld.get_locations() if not location.event]
+ tester.assertEqual(len(multiworld.itempool), len(non_event_locations))
\ No newline at end of file
diff --git a/worlds/dlcquest/test/option_names.py b/worlds/dlcquest/test/option_names.py
new file mode 100644
index 000000000000..4a4b46e906cb
--- /dev/null
+++ b/worlds/dlcquest/test/option_names.py
@@ -0,0 +1,5 @@
+from .. import DLCqworld
+
+options_to_exclude = ["progression_balancing", "accessibility", "start_inventory", "start_hints", "death_link"]
+options_to_include = [option for option_name, option in DLCqworld.options_dataclass.type_hints.items()
+ if option_name not in options_to_exclude]
diff --git a/worlds/factorio/Locations.py b/worlds/factorio/Locations.py
index f9db5f4a2bd8..52f0954cba30 100644
--- a/worlds/factorio/Locations.py
+++ b/worlds/factorio/Locations.py
@@ -3,18 +3,13 @@
from .Technologies import factorio_base_id
from .Options import MaxSciencePack
-boundary: int = 0xff
-total_locations: int = 0xff
-
-assert total_locations <= boundary
-
def make_pools() -> Dict[str, List[str]]:
pools: Dict[str, List[str]] = {}
for i, pack in enumerate(MaxSciencePack.get_ordered_science_packs(), start=1):
- max_needed: int = 0xff
+ max_needed: int = 999
prefix: str = f"AP-{i}-"
- pools[pack] = [prefix + hex(x)[2:].upper().zfill(2) for x in range(1, max_needed + 1)]
+ pools[pack] = [prefix + str(x).upper().zfill(3) for x in range(1, max_needed + 1)]
return pools
diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py
index 8308bb2d6559..eb078720c668 100644
--- a/worlds/factorio/__init__.py
+++ b/worlds/factorio/__init__.py
@@ -541,7 +541,7 @@ def __init__(self, player: int, name: str, address: int, parent: Region):
super(FactorioScienceLocation, self).__init__(player, name, address, parent)
# "AP-{Complexity}-{Cost}"
self.complexity = int(self.name[3]) - 1
- self.rel_cost = int(self.name[5:], 16)
+ self.rel_cost = int(self.name[5:])
self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1}
for complexity in range(self.complexity):
diff --git a/worlds/factorio/docs/setup_en.md b/worlds/factorio/docs/setup_en.md
index 09ad431a21cc..b6d45459253a 100644
--- a/worlds/factorio/docs/setup_en.md
+++ b/worlds/factorio/docs/setup_en.md
@@ -31,7 +31,7 @@ them. Factorio player settings page: [Factorio Settings Page](/games/Factorio/pl
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
-Validator page: [Yaml Validation Page](/mysterycheck)
+Validator page: [Yaml Validation Page](/check)
## Connecting to Someone Else's Factorio Game
diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py
index 432467399ea6..16905cc6da0c 100644
--- a/worlds/ff1/__init__.py
+++ b/worlds/ff1/__init__.py
@@ -14,7 +14,7 @@ class FF1Settings(settings.Group):
class FF1Web(WebWorld):
- settings_page = "https://finalfantasyrandomizer.com/"
+ options_page = "https://finalfantasyrandomizer.com/"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to playing Final Fantasy multiworld. This guide only covers playing multiworld.",
diff --git a/worlds/ff1/docs/en_Final Fantasy.md b/worlds/ff1/docs/en_Final Fantasy.md
index 89629197434f..59fa85d91613 100644
--- a/worlds/ff1/docs/en_Final Fantasy.md
+++ b/worlds/ff1/docs/en_Final Fantasy.md
@@ -26,6 +26,7 @@ All local and remote items appear the same. Final Fantasy will say that you rece
emulator will display what was found external to the in-game text box.
## Unique Local Commands
-The following command is only available when using the FF1Client for the Final Fantasy Randomizer.
+The following commands are only available when using the FF1Client for the Final Fantasy Randomizer.
- `/nes` Shows the current status of the NES connection.
+- `/toggle_msgs` Toggle displaying messages in EmuHawk
diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md
index 456795dac4a7..6d5e20462f13 100644
--- a/worlds/generic/docs/advanced_settings_en.md
+++ b/worlds/generic/docs/advanced_settings_en.md
@@ -108,7 +108,9 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
* `minimal` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically
but may not be able to access all locations or acquire all items. A good example of this is having a big key in
the big chest in a dungeon in ALTTP making it impossible to get and finish the dungeon.
-* `progression_balancing` is a system the Archipelago generator uses to try and reduce "BK mode" as much as possible.
+* `progression_balancing` is a system the Archipelago generator uses to try and reduce
+ ["BK mode"](/glossary/en/#burger-king-/-bk-mode)
+ as much as possible.
This primarily involves moving necessary progression items into earlier logic spheres to make the games more
accessible so that players almost always have something to do. This can be in a range from 0 to 99, and is 50 by
default. This number represents a percentage of the furthest progressible player.
@@ -130,7 +132,7 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
there without using any hint points.
* `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk"
item which isn't necessary for progression to go in these locations.
-* `priority_locations` is the inverse of `exlcude_locations`, forcing a progression item in the defined locations.
+* `priority_locations` is the inverse of `exclude_locations`, forcing a progression item in the defined locations.
* `item_links` allows players to link their items into a group with the same item link name and game. The items declared
in `item_pool` get combined and when an item is found for the group, all players in the group receive it. Item links
can also have local and non local items, forcing the items to either be placed within the worlds of the group or in
diff --git a/worlds/generic/docs/setup_en.md b/worlds/generic/docs/setup_en.md
index 132b88e28553..93ae217e0d33 100644
--- a/worlds/generic/docs/setup_en.md
+++ b/worlds/generic/docs/setup_en.md
@@ -40,7 +40,7 @@ game you will be playing as well as the settings you would like for that game.
YAML is a format very similar to JSON however it is made to be more human-readable. If you are ever unsure of the
validity of your YAML file you may check the file by uploading it to the check page on the Archipelago website:
-[YAML Validation Page](/mysterycheck)
+[YAML Validation Page](/check)
### Creating a YAML
diff --git a/worlds/hk/Items.py b/worlds/hk/Items.py
index a9acbf48f303..def5c3298102 100644
--- a/worlds/hk/Items.py
+++ b/worlds/hk/Items.py
@@ -19,18 +19,43 @@ class HKItemData(NamedTuple):
for item, item_data in item_table.items():
lookup_type_to_names.setdefault(item_data.type, set()).add(item)
-item_name_groups = {group: lookup_type_to_names[group] for group in ("Skill", "Charm", "Mask", "Vessel",
- "Relic", "Root", "Map", "Stag", "Cocoon",
- "Soul", "DreamWarrior", "DreamBoss")}
-
directionals = ('', 'Left_', 'Right_')
-
-item_name_groups.update({
+item_name_groups = ({
+ "BossEssence": lookup_type_to_names["DreamWarrior"] | lookup_type_to_names["DreamBoss"],
+ "BossGeo": lookup_type_to_names["Boss_Geo"],
+ "CDash": {x + "Crystal_Heart" for x in directionals},
+ "Charms": lookup_type_to_names["Charm"],
+ "CharmNotches": lookup_type_to_names["Notch"],
+ "Claw": {x + "Mantis_Claw" for x in directionals},
+ "Cloak": {x + "Mothwing_Cloak" for x in directionals} | {"Shade_Cloak", "Split_Shade_Cloak"},
+ "Dive": {"Desolate_Dive", "Descending_Dark"},
+ "LifebloodCocoons": lookup_type_to_names["Cocoon"],
"Dreamers": {"Herrah", "Monomon", "Lurien"},
- "Cloak": {x + 'Mothwing_Cloak' for x in directionals} | {'Shade_Cloak', 'Split_Shade_Cloak'},
- "Claw": {x + 'Mantis_Claw' for x in directionals},
- "CDash": {x + 'Crystal_Heart' for x in directionals},
- "Fragments": {"Queen_Fragment", "King_Fragment", "Void_Heart"},
+ "Fireball": {"Vengeful_Spirit", "Shade_Soul"},
+ "GeoChests": lookup_type_to_names["Geo"],
+ "GeoRocks": lookup_type_to_names["Rock"],
+ "GrimmkinFlames": lookup_type_to_names["Flame"],
+ "Grubs": lookup_type_to_names["Grub"],
+ "JournalEntries": lookup_type_to_names["Journal"],
+ "JunkPitChests": lookup_type_to_names["JunkPitChest"],
+ "Keys": lookup_type_to_names["Key"],
+ "LoreTablets": lookup_type_to_names["Lore"] | lookup_type_to_names["PalaceLore"],
+ "Maps": lookup_type_to_names["Map"],
+ "MaskShards": lookup_type_to_names["Mask"],
+ "Mimics": lookup_type_to_names["Mimic"],
+ "Nail": lookup_type_to_names["CursedNail"],
+ "PalaceJournal": {"Journal_Entry-Seal_of_Binding"},
+ "PalaceLore": lookup_type_to_names["PalaceLore"],
+ "PalaceTotem": {"Soul_Totem-Palace", "Soul_Totem-Path_of_Pain"},
+ "RancidEggs": lookup_type_to_names["Egg"],
+ "Relics": lookup_type_to_names["Relic"],
+ "Scream": {"Howling_Wraiths", "Abyss_Shriek"},
+ "Skills": lookup_type_to_names["Skill"],
+ "SoulTotems": lookup_type_to_names["Soul"],
+ "Stags": lookup_type_to_names["Stag"],
+ "VesselFragments": lookup_type_to_names["Vessel"],
+ "WhisperingRoots": lookup_type_to_names["Root"],
+ "WhiteFragments": {"Queen_Fragment", "King_Fragment", "Void_Heart"},
})
item_name_groups['Horizontal'] = item_name_groups['Cloak'] | item_name_groups['CDash']
item_name_groups['Vertical'] = item_name_groups['Claw'] | {'Monarch_Wings'}
diff --git a/worlds/hk/Rules.py b/worlds/hk/Rules.py
index 4fe4160b4cfc..2dc512eca76e 100644
--- a/worlds/hk/Rules.py
+++ b/worlds/hk/Rules.py
@@ -1,5 +1,4 @@
from ..generic.Rules import set_rule, add_rule
-from BaseClasses import MultiWorld
from ..AutoWorld import World
from .GeneratedRules import set_generated_rules
from typing import NamedTuple
@@ -39,14 +38,12 @@ def hk_set_rule(hk_world: World, location: str, rule):
def set_rules(hk_world: World):
player = hk_world.player
- world = hk_world.multiworld
set_generated_rules(hk_world, hk_set_rule)
# Shop costs
- for region in world.get_regions(player):
- for location in region.locations:
- if location.costs:
- for term, amount in location.costs.items():
- if term == "GEO": # No geo logic!
- continue
- add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount)
+ for location in hk_world.multiworld.get_locations(player):
+ if location.costs:
+ for term, amount in location.costs.items():
+ if term == "GEO": # No geo logic!
+ continue
+ add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount)
diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py
index 1a9d4b5d6160..c16a108cd169 100644
--- a/worlds/hk/__init__.py
+++ b/worlds/hk/__init__.py
@@ -517,12 +517,12 @@ def collect(self, state, item: HKItem) -> bool:
change = super(HKWorld, self).collect(state, item)
if change:
for effect_name, effect_value in item_effects.get(item.name, {}).items():
- state.prog_items[effect_name, item.player] += effect_value
+ state.prog_items[item.player][effect_name] += effect_value
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
- if state.prog_items.get(('RIGHTDASH', item.player), 0) and \
- state.prog_items.get(('LEFTDASH', item.player), 0):
- (state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player]) = \
- ([max(state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player])] * 2)
+ if state.prog_items[item.player].get('RIGHTDASH', 0) and \
+ state.prog_items[item.player].get('LEFTDASH', 0):
+ (state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \
+ ([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
return change
def remove(self, state, item: HKItem) -> bool:
@@ -530,9 +530,9 @@ def remove(self, state, item: HKItem) -> bool:
if change:
for effect_name, effect_value in item_effects.get(item.name, {}).items():
- if state.prog_items[effect_name, item.player] == effect_value:
- del state.prog_items[effect_name, item.player]
- state.prog_items[effect_name, item.player] -= effect_value
+ if state.prog_items[item.player][effect_name] == effect_value:
+ del state.prog_items[item.player][effect_name]
+ state.prog_items[item.player][effect_name] -= effect_value
return change
diff --git a/worlds/hk/docs/setup_en.md b/worlds/hk/docs/setup_en.md
index adf975ff515e..fef0f051fec0 100644
--- a/worlds/hk/docs/setup_en.md
+++ b/worlds/hk/docs/setup_en.md
@@ -1,27 +1,27 @@
# Hollow Knight for Archipelago Setup Guide
## Required Software
-* Download and unzip the Scarab+ Mod Manager from the [Scarab+ website](https://themulhima.github.io/Scarab/).
+* Download and unzip the Lumafly Mod Manager from the [Lumafly website](https://themulhima.github.io/Lumafly/).
* A legal copy of Hollow Knight.
-## Installing the Archipelago Mod using Scarab+
-1. Launch Scarab+ and ensure it locates your Hollow Knight installation directory.
+## Installing the Archipelago Mod using Lumafly
+1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.
2. Click the "Install" button near the "Archipelago" mod entry.
* If desired, also install "Archipelago Map Mod" to use as an in-game tracker.
3. Launch the game, you're all set!
-### What to do if Scarab+ fails to find your XBox Game Pass installation directory
+### What to do if Lumafly fails to find your XBox Game Pass installation directory
1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar.
2. Click the three points then click "Manage".
3. Go to the "Files" tab and select "Browse...".
4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
-5. Run Scarab+ as an administrator and, when it asks you for the path, paste what you copied in step 4.
+5. Run Lumafly as an administrator and, when it asks you for the path, paste what you copied in step 4.
#### Alternative Method:
1. Click on your profile then "Settings".
2. Go to the "General" tab and select "CHANGE FOLDER".
3. Look for a folder where you want to install the game (preferably inside a folder on your desktop) and copy the path.
-4. Run Scarab+ as an administrator and, when it asks you for the path, paste what you copied in step 3.
+4. Run Lumafly as an administrator and, when it asks you for the path, paste what you copied in step 3.
Note: The path folder needs to have the "Hollow Knight_Data" folder inside.
diff --git a/worlds/hylics2/Rules.py b/worlds/hylics2/Rules.py
index 12c22e01cd2f..6c55c8745b17 100644
--- a/worlds/hylics2/Rules.py
+++ b/worlds/hylics2/Rules.py
@@ -1,91 +1,128 @@
from worlds.generic.Rules import add_rule
-from ..AutoWorld import LogicMixin
+from BaseClasses import CollectionState
-class Hylics2Logic(LogicMixin):
+def air_dash(state: CollectionState, player: int) -> bool:
+ return state.has("PNEUMATOPHORE", player)
- def _hylics2_can_air_dash(self, player):
- return self.has("PNEUMATOPHORE", player)
- def _hylics2_has_airship(self, player):
- return self.has("DOCK KEY", player)
+def airship(state: CollectionState, player: int) -> bool:
+ return state.has("DOCK KEY", player)
- def _hylics2_has_jail_key(self, player):
- return self.has("JAIL KEY", player)
- def _hylics2_has_paddle(self, player):
- return self.has("PADDLE", player)
+def jail_key(state: CollectionState, player: int) -> bool:
+ return state.has("JAIL KEY", player)
- def _hylics2_has_worm_room_key(self, player):
- return self.has("WORM ROOM KEY", player)
- def _hylics2_has_bridge_key(self, player):
- return self.has("BRIDGE KEY", player)
+def paddle(state: CollectionState, player: int) -> bool:
+ return state.has("PADDLE", player)
- def _hylics2_has_upper_chamber_key(self, player):
- return self.has("UPPER CHAMBER KEY", player)
- def _hylics2_has_vessel_room_key(self, player):
- return self.has("VESSEL ROOM KEY", player)
+def worm_room_key(state: CollectionState, player: int) -> bool:
+ return state.has("WORM ROOM KEY", player)
- def _hylics2_has_house_key(self, player):
- return self.has("HOUSE KEY", player)
- def _hylics2_has_cave_key(self, player):
- return self.has("CAVE KEY", player)
+def bridge_key(state: CollectionState, player: int) -> bool:
+ return state.has("BRIDGE KEY", player)
- def _hylics2_has_skull_bomb(self, player):
- return self.has("SKULL BOMB", player)
- def _hylics2_has_tower_key(self, player):
- return self.has("TOWER KEY", player)
+def upper_chamber_key(state: CollectionState, player: int) -> bool:
+ return state.has("UPPER CHAMBER KEY", player)
- def _hylics2_has_deep_key(self, player):
- return self.has("DEEP KEY", player)
- def _hylics2_has_upper_house_key(self, player):
- return self.has("UPPER HOUSE KEY", player)
+def vessel_room_key(state: CollectionState, player: int) -> bool:
+ return state.has("VESSEL ROOM KEY", player)
- def _hylics2_has_clicker(self, player):
- return self.has("CLICKER", player)
- def _hylics2_has_tokens(self, player):
- return self.has("SAGE TOKEN", player, 3)
+def house_key(state: CollectionState, player: int) -> bool:
+ return state.has("HOUSE KEY", player)
- def _hylics2_has_charge_up(self, player):
- return self.has("CHARGE UP", player)
- def _hylics2_has_cup(self, player):
- return self.has("PAPER CUP", player, 1)
+def cave_key(state: CollectionState, player: int) -> bool:
+ return state.has("CAVE KEY", player)
- def _hylics2_has_1_member(self, player):
- return self.has("Pongorma", player) or self.has("Dedusmuln", player) or self.has("Somsnosa", player)
- def _hylics2_has_2_members(self, player):
- return (self.has("Pongorma", player) and self.has("Dedusmuln", player)) or\
- (self.has("Pongorma", player) and self.has("Somsnosa", player)) or\
- (self.has("Dedusmuln", player) and self.has("Somsnosa", player))
+def skull_bomb(state: CollectionState, player: int) -> bool:
+ return state.has("SKULL BOMB", player)
- def _hylics2_has_3_members(self, player):
- return self.has("Pongorma", player) and self.has("Dedusmuln", player) and self.has("Somsnosa", player)
- def _hylics2_enter_arcade2(self, player):
- return self._hylics2_can_air_dash(player) and self._hylics2_has_airship(player)
+def tower_key(state: CollectionState, player: int) -> bool:
+ return state.has("TOWER KEY", player)
- def _hylics2_enter_wormpod(self, player):
- return self._hylics2_has_airship(player) and self._hylics2_has_worm_room_key(player) and\
- self._hylics2_has_paddle(player)
- def _hylics2_enter_sageship(self, player):
- return self._hylics2_has_skull_bomb(player) and self._hylics2_has_airship(player) and\
- self._hylics2_has_paddle(player)
+def deep_key(state: CollectionState, player: int) -> bool:
+ return state.has("DEEP KEY", player)
- def _hylics2_enter_foglast(self, player):
- return self._hylics2_enter_wormpod(player)
- def _hylics2_enter_hylemxylem(self, player):
- return self._hylics2_can_air_dash(player) and self._hylics2_enter_foglast(player) and\
- self._hylics2_has_bridge_key(player)
+def upper_house_key(state: CollectionState, player: int) -> bool:
+ return state.has("UPPER HOUSE KEY", player)
+
+
+def clicker(state: CollectionState, player: int) -> bool:
+ return state.has("CLICKER", player)
+
+
+def all_tokens(state: CollectionState, player: int) -> bool:
+ return state.has("SAGE TOKEN", player, 3)
+
+
+def charge_up(state: CollectionState, player: int) -> bool:
+ return state.has("CHARGE UP", player)
+
+
+def paper_cup(state: CollectionState, player: int) -> bool:
+ return state.has("PAPER CUP", player)
+
+
+def party_1(state: CollectionState, player: int) -> bool:
+ return state.has_any({"Pongorma", "Dedusmuln", "Somsnosa"}, player)
+
+
+def party_2(state: CollectionState, player: int) -> bool:
+ return (
+ state.has_all({"Pongorma", "Dedusmuln"}, player)
+ or state.has_all({"Pongorma", "Somsnosa"}, player)
+ or state.has_all({"Dedusmuln", "Somsnosa"}, player)
+ )
+
+
+def party_3(state: CollectionState, player: int) -> bool:
+ return state.has_all({"Pongorma", "Dedusmuln", "Somsnosa"}, player)
+
+
+def enter_arcade2(state: CollectionState, player: int) -> bool:
+ return (
+ air_dash(state, player)
+ and airship(state, player)
+ )
+
+
+def enter_wormpod(state: CollectionState, player: int) -> bool:
+ return (
+ airship(state, player)
+ and worm_room_key(state, player)
+ and paddle(state, player)
+ )
+
+
+def enter_sageship(state: CollectionState, player: int) -> bool:
+ return (
+ skull_bomb(state, player)
+ and airship(state, player)
+ and paddle(state, player)
+ )
+
+
+def enter_foglast(state: CollectionState, player: int) -> bool:
+ return enter_wormpod(state, player)
+
+
+def enter_hylemxylem(state: CollectionState, player: int) -> bool:
+ return (
+ air_dash(state, player)
+ and enter_foglast(state, player)
+ and bridge_key(state, player)
+ )
def set_rules(hylics2world):
@@ -94,342 +131,439 @@ def set_rules(hylics2world):
# Afterlife
add_rule(world.get_location("Afterlife: TV", player),
- lambda state: state._hylics2_has_cave_key(player))
+ lambda state: cave_key(state, player))
# New Muldul
add_rule(world.get_location("New Muldul: Underground Chest", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("New Muldul: TV", player),
- lambda state: state._hylics2_has_house_key(player))
+ lambda state: house_key(state, player))
add_rule(world.get_location("New Muldul: Upper House Chest 1", player),
- lambda state: state._hylics2_has_upper_house_key(player))
+ lambda state: upper_house_key(state, player))
add_rule(world.get_location("New Muldul: Upper House Chest 2", player),
- lambda state: state._hylics2_has_upper_house_key(player))
+ lambda state: upper_house_key(state, player))
add_rule(world.get_location("New Muldul: Pot above Vault", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
# New Muldul Vault
add_rule(world.get_location("New Muldul: Rescued Blerol 1", player),
- lambda state: ((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) and\
- (state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))) or\
- state._hylics2_enter_hylemxylem(player))
+ lambda state: (
+ (
+ (
+ jail_key(state, player)
+ and paddle(state, player)
+ )
+ and (
+ air_dash(state, player)
+ or airship(state, player)
+ )
+ )
+ or enter_hylemxylem(state, player)
+ ))
add_rule(world.get_location("New Muldul: Rescued Blerol 2", player),
- lambda state: ((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) and\
- (state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))) or\
- state._hylics2_enter_hylemxylem(player))
+ lambda state: (
+ (
+ (
+ jail_key(state, player)
+ and paddle(state, player)
+ )
+ and (
+ air_dash(state, player)
+ or airship(state, player)
+ )
+ )
+ or enter_hylemxylem(state, player)
+ ))
add_rule(world.get_location("New Muldul: Vault Left Chest", player),
- lambda state: state._hylics2_enter_hylemxylem(player))
+ lambda state: enter_hylemxylem(state, player))
add_rule(world.get_location("New Muldul: Vault Right Chest", player),
- lambda state: state._hylics2_enter_hylemxylem(player))
+ lambda state: enter_hylemxylem(state, player))
add_rule(world.get_location("New Muldul: Vault Bomb", player),
- lambda state: state._hylics2_enter_hylemxylem(player))
+ lambda state: enter_hylemxylem(state, player))
# Viewax's Edifice
add_rule(world.get_location("Viewax's Edifice: Canopic Jar", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Cave Sarcophagus", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Shielded Key", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Shielded Key", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Tower Pot", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Tower Jar", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Tower Chest", player),
- lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_tower_key(player))
+ lambda state: (
+ paddle(state, player)
+ and tower_key(state, player)
+ ))
add_rule(world.get_location("Viewax's Edifice: Viewax Pot", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: TV", player),
- lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_jail_key(player))
+ lambda state: (
+ paddle(state, player)
+ and jail_key(state, player)
+ ))
add_rule(world.get_location("Viewax's Edifice: Sage Fridge", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Viewax's Edifice: Sage Item 1", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Viewax's Edifice: Sage Item 2", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
# Arcade 1
add_rule(world.get_location("Arcade 1: Key", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Coin Dash", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Burrito Alcove 1", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Burrito Alcove 2", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Behind Spikes Banana", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Pyramid Banana", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Moving Platforms Muscle Applique", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Bed Banana", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
# Airship
add_rule(world.get_location("Airship: Talk to Somsnosa", player),
- lambda state: state._hylics2_has_worm_room_key(player))
+ lambda state: worm_room_key(state, player))
# Foglast
add_rule(world.get_location("Foglast: Underground Sarcophagus", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Foglast: Shielded Key", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Foglast: TV", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_clicker(player))
+ lambda state: (
+ air_dash(state, player)
+ and clicker(state, player)
+ ))
add_rule(world.get_location("Foglast: Buy Clicker", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Foglast: Shielded Chest", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Foglast: Cave Fridge", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Foglast: Roof Sarcophagus", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ air_dash(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 1", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ air_dash(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 2", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ air_dash(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 3", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ air_dash(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("Foglast: Sage Sarcophagus", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ air_dash(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("Foglast: Sage Item 1", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ air_dash(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("Foglast: Sage Item 2", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ air_dash(state, player)
+ and bridge_key(state, player)
+ ))
# Drill Castle
add_rule(world.get_location("Drill Castle: Island Banana", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Drill Castle: Island Pot", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Drill Castle: Cave Sarcophagus", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Drill Castle: TV", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
# Sage Labyrinth
add_rule(world.get_location("Sage Labyrinth: Sage Item 1", player),
- lambda state: state._hylics2_has_deep_key(player))
+ lambda state: deep_key(state, player))
add_rule(world.get_location("Sage Labyrinth: Sage Item 2", player),
- lambda state: state._hylics2_has_deep_key(player))
+ lambda state: deep_key(state, player))
add_rule(world.get_location("Sage Labyrinth: Sage Left Arm", player),
- lambda state: state._hylics2_has_deep_key(player))
+ lambda state: deep_key(state, player))
add_rule(world.get_location("Sage Labyrinth: Sage Right Arm", player),
- lambda state: state._hylics2_has_deep_key(player))
+ lambda state: deep_key(state, player))
add_rule(world.get_location("Sage Labyrinth: Sage Left Leg", player),
- lambda state: state._hylics2_has_deep_key(player))
+ lambda state: deep_key(state, player))
add_rule(world.get_location("Sage Labyrinth: Sage Right Leg", player),
- lambda state: state._hylics2_has_deep_key(player))
+ lambda state: deep_key(state, player))
# Sage Airship
add_rule(world.get_location("Sage Airship: TV", player),
- lambda state: state._hylics2_has_tokens(player))
+ lambda state: all_tokens(state, player))
# Hylemxylem
add_rule(world.get_location("Hylemxylem: Upper Chamber Banana", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Across Upper Reservoir Chest", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Chest", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 1", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 2", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 1", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 2", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 3", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Sarcophagus", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 1", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 2", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 3", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Upper Reservoir Hole Key", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
# extra rules if Extra Items in Logic is enabled
if world.extra_items_in_logic[player]:
for i in world.get_region("Foglast", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_charge_up(player))
+ add_rule(i, lambda state: charge_up(state, player))
for i in world.get_region("Sage Airship", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player) and\
- state._hylics2_has_worm_room_key(player))
+ add_rule(i, lambda state: (
+ charge_up(state, player)
+ and paper_cup(state, player)
+ and worm_room_key(state, player)
+ ))
for i in world.get_region("Hylemxylem", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player))
+ add_rule(i, lambda state: (
+ charge_up(state, player)
+ and paper_cup(state, player)
+ ))
add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player),
- lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player))
+ lambda state: (
+ charge_up(state, player)
+ and paper_cup(state, player)
+ ))
# extra rules if Shuffle Party Members is enabled
if world.party_shuffle[player]:
for i in world.get_region("Arcade Island", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_3_members(player))
+ add_rule(i, lambda state: party_3(state, player))
for i in world.get_region("Foglast", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_3_members(player) or\
- (state._hylics2_has_2_members(player) and state._hylics2_has_jail_key(player)))
+ add_rule(i, lambda state: (
+ party_3(state, player)
+ or (
+ party_2(state, player)
+ and jail_key(state, player)
+ )
+ ))
for i in world.get_region("Sage Airship", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_3_members(player))
+ add_rule(i, lambda state: party_3(state, player))
for i in world.get_region("Hylemxylem", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_3_members(player))
+ add_rule(i, lambda state: party_3(state, player))
add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player),
- lambda state: state._hylics2_has_2_members(player))
+ lambda state: party_2(state, player))
add_rule(world.get_location("New Muldul: Rescued Blerol 1", player),
- lambda state: state._hylics2_has_2_members(player))
+ lambda state: party_2(state, player))
add_rule(world.get_location("New Muldul: Rescued Blerol 2", player),
- lambda state: state._hylics2_has_2_members(player))
+ lambda state: party_2(state, player))
add_rule(world.get_location("New Muldul: Vault Left Chest", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("New Muldul: Vault Right Chest", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("New Muldul: Vault Bomb", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("Juice Ranch: Battle with Somsnosa", player),
- lambda state: state._hylics2_has_2_members(player))
+ lambda state: party_2(state, player))
add_rule(world.get_location("Juice Ranch: Somsnosa Joins", player),
- lambda state: state._hylics2_has_2_members(player))
+ lambda state: party_2(state, player))
add_rule(world.get_location("Airship: Talk to Somsnosa", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
# extra rules if Shuffle Red Medallions is enabled
if world.medallion_shuffle[player]:
add_rule(world.get_location("New Muldul: Upper House Medallion", player),
- lambda state: state._hylics2_has_upper_house_key(player))
+ lambda state: upper_house_key(state, player))
add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player),
- lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ enter_foglast(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player),
- lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ enter_foglast(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("New Muldul: Vault Center Medallion", player),
- lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ enter_foglast(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player),
- lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ enter_foglast(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player),
- lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ enter_foglast(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("Viewax's Edifice: Fort Wall Medallion", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Jar Medallion", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Sage Chair Medallion", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Arcade 1: Lonely Medallion", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Alcove Medallion", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Foglast: Under Lair Medallion", player),
- lambda state: state._hylics2_has_bridge_key(player))
+ lambda state: bridge_key(state, player))
add_rule(world.get_location("Foglast: Mid-Air Medallion", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Foglast: Top of Tower Medallion", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Medallion", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
- # extra rules is Shuffle Red Medallions and Party Shuffle are enabled
+ # extra rules if Shuffle Red Medallions and Party Shuffle are enabled
if world.party_shuffle[player] and world.medallion_shuffle[player]:
add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("New Muldul: Vault Center Medallion", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
# entrances
for i in world.get_region("Airship", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Arcade Island", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player) and state._hylics2_can_air_dash(player))
+ add_rule(i, lambda state: (
+ airship(state, player)
+ and air_dash(state, player)
+ ))
for i in world.get_region("Worm Pod", player).entrances:
- add_rule(i, lambda state: state._hylics2_enter_wormpod(player))
+ add_rule(i, lambda state: enter_wormpod(state, player))
for i in world.get_region("Foglast", player).entrances:
- add_rule(i, lambda state: state._hylics2_enter_foglast(player))
+ add_rule(i, lambda state: enter_foglast(state, player))
for i in world.get_region("Sage Labyrinth", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_skull_bomb(player))
+ add_rule(i, lambda state: skull_bomb(state, player))
for i in world.get_region("Sage Airship", player).entrances:
- add_rule(i, lambda state: state._hylics2_enter_sageship(player))
+ add_rule(i, lambda state: enter_sageship(state, player))
for i in world.get_region("Hylemxylem", player).entrances:
- add_rule(i, lambda state: state._hylics2_enter_hylemxylem(player))
+ add_rule(i, lambda state: enter_hylemxylem(state, player))
# random start logic (default)
if ((not world.random_start[player]) or \
(world.random_start[player] and hylics2world.start_location == "Waynehouse")):
# entrances
for i in world.get_region("Viewax", player).entrances:
- add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
+ add_rule(i, lambda state: (
+ air_dash(state, player)
+ and airship(state, player)
+ ))
for i in world.get_region("TV Island", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Shield Facility", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Juice Ranch", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
# random start logic (Viewax's Edifice)
elif (world.random_start[player] and hylics2world.start_location == "Viewax's Edifice"):
for i in world.get_region("Waynehouse", player).entrances:
- add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
+ add_rule(i, lambda state: (
+ air_dash(state, player)
+ or airship(state, player)
+ ))
for i in world.get_region("New Muldul", player).entrances:
- add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
+ add_rule(i, lambda state: (
+ air_dash(state, player)
+ or airship(state, player)
+ ))
for i in world.get_region("New Muldul Vault", player).entrances:
- add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
+ add_rule(i, lambda state: (
+ air_dash(state, player)
+ or airship(state, player)
+ ))
for i in world.get_region("Drill Castle", player).entrances:
- add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
+ add_rule(i, lambda state: (
+ air_dash(state, player)
+ or airship(state, player)
+ ))
for i in world.get_region("TV Island", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Shield Facility", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Juice Ranch", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Sage Labyrinth", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
# random start logic (TV Island)
elif (world.random_start[player] and hylics2world.start_location == "TV Island"):
for i in world.get_region("Waynehouse", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("New Muldul", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("New Muldul Vault", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Drill Castle", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Viewax", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Shield Facility", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Juice Ranch", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Sage Labyrinth", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
# random start logic (Shield Facility)
elif (world.random_start[player] and hylics2world.start_location == "Shield Facility"):
for i in world.get_region("Waynehouse", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("New Muldul", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("New Muldul Vault", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Drill Castle", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Viewax", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("TV Island", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Sage Labyrinth", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
\ No newline at end of file
diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py
index f721fb474923..19d901bf5a05 100644
--- a/worlds/hylics2/__init__.py
+++ b/worlds/hylics2/__init__.py
@@ -130,11 +130,11 @@ def pre_fill(self):
tvs = list(Locations.tv_location_table.items())
# if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get
- # placed at Sage Airship: TV
+ # placed at Sage Airship: TV or Foglast: TV
if self.multiworld.extra_items_in_logic[self.player]:
tv = self.multiworld.random.choice(tvs)
gest = gestures.index((200681, Items.gesture_item_table[200681]))
- while tv[1]["name"] == "Sage Airship: TV":
+ while tv[1]["name"] == "Sage Airship: TV" or tv[1]["name"] == "Foglast: TV":
tv = self.multiworld.random.choice(tvs)
self.multiworld.get_location(tv[1]["name"], self.player)\
.place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"],
@@ -146,7 +146,7 @@ def pre_fill(self):
gest = self.multiworld.random.choice(gestures)
tv = self.multiworld.random.choice(tvs)
self.multiworld.get_location(tv[1]["name"], self.player)\
- .place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[1]))
+ .place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[0]))
gestures.remove(gest)
tvs.remove(tv)
@@ -232,8 +232,10 @@ def create_regions(self) -> None:
# create location for beating the game and place Victory event there
loc = Location(self.player, "Defeat Gibby", None, self.multiworld.get_region("Hylemxylem", self.player))
loc.place_locked_item(self.create_event("Victory"))
- set_rule(loc, lambda state: state._hylics2_has_upper_chamber_key(self.player)
- and state._hylics2_has_vessel_room_key(self.player))
+ set_rule(loc, lambda state: (
+ state.has("UPPER CHAMBER KEY", self.player)
+ and state.has("VESSEL ROOM KEY", self.player)
+ ))
self.multiworld.get_region("Hylemxylem", self.player).locations.append(loc)
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py
index 6c89db3891b5..c7b127ef2b54 100644
--- a/worlds/ladx/Locations.py
+++ b/worlds/ladx/Locations.py
@@ -124,13 +124,13 @@ def get(self, item, default):
# Don't allow any money usage if you can't get back wasted rupees
if item == "RUPEES":
if can_farm_rupees(self.state, self.player):
- return self.state.prog_items["RUPEES", self.player]
+ return self.state.prog_items[self.player]["RUPEES"]
return 0
elif item.endswith("_USED"):
return 0
else:
item = ladxr_item_to_la_item_name[item]
- return self.state.prog_items.get((item, self.player), default)
+ return self.state.prog_items[self.player].get(item, default)
class LinksAwakeningEntrance(Entrance):
@@ -219,7 +219,7 @@ def print_items(n):
r = LinksAwakeningRegion(
name=name, ladxr_region=l, hint="", player=player, world=multiworld)
- r.locations = [LinksAwakeningLocation(player, r, i) for i in l.items]
+ r.locations += [LinksAwakeningLocation(player, r, i) for i in l.items]
regions[l] = r
for ladxr_location in logic.location_list:
diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py
index 1d6c85dd6449..eaaea5be2f67 100644
--- a/worlds/ladx/__init__.py
+++ b/worlds/ladx/__init__.py
@@ -231,9 +231,7 @@ def create_items(self) -> None:
# Find instrument, lock
# TODO: we should be able to pinpoint the region we want, save a lookup table please
found = False
- for r in self.multiworld.get_regions():
- if r.player != self.player:
- continue
+ for r in self.multiworld.get_regions(self.player):
if r.dungeon_index != item.item_data.dungeon_index:
continue
for loc in r.locations:
@@ -269,10 +267,7 @@ def create_items(self) -> None:
event_location.place_locked_item(self.create_event("Can Play Trendy Game"))
self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []]
- for r in self.multiworld.get_regions():
- if r.player != self.player:
- continue
-
+ for r in self.multiworld.get_regions(self.player):
# Set aside dungeon locations
if r.dungeon_index:
self.dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations
@@ -518,7 +513,7 @@ def collect(self, state, item: Item) -> bool:
change = super().collect(state, item)
if change:
rupees = self.rupees.get(item.name, 0)
- state.prog_items["RUPEES", item.player] += rupees
+ state.prog_items[item.player]["RUPEES"] += rupees
return change
@@ -526,6 +521,6 @@ def remove(self, state, item: Item) -> bool:
change = super().remove(state, item)
if change:
rupees = self.rupees.get(item.name, 0)
- state.prog_items["RUPEES", item.player] -= rupees
+ state.prog_items[item.player]["RUPEES"] -= rupees
return change
diff --git a/worlds/ladx/docs/setup_en.md b/worlds/ladx/docs/setup_en.md
index 538d70d45e4a..e21c5bddc489 100644
--- a/worlds/ladx/docs/setup_en.md
+++ b/worlds/ladx/docs/setup_en.md
@@ -40,7 +40,7 @@ your personal settings and export a config file from them.
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the
-[YAML Validator](/mysterycheck) page.
+[YAML Validator](/check) page.
## Generating a Single-Player Game
diff --git a/worlds/lingo/LL1.yaml b/worlds/lingo/LL1.yaml
new file mode 100644
index 000000000000..7ae015dc6432
--- /dev/null
+++ b/worlds/lingo/LL1.yaml
@@ -0,0 +1,7505 @@
+---
+ # This file is an associative array where the keys are region names. Rooms
+ # have four properties: entrances, panels, doors, and paintings.
+ #
+ # entrances is an array of regions from which this room can be accessed. The
+ # key of each entry is the room that can access this one. The value is a list
+ # of OR'd requirements for being able to access this room from the other one,
+ # although the list can be elided if there is only one requirement, and the
+ # value True can be used if there are no requirements (i.e. you always have
+ # access to this room if you have access to the other). Each requirement
+ # describes a door that must be opened in order to access this room from the
+ # other. The door is described by both the door's name and the name of the
+ # room that the door is in. The room name may be omitted if the door is
+ # located in the current room.
+ #
+ # panels is an array of panels in the room. The key of the array is an
+ # arbitrary name for the panel. Panels can have the following fields:
+ # - id: The internal ID of the panel in the LINGO map
+ # - required_room: In addition to having access to this room, the player must
+ # also have access to this other room in order to solve this
+ # panel.
+ # - required_door: In addition to having access to this room, the player must
+ # also have this door opened in order to solve this panel.
+ # - required_panel: In addition to having access to this room, the player must
+ # also be able to access this other panel in order to solve
+ # this panel.
+ # - colors: A list of colors that are required to be unlocked in order
+ # to solve this panel
+ # - check: A location check will be created for this individual panel.
+ # - exclude_reduce: Panel checks are assumed to be INCLUDED when reduce checks
+ # is on. This option excludes the check anyway.
+ # - tag: Label that describes how panel randomization should be
+ # done. In reorder mode, panels with the same tag can be
+ # shuffled amongst themselves. "forbid" is a special value
+ # meaning that no randomization should be done. This field is
+ # mandatory.
+ # - link: Panels with the same link label are randomized as a group.
+ # - subtag: Used to identify the separate parts of a linked group.
+ # - copy_to_sign: When randomizing this panel, the hint should be copied to
+ # the specified sign(s).
+ # - achievement: The name of the achievement that is received upon solving
+ # this panel.
+ # - non_counting: If True, this panel does not contribute to the total needed
+ # to unlock Level 2.
+ #
+ # doors is an array of doors associated with this room. When door
+ # randomization is enabled, each of these is an item. The key is a name that
+ # will be displayed as part of the item's name. Doors can have the following
+ # fields:
+ # - id: A string or list of internal door IDs from the LINGO map.
+ # In door shuffle mode, collecting the item generated for
+ # this door will open the doors listed here.
+ # - painting_id: An internal ID of a painting that should be moved upon
+ # receiving this door.
+ # - panels: These are the panels that canonically open this door. If
+ # there is only one panel for the door, then that panel is a
+ # check. If there is more than one panel, then that entire
+ # set of panels must be solved for a check. Panels can
+ # either be a string (representing a panel in this room) or
+ # a dict containing "room" and "panel".
+ # - item_name: Overrides the name of the item generated for this door.
+ # If not specified, the item name will be generated from
+ # the room name and the door name.
+ # - location_name: Overrides the name of the location generated for this
+ # door. If not specified, the location name will be
+ # generated using the names of the panels.
+ # - skip_location: If true, no location is generated for this door.
+ # - skip_item: If true, no item is generated for this door.
+ # - group: When simple doors is used, all doors with the same group
+ # will be covered by a single item.
+ # - include_reduce: Door checks are assumed to be EXCLUDED when reduce checks
+ # is on. This option includes the check anyway.
+ # - junk_item: If on, the item for this door will be considered a junk
+ # item instead of a progression item. Only use this for
+ # doors that could never gate progression regardless of
+ # options and state.
+ # - event: Denotes that the door is event only. This is similar to
+ # setting both skip_location and skip_item.
+ #
+ # paintings is an array of paintings in the room. This is used for painting
+ # shuffling.
+ # - id: The internal painting ID from the LINGO map.
+ # - enter_only: If true, painting shuffling will not place a warp exit on
+ # this painting.
+ # - exit_only: If true, painting shuffling will not place a warp entrance
+ # on this painting.
+ # - orientation: One of north/south/east/west. This is the direction that
+ # the player is facing when they are interacting with it,
+ # not the orientation of the painting itself. "North" is
+ # the direction the player faces at a new game, with the
+ # positive X axis to the right.
+ # - required_door: This door must be open for the painting to be usable as an
+ # entrance. If required_door is set, enter_only must be
+ # True.
+ # - required: Marks a painting as being the only entrance for a room,
+ # and thus it is required to be an exit when randomized.
+ # Use "required_when_no_doors" instead if it would be
+ # possible to enter the room without the painting in door
+ # shuffle mode.
+ # - move: Denotes that the painting is able to move.
+ Starting Room:
+ entrances:
+ Menu: True
+ panels:
+ HI:
+ id: Entry Room/Panel_hi_hi
+ tag: midwhite
+ HIDDEN:
+ id: Entry Room/Panel_hidden_hidden
+ tag: midwhite
+ TYPE:
+ id: Entry Room/Panel_type_type
+ tag: midwhite
+ THIS:
+ id: Entry Room/Panel_this_this
+ tag: midwhite
+ WRITE:
+ id: Entry Room/Panel_write_write
+ tag: midwhite
+ SAME:
+ id: Entry Room/Panel_same_same
+ tag: midwhite
+ doors:
+ Main Door:
+ event: True
+ panels:
+ - HI
+ Back Right Door:
+ id: Entry Room Area Doors/Door_hidden_hidden
+ include_reduce: True
+ panels:
+ - HIDDEN
+ Rhyme Room Entrance:
+ id:
+ - Palindrome Room Area Doors/Door_level_level_2
+ - Palindrome Room Area Doors/Door_racecar_racecar_2
+ - Palindrome Room Area Doors/Door_solos_solos_2
+ skip_location: True
+ group: Rhyme Room Doors
+ panels:
+ - room: The Tenacious
+ panel: LEVEL (Black)
+ - room: The Tenacious
+ panel: RACECAR (Black)
+ - room: The Tenacious
+ panel: SOLOS (Black)
+ paintings:
+ - id: arrows_painting
+ exit_only: True
+ orientation: south
+ - id: arrows_painting2
+ disable: True
+ move: True
+ - id: arrows_painting3
+ disable: True
+ move: True
+ - id: garden_painting_tower2
+ enter_only: True
+ orientation: north
+ move: True
+ required_door:
+ room: Hedge Maze
+ door: Painting Shortcut
+ - id: flower_painting_8
+ enter_only: True
+ orientation: north
+ move: True
+ required_door:
+ room: Courtyard
+ door: Painting Shortcut
+ - id: symmetry_painting_a_starter
+ enter_only: True
+ orientation: west
+ move: True
+ required_door:
+ room: The Wondrous (Doorknob)
+ door: Painting Shortcut
+ - id: pencil_painting6
+ enter_only: True
+ orientation: east
+ move: True
+ required_door:
+ room: Outside The Bold
+ door: Painting Shortcut
+ - id: blueman_painting_3
+ enter_only: True
+ orientation: east
+ move: True
+ required_door:
+ room: Outside The Undeterred
+ door: Painting Shortcut
+ - id: eyes_yellow_painting2
+ enter_only: True
+ orientation: west
+ move: True
+ required_door:
+ room: Outside The Agreeable
+ door: Painting Shortcut
+ Hidden Room:
+ entrances:
+ Starting Room:
+ room: Starting Room
+ door: Back Right Door
+ The Seeker:
+ door: Seeker Entrance
+ Dead End Area:
+ door: Dead End Door
+ Knight Night (Outer Ring):
+ door: Knight Night Entrance
+ panels:
+ DEAD END:
+ id: Appendix Room/Panel_deadend_deadened
+ check: True
+ exclude_reduce: True
+ tag: topwhite
+ OPEN:
+ id: Heteronym Room/Panel_entrance_entrance
+ tag: midwhite
+ LIES:
+ id: Appendix Room/Panel_lies_lies
+ tag: midwhite
+ doors:
+ Dead End Door:
+ id: Appendix Room Area Doors/Door_rat_tar_2
+ skip_location: true
+ group: Dead End Area Access
+ panels:
+ - room: Hub Room
+ panel: RAT
+ Knight Night Entrance:
+ id: Appendix Room Area Doors/Door_rat_tar_4
+ skip_location: true
+ panels:
+ - room: Hub Room
+ panel: RAT
+ Seeker Entrance:
+ id: Entry Room Area Doors/Door_entrance_entrance
+ item_name: The Seeker - Entrance
+ panels:
+ - OPEN
+ Rhyme Room Entrance:
+ id:
+ - Appendix Room Area Doors/Door_rat_tar_3
+ - Double Room Area Doors/Door_room_entry_stairs
+ skip_location: True
+ group: Rhyme Room Doors
+ panels:
+ - room: The Tenacious
+ panel: LEVEL (Black)
+ - room: The Tenacious
+ panel: RACECAR (Black)
+ - room: The Tenacious
+ panel: SOLOS (Black)
+ - room: Hub Room
+ panel: RAT
+ paintings:
+ - id: owl_painting
+ orientation: north
+ The Seeker:
+ entrances:
+ Hidden Room:
+ room: Hidden Room
+ door: Seeker Entrance
+ Pilgrim Room:
+ room: Pilgrim Room
+ door: Shortcut to The Seeker
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_seeker_seeker
+ required_room: Hidden Room
+ tag: forbid
+ check: True
+ achievement: The Seeker
+ BEAR:
+ id: Heteronym Room/Panel_bear_bear
+ tag: midwhite
+ MINE:
+ id: Heteronym Room/Panel_mine_mine
+ tag: double midwhite
+ subtag: left
+ link: exact MINE
+ MINE (2):
+ id: Heteronym Room/Panel_mine_mine_2
+ tag: double midwhite
+ subtag: right
+ link: exact MINE
+ BOW:
+ id: Heteronym Room/Panel_bow_bow
+ tag: midwhite
+ DOES:
+ id: Heteronym Room/Panel_does_does
+ tag: midwhite
+ MOBILE:
+ id: Heteronym Room/Panel_mobile_mobile
+ tag: double midwhite
+ subtag: left
+ link: exact MOBILE
+ MOBILE (2):
+ id: Heteronym Room/Panel_mobile_mobile_2
+ tag: double midwhite
+ subtag: right
+ link: exact MOBILE
+ DESERT:
+ id: Heteronym Room/Panel_desert_desert
+ tag: topmid white stack
+ subtag: mid
+ link: topmid DESERT
+ DESSERT:
+ id: Heteronym Room/Panel_desert_dessert
+ tag: topmid white stack
+ subtag: top
+ link: topmid DESERT
+ SOW:
+ id: Heteronym Room/Panel_sow_sow
+ tag: topmid white stack
+ subtag: mid
+ link: topmid SOW
+ SEW:
+ id: Heteronym Room/Panel_sow_so
+ tag: topmid white stack
+ subtag: top
+ link: topmid SOW
+ TO:
+ id: Heteronym Room/Panel_two_to
+ tag: double topwhite
+ subtag: left
+ link: hp TWO
+ TOO:
+ id: Heteronym Room/Panel_two_too
+ tag: double topwhite
+ subtag: right
+ link: hp TWO
+ WRITE:
+ id: Heteronym Room/Panel_write_right
+ tag: topwhite
+ EWE:
+ id: Heteronym Room/Panel_you_ewe
+ tag: topwhite
+ KNOT:
+ id: Heteronym Room/Panel_not_knot
+ tag: double topwhite
+ subtag: left
+ link: hp NOT
+ NAUGHT:
+ id: Heteronym Room/Panel_not_naught
+ tag: double topwhite
+ subtag: right
+ link: hp NOT
+ BEAR (2):
+ id: Heteronym Room/Panel_bear_bare
+ tag: topwhite
+ Second Room:
+ entrances:
+ Starting Room:
+ room: Starting Room
+ door: Main Door
+ Hub Room:
+ door: Exit Door
+ panels:
+ HI:
+ id: Entry Room/Panel_hi_high
+ tag: topwhite
+ LOW:
+ id: Entry Room/Panel_low_low
+ tag: forbid # This is a midwhite pretending to be a botwhite
+ ANOTHER TRY:
+ id: Entry Room/Panel_advance
+ tag: topwhite
+ LEVEL 2:
+ # We will set up special rules for this in code.
+ id: EndPanel/Panel_level_2
+ tag: forbid
+ non_counting: True
+ check: True
+ required_panel:
+ - panel: ANOTHER TRY
+ doors:
+ Exit Door:
+ id: Entry Room Area Doors/Door_hi_high
+ location_name: Second Room - Good Luck
+ include_reduce: True
+ panels:
+ - HI
+ - LOW
+ Hub Room:
+ entrances:
+ Second Room:
+ room: Second Room
+ door: Exit Door
+ Dead End Area:
+ door: Near RAT Door
+ Crossroads:
+ door: Crossroads Entrance
+ The Tenacious:
+ door: Tenacious Entrance
+ Warts Straw Area:
+ door: Symmetry Door
+ Hedge Maze:
+ door: Shortcut to Hedge Maze
+ Orange Tower First Floor:
+ room: Orange Tower First Floor
+ door: Shortcut to Hub Room
+ Owl Hallway:
+ painting: True
+ Outside The Initiated:
+ room: Outside The Initiated
+ door: Shortcut to Hub Room
+ The Traveled:
+ door: Traveled Entrance
+ Roof: True # through the sunwarp
+ Outside The Undeterred: # (NOTE: used in hardcoded pilgrimage)
+ room: Outside The Undeterred
+ door: Green Painting
+ painting: True
+ panels:
+ ORDER:
+ id: Shuffle Room/Panel_order_chaos
+ colors: black
+ tag: botblack
+ SLAUGHTER:
+ id: Palindrome Room/Panel_slaughter_laughter
+ colors: red
+ tag: midred
+ NEAR:
+ id: Symmetry Room/Panel_near_far
+ colors: black
+ tag: botblack
+ FAR:
+ id: Symmetry Room/Panel_far_near
+ colors: black
+ tag: botblack
+ TRACE:
+ id: Maze Room/Panel_trace_trace
+ tag: midwhite
+ RAT:
+ id: Appendix Room/Panel_rat_tar
+ colors: black
+ check: True
+ exclude_reduce: True
+ tag: midblack
+ OPEN:
+ id: Synonym Room/Panel_open_open
+ tag: midwhite
+ FOUR:
+ id: Backside Room/Panel_four_four_3
+ tag: midwhite
+ required_door:
+ room: Outside The Undeterred
+ door: Fours
+ LOST:
+ id: Shuffle Room/Panel_lost_found
+ colors: black
+ tag: botblack
+ FORWARD:
+ id: Entry Room/Panel_forward_forward
+ tag: midwhite
+ BETWEEN:
+ id: Entry Room/Panel_between_between
+ tag: midwhite
+ BACKWARD:
+ id: Entry Room/Panel_backward_backward
+ tag: midwhite
+ doors:
+ Crossroads Entrance:
+ id: Shuffle Room Area Doors/Door_chaos
+ panels:
+ - ORDER
+ Tenacious Entrance:
+ id: Palindrome Room Area Doors/Door_slaughter_laughter
+ group: Entrances to The Tenacious
+ panels:
+ - SLAUGHTER
+ Symmetry Door:
+ id:
+ - Symmetry Room Area Doors/Door_near_far
+ - Symmetry Room Area Doors/Door_far_near
+ group: Symmetry Doors
+ panels:
+ - NEAR
+ - FAR
+ Shortcut to Hedge Maze:
+ id: Maze Area Doors/Door_trace_trace
+ group: Hedge Maze Doors
+ panels:
+ - TRACE
+ Near RAT Door:
+ id: Appendix Room Area Doors/Door_deadend_deadened
+ skip_location: True
+ group: Dead End Area Access
+ panels:
+ - room: Hidden Room
+ panel: DEAD END
+ Traveled Entrance:
+ id: Appendix Room Area Doors/Door_open_open
+ item_name: The Traveled - Entrance
+ group: Entrance to The Traveled
+ panels:
+ - OPEN
+ Lost Door:
+ id: Shuffle Room Area Doors/Door_lost_found
+ junk_item: True
+ panels:
+ - LOST
+ paintings:
+ - id: maze_painting
+ orientation: west
+ Dead End Area:
+ entrances:
+ Hidden Room:
+ room: Hidden Room
+ door: Dead End Door
+ Hub Room:
+ room: Hub Room
+ door: Near RAT Door
+ panels:
+ FOUR:
+ id: Backside Room/Panel_four_four_2
+ tag: midwhite
+ required_door:
+ room: Outside The Undeterred
+ door: Fours
+ EIGHT:
+ id: Backside Room/Panel_eight_eight_8
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Eights
+ paintings:
+ - id: smile_painting_6
+ orientation: north
+ Pilgrim Antechamber:
+ # Let's not shuffle the paintings yet.
+ entrances:
+ # The pilgrimage is hardcoded in rules.py
+ Starting Room:
+ door: Sun Painting
+ panels:
+ HOT CRUST:
+ id: Lingo Room/Panel_shortcut
+ colors: yellow
+ tag: midyellow
+ PILGRIMAGE:
+ id: Lingo Room/Panel_pilgrim
+ colors: blue
+ tag: midblue
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery14
+ tag: midwhite
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ doors:
+ Sun Painting:
+ item_name: Pilgrim Room - Sun Painting
+ location_name: Pilgrim Room - HOT CRUST
+ painting_id: pilgrim_painting2
+ panels:
+ - HOT CRUST
+ Exit:
+ event: True
+ panels:
+ - PILGRIMAGE
+ Pilgrim Room:
+ entrances:
+ The Seeker:
+ door: Shortcut to The Seeker
+ Pilgrim Antechamber:
+ room: Pilgrim Antechamber
+ door: Exit
+ panels:
+ THIS:
+ id: Lingo Room/Panel_lingo_9
+ colors: gray
+ tag: forbid
+ TIME ROOM:
+ id: Lingo Room/Panel_lingo_1
+ colors: purple
+ tag: toppurp
+ SCIENCE ROOM:
+ id: Lingo Room/Panel_lingo_2
+ tag: botwhite
+ SHINY ROCK ROOM:
+ id: Lingo Room/Panel_lingo_3
+ tag: botwhite
+ ANGRY POWER:
+ id: Lingo Room/Panel_lingo_4
+ colors:
+ - purple
+ tag: forbid
+ MICRO LEGION:
+ id: Lingo Room/Panel_lingo_5
+ colors: yellow
+ tag: midyellow
+ LOSERS RELAX:
+ id: Lingo Room/Panel_lingo_6
+ colors:
+ - black
+ tag: forbid
+ "906234":
+ id: Lingo Room/Panel_lingo_7
+ colors:
+ - orange
+ - blue
+ tag: forbid
+ MOOR EMORDNILAP:
+ id: Lingo Room/Panel_lingo_8
+ colors: black
+ tag: midblack
+ HALL ROOMMATE:
+ id: Lingo Room/Panel_lingo_10
+ colors:
+ - red
+ - blue
+ tag: forbid
+ ALL GREY:
+ id: Lingo Room/Panel_lingo_11
+ colors: yellow
+ tag: midyellow
+ PLUNDER ISLAND:
+ id: Lingo Room/Panel_lingo_12
+ colors:
+ - purple
+ - red
+ tag: forbid
+ FLOSS PATHS:
+ id: Lingo Room/Panel_lingo_13
+ colors:
+ - purple
+ - brown
+ tag: forbid
+ doors:
+ Shortcut to The Seeker:
+ id: Master Room Doors/Door_pilgrim_shortcut
+ include_reduce: True
+ panels:
+ - THIS
+ Crossroads:
+ entrances:
+ Hub Room: True # The sunwarp means that we never need the ORDER door
+ Color Hallways: True
+ The Tenacious:
+ door: Tenacious Entrance
+ Orange Tower Fourth Floor: True # through IRK HORN
+ Amen Name Area:
+ room: Lost Area
+ door: Exit
+ Roof: True # through the sunwarp
+ panels:
+ DECAY:
+ id: Palindrome Room/Panel_decay_day
+ colors: red
+ tag: midred
+ NOPE:
+ id: Sun Room/Panel_nope_open
+ colors: yellow
+ tag: midyellow
+ EIGHT:
+ id: Backside Room/Panel_eight_eight_5
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Eights
+ WE ROT:
+ id: Shuffle Room/Panel_tower
+ colors: yellow
+ tag: midyellow
+ WORDS:
+ id: Shuffle Room/Panel_words_sword
+ colors: yellow
+ tag: midyellow
+ SWORD:
+ id: Shuffle Room/Panel_sword_words
+ colors: yellow
+ tag: midyellow
+ TURN:
+ id: Shuffle Room/Panel_turn_runt
+ colors: yellow
+ tag: midyellow
+ BEND HI:
+ id: Shuffle Room/Panel_behind
+ colors: yellow
+ tag: midyellow
+ THE EYES:
+ id: Shuffle Room/Panel_eyes_see_shuffle
+ colors: yellow
+ check: True
+ exclude_reduce: True
+ required_door:
+ door: Hollow Hallway
+ tag: midyellow
+ CORNER:
+ id: Shuffle Room/Panel_corner_corner
+ required_door:
+ door: Hollow Hallway
+ tag: midwhite
+ HOLLOW:
+ id: Shuffle Room/Panel_hollow_hollow
+ required_door:
+ door: Hollow Hallway
+ tag: midwhite
+ SWAP:
+ id: Shuffle Room/Panel_swap_wasp
+ colors: yellow
+ tag: midyellow
+ GEL:
+ id: Shuffle Room/Panel_gel
+ colors: yellow
+ tag: topyellow
+ required_door:
+ door: Tower Entrance
+ THOUGH:
+ id: Shuffle Room/Panel_though
+ colors: yellow
+ tag: topyellow
+ required_door:
+ door: Tower Entrance
+ CROSSROADS:
+ id: Shuffle Room/Panel_crossroads_crossroads
+ tag: midwhite
+ doors:
+ Tenacious Entrance:
+ id: Palindrome Room Area Doors/Door_decay_day
+ group: Entrances to The Tenacious
+ panels:
+ - DECAY
+ Discerning Entrance:
+ id: Shuffle Room Area Doors/Door_nope_open
+ item_name: The Discerning - Entrance
+ panels:
+ - NOPE
+ Tower Entrance:
+ id:
+ - Shuffle Room Area Doors/Door_tower
+ - Shuffle Room Area Doors/Door_tower2
+ - Shuffle Room Area Doors/Door_tower3
+ - Shuffle Room Area Doors/Door_tower4
+ group: Crossroads - Tower Entrances
+ panels:
+ - WE ROT
+ Tower Back Entrance:
+ id: Shuffle Room Area Doors/Door_runt
+ location_name: Crossroads - TURN/RUNT
+ group: Crossroads - Tower Entrances
+ panels:
+ - TURN
+ - room: Orange Tower Fourth Floor
+ panel: RUNT
+ Words Sword Door:
+ id:
+ - Shuffle Room Area Doors/Door_words_shuffle_3
+ - Shuffle Room Area Doors/Door_words_shuffle_4
+ group: Crossroads Doors
+ panels:
+ - WORDS
+ - SWORD
+ Eye Wall:
+ id: Shuffle Room Area Doors/Door_behind
+ junk_item: True
+ group: Crossroads Doors
+ panels:
+ - BEND HI
+ Hollow Hallway:
+ id: Shuffle Room Area Doors/Door_crossroads6
+ skip_location: True
+ group: Crossroads Doors
+ panels:
+ - BEND HI
+ Roof Access:
+ id: Tower Room Area Doors/Door_level_6_2
+ skip_location: True
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ - room: Outside The Undeterred
+ panel: ART + ART
+ - room: Orange Tower Third Floor
+ panel: DEER + WREN
+ - room: Orange Tower Fourth Floor
+ panel: LEARNS + UNSEW
+ - room: Orange Tower Fifth Floor
+ panel: DRAWL + RUNS
+ - room: Owl Hallway
+ panel: READS + RUST
+ paintings:
+ - id: eye_painting
+ disable: True
+ orientation: east
+ move: True
+ required_door:
+ door: Eye Wall
+ - id: smile_painting_4
+ orientation: south
+ Lost Area:
+ entrances:
+ Outside The Agreeable:
+ door: Exit
+ Crossroads:
+ room: Crossroads
+ door: Words Sword Door
+ panels:
+ LOST (1):
+ id: Shuffle Room/Panel_lost_lots
+ colors: yellow
+ tag: midyellow
+ LOST (2):
+ id: Shuffle Room/Panel_lost_slot
+ colors: yellow
+ tag: midyellow
+ doors:
+ Exit:
+ id:
+ - Shuffle Room Area Doors/Door_lost_shuffle_1
+ - Shuffle Room Area Doors/Door_lost_shuffle_2
+ location_name: Crossroads - LOST Pair
+ panels:
+ - LOST (1)
+ - LOST (2)
+ Amen Name Area:
+ entrances:
+ Crossroads:
+ room: Lost Area
+ door: Exit
+ Suits Area:
+ door: Exit
+ panels:
+ AMEN:
+ id: Shuffle Room/Panel_amen_mean
+ colors: yellow
+ tag: double midyellow
+ subtag: left
+ link: ana MEAN
+ NAME:
+ id: Shuffle Room/Panel_name_mean
+ colors: yellow
+ tag: double midyellow
+ subtag: right
+ link: ana MEAN
+ NINE:
+ id: Backside Room/Panel_nine_nine_3
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Nines
+ doors:
+ Exit:
+ id: Shuffle Room Area Doors/Door_mean
+ panels:
+ - AMEN
+ - NAME
+ Suits Area:
+ entrances:
+ Amen Name Area:
+ room: Amen Name Area
+ door: Exit
+ Roof: True
+ panels:
+ SPADES:
+ id: Cross Room/Panel_spades_spades
+ tag: midwhite
+ CLUBS:
+ id: Cross Room/Panel_clubs_clubs
+ tag: midwhite
+ HEARTS:
+ id: Cross Room/Panel_hearts_hearts
+ tag: midwhite
+ paintings:
+ - id: west_afar
+ orientation: south
+ The Tenacious:
+ entrances:
+ Hub Room:
+ - room: Hub Room
+ door: Tenacious Entrance
+ - door: Shortcut to Hub Room
+ Crossroads:
+ room: Crossroads
+ door: Tenacious Entrance
+ Outside The Agreeable:
+ room: Outside The Agreeable
+ door: Tenacious Entrance
+ Dread Hallway:
+ room: Dread Hallway
+ door: Tenacious Entrance
+ panels:
+ LEVEL (Black):
+ id: Palindrome Room/Panel_level_level
+ colors: black
+ tag: midblack
+ RACECAR (Black):
+ id: Palindrome Room/Panel_racecar_racecar
+ colors: black
+ tag: palindrome
+ copy_to_sign: sign4
+ SOLOS (Black):
+ id: Palindrome Room/Panel_solos_solos
+ colors: black
+ tag: palindrome
+ copy_to_sign:
+ - sign5
+ - sign6
+ LEVEL (White):
+ id: Palindrome Room/Panel_level_level_2
+ tag: midwhite
+ RACECAR (White):
+ id: Palindrome Room/Panel_racecar_racecar_2
+ tag: midwhite
+ copy_to_sign: sign3
+ SOLOS (White):
+ id: Palindrome Room/Panel_solos_solos_2
+ tag: midwhite
+ copy_to_sign:
+ - sign1
+ - sign2
+ Achievement:
+ id: Countdown Panels/Panel_tenacious_tenacious
+ check: True
+ tag: forbid
+ required_panel:
+ - panel: LEVEL (Black)
+ - panel: RACECAR (Black)
+ - panel: SOLOS (Black)
+ - panel: LEVEL (White)
+ - panel: RACECAR (White)
+ - panel: SOLOS (White)
+ - room: Hub Room
+ panel: SLAUGHTER
+ - room: Crossroads
+ panel: DECAY
+ - room: Outside The Agreeable
+ panel: MASSACRED
+ - room: Dread Hallway
+ panel: DREAD
+ achievement: The Tenacious
+ doors:
+ Shortcut to Hub Room:
+ id:
+ - Palindrome Room Area Doors/Door_level_level_1
+ - Palindrome Room Area Doors/Door_racecar_racecar_1
+ - Palindrome Room Area Doors/Door_solos_solos_1
+ location_name: The Tenacious - Palindromes
+ group: Entrances to The Tenacious
+ panels:
+ - LEVEL (Black)
+ - RACECAR (Black)
+ - SOLOS (Black)
+ White Palindromes:
+ location_name: The Tenacious - White Palindromes
+ skip_item: True
+ panels:
+ - LEVEL (White)
+ - RACECAR (White)
+ - SOLOS (White)
+ Warts Straw Area:
+ entrances:
+ Hub Room:
+ room: Hub Room
+ door: Symmetry Door
+ Leaf Feel Area:
+ door: Door
+ panels:
+ WARTS:
+ id: Symmetry Room/Panel_warts_straw
+ colors: black
+ tag: midblack
+ STRAW:
+ id: Symmetry Room/Panel_straw_warts
+ colors: black
+ tag: midblack
+ doors:
+ Door:
+ id:
+ - Symmetry Room Area Doors/Door_warts_straw
+ - Symmetry Room Area Doors/Door_straw_warts
+ group: Symmetry Doors
+ panels:
+ - WARTS
+ - STRAW
+ Leaf Feel Area:
+ entrances:
+ Warts Straw Area:
+ room: Warts Straw Area
+ door: Door
+ Outside The Agreeable:
+ door: Door
+ panels:
+ LEAF:
+ id: Symmetry Room/Panel_leaf_feel
+ colors: black
+ tag: topblack
+ FEEL:
+ id: Symmetry Room/Panel_feel_leaf
+ colors: black
+ tag: topblack
+ doors:
+ Door:
+ id:
+ - Symmetry Room Area Doors/Door_leaf_feel
+ - Symmetry Room Area Doors/Door_feel_leaf
+ group: Symmetry Doors
+ panels:
+ - LEAF
+ - FEEL
+ Outside The Agreeable:
+ # Let's ignore the blue warp thing for now because the lookout is a dead
+ # end. Later on it could be filler checks.
+ entrances:
+ # We don't have to list Lost Area because of Crossroads.
+ Crossroads: True
+ The Tenacious:
+ door: Tenacious Entrance
+ The Agreeable:
+ door: Agreeable Entrance
+ Dread Hallway:
+ door: Black Door
+ Leaf Feel Area:
+ room: Leaf Feel Area
+ door: Door
+ Starting Room:
+ door: Painting Shortcut
+ painting: True
+ Hallway Room (2): True
+ Hallway Room (3): True
+ Hallway Room (4): True
+ Hedge Maze: True # through the door to the sectioned-off part of the hedge maze
+ panels:
+ MASSACRED:
+ id: Palindrome Room/Panel_massacred_sacred
+ colors: red
+ tag: midred
+ BLACK:
+ id: Symmetry Room/Panel_black_white
+ colors: black
+ tag: botblack
+ CLOSE:
+ id: Antonym Room/Panel_close_open
+ colors: black
+ tag: botblack
+ LEFT:
+ id: Symmetry Room/Panel_left_right
+ colors: black
+ tag: botblack
+ LEFT (2):
+ id: Symmetry Room/Panel_left_wrong
+ colors: black
+ tag: bot black black
+ RIGHT:
+ id: Symmetry Room/Panel_right_left
+ colors: black
+ tag: botblack
+ PURPLE:
+ id: Color Arrow Room/Panel_purple_afar
+ tag: midwhite
+ required_door:
+ door: Purple Barrier
+ FIVE (1):
+ id: Backside Room/Panel_five_five_5
+ tag: midwhite
+ required_door:
+ room: Outside The Undeterred
+ door: Fives
+ FIVE (2):
+ id: Backside Room/Panel_five_five_4
+ tag: midwhite
+ required_door:
+ room: Outside The Undeterred
+ door: Fives
+ OUT:
+ id: Hallway Room/Panel_out_out
+ check: True
+ exclude_reduce: True
+ tag: midwhite
+ HIDE:
+ id: Maze Room/Panel_hide_seek_4
+ colors: black
+ tag: botblack
+ DAZE:
+ id: Maze Room/Panel_daze_maze
+ colors: purple
+ tag: midpurp
+ WALL:
+ id: Hallway Room/Panel_castle_1
+ colors: blue
+ tag: quad bot blue
+ link: qbb CASTLE
+ KEEP:
+ id: Hallway Room/Panel_castle_2
+ colors: blue
+ tag: quad bot blue
+ link: qbb CASTLE
+ BAILEY:
+ id: Hallway Room/Panel_castle_3
+ colors: blue
+ tag: quad bot blue
+ link: qbb CASTLE
+ TOWER:
+ id: Hallway Room/Panel_castle_4
+ colors: blue
+ tag: quad bot blue
+ link: qbb CASTLE
+ NORTH:
+ id: Cross Room/Panel_north_missing
+ colors: green
+ tag: forbid
+ required_room: Outside The Bold
+ DIAMONDS:
+ id: Cross Room/Panel_diamonds_missing
+ colors: green
+ tag: forbid
+ required_room: Suits Area
+ FIRE:
+ id: Cross Room/Panel_fire_missing
+ colors: green
+ tag: forbid
+ required_room: Elements Area
+ WINTER:
+ id: Cross Room/Panel_winter_missing
+ colors: green
+ tag: forbid
+ required_room: Orange Tower Fifth Floor
+ doors:
+ Tenacious Entrance:
+ id: Palindrome Room Area Doors/Door_massacred_sacred
+ group: Entrances to The Tenacious
+ panels:
+ - MASSACRED
+ Black Door:
+ id: Symmetry Room Area Doors/Door_black_white
+ group: Entrances to The Tenacious
+ panels:
+ - BLACK
+ Agreeable Entrance:
+ id: Symmetry Room Area Doors/Door_close_open
+ item_name: The Agreeable - Entrance
+ panels:
+ - CLOSE
+ Painting Shortcut:
+ item_name: Starting Room - Street Painting
+ painting_id: eyes_yellow_painting2
+ panels:
+ - RIGHT
+ Purple Barrier:
+ id: Color Arrow Room Doors/Door_purple_3
+ group: Color Hunt Barriers
+ skip_location: True
+ panels:
+ - room: Champion's Rest
+ panel: PURPLE
+ Hallway Door:
+ id: Red Blue Purple Room Area Doors/Door_room_2
+ group: Hallway Room Doors
+ location_name: Hallway Room - First Room
+ panels:
+ - WALL
+ - KEEP
+ - BAILEY
+ - TOWER
+ paintings:
+ - id: panda_painting
+ orientation: south
+ - id: eyes_yellow_painting
+ orientation: east
+ progression:
+ Progressive Hallway Room:
+ - Hallway Door
+ - room: Hallway Room (2)
+ door: Exit
+ - room: Hallway Room (3)
+ door: Exit
+ - room: Hallway Room (4)
+ door: Exit
+ Dread Hallway:
+ entrances:
+ Outside The Agreeable:
+ room: Outside The Agreeable
+ door: Black Door
+ The Tenacious:
+ door: Tenacious Entrance
+ panels:
+ DREAD:
+ id: Palindrome Room/Panel_dread_dead
+ colors: red
+ tag: midred
+ doors:
+ Tenacious Entrance:
+ id: Palindrome Room Area Doors/Door_dread_dead
+ group: Entrances to The Tenacious
+ panels:
+ - DREAD
+ The Agreeable:
+ entrances:
+ Outside The Agreeable:
+ room: Outside The Agreeable
+ door: Agreeable Entrance
+ Hedge Maze:
+ door: Shortcut to Hedge Maze
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_disagreeable_agreeable
+ colors: black
+ tag: forbid
+ required_room: Outside The Agreeable
+ check: True
+ achievement: The Agreeable
+ BYE:
+ id: Antonym Room/Panel_bye_hi
+ colors: black
+ tag: botblack
+ RETOOL:
+ id: Antonym Room/Panel_retool_looter
+ colors: black
+ tag: midblack
+ DRAWER:
+ id: Antonym Room/Panel_drawer_reward
+ colors: black
+ tag: midblack
+ READ:
+ id: Antonym Room/Panel_read_write
+ colors: black
+ tag: botblack
+ DIFFERENT:
+ id: Antonym Room/Panel_different_same
+ colors: black
+ tag: botblack
+ LOW:
+ id: Antonym Room/Panel_low_high
+ colors: black
+ tag: botblack
+ ALIVE:
+ id: Antonym Room/Panel_alive_dead
+ colors: black
+ tag: botblack
+ THAT:
+ id: Antonym Room/Panel_that_this
+ colors: black
+ tag: botblack
+ STRESSED:
+ id: Antonym Room/Panel_stressed_desserts
+ colors: black
+ tag: midblack
+ STAR:
+ id: Antonym Room/Panel_star_rats
+ colors: black
+ tag: midblack
+ TAME:
+ id: Antonym Room/Panel_tame_mate
+ colors: black
+ tag: topblack
+ CAT:
+ id: Antonym Room/Panel_cat_tack
+ colors: black
+ tag: topblack
+ doors:
+ Shortcut to Hedge Maze:
+ id: Symmetry Room Area Doors/Door_bye_hi
+ group: Hedge Maze Doors
+ panels:
+ - BYE
+ Hedge Maze:
+ entrances:
+ Hub Room:
+ room: Hub Room
+ door: Shortcut to Hedge Maze
+ Color Hallways: True
+ The Agreeable:
+ room: The Agreeable
+ door: Shortcut to Hedge Maze
+ The Perceptive: True
+ The Observant:
+ door: Observant Entrance
+ Owl Hallway:
+ room: Owl Hallway
+ door: Shortcut to Hedge Maze
+ Roof: True
+ panels:
+ DOWN:
+ id: Maze Room/Panel_down_up
+ colors: black
+ tag: botblack
+ HIDE (1):
+ id: Maze Room/Panel_hide_seek
+ colors: black
+ tag: botblack
+ HIDE (2):
+ id: Maze Room/Panel_hide_seek_2
+ colors: black
+ tag: botblack
+ HIDE (3):
+ id: Maze Room/Panel_hide_seek_3
+ colors: black
+ tag: botblack
+ MASTERY (1):
+ id: Master Room/Panel_mastery_mastery5
+ tag: midwhite
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ MASTERY (2):
+ id: Master Room/Panel_mastery_mastery9
+ tag: midwhite
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ PATH (1):
+ id: Maze Room/Panel_path_lock
+ colors: green
+ tag: forbid
+ PATH (2):
+ id: Maze Room/Panel_path_knot
+ colors: green
+ tag: forbid
+ PATH (3):
+ id: Maze Room/Panel_path_lost
+ colors: green
+ tag: forbid
+ PATH (4):
+ id: Maze Room/Panel_path_open
+ colors: green
+ tag: forbid
+ PATH (5):
+ id: Maze Room/Panel_path_help
+ colors: green
+ tag: forbid
+ PATH (6):
+ id: Maze Room/Panel_path_hunt
+ colors: green
+ tag: forbid
+ PATH (7):
+ id: Maze Room/Panel_path_nest
+ colors: green
+ tag: forbid
+ PATH (8):
+ id: Maze Room/Panel_path_look
+ colors: green
+ tag: forbid
+ REFLOW:
+ id: Maze Room/Panel_reflow_flower
+ colors: yellow
+ tag: midyellow
+ LEAP:
+ id: Maze Room/Panel_leap_jump
+ tag: botwhite
+ doors:
+ Perceptive Entrance:
+ id: Maze Area Doors/Door_maze_maze
+ item_name: The Perceptive - Entrance
+ group: Hedge Maze Doors
+ panels:
+ - DOWN
+ Painting Shortcut:
+ painting_id: garden_painting_tower2
+ item_name: Starting Room - Hedge Maze Painting
+ skip_location: True
+ panels:
+ - DOWN
+ Observant Entrance:
+ id:
+ - Maze Area Doors/Door_look_room_1
+ - Maze Area Doors/Door_look_room_2
+ - Maze Area Doors/Door_look_room_3
+ skip_location: True
+ item_name: The Observant - Entrance
+ group: Observant Doors
+ panels:
+ - room: The Perceptive
+ panel: GAZE
+ Hide and Seek:
+ skip_item: True
+ location_name: Hedge Maze - Hide and Seek
+ include_reduce: True
+ panels:
+ - HIDE (1)
+ - HIDE (2)
+ - HIDE (3)
+ - room: Outside The Agreeable
+ panel: HIDE
+ The Perceptive:
+ entrances:
+ Starting Room:
+ room: Hedge Maze
+ door: Painting Shortcut
+ painting: True
+ Hedge Maze:
+ room: Hedge Maze
+ door: Perceptive Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_perceptive_perceptive
+ colors: green
+ tag: forbid
+ check: True
+ achievement: The Perceptive
+ GAZE:
+ id: Maze Room/Panel_look_look
+ check: True
+ exclude_reduce: True
+ tag: botwhite
+ paintings:
+ - id: garden_painting_tower
+ orientation: north
+ The Fearless (First Floor):
+ entrances:
+ The Perceptive: True
+ panels:
+ NAPS:
+ id: Naps Room/Panel_naps_span
+ colors: black
+ tag: midblack
+ TEAM:
+ id: Naps Room/Panel_team_meet
+ colors: black
+ tag: topblack
+ TEEM:
+ id: Naps Room/Panel_teem_meat
+ colors: black
+ tag: topblack
+ IMPATIENT:
+ id: Naps Room/Panel_impatient_doctor
+ colors: black
+ tag: bot black black
+ EAT:
+ id: Naps Room/Panel_eat_tea
+ colors: black
+ tag: topblack
+ doors:
+ Second Floor:
+ id: Naps Room Doors/Door_hider_5
+ location_name: The Fearless - First Floor Puzzles
+ group: Fearless Doors
+ panels:
+ - NAPS
+ - TEAM
+ - TEEM
+ - IMPATIENT
+ - EAT
+ progression:
+ Progressive Fearless:
+ - Second Floor
+ - room: The Fearless (Second Floor)
+ door: Third Floor
+ The Fearless (Second Floor):
+ entrances:
+ The Fearless (First Floor):
+ room: The Fearless (First Floor)
+ door: Second Floor
+ panels:
+ NONE:
+ id: Naps Room/Panel_one_many
+ colors: black
+ tag: bot black top white
+ SUM:
+ id: Naps Room/Panel_one_none
+ colors: black
+ tag: top white bot black
+ FUNNY:
+ id: Naps Room/Panel_funny_enough
+ colors: black
+ tag: topblack
+ MIGHT:
+ id: Naps Room/Panel_might_time
+ colors: black
+ tag: topblack
+ SAFE:
+ id: Naps Room/Panel_safe_face
+ colors: black
+ tag: topblack
+ SAME:
+ id: Naps Room/Panel_same_mace
+ colors: black
+ tag: topblack
+ CAME:
+ id: Naps Room/Panel_came_make
+ colors: black
+ tag: topblack
+ doors:
+ Third Floor:
+ id:
+ - Naps Room Doors/Door_hider_1b2
+ - Naps Room Doors/Door_hider_new1
+ location_name: The Fearless - Second Floor Puzzles
+ group: Fearless Doors
+ panels:
+ - NONE
+ - SUM
+ - FUNNY
+ - MIGHT
+ - SAFE
+ - SAME
+ - CAME
+ The Fearless:
+ entrances:
+ The Fearless (First Floor):
+ room: The Fearless (Second Floor)
+ door: Third Floor
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_fearless_fearless
+ colors: black
+ tag: forbid
+ check: True
+ achievement: The Fearless
+ EASY:
+ id: Naps Room/Panel_easy_soft
+ colors: black
+ tag: bot black black
+ SOMETIMES:
+ id: Naps Room/Panel_sometimes_always
+ colors: black
+ tag: bot black black
+ DARK:
+ id: Naps Room/Panel_dark_extinguish
+ colors: black
+ tag: bot black black
+ EVEN:
+ id: Naps Room/Panel_even_ordinary
+ colors: black
+ tag: bot black black
+ The Observant:
+ entrances:
+ Hedge Maze:
+ room: Hedge Maze
+ door: Observant Entrance
+ The Incomparable: True
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_observant_observant
+ colors: green
+ check: True
+ tag: forbid
+ required_door:
+ door: Stairs
+ achievement: The Observant
+ BACK:
+ id: Look Room/Panel_four_back
+ colors: green
+ tag: forbid
+ SIDE:
+ id: Look Room/Panel_four_side
+ colors: green
+ tag: forbid
+ BACKSIDE:
+ id: Backside Room/Panel_backside_2
+ tag: midwhite
+ required_door:
+ door: Backside Door
+ STAIRS:
+ id: Look Room/Panel_six_stairs
+ colors: green
+ tag: forbid
+ WAYS:
+ id: Look Room/Panel_four_ways
+ colors: green
+ tag: forbid
+ "ON":
+ id: Look Room/Panel_two_on
+ colors: green
+ tag: forbid
+ UP:
+ id: Look Room/Panel_two_up
+ colors: green
+ tag: forbid
+ SWIMS:
+ id: Look Room/Panel_five_swims
+ colors: green
+ tag: forbid
+ UPSTAIRS:
+ id: Look Room/Panel_eight_upstairs
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ TOIL:
+ id: Look Room/Panel_blue_toil
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ STOP:
+ id: Look Room/Panel_four_stop
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ TOP:
+ id: Look Room/Panel_aqua_top
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ HI:
+ id: Look Room/Panel_blue_hi
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ HI (2):
+ id: Look Room/Panel_blue_hi2
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ "31":
+ id: Look Room/Panel_numbers_31
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ "52":
+ id: Look Room/Panel_numbers_52
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ OIL:
+ id: Look Room/Panel_aqua_oil
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ BACKSIDE (GREEN):
+ id: Look Room/Panel_eight_backside
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ SIDEWAYS:
+ id: Look Room/Panel_eight_sideways
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ doors:
+ Backside Door:
+ id: Maze Area Doors/Door_backside
+ group: Backside Doors
+ panels:
+ - BACK
+ - SIDE
+ Stairs:
+ id: Maze Area Doors/Door_stairs
+ group: Observant Doors
+ panels:
+ - STAIRS
+ The Incomparable:
+ entrances:
+ The Observant: True # Assuming that access to The Observant includes access to the right entrance
+ Eight Room: True
+ Eight Alcove:
+ door: Eight Painting
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_incomparable_incomparable
+ colors: blue
+ check: True
+ tag: forbid
+ required_room:
+ - Elements Area
+ - Courtyard
+ - Eight Room
+ achievement: The Incomparable
+ A (One):
+ id: Strand Room/Panel_blank_a
+ colors: blue
+ tag: forbid
+ A (Two):
+ id: Strand Room/Panel_a_an
+ colors: blue
+ tag: forbid
+ A (Three):
+ id: Strand Room/Panel_a_and
+ colors: blue
+ tag: forbid
+ A (Four):
+ id: Strand Room/Panel_a_sand
+ colors: blue
+ tag: forbid
+ A (Five):
+ id: Strand Room/Panel_a_stand
+ colors: blue
+ tag: forbid
+ A (Six):
+ id: Strand Room/Panel_a_strand
+ colors: blue
+ tag: forbid
+ I (One):
+ id: Strand Room/Panel_blank_i
+ colors: blue
+ tag: forbid
+ I (Two):
+ id: Strand Room/Panel_i_in
+ colors: blue
+ tag: forbid
+ I (Three):
+ id: Strand Room/Panel_i_sin
+ colors: blue
+ tag: forbid
+ I (Four):
+ id: Strand Room/Panel_i_sing
+ colors: blue
+ tag: forbid
+ I (Five):
+ id: Strand Room/Panel_i_sting
+ colors: blue
+ tag: forbid
+ I (Six):
+ id: Strand Room/Panel_i_string
+ colors: blue
+ tag: forbid
+ I (Seven):
+ id: Strand Room/Panel_i_strings
+ colors: blue
+ tag: forbid
+ doors:
+ Eight Painting:
+ id: Red Blue Purple Room Area Doors/Door_a_strands
+ location_name: Giant Sevens
+ group: Observant Doors
+ panels:
+ - I (Seven)
+ - room: Courtyard
+ panel: I
+ - room: Elements Area
+ panel: A
+ Eight Alcove:
+ entrances:
+ The Incomparable:
+ room: The Incomparable
+ door: Eight Painting
+ paintings:
+ - id: eight_painting2
+ orientation: north
+ Eight Room:
+ entrances:
+ Eight Alcove:
+ painting: True
+ panels:
+ Eight Back:
+ id: Strand Room/Panel_i_starling
+ colors: blue
+ tag: forbid
+ Eight Front:
+ id: Strand Room/Panel_i_starting
+ colors: blue
+ tag: forbid
+ Nine:
+ id: Strand Room/Panel_i_startling
+ colors: blue
+ tag: forbid
+ paintings:
+ - id: eight_painting
+ orientation: south
+ exit_only: True
+ required: True
+ Orange Tower:
+ # This is a special, meta-ish room.
+ entrances:
+ Menu: True
+ doors:
+ Second Floor:
+ id: Tower Room Area Doors/Door_level_1
+ skip_location: True
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ Third Floor:
+ id: Tower Room Area Doors/Door_level_2
+ skip_location: True
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ - room: Outside The Undeterred
+ panel: ART + ART
+ Fourth Floor:
+ id: Tower Room Area Doors/Door_level_3
+ skip_location: True
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ - room: Outside The Undeterred
+ panel: ART + ART
+ - room: Orange Tower Third Floor
+ panel: DEER + WREN
+ Fifth Floor:
+ id: Tower Room Area Doors/Door_level_4
+ skip_location: True
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ - room: Outside The Undeterred
+ panel: ART + ART
+ - room: Orange Tower Third Floor
+ panel: DEER + WREN
+ - room: Orange Tower Fourth Floor
+ panel: LEARNS + UNSEW
+ Sixth Floor:
+ id: Tower Room Area Doors/Door_level_5
+ skip_location: True
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ - room: Outside The Undeterred
+ panel: ART + ART
+ - room: Orange Tower Third Floor
+ panel: DEER + WREN
+ - room: Orange Tower Fourth Floor
+ panel: LEARNS + UNSEW
+ - room: Orange Tower Fifth Floor
+ panel: DRAWL + RUNS
+ Seventh Floor:
+ id: Tower Room Area Doors/Door_level_6
+ skip_location: True
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ - room: Outside The Undeterred
+ panel: ART + ART
+ - room: Orange Tower Third Floor
+ panel: DEER + WREN
+ - room: Orange Tower Fourth Floor
+ panel: LEARNS + UNSEW
+ - room: Orange Tower Fifth Floor
+ panel: DRAWL + RUNS
+ - room: Owl Hallway
+ panel: READS + RUST
+ progression:
+ Progressive Orange Tower:
+ - Second Floor
+ - Third Floor
+ - Fourth Floor
+ - Fifth Floor
+ - Sixth Floor
+ - Seventh Floor
+ Orange Tower First Floor:
+ entrances:
+ Hub Room:
+ door: Shortcut to Hub Room
+ Outside The Wanderer:
+ room: Outside The Wanderer
+ door: Tower Entrance
+ Orange Tower Second Floor:
+ room: Orange Tower
+ door: Second Floor
+ Directional Gallery:
+ door: Salt Pepper Door
+ Roof: True # through the sunwarp
+ panels:
+ SECRET:
+ id: Shuffle Room/Panel_secret_secret
+ tag: midwhite
+ DADS + ALE:
+ id: Tower Room/Panel_dads_ale_dead_1
+ colors: orange
+ check: True
+ tag: midorange
+ SALT:
+ id: Backside Room/Panel_salt_pepper
+ colors: black
+ tag: botblack
+ doors:
+ Shortcut to Hub Room:
+ id: Shuffle Room Area Doors/Door_secret_secret
+ group: Orange Tower First Floor - Shortcuts
+ panels:
+ - SECRET
+ Salt Pepper Door:
+ id: Count Up Room Area Doors/Door_salt_pepper
+ location_name: Orange Tower First Floor - Salt Pepper Door
+ group: Orange Tower First Floor - Shortcuts
+ panels:
+ - SALT
+ - room: Directional Gallery
+ panel: PEPPER
+ Orange Tower Second Floor:
+ entrances:
+ Orange Tower First Floor:
+ room: Orange Tower
+ door: Second Floor
+ Orange Tower Third Floor:
+ room: Orange Tower
+ door: Third Floor
+ Outside The Undeterred: True
+ Orange Tower Third Floor:
+ entrances:
+ Knight Night Exit:
+ room: Knight Night (Final)
+ door: Exit
+ Orange Tower Second Floor:
+ room: Orange Tower
+ door: Third Floor
+ Orange Tower Fourth Floor:
+ room: Orange Tower
+ door: Fourth Floor
+ Hot Crusts Area: True # sunwarp
+ Bearer Side Area: # This is complicated because of The Bearer's topology
+ room: Bearer Side Area
+ door: Shortcut to Tower
+ Rhyme Room (Smiley):
+ door: Rhyme Room Entrance
+ panels:
+ RED:
+ id: Color Arrow Room/Panel_red_afar
+ tag: midwhite
+ required_door:
+ door: Red Barrier
+ DEER + WREN:
+ id: Tower Room/Panel_deer_wren_rats_3
+ colors: orange
+ check: True
+ tag: midorange
+ doors:
+ Red Barrier:
+ id: Color Arrow Room Doors/Door_red_6
+ group: Color Hunt Barriers
+ skip_location: True
+ panels:
+ - room: Champion's Rest
+ panel: RED
+ Rhyme Room Entrance:
+ id: Double Room Area Doors/Door_room_entry_stairs2
+ skip_location: True
+ group: Rhyme Room Doors
+ panels:
+ - room: The Tenacious
+ panel: LEVEL (Black)
+ - room: The Tenacious
+ panel: RACECAR (Black)
+ - room: The Tenacious
+ panel: SOLOS (Black)
+ Orange Barrier: # see note in Outside The Initiated
+ id:
+ - Color Arrow Room Doors/Door_orange_hider_1
+ - Color Arrow Room Doors/Door_orange_hider_2
+ - Color Arrow Room Doors/Door_orange_hider_3
+ location_name: Color Hunt - RED and YELLOW
+ group: Champion's Rest - Color Barriers
+ item_name: Champion's Rest - Orange Barrier
+ panels:
+ - RED
+ - room: Directional Gallery
+ panel: YELLOW
+ paintings:
+ - id: arrows_painting_6
+ orientation: east
+ - id: flower_painting_5
+ orientation: south
+ Orange Tower Fourth Floor:
+ entrances:
+ Orange Tower Third Floor:
+ room: Orange Tower
+ door: Fourth Floor
+ Orange Tower Fifth Floor:
+ room: Orange Tower
+ door: Fifth Floor
+ Hot Crusts Area:
+ door: Hot Crusts Door
+ Crossroads:
+ - room: Crossroads
+ door: Tower Entrance
+ - room: Crossroads
+ door: Tower Back Entrance
+ Courtyard: True
+ Roof: True # through the sunwarp
+ panels:
+ RUNT:
+ id: Shuffle Room/Panel_turn_runt2
+ colors: yellow
+ tag: midyellow
+ RUNT (2):
+ id: Shuffle Room/Panel_runt3
+ colors:
+ - yellow
+ - blue
+ tag: mid yellow blue
+ LEARNS + UNSEW:
+ id: Tower Room/Panel_learns_unsew_unrest_4
+ colors: orange
+ check: True
+ tag: midorange
+ HOT CRUSTS:
+ id: Shuffle Room/Panel_shortcuts
+ colors: yellow
+ tag: midyellow
+ IRK HORN:
+ id: Shuffle Room/Panel_corner
+ colors: yellow
+ check: True
+ exclude_reduce: True
+ tag: topyellow
+ doors:
+ Hot Crusts Door:
+ id: Shuffle Room Area Doors/Door_hotcrust_shortcuts
+ panels:
+ - HOT CRUSTS
+ Hot Crusts Area:
+ entrances:
+ Orange Tower Fourth Floor:
+ room: Orange Tower Fourth Floor
+ door: Hot Crusts Door
+ Roof: True # through the sunwarp
+ panels:
+ EIGHT:
+ id: Backside Room/Panel_eight_eight_3
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Eights
+ paintings:
+ - id: smile_painting_8
+ orientation: north
+ Orange Tower Fifth Floor:
+ entrances:
+ Orange Tower Fourth Floor:
+ room: Orange Tower
+ door: Fifth Floor
+ Orange Tower Sixth Floor:
+ room: Orange Tower
+ door: Sixth Floor
+ Cellar:
+ room: Room Room
+ door: Shortcut to Fifth Floor
+ Welcome Back Area:
+ door: Welcome Back
+ Art Gallery:
+ room: Art Gallery
+ door: Exit
+ The Bearer:
+ room: Art Gallery
+ door: Exit
+ Outside The Initiated:
+ room: Art Gallery
+ door: Exit
+ panels:
+ SIZE (Small):
+ id: Entry Room/Panel_size_small
+ colors: gray
+ tag: forbid
+ SIZE (Big):
+ id: Entry Room/Panel_size_big
+ colors: gray
+ tag: forbid
+ DRAWL + RUNS:
+ id: Tower Room/Panel_drawl_runs_enter_5
+ colors: orange
+ check: True
+ tag: midorange
+ NINE:
+ id: Backside Room/Panel_nine_nine_2
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Nines
+ SUMMER:
+ id: Entry Room/Panel_summer_summer
+ tag: midwhite
+ AUTUMN:
+ id: Entry Room/Panel_autumn_autumn
+ tag: midwhite
+ SPRING:
+ id: Entry Room/Panel_spring_spring
+ tag: midwhite
+ PAINTING (1):
+ id: Panel Room/Panel_painting_flower
+ colors: green
+ tag: forbid
+ required_room: Cellar
+ PAINTING (2):
+ id: Panel Room/Panel_painting_eye
+ colors: green
+ tag: forbid
+ required_room: Cellar
+ PAINTING (3):
+ id: Panel Room/Panel_painting_snowman
+ colors: green
+ tag: forbid
+ required_room: Cellar
+ PAINTING (4):
+ id: Panel Room/Panel_painting_owl
+ colors: green
+ tag: forbid
+ required_room: Cellar
+ PAINTING (5):
+ id: Panel Room/Panel_painting_panda
+ colors: green
+ tag: forbid
+ required_room: Cellar
+ ROOM:
+ id: Panel Room/Panel_room_stairs
+ colors: gray
+ tag: forbid
+ required_room: Cellar
+ doors:
+ Welcome Back:
+ id: Entry Room Area Doors/Door_sizes
+ group: Welcome Back Doors
+ panels:
+ - SIZE (Small)
+ - SIZE (Big)
+ paintings:
+ - id: hi_solved_painting3
+ orientation: south
+ - id: hi_solved_painting2
+ orientation: south
+ - id: east_afar
+ orientation: north
+ Orange Tower Sixth Floor:
+ entrances:
+ Orange Tower Fifth Floor:
+ room: Orange Tower
+ door: Sixth Floor
+ The Scientific:
+ painting: True
+ paintings:
+ - id: arrows_painting_10
+ orientation: east
+ - id: owl_painting_3
+ orientation: north
+ - id: clock_painting
+ orientation: west
+ - id: scenery_painting_5d_2
+ orientation: south
+ - id: symmetry_painting_b_7
+ orientation: north
+ - id: panda_painting_2
+ orientation: south
+ - id: pencil_painting
+ orientation: north
+ - id: colors_painting2
+ orientation: south
+ - id: cherry_painting2
+ orientation: east
+ - id: hi_solved_painting
+ orientation: west
+ Orange Tower Seventh Floor:
+ entrances:
+ Orange Tower Sixth Floor:
+ room: Orange Tower
+ door: Seventh Floor
+ panels:
+ THE END:
+ id: EndPanel/Panel_end_end
+ check: True
+ tag: forbid
+ non_counting: True
+ THE MASTER:
+ # We will set up special rules for this in code.
+ id: Countdown Panels/Panel_master_master
+ check: True
+ tag: forbid
+ MASTERY:
+ # This is the MASTERY on the other side of THE FEARLESS. It can only be
+ # accessed by jumping from the top of the tower.
+ id: Master Room/Panel_mastery_mastery8
+ tag: midwhite
+ required_door:
+ door: Mastery
+ doors:
+ Mastery:
+ id:
+ - Master Room Doors/Door_tower_down
+ - Master Room Doors/Door_master_master
+ - Master Room Doors/Door_master_master_2
+ - Master Room Doors/Door_master_master_3
+ - Master Room Doors/Door_master_master_4
+ - Master Room Doors/Door_master_master_5
+ - Master Room Doors/Door_master_master_6
+ - Master Room Doors/Door_master_master_10
+ - Master Room Doors/Door_master_master_11
+ - Master Room Doors/Door_master_master_12
+ - Master Room Doors/Door_master_master_13
+ - Master Room Doors/Door_master_master_14
+ - Master Room Doors/Door_master_master_15
+ - Master Room Doors/Door_master_down
+ - Master Room Doors/Door_master_down2
+ skip_location: True
+ panels:
+ - THE MASTER
+ Mastery Panels:
+ skip_item: True
+ location_name: Mastery Panels
+ panels:
+ - room: Room Room
+ panel: MASTERY
+ - room: The Steady (Topaz)
+ panel: MASTERY
+ - room: Orange Tower Basement
+ panel: MASTERY
+ - room: Arrow Garden
+ panel: MASTERY
+ - room: Hedge Maze
+ panel: MASTERY (1)
+ - room: Roof
+ panel: MASTERY (1)
+ - room: Roof
+ panel: MASTERY (2)
+ - MASTERY
+ - room: Hedge Maze
+ panel: MASTERY (2)
+ - room: Roof
+ panel: MASTERY (3)
+ - room: Roof
+ panel: MASTERY (4)
+ - room: Roof
+ panel: MASTERY (5)
+ - room: Elements Area
+ panel: MASTERY
+ - room: Pilgrim Antechamber
+ panel: MASTERY
+ - room: Roof
+ panel: MASTERY (6)
+ paintings:
+ - id: map_painting2
+ orientation: north
+ enter_only: True # otherwise you might just skip the whole game!
+ Roof:
+ entrances:
+ Orange Tower Seventh Floor: True
+ Crossroads:
+ room: Crossroads
+ door: Roof Access
+ panels:
+ MASTERY (1):
+ id: Master Room/Panel_mastery_mastery6
+ tag: midwhite
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ MASTERY (2):
+ id: Master Room/Panel_mastery_mastery7
+ tag: midwhite
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ MASTERY (3):
+ id: Master Room/Panel_mastery_mastery10
+ tag: midwhite
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ MASTERY (4):
+ id: Master Room/Panel_mastery_mastery11
+ tag: midwhite
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ MASTERY (5):
+ id: Master Room/Panel_mastery_mastery12
+ tag: midwhite
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ MASTERY (6):
+ id: Master Room/Panel_mastery_mastery15
+ tag: midwhite
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ STAIRCASE:
+ id: Open Areas/Panel_staircase
+ tag: midwhite
+ Orange Tower Basement:
+ entrances:
+ Orange Tower Sixth Floor:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ panels:
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery3
+ tag: midwhite
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ THE LIBRARY:
+ id: EndPanel/Panel_library
+ check: True
+ tag: forbid
+ non_counting: True
+ paintings:
+ - id: arrows_painting_11
+ orientation: east
+ Courtyard:
+ entrances:
+ Roof: True
+ Orange Tower Fourth Floor: True
+ Arrow Garden:
+ painting: True
+ Starting Room:
+ door: Painting Shortcut
+ painting: True
+ Yellow Backside Area:
+ room: First Second Third Fourth
+ door: Backside Door
+ The Colorful (White): True
+ panels:
+ I:
+ id: Strand Room/Panel_i_staring
+ colors: blue
+ tag: forbid
+ GREEN:
+ id: Color Arrow Room/Panel_green_afar
+ tag: midwhite
+ required_door:
+ door: Green Barrier
+ PINECONE:
+ id: Shuffle Room/Panel_pinecone_pine
+ colors: brown
+ tag: botbrown
+ ACORN:
+ id: Shuffle Room/Panel_acorn_oak
+ colors: brown
+ tag: botbrown
+ doors:
+ Painting Shortcut:
+ painting_id: flower_painting_8
+ item_name: Starting Room - Flower Painting
+ skip_location: True
+ panels:
+ - room: First Second Third Fourth
+ panel: FIRST
+ - room: First Second Third Fourth
+ panel: SECOND
+ - room: First Second Third Fourth
+ panel: THIRD
+ - room: First Second Third Fourth
+ panel: FOURTH
+ Green Barrier:
+ id: Color Arrow Room Doors/Door_green_5
+ group: Color Hunt Barriers
+ skip_location: True
+ panels:
+ - room: Champion's Rest
+ panel: GREEN
+ paintings:
+ - id: flower_painting_7
+ orientation: north
+ Yellow Backside Area:
+ entrances:
+ Courtyard:
+ room: First Second Third Fourth
+ door: Backside Door
+ Roof: True
+ panels:
+ BACKSIDE:
+ id: Backside Room/Panel_backside_3
+ tag: midwhite
+ NINE:
+ id: Backside Room/Panel_nine_nine_8
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Nines
+ paintings:
+ - id: blueman_painting
+ orientation: east
+ First Second Third Fourth:
+ # We are separating this door + its panels into its own room because they
+ # are accessible from two distinct regions (Courtyard and Yellow Backside
+ # Area). We need to do this because painting shuffle makes it possible to
+ # have access to Yellow Backside Area without having access to Courtyard,
+ # and we want it to still be in logic to solve these panels.
+ entrances:
+ Courtyard: True
+ Yellow Backside Area: True
+ panels:
+ FIRST:
+ id: Backside Room/Panel_first_first
+ tag: midwhite
+ SECOND:
+ id: Backside Room/Panel_second_second
+ tag: midwhite
+ THIRD:
+ id: Backside Room/Panel_third_third
+ tag: midwhite
+ FOURTH:
+ id: Backside Room/Panel_fourth_fourth
+ tag: midwhite
+ doors:
+ Backside Door:
+ id: Count Up Room Area Doors/Door_yellow_backside
+ group: Backside Doors
+ location_name: Courtyard - FIRST, SECOND, THIRD, FOURTH
+ item_name: Courtyard - Backside Door
+ panels:
+ - FIRST
+ - SECOND
+ - THIRD
+ - FOURTH
+ The Colorful (White):
+ entrances:
+ Courtyard: True
+ The Colorful (Black):
+ door: Progress Door
+ panels:
+ BEGIN:
+ id: Doorways Room/Panel_begin_start
+ tag: botwhite
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_white
+ item_name: The Colorful - White Door
+ group: Colorful Doors
+ location_name: The Colorful - White
+ panels:
+ - BEGIN
+ The Colorful (Black):
+ entrances:
+ The Colorful (White):
+ room: The Colorful (White)
+ door: Progress Door
+ The Colorful (Red):
+ door: Progress Door
+ panels:
+ FOUND:
+ id: Doorways Room/Panel_found_lost
+ colors: black
+ tag: botblack
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_black
+ item_name: The Colorful - Black Door
+ location_name: The Colorful - Black
+ group: Colorful Doors
+ panels:
+ - FOUND
+ The Colorful (Red):
+ entrances:
+ The Colorful (Black):
+ room: The Colorful (Black)
+ door: Progress Door
+ The Colorful (Yellow):
+ door: Progress Door
+ panels:
+ LOAF:
+ id: Doorways Room/Panel_loaf_crust
+ colors: red
+ tag: botred
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_red
+ item_name: The Colorful - Red Door
+ location_name: The Colorful - Red
+ group: Colorful Doors
+ panels:
+ - LOAF
+ The Colorful (Yellow):
+ entrances:
+ The Colorful (Red):
+ room: The Colorful (Red)
+ door: Progress Door
+ The Colorful (Blue):
+ door: Progress Door
+ panels:
+ CREAM:
+ id: Doorways Room/Panel_eggs_breakfast
+ colors: yellow
+ tag: botyellow
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_yellow
+ item_name: The Colorful - Yellow Door
+ location_name: The Colorful - Yellow
+ group: Colorful Doors
+ panels:
+ - CREAM
+ The Colorful (Blue):
+ entrances:
+ The Colorful (Yellow):
+ room: The Colorful (Yellow)
+ door: Progress Door
+ The Colorful (Purple):
+ door: Progress Door
+ panels:
+ SUN:
+ id: Doorways Room/Panel_sun_sky
+ colors: blue
+ tag: botblue
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_blue
+ item_name: The Colorful - Blue Door
+ location_name: The Colorful - Blue
+ group: Colorful Doors
+ panels:
+ - SUN
+ The Colorful (Purple):
+ entrances:
+ The Colorful (Blue):
+ room: The Colorful (Blue)
+ door: Progress Door
+ The Colorful (Orange):
+ door: Progress Door
+ panels:
+ SPOON:
+ id: Doorways Room/Panel_teacher_substitute
+ colors: purple
+ tag: botpurple
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_purple
+ item_name: The Colorful - Purple Door
+ location_name: The Colorful - Purple
+ group: Colorful Doors
+ panels:
+ - SPOON
+ The Colorful (Orange):
+ entrances:
+ The Colorful (Purple):
+ room: The Colorful (Purple)
+ door: Progress Door
+ The Colorful (Green):
+ door: Progress Door
+ panels:
+ LETTERS:
+ id: Doorways Room/Panel_walnuts_orange
+ colors: orange
+ tag: botorange
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_orange
+ item_name: The Colorful - Orange Door
+ location_name: The Colorful - Orange
+ group: Colorful Doors
+ panels:
+ - LETTERS
+ The Colorful (Green):
+ entrances:
+ The Colorful (Orange):
+ room: The Colorful (Orange)
+ door: Progress Door
+ The Colorful (Brown):
+ door: Progress Door
+ panels:
+ WALLS:
+ id: Doorways Room/Panel_path_i
+ colors: green
+ tag: forbid
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_green
+ item_name: The Colorful - Green Door
+ location_name: The Colorful - Green
+ group: Colorful Doors
+ panels:
+ - WALLS
+ The Colorful (Brown):
+ entrances:
+ The Colorful (Green):
+ room: The Colorful (Green)
+ door: Progress Door
+ The Colorful (Gray):
+ door: Progress Door
+ panels:
+ IRON:
+ id: Doorways Room/Panel_iron_rust
+ colors: brown
+ tag: botbrown
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_brown
+ item_name: The Colorful - Brown Door
+ location_name: The Colorful - Brown
+ group: Colorful Doors
+ panels:
+ - IRON
+ The Colorful (Gray):
+ entrances:
+ The Colorful (Brown):
+ room: The Colorful (Brown)
+ door: Progress Door
+ The Colorful:
+ door: Progress Door
+ panels:
+ OBSTACLE:
+ id: Doorways Room/Panel_obstacle_door
+ colors: gray
+ tag: forbid
+ doors:
+ Progress Door:
+ id:
+ - Doorway Room Doors/Door_gray
+ - Doorway Room Doors/Door_gray2 # See comment below
+ item_name: The Colorful - Gray Door
+ location_name: The Colorful - Gray
+ group: Colorful Doors
+ panels:
+ - OBSTACLE
+ The Colorful:
+ # The set of required_doors in the achievement panel should prevent
+ # generation from asking you to solve The Colorful before opening all of the
+ # doors. Access from the roof is included so that the painting here could be
+ # an entrance. The client will have to be hardcoded to not open the door to
+ # the achievement until all of the doors are open, whether by solving the
+ # panels or through receiving items.
+ entrances:
+ The Colorful (Gray):
+ room: The Colorful (Gray)
+ door: Progress Door
+ Roof: True
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_colorful_colorful
+ check: True
+ tag: forbid
+ required_door:
+ - room: The Colorful (White)
+ door: Progress Door
+ - room: The Colorful (Black)
+ door: Progress Door
+ - room: The Colorful (Red)
+ door: Progress Door
+ - room: The Colorful (Yellow)
+ door: Progress Door
+ - room: The Colorful (Blue)
+ door: Progress Door
+ - room: The Colorful (Purple)
+ door: Progress Door
+ - room: The Colorful (Orange)
+ door: Progress Door
+ - room: The Colorful (Green)
+ door: Progress Door
+ - room: The Colorful (Brown)
+ door: Progress Door
+ - room: The Colorful (Gray)
+ door: Progress Door
+ achievement: The Colorful
+ paintings:
+ - id: arrows_painting_12
+ orientation: north
+ Welcome Back Area:
+ entrances:
+ Starting Room:
+ door: Shortcut to Starting Room
+ Hub Room: True
+ Outside The Wondrous: True
+ Outside The Undeterred: True
+ Outside The Initiated: True
+ Outside The Agreeable: True
+ Outside The Wanderer: True
+ Eight Alcove: True
+ Orange Tower Fifth Floor:
+ room: Orange Tower Fifth Floor
+ door: Welcome Back
+ Challenge Room:
+ room: Challenge Room
+ door: Welcome Door
+ panels:
+ WELCOME BACK:
+ id: Entry Room/Panel_return_return
+ tag: midwhite
+ SECRET:
+ id: Entry Room/Panel_secret_secret
+ tag: midwhite
+ CLOCKWISE:
+ id: Shuffle Room/Panel_clockwise_counterclockwise
+ colors: black
+ check: True
+ exclude_reduce: True
+ tag: botblack
+ doors:
+ Shortcut to Starting Room:
+ id: Entry Room Area Doors/Door_return_return
+ group: Welcome Back Doors
+ include_reduce: True
+ panels:
+ - WELCOME BACK
+ Owl Hallway:
+ entrances:
+ Hidden Room:
+ painting: True
+ Hedge Maze:
+ door: Shortcut to Hedge Maze
+ Orange Tower Sixth Floor:
+ painting: True
+ panels:
+ STRAYS:
+ id: Maze Room/Panel_strays_maze
+ colors: purple
+ tag: toppurp
+ READS + RUST:
+ id: Tower Room/Panel_reads_rust_lawns_6
+ colors: orange
+ check: True
+ tag: midorange
+ doors:
+ Shortcut to Hedge Maze:
+ id: Maze Area Doors/Door_strays_maze
+ group: Hedge Maze Doors
+ panels:
+ - STRAYS
+ paintings:
+ - id: arrows_painting_8
+ orientation: south
+ - id: maze_painting_2
+ orientation: north
+ - id: owl_painting_2
+ orientation: south
+ required_when_no_doors: True
+ - id: clock_painting_4
+ orientation: north
+ Outside The Initiated:
+ entrances:
+ Hub Room:
+ door: Shortcut to Hub Room
+ Knight Night Exit:
+ room: Knight Night (Final)
+ door: Exit
+ Orange Tower Third Floor: True # sunwarp
+ Orange Tower Fifth Floor:
+ room: Art Gallery
+ door: Exit
+ panels:
+ SEVEN (1):
+ id: Backside Room/Panel_seven_seven_5
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Sevens
+ SEVEN (2):
+ id: Backside Room/Panel_seven_seven_6
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Sevens
+ EIGHT:
+ id: Backside Room/Panel_eight_eight_7
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Eights
+ NINE:
+ id: Backside Room/Panel_nine_nine_4
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Nines
+ BLUE:
+ id: Color Arrow Room/Panel_blue_afar
+ tag: midwhite
+ required_door:
+ door: Blue Barrier
+ ORANGE:
+ id: Color Arrow Room/Panel_orange_afar
+ tag: midwhite
+ required_door:
+ door: Orange Barrier
+ UNCOVER:
+ id: Appendix Room/Panel_discover_recover
+ colors: purple
+ tag: midpurp
+ OXEN:
+ id: Rhyme Room/Panel_locked_knocked
+ colors: purple
+ tag: midpurp
+ BACKSIDE:
+ id: Backside Room/Panel_backside_1
+ tag: midwhite
+ The Optimistic:
+ id: Countdown Panels/Panel_optimistic_optimistic
+ check: True
+ tag: forbid
+ required_door:
+ door: Backsides
+ achievement: The Optimistic
+ PAST:
+ id: Shuffle Room/Panel_past_present
+ colors: brown
+ tag: botbrown
+ FUTURE:
+ id: Shuffle Room/Panel_future_present
+ colors:
+ - brown
+ - black
+ tag: bot brown black
+ FUTURE (2):
+ id: Shuffle Room/Panel_future_past
+ colors: black
+ tag: botblack
+ PAST (2):
+ id: Shuffle Room/Panel_past_future
+ colors: black
+ tag: botblack
+ PRESENT:
+ id: Shuffle Room/Panel_past_past
+ colors:
+ - brown
+ - black
+ tag: bot brown black
+ SMILE:
+ id: Open Areas/Panel_smile_smile
+ tag: midwhite
+ ANGERED:
+ id: Open Areas/Panel_angered_enraged
+ colors:
+ - yellow
+ tag: syn anagram
+ copy_to_sign: sign18
+ VOTE:
+ id: Open Areas/Panel_vote_veto
+ colors:
+ - yellow
+ - black
+ tag: ant anagram
+ copy_to_sign: sign17
+ doors:
+ Shortcut to Hub Room:
+ id: Appendix Room Area Doors/Door_recover_discover
+ panels:
+ - UNCOVER
+ Blue Barrier:
+ id: Color Arrow Room Doors/Door_blue_3
+ group: Color Hunt Barriers
+ skip_location: True
+ panels:
+ - room: Champion's Rest
+ panel: BLUE
+ Orange Barrier:
+ id: Color Arrow Room Doors/Door_orange_3
+ group: Color Hunt Barriers
+ skip_location: True
+ panels:
+ - room: Champion's Rest
+ panel: ORANGE
+ Initiated Entrance:
+ id: Red Blue Purple Room Area Doors/Door_locked_knocked
+ item_name: The Initiated - Entrance
+ panels:
+ - OXEN
+ # These would be more appropriate in Champion's Rest, but as currently
+ # implemented, locations need to include at least one panel from the
+ # containing region.
+ Green Barrier:
+ id: Color Arrow Room Doors/Door_green_hider_1
+ location_name: Color Hunt - BLUE and YELLOW
+ item_name: Champion's Rest - Green Barrier
+ group: Champion's Rest - Color Barriers
+ panels:
+ - BLUE
+ - room: Directional Gallery
+ panel: YELLOW
+ Purple Barrier:
+ id:
+ - Color Arrow Room Doors/Door_purple_hider_1
+ - Color Arrow Room Doors/Door_purple_hider_2
+ - Color Arrow Room Doors/Door_purple_hider_3
+ location_name: Color Hunt - RED and BLUE
+ item_name: Champion's Rest - Purple Barrier
+ group: Champion's Rest - Color Barriers
+ panels:
+ - BLUE
+ - room: Orange Tower Third Floor
+ panel: RED
+ Entrance:
+ id:
+ - Color Arrow Room Doors/Door_all_hider_1
+ - Color Arrow Room Doors/Door_all_hider_2
+ - Color Arrow Room Doors/Door_all_hider_3
+ location_name: Color Hunt - GREEN, ORANGE and PURPLE
+ item_name: Champion's Rest - Entrance
+ panels:
+ - ORANGE
+ - room: Courtyard
+ panel: GREEN
+ - room: Outside The Agreeable
+ panel: PURPLE
+ Backsides:
+ event: True
+ panels:
+ - room: The Observant
+ panel: BACKSIDE
+ - room: Yellow Backside Area
+ panel: BACKSIDE
+ - room: Directional Gallery
+ panel: BACKSIDE
+ - room: The Bearer
+ panel: BACKSIDE
+ paintings:
+ - id: clock_painting_5
+ orientation: east
+ - id: smile_painting_1
+ orientation: north
+ The Initiated:
+ entrances:
+ Outside The Initiated:
+ room: Outside The Initiated
+ door: Initiated Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_illuminated_initiated
+ colors: purple
+ tag: forbid
+ check: True
+ achievement: The Initiated
+ DAUGHTER:
+ id: Rhyme Room/Panel_daughter_laughter
+ colors: purple
+ tag: midpurp
+ START:
+ id: Rhyme Room/Panel_move_love
+ colors: purple
+ tag: double midpurp
+ subtag: left
+ link: change STARS
+ STARE:
+ id: Rhyme Room/Panel_stove_love
+ colors: purple
+ tag: double midpurp
+ subtag: right
+ link: change STARS
+ HYPE:
+ id: Rhyme Room/Panel_scope_type
+ colors: purple
+ tag: midpurp and rhyme
+ copy_to_sign: sign16
+ ABYSS:
+ id: Rhyme Room/Panel_abyss_this
+ colors: purple
+ tag: toppurp
+ SWEAT:
+ id: Rhyme Room/Panel_sweat_great
+ colors: purple
+ tag: double midpurp
+ subtag: left
+ link: change GREAT
+ BEAT:
+ id: Rhyme Room/Panel_beat_great
+ colors: purple
+ tag: double midpurp
+ subtag: right
+ link: change GREAT
+ ALUMNI:
+ id: Rhyme Room/Panel_alumni_hi
+ colors: purple
+ tag: midpurp and rhyme
+ copy_to_sign: sign14
+ PATS:
+ id: Rhyme Room/Panel_wrath_path
+ colors: purple
+ tag: midpurp and rhyme
+ copy_to_sign: sign15
+ KNIGHT:
+ id: Rhyme Room/Panel_knight_write
+ colors: purple
+ tag: double toppurp
+ subtag: left
+ link: change WRITE
+ BYTE:
+ id: Rhyme Room/Panel_byte_write
+ colors: purple
+ tag: double toppurp
+ subtag: right
+ link: change WRITE
+ MAIM:
+ id: Rhyme Room/Panel_maim_same
+ colors: purple
+ tag: toppurp
+ MORGUE:
+ id: Rhyme Room/Panel_chair_bear
+ colors: purple
+ tag: purple rhyme change stack
+ subtag: top
+ link: prcs CYBORG
+ CHAIR:
+ id: Rhyme Room/Panel_bare_bear
+ colors: purple
+ tag: toppurp
+ HUMAN:
+ id: Rhyme Room/Panel_cost_most
+ colors: purple
+ tag: purple rhyme change stack
+ subtag: bot
+ link: prcs CYBORG
+ BED:
+ id: Rhyme Room/Panel_bed_dead
+ colors: purple
+ tag: toppurp
+ The Traveled:
+ entrances:
+ Hub Room:
+ room: Hub Room
+ door: Traveled Entrance
+ Color Hallways:
+ door: Color Hallways Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_traveled_traveled
+ required_room: Hub Room
+ tag: forbid
+ check: True
+ achievement: The Traveled
+ CLOSE:
+ id: Synonym Room/Panel_close_near
+ tag: botwhite
+ COMPOSE:
+ id: Synonym Room/Panel_compose_write
+ tag: double botwhite
+ subtag: left
+ link: syn WRITE
+ RECORD:
+ id: Synonym Room/Panel_record_write
+ tag: double botwhite
+ subtag: right
+ link: syn WRITE
+ CATEGORY:
+ id: Synonym Room/Panel_category_type
+ tag: botwhite
+ HELLO:
+ id: Synonym Room/Panel_hello_hi
+ tag: botwhite
+ DUPLICATE:
+ id: Synonym Room/Panel_duplicate_same
+ tag: double botwhite
+ subtag: left
+ link: syn SAME
+ IDENTICAL:
+ id: Synonym Room/Panel_identical_same
+ tag: double botwhite
+ subtag: right
+ link: syn SAME
+ DISTANT:
+ id: Synonym Room/Panel_distant_far
+ tag: botwhite
+ HAY:
+ id: Synonym Room/Panel_hay_straw
+ tag: botwhite
+ GIGGLE:
+ id: Synonym Room/Panel_giggle_laugh
+ tag: double botwhite
+ subtag: left
+ link: syn LAUGH
+ CHUCKLE:
+ id: Synonym Room/Panel_chuckle_laugh
+ tag: double botwhite
+ subtag: right
+ link: syn LAUGH
+ SNITCH:
+ id: Synonym Room/Panel_snitch_rat
+ tag: botwhite
+ CONCEALED:
+ id: Synonym Room/Panel_concealed_hidden
+ tag: botwhite
+ PLUNGE:
+ id: Synonym Room/Panel_plunge_fall
+ tag: double botwhite
+ subtag: left
+ link: syn FALL
+ AUTUMN:
+ id: Synonym Room/Panel_autumn_fall
+ tag: double botwhite
+ subtag: right
+ link: syn FALL
+ ROAD:
+ id: Synonym Room/Panel_growths_warts
+ tag: botwhite
+ FOUR:
+ id: Backside Room/Panel_four_four_4
+ tag: midwhite
+ required_door:
+ room: Outside The Undeterred
+ door: Fours
+ doors:
+ Color Hallways Entrance:
+ id: Appendix Room Area Doors/Door_hello_hi
+ group: Entrance to The Traveled
+ panels:
+ - HELLO
+ Color Hallways:
+ entrances:
+ The Traveled:
+ room: The Traveled
+ door: Color Hallways Entrance
+ Outside The Bold: True
+ Outside The Undeterred: True
+ Crossroads: True
+ Hedge Maze: True
+ Outside The Initiated: True # backside
+ Directional Gallery: True # backside
+ Yellow Backside Area: True
+ The Bearer:
+ room: The Bearer
+ door: Backside Door
+ The Observant:
+ room: The Observant
+ door: Backside Door
+ Outside The Bold:
+ entrances:
+ Color Hallways: True
+ Champion's Rest:
+ room: Champion's Rest
+ door: Shortcut to The Steady
+ The Bearer:
+ room: The Bearer
+ door: Shortcut to The Bold
+ Directional Gallery:
+ # There is a painting warp here from the Directional Gallery, but it
+ # only appears when the sixes are revealed. It could be its own item if
+ # we wanted.
+ room: Number Hunt
+ door: Sixes
+ painting: True
+ Starting Room:
+ door: Painting Shortcut
+ painting: True
+ Room Room: True # trapdoor
+ panels:
+ UNOPEN:
+ id: Truncate Room/Panel_unopened_open
+ colors: red
+ tag: midred
+ BEGIN:
+ id: Rock Room/Panel_begin_begin
+ tag: midwhite
+ SIX:
+ id: Backside Room/Panel_six_six_4
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Sixes
+ NINE:
+ id: Backside Room/Panel_nine_nine_5
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Nines
+ LEFT:
+ id: Shuffle Room/Panel_left_left_2
+ tag: midwhite
+ RIGHT:
+ id: Shuffle Room/Panel_right_right_2
+ tag: midwhite
+ RISE (Horizon):
+ id: Open Areas/Panel_rise_horizon
+ colors: blue
+ tag: double topblue
+ subtag: left
+ link: expand HORIZON
+ RISE (Sunrise):
+ id: Open Areas/Panel_rise_sunrise
+ colors: blue
+ tag: double topblue
+ subtag: left
+ link: expand SUNRISE
+ ZEN:
+ id: Open Areas/Panel_son_horizon
+ colors: blue
+ tag: double topblue
+ subtag: right
+ link: expand HORIZON
+ SON:
+ id: Open Areas/Panel_son_sunrise
+ colors: blue
+ tag: double topblue
+ subtag: right
+ link: expand SUNRISE
+ STARGAZER:
+ id: Open Areas/Panel_stargazer_stargazer
+ tag: midwhite
+ required_door:
+ door: Stargazer Door
+ MOUTH:
+ id: Cross Room/Panel_mouth_south
+ colors: purple
+ tag: midpurp
+ YEAST:
+ id: Cross Room/Panel_yeast_east
+ colors: red
+ tag: midred
+ WET:
+ id: Cross Room/Panel_wet_west
+ colors: blue
+ tag: midblue
+ doors:
+ Bold Entrance:
+ id: Red Blue Purple Room Area Doors/Door_unopened_open
+ item_name: The Bold - Entrance
+ panels:
+ - UNOPEN
+ Painting Shortcut:
+ painting_id: pencil_painting6
+ skip_location: True
+ item_name: Starting Room - Pencil Painting
+ panels:
+ - UNOPEN
+ Steady Entrance:
+ id: Rock Room Doors/Door_2
+ item_name: The Steady - Entrance
+ panels:
+ - BEGIN
+ Lilac Entrance:
+ event: True
+ panels:
+ - room: The Steady (Rose)
+ panel: SOAR
+ Stargazer Door:
+ event: True
+ panels:
+ - RISE (Horizon)
+ - RISE (Sunrise)
+ - ZEN
+ - SON
+ paintings:
+ - id: pencil_painting2
+ orientation: west
+ - id: north_missing2
+ orientation: north
+ The Bold:
+ entrances:
+ Outside The Bold:
+ room: Outside The Bold
+ door: Bold Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_emboldened_bold
+ colors: red
+ tag: forbid
+ check: True
+ achievement: The Bold
+ FOOT:
+ id: Truncate Room/Panel_foot_toe
+ colors: red
+ tag: botred
+ NEEDLE:
+ id: Truncate Room/Panel_needle_eye
+ colors: red
+ tag: double botred
+ subtag: left
+ link: mero EYE
+ FACE:
+ id: Truncate Room/Panel_face_eye
+ colors: red
+ tag: double botred
+ subtag: right
+ link: mero EYE
+ SIGN:
+ id: Truncate Room/Panel_sign_sigh
+ colors: red
+ tag: topred
+ HEARTBREAK:
+ id: Truncate Room/Panel_heartbreak_brake
+ colors: red
+ tag: topred
+ UNDEAD:
+ id: Truncate Room/Panel_undead_dead
+ colors: red
+ tag: double midred
+ subtag: left
+ link: trunc DEAD
+ DEADLINE:
+ id: Truncate Room/Panel_deadline_dead
+ colors: red
+ tag: double midred
+ subtag: right
+ link: trunc DEAD
+ SUSHI:
+ id: Truncate Room/Panel_sushi_hi
+ colors: red
+ tag: midred
+ THISTLE:
+ id: Truncate Room/Panel_thistle_this
+ colors: red
+ tag: midred
+ LANDMASS:
+ id: Truncate Room/Panel_landmass_mass
+ colors: red
+ tag: double midred
+ subtag: left
+ link: trunc MASS
+ MASSACRED:
+ id: Truncate Room/Panel_massacred_mass
+ colors: red
+ tag: double midred
+ subtag: right
+ link: trunc MASS
+ AIRPLANE:
+ id: Truncate Room/Panel_airplane_plain
+ colors: red
+ tag: topred
+ NIGHTMARE:
+ id: Truncate Room/Panel_nightmare_knight
+ colors: red
+ tag: topred
+ MOUTH:
+ id: Truncate Room/Panel_mouth_teeth
+ colors: red
+ tag: double botred
+ subtag: left
+ link: mero TEETH
+ SAW:
+ id: Truncate Room/Panel_saw_teeth
+ colors: red
+ tag: double botred
+ subtag: right
+ link: mero TEETH
+ HAND:
+ id: Truncate Room/Panel_hand_finger
+ colors: red
+ tag: botred
+ Outside The Undeterred:
+ entrances:
+ Color Hallways: True
+ Orange Tower First Floor: True # sunwarp
+ Orange Tower Second Floor: True
+ The Artistic (Smiley): True
+ The Artistic (Panda): True
+ The Artistic (Apple): True
+ The Artistic (Lattice): True
+ Yellow Backside Area:
+ painting: True
+ Number Hunt:
+ door: Number Hunt
+ Directional Gallery:
+ room: Directional Gallery
+ door: Shortcut to The Undeterred
+ Starting Room:
+ door: Painting Shortcut
+ painting: True
+ panels:
+ HOLLOW:
+ id: Hallway Room/Panel_hollow_hollow
+ tag: midwhite
+ ART + ART:
+ id: Tower Room/Panel_art_art_eat_2
+ colors: orange
+ check: True
+ tag: midorange
+ PEN:
+ id: Blue Room/Panel_pen_open
+ colors: blue
+ tag: midblue
+ HUSTLING:
+ id: Open Areas/Panel_hustling_sunlight
+ colors: yellow
+ tag: midyellow
+ SUNLIGHT:
+ id: Open Areas/Panel_sunlight_light
+ colors: red
+ tag: midred
+ required_panel:
+ panel: HUSTLING
+ LIGHT:
+ id: Open Areas/Panel_light_bright
+ colors: purple
+ tag: midpurp
+ required_panel:
+ panel: SUNLIGHT
+ BRIGHT:
+ id: Open Areas/Panel_bright_sunny
+ tag: botwhite
+ required_panel:
+ panel: LIGHT
+ SUNNY:
+ id: Open Areas/Panel_sunny_rainy
+ colors: black
+ tag: botblack
+ required_panel:
+ panel: BRIGHT
+ RAINY:
+ id: Open Areas/Panel_rainy_rainbow
+ colors: brown
+ tag: botbrown
+ required_panel:
+ panel: SUNNY
+ check: True
+ ZERO:
+ id: Backside Room/Panel_zero_zero
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Zero Door
+ ONE:
+ id: Backside Room/Panel_one_one
+ tag: midwhite
+ TWO (1):
+ id: Backside Room/Panel_two_two
+ tag: midwhite
+ required_door:
+ door: Twos
+ TWO (2):
+ id: Backside Room/Panel_two_two_2
+ tag: midwhite
+ required_door:
+ door: Twos
+ THREE (1):
+ id: Backside Room/Panel_three_three
+ tag: midwhite
+ required_door:
+ door: Threes
+ THREE (2):
+ id: Backside Room/Panel_three_three_2
+ tag: midwhite
+ required_door:
+ door: Threes
+ THREE (3):
+ id: Backside Room/Panel_three_three_3
+ tag: midwhite
+ required_door:
+ door: Threes
+ FOUR:
+ id: Backside Room/Panel_four_four
+ tag: midwhite
+ required_door:
+ door: Fours
+ doors:
+ Undeterred Entrance:
+ id: Red Blue Purple Room Area Doors/Door_pen_open
+ item_name: The Undeterred - Entrance
+ panels:
+ - PEN
+ Painting Shortcut:
+ painting_id:
+ - blueman_painting_3
+ - arrows_painting3
+ skip_location: True
+ item_name: Starting Room - Blue Painting
+ panels:
+ - PEN
+ Green Painting:
+ painting_id: maze_painting_3
+ skip_location: True
+ panels:
+ - FOUR
+ Twos:
+ id:
+ - Count Up Room Area Doors/Door_two_hider
+ - Count Up Room Area Doors/Door_two_hider_2
+ include_reduce: True
+ panels:
+ - ONE
+ Threes:
+ id:
+ - Count Up Room Area Doors/Door_three_hider
+ - Count Up Room Area Doors/Door_three_hider_2
+ - Count Up Room Area Doors/Door_three_hider_3
+ location_name: Twos
+ include_reduce: True
+ panels:
+ - TWO (1)
+ - TWO (2)
+ Number Hunt:
+ id: Count Up Room Area Doors/Door_three_unlocked
+ location_name: Threes
+ include_reduce: True
+ panels:
+ - THREE (1)
+ - THREE (2)
+ - THREE (3)
+ Fours:
+ id:
+ - Count Up Room Area Doors/Door_four_hider
+ - Count Up Room Area Doors/Door_four_hider_2
+ - Count Up Room Area Doors/Door_four_hider_3
+ - Count Up Room Area Doors/Door_four_hider_4
+ skip_location: True
+ panels:
+ - THREE (1)
+ - THREE (2)
+ - THREE (3)
+ Fives:
+ id:
+ - Count Up Room Area Doors/Door_five_hider
+ - Count Up Room Area Doors/Door_five_hider_4
+ - Count Up Room Area Doors/Door_five_hider_5
+ location_name: Fours
+ item_name: Number Hunt - Fives
+ include_reduce: True
+ panels:
+ - FOUR
+ - room: Hub Room
+ panel: FOUR
+ - room: Dead End Area
+ panel: FOUR
+ - room: The Traveled
+ panel: FOUR
+ Challenge Entrance:
+ id: Count Up Room Area Doors/Door_zero_unlocked
+ item_name: Number Hunt - Challenge Entrance
+ panels:
+ - ZERO
+ paintings:
+ - id: maze_painting_3
+ enter_only: True
+ orientation: north
+ move: True
+ required_door:
+ door: Green Painting
+ - id: blueman_painting_2
+ orientation: east
+ The Undeterred:
+ entrances:
+ Outside The Undeterred:
+ room: Outside The Undeterred
+ door: Undeterred Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_deterred_undeterred
+ colors: blue
+ tag: forbid
+ check: True
+ achievement: The Undeterred
+ BONE:
+ id: Blue Room/Panel_bone_skeleton
+ colors: blue
+ tag: botblue
+ EYE:
+ id: Blue Room/Panel_mouth_face
+ colors: blue
+ tag: double botblue
+ subtag: left
+ link: holo FACE
+ MOUTH:
+ id: Blue Room/Panel_eye_face
+ colors: blue
+ tag: double botblue
+ subtag: right
+ link: holo FACE
+ IRIS:
+ id: Blue Room/Panel_toucan_bird
+ colors: blue
+ tag: botblue
+ EYE (2):
+ id: Blue Room/Panel_two_toucan
+ colors: blue
+ tag: topblue
+ ICE:
+ id: Blue Room/Panel_ice_eyesight
+ colors: blue
+ tag: double topblue
+ subtag: left
+ link: hex EYESIGHT
+ HEIGHT:
+ id: Blue Room/Panel_height_eyesight
+ colors: blue
+ tag: double topblue
+ subtag: right
+ link: hex EYESIGHT
+ EYE (3):
+ id: Blue Room/Panel_eye_hi
+ colors: blue
+ tag: topblue
+ NOT:
+ id: Blue Room/Panel_not_notice
+ colors: blue
+ tag: midblue
+ JUST:
+ id: Blue Room/Panel_just_readjust
+ colors: blue
+ tag: double midblue
+ subtag: left
+ link: exp READJUST
+ READ:
+ id: Blue Room/Panel_read_readjust
+ colors: blue
+ tag: double midblue
+ subtag: right
+ link: exp READJUST
+ FATHER:
+ id: Blue Room/Panel_ate_primate
+ colors: blue
+ tag: midblue
+ FEATHER:
+ id: Blue Room/Panel_primate_mammal
+ colors: blue
+ tag: botblue
+ CONTINENT:
+ id: Blue Room/Panel_continent_planet
+ colors: blue
+ tag: double botblue
+ subtag: left
+ link: holo PLANET
+ OCEAN:
+ id: Blue Room/Panel_ocean_planet
+ colors: blue
+ tag: double botblue
+ subtag: right
+ link: holo PLANET
+ WALL:
+ id: Blue Room/Panel_wall_room
+ colors: blue
+ tag: botblue
+ Number Hunt:
+ # This works a little differently than in the base game. The door to the
+ # initial number in each set opens at the same time as the rest of the doors
+ # in that set.
+ entrances:
+ Outside The Undeterred:
+ room: Outside The Undeterred
+ door: Number Hunt
+ Directional Gallery:
+ door: Door to Directional Gallery
+ Challenge Room:
+ room: Outside The Undeterred
+ door: Challenge Entrance
+ panels:
+ FIVE:
+ id: Backside Room/Panel_five_five
+ tag: midwhite
+ required_door:
+ room: Outside The Undeterred
+ door: Fives
+ SIX:
+ id: Backside Room/Panel_six_six
+ tag: midwhite
+ required_door:
+ door: Sixes
+ SEVEN:
+ id: Backside Room/Panel_seven_seven
+ tag: midwhite
+ required_door:
+ door: Sevens
+ EIGHT:
+ id: Backside Room/Panel_eight_eight
+ tag: midwhite
+ required_door:
+ door: Eights
+ NINE:
+ id: Backside Room/Panel_nine_nine
+ tag: midwhite
+ required_door:
+ door: Nines
+ doors:
+ Door to Directional Gallery:
+ id: Count Up Room Area Doors/Door_five_unlocked
+ group: Directional Gallery Doors
+ skip_location: True
+ panels:
+ - FIVE
+ Sixes:
+ id:
+ - Count Up Room Area Doors/Door_six_hider
+ - Count Up Room Area Doors/Door_six_hider_2
+ - Count Up Room Area Doors/Door_six_hider_3
+ - Count Up Room Area Doors/Door_six_hider_4
+ - Count Up Room Area Doors/Door_six_hider_5
+ - Count Up Room Area Doors/Door_six_hider_6
+ painting_id: pencil_painting3 # See note in Outside The Bold
+ location_name: Fives
+ include_reduce: True
+ panels:
+ - FIVE
+ - room: Outside The Agreeable
+ panel: FIVE (1)
+ - room: Outside The Agreeable
+ panel: FIVE (2)
+ - room: Directional Gallery
+ panel: FIVE (1)
+ - room: Directional Gallery
+ panel: FIVE (2)
+ Sevens:
+ id:
+ - Count Up Room Area Doors/Door_seven_hider
+ - Count Up Room Area Doors/Door_seven_unlocked
+ - Count Up Room Area Doors/Door_seven_hider_2
+ - Count Up Room Area Doors/Door_seven_hider_3
+ - Count Up Room Area Doors/Door_seven_hider_4
+ - Count Up Room Area Doors/Door_seven_hider_5
+ - Count Up Room Area Doors/Door_seven_hider_6
+ - Count Up Room Area Doors/Door_seven_hider_7
+ location_name: Sixes
+ include_reduce: True
+ panels:
+ - SIX
+ - room: Outside The Bold
+ panel: SIX
+ - room: Directional Gallery
+ panel: SIX (1)
+ - room: Directional Gallery
+ panel: SIX (2)
+ - room: The Bearer (East)
+ panel: SIX
+ - room: The Bearer (South)
+ panel: SIX
+ Eights:
+ id:
+ - Count Up Room Area Doors/Door_eight_hider
+ - Count Up Room Area Doors/Door_eight_unlocked
+ - Count Up Room Area Doors/Door_eight_hider_2
+ - Count Up Room Area Doors/Door_eight_hider_3
+ - Count Up Room Area Doors/Door_eight_hider_4
+ - Count Up Room Area Doors/Door_eight_hider_5
+ - Count Up Room Area Doors/Door_eight_hider_6
+ - Count Up Room Area Doors/Door_eight_hider_7
+ - Count Up Room Area Doors/Door_eight_hider_8
+ location_name: Sevens
+ include_reduce: True
+ panels:
+ - SEVEN
+ - room: Directional Gallery
+ panel: SEVEN
+ - room: Knight Night Exit
+ panel: SEVEN (1)
+ - room: Knight Night Exit
+ panel: SEVEN (2)
+ - room: Knight Night Exit
+ panel: SEVEN (3)
+ - room: Outside The Initiated
+ panel: SEVEN (1)
+ - room: Outside The Initiated
+ panel: SEVEN (2)
+ Nines:
+ id:
+ - Count Up Room Area Doors/Door_nine_hider
+ - Count Up Room Area Doors/Door_nine_hider_2
+ - Count Up Room Area Doors/Door_nine_hider_3
+ - Count Up Room Area Doors/Door_nine_hider_4
+ - Count Up Room Area Doors/Door_nine_hider_5
+ - Count Up Room Area Doors/Door_nine_hider_6
+ - Count Up Room Area Doors/Door_nine_hider_7
+ - Count Up Room Area Doors/Door_nine_hider_8
+ - Count Up Room Area Doors/Door_nine_hider_9
+ location_name: Eights
+ include_reduce: True
+ panels:
+ - EIGHT
+ - room: Directional Gallery
+ panel: EIGHT
+ - room: The Eyes They See
+ panel: EIGHT
+ - room: Dead End Area
+ panel: EIGHT
+ - room: Crossroads
+ panel: EIGHT
+ - room: Hot Crusts Area
+ panel: EIGHT
+ - room: Art Gallery
+ panel: EIGHT
+ - room: Outside The Initiated
+ panel: EIGHT
+ Zero Door:
+ # The black wall isn't a door, so we can't ever hide it.
+ id: Count Up Room Area Doors/Door_zero_hider_2
+ location_name: Nines
+ item_name: Outside The Undeterred - Zero Door
+ include_reduce: True
+ panels:
+ - NINE
+ - room: Directional Gallery
+ panel: NINE
+ - room: Amen Name Area
+ panel: NINE
+ - room: Yellow Backside Area
+ panel: NINE
+ - room: Outside The Initiated
+ panel: NINE
+ - room: Outside The Bold
+ panel: NINE
+ - room: Rhyme Room (Cross)
+ panel: NINE
+ - room: Orange Tower Fifth Floor
+ panel: NINE
+ - room: Elements Area
+ panel: NINE
+ paintings:
+ - id: smile_painting_5
+ enter_only: True
+ orientation: east
+ required_door:
+ door: Eights
+ Directional Gallery:
+ entrances:
+ Outside The Agreeable: True # sunwarp
+ Orange Tower First Floor:
+ room: Orange Tower First Floor
+ door: Salt Pepper Door
+ Outside The Undeterred:
+ door: Shortcut to The Undeterred
+ Number Hunt:
+ room: Number Hunt
+ door: Door to Directional Gallery
+ panels:
+ PEPPER:
+ id: Backside Room/Panel_pepper_salt
+ colors: black
+ tag: botblack
+ TURN:
+ id: Backside Room/Panel_turn_return
+ colors: blue
+ tag: midblue
+ LEARN:
+ id: Backside Room/Panel_learn_return
+ colors: purple
+ tag: midpurp
+ FIVE (1):
+ id: Backside Room/Panel_five_five_3
+ tag: midwhite
+ required_panel:
+ panel: LIGHT
+ FIVE (2):
+ id: Backside Room/Panel_five_five_2
+ tag: midwhite
+ required_panel:
+ panel: WARD
+ SIX (1):
+ id: Backside Room/Panel_six_six_3
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Sixes
+ SIX (2):
+ id: Backside Room/Panel_six_six_2
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Sixes
+ SEVEN:
+ id: Backside Room/Panel_seven_seven_2
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Sevens
+ EIGHT:
+ id: Backside Room/Panel_eight_eight_2
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Eights
+ NINE:
+ id: Backside Room/Panel_nine_nine_6
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Nines
+ BACKSIDE:
+ id: Backside Room/Panel_backside_4
+ tag: midwhite
+ "834283054":
+ id: Tower Room/Panel_834283054_undaunted
+ colors: orange
+ check: True
+ exclude_reduce: True
+ tag: midorange
+ required_door:
+ room: Number Hunt
+ door: Sixes
+ PARANOID:
+ id: Backside Room/Panel_paranoid_paranoid
+ tag: midwhite
+ check: True
+ exclude_reduce: True
+ required_door:
+ room: Number Hunt
+ door: Sixes
+ YELLOW:
+ id: Color Arrow Room/Panel_yellow_afar
+ tag: midwhite
+ required_door:
+ door: Yellow Barrier
+ WADED + WEE:
+ id: Tower Room/Panel_waded_wee_warts_7
+ colors: orange
+ check: True
+ exclude_reduce: True
+ tag: midorange
+ THE EYES:
+ id: Shuffle Room/Panel_theeyes_theeyes
+ tag: midwhite
+ LEFT:
+ id: Shuffle Room/Panel_left_left
+ tag: midwhite
+ RIGHT:
+ id: Shuffle Room/Panel_right_right
+ tag: midwhite
+ MIDDLE:
+ id: Shuffle Room/Panel_middle_middle
+ tag: midwhite
+ WARD:
+ id: Backside Room/Panel_ward_forward
+ colors: blue
+ tag: midblue
+ HIND:
+ id: Backside Room/Panel_hind_behind
+ colors: blue
+ tag: midblue
+ RIG:
+ id: Backside Room/Panel_rig_right
+ colors: blue
+ tag: midblue
+ WINDWARD:
+ id: Backside Room/Panel_windward_forward
+ colors: purple
+ tag: midpurp
+ LIGHT:
+ id: Backside Room/Panel_light_right
+ colors: purple
+ tag: midpurp
+ REWIND:
+ id: Backside Room/Panel_rewind_behind
+ colors: purple
+ tag: midpurp
+ doors:
+ Shortcut to The Undeterred:
+ id: Count Up Room Area Doors/Door_return_double
+ group: Directional Gallery Doors
+ panels:
+ - TURN
+ - LEARN
+ Yellow Barrier:
+ id: Color Arrow Room Doors/Door_yellow_4
+ group: Color Hunt Barriers
+ skip_location: True
+ panels:
+ - room: Champion's Rest
+ panel: YELLOW
+ paintings:
+ - id: smile_painting_7
+ orientation: south
+ - id: flower_painting_4
+ orientation: south
+ - id: pencil_painting3
+ enter_only: True
+ orientation: east
+ move: True
+ required_door:
+ room: Number Hunt
+ door: Sixes
+ - id: boxes_painting
+ orientation: south
+ - id: cherry_painting
+ orientation: east
+ Champion's Rest:
+ entrances:
+ Outside The Bold:
+ door: Shortcut to The Steady
+ Orange Tower Fourth Floor: True # sunwarp
+ Roof: True # through ceiling of sunwarp
+ panels:
+ EXIT:
+ id: Rock Room/Panel_red_red
+ tag: midwhite
+ HUES:
+ id: Color Arrow Room/Panel_hues_colors
+ tag: botwhite
+ RED:
+ id: Color Arrow Room/Panel_red_near
+ check: True
+ tag: midwhite
+ BLUE:
+ id: Color Arrow Room/Panel_blue_near
+ check: True
+ tag: midwhite
+ YELLOW:
+ id: Color Arrow Room/Panel_yellow_near
+ check: True
+ tag: midwhite
+ GREEN:
+ id: Color Arrow Room/Panel_green_near
+ check: True
+ tag: midwhite
+ required_door:
+ room: Outside The Initiated
+ door: Green Barrier
+ PURPLE:
+ id: Color Arrow Room/Panel_purple_near
+ check: True
+ tag: midwhite
+ required_door:
+ room: Outside The Initiated
+ door: Purple Barrier
+ ORANGE:
+ id: Color Arrow Room/Panel_orange_near
+ check: True
+ tag: midwhite
+ required_door:
+ room: Orange Tower Third Floor
+ door: Orange Barrier
+ YOU:
+ id: Color Arrow Room/Panel_you
+ required_door:
+ room: Outside The Initiated
+ door: Entrance
+ check: True
+ colors: gray
+ tag: forbid
+ ME:
+ id: Color Arrow Room/Panel_me
+ colors: gray
+ tag: forbid
+ required_door:
+ room: Outside The Initiated
+ door: Entrance
+ SECRET BLUE:
+ # Pretend this and the other two are white, because they are snipes.
+ # TODO: Extract them and randomize them?
+ id: Color Arrow Room/Panel_secret_blue
+ tag: forbid
+ required_door:
+ room: Outside The Initiated
+ door: Entrance
+ SECRET YELLOW:
+ id: Color Arrow Room/Panel_secret_yellow
+ tag: forbid
+ required_door:
+ room: Outside The Initiated
+ door: Entrance
+ SECRET RED:
+ id: Color Arrow Room/Panel_secret_red
+ tag: forbid
+ required_door:
+ room: Outside The Initiated
+ door: Entrance
+ doors:
+ Shortcut to The Steady:
+ id: Rock Room Doors/Door_hint
+ panels:
+ - EXIT
+ paintings:
+ - id: arrows_painting_7
+ orientation: east
+ - id: fruitbowl_painting3
+ orientation: west
+ enter_only: True
+ required_door:
+ room: Outside The Initiated
+ door: Entrance
+ - id: colors_painting
+ orientation: south
+ enter_only: True
+ required_door:
+ room: Outside The Initiated
+ door: Entrance
+ The Bearer:
+ entrances:
+ Outside The Bold:
+ door: Shortcut to The Bold
+ Orange Tower Fifth Floor:
+ room: Art Gallery
+ door: Exit
+ The Bearer (East): True
+ The Bearer (North): True
+ The Bearer (South): True
+ The Bearer (West): True
+ Roof: True
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_bearer_bearer
+ check: True
+ tag: forbid
+ required_panel:
+ - panel: PART
+ - panel: HEART
+ - room: Cross Tower (East)
+ panel: WINTER
+ - room: The Bearer (East)
+ panel: PEACE
+ - room: Cross Tower (North)
+ panel: NORTH
+ - room: The Bearer (North)
+ panel: SILENT (1)
+ - room: The Bearer (North)
+ panel: SILENT (2)
+ - room: The Bearer (North)
+ panel: SPACE
+ - room: The Bearer (North)
+ panel: WARTS
+ - room: Cross Tower (South)
+ panel: FIRE
+ - room: The Bearer (South)
+ panel: TENT
+ - room: The Bearer (South)
+ panel: BOWL
+ - room: Cross Tower (West)
+ panel: DIAMONDS
+ - room: The Bearer (West)
+ panel: SNOW
+ - room: The Bearer (West)
+ panel: SMILE
+ - room: Bearer Side Area
+ panel: SHORTCUT
+ - room: Bearer Side Area
+ panel: POTS
+ achievement: The Bearer
+ MIDDLE:
+ id: Shuffle Room/Panel_middle_middle_2
+ tag: midwhite
+ FARTHER:
+ id: Backside Room/Panel_farther_far
+ colors: red
+ tag: midred
+ BACKSIDE:
+ id: Backside Room/Panel_backside_5
+ tag: midwhite
+ required_door:
+ door: Backside Door
+ PART:
+ id: Cross Room/Panel_part_rap
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ required_panel:
+ room: The Bearer (East)
+ panel: PEACE
+ HEART:
+ id: Cross Room/Panel_heart_tar
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ doors:
+ Shortcut to The Bold:
+ id: Red Blue Purple Room Area Doors/Door_middle_middle
+ panels:
+ - MIDDLE
+ Backside Door:
+ id: Red Blue Purple Room Area Doors/Door_locked_knocked2 # yeah...
+ group: Backside Doors
+ panels:
+ - FARTHER
+ East Entrance:
+ event: True
+ panels:
+ - HEART
+ The Bearer (East):
+ entrances:
+ Cross Tower (East): True
+ Bearer Side Area:
+ door: Side Area Access
+ Roof: True
+ panels:
+ SIX:
+ id: Backside Room/Panel_six_six_5
+ tag: midwhite
+ colors:
+ - red
+ - yellow
+ required_door:
+ room: Number Hunt
+ door: Sixes
+ PEACE:
+ id: Cross Room/Panel_peace_ape
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ doors:
+ North Entrance:
+ event: True
+ panels:
+ - room: The Bearer
+ panel: PART
+ Side Area Access:
+ event: True
+ panels:
+ - room: The Bearer (North)
+ panel: SPACE
+ The Bearer (North):
+ entrances:
+ Cross Tower (East): True
+ Roof: True
+ panels:
+ SILENT (1):
+ id: Cross Room/Panel_silent_list
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ required_panel:
+ room: The Bearer (West)
+ panel: SMILE
+ SILENT (2):
+ id: Cross Room/Panel_silent_list_2
+ colors:
+ - red
+ - yellow
+ tag: mid yellow red
+ required_panel:
+ room: The Bearer (West)
+ panel: SMILE
+ SPACE:
+ id: Cross Room/Panel_space_cape
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ WARTS:
+ id: Cross Room/Panel_warts_star
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ required_panel:
+ room: The Bearer (West)
+ panel: SNOW
+ doors:
+ South Entrance:
+ event: True
+ panels:
+ - room: Bearer Side Area
+ panel: POTS
+ The Bearer (South):
+ entrances:
+ Cross Tower (North): True
+ Bearer Side Area:
+ door: Side Area Shortcut
+ Roof: True
+ panels:
+ SIX:
+ id: Backside Room/Panel_six_six_6
+ tag: midwhite
+ colors:
+ - red
+ - yellow
+ required_door:
+ room: Number Hunt
+ door: Sixes
+ TENT:
+ id: Cross Room/Panel_tent_net
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ BOWL:
+ id: Cross Room/Panel_bowl_low
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ required_panel:
+ panel: TENT
+ doors:
+ Side Area Shortcut:
+ event: True
+ panels:
+ - room: The Bearer (North)
+ panel: SILENT (1)
+ The Bearer (West):
+ entrances:
+ Cross Tower (West): True
+ Bearer Side Area:
+ door: Side Area Shortcut
+ Roof: True
+ panels:
+ SNOW:
+ id: Cross Room/Panel_smile_lime
+ colors:
+ - red
+ - yellow
+ tag: mid yellow red
+ SMILE:
+ id: Cross Room/Panel_snow_won
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ required_panel:
+ room: The Bearer (North)
+ panel: WARTS
+ doors:
+ Side Area Shortcut:
+ event: True
+ panels:
+ - room: Cross Tower (East)
+ panel: WINTER
+ - room: Cross Tower (North)
+ panel: NORTH
+ - room: Cross Tower (South)
+ panel: FIRE
+ - room: Cross Tower (West)
+ panel: DIAMONDS
+ Bearer Side Area:
+ entrances:
+ The Bearer (East):
+ room: The Bearer (East)
+ door: Side Area Access
+ The Bearer (South):
+ room: The Bearer (South)
+ door: Side Area Shortcut
+ The Bearer (West):
+ room: The Bearer (West)
+ door: Side Area Shortcut
+ Orange Tower Third Floor:
+ door: Shortcut to Tower
+ Roof: True
+ panels:
+ SHORTCUT:
+ id: Cross Room/Panel_shortcut_shortcut
+ tag: midwhite
+ POTS:
+ id: Cross Room/Panel_pots_top
+ colors:
+ - red
+ - yellow
+ tag: mid yellow red
+ doors:
+ Shortcut to Tower:
+ id: Cross Room Doors/Door_shortcut
+ item_name: The Bearer - Shortcut to Tower
+ location_name: The Bearer - SHORTCUT
+ panels:
+ - SHORTCUT
+ West Entrance:
+ event: True
+ panels:
+ - room: The Bearer (South)
+ panel: BOWL
+ Cross Tower (East):
+ entrances:
+ The Bearer:
+ room: The Bearer
+ door: East Entrance
+ Roof: True
+ panels:
+ WINTER:
+ id: Cross Room/Panel_winter_winter
+ colors: blue
+ tag: forbid
+ required_panel:
+ room: The Bearer (North)
+ panel: SPACE
+ required_room: Orange Tower Fifth Floor
+ Cross Tower (North):
+ entrances:
+ The Bearer (East):
+ room: The Bearer (East)
+ door: North Entrance
+ Roof: True
+ panels:
+ NORTH:
+ id: Cross Room/Panel_north_north
+ colors: blue
+ tag: forbid
+ required_panel:
+ room: The Bearer (West)
+ panel: SMILE
+ required_room: Outside The Bold
+ Cross Tower (South):
+ entrances: # No roof access
+ The Bearer (North):
+ room: The Bearer (North)
+ door: South Entrance
+ panels:
+ FIRE:
+ id: Cross Room/Panel_fire_fire
+ colors: blue
+ tag: forbid
+ required_panel:
+ room: The Bearer (North)
+ panel: SILENT (1)
+ required_room: Elements Area
+ Cross Tower (West):
+ entrances:
+ Bearer Side Area:
+ room: Bearer Side Area
+ door: West Entrance
+ Roof: True
+ panels:
+ DIAMONDS:
+ id: Cross Room/Panel_diamonds_diamonds
+ colors: blue
+ tag: forbid
+ required_panel:
+ room: The Bearer (North)
+ panel: WARTS
+ required_room: Suits Area
+ The Steady (Rose):
+ entrances:
+ Outside The Bold:
+ room: Outside The Bold
+ door: Steady Entrance
+ The Steady (Lilac):
+ room: The Steady
+ door: Reveal
+ The Steady (Ruby):
+ door: Forward Exit
+ The Steady (Carnation):
+ door: Right Exit
+ panels:
+ SOAR:
+ id: Rock Room/Panel_soar_rose
+ colors: black
+ tag: topblack
+ doors:
+ Forward Exit:
+ event: True
+ panels:
+ - SOAR
+ Right Exit:
+ event: True
+ panels:
+ - room: The Steady (Lilac)
+ panel: LIE LACK
+ The Steady (Ruby):
+ entrances:
+ The Steady (Rose):
+ room: The Steady (Rose)
+ door: Forward Exit
+ The Steady (Amethyst):
+ room: The Steady
+ door: Reveal
+ The Steady (Cherry):
+ door: Forward Exit
+ The Steady (Amber):
+ door: Right Exit
+ panels:
+ BURY:
+ id: Rock Room/Panel_bury_ruby
+ colors: yellow
+ tag: midyellow
+ doors:
+ Forward Exit:
+ event: True
+ panels:
+ - room: The Steady (Lime)
+ panel: LIMELIGHT
+ Right Exit:
+ event: True
+ panels:
+ - room: The Steady (Carnation)
+ panel: INCARNATION
+ The Steady (Carnation):
+ entrances:
+ The Steady (Rose):
+ room: The Steady (Rose)
+ door: Right Exit
+ Outside The Bold:
+ room: The Steady
+ door: Reveal
+ The Steady (Amber):
+ room: The Steady
+ door: Reveal
+ The Steady (Sunflower):
+ door: Right Exit
+ panels:
+ INCARNATION:
+ id: Rock Room/Panel_incarnation_carnation
+ colors: red
+ tag: midred
+ doors:
+ Right Exit:
+ event: True
+ panels:
+ - room: The Steady (Amethyst)
+ panel: PACIFIST
+ The Steady (Sunflower):
+ entrances:
+ The Steady (Carnation):
+ room: The Steady (Carnation)
+ door: Right Exit
+ The Steady (Topaz):
+ room: The Steady (Topaz)
+ door: Back Exit
+ panels:
+ SUN:
+ id: Rock Room/Panel_sun_sunflower
+ colors: blue
+ tag: midblue
+ doors:
+ Back Exit:
+ event: True
+ panels:
+ - SUN
+ The Steady (Plum):
+ entrances:
+ The Steady (Amethyst):
+ room: The Steady
+ door: Reveal
+ The Steady (Blueberry):
+ room: The Steady
+ door: Reveal
+ The Steady (Cherry):
+ room: The Steady (Cherry)
+ door: Left Exit
+ panels:
+ LUMP:
+ id: Rock Room/Panel_lump_plum
+ colors: yellow
+ tag: midyellow
+ The Steady (Lime):
+ entrances:
+ The Steady (Sunflower): True
+ The Steady (Emerald):
+ room: The Steady
+ door: Reveal
+ The Steady (Blueberry):
+ door: Right Exit
+ panels:
+ LIMELIGHT:
+ id: Rock Room/Panel_limelight_lime
+ colors: red
+ tag: midred
+ doors:
+ Right Exit:
+ event: True
+ panels:
+ - room: The Steady (Amber)
+ panel: ANTECHAMBER
+ paintings:
+ - id: pencil_painting5
+ orientation: south
+ The Steady (Lemon):
+ entrances:
+ The Steady (Emerald): True
+ The Steady (Orange):
+ room: The Steady
+ door: Reveal
+ The Steady (Topaz):
+ door: Back Exit
+ panels:
+ MELON:
+ id: Rock Room/Panel_melon_lemon
+ colors: yellow
+ tag: midyellow
+ doors:
+ Back Exit:
+ event: True
+ panels:
+ - MELON
+ paintings:
+ - id: pencil_painting4
+ orientation: south
+ The Steady (Topaz):
+ entrances:
+ The Steady (Lemon):
+ room: The Steady (Lemon)
+ door: Back Exit
+ The Steady (Amber):
+ room: The Steady
+ door: Reveal
+ The Steady (Sunflower):
+ door: Back Exit
+ panels:
+ TOP:
+ id: Rock Room/Panel_top_topaz
+ colors: blue
+ tag: midblue
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery2
+ tag: midwhite
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ doors:
+ Back Exit:
+ event: True
+ panels:
+ - TOP
+ The Steady (Orange):
+ entrances:
+ The Steady (Cherry):
+ room: The Steady
+ door: Reveal
+ The Steady (Lemon):
+ room: The Steady
+ door: Reveal
+ The Steady (Amber):
+ room: The Steady (Amber)
+ door: Forward Exit
+ panels:
+ BLUE:
+ id: Rock Room/Panel_blue_orange
+ colors: black
+ tag: botblack
+ The Steady (Sapphire):
+ entrances:
+ The Steady (Emerald):
+ door: Left Exit
+ The Steady (Blueberry):
+ room: The Steady
+ door: Reveal
+ The Steady (Amethyst):
+ room: The Steady (Amethyst)
+ door: Left Exit
+ panels:
+ SAP:
+ id: Rock Room/Panel_sap_sapphire
+ colors: blue
+ tag: midblue
+ doors:
+ Left Exit:
+ event: True
+ panels:
+ - room: The Steady (Plum)
+ panel: LUMP
+ - room: The Steady (Orange)
+ panel: BLUE
+ The Steady (Blueberry):
+ entrances:
+ The Steady (Lime):
+ room: The Steady (Lime)
+ door: Right Exit
+ The Steady (Sapphire):
+ room: The Steady
+ door: Reveal
+ The Steady (Plum):
+ room: The Steady
+ door: Reveal
+ panels:
+ BLUE:
+ id: Rock Room/Panel_blue_blueberry
+ colors: blue
+ tag: midblue
+ The Steady (Amber):
+ entrances:
+ The Steady (Ruby):
+ room: The Steady (Ruby)
+ door: Right Exit
+ The Steady (Carnation):
+ room: The Steady
+ door: Reveal
+ The Steady (Orange):
+ door: Forward Exit
+ The Steady (Topaz):
+ room: The Steady
+ door: Reveal
+ panels:
+ ANTECHAMBER:
+ id: Rock Room/Panel_antechamber_amber
+ colors: red
+ tag: midred
+ doors:
+ Forward Exit:
+ event: True
+ panels:
+ - room: The Steady (Blueberry)
+ panel: BLUE
+ The Steady (Emerald):
+ entrances:
+ The Steady (Sapphire):
+ room: The Steady (Sapphire)
+ door: Left Exit
+ The Steady (Lime):
+ room: The Steady
+ door: Reveal
+ panels:
+ HERALD:
+ id: Rock Room/Panel_herald_emerald
+ colors: purple
+ tag: midpurp
+ The Steady (Amethyst):
+ entrances:
+ The Steady (Lilac):
+ room: The Steady (Lilac)
+ door: Forward Exit
+ The Steady (Sapphire):
+ door: Left Exit
+ The Steady (Plum):
+ room: The Steady
+ door: Reveal
+ The Steady (Ruby):
+ room: The Steady
+ door: Reveal
+ panels:
+ PACIFIST:
+ id: Rock Room/Panel_thistle_amethyst
+ colors: purple
+ tag: toppurp
+ doors:
+ Left Exit:
+ event: True
+ panels:
+ - room: The Steady (Sunflower)
+ panel: SUN
+ The Steady (Lilac):
+ entrances:
+ Outside The Bold:
+ room: Outside The Bold
+ door: Lilac Entrance
+ The Steady (Amethyst):
+ door: Forward Exit
+ The Steady (Rose):
+ room: The Steady
+ door: Reveal
+ panels:
+ LIE LACK:
+ id: Rock Room/Panel_lielack_lilac
+ tag: topwhite
+ doors:
+ Forward Exit:
+ event: True
+ panels:
+ - room: The Steady (Ruby)
+ panel: BURY
+ The Steady (Cherry):
+ entrances:
+ The Steady (Plum):
+ door: Left Exit
+ The Steady (Orange):
+ room: The Steady
+ door: Reveal
+ The Steady (Ruby):
+ room: The Steady (Ruby)
+ door: Forward Exit
+ panels:
+ HAIRY:
+ id: Rock Room/Panel_hairy_cherry
+ colors: blue
+ tag: topblue
+ doors:
+ Left Exit:
+ event: True
+ panels:
+ - room: The Steady (Sapphire)
+ panel: SAP
+ The Steady:
+ entrances:
+ The Steady (Sunflower):
+ room: The Steady (Sunflower)
+ door: Back Exit
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_steady_steady
+ required_panel:
+ - room: The Steady (Rose)
+ panel: SOAR
+ - room: The Steady (Carnation)
+ panel: INCARNATION
+ - room: The Steady (Sunflower)
+ panel: SUN
+ - room: The Steady (Ruby)
+ panel: BURY
+ - room: The Steady (Plum)
+ panel: LUMP
+ - room: The Steady (Lime)
+ panel: LIMELIGHT
+ - room: The Steady (Lemon)
+ panel: MELON
+ - room: The Steady (Topaz)
+ panel: TOP
+ - room: The Steady (Orange)
+ panel: BLUE
+ - room: The Steady (Sapphire)
+ panel: SAP
+ - room: The Steady (Blueberry)
+ panel: BLUE
+ - room: The Steady (Amber)
+ panel: ANTECHAMBER
+ - room: The Steady (Emerald)
+ panel: HERALD
+ - room: The Steady (Amethyst)
+ panel: PACIFIST
+ - room: The Steady (Lilac)
+ panel: LIE LACK
+ - room: The Steady (Cherry)
+ panel: HAIRY
+ tag: forbid
+ check: True
+ achievement: The Steady
+ doors:
+ Reveal:
+ event: True
+ panels:
+ - Achievement
+ Knight Night (Outer Ring):
+ entrances:
+ Hidden Room:
+ room: Hidden Room
+ door: Knight Night Entrance
+ Knight Night Exit: True
+ panels:
+ NIGHT:
+ id: Appendix Room/Panel_night_knight
+ colors: blue
+ tag: homophone midblue
+ copy_to_sign: sign7
+ KNIGHT:
+ id: Appendix Room/Panel_knight_night
+ colors: red
+ tag: homophone midred
+ copy_to_sign: sign8
+ BEE:
+ id: Appendix Room/Panel_bee_be
+ colors: red
+ tag: homophone midred
+ copy_to_sign: sign9
+ NEW:
+ id: Appendix Room/Panel_new_knew
+ colors: blue
+ tag: homophone midblue
+ copy_to_sign: sign11
+ FORE:
+ id: Appendix Room/Panel_fore_for
+ colors: red
+ tag: homophone midred
+ copy_to_sign: sign10
+ TRUSTED (1):
+ id: Appendix Room/Panel_trusted_trust
+ colors: red
+ tag: midred
+ required_panel:
+ room: Knight Night (Right Lower Segment)
+ panel: BEFORE
+ TRUSTED (2):
+ id: Appendix Room/Panel_trusted_rusted
+ colors: red
+ tag: midred
+ required_panel:
+ room: Knight Night (Right Lower Segment)
+ panel: BEFORE
+ ENCRUSTED:
+ id: Appendix Room/Panel_encrusted_rust
+ colors: red
+ tag: midred
+ required_panel:
+ - panel: TRUSTED (1)
+ - panel: TRUSTED (2)
+ ADJUST (1):
+ id: Appendix Room/Panel_adjust_readjust
+ colors: blue
+ tag: midblue and phone
+ required_panel:
+ room: Knight Night (Right Lower Segment)
+ panel: BE
+ ADJUST (2):
+ id: Appendix Room/Panel_adjust_adjusted
+ colors: blue
+ tag: midblue and phone
+ required_panel:
+ room: Knight Night (Right Lower Segment)
+ panel: BE
+ RIGHT:
+ id: Appendix Room/Panel_right_right
+ tag: midwhite
+ required_panel:
+ room: Knight Night (Right Lower Segment)
+ panel: ADJUST
+ TRUST:
+ id: Appendix Room/Panel_trust_crust
+ colors:
+ - red
+ - blue
+ tag: mid red blue
+ required_panel:
+ - room: Knight Night (Right Lower Segment)
+ panel: ADJUST
+ - room: Knight Night (Right Lower Segment)
+ panel: LEFT
+ doors:
+ Fore Door:
+ event: True
+ panels:
+ - FORE
+ New Door:
+ event: True
+ panels:
+ - NEW
+ To End:
+ event: True
+ panels:
+ - RIGHT
+ - room: Knight Night (Right Lower Segment)
+ panel: LEFT
+ Knight Night (Right Upper Segment):
+ entrances:
+ Knight Night Exit: True
+ Knight Night (Outer Ring):
+ room: Knight Night (Outer Ring)
+ door: Fore Door
+ Knight Night (Right Lower Segment):
+ door: Segment Door
+ panels:
+ RUST (1):
+ id: Appendix Room/Panel_rust_trust
+ colors: blue
+ tag: midblue
+ required_panel:
+ room: Knight Night (Outer Ring)
+ panel: BEE
+ RUST (2):
+ id: Appendix Room/Panel_rust_crust
+ colors: blue
+ tag: midblue
+ required_panel:
+ room: Knight Night (Outer Ring)
+ panel: BEE
+ doors:
+ Segment Door:
+ event: True
+ panels:
+ - RUST (2)
+ - room: Knight Night (Right Lower Segment)
+ panel: BEFORE
+ Knight Night (Right Lower Segment):
+ entrances:
+ Knight Night Exit: True
+ Knight Night (Right Upper Segment):
+ room: Knight Night (Right Upper Segment)
+ door: Segment Door
+ Knight Night (Outer Ring):
+ room: Knight Night (Outer Ring)
+ door: New Door
+ panels:
+ ADJUST:
+ id: Appendix Room/Panel_adjust_readjusted
+ colors: blue
+ tag: midblue
+ required_panel:
+ - room: Knight Night (Outer Ring)
+ panel: ADJUST (1)
+ - room: Knight Night (Outer Ring)
+ panel: ADJUST (2)
+ BEFORE:
+ id: Appendix Room/Panel_before_fore
+ colors: red
+ tag: midred and phone
+ required_panel:
+ room: Knight Night (Right Upper Segment)
+ panel: RUST (1)
+ BE:
+ id: Appendix Room/Panel_be_before
+ colors: blue
+ tag: midblue and phone
+ required_panel:
+ room: Knight Night (Right Upper Segment)
+ panel: RUST (1)
+ LEFT:
+ id: Appendix Room/Panel_left_left
+ tag: midwhite
+ required_panel:
+ room: Knight Night (Outer Ring)
+ panel: ENCRUSTED
+ TRUST:
+ id: Appendix Room/Panel_trust_crust_2
+ colors: purple
+ tag: midpurp
+ required_panel:
+ - room: Knight Night (Outer Ring)
+ panel: ENCRUSTED
+ - room: Knight Night (Outer Ring)
+ panel: RIGHT
+ Knight Night (Final):
+ entrances:
+ Knight Night Exit: True
+ Knight Night (Outer Ring):
+ room: Knight Night (Outer Ring)
+ door: To End
+ Knight Night (Right Upper Segment):
+ room: Knight Night (Outer Ring)
+ door: To End
+ panels:
+ TRUSTED:
+ id: Appendix Room/Panel_trusted_readjusted
+ colors: purple
+ tag: midpurp
+ doors:
+ Exit:
+ id:
+ - Appendix Room Area Doors/Door_trusted_readjusted
+ - Appendix Room Area Doors/Door_trusted_readjusted2
+ - Appendix Room Area Doors/Door_trusted_readjusted3
+ - Appendix Room Area Doors/Door_trusted_readjusted4
+ - Appendix Room Area Doors/Door_trusted_readjusted5
+ - Appendix Room Area Doors/Door_trusted_readjusted6
+ - Appendix Room Area Doors/Door_trusted_readjusted7
+ - Appendix Room Area Doors/Door_trusted_readjusted8
+ - Appendix Room Area Doors/Door_trusted_readjusted9
+ - Appendix Room Area Doors/Door_trusted_readjusted10
+ - Appendix Room Area Doors/Door_trusted_readjusted11
+ - Appendix Room Area Doors/Door_trusted_readjusted12
+ - Appendix Room Area Doors/Door_trusted_readjusted13
+ include_reduce: True
+ location_name: Knight Night Room - TRUSTED
+ item_name: Knight Night Room - Exit
+ panels:
+ - TRUSTED
+ Knight Night Exit:
+ entrances:
+ Knight Night (Outer Ring):
+ room: Knight Night (Final)
+ door: Exit
+ Orange Tower Third Floor:
+ room: Knight Night (Final)
+ door: Exit
+ Outside The Initiated:
+ room: Knight Night (Final)
+ door: Exit
+ panels:
+ SEVEN (1):
+ id: Backside Room/Panel_seven_seven_7
+ tag: midwhite
+ required_door:
+ - room: Number Hunt
+ door: Sevens
+ SEVEN (2):
+ id: Backside Room/Panel_seven_seven_3
+ tag: midwhite
+ required_door:
+ - room: Number Hunt
+ door: Sevens
+ SEVEN (3):
+ id: Backside Room/Panel_seven_seven_4
+ tag: midwhite
+ required_door:
+ - room: Number Hunt
+ door: Sevens
+ DEAD END:
+ id: Appendix Room/Panel_deadend_deadend
+ tag: midwhite
+ WARNER:
+ id: Appendix Room/Panel_warner_corner
+ colors: purple
+ tag: toppurp
+ The Artistic (Smiley):
+ entrances:
+ Dead End Area:
+ painting: True
+ Crossroads:
+ painting: True
+ Hot Crusts Area:
+ painting: True
+ Outside The Initiated:
+ painting: True
+ Directional Gallery:
+ painting: True
+ Number Hunt:
+ room: Number Hunt
+ door: Eights
+ painting: True
+ Art Gallery:
+ painting: True
+ The Eyes They See:
+ painting: True
+ The Artistic (Panda):
+ door: Door to Panda
+ The Artistic (Apple):
+ room: The Artistic (Apple)
+ door: Door to Smiley
+ Elements Area:
+ room: Hallway Room (4)
+ door: Exit
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_artistic_artistic
+ colors:
+ - red
+ - black
+ - yellow
+ - blue
+ tag: forbid
+ required_room:
+ - The Artistic (Panda)
+ - The Artistic (Apple)
+ - The Artistic (Lattice)
+ check: True
+ achievement: The Artistic
+ FINE:
+ id: Ceiling Room/Panel_yellow_top_5
+ colors:
+ - yellow
+ - blue
+ tag: yellow top blue bot
+ subtag: top
+ link: yxu KNIFE
+ BLADE:
+ id: Ceiling Room/Panel_blue_bot_5
+ colors:
+ - blue
+ - yellow
+ tag: yellow top blue bot
+ subtag: bot
+ link: yxu KNIFE
+ RED:
+ id: Ceiling Room/Panel_blue_top_6
+ colors:
+ - blue
+ - yellow
+ tag: blue top yellow mid
+ subtag: top
+ link: uyx BREAD
+ BEARD:
+ id: Ceiling Room/Panel_yellow_mid_6
+ colors:
+ - yellow
+ - blue
+ tag: blue top yellow mid
+ subtag: mid
+ link: uyx BREAD
+ ICE:
+ id: Ceiling Room/Panel_blue_mid_7
+ colors:
+ - blue
+ - yellow
+ tag: blue mid yellow bot
+ subtag: mid
+ link: xuy SPICE
+ ROOT:
+ id: Ceiling Room/Panel_yellow_bot_7
+ colors:
+ - yellow
+ - blue
+ tag: blue mid yellow bot
+ subtag: bot
+ link: xuy SPICE
+ doors:
+ Door to Panda:
+ id:
+ - Ceiling Room Doors/Door_blue
+ - Ceiling Room Doors/Door_blue2
+ location_name: The Artistic - Smiley and Panda
+ group: Artistic Doors
+ panels:
+ - FINE
+ - BLADE
+ - RED
+ - BEARD
+ - ICE
+ - ROOT
+ - room: The Artistic (Panda)
+ panel: EYE (Top)
+ - room: The Artistic (Panda)
+ panel: EYE (Bottom)
+ - room: The Artistic (Panda)
+ panel: LADYLIKE
+ - room: The Artistic (Panda)
+ panel: WATER
+ - room: The Artistic (Panda)
+ panel: OURS
+ - room: The Artistic (Panda)
+ panel: DAYS
+ - room: The Artistic (Panda)
+ panel: NIGHTTIME
+ - room: The Artistic (Panda)
+ panel: NIGHT
+ paintings:
+ - id: smile_painting_9
+ orientation: north
+ exit_only: True
+ The Artistic (Panda):
+ entrances:
+ Orange Tower Sixth Floor:
+ painting: True
+ Outside The Agreeable:
+ painting: True
+ The Artistic (Smiley):
+ room: The Artistic (Smiley)
+ door: Door to Panda
+ The Artistic (Lattice):
+ door: Door to Lattice
+ panels:
+ EYE (Top):
+ id: Ceiling Room/Panel_blue_top_1
+ colors:
+ - blue
+ - red
+ tag: blue top red bot
+ subtag: top
+ link: uxr IRIS
+ EYE (Bottom):
+ id: Ceiling Room/Panel_red_bot_1
+ colors:
+ - red
+ - blue
+ tag: blue top red bot
+ subtag: bot
+ link: uxr IRIS
+ LADYLIKE:
+ id: Ceiling Room/Panel_red_mid_2
+ colors:
+ - red
+ - blue
+ tag: red mid blue bot
+ subtag: mid
+ link: xru LAKE
+ WATER:
+ id: Ceiling Room/Panel_blue_bot_2
+ colors:
+ - blue
+ - red
+ tag: red mid blue bot
+ subtag: bot
+ link: xru LAKE
+ OURS:
+ id: Ceiling Room/Panel_blue_mid_3
+ colors:
+ - blue
+ - red
+ tag: blue mid red bot
+ subtag: mid
+ link: xur HOURS
+ DAYS:
+ id: Ceiling Room/Panel_red_bot_3
+ colors:
+ - red
+ - blue
+ tag: blue mid red bot
+ subtag: bot
+ link: xur HOURS
+ NIGHTTIME:
+ id: Ceiling Room/Panel_red_top_4
+ colors:
+ - red
+ - blue
+ tag: red top mid blue
+ subtag: top
+ link: rux KNIGHT
+ NIGHT:
+ id: Ceiling Room/Panel_blue_mid_4
+ colors:
+ - blue
+ - red
+ tag: red top mid blue
+ subtag: mid
+ link: rux KNIGHT
+ doors:
+ Door to Lattice:
+ id:
+ - Ceiling Room Doors/Door_red
+ - Ceiling Room Doors/Door_red2
+ location_name: The Artistic - Panda and Lattice
+ group: Artistic Doors
+ panels:
+ - EYE (Top)
+ - EYE (Bottom)
+ - LADYLIKE
+ - WATER
+ - OURS
+ - DAYS
+ - NIGHTTIME
+ - NIGHT
+ - room: The Artistic (Lattice)
+ panel: POSH
+ - room: The Artistic (Lattice)
+ panel: MALL
+ - room: The Artistic (Lattice)
+ panel: DEICIDE
+ - room: The Artistic (Lattice)
+ panel: WAVER
+ - room: The Artistic (Lattice)
+ panel: REPAID
+ - room: The Artistic (Lattice)
+ panel: BABY
+ - room: The Artistic (Lattice)
+ panel: LOBE
+ - room: The Artistic (Lattice)
+ panel: BOWELS
+ paintings:
+ - id: panda_painting_3
+ exit_only: True
+ orientation: south
+ required_when_no_doors: True
+ The Artistic (Lattice):
+ entrances:
+ Directional Gallery:
+ painting: True
+ The Artistic (Panda):
+ room: The Artistic (Panda)
+ door: Door to Lattice
+ The Artistic (Apple):
+ door: Door to Apple
+ panels:
+ POSH:
+ id: Ceiling Room/Panel_black_top_12
+ colors:
+ - black
+ - red
+ tag: black top red bot
+ subtag: top
+ link: bxr SHOP
+ MALL:
+ id: Ceiling Room/Panel_red_bot_12
+ colors:
+ - red
+ - black
+ tag: black top red bot
+ subtag: bot
+ link: bxr SHOP
+ DEICIDE:
+ id: Ceiling Room/Panel_red_top_13
+ colors:
+ - red
+ - black
+ tag: red top black bot
+ subtag: top
+ link: rxb DECIDE
+ WAVER:
+ id: Ceiling Room/Panel_black_bot_13
+ colors:
+ - black
+ - red
+ tag: red top black bot
+ subtag: bot
+ link: rxb DECIDE
+ REPAID:
+ id: Ceiling Room/Panel_black_mid_14
+ colors:
+ - black
+ - red
+ tag: black mid red bot
+ subtag: mid
+ link: xbr DIAPER
+ BABY:
+ id: Ceiling Room/Panel_red_bot_14
+ colors:
+ - red
+ - black
+ tag: black mid red bot
+ subtag: bot
+ link: xbr DIAPER
+ LOBE:
+ id: Ceiling Room/Panel_black_top_15
+ colors:
+ - black
+ - red
+ tag: black top red mid
+ subtag: top
+ link: brx BOWL
+ BOWELS:
+ id: Ceiling Room/Panel_red_mid_15
+ colors:
+ - red
+ - black
+ tag: black top red mid
+ subtag: mid
+ link: brx BOWL
+ doors:
+ Door to Apple:
+ id:
+ - Ceiling Room Doors/Door_black
+ - Ceiling Room Doors/Door_black2
+ location_name: The Artistic - Lattice and Apple
+ group: Artistic Doors
+ panels:
+ - POSH
+ - MALL
+ - DEICIDE
+ - WAVER
+ - REPAID
+ - BABY
+ - LOBE
+ - BOWELS
+ - room: The Artistic (Apple)
+ panel: SPRIG
+ - room: The Artistic (Apple)
+ panel: RELEASES
+ - room: The Artistic (Apple)
+ panel: MUCH
+ - room: The Artistic (Apple)
+ panel: FISH
+ - room: The Artistic (Apple)
+ panel: MASK
+ - room: The Artistic (Apple)
+ panel: HILL
+ - room: The Artistic (Apple)
+ panel: TINE
+ - room: The Artistic (Apple)
+ panel: THING
+ paintings:
+ - id: boxes_painting2
+ orientation: south
+ exit_only: True
+ required_when_no_doors: True
+ The Artistic (Apple):
+ entrances:
+ Orange Tower Sixth Floor:
+ painting: True
+ Directional Gallery:
+ painting: True
+ The Artistic (Lattice):
+ room: The Artistic (Lattice)
+ door: Door to Apple
+ The Artistic (Smiley):
+ door: Door to Smiley
+ panels:
+ SPRIG:
+ id: Ceiling Room/Panel_yellow_mid_8
+ colors:
+ - yellow
+ - black
+ tag: yellow mid black bot
+ subtag: mid
+ link: xyb GRIPS
+ RELEASES:
+ id: Ceiling Room/Panel_black_bot_8
+ colors:
+ - black
+ - yellow
+ tag: yellow mid black bot
+ subtag: bot
+ link: xyb GRIPS
+ MUCH:
+ id: Ceiling Room/Panel_black_top_9
+ colors:
+ - black
+ - yellow
+ tag: black top yellow bot
+ subtag: top
+ link: bxy CHUM
+ FISH:
+ id: Ceiling Room/Panel_yellow_bot_9
+ colors:
+ - yellow
+ - black
+ tag: black top yellow bot
+ subtag: bot
+ link: bxy CHUM
+ MASK:
+ id: Ceiling Room/Panel_yellow_top_10
+ colors:
+ - yellow
+ - black
+ tag: yellow top black bot
+ subtag: top
+ link: yxb CHASM
+ HILL:
+ id: Ceiling Room/Panel_black_bot_10
+ colors:
+ - black
+ - yellow
+ tag: yellow top black bot
+ subtag: bot
+ link: yxb CHASM
+ TINE:
+ id: Ceiling Room/Panel_black_top_11
+ colors:
+ - black
+ - yellow
+ tag: black top yellow mid
+ subtag: top
+ link: byx NIGHT
+ THING:
+ id: Ceiling Room/Panel_yellow_mid_11
+ colors:
+ - yellow
+ - black
+ tag: black top yellow mid
+ subtag: mid
+ link: byx NIGHT
+ doors:
+ Door to Smiley:
+ id:
+ - Ceiling Room Doors/Door_yellow
+ - Ceiling Room Doors/Door_yellow2
+ location_name: The Artistic - Apple and Smiley
+ group: Artistic Doors
+ panels:
+ - SPRIG
+ - RELEASES
+ - MUCH
+ - FISH
+ - MASK
+ - HILL
+ - TINE
+ - THING
+ - room: The Artistic (Smiley)
+ panel: FINE
+ - room: The Artistic (Smiley)
+ panel: BLADE
+ - room: The Artistic (Smiley)
+ panel: RED
+ - room: The Artistic (Smiley)
+ panel: BEARD
+ - room: The Artistic (Smiley)
+ panel: ICE
+ - room: The Artistic (Smiley)
+ panel: ROOT
+ paintings:
+ - id: cherry_painting3
+ orientation: north
+ exit_only: True
+ required_when_no_doors: True
+ The Artistic (Hint Room):
+ entrances:
+ The Artistic (Lattice):
+ room: The Artistic (Lattice)
+ door: Door to Apple
+ panels:
+ THEME:
+ id: Ceiling Room/Panel_answer_1
+ colors: red
+ tag: midred
+ PAINTS:
+ id: Ceiling Room/Panel_answer_2
+ colors: yellow
+ tag: botyellow
+ I:
+ id: Ceiling Room/Panel_answer_3
+ colors: blue
+ tag: midblue
+ KIT:
+ id: Ceiling Room/Panel_answer_4
+ colors: black
+ tag: topblack
+ The Discerning:
+ entrances:
+ Crossroads:
+ room: Crossroads
+ door: Discerning Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_discerning_scramble
+ colors: yellow
+ tag: forbid
+ check: True
+ achievement: The Discerning
+ HITS:
+ id: Sun Room/Panel_hits_this
+ colors: yellow
+ tag: midyellow
+ WARRED:
+ id: Sun Room/Panel_warred_drawer
+ colors: yellow
+ tag: double midyellow
+ subtag: left
+ link: ana DRAWER
+ REDRAW:
+ id: Sun Room/Panel_redraw_drawer
+ colors: yellow
+ tag: double midyellow
+ subtag: right
+ link: ana DRAWER
+ ADDER:
+ id: Sun Room/Panel_adder_dread
+ colors: yellow
+ tag: midyellow
+ LAUGHTERS:
+ id: Sun Room/Panel_laughters_slaughter
+ colors: yellow
+ tag: midyellow
+ STONE:
+ id: Sun Room/Panel_stone_notes
+ colors: yellow
+ tag: double midyellow
+ subtag: left
+ link: ana NOTES
+ ONSET:
+ id: Sun Room/Panel_onset_notes
+ colors: yellow
+ tag: double midyellow
+ subtag: right
+ link: ana NOTES
+ RAT:
+ id: Sun Room/Panel_rat_art
+ colors: yellow
+ tag: midyellow
+ DUSTY:
+ id: Sun Room/Panel_dusty_study
+ colors: yellow
+ tag: midyellow
+ ARTS:
+ id: Sun Room/Panel_arts_star
+ colors: yellow
+ tag: double midyellow
+ subtag: left
+ link: ana STAR
+ TSAR:
+ id: Sun Room/Panel_tsar_star
+ colors: yellow
+ tag: double midyellow
+ subtag: right
+ link: ana STAR
+ STATE:
+ id: Sun Room/Panel_state_taste
+ colors: yellow
+ tag: midyellow
+ REACT:
+ id: Sun Room/Panel_react_trace
+ colors: yellow
+ tag: midyellow
+ DEAR:
+ id: Sun Room/Panel_dear_read
+ colors: yellow
+ tag: double midyellow
+ subtag: left
+ link: ana READ
+ DARE:
+ id: Sun Room/Panel_dare_read
+ colors: yellow
+ tag: double midyellow
+ subtag: right
+ link: ana READ
+ SEAM:
+ id: Sun Room/Panel_seam_same
+ colors: yellow
+ tag: midyellow
+ The Eyes They See:
+ entrances:
+ Crossroads:
+ room: Crossroads
+ door: Eye Wall
+ painting: True
+ Wondrous Lobby:
+ door: Exit
+ Directional Gallery: True
+ panels:
+ NEAR:
+ id: Shuffle Room/Panel_near_near
+ tag: midwhite
+ EIGHT:
+ id: Backside Room/Panel_eight_eight_4
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Eights
+ doors:
+ Exit:
+ id: Count Up Room Area Doors/Door_near_near
+ group: Crossroads Doors
+ panels:
+ - NEAR
+ paintings:
+ - id: eye_painting_2
+ orientation: west
+ - id: smile_painting_2
+ orientation: north
+ Far Window:
+ entrances:
+ Crossroads:
+ room: Crossroads
+ door: Eye Wall
+ The Eyes They See: True
+ panels:
+ FAR:
+ id: Shuffle Room/Panel_far_far
+ tag: midwhite
+ Wondrous Lobby:
+ entrances:
+ Directional Gallery: True
+ The Eyes They See:
+ room: The Eyes They See
+ door: Exit
+ paintings:
+ - id: arrows_painting_5
+ orientation: east
+ Outside The Wondrous:
+ entrances:
+ Wondrous Lobby: True
+ The Wondrous (Doorknob):
+ door: Wondrous Entrance
+ The Wondrous (Window): True
+ panels:
+ SHRINK:
+ id: Wonderland Room/Panel_shrink_shrink
+ tag: midwhite
+ doors:
+ Wondrous Entrance:
+ id: Red Blue Purple Room Area Doors/Door_wonderland
+ item_name: The Wondrous - Entrance
+ panels:
+ - SHRINK
+ The Wondrous (Doorknob):
+ entrances:
+ Outside The Wondrous:
+ room: Outside The Wondrous
+ door: Wondrous Entrance
+ Starting Room:
+ door: Painting Shortcut
+ painting: True
+ The Wondrous (Chandelier):
+ painting: True
+ The Wondrous (Table): True # There is a way that doesn't use the painting
+ doors:
+ Painting Shortcut:
+ painting_id:
+ - symmetry_painting_a_starter
+ - arrows_painting2
+ skip_location: True
+ item_name: Starting Room - Symmetry Painting
+ panels:
+ - room: Outside The Wondrous
+ panel: SHRINK
+ paintings:
+ - id: symmetry_painting_a_1
+ orientation: east
+ exit_only: True
+ - id: symmetry_painting_b_1
+ orientation: south
+ The Wondrous (Bookcase):
+ entrances:
+ The Wondrous (Doorknob): True
+ panels:
+ CASE:
+ id: Wonderland Room/Panel_case_bookcase
+ colors: blue
+ tag: midblue
+ paintings:
+ - id: symmetry_painting_a_3
+ orientation: west
+ exit_only: True
+ - id: symmetry_painting_b_3
+ disable: True
+ The Wondrous (Chandelier):
+ entrances:
+ The Wondrous (Bookcase): True
+ panels:
+ CANDLE HEIR:
+ id: Wonderland Room/Panel_candleheir_chandelier
+ colors: yellow
+ tag: midyellow
+ paintings:
+ - id: symmetry_painting_a_5
+ orientation: east
+ - id: symmetry_painting_a_5
+ disable: True
+ The Wondrous (Window):
+ entrances:
+ The Wondrous (Bookcase): True
+ panels:
+ GLASS:
+ id: Wonderland Room/Panel_glass_window
+ colors: brown
+ tag: botbrown
+ paintings:
+ - id: symmetry_painting_b_4
+ orientation: north
+ exit_only: True
+ - id: symmetry_painting_a_4
+ disable: True
+ The Wondrous (Table):
+ entrances:
+ The Wondrous (Doorknob):
+ painting: True
+ The Wondrous:
+ painting: True
+ panels:
+ WOOD:
+ id: Wonderland Room/Panel_wood_table
+ colors: brown
+ tag: botbrown
+ BROOK NOD:
+ # This panel, while physically being in the first room, is facing upward
+ # and is only really solvable while standing on the windowsill, which is
+ # a location you can only get to from Table.
+ id: Wonderland Room/Panel_brooknod_doorknob
+ colors: yellow
+ tag: midyellow
+ paintings:
+ - id: symmetry_painting_a_2
+ orientation: west
+ - id: symmetry_painting_b_2
+ orientation: south
+ exit_only: True
+ required: True
+ The Wondrous:
+ entrances:
+ The Wondrous (Table): True
+ Arrow Garden:
+ door: Exit
+ panels:
+ FIREPLACE:
+ id: Wonderland Room/Panel_fireplace_fire
+ colors: red
+ tag: midred
+ Achievement:
+ id: Countdown Panels/Panel_wondrous_wondrous
+ required_panel:
+ - panel: FIREPLACE
+ - room: The Wondrous (Table)
+ panel: BROOK NOD
+ - room: The Wondrous (Bookcase)
+ panel: CASE
+ - room: The Wondrous (Chandelier)
+ panel: CANDLE HEIR
+ - room: The Wondrous (Window)
+ panel: GLASS
+ - room: The Wondrous (Table)
+ panel: WOOD
+ tag: forbid
+ achievement: The Wondrous
+ doors:
+ Exit:
+ id: Red Blue Purple Room Area Doors/Door_wonderland_exit
+ painting_id: arrows_painting_9
+ include_reduce: True
+ panels:
+ - Achievement
+ paintings:
+ - id: arrows_painting_9
+ enter_only: True
+ orientation: south
+ move: True
+ required_door:
+ door: Exit
+ - id: symmetry_painting_a_6
+ orientation: west
+ exit_only: True
+ - id: symmetry_painting_b_6
+ orientation: north
+ Arrow Garden:
+ entrances:
+ The Wondrous:
+ room: The Wondrous
+ door: Exit
+ Roof: True
+ panels:
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery4
+ tag: midwhite
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ SHARP:
+ id: Open Areas/Panel_rainy_rainbow2
+ tag: midwhite
+ paintings:
+ - id: flower_painting_6
+ orientation: south
+ Hallway Room (2):
+ entrances:
+ Outside The Agreeable:
+ room: Outside The Agreeable
+ door: Hallway Door
+ Elements Area: True
+ panels:
+ WISE:
+ id: Hallway Room/Panel_counterclockwise_1
+ colors: blue
+ tag: quad mid blue
+ link: qmb COUNTERCLOCKWISE
+ CLOCK:
+ id: Hallway Room/Panel_counterclockwise_2
+ colors: blue
+ tag: quad mid blue
+ link: qmb COUNTERCLOCKWISE
+ ER:
+ id: Hallway Room/Panel_counterclockwise_3
+ colors: blue
+ tag: quad mid blue
+ link: qmb COUNTERCLOCKWISE
+ COUNT:
+ id: Hallway Room/Panel_counterclockwise_4
+ colors: blue
+ tag: quad mid blue
+ link: qmb COUNTERCLOCKWISE
+ doors:
+ Exit:
+ id: Red Blue Purple Room Area Doors/Door_room_3
+ location_name: Hallway Room - Second Room
+ group: Hallway Room Doors
+ panels:
+ - WISE
+ - CLOCK
+ - ER
+ - COUNT
+ Hallway Room (3):
+ entrances:
+ Hallway Room (2):
+ room: Hallway Room (2)
+ door: Exit
+ # No entrance from Elements Area. The winding hallway does not connect.
+ panels:
+ TRANCE:
+ id: Hallway Room/Panel_transformation_1
+ colors: blue
+ tag: quad top blue
+ link: qtb TRANSFORMATION
+ FORM:
+ id: Hallway Room/Panel_transformation_2
+ colors: blue
+ tag: quad top blue
+ link: qtb TRANSFORMATION
+ A:
+ id: Hallway Room/Panel_transformation_3
+ colors: blue
+ tag: quad top blue
+ link: qtb TRANSFORMATION
+ SHUN:
+ id: Hallway Room/Panel_transformation_4
+ colors: blue
+ tag: quad top blue
+ link: qtb TRANSFORMATION
+ doors:
+ Exit:
+ id: Red Blue Purple Room Area Doors/Door_room_4
+ location_name: Hallway Room - Third Room
+ group: Hallway Room Doors
+ panels:
+ - TRANCE
+ - FORM
+ - A
+ - SHUN
+ Hallway Room (4):
+ entrances:
+ Hallway Room (3):
+ room: Hallway Room (3)
+ door: Exit
+ Elements Area: True
+ panels:
+ WHEEL:
+ id: Hallway Room/Panel_room_5
+ colors: blue
+ tag: full stack blue
+ doors:
+ Exit:
+ id:
+ - Red Blue Purple Room Area Doors/Door_room_5
+ - Red Blue Purple Room Area Doors/Door_room_6 # this is the connection to The Artistic
+ group: Hallway Room Doors
+ location_name: Hallway Room - Fourth Room
+ panels:
+ - WHEEL
+ include_reduce: True
+ Elements Area:
+ entrances:
+ Roof: True
+ Hallway Room (4):
+ room: Hallway Room (4)
+ door: Exit
+ The Artistic (Smiley):
+ room: Hallway Room (4)
+ door: Exit
+ panels:
+ A:
+ id: Strand Room/Panel_a_strands
+ colors: blue
+ tag: forbid
+ NINE:
+ id: Backside Room/Panel_nine_nine_7
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Nines
+ UNDISTRACTED:
+ id: Open Areas/Panel_undistracted
+ check: True
+ exclude_reduce: True
+ tag: midwhite
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery13
+ tag: midwhite
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ EARTH:
+ id: Cross Room/Panel_earth_earth
+ tag: midwhite
+ WATER:
+ id: Cross Room/Panel_water_water
+ tag: midwhite
+ AIR:
+ id: Cross Room/Panel_air_air
+ tag: midwhite
+ paintings:
+ - id: south_afar
+ orientation: south
+ Outside The Wanderer:
+ entrances:
+ Orange Tower First Floor:
+ door: Tower Entrance
+ Rhyme Room (Cross):
+ room: Rhyme Room (Cross)
+ door: Exit
+ Roof: True
+ panels:
+ WANDERLUST:
+ id: Tower Room/Panel_wanderlust_1234567890
+ colors: orange
+ tag: midorange
+ doors:
+ Wanderer Entrance:
+ id: Tower Room Area Doors/Door_wanderer_entrance
+ item_name: The Wanderer - Entrance
+ panels:
+ - WANDERLUST
+ Tower Entrance:
+ id: Tower Room Area Doors/Door_wanderlust_start
+ skip_location: True
+ panels:
+ - room: The Wanderer
+ panel: Achievement
+ The Wanderer:
+ entrances:
+ Outside The Wanderer:
+ room: Outside The Wanderer
+ door: Wanderer Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_1234567890_wanderlust
+ colors: orange
+ check: True
+ tag: forbid
+ achievement: The Wanderer
+ "7890":
+ id: Orange Room/Panel_lust
+ colors: orange
+ tag: midorange
+ "6524":
+ id: Orange Room/Panel_read
+ colors: orange
+ tag: midorange
+ "951":
+ id: Orange Room/Panel_sew
+ colors: orange
+ tag: midorange
+ "4524":
+ id: Orange Room/Panel_dead
+ colors: orange
+ tag: midorange
+ LEARN:
+ id: Orange Room/Panel_learn
+ colors: orange
+ tag: midorange
+ DUST:
+ id: Orange Room/Panel_dust
+ colors: orange
+ tag: midorange
+ STAR:
+ id: Orange Room/Panel_star
+ colors: orange
+ tag: midorange
+ WANDER:
+ id: Orange Room/Panel_wander
+ colors: orange
+ tag: midorange
+ Art Gallery:
+ entrances:
+ Orange Tower Third Floor: True
+ Art Gallery (Second Floor): True
+ Art Gallery (Third Floor): True
+ Art Gallery (Fourth Floor): True
+ Orange Tower Fifth Floor:
+ door: Exit
+ panels:
+ EIGHT:
+ id: Backside Room/Panel_eight_eight_6
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Eights
+ EON:
+ id: Painting Room/Panel_eon_one
+ colors: yellow
+ tag: midyellow
+ TRUSTWORTHY:
+ id: Painting Room/Panel_to_two
+ colors: red
+ tag: midred
+ FREE:
+ id: Painting Room/Panel_free_three
+ colors: purple
+ tag: midpurp
+ OUR:
+ id: Painting Room/Panel_our_four
+ colors: blue
+ tag: midblue
+ ONE ROAD MANY TURNS:
+ id: Painting Room/Panel_order_onepathmanyturns
+ tag: forbid
+ colors:
+ - yellow
+ - blue
+ - gray
+ - brown
+ - orange
+ required_door:
+ door: Fifth Floor
+ doors:
+ Second Floor:
+ painting_id:
+ - scenery_painting_2b
+ - scenery_painting_2c
+ skip_location: True
+ panels:
+ - EON
+ First Floor Puzzles:
+ skip_item: True
+ location_name: Art Gallery - First Floor Puzzles
+ panels:
+ - EON
+ - TRUSTWORTHY
+ - FREE
+ - OUR
+ Third Floor:
+ painting_id:
+ - scenery_painting_3b
+ - scenery_painting_3c
+ skip_location: True
+ panels:
+ - room: Art Gallery (Second Floor)
+ panel: PATH
+ Fourth Floor:
+ painting_id:
+ - scenery_painting_4b
+ - scenery_painting_4c
+ skip_location: True
+ panels:
+ - room: Art Gallery (Third Floor)
+ panel: ANY
+ Fifth Floor:
+ id: Tower Room Area Doors/Door_painting_backroom
+ painting_id:
+ - scenery_painting_5b
+ - scenery_painting_5c
+ skip_location: True
+ panels:
+ - room: Art Gallery (Fourth Floor)
+ panel: SEND - USE
+ Exit:
+ id: Tower Room Area Doors/Door_painting_exit
+ include_reduce: True
+ panels:
+ - ONE ROAD MANY TURNS
+ paintings:
+ - id: smile_painting_3
+ orientation: west
+ - id: flower_painting_2
+ orientation: east
+ - id: scenery_painting_0a
+ orientation: north
+ - id: map_painting
+ orientation: east
+ - id: fruitbowl_painting4
+ orientation: south
+ progression:
+ Progressive Art Gallery:
+ - Second Floor
+ - Third Floor
+ - Fourth Floor
+ - Fifth Floor
+ - Exit
+ Art Gallery (Second Floor):
+ entrances:
+ Art Gallery:
+ room: Art Gallery
+ door: Second Floor
+ panels:
+ HOUSE:
+ id: Painting Room/Panel_house_neighborhood
+ colors: blue
+ tag: botblue
+ PATH:
+ id: Painting Room/Panel_path_road
+ colors: brown
+ tag: botbrown
+ PARK:
+ id: Painting Room/Panel_park_drive
+ colors: black
+ tag: botblack
+ CARRIAGE:
+ id: Painting Room/Panel_carriage_horse
+ colors: red
+ tag: botred
+ doors:
+ Puzzles:
+ skip_item: True
+ location_name: Art Gallery - Second Floor Puzzles
+ panels:
+ - HOUSE
+ - PATH
+ - PARK
+ - CARRIAGE
+ Art Gallery (Third Floor):
+ entrances:
+ Art Gallery:
+ room: Art Gallery
+ door: Third Floor
+ panels:
+ AN:
+ id: Painting Room/Panel_an_many
+ colors: blue
+ tag: midblue
+ MAY:
+ id: Painting Room/Panel_may_many
+ colors: blue
+ tag: midblue
+ ANY:
+ id: Painting Room/Panel_any_many
+ colors: blue
+ tag: midblue
+ MAN:
+ id: Painting Room/Panel_man_many
+ colors: blue
+ tag: midblue
+ doors:
+ Puzzles:
+ skip_item: True
+ location_name: Art Gallery - Third Floor Puzzles
+ panels:
+ - AN
+ - MAY
+ - ANY
+ - MAN
+ Art Gallery (Fourth Floor):
+ entrances:
+ Art Gallery:
+ room: Art Gallery
+ door: Fourth Floor
+ panels:
+ URNS:
+ id: Painting Room/Panel_urns_turns
+ colors: blue
+ tag: midblue
+ LEARNS:
+ id: Painting Room/Panel_learns_turns
+ colors: purple
+ tag: midpurp
+ RUNTS:
+ id: Painting Room/Panel_runts_turns
+ colors: yellow
+ tag: midyellow
+ SEND - USE:
+ id: Painting Room/Panel_send_use_turns
+ colors: orange
+ tag: midorange
+ TRUST:
+ id: Painting Room/Panel_trust_06890
+ colors: orange
+ tag: midorange
+ "062459":
+ id: Painting Room/Panel_06890_trust
+ colors: orange
+ tag: midorange
+ doors:
+ Puzzles:
+ skip_item: True
+ location_name: Art Gallery - Fourth Floor Puzzles
+ panels:
+ - URNS
+ - LEARNS
+ - RUNTS
+ - SEND - USE
+ - TRUST
+ - "062459"
+ Rhyme Room (Smiley):
+ entrances:
+ Orange Tower Third Floor:
+ room: Orange Tower Third Floor
+ door: Rhyme Room Entrance
+ Rhyme Room (Circle):
+ room: Rhyme Room (Circle)
+ door: Door to Smiley
+ Rhyme Room (Cross): True # one-way
+ panels:
+ LOANS:
+ id: Double Room/Panel_bones_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme BONES
+ SKELETON:
+ id: Double Room/Panel_bones_syn
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme BONES
+ REPENTANCE:
+ id: Double Room/Panel_sentence_rhyme
+ colors: purple
+ tag: whole rhyme
+ subtag: top
+ link: rhyme SENTENCE
+ WORD:
+ id: Double Room/Panel_sentence_whole
+ colors: blue
+ tag: whole rhyme
+ subtag: bot
+ link: rhyme SENTENCE
+ SCHEME:
+ id: Double Room/Panel_dream_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme DREAM
+ FANTASY:
+ id: Double Room/Panel_dream_syn
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme DREAM
+ HISTORY:
+ id: Double Room/Panel_mystery_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme MYSTERY
+ SECRET:
+ id: Double Room/Panel_mystery_syn
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme MYSTERY
+ doors:
+ # This is complicated. I want the location in here to just be the four
+ # panels against the wall toward Target. But in vanilla, you also need to
+ # solve the panels in Circle that are against the Smiley wall. Logic needs
+ # to know this so that it can handle no door shuffle properly. So we split
+ # the item and location up.
+ Door to Target:
+ id:
+ - Double Room Area Doors/Door_room_3a
+ - Double Room Area Doors/Door_room_3bc
+ skip_location: True
+ group: Rhyme Room Doors
+ panels:
+ - SCHEME
+ - FANTASY
+ - HISTORY
+ - SECRET
+ - room: Rhyme Room (Circle)
+ panel: BIRD
+ - room: Rhyme Room (Circle)
+ panel: LETTER
+ - room: Rhyme Room (Circle)
+ panel: VIOLENT
+ - room: Rhyme Room (Circle)
+ panel: MUTE
+ Door to Target (Location):
+ location_name: Rhyme Room (Smiley) - Puzzles Toward Target
+ skip_item: True
+ panels:
+ - SCHEME
+ - FANTASY
+ - HISTORY
+ - SECRET
+ Rhyme Room (Cross):
+ entrances:
+ Rhyme Room (Target): # one-way
+ room: Rhyme Room (Target)
+ door: Door to Cross
+ Rhyme Room (Looped Square):
+ room: Rhyme Room (Looped Square)
+ door: Door to Cross
+ panels:
+ NINE:
+ id: Backside Room/Panel_nine_nine_9
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Nines
+ FERN:
+ id: Double Room/Panel_return_rhyme
+ colors: purple
+ tag: ant rhyme
+ subtag: top
+ link: rhyme RETURN
+ STAY:
+ id: Double Room/Panel_return_ant
+ colors: black
+ tag: ant rhyme
+ subtag: bot
+ link: rhyme RETURN
+ FRIEND:
+ id: Double Room/Panel_descend_rhyme
+ colors: purple
+ tag: ant rhyme
+ subtag: top
+ link: rhyme DESCEND
+ RISE:
+ id: Double Room/Panel_descend_ant
+ colors: black
+ tag: ant rhyme
+ subtag: bot
+ link: rhyme DESCEND
+ PLUMP:
+ id: Double Room/Panel_jump_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme JUMP
+ BOUNCE:
+ id: Double Room/Panel_jump_syn
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme JUMP
+ SCRAWL:
+ id: Double Room/Panel_fall_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme FALL
+ PLUNGE:
+ id: Double Room/Panel_fall_syn
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme FALL
+ LEAP:
+ id: Double Room/Panel_leap_leap
+ tag: midwhite
+ doors:
+ Exit:
+ id: Double Room Area Doors/Door_room_exit
+ location_name: Rhyme Room (Cross) - Exit Puzzles
+ group: Rhyme Room Doors
+ panels:
+ - PLUMP
+ - BOUNCE
+ - SCRAWL
+ - PLUNGE
+ Rhyme Room (Circle):
+ entrances:
+ Rhyme Room (Looped Square):
+ room: Rhyme Room (Looped Square)
+ door: Door to Circle
+ Hidden Room:
+ room: Hidden Room
+ door: Rhyme Room Entrance
+ Rhyme Room (Smiley):
+ door: Door to Smiley
+ panels:
+ BIRD:
+ id: Double Room/Panel_word_rhyme
+ colors: purple
+ tag: whole rhyme
+ subtag: top
+ link: rhyme WORD
+ LETTER:
+ id: Double Room/Panel_word_whole
+ colors: blue
+ tag: whole rhyme
+ subtag: bot
+ link: rhyme WORD
+ FORBIDDEN:
+ id: Double Room/Panel_hidden_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme HIDDEN
+ CONCEALED:
+ id: Double Room/Panel_hidden_syn
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme HIDDEN
+ VIOLENT:
+ id: Double Room/Panel_silent_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme SILENT
+ MUTE:
+ id: Double Room/Panel_silent_syn
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme SILENT
+ doors:
+ Door to Smiley:
+ id:
+ - Double Room Area Doors/Door_room_2b
+ - Double Room Area Doors/Door_room_3b
+ location_name: Rhyme Room - Circle/Smiley Wall
+ group: Rhyme Room Doors
+ panels:
+ - BIRD
+ - LETTER
+ - VIOLENT
+ - MUTE
+ - room: Rhyme Room (Smiley)
+ panel: LOANS
+ - room: Rhyme Room (Smiley)
+ panel: SKELETON
+ - room: Rhyme Room (Smiley)
+ panel: REPENTANCE
+ - room: Rhyme Room (Smiley)
+ panel: WORD
+ paintings:
+ - id: arrows_painting_3
+ orientation: north
+ Rhyme Room (Looped Square):
+ entrances:
+ Starting Room:
+ room: Starting Room
+ door: Rhyme Room Entrance
+ Rhyme Room (Circle):
+ door: Door to Circle
+ Rhyme Room (Cross):
+ door: Door to Cross
+ Rhyme Room (Target):
+ door: Door to Target
+ panels:
+ WALKED:
+ id: Double Room/Panel_blocked_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme BLOCKED
+ OBSTRUCTED:
+ id: Double Room/Panel_blocked_syn
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme BLOCKED
+ SKIES:
+ id: Double Room/Panel_rise_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme RISE
+ SWELL:
+ id: Double Room/Panel_rise_syn
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme RISE
+ PENNED:
+ id: Double Room/Panel_ascend_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme ASCEND
+ CLIMB:
+ id: Double Room/Panel_ascend_syn
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme ASCEND
+ TROUBLE:
+ id: Double Room/Panel_double_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme DOUBLE
+ DUPLICATE:
+ id: Double Room/Panel_double_syn
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme DOUBLE
+ doors:
+ Door to Circle:
+ id:
+ - Double Room Area Doors/Door_room_2a
+ - Double Room Area Doors/Door_room_1c
+ location_name: Rhyme Room - Circle/Looped Square Wall
+ group: Rhyme Room Doors
+ panels:
+ - WALKED
+ - OBSTRUCTED
+ - SKIES
+ - SWELL
+ - room: Rhyme Room (Circle)
+ panel: BIRD
+ - room: Rhyme Room (Circle)
+ panel: LETTER
+ - room: Rhyme Room (Circle)
+ panel: FORBIDDEN
+ - room: Rhyme Room (Circle)
+ panel: CONCEALED
+ Door to Cross:
+ id:
+ - Double Room Area Doors/Door_room_1a
+ - Double Room Area Doors/Door_room_5a
+ location_name: Rhyme Room - Cross/Looped Square Wall
+ group: Rhyme Room Doors
+ panels:
+ - SKIES
+ - SWELL
+ - PENNED
+ - CLIMB
+ - room: Rhyme Room (Cross)
+ panel: FERN
+ - room: Rhyme Room (Cross)
+ panel: STAY
+ - room: Rhyme Room (Cross)
+ panel: FRIEND
+ - room: Rhyme Room (Cross)
+ panel: RISE
+ Door to Target:
+ id:
+ - Double Room Area Doors/Door_room_1b
+ - Double Room Area Doors/Door_room_4b
+ location_name: Rhyme Room - Target/Looped Square Wall
+ group: Rhyme Room Doors
+ panels:
+ - PENNED
+ - CLIMB
+ - TROUBLE
+ - DUPLICATE
+ - room: Rhyme Room (Target)
+ panel: WILD
+ - room: Rhyme Room (Target)
+ panel: KID
+ - room: Rhyme Room (Target)
+ panel: PISTOL
+ - room: Rhyme Room (Target)
+ panel: QUARTZ
+ Rhyme Room (Target):
+ entrances:
+ Rhyme Room (Smiley): # one-way
+ room: Rhyme Room (Smiley)
+ door: Door to Target
+ Rhyme Room (Looped Square):
+ room: Rhyme Room (Looped Square)
+ door: Door to Target
+ panels:
+ WILD:
+ id: Double Room/Panel_child_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme CHILD
+ KID:
+ id: Double Room/Panel_child_syn
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme CHILD
+ PISTOL:
+ id: Double Room/Panel_crystal_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme CRYSTAL
+ QUARTZ:
+ id: Double Room/Panel_crystal_syn
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme CRYSTAL
+ INNOVATIVE (Top):
+ id: Double Room/Panel_creative_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme CREATIVE
+ INNOVATIVE (Bottom):
+ id: Double Room/Panel_creative_syn
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme CREATIVE
+ doors:
+ Door to Cross:
+ id: Double Room Area Doors/Door_room_4a
+ location_name: Rhyme Room (Target) - Puzzles Toward Cross
+ group: Rhyme Room Doors
+ panels:
+ - PISTOL
+ - QUARTZ
+ - INNOVATIVE (Top)
+ - INNOVATIVE (Bottom)
+ paintings:
+ - id: arrows_painting_4
+ orientation: north
+ Room Room:
+ # This is a bit of a weird room. You can't really get to it from the roof.
+ # And even if you were to go through the shortcut on the fifth floor into
+ # the basement and up the stairs, you'd be blocked by the backsides of the
+ # ROOM panels, which isn't ideal. So we will, at least for now, say that
+ # this room is vanilla.
+ #
+ # For pretty much the same reason, I don't want to shuffle the paintings in
+ # here.
+ entrances:
+ Orange Tower Fourth Floor: True
+ panels:
+ DOOR (1):
+ id: Panel Room/Panel_room_door_1
+ colors: gray
+ tag: forbid
+ DOOR (2):
+ id: Panel Room/Panel_room_door_2
+ colors: gray
+ tag: forbid
+ WINDOW:
+ id: Panel Room/Panel_room_window_1
+ colors: gray
+ tag: forbid
+ STAIRS:
+ id: Panel Room/Panel_room_stairs_1
+ colors: gray
+ tag: forbid
+ PAINTING:
+ id: Panel Room/Panel_room_painting_1
+ colors: gray
+ tag: forbid
+ FLOOR (1):
+ id: Panel Room/Panel_room_floor_1
+ colors: gray
+ tag: forbid
+ FLOOR (2):
+ id: Panel Room/Panel_room_floor_2
+ colors: gray
+ tag: forbid
+ FLOOR (3):
+ id: Panel Room/Panel_room_floor_3
+ colors: gray
+ tag: forbid
+ FLOOR (4):
+ id: Panel Room/Panel_room_floor_4
+ colors: gray
+ tag: forbid
+ FLOOR (5):
+ id: Panel Room/Panel_room_floor_5
+ colors: gray
+ tag: forbid
+ FLOOR (7):
+ id: Panel Room/Panel_room_floor_7
+ colors: gray
+ tag: forbid
+ FLOOR (8):
+ id: Panel Room/Panel_room_floor_8
+ colors: gray
+ tag: forbid
+ FLOOR (9):
+ id: Panel Room/Panel_room_floor_9
+ colors: gray
+ tag: forbid
+ FLOOR (10):
+ id: Panel Room/Panel_room_floor_10
+ colors: gray
+ tag: forbid
+ CEILING (1):
+ id: Panel Room/Panel_room_ceiling_1
+ colors: gray
+ tag: forbid
+ CEILING (2):
+ id: Panel Room/Panel_room_ceiling_2
+ colors: gray
+ tag: forbid
+ CEILING (3):
+ id: Panel Room/Panel_room_ceiling_3
+ colors: gray
+ tag: forbid
+ CEILING (4):
+ id: Panel Room/Panel_room_ceiling_4
+ colors: gray
+ tag: forbid
+ CEILING (5):
+ id: Panel Room/Panel_room_ceiling_5
+ colors: gray
+ tag: forbid
+ WALL (1):
+ id: Panel Room/Panel_room_wall_1
+ colors: gray
+ tag: forbid
+ WALL (2):
+ id: Panel Room/Panel_room_wall_2
+ colors: gray
+ tag: forbid
+ WALL (3):
+ id: Panel Room/Panel_room_wall_3
+ colors: gray
+ tag: forbid
+ WALL (4):
+ id: Panel Room/Panel_room_wall_4
+ colors: gray
+ tag: forbid
+ WALL (5):
+ id: Panel Room/Panel_room_wall_5
+ colors: gray
+ tag: forbid
+ WALL (6):
+ id: Panel Room/Panel_room_wall_6
+ colors: gray
+ tag: forbid
+ WALL (7):
+ id: Panel Room/Panel_room_wall_7
+ colors: gray
+ tag: forbid
+ WALL (8):
+ id: Panel Room/Panel_room_wall_8
+ colors: gray
+ tag: forbid
+ WALL (9):
+ id: Panel Room/Panel_room_wall_9
+ colors: gray
+ tag: forbid
+ WALL (10):
+ id: Panel Room/Panel_room_wall_10
+ colors: gray
+ tag: forbid
+ WALL (11):
+ id: Panel Room/Panel_room_wall_11
+ colors: gray
+ tag: forbid
+ WALL (12):
+ id: Panel Room/Panel_room_wall_12
+ colors: gray
+ tag: forbid
+ WALL (13):
+ id: Panel Room/Panel_room_wall_13
+ colors: gray
+ tag: forbid
+ WALL (14):
+ id: Panel Room/Panel_room_wall_14
+ colors: gray
+ tag: forbid
+ WALL (15):
+ id: Panel Room/Panel_room_wall_15
+ colors: gray
+ tag: forbid
+ WALL (16):
+ id: Panel Room/Panel_room_wall_16
+ colors: gray
+ tag: forbid
+ WALL (17):
+ id: Panel Room/Panel_room_wall_17
+ colors: gray
+ tag: forbid
+ WALL (18):
+ id: Panel Room/Panel_room_wall_18
+ colors: gray
+ tag: forbid
+ WALL (19):
+ id: Panel Room/Panel_room_wall_19
+ colors: gray
+ tag: forbid
+ WALL (20):
+ id: Panel Room/Panel_room_wall_20
+ colors: gray
+ tag: forbid
+ WALL (21):
+ id: Panel Room/Panel_room_wall_21
+ colors: gray
+ tag: forbid
+ BROOMED:
+ id: Panel Room/Panel_broomed_bedroom
+ colors: yellow
+ tag: midyellow
+ required_door:
+ door: Excavation
+ LAYS:
+ id: Panel Room/Panel_lays_maze
+ colors: purple
+ tag: toppurp
+ required_panel:
+ panel: BROOMED
+ BASE:
+ id: Panel Room/Panel_base_basement
+ colors: blue
+ tag: midblue
+ required_panel:
+ panel: LAYS
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery
+ tag: midwhite
+ colors: gray
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ doors:
+ Excavation:
+ event: True
+ panels:
+ - WALL (1)
+ Shortcut to Fifth Floor:
+ id:
+ - Tower Room Area Doors/Door_panel_basement
+ - Tower Room Area Doors/Door_panel_basement2
+ panels:
+ - BASE
+ Cellar:
+ entrances:
+ Room Room:
+ room: Room Room
+ door: Excavation
+ Orange Tower Fifth Floor:
+ room: Room Room
+ door: Shortcut to Fifth Floor
+ Outside The Wise:
+ entrances:
+ Orange Tower Sixth Floor:
+ painting: True
+ Outside The Initiated:
+ painting: True
+ panels:
+ KITTEN:
+ id: Clock Room/Panel_kitten_cat
+ colors: brown
+ tag: botbrown
+ CAT:
+ id: Clock Room/Panel_cat_kitten
+ tag: bot brown black
+ colors:
+ - brown
+ - black
+ doors:
+ Wise Entrance:
+ id: Clock Room Area Doors/Door_time_start
+ item_name: The Wise - Entrance
+ panels:
+ - KITTEN
+ - CAT
+ paintings:
+ - id: arrows_painting_2
+ orientation: east
+ - id: clock_painting_2
+ orientation: east
+ exit_only: True
+ required: True
+ The Wise:
+ entrances:
+ Outside The Wise:
+ room: Outside The Wise
+ door: Wise Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_intelligent_wise
+ colors:
+ - brown
+ - black
+ tag: forbid
+ check: True
+ achievement: The Wise
+ PUPPY:
+ id: Clock Room/Panel_puppy_dog
+ colors: brown
+ tag: botbrown
+ ADULT:
+ id: Clock Room/Panel_adult_child
+ colors:
+ - brown
+ - black
+ tag: bot brown black
+ BREAD:
+ id: Clock Room/Panel_bread_mold
+ colors: brown
+ tag: botbrown
+ DINOSAUR:
+ id: Clock Room/Panel_dinosaur_fossil
+ colors: brown
+ tag: botbrown
+ OAK:
+ id: Clock Room/Panel_oak_acorn
+ colors:
+ - brown
+ - black
+ tag: bot brown black
+ CORPSE:
+ id: Clock Room/Panel_corpse_skeleton
+ colors: brown
+ tag: botbrown
+ BEFORE:
+ id: Clock Room/Panel_before_ere
+ colors:
+ - brown
+ - black
+ tag: mid brown black
+ YOUR:
+ id: Clock Room/Panel_your_thy
+ colors:
+ - brown
+ - black
+ tag: mid brown black
+ BETWIXT:
+ id: Clock Room/Panel_betwixt_between
+ colors: brown
+ tag: midbrown
+ NIGH:
+ id: Clock Room/Panel_nigh_near
+ colors: brown
+ tag: midbrown
+ CONNEXION:
+ id: Clock Room/Panel_connexion_connection
+ colors: brown
+ tag: midbrown
+ THOU:
+ id: Clock Room/Panel_thou_you
+ colors: brown
+ tag: midbrown
+ paintings:
+ - id: clock_painting_3
+ orientation: east
+ The Red:
+ entrances:
+ Roof: True
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_grandfathered_red
+ colors: red
+ tag: forbid
+ check: True
+ achievement: The Red
+ PANDEMIC (1):
+ id: Hangry Room/Panel_red_top_1
+ colors: red
+ tag: topred
+ TRINITY:
+ id: Hangry Room/Panel_red_top_2
+ colors: red
+ tag: topred
+ CHEMISTRY:
+ id: Hangry Room/Panel_red_top_3
+ colors: red
+ tag: topred
+ FLUMMOXED:
+ id: Hangry Room/Panel_red_top_4
+ colors: red
+ tag: topred
+ PANDEMIC (2):
+ id: Hangry Room/Panel_red_mid_1
+ colors: red
+ tag: midred
+ COUNTERCLOCKWISE:
+ id: Hangry Room/Panel_red_mid_2
+ colors: red
+ tag: red top red mid black bot
+ FEARLESS:
+ id: Hangry Room/Panel_red_mid_3
+ colors: red
+ tag: midred
+ DEFORESTATION:
+ id: Hangry Room/Panel_red_mid_4
+ colors: red
+ tag: red mid bot
+ subtag: mid
+ link: rmb FORE
+ CRAFTSMANSHIP:
+ id: Hangry Room/Panel_red_mid_5
+ colors: red
+ tag: red mid bot
+ subtag: mid
+ link: rmb AFT
+ CAMEL:
+ id: Hangry Room/Panel_red_bot_1
+ colors: red
+ tag: botred
+ LION:
+ id: Hangry Room/Panel_red_bot_2
+ colors: red
+ tag: botred
+ TIGER:
+ id: Hangry Room/Panel_red_bot_3
+ colors: red
+ tag: botred
+ SHIP (1):
+ id: Hangry Room/Panel_red_bot_4
+ colors: red
+ tag: red mid bot
+ subtag: bot
+ link: rmb FORE
+ SHIP (2):
+ id: Hangry Room/Panel_red_bot_5
+ colors: red
+ tag: red mid bot
+ subtag: bot
+ link: rmb AFT
+ GIRAFFE:
+ id: Hangry Room/Panel_red_bot_6
+ colors: red
+ tag: botred
+ The Ecstatic:
+ entrances:
+ Roof: True
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_ecstatic_ecstatic
+ colors: yellow
+ tag: forbid
+ check: True
+ achievement: The Ecstatic
+ FORM (1):
+ id: Smiley Room/Panel_soundgram_1
+ colors: yellow
+ tag: yellow top bot
+ subtag: bottom
+ link: ytb FORM
+ WIND:
+ id: Smiley Room/Panel_soundgram_2
+ colors: yellow
+ tag: botyellow
+ EGGS:
+ id: Smiley Room/Panel_scrambled_1
+ colors: yellow
+ tag: botyellow
+ VEGETABLES:
+ id: Smiley Room/Panel_scrambled_2
+ colors: yellow
+ tag: botyellow
+ WATER:
+ id: Smiley Room/Panel_anagram_6_1
+ colors: yellow
+ tag: botyellow
+ FRUITS:
+ id: Smiley Room/Panel_anagram_6_2
+ colors: yellow
+ tag: botyellow
+ LEAVES:
+ id: Smiley Room/Panel_anagram_7_1
+ colors: yellow
+ tag: topyellow
+ VINES:
+ id: Smiley Room/Panel_anagram_7_2
+ colors: yellow
+ tag: topyellow
+ ICE:
+ id: Smiley Room/Panel_anagram_7_3
+ colors: yellow
+ tag: topyellow
+ STYLE:
+ id: Smiley Room/Panel_anagram_7_4
+ colors: yellow
+ tag: topyellow
+ FIR:
+ id: Smiley Room/Panel_anagram_8_1
+ colors: yellow
+ tag: topyellow
+ REEF:
+ id: Smiley Room/Panel_anagram_8_2
+ colors: yellow
+ tag: topyellow
+ ROTS:
+ id: Smiley Room/Panel_anagram_8_3
+ colors: yellow
+ tag: topyellow
+ FORM (2):
+ id: Smiley Room/Panel_anagram_9_1
+ colors: yellow
+ tag: yellow top bot
+ subtag: top
+ link: ytb FORM
+ Outside The Scientific:
+ entrances:
+ Roof: True
+ The Scientific:
+ door: Scientific Entrance
+ panels:
+ OPEN:
+ id: Chemistry Room/Panel_open
+ tag: midwhite
+ CLOSE:
+ id: Chemistry Room/Panel_close
+ colors: black
+ tag: botblack
+ AHEAD:
+ id: Chemistry Room/Panel_ahead
+ colors: black
+ tag: botblack
+ doors:
+ Scientific Entrance:
+ id: Red Blue Purple Room Area Doors/Door_chemistry_lab
+ item_name: The Scientific - Entrance
+ panels:
+ - OPEN
+ The Scientific:
+ entrances:
+ Outside The Scientific:
+ room: Outside The Scientific
+ door: Scientific Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_scientific_scientific
+ colors:
+ - yellow
+ - red
+ - blue
+ - brown
+ - black
+ - purple
+ tag: forbid
+ check: True
+ achievement: The Scientific
+ HYDROGEN (1):
+ id: Chemistry Room/Panel_blue_bot_3
+ colors: blue
+ tag: tri botblue
+ link: tbb WATER
+ OXYGEN:
+ id: Chemistry Room/Panel_blue_bot_2
+ colors: blue
+ tag: tri botblue
+ link: tbb WATER
+ HYDROGEN (2):
+ id: Chemistry Room/Panel_blue_bot_4
+ colors: blue
+ tag: tri botblue
+ link: tbb WATER
+ SUGAR (1):
+ id: Chemistry Room/Panel_sugar_1
+ colors: red
+ tag: botred
+ SUGAR (2):
+ id: Chemistry Room/Panel_sugar_2
+ colors: red
+ tag: botred
+ SUGAR (3):
+ id: Chemistry Room/Panel_sugar_3
+ colors: red
+ tag: botred
+ CHLORINE:
+ id: Chemistry Room/Panel_blue_bot_5
+ colors: blue
+ tag: double botblue
+ subtag: left
+ link: holo SALT
+ SODIUM:
+ id: Chemistry Room/Panel_blue_bot_6
+ colors: blue
+ tag: double botblue
+ subtag: right
+ link: holo SALT
+ FOREST:
+ id: Chemistry Room/Panel_long_bot_1
+ colors:
+ - red
+ - blue
+ tag: chain red bot blue top
+ POUND:
+ id: Chemistry Room/Panel_long_top_1
+ colors:
+ - red
+ - blue
+ tag: chain blue mid red bot
+ ICE:
+ id: Chemistry Room/Panel_brown_bot_1
+ colors: brown
+ tag: botbrown
+ FISSION:
+ id: Chemistry Room/Panel_black_bot_1
+ colors: black
+ tag: botblack
+ FUSION:
+ id: Chemistry Room/Panel_black_bot_2
+ colors: black
+ tag: botblack
+ MISS:
+ id: Chemistry Room/Panel_blue_top_1
+ colors: blue
+ tag: double topblue
+ subtag: left
+ link: exp CHEMISTRY
+ TREE (1):
+ id: Chemistry Room/Panel_blue_top_2
+ colors: blue
+ tag: double topblue
+ subtag: right
+ link: exp CHEMISTRY
+ BIOGRAPHY:
+ id: Chemistry Room/Panel_biology_9
+ colors: purple
+ tag: midpurp
+ CACTUS:
+ id: Chemistry Room/Panel_biology_4
+ colors: red
+ tag: double botred
+ subtag: right
+ link: mero SPINE
+ VERTEBRATE:
+ id: Chemistry Room/Panel_biology_8
+ colors: red
+ tag: double botred
+ subtag: left
+ link: mero SPINE
+ ROSE:
+ id: Chemistry Room/Panel_biology_2
+ colors: red
+ tag: botred
+ TREE (2):
+ id: Chemistry Room/Panel_biology_3
+ colors: red
+ tag: botred
+ FRUIT:
+ id: Chemistry Room/Panel_biology_1
+ colors: red
+ tag: botred
+ MAMMAL:
+ id: Chemistry Room/Panel_biology_5
+ colors: red
+ tag: botred
+ BIRD:
+ id: Chemistry Room/Panel_biology_6
+ colors: red
+ tag: botred
+ FISH:
+ id: Chemistry Room/Panel_biology_7
+ colors: red
+ tag: botred
+ GRAVELY:
+ id: Chemistry Room/Panel_physics_9
+ colors: purple
+ tag: double midpurp
+ subtag: left
+ link: change GRAVITY
+ BREVITY:
+ id: Chemistry Room/Panel_biology_10
+ colors: purple
+ tag: double midpurp
+ subtag: right
+ link: change GRAVITY
+ PART:
+ id: Chemistry Room/Panel_physics_2
+ colors: blue
+ tag: blue mid red bot
+ subtag: mid
+ link: xur PARTICLE
+ MATTER:
+ id: Chemistry Room/Panel_physics_1
+ colors: red
+ tag: blue mid red bot
+ subtag: bot
+ link: xur PARTICLE
+ ELECTRIC:
+ id: Chemistry Room/Panel_physics_6
+ colors: purple
+ tag: purple mid red bot
+ subtag: mid
+ link: xpr ELECTRON
+ ATOM (1):
+ id: Chemistry Room/Panel_physics_3
+ colors: red
+ tag: purple mid red bot
+ subtag: bot
+ link: xpr ELECTRON
+ NEUTRAL:
+ id: Chemistry Room/Panel_physics_7
+ colors: purple
+ tag: purple mid red bot
+ subtag: mid
+ link: xpr NEUTRON
+ ATOM (2):
+ id: Chemistry Room/Panel_physics_4
+ colors: red
+ tag: purple mid red bot
+ subtag: bot
+ link: xpr NEUTRON
+ PROPEL:
+ id: Chemistry Room/Panel_physics_8
+ colors: purple
+ tag: purple mid red bot
+ subtag: mid
+ link: xpr PROTON
+ ATOM (3):
+ id: Chemistry Room/Panel_physics_5
+ colors: red
+ tag: purple mid red bot
+ subtag: bot
+ link: xpr PROTON
+ ORDER:
+ id: Chemistry Room/Panel_physics_11
+ colors: brown
+ tag: botbrown
+ OPTICS:
+ id: Chemistry Room/Panel_physics_10
+ colors: yellow
+ tag: midyellow
+ GRAPHITE:
+ id: Chemistry Room/Panel_yellow_bot_1
+ colors: yellow
+ tag: botyellow
+ HOT RYE:
+ id: Chemistry Room/Panel_anagram_1
+ colors: yellow
+ tag: midyellow
+ SIT SHY HOPE:
+ id: Chemistry Room/Panel_anagram_2
+ colors: yellow
+ tag: midyellow
+ ME NEXT PIER:
+ id: Chemistry Room/Panel_anagram_3
+ colors: yellow
+ tag: midyellow
+ RUT LESS:
+ id: Chemistry Room/Panel_anagram_4
+ colors: yellow
+ tag: midyellow
+ SON COUNCIL:
+ id: Chemistry Room/Panel_anagram_5
+ colors: yellow
+ tag: midyellow
+ doors:
+ Chemistry Puzzles:
+ skip_item: True
+ location_name: The Scientific - Chemistry Puzzles
+ panels:
+ - HYDROGEN (1)
+ - OXYGEN
+ - HYDROGEN (2)
+ - SUGAR (1)
+ - SUGAR (2)
+ - SUGAR (3)
+ - CHLORINE
+ - SODIUM
+ - FOREST
+ - POUND
+ - ICE
+ - FISSION
+ - FUSION
+ - MISS
+ - TREE (1)
+ Biology Puzzles:
+ skip_item: True
+ location_name: The Scientific - Biology Puzzles
+ panels:
+ - BIOGRAPHY
+ - CACTUS
+ - VERTEBRATE
+ - ROSE
+ - TREE (2)
+ - FRUIT
+ - MAMMAL
+ - BIRD
+ - FISH
+ Physics Puzzles:
+ skip_item: True
+ location_name: The Scientific - Physics Puzzles
+ panels:
+ - GRAVELY
+ - BREVITY
+ - PART
+ - MATTER
+ - ELECTRIC
+ - ATOM (1)
+ - NEUTRAL
+ - ATOM (2)
+ - PROPEL
+ - ATOM (3)
+ - ORDER
+ - OPTICS
+ paintings:
+ - id: hi_solved_painting4
+ orientation: south
+ Challenge Room:
+ entrances:
+ Welcome Back Area:
+ door: Welcome Door
+ Number Hunt:
+ room: Outside The Undeterred
+ door: Challenge Entrance
+ panels:
+ WELCOME:
+ id: Challenge Room/Panel_welcome_welcome
+ tag: midwhite
+ CHALLENGE:
+ id: Challenge Room/Panel_challenge_challenge
+ tag: midwhite
+ Achievement:
+ id: Countdown Panels/Panel_challenged_unchallenged
+ check: True
+ colors:
+ - black
+ - gray
+ - red
+ - blue
+ - yellow
+ - purple
+ - brown
+ - orange
+ tag: forbid
+ achievement: The Unchallenged
+ OPEN:
+ id: Challenge Room/Panel_open_nepotism
+ colors:
+ - black
+ - blue
+ tag: chain mid black !!! blue
+ SINGED:
+ id: Challenge Room/Panel_singed_singsong
+ colors:
+ - red
+ - blue
+ tag: chain mid red blue
+ NEVER TRUSTED:
+ id: Challenge Room/Panel_nevertrusted_maladjusted
+ colors: purple
+ tag: midpurp
+ CORNER:
+ id: Challenge Room/Panel_corner_corn
+ colors: red
+ tag: midred
+ STRAWBERRIES:
+ id: Challenge Room/Panel_strawberries_mold
+ colors: brown
+ tag: double botbrown
+ subtag: left
+ link: time MOLD
+ GRUB:
+ id: Challenge Room/Panel_grub_burger
+ colors:
+ - black
+ - blue
+ tag: chain mid black blue
+ BREAD:
+ id: Challenge Room/Panel_bread_mold
+ colors: brown
+ tag: double botbrown
+ subtag: right
+ link: time MOLD
+ COLOR:
+ id: Challenge Room/Panel_color_gray
+ colors: gray
+ tag: forbid
+ WRITER:
+ id: Challenge Room/Panel_writer_songwriter
+ colors: blue
+ tag: midblue
+ "02759":
+ id: Challenge Room/Panel_tales_stale
+ colors:
+ - orange
+ - yellow
+ tag: chain mid orange yellow
+ REAL EYES:
+ id: Challenge Room/Panel_realeyes_realize
+ tag: topwhite
+ LOBS:
+ id: Challenge Room/Panel_lobs_lobster
+ colors: blue
+ tag: midblue
+ PEST ALLY:
+ id: Challenge Room/Panel_double_anagram_1
+ colors: yellow
+ tag: midyellow
+ GENIAL HALO:
+ id: Challenge Room/Panel_double_anagram_2
+ colors: yellow
+ tag: midyellow
+ DUCK LOGO:
+ id: Challenge Room/Panel_double_anagram_3
+ colors: yellow
+ tag: midyellow
+ AVIAN GREEN:
+ id: Challenge Room/Panel_double_anagram_4
+ colors: yellow
+ tag: midyellow
+ FEVER TEAR:
+ id: Challenge Room/Panel_double_anagram_5
+ colors: yellow
+ tag: midyellow
+ FACTS:
+ id: Challenge Room/Panel_facts
+ colors:
+ - red
+ - blue
+ tag: forbid
+ FACTS (1):
+ id: Challenge Room/Panel_facts2
+ colors: red
+ tag: forbid
+ FACTS (3):
+ id: Challenge Room/Panel_facts3
+ tag: forbid
+ FACTS (4):
+ id: Challenge Room/Panel_facts4
+ colors: blue
+ tag: forbid
+ FACTS (5):
+ id: Challenge Room/Panel_facts5
+ colors: blue
+ tag: forbid
+ FACTS (6):
+ id: Challenge Room/Panel_facts6
+ colors: blue
+ tag: forbid
+ LAPEL SHEEP:
+ id: Challenge Room/Panel_double_anagram_6
+ colors: yellow
+ tag: midyellow
+ doors:
+ Welcome Door:
+ id: Entry Room Area Doors/Door_challenge_challenge
+ panels:
+ - WELCOME
diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py
new file mode 100644
index 000000000000..1f426c92f24a
--- /dev/null
+++ b/worlds/lingo/__init__.py
@@ -0,0 +1,112 @@
+"""
+Archipelago init file for Lingo
+"""
+from BaseClasses import Item, Tutorial
+from worlds.AutoWorld import WebWorld, World
+from .items import ALL_ITEM_TABLE, LingoItem
+from .locations import ALL_LOCATION_TABLE
+from .options import LingoOptions
+from .player_logic import LingoPlayerLogic
+from .regions import create_regions
+from .static_logic import Room, RoomEntrance
+from .testing import LingoTestOptions
+
+
+class LingoWebWorld(WebWorld):
+ theme = "grass"
+ tutorials = [Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to playing Lingo with Archipelago.",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["hatkirby"]
+ )]
+
+
+class LingoWorld(World):
+ """
+ Lingo is a first person indie puzzle game in the vein of The Witness. You find yourself in a mazelike, non-Euclidean
+ world filled with 800 word puzzles that use a variety of different mechanics.
+ """
+ game = "Lingo"
+ web = LingoWebWorld()
+
+ base_id = 444400
+ topology_present = True
+ data_version = 1
+
+ options_dataclass = LingoOptions
+ options: LingoOptions
+
+ item_name_to_id = {
+ name: data.code for name, data in ALL_ITEM_TABLE.items()
+ }
+ location_name_to_id = {
+ name: data.code for name, data in ALL_LOCATION_TABLE.items()
+ }
+
+ player_logic: LingoPlayerLogic
+
+ def generate_early(self):
+ self.player_logic = LingoPlayerLogic(self)
+
+ def create_regions(self):
+ create_regions(self, self.player_logic)
+
+ def create_items(self):
+ pool = [self.create_item(name) for name in self.player_logic.REAL_ITEMS]
+
+ if self.player_logic.FORCED_GOOD_ITEM != "":
+ new_item = self.create_item(self.player_logic.FORCED_GOOD_ITEM)
+ location_obj = self.multiworld.get_location("Second Room - Good Luck", self.player)
+ location_obj.place_locked_item(new_item)
+
+ item_difference = len(self.player_logic.REAL_LOCATIONS) - len(pool)
+ if item_difference:
+ trap_percentage = self.options.trap_percentage
+ traps = int(item_difference * trap_percentage / 100.0)
+ non_traps = item_difference - traps
+
+ if non_traps:
+ skip_percentage = self.options.puzzle_skip_percentage
+ skips = int(non_traps * skip_percentage / 100.0)
+ non_skips = non_traps - skips
+
+ filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"]
+ for i in range(0, non_skips):
+ pool.append(self.create_item(filler_list[i % len(filler_list)]))
+
+ for i in range(0, skips):
+ pool.append(self.create_item("Puzzle Skip"))
+
+ if traps:
+ traps_list = ["Slowness Trap", "Iceland Trap", "Atbash Trap"]
+
+ for i in range(0, traps):
+ pool.append(self.create_item(traps_list[i % len(traps_list)]))
+
+ self.multiworld.itempool += pool
+
+ def create_item(self, name: str) -> Item:
+ item = ALL_ITEM_TABLE[name]
+ return LingoItem(name, item.classification, item.code, self.player)
+
+ def set_rules(self):
+ self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
+
+ def fill_slot_data(self):
+ slot_options = [
+ "death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels",
+ "mastery_achievements", "level_2_requirement", "location_checks", "early_color_hallways"
+ ]
+
+ slot_data = {
+ "seed": self.random.randint(0, 1000000),
+ **self.options.as_dict(*slot_options),
+ }
+
+ if self.options.shuffle_paintings:
+ slot_data["painting_entrance_to_exit"] = self.player_logic.PAINTING_MAPPING
+
+ return slot_data
diff --git a/worlds/lingo/docs/en_Lingo.md b/worlds/lingo/docs/en_Lingo.md
new file mode 100644
index 000000000000..cff0581d9b2f
--- /dev/null
+++ b/worlds/lingo/docs/en_Lingo.md
@@ -0,0 +1,42 @@
+# Lingo
+
+## Where is the settings page?
+
+The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+config file.
+
+## What does randomization do to this game?
+
+There are a couple of modes of randomization currently available, and you can pick and choose which ones you would like
+to use.
+
+* **Door shuffle**: There are many doors in the game, which are opened by completing a set of panels. With door shuffle
+ on, the doors become items and only open up once you receive the corresponding item. The panel sets that would
+ ordinarily open the doors become locations.
+
+* **Color shuffle**: There are ten different colors of puzzle in the game, each representing a different mechanic. With
+ color shuffle on, you would start with only access to white puzzles. Puzzles of other colors will require you to
+ receive an item in order to solve them (e.g. you can't solve any red puzzles until you receive the "Red" item).
+
+* **Panel shuffle**: Panel shuffling replaces the puzzles on each panel with different ones. So far, the only mode of
+ panel shuffling is "rearrange" mode, which simply shuffles the already-existing puzzles from the base game onto
+ different panels.
+
+* **Painting shuffle**: This randomizes the appearance of the paintings in the game, as well as which of them are warps,
+ and the locations that they warp you to. It is the equivalent of an entrance randomizer in another game.
+
+## What is a "check" in this game?
+
+Most panels / panel sets that open a door are now location checks, even if door shuffle is not enabled. Various other
+puzzles are also location checks, including the achievement panels for each area.
+
+## What about wall snipes?
+
+"Wall sniping" refers to the fact that you are able to solve puzzles on the other side of opaque walls. This randomizer
+does not change how wall snipes work, but it will never require the use of them. There are three puzzles from the base
+game that you would ordinarily be expected to wall snipe. The randomizer moves these panels out of the wall or otherwise
+reveals them so that a snipe is not necessary.
+
+Because of this, all wall snipes are considered out of logic. This includes sniping The Bearer's MIDDLE while standing
+outside The Bold, sniping The Colorful without opening all of the color doors, and sniping WELCOME from next to WELCOME
+BACK.
diff --git a/worlds/lingo/docs/setup_en.md b/worlds/lingo/docs/setup_en.md
new file mode 100644
index 000000000000..97f3ce594063
--- /dev/null
+++ b/worlds/lingo/docs/setup_en.md
@@ -0,0 +1,45 @@
+# Lingo Randomizer Setup
+
+## Required Software
+
+- [Lingo](https://store.steampowered.com/app/1814170/Lingo/)
+- [Lingo Archipelago Randomizer](https://code.fourisland.com/lingo-archipelago/about/CHANGELOG.md)
+
+## Optional Software
+
+- [Archipelago Text Client](https://github.com/ArchipelagoMW/Archipelago/releases)
+- [Lingo AP Tracker](https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md)
+
+## Installation
+
+1. Download the Lingo Archipelago Randomizer from the above link.
+2. Open up Lingo, go to settings, and click View Game Data. This should open up
+ a folder in Windows Explorer.
+3. Unzip the contents of the randomizer into the "maps" folder. You may need to
+ create the "maps" folder if you have not played a custom Lingo map before.
+4. Installation complete! You may have to click Return to go back to the main
+ menu and then click Settings again in order to get the randomizer to show up
+ in the level selection list.
+
+## Joining a Multiworld game
+
+1. Launch Lingo
+2. Click on Settings, and then Level. Choose Archipelago from the list.
+3. Start a new game. Leave the name field blank (anything you type in will be
+ ignored).
+4. Enter the Archipelago address, slot name, and password into the fields.
+5. Press Connect.
+6. Enjoy!
+
+To continue an earlier game, you can perform the exact same steps as above. You
+do not have to re-select Archipelago in the level selection screen if you were
+using Archipelago the last time you launched the game.
+
+In order to play the base game again, simply return to the level selection
+screen and choose Level 1 (or whatever else you want to play). The randomizer
+will not affect gameplay unless you launch it by starting a new game while it is
+selected in the level selection screen, so it is safe to play the game normally
+while the client is installed.
+
+**Note**: Running the randomizer modifies the game's memory. If you want to play
+the base game after playing the randomizer, you need to restart Lingo first.
diff --git a/worlds/lingo/ids.yaml b/worlds/lingo/ids.yaml
new file mode 100644
index 000000000000..f48858a285f0
--- /dev/null
+++ b/worlds/lingo/ids.yaml
@@ -0,0 +1,1449 @@
+---
+special_items:
+ Black: 444400
+ Red: 444401
+ Blue: 444402
+ Yellow: 444403
+ Green: 444404
+ Orange: 444405
+ Gray: 444406
+ Brown: 444407
+ Purple: 444408
+ ":)": 444409
+ The Feeling of Being Lost: 444575
+ Wanderlust: 444576
+ Empty White Hallways: 444577
+ Slowness Trap: 444410
+ Iceland Trap: 444411
+ Atbash Trap: 444412
+ Puzzle Skip: 444413
+panels:
+ Starting Room:
+ HI: 444400
+ HIDDEN: 444401
+ TYPE: 444402
+ THIS: 444403
+ WRITE: 444404
+ SAME: 444405
+ Hidden Room:
+ DEAD END: 444406
+ OPEN: 444407
+ LIES: 444408
+ The Seeker:
+ Achievement: 444409
+ BEAR: 444410
+ MINE: 444411
+ MINE (2): 444412
+ BOW: 444413
+ DOES: 444414
+ MOBILE: 444415
+ MOBILE (2): 444416
+ DESERT: 444417
+ DESSERT: 444418
+ SOW: 444419
+ SEW: 444420
+ TO: 444421
+ TOO: 444422
+ WRITE: 444423
+ EWE: 444424
+ KNOT: 444425
+ NAUGHT: 444426
+ BEAR (2): 444427
+ Second Room:
+ HI: 444428
+ LOW: 444429
+ ANOTHER TRY: 444430
+ LEVEL 2: 444431
+ Hub Room:
+ ORDER: 444432
+ SLAUGHTER: 444433
+ NEAR: 444434
+ FAR: 444435
+ TRACE: 444436
+ RAT: 444437
+ OPEN: 444438
+ FOUR: 444439
+ LOST: 444440
+ FORWARD: 444441
+ BETWEEN: 444442
+ BACKWARD: 444443
+ Dead End Area:
+ FOUR: 444444
+ EIGHT: 444445
+ Pilgrim Antechamber:
+ HOT CRUST: 444446
+ PILGRIMAGE: 444447
+ MASTERY: 444448
+ Pilgrim Room:
+ THIS: 444449
+ TIME ROOM: 444450
+ SCIENCE ROOM: 444451
+ SHINY ROCK ROOM: 444452
+ ANGRY POWER: 444453
+ MICRO LEGION: 444454
+ LOSERS RELAX: 444455
+ '906234': 444456
+ MOOR EMORDNILAP: 444457
+ HALL ROOMMATE: 444458
+ ALL GREY: 444459
+ PLUNDER ISLAND: 444460
+ FLOSS PATHS: 444461
+ Crossroads:
+ DECAY: 444462
+ NOPE: 444463
+ EIGHT: 444464
+ WE ROT: 444465
+ WORDS: 444466
+ SWORD: 444467
+ TURN: 444468
+ BEND HI: 444469
+ THE EYES: 444470
+ CORNER: 444471
+ HOLLOW: 444472
+ SWAP: 444473
+ GEL: 444474
+ THOUGH: 444475
+ CROSSROADS: 444476
+ Lost Area:
+ LOST (1): 444477
+ LOST (2): 444478
+ Amen Name Area:
+ AMEN: 444479
+ NAME: 444480
+ NINE: 444481
+ Suits Area:
+ SPADES: 444482
+ CLUBS: 444483
+ HEARTS: 444484
+ The Tenacious:
+ LEVEL (Black): 444485
+ RACECAR (Black): 444486
+ SOLOS (Black): 444487
+ LEVEL (White): 444488
+ RACECAR (White): 444489
+ SOLOS (White): 444490
+ Achievement: 444491
+ Warts Straw Area:
+ WARTS: 444492
+ STRAW: 444493
+ Leaf Feel Area:
+ LEAF: 444494
+ FEEL: 444495
+ Outside The Agreeable:
+ MASSACRED: 444496
+ BLACK: 444497
+ CLOSE: 444498
+ LEFT: 444499
+ LEFT (2): 444500
+ RIGHT: 444501
+ PURPLE: 444502
+ FIVE (1): 444503
+ FIVE (2): 444504
+ OUT: 444505
+ HIDE: 444506
+ DAZE: 444507
+ WALL: 444508
+ KEEP: 444509
+ BAILEY: 444510
+ TOWER: 444511
+ NORTH: 444512
+ DIAMONDS: 444513
+ FIRE: 444514
+ WINTER: 444515
+ Dread Hallway:
+ DREAD: 444516
+ The Agreeable:
+ Achievement: 444517
+ BYE: 444518
+ RETOOL: 444519
+ DRAWER: 444520
+ READ: 444521
+ DIFFERENT: 444522
+ LOW: 444523
+ ALIVE: 444524
+ THAT: 444525
+ STRESSED: 444526
+ STAR: 444527
+ TAME: 444528
+ CAT: 444529
+ Hedge Maze:
+ DOWN: 444530
+ HIDE (1): 444531
+ HIDE (2): 444532
+ HIDE (3): 444533
+ MASTERY (1): 444534
+ MASTERY (2): 444535
+ PATH (1): 444536
+ PATH (2): 444537
+ PATH (3): 444538
+ PATH (4): 444539
+ PATH (5): 444540
+ PATH (6): 444541
+ PATH (7): 444542
+ PATH (8): 444543
+ REFLOW: 444544
+ LEAP: 444545
+ The Perceptive:
+ Achievement: 444546
+ GAZE: 444547
+ The Fearless (First Floor):
+ NAPS: 444548
+ TEAM: 444549
+ TEEM: 444550
+ IMPATIENT: 444551
+ EAT: 444552
+ The Fearless (Second Floor):
+ NONE: 444553
+ SUM: 444554
+ FUNNY: 444555
+ MIGHT: 444556
+ SAFE: 444557
+ SAME: 444558
+ CAME: 444559
+ The Fearless:
+ Achievement: 444560
+ EASY: 444561
+ SOMETIMES: 444562
+ DARK: 444563
+ EVEN: 444564
+ The Observant:
+ Achievement: 444565
+ BACK: 444566
+ SIDE: 444567
+ BACKSIDE: 444568
+ STAIRS: 444569
+ WAYS: 444570
+ 'ON': 444571
+ UP: 444572
+ SWIMS: 444573
+ UPSTAIRS: 444574
+ TOIL: 444575
+ STOP: 444576
+ TOP: 444577
+ HI: 444578
+ HI (2): 444579
+ '31': 444580
+ '52': 444581
+ OIL: 444582
+ BACKSIDE (GREEN): 444583
+ SIDEWAYS: 444584
+ The Incomparable:
+ Achievement: 444585
+ A (One): 444586
+ A (Two): 444587
+ A (Three): 444588
+ A (Four): 444589
+ A (Five): 444590
+ A (Six): 444591
+ I (One): 444592
+ I (Two): 444593
+ I (Three): 444594
+ I (Four): 444595
+ I (Five): 444596
+ I (Six): 444597
+ I (Seven): 444598
+ Eight Room:
+ Eight Back: 444599
+ Eight Front: 444600
+ Nine: 444601
+ Orange Tower First Floor:
+ SECRET: 444602
+ DADS + ALE: 444603
+ SALT: 444604
+ Orange Tower Third Floor:
+ RED: 444605
+ DEER + WREN: 444606
+ Orange Tower Fourth Floor:
+ RUNT: 444607
+ RUNT (2): 444608
+ LEARNS + UNSEW: 444609
+ HOT CRUSTS: 444610
+ IRK HORN: 444611
+ Hot Crusts Area:
+ EIGHT: 444612
+ Orange Tower Fifth Floor:
+ SIZE (Small): 444613
+ SIZE (Big): 444614
+ DRAWL + RUNS: 444615
+ NINE: 444616
+ SUMMER: 444617
+ AUTUMN: 444618
+ SPRING: 444619
+ PAINTING (1): 445078
+ PAINTING (2): 445079
+ PAINTING (3): 445080
+ PAINTING (4): 445081
+ PAINTING (5): 445082
+ ROOM: 445083
+ Orange Tower Seventh Floor:
+ THE END: 444620
+ THE MASTER: 444621
+ MASTERY: 444622
+ Roof:
+ MASTERY (1): 444623
+ MASTERY (2): 444624
+ MASTERY (3): 444625
+ MASTERY (4): 444626
+ MASTERY (5): 444627
+ MASTERY (6): 444628
+ STAIRCASE: 444629
+ Orange Tower Basement:
+ MASTERY: 444630
+ THE LIBRARY: 444631
+ Courtyard:
+ I: 444632
+ GREEN: 444633
+ PINECONE: 444634
+ ACORN: 444635
+ Yellow Backside Area:
+ BACKSIDE: 444636
+ NINE: 444637
+ First Second Third Fourth:
+ FIRST: 444638
+ SECOND: 444639
+ THIRD: 444640
+ FOURTH: 444641
+ The Colorful (White):
+ BEGIN: 444642
+ The Colorful (Black):
+ FOUND: 444643
+ The Colorful (Red):
+ LOAF: 444644
+ The Colorful (Yellow):
+ CREAM: 444645
+ The Colorful (Blue):
+ SUN: 444646
+ The Colorful (Purple):
+ SPOON: 444647
+ The Colorful (Orange):
+ LETTERS: 444648
+ The Colorful (Green):
+ WALLS: 444649
+ The Colorful (Brown):
+ IRON: 444650
+ The Colorful (Gray):
+ OBSTACLE: 444651
+ The Colorful:
+ Achievement: 444652
+ Welcome Back Area:
+ WELCOME BACK: 444653
+ SECRET: 444654
+ CLOCKWISE: 444655
+ Owl Hallway:
+ STRAYS: 444656
+ READS + RUST: 444657
+ Outside The Initiated:
+ SEVEN (1): 444658
+ SEVEN (2): 444659
+ EIGHT: 444660
+ NINE: 444661
+ BLUE: 444662
+ ORANGE: 444663
+ UNCOVER: 444664
+ OXEN: 444665
+ BACKSIDE: 444666
+ The Optimistic: 444667
+ PAST: 444668
+ FUTURE: 444669
+ FUTURE (2): 444670
+ PAST (2): 444671
+ PRESENT: 444672
+ SMILE: 444673
+ ANGERED: 444674
+ VOTE: 444675
+ The Initiated:
+ Achievement: 444676
+ DAUGHTER: 444677
+ START: 444678
+ STARE: 444679
+ HYPE: 444680
+ ABYSS: 444681
+ SWEAT: 444682
+ BEAT: 444683
+ ALUMNI: 444684
+ PATS: 444685
+ KNIGHT: 444686
+ BYTE: 444687
+ MAIM: 444688
+ MORGUE: 444689
+ CHAIR: 444690
+ HUMAN: 444691
+ BED: 444692
+ The Traveled:
+ Achievement: 444693
+ CLOSE: 444694
+ COMPOSE: 444695
+ RECORD: 444696
+ CATEGORY: 444697
+ HELLO: 444698
+ DUPLICATE: 444699
+ IDENTICAL: 444700
+ DISTANT: 444701
+ HAY: 444702
+ GIGGLE: 444703
+ CHUCKLE: 444704
+ SNITCH: 444705
+ CONCEALED: 444706
+ PLUNGE: 444707
+ AUTUMN: 444708
+ ROAD: 444709
+ FOUR: 444710
+ Outside The Bold:
+ UNOPEN: 444711
+ BEGIN: 444712
+ SIX: 444713
+ NINE: 444714
+ LEFT: 444715
+ RIGHT: 444716
+ RISE (Horizon): 444717
+ RISE (Sunrise): 444718
+ ZEN: 444719
+ SON: 444720
+ STARGAZER: 444721
+ MOUTH: 444722
+ YEAST: 444723
+ WET: 444724
+ The Bold:
+ Achievement: 444725
+ FOOT: 444726
+ NEEDLE: 444727
+ FACE: 444728
+ SIGN: 444729
+ HEARTBREAK: 444730
+ UNDEAD: 444731
+ DEADLINE: 444732
+ SUSHI: 444733
+ THISTLE: 444734
+ LANDMASS: 444735
+ MASSACRED: 444736
+ AIRPLANE: 444737
+ NIGHTMARE: 444738
+ MOUTH: 444739
+ SAW: 444740
+ HAND: 444741
+ Outside The Undeterred:
+ HOLLOW: 444742
+ ART + ART: 444743
+ PEN: 444744
+ HUSTLING: 444745
+ SUNLIGHT: 444746
+ LIGHT: 444747
+ BRIGHT: 444748
+ SUNNY: 444749
+ RAINY: 444750
+ ZERO: 444751
+ ONE: 444752
+ TWO (1): 444753
+ TWO (2): 444754
+ THREE (1): 444755
+ THREE (2): 444756
+ THREE (3): 444757
+ FOUR: 444758
+ The Undeterred:
+ Achievement: 444759
+ BONE: 444760
+ EYE: 444761
+ MOUTH: 444762
+ IRIS: 444763
+ EYE (2): 444764
+ ICE: 444765
+ HEIGHT: 444766
+ EYE (3): 444767
+ NOT: 444768
+ JUST: 444769
+ READ: 444770
+ FATHER: 444771
+ FEATHER: 444772
+ CONTINENT: 444773
+ OCEAN: 444774
+ WALL: 444775
+ Number Hunt:
+ FIVE: 444776
+ SIX: 444777
+ SEVEN: 444778
+ EIGHT: 444779
+ NINE: 444780
+ Directional Gallery:
+ PEPPER: 444781
+ TURN: 444782
+ LEARN: 444783
+ FIVE (1): 444784
+ FIVE (2): 444785
+ SIX (1): 444786
+ SIX (2): 444787
+ SEVEN: 444788
+ EIGHT: 444789
+ NINE: 444790
+ BACKSIDE: 444791
+ '834283054': 444792
+ PARANOID: 444793
+ YELLOW: 444794
+ WADED + WEE: 444795
+ THE EYES: 444796
+ LEFT: 444797
+ RIGHT: 444798
+ MIDDLE: 444799
+ WARD: 444800
+ HIND: 444801
+ RIG: 444802
+ WINDWARD: 444803
+ LIGHT: 444804
+ REWIND: 444805
+ Champion's Rest:
+ EXIT: 444806
+ HUES: 444807
+ RED: 444808
+ BLUE: 444809
+ YELLOW: 444810
+ GREEN: 444811
+ PURPLE: 444812
+ ORANGE: 444813
+ YOU: 444814
+ ME: 444815
+ SECRET BLUE: 444816
+ SECRET YELLOW: 444817
+ SECRET RED: 444818
+ The Bearer:
+ Achievement: 444819
+ MIDDLE: 444820
+ FARTHER: 444821
+ BACKSIDE: 444822
+ PART: 444823
+ HEART: 444824
+ The Bearer (East):
+ SIX: 444825
+ PEACE: 444826
+ The Bearer (North):
+ SILENT (1): 444827
+ SILENT (2): 444828
+ SPACE: 444829
+ WARTS: 444830
+ The Bearer (South):
+ SIX: 444831
+ TENT: 444832
+ BOWL: 444833
+ The Bearer (West):
+ SNOW: 444834
+ SMILE: 444835
+ Bearer Side Area:
+ SHORTCUT: 444836
+ POTS: 444837
+ Cross Tower (East):
+ WINTER: 444838
+ Cross Tower (North):
+ NORTH: 444839
+ Cross Tower (South):
+ FIRE: 444840
+ Cross Tower (West):
+ DIAMONDS: 444841
+ The Steady (Rose):
+ SOAR: 444842
+ The Steady (Ruby):
+ BURY: 444843
+ The Steady (Carnation):
+ INCARNATION: 444844
+ The Steady (Sunflower):
+ SUN: 444845
+ The Steady (Plum):
+ LUMP: 444846
+ The Steady (Lime):
+ LIMELIGHT: 444847
+ The Steady (Lemon):
+ MELON: 444848
+ The Steady (Topaz):
+ TOP: 444849
+ MASTERY: 444850
+ The Steady (Orange):
+ BLUE: 444851
+ The Steady (Sapphire):
+ SAP: 444852
+ The Steady (Blueberry):
+ BLUE: 444853
+ The Steady (Amber):
+ ANTECHAMBER: 444854
+ The Steady (Emerald):
+ HERALD: 444855
+ The Steady (Amethyst):
+ PACIFIST: 444856
+ The Steady (Lilac):
+ LIE LACK: 444857
+ The Steady (Cherry):
+ HAIRY: 444858
+ The Steady:
+ Achievement: 444859
+ Knight Night (Outer Ring):
+ NIGHT: 444860
+ KNIGHT: 444861
+ BEE: 444862
+ NEW: 444863
+ FORE: 444864
+ TRUSTED (1): 444865
+ TRUSTED (2): 444866
+ ENCRUSTED: 444867
+ ADJUST (1): 444868
+ ADJUST (2): 444869
+ RIGHT: 444870
+ TRUST: 444871
+ Knight Night (Right Upper Segment):
+ RUST (1): 444872
+ RUST (2): 444873
+ Knight Night (Right Lower Segment):
+ ADJUST: 444874
+ BEFORE: 444875
+ BE: 444876
+ LEFT: 444877
+ TRUST: 444878
+ Knight Night (Final):
+ TRUSTED: 444879
+ Knight Night Exit:
+ SEVEN (1): 444880
+ SEVEN (2): 444881
+ SEVEN (3): 444882
+ DEAD END: 444883
+ WARNER: 444884
+ The Artistic (Smiley):
+ Achievement: 444885
+ FINE: 444886
+ BLADE: 444887
+ RED: 444888
+ BEARD: 444889
+ ICE: 444890
+ ROOT: 444891
+ The Artistic (Panda):
+ EYE (Top): 444892
+ EYE (Bottom): 444893
+ LADYLIKE: 444894
+ WATER: 444895
+ OURS: 444896
+ DAYS: 444897
+ NIGHTTIME: 444898
+ NIGHT: 444899
+ The Artistic (Lattice):
+ POSH: 444900
+ MALL: 444901
+ DEICIDE: 444902
+ WAVER: 444903
+ REPAID: 444904
+ BABY: 444905
+ LOBE: 444906
+ BOWELS: 444907
+ The Artistic (Apple):
+ SPRIG: 444908
+ RELEASES: 444909
+ MUCH: 444910
+ FISH: 444911
+ MASK: 444912
+ HILL: 444913
+ TINE: 444914
+ THING: 444915
+ The Artistic (Hint Room):
+ THEME: 444916
+ PAINTS: 444917
+ I: 444918
+ KIT: 444919
+ The Discerning:
+ Achievement: 444920
+ HITS: 444921
+ WARRED: 444922
+ REDRAW: 444923
+ ADDER: 444924
+ LAUGHTERS: 444925
+ STONE: 444926
+ ONSET: 444927
+ RAT: 444928
+ DUSTY: 444929
+ ARTS: 444930
+ TSAR: 444931
+ STATE: 444932
+ REACT: 444933
+ DEAR: 444934
+ DARE: 444935
+ SEAM: 444936
+ The Eyes They See:
+ NEAR: 444937
+ EIGHT: 444938
+ Far Window:
+ FAR: 444939
+ Outside The Wondrous:
+ SHRINK: 444940
+ The Wondrous (Bookcase):
+ CASE: 444941
+ The Wondrous (Chandelier):
+ CANDLE HEIR: 444942
+ The Wondrous (Window):
+ GLASS: 444943
+ The Wondrous (Table):
+ WOOD: 444944
+ BROOK NOD: 444945
+ The Wondrous:
+ FIREPLACE: 444946
+ Achievement: 444947
+ Arrow Garden:
+ MASTERY: 444948
+ SHARP: 444949
+ Hallway Room (2):
+ WISE: 444950
+ CLOCK: 444951
+ ER: 444952
+ COUNT: 444953
+ Hallway Room (3):
+ TRANCE: 444954
+ FORM: 444955
+ A: 444956
+ SHUN: 444957
+ Hallway Room (4):
+ WHEEL: 444958
+ Elements Area:
+ A: 444959
+ NINE: 444960
+ UNDISTRACTED: 444961
+ MASTERY: 444962
+ EARTH: 444963
+ WATER: 444964
+ AIR: 444965
+ Outside The Wanderer:
+ WANDERLUST: 444966
+ The Wanderer:
+ Achievement: 444967
+ '7890': 444968
+ '6524': 444969
+ '951': 444970
+ '4524': 444971
+ LEARN: 444972
+ DUST: 444973
+ STAR: 444974
+ WANDER: 444975
+ Art Gallery:
+ EIGHT: 444976
+ EON: 444977
+ TRUSTWORTHY: 444978
+ FREE: 444979
+ OUR: 444980
+ ONE ROAD MANY TURNS: 444981
+ Art Gallery (Second Floor):
+ HOUSE: 444982
+ PATH: 444983
+ PARK: 444984
+ CARRIAGE: 444985
+ Art Gallery (Third Floor):
+ AN: 444986
+ MAY: 444987
+ ANY: 444988
+ MAN: 444989
+ Art Gallery (Fourth Floor):
+ URNS: 444990
+ LEARNS: 444991
+ RUNTS: 444992
+ SEND - USE: 444993
+ TRUST: 444994
+ '062459': 444995
+ Rhyme Room (Smiley):
+ LOANS: 444996
+ SKELETON: 444997
+ REPENTANCE: 444998
+ WORD: 444999
+ SCHEME: 445000
+ FANTASY: 445001
+ HISTORY: 445002
+ SECRET: 445003
+ Rhyme Room (Cross):
+ NINE: 445004
+ FERN: 445005
+ STAY: 445006
+ FRIEND: 445007
+ RISE: 445008
+ PLUMP: 445009
+ BOUNCE: 445010
+ SCRAWL: 445011
+ PLUNGE: 445012
+ LEAP: 445013
+ Rhyme Room (Circle):
+ BIRD: 445014
+ LETTER: 445015
+ FORBIDDEN: 445016
+ CONCEALED: 445017
+ VIOLENT: 445018
+ MUTE: 445019
+ Rhyme Room (Looped Square):
+ WALKED: 445020
+ OBSTRUCTED: 445021
+ SKIES: 445022
+ SWELL: 445023
+ PENNED: 445024
+ CLIMB: 445025
+ TROUBLE: 445026
+ DUPLICATE: 445027
+ Rhyme Room (Target):
+ WILD: 445028
+ KID: 445029
+ PISTOL: 445030
+ QUARTZ: 445031
+ INNOVATIVE (Top): 445032
+ INNOVATIVE (Bottom): 445033
+ Room Room:
+ DOOR (1): 445034
+ DOOR (2): 445035
+ WINDOW: 445036
+ STAIRS: 445037
+ PAINTING: 445038
+ FLOOR (1): 445039
+ FLOOR (2): 445040
+ FLOOR (3): 445041
+ FLOOR (4): 445042
+ FLOOR (5): 445043
+ FLOOR (7): 445044
+ FLOOR (8): 445045
+ FLOOR (9): 445046
+ FLOOR (10): 445047
+ CEILING (1): 445048
+ CEILING (2): 445049
+ CEILING (3): 445050
+ CEILING (4): 445051
+ CEILING (5): 445052
+ WALL (1): 445053
+ WALL (2): 445054
+ WALL (3): 445055
+ WALL (4): 445056
+ WALL (5): 445057
+ WALL (6): 445058
+ WALL (7): 445059
+ WALL (8): 445060
+ WALL (9): 445061
+ WALL (10): 445062
+ WALL (11): 445063
+ WALL (12): 445064
+ WALL (13): 445065
+ WALL (14): 445066
+ WALL (15): 445067
+ WALL (16): 445068
+ WALL (17): 445069
+ WALL (18): 445070
+ WALL (19): 445071
+ WALL (20): 445072
+ WALL (21): 445073
+ BROOMED: 445074
+ LAYS: 445075
+ BASE: 445076
+ MASTERY: 445077
+ Outside The Wise:
+ KITTEN: 445084
+ CAT: 445085
+ The Wise:
+ Achievement: 445086
+ PUPPY: 445087
+ ADULT: 445088
+ BREAD: 445089
+ DINOSAUR: 445090
+ OAK: 445091
+ CORPSE: 445092
+ BEFORE: 445093
+ YOUR: 445094
+ BETWIXT: 445095
+ NIGH: 445096
+ CONNEXION: 445097
+ THOU: 445098
+ The Red:
+ Achievement: 445099
+ PANDEMIC (1): 445100
+ TRINITY: 445101
+ CHEMISTRY: 445102
+ FLUMMOXED: 445103
+ PANDEMIC (2): 445104
+ COUNTERCLOCKWISE: 445105
+ FEARLESS: 445106
+ DEFORESTATION: 445107
+ CRAFTSMANSHIP: 445108
+ CAMEL: 445109
+ LION: 445110
+ TIGER: 445111
+ SHIP (1): 445112
+ SHIP (2): 445113
+ GIRAFFE: 445114
+ The Ecstatic:
+ Achievement: 445115
+ FORM (1): 445116
+ WIND: 445117
+ EGGS: 445118
+ VEGETABLES: 445119
+ WATER: 445120
+ FRUITS: 445121
+ LEAVES: 445122
+ VINES: 445123
+ ICE: 445124
+ STYLE: 445125
+ FIR: 445126
+ REEF: 445127
+ ROTS: 445128
+ FORM (2): 445129
+ Outside The Scientific:
+ OPEN: 445130
+ CLOSE: 445131
+ AHEAD: 445132
+ The Scientific:
+ Achievement: 445133
+ HYDROGEN (1): 445134
+ OXYGEN: 445135
+ HYDROGEN (2): 445136
+ SUGAR (1): 445137
+ SUGAR (2): 445138
+ SUGAR (3): 445139
+ CHLORINE: 445140
+ SODIUM: 445141
+ FOREST: 445142
+ POUND: 445143
+ ICE: 445144
+ FISSION: 445145
+ FUSION: 445146
+ MISS: 445147
+ TREE (1): 445148
+ BIOGRAPHY: 445149
+ CACTUS: 445150
+ VERTEBRATE: 445151
+ ROSE: 445152
+ TREE (2): 445153
+ FRUIT: 445154
+ MAMMAL: 445155
+ BIRD: 445156
+ FISH: 445157
+ GRAVELY: 445158
+ BREVITY: 445159
+ PART: 445160
+ MATTER: 445161
+ ELECTRIC: 445162
+ ATOM (1): 445163
+ NEUTRAL: 445164
+ ATOM (2): 445165
+ PROPEL: 445166
+ ATOM (3): 445167
+ ORDER: 445168
+ OPTICS: 445169
+ GRAPHITE: 445170
+ HOT RYE: 445171
+ SIT SHY HOPE: 445172
+ ME NEXT PIER: 445173
+ RUT LESS: 445174
+ SON COUNCIL: 445175
+ Challenge Room:
+ WELCOME: 445176
+ CHALLENGE: 445177
+ Achievement: 445178
+ OPEN: 445179
+ SINGED: 445180
+ NEVER TRUSTED: 445181
+ CORNER: 445182
+ STRAWBERRIES: 445183
+ GRUB: 445184
+ BREAD: 445185
+ COLOR: 445186
+ WRITER: 445187
+ '02759': 445188
+ REAL EYES: 445189
+ LOBS: 445190
+ PEST ALLY: 445191
+ GENIAL HALO: 445192
+ DUCK LOGO: 445193
+ AVIAN GREEN: 445194
+ FEVER TEAR: 445195
+ FACTS: 445196
+ FACTS (1): 445197
+ FACTS (3): 445198
+ FACTS (4): 445199
+ FACTS (5): 445200
+ FACTS (6): 445201
+ LAPEL SHEEP: 445202
+doors:
+ Starting Room:
+ Back Right Door:
+ item: 444416
+ location: 444401
+ Rhyme Room Entrance:
+ item: 444417
+ Hidden Room:
+ Dead End Door:
+ item: 444419
+ Knight Night Entrance:
+ item: 444421
+ Seeker Entrance:
+ item: 444422
+ location: 444407
+ Rhyme Room Entrance:
+ item: 444423
+ Second Room:
+ Exit Door:
+ item: 444424
+ location: 445203
+ Hub Room:
+ Crossroads Entrance:
+ item: 444425
+ location: 444432
+ Tenacious Entrance:
+ item: 444426
+ location: 444433
+ Symmetry Door:
+ item: 444428
+ location: 445204
+ Shortcut to Hedge Maze:
+ item: 444430
+ location: 444436
+ Near RAT Door:
+ item: 444432
+ Traveled Entrance:
+ item: 444433
+ location: 444438
+ Lost Door:
+ item: 444435
+ location: 444440
+ Pilgrim Antechamber:
+ Sun Painting:
+ item: 444436
+ location: 445205
+ Pilgrim Room:
+ Shortcut to The Seeker:
+ item: 444437
+ location: 444449
+ Crossroads:
+ Tenacious Entrance:
+ item: 444438
+ location: 444462
+ Discerning Entrance:
+ item: 444439
+ location: 444463
+ Tower Entrance:
+ item: 444440
+ location: 444465
+ Tower Back Entrance:
+ item: 444442
+ location: 445206
+ Words Sword Door:
+ item: 444443
+ location: 445207
+ Eye Wall:
+ item: 444445
+ location: 444469
+ Hollow Hallway:
+ item: 444446
+ Roof Access:
+ item: 444447
+ Lost Area:
+ Exit:
+ item: 444448
+ location: 445208
+ Amen Name Area:
+ Exit:
+ item: 444449
+ location: 445209
+ The Tenacious:
+ Shortcut to Hub Room:
+ item: 444450
+ location: 445210
+ White Palindromes:
+ location: 445211
+ Warts Straw Area:
+ Door:
+ item: 444451
+ location: 445212
+ Leaf Feel Area:
+ Door:
+ item: 444452
+ location: 445213
+ Outside The Agreeable:
+ Tenacious Entrance:
+ item: 444453
+ location: 444496
+ Black Door:
+ item: 444454
+ location: 444497
+ Agreeable Entrance:
+ item: 444455
+ location: 444498
+ Painting Shortcut:
+ item: 444456
+ location: 444501
+ Purple Barrier:
+ item: 444457
+ Hallway Door:
+ item: 444459
+ location: 445214
+ Dread Hallway:
+ Tenacious Entrance:
+ item: 444462
+ location: 444516
+ The Agreeable:
+ Shortcut to Hedge Maze:
+ item: 444463
+ location: 444518
+ Hedge Maze:
+ Perceptive Entrance:
+ item: 444464
+ location: 444530
+ Painting Shortcut:
+ item: 444465
+ Observant Entrance:
+ item: 444466
+ Hide and Seek:
+ location: 445215
+ The Fearless (First Floor):
+ Second Floor:
+ item: 444468
+ location: 445216
+ The Fearless (Second Floor):
+ Third Floor:
+ item: 444471
+ location: 445217
+ The Observant:
+ Backside Door:
+ item: 444472
+ location: 445218
+ Stairs:
+ item: 444474
+ location: 444569
+ The Incomparable:
+ Eight Painting:
+ item: 444475
+ location: 445219
+ Orange Tower:
+ Second Floor:
+ item: 444476
+ Third Floor:
+ item: 444477
+ Fourth Floor:
+ item: 444478
+ Fifth Floor:
+ item: 444479
+ Sixth Floor:
+ item: 444480
+ Seventh Floor:
+ item: 444481
+ Orange Tower First Floor:
+ Shortcut to Hub Room:
+ item: 444483
+ location: 444602
+ Salt Pepper Door:
+ item: 444485
+ location: 445220
+ Orange Tower Third Floor:
+ Red Barrier:
+ item: 444486
+ Rhyme Room Entrance:
+ item: 444487
+ Orange Barrier:
+ item: 444488
+ location: 445221
+ Orange Tower Fourth Floor:
+ Hot Crusts Door:
+ item: 444490
+ location: 444610
+ Orange Tower Fifth Floor:
+ Welcome Back:
+ item: 444491
+ location: 445222
+ Orange Tower Seventh Floor:
+ Mastery:
+ item: 444493
+ Mastery Panels:
+ location: 445223
+ Courtyard:
+ Painting Shortcut:
+ item: 444494
+ Green Barrier:
+ item: 444495
+ First Second Third Fourth:
+ Backside Door:
+ item: 444496
+ location: 445224
+ The Colorful (White):
+ Progress Door:
+ item: 444497
+ location: 445225
+ The Colorful (Black):
+ Progress Door:
+ item: 444499
+ location: 445226
+ The Colorful (Red):
+ Progress Door:
+ item: 444500
+ location: 445227
+ The Colorful (Yellow):
+ Progress Door:
+ item: 444501
+ location: 445228
+ The Colorful (Blue):
+ Progress Door:
+ item: 444502
+ location: 445229
+ The Colorful (Purple):
+ Progress Door:
+ item: 444503
+ location: 445230
+ The Colorful (Orange):
+ Progress Door:
+ item: 444504
+ location: 445231
+ The Colorful (Green):
+ Progress Door:
+ item: 444505
+ location: 445232
+ The Colorful (Brown):
+ Progress Door:
+ item: 444506
+ location: 445233
+ The Colorful (Gray):
+ Progress Door:
+ item: 444507
+ location: 445234
+ Welcome Back Area:
+ Shortcut to Starting Room:
+ item: 444508
+ location: 444653
+ Owl Hallway:
+ Shortcut to Hedge Maze:
+ item: 444509
+ location: 444656
+ Outside The Initiated:
+ Shortcut to Hub Room:
+ item: 444510
+ location: 444664
+ Blue Barrier:
+ item: 444511
+ Orange Barrier:
+ item: 444512
+ Initiated Entrance:
+ item: 444513
+ location: 444665
+ Green Barrier:
+ item: 444514
+ location: 445235
+ Purple Barrier:
+ item: 444515
+ location: 445236
+ Entrance:
+ item: 444516
+ location: 445237
+ The Traveled:
+ Color Hallways Entrance:
+ item: 444517
+ location: 444698
+ Outside The Bold:
+ Bold Entrance:
+ item: 444518
+ location: 444711
+ Painting Shortcut:
+ item: 444519
+ Steady Entrance:
+ item: 444520
+ location: 444712
+ Outside The Undeterred:
+ Undeterred Entrance:
+ item: 444521
+ location: 444744
+ Painting Shortcut:
+ item: 444522
+ Green Painting:
+ item: 444523
+ Twos:
+ item: 444524
+ location: 444752
+ Threes:
+ item: 444525
+ location: 445238
+ Number Hunt:
+ item: 444526
+ location: 445239
+ Fours:
+ item: 444527
+ Fives:
+ item: 444528
+ location: 445240
+ Challenge Entrance:
+ item: 444529
+ location: 444751
+ Number Hunt:
+ Door to Directional Gallery:
+ item: 444530
+ Sixes:
+ item: 444532
+ location: 445241
+ Sevens:
+ item: 444533
+ location: 445242
+ Eights:
+ item: 444534
+ location: 445243
+ Nines:
+ item: 444535
+ location: 445244
+ Zero Door:
+ item: 444536
+ location: 445245
+ Directional Gallery:
+ Shortcut to The Undeterred:
+ item: 444537
+ location: 445246
+ Yellow Barrier:
+ item: 444538
+ Champion's Rest:
+ Shortcut to The Steady:
+ item: 444539
+ location: 444806
+ The Bearer:
+ Shortcut to The Bold:
+ item: 444540
+ location: 444820
+ Backside Door:
+ item: 444541
+ location: 444821
+ Bearer Side Area:
+ Shortcut to Tower:
+ item: 444542
+ location: 445247
+ Knight Night (Final):
+ Exit:
+ item: 444543
+ location: 445248
+ The Artistic (Smiley):
+ Door to Panda:
+ item: 444544
+ location: 445249
+ The Artistic (Panda):
+ Door to Lattice:
+ item: 444546
+ location: 445250
+ The Artistic (Lattice):
+ Door to Apple:
+ item: 444547
+ location: 445251
+ The Artistic (Apple):
+ Door to Smiley:
+ item: 444548
+ location: 445252
+ The Eyes They See:
+ Exit:
+ item: 444549
+ location: 444937
+ Outside The Wondrous:
+ Wondrous Entrance:
+ item: 444550
+ location: 444940
+ The Wondrous (Doorknob):
+ Painting Shortcut:
+ item: 444551
+ The Wondrous:
+ Exit:
+ item: 444552
+ location: 444947
+ Hallway Room (2):
+ Exit:
+ item: 444553
+ location: 445253
+ Hallway Room (3):
+ Exit:
+ item: 444554
+ location: 445254
+ Hallway Room (4):
+ Exit:
+ item: 444555
+ location: 445255
+ Outside The Wanderer:
+ Wanderer Entrance:
+ item: 444556
+ location: 444966
+ Tower Entrance:
+ item: 444557
+ Art Gallery:
+ Second Floor:
+ item: 444558
+ First Floor Puzzles:
+ location: 445256
+ Third Floor:
+ item: 444559
+ Fourth Floor:
+ item: 444560
+ Fifth Floor:
+ item: 444561
+ Exit:
+ item: 444562
+ location: 444981
+ Art Gallery (Second Floor):
+ Puzzles:
+ location: 445257
+ Art Gallery (Third Floor):
+ Puzzles:
+ location: 445258
+ Art Gallery (Fourth Floor):
+ Puzzles:
+ location: 445259
+ Rhyme Room (Smiley):
+ Door to Target:
+ item: 444564
+ Door to Target (Location):
+ location: 445260
+ Rhyme Room (Cross):
+ Exit:
+ item: 444565
+ location: 445261
+ Rhyme Room (Circle):
+ Door to Smiley:
+ item: 444566
+ location: 445262
+ Rhyme Room (Looped Square):
+ Door to Circle:
+ item: 444567
+ location: 445263
+ Door to Cross:
+ item: 444568
+ location: 445264
+ Door to Target:
+ item: 444569
+ location: 445265
+ Rhyme Room (Target):
+ Door to Cross:
+ item: 444570
+ location: 445266
+ Room Room:
+ Shortcut to Fifth Floor:
+ item: 444571
+ location: 445076
+ Outside The Wise:
+ Wise Entrance:
+ item: 444572
+ location: 445267
+ Outside The Scientific:
+ Scientific Entrance:
+ item: 444573
+ location: 445130
+ The Scientific:
+ Chemistry Puzzles:
+ location: 445268
+ Biology Puzzles:
+ location: 445269
+ Physics Puzzles:
+ location: 445270
+ Challenge Room:
+ Welcome Door:
+ item: 444574
+ location: 445176
+door_groups:
+ Rhyme Room Doors: 444418
+ Dead End Area Access: 444420
+ Entrances to The Tenacious: 444427
+ Symmetry Doors: 444429
+ Hedge Maze Doors: 444431
+ Entrance to The Traveled: 444434
+ Crossroads - Tower Entrances: 444441
+ Crossroads Doors: 444444
+ Color Hunt Barriers: 444458
+ Hallway Room Doors: 444460
+ Observant Doors: 444467
+ Fearless Doors: 444469
+ Backside Doors: 444473
+ Orange Tower First Floor - Shortcuts: 444484
+ Champion's Rest - Color Barriers: 444489
+ Welcome Back Doors: 444492
+ Colorful Doors: 444498
+ Directional Gallery Doors: 444531
+ Artistic Doors: 444545
+progression:
+ Progressive Hallway Room: 444461
+ Progressive Fearless: 444470
+ Progressive Orange Tower: 444482
+ Progressive Art Gallery: 444563
diff --git a/worlds/lingo/items.py b/worlds/lingo/items.py
new file mode 100644
index 000000000000..af24570f278e
--- /dev/null
+++ b/worlds/lingo/items.py
@@ -0,0 +1,106 @@
+from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING
+
+from BaseClasses import Item, ItemClassification
+from .options import ShuffleDoors
+from .static_logic import DOORS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, get_door_group_item_id, \
+ get_door_item_id, get_progressive_item_id, get_special_item_id
+
+if TYPE_CHECKING:
+ from . import LingoWorld
+
+
+class ItemData(NamedTuple):
+ """
+ ItemData for an item in Lingo
+ """
+ code: int
+ classification: ItemClassification
+ mode: Optional[str]
+ door_ids: List[str]
+ painting_ids: List[str]
+
+ def should_include(self, world: "LingoWorld") -> bool:
+ if self.mode == "colors":
+ return world.options.shuffle_colors > 0
+ elif self.mode == "doors":
+ return world.options.shuffle_doors != ShuffleDoors.option_none
+ elif self.mode == "orange tower":
+ # door shuffle is on and tower isn't progressive
+ return world.options.shuffle_doors != ShuffleDoors.option_none \
+ and not world.options.progressive_orange_tower
+ elif self.mode == "complex door":
+ return world.options.shuffle_doors == ShuffleDoors.option_complex
+ elif self.mode == "door group":
+ return world.options.shuffle_doors == ShuffleDoors.option_simple
+ elif self.mode == "special":
+ return False
+ else:
+ return True
+
+
+class LingoItem(Item):
+ """
+ Item from the game Lingo
+ """
+ game: str = "Lingo"
+
+
+ALL_ITEM_TABLE: Dict[str, ItemData] = {}
+
+
+def load_item_data():
+ global ALL_ITEM_TABLE
+
+ for color in ["Black", "Red", "Blue", "Yellow", "Green", "Orange", "Gray", "Brown", "Purple"]:
+ ALL_ITEM_TABLE[color] = ItemData(get_special_item_id(color), ItemClassification.progression,
+ "colors", [], [])
+
+ door_groups: Dict[str, List[str]] = {}
+ for room_name, doors in DOORS_BY_ROOM.items():
+ for door_name, door in doors.items():
+ if door.skip_item is True or door.event is True:
+ continue
+
+ if door.group is None:
+ door_mode = "doors"
+ else:
+ door_mode = "complex door"
+ door_groups.setdefault(door.group, []).extend(door.door_ids)
+
+ if room_name in PROGRESSION_BY_ROOM and door_name in PROGRESSION_BY_ROOM[room_name]:
+ if room_name == "Orange Tower":
+ door_mode = "orange tower"
+ else:
+ door_mode = "special"
+
+ ALL_ITEM_TABLE[door.item_name] = \
+ ItemData(get_door_item_id(room_name, door_name),
+ ItemClassification.filler if door.junk_item else ItemClassification.progression, door_mode,
+ door.door_ids, door.painting_ids)
+
+ for group, group_door_ids in door_groups.items():
+ ALL_ITEM_TABLE[group] = ItemData(get_door_group_item_id(group),
+ ItemClassification.progression, "door group", group_door_ids, [])
+
+ special_items: Dict[str, ItemClassification] = {
+ ":)": ItemClassification.filler,
+ "The Feeling of Being Lost": ItemClassification.filler,
+ "Wanderlust": ItemClassification.filler,
+ "Empty White Hallways": ItemClassification.filler,
+ "Slowness Trap": ItemClassification.trap,
+ "Iceland Trap": ItemClassification.trap,
+ "Atbash Trap": ItemClassification.trap,
+ "Puzzle Skip": ItemClassification.useful,
+ }
+
+ for item_name, classification in special_items.items():
+ ALL_ITEM_TABLE[item_name] = ItemData(get_special_item_id(item_name), classification,
+ "special", [], [])
+
+ for item_name in PROGRESSIVE_ITEMS:
+ ALL_ITEM_TABLE[item_name] = ItemData(get_progressive_item_id(item_name),
+ ItemClassification.progression, "special", [], [])
+
+
+# Initialize the item data at module scope.
+load_item_data()
diff --git a/worlds/lingo/locations.py b/worlds/lingo/locations.py
new file mode 100644
index 000000000000..5903d603ec4f
--- /dev/null
+++ b/worlds/lingo/locations.py
@@ -0,0 +1,80 @@
+from enum import Flag, auto
+from typing import Dict, List, NamedTuple
+
+from BaseClasses import Location
+from .static_logic import DOORS_BY_ROOM, PANELS_BY_ROOM, RoomAndPanel, get_door_location_id, get_panel_location_id
+
+
+class LocationClassification(Flag):
+ normal = auto()
+ reduced = auto()
+ insanity = auto()
+
+
+class LocationData(NamedTuple):
+ """
+ LocationData for a location in Lingo
+ """
+ code: int
+ room: str
+ panels: List[RoomAndPanel]
+ classification: LocationClassification
+
+ def panel_ids(self):
+ ids = set()
+ for panel in self.panels:
+ effective_room = self.room if panel.room is None else panel.room
+ panel_data = PANELS_BY_ROOM[effective_room][panel.panel]
+ ids = ids | set(panel_data.internal_ids)
+ return ids
+
+
+class LingoLocation(Location):
+ """
+ Location from the game Lingo
+ """
+ game: str = "Lingo"
+
+
+ALL_LOCATION_TABLE: Dict[str, LocationData] = {}
+
+
+def load_location_data():
+ global ALL_LOCATION_TABLE
+
+ for room_name, panels in PANELS_BY_ROOM.items():
+ for panel_name, panel in panels.items():
+ location_name = f"{room_name} - {panel_name}"
+
+ classification = LocationClassification.insanity
+ if panel.check:
+ classification |= LocationClassification.normal
+
+ if not panel.exclude_reduce:
+ classification |= LocationClassification.reduced
+
+ ALL_LOCATION_TABLE[location_name] = \
+ LocationData(get_panel_location_id(room_name, panel_name), room_name,
+ [RoomAndPanel(None, panel_name)], classification)
+
+ for room_name, doors in DOORS_BY_ROOM.items():
+ for door_name, door in doors.items():
+ if door.skip_location or door.event or door.panels is None:
+ continue
+
+ location_name = door.location_name
+ classification = LocationClassification.normal
+ if door.include_reduce:
+ classification |= LocationClassification.reduced
+
+ if location_name in ALL_LOCATION_TABLE:
+ new_id = ALL_LOCATION_TABLE[location_name].code
+ classification |= ALL_LOCATION_TABLE[location_name].classification
+ else:
+ new_id = get_door_location_id(room_name, door_name)
+
+ ALL_LOCATION_TABLE[location_name] = LocationData(new_id, room_name, door.panels, classification)
+
+
+# Initialize location data on the module scope.
+load_location_data()
diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py
new file mode 100644
index 000000000000..7dc6a1389c0c
--- /dev/null
+++ b/worlds/lingo/options.py
@@ -0,0 +1,126 @@
+from dataclasses import dataclass
+
+from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions
+
+
+class ShuffleDoors(Choice):
+ """If on, opening doors will require their respective "keys".
+ In "simple", doors are sorted into logical groups, which are all opened by receiving an item.
+ In "complex", the items are much more granular, and will usually only open a single door each."""
+ display_name = "Shuffle Doors"
+ option_none = 0
+ option_simple = 1
+ option_complex = 2
+
+
+class ProgressiveOrangeTower(DefaultOnToggle):
+ """When "Shuffle Doors" is on, this setting governs the manner in which the Orange Tower floors open up.
+ If off, there is an item for each floor of the tower, and each floor's item is the only one needed to access that floor.
+ If on, there are six progressive items, which open up the tower from the bottom floor upward.
+ """
+ display_name = "Progressive Orange Tower"
+
+
+class LocationChecks(Choice):
+ """On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for
+ achievement panels and a small handful of other panels.
+ On "reduced", many of the locations that are associated with opening doors are removed.
+ On "insanity", every individual panel in the game is a location check."""
+ display_name = "Location Checks"
+ option_normal = 0
+ option_reduced = 1
+ option_insanity = 2
+
+
+class ShuffleColors(Toggle):
+ """If on, an item is added to the pool for every puzzle color (besides White).
+ You will need to unlock the requisite colors in order to be able to solve puzzles of that color."""
+ display_name = "Shuffle Colors"
+
+
+class ShufflePanels(Choice):
+ """If on, the puzzles on each panel are randomized.
+ On "rearrange", the puzzles are the same as the ones in the base game, but are placed in different areas."""
+ display_name = "Shuffle Panels"
+ option_none = 0
+ option_rearrange = 1
+
+
+class ShufflePaintings(Toggle):
+ """If on, the destination, location, and appearance of the painting warps in the game will be randomized."""
+ display_name = "Shuffle Paintings"
+
+
+class VictoryCondition(Choice):
+ """Change the victory condition."""
+ display_name = "Victory Condition"
+ option_the_end = 0
+ option_the_master = 1
+ option_level_2 = 2
+
+
+class MasteryAchievements(Range):
+ """The number of achievements required to unlock THE MASTER.
+ In the base game, 21 achievements are needed.
+ If you include The Scientific and The Unchallenged, which are in the base game but are not counted for mastery, 23 would be required.
+ If you include the custom achievement (The Wanderer), 24 would be required.
+ """
+ display_name = "Mastery Achievements"
+ range_start = 1
+ range_end = 24
+ default = 21
+
+
+class Level2Requirement(Range):
+ """The number of panel solves required to unlock LEVEL 2.
+ In the base game, 223 are needed.
+ Note that this count includes ANOTHER TRY.
+ """
+ display_name = "Level 2 Requirement"
+ range_start = 2
+ range_end = 800
+ default = 223
+
+
+class EarlyColorHallways(Toggle):
+ """When on, a painting warp to the color hallways area will appear in the starting room.
+ This lets you avoid being trapped in the starting room for long periods of time when door shuffle is on."""
+ display_name = "Early Color Hallways"
+
+
+class TrapPercentage(Range):
+ """Replaces junk items with traps, at the specified rate."""
+ display_name = "Trap Percentage"
+ range_start = 0
+ range_end = 100
+ default = 20
+
+
+class PuzzleSkipPercentage(Range):
+ """Replaces junk items with puzzle skips, at the specified rate."""
+ display_name = "Puzzle Skip Percentage"
+ range_start = 0
+ range_end = 100
+ default = 20
+
+
+class DeathLink(Toggle):
+ """If on: Whenever another player on death link dies, you will be returned to the starting room."""
+ display_name = "Death Link"
+
+
+@dataclass
+class LingoOptions(PerGameCommonOptions):
+ shuffle_doors: ShuffleDoors
+ progressive_orange_tower: ProgressiveOrangeTower
+ location_checks: LocationChecks
+ shuffle_colors: ShuffleColors
+ shuffle_panels: ShufflePanels
+ shuffle_paintings: ShufflePaintings
+ victory_condition: VictoryCondition
+ mastery_achievements: MasteryAchievements
+ level_2_requirement: Level2Requirement
+ early_color_hallways: EarlyColorHallways
+ trap_percentage: TrapPercentage
+ puzzle_skip_percentage: PuzzleSkipPercentage
+ death_link: DeathLink
diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py
new file mode 100644
index 000000000000..217ad91fcd23
--- /dev/null
+++ b/worlds/lingo/player_logic.py
@@ -0,0 +1,298 @@
+from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING
+
+from .items import ALL_ITEM_TABLE
+from .locations import ALL_LOCATION_TABLE, LocationClassification
+from .options import LocationChecks, ShuffleDoors, VictoryCondition
+from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \
+ PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, ROOMS, \
+ RoomAndPanel
+from .testing import LingoTestOptions
+
+if TYPE_CHECKING:
+ from . import LingoWorld
+
+
+class PlayerLocation(NamedTuple):
+ name: str
+ code: Optional[int] = None
+ panels: List[RoomAndPanel] = []
+
+
+class LingoPlayerLogic:
+ """
+ Defines logic after a player's options have been applied
+ """
+
+ ITEM_BY_DOOR: Dict[str, Dict[str, str]]
+
+ LOCATIONS_BY_ROOM: Dict[str, List[PlayerLocation]]
+ REAL_LOCATIONS: List[str]
+
+ EVENT_LOC_TO_ITEM: Dict[str, str]
+ REAL_ITEMS: List[str]
+
+ VICTORY_CONDITION: str
+ MASTERY_LOCATION: str
+ LEVEL_2_LOCATION: str
+
+ PAINTING_MAPPING: Dict[str, str]
+
+ FORCED_GOOD_ITEM: str
+
+ def add_location(self, room: str, loc: PlayerLocation):
+ self.LOCATIONS_BY_ROOM.setdefault(room, []).append(loc)
+
+ def set_door_item(self, room: str, door: str, item: str):
+ self.ITEM_BY_DOOR.setdefault(room, {})[door] = item
+
+ def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"):
+ if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]:
+ if room_name == "Orange Tower" and not world.options.progressive_orange_tower:
+ self.set_door_item(room_name, door_data.name, door_data.item_name)
+ else:
+ progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name
+ self.set_door_item(room_name, door_data.name, progressive_item_name)
+ self.REAL_ITEMS.append(progressive_item_name)
+ else:
+ self.set_door_item(room_name, door_data.name, door_data.item_name)
+
+ def __init__(self, world: "LingoWorld"):
+ self.ITEM_BY_DOOR = {}
+ self.LOCATIONS_BY_ROOM = {}
+ self.REAL_LOCATIONS = []
+ self.EVENT_LOC_TO_ITEM = {}
+ self.REAL_ITEMS = []
+ self.VICTORY_CONDITION = ""
+ self.MASTERY_LOCATION = ""
+ self.LEVEL_2_LOCATION = ""
+ self.PAINTING_MAPPING = {}
+ self.FORCED_GOOD_ITEM = ""
+
+ door_shuffle = world.options.shuffle_doors
+ color_shuffle = world.options.shuffle_colors
+ painting_shuffle = world.options.shuffle_paintings
+ location_checks = world.options.location_checks
+ victory_condition = world.options.victory_condition
+ early_color_hallways = world.options.early_color_hallways
+
+ if location_checks == LocationChecks.option_reduced and door_shuffle != ShuffleDoors.option_none:
+ raise Exception("You cannot have reduced location checks when door shuffle is on, because there would not "
+ "be enough locations for all of the door items.")
+
+ # Create an event for every room that represents being able to reach that room.
+ for room_name in ROOMS.keys():
+ roomloc_name = f"{room_name} (Reached)"
+ self.add_location(room_name, PlayerLocation(roomloc_name, None, []))
+ self.EVENT_LOC_TO_ITEM[roomloc_name] = roomloc_name
+
+ # Create an event for every door, representing whether that door has been opened. Also create event items for
+ # doors that are event-only.
+ for room_name, room_data in DOORS_BY_ROOM.items():
+ for door_name, door_data in room_data.items():
+ if door_shuffle == ShuffleDoors.option_none:
+ itemloc_name = f"{room_name} - {door_name} (Opened)"
+ self.add_location(room_name, PlayerLocation(itemloc_name, None, door_data.panels))
+ self.EVENT_LOC_TO_ITEM[itemloc_name] = itemloc_name
+ self.set_door_item(room_name, door_name, itemloc_name)
+ else:
+ # This line is duplicated from StaticLingoItems
+ if door_data.skip_item is False and door_data.event is False:
+ if door_data.group is not None and door_shuffle == ShuffleDoors.option_simple:
+ # Grouped doors are handled differently if shuffle doors is on simple.
+ self.set_door_item(room_name, door_name, door_data.group)
+ else:
+ self.handle_non_grouped_door(room_name, door_data, world)
+
+ if door_data.event:
+ self.add_location(room_name, PlayerLocation(door_data.item_name, None, door_data.panels))
+ self.EVENT_LOC_TO_ITEM[door_data.item_name] = door_data.item_name + " (Opened)"
+ self.set_door_item(room_name, door_name, door_data.item_name + " (Opened)")
+
+ # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. We also
+ # create events for each counting panel, so that we can determine when LEVEL 2 is accessible.
+ for room_name, room_data in PANELS_BY_ROOM.items():
+ for panel_name, panel_data in room_data.items():
+ if panel_data.achievement:
+ event_name = room_name + " - " + panel_name + " (Achieved)"
+ self.add_location(room_name, PlayerLocation(event_name, None,
+ [RoomAndPanel(room_name, panel_name)]))
+ self.EVENT_LOC_TO_ITEM[event_name] = "Mastery Achievement"
+
+ if not panel_data.non_counting and victory_condition == VictoryCondition.option_level_2:
+ event_name = room_name + " - " + panel_name + " (Counted)"
+ self.add_location(room_name, PlayerLocation(event_name, None,
+ [RoomAndPanel(room_name, panel_name)]))
+ self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved"
+
+ # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need
+ # to prevent the actual victory condition from becoming a check.
+ self.MASTERY_LOCATION = "Orange Tower Seventh Floor - THE MASTER"
+ self.LEVEL_2_LOCATION = "N/A"
+
+ if victory_condition == VictoryCondition.option_the_end:
+ self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE END"
+ self.add_location("Orange Tower Seventh Floor", PlayerLocation("The End (Solved)"))
+ self.EVENT_LOC_TO_ITEM["The End (Solved)"] = "Victory"
+ elif victory_condition == VictoryCondition.option_the_master:
+ self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE MASTER"
+ self.MASTERY_LOCATION = "Orange Tower Seventh Floor - Mastery Achievements"
+
+ self.add_location("Orange Tower Seventh Floor", PlayerLocation(self.MASTERY_LOCATION, None, []))
+ self.EVENT_LOC_TO_ITEM[self.MASTERY_LOCATION] = "Victory"
+ elif victory_condition == VictoryCondition.option_level_2:
+ self.VICTORY_CONDITION = "Second Room - LEVEL 2"
+ self.LEVEL_2_LOCATION = "Second Room - Unlock Level 2"
+
+ self.add_location("Second Room", PlayerLocation(self.LEVEL_2_LOCATION, None,
+ [RoomAndPanel("Second Room", "LEVEL 2")]))
+ self.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory"
+
+ # Instantiate all real locations.
+ location_classification = LocationClassification.normal
+ if location_checks == LocationChecks.option_reduced:
+ location_classification = LocationClassification.reduced
+ elif location_checks == LocationChecks.option_insanity:
+ location_classification = LocationClassification.insanity
+
+ for location_name, location_data in ALL_LOCATION_TABLE.items():
+ if location_name != self.VICTORY_CONDITION:
+ if location_classification not in location_data.classification:
+ continue
+
+ self.add_location(location_data.room, PlayerLocation(location_name, location_data.code,
+ location_data.panels))
+ self.REAL_LOCATIONS.append(location_name)
+
+ # Instantiate all real items.
+ for name, item in ALL_ITEM_TABLE.items():
+ if item.should_include(world):
+ self.REAL_ITEMS.append(name)
+
+ # Create the paintings mapping, if painting shuffle is on.
+ if painting_shuffle:
+ # Shuffle paintings until we get something workable.
+ workable_paintings = False
+ for i in range(0, 20):
+ workable_paintings = self.randomize_paintings(world)
+ if workable_paintings:
+ break
+
+ if not workable_paintings:
+ raise Exception("This Lingo world was unable to generate a workable painting mapping after 20 "
+ "iterations. This is very unlikely to happen on its own, and probably indicates some "
+ "kind of logic error.")
+
+ if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \
+ and not early_color_hallways and LingoTestOptions.disable_forced_good_item is False:
+ # If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK,
+ # but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right
+ # now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are
+ # no extra checks in there. We only include the entrance to the Rhyme Room when color shuffle is off and
+ # door shuffle is on simple, because otherwise there are no extra checks in there.
+ good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"]
+
+ if not color_shuffle:
+ good_item_options.append("Pilgrim Room - Sun Painting")
+
+ if door_shuffle == ShuffleDoors.option_simple:
+ good_item_options += ["Welcome Back Doors"]
+
+ if not color_shuffle:
+ good_item_options.append("Rhyme Room Doors")
+ else:
+ good_item_options += ["Welcome Back Area - Shortcut to Starting Room"]
+
+ for painting_obj in PAINTINGS_BY_ROOM["Starting Room"]:
+ if not painting_obj.enter_only or painting_obj.required_door is None:
+ continue
+
+ # If painting shuffle is on, we only want to consider paintings that actually go somewhere.
+ if painting_shuffle and painting_obj.id not in self.PAINTING_MAPPING.keys():
+ continue
+
+ pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door]
+ good_item_options.append(pdoor.item_name)
+
+ # Copied from The Witness -- remove any plandoed items from the possible good items set.
+ for v in world.multiworld.plando_items[world.player]:
+ if v.get("from_pool", True):
+ for item_key in {"item", "items"}:
+ if item_key in v:
+ if type(v[item_key]) is str:
+ if v[item_key] in good_item_options:
+ good_item_options.remove(v[item_key])
+ elif type(v[item_key]) is dict:
+ for item, weight in v[item_key].items():
+ if weight and item in good_item_options:
+ good_item_options.remove(item)
+ else:
+ # Other type of iterable
+ for item in v[item_key]:
+ if item in good_item_options:
+ good_item_options.remove(item)
+
+ if len(good_item_options) > 0:
+ self.FORCED_GOOD_ITEM = world.random.choice(good_item_options)
+ self.REAL_ITEMS.remove(self.FORCED_GOOD_ITEM)
+ self.REAL_LOCATIONS.remove("Second Room - Good Luck")
+
+ def randomize_paintings(self, world: "LingoWorld") -> bool:
+ self.PAINTING_MAPPING.clear()
+
+ door_shuffle = world.options.shuffle_doors
+
+ # Determine the set of exit paintings. All required-exit paintings are included, as are all
+ # required-when-no-doors paintings if door shuffle is off. We then fill the set with random other paintings.
+ chosen_exits = []
+ if door_shuffle == ShuffleDoors.option_none:
+ chosen_exits = [painting_id for painting_id, painting in PAINTINGS.items()
+ if painting.required_when_no_doors]
+ chosen_exits += [painting_id for painting_id, painting in PAINTINGS.items()
+ if painting.exit_only and painting.required]
+ exitable = [painting_id for painting_id, painting in PAINTINGS.items()
+ if not painting.enter_only and not painting.disable and not painting.required]
+ chosen_exits += world.random.sample(exitable, PAINTING_EXITS - len(chosen_exits))
+
+ # Determine the set of entrance paintings.
+ enterable = [painting_id for painting_id, painting in PAINTINGS.items()
+ if not painting.exit_only and not painting.disable and painting_id not in chosen_exits]
+ chosen_entrances = world.random.sample(enterable, PAINTING_ENTRANCES)
+
+ # Create a mapping from entrances to exits.
+ for warp_exit in chosen_exits:
+ warp_enter = world.random.choice(chosen_entrances)
+
+ # Check whether this is a warp from a required painting room to another (or the same) required painting
+ # room. This could cause a cycle that would make certain regions inaccessible.
+ warp_exit_room = PAINTINGS[warp_exit].room
+ warp_enter_room = PAINTINGS[warp_enter].room
+
+ required_painting_rooms = REQUIRED_PAINTING_ROOMS
+ if door_shuffle == ShuffleDoors.option_none:
+ required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
+
+ if warp_exit_room in required_painting_rooms and warp_enter_room in required_painting_rooms:
+ # This shuffling is non-workable. Start over.
+ return False
+
+ chosen_entrances.remove(warp_enter)
+ self.PAINTING_MAPPING[warp_enter] = warp_exit
+
+ for warp_enter in chosen_entrances:
+ warp_exit = world.random.choice(chosen_exits)
+ self.PAINTING_MAPPING[warp_enter] = warp_exit
+
+ # The Eye Wall painting is unique in that it is both double-sided and also enter only (because it moves).
+ # There is only one eligible double-sided exit painting, which is the vanilla exit for this warp. If the
+ # exit painting is an entrance in the shuffle, we will disable the Eye Wall painting. Otherwise, Eye Wall
+ # is forced to point to the vanilla exit.
+ if "eye_painting_2" not in self.PAINTING_MAPPING.keys():
+ self.PAINTING_MAPPING["eye_painting"] = "eye_painting_2"
+
+ # Just for sanity's sake, ensure that all required painting rooms are accessed.
+ for painting_id, painting in PAINTINGS.items():
+ if painting_id not in self.PAINTING_MAPPING.values() \
+ and (painting.required or (painting.required_when_no_doors and door_shuffle == 0)):
+ return False
+
+ return True
diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py
new file mode 100644
index 000000000000..c75cf4956d0b
--- /dev/null
+++ b/worlds/lingo/regions.py
@@ -0,0 +1,84 @@
+from typing import Dict, TYPE_CHECKING
+
+from BaseClasses import ItemClassification, Region
+from .items import LingoItem
+from .locations import LingoLocation
+from .player_logic import LingoPlayerLogic
+from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda
+from .static_logic import ALL_ROOMS, PAINTINGS, Room
+
+if TYPE_CHECKING:
+ from . import LingoWorld
+
+
+def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogic) -> Region:
+ new_region = Region(room.name, world.player, world.multiworld)
+ for location in player_logic.LOCATIONS_BY_ROOM.get(room.name, {}):
+ new_location = LingoLocation(world.player, location.name, location.code, new_region)
+ new_location.access_rule = make_location_lambda(location, room.name, world, player_logic)
+ new_region.locations.append(new_location)
+ if location.name in player_logic.EVENT_LOC_TO_ITEM:
+ event_name = player_logic.EVENT_LOC_TO_ITEM[location.name]
+ event_item = LingoItem(event_name, ItemClassification.progression, None, world.player)
+ new_location.place_locked_item(event_item)
+
+ return new_region
+
+
+def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
+ target_region = regions["Pilgrim Antechamber"]
+ source_region = regions["Outside The Agreeable"]
+ source_region.connect(
+ target_region,
+ "Pilgrimage",
+ lambda state: lingo_can_use_pilgrimage(state, world.player, player_logic))
+
+
+def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld",
+ player_logic: LingoPlayerLogic) -> None:
+ source_painting = PAINTINGS[warp_enter]
+ target_painting = PAINTINGS[warp_exit]
+
+ target_region = regions[target_painting.room]
+ source_region = regions[source_painting.room]
+ source_region.connect(
+ target_region,
+ f"{source_painting.room} to {target_painting.room} (Painting)",
+ lambda state: lingo_can_use_entrance(state, target_painting.room, source_painting.required_door, world.player,
+ player_logic))
+
+
+def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
+ regions = {
+ "Menu": Region("Menu", world.player, world.multiworld)
+ }
+
+ painting_shuffle = world.options.shuffle_paintings
+ early_color_hallways = world.options.early_color_hallways
+
+ # Instantiate all rooms as regions with their locations first.
+ for room in ALL_ROOMS:
+ regions[room.name] = create_region(room, world, player_logic)
+
+ # Connect all created regions now that they exist.
+ for room in ALL_ROOMS:
+ for entrance in room.entrances:
+ # Don't use the vanilla painting connections if we are shuffling paintings.
+ if entrance.painting and painting_shuffle:
+ continue
+
+ regions[entrance.room].connect(
+ regions[room.name],
+ f"{entrance.room} to {room.name}",
+ lambda state, r=room, e=entrance: lingo_can_use_entrance(state, r.name, e.door, world.player, player_logic))
+
+ handle_pilgrim_room(regions, world, player_logic)
+
+ if early_color_hallways:
+ regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways")
+
+ if painting_shuffle:
+ for warp_enter, warp_exit in player_logic.PAINTING_MAPPING.items():
+ connect_painting(regions, warp_enter, warp_exit, world, player_logic)
+
+ world.multiworld.regions += regions.values()
diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py
new file mode 100644
index 000000000000..90c889b7f098
--- /dev/null
+++ b/worlds/lingo/rules.py
@@ -0,0 +1,104 @@
+from typing import TYPE_CHECKING
+
+from BaseClasses import CollectionState
+from .options import VictoryCondition
+from .player_logic import LingoPlayerLogic, PlayerLocation
+from .static_logic import PANELS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor
+
+if TYPE_CHECKING:
+ from . import LingoWorld
+
+
+def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, player: int,
+ player_logic: LingoPlayerLogic):
+ if door is None:
+ return True
+
+ return _lingo_can_open_door(state, room, room if door.room is None else door.room, door.door, player, player_logic)
+
+
+def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: LingoPlayerLogic):
+ fake_pilgrimage = [
+ ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"],
+ ["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"],
+ ["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"],
+ ["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"],
+ ["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"],
+ ["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"],
+ ["Outside The Agreeable", "Tenacious Entrance"]
+ ]
+ for entrance in fake_pilgrimage:
+ if not state.has(player_logic.ITEM_BY_DOOR[entrance[0]][entrance[1]], player):
+ return False
+
+ return True
+
+
+def lingo_can_use_location(state: CollectionState, location: PlayerLocation, room_name: str, world: "LingoWorld",
+ player_logic: LingoPlayerLogic):
+ for panel in location.panels:
+ panel_room = room_name if panel.room is None else panel.room
+ if not _lingo_can_solve_panel(state, room_name, panel_room, panel.panel, world, player_logic):
+ return False
+
+ return True
+
+
+def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"):
+ return state.has("Mastery Achievement", world.player, world.options.mastery_achievements.value)
+
+
+def _lingo_can_open_door(state: CollectionState, start_room: str, room: str, door: str, player: int,
+ player_logic: LingoPlayerLogic):
+ """
+ Determines whether a door can be opened
+ """
+ item_name = player_logic.ITEM_BY_DOOR[room][door]
+ if item_name in PROGRESSIVE_ITEMS:
+ progression = PROGRESSION_BY_ROOM[room][door]
+ return state.has(item_name, player, progression.index)
+
+ return state.has(item_name, player)
+
+
+def _lingo_can_solve_panel(state: CollectionState, start_room: str, room: str, panel: str, world: "LingoWorld",
+ player_logic: LingoPlayerLogic):
+ """
+ Determines whether a panel can be solved
+ """
+ if start_room != room and not state.has(f"{room} (Reached)", world.player):
+ return False
+
+ if room == "Second Room" and panel == "ANOTHER TRY" \
+ and world.options.victory_condition == VictoryCondition.option_level_2 \
+ and not state.has("Counting Panel Solved", world.player, world.options.level_2_requirement.value - 1):
+ return False
+
+ panel_object = PANELS_BY_ROOM[room][panel]
+ for req_room in panel_object.required_rooms:
+ if not state.has(f"{req_room} (Reached)", world.player):
+ return False
+
+ for req_door in panel_object.required_doors:
+ if not _lingo_can_open_door(state, start_room, room if req_door.room is None else req_door.room,
+ req_door.door, world.player, player_logic):
+ return False
+
+ for req_panel in panel_object.required_panels:
+ if not _lingo_can_solve_panel(state, start_room, room if req_panel.room is None else req_panel.room,
+ req_panel.panel, world, player_logic):
+ return False
+
+ if len(panel_object.colors) > 0 and world.options.shuffle_colors:
+ for color in panel_object.colors:
+ if not state.has(color.capitalize(), world.player):
+ return False
+
+ return True
+
+
+def make_location_lambda(location: PlayerLocation, room_name: str, world: "LingoWorld", player_logic: LingoPlayerLogic):
+ if location.name == player_logic.MASTERY_LOCATION:
+ return lambda state: lingo_can_use_mastery_location(state, world)
+
+ return lambda state: lingo_can_use_location(state, location, room_name, world, player_logic)
diff --git a/worlds/lingo/static_logic.py b/worlds/lingo/static_logic.py
new file mode 100644
index 000000000000..d122169c5d03
--- /dev/null
+++ b/worlds/lingo/static_logic.py
@@ -0,0 +1,544 @@
+from typing import Dict, List, NamedTuple, Optional, Set
+
+import yaml
+
+
+class RoomAndDoor(NamedTuple):
+ room: Optional[str]
+ door: str
+
+
+class RoomAndPanel(NamedTuple):
+ room: Optional[str]
+ panel: str
+
+
+class RoomEntrance(NamedTuple):
+ room: str # source room
+ door: Optional[RoomAndDoor]
+ painting: bool
+
+
+class Room(NamedTuple):
+ name: str
+ entrances: List[RoomEntrance]
+
+
+class Door(NamedTuple):
+ name: str
+ item_name: str
+ location_name: Optional[str]
+ panels: Optional[List[RoomAndPanel]]
+ skip_location: bool
+ skip_item: bool
+ door_ids: List[str]
+ painting_ids: List[str]
+ event: bool
+ group: Optional[str]
+ include_reduce: bool
+ junk_item: bool
+
+
+class Panel(NamedTuple):
+ required_rooms: List[str]
+ required_doors: List[RoomAndDoor]
+ required_panels: List[RoomAndPanel]
+ colors: List[str]
+ check: bool
+ event: bool
+ internal_ids: List[str]
+ exclude_reduce: bool
+ achievement: bool
+ non_counting: bool
+
+
+class Painting(NamedTuple):
+ id: str
+ room: str
+ enter_only: bool
+ exit_only: bool
+ orientation: str
+ required: bool
+ required_when_no_doors: bool
+ required_door: Optional[RoomAndDoor]
+ disable: bool
+ move: bool
+
+
+class Progression(NamedTuple):
+ item_name: str
+ index: int
+
+
+ROOMS: Dict[str, Room] = {}
+PANELS: Dict[str, Panel] = {}
+DOORS: Dict[str, Door] = {}
+PAINTINGS: Dict[str, Painting] = {}
+
+ALL_ROOMS: List[Room] = []
+DOORS_BY_ROOM: Dict[str, Dict[str, Door]] = {}
+PANELS_BY_ROOM: Dict[str, Dict[str, Panel]] = {}
+PAINTINGS_BY_ROOM: Dict[str, List[Painting]] = {}
+
+PROGRESSIVE_ITEMS: List[str] = []
+PROGRESSION_BY_ROOM: Dict[str, Dict[str, Progression]] = {}
+
+PAINTING_ENTRANCES: int = 0
+PAINTING_EXIT_ROOMS: Set[str] = set()
+PAINTING_EXITS: int = 0
+REQUIRED_PAINTING_ROOMS: List[str] = []
+REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS: List[str] = []
+
+SPECIAL_ITEM_IDS: Dict[str, int] = {}
+PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
+DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
+DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {}
+DOOR_GROUP_ITEM_IDS: Dict[str, int] = {}
+PROGRESSIVE_ITEM_IDS: Dict[str, int] = {}
+
+
+def load_static_data():
+ global PAINTING_EXITS, SPECIAL_ITEM_IDS, PANEL_LOCATION_IDS, DOOR_LOCATION_IDS, DOOR_ITEM_IDS, \
+ DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS
+
+ try:
+ from importlib.resources import files
+ except ImportError:
+ from importlib_resources import files
+
+ # Load in all item and location IDs. These are broken up into groups based on the type of item/location.
+ with files("worlds.lingo").joinpath("ids.yaml").open() as file:
+ config = yaml.load(file, Loader=yaml.Loader)
+
+ if "special_items" in config:
+ for item_name, item_id in config["special_items"].items():
+ SPECIAL_ITEM_IDS[item_name] = item_id
+
+ if "panels" in config:
+ for room_name in config["panels"].keys():
+ PANEL_LOCATION_IDS[room_name] = {}
+
+ for panel_name, location_id in config["panels"][room_name].items():
+ PANEL_LOCATION_IDS[room_name][panel_name] = location_id
+
+ if "doors" in config:
+ for room_name in config["doors"].keys():
+ DOOR_LOCATION_IDS[room_name] = {}
+ DOOR_ITEM_IDS[room_name] = {}
+
+ for door_name, door_data in config["doors"][room_name].items():
+ if "location" in door_data:
+ DOOR_LOCATION_IDS[room_name][door_name] = door_data["location"]
+
+ if "item" in door_data:
+ DOOR_ITEM_IDS[room_name][door_name] = door_data["item"]
+
+ if "door_groups" in config:
+ for item_name, item_id in config["door_groups"].items():
+ DOOR_GROUP_ITEM_IDS[item_name] = item_id
+
+ if "progression" in config:
+ for item_name, item_id in config["progression"].items():
+ PROGRESSIVE_ITEM_IDS[item_name] = item_id
+
+ # Process the main world file.
+ with files("worlds.lingo").joinpath("LL1.yaml").open() as file:
+ config = yaml.load(file, Loader=yaml.Loader)
+
+ for room_name, room_data in config.items():
+ process_room(room_name, room_data)
+
+ PAINTING_EXITS = len(PAINTING_EXIT_ROOMS)
+
+
+def get_special_item_id(name: str):
+ if name not in SPECIAL_ITEM_IDS:
+ raise Exception(f"Item ID for special item {name} not found in ids.yaml.")
+
+ return SPECIAL_ITEM_IDS[name]
+
+
+def get_panel_location_id(room: str, name: str):
+ if room not in PANEL_LOCATION_IDS or name not in PANEL_LOCATION_IDS[room]:
+ raise Exception(f"Location ID for panel {room} - {name} not found in ids.yaml.")
+
+ return PANEL_LOCATION_IDS[room][name]
+
+
+def get_door_location_id(room: str, name: str):
+ if room not in DOOR_LOCATION_IDS or name not in DOOR_LOCATION_IDS[room]:
+ raise Exception(f"Location ID for door {room} - {name} not found in ids.yaml.")
+
+ return DOOR_LOCATION_IDS[room][name]
+
+
+def get_door_item_id(room: str, name: str):
+ if room not in DOOR_ITEM_IDS or name not in DOOR_ITEM_IDS[room]:
+ raise Exception(f"Item ID for door {room} - {name} not found in ids.yaml.")
+
+ return DOOR_ITEM_IDS[room][name]
+
+
+def get_door_group_item_id(name: str):
+ if name not in DOOR_GROUP_ITEM_IDS:
+ raise Exception(f"Item ID for door group {name} not found in ids.yaml.")
+
+ return DOOR_GROUP_ITEM_IDS[name]
+
+
+def get_progressive_item_id(name: str):
+ if name not in PROGRESSIVE_ITEM_IDS:
+ raise Exception(f"Item ID for progressive item {name} not found in ids.yaml.")
+
+ return PROGRESSIVE_ITEM_IDS[name]
+
+
+def process_entrance(source_room, doors, room_obj):
+ global PAINTING_ENTRANCES, PAINTING_EXIT_ROOMS
+
+ # If the value of an entrance is just True, that means that the entrance is always accessible.
+ if doors is True:
+ room_obj.entrances.append(RoomEntrance(source_room, None, False))
+ elif isinstance(doors, dict):
+ # If the value of an entrance is a dictionary, that means the entrance requires a door to be accessible, is a
+ # painting-based entrance, or both.
+ if "painting" in doors and "door" not in doors:
+ PAINTING_EXIT_ROOMS.add(room_obj.name)
+ PAINTING_ENTRANCES += 1
+
+ room_obj.entrances.append(RoomEntrance(source_room, None, True))
+ else:
+ if "painting" in doors and doors["painting"]:
+ PAINTING_EXIT_ROOMS.add(room_obj.name)
+ PAINTING_ENTRANCES += 1
+
+ room_obj.entrances.append(RoomEntrance(source_room, RoomAndDoor(
+ doors["room"] if "room" in doors else None,
+ doors["door"]
+ ), doors["painting"] if "painting" in doors else False))
+ else:
+ # If the value of an entrance is a list, then there are multiple possible doors that can give access to the
+ # entrance.
+ for door in doors:
+ if "painting" in door and door["painting"]:
+ PAINTING_EXIT_ROOMS.add(room_obj.name)
+ PAINTING_ENTRANCES += 1
+
+ room_obj.entrances.append(RoomEntrance(source_room, RoomAndDoor(
+ door["room"] if "room" in door else None,
+ door["door"]
+ ), door["painting"] if "painting" in door else False))
+
+
+def process_panel(room_name, panel_name, panel_data):
+ global PANELS, PANELS_BY_ROOM
+
+ full_name = f"{room_name} - {panel_name}"
+
+ # required_room can either be a single room or a list of rooms.
+ if "required_room" in panel_data:
+ if isinstance(panel_data["required_room"], list):
+ required_rooms = panel_data["required_room"]
+ else:
+ required_rooms = [panel_data["required_room"]]
+ else:
+ required_rooms = []
+
+ # required_door can either be a single door or a list of doors. For convenience, the room key for each door does not
+ # need to be specified if the door is in this room.
+ required_doors = list()
+ if "required_door" in panel_data:
+ if isinstance(panel_data["required_door"], dict):
+ door = panel_data["required_door"]
+ required_doors.append(RoomAndDoor(
+ door["room"] if "room" in door else None,
+ door["door"]
+ ))
+ else:
+ for door in panel_data["required_door"]:
+ required_doors.append(RoomAndDoor(
+ door["room"] if "room" in door else None,
+ door["door"]
+ ))
+
+ # required_panel can either be a single panel or a list of panels. For convenience, the room key for each panel does
+ # not need to be specified if the panel is in this room.
+ required_panels = list()
+ if "required_panel" in panel_data:
+ if isinstance(panel_data["required_panel"], dict):
+ other_panel = panel_data["required_panel"]
+ required_panels.append(RoomAndPanel(
+ other_panel["room"] if "room" in other_panel else None,
+ other_panel["panel"]
+ ))
+ else:
+ for other_panel in panel_data["required_panel"]:
+ required_panels.append(RoomAndPanel(
+ other_panel["room"] if "room" in other_panel else None,
+ other_panel["panel"]
+ ))
+
+ # colors can either be a single color or a list of colors.
+ if "colors" in panel_data:
+ if isinstance(panel_data["colors"], list):
+ colors = panel_data["colors"]
+ else:
+ colors = [panel_data["colors"]]
+ else:
+ colors = []
+
+ if "check" in panel_data:
+ check = panel_data["check"]
+ else:
+ check = False
+
+ if "event" in panel_data:
+ event = panel_data["event"]
+ else:
+ event = False
+
+ if "achievement" in panel_data:
+ achievement = True
+ else:
+ achievement = False
+
+ if "exclude_reduce" in panel_data:
+ exclude_reduce = panel_data["exclude_reduce"]
+ else:
+ exclude_reduce = False
+
+ if "non_counting" in panel_data:
+ non_counting = panel_data["non_counting"]
+ else:
+ non_counting = False
+
+ if "id" in panel_data:
+ if isinstance(panel_data["id"], list):
+ internal_ids = panel_data["id"]
+ else:
+ internal_ids = [panel_data["id"]]
+ else:
+ internal_ids = []
+
+ panel_obj = Panel(required_rooms, required_doors, required_panels, colors, check, event, internal_ids,
+ exclude_reduce, achievement, non_counting)
+ PANELS[full_name] = panel_obj
+ PANELS_BY_ROOM[room_name][panel_name] = panel_obj
+
+
+def process_door(room_name, door_name, door_data):
+ global DOORS, DOORS_BY_ROOM
+
+ # The item name associated with a door can be explicitly specified in the configuration. If it is not, it is
+ # generated from the room and door name.
+ if "item_name" in door_data:
+ item_name = door_data["item_name"]
+ else:
+ item_name = f"{room_name} - {door_name}"
+
+ if "skip_location" in door_data:
+ skip_location = door_data["skip_location"]
+ else:
+ skip_location = False
+
+ if "skip_item" in door_data:
+ skip_item = door_data["skip_item"]
+ else:
+ skip_item = False
+
+ if "event" in door_data:
+ event = door_data["event"]
+ else:
+ event = False
+
+ if "include_reduce" in door_data:
+ include_reduce = door_data["include_reduce"]
+ else:
+ include_reduce = False
+
+ if "junk_item" in door_data:
+ junk_item = door_data["junk_item"]
+ else:
+ junk_item = False
+
+ if "group" in door_data:
+ group = door_data["group"]
+ else:
+ group = None
+
+ # panels is a list of panels. Each panel can either be a simple string (the name of a panel in the current room) or
+ # a dictionary specifying a panel in a different room.
+ if "panels" in door_data:
+ panels = list()
+ for panel in door_data["panels"]:
+ if isinstance(panel, dict):
+ panels.append(RoomAndPanel(panel["room"], panel["panel"]))
+ else:
+ panels.append(RoomAndPanel(None, panel))
+ else:
+ skip_location = True
+ panels = None
+
+ # The location name associated with a door can be explicitly specified in the configuration. If it is not, then the
+ # name is generated using a combination of all of the panels that would ordinarily open the door. This can get quite
+ # messy if there are a lot of panels, especially if panels from multiple rooms are involved, so in these cases it
+ # would be better to specify a name.
+ if "location_name" in door_data:
+ location_name = door_data["location_name"]
+ elif skip_location is False:
+ panel_per_room = dict()
+ for panel in panels:
+ panel_room_name = room_name if panel.room is None else panel.room
+ panel_per_room.setdefault(panel_room_name, []).append(panel.panel)
+
+ room_strs = list()
+ for door_room_str, door_panels_str in panel_per_room.items():
+ room_strs.append(door_room_str + " - " + ", ".join(door_panels_str))
+
+ location_name = " and ".join(room_strs)
+ else:
+ location_name = None
+
+ # The id field can be a single item, or a list of door IDs, in the event that the item for this logical door should
+ # open more than one actual in-game door.
+ if "id" in door_data:
+ if isinstance(door_data["id"], list):
+ door_ids = door_data["id"]
+ else:
+ door_ids = [door_data["id"]]
+ else:
+ door_ids = []
+
+ # The painting_id field can be a single item, or a list of painting IDs, in the event that the item for this logical
+ # door should move more than one actual in-game painting.
+ if "painting_id" in door_data:
+ if isinstance(door_data["painting_id"], list):
+ painting_ids = door_data["painting_id"]
+ else:
+ painting_ids = [door_data["painting_id"]]
+ else:
+ painting_ids = []
+
+ door_obj = Door(door_name, item_name, location_name, panels, skip_location, skip_item, door_ids,
+ painting_ids, event, group, include_reduce, junk_item)
+
+ DOORS[door_obj.item_name] = door_obj
+ DOORS_BY_ROOM[room_name][door_name] = door_obj
+
+
+def process_painting(room_name, painting_data):
+ global PAINTINGS, PAINTINGS_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
+
+ # Read in information about this painting and store it in an object.
+ painting_id = painting_data["id"]
+
+ if "orientation" in painting_data:
+ orientation = painting_data["orientation"]
+ else:
+ orientation = ""
+
+ if "disable" in painting_data:
+ disable_painting = painting_data["disable"]
+ else:
+ disable_painting = False
+
+ if "required" in painting_data:
+ required_painting = painting_data["required"]
+ if required_painting:
+ REQUIRED_PAINTING_ROOMS.append(room_name)
+ else:
+ required_painting = False
+
+ if "move" in painting_data:
+ move_painting = painting_data["move"]
+ else:
+ move_painting = False
+
+ if "required_when_no_doors" in painting_data:
+ rwnd = painting_data["required_when_no_doors"]
+ if rwnd:
+ REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS.append(room_name)
+ else:
+ rwnd = False
+
+ if "exit_only" in painting_data:
+ exit_only = painting_data["exit_only"]
+ else:
+ exit_only = False
+
+ if "enter_only" in painting_data:
+ enter_only = painting_data["enter_only"]
+ else:
+ enter_only = False
+
+ required_door = None
+ if "required_door" in painting_data:
+ door = painting_data["required_door"]
+ required_door = RoomAndDoor(
+ door["room"] if "room" in door else room_name,
+ door["door"]
+ )
+
+ painting_obj = Painting(painting_id, room_name, enter_only, exit_only, orientation,
+ required_painting, rwnd, required_door, disable_painting, move_painting)
+ PAINTINGS[painting_id] = painting_obj
+ PAINTINGS_BY_ROOM[room_name].append(painting_obj)
+
+
+def process_progression(room_name, progression_name, progression_doors):
+ global PROGRESSIVE_ITEMS, PROGRESSION_BY_ROOM
+
+ # Progressive items are configured as a list of doors.
+ PROGRESSIVE_ITEMS.append(progression_name)
+
+ progression_index = 1
+ for door in progression_doors:
+ if isinstance(door, Dict):
+ door_room = door["room"]
+ door_door = door["door"]
+ else:
+ door_room = room_name
+ door_door = door
+
+ room_progressions = PROGRESSION_BY_ROOM.setdefault(door_room, {})
+ room_progressions[door_door] = Progression(progression_name, progression_index)
+ progression_index += 1
+
+
+def process_room(room_name, room_data):
+ global ROOMS, ALL_ROOMS
+
+ room_obj = Room(room_name, [])
+
+ if "entrances" in room_data:
+ for source_room, doors in room_data["entrances"].items():
+ process_entrance(source_room, doors, room_obj)
+
+ if "panels" in room_data:
+ PANELS_BY_ROOM[room_name] = dict()
+
+ for panel_name, panel_data in room_data["panels"].items():
+ process_panel(room_name, panel_name, panel_data)
+
+ if "doors" in room_data:
+ DOORS_BY_ROOM[room_name] = dict()
+
+ for door_name, door_data in room_data["doors"].items():
+ process_door(room_name, door_name, door_data)
+
+ if "paintings" in room_data:
+ PAINTINGS_BY_ROOM[room_name] = []
+
+ for painting_data in room_data["paintings"]:
+ process_painting(room_name, painting_data)
+
+ if "progression" in room_data:
+ for progression_name, progression_doors in room_data["progression"].items():
+ process_progression(room_name, progression_name, progression_doors)
+
+ ROOMS[room_name] = room_obj
+ ALL_ROOMS.append(room_obj)
+
+
+# Initialize the static data at module scope.
+load_static_data()
diff --git a/worlds/lingo/test/TestDoors.py b/worlds/lingo/test/TestDoors.py
new file mode 100644
index 000000000000..5dc989af5989
--- /dev/null
+++ b/worlds/lingo/test/TestDoors.py
@@ -0,0 +1,89 @@
+from . import LingoTestBase
+
+
+class TestRequiredRoomLogic(LingoTestBase):
+ options = {
+ "shuffle_doors": "complex"
+ }
+
+ def test_pilgrim_first(self) -> None:
+ self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
+ self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
+
+ self.collect_by_name("Pilgrim Room - Sun Painting")
+ self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
+ self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
+
+ self.collect_by_name("Pilgrim Room - Shortcut to The Seeker")
+ self.assertTrue(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
+ self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
+
+ self.collect_by_name("Starting Room - Back Right Door")
+ self.assertTrue(self.can_reach_location("The Seeker - Achievement"))
+
+ def test_hidden_first(self) -> None:
+ self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
+ self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
+
+ self.collect_by_name("Starting Room - Back Right Door")
+ self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
+ self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
+
+ self.collect_by_name("Pilgrim Room - Shortcut to The Seeker")
+ self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
+ self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
+
+ self.collect_by_name("Pilgrim Room - Sun Painting")
+ self.assertTrue(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
+ self.assertTrue(self.can_reach_location("The Seeker - Achievement"))
+
+
+class TestRequiredDoorLogic(LingoTestBase):
+ options = {
+ "shuffle_doors": "complex"
+ }
+
+ def test_through_rhyme(self) -> None:
+ self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
+
+ self.collect_by_name("Starting Room - Rhyme Room Entrance")
+ self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
+
+ self.collect_by_name("Rhyme Room (Looped Square) - Door to Circle")
+ self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
+
+ def test_through_hidden(self) -> None:
+ self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
+
+ self.collect_by_name("Starting Room - Rhyme Room Entrance")
+ self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
+
+ self.collect_by_name("Starting Room - Back Right Door")
+ self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
+
+ self.collect_by_name("Hidden Room - Rhyme Room Entrance")
+ self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
+
+
+class TestSimpleDoors(LingoTestBase):
+ options = {
+ "shuffle_doors": "simple"
+ }
+
+ def test_requirement(self):
+ self.assertFalse(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+
+ self.collect_by_name("Rhyme Room Doors")
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+
diff --git a/worlds/lingo/test/TestMastery.py b/worlds/lingo/test/TestMastery.py
new file mode 100644
index 000000000000..3fb3c95a0208
--- /dev/null
+++ b/worlds/lingo/test/TestMastery.py
@@ -0,0 +1,39 @@
+from . import LingoTestBase
+
+
+class TestMasteryWhenVictoryIsTheEnd(LingoTestBase):
+ options = {
+ "mastery_achievements": "22",
+ "victory_condition": "the_end",
+ "shuffle_colors": "true"
+ }
+
+ def test_requirement(self):
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect_by_name(["Red", "Blue", "Black", "Purple", "Orange"])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+ self.assertTrue(self.can_reach_location("The End (Solved)"))
+ self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - THE MASTER"))
+
+ self.collect_by_name(["Green", "Brown", "Yellow"])
+ self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - THE MASTER"))
+
+
+class TestMasteryWhenVictoryIsTheMaster(LingoTestBase):
+ options = {
+ "mastery_achievements": "24",
+ "victory_condition": "the_master",
+ "shuffle_colors": "true"
+ }
+
+ def test_requirement(self):
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect_by_name(["Red", "Blue", "Black", "Purple", "Orange"])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+ self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - THE END"))
+ self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements"))
+
+ self.collect_by_name(["Green", "Gray", "Brown", "Yellow"])
+ self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements"))
\ No newline at end of file
diff --git a/worlds/lingo/test/TestOptions.py b/worlds/lingo/test/TestOptions.py
new file mode 100644
index 000000000000..176967786243
--- /dev/null
+++ b/worlds/lingo/test/TestOptions.py
@@ -0,0 +1,31 @@
+from . import LingoTestBase
+
+
+class TestMultiShuffleOptions(LingoTestBase):
+ options = {
+ "shuffle_doors": "complex",
+ "progressive_orange_tower": "true",
+ "shuffle_colors": "true",
+ "shuffle_paintings": "true",
+ "early_color_hallways": "true"
+ }
+
+
+class TestPanelsanity(LingoTestBase):
+ options = {
+ "shuffle_doors": "complex",
+ "progressive_orange_tower": "true",
+ "location_checks": "insanity",
+ "shuffle_colors": "true"
+ }
+
+
+class TestAllPanelHunt(LingoTestBase):
+ options = {
+ "shuffle_doors": "complex",
+ "progressive_orange_tower": "true",
+ "shuffle_colors": "true",
+ "victory_condition": "level_2",
+ "level_2_requirement": "800",
+ "early_color_hallways": "true"
+ }
diff --git a/worlds/lingo/test/TestOrangeTower.py b/worlds/lingo/test/TestOrangeTower.py
new file mode 100644
index 000000000000..7b0c3bb52518
--- /dev/null
+++ b/worlds/lingo/test/TestOrangeTower.py
@@ -0,0 +1,175 @@
+from . import LingoTestBase
+
+
+class TestProgressiveOrangeTower(LingoTestBase):
+ options = {
+ "shuffle_doors": "complex",
+ "progressive_orange_tower": "true"
+ }
+
+ def test_from_welcome_back(self) -> None:
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect_by_name("Welcome Back Area - Shortcut to Starting Room")
+ self.collect_by_name("Orange Tower Fifth Floor - Welcome Back")
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ progressive_tower = self.get_items_by_name("Progressive Orange Tower")
+
+ self.collect(progressive_tower[0])
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[1])
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[2])
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[3])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[4])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[5])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ def test_from_hub_room(self) -> None:
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect_by_name("Second Room - Exit Door")
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect_by_name("Orange Tower First Floor - Shortcut to Hub Room")
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ progressive_tower = self.get_items_by_name("Progressive Orange Tower")
+
+ self.collect(progressive_tower[0])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.remove(self.get_item_by_name("Orange Tower First Floor - Shortcut to Hub Room"))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[1])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[2])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[3])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[4])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[5])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
diff --git a/worlds/lingo/test/TestProgressive.py b/worlds/lingo/test/TestProgressive.py
new file mode 100644
index 000000000000..026971c45d65
--- /dev/null
+++ b/worlds/lingo/test/TestProgressive.py
@@ -0,0 +1,191 @@
+from . import LingoTestBase
+
+
+class TestComplexProgressiveHallwayRoom(LingoTestBase):
+ options = {
+ "shuffle_doors": "complex"
+ }
+
+ def test_item(self):
+ self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+ self.collect_by_name(["Second Room - Exit Door", "The Tenacious - Shortcut to Hub Room",
+ "Outside The Agreeable - Tenacious Entrance"])
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+ progressive_hallway_room = self.get_items_by_name("Progressive Hallway Room")
+
+ self.collect(progressive_hallway_room[0])
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+ self.collect(progressive_hallway_room[1])
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+ self.collect(progressive_hallway_room[2])
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+ self.collect(progressive_hallway_room[3])
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+
+class TestSimpleHallwayRoom(LingoTestBase):
+ options = {
+ "shuffle_doors": "simple"
+ }
+
+ def test_item(self):
+ self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+ self.collect_by_name(["Second Room - Exit Door", "Entrances to The Tenacious"])
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+ self.collect_by_name("Hallway Room Doors")
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+
+class TestProgressiveArtGallery(LingoTestBase):
+ options = {
+ "shuffle_doors": "complex"
+ }
+
+ def test_item(self):
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect_by_name(["Second Room - Exit Door", "Crossroads - Tower Entrance",
+ "Orange Tower Fourth Floor - Hot Crusts Door"])
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ progressive_gallery_room = self.get_items_by_name("Progressive Art Gallery")
+
+ self.collect(progressive_gallery_room[0])
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect(progressive_gallery_room[1])
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect(progressive_gallery_room[2])
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect(progressive_gallery_room[3])
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect(progressive_gallery_room[4])
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+
+class TestNoDoorsArtGallery(LingoTestBase):
+ options = {
+ "shuffle_doors": "none",
+ "shuffle_colors": "true"
+ }
+
+ def test_item(self):
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect_by_name("Yellow")
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect_by_name("Brown")
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect_by_name("Blue")
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect_by_name(["Orange", "Gray"])
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
diff --git a/worlds/lingo/test/__init__.py b/worlds/lingo/test/__init__.py
new file mode 100644
index 000000000000..ffbf9032b64a
--- /dev/null
+++ b/worlds/lingo/test/__init__.py
@@ -0,0 +1,13 @@
+from typing import ClassVar
+
+from test.bases import WorldTestBase
+from .. import LingoTestOptions
+
+
+class LingoTestBase(WorldTestBase):
+ game = "Lingo"
+ player: ClassVar[int] = 1
+
+ def world_setup(self, *args, **kwargs):
+ LingoTestOptions.disable_forced_good_item = True
+ super().world_setup(*args, **kwargs)
diff --git a/worlds/lingo/testing.py b/worlds/lingo/testing.py
new file mode 100644
index 000000000000..22fafea0fc6a
--- /dev/null
+++ b/worlds/lingo/testing.py
@@ -0,0 +1,2 @@
+class LingoTestOptions:
+ disable_forced_good_item: bool = False
diff --git a/worlds/lingo/utils/assign_ids.rb b/worlds/lingo/utils/assign_ids.rb
new file mode 100644
index 000000000000..9e1ce67bd2db
--- /dev/null
+++ b/worlds/lingo/utils/assign_ids.rb
@@ -0,0 +1,178 @@
+# This utility goes through the provided Lingo config and assigns item and
+# location IDs to entities that require them (such as doors and panels). These
+# IDs are output in a separate yaml file. If the output file already exists,
+# then it will be updated with any newly assigned IDs rather than overwritten.
+# In this event, all new IDs will be greater than any already existing IDs,
+# even if there are gaps in the ID space; this is to prevent collision when IDs
+# are retired.
+#
+# This utility should be run whenever logically new items or locations are
+# required. If an item or location is created that is logically equivalent to
+# one that used to exist, this utility should not be used, and instead the ID
+# file should be manually edited so that the old ID can be reused.
+
+require 'set'
+require 'yaml'
+
+configpath = ARGV[0]
+outputpath = ARGV[1]
+
+next_item_id = 444400
+next_location_id = 444400
+
+location_id_by_name = {}
+
+old_generated = YAML.load_file(outputpath)
+File.write(outputpath + ".old", old_generated.to_yaml)
+
+if old_generated.include? "special_items" then
+ old_generated["special_items"].each do |name, id|
+ if id >= next_item_id then
+ next_item_id = id + 1
+ end
+ end
+end
+if old_generated.include? "special_locations" then
+ old_generated["special_locations"].each do |name, id|
+ if id >= next_location_id then
+ next_location_id = id + 1
+ end
+ end
+end
+if old_generated.include? "panels" then
+ old_generated["panels"].each do |room, panels|
+ panels.each do |name, id|
+ if id >= next_location_id then
+ next_location_id = id + 1
+ end
+ location_name = "#{room} - #{name}"
+ location_id_by_name[location_name] = id
+ end
+ end
+end
+if old_generated.include? "doors" then
+ old_generated["doors"].each do |room, doors|
+ doors.each do |name, ids|
+ if ids.include? "location" then
+ if ids["location"] >= next_location_id then
+ next_location_id = ids["location"] + 1
+ end
+ end
+ if ids.include? "item" then
+ if ids["item"] >= next_item_id then
+ next_item_id = ids["item"] + 1
+ end
+ end
+ end
+ end
+end
+if old_generated.include? "door_groups" then
+ old_generated["door_groups"].each do |name, id|
+ if id >= next_item_id then
+ next_item_id = id + 1
+ end
+ end
+end
+if old_generated.include? "progression" then
+ old_generated["progression"].each do |name, id|
+ if id >= next_item_id then
+ next_item_id = id + 1
+ end
+ end
+end
+
+door_groups = Set[]
+
+config = YAML.load_file(configpath)
+config.each do |room_name, room_data|
+ if room_data.include? "panels"
+ room_data["panels"].each do |panel_name, panel|
+ unless old_generated.include? "panels" and old_generated["panels"].include? room_name and old_generated["panels"][room_name].include? panel_name then
+ old_generated["panels"] ||= {}
+ old_generated["panels"][room_name] ||= {}
+ old_generated["panels"][room_name][panel_name] = next_location_id
+
+ location_name = "#{room_name} - #{panel_name}"
+ location_id_by_name[location_name] = next_location_id
+
+ next_location_id += 1
+ end
+ end
+ end
+end
+
+config.each do |room_name, room_data|
+ if room_data.include? "doors"
+ room_data["doors"].each do |door_name, door|
+ if door.include? "event" and door["event"] then
+ next
+ end
+
+ unless door.include? "skip_item" and door["skip_item"] then
+ unless old_generated.include? "doors" and old_generated["doors"].include? room_name and old_generated["doors"][room_name].include? door_name and old_generated["doors"][room_name][door_name].include? "item" then
+ old_generated["doors"] ||= {}
+ old_generated["doors"][room_name] ||= {}
+ old_generated["doors"][room_name][door_name] ||= {}
+ old_generated["doors"][room_name][door_name]["item"] = next_item_id
+
+ next_item_id += 1
+ end
+
+ if door.include? "group" and not door_groups.include? door["group"] then
+ door_groups.add(door["group"])
+
+ unless old_generated.include? "door_groups" and old_generated["door_groups"].include? door["group"] then
+ old_generated["door_groups"] ||= {}
+ old_generated["door_groups"][door["group"]] = next_item_id
+
+ next_item_id += 1
+ end
+ end
+ end
+
+ unless door.include? "skip_location" and door["skip_location"] then
+ location_name = ""
+ if door.include? "location_name" then
+ location_name = door["location_name"]
+ elsif door.include? "panels" then
+ location_name = door["panels"].map do |panel|
+ if panel.kind_of? Hash then
+ panel
+ else
+ {"room" => room_name, "panel" => panel}
+ end
+ end.sort_by {|panel| panel["room"]}.chunk {|panel| panel["room"]}.map do |room_panels|
+ room_panels[0] + " - " + room_panels[1].map{|panel| panel["panel"]}.join(", ")
+ end.join(" and ")
+ end
+
+ if location_id_by_name.has_key? location_name then
+ old_generated["doors"] ||= {}
+ old_generated["doors"][room_name] ||= {}
+ old_generated["doors"][room_name][door_name] ||= {}
+ old_generated["doors"][room_name][door_name]["location"] = location_id_by_name[location_name]
+ elsif not (old_generated.include? "doors" and old_generated["doors"].include? room_name and old_generated["doors"][room_name].include? door_name and old_generated["doors"][room_name][door_name].include? "location") then
+ old_generated["doors"] ||= {}
+ old_generated["doors"][room_name] ||= {}
+ old_generated["doors"][room_name][door_name] ||= {}
+ old_generated["doors"][room_name][door_name]["location"] = next_location_id
+
+ next_location_id += 1
+ end
+ end
+ end
+ end
+
+ if room_data.include? "progression"
+ room_data["progression"].each do |progression_name, pdata|
+ unless old_generated.include? "progression" and old_generated["progression"].include? progression_name then
+ old_generated["progression"] ||= {}
+ old_generated["progression"][progression_name] = next_item_id
+
+ next_item_id += 1
+ end
+ end
+ end
+end
+
+File.write(outputpath, old_generated.to_yaml)
diff --git a/worlds/lingo/utils/validate_config.rb b/worlds/lingo/utils/validate_config.rb
new file mode 100644
index 000000000000..ed2e9058f9ad
--- /dev/null
+++ b/worlds/lingo/utils/validate_config.rb
@@ -0,0 +1,329 @@
+# Script to validate a level config file. This checks that the names used within
+# the file are consistent. It also checks that the panel and door IDs mentioned
+# all exist in the map file.
+#
+# Usage: validate_config.rb [config file] [map file]
+
+require 'set'
+require 'yaml'
+
+configpath = ARGV[0]
+mappath = ARGV[1]
+
+panels = Set["Countdown Panels/Panel_1234567890_wanderlust"]
+doors = Set["Naps Room Doors/Door_hider_new1", "Tower Room Area Doors/Door_wanderer_entrance"]
+paintings = Set[]
+
+File.readlines(mappath).each do |line|
+ line.match(/node name=\"(.*)\" parent=\"Panels\/(.*)\" instance/) do |m|
+ panels.add(m[2] + "/" + m[1])
+ end
+ line.match(/node name=\"(.*)\" parent=\"Doors\/(.*)\" instance/) do |m|
+ doors.add(m[2] + "/" + m[1])
+ end
+ line.match(/node name=\"(.*)\" parent=\"Decorations\/Paintings\" instance/) do |m|
+ paintings.add(m[1])
+ end
+ line.match(/node name=\"(.*)\" parent=\"Decorations\/EndPanel\" instance/) do |m|
+ panels.add("EndPanel/" + m[1])
+ end
+end
+
+configured_rooms = Set["Menu"]
+configured_doors = Set[]
+configured_panels = Set[]
+
+mentioned_rooms = Set[]
+mentioned_doors = Set[]
+mentioned_panels = Set[]
+
+door_groups = {}
+
+directives = Set["entrances", "panels", "doors", "paintings", "progression"]
+panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting"]
+door_directives = Set["id", "painting_id", "panels", "item_name", "location_name", "skip_location", "skip_item", "group", "include_reduce", "junk_item", "event"]
+painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move"]
+
+non_counting = 0
+
+config = YAML.load_file(configpath)
+config.each do |room_name, room|
+ configured_rooms.add(room_name)
+
+ used_directives = Set[]
+ room.each_key do |key|
+ used_directives.add(key)
+ end
+ diff_directives = used_directives - directives
+ unless diff_directives.empty? then
+ puts("#{room_name} has the following invalid top-level directives: #{diff_directives.to_s}")
+ end
+
+ (room["entrances"] || {}).each do |source_room, entrance|
+ mentioned_rooms.add(source_room)
+
+ entrances = []
+ if entrance.kind_of? Hash
+ if entrance.keys() != ["painting"] then
+ entrances = [entrance]
+ end
+ elsif entrance.kind_of? Array
+ entrances = entrance
+ end
+
+ entrances.each do |e|
+ entrance_room = e.include?("room") ? e["room"] : room_name
+ mentioned_rooms.add(entrance_room)
+ mentioned_doors.add(entrance_room + " - " + e["door"])
+ end
+ end
+
+ (room["panels"] || {}).each do |panel_name, panel|
+ unless panel_name.kind_of? String then
+ puts "#{room_name} has an invalid panel name"
+ end
+
+ configured_panels.add(room_name + " - " + panel_name)
+
+ if panel.include?("id")
+ panel_ids = []
+ if panel["id"].kind_of? Array
+ panel_ids = panel["id"]
+ else
+ panel_ids = [panel["id"]]
+ end
+
+ panel_ids.each do |panel_id|
+ unless panels.include? panel_id then
+ puts "#{room_name} - #{panel_name} :::: Invalid Panel ID #{panel_id}"
+ end
+ end
+ else
+ puts "#{room_name} - #{panel_name} :::: Panel is missing an ID"
+ end
+
+ if panel.include?("required_room")
+ required_rooms = []
+ if panel["required_room"].kind_of? Array
+ required_rooms = panel["required_room"]
+ else
+ required_rooms = [panel["required_room"]]
+ end
+
+ required_rooms.each do |required_room|
+ mentioned_rooms.add(required_room)
+ end
+ end
+
+ if panel.include?("required_door")
+ required_doors = []
+ if panel["required_door"].kind_of? Array
+ required_doors = panel["required_door"]
+ else
+ required_doors = [panel["required_door"]]
+ end
+
+ required_doors.each do |required_door|
+ other_room = required_door.include?("room") ? required_door["room"] : room_name
+ mentioned_rooms.add(other_room)
+ mentioned_doors.add("#{other_room} - #{required_door["door"]}")
+ end
+ end
+
+ if panel.include?("required_panel")
+ required_panels = []
+ if panel["required_panel"].kind_of? Array
+ required_panels = panel["required_panel"]
+ else
+ required_panels = [panel["required_panel"]]
+ end
+
+ required_panels.each do |required_panel|
+ other_room = required_panel.include?("room") ? required_panel["room"] : room_name
+ mentioned_rooms.add(other_room)
+ mentioned_panels.add("#{other_room} - #{required_panel["panel"]}")
+ end
+ end
+
+ unless panel.include?("tag") then
+ puts "#{room_name} - #{panel_name} :::: Panel is missing a tag"
+ end
+
+ if panel.include?("non_counting") then
+ non_counting += 1
+ end
+
+ bad_subdirectives = []
+ panel.keys.each do |key|
+ unless panel_directives.include?(key) then
+ bad_subdirectives << key
+ end
+ end
+ unless bad_subdirectives.empty? then
+ puts "#{room_name} - #{panel_name} :::: Panel has the following invalid subdirectives: #{bad_subdirectives.join(", ")}"
+ end
+ end
+
+ (room["doors"] || {}).each do |door_name, door|
+ configured_doors.add("#{room_name} - #{door_name}")
+
+ if door.include?("id")
+ door_ids = []
+ if door["id"].kind_of? Array
+ door_ids = door["id"]
+ else
+ door_ids = [door["id"]]
+ end
+
+ door_ids.each do |door_id|
+ unless doors.include? door_id then
+ puts "#{room_name} - #{door_name} :::: Invalid Door ID #{door_id}"
+ end
+ end
+ end
+
+ if door.include?("painting_id")
+ painting_ids = []
+ if door["painting_id"].kind_of? Array
+ painting_ids = door["painting_id"]
+ else
+ painting_ids = [door["painting_id"]]
+ end
+
+ painting_ids.each do |painting_id|
+ unless paintings.include? painting_id then
+ puts "#{room_name} - #{door_name} :::: Invalid Painting ID #{painting_id}"
+ end
+ end
+ end
+
+ if not door.include?("id") and not door.include?("painting_id") and not door["skip_item"] and not door["event"] then
+ puts "#{room_name} - #{door_name} :::: Should be marked skip_item or event if there are no doors or paintings"
+ end
+
+ if door.include?("panels")
+ door["panels"].each do |panel|
+ if panel.kind_of? Hash then
+ other_room = panel.include?("room") ? panel["room"] : room_name
+ mentioned_panels.add("#{other_room} - #{panel["panel"]}")
+ else
+ other_room = panel.include?("room") ? panel["room"] : room_name
+ mentioned_panels.add("#{room_name} - #{panel}")
+ end
+ end
+ elsif not door["skip_location"]
+ puts "#{room_name} - #{door_name} :::: Should be marked skip_location if there are no panels"
+ end
+
+ if door.include?("group")
+ door_groups[door["group"]] ||= 0
+ door_groups[door["group"]] += 1
+ end
+
+ bad_subdirectives = []
+ door.keys.each do |key|
+ unless door_directives.include?(key) then
+ bad_subdirectives << key
+ end
+ end
+ unless bad_subdirectives.empty? then
+ puts "#{room_name} - #{door_name} :::: Door has the following invalid subdirectives: #{bad_subdirectives.join(", ")}"
+ end
+ end
+
+ (room["paintings"] || []).each do |painting|
+ if painting.include?("id") and painting["id"].kind_of? String then
+ unless paintings.include? painting["id"] then
+ puts "#{room_name} :::: Invalid Painting ID #{painting["id"]}"
+ end
+ else
+ puts "#{room_name} :::: Painting is missing an ID"
+ end
+
+ if painting["disable"] then
+ # We're good.
+ next
+ end
+
+ if painting.include?("orientation") then
+ unless ["north", "south", "east", "west"].include? painting["orientation"] then
+ puts "#{room_name} - #{painting["id"] || "painting"} :::: Invalid orientation #{painting["orientation"]}"
+ end
+ else
+ puts "#{room_name} :::: Painting is missing an orientation"
+ end
+
+ if painting.include?("required_door")
+ other_room = painting["required_door"].include?("room") ? painting["required_door"]["room"] : room_name
+ mentioned_doors.add("#{other_room} - #{painting["required_door"]["door"]}")
+
+ unless painting["enter_only"] then
+ puts "#{room_name} - #{painting["id"] || "painting"} :::: Should be marked enter_only if there is a required_door"
+ end
+ end
+
+ bad_subdirectives = []
+ painting.keys.each do |key|
+ unless painting_directives.include?(key) then
+ bad_subdirectives << key
+ end
+ end
+ unless bad_subdirectives.empty? then
+ puts "#{room_name} - #{painting["id"] || "painting"} :::: Painting has the following invalid subdirectives: #{bad_subdirectives.join(", ")}"
+ end
+ end
+
+ (room["progression"] || {}).each do |progression_name, door_list|
+ door_list.each do |door|
+ if door.kind_of? Hash then
+ mentioned_doors.add("#{door["room"]} - #{door["door"]}")
+ else
+ mentioned_doors.add("#{room_name} - #{door}")
+ end
+ end
+ end
+end
+
+errored_rooms = mentioned_rooms - configured_rooms
+unless errored_rooms.empty? then
+ puts "The folloring rooms are mentioned but do not exist: " + errored_rooms.to_s
+end
+
+errored_panels = mentioned_panels - configured_panels
+unless errored_panels.empty? then
+ puts "The folloring panels are mentioned but do not exist: " + errored_panels.to_s
+end
+
+errored_doors = mentioned_doors - configured_doors
+unless errored_doors.empty? then
+ puts "The folloring doors are mentioned but do not exist: " + errored_doors.to_s
+end
+
+door_groups.each do |group,num|
+ if num == 1 then
+ puts "Door group \"#{group}\" only has one door in it"
+ end
+end
+
+slashed_rooms = configured_rooms.select do |room|
+ room.include? "/"
+end
+unless slashed_rooms.empty? then
+ puts "The following rooms have slashes in their names: " + slashed_rooms.to_s
+end
+
+slashed_panels = configured_panels.select do |panel|
+ panel.include? "/"
+end
+unless slashed_panels.empty? then
+ puts "The following panels have slashes in their names: " + slashed_panels.to_s
+end
+
+slashed_doors = configured_doors.select do |door|
+ door.include? "/"
+end
+unless slashed_doors.empty? then
+ puts "The following doors have slashes in their names: " + slashed_doors.to_s
+end
+
+puts "#{configured_panels.size} panels (#{non_counting} non counting)"
diff --git a/worlds/lufia2ac/Items.py b/worlds/lufia2ac/Items.py
index 20159f480a9c..190b913c8ec1 100644
--- a/worlds/lufia2ac/Items.py
+++ b/worlds/lufia2ac/Items.py
@@ -2,9 +2,8 @@
from typing import Dict, NamedTuple, Optional
from BaseClasses import Item, ItemClassification
-from . import Locations
-start_id: int = Locations.start_id
+start_id: int = 0xAC0000
class ItemType(Enum):
@@ -500,7 +499,7 @@ def __init__(self, name: str, classification: ItemClassification, code: Optional
# 0x01C8: "Key28"
# 0x01C9: "Key29"
# 0x01CA: "AP item" # replaces "Key30"
- # 0x01CB: "Crown"
+ # 0x01CB: "SOLD OUT" # replaces "Crown"
# 0x01CC: "Ruby apple"
# 0x01CD: "PURIFIA"
# 0x01CE: "Tag ring"
diff --git a/worlds/lufia2ac/Options.py b/worlds/lufia2ac/Options.py
index 3f1c58f9d0f3..783da8e407b7 100644
--- a/worlds/lufia2ac/Options.py
+++ b/worlds/lufia2ac/Options.py
@@ -1,13 +1,16 @@
from __future__ import annotations
+import functools
+import numbers
import random
from dataclasses import dataclass
from itertools import accumulate, chain, combinations
from typing import Any, cast, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Type, TYPE_CHECKING, Union
-from Options import AssembleOptions, Choice, DeathLink, ItemDict, PerGameCommonOptions, Range, SpecialRange, \
- TextChoice, Toggle
+from Options import AssembleOptions, Choice, DeathLink, ItemDict, OptionDict, PerGameCommonOptions, Range, \
+ SpecialRange, TextChoice, Toggle
from .Enemies import enemy_name_to_sprite
+from .Items import ItemType, l2ac_item_table
if TYPE_CHECKING:
from BaseClasses import PlandoOptions
@@ -558,6 +561,25 @@ class Goal(Choice):
default = option_boss
+class GoldModifier(Range):
+ """Percentage modifier for gold gained from enemies.
+
+ Supported values: 25 – 400
+ Default value: 100 (same as in an unmodified game)
+ """
+
+ display_name = "Gold modifier"
+ range_start = 25
+ range_end = 400
+ default = 100
+
+ def __call__(self, gold: bytes) -> bytes:
+ try:
+ return (int.from_bytes(gold, "little") * self.value // 100).to_bytes(2, "little")
+ except OverflowError:
+ return b"\xFF\xFF"
+
+
class HealingFloorChance(Range):
"""The chance of a floor having a healing tile hidden under a bush.
@@ -662,6 +684,105 @@ class RunSpeed(Choice):
default = option_disabled
+class ShopInterval(SpecialRange):
+ """Place shops after a certain number of floors.
+
+ E.g., if you set this to 5, then you will be given the opportunity to shop after completing B5, B10, B15, etc.,
+ whereas if you set it to 1, then there will be a shop after every single completed floor.
+ Shops will offer a random selection of wares; on deeper floors, more expensive items might appear.
+ You can customize the stock that can appear in shops using the shop_inventory option.
+ You can control how much gold you will be obtaining from enemies using the gold_multiplier option.
+ Supported values: disabled, 1 – 10
+ Default value: disabled (same as in an unmodified game)
+ """
+
+ display_name = "Shop interval"
+ range_start = 0
+ range_end = 10
+ default = 0
+ special_range_cutoff = 1
+ special_range_names = {
+ "disabled": 0,
+ }
+
+
+class ShopInventory(OptionDict):
+ """Determine the item types that can appear in shops.
+
+ The value of this option should be a mapping of item categories (or individual items) to weights (non-negative
+ integers), which are used as relative probabilities when it comes to including these things in shops. (The actual
+ contents of the generated shops are selected randomly and are subject to additional constraints such as more
+ expensive things being allowed only on later floors.)
+ Supported keys:
+ non_restorative — a selection of mostly non-restorative red chest consumables
+ restorative — all HP- or MP-restoring red chest consumables
+ blue_chest — all blue chest items
+ spell — all red chest spells
+ gear — all red chest armors, shields, headgear, rings, and rocks (this respects the gear_variety_after_b9 option,
+ meaning that you will not encounter any shields, headgear, rings, or rocks in shops from B10 onward unless you
+ also enabled that option)
+ weapon — all red chest weapons
+ Additionally, you can also add extra weights for any specific cave item. If you want your shops to have a
+ higher than normal chance of selling a Dekar blade, you can, e.g., add "Dekar blade: 5".
+ You can even forego the predefined categories entirely and design a custom shop pool from scratch by providing
+ separate weights for each item you want to include.
+ (Spells, however, cannot be weighted individually and are only available as part of the "spell" category.)
+ Default value: {spell: 30, gear: 45, weapon: 82}
+ """
+
+ display_name = "Shop inventory"
+ _special_keys = {"non_restorative", "restorative", "blue_chest", "spell", "gear", "weapon"}
+ valid_keys = _special_keys | {item for item, data in l2ac_item_table.items()
+ if data.type in {ItemType.BLUE_CHEST, ItemType.ENEMY_DROP, ItemType.ENTRANCE_CHEST,
+ ItemType.RED_CHEST, ItemType.RED_CHEST_PATCH}}
+ default: Dict[str, int] = {
+ "spell": 30,
+ "gear": 45,
+ "weapon": 82,
+ }
+ value: Dict[str, int]
+
+ def verify(self, world: Type[World], player_name: str, plando_options: PlandoOptions) -> None:
+ super().verify(world, player_name, plando_options)
+ for item, weight in self.value.items():
+ if not isinstance(weight, numbers.Integral) or weight < 0:
+ raise Exception(f"Weight for item \"{item}\" from option {self} must be a non-negative integer, "
+ f"but was \"{weight}\".")
+
+ @property
+ def total(self) -> int:
+ return sum(self.value.values())
+
+ @property
+ def non_restorative(self) -> int:
+ return self.value.get("non_restorative", 0)
+
+ @property
+ def restorative(self) -> int:
+ return self.value.get("restorative", 0)
+
+ @property
+ def blue_chest(self) -> int:
+ return self.value.get("blue_chest", 0)
+
+ @property
+ def spell(self) -> int:
+ return self.value.get("spell", 0)
+
+ @property
+ def gear(self) -> int:
+ return self.value.get("gear", 0)
+
+ @property
+ def weapon(self) -> int:
+ return self.value.get("weapon", 0)
+
+ @functools.cached_property
+ def custom(self) -> Dict[int, int]:
+ return {l2ac_item_table[item].code & 0x01FF: weight for item, weight in self.value.items()
+ if item not in self._special_keys}
+
+
class ShuffleCapsuleMonsters(Toggle):
"""Shuffle the capsule monsters into the multiworld.
@@ -717,6 +838,7 @@ class L2ACOptions(PerGameCommonOptions):
final_floor: FinalFloor
gear_variety_after_b9: GearVarietyAfterB9
goal: Goal
+ gold_modifier: GoldModifier
healing_floor_chance: HealingFloorChance
initial_floor: InitialFloor
iris_floor_chance: IrisFloorChance
@@ -724,5 +846,7 @@ class L2ACOptions(PerGameCommonOptions):
master_hp: MasterHp
party_starting_level: PartyStartingLevel
run_speed: RunSpeed
+ shop_interval: ShopInterval
+ shop_inventory: ShopInventory
shuffle_capsule_monsters: ShuffleCapsuleMonsters
shuffle_party_members: ShufflePartyMembers
diff --git a/worlds/lufia2ac/Rom.py b/worlds/lufia2ac/Rom.py
index 1da8d235a691..446668d39243 100644
--- a/worlds/lufia2ac/Rom.py
+++ b/worlds/lufia2ac/Rom.py
@@ -3,7 +3,7 @@
from typing import Optional
import Utils
-from Utils import OptionsType
+from settings import get_settings
from worlds.Files import APDeltaPatch
L2USHASH: str = "6efc477d6203ed2b3b9133c1cd9e9c5d"
@@ -35,9 +35,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
def get_base_rom_path(file_name: str = "") -> str:
- options: OptionsType = Utils.get_options()
if not file_name:
- file_name = options["lufia2ac_options"]["rom_file"]
+ file_name = get_settings()["lufia2ac_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.user_path(file_name)
return file_name
diff --git a/worlds/lufia2ac/Utils.py b/worlds/lufia2ac/Utils.py
index 6c2e28d1379f..1fd7e0e171c1 100644
--- a/worlds/lufia2ac/Utils.py
+++ b/worlds/lufia2ac/Utils.py
@@ -1,5 +1,7 @@
+import itertools
+from operator import itemgetter
from random import Random
-from typing import Dict, List, MutableSequence, Sequence, Set, Tuple
+from typing import Dict, Iterable, List, MutableSequence, Sequence, Set, Tuple
def constrained_choices(population: Sequence[int], d: int, *, k: int, random: Random) -> List[int]:
@@ -19,3 +21,10 @@ def constrained_shuffle(x: MutableSequence[int], d: int, random: Random) -> None
i, j = random.randrange(n), random.randrange(n)
if x[i] in constraints[j] and x[j] in constraints[i]:
x[i], x[j] = x[j], x[i]
+
+
+def weighted_sample(population: Iterable[int], weights: Iterable[float], k: int, *, random: Random) -> List[int]:
+ population, keys = zip(*((item, pow(random.random(), 1 / group_weight))
+ for item, group in itertools.groupby(sorted(zip(population, weights)), key=itemgetter(0))
+ if (group_weight := sum(weight for _, weight in group))))
+ return sorted(population, key=dict(zip(population, keys)).__getitem__)[-k:]
diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py
index fad7109a4abd..acb988daaf82 100644
--- a/worlds/lufia2ac/__init__.py
+++ b/worlds/lufia2ac/__init__.py
@@ -14,9 +14,9 @@
from .Items import ItemData, ItemType, l2ac_item_name_to_id, l2ac_item_table, L2ACItem, start_id as items_start_id
from .Locations import l2ac_location_name_to_id, L2ACLocation
from .Options import CapsuleStartingLevel, DefaultParty, EnemyFloorNumbers, EnemyMovementPatterns, EnemySprites, \
- ExpModifier, Goal, L2ACOptions
+ Goal, L2ACOptions
from .Rom import get_base_rom_bytes, get_base_rom_path, L2ACDeltaPatch
-from .Utils import constrained_choices, constrained_shuffle
+from .Utils import constrained_choices, constrained_shuffle, weighted_sample
from .basepatch import apply_basepatch
CHESTS_PER_SPHERE: int = 5
@@ -222,6 +222,7 @@ def generate_output(self, output_directory: str) -> None:
rom_bytearray[0x09D59B:0x09D59B + 256] = self.get_node_connection_table()
rom_bytearray[0x0B05C0:0x0B05C0 + 18843] = self.get_enemy_stats()
rom_bytearray[0x0B4F02:0x0B4F02 + 2] = self.o.master_hp.value.to_bytes(2, "little")
+ rom_bytearray[0x0BEE9F:0x0BEE9F + 1948] = self.get_shops()
rom_bytearray[0x280010:0x280010 + 2] = self.o.blue_chest_count.value.to_bytes(2, "little")
rom_bytearray[0x280012:0x280012 + 3] = self.o.capsule_starting_level.xp.to_bytes(3, "little")
rom_bytearray[0x280015:0x280015 + 1] = self.o.initial_floor.value.to_bytes(1, "little")
@@ -229,6 +230,7 @@ def generate_output(self, output_directory: str) -> None:
rom_bytearray[0x280017:0x280017 + 1] = self.o.iris_treasures_required.value.to_bytes(1, "little")
rom_bytearray[0x280018:0x280018 + 1] = self.o.shuffle_party_members.unlock.to_bytes(1, "little")
rom_bytearray[0x280019:0x280019 + 1] = self.o.shuffle_capsule_monsters.unlock.to_bytes(1, "little")
+ rom_bytearray[0x28001A:0x28001A + 1] = self.o.shop_interval.value.to_bytes(1, "little")
rom_bytearray[0x280030:0x280030 + 1] = self.o.goal.value.to_bytes(1, "little")
rom_bytearray[0x28003D:0x28003D + 1] = self.o.death_link.value.to_bytes(1, "little")
rom_bytearray[0x281200:0x281200 + 470] = self.get_capsule_cravings_table()
@@ -357,7 +359,7 @@ def get_enemy_floors_sprites_and_movement_patterns(self) -> Tuple[bytes, bytes,
def get_enemy_stats(self) -> bytes:
rom: bytes = get_base_rom_bytes()
- if self.o.exp_modifier == ExpModifier.default:
+ if self.o.exp_modifier == 100 and self.o.gold_modifier == 100:
return rom[0x0B05C0:0x0B05C0 + 18843]
number_of_enemies: int = 224
@@ -366,6 +368,7 @@ def get_enemy_stats(self) -> bytes:
for enemy_id in range(number_of_enemies):
pointer: int = int.from_bytes(enemy_stats[2 * enemy_id:2 * enemy_id + 2], "little")
enemy_stats[pointer + 29:pointer + 31] = self.o.exp_modifier(enemy_stats[pointer + 29:pointer + 31])
+ enemy_stats[pointer + 31:pointer + 33] = self.o.gold_modifier(enemy_stats[pointer + 31:pointer + 33])
return enemy_stats
def get_goal_text_bytes(self) -> bytes:
@@ -383,6 +386,90 @@ def get_goal_text_bytes(self) -> bytes:
goal_text_bytes = bytes((0x08, *b"\x03".join(line.encode("ascii") for line in goal_text), 0x00))
return goal_text_bytes + b"\x00" * (147 - len(goal_text_bytes))
+ def get_shops(self) -> bytes:
+ rom: bytes = get_base_rom_bytes()
+
+ if not self.o.shop_interval:
+ return rom[0x0BEE9F:0x0BEE9F + 1948]
+
+ non_restorative_ids = {int.from_bytes(rom[0x0A713D + 2 * i:0x0A713D + 2 * i + 2], "little") for i in range(31)}
+ restorative_ids = {int.from_bytes(rom[0x08FFDC + 2 * i:0x08FFDC + 2 * i + 2], "little") for i in range(9)}
+ blue_ids = {int.from_bytes(rom[0x0A6EA0 + 2 * i:0x0A6EA0 + 2 * i + 2], "little") for i in range(41)}
+ number_of_spells: int = 35
+ number_of_items: int = 467
+ spells_offset: int = 0x0AFA5B
+ items_offset: int = 0x0B4F69
+ non_restorative_list: List[List[int]] = [list() for _ in range(99)]
+ restorative_list: List[List[int]] = [list() for _ in range(99)]
+ blue_list: List[List[int]] = [list() for _ in range(99)]
+ spell_list: List[List[int]] = [list() for _ in range(99)]
+ gear_list: List[List[int]] = [list() for _ in range(99)]
+ weapon_list: List[List[int]] = [list() for _ in range(99)]
+ custom_list: List[List[int]] = [list() for _ in range(99)]
+
+ for spell_id in range(number_of_spells):
+ pointer: int = int.from_bytes(rom[spells_offset + 2 * spell_id:spells_offset + 2 * spell_id + 2], "little")
+ value: int = int.from_bytes(rom[spells_offset + pointer + 15:spells_offset + pointer + 17], "little")
+ for f in range(value // 1000, 99):
+ spell_list[f].append(spell_id)
+ for item_id in range(number_of_items):
+ pointer = int.from_bytes(rom[items_offset + 2 * item_id:items_offset + 2 * item_id + 2], "little")
+ buckets: List[List[List[int]]] = list()
+ if item_id in non_restorative_ids:
+ buckets.append(non_restorative_list)
+ if item_id in restorative_ids:
+ buckets.append(restorative_list)
+ if item_id in blue_ids:
+ buckets.append(blue_list)
+ if not rom[items_offset + pointer] & 0x20 and not rom[items_offset + pointer + 1] & 0x20:
+ category: int = rom[items_offset + pointer + 7]
+ if category >= 0x02:
+ buckets.append(gear_list)
+ elif category == 0x01:
+ buckets.append(weapon_list)
+ if item_id in self.o.shop_inventory.custom:
+ buckets.append(custom_list)
+ value = int.from_bytes(rom[items_offset + pointer + 5:items_offset + pointer + 7], "little")
+ for bucket in buckets:
+ for f in range(value // 1000, 99):
+ bucket[f].append(item_id)
+
+ if not self.o.gear_variety_after_b9:
+ for f in range(99):
+ del gear_list[f][len(gear_list[f]) % 128:]
+
+ def create_shop(floor: int) -> Tuple[int, ...]:
+ if self.random.randrange(self.o.shop_inventory.total) < self.o.shop_inventory.spell:
+ return create_spell_shop(floor)
+ else:
+ return create_item_shop(floor)
+
+ def create_spell_shop(floor: int) -> Tuple[int, ...]:
+ spells = self.random.sample(spell_list[floor], 3)
+ return 0x03, 0x20, 0x00, *spells, 0xFF
+
+ def create_item_shop(floor: int) -> Tuple[int, ...]:
+ population = non_restorative_list[floor] + restorative_list[floor] + blue_list[floor] \
+ + gear_list[floor] + weapon_list[floor] + custom_list[floor]
+ weights = itertools.chain(*([weight / len_] * len_ if (len_ := len(list_)) else [] for weight, list_ in
+ [(self.o.shop_inventory.non_restorative, non_restorative_list[floor]),
+ (self.o.shop_inventory.restorative, restorative_list[floor]),
+ (self.o.shop_inventory.blue_chest, blue_list[floor]),
+ (self.o.shop_inventory.gear, gear_list[floor]),
+ (self.o.shop_inventory.weapon, weapon_list[floor])]),
+ (self.o.shop_inventory.custom[item] for item in custom_list[floor]))
+ items = weighted_sample(population, weights, 5, random=self.random)
+ return 0x01, 0x04, 0x00, *(b for item in items for b in item.to_bytes(2, "little")), 0x00, 0x00
+
+ shops = [create_shop(floor)
+ for floor in range(self.o.shop_interval, 99, self.o.shop_interval)
+ for _ in range(self.o.shop_interval)]
+ shop_pointers = itertools.accumulate((len(shop) for shop in shops[:-1]), initial=2 * len(shops))
+ shop_bytes = bytes(itertools.chain(*(p.to_bytes(2, "little") for p in shop_pointers), *shops))
+
+ assert len(shop_bytes) <= 1948, shop_bytes
+ return shop_bytes.ljust(1948, b"\x00")
+
@staticmethod
def get_node_connection_table() -> bytes:
class Connect(IntFlag):
diff --git a/worlds/lufia2ac/basepatch/basepatch.asm b/worlds/lufia2ac/basepatch/basepatch.asm
index aeae6846e32c..f298a1129d93 100644
--- a/worlds/lufia2ac/basepatch/basepatch.asm
+++ b/worlds/lufia2ac/basepatch/basepatch.asm
@@ -71,6 +71,11 @@ org $9EDD60 ; name
org $9FA900 ; sprite
incbin "ap_logo/ap_logo.bin"
warnpc $9FA980
+; sold out item
+org $96F9BA ; properties
+ DB $00,$00,$00,$10,$00,$00,$00,$00,$00,$00,$00,$00,$00
+org $9EDD6C ; name
+ DB "SOLD OUT " ; overwrites "Crown "
org $D08000 ; signature, start of expanded data area
@@ -825,6 +830,119 @@ SpellRNG:
+; shops
+pushpc
+org $83B442
+ ; DB=$83, x=1, m=1
+ JSL Shop ; overwrites STA $7FD0BF
+pullpc
+
+Shop:
+ STA $7FD0BF ; (overwritten instruction)
+ LDY $05AC ; load map number
+ CPY.b #$F0 ; check if ancient cave
+ BCC +
+ LDA $05B4 ; check if going to ancient cave entrance
+ BEQ +
+ LDA $7FE696 ; load next to next floor number
+ DEC
+ CPY.b #$F1 ; check if going to final floor
+ BCS ++ ; skip a decrement because next floor number is not incremented on final floor
+ DEC
+++: CMP $D08015 ; check if past initial floor
+ BCC +
+ STA $4204 ; WRDIVL; dividend = floor number
+ STZ $4205 ; WRDIVH
+ TAX
+ LDA $D0801A
+ STA $4206 ; WRDIVB; divisor = shop_interval
+ STA $211C ; M7B; second factor = shop_interval
+ JSL $8082C7 ; advance RNG (while waiting for division to complete)
+ LDY $4216 ; RDMPYL; skip if remainder (i.e., floor number mod shop_interval) is not 0
+ BNE +
+ STA $211B
+ STZ $211B ; M7A; first factor = random number from 0 to 255
+ TXA
+ CLC
+ SBC $2135 ; MPYM; calculate (floor number) - (random number from 0 to shop_interval-1) - 1
+ STA $30 ; set shop id
+ STZ $05A8 ; initialize variable for sold out item tracking
+ STZ $05A9
+ PHB
+ PHP
+ JML $80A33A ; open shop menu
++: RTL
+
+; shop item select
+pushpc
+org $82DF50
+ ; DB=$83, x=0, m=1
+ JML ShopItemSelected ; overwrites JSR $8B08 : CMP.b #$01
+pullpc
+
+ShopItemSelected:
+ LDA $1548 ; check inventory free space
+ BEQ +
+ JSR LoadShopSlotAsFlag
+ BIT $05A8 ; test item not already sold
+ BNE +
+ JML $82DF79 ; skip quantity selection and go directly to buy/equip
++: JML $82DF80 ; abort and go back to item selection
+
+; track bought shop items
+pushpc
+org $82E084
+ ; DB=$83, x=0, m=1
+ JSL ShopBuy ; overwrites LDA.b #$05 : LDX.w #$0007
+ NOP
+org $82E10E
+ ; DB=$83, x=0, m=1
+ JSL ShopEquip ; overwrites SEP #$10 : LDX $14DC
+ NOP
+pullpc
+
+ShopBuy:
+ JSR LoadShopSlotAsFlag
+ TSB $05A8 ; mark item as sold
+ LDA.b #$05 ; (overwritten instruction)
+ LDX.w #$0007 ; (overwritten instruction)
+ RTL
+
+ShopEquip:
+ JSR LoadShopSlotAsFlag
+ TSB $05A8 ; mark item as sold
+ SEP #$10 ; (overwritten instruction)
+ LDX $14DC ; (overwritten instruction)
+ RTL
+
+LoadShopSlotAsFlag:
+ TDC
+ LDA $14EC ; load currently selected shop slot number
+ ASL
+ TAX
+ LDA $8ED8C3,X ; load predefined bitmask with a single bit set
+ RTS
+
+; mark bought items as sold out
+pushpc
+org $8285EA
+ ; DB=$83, x=0, m=0
+ JSL SoldOut ; overwrites LDA [$FC],Y : AND #$01FF
+ NOP
+pullpc
+
+SoldOut:
+ LDA $8ED8C3,X ; load predefined bitmask with a single bit set
+ BIT $05A8 ; test sold items
+ BEQ +
+ LDA.w #$01CB ; load sold out item id
+ BRA ++
++: LDA [$FC],Y ; (overwritten instruction)
+ AND #$01FF ; (overwritten instruction)
+++: RTL
+
+
+
; increase variety of red chest gear after B9
pushpc
org $839176
@@ -1009,6 +1127,53 @@ pullpc
+; door stairs fix
+pushpc
+org $839453
+ ; DB=$7F, x=0, m=1
+ JSL DoorStairsFix ; overwrites JSR $9B18 : JSR $9D11
+ NOP #2
+pullpc
+
+DoorStairsFix:
+ CLC
+ LDY.w #$0000
+--: LDX.w #$00FF ; loop through floor layout starting from the bottom right
+-: LDA $EA00,X ; read node contents
+ BEQ + ; always skip empty nodes
+ BCC ++ ; 1st pass: skip all blocked nodes (would cause door stairs or rare stairs)
+ LDA $E9F0,X ; 2nd pass: skip only if the one above is also blocked (would cause door stairs)
+++: BMI +
+ INY ; count usable nodes
++: DEX
+ BPL -
+ TYA
+ BNE ++ ; all nodes blocked?
+ SEC ; set up 2nd, less restrictive pass
+ BRA --
+++: JSL $8082C7 ; advance RNG
+ STA $00211B
+ TDC
+ STA $00211B ; M7A; first factor = random number from 0 to 255
+ TYA
+ STA $00211C ; M7B; second factor = number of possible stair positions
+ LDA $002135 ; MPYM; calculate random number from 0 to number of possible stair positions - 1
+ TAY
+ LDX.w #$00FF ; loop through floor layout starting from the bottom right
+-: LDA $EA00,X ; read node contents
+ BEQ + ; always skip empty nodes
+ BCC ++ ; if 1st pass was sufficient: skip all blocked nodes (prevent door stairs and rare stairs)
+ LDA $E9F0,X ; if 2nd pass was needed: skip only if the one above is also blocked (prevent door stairs)
+++: BMI +
+ DEY ; count down to locate the (Y+1)th usable node
+ BMI ++
++: DEX
+ BPL -
+++: TXA ; return selected stair node coordinate
+ RTL
+
+
+
; equipment text fix
pushpc
org $81F2E3
@@ -1054,6 +1219,7 @@ pullpc
; $F02017 1 iris treasures required
; $F02018 1 party members available
; $F02019 1 capsule monsters available
+; $F0201A 1 shop interval
; $F02030 1 selected goal
; $F02031 1 goal completion: boss
; $F02032 1 goal completion: iris_treasure_hunt
diff --git a/worlds/lufia2ac/basepatch/basepatch.bsdiff4 b/worlds/lufia2ac/basepatch/basepatch.bsdiff4
index 51478e5d5256..4ed1815039a0 100644
Binary files a/worlds/lufia2ac/basepatch/basepatch.bsdiff4 and b/worlds/lufia2ac/basepatch/basepatch.bsdiff4 differ
diff --git a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md
index 64658a7d2746..d24c4ef9f9af 100644
--- a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md
+++ b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md
@@ -49,8 +49,9 @@ Your Party Leader will hold up the item they received when not in a fight or in
- Customize the multiworld item pool. (By default, your pool is filled with random blue chest items, but you can place
any cave item you want instead)
- Customize start inventory, i.e., begin every run with certain items or spells of your choice
-- Adjust how much EXP is gained from enemies
+- Adjust how much EXP and gold is gained from enemies
- Randomize enemy movement patterns, enemy sprites, and which enemy types can appear at which floor numbers
+- Option to make shops appear in the cave so that you have a way to spend your hard-earned gold
- Option to shuffle your party members and/or capsule monsters into the multiworld, meaning that someone will have to
find them in order to unlock them for you to use. While cave diving, you can add newly unlocked members to your party
by using the character items from your inventory
@@ -75,7 +76,7 @@ Your Party Leader will hold up the item they received when not in a fight or in
###### Bug fixes:
-- Vanilla game bugs that could result in softlocks or save file corruption have been fixed
+- Vanilla game bugs that could result in anomalous floors, softlocks, or save file corruption have been fixed
- (optional) Bugfix for the algorithm that determines the item pool for red chest gear. Enabling this allows the cave to
generate shields, headgear, rings, and jewels in red chests even after floor B9
- (optional) Bugfix for the outlandish cravings of capsule monsters in the US version. Enabling this makes feeding work
diff --git a/worlds/lufia2ac/docs/setup_en.md b/worlds/lufia2ac/docs/setup_en.md
index 4236c26e8a70..3762f32fb4a8 100644
--- a/worlds/lufia2ac/docs/setup_en.md
+++ b/worlds/lufia2ac/docs/setup_en.md
@@ -44,7 +44,7 @@ your personal settings and export a config file from them.
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the
-[YAML Validator](/mysterycheck) page.
+[YAML Validator](/check) page.
## Generating a Single-Player Game
diff --git a/worlds/meritous/Regions.py b/worlds/meritous/Regions.py
index 2c66a024ca69..de34570d0236 100644
--- a/worlds/meritous/Regions.py
+++ b/worlds/meritous/Regions.py
@@ -54,12 +54,12 @@ def create_regions(world: MultiWorld, player: int):
world.regions.append(boss_region)
region_final_boss = Region("Final Boss", player, world)
- region_final_boss.locations = [MeritousLocation(
+ region_final_boss.locations += [MeritousLocation(
player, "Wervyn Anixil", None, region_final_boss)]
world.regions.append(region_final_boss)
region_tfb = Region("True Final Boss", player, world)
- region_tfb.locations = [MeritousLocation(
+ region_tfb.locations += [MeritousLocation(
player, "Wervyn Anixil?", None, region_tfb)]
world.regions.append(region_tfb)
diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py
index 4be699e9cf7d..3fe13a3cb421 100644
--- a/worlds/messenger/__init__.py
+++ b/worlds/messenger/__init__.py
@@ -82,9 +82,7 @@ def generate_early(self) -> None:
self.shop_prices, self.figurine_prices = shuffle_shop_prices(self)
def create_regions(self) -> None:
- for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]:
- if region.name in REGION_CONNECTIONS:
- region.add_exits(REGION_CONNECTIONS[region.name])
+ self.multiworld.regions += [MessengerRegion(reg_name, self) for reg_name in REGIONS]
def create_items(self) -> None:
# create items that are always in the item pool
@@ -138,6 +136,8 @@ def create_items(self) -> None:
self.multiworld.itempool += itempool
def set_rules(self) -> None:
+ for reg_name, connections in REGION_CONNECTIONS.items():
+ self.multiworld.get_region(reg_name, self.player).add_exits(connections)
logic = self.options.logic_level
if logic == Logic.option_normal:
MessengerRules(self).set_messenger_rules()
@@ -188,6 +188,6 @@ def collect_item(self, state: "CollectionState", item: "Item", remove: bool = Fa
shard_count = int(item.name.strip("Time Shard ()"))
if remove:
shard_count = -shard_count
- state.prog_items["Shards", self.player] += shard_count
+ state.prog_items[self.player]["Shards"] += shard_count
return super().collect_item(state, item, remove)
diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py
index c5d90e00c857..ce31d43d60b0 100644
--- a/worlds/messenger/subclasses.py
+++ b/worlds/messenger/subclasses.py
@@ -32,7 +32,6 @@ def __init__(self, name: str, world: "MessengerWorld") -> None:
loc_dict = {loc: world.location_name_to_id[loc] if loc in world.location_name_to_id else None
for loc in locations}
self.add_locations(loc_dict, MessengerLocation)
- world.multiworld.regions.append(self)
class MessengerLocation(Location):
diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py
index fa992e1e1146..187f1fdf196a 100644
--- a/worlds/minecraft/__init__.py
+++ b/worlds/minecraft/__init__.py
@@ -173,7 +173,7 @@ def create_items(self) -> None:
def generate_output(self, output_directory: str) -> None:
data = self._get_mc_data()
- filename = f"AP_{self.multiworld.get_out_file_name_base(self.player)}.apmc"
+ filename = f"{self.multiworld.get_out_file_name_base(self.player)}.apmc"
with open(os.path.join(output_directory, filename), 'wb') as f:
f.write(b64encode(bytes(json.dumps(data), 'utf-8')))
diff --git a/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md b/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md
index 854034d5a8f1..7ffa4665fd2a 100644
--- a/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md
+++ b/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md
@@ -72,3 +72,10 @@ what item and what player is receiving the item
Whenever you have an item pending, the next time you are not in a battle, menu, or dialog box, you will receive a
message on screen notifying you of the item and sender, and the item will be added directly to your inventory.
+
+## Unique Local Commands
+
+The following commands are only available when using the MMBN3Client to play with Archipelago.
+
+- `/gba` Check GBA Connection State
+- `/debug` Toggle the Debug Text overlay in ROM
diff --git a/worlds/musedash/Items.py b/worlds/musedash/Items.py
index be229228bd40..63fd3aa51b94 100644
--- a/worlds/musedash/Items.py
+++ b/worlds/musedash/Items.py
@@ -6,7 +6,7 @@ class SongData(NamedTuple):
"""Special data container to contain the metadata of each song to make filtering work."""
code: Optional[int]
- song_is_free: bool
+ album: str
streamer_mode: bool
easy: Optional[int]
hard: Optional[int]
diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py
index 7812e28b7a8c..1807dce2f937 100644
--- a/worlds/musedash/MuseDashCollection.py
+++ b/worlds/musedash/MuseDashCollection.py
@@ -1,5 +1,5 @@
from .Items import SongData, AlbumData
-from typing import Dict, List, Optional
+from typing import Dict, List, Set, Optional
from collections import ChainMap
@@ -15,13 +15,21 @@ class MuseDashCollections:
MUSIC_SHEET_NAME: str = "Music Sheet"
MUSIC_SHEET_CODE: int = STARTING_CODE
- FREE_ALBUMS = [
+ FREE_ALBUMS: List[str] = [
"Default Music",
"Budget Is Burning: Nano Core",
"Budget Is Burning Vol.1",
]
- DIFF_OVERRIDES = [
+ MUSE_PLUS_DLC: str = "Muse Plus"
+ DLC: List[str] = [
+ # MUSE_PLUS_DLC, # To be included when OptionSets are rendered as part of basic settings.
+ # "maimai DX Limited-time Suite", # Part of Muse Plus. Goes away 31st Jan 2026.
+ "Miku in Museland", # Paid DLC not included in Muse Plus
+ "MSR Anthology", # Part of Muse Plus. Goes away 20th Jan 2024.
+ ]
+
+ DIFF_OVERRIDES: List[str] = [
"MuseDash ka nanika hi",
"Rush-Hour",
"Find this Month's Featured Playlist",
@@ -48,8 +56,8 @@ class MuseDashCollections:
"Error SFX Trap": STARTING_CODE + 9,
}
- item_names_to_id = ChainMap({}, sfx_trap_items, vfx_trap_items)
- location_names_to_id = ChainMap(song_locations, album_locations)
+ item_names_to_id: ChainMap = ChainMap({}, sfx_trap_items, vfx_trap_items)
+ location_names_to_id: ChainMap = ChainMap(song_locations, album_locations)
def __init__(self) -> None:
self.item_names_to_id[self.MUSIC_SHEET_NAME] = self.MUSIC_SHEET_CODE
@@ -70,7 +78,6 @@ def __init__(self) -> None:
# Data is in the format 'Song|UID|Album|StreamerMode|EasyDiff|HardDiff|MasterDiff|SecretDiff'
song_name = sections[0]
# [1] is used in the client copy to make sure item id's match.
- song_is_free = album in self.FREE_ALBUMS
steamer_mode = sections[3] == "True"
if song_name in self.DIFF_OVERRIDES:
@@ -84,7 +91,7 @@ def __init__(self) -> None:
diff_of_hard = self.parse_song_difficulty(sections[5])
diff_of_master = self.parse_song_difficulty(sections[6])
- self.song_items[song_name] = SongData(item_id_index, song_is_free, steamer_mode,
+ self.song_items[song_name] = SongData(item_id_index, album, steamer_mode,
diff_of_easy, diff_of_hard, diff_of_master)
item_id_index += 1
@@ -102,13 +109,13 @@ def __init__(self) -> None:
self.song_locations[f"{name}-1"] = location_id_index + 1
location_id_index += 2
- def get_songs_with_settings(self, dlc_songs: bool, streamer_mode_active: bool,
+ def get_songs_with_settings(self, dlc_songs: Set[str], streamer_mode_active: bool,
diff_lower: int, diff_higher: int) -> List[str]:
"""Gets a list of all songs that match the filter settings. Difficulty thresholds are inclusive."""
filtered_list = []
for songKey, songData in self.song_items.items():
- if not dlc_songs and not songData.song_is_free:
+ if not self.song_matches_dlc_filter(songData, dlc_songs):
continue
if streamer_mode_active and not songData.streamer_mode:
@@ -128,6 +135,19 @@ def get_songs_with_settings(self, dlc_songs: bool, streamer_mode_active: bool,
return filtered_list
+ def song_matches_dlc_filter(self, song: SongData, dlc_songs: Set[str]) -> bool:
+ if song.album in self.FREE_ALBUMS:
+ return True
+
+ if song.album in dlc_songs:
+ return True
+
+ # Muse Plus provides access to any DLC not included as a seperate pack
+ if song.album not in self.DLC and self.MUSE_PLUS_DLC in dlc_songs:
+ return True
+
+ return False
+
def parse_song_difficulty(self, difficulty: str) -> Optional[int]:
"""Attempts to parse the song difficulty."""
if len(difficulty) <= 0 or difficulty == "?" or difficulty == "¿":
diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt
index 8d6c3f375314..5b3ef40e5421 100644
--- a/worlds/musedash/MuseDashData.txt
+++ b/worlds/musedash/MuseDashData.txt
@@ -51,42 +51,42 @@ Mujinku-Vacuum|0-28|Default Music|False|5|7|11|
MilK|0-36|Default Music|False|5|7|9|
umpopoff|0-41|Default Music|False|0|?|0|
Mopemope|0-45|Default Music|False|4|7|9|11
-The Happycore Idol|43-0|Just as Planned Plus|True|2|5|7|
-Amatsumikaboshi|43-1|Just as Planned Plus|True|4|6|8|10
-ARIGA THESIS|43-2|Just as Planned Plus|True|3|6|10|
-Night of Nights|43-3|Just as Planned Plus|False|4|7|10|
-#Psychedelic_Meguro_River|43-4|Just as Planned Plus|False|3|6|8|
-can you feel it|43-5|Just as Planned Plus|False|4|6|8|9
-Midnight O'clock|43-6|Just as Planned Plus|True|3|6|8|
-Rin|43-7|Just as Planned Plus|True|5|7|10|
-Smile-mileS|43-8|Just as Planned Plus|False|6|8|10|
-Believing and Being|43-9|Just as Planned Plus|True|4|6|9|
-Catalyst|43-10|Just as Planned Plus|False|5|7|9|
-don't!stop!eroero!|43-11|Just as Planned Plus|True|5|7|9|
-pa pi pu pi pu pi pa|43-12|Just as Planned Plus|False|6|8|10|
-Sand Maze|43-13|Just as Planned Plus|True|6|8|10|11
-Diffraction|43-14|Just as Planned Plus|True|5|8|10|
-AKUMU|43-15|Just as Planned Plus|False|4|6|8|
-Queen Aluett|43-16|Just as Planned Plus|True|7|9|11|
-DROPS|43-17|Just as Planned Plus|False|2|5|8|
-Frightfully-insane Flan-chan's frightful song|43-18|Just as Planned Plus|False|5|7|10|
-snooze|43-19|Just as Planned Plus|False|5|7|10|
-Kuishinbo Hacker feat.Kuishinbo Akachan|43-20|Just as Planned Plus|True|5|7|9|
-Inu no outa|43-21|Just as Planned Plus|True|3|5|7|
-Prism Fountain|43-22|Just as Planned Plus|True|7|9|11|
-Gospel|43-23|Just as Planned Plus|False|4|6|9|
+The Happycore Idol|43-0|MD Plus Project|True|2|5|7|
+Amatsumikaboshi|43-1|MD Plus Project|True|4|6|8|10
+ARIGA THESIS|43-2|MD Plus Project|True|3|6|10|
+Night of Nights|43-3|MD Plus Project|False|4|7|10|
+#Psychedelic_Meguro_River|43-4|MD Plus Project|False|3|6|8|
+can you feel it|43-5|MD Plus Project|False|4|6|8|9
+Midnight O'clock|43-6|MD Plus Project|True|3|6|8|
+Rin|43-7|MD Plus Project|True|5|7|10|
+Smile-mileS|43-8|MD Plus Project|False|6|8|10|
+Believing and Being|43-9|MD Plus Project|True|4|6|9|
+Catalyst|43-10|MD Plus Project|False|5|7|9|
+don't!stop!eroero!|43-11|MD Plus Project|True|5|7|9|
+pa pi pu pi pu pi pa|43-12|MD Plus Project|False|6|8|10|
+Sand Maze|43-13|MD Plus Project|True|6|8|10|11
+Diffraction|43-14|MD Plus Project|True|5|8|10|
+AKUMU|43-15|MD Plus Project|False|4|6|8|
+Queen Aluett|43-16|MD Plus Project|True|7|9|11|
+DROPS|43-17|MD Plus Project|False|2|5|8|
+Frightfully-insane Flan-chan's frightful song|43-18|MD Plus Project|False|5|7|10|
+snooze|43-19|MD Plus Project|False|5|7|10|
+Kuishinbo Hacker feat.Kuishinbo Akachan|43-20|MD Plus Project|True|5|7|9|
+Inu no outa|43-21|MD Plus Project|True|3|5|7|
+Prism Fountain|43-22|MD Plus Project|True|7|9|11|
+Gospel|43-23|MD Plus Project|False|4|6|9|
East Ai Li Lovely|62-0|Happy Otaku Pack Vol.17|False|2|4|7|
Mori Umi no Fune|62-1|Happy Otaku Pack Vol.17|True|5|7|9|
Ooi|62-2|Happy Otaku Pack Vol.17|True|5|7|10|
Numatta!!|62-3|Happy Otaku Pack Vol.17|True|5|7|9|
-SATELLITE|62-4|Happy Otaku Pack Vol.17|False|5|7|9|
+SATELLITE|62-4|Happy Otaku Pack Vol.17|False|5|7|9|10
Fantasia Sonata Colorful feat. V!C|62-5|Happy Otaku Pack Vol.17|True|6|8|11|
MuseDash ka nanika hi|61-0|Ola Dash|True|?|?|¿|
Aleph-0|61-1|Ola Dash|True|7|9|11|
Buttoba Supernova|61-2|Ola Dash|False|5|7|10|11
Rush-Hour|61-3|Ola Dash|False|IG|Jh|a2|Eh
3rd Avenue|61-4|Ola Dash|False|3|5|〇|
-WORLDINVADER|61-5|Ola Dash|True|5|8|10|
+WORLDINVADER|61-5|Ola Dash|True|5|8|10|11
N3V3R G3T OV3R|60-0|maimai DX Limited-time Suite|True|4|7|10|
Oshama Scramble!|60-1|maimai DX Limited-time Suite|True|5|7|10|
Valsqotch|60-2|maimai DX Limited-time Suite|True|5|9|11|
@@ -404,7 +404,7 @@ trippers feeling!|8-4|Give Up TREATMENT Vol.3|True|5|7|9|11
Lilith ambivalence lovers|8-5|Give Up TREATMENT Vol.3|False|5|8|10|
Brave My Soul|7-0|Give Up TREATMENT Vol.2|False|4|6|8|
Halcyon|7-1|Give Up TREATMENT Vol.2|False|4|7|10|
-Crimson Nightingle|7-2|Give Up TREATMENT Vol.2|True|4|7|10|
+Crimson Nightingale|7-2|Give Up TREATMENT Vol.2|True|4|7|10|
Invader|7-3|Give Up TREATMENT Vol.2|True|3|7|11|
Lyrith|7-4|Give Up TREATMENT Vol.2|False|5|7|10|
GOODBOUNCE|7-5|Give Up TREATMENT Vol.2|False|4|6|9|
@@ -450,13 +450,13 @@ Love Patrol|63-2|MUSE RADIO FM104|True|3|5|7|
Mahorova|63-3|MUSE RADIO FM104|True|3|5|8|
Yoru no machi|63-4|MUSE RADIO FM104|True|1|4|7|
INTERNET YAMERO|63-5|MUSE RADIO FM104|True|6|8|10|
-Abracadabra|43-24|Just as Planned Plus|False|6|8|10|
-Squalldecimator feat. EZ-Ven|43-25|Just as Planned Plus|True|5|7|9|
-Amateras Rhythm|43-26|Just as Planned Plus|True|6|8|11|
-Record one's Dream|43-27|Just as Planned Plus|False|4|7|10|
-Lunatic|43-28|Just as Planned Plus|True|5|8|10|
-Jiumeng|43-29|Just as Planned Plus|True|3|6|8|
-The Day We Become Family|43-30|Just as Planned Plus|True|3|5|8|
+Abracadabra|43-24|MD Plus Project|False|6|8|10|
+Squalldecimator feat. EZ-Ven|43-25|MD Plus Project|True|5|7|9|
+Amateras Rhythm|43-26|MD Plus Project|True|6|8|11|
+Record one's Dream|43-27|MD Plus Project|False|4|7|10|
+Lunatic|43-28|MD Plus Project|True|5|8|10|
+Jiumeng|43-29|MD Plus Project|True|3|6|8|
+The Day We Become Family|43-30|MD Plus Project|True|3|5|8|
Sutori ma FIRE!?!?|64-0|COSMIC RADIO PEROLIST|True|3|5|8|
Tanuki Step|64-1|COSMIC RADIO PEROLIST|True|5|7|10|11
Space Stationery|64-2|COSMIC RADIO PEROLIST|True|5|7|10|
@@ -465,7 +465,34 @@ Kawai Splendid Space Thief|64-4|COSMIC RADIO PEROLIST|False|6|8|10|11
Night City Runway|64-5|COSMIC RADIO PEROLIST|True|4|6|8|
Chaos Shotgun feat. ChumuNote|64-6|COSMIC RADIO PEROLIST|True|6|8|10|
mew mew magical summer|64-7|COSMIC RADIO PEROLIST|False|5|8|10|11
-BrainDance|65-0|Neon Abyss|True|3|6|9|
-My Focus!|65-1|Neon Abyss|True|5|7|10|
-ABABABA BURST|65-2|Neon Abyss|True|5|7|9|
-ULTRA HIGHER|65-3|Neon Abyss|True|4|7|10|
\ No newline at end of file
+BrainDance|65-0|NeonAbyss|True|3|6|9|
+My Focus!|65-1|NeonAbyss|True|5|7|10|
+ABABABA BURST|65-2|NeonAbyss|True|5|7|9|
+ULTRA HIGHER|65-3|NeonAbyss|True|4|7|10|
+Silver Bullet|43-31|MD Plus Project|True|5|7|10|
+Random|43-32|MD Plus Project|True|4|7|9|
+OTOGE-BOSS-KYOKU-CHAN|43-33|MD Plus Project|False|6|8|10|11
+Crow Rabbit|43-34|MD Plus Project|True|7|9|11|
+SyZyGy|43-35|MD Plus Project|True|6|8|10|11
+Mermaid Radio|43-36|MD Plus Project|True|3|5|7|
+Helixir|43-37|MD Plus Project|False|6|8|10|
+Highway Cruisin'|43-38|MD Plus Project|False|3|5|8|
+JACK PT BOSS|43-39|MD Plus Project|False|6|8|10|
+Time Capsule|43-40|MD Plus Project|False|7|9|11|
+39 Music!|66-0|Miku in Museland|False|3|5|8|
+Hand in Hand|66-1|Miku in Museland|False|1|3|6|
+Cynical Night Plan|66-2|Miku in Museland|False|4|6|8|
+God-ish|66-3|Miku in Museland|False|4|7|10|
+Darling Dance|66-4|Miku in Museland|False|4|7|9|
+Hatsune Creation Myth|66-5|Miku in Museland|False|6|8|10|
+The Vampire|66-6|Miku in Museland|False|4|6|9|
+Future Eve|66-7|Miku in Museland|False|4|8|11|
+Unknown Mother Goose|66-8|Miku in Museland|False|4|8|10|
+Shun-ran|66-9|Miku in Museland|False|4|7|9|
+NICE TYPE feat. monii|43-41|MD Plus Project|True|3|6|8|
+Rainy Angel|67-0|Happy Otaku Pack Vol.18|True|4|6|9|11
+Gullinkambi|67-1|Happy Otaku Pack Vol.18|True|4|7|10|
+RakiRaki Rebuilders!!!|67-2|Happy Otaku Pack Vol.18|True|5|7|10|
+Laniakea|67-3|Happy Otaku Pack Vol.18|False|5|8|10|
+OTTAMA GAZER|67-4|Happy Otaku Pack Vol.18|True|5|8|10|
+Sleep Tight feat.Macoto|67-5|Happy Otaku Pack Vol.18|True|3|5|8|
\ No newline at end of file
diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py
index b2f15ecc8e6c..3fe28187fae6 100644
--- a/worlds/musedash/Options.py
+++ b/worlds/musedash/Options.py
@@ -1,10 +1,19 @@
from typing import Dict
-from Options import Toggle, Option, Range, Choice, DeathLink, ItemSet
+from Options import Toggle, Option, Range, Choice, DeathLink, ItemSet, OptionSet, PerGameCommonOptions
+from dataclasses import dataclass
+from .MuseDashCollection import MuseDashCollections
class AllowJustAsPlannedDLCSongs(Toggle):
- """Whether [Just as Planned]/[Muse Plus] DLC Songs, and all the DLCs along with it, will be included in the randomizer."""
- display_name = "Allow [Just as Planned]/[Muse Plus] DLC Songs"
+ """Whether [Muse Plus] DLC Songs, and all the albums included in it, can be chosen as randomised songs.
+ Note: The [Just As Planned] DLC contains all [Muse Plus] songs."""
+ display_name = "Allow [Muse Plus] DLC Songs"
+
+class DLCMusicPacks(OptionSet):
+ """Which non-[Muse Plus] DLC packs can be chosen as randomised songs."""
+ display_name = "DLC Packs"
+ default = {}
+ valid_keys = [dlc for dlc in MuseDashCollections.DLC]
class StreamerModeEnabled(Toggle):
@@ -159,21 +168,22 @@ class ExcludeSongs(ItemSet):
display_name = "Exclude Songs"
-musedash_options: Dict[str, type(Option)] = {
- "allow_just_as_planned_dlc_songs": AllowJustAsPlannedDLCSongs,
- "streamer_mode_enabled": StreamerModeEnabled,
- "starting_song_count": StartingSongs,
- "additional_song_count": AdditionalSongs,
- "additional_item_percentage": AdditionalItemPercentage,
- "song_difficulty_mode": DifficultyMode,
- "song_difficulty_min": DifficultyModeOverrideMin,
- "song_difficulty_max": DifficultyModeOverrideMax,
- "grade_needed": GradeNeeded,
- "music_sheet_count_percentage": MusicSheetCountPercentage,
- "music_sheet_win_count_percentage": MusicSheetWinCountPercentage,
- "available_trap_types": TrapTypes,
- "trap_count_percentage": TrapCountPercentage,
- "death_link": DeathLink,
- "include_songs": IncludeSongs,
- "exclude_songs": ExcludeSongs
-}
+@dataclass
+class MuseDashOptions(PerGameCommonOptions):
+ allow_just_as_planned_dlc_songs: AllowJustAsPlannedDLCSongs
+ dlc_packs: DLCMusicPacks
+ streamer_mode_enabled: StreamerModeEnabled
+ starting_song_count: StartingSongs
+ additional_song_count: AdditionalSongs
+ additional_item_percentage: AdditionalItemPercentage
+ song_difficulty_mode: DifficultyMode
+ song_difficulty_min: DifficultyModeOverrideMin
+ song_difficulty_max: DifficultyModeOverrideMax
+ grade_needed: GradeNeeded
+ music_sheet_count_percentage: MusicSheetCountPercentage
+ music_sheet_win_count_percentage: MusicSheetWinCountPercentage
+ available_trap_types: TrapTypes
+ trap_count_percentage: TrapCountPercentage
+ death_link: DeathLink
+ include_songs: IncludeSongs
+ exclude_songs: ExcludeSongs
diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py
index 754d2352e03e..bfe321b64afe 100644
--- a/worlds/musedash/__init__.py
+++ b/worlds/musedash/__init__.py
@@ -1,10 +1,10 @@
from worlds.AutoWorld import World, WebWorld
-from worlds.generic.Rules import set_rule
from BaseClasses import Region, Item, ItemClassification, Entrance, Tutorial
-from typing import List
+from typing import List, ClassVar, Type
from math import floor
+from Options import PerGameCommonOptions
-from .Options import musedash_options
+from .Options import MuseDashOptions
from .Items import MuseDashSongItem, MuseDashFixedItem
from .Locations import MuseDashLocation
from .MuseDashCollection import MuseDashCollections
@@ -47,9 +47,9 @@ class MuseDashWorld(World):
# World Options
game = "Muse Dash"
- option_definitions = musedash_options
+ options_dataclass: ClassVar[Type[PerGameCommonOptions]] = MuseDashOptions
topology_present = False
- data_version = 9
+ data_version = 11
web = MuseDashWebWorld()
# Necessary Data
@@ -66,14 +66,17 @@ class MuseDashWorld(World):
location_count: int
def generate_early(self):
- dlc_songs = self.multiworld.allow_just_as_planned_dlc_songs[self.player]
- streamer_mode = self.multiworld.streamer_mode_enabled[self.player]
+ dlc_songs = {key for key in self.options.dlc_packs.value}
+ if (self.options.allow_just_as_planned_dlc_songs.value):
+ dlc_songs.add(self.md_collection.MUSE_PLUS_DLC)
+
+ streamer_mode = self.options.streamer_mode_enabled
(lower_diff_threshold, higher_diff_threshold) = self.get_difficulty_range()
# The minimum amount of songs to make an ok rando would be Starting Songs + 10 interim songs + Goal song.
# - Interim songs being equal to max starting song count.
# Note: The worst settings still allow 25 songs (Streamer Mode + No DLC).
- starter_song_count = self.multiworld.starting_song_count[self.player].value
+ starter_song_count = self.options.starting_song_count.value
while True:
# In most cases this should only need to run once
@@ -104,9 +107,9 @@ def generate_early(self):
def handle_plando(self, available_song_keys: List[str]) -> List[str]:
song_items = self.md_collection.song_items
- start_items = self.multiworld.start_inventory[self.player].value.keys()
- include_songs = self.multiworld.include_songs[self.player].value
- exclude_songs = self.multiworld.exclude_songs[self.player].value
+ start_items = self.options.start_inventory.value.keys()
+ include_songs = self.options.include_songs.value
+ exclude_songs = self.options.exclude_songs.value
self.starting_songs = [s for s in start_items if s in song_items]
self.included_songs = [s for s in include_songs if s in song_items and s not in self.starting_songs]
@@ -115,8 +118,8 @@ def handle_plando(self, available_song_keys: List[str]) -> List[str]:
and s not in include_songs and s not in exclude_songs]
def create_song_pool(self, available_song_keys: List[str]):
- starting_song_count = self.multiworld.starting_song_count[self.player].value
- additional_song_count = self.multiworld.additional_song_count[self.player].value
+ starting_song_count = self.options.starting_song_count.value
+ additional_song_count = self.options.additional_song_count.value
self.random.shuffle(available_song_keys)
@@ -150,7 +153,7 @@ def create_song_pool(self, available_song_keys: List[str]):
# Then attempt to fufill any remaining songs for interim songs
if len(self.included_songs) < additional_song_count:
- for _ in range(len(self.included_songs), self.multiworld.additional_song_count[self.player]):
+ for _ in range(len(self.included_songs), self.options.additional_song_count):
if len(available_song_keys) <= 0:
break
self.included_songs.append(available_song_keys.pop())
@@ -258,40 +261,40 @@ def set_rules(self) -> None:
state.has(self.md_collection.MUSIC_SHEET_NAME, self.player, self.get_music_sheet_win_count())
def get_available_traps(self) -> List[str]:
- dlc_songs = self.multiworld.allow_just_as_planned_dlc_songs[self.player]
+ sfx_traps_available = self.options.allow_just_as_planned_dlc_songs.value
trap_list = []
- if self.multiworld.available_trap_types[self.player].value & 1 != 0:
+ if self.options.available_trap_types.value & 1 != 0:
trap_list += self.md_collection.vfx_trap_items.keys()
# SFX options are only available under Just as Planned DLC.
- if dlc_songs and self.multiworld.available_trap_types[self.player].value & 2 != 0:
+ if sfx_traps_available and self.options.available_trap_types.value & 2 != 0:
trap_list += self.md_collection.sfx_trap_items.keys()
return trap_list
def get_additional_item_percentage(self) -> int:
- trap_count = self.multiworld.trap_count_percentage[self.player].value
- song_count = self.multiworld.music_sheet_count_percentage[self.player].value
- return max(trap_count + song_count, self.multiworld.additional_item_percentage[self.player].value)
+ trap_count = self.options.trap_count_percentage.value
+ song_count = self.options.music_sheet_count_percentage.value
+ return max(trap_count + song_count, self.options.additional_item_percentage.value)
def get_trap_count(self) -> int:
- multiplier = self.multiworld.trap_count_percentage[self.player].value / 100.0
+ multiplier = self.options.trap_count_percentage.value / 100.0
trap_count = (len(self.starting_songs) * 2) + len(self.included_songs)
return max(0, floor(trap_count * multiplier))
def get_music_sheet_count(self) -> int:
- multiplier = self.multiworld.music_sheet_count_percentage[self.player].value / 100.0
+ multiplier = self.options.music_sheet_count_percentage.value / 100.0
song_count = (len(self.starting_songs) * 2) + len(self.included_songs)
return max(1, floor(song_count * multiplier))
def get_music_sheet_win_count(self) -> int:
- multiplier = self.multiworld.music_sheet_win_count_percentage[self.player].value / 100.0
+ multiplier = self.options.music_sheet_win_count_percentage.value / 100.0
sheet_count = self.get_music_sheet_count()
return max(1, floor(sheet_count * multiplier))
def get_difficulty_range(self) -> List[int]:
- difficulty_mode = self.multiworld.song_difficulty_mode[self.player]
+ difficulty_mode = self.options.song_difficulty_mode
# Valid difficulties are between 1 and 11. But make it 0 to 12 for safety
difficulty_bounds = [0, 12]
@@ -309,8 +312,8 @@ def get_difficulty_range(self) -> List[int]:
elif difficulty_mode == 5:
difficulty_bounds[0] = 10
elif difficulty_mode == 6:
- minimum_difficulty = self.multiworld.song_difficulty_min[self.player].value
- maximum_difficulty = self.multiworld.song_difficulty_max[self.player].value
+ minimum_difficulty = self.options.song_difficulty_min.value
+ maximum_difficulty = self.options.song_difficulty_max.value
difficulty_bounds[0] = min(minimum_difficulty, maximum_difficulty)
difficulty_bounds[1] = max(minimum_difficulty, maximum_difficulty)
@@ -320,7 +323,7 @@ def get_difficulty_range(self) -> List[int]:
def fill_slot_data(self):
return {
"victoryLocation": self.victory_song_name,
- "deathLink": self.multiworld.death_link[self.player].value,
+ "deathLink": self.options.death_link.value,
"musicSheetWinCount": self.get_music_sheet_win_count(),
- "gradeNeeded": self.multiworld.grade_needed[self.player].value
+ "gradeNeeded": self.options.grade_needed.value
}
diff --git a/worlds/musedash/docs/setup_en.md b/worlds/musedash/docs/setup_en.md
index 1ab61ff22ac6..ebf165c7dd78 100644
--- a/worlds/musedash/docs/setup_en.md
+++ b/worlds/musedash/docs/setup_en.md
@@ -8,10 +8,10 @@
- Windows 8 or Newer.
- Muse Dash: [Available on Steam](https://store.steampowered.com/app/774171/Muse_Dash/)
- - \[Optional\] [Just as Planned] DLC: [Also Available on Steam](https://store.steampowered.com/app/1055810/Muse_Dash__Just_as_planned/)
+ - \[Optional\] [Muse Plus] DLC: [Also Available on Steam](https://store.steampowered.com/app/2593750/Muse_Dash__Muse_Plus/)
- Melon Loader: [GitHub](https://github.com/LavaGang/MelonLoader/releases/latest)
- .Net Framework 4.8 may be needed for the installer: [Download](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net48)
-- .Net 6.0 (If not already installed): [Download](https://dotnet.microsoft.com/en-us/download/dotnet/6.0#runtime-6.0.15)
+- .NET Desktop Runtime 6.0.XX (If not already installed): [Download](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
- Muse Dash Archipelago Mod: [GitHub](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest)
## Installing the Archipelago mod to Muse Dash
diff --git a/worlds/musedash/docs/setup_es.md b/worlds/musedash/docs/setup_es.md
index 21fc69e7eb5b..0d737c26d726 100644
--- a/worlds/musedash/docs/setup_es.md
+++ b/worlds/musedash/docs/setup_es.md
@@ -8,10 +8,10 @@
- Windows 8 o más reciente.
- Muse Dash: [Disponible en Steam](https://store.steampowered.com/app/774171/Muse_Dash/)
- - \[Opcional\] [Just as Planned] DLC: [tambien disponible on Steam](https://store.steampowered.com/app/1055810/Muse_Dash__Just_as_planned/)
+ - \[Opcional\] [Muse Plus] DLC: [tambien disponible on Steam](https://store.steampowered.com/app/2593750/Muse_Dash__Muse_Plus/)
- Melon Loader: [GitHub](https://github.com/LavaGang/MelonLoader/releases/latest)
- - .Net Framework 4.8 podría ser necesario para el instalador: [Descarga](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net48)
-- .Net 6.0 (si aún no está instalado): [Descarga](https://dotnet.microsoft.com/en-us/download/dotnet/6.0#runtime-6.0.15)
+ - .Net Framework 4.8 podría ser necesario para el instalador: [Descarga](https://dotnet.microsoft.com/es-es/download/dotnet-framework/net48)
+- Entorno de ejecución de escritorio de .NET 6.0.XX (si aún no está instalado): [Descarga](https://dotnet.microsoft.com/es-es/download/dotnet/6.0)
- Muse Dash Archipelago Mod: [GitHub](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest)
## Instalar el mod de Archipelago en Muse Dash
diff --git a/worlds/musedash/test/TestCollection.py b/worlds/musedash/test/TestCollection.py
index 23348af104b5..f9422388ae1e 100644
--- a/worlds/musedash/test/TestCollection.py
+++ b/worlds/musedash/test/TestCollection.py
@@ -36,14 +36,27 @@ def test_ids_dont_change(self) -> None:
def test_free_dlc_included_in_base_songs(self) -> None:
collection = MuseDashCollections()
- songs = collection.get_songs_with_settings(False, False, 0, 11)
+ songs = collection.get_songs_with_settings(set(), False, 0, 12)
self.assertIn("Glimmer", songs, "Budget Is Burning Vol.1 is not being included in base songs")
self.assertIn("Out of Sense", songs, "Budget Is Burning: Nano Core is not being included in base songs")
+ def test_dlcs(self) -> None:
+ collection = MuseDashCollections()
+ free_song_count = len(collection.get_songs_with_settings(set(), False, 0, 12))
+ known_mp_song = "The Happycore Idol"
+
+ for dlc in collection.DLC:
+ songs_with_dlc = collection.get_songs_with_settings({dlc}, False, 0, 12)
+ self.assertGreater(len(songs_with_dlc), free_song_count, f"DLC {dlc} did not include extra songs.")
+ if dlc == collection.MUSE_PLUS_DLC:
+ self.assertIn(known_mp_song, songs_with_dlc, f"Muse Plus missing muse plus song.")
+ else:
+ self.assertNotIn(known_mp_song, songs_with_dlc, f"DLC {dlc} includes Muse Plus songs.")
+
def test_remove_songs_are_not_generated(self) -> None:
collection = MuseDashCollections()
- songs = collection.get_songs_with_settings(True, False, 0, 11)
+ songs = collection.get_songs_with_settings({x for x in collection.DLC}, False, 0, 12)
for song_name in self.REMOVED_SONGS:
self.assertNotIn(song_name, songs, f"Song '{song_name}' wasn't removed correctly.")
diff --git a/worlds/musedash/test/TestDifficultyRanges.py b/worlds/musedash/test/TestDifficultyRanges.py
index 58817d0fc3ef..01420347af15 100644
--- a/worlds/musedash/test/TestDifficultyRanges.py
+++ b/worlds/musedash/test/TestDifficultyRanges.py
@@ -4,6 +4,7 @@
class DifficultyRanges(MuseDashTestBase):
def test_all_difficulty_ranges(self) -> None:
muse_dash_world = self.multiworld.worlds[1]
+ dlc_set = {x for x in muse_dash_world.md_collection.DLC}
difficulty_choice = self.multiworld.song_difficulty_mode[1]
difficulty_min = self.multiworld.song_difficulty_min[1]
difficulty_max = self.multiworld.song_difficulty_max[1]
@@ -12,7 +13,7 @@ def test_range(inputRange, lower, upper):
self.assertEqual(inputRange[0], lower)
self.assertEqual(inputRange[1], upper)
- songs = muse_dash_world.md_collection.get_songs_with_settings(True, False, inputRange[0], inputRange[1])
+ songs = muse_dash_world.md_collection.get_songs_with_settings(dlc_set, False, inputRange[0], inputRange[1])
for songKey in songs:
song = muse_dash_world.md_collection.song_items[songKey]
if (song.easy is not None and inputRange[0] <= song.easy <= inputRange[1]):
diff --git a/worlds/noita/Items.py b/worlds/noita/Items.py
index ca53c9623387..c859a8039494 100644
--- a/worlds/noita/Items.py
+++ b/worlds/noita/Items.py
@@ -44,20 +44,18 @@ def create_kantele(victory_condition: VictoryCondition) -> List[str]:
return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else []
-def create_random_items(multiworld: MultiWorld, player: int, random_count: int) -> List[str]:
- filler_pool = filler_weights.copy()
+def create_random_items(multiworld: MultiWorld, player: int, weights: Dict[str, int], count: int) -> List[str]:
+ filler_pool = weights.copy()
if multiworld.bad_effects[player].value == 0:
del filler_pool["Trap"]
- return multiworld.random.choices(
- population=list(filler_pool.keys()),
- weights=list(filler_pool.values()),
- k=random_count
- )
+ return multiworld.random.choices(population=list(filler_pool.keys()),
+ weights=list(filler_pool.values()),
+ k=count)
def create_all_items(multiworld: MultiWorld, player: int) -> None:
- sum_locations = len(multiworld.get_unfilled_locations(player))
+ locations_to_fill = len(multiworld.get_unfilled_locations(player))
itempool = (
create_fixed_item_pool()
@@ -66,9 +64,18 @@ def create_all_items(multiworld: MultiWorld, player: int) -> None:
+ create_kantele(multiworld.victory_condition[player])
)
- random_count = sum_locations - len(itempool)
- itempool += create_random_items(multiworld, player, random_count)
-
+ # if there's not enough shop-allowed items in the pool, we can encounter gen issues
+ # 39 is the number of shop-valid items we need to guarantee
+ if len(itempool) < 39:
+ itempool += create_random_items(multiworld, player, shop_only_filler_weights, 39 - len(itempool))
+ # this is so that it passes tests and gens if you have minimal locations and only one player
+ if multiworld.players == 1:
+ for location in multiworld.get_unfilled_locations(player):
+ if "Shop Item" in location.name:
+ location.item = create_item(player, itempool.pop())
+ locations_to_fill = len(multiworld.get_unfilled_locations(player))
+
+ itempool += create_random_items(multiworld, player, filler_weights, locations_to_fill - len(itempool))
multiworld.itempool += [create_item(player, name) for name in itempool]
@@ -84,8 +91,8 @@ def create_all_items(multiworld: MultiWorld, player: int) -> None:
"Wand (Tier 2)": ItemData(110007, "Wands", ItemClassification.useful),
"Wand (Tier 3)": ItemData(110008, "Wands", ItemClassification.useful),
"Wand (Tier 4)": ItemData(110009, "Wands", ItemClassification.useful),
- "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful),
- "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful),
+ "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful, 1),
+ "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful, 1),
"Kantele": ItemData(110012, "Wands", ItemClassification.useful),
"Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression, 1),
"Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression, 1),
@@ -95,43 +102,46 @@ def create_all_items(multiworld: MultiWorld, player: int) -> None:
"Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression, 1),
"All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression, 1),
"Spatial Awareness Perk": ItemData(110020, "Perks", ItemClassification.progression),
- "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful),
+ "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful, 1),
"Orb": ItemData(110022, "Orbs", ItemClassification.progression_skip_balancing),
"Random Potion": ItemData(110023, "Items", ItemClassification.filler),
"Secret Potion": ItemData(110024, "Items", ItemClassification.filler),
"Powder Pouch": ItemData(110025, "Items", ItemClassification.filler),
"Chaos Die": ItemData(110026, "Items", ItemClassification.filler),
"Greed Die": ItemData(110027, "Items", ItemClassification.filler),
- "Kammi": ItemData(110028, "Items", ItemClassification.filler),
- "Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler),
+ "Kammi": ItemData(110028, "Items", ItemClassification.filler, 1),
+ "Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler, 1),
"Sädekivi": ItemData(110030, "Items", ItemClassification.filler),
"Broken Wand": ItemData(110031, "Items", ItemClassification.filler),
+}
+shop_only_filler_weights: Dict[str, int] = {
+ "Trap": 15,
+ "Extra Max HP": 25,
+ "Spell Refresher": 20,
+ "Wand (Tier 1)": 10,
+ "Wand (Tier 2)": 8,
+ "Wand (Tier 3)": 7,
+ "Wand (Tier 4)": 6,
+ "Wand (Tier 5)": 5,
+ "Wand (Tier 6)": 4,
+ "Extra Life Perk": 10,
}
filler_weights: Dict[str, int] = {
- "Trap": 15,
- "Extra Max HP": 25,
- "Spell Refresher": 20,
- "Potion": 40,
- "Gold (200)": 15,
- "Gold (1000)": 6,
- "Wand (Tier 1)": 10,
- "Wand (Tier 2)": 8,
- "Wand (Tier 3)": 7,
- "Wand (Tier 4)": 6,
- "Wand (Tier 5)": 5,
- "Wand (Tier 6)": 4,
- "Extra Life Perk": 10,
- "Random Potion": 9,
- "Secret Potion": 10,
- "Powder Pouch": 10,
- "Chaos Die": 4,
- "Greed Die": 4,
- "Kammi": 4,
- "Refreshing Gourd": 4,
- "Sädekivi": 3,
- "Broken Wand": 10,
+ **shop_only_filler_weights,
+ "Gold (200)": 15,
+ "Gold (1000)": 6,
+ "Potion": 40,
+ "Random Potion": 9,
+ "Secret Potion": 10,
+ "Powder Pouch": 10,
+ "Chaos Die": 4,
+ "Greed Die": 4,
+ "Kammi": 4,
+ "Refreshing Gourd": 4,
+ "Sädekivi": 3,
+ "Broken Wand": 10,
}
diff --git a/worlds/noita/Regions.py b/worlds/noita/Regions.py
index a239b437d75f..561d483b4865 100644
--- a/worlds/noita/Regions.py
+++ b/worlds/noita/Regions.py
@@ -1,5 +1,5 @@
# Regions are areas in your game that you travel to.
-from typing import Dict, Set
+from typing import Dict, Set, List
from BaseClasses import Entrance, MultiWorld, Region
from . import Locations
@@ -79,70 +79,46 @@ def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> N
# - Lake is connected to The Laboratory, since the boss is hard without specific set-ups (which means late game)
# - Snowy Depths connects to Lava Lake orb since you need digging for it, so fairly early is acceptable
# - Ancient Laboratory is connected to the Coal Pits, so that Ylialkemisti isn't sphere 1
-noita_connections: Dict[str, Set[str]] = {
- "Menu": {"Forest"},
- "Forest": {"Mines", "Floating Island", "Desert", "Snowy Wasteland"},
- "Snowy Wasteland": {"Forest"},
- "Frozen Vault": {"The Vault"},
- "Lake": {"The Laboratory"},
- "Desert": {"Forest"},
- "Floating Island": {"Forest"},
- "Pyramid": {"Hiisi Base"},
- "Overgrown Cavern": {"Sandcave", "Undeground Jungle"},
- "Sandcave": {"Overgrown Cavern"},
+noita_connections: Dict[str, List[str]] = {
+ "Menu": ["Forest"],
+ "Forest": ["Mines", "Floating Island", "Desert", "Snowy Wasteland"],
+ "Frozen Vault": ["The Vault"],
+ "Overgrown Cavern": ["Sandcave"],
###
- "Mines": {"Collapsed Mines", "Coal Pits Holy Mountain", "Lava Lake", "Forest"},
- "Collapsed Mines": {"Mines", "Dark Cave"},
- "Lava Lake": {"Mines", "Abyss Orb Room"},
- "Abyss Orb Room": {"Lava Lake"},
- "Below Lava Lake": {"Snowy Depths"},
- "Dark Cave": {"Collapsed Mines"},
- "Ancient Laboratory": {"Coal Pits"},
+ "Mines": ["Collapsed Mines", "Coal Pits Holy Mountain", "Lava Lake"],
+ "Lava Lake": ["Abyss Orb Room"],
###
- "Coal Pits Holy Mountain": {"Coal Pits"},
- "Coal Pits": {"Coal Pits Holy Mountain", "Fungal Caverns", "Snowy Depths Holy Mountain", "Ancient Laboratory"},
- "Fungal Caverns": {"Coal Pits"},
+ "Coal Pits Holy Mountain": ["Coal Pits"],
+ "Coal Pits": ["Fungal Caverns", "Snowy Depths Holy Mountain", "Ancient Laboratory"],
###
- "Snowy Depths Holy Mountain": {"Snowy Depths"},
- "Snowy Depths": {"Snowy Depths Holy Mountain", "Hiisi Base Holy Mountain", "Magical Temple", "Below Lava Lake"},
- "Magical Temple": {"Snowy Depths"},
+ "Snowy Depths Holy Mountain": ["Snowy Depths"],
+ "Snowy Depths": ["Hiisi Base Holy Mountain", "Magical Temple", "Below Lava Lake"],
###
- "Hiisi Base Holy Mountain": {"Hiisi Base"},
- "Hiisi Base": {"Hiisi Base Holy Mountain", "Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"},
- "Secret Shop": {"Hiisi Base"},
+ "Hiisi Base Holy Mountain": ["Hiisi Base"],
+ "Hiisi Base": ["Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"],
###
- "Underground Jungle Holy Mountain": {"Underground Jungle"},
- "Underground Jungle": {"Underground Jungle Holy Mountain", "Dragoncave", "Overgrown Cavern", "Vault Holy Mountain",
- "Lukki Lair"},
- "Dragoncave": {"Underground Jungle"},
- "Lukki Lair": {"Underground Jungle", "Snow Chasm", "Frozen Vault"},
- "Snow Chasm": {},
+ "Underground Jungle Holy Mountain": ["Underground Jungle"],
+ "Underground Jungle": ["Dragoncave", "Overgrown Cavern", "Vault Holy Mountain", "Lukki Lair", "Snow Chasm"],
###
- "Vault Holy Mountain": {"The Vault"},
- "The Vault": {"Vault Holy Mountain", "Frozen Vault", "Temple of the Art Holy Mountain"},
+ "Vault Holy Mountain": ["The Vault"],
+ "The Vault": ["Frozen Vault", "Temple of the Art Holy Mountain"],
###
- "Temple of the Art Holy Mountain": {"Temple of the Art"},
- "Temple of the Art": {"Temple of the Art Holy Mountain", "Laboratory Holy Mountain", "The Tower",
- "Wizards' Den"},
- "Wizards' Den": {"Temple of the Art", "Powerplant"},
- "Powerplant": {"Wizards' Den", "Deep Underground"},
- "The Tower": {"Forest"},
- "Deep Underground": {},
+ "Temple of the Art Holy Mountain": ["Temple of the Art"],
+ "Temple of the Art": ["Laboratory Holy Mountain", "The Tower", "Wizards' Den"],
+ "Wizards' Den": ["Powerplant"],
+ "Powerplant": ["Deep Underground"],
###
- "Laboratory Holy Mountain": {"The Laboratory"},
- "The Laboratory": {"Laboratory Holy Mountain", "The Work", "Friend Cave", "The Work (Hell)", "Lake"},
- "Friend Cave": {},
- "The Work": {},
- "The Work (Hell)": {},
+ "Laboratory Holy Mountain": ["The Laboratory"],
+ "The Laboratory": ["The Work", "Friend Cave", "The Work (Hell)", "Lake"],
###
}
-noita_regions: Set[str] = set(noita_connections.keys()).union(*noita_connections.values())
+noita_regions: List[str] = sorted(set(noita_connections.keys()).union(*noita_connections.values()))
diff --git a/worlds/noita/Rules.py b/worlds/noita/Rules.py
index 3eb6be5a7c6d..808dd3a200a6 100644
--- a/worlds/noita/Rules.py
+++ b/worlds/noita/Rules.py
@@ -44,12 +44,10 @@ class EntranceLock(NamedTuple):
"Wand (Tier 6)", # Temple of the Art
]
-
items_hidden_from_shops: List[str] = ["Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
"Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand",
"Powder Pouch"]
-
perk_list: List[str] = list(filter(Items.item_is_perk, Items.item_table.keys()))
@@ -155,11 +153,12 @@ def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None:
def create_all_rules(multiworld: MultiWorld, player: int) -> None:
- ban_items_from_shops(multiworld, player)
- ban_early_high_tier_wands(multiworld, player)
- lock_holy_mountains_into_spheres(multiworld, player)
- holy_mountain_unlock_conditions(multiworld, player)
- biome_unlock_conditions(multiworld, player)
+ if multiworld.players > 1:
+ ban_items_from_shops(multiworld, player)
+ ban_early_high_tier_wands(multiworld, player)
+ lock_holy_mountains_into_spheres(multiworld, player)
+ holy_mountain_unlock_conditions(multiworld, player)
+ biome_unlock_conditions(multiworld, player)
victory_unlock_conditions(multiworld, player)
# Prevent the Map perk (used to find Toveri) from being on Toveri (boss)
diff --git a/worlds/oot/Entrance.py b/worlds/oot/Entrance.py
index e480c957a672..6c4b6428f53e 100644
--- a/worlds/oot/Entrance.py
+++ b/worlds/oot/Entrance.py
@@ -1,6 +1,4 @@
-
from BaseClasses import Entrance
-from .Regions import TimeOfDay
class OOTEntrance(Entrance):
game: str = 'Ocarina of Time'
@@ -29,16 +27,16 @@ def disconnect(self):
self.connected_region = None
return previously_connected
- def get_new_target(self):
+ def get_new_target(self, pool_type):
root = self.multiworld.get_region('Root Exits', self.player)
- target_entrance = OOTEntrance(self.player, self.multiworld, 'Root -> ' + self.connected_region.name, root)
+ target_entrance = OOTEntrance(self.player, self.multiworld, f'Root -> ({self.name}) ({pool_type})', root)
target_entrance.connect(self.connected_region)
target_entrance.replaces = self
root.exits.append(target_entrance)
return target_entrance
- def assume_reachable(self):
+ def assume_reachable(self, pool_type):
if self.assumed == None:
- self.assumed = self.get_new_target()
+ self.assumed = self.get_new_target(pool_type)
self.disconnect()
return self.assumed
diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py
index 3c1b2d78c6c9..bbdc30490c18 100644
--- a/worlds/oot/EntranceShuffle.py
+++ b/worlds/oot/EntranceShuffle.py
@@ -2,6 +2,7 @@
import logging
from worlds.generic.Rules import set_rule, add_rule
+from BaseClasses import CollectionState
from .Hints import get_hint_area, HintAreaNotFound
from .Regions import TimeOfDay
@@ -25,12 +26,12 @@ def set_all_entrances_data(world, player):
return_entrance.data['index'] = 0x7FFF
-def assume_entrance_pool(entrance_pool, ootworld):
+def assume_entrance_pool(entrance_pool, ootworld, pool_type):
assumed_pool = []
for entrance in entrance_pool:
- assumed_forward = entrance.assume_reachable()
+ assumed_forward = entrance.assume_reachable(pool_type)
if entrance.reverse != None and not ootworld.decouple_entrances:
- assumed_return = entrance.reverse.assume_reachable()
+ assumed_return = entrance.reverse.assume_reachable(pool_type)
if not (ootworld.mix_entrance_pools != 'off' and (ootworld.shuffle_overworld_entrances or ootworld.shuffle_special_interior_entrances)):
if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \
(entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances):
@@ -41,15 +42,15 @@ def assume_entrance_pool(entrance_pool, ootworld):
return assumed_pool
-def build_one_way_targets(world, types_to_include, exclude=(), target_region_names=()):
+def build_one_way_targets(world, pool, types_to_include, exclude=(), target_region_names=()):
one_way_entrances = []
for pool_type in types_to_include:
one_way_entrances += world.get_shufflable_entrances(type=pool_type)
valid_one_way_entrances = list(filter(lambda entrance: entrance.name not in exclude, one_way_entrances))
if target_region_names:
- return [entrance.get_new_target() for entrance in valid_one_way_entrances
+ return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances
if entrance.connected_region.name in target_region_names]
- return [entrance.get_new_target() for entrance in valid_one_way_entrances]
+ return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances]
# Abbreviations
@@ -423,14 +424,14 @@ def _add_boss_entrances():
}
interior_entrance_bias = {
- 'Kakariko Village -> Kak Potion Shop Front': 4,
- 'Kak Backyard -> Kak Potion Shop Back': 4,
- 'Kakariko Village -> Kak Impas House': 3,
- 'Kak Impas Ledge -> Kak Impas House Back': 3,
- 'Goron City -> GC Shop': 2,
- 'Zoras Domain -> ZD Shop': 2,
+ 'ToT Entrance -> Temple of Time': 4,
+ 'Kakariko Village -> Kak Potion Shop Front': 3,
+ 'Kak Backyard -> Kak Potion Shop Back': 3,
+ 'Kakariko Village -> Kak Impas House': 2,
+ 'Kak Impas Ledge -> Kak Impas House Back': 2,
'Market Entrance -> Market Guard House': 2,
- 'ToT Entrance -> Temple of Time': 1,
+ 'Goron City -> GC Shop': 1,
+ 'Zoras Domain -> ZD Shop': 1,
}
@@ -443,7 +444,8 @@ def shuffle_random_entrances(ootworld):
player = ootworld.player
# Gather locations to keep reachable for validation
- all_state = world.get_all_state(use_cache=True)
+ all_state = ootworld.get_state_with_complete_itempool()
+ all_state.sweep_for_events(locations=ootworld.get_locations())
locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}
# Set entrance data for all entrances
@@ -523,12 +525,12 @@ def shuffle_random_entrances(ootworld):
for pool_type, entrance_pool in one_way_entrance_pools.items():
if pool_type == 'OwlDrop':
valid_target_types = ('WarpSong', 'OwlDrop', 'Overworld', 'Extra')
- one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time'])
+ one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time'])
for target in one_way_target_entrance_pools[pool_type]:
set_rule(target, lambda state: state._oot_reach_as_age(target.parent_region, 'child', player))
elif pool_type in {'Spawn', 'WarpSong'}:
valid_target_types = ('Spawn', 'WarpSong', 'OwlDrop', 'Overworld', 'Interior', 'SpecialInterior', 'Extra')
- one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types)
+ one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types)
# Ensure that the last entrance doesn't assume the rest of the targets are reachable
for target in one_way_target_entrance_pools[pool_type]:
add_rule(target, (lambda entrances=entrance_pool: (lambda state: any(entrance.connected_region == None for entrance in entrances)))())
@@ -538,14 +540,11 @@ def shuffle_random_entrances(ootworld):
target_entrance_pools = {}
for pool_type, entrance_pool in entrance_pools.items():
- target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld)
+ target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld, pool_type)
# Build all_state and none_state
all_state = ootworld.get_state_with_complete_itempool()
- none_state = all_state.copy()
- for item_tuple in none_state.prog_items:
- if item_tuple[1] == player:
- none_state.prog_items[item_tuple] = 0
+ none_state = CollectionState(ootworld.multiworld)
# Plando entrances
if world.plando_connections[player]:
@@ -628,7 +627,7 @@ def shuffle_random_entrances(ootworld):
logging.getLogger('').error(f'Root Exit: {exit} -> {exit.connected_region}')
logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances')
# Game is beatable
- new_all_state = world.get_all_state(use_cache=False)
+ new_all_state = ootworld.get_state_with_complete_itempool()
if not world.has_beaten_game(new_all_state, player):
raise EntranceShuffleError('Cannot beat game')
# Validate world
@@ -700,7 +699,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al
raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}')
-def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20):
+def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=10):
restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances)
@@ -745,7 +744,6 @@ def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollback
def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entrances):
- world = ootworld.multiworld
player = ootworld.player
# Disconnect all root assumed entrances and save original connections
@@ -755,7 +753,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran
if entrance.connected_region:
original_connected_regions[entrance] = entrance.disconnect()
- all_state = world.get_all_state(use_cache=False)
+ all_state = ootworld.get_state_with_complete_itempool()
restrictive_entrances = []
soft_entrances = []
@@ -793,8 +791,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
all_state = all_state_orig.copy()
none_state = none_state_orig.copy()
- all_state.sweep_for_events()
- none_state.sweep_for_events()
+ all_state.sweep_for_events(locations=ootworld.get_locations())
+ none_state.sweep_for_events(locations=ootworld.get_locations())
if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions:
time_travel_state = none_state.copy()
diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py
index 94e1011ddc63..6ca6bc9268a9 100644
--- a/worlds/oot/ItemPool.py
+++ b/worlds/oot/ItemPool.py
@@ -350,7 +350,7 @@ def generate_itempool(ootworld):
ootworld.itempool = [ootworld.create_item(item) for item in pool]
for (location_name, item) in placed_items.items():
location = world.get_location(location_name, player)
- location.place_locked_item(ootworld.create_item(item))
+ location.place_locked_item(ootworld.create_item(item, allow_arbitrary_name=True))
def get_pool_core(world):
@@ -675,7 +675,7 @@ def get_pool_core(world):
world.remove_from_start_inventory.append('Scarecrow Song')
if world.no_epona_race:
- world.multiworld.push_precollected(world.create_item('Epona'))
+ world.multiworld.push_precollected(world.create_item('Epona', allow_arbitrary_name=True))
world.remove_from_start_inventory.append('Epona')
if world.shuffle_smallkeys == 'vanilla':
diff --git a/worlds/oot/Location.py b/worlds/oot/Location.py
index e2b0e52e4dc5..3f7d75517e30 100644
--- a/worlds/oot/Location.py
+++ b/worlds/oot/Location.py
@@ -2,6 +2,8 @@
from .LocationList import location_table
from BaseClasses import Location
+non_indexed_location_types = {'Boss', 'Event', 'Drop', 'HintStone', 'Hint'}
+
location_id_offset = 67000
locnames_pre_70 = {
"Gift from Sages",
@@ -18,7 +20,7 @@
else 0)
location_name_to_id = {name: (location_id_offset + index) for (index, name) in enumerate(new_name_order)
- if location_table[name][0] not in {'Boss', 'Event', 'Drop', 'HintStone', 'Hint'}}
+ if location_table[name][0] not in non_indexed_location_types}
class DisableType(Enum):
ENABLED = 0
@@ -83,3 +85,57 @@ def LocationFactory(locations, player: int):
return ret
+def build_location_name_groups() -> dict:
+
+ def fix_sing(t) -> tuple:
+ if isinstance(t, str):
+ return (t,)
+ return t
+
+ def rename(d, k1, k2) -> None:
+ d[k2] = d[k1]
+ del d[k1]
+
+ # whoever wrote the location table didn't realize they need to add a comma to mark a singleton as a tuple
+ # so we have to check types unfortunately
+ tags = set()
+ for v in location_table.values():
+ if v[5] is not None:
+ tags.update(fix_sing(v[5]))
+
+ sorted_tags = sorted(list(tags))
+
+ ret = {
+ tag: {k for k, v in location_table.items()
+ if v[5] is not None
+ and tag in fix_sing(v[5])
+ and v[0] not in non_indexed_location_types}
+ for tag in sorted_tags
+ }
+
+ # Delete tags which are a combination of other tags
+ del ret['Death Mountain']
+ del ret['Forest']
+ del ret['Gerudo']
+ del ret['Kakariko']
+ del ret['Market']
+
+ # Delete Vanilla and MQ tags because they are just way too broad
+ del ret['Vanilla']
+ del ret['Master Quest']
+
+ rename(ret, 'Beehive', 'Beehives')
+ rename(ret, 'Cow', 'Cows')
+ rename(ret, 'Crate', 'Crates')
+ rename(ret, 'Deku Scrub', 'Deku Scrubs')
+ rename(ret, 'FlyingPot', 'Flying Pots')
+ rename(ret, 'Freestanding', 'Freestanding Items')
+ rename(ret, 'Pot', 'Pots')
+ rename(ret, 'RupeeTower', 'Rupee Groups')
+ rename(ret, 'SmallCrate', 'Small Crates')
+ rename(ret, 'the Market', 'Market')
+ rename(ret, 'the Graveyard', 'Graveyard')
+ rename(ret, 'the Lost Woods', 'Lost Woods')
+
+ return ret
+
diff --git a/worlds/oot/LocationList.py b/worlds/oot/LocationList.py
index 3f4602c428c1..27ad575699f5 100644
--- a/worlds/oot/LocationList.py
+++ b/worlds/oot/LocationList.py
@@ -238,7 +238,7 @@ def shop_address(shop_id, shelf_id):
("Market Night Green Rupee Crate 1", ("Crate", 0x21, (0,0,24), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
("Market Night Green Rupee Crate 2", ("Crate", 0x21, (0,0,25), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
("Market Night Green Rupee Crate 3", ("Crate", 0x21, (0,0,26), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
- ("Market Dog Lady House Crate", ("Crate", 0x35, (0,0,3), None, 'Rupees (5)', ("Market", "Market", "Crate"))),
+ ("Market Dog Lady House Crate", ("Crate", 0x35, (0,0,3), None, 'Rupees (5)', ("the Market", "Market", "Crate"))),
("Market Guard House Child Crate", ("Crate", 0x4D, (0,0,6), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
("Market Guard House Child Pot 1", ("Pot", 0x4D, (0,0,9), None, 'Rupee (1)', ("the Market", "Market", "Pot"))),
("Market Guard House Child Pot 2", ("Pot", 0x4D, (0,0,10), None, 'Rupee (1)', ("the Market", "Market", "Pot"))),
diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py
index 03f5346ceeed..120027e29dfa 100644
--- a/worlds/oot/Options.py
+++ b/worlds/oot/Options.py
@@ -30,7 +30,17 @@ def from_any(cls, data: typing.Any) -> Range:
class Logic(Choice):
- """Set the logic used for the generator."""
+ """Set the logic used for the generator.
+ Glitchless: Normal gameplay. Can enable more difficult logical paths using the Logic Tricks option.
+ Glitched: Many powerful glitches expected, such as bomb hovering and clipping.
+ Glitched is incompatible with the following settings:
+ - All forms of entrance randomizer
+ - MQ dungeons
+ - Pot shuffle
+ - Freestanding item shuffle
+ - Crate shuffle
+ - Beehive shuffle
+ No Logic: No logic is used when placing items. Not recommended for most players."""
display_name = "Logic Rules"
option_glitchless = 0
option_glitched = 1
@@ -38,12 +48,16 @@ class Logic(Choice):
class NightTokens(Toggle):
- """Nighttime skulltulas will logically require Sun's Song."""
+ """When enabled, nighttime skulltulas logically require Sun's Song."""
display_name = "Nighttime Skulltulas Expect Sun's Song"
class Forest(Choice):
- """Set the state of Kokiri Forest and the path to Deku Tree."""
+ """Set the state of Kokiri Forest and the path to Deku Tree.
+ Open: Neither the forest exit nor the path to Deku Tree is blocked.
+ Closed Deku: The forest exit is not blocked; the path to Deku Tree requires Kokiri Sword and Deku Shield.
+ Closed: Path to Deku Tree requires sword and shield. The forest exit is blocked until Deku Tree is beaten.
+ Closed forest will force child start, and becomes Closed Deku if interior entrances, overworld entrances, warp songs, or random spawn positions are enabled."""
display_name = "Forest"
option_open = 0
option_closed_deku = 1
@@ -53,7 +67,10 @@ class Forest(Choice):
class Gate(Choice):
- """Set the state of the Kakariko Village gate."""
+ """Set the state of the Kakariko Village gate for child. The gate is always open as adult.
+ Open: The gate starts open. Happy Mask Shop opens upon receiving Zelda's Letter.
+ Zelda: The gate and Mask Shop open upon receiving Zelda's Letter, without needing to show it to the guard.
+ Closed: Vanilla behavior; the gate and Mask Shop open upon showing Zelda's Letter to the gate guard."""
display_name = "Kakariko Gate"
option_open = 0
option_zelda = 1
@@ -61,12 +78,15 @@ class Gate(Choice):
class DoorOfTime(DefaultOnToggle):
- """Open the Door of Time by default, without the Song of Time."""
+ """When enabled, the Door of Time starts opened, without needing Song of Time."""
display_name = "Open Door of Time"
class Fountain(Choice):
- """Set the state of King Zora, blocking the way to Zora's Fountain."""
+ """Set the state of King Zora, blocking the way to Zora's Fountain.
+ Open: King Zora starts moved as both ages. Ruto's Letter is removed.
+ Adult: King Zora must be moved as child, but is always moved for adult.
+ Closed: Vanilla behavior; King Zora must be shown Ruto's Letter as child to move him as both ages."""
display_name = "Zora's Fountain"
option_open = 0
option_adult = 1
@@ -75,7 +95,10 @@ class Fountain(Choice):
class Fortress(Choice):
- """Set the requirements for access to Gerudo Fortress."""
+ """Set the requirements for access to Gerudo Fortress.
+ Normal: Vanilla behavior; all four carpenters must be rescued.
+ Fast: Only one carpenter must be rescued, which is the one in the bottom-left of the fortress.
+ Open: The Gerudo Valley bridge starts repaired. Gerudo Membership Card is given to start if not shuffled."""
display_name = "Gerudo Fortress"
option_normal = 0
option_fast = 1
@@ -84,7 +107,14 @@ class Fortress(Choice):
class Bridge(Choice):
- """Set the requirements for the Rainbow Bridge."""
+ """Set the requirements for the Rainbow Bridge.
+ Open: The bridge is always present.
+ Vanilla: Bridge requires Shadow Medallion, Spirit Medallion, and Light Arrows.
+ Stones: Bridge requires a configurable amount of Spiritual Stones.
+ Medallions: Bridge requires a configurable amount of medallions.
+ Dungeons: Bridge requires a configurable amount of rewards (stones + medallions).
+ Tokens: Bridge requires a configurable amount of gold skulltula tokens.
+ Hearts: Bridge requires a configurable amount of hearts."""
display_name = "Rainbow Bridge Requirement"
option_open = 0
option_vanilla = 1
@@ -122,8 +152,9 @@ class StartingAge(Choice):
class InteriorEntrances(Choice):
- """Shuffles interior entrances. "Simple" shuffles houses and Great Fairies; "All" includes Windmill, Link's House,
- Temple of Time, and Kak potion shop."""
+ """Shuffles interior entrances.
+ Simple: Houses and Great Fairies are shuffled.
+ All: In addition to Simple, includes Windmill, Link's House, Temple of Time, and the Kakariko potion shop."""
display_name = "Shuffle Interior Entrances"
option_off = 0
option_simple = 1
@@ -137,7 +168,9 @@ class GrottoEntrances(Toggle):
class DungeonEntrances(Choice):
- """Shuffles dungeon entrances. Opens Deku, Fire and BotW to both ages. "All" includes Ganon's Castle."""
+ """Shuffles dungeon entrances. When enabled, both ages will have access to Fire Temple, Bottom of the Well, and Deku Tree.
+ Simple: Shuffle dungeon entrances except for Ganon's Castle.
+ All: Include Ganon's Castle as well."""
display_name = "Shuffle Dungeon Entrances"
option_off = 0
option_simple = 1
@@ -146,7 +179,9 @@ class DungeonEntrances(Choice):
class BossEntrances(Choice):
- """Shuffles boss entrances. "Limited" prevents age-mixing of bosses."""
+ """Shuffles boss entrances.
+ Limited: Bosses will be limited to the ages that typically fight them.
+ Full: Bosses may be fought as different ages than usual. Child can defeat Phantom Ganon and Bongo Bongo."""
display_name = "Shuffle Boss Entrances"
option_off = 0
option_limited = 1
@@ -178,19 +213,19 @@ class SpawnPositions(Choice):
alias_true = 3
-class MixEntrancePools(Choice):
- """Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all"
- mixes them in."""
- display_name = "Mix Entrance Pools"
- option_off = 0
- option_indoor = 1
- option_all = 2
+# class MixEntrancePools(Choice):
+# """Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all"
+# mixes them in."""
+# display_name = "Mix Entrance Pools"
+# option_off = 0
+# option_indoor = 1
+# option_all = 2
-class DecoupleEntrances(Toggle):
- """Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if
- overworld is shuffled."""
- display_name = "Decouple Entrances"
+# class DecoupleEntrances(Toggle):
+# """Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if
+# overworld is shuffled."""
+# display_name = "Decouple Entrances"
class TriforceHunt(Toggle):
@@ -216,13 +251,17 @@ class ExtraTriforces(Range):
class LogicalChus(Toggle):
- """Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell
- refills; bombchus open Bombchu Bowling."""
+ """Bombchus are properly considered in logic.
+ The first found pack will always have 20 chus.
+ Kokiri Shop and Bazaar will sell refills at reduced cost.
+ Bombchus open Bombchu Bowling."""
display_name = "Bombchus Considered in Logic"
class DungeonShortcuts(Choice):
- """Shortcuts to dungeon bosses are available without any requirements."""
+ """Shortcuts to dungeon bosses are available without any requirements.
+ If enabled, this will impact the logic of dungeons where shortcuts are available.
+ Choice: Use the option "dungeon_shortcuts_list" to choose shortcuts."""
display_name = "Dungeon Boss Shortcuts Mode"
option_off = 0
option_choice = 1
@@ -246,7 +285,11 @@ class DungeonShortcutsList(OptionSet):
class MQDungeons(Choice):
- """Choose between vanilla and Master Quest dungeon layouts."""
+ """Choose between vanilla and Master Quest dungeon layouts.
+ Vanilla: All layouts are vanilla.
+ MQ: All layouts are Master Quest.
+ Specific: Use the option "mq_dungeons_list" to choose which dungeons are MQ.
+ Count: Use the option "mq_dungeons_count" to choose a number of random dungeons as MQ."""
display_name = "MQ Dungeon Mode"
option_vanilla = 0
option_mq = 1
@@ -255,7 +298,7 @@ class MQDungeons(Choice):
class MQDungeonList(OptionSet):
- """Chosen dungeons to be MQ layout."""
+ """With MQ dungeons as Specific: chosen dungeons to be MQ layout."""
display_name = "MQ Dungeon List"
valid_keys = {
"Deku Tree",
@@ -274,41 +317,41 @@ class MQDungeonList(OptionSet):
class MQDungeonCount(TrackRandomRange):
- """Number of MQ dungeons, chosen randomly."""
+ """With MQ dungeons as Count: number of randomly-selected dungeons to be MQ layout."""
display_name = "MQ Dungeon Count"
range_start = 0
range_end = 12
default = 0
-class EmptyDungeons(Choice):
- """Pre-completed dungeons are barren and rewards are given for free."""
- display_name = "Pre-completed Dungeons Mode"
- option_none = 0
- option_specific = 1
- option_count = 2
+# class EmptyDungeons(Choice):
+# """Pre-completed dungeons are barren and rewards are given for free."""
+# display_name = "Pre-completed Dungeons Mode"
+# option_none = 0
+# option_specific = 1
+# option_count = 2
-class EmptyDungeonList(OptionSet):
- """Chosen dungeons to be pre-completed."""
- display_name = "Pre-completed Dungeon List"
- valid_keys = {
- "Deku Tree",
- "Dodongo's Cavern",
- "Jabu Jabu's Belly",
- "Forest Temple",
- "Fire Temple",
- "Water Temple",
- "Shadow Temple",
- "Spirit Temple",
- }
+# class EmptyDungeonList(OptionSet):
+# """Chosen dungeons to be pre-completed."""
+# display_name = "Pre-completed Dungeon List"
+# valid_keys = {
+# "Deku Tree",
+# "Dodongo's Cavern",
+# "Jabu Jabu's Belly",
+# "Forest Temple",
+# "Fire Temple",
+# "Water Temple",
+# "Shadow Temple",
+# "Spirit Temple",
+# }
-class EmptyDungeonCount(Range):
- display_name = "Pre-completed Dungeon Count"
- range_start = 1
- range_end = 8
- default = 2
+# class EmptyDungeonCount(Range):
+# display_name = "Pre-completed Dungeon Count"
+# range_start = 1
+# range_end = 8
+# default = 2
world_options: typing.Dict[str, type(Option)] = {
@@ -341,59 +384,8 @@ class EmptyDungeonCount(Range):
}
-# class LacsCondition(Choice):
-# """Set the requirements for the Light Arrow Cutscene in the Temple of Time."""
-# display_name = "Light Arrow Cutscene Requirement"
-# option_vanilla = 0
-# option_stones = 1
-# option_medallions = 2
-# option_dungeons = 3
-# option_tokens = 4
-
-
-# class LacsStones(Range):
-# """Set the number of Spiritual Stones required for LACS."""
-# display_name = "Spiritual Stones Required for LACS"
-# range_start = 0
-# range_end = 3
-# default = 3
-
-
-# class LacsMedallions(Range):
-# """Set the number of medallions required for LACS."""
-# display_name = "Medallions Required for LACS"
-# range_start = 0
-# range_end = 6
-# default = 6
-
-
-# class LacsRewards(Range):
-# """Set the number of dungeon rewards required for LACS."""
-# display_name = "Dungeon Rewards Required for LACS"
-# range_start = 0
-# range_end = 9
-# default = 9
-
-
-# class LacsTokens(Range):
-# """Set the number of Gold Skulltula Tokens required for LACS."""
-# display_name = "Tokens Required for LACS"
-# range_start = 0
-# range_end = 100
-# default = 40
-
-
-# lacs_options: typing.Dict[str, type(Option)] = {
-# "lacs_condition": LacsCondition,
-# "lacs_stones": LacsStones,
-# "lacs_medallions": LacsMedallions,
-# "lacs_rewards": LacsRewards,
-# "lacs_tokens": LacsTokens,
-# }
-
-
class BridgeStones(Range):
- """Set the number of Spiritual Stones required for the rainbow bridge."""
+ """With Stones bridge: set the number of Spiritual Stones required."""
display_name = "Spiritual Stones Required for Bridge"
range_start = 0
range_end = 3
@@ -401,7 +393,7 @@ class BridgeStones(Range):
class BridgeMedallions(Range):
- """Set the number of medallions required for the rainbow bridge."""
+ """With Medallions bridge: set the number of medallions required."""
display_name = "Medallions Required for Bridge"
range_start = 0
range_end = 6
@@ -409,7 +401,7 @@ class BridgeMedallions(Range):
class BridgeRewards(Range):
- """Set the number of dungeon rewards required for the rainbow bridge."""
+ """With Dungeons bridge: set the number of dungeon rewards required."""
display_name = "Dungeon Rewards Required for Bridge"
range_start = 0
range_end = 9
@@ -417,7 +409,7 @@ class BridgeRewards(Range):
class BridgeTokens(Range):
- """Set the number of Gold Skulltula Tokens required for the rainbow bridge."""
+ """With Tokens bridge: set the number of Gold Skulltula Tokens required."""
display_name = "Tokens Required for Bridge"
range_start = 0
range_end = 100
@@ -425,7 +417,7 @@ class BridgeTokens(Range):
class BridgeHearts(Range):
- """Set the number of hearts required for the rainbow bridge."""
+ """With Hearts bridge: set the number of hearts required."""
display_name = "Hearts Required for Bridge"
range_start = 4
range_end = 20
@@ -442,7 +434,15 @@ class BridgeHearts(Range):
class SongShuffle(Choice):
- """Set where songs can appear."""
+ """Set where songs can appear.
+ Song: Songs are shuffled into other song locations.
+ Dungeon: Songs are placed into end-of-dungeon locations:
+ - The 8 boss heart containers
+ - Sheik in Ice Cavern
+ - Lens of Truth chest in Bottom of the Well
+ - Ice Arrows chest in Gerudo Training Ground
+ - Impa at Hyrule Castle
+ Any: Songs can appear anywhere in the multiworld."""
display_name = "Shuffle Songs"
option_song = 0
option_dungeon = 1
@@ -450,8 +450,10 @@ class SongShuffle(Choice):
class ShopShuffle(Choice):
- """Randomizes shop contents. "fixed_number" randomizes a specific number of items per shop;
- "random_number" randomizes the value for each shop. """
+ """Randomizes shop contents.
+ Off: Shops are not randomized at all.
+ Fixed Number: Shop contents are shuffled, and a specific number of multiworld locations exist in each shop, controlled by the "shop_slots" option.
+ Random Number: Same as Fixed Number, but the number of locations per shop is random and may differ between shops."""
display_name = "Shopsanity"
option_off = 0
option_fixed_number = 1
@@ -459,15 +461,20 @@ class ShopShuffle(Choice):
class ShopSlots(Range):
- """Number of items per shop to be randomized into the main itempool.
- Only active if Shopsanity is set to "fixed_number." """
+ """With Shopsanity fixed number: quantity of multiworld locations per shop to be randomized."""
display_name = "Shuffled Shop Slots"
range_start = 0
range_end = 4
class ShopPrices(Choice):
- """Controls prices of shop items. "Normal" is a distribution from 0 to 300. "X Wallet" requires that wallet at max. "Affordable" is always 10 rupees."""
+ """Controls prices of shop locations.
+ Normal: Balanced distribution from 0 to 300.
+ Affordable: Every shop location costs 10 rupees.
+ Starting Wallet: Prices capped at 99 rupees.
+ Adult's Wallet: Prices capped at 200 rupees.
+ Giant's Wallet: Prices capped at 500 rupees.
+ Tycoon's Wallet: Prices capped at 999 rupees."""
display_name = "Shopsanity Prices"
option_normal = 0
option_affordable = 1
@@ -478,7 +485,10 @@ class ShopPrices(Choice):
class TokenShuffle(Choice):
- """Token rewards from Gold Skulltulas are shuffled into the pool."""
+ """Token rewards from Gold Skulltulas can be shuffled into the pool.
+ Dungeons: Only skulltulas in dungeons are shuffled.
+ Overworld: Only skulltulas on the overworld (all skulltulas not in dungeons) are shuffled.
+ All: Every skulltula is shuffled."""
display_name = "Tokensanity"
option_off = 0
option_dungeons = 1
@@ -487,7 +497,11 @@ class TokenShuffle(Choice):
class ScrubShuffle(Choice):
- """Shuffle the items sold by Business Scrubs, and set the prices."""
+ """Shuffle the items sold by Business Scrubs, and set the prices.
+ Off: Only the three business scrubs that sell one-time upgrades in vanilla will have items at their vanilla prices.
+ Low/"Affordable": All scrub prices are 10 rupees.
+ Regular/"Expensive": All scrub prices are vanilla.
+ Random Prices: All scrub prices are randomized between 0 and 99 rupees."""
display_name = "Scrub Shuffle"
option_off = 0
option_low = 1
@@ -513,7 +527,11 @@ class ShuffleOcarinas(Toggle):
class ShuffleChildTrade(Choice):
- """Controls the behavior of the start of the child trade quest."""
+ """Controls the behavior of the start of the child trade quest.
+ Vanilla: Malon will give you the Weird Egg at Hyrule Castle.
+ Shuffle: Malon will give you a random item, and the Weird Egg is shuffled.
+ Skip Child Zelda: The game starts with Zelda already met, Zelda's Letter obtained, and the item from Impa obtained.
+ """
display_name = "Shuffle Child Trade Item"
option_vanilla = 0
option_shuffle = 1
@@ -538,30 +556,39 @@ class ShuffleMedigoronCarpet(Toggle):
class ShuffleFreestanding(Choice):
- """Shuffles freestanding rupees, recovery hearts, Shadow Temple Spinning Pots, and Goron Pot."""
+ """Shuffles freestanding rupees, recovery hearts, Shadow Temple Spinning Pots, and Goron Pot drops.
+ Dungeons: Only freestanding items in dungeons are shuffled.
+ Overworld: Only freestanding items in the overworld are shuffled.
+ All: All freestanding items are shuffled."""
display_name = "Shuffle Rupees & Hearts"
option_off = 0
- option_all = 1
+ option_dungeons = 1
option_overworld = 2
- option_dungeons = 3
+ option_all = 3
class ShufflePots(Choice):
- """Shuffles pots and flying pots which normally contain an item."""
+ """Shuffles pots and flying pots which normally contain an item.
+ Dungeons: Only pots in dungeons are shuffled.
+ Overworld: Only pots in the overworld are shuffled.
+ All: All pots are shuffled."""
display_name = "Shuffle Pots"
option_off = 0
- option_all = 1
+ option_dungeons = 1
option_overworld = 2
- option_dungeons = 3
+ option_all = 3
class ShuffleCrates(Choice):
- """Shuffles large and small crates containing an item."""
+ """Shuffles large and small crates containing an item.
+ Dungeons: Only crates in dungeons are shuffled.
+ Overworld: Only crates in the overworld are shuffled.
+ All: All crates are shuffled."""
display_name = "Shuffle Crates"
option_off = 0
- option_all = 1
+ option_dungeons = 1
option_overworld = 2
- option_dungeons = 3
+ option_all = 3
class ShuffleBeehives(Toggle):
@@ -597,72 +624,113 @@ class ShuffleFrogRupees(Toggle):
class ShuffleMapCompass(Choice):
- """Control where to shuffle dungeon maps and compasses."""
+ """Control where to shuffle dungeon maps and compasses.
+ Remove: There will be no maps or compasses in the itempool.
+ Startwith: You start with all maps and compasses.
+ Vanilla: Maps and compasses remain vanilla.
+ Dungeon: Maps and compasses are shuffled within their original dungeon.
+ Regional: Maps and compasses are shuffled only in regions near the original dungeon.
+ Overworld: Maps and compasses are shuffled locally outside of dungeons.
+ Any Dungeon: Maps and compasses are shuffled locally in any dungeon.
+ Keysanity: Maps and compasses can be anywhere in the multiworld."""
display_name = "Maps & Compasses"
option_remove = 0
option_startwith = 1
option_vanilla = 2
option_dungeon = 3
- option_overworld = 4
- option_any_dungeon = 5
- option_keysanity = 6
- option_regional = 7
+ option_regional = 4
+ option_overworld = 5
+ option_any_dungeon = 6
+ option_keysanity = 7
default = 1
- alias_anywhere = 6
+ alias_anywhere = 7
class ShuffleKeys(Choice):
- """Control where to shuffle dungeon small keys."""
+ """Control where to shuffle dungeon small keys.
+ Remove/"Keysy": There will be no small keys in the itempool. All small key doors are automatically unlocked.
+ Vanilla: Small keys remain vanilla. You may start with extra small keys in some dungeons to prevent softlocks.
+ Dungeon: Small keys are shuffled within their original dungeon.
+ Regional: Small keys are shuffled only in regions near the original dungeon.
+ Overworld: Small keys are shuffled locally outside of dungeons.
+ Any Dungeon: Small keys are shuffled locally in any dungeon.
+ Keysanity: Small keys can be anywhere in the multiworld."""
display_name = "Small Keys"
option_remove = 0
option_vanilla = 2
option_dungeon = 3
- option_overworld = 4
- option_any_dungeon = 5
- option_keysanity = 6
- option_regional = 7
+ option_regional = 4
+ option_overworld = 5
+ option_any_dungeon = 6
+ option_keysanity = 7
default = 3
alias_keysy = 0
- alias_anywhere = 6
+ alias_anywhere = 7
class ShuffleGerudoKeys(Choice):
- """Control where to shuffle the Thieves' Hideout small keys."""
+ """Control where to shuffle the Thieves' Hideout small keys.
+ Vanilla: Hideout keys remain vanilla.
+ Regional: Hideout keys are shuffled only in the Gerudo Valley/Desert Colossus area.
+ Overworld: Hideout keys are shuffled locally outside of dungeons.
+ Any Dungeon: Hideout keys are shuffled locally in any dungeon.
+ Keysanity: Hideout keys can be anywhere in the multiworld."""
display_name = "Thieves' Hideout Keys"
option_vanilla = 0
- option_overworld = 1
- option_any_dungeon = 2
- option_keysanity = 3
- option_regional = 4
- alias_anywhere = 3
+ option_regional = 1
+ option_overworld = 2
+ option_any_dungeon = 3
+ option_keysanity = 4
+ alias_anywhere = 4
class ShuffleBossKeys(Choice):
- """Control where to shuffle boss keys, except the Ganon's Castle Boss Key."""
+ """Control where to shuffle boss keys, except the Ganon's Castle Boss Key.
+ Remove/"Keysy": There will be no boss keys in the itempool. All boss key doors are automatically unlocked.
+ Vanilla: Boss keys remain vanilla. You may start with extra small keys in some dungeons to prevent softlocks.
+ Dungeon: Boss keys are shuffled within their original dungeon.
+ Regional: Boss keys are shuffled only in regions near the original dungeon.
+ Overworld: Boss keys are shuffled locally outside of dungeons.
+ Any Dungeon: Boss keys are shuffled locally in any dungeon.
+ Keysanity: Boss keys can be anywhere in the multiworld."""
display_name = "Boss Keys"
option_remove = 0
option_vanilla = 2
option_dungeon = 3
- option_overworld = 4
- option_any_dungeon = 5
- option_keysanity = 6
- option_regional = 7
+ option_regional = 4
+ option_overworld = 5
+ option_any_dungeon = 6
+ option_keysanity = 7
default = 3
alias_keysy = 0
- alias_anywhere = 6
+ alias_anywhere = 7
class ShuffleGanonBK(Choice):
- """Control how to shuffle the Ganon's Castle Boss Key."""
+ """Control how to shuffle the Ganon's Castle Boss Key (GCBK).
+ Remove: GCBK is removed, and the boss key door is automatically unlocked.
+ Vanilla: GCBK remains vanilla.
+ Dungeon: GCBK is shuffled within its original dungeon.
+ Regional: GCBK is shuffled only in Hyrule Field, Market, and Hyrule Castle areas.
+ Overworld: GCBK is shuffled locally outside of dungeons.
+ Any Dungeon: GCBK is shuffled locally in any dungeon.
+ Keysanity: GCBK can be anywhere in the multiworld.
+ On LACS: GCBK is on the Light Arrow Cutscene, which requires Shadow and Spirit Medallions.
+ Stones: GCBK will be awarded when reaching the target number of Spiritual Stones.
+ Medallions: GCBK will be awarded when reaching the target number of medallions.
+ Dungeons: GCBK will be awarded when reaching the target number of dungeon rewards.
+ Tokens: GCBK will be awarded when reaching the target number of Gold Skulltula Tokens.
+ Hearts: GCBK will be awarded when reaching the target number of hearts.
+ """
display_name = "Ganon's Boss Key"
option_remove = 0
option_vanilla = 2
option_dungeon = 3
- option_overworld = 4
- option_any_dungeon = 5
- option_keysanity = 6
- option_on_lacs = 7
- option_regional = 8
+ option_regional = 4
+ option_overworld = 5
+ option_any_dungeon = 6
+ option_keysanity = 7
+ option_on_lacs = 8
option_stones = 9
option_medallions = 10
option_dungeons = 11
@@ -670,7 +738,7 @@ class ShuffleGanonBK(Choice):
option_hearts = 13
default = 0
alias_keysy = 0
- alias_anywhere = 6
+ alias_anywhere = 7
class EnhanceMC(Toggle):
@@ -679,7 +747,7 @@ class EnhanceMC(Toggle):
class GanonBKMedallions(Range):
- """Set how many medallions are required to receive Ganon BK."""
+ """With medallions GCBK: set how many medallions are required to receive GCBK."""
display_name = "Medallions Required for Ganon's BK"
range_start = 1
range_end = 6
@@ -687,7 +755,7 @@ class GanonBKMedallions(Range):
class GanonBKStones(Range):
- """Set how many Spiritual Stones are required to receive Ganon BK."""
+ """With stones GCBK: set how many Spiritual Stones are required to receive GCBK."""
display_name = "Spiritual Stones Required for Ganon's BK"
range_start = 1
range_end = 3
@@ -695,7 +763,7 @@ class GanonBKStones(Range):
class GanonBKRewards(Range):
- """Set how many dungeon rewards are required to receive Ganon BK."""
+ """With dungeons GCBK: set how many dungeon rewards are required to receive GCBK."""
display_name = "Dungeon Rewards Required for Ganon's BK"
range_start = 1
range_end = 9
@@ -703,7 +771,7 @@ class GanonBKRewards(Range):
class GanonBKTokens(Range):
- """Set how many Gold Skulltula Tokens are required to receive Ganon BK."""
+ """With tokens GCBK: set how many Gold Skulltula Tokens are required to receive GCBK."""
display_name = "Tokens Required for Ganon's BK"
range_start = 1
range_end = 100
@@ -711,7 +779,7 @@ class GanonBKTokens(Range):
class GanonBKHearts(Range):
- """Set how many hearts are required to receive Ganon BK."""
+ """With hearts GCBK: set how many hearts are required to receive GCBK."""
display_name = "Hearts Required for Ganon's BK"
range_start = 4
range_end = 20
@@ -719,7 +787,9 @@ class GanonBKHearts(Range):
class KeyRings(Choice):
- """Dungeons have all small keys found at once, rather than individually."""
+ """A key ring grants all dungeon small keys at once, rather than individually.
+ Choose: Use the option "key_rings_list" to choose which dungeons have key rings.
+ All: All dungeons have key rings instead of small keys."""
display_name = "Key Rings Mode"
option_off = 0
option_choose = 1
@@ -728,7 +798,7 @@ class KeyRings(Choice):
class KeyRingList(OptionSet):
- """Select areas with keyrings rather than individual small keys."""
+ """With key rings as Choose: select areas with key rings rather than individual small keys."""
display_name = "Key Ring Areas"
valid_keys = {
"Thieves' Hideout",
@@ -828,7 +898,8 @@ class BigPoeCount(Range):
class FAETorchCount(Range):
- """Number of lit torches required to open Shadow Temple."""
+ """Number of lit torches required to open Shadow Temple.
+ Does not affect logic; use the trick Shadow Temple Entry with Fire Arrows if desired."""
display_name = "Fire Arrow Entry Torch Count"
range_start = 1
range_end = 24
@@ -853,7 +924,11 @@ class FAETorchCount(Range):
class CorrectChestAppearance(Choice):
- """Changes chest textures and/or sizes to match their contents. "Classic" is the old behavior of CSMC."""
+ """Changes chest textures and/or sizes to match their contents.
+ Off: All chests have their vanilla size/appearance.
+ Textures: Chest textures reflect their contents.
+ Both: Like Textures, but progression items and boss keys get big chests, and other items get small chests.
+ Classic: Old behavior of CSMC; textures distinguish keys from non-keys, and size distinguishes importance."""
display_name = "Chest Appearance Matches Contents"
option_off = 0
option_textures = 1
@@ -872,15 +947,24 @@ class InvisibleChests(Toggle):
class CorrectPotCrateAppearance(Choice):
- """Unchecked pots and crates have a different texture; unchecked beehives will wiggle. With textures_content, pots and crates have an appearance based on their contents; with textures_unchecked, all unchecked pots/crates have the same appearance."""
+ """Changes the appearance of pots, crates, and beehives that contain items.
+ Off: Vanilla appearance for all containers.
+ Textures (Content): Unchecked pots and crates have a texture reflecting their contents. Unchecked beehives with progression items will wiggle.
+ Textures (Unchecked): Unchecked pots and crates are golden. Unchecked beehives will wiggle.
+ """
display_name = "Pot, Crate, and Beehive Appearance"
option_off = 0
option_textures_content = 1
option_textures_unchecked = 2
+ default = 2
class Hints(Choice):
- """Gossip Stones can give hints about item locations."""
+ """Gossip Stones can give hints about item locations.
+ None: Gossip Stones do not give hints.
+ Mask: Gossip Stones give hints with Mask of Truth.
+ Agony: Gossip Stones give hints wtih Stone of Agony.
+ Always: Gossip Stones always give hints."""
display_name = "Gossip Stones"
option_none = 0
option_mask = 1
@@ -895,7 +979,9 @@ class MiscHints(DefaultOnToggle):
class HintDistribution(Choice):
- """Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc."""
+ """Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.
+ Detailed documentation on hint distributions can be found on the Archipelago GitHub or OoTRandomizer.com.
+ The Async hint distribution is intended for async multiworlds. It removes Way of the Hero hints to improve generation times, since they are not very useful in asyncs."""
display_name = "Hint Distribution"
option_balanced = 0
option_ddr = 1
@@ -907,10 +993,13 @@ class HintDistribution(Choice):
option_useless = 7
option_very_strong = 8
option_async = 9
+ default = 9
class TextShuffle(Choice):
- """Randomizes text in the game for comedic effect."""
+ """Randomizes text in the game for comedic effect.
+ Except Hints: does not randomize important text such as hints, small/boss key information, and item prices.
+ Complete: randomizes every textbox, including the useful ones."""
display_name = "Text Shuffle"
option_none = 0
option_except_hints = 1
@@ -946,7 +1035,8 @@ class HeroMode(Toggle):
class StartingToD(Choice):
- """Change the starting time of day."""
+ """Change the starting time of day.
+ Daytime starts at Sunrise and ends at Sunset. Default is between Morning and Noon."""
display_name = "Starting Time of Day"
option_default = 0
option_sunrise = 1
@@ -999,7 +1089,11 @@ class RupeeStart(Toggle):
}
class ItemPoolValue(Choice):
- """Changes the number of items available in the game."""
+ """Changes the number of items available in the game.
+ Plentiful: One extra copy of every major item.
+ Balanced: Original item pool.
+ Scarce: Extra copies of major items are removed. Heart containers are removed.
+ Minimal: All major item upgrades not used for locations are removed. All health is removed."""
display_name = "Item Pool"
option_plentiful = 0
option_balanced = 1
@@ -1009,7 +1103,12 @@ class ItemPoolValue(Choice):
class IceTraps(Choice):
- """Adds ice traps to the item pool."""
+ """Adds ice traps to the item pool.
+ Off: All ice traps are removed.
+ Normal: The vanilla quantity of ice traps are placed.
+ On/"Extra": There is a chance for some extra ice traps to be placed.
+ Mayhem: All added junk items are ice traps.
+ Onslaught: All junk items are replaced by ice traps, even those in the base pool."""
display_name = "Ice Traps"
option_off = 0
option_normal = 1
@@ -1021,34 +1120,27 @@ class IceTraps(Choice):
class IceTrapVisual(Choice):
- """Changes the appearance of ice traps as freestanding items."""
- display_name = "Ice Trap Appearance"
+ """Changes the appearance of traps, including other games' traps, as freestanding items."""
+ display_name = "Trap Appearance"
option_major_only = 0
option_junk_only = 1
option_anything = 2
-class AdultTradeStart(OptionSet):
- """Choose the items that can appear to start the adult trade sequence. By default it is Claim Check only."""
- display_name = "Adult Trade Sequence Items"
- default = {"Claim Check"}
- valid_keys = {
- "Pocket Egg",
- "Pocket Cucco",
- "Cojiro",
- "Odd Mushroom",
- "Poachers Saw",
- "Broken Sword",
- "Prescription",
- "Eyeball Frog",
- "Eyedrops",
- "Claim Check",
- }
-
- def __init__(self, value: typing.Iterable[str]):
- if not value:
- value = self.default
- super().__init__(value)
+class AdultTradeStart(Choice):
+ """Choose the item that starts the adult trade sequence."""
+ display_name = "Adult Trade Sequence Start"
+ option_pocket_egg = 0
+ option_pocket_cucco = 1
+ option_cojiro = 2
+ option_odd_mushroom = 3
+ option_poachers_saw = 4
+ option_broken_sword = 5
+ option_prescription = 6
+ option_eyeball_frog = 7
+ option_eyedrops = 8
+ option_claim_check = 9
+ default = 9
itempool_options: typing.Dict[str, type(Option)] = {
@@ -1068,7 +1160,7 @@ class Targeting(Choice):
class DisplayDpad(DefaultOnToggle):
- """Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots)."""
+ """Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots, mask)."""
display_name = "Display D-Pad HUD"
@@ -1191,7 +1283,6 @@ class LogicTricks(OptionList):
**world_options,
**bridge_options,
**dungeon_items_options,
- # **lacs_options,
**shuffle_options,
**timesavers_options,
**misc_options,
diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py
index ab1e75d1b997..0f1d3f4dcb8c 100644
--- a/worlds/oot/Patches.py
+++ b/worlds/oot/Patches.py
@@ -2094,10 +2094,14 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name
if not world.dungeon_mq['Ganons Castle']:
chest_name = 'Ganons Castle Light Trial Lullaby Chest'
location = world.get_location(chest_name)
- if location.item.game == 'Ocarina of Time':
- item = read_rom_item(rom, location.item.index)
+ if not location.item.trap:
+ if location.item.game == 'Ocarina of Time':
+ item = read_rom_item(rom, location.item.index)
+ else:
+ item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
else:
- item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
+ looks_like_index = get_override_entry(world, location)[5]
+ item = read_rom_item(rom, looks_like_index)
if item['chest_type'] in (GOLD_CHEST, GILDED_CHEST, SKULL_CHEST_BIG):
rom.write_int16(0x321B176, 0xFC40) # original 0xFC48
@@ -2106,10 +2110,14 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name
chest_name = 'Spirit Temple Compass Chest'
chest_address = 0x2B6B07C
location = world.get_location(chest_name)
- if location.item.game == 'Ocarina of Time':
- item = read_rom_item(rom, location.item.index)
+ if not location.item.trap:
+ if location.item.game == 'Ocarina of Time':
+ item = read_rom_item(rom, location.item.index)
+ else:
+ item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
else:
- item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
+ looks_like_index = get_override_entry(world, location)[5]
+ item = read_rom_item(rom, looks_like_index)
if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL):
rom.write_int16(chest_address + 2, 0x0190) # X pos
rom.write_int16(chest_address + 6, 0xFABC) # Z pos
@@ -2120,10 +2128,14 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name
chest_address_0 = 0x21A02D0 # Address in setup 0
chest_address_2 = 0x21A06E4 # Address in setup 2
location = world.get_location(chest_name)
- if location.item.game == 'Ocarina of Time':
- item = read_rom_item(rom, location.item.index)
+ if not location.item.trap:
+ if location.item.game == 'Ocarina of Time':
+ item = read_rom_item(rom, location.item.index)
+ else:
+ item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
else:
- item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
+ looks_like_index = get_override_entry(world, location)[5]
+ item = read_rom_item(rom, looks_like_index)
if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL):
rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos
rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos
@@ -2170,7 +2182,7 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name
'Shadow Temple': ("the \x05\x45Shadow Temple", 'Bongo Bongo', 0x7f, 0xa3),
}
for dungeon in world.dungeon_mq:
- if dungeon in ['Gerudo Training Ground', 'Ganons Castle']:
+ if dungeon in ['Thieves Hideout', 'Gerudo Training Ground', 'Ganons Castle']:
pass
elif dungeon in ['Bottom of the Well', 'Ice Cavern']:
dungeon_name, boss_name, compass_id, map_id = dungeon_list[dungeon]
diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py
index 1f44cebdcfe2..529411f6fc2c 100644
--- a/worlds/oot/Rules.py
+++ b/worlds/oot/Rules.py
@@ -1,8 +1,12 @@
from collections import deque
import logging
+import typing
from .Regions import TimeOfDay
+from .DungeonList import dungeon_table
+from .Hints import HintArea
from .Items import oot_is_item_of_type
+from .LocationList import dungeon_song_locations
from BaseClasses import CollectionState
from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item
@@ -150,11 +154,16 @@ def set_rules(ootworld):
location = world.get_location('Forest Temple MQ First Room Chest', player)
forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player)
- if ootworld.shuffle_song_items == 'song' and not ootworld.songs_as_items:
+ if ootworld.shuffle_song_items in {'song', 'dungeon'} and not ootworld.songs_as_items:
# Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else.
# This is required if map/compass included, or any_dungeon shuffle.
location = world.get_location('Sheik in Ice Cavern', player)
- add_item_rule(location, lambda item: item.player == player and oot_is_item_of_type(item, 'Song'))
+ add_item_rule(location, lambda item: oot_is_item_of_type(item, 'Song'))
+
+ if ootworld.shuffle_child_trade == 'skip_child_zelda':
+ # Song from Impa must be local
+ location = world.get_location('Song from Impa', player)
+ add_item_rule(location, lambda item: item.player == player)
for name in ootworld.always_hints:
add_rule(world.get_location(name, player), guarantee_hint)
@@ -176,11 +185,6 @@ def required_wallets(price):
return parser.parse_rule('(Progressive_Wallet, %d)' % required_wallets(location.price))
-def limit_to_itemset(location, itemset):
- old_rule = location.item_rule
- location.item_rule = lambda item: item.name in itemset and old_rule(item)
-
-
# This function should be run once after the shop items are placed in the world.
# It should be run before other items are placed in the world so that logic has
# the correct checks for them. This is safe to do since every shop is still
@@ -223,10 +227,8 @@ def set_shop_rules(ootworld):
# The goal is to automatically set item rules based on age requirements in case entrances were shuffled
def set_entrances_based_rules(ootworld):
- if ootworld.multiworld.accessibility == 'beatable':
- return
-
- all_state = ootworld.multiworld.get_all_state(False)
+ all_state = ootworld.get_state_with_complete_itempool()
+ all_state.sweep_for_events(locations=ootworld.get_locations())
for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()):
# If a shop is not reachable as adult, it can't have Goron Tunic or Zora Tunic as child can't buy these
diff --git a/worlds/oot/Utils.py b/worlds/oot/Utils.py
index c2444cd1fee9..9faffbdeddfc 100644
--- a/worlds/oot/Utils.py
+++ b/worlds/oot/Utils.py
@@ -11,7 +11,7 @@ def data_path(*args):
return os.path.join(os.path.dirname(__file__), 'data', *args)
-@lru_cache(maxsize=13) # Cache Overworld.json and the 12 dungeons
+@lru_cache
def read_json(file_path):
json_string = ""
with io.open(file_path, 'r') as file:
diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py
index 539abd96747f..e9c889d6f653 100644
--- a/worlds/oot/__init__.py
+++ b/worlds/oot/__init__.py
@@ -10,7 +10,7 @@
logger = logging.getLogger("Ocarina of Time")
-from .Location import OOTLocation, LocationFactory, location_name_to_id
+from .Location import OOTLocation, LocationFactory, location_name_to_id, build_location_name_groups
from .Entrance import OOTEntrance
from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError
from .HintList import getRequiredHints
@@ -43,14 +43,14 @@
class OOTCollectionState(metaclass=AutoLogicRegister):
def init_mixin(self, parent: MultiWorld):
- all_ids = parent.get_all_ids()
- self.child_reachable_regions = {player: set() for player in all_ids}
- self.adult_reachable_regions = {player: set() for player in all_ids}
- self.child_blocked_connections = {player: set() for player in all_ids}
- self.adult_blocked_connections = {player: set() for player in all_ids}
- self.day_reachable_regions = {player: set() for player in all_ids}
- self.dampe_reachable_regions = {player: set() for player in all_ids}
- self.age = {player: None for player in all_ids}
+ oot_ids = parent.get_game_players(OOTWorld.game) + parent.get_game_groups(OOTWorld.game)
+ self.child_reachable_regions = {player: set() for player in oot_ids}
+ self.adult_reachable_regions = {player: set() for player in oot_ids}
+ self.child_blocked_connections = {player: set() for player in oot_ids}
+ self.adult_blocked_connections = {player: set() for player in oot_ids}
+ self.day_reachable_regions = {player: set() for player in oot_ids}
+ self.dampe_reachable_regions = {player: set() for player in oot_ids}
+ self.age = {player: None for player in oot_ids}
def copy_mixin(self, ret) -> CollectionState:
ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in
@@ -163,20 +163,26 @@ class OOTWorld(World):
"Bottle with Big Poe", "Bottle with Red Potion", "Bottle with Green Potion",
"Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish",
"Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"},
- "Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Odd Mushroom",
+ "Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Cojiro", "Odd Mushroom",
"Odd Potion", "Poachers Saw", "Broken Sword", "Prescription",
- "Eyeball Frog", "Eyedrops", "Claim Check"}
+ "Eyeball Frog", "Eyedrops", "Claim Check"},
}
+ location_name_groups = build_location_name_groups()
+
+
def __init__(self, world, player):
self.hint_data_available = threading.Event()
self.collectible_flags_available = threading.Event()
super(OOTWorld, self).__init__(world, player)
+
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld):
rom = Rom(file=get_options()['oot_options']['rom_file'])
+
+ # Option parsing, handling incompatible options, building useful-item table
def generate_early(self):
self.parser = Rule_AST_Transformer(self, self.player)
@@ -192,8 +198,10 @@ def generate_early(self):
option_value = result.current_key
setattr(self, option_name, option_value)
+ self.regions = [] # internal caches of regions for this world, used later
+ self._regions_cache = {}
+
self.shop_prices = {}
- self.regions = [] # internal cache of regions for this world, used later
self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory
self.starting_items = Counter()
self.songs_as_items = False
@@ -384,6 +392,7 @@ def generate_early(self):
self.mq_dungeons_mode = 'count'
self.mq_dungeons_count = 0
self.dungeon_mq = {item['name']: (item['name'] in mq_dungeons) for item in dungeon_table}
+ self.dungeon_mq['Thieves Hideout'] = False # fix for bug in SaveContext:287
# Empty dungeon placeholder for the moment
self.empty_dungeons = {name: False for name in self.dungeon_mq}
@@ -409,6 +418,9 @@ def generate_early(self):
self.starting_tod = self.starting_tod.replace('_', '-')
self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '')
+ # Convert adult trade option to expected Set
+ self.adult_trade_start = {self.adult_trade_start.title().replace('_', ' ')}
+
# Get hint distribution
self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json'))
@@ -446,7 +458,7 @@ def generate_early(self):
self.always_hints = [hint.name for hint in getRequiredHints(self)]
# Determine items which are not considered advancement based on settings. They will never be excluded.
- self.nonadvancement_items = {'Double Defense'}
+ self.nonadvancement_items = {'Double Defense', 'Deku Stick Capacity', 'Deku Nut Capacity'}
if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and
self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances):
# nayru's love may be required to prevent forced damage
@@ -483,6 +495,8 @@ def generate_early(self):
# Farore's Wind skippable if not used for this logic trick in Water Temple
self.nonadvancement_items.add('Farores Wind')
+
+ # Reads a group of regions from the given JSON file.
def load_regions_from_json(self, file_path):
region_json = read_json(file_path)
@@ -520,6 +534,10 @@ def load_regions_from_json(self, file_path):
# We still need to fill the location even if ALR is off.
logger.debug('Unreachable location: %s', new_location.name)
new_location.player = self.player
+ # Change some attributes of Drop locations
+ if new_location.type == 'Drop':
+ new_location.name = new_region.name + ' ' + new_location.name
+ new_location.show_in_spoiler = False
new_region.locations.append(new_location)
if 'events' in region:
for event, rule in region['events'].items():
@@ -549,8 +567,10 @@ def load_regions_from_json(self, file_path):
self.multiworld.regions.append(new_region)
self.regions.append(new_region)
- self.multiworld._recache()
+ self._regions_cache[new_region.name] = new_region
+
+ # Sets deku scrub prices
def set_scrub_prices(self):
# Get Deku Scrub Locations
scrub_locations = [location for location in self.get_locations() if location.type in {'Scrub', 'GrottoScrub'}]
@@ -579,6 +599,8 @@ def set_scrub_prices(self):
if location.item is not None:
location.item.price = price
+
+ # Sets prices for shuffled shop locations
def random_shop_prices(self):
shop_item_indexes = ['7', '5', '8', '6']
self.shop_prices = {}
@@ -604,6 +626,8 @@ def random_shop_prices(self):
elif self.shopsanity_prices == 'tycoons_wallet':
self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5)
+
+ # Fill boss prizes
def fill_bosses(self, bossCount=9):
boss_location_names = (
'Queen Gohma',
@@ -616,7 +640,7 @@ def fill_bosses(self, bossCount=9):
'Twinrova',
'Links Pocket'
)
- boss_rewards = [item for item in self.itempool if item.type == 'DungeonReward']
+ boss_rewards = sorted(map(self.create_item, self.item_name_groups['rewards']))
boss_locations = [self.multiworld.get_location(loc, self.player) for loc in boss_location_names]
placed_prizes = [loc.item.name for loc in boss_locations if loc.item is not None]
@@ -630,19 +654,58 @@ def fill_bosses(self, bossCount=9):
item = prizepool.pop()
loc = prize_locs.pop()
loc.place_locked_item(item)
- self.multiworld.itempool.remove(item)
self.hinted_dungeon_reward_locations[item.name] = loc
- def create_item(self, name: str):
+
+ # Separate the result from generate_itempool into main and prefill pools
+ def divide_itempools(self):
+ prefill_item_types = set()
+ if self.shopsanity != 'off':
+ prefill_item_types.add('Shop')
+ if self.shuffle_song_items != 'any':
+ prefill_item_types.add('Song')
+ if self.shuffle_smallkeys != 'keysanity':
+ prefill_item_types.add('SmallKey')
+ if self.shuffle_bosskeys != 'keysanity':
+ prefill_item_types.add('BossKey')
+ if self.shuffle_hideoutkeys != 'keysanity':
+ prefill_item_types.add('HideoutSmallKey')
+ if self.shuffle_ganon_bosskey != 'keysanity':
+ prefill_item_types.add('GanonBossKey')
+ if self.shuffle_mapcompass != 'keysanity':
+ prefill_item_types.update({'Map', 'Compass'})
+
+ main_items = []
+ prefill_items = []
+ for item in self.itempool:
+ if item.type in prefill_item_types:
+ prefill_items.append(item)
+ else:
+ main_items.append(item)
+ return main_items, prefill_items
+
+
+ # only returns proper result after create_items and divide_itempools are run
+ def get_pre_fill_items(self):
+ return self.pre_fill_items
+
+
+ # Note on allow_arbitrary_name:
+ # OoT defines many helper items and event names that are treated indistinguishably from regular items,
+ # but are only defined in the logic files. This means we need to create items for any name.
+ # Allowing any item name to be created is dangerous in case of plando, so this is a middle ground.
+ def create_item(self, name: str, allow_arbitrary_name: bool = False):
if name in item_table:
return OOTItem(name, self.player, item_table[name], False,
(name in self.nonadvancement_items if getattr(self, 'nonadvancement_items',
None) else False))
- return OOTItem(name, self.player, ('Event', True, None, None), True, False)
+ if allow_arbitrary_name:
+ return OOTItem(name, self.player, ('Event', True, None, None), True, False)
+ raise Exception(f"Invalid item name: {name}")
def make_event_item(self, name, location, item=None):
if item is None:
- item = self.create_item(name)
+ item = self.create_item(name, allow_arbitrary_name=True)
self.multiworld.push_item(location, item, collect=False)
location.locked = True
location.event = True
@@ -650,7 +713,9 @@ def make_event_item(self, name, location, item=None):
location.internal = True
return item
- def create_regions(self): # create and link regions
+
+ # Create regions, locations, and entrances
+ def create_regions(self):
if self.logic_rules == 'glitchless' or self.logic_rules == 'no_logic': # enables ER + NL
world_type = 'World'
else:
@@ -663,7 +728,7 @@ def create_regions(self): # create and link regions
self.multiworld.regions.append(menu)
self.load_regions_from_json(overworld_data_path)
self.load_regions_from_json(bosses_data_path)
- start.connect(self.multiworld.get_region('Root', self.player))
+ start.connect(self.get_region('Root'))
create_dungeons(self)
self.parser.create_delayed_rules()
@@ -674,16 +739,13 @@ def create_regions(self): # create and link regions
# Bind entrances to vanilla
for region in self.regions:
for exit in region.exits:
- exit.connect(self.multiworld.get_region(exit.vanilla_connected_region, self.player))
+ exit.connect(self.get_region(exit.vanilla_connected_region))
+
+ # Create items, starting item handling, boss prize fill (before entrance randomizer)
def create_items(self):
- # Uniquely rename drop locations for each region and erase them from the spoiler
- set_drop_location_names(self)
# Generate itempool
generate_itempool(self)
- # Add dungeon rewards
- rewardlist = sorted(list(self.item_name_groups['rewards']))
- self.itempool += map(self.create_item, rewardlist)
junk_pool = get_junk_pool(self)
removed_items = []
@@ -706,12 +768,16 @@ def create_items(self):
if self.start_with_rupees:
self.starting_items['Rupees'] = 999
+ # Divide itempool into prefill and main pools
+ self.itempool, self.pre_fill_items = self.divide_itempools()
+
self.multiworld.itempool += self.itempool
self.remove_from_start_inventory.extend(removed_items)
# Fill boss prizes. needs to happen before entrance shuffle
self.fill_bosses()
+
def set_rules(self):
# This has to run AFTER creating items but BEFORE set_entrances_based_rules
if self.entrance_shuffle:
@@ -749,6 +815,7 @@ def set_rules(self):
set_rules(self)
set_entrances_based_rules(self)
+
def generate_basic(self): # mostly killing locations that shouldn't exist by settings
# Gather items for ice trap appearances
@@ -761,8 +828,9 @@ def generate_basic(self): # mostly killing locations that shouldn't exist by se
# Kill unreachable events that can't be gotten even with all items
# Make sure to only kill actual internal events, not in-game "events"
- all_state = self.multiworld.get_all_state(False)
+ all_state = self.get_state_with_complete_itempool()
all_locations = self.get_locations()
+ all_state.sweep_for_events(locations=all_locations)
reachable = self.multiworld.get_reachable_locations(all_state, self.player)
unreachable = [loc for loc in all_locations if
(loc.internal or loc.type == 'Drop') and loc.event and loc.locked and loc not in reachable]
@@ -773,7 +841,6 @@ def generate_basic(self): # mostly killing locations that shouldn't exist by se
bigpoe = self.multiworld.get_location('Sell Big Poe from Market Guard House', self.player)
if not all_state.has('Bottle with Big Poe', self.player) and bigpoe not in reachable:
bigpoe.parent_region.locations.remove(bigpoe)
- self.multiworld.clear_location_cache()
# If fast scarecrow then we need to kill the Pierre location as it will be unreachable
if self.free_scarecrow:
@@ -784,39 +851,69 @@ def generate_basic(self): # mostly killing locations that shouldn't exist by se
loc = self.multiworld.get_location("Deliver Rutos Letter", self.player)
loc.parent_region.locations.remove(loc)
+
def pre_fill(self):
+ def prefill_state(base_state):
+ state = base_state.copy()
+ for item in self.get_pre_fill_items():
+ self.collect(state, item)
+ state.sweep_for_events(locations=self.get_locations())
+ return state
+
+ # Prefill shops, songs, and dungeon items
+ items = self.get_pre_fill_items()
+ locations = list(self.multiworld.get_unfilled_locations(self.player))
+ self.multiworld.random.shuffle(locations)
+
+ # Set up initial state
+ state = CollectionState(self.multiworld)
+ for item in self.itempool:
+ self.collect(state, item)
+ state.sweep_for_events(locations=self.get_locations())
+
# Place dungeon items
special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass']
- world_items = [item for item in self.multiworld.itempool if item.player == self.player]
+ type_to_setting = {
+ 'Map': 'shuffle_mapcompass',
+ 'Compass': 'shuffle_mapcompass',
+ 'SmallKey': 'shuffle_smallkeys',
+ 'BossKey': 'shuffle_bosskeys',
+ 'HideoutSmallKey': 'shuffle_hideoutkeys',
+ 'GanonBossKey': 'shuffle_ganon_bosskey',
+ }
+ special_fill_types.sort(key=lambda x: 0 if getattr(self, type_to_setting[x]) == 'dungeon' else 1)
+
for fill_stage in special_fill_types:
- stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), world_items))
+ stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), self.pre_fill_items))
if not stage_items:
continue
if fill_stage in ['GanonBossKey', 'HideoutSmallKey']:
locations = gather_locations(self.multiworld, fill_stage, self.player)
if isinstance(locations, list):
for item in stage_items:
- self.multiworld.itempool.remove(item)
+ self.pre_fill_items.remove(item)
self.multiworld.random.shuffle(locations)
- fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items,
- single_player_placement=True, lock=True)
+ fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items,
+ single_player_placement=True, lock=True, allow_excluded=True)
else:
for dungeon_info in dungeon_table:
dungeon_name = dungeon_info['name']
+ dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items))
+ if not dungeon_items:
+ continue
locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name)
if isinstance(locations, list):
- dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items))
for item in dungeon_items:
- self.multiworld.itempool.remove(item)
+ self.pre_fill_items.remove(item)
self.multiworld.random.shuffle(locations)
- fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items,
- single_player_placement=True, lock=True)
+ fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items,
+ single_player_placement=True, lock=True, allow_excluded=True)
# Place songs
# 5 built-in retries because this section can fail sometimes
if self.shuffle_song_items != 'any':
- tries = 5
+ tries = 10
if self.shuffle_song_items == 'song':
song_locations = list(filter(lambda location: location.type == 'Song',
self.multiworld.get_unfilled_locations(player=self.player)))
@@ -826,9 +923,9 @@ def pre_fill(self):
else:
raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}")
- songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.multiworld.itempool))
+ songs = list(filter(lambda item: item.type == 'Song', self.pre_fill_items))
for song in songs:
- self.multiworld.itempool.remove(song)
+ self.pre_fill_items.remove(song)
important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or
self.warp_songs or self.spawn_positions)
@@ -851,8 +948,8 @@ def pre_fill(self):
while tries:
try:
self.multiworld.random.shuffle(song_locations)
- fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:],
- True, True)
+ fill_restrictive(self.multiworld, prefill_state(state), song_locations[:], songs[:],
+ single_player_placement=True, lock=True, allow_excluded=True)
logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
except FillError as e:
tries -= 1
@@ -873,10 +970,8 @@ def pre_fill(self):
# Place shop items
# fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items
if self.shopsanity != 'off':
- shop_prog = list(filter(lambda item: item.player == self.player and item.type == 'Shop'
- and item.advancement, self.multiworld.itempool))
- shop_junk = list(filter(lambda item: item.player == self.player and item.type == 'Shop'
- and not item.advancement, self.multiworld.itempool))
+ shop_prog = list(filter(lambda item: item.type == 'Shop' and item.advancement, self.pre_fill_items))
+ shop_junk = list(filter(lambda item: item.type == 'Shop' and not item.advancement, self.pre_fill_items))
shop_locations = list(
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
self.multiworld.get_unfilled_locations(player=self.player)))
@@ -886,29 +981,14 @@ def pre_fill(self):
'Buy Zora Tunic': 1,
}.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement
self.multiworld.random.shuffle(shop_locations)
- for item in shop_prog + shop_junk:
- self.multiworld.itempool.remove(item)
- fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog, True, True)
+ self.pre_fill_items = [] # all prefill should be done
+ fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog,
+ single_player_placement=True, lock=True, allow_excluded=True)
fast_fill(self.multiworld, shop_junk, shop_locations)
for loc in shop_locations:
loc.locked = True
set_shop_rules(self) # sets wallet requirements on shop items, must be done after they are filled
- # If skip child zelda is active and Song from Impa is unfilled, put a local giveable item into it.
- impa = self.multiworld.get_location("Song from Impa", self.player)
- if self.shuffle_child_trade == 'skip_child_zelda':
- if impa.item is None:
- candidate_items = list(item for item in self.multiworld.itempool if item.player == self.player)
- if candidate_items:
- item_to_place = self.multiworld.random.choice(candidate_items)
- self.multiworld.itempool.remove(item_to_place)
- else:
- item_to_place = self.create_item("Recovery Heart")
- impa.place_locked_item(item_to_place)
- # Give items to startinventory
- self.multiworld.push_precollected(impa.item)
- self.multiworld.push_precollected(self.create_item("Zeldas Letter"))
-
# Exclude locations in Ganon's Castle proportional to the number of items required to make the bridge
# Check for dungeon ER later
if self.logic_rules == 'glitchless':
@@ -943,48 +1023,6 @@ def pre_fill(self):
or (self.shuffle_child_trade == 'skip_child_zelda' and loc.name in ['HC Zeldas Letter', 'Song from Impa'])):
loc.address = None
- # Handle item-linked dungeon items and songs
- @classmethod
- def stage_pre_fill(cls, multiworld: MultiWorld):
- special_fill_types = ['Song', 'GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass']
- for group_id, group in multiworld.groups.items():
- if group['game'] != cls.game:
- continue
- group_items = [item for item in multiworld.itempool if item.player == group_id]
- for fill_stage in special_fill_types:
- group_stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), group_items))
- if not group_stage_items:
- continue
- if fill_stage in ['Song', 'GanonBossKey', 'HideoutSmallKey']:
- # No need to subdivide by dungeon name
- locations = gather_locations(multiworld, fill_stage, group['players'])
- if isinstance(locations, list):
- for item in group_stage_items:
- multiworld.itempool.remove(item)
- multiworld.random.shuffle(locations)
- fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items,
- single_player_placement=False, lock=True)
- if fill_stage == 'Song':
- # We don't want song locations to contain progression unless it's a song
- # or it was marked as priority.
- # We do this manually because we'd otherwise have to either
- # iterate twice or do many function calls.
- for loc in locations:
- if loc.progress_type == LocationProgressType.DEFAULT:
- loc.progress_type = LocationProgressType.EXCLUDED
- add_item_rule(loc, lambda i: not (i.advancement or i.useful))
- else:
- # Perform the fill task once per dungeon
- for dungeon_info in dungeon_table:
- dungeon_name = dungeon_info['name']
- locations = gather_locations(multiworld, fill_stage, group['players'], dungeon=dungeon_name)
- if isinstance(locations, list):
- group_dungeon_items = list(filter(lambda item: dungeon_name in item.name, group_stage_items))
- for item in group_dungeon_items:
- multiworld.itempool.remove(item)
- multiworld.random.shuffle(locations)
- fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items,
- single_player_placement=False, lock=True)
def generate_output(self, output_directory: str):
if self.hints != 'none':
@@ -1021,37 +1059,16 @@ def generate_output(self, output_directory: str):
player_name=self.multiworld.get_player_name(self.player))
apz5.write()
- # Write entrances to spoiler log
- all_entrances = self.get_shuffled_entrances()
- all_entrances.sort(reverse=True, key=lambda x: x.name)
- all_entrances.sort(reverse=True, key=lambda x: x.type)
- if not self.decouple_entrances:
- while all_entrances:
- loadzone = all_entrances.pop()
- if loadzone.type != 'Overworld':
- if loadzone.primary:
- entrance = loadzone
- else:
- entrance = loadzone.reverse
- if entrance.reverse is not None:
- self.multiworld.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player)
- else:
- self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
- else:
- reverse = loadzone.replaces.reverse
- if reverse in all_entrances:
- all_entrances.remove(reverse)
- self.multiworld.spoiler.set_entrance(loadzone, reverse, 'both', self.player)
- else:
- for entrance in all_entrances:
- self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
# Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations.
@classmethod
def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str):
def hint_type_players(hint_type: str) -> set:
return {autoworld.player for autoworld in multiworld.get_game_worlds("Ocarina of Time")
- if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0}
+ if autoworld.hints != 'none'
+ and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0
+ and (autoworld.hint_dist_user['distribution'][hint_type]['fixed'] > 0
+ or autoworld.hint_dist_user['distribution'][hint_type]['weight'] > 0)}
try:
item_hint_players = hint_type_players('item')
@@ -1078,10 +1095,10 @@ def hint_type_players(hint_type: str) -> set:
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or
(oot_is_item_of_type(loc.item, 'Song') or
- (oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or
- (oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys == 'any_dungeon') or
- (oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or
- (oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))):
+ (oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys in ('overworld', 'any_dungeon', 'regional')) or
+ (oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys in ('overworld', 'any_dungeon', 'regional')) or
+ (oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys in ('overworld', 'any_dungeon', 'regional')) or
+ (oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))):
if loc.player in barren_hint_players:
hint_area = get_hint_area(loc)
items_by_region[loc.player][hint_area]['weight'] += 1
@@ -1096,7 +1113,12 @@ def hint_type_players(hint_type: str) -> set:
elif barren_hint_players or woth_hint_players: # Check only relevant oot locations for barren/woth
for player in (barren_hint_players | woth_hint_players):
for loc in multiworld.worlds[player].get_locations():
- if loc.item.code and (not loc.locked or oot_is_item_of_type(loc.item, 'Song')):
+ if loc.item.code and (not loc.locked or
+ (oot_is_item_of_type(loc.item, 'Song') or
+ (oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys in ('overworld', 'any_dungeon', 'regional')) or
+ (oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys in ('overworld', 'any_dungeon', 'regional')) or
+ (oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys in ('overworld', 'any_dungeon', 'regional')) or
+ (oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))):
if player in barren_hint_players:
hint_area = get_hint_area(loc)
items_by_region[player][hint_area]['weight'] += 1
@@ -1116,6 +1138,7 @@ def hint_type_players(hint_type: str) -> set:
for autoworld in multiworld.get_game_worlds("Ocarina of Time"):
autoworld.hint_data_available.set()
+
def fill_slot_data(self):
self.collectible_flags_available.wait()
return {
@@ -1123,6 +1146,7 @@ def fill_slot_data(self):
'collectible_flag_offsets': self.collectible_flag_offsets
}
+
def modify_multidata(self, multidata: dict):
# Replace connect name
@@ -1137,6 +1161,16 @@ def modify_multidata(self, multidata: dict):
continue
multidata["precollected_items"][self.player].remove(item_id)
+ # If skip child zelda, push item onto autotracker
+ if self.shuffle_child_trade == 'skip_child_zelda':
+ impa_item_id = self.item_name_to_id.get(self.get_location('Song from Impa').item.name, None)
+ zelda_item_id = self.item_name_to_id.get(self.get_location('HC Zeldas Letter').item.name, None)
+ if impa_item_id:
+ multidata["precollected_items"][self.player].append(impa_item_id)
+ if zelda_item_id:
+ multidata["precollected_items"][self.player].append(zelda_item_id)
+
+
def extend_hint_information(self, er_hint_data: dict):
er_hint_data[self.player] = {}
@@ -1183,6 +1217,42 @@ def get_entrance_to_region(region):
er_hint_data[self.player][location.address] = main_entrance.name
logger.debug(f"Set {location.name} hint data to {main_entrance.name}")
+
+ def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
+ required_trials_str = ", ".join(t for t in self.skipped_trials if not self.skipped_trials[t])
+ spoiler_handle.write(f"\n\nTrials ({self.multiworld.get_player_name(self.player)}): {required_trials_str}\n")
+
+ if self.shopsanity != 'off':
+ spoiler_handle.write(f"\nShop Prices ({self.multiworld.get_player_name(self.player)}):\n")
+ for k, v in self.shop_prices.items():
+ spoiler_handle.write(f"{k}: {v} Rupees\n")
+
+ # Write entrances to spoiler log
+ all_entrances = self.get_shuffled_entrances()
+ all_entrances.sort(reverse=True, key=lambda x: x.name)
+ all_entrances.sort(reverse=True, key=lambda x: x.type)
+ if not self.decouple_entrances:
+ while all_entrances:
+ loadzone = all_entrances.pop()
+ if loadzone.type != 'Overworld':
+ if loadzone.primary:
+ entrance = loadzone
+ else:
+ entrance = loadzone.reverse
+ if entrance.reverse is not None:
+ self.multiworld.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player)
+ else:
+ self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
+ else:
+ reverse = loadzone.replaces.reverse
+ if reverse in all_entrances:
+ all_entrances.remove(reverse)
+ self.multiworld.spoiler.set_entrance(loadzone, reverse, 'both', self.player)
+ else:
+ for entrance in all_entrances:
+ self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
+
+
# Key ring handling:
# Key rings are multiple items glued together into one, so we need to give
# the appropriate number of keys in the collection state when they are
@@ -1190,16 +1260,16 @@ def get_entrance_to_region(region):
def collect(self, state: CollectionState, item: OOTItem) -> bool:
if item.advancement and item.special and item.special.get('alias', False):
alt_item_name, count = item.special.get('alias')
- state.prog_items[alt_item_name, self.player] += count
+ state.prog_items[self.player][alt_item_name] += count
return True
return super().collect(state, item)
def remove(self, state: CollectionState, item: OOTItem) -> bool:
if item.advancement and item.special and item.special.get('alias', False):
alt_item_name, count = item.special.get('alias')
- state.prog_items[alt_item_name, self.player] -= count
- if state.prog_items[alt_item_name, self.player] < 1:
- del (state.prog_items[alt_item_name, self.player])
+ state.prog_items[self.player][alt_item_name] -= count
+ if state.prog_items[self.player][alt_item_name] < 1:
+ del (state.prog_items[self.player][alt_item_name])
return True
return super().remove(state, item)
@@ -1214,24 +1284,29 @@ def region_has_shortcuts(self, regionname):
return False
def get_shufflable_entrances(self, type=None, only_primary=False):
- return [entrance for entrance in self.multiworld.get_entrances() if (entrance.player == self.player and
- (type == None or entrance.type == type) and
- (not only_primary or entrance.primary))]
+ return [entrance for entrance in self.get_entrances() if ((type == None or entrance.type == type)
+ and (not only_primary or entrance.primary))]
def get_shuffled_entrances(self, type=None, only_primary=False):
return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if
entrance.shuffled]
def get_locations(self):
- for region in self.regions:
- for loc in region.locations:
- yield loc
+ return self.multiworld.get_locations(self.player)
def get_location(self, location):
return self.multiworld.get_location(location, self.player)
- def get_region(self, region):
- return self.multiworld.get_region(region, self.player)
+ def get_region(self, region_name):
+ try:
+ return self._regions_cache[region_name]
+ except KeyError:
+ ret = self.multiworld.get_region(region_name, self.player)
+ self._regions_cache[region_name] = ret
+ return ret
+
+ def get_entrances(self):
+ return self.multiworld.get_entrances(self.player)
def get_entrance(self, entrance):
return self.multiworld.get_entrance(entrance, self.player)
@@ -1265,25 +1340,12 @@ def is_major_item(self, item: OOTItem):
# Specifically ensures that only real items are gotten, not any events.
# In particular, ensures that Time Travel needs to be found.
def get_state_with_complete_itempool(self):
- all_state = self.multiworld.get_all_state(use_cache=False)
- # Remove event progression items
- for item, player in all_state.prog_items:
- if player == self.player and (item not in item_table or item_table[item][2] is None):
- all_state.prog_items[(item, player)] = 0
- # Remove all events and checked locations
- all_state.locations_checked = {loc for loc in all_state.locations_checked if loc.player != self.player}
- all_state.events = {loc for loc in all_state.events if loc.player != self.player}
+ all_state = CollectionState(self.multiworld)
+ for item in self.itempool + self.pre_fill_items:
+ self.multiworld.worlds[item.player].collect(all_state, item)
# If free_scarecrow give Scarecrow Song
if self.free_scarecrow:
all_state.collect(self.create_item("Scarecrow Song"), event=True)
-
- # Invalidate caches
- all_state.child_reachable_regions[self.player] = set()
- all_state.adult_reachable_regions[self.player] = set()
- all_state.child_blocked_connections[self.player] = set()
- all_state.adult_blocked_connections[self.player] = set()
- all_state.day_reachable_regions[self.player] = set()
- all_state.dampe_reachable_regions[self.player] = set()
all_state.stale[self.player] = True
return all_state
@@ -1320,7 +1382,6 @@ def gather_locations(multiworld: MultiWorld,
dungeon: str = ''
) -> Optional[List[OOTLocation]]:
type_to_setting = {
- 'Song': 'shuffle_song_items',
'Map': 'shuffle_mapcompass',
'Compass': 'shuffle_mapcompass',
'SmallKey': 'shuffle_smallkeys',
@@ -1339,21 +1400,12 @@ def gather_locations(multiworld: MultiWorld,
players = {players}
fill_opts = {p: getattr(multiworld.worlds[p], type_to_setting[item_type]) for p in players}
locations = []
- if item_type == 'Song':
- if any(map(lambda v: v == 'any', fill_opts.values())):
- return None
- for player, option in fill_opts.items():
- if option == 'song':
- condition = lambda location: location.type == 'Song'
- elif option == 'dungeon':
- condition = lambda location: location.name in dungeon_song_locations
- locations += filter(condition, multiworld.get_unfilled_locations(player=player))
- else:
- if any(map(lambda v: v in {'keysanity'}, fill_opts.values())):
- return None
- for player, option in fill_opts.items():
- condition = functools.partial(valid_dungeon_item_location,
- multiworld.worlds[player], option, dungeon)
- locations += filter(condition, multiworld.get_unfilled_locations(player=player))
+ if any(map(lambda v: v == 'keysanity', fill_opts.values())):
+ return None
+ for player, option in fill_opts.items():
+ condition = functools.partial(valid_dungeon_item_location,
+ multiworld.worlds[player], option, dungeon)
+ locations += filter(condition, multiworld.get_unfilled_locations(player=player))
return locations
+
diff --git a/worlds/oot/docs/en_Ocarina of Time.md b/worlds/oot/docs/en_Ocarina of Time.md
index b4610878b610..fa8e148957c7 100644
--- a/worlds/oot/docs/en_Ocarina of Time.md
+++ b/worlds/oot/docs/en_Ocarina of Time.md
@@ -31,3 +31,10 @@ Items belonging to other worlds are represented by the Zelda's Letter item.
When the player receives an item, Link will hold the item above his head and display it to the world. It's good for
business!
+
+## Unique Local Commands
+
+The following commands are only available when using the OoTClient to play with Archipelago.
+
+- `/n64` Check N64 Connection State
+- `/deathlink` Toggle deathlink from client. Overrides default setting.
diff --git a/worlds/oot/docs/setup_en.md b/worlds/oot/docs/setup_en.md
index 612c5efd8f99..72f15fa6c768 100644
--- a/worlds/oot/docs/setup_en.md
+++ b/worlds/oot/docs/setup_en.md
@@ -42,360 +42,22 @@ and select EmuHawk.exe.
An alternative BizHawk setup guide as well as various pieces of troubleshooting advice can be found
[here](https://wiki.ootrandomizer.com/index.php?title=Bizhawk).
-## Configuring your YAML file
+## Create a Config (.yaml) File
-### What is a YAML file and why do I need one?
+### What is a config file and why do I need one?
-Your YAML file contains a set of configuration options which provide the generator with information about how it should
-generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
-an experience customized for their taste, and different players in the same multiworld can all have different options.
+See the guide on setting up a basic YAML at the Archipelago setup
+guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
-### Where do I get a YAML file?
+### Where do I get a config file?
-A basic OoT yaml will look like this. There are lots of cosmetic options that have been removed for the sake of this
-tutorial, if you want to see a complete list, download Archipelago from
-the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) and look for the sample file in
-the "Players" folder.
+The Player Settings page on the website allows you to configure your personal settings and export a config file from
+them. Player settings page: [Ocarina of Time Player Settings Page](/games/Ocarina%20of%20Time/player-settings)
-```yaml
-description: Default Ocarina of Time Template # Used to describe your yaml. Useful if you have multiple files
-# Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
-name: YourName
-game:
- Ocarina of Time: 1
-requires:
- version: 0.1.7 # Version of Archipelago required for this yaml to work as expected.
-# Shared Options supported by all games:
-accessibility:
- items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
- locations: 50 # Guarantees you will be able to access all locations, and therefore all items
- none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items
-progression_balancing: # A system to reduce BK, as in times during which you can't do anything, by moving your items into an earlier access sphere
- 0: 0 # Choose a lower number if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
- 25: 0
- 50: 50 # Make it likely you have stuff to do.
- 99: 0 # Get important items early, and stay at the front of the progression.
-Ocarina of Time:
- logic_rules: # Set the logic used for the generator.
- glitchless: 50
- glitched: 0
- no_logic: 0
- logic_no_night_tokens_without_suns_song: # Nighttime skulltulas will logically require Sun's Song.
- false: 50
- true: 0
- open_forest: # Set the state of Kokiri Forest and the path to Deku Tree.
- open: 50
- closed_deku: 0
- closed: 0
- open_kakariko: # Set the state of the Kakariko Village gate.
- open: 50
- zelda: 0
- closed: 0
- open_door_of_time: # Open the Door of Time by default, without the Song of Time.
- false: 0
- true: 50
- zora_fountain: # Set the state of King Zora, blocking the way to Zora's Fountain.
- open: 0
- adult: 0
- closed: 50
- gerudo_fortress: # Set the requirements for access to Gerudo Fortress.
- normal: 0
- fast: 50
- open: 0
- bridge: # Set the requirements for the Rainbow Bridge.
- open: 0
- vanilla: 0
- stones: 0
- medallions: 50
- dungeons: 0
- tokens: 0
- trials: # Set the number of required trials in Ganon's Castle.
- # you can add additional values between minimum and maximum
- 0: 50 # minimum value
- 6: 0 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- starting_age: # Choose which age Link will start as.
- child: 50
- adult: 0
- triforce_hunt: # Gather pieces of the Triforce scattered around the world to complete the game.
- false: 50
- true: 0
- triforce_goal: # Number of Triforce pieces required to complete the game. Total number placed determined by the Item Pool setting.
- # you can add additional values between minimum and maximum
- 1: 0 # minimum value
- 50: 0 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- 20: 50
- bombchus_in_logic: # Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell refills; bombchus open Bombchu Bowling.
- false: 50
- true: 0
- bridge_stones: # Set the number of Spiritual Stones required for the rainbow bridge.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 3: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- bridge_medallions: # Set the number of medallions required for the rainbow bridge.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 6: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- bridge_rewards: # Set the number of dungeon rewards required for the rainbow bridge.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 9: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- bridge_tokens: # Set the number of Gold Skulltula Tokens required for the rainbow bridge.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 100: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- shuffle_mapcompass: # Control where to shuffle dungeon maps and compasses.
- remove: 0
- startwith: 50
- vanilla: 0
- dungeon: 0
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- shuffle_smallkeys: # Control where to shuffle dungeon small keys.
- remove: 0
- vanilla: 0
- dungeon: 50
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- shuffle_hideoutkeys: # Control where to shuffle the Gerudo Fortress small keys.
- vanilla: 50
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- shuffle_bosskeys: # Control where to shuffle boss keys, except the Ganon's Castle Boss Key.
- remove: 0
- vanilla: 0
- dungeon: 50
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- shuffle_ganon_bosskey: # Control where to shuffle the Ganon's Castle Boss Key.
- remove: 50
- vanilla: 0
- dungeon: 0
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- on_lacs: 0
- enhance_map_compass: # Map tells if a dungeon is vanilla or MQ. Compass tells what the dungeon reward is.
- false: 50
- true: 0
- lacs_condition: # Set the requirements for the Light Arrow Cutscene in the Temple of Time.
- vanilla: 50
- stones: 0
- medallions: 0
- dungeons: 0
- tokens: 0
- lacs_stones: # Set the number of Spiritual Stones required for LACS.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 3: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- lacs_medallions: # Set the number of medallions required for LACS.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 6: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- lacs_rewards: # Set the number of dungeon rewards required for LACS.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 9: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- lacs_tokens: # Set the number of Gold Skulltula Tokens required for LACS.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 100: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- shuffle_song_items: # Set where songs can appear.
- song: 50
- dungeon: 0
- any: 0
- shopsanity: # Randomizes shop contents. Set to "off" to not shuffle shops; "0" shuffles shops but does not allow multiworld items in shops.
- 0: 0
- 1: 0
- 2: 0
- 3: 0
- 4: 0
- random_value: 0
- off: 50
- tokensanity: # Token rewards from Gold Skulltulas are shuffled into the pool.
- off: 50
- dungeons: 0
- overworld: 0
- all: 0
- shuffle_scrubs: # Shuffle the items sold by Business Scrubs, and set the prices.
- off: 50
- low: 0
- regular: 0
- random_prices: 0
- shuffle_cows: # Cows give items when Epona's Song is played.
- false: 50
- true: 0
- shuffle_kokiri_sword: # Shuffle Kokiri Sword into the item pool.
- false: 50
- true: 0
- shuffle_ocarinas: # Shuffle the Fairy Ocarina and Ocarina of Time into the item pool.
- false: 50
- true: 0
- shuffle_weird_egg: # Shuffle the Weird Egg from Malon at Hyrule Castle.
- false: 50
- true: 0
- shuffle_gerudo_card: # Shuffle the Gerudo Membership Card into the item pool.
- false: 50
- true: 0
- shuffle_beans: # Adds a pack of 10 beans to the item pool and changes the bean salesman to sell one item for 60 rupees.
- false: 50
- true: 0
- shuffle_medigoron_carpet_salesman: # Shuffle the items sold by Medigoron and the Haunted Wasteland Carpet Salesman.
- false: 50
- true: 0
- skip_child_zelda: # Game starts with Zelda's Letter, the item at Zelda's Lullaby, and the relevant events already completed.
- false: 50
- true: 0
- no_escape_sequence: # Skips the tower collapse sequence between the Ganondorf and Ganon fights.
- false: 0
- true: 50
- no_guard_stealth: # The crawlspace into Hyrule Castle skips straight to Zelda.
- false: 0
- true: 50
- no_epona_race: # Epona can always be summoned with Epona's Song.
- false: 0
- true: 50
- skip_some_minigame_phases: # Dampe Race and Horseback Archery give both rewards if the second condition is met on the first attempt.
- false: 0
- true: 50
- complete_mask_quest: # All masks are immediately available to borrow from the Happy Mask Shop.
- false: 50
- true: 0
- useful_cutscenes: # Reenables the Poe cutscene in Forest Temple, Darunia in Fire Temple, and Twinrova introduction. Mostly useful for glitched.
- false: 50
- true: 0
- fast_chests: # All chest animations are fast. If disabled, major items have a slow animation.
- false: 0
- true: 50
- free_scarecrow: # Pulling out the ocarina near a scarecrow spot spawns Pierre without needing the song.
- false: 50
- true: 0
- fast_bunny_hood: # Bunny Hood lets you move 1.5x faster like in Majora's Mask.
- false: 50
- true: 0
- chicken_count: # Controls the number of Cuccos for Anju to give an item as child.
- \# you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 7: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- hints: # Gossip Stones can give hints about item locations.
- none: 0
- mask: 0
- agony: 0
- always: 50
- hint_dist: # Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.
- balanced: 50
- ddr: 0
- league: 0
- mw2: 0
- scrubs: 0
- strong: 0
- tournament: 0
- useless: 0
- very_strong: 0
- text_shuffle: # Randomizes text in the game for comedic effect.
- none: 50
- except_hints: 0
- complete: 0
- damage_multiplier: # Controls the amount of damage Link takes.
- half: 0
- normal: 50
- double: 0
- quadruple: 0
- ohko: 0
- no_collectible_hearts: # Hearts will not drop from enemies or objects.
- false: 50
- true: 0
- starting_tod: # Change the starting time of day.
- default: 50
- sunrise: 0
- morning: 0
- noon: 0
- afternoon: 0
- sunset: 0
- evening: 0
- midnight: 0
- witching_hour: 0
- start_with_consumables: # Start the game with full Deku Sticks and Deku Nuts.
- false: 50
- true: 0
- start_with_rupees: # Start with a full wallet. Wallet upgrades will also fill your wallet.
- false: 50
- true: 0
- item_pool_value: # Changes the number of items available in the game.
- plentiful: 0
- balanced: 50
- scarce: 0
- minimal: 0
- junk_ice_traps: # Adds ice traps to the item pool.
- off: 0
- normal: 50
- on: 0
- mayhem: 0
- onslaught: 0
- ice_trap_appearance: # Changes the appearance of ice traps as freestanding items.
- major_only: 50
- junk_only: 0
- anything: 0
- logic_earliest_adult_trade: # Earliest item that can appear in the adult trade sequence.
- pocket_egg: 0
- pocket_cucco: 0
- cojiro: 0
- odd_mushroom: 0
- poachers_saw: 0
- broken_sword: 0
- prescription: 50
- eyeball_frog: 0
- eyedrops: 0
- claim_check: 0
- logic_latest_adult_trade: # Latest item that can appear in the adult trade sequence.
- pocket_egg: 0
- pocket_cucco: 0
- cojiro: 0
- odd_mushroom: 0
- poachers_saw: 0
- broken_sword: 0
- prescription: 0
- eyeball_frog: 0
- eyedrops: 0
- claim_check: 50
+### Verifying your config file
-```
+If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
+validator page: [YAML Validation page](/mysterycheck)
## Joining a MultiWorld Game
diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py
index 2bf523b347c8..0451f32bdd49 100644
--- a/worlds/overcooked2/__init__.py
+++ b/worlds/overcooked2/__init__.py
@@ -172,7 +172,7 @@ def get_priority_locations(self) -> List[int]:
# random priority locations have no desirable effect on solo seeds
return list()
- balancing_mode = self.get_options()["LocationBalancing"]
+ balancing_mode = self.options.location_balancing
if balancing_mode == LocationBalancingMode.disabled:
# Location balancing is disabled, progression density is purely determined by filler
@@ -528,7 +528,7 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]:
# Game Modifications
"LevelPurchaseRequirements": level_purchase_requirements,
"Custom66TimerScale": max(0.4, 0.25 + (1.0 - star_threshold_scale)*0.6),
- "ShortHordeLevels": self.options.short_horde_levels,
+ "ShortHordeLevels": self.options.short_horde_levels.result,
"CustomLevelOrder": custom_level_order,
# Items (Starting Inventory)
@@ -584,6 +584,7 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]:
"TwoStars": star_threshold_scale * 0.75,
"OneStar": star_threshold_scale * 0.35,
}
+ base_data["AlwaysServeOldestOrder"] = self.options.always_serve_oldest_order.result
return base_data
diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py
index 11aa737e0f26..b2ee0702c91e 100644
--- a/worlds/pokemon_rb/__init__.py
+++ b/worlds/pokemon_rb/__init__.py
@@ -445,13 +445,9 @@ def number_of_zones(mon):
# Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not
# fail. Re-use test_state from previous final loop.
evolutions_region = self.multiworld.get_region("Evolution", self.player)
- clear_cache = False
for location in evolutions_region.locations.copy():
if not test_state.can_reach(location, player=self.player):
evolutions_region.locations.remove(location)
- clear_cache = True
- if clear_cache:
- self.multiworld.clear_location_cache()
if self.multiworld.old_man[self.player] == "early_parcel":
self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1
@@ -467,13 +463,17 @@ def number_of_zones(mon):
locs = {self.multiworld.get_location("Fossil - Choice A", self.player),
self.multiworld.get_location("Fossil - Choice B", self.player)}
- for loc in locs:
+ if not self.multiworld.key_items_only[self.player]:
+ rule = None
if self.multiworld.fossil_check_item_types[self.player] == "key_items":
- add_item_rule(loc, lambda i: i.advancement)
+ rule = lambda i: i.advancement
elif self.multiworld.fossil_check_item_types[self.player] == "unique_items":
- add_item_rule(loc, lambda i: i.name in item_groups["Unique"])
+ rule = lambda i: i.name in item_groups["Unique"]
elif self.multiworld.fossil_check_item_types[self.player] == "no_key_items":
- add_item_rule(loc, lambda i: not i.advancement)
+ rule = lambda i: not i.advancement
+ if rule:
+ for loc in locs:
+ add_item_rule(loc, rule)
for mon in ([" ".join(self.multiworld.get_location(
f"Oak's Lab - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)]
@@ -559,7 +559,6 @@ def number_of_zones(mon):
else:
raise Exception("Failed to remove corresponding item while deleting unreachable Dexsanity location")
- self.multiworld._recache()
if self.multiworld.door_shuffle[self.player] == "decoupled":
swept_state = self.multiworld.state.copy()
diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4
index b7bdda7fbbed..eb4d83360cd8 100644
Binary files a/worlds/pokemon_rb/basepatch_blue.bsdiff4 and b/worlds/pokemon_rb/basepatch_blue.bsdiff4 differ
diff --git a/worlds/pokemon_rb/basepatch_red.bsdiff4 b/worlds/pokemon_rb/basepatch_red.bsdiff4
index 51440789fd47..cffb0b7e0653 100644
Binary files a/worlds/pokemon_rb/basepatch_red.bsdiff4 and b/worlds/pokemon_rb/basepatch_red.bsdiff4 differ
diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md
index daefd6b2f7eb..086ec347f34f 100644
--- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md
+++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md
@@ -80,3 +80,9 @@ All items for other games will display simply as "AP ITEM," including those for
A "received item" sound effect will play. Currently, there is no in-game message informing you of what the item is.
If you are in battle, have menus or text boxes opened, or scripted events are occurring, the items will not be given to
you until these have ended.
+
+## Unique Local Commands
+
+The following command is only available when using the PokemonClient to play with Archipelago.
+
+- `/gb` Check Gameboy Connection State
diff --git a/worlds/pokemon_rb/docs/setup_en.md b/worlds/pokemon_rb/docs/setup_en.md
index 488f3fdc0791..7ba9b3aa09e3 100644
--- a/worlds/pokemon_rb/docs/setup_en.md
+++ b/worlds/pokemon_rb/docs/setup_en.md
@@ -7,7 +7,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
## Required Software
- BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- - Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
+ - Version 2.3.1 and later are supported. Version 2.9.1 is recommended.
- Detailed installation instructions for BizHawk can be found at the above link.
- Windows users must run the prereq installer first, which can also be found at the above link.
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
@@ -23,7 +23,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
Once BizHawk has been installed, open EmuHawk and change the following settings:
-- (≤ 2.8) Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
+- (If using 2.8 or earlier) Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
"Lua+LuaInterface". Then restart EmuHawk. This is required for the Lua script to function correctly.
**NOTE: Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs**
**of newer versions of EmuHawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load**
@@ -57,7 +57,7 @@ For `trainer_name` and `rival_name` the following regular characters are allowed
* `‘’“”·… ABCDEFGHIJKLMNOPQRSTUVWXYZ():;[]abcdefghijklmnopqrstuvwxyzé'-?!.♂$×/,♀0123456789`
-And the following special characters (these each take up one character):
+And the following special characters (these each count as one character):
* `<'d>`
* `<'l>`
* `<'t>`
diff --git a/worlds/pokemon_rb/docs/setup_es.md b/worlds/pokemon_rb/docs/setup_es.md
index 2a943da72f59..a6a6aa6ce793 100644
--- a/worlds/pokemon_rb/docs/setup_es.md
+++ b/worlds/pokemon_rb/docs/setup_es.md
@@ -7,7 +7,7 @@ Al usar BizHawk, esta guía solo es aplicable en los sistemas de Windows y Linux
## Software Requerido
- BizHawk: [BizHawk Releases en TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- - La versión 2.3.1 y posteriores son soportadas. Se recomienda la versión 2.7 para estabilidad.
+ - La versión 2.3.1 y posteriores son soportadas. Se recomienda la versión 2.9.1.
- Instrucciones de instalación detalladas para BizHawk se pueden encontrar en el enlace de arriba.
- Los usuarios de Windows deben ejecutar el instalador de prerrequisitos (prereq installer) primero, que también se
encuentra en el enlace de arriba.
diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py
index ec6375859bb9..4f1b55a00dd7 100644
--- a/worlds/pokemon_rb/locations.py
+++ b/worlds/pokemon_rb/locations.py
@@ -795,7 +795,7 @@ def __init__(self, flag):
LocationData("Pewter Gym", "Defeat Brock", "Defeat Brock", event=True),
LocationData("Cerulean Gym", "Defeat Misty", "Defeat Misty", event=True),
LocationData("Vermilion Gym", "Defeat Lt. Surge", "Defeat Lt. Surge", event=True),
- LocationData("Celadon Gym", "Defeat Erika", "Defeat Erika", event=True),
+ LocationData("Celadon Gym-C", "Defeat Erika", "Defeat Erika", event=True),
LocationData("Fuchsia Gym", "Defeat Koga", "Defeat Koga", event=True),
LocationData("Cinnabar Gym", "Defeat Blaine", "Defeat Blaine", event=True),
LocationData("Saffron Gym-C", "Defeat Sabrina", "Defeat Sabrina", event=True),
diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py
index cc788dd2ba5c..f844976548bd 100644
--- a/worlds/pokemon_rb/regions.py
+++ b/worlds/pokemon_rb/regions.py
@@ -1456,7 +1456,9 @@ def pair(a, b):
unreachable_outdoor_entrances = [
"Route 4-C to Mt Moon B1F-NE",
"Fuchsia City-Good Rod House Backyard to Fuchsia Good Rod House",
- "Cerulean City-Badge House Backyard to Cerulean Badge House"
+ "Cerulean City-Badge House Backyard to Cerulean Badge House",
+ # TODO: This doesn't need to be forced if fly location is Pokemon League?
+ "Route 23-N to Victory Road 2F-E"
]
@@ -1592,7 +1594,7 @@ def create_regions(self):
connect(multiworld, player, "Menu", "Pallet Town", one_way=True)
connect(multiworld, player, "Menu", "Pokedex", one_way=True)
connect(multiworld, player, "Menu", "Evolution", one_way=True)
- connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state,
+ connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state,
state.multiworld.second_fossil_check_condition[player].value, player), one_way=True)
connect(multiworld, player, "Pallet Town", "Route 1")
connect(multiworld, player, "Route 1", "Viridian City")
@@ -2220,7 +2222,7 @@ def cerulean_city_problem():
"Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"]:
badge_locs.append(multiworld.get_location(loc, player))
multiworld.random.shuffle(badges)
- while badges[3].name == "Cascade Badge" and multiworld.badges_needed_for_hm_moves[player] == "on":
+ while badges[3].name == "Cascade Badge" and multiworld.badges_needed_for_hm_moves[player]:
multiworld.random.shuffle(badges)
for badge, loc in zip(badges, badge_locs):
loc.place_locked_item(badge)
@@ -2265,22 +2267,30 @@ def cerulean_city_problem():
"Defeat Viridian Gym Giovanni",
]
- def adds_reachable_entrances(entrances_copy, item):
- state.collect(item, False)
+ event_locations = self.multiworld.get_filled_locations(player)
+
+ def adds_reachable_entrances(entrances_copy, item, dead_end_cache):
+ ret = dead_end_cache.get(item.name)
+ if (ret != None):
+ return ret
+
+ state_copy = state.copy()
+ state_copy.collect(item, True)
+ state.sweep_for_events(locations=event_locations)
ret = len([entrance for entrance in entrances_copy if entrance in reachable_entrances or
- entrance.parent_region.can_reach(state)]) > len(reachable_entrances)
- state.remove(item)
+ entrance.parent_region.can_reach(state_copy)]) > len(reachable_entrances)
+ dead_end_cache[item.name] = ret
return ret
- def dead_end(entrances_copy, e):
+ def dead_end(entrances_copy, e, dead_end_cache):
region = e.parent_region
check_warps = set()
checked_regions = {region}
check_warps.update(region.exits)
check_warps.remove(e)
for location in region.locations:
- if location.item and location.item.name in relevant_events and adds_reachable_entrances(entrances_copy,
- location.item):
+ if location.item and location.item.name in relevant_events and \
+ adds_reachable_entrances(entrances_copy, location.item, dead_end_cache):
return False
while check_warps:
warp = check_warps.pop()
@@ -2297,16 +2307,22 @@ def dead_end(entrances_copy, e):
check_warps.update(warp.connected_region.exits)
for location in warp.connected_region.locations:
if (location.item and location.item.name in relevant_events and
- adds_reachable_entrances(entrances_copy, location.item)):
+ adds_reachable_entrances(entrances_copy, location.item, dead_end_cache)):
return False
return True
starting_entrances = len(entrances)
dc_connected = []
- event_locations = self.multiworld.get_filled_locations(player)
+ rock_tunnel_entrances = [entrance for entrance in entrances if "Rock Tunnel" in entrance.name]
+ entrances = [entrance for entrance in entrances if entrance not in rock_tunnel_entrances]
while entrances:
state.update_reachable_regions(player)
state.sweep_for_events(locations=event_locations)
+
+ if rock_tunnel_entrances and logic.rock_tunnel(state, player):
+ entrances += rock_tunnel_entrances
+ rock_tunnel_entrances = None
+
reachable_entrances = [entrance for entrance in entrances if entrance in reachable_entrances or
entrance.parent_region.can_reach(state)]
assert reachable_entrances, \
@@ -2321,30 +2337,29 @@ def dead_end(entrances_copy, e):
if multiworld.door_shuffle[player] == "full" or len(entrances) != len(reachable_entrances):
entrances.sort(key=lambda e: e.name not in entrance_only)
- if len(entrances) < 48 and multiworld.door_shuffle[player] == "full":
- # Prevent a situation where the only remaining outdoor entrances are ones that cannot be reached
- # except by connecting directly to it.
- entrances.sort(key=lambda e: e.name in unreachable_outdoor_entrances)
+ dead_end_cache = {}
+
# entrances list is empty while it's being sorted, must pass a copy to iterate through
entrances_copy = entrances.copy()
if multiworld.door_shuffle[player] == "decoupled":
- if len(reachable_entrances) <= 8 and not logic.rock_tunnel(state, player):
- entrances.sort(key=lambda e: 1 if "Rock Tunnel" in e.name else 2 if e.connected_region is not
- None else 3 if e not in reachable_entrances else 0)
- else:
- entrances.sort(key=lambda e: 1 if e.connected_region is not None else 2 if e not in
- reachable_entrances else 0)
+ entrances.sort(key=lambda e: 1 if e.connected_region is not None else 2 if e not in
+ reachable_entrances else 0)
assert entrances[0].connected_region is None,\
"Ran out of valid reachable entrances in Pokemon Red and Blue door shuffle"
elif len(reachable_entrances) > (1 if multiworld.door_shuffle[player] == "insanity" else 8) and len(
entrances) <= (starting_entrances - 3):
entrances.sort(key=lambda e: 0 if e in reachable_entrances else 2 if
- dead_end(entrances_copy, e) else 1)
+ dead_end(entrances_copy, e, dead_end_cache) else 1)
else:
entrances.sort(key=lambda e: 0 if e in reachable_entrances else 1 if
- dead_end(entrances_copy, e) else 2)
+ dead_end(entrances_copy, e, dead_end_cache) else 2)
if multiworld.door_shuffle[player] == "full":
outdoor = outdoor_map(entrances[0].parent_region.name)
+ if len(entrances) < 48 and not outdoor:
+ # Prevent a situation where the only remaining outdoor entrances are ones that cannot be reached
+ # except by connecting directly to it.
+ entrances.sort(key=lambda e: e.name in unreachable_outdoor_entrances)
+
entrances.sort(key=lambda e: outdoor_map(e.parent_region.name) != outdoor)
assert entrances[0] in reachable_entrances, \
"Ran out of valid reachable entrances in Pokemon Red and Blue door shuffle"
diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py
index 4b191d91765b..096ab8e0a1f6 100644
--- a/worlds/pokemon_rb/rom.py
+++ b/worlds/pokemon_rb/rom.py
@@ -546,10 +546,8 @@ def set_trade_mon(address, loc):
write_quizzes(self, data, random)
- for location in self.multiworld.get_locations():
- if location.player != self.player:
- continue
- elif location.party_data:
+ for location in self.multiworld.get_locations(self.player):
+ if location.party_data:
for party in location.party_data:
if not isinstance(party["party_address"], list):
addresses = [rom_addresses[party["party_address"]]]
diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py
index 9c6621523cd1..97faf7bff205 100644
--- a/worlds/pokemon_rb/rom_addresses.py
+++ b/worlds/pokemon_rb/rom_addresses.py
@@ -1,10 +1,10 @@
rom_addresses = {
"Option_Encounter_Minimum_Steps": 0x3c1,
- "Option_Pitch_Black_Rock_Tunnel": 0x758,
- "Option_Blind_Trainers": 0x30c3,
- "Option_Trainersanity1": 0x3153,
- "Option_Split_Card_Key": 0x3e0c,
- "Option_Fix_Combat_Bugs": 0x3e0d,
+ "Option_Pitch_Black_Rock_Tunnel": 0x75c,
+ "Option_Blind_Trainers": 0x30c7,
+ "Option_Trainersanity1": 0x3157,
+ "Option_Split_Card_Key": 0x3e10,
+ "Option_Fix_Combat_Bugs": 0x3e11,
"Option_Lose_Money": 0x40d4,
"Base_Stats_Mew": 0x4260,
"Title_Mon_First": 0x4373,
diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py
index 0855e7a108bd..21dceb75e8df 100644
--- a/worlds/pokemon_rb/rules.py
+++ b/worlds/pokemon_rb/rules.py
@@ -103,25 +103,25 @@ def prize_rule(i):
"Route 22 - Trainer Parties": lambda state: state.has("Oak's Parcel", player),
# # Rock Tunnel
- # "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, player),
# Pokédex check
"Oak's Lab - Oak's Parcel Reward": lambda state: state.has("Oak's Parcel", player),
diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py
index fec60c3bd51b..8e4eda09e10f 100644
--- a/worlds/raft/__init__.py
+++ b/worlds/raft/__init__.py
@@ -1,5 +1,4 @@
import typing
-import random
from .Locations import location_table, lookup_name_to_id as locations_lookup_name_to_id
from .Items import (createResourcePackName, item_table, progressive_table, progressive_item_list,
@@ -100,7 +99,7 @@ def create_items(self):
extraItemNamePool.append(item["name"])
if (len(extraItemNamePool) > 0):
- for randomItem in random.choices(extraItemNamePool, k=extras):
+ for randomItem in self.random.choices(extraItemNamePool, k=extras):
raft_item = self.create_item_replaceAsNecessary(randomItem)
pool.append(raft_item)
@@ -194,7 +193,7 @@ def pre_fill(self):
previousLocation = "RadioTower"
while (len(availableLocationList) > 0):
if (len(availableLocationList) > 1):
- currentLocation = availableLocationList[random.randint(0, len(availableLocationList) - 2)]
+ currentLocation = availableLocationList[self.random.randint(0, len(availableLocationList) - 2)]
else:
currentLocation = availableLocationList[0] # Utopia (only one left in list)
availableLocationList.remove(currentLocation)
@@ -212,7 +211,7 @@ def setLocationItem(self, location: str, itemName: str):
def setLocationItemFromRegion(self, region: str, itemName: str):
itemToUse = next(filter(lambda itm: itm.name == itemName, self.multiworld.raft_frequencyItemsPerPlayer[self.player]))
self.multiworld.raft_frequencyItemsPerPlayer[self.player].remove(itemToUse)
- location = random.choice(list(loc for loc in location_table if loc["region"] == region))
+ location = self.random.choice(list(loc for loc in location_table if loc["region"] == region))
self.multiworld.get_location(location["name"], self.player).place_locked_item(itemToUse)
def fill_slot_data(self):
diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py
index 79739e85efcb..0ed0a87b17d6 100644
--- a/worlds/ror2/Options.py
+++ b/worlds/ror2/Options.py
@@ -16,7 +16,7 @@ class Goal(Choice):
display_name = "Game Mode"
option_classic = 0
option_explore = 1
- default = 0
+ default = 1
class TotalLocations(Range):
@@ -48,7 +48,8 @@ class ScavengersPerEnvironment(Range):
display_name = "Scavenger per Environment"
range_start = 0
range_end = 1
- default = 1
+ default = 0
+
class ScannersPerEnvironment(Range):
"""Explore Mode: The number of scanners locations per environment."""
@@ -57,6 +58,7 @@ class ScannersPerEnvironment(Range):
range_end = 1
default = 1
+
class AltarsPerEnvironment(Range):
"""Explore Mode: The number of altars locations per environment."""
display_name = "Newts Per Environment"
@@ -64,6 +66,7 @@ class AltarsPerEnvironment(Range):
range_end = 2
default = 1
+
class TotalRevivals(Range):
"""Total Percentage of `Dio's Best Friend` item put in the item pool."""
display_name = "Total Revives as percentage"
@@ -83,6 +86,7 @@ class ItemPickupStep(Range):
range_end = 5
default = 1
+
class ShrineUseStep(Range):
"""
Explore Mode:
@@ -131,7 +135,6 @@ class DLC_SOTV(Toggle):
display_name = "Enable DLC - SOTV"
-
class GreenScrap(Range):
"""Weight of Green Scraps in the item pool.
@@ -274,25 +277,8 @@ class ItemWeights(Choice):
option_void = 9
-
-
-# define a class for the weights of the generated item pool.
@dataclass
-class ROR2Weights:
- green_scrap: GreenScrap
- red_scrap: RedScrap
- yellow_scrap: YellowScrap
- white_scrap: WhiteScrap
- common_item: CommonItem
- uncommon_item: UncommonItem
- legendary_item: LegendaryItem
- boss_item: BossItem
- lunar_item: LunarItem
- void_item: VoidItem
- equipment: Equipment
-
-@dataclass
-class ROR2Options(PerGameCommonOptions, ROR2Weights):
+class ROR2Options(PerGameCommonOptions):
goal: Goal
total_locations: TotalLocations
chests_per_stage: ChestsPerEnvironment
@@ -310,4 +296,16 @@ class ROR2Options(PerGameCommonOptions, ROR2Weights):
shrine_use_step: ShrineUseStep
enable_lunar: AllowLunarItems
item_weights: ItemWeights
- item_pool_presets: ItemPoolPresetToggle
\ No newline at end of file
+ item_pool_presets: ItemPoolPresetToggle
+ # define the weights of the generated item pool.
+ green_scrap: GreenScrap
+ red_scrap: RedScrap
+ yellow_scrap: YellowScrap
+ white_scrap: WhiteScrap
+ common_item: CommonItem
+ uncommon_item: UncommonItem
+ legendary_item: LegendaryItem
+ boss_item: BossItem
+ lunar_item: LunarItem
+ void_item: VoidItem
+ equipment: Equipment
diff --git a/worlds/ror2/Rules.py b/worlds/ror2/Rules.py
index 7d94177417ea..65c04d06cba6 100644
--- a/worlds/ror2/Rules.py
+++ b/worlds/ror2/Rules.py
@@ -96,8 +96,7 @@ def set_rules(multiworld: MultiWorld, player: int) -> None:
# a long enough run to have enough director credits for scavengers and
# help prevent being stuck in the same stages until that point.)
- for location in multiworld.get_locations():
- if location.player != player: continue # ignore all checks that don't belong to this player
+ for location in multiworld.get_locations(player):
if "Scavenger" in location.name:
add_rule(location, lambda state: state.has("Stage_5", player))
# Regions
diff --git a/worlds/sc2wol/Client.py b/worlds/sc2wol/Client.py
index a9bb826b7447..3dbd2047debd 100644
--- a/worlds/sc2wol/Client.py
+++ b/worlds/sc2wol/Client.py
@@ -9,6 +9,7 @@
import os.path
import re
import sys
+import tempfile
import typing
import queue
import zipfile
@@ -286,6 +287,8 @@ async def server_auth(self, password_requested: bool = False):
await super(SC2Context, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
+ if self.ui:
+ self.ui.first_check = True
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected"}:
@@ -1166,10 +1169,12 @@ def download_latest_release_zip(owner: str, repo: str, api_version: str, metadat
r2 = requests.get(download_url, headers=headers)
if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)):
- with open(f"{repo}.zip", "wb") as fh:
+ tempdir = tempfile.gettempdir()
+ file = tempdir + os.sep + f"{repo}.zip"
+ with open(file, "wb") as fh:
fh.write(r2.content)
sc2_logger.info(f"Successfully downloaded {repo}.zip.")
- return f"{repo}.zip", latest_metadata
+ return file, latest_metadata
else:
sc2_logger.warning(f"Status code: {r2.status_code}")
sc2_logger.warning("Download failed.")
diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py
index ae31fa8eaadd..fba7051337df 100644
--- a/worlds/sc2wol/Locations.py
+++ b/worlds/sc2wol/Locations.py
@@ -68,10 +68,10 @@ def get_locations(multiworld: Optional[MultiWorld], player: Optional[int]) -> Tu
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("Evacuation", "Evacuation: First Chrysalis", SC2WOL_LOC_ID_OFFSET + 401, LocationType.BONUS),
- LocationData("Evacuation", "Evacuation: Second Chrysalis", SC2WOL_LOC_ID_OFFSET + 402, LocationType.BONUS,
+ LocationData("Evacuation", "Evacuation: North Chrysalis", SC2WOL_LOC_ID_OFFSET + 401, LocationType.BONUS),
+ LocationData("Evacuation", "Evacuation: West Chrysalis", SC2WOL_LOC_ID_OFFSET + 402, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Evacuation", "Evacuation: Third Chrysalis", SC2WOL_LOC_ID_OFFSET + 403, LocationType.BONUS,
+ LocationData("Evacuation", "Evacuation: East Chrysalis", SC2WOL_LOC_ID_OFFSET + 403, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
LocationData("Evacuation", "Evacuation: Reach Hanson", SC2WOL_LOC_ID_OFFSET + 404, LocationType.MISSION_PROGRESS),
LocationData("Evacuation", "Evacuation: Secret Resource Stash", SC2WOL_LOC_ID_OFFSET + 405, LocationType.BONUS),
@@ -419,7 +419,7 @@ def get_locations(multiworld: Optional[MultiWorld], player: Optional[int]) -> Tu
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Northeast Base", SC2WOL_LOC_ID_OFFSET + 2304, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Southeast Base", SC2WOL_LOC_ID_OFFSET + 2305, LocationType.MISSION_PROGRESS,
+ LocationData("A Sinister Turn", "A Sinister Turn: Southwest Base", SC2WOL_LOC_ID_OFFSET + 2305, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Maar", SC2WOL_LOC_ID_OFFSET + 2306, LocationType.MISSION_PROGRESS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(multiworld, player)),
diff --git a/worlds/sc2wol/Options.py b/worlds/sc2wol/Options.py
index 13b01c42a22c..e4b6a740669a 100644
--- a/worlds/sc2wol/Options.py
+++ b/worlds/sc2wol/Options.py
@@ -41,6 +41,10 @@ class FinalMap(Choice):
Vanilla mission order always ends with All in mission!
+ Warning: Using All-in with a short mission order (7 or fewer missions) is not recommended,
+ as there might not be enough locations to place all the required items,
+ any excess required items will be placed into the player's starting inventory!
+
This option is short-lived. It may be changed in the future
"""
display_name = "Final Map"
@@ -265,7 +269,6 @@ class MissionProgressLocations(LocationInclusion):
Nothing: No rewards for this type of tasks, effectively disabling such locations
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
- Warning: The generation may fail if too many locations are excluded by this way.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Mission Progress Locations"
@@ -282,7 +285,6 @@ class BonusLocations(LocationInclusion):
Nothing: No rewards for this type of tasks, effectively disabling such locations
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
- Warning: The generation may fail if too many locations are excluded by this way.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Bonus Locations"
@@ -300,7 +302,6 @@ class ChallengeLocations(LocationInclusion):
Nothing: No rewards for this type of tasks, effectively disabling such locations
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
- Warning: The generation may fail if too many locations are excluded by this way.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Challenge Locations"
@@ -317,7 +318,6 @@ class OptionalBossLocations(LocationInclusion):
Nothing: No rewards for this type of tasks, effectively disabling such locations
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
- Warning: The generation may fail if too many locations are excluded by this way.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Optional Boss Locations"
diff --git a/worlds/sc2wol/PoolFilter.py b/worlds/sc2wol/PoolFilter.py
index 4a19e2dbb305..23422a3d1ea5 100644
--- a/worlds/sc2wol/PoolFilter.py
+++ b/worlds/sc2wol/PoolFilter.py
@@ -1,6 +1,7 @@
from typing import Callable, Dict, List, Set
from BaseClasses import MultiWorld, ItemClassification, Item, Location
-from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, filler_items
+from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, filler_items, \
+ progressive_if_nco
from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\
mission_orders, MissionInfo, alt_final_mission_locations, MissionPools
from .Options import get_option_value, MissionOrder, FinalMap, MissionProgressLocations, LocationInclusion
@@ -15,7 +16,7 @@
]
BARRACKS_UNITS = {"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre"}
-FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine"}
+FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine", "Cyclone"}
STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven", "Liberator", "Valkyrie"}
PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"}
@@ -93,7 +94,10 @@ def get_item_upgrades(inventory: List[Item], parent_item: Item or str):
]
-def get_item_quantity(item):
+def get_item_quantity(item: Item, multiworld: MultiWorld, player: int):
+ if (not get_option_value(multiworld, player, "nco_items")) \
+ and item.name in progressive_if_nco:
+ return 1
return get_full_item_list()[item.name].quantity
@@ -138,13 +142,13 @@ def attempt_removal(item: Item) -> bool:
if not all(requirement(self) for requirement in requirements):
# If item cannot be removed, lock or revert
self.logical_inventory.add(item.name)
- for _ in range(get_item_quantity(item)):
+ for _ in range(get_item_quantity(item, self.multiworld, self.player)):
locked_items.append(copy_item(item))
return False
return True
-
+
# Limit the maximum number of upgrades
- maxUpgrad = get_option_value(self.multiworld, self.player,
+ maxUpgrad = get_option_value(self.multiworld, self.player,
"max_number_of_upgrades")
if maxUpgrad != -1:
unit_avail_upgrades = {}
@@ -197,15 +201,16 @@ def attempt_removal(item: Item) -> bool:
# Don't process general upgrades, they may have been pre-locked per-level
for item in items_to_lock:
if item in inventory:
+ item_quantity = inventory.count(item)
# Unit upgrades, lock all levels
- for _ in range(inventory.count(item)):
+ for _ in range(item_quantity):
inventory.remove(item)
if item not in locked_items:
# Lock all the associated items if not already locked
- for _ in range(get_item_quantity(item)):
+ for _ in range(item_quantity):
locked_items.append(copy_item(item))
- if item in existing_items:
- existing_items.remove(item)
+ if item in existing_items:
+ existing_items.remove(item)
if self.min_units_per_structure > 0 and self.has_units_per_structure():
requirements.append(lambda state: state.has_units_per_structure())
@@ -216,7 +221,13 @@ def attempt_removal(item: Item) -> bool:
while len(inventory) + len(locked_items) > inventory_size:
if len(inventory) == 0:
- raise Exception("Reduced item pool generation failed - not enough locations available to place items.")
+ # There are more items than locations and all of them are already locked due to YAML or logic.
+ # Random items from locked ones will go to starting items
+ self.multiworld.random.shuffle(locked_items)
+ while len(locked_items) > inventory_size:
+ item: Item = locked_items.pop()
+ self.multiworld.push_precollected(item)
+ break
# Select random item from removable items
item = self.multiworld.random.choice(inventory)
# Cascade removals to associated items
@@ -245,7 +256,7 @@ def attempt_removal(item: Item) -> bool:
for _ in range(inventory.count(transient_item)):
inventory.remove(transient_item)
if transient_item not in locked_items:
- for _ in range(get_item_quantity(transient_item)):
+ for _ in range(get_item_quantity(transient_item, self.multiworld, self.player)):
locked_items.append(copy_item(transient_item))
if transient_item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing):
self.logical_inventory.add(transient_item.name)
diff --git a/worlds/sc2wol/Starcraft2.kv b/worlds/sc2wol/Starcraft2.kv
index 9c52d64c4702..f0785b89e428 100644
--- a/worlds/sc2wol/Starcraft2.kv
+++ b/worlds/sc2wol/Starcraft2.kv
@@ -11,6 +11,6 @@
markup: True
halign: 'center'
valign: 'middle'
- padding_x: 5
+ padding: [5,0,5,0]
markup: True
outline_width: 1
diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py
index 93aebb7ad15a..5c487f8fee09 100644
--- a/worlds/sc2wol/__init__.py
+++ b/worlds/sc2wol/__init__.py
@@ -34,7 +34,7 @@ class SC2WoLWorld(World):
game = "Starcraft 2 Wings of Liberty"
web = Starcraft2WoLWebWorld()
- data_version = 4
+ data_version = 5
item_name_to_id = {name: data.code for name, data in get_full_item_list().items()}
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
@@ -46,7 +46,7 @@ class SC2WoLWorld(World):
mission_req_table = {}
final_mission_id: int
victory_item: str
- required_client_version = 0, 3, 6
+ required_client_version = 0, 4, 3
def __init__(self, multiworld: MultiWorld, player: int):
super(SC2WoLWorld, self).__init__(multiworld, player)
diff --git a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md
index f7c8519a2a7c..18bda6478457 100644
--- a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md
+++ b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md
@@ -31,4 +31,24 @@ The goal is to beat the final mission: 'All In'. The config file determines whic
By default, any of StarCraft 2's items (specified above) can be in another player's world. See the
[Advanced YAML Guide](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en)
-for more information on how to change this.
\ No newline at end of file
+for more information on how to change this.
+
+## Unique Local Commands
+
+The following commands are only available when using the Starcraft 2 Client to play with Archipelago.
+
+- `/difficulty [difficulty]` Overrides the difficulty set for the world.
+ - Options: casual, normal, hard, brutal
+- `/game_speed [game_speed]` Overrides the game speed for the world
+ - Options: default, slower, slow, normal, fast, faster
+- `/color [color]` Changes your color (Currently has no effect)
+ - Options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen, brown,
+ lightgreen, darkgrey, pink, rainbow, random, default
+- `/disable_mission_check` Disables the check to see if a mission is available to play. Meant for co-op runs where one
+ player can play the next mission in a chain the other player is doing.
+- `/play [mission_id]` Starts a Starcraft 2 mission based off of the mission_id provided
+- `/available` Get what missions are currently available to play
+- `/unfinished` Get what missions are currently available to play and have not had all locations checked
+- `/set_path [path]` Menually set the SC2 install directory (if the automatic detection fails)
+- `/download_data` Download the most recent release of the necassry files for playing SC2 with Archipelago. Will
+ overwrite existing files
diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py
index 9d6f28607ec1..3e9015eab766 100644
--- a/worlds/sm/__init__.py
+++ b/worlds/sm/__init__.py
@@ -1,18 +1,17 @@
from __future__ import annotations
-import logging
+import base64
import copy
-import os
+import logging
import threading
-import base64
-import settings
import typing
from typing import Any, Dict, Iterable, List, Set, TextIO, TypedDict
-from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, CollectionState, Tutorial
-from Fill import fill_restrictive
-from worlds.AutoWorld import World, AutoLogicRegister, WebWorld
-from worlds.generic.Rules import set_rule, add_rule, add_item_rule
+import settings
+from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial
+from Options import Accessibility
+from worlds.AutoWorld import AutoLogicRegister, WebWorld, World
+from worlds.generic.Rules import add_rule, set_rule
logger = logging.getLogger("Super Metroid")
@@ -113,15 +112,12 @@ class SMWorld(World):
required_client_version = (0, 2, 6)
itemManager: ItemManager
- spheres = None
Logic.factory('vanilla')
def __init__(self, world: MultiWorld, player: int):
self.rom_name_available_event = threading.Event()
self.locations = {}
- if SMWorld.spheres != None:
- SMWorld.spheres = None
super().__init__(world, player)
@classmethod
@@ -295,7 +291,7 @@ def create_regions(self):
for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions:
src_region = self.multiworld.get_region(src.Name, self.player)
dest_region = self.multiworld.get_region(dest.Name, self.player)
- if ((src.Name + "->" + dest.Name, self.player) not in self.multiworld._entrance_cache):
+ if src.Name + "->" + dest.Name not in self.multiworld.regions.entrance_cache[self.player]:
src_region.exits.append(Entrance(self.player, src.Name + "->" + dest.Name, src_region))
srcDestEntrance = self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player)
srcDestEntrance.connect(dest_region)
@@ -369,7 +365,7 @@ def get_player_ItemLocation(progression_only: bool):
locationsDict[first_local_collected_loc.name]),
itemLoc.item.player,
True)
- for itemLoc in SMWorld.spheres if itemLoc.item.player == self.player and (not progression_only or itemLoc.item.advancement)
+ for itemLoc in spheres if itemLoc.item.player == self.player and (not progression_only or itemLoc.item.advancement)
]
# Having a sorted itemLocs from collection order is required for escapeTrigger when Tourian is Disabled.
@@ -377,8 +373,10 @@ def get_player_ItemLocation(progression_only: bool):
# get_spheres could be cached in multiworld?
# Another possible solution would be to have a globally accessible list of items in the order in which the get placed in push_item
# and use the inversed starting from the first progression item.
- if (SMWorld.spheres == None):
- SMWorld.spheres = [itemLoc for sphere in self.multiworld.get_spheres() for itemLoc in sorted(sphere, key=lambda location: location.name)]
+ spheres: List[Location] = getattr(self.multiworld, "_sm_spheres", None)
+ if spheres is None:
+ spheres = [itemLoc for sphere in self.multiworld.get_spheres() for itemLoc in sorted(sphere, key=lambda location: location.name)]
+ setattr(self.multiworld, "_sm_spheres", spheres)
self.itemLocs = [
ItemLocation(copy.copy(ItemManager.Items[itemLoc.item.type
@@ -391,7 +389,7 @@ def get_player_ItemLocation(progression_only: bool):
escapeTrigger = None
if self.variaRando.randoExec.randoSettings.restrictions["EscapeTrigger"]:
#used to simulate received items
- first_local_collected_loc = next(itemLoc for itemLoc in SMWorld.spheres if itemLoc.player == self.player)
+ first_local_collected_loc = next(itemLoc for itemLoc in spheres if itemLoc.player == self.player)
playerItemsItemLocs = get_player_ItemLocation(False)
playerProgItemsItemLocs = get_player_ItemLocation(True)
@@ -564,8 +562,8 @@ def APPostPatchRom(self, romPatcher):
multiWorldItems: List[ByteEdit] = []
idx = 0
vanillaItemTypesCount = 21
- for itemLoc in self.multiworld.get_locations():
- if itemLoc.player == self.player and "Boss" not in locationsDict[itemLoc.name].Class:
+ for itemLoc in self.multiworld.get_locations(self.player):
+ if "Boss" not in locationsDict[itemLoc.name].Class:
SMZ3NameToSMType = {
"ETank": "ETank", "Missile": "Missile", "Super": "Super", "PowerBomb": "PowerBomb", "Bombs": "Bomb",
"Charge": "Charge", "Ice": "Ice", "HiJump": "HiJump", "SpeedBooster": "SpeedBooster",
diff --git a/worlds/sm/docs/multiworld_en.md b/worlds/sm/docs/multiworld_en.md
index ce91e7a7e403..129150774341 100644
--- a/worlds/sm/docs/multiworld_en.md
+++ b/worlds/sm/docs/multiworld_en.md
@@ -49,7 +49,7 @@ them. Player settings page: [Super Metroid Player Settings Page](/games/Super%20
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
-validator page: [YAML Validation page](/mysterycheck)
+validator page: [YAML Validation page](/check)
## Generating a Single-Player Game
diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py
index a603b61c5809..8a10f3edea55 100644
--- a/worlds/sm64ex/Options.py
+++ b/worlds/sm64ex/Options.py
@@ -88,6 +88,12 @@ class ExclamationBoxes(Choice):
option_Off = 0
option_1Ups_Only = 1
+class CompletionType(Choice):
+ """Set goal for game completion"""
+ display_name = "Completion Goal"
+ option_Last_Bowser_Stage = 0
+ option_All_Bowser_Stages = 1
+
class ProgressiveKeys(DefaultOnToggle):
"""Keys will first grant you access to the Basement, then to the Secound Floor"""
@@ -110,4 +116,5 @@ class ProgressiveKeys(DefaultOnToggle):
"death_link": DeathLink,
"BuddyChecks": BuddyChecks,
"ExclamationBoxes": ExclamationBoxes,
+ "CompletionType" : CompletionType,
}
diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py
index 7c50ba4708af..27b5fc8f7e38 100644
--- a/worlds/sm64ex/Rules.py
+++ b/worlds/sm64ex/Rules.py
@@ -124,4 +124,9 @@ def set_rules(world, player: int, area_connections):
add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS1Cost[player].value))
add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS2Cost[player].value))
- world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player)
+ if world.CompletionType[player] == "last_bowser_stage":
+ world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player)
+ elif world.CompletionType[player] == "all_bowser_stages":
+ world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Dark World", 'Region', player) and \
+ state.can_reach("Bowser in the Fire Sea", 'Region', player) and \
+ state.can_reach("Bowser in the Sky", 'Region', player)
diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py
index 6a7a3bd27214..3cc87708e723 100644
--- a/worlds/sm64ex/__init__.py
+++ b/worlds/sm64ex/__init__.py
@@ -154,6 +154,7 @@ def fill_slot_data(self):
"MIPS2Cost": self.multiworld.MIPS2Cost[self.player].value,
"StarsToFinish": self.multiworld.StarsToFinish[self.player].value,
"DeathLink": self.multiworld.death_link[self.player].value,
+ "CompletionType" : self.multiworld.CompletionType[self.player].value,
}
def generate_output(self, output_directory: str):
diff --git a/worlds/smw/docs/setup_en.md b/worlds/smw/docs/setup_en.md
index 9ca8bdf58a16..3967f544a056 100644
--- a/worlds/smw/docs/setup_en.md
+++ b/worlds/smw/docs/setup_en.md
@@ -50,7 +50,7 @@ them. Player settings page: [Super Mario World Player Settings Page](/games/Supe
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
-validator page: [YAML Validation page](/mysterycheck)
+validator page: [YAML Validation page](/check)
## Joining a MultiWorld Game
diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py
index e2eb2ac80a13..2cc2ac97d952 100644
--- a/worlds/smz3/__init__.py
+++ b/worlds/smz3/__init__.py
@@ -470,7 +470,7 @@ def fill_slot_data(self):
def collect(self, state: CollectionState, item: Item) -> bool:
state.smz3state[self.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)])
if item.advancement:
- state.prog_items[item.name, item.player] += 1
+ state.prog_items[item.player][item.name] += 1
return True # indicate that a logical state change has occured
return False
@@ -478,9 +478,9 @@ def remove(self, state: CollectionState, item: Item) -> bool:
name = self.collect_item(state, item, True)
if name:
state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)])
- state.prog_items[name, item.player] -= 1
- if state.prog_items[name, item.player] < 1:
- del (state.prog_items[name, item.player])
+ state.prog_items[item.player][item.name] -= 1
+ if state.prog_items[item.player][item.name] < 1:
+ del (state.prog_items[item.player][item.name])
return True
return False
diff --git a/worlds/smz3/docs/multiworld_en.md b/worlds/smz3/docs/multiworld_en.md
index da6e29ab6923..53842a3c6fa4 100644
--- a/worlds/smz3/docs/multiworld_en.md
+++ b/worlds/smz3/docs/multiworld_en.md
@@ -47,7 +47,7 @@ them. Player settings page: [SMZ3 Player Settings Page](/games/SMZ3/player-setti
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
-validator page: [YAML Validation page](/mysterycheck)
+validator page: [YAML Validation page](/check)
## Generating a Single-Player Game
diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py
index 3c173dec2f31..e464b7fd3b8e 100644
--- a/worlds/soe/Logic.py
+++ b/worlds/soe/Logic.py
@@ -3,7 +3,7 @@
from BaseClasses import MultiWorld
from worlds.AutoWorld import LogicMixin
from . import pyevermizer
-from .Options import EnergyCore
+from .Options import EnergyCore, OutOfBounds, SequenceBreaks
# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early?
@@ -61,4 +61,10 @@ def soe_has(self: LogicProtocol, progress: int, world: MultiWorld, player: int,
if w.energy_core == EnergyCore.option_fragments:
progress = pyevermizer.P_CORE_FRAGMENT
count = w.required_fragments
+ elif progress == pyevermizer.P_ALLOW_OOB:
+ if world.worlds[player].out_of_bounds == OutOfBounds.option_logic:
+ return True
+ elif progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS:
+ if world.worlds[player].sequence_breaks == SequenceBreaks.option_logic:
+ return True
return self._soe_count(progress, world, player, count) >= count
diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py
index f1a30745f8f0..3de2de34ac67 100644
--- a/worlds/soe/Options.py
+++ b/worlds/soe/Options.py
@@ -38,6 +38,12 @@ class OffOnFullChoice(Choice):
alias_chaos = 2
+class OffOnLogicChoice(Choice):
+ option_off = 0
+ option_on = 1
+ option_logic = 2
+
+
# actual options
class Difficulty(EvermizerFlags, Choice):
"""Changes relative spell cost and stuff"""
@@ -93,10 +99,18 @@ class ExpModifier(Range):
default = 200
-class FixSequence(EvermizerFlag, DefaultOnToggle):
- """Fix some sequence breaks"""
- display_name = "Fix Sequence"
- flag = '1'
+class SequenceBreaks(EvermizerFlags, OffOnLogicChoice):
+ """Disable, enable some sequence breaks or put them in logic"""
+ display_name = "Sequence Breaks"
+ default = 0
+ flags = ['', 'j', 'J']
+
+
+class OutOfBounds(EvermizerFlags, OffOnLogicChoice):
+ """Disable, enable the out-of-bounds glitch or put it in logic"""
+ display_name = "Out Of Bounds"
+ default = 0
+ flags = ['', 'u', 'U']
class FixCheats(EvermizerFlag, DefaultOnToggle):
@@ -240,7 +254,8 @@ class SoEProgressionBalancing(ProgressionBalancing):
"available_fragments": AvailableFragments,
"money_modifier": MoneyModifier,
"exp_modifier": ExpModifier,
- "fix_sequence": FixSequence,
+ "sequence_breaks": SequenceBreaks,
+ "out_of_bounds": OutOfBounds,
"fix_cheats": FixCheats,
"fix_infinite_ammo": FixInfiniteAmmo,
"fix_atlas_glitch": FixAtlasGlitch,
diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py
index f887325c60ea..d02a8d02ee97 100644
--- a/worlds/soe/__init__.py
+++ b/worlds/soe/__init__.py
@@ -10,12 +10,8 @@
from BaseClasses import Entrance, Item, ItemClassification, Location, LocationProgressType, Region, Tutorial
from Utils import output_path
-try:
- import pyevermizer # from package
-except ImportError:
- import traceback
- traceback.print_exc()
- from . import pyevermizer # as part of the source tree
+import pyevermizer # from package
+# from . import pyevermizer # as part of the source tree
from . import Logic # load logic mixin
from .Options import soe_options, Difficulty, EnergyCore, RequiredFragments, AvailableFragments
@@ -179,6 +175,8 @@ class SoEWorld(World):
evermizer_seed: int
connect_name: str
energy_core: int
+ sequence_breaks: int
+ out_of_bounds: int
available_fragments: int
required_fragments: int
@@ -191,6 +189,8 @@ def __init__(self, *args, **kwargs):
def generate_early(self) -> None:
# store option values that change logic
self.energy_core = self.multiworld.energy_core[self.player].value
+ self.sequence_breaks = self.multiworld.sequence_breaks[self.player].value
+ self.out_of_bounds = self.multiworld.out_of_bounds[self.player].value
self.required_fragments = self.multiworld.required_fragments[self.player].value
if self.required_fragments > self.multiworld.available_fragments[self.player].value:
self.multiworld.available_fragments[self.player].value = self.required_fragments
@@ -224,9 +224,8 @@ def create_regions(self):
max_difficulty = 1 if self.multiworld.difficulty[self.player] == Difficulty.option_easy else 256
# TODO: generate *some* regions from locations' requirements?
- r = Region('Menu', self.player, self.multiworld)
- r.exits = [Entrance(self.player, 'New Game', r)]
- self.multiworld.regions += [r]
+ menu = Region('Menu', self.player, self.multiworld)
+ self.multiworld.regions += [menu]
def get_sphere_index(evermizer_loc):
"""Returns 0, 1 or 2 for locations in spheres 1, 2, 3+"""
@@ -234,11 +233,14 @@ def get_sphere_index(evermizer_loc):
return 2
return min(2, len(evermizer_loc.requires))
+ # create ingame region
+ ingame = Region('Ingame', self.player, self.multiworld)
+
# group locations into spheres (1, 2, 3+ at index 0, 1, 2)
spheres: typing.Dict[int, typing.Dict[int, typing.List[SoELocation]]] = {}
for loc in _locations:
spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append(
- SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r,
+ SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame,
loc.difficulty > max_difficulty))
# location balancing data
@@ -280,18 +282,16 @@ def sphere1_blocked_items_rule(item):
late_locations = self.multiworld.random.sample(late_bosses, late_count)
# add locations to the world
- r = Region('Ingame', self.player, self.multiworld)
for sphere in spheres.values():
for locations in sphere.values():
for location in locations:
- r.locations.append(location)
+ ingame.locations.append(location)
if location.name in late_locations:
location.progress_type = LocationProgressType.PRIORITY
- r.locations.append(SoELocation(self.player, 'Done', None, r))
- self.multiworld.regions += [r]
-
- self.multiworld.get_entrance('New Game', self.player).connect(self.multiworld.get_region('Ingame', self.player))
+ ingame.locations.append(SoELocation(self.player, 'Done', None, ingame))
+ menu.connect(ingame, "New Game")
+ self.multiworld.regions += [ingame]
def create_items(self):
# add regular items to the pool
@@ -417,7 +417,7 @@ def generate_output(self, output_directory: str):
flags += option.to_flag()
with open(placement_file, "wb") as f: # generate placement file
- for location in filter(lambda l: l.player == self.player, self.multiworld.get_locations()):
+ for location in self.multiworld.get_locations(self.player):
item = location.item
assert item is not None, "Can't handle unfilled location"
if item.code is None or location.address is None:
diff --git a/worlds/soe/docs/multiworld_en.md b/worlds/soe/docs/multiworld_en.md
index d995cea56ae9..58b9aabf6a9a 100644
--- a/worlds/soe/docs/multiworld_en.md
+++ b/worlds/soe/docs/multiworld_en.md
@@ -29,7 +29,7 @@ them. Player settings page: [Secret of Evermore Player Settings PAge](/games/Sec
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator
-page: [YAML Validation page](/mysterycheck)
+page: [YAML Validation page](/check)
## Generating a Single-Player Game
diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt
index 878a2a80cc32..710f51ddb09a 100644
--- a/worlds/soe/requirements.txt
+++ b/worlds/soe/requirements.txt
@@ -1,18 +1,36 @@
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.11'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.11'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.11'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-macosx_10_9_x86_64.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.8'
-#pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-macosx_10_9_x86_64.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.9'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.9'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.10'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.11'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-1.tar.gz#0.44.0 ; python_version < '3.8' or python_version > '3.11' or (sys_platform != 'win32' and sys_platform != 'linux' and sys_platform != 'darwin') or (platform_machine != 'AMD64' and platform_machine != 'x86_64' and platform_machine != 'aarch64' and platform_machine != 'universal2' and platform_machine != 'arm64')
+pyevermizer==0.46.1 \
+ --hash=sha256:9fd71b5e4af26a5dd24a9cbf5320bf0111eef80320613401a1c03011b1515806 \
+ --hash=sha256:23f553ed0509d9a238b2832f775e0b5abd7741b38ab60d388294ee8a7b96c5fb \
+ --hash=sha256:7189b67766418a3e7e6c683f09c5e758aa1a5c24316dd9b714984bac099c4b75 \
+ --hash=sha256:befa930711e63d5d5892f67fd888b2e65e746363e74599c53e71ecefb90ae16a \
+ --hash=sha256:202933ce21e0f33859537bf3800d9a626c70262a9490962e3f450171758507ca \
+ --hash=sha256:c20ca69311c696528e1122ebc7d33775ee971f538c0e3e05dd3bfd4de10b82d4 \
+ --hash=sha256:74dc689a771ae5ffcd5257e763f571ee890e3e87bdb208233b7f451522c00d66 \
+ --hash=sha256:072296baef464daeb6304cf58827dcbae441ad0803039aee1c0caa10d56e0674 \
+ --hash=sha256:7921baf20d52d92d6aeb674125963c335b61abb7e1298bde4baf069d11a2d05e \
+ --hash=sha256:ca098034a84007038c2bff004582e6e6ac2fa9cc8b9251301d25d7e2adcee6da \
+ --hash=sha256:22ddb29823c19be9b15e1b3627db1babfe08b486aede7d5cc463a0a1ae4c75d8 \
+ --hash=sha256:bf1c441b49026d9000166be6e2f63fc351a3fda170aa3fdf18d44d5e5d044640 \
+ --hash=sha256:9710aa7957b4b1f14392006237eb95803acf27897377df3e85395f057f4316b9 \
+ --hash=sha256:8feb676c198bee17ab991ee015828345ac3f87c27dfdb3061d92d1fe47c184b4 \
+ --hash=sha256:597026dede72178ff3627a4eb3315de8444461c7f0f856f5773993c3f9790c53 \
+ --hash=sha256:70f9b964bdfb5191e8f264644c5d1af3041c66fe15261df8a99b3d719dc680d6 \
+ --hash=sha256:74655c0353ffb6cda30485091d0917ce703b128cd824b612b3110a85c79a93d0 \
+ --hash=sha256:0e9c74d105d4ec3af12404e85bb8776931c043657add19f798ee69465f92b999 \
+ --hash=sha256:d3c13446d3d482b9cce61ac73b38effd26fcdcf7f693a405868d3aaaa4d18ca6 \
+ --hash=sha256:371ac3360640ef439a5920ddfe11a34e9d2e546ed886bb8c9ed312611f9f4655 \
+ --hash=sha256:6e5cf63b036f24d2ae4375a88df8d0bc93208352939521d1fcac3c829ef2c363 \
+ --hash=sha256:edf28f5c4d1950d17343adf6d8d40d12c7e982d1e39535d55f7915e122cd8b0e \
+ --hash=sha256:b5ef6f3b4e04f677c296f60f7f4c320ac22cd5bc09c05574460116c8641c801a \
+ --hash=sha256:dd651f66720af4abe2ddae29944e299a57ff91e6fca1739e6dc1f8fd7a8c2b39 \
+ --hash=sha256:4e278f5f72c27f9703bce5514d2fead8c00361caac03e94b0bf9ad8a144f1eeb \
+ --hash=sha256:38f36ea1f545b835c3ecd6e081685a233ac2e3cf0eec8916adc92e4d791098a6 \
+ --hash=sha256:0a2e58ed6e7c42f006cc17d32cec1f432f01b3fe490e24d71471b36e0d0d8742 \
+ --hash=sha256:c1b658db76240596c03571c60635abe953f36fb55b363202971831c2872ea9a0 \
+ --hash=sha256:deb5a84a6a56325eb6701336cdbf70f72adaaeab33cbe953d0e551ecf2592f20 \
+ --hash=sha256:b1425c793e0825f58b3726e7afebaf5a296c07cb0d28580d0ee93dbe10dcdf63 \
+ --hash=sha256:11995fb4dfd14b5c359591baee2a864c5814650ba0084524d4ea0466edfaf029 \
+ --hash=sha256:5d2120b5c93ae322fe2a85d48e3eab4168a19e974a880908f1ac291c0300940f \
+ --hash=sha256:254912ea4bfaaffb0abe366e73bd9ecde622677d6afaf2ce8a0c330df99fefd9 \
+ --hash=sha256:540d8e4525f0b5255c1554b4589089dc58e15df22f343e9545ea00f7012efa07 \
+ --hash=sha256:f69b8ebded7eed181fabe30deabae89fd10c41964f38abb26b19664bbe55c1ae
diff --git a/worlds/soe/test/__init__.py b/worlds/soe/test/__init__.py
index 3c2a0dc1b625..27d38605aae4 100644
--- a/worlds/soe/test/__init__.py
+++ b/worlds/soe/test/__init__.py
@@ -1,5 +1,20 @@
from test.TestBase import WorldTestBase
+from typing import Iterable
class SoETestBase(WorldTestBase):
game = "Secret of Evermore"
+
+ def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (),
+ satisfied=True) -> None:
+ """
+ Tests that unreachable can't be reached. Tests that reachable can be reached if satisfied=True.
+ Usage: test with satisfied=False, collect requirements into state, test again with satisfied=True
+ """
+ for location in reachable:
+ self.assertEqual(self.can_reach_location(location), satisfied,
+ f"{location} is unreachable but should be" if satisfied else
+ f"{location} is reachable but shouldn't be")
+ for location in unreachable:
+ self.assertFalse(self.can_reach_location(location),
+ f"{location} is reachable but shouldn't be")
diff --git a/worlds/soe/test/TestAccess.py b/worlds/soe/test/test_access.py
similarity index 100%
rename from worlds/soe/test/TestAccess.py
rename to worlds/soe/test/test_access.py
diff --git a/worlds/soe/test/TestGoal.py b/worlds/soe/test/test_goal.py
similarity index 100%
rename from worlds/soe/test/TestGoal.py
rename to worlds/soe/test/test_goal.py
diff --git a/worlds/soe/test/test_oob.py b/worlds/soe/test/test_oob.py
new file mode 100644
index 000000000000..27e00cd3e764
--- /dev/null
+++ b/worlds/soe/test/test_oob.py
@@ -0,0 +1,51 @@
+import typing
+from . import SoETestBase
+
+
+class OoBTest(SoETestBase):
+ """Tests that 'on' doesn't put out-of-bounds in logic. This is also the test base for OoB in logic."""
+ options: typing.Dict[str, typing.Any] = {"out_of_bounds": "on"}
+
+ def testOoBAccess(self):
+ in_logic = self.options["out_of_bounds"] == "logic"
+
+ # some locations that just need a weapon + OoB
+ oob_reachable = [
+ "Aquagoth", "Sons of Sth.", "Mad Monk", "Magmar", # OoB can use volcano shop to skip rock skip
+ "Levitate", "Fireball", "Drain", "Speed",
+ "E. Crustacia #107", "Energy Core #285", "Vanilla Gauge #57",
+ ]
+ # some locations that should still be unreachable
+ oob_unreachable = [
+ "Tiny", "Rimsala",
+ "Barrier", "Call Up", "Reflect", "Force Field", "Stop", # Stop guy doesn't spawn for the other entrances
+ "Pyramid bottom #118", "Tiny's hideout #160", "Tiny's hideout #161", "Greenhouse #275",
+ ]
+ # OoB + Diamond Eyes
+ de_reachable = [
+ "Tiny's hideout #160",
+ ]
+ # still unreachable
+ de_unreachable = [
+ "Tiny",
+ "Tiny's hideout #161",
+ ]
+
+ self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=False)
+ self.collect_by_name("Gladiator Sword")
+ self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=in_logic)
+ self.collect_by_name("Diamond Eye")
+ self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic)
+
+ def testOoBGoal(self):
+ # still need Energy Core with OoB if sequence breaks are not in logic
+ for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]:
+ self.collect_by_name(item)
+ self.assertBeatable(False)
+ self.collect_by_name("Energy Core")
+ self.assertBeatable(True)
+
+
+class OoBInLogicTest(OoBTest):
+ """Tests that stuff that should be reachable/unreachable with out-of-bounds actually is."""
+ options: typing.Dict[str, typing.Any] = {"out_of_bounds": "logic"}
diff --git a/worlds/soe/test/test_sequence_breaks.py b/worlds/soe/test/test_sequence_breaks.py
new file mode 100644
index 000000000000..4248f9b47d97
--- /dev/null
+++ b/worlds/soe/test/test_sequence_breaks.py
@@ -0,0 +1,45 @@
+import typing
+from . import SoETestBase
+
+
+class SequenceBreaksTest(SoETestBase):
+ """Tests that 'on' doesn't put sequence breaks in logic. This is also the test base for in-logic."""
+ options: typing.Dict[str, typing.Any] = {"sequence_breaks": "on"}
+
+ def testSequenceBreaksAccess(self):
+ in_logic = self.options["sequence_breaks"] == "logic"
+
+ # some locations that just need any weapon + sequence break
+ break_reachable = [
+ "Sons of Sth.", "Mad Monk", "Magmar",
+ "Fireball",
+ "Volcano Room1 #73", "Pyramid top #135",
+ ]
+ # some locations that should still be unreachable
+ break_unreachable = [
+ "Aquagoth", "Megataur", "Tiny", "Rimsala",
+ "Barrier", "Call Up", "Levitate", "Stop", "Drain", "Escape",
+ "Greenhouse #275", "E. Crustacia #107", "Energy Core #285", "Vanilla Gauge #57",
+ ]
+
+ self.assertLocationReachability(reachable=break_reachable, unreachable=break_unreachable, satisfied=False)
+ self.collect_by_name("Gladiator Sword")
+ self.assertLocationReachability(reachable=break_reachable, unreachable=break_unreachable, satisfied=in_logic)
+ self.collect_by_name("Spider Claw") # Gauge now just needs non-sword
+ self.assertEqual(self.can_reach_location("Vanilla Gauge #57"), in_logic)
+ self.collect_by_name("Bronze Spear") # Escape now just needs either Megataur or Rimsala dead
+ self.assertEqual(self.can_reach_location("Escape"), in_logic)
+
+ def testSequenceBreaksGoal(self):
+ in_logic = self.options["sequence_breaks"] == "logic"
+
+ # don't need Energy Core with sequence breaks in logic
+ for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]:
+ self.assertBeatable(False)
+ self.collect_by_name(item)
+ self.assertBeatable(in_logic)
+
+
+class SequenceBreaksInLogicTest(SequenceBreaksTest):
+ """Tests that stuff that should be reachable/unreachable with sequence breaks actually is."""
+ options: typing.Dict[str, typing.Any] = {"sequence_breaks": "logic"}
diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py
index 1f46eb79d79a..177b6436ae56 100644
--- a/worlds/stardew_valley/__init__.py
+++ b/worlds/stardew_valley/__init__.py
@@ -100,15 +100,15 @@ def create_region(name: str, exits: Iterable[str]) -> Region:
return region
world_regions, self.randomized_entrances = create_regions(create_region, self.multiworld.random, self.options)
- self.multiworld.regions.extend(world_regions)
def add_location(name: str, code: Optional[int], region: str):
- region = self.multiworld.get_region(region, self.player)
+ region = world_regions[region]
location = StardewLocation(self.player, name, code, region)
location.access_rule = lambda _: True
region.locations.append(location)
create_locations(add_location, self.options, self.multiworld.random)
+ self.multiworld.regions.extend(world_regions.values())
def create_items(self):
self.precollect_starting_season()
diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py
index d5c71dae4694..2d28b4de43c1 100644
--- a/worlds/stardew_valley/items.py
+++ b/worlds/stardew_valley/items.py
@@ -368,8 +368,8 @@ def create_arcade_machine_items(item_factory: StardewItemFactory, options: Stard
def create_player_buffs(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
- items.extend(item_factory(item) for item in [Buff.movement] * options.number_of_movement_buffs.value)
- items.extend(item_factory(item) for item in [Buff.luck] * options.number_of_luck_buffs.value)
+ items.extend(item_factory(item) for item in [Buff.movement] * options.movement_buff_number.value)
+ items.extend(item_factory(item) for item in [Buff.luck] * options.luck_buff_number.value)
def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]):
diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py
index b2841d1566da..0746bd775242 100644
--- a/worlds/stardew_valley/logic.py
+++ b/worlds/stardew_valley/logic.py
@@ -927,7 +927,7 @@ def can_chop_perfectly(self) -> StardewRule:
return region_rule & ((tool_rule & foraging_rule) | magic_rule)
def has_max_buffs(self) -> StardewRule:
- return self.received(Buff.movement, self.options.number_of_movement_buffs.value) & self.received(Buff.luck, self.options.number_of_luck_buffs.value)
+ return self.received(Buff.movement, self.options.movement_buff_number.value) & self.received(Buff.luck, self.options.luck_buff_number.value)
def get_weapon_rule_for_floor_tier(self, tier: int):
if tier >= 4:
@@ -1376,7 +1376,7 @@ def has_rusty_key(self) -> StardewRule:
return self.received(Wallet.rusty_key)
def can_win_egg_hunt(self) -> StardewRule:
- number_of_movement_buffs = self.options.number_of_movement_buffs.value
+ number_of_movement_buffs = self.options.movement_buff_number.value
if self.options.festival_locations == FestivalLocations.option_hard or number_of_movement_buffs < 2:
return True_()
return self.received(Buff.movement, number_of_movement_buffs // 2)
diff --git a/worlds/stardew_valley/mods/mod_data.py b/worlds/stardew_valley/mods/mod_data.py
index 81c498941132..30fe96c9d906 100644
--- a/worlds/stardew_valley/mods/mod_data.py
+++ b/worlds/stardew_valley/mods/mod_data.py
@@ -21,3 +21,11 @@ class ModNames:
ayeisha = "Ayeisha - The Postal Worker (Custom NPC)"
riley = "Custom NPC - Riley"
skull_cavern_elevator = "Skull Cavern Elevator"
+
+
+all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
+ ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
+ ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
+ ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
+ ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
+ ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator})
diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py
index 75573359a5ab..f462f507d4a3 100644
--- a/worlds/stardew_valley/options.py
+++ b/worlds/stardew_valley/options.py
@@ -556,8 +556,8 @@ class StardewValleyOptions(PerGameCommonOptions):
museumsanity: Museumsanity
friendsanity: Friendsanity
friendsanity_heart_size: FriendsanityHeartSize
- number_of_movement_buffs: NumberOfMovementBuffs
- number_of_luck_buffs: NumberOfLuckBuffs
+ movement_buff_number: NumberOfMovementBuffs
+ luck_buff_number: NumberOfLuckBuffs
exclude_ginger_island: ExcludeGingerIsland
trap_items: TrapItems
multiple_day_sleep_enabled: MultipleDaySleepEnabled
diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py
index e8daa772d887..d8e224841143 100644
--- a/worlds/stardew_valley/regions.py
+++ b/worlds/stardew_valley/regions.py
@@ -429,7 +429,7 @@ def create_final_connections(world_options) -> List[ConnectionData]:
def create_regions(region_factory: RegionFactory, random: Random, world_options) -> Tuple[
- Iterable[Region], Dict[str, str]]:
+ Dict[str, Region], Dict[str, str]]:
final_regions = create_final_regions(world_options)
regions: Dict[str: Region] = {region.name: region_factory(region.name, region.exits) for region in
final_regions}
@@ -444,7 +444,7 @@ def create_regions(region_factory: RegionFactory, random: Random, world_options)
if connection.name in entrances:
entrances[connection.name].connect(regions[connection.destination])
- return regions.values(), randomized_data
+ return regions, randomized_data
def randomize_connections(random: Random, world_options, regions_by_name) -> Tuple[
diff --git a/worlds/stardew_valley/stardew_rule.py b/worlds/stardew_valley/stardew_rule.py
index d0fa9858cc0b..9c96de00d333 100644
--- a/worlds/stardew_valley/stardew_rule.py
+++ b/worlds/stardew_valley/stardew_rule.py
@@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
-from typing import Iterable, Dict, List, Union, FrozenSet
+from typing import Iterable, Dict, List, Union, FrozenSet, Set
from BaseClasses import CollectionState, ItemClassification
from .items import item_table
@@ -14,13 +14,13 @@ def __call__(self, state: CollectionState) -> bool:
raise NotImplementedError
def __or__(self, other) -> StardewRule:
- if isinstance(other, Or):
+ if type(other) is Or:
return Or(self, *other.rules)
return Or(self, other)
def __and__(self, other) -> StardewRule:
- if isinstance(other, And):
+ if type(other) is And:
return And(other.rules.union({self}))
return And(self, other)
@@ -80,30 +80,40 @@ def get_difficulty(self):
return 999999999
+false_ = False_()
+true_ = True_()
+assert false_ is False_()
+assert true_ is True_()
+
+
class Or(StardewRule):
rules: FrozenSet[StardewRule]
+ _simplified: bool
def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule):
- rules_list = set()
+ rules_list: Set[StardewRule]
+
if isinstance(rule, Iterable):
- rules_list.update(rule)
+ rules_list = {*rule}
else:
- rules_list.add(rule)
+ rules_list = {rule}
if rules is not None:
rules_list.update(rules)
assert rules_list, "Can't create a Or conditions without rules"
- new_rules = set()
- for rule in rules_list:
- if isinstance(rule, Or):
- new_rules.update(rule.rules)
- else:
- new_rules.add(rule)
- rules_list = new_rules
+ if any(type(rule) is Or for rule in rules_list):
+ new_rules: Set[StardewRule] = set()
+ for rule in rules_list:
+ if type(rule) is Or:
+ new_rules.update(rule.rules)
+ else:
+ new_rules.add(rule)
+ rules_list = new_rules
self.rules = frozenset(rules_list)
+ self._simplified = False
def __call__(self, state: CollectionState) -> bool:
return any(rule(state) for rule in self.rules)
@@ -112,11 +122,11 @@ def __repr__(self):
return f"({' | '.join(repr(rule) for rule in self.rules)})"
def __or__(self, other):
- if isinstance(other, True_):
+ if other is true_:
return other
- if isinstance(other, False_):
+ if other is false_:
return self
- if isinstance(other, Or):
+ if type(other) is Or:
return Or(self.rules.union(other.rules))
return Or(self.rules.union({other}))
@@ -131,46 +141,53 @@ def get_difficulty(self):
return min(rule.get_difficulty() for rule in self.rules)
def simplify(self) -> StardewRule:
- if any(isinstance(rule, True_) for rule in self.rules):
- return True_()
+ if self._simplified:
+ return self
+ if true_ in self.rules:
+ return true_
- simplified_rules = {rule.simplify() for rule in self.rules}
- simplified_rules = {rule for rule in simplified_rules if rule is not False_()}
+ simplified_rules = [simplified for simplified in {rule.simplify() for rule in self.rules}
+ if simplified is not false_]
if not simplified_rules:
- return False_()
+ return false_
if len(simplified_rules) == 1:
- return next(iter(simplified_rules))
+ return simplified_rules[0]
- return Or(simplified_rules)
+ self.rules = frozenset(simplified_rules)
+ self._simplified = True
+ return self
class And(StardewRule):
rules: FrozenSet[StardewRule]
+ _simplified: bool
def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule):
- rules_list = set()
+ rules_list: Set[StardewRule]
+
if isinstance(rule, Iterable):
- rules_list.update(rule)
+ rules_list = {*rule}
else:
- rules_list.add(rule)
+ rules_list = {rule}
if rules is not None:
rules_list.update(rules)
- if len(rules_list) < 1:
- rules_list.add(True_())
-
- new_rules = set()
- for rule in rules_list:
- if isinstance(rule, And):
- new_rules.update(rule.rules)
- else:
- new_rules.add(rule)
- rules_list = new_rules
+ if not rules_list:
+ rules_list.add(true_)
+ elif any(type(rule) is And for rule in rules_list):
+ new_rules: Set[StardewRule] = set()
+ for rule in rules_list:
+ if type(rule) is And:
+ new_rules.update(rule.rules)
+ else:
+ new_rules.add(rule)
+ rules_list = new_rules
self.rules = frozenset(rules_list)
+ self._simplified = False
def __call__(self, state: CollectionState) -> bool:
return all(rule(state) for rule in self.rules)
@@ -179,11 +196,11 @@ def __repr__(self):
return f"({' & '.join(repr(rule) for rule in self.rules)})"
def __and__(self, other):
- if isinstance(other, True_):
+ if other is true_:
return self
- if isinstance(other, False_):
+ if other is false_:
return other
- if isinstance(other, And):
+ if type(other) is And:
return And(self.rules.union(other.rules))
return And(self.rules.union({other}))
@@ -198,19 +215,23 @@ def get_difficulty(self):
return max(rule.get_difficulty() for rule in self.rules)
def simplify(self) -> StardewRule:
- if any(isinstance(rule, False_) for rule in self.rules):
- return False_()
+ if self._simplified:
+ return self
+ if false_ in self.rules:
+ return false_
- simplified_rules = {rule.simplify() for rule in self.rules}
- simplified_rules = {rule for rule in simplified_rules if rule is not True_()}
+ simplified_rules = [simplified for simplified in {rule.simplify() for rule in self.rules}
+ if simplified is not true_]
if not simplified_rules:
- return True_()
+ return true_
if len(simplified_rules) == 1:
- return next(iter(simplified_rules))
+ return simplified_rules[0]
- return And(simplified_rules)
+ self.rules = frozenset(simplified_rules)
+ self._simplified = True
+ return self
class Count(StardewRule):
@@ -218,11 +239,12 @@ class Count(StardewRule):
rules: List[StardewRule]
def __init__(self, count: int, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule):
- rules_list = []
+ rules_list: List[StardewRule]
+
if isinstance(rule, Iterable):
- rules_list.extend(rule)
+ rules_list = [*rule]
else:
- rules_list.append(rule)
+ rules_list = [rule]
if rules is not None:
rules_list.extend(rules)
@@ -260,11 +282,12 @@ class TotalReceived(StardewRule):
player: int
def __init__(self, count: int, items: Union[str, Iterable[str]], player: int):
- items_list = []
+ items_list: List[str]
+
if isinstance(items, Iterable):
- items_list.extend(items)
+ items_list = [*items]
else:
- items_list.append(items)
+ items_list = [items]
assert items_list, "Can't create a Total Received conditions without items"
for item in items_list:
diff --git a/worlds/stardew_valley/test/TestBackpack.py b/worlds/stardew_valley/test/TestBackpack.py
index f26a7c1f03d4..378c90e40a7f 100644
--- a/worlds/stardew_valley/test/TestBackpack.py
+++ b/worlds/stardew_valley/test/TestBackpack.py
@@ -5,40 +5,41 @@
class TestBackpackVanilla(SVTestBase):
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla}
- def test_no_backpack_in_pool(self):
- item_names = {item.name for item in self.multiworld.get_items()}
- self.assertNotIn("Progressive Backpack", item_names)
+ def test_no_backpack(self):
+ with self.subTest("no items"):
+ item_names = {item.name for item in self.multiworld.get_items()}
+ self.assertNotIn("Progressive Backpack", item_names)
- def test_no_backpack_locations(self):
- location_names = {location.name for location in self.multiworld.get_locations()}
- self.assertNotIn("Large Pack", location_names)
- self.assertNotIn("Deluxe Pack", location_names)
+ with self.subTest("no locations"):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertNotIn("Large Pack", location_names)
+ self.assertNotIn("Deluxe Pack", location_names)
class TestBackpackProgressive(SVTestBase):
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive}
- def test_backpack_is_in_pool_2_times(self):
- item_names = [item.name for item in self.multiworld.get_items()]
- self.assertEqual(item_names.count("Progressive Backpack"), 2)
+ def test_backpack(self):
+ with self.subTest(check="has items"):
+ item_names = [item.name for item in self.multiworld.get_items()]
+ self.assertEqual(item_names.count("Progressive Backpack"), 2)
- def test_2_backpack_locations(self):
- location_names = {location.name for location in self.multiworld.get_locations()}
- self.assertIn("Large Pack", location_names)
- self.assertIn("Deluxe Pack", location_names)
+ with self.subTest(check="has locations"):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertIn("Large Pack", location_names)
+ self.assertIn("Deluxe Pack", location_names)
-class TestBackpackEarlyProgressive(SVTestBase):
+class TestBackpackEarlyProgressive(TestBackpackProgressive):
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive}
- def test_backpack_is_in_pool_2_times(self):
- item_names = [item.name for item in self.multiworld.get_items()]
- self.assertEqual(item_names.count("Progressive Backpack"), 2)
+ @property
+ def run_default_tests(self) -> bool:
+ # EarlyProgressive is default
+ return False
- def test_2_backpack_locations(self):
- location_names = {location.name for location in self.multiworld.get_locations()}
- self.assertIn("Large Pack", location_names)
- self.assertIn("Deluxe Pack", location_names)
+ def test_backpack(self):
+ super().test_backpack()
- def test_progressive_backpack_is_in_early_pool(self):
- self.assertIn("Progressive Backpack", self.multiworld.early_items[1])
+ with self.subTest(check="is early"):
+ self.assertIn("Progressive Backpack", self.multiworld.early_items[1])
diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py
index 0142ad007908..46c6685ad536 100644
--- a/worlds/stardew_valley/test/TestGeneration.py
+++ b/worlds/stardew_valley/test/TestGeneration.py
@@ -1,5 +1,8 @@
+import typing
+
from BaseClasses import ItemClassification, MultiWorld
-from . import setup_solo_multiworld, SVTestBase
+from . import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_with_mods, \
+ allsanity_options_without_mods, minimal_locations_maximal_items
from .. import locations, items, location_table, options
from ..data.villagers_data import all_villagers_by_name, all_villagers_by_mod_by_name
from ..items import items_by_group, Group
@@ -7,11 +10,11 @@
from ..mods.mod_data import ModNames
-def get_real_locations(tester: SVTestBase, multiworld: MultiWorld):
+def get_real_locations(tester: typing.Union[SVTestBase, SVTestCase], multiworld: MultiWorld):
return [location for location in multiworld.get_locations(tester.player) if not location.event]
-def get_real_location_names(tester: SVTestBase, multiworld: MultiWorld):
+def get_real_location_names(tester: typing.Union[SVTestBase, SVTestCase], multiworld: MultiWorld):
return [location.name for location in multiworld.get_locations(tester.player) if not location.event]
@@ -115,21 +118,6 @@ def test_does_not_create_exactly_two_items(self):
self.assertTrue(count == 0 or count == 2)
-class TestGivenProgressiveBackpack(SVTestBase):
- options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive}
-
- def test_when_generate_world_then_two_progressive_backpack_are_added(self):
- self.assertEqual(self.multiworld.itempool.count(self.world.create_item("Progressive Backpack")), 2)
-
- def test_when_generate_world_then_backpack_locations_are_added(self):
- created_locations = {location.name for location in self.multiworld.get_locations(1)}
- backpacks_exist = [location.name in created_locations
- for location in locations.locations_by_tag[LocationTags.BACKPACK]
- if location.name != "Premium Pack"]
- all_exist = all(backpacks_exist)
- self.assertTrue(all_exist)
-
-
class TestRemixedMineRewards(SVTestBase):
def test_when_generate_world_then_one_reward_is_added_per_chest(self):
# assert self.world.create_item("Rusty Sword") in self.multiworld.itempool
@@ -205,17 +193,17 @@ def test_all_location_created_are_in_location_table(self):
self.assertIn(location.name, location_table)
-class TestLocationAndItemCount(SVTestBase):
+class TestLocationAndItemCount(SVTestCase):
def test_minimal_location_maximal_items_still_valid(self):
- min_max_options = self.minimal_locations_maximal_items()
+ min_max_options = minimal_locations_maximal_items()
multiworld = setup_solo_multiworld(min_max_options)
valid_locations = get_real_locations(self, multiworld)
self.assertGreaterEqual(len(valid_locations), len(multiworld.itempool))
def test_allsanity_without_mods_has_at_least_locations(self):
expected_locations = 994
- allsanity_options = self.allsanity_options_without_mods()
+ allsanity_options = allsanity_options_without_mods()
multiworld = setup_solo_multiworld(allsanity_options)
number_locations = len(get_real_locations(self, multiworld))
self.assertGreaterEqual(number_locations, expected_locations)
@@ -228,7 +216,7 @@ def test_allsanity_without_mods_has_at_least_locations(self):
def test_allsanity_with_mods_has_at_least_locations(self):
expected_locations = 1246
- allsanity_options = self.allsanity_options_with_mods()
+ allsanity_options = allsanity_options_with_mods()
multiworld = setup_solo_multiworld(allsanity_options)
number_locations = len(get_real_locations(self, multiworld))
self.assertGreaterEqual(number_locations, expected_locations)
@@ -245,6 +233,11 @@ class TestFriendsanityNone(SVTestBase):
options.Friendsanity.internal_name: options.Friendsanity.option_none,
}
+ @property
+ def run_default_tests(self) -> bool:
+ # None is default
+ return False
+
def test_no_friendsanity_items(self):
for item in self.multiworld.itempool:
self.assertFalse(item.name.endswith(" <3"))
@@ -416,6 +409,7 @@ def test_friendsanity_all_with_marriage_locations(self):
self.assertLessEqual(int(hearts), 10)
+""" # Assuming math is correct if we check 2 points
class TestFriendsanityAllNpcsWithMarriageHeartSize2(SVTestBase):
options = {
options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
@@ -528,6 +522,7 @@ def test_friendsanity_all_with_marriage_locations(self):
self.assertTrue(hearts == 4 or hearts == 8 or hearts == 12 or hearts == 14)
else:
self.assertTrue(hearts == 4 or hearts == 8 or hearts == 10)
+"""
class TestFriendsanityAllNpcsWithMarriageHeartSize5(SVTestBase):
diff --git a/worlds/stardew_valley/test/TestItems.py b/worlds/stardew_valley/test/TestItems.py
index 7f48f9347c10..38f59c74904f 100644
--- a/worlds/stardew_valley/test/TestItems.py
+++ b/worlds/stardew_valley/test/TestItems.py
@@ -6,12 +6,12 @@
from typing import Set
from BaseClasses import ItemClassification, MultiWorld
-from . import setup_solo_multiworld, SVTestBase
+from . import setup_solo_multiworld, SVTestCase, allsanity_options_without_mods
from .. import ItemData, StardewValleyWorld
from ..items import Group, item_table
-class TestItems(SVTestBase):
+class TestItems(SVTestCase):
def test_can_create_item_of_resource_pack(self):
item_name = "Resource Pack: 500 Money"
@@ -46,7 +46,7 @@ def test_babies_come_in_all_shapes_and_sizes(self):
def test_correct_number_of_stardrops(self):
seed = random.randrange(sys.maxsize)
- allsanity_options = self.allsanity_options_without_mods()
+ allsanity_options = allsanity_options_without_mods()
multiworld = setup_solo_multiworld(allsanity_options, seed=seed)
stardrop_items = [item for item in multiworld.get_items() if "Stardrop" in item.name]
self.assertEqual(len(stardrop_items), 5)
diff --git a/worlds/stardew_valley/test/TestLogicSimplification.py b/worlds/stardew_valley/test/TestLogicSimplification.py
index 33b2428098bc..3f02643b83dc 100644
--- a/worlds/stardew_valley/test/TestLogicSimplification.py
+++ b/worlds/stardew_valley/test/TestLogicSimplification.py
@@ -1,56 +1,57 @@
+import unittest
from .. import True_
from ..logic import Received, Has, False_, And, Or
-def test_simplify_true_in_and():
- rules = {
- "Wood": True_(),
- "Rock": True_(),
- }
- summer = Received("Summer", 0, 1)
- assert (Has("Wood", rules) & summer & Has("Rock", rules)).simplify() == summer
-
-
-def test_simplify_false_in_or():
- rules = {
- "Wood": False_(),
- "Rock": False_(),
- }
- summer = Received("Summer", 0, 1)
- assert (Has("Wood", rules) | summer | Has("Rock", rules)).simplify() == summer
-
-
-def test_simplify_and_in_and():
- rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)),
- And(Received('Winter', 0, 1), Received('Spring', 0, 1)))
- assert rule.simplify() == And(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1),
- Received('Spring', 0, 1))
-
-
-def test_simplify_duplicated_and():
- rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)),
- And(Received('Summer', 0, 1), Received('Fall', 0, 1)))
- assert rule.simplify() == And(Received('Summer', 0, 1), Received('Fall', 0, 1))
-
-
-def test_simplify_or_in_or():
- rule = Or(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)),
- Or(Received('Winter', 0, 1), Received('Spring', 0, 1)))
- assert rule.simplify() == Or(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1),
- Received('Spring', 0, 1))
-
-
-def test_simplify_duplicated_or():
- rule = And(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)),
- Or(Received('Summer', 0, 1), Received('Fall', 0, 1)))
- assert rule.simplify() == Or(Received('Summer', 0, 1), Received('Fall', 0, 1))
-
-
-def test_simplify_true_in_or():
- rule = Or(True_(), Received('Summer', 0, 1))
- assert rule.simplify() == True_()
-
-
-def test_simplify_false_in_and():
- rule = And(False_(), Received('Summer', 0, 1))
- assert rule.simplify() == False_()
+class TestSimplification(unittest.TestCase):
+ def test_simplify_true_in_and(self):
+ rules = {
+ "Wood": True_(),
+ "Rock": True_(),
+ }
+ summer = Received("Summer", 0, 1)
+ self.assertEqual((Has("Wood", rules) & summer & Has("Rock", rules)).simplify(),
+ summer)
+
+ def test_simplify_false_in_or(self):
+ rules = {
+ "Wood": False_(),
+ "Rock": False_(),
+ }
+ summer = Received("Summer", 0, 1)
+ self.assertEqual((Has("Wood", rules) | summer | Has("Rock", rules)).simplify(),
+ summer)
+
+ def test_simplify_and_in_and(self):
+ rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)),
+ And(Received('Winter', 0, 1), Received('Spring', 0, 1)))
+ self.assertEqual(rule.simplify(),
+ And(Received('Summer', 0, 1), Received('Fall', 0, 1),
+ Received('Winter', 0, 1), Received('Spring', 0, 1)))
+
+ def test_simplify_duplicated_and(self):
+ rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)),
+ And(Received('Summer', 0, 1), Received('Fall', 0, 1)))
+ self.assertEqual(rule.simplify(),
+ And(Received('Summer', 0, 1), Received('Fall', 0, 1)))
+
+ def test_simplify_or_in_or(self):
+ rule = Or(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)),
+ Or(Received('Winter', 0, 1), Received('Spring', 0, 1)))
+ self.assertEqual(rule.simplify(),
+ Or(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1),
+ Received('Spring', 0, 1)))
+
+ def test_simplify_duplicated_or(self):
+ rule = And(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)),
+ Or(Received('Summer', 0, 1), Received('Fall', 0, 1)))
+ self.assertEqual(rule.simplify(),
+ Or(Received('Summer', 0, 1), Received('Fall', 0, 1)))
+
+ def test_simplify_true_in_or(self):
+ rule = Or(True_(), Received('Summer', 0, 1))
+ self.assertEqual(rule.simplify(), True_())
+
+ def test_simplify_false_in_and(self):
+ rule = And(False_(), Received('Summer', 0, 1))
+ self.assertEqual(rule.simplify(), False_())
diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py
index 712aa300d537..02b1ebf64373 100644
--- a/worlds/stardew_valley/test/TestOptions.py
+++ b/worlds/stardew_valley/test/TestOptions.py
@@ -1,10 +1,11 @@
import itertools
+import unittest
from random import random
from typing import Dict
from BaseClasses import ItemClassification, MultiWorld
from Options import SpecialRange
-from . import setup_solo_multiworld, SVTestBase
+from . import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods
from .. import StardewItem, items_by_group, Group, StardewValleyWorld
from ..locations import locations_by_tag, LocationTags, location_table
from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations
@@ -17,21 +18,21 @@
TOOLS = {"Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can", "Fishing Rod"}
-def assert_can_win(tester: SVTestBase, multiworld: MultiWorld):
+def assert_can_win(tester: unittest.TestCase, multiworld: MultiWorld):
for item in multiworld.get_items():
multiworld.state.collect(item)
tester.assertTrue(multiworld.find_item("Victory", 1).can_reach(multiworld.state))
-def basic_checks(tester: SVTestBase, multiworld: MultiWorld):
+def basic_checks(tester: unittest.TestCase, multiworld: MultiWorld):
tester.assertIn(StardewItem("Victory", ItemClassification.progression, None, 1), multiworld.get_items())
assert_can_win(tester, multiworld)
non_event_locations = [location for location in multiworld.get_locations() if not location.event]
tester.assertEqual(len(multiworld.itempool), len(non_event_locations))
-def check_no_ginger_island(tester: SVTestBase, multiworld: MultiWorld):
+def check_no_ginger_island(tester: unittest.TestCase, multiworld: MultiWorld):
ginger_island_items = [item_data.name for item_data in items_by_group[Group.GINGER_ISLAND]]
ginger_island_locations = [location_data.name for location_data in locations_by_tag[LocationTags.GINGER_ISLAND]]
for item in multiworld.get_items():
@@ -48,9 +49,9 @@ def get_option_choices(option) -> Dict[str, int]:
return {}
-class TestGenerateDynamicOptions(SVTestBase):
+class TestGenerateDynamicOptions(SVTestCase):
def test_given_special_range_when_generate_then_basic_checks(self):
- options = self.world.options_dataclass.type_hints
+ options = StardewValleyWorld.options_dataclass.type_hints
for option_name, option in options.items():
if not isinstance(option, SpecialRange):
continue
@@ -62,7 +63,7 @@ def test_given_special_range_when_generate_then_basic_checks(self):
def test_given_choice_when_generate_then_basic_checks(self):
seed = int(random() * pow(10, 18) - 1)
- options = self.world.options_dataclass.type_hints
+ options = StardewValleyWorld.options_dataclass.type_hints
for option_name, option in options.items():
if not option.options:
continue
@@ -73,7 +74,7 @@ def test_given_choice_when_generate_then_basic_checks(self):
basic_checks(self, multiworld)
-class TestGoal(SVTestBase):
+class TestGoal(SVTestCase):
def test_given_goal_when_generate_then_victory_is_in_correct_location(self):
for goal, location in [("community_center", GoalName.community_center),
("grandpa_evaluation", GoalName.grandpa_evaluation),
@@ -90,7 +91,7 @@ def test_given_goal_when_generate_then_victory_is_in_correct_location(self):
self.assertEqual(victory.name, location)
-class TestSeasonRandomization(SVTestBase):
+class TestSeasonRandomization(SVTestCase):
def test_given_disabled_when_generate_then_all_seasons_are_precollected(self):
world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_disabled}
multi_world = setup_solo_multiworld(world_options)
@@ -114,7 +115,7 @@ def test_given_progressive_when_generate_then_3_progressive_seasons_are_in_the_p
self.assertEqual(items.count(Season.progressive), 3)
-class TestToolProgression(SVTestBase):
+class TestToolProgression(SVTestCase):
def test_given_vanilla_when_generate_then_no_tool_in_pool(self):
world_options = {ToolProgression.internal_name: ToolProgression.option_vanilla}
multi_world = setup_solo_multiworld(world_options)
@@ -147,9 +148,9 @@ def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self):
self.assertIn("Purchase Iridium Rod", locations)
-class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase):
+class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestCase):
def test_given_special_range_when_generate_exclude_ginger_island(self):
- options = self.world.options_dataclass.type_hints
+ options = StardewValleyWorld.options_dataclass.type_hints
for option_name, option in options.items():
if not isinstance(option, SpecialRange) or option_name == ExcludeGingerIsland.internal_name:
continue
@@ -162,7 +163,7 @@ def test_given_special_range_when_generate_exclude_ginger_island(self):
def test_given_choice_when_generate_exclude_ginger_island(self):
seed = int(random() * pow(10, 18) - 1)
- options = self.world.options_dataclass.type_hints
+ options = StardewValleyWorld.options_dataclass.type_hints
for option_name, option in options.items():
if not option.options or option_name == ExcludeGingerIsland.internal_name:
continue
@@ -191,9 +192,9 @@ def test_given_island_related_goal_then_override_exclude_ginger_island(self):
basic_checks(self, multiworld)
-class TestTraps(SVTestBase):
+class TestTraps(SVTestCase):
def test_given_no_traps_when_generate_then_no_trap_in_pool(self):
- world_options = self.allsanity_options_without_mods()
+ world_options = allsanity_options_without_mods()
world_options.update({TrapItems.internal_name: TrapItems.option_no_traps})
multi_world = setup_solo_multiworld(world_options)
@@ -209,7 +210,7 @@ def test_given_traps_when_generate_then_all_traps_in_pool(self):
for value in trap_option.options:
if value == "no_traps":
continue
- world_options = self.allsanity_options_with_mods()
+ world_options = allsanity_options_with_mods()
world_options.update({TrapItems.internal_name: trap_option.options[value]})
multi_world = setup_solo_multiworld(world_options)
trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups and item_data.mod_name is None]
@@ -219,7 +220,7 @@ def test_given_traps_when_generate_then_all_traps_in_pool(self):
self.assertIn(item, multiworld_items)
-class TestSpecialOrders(SVTestBase):
+class TestSpecialOrders(SVTestCase):
def test_given_disabled_then_no_order_in_pool(self):
world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled}
multi_world = setup_solo_multiworld(world_options)
diff --git a/worlds/stardew_valley/test/TestRegions.py b/worlds/stardew_valley/test/TestRegions.py
index 2347ca33db05..7ebbcece5c2c 100644
--- a/worlds/stardew_valley/test/TestRegions.py
+++ b/worlds/stardew_valley/test/TestRegions.py
@@ -2,7 +2,7 @@
import sys
import unittest
-from . import SVTestBase, setup_solo_multiworld
+from . import SVTestCase, setup_solo_multiworld
from .. import options, StardewValleyWorld, StardewValleyOptions
from ..options import EntranceRandomization, ExcludeGingerIsland
from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag
@@ -88,7 +88,7 @@ def test_entrance_randomization_without_island(self):
f"Connections are duplicated in randomization. Seed = {seed}")
-class TestEntranceClassifications(SVTestBase):
+class TestEntranceClassifications(SVTestCase):
def test_non_progression_are_all_accessible_with_empty_inventory(self):
for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py
index 0847d8a63b95..72337812cd80 100644
--- a/worlds/stardew_valley/test/TestRules.py
+++ b/worlds/stardew_valley/test/TestRules.py
@@ -24,7 +24,7 @@ class TestProgressiveToolsLogic(SVTestBase):
def setUp(self):
super().setUp()
- self.multiworld.state.prog_items = Counter()
+ self.multiworld.state.prog_items = {1: Counter()}
def test_sturgeon(self):
self.assertFalse(self.world.logic.has("Sturgeon")(self.multiworld.state))
diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py
index 53181154d391..b0c4ba2c7bcb 100644
--- a/worlds/stardew_valley/test/__init__.py
+++ b/worlds/stardew_valley/test/__init__.py
@@ -1,8 +1,10 @@
import os
+import unittest
from argparse import Namespace
from typing import Dict, FrozenSet, Tuple, Any, ClassVar
from BaseClasses import MultiWorld
+from Utils import cache_argsless
from test.TestBase import WorldTestBase
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
from .. import StardewValleyWorld
@@ -13,12 +15,18 @@
BundleRandomization, BundlePrice, FestivalLocations, FriendsanityHeartSize, ExcludeGingerIsland, TrapItems, Goal, Mods
-class SVTestBase(WorldTestBase):
- game = "Stardew Valley"
- world: StardewValleyWorld
+class SVTestCase(unittest.TestCase):
player: ClassVar[int] = 1
+ """Set to False to not skip some 'extra' tests"""
+ skip_extra_tests: bool = True
+ """Set to False to run tests that take long"""
skip_long_tests: bool = True
+
+class SVTestBase(WorldTestBase, SVTestCase):
+ game = "Stardew Valley"
+ world: StardewValleyWorld
+
def world_setup(self, *args, **kwargs):
super().world_setup(*args, **kwargs)
long_tests_key = "long"
@@ -34,66 +42,73 @@ def run_default_tests(self) -> bool:
should_run_default_tests = is_not_stardew_test and super().run_default_tests
return should_run_default_tests
- def minimal_locations_maximal_items(self):
- min_max_options = {
- SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
- Cropsanity.internal_name: Cropsanity.option_shuffled,
- BackpackProgression.internal_name: BackpackProgression.option_vanilla,
- ToolProgression.internal_name: ToolProgression.option_vanilla,
- SkillProgression.internal_name: SkillProgression.option_vanilla,
- BuildingProgression.internal_name: BuildingProgression.option_vanilla,
- ElevatorProgression.internal_name: ElevatorProgression.option_vanilla,
- ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled,
- SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled,
- HelpWantedLocations.internal_name: 0,
- Fishsanity.internal_name: Fishsanity.option_none,
- Museumsanity.internal_name: Museumsanity.option_none,
- Friendsanity.internal_name: Friendsanity.option_none,
- NumberOfMovementBuffs.internal_name: 12,
- NumberOfLuckBuffs.internal_name: 12,
- }
- return min_max_options
-
- def allsanity_options_without_mods(self):
- allsanity = {
- Goal.internal_name: Goal.option_perfection,
- BundleRandomization.internal_name: BundleRandomization.option_shuffled,
- BundlePrice.internal_name: BundlePrice.option_expensive,
- SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
- Cropsanity.internal_name: Cropsanity.option_shuffled,
- BackpackProgression.internal_name: BackpackProgression.option_progressive,
- ToolProgression.internal_name: ToolProgression.option_progressive,
- SkillProgression.internal_name: SkillProgression.option_progressive,
- BuildingProgression.internal_name: BuildingProgression.option_progressive,
- FestivalLocations.internal_name: FestivalLocations.option_hard,
- ElevatorProgression.internal_name: ElevatorProgression.option_progressive,
- ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling,
- SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
- HelpWantedLocations.internal_name: 56,
- Fishsanity.internal_name: Fishsanity.option_all,
- Museumsanity.internal_name: Museumsanity.option_all,
- Friendsanity.internal_name: Friendsanity.option_all_with_marriage,
- FriendsanityHeartSize.internal_name: 1,
- NumberOfMovementBuffs.internal_name: 12,
- NumberOfLuckBuffs.internal_name: 12,
- ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
- TrapItems.internal_name: TrapItems.option_nightmare,
- }
- return allsanity
-
- def allsanity_options_with_mods(self):
- allsanity = {}
- allsanity.update(self.allsanity_options_without_mods())
- all_mods = (
- ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
- ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
- ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
- ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
- ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
- ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator
- )
- allsanity.update({Mods.internal_name: all_mods})
- return allsanity
+
+@cache_argsless
+def minimal_locations_maximal_items():
+ min_max_options = {
+ SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
+ Cropsanity.internal_name: Cropsanity.option_shuffled,
+ BackpackProgression.internal_name: BackpackProgression.option_vanilla,
+ ToolProgression.internal_name: ToolProgression.option_vanilla,
+ SkillProgression.internal_name: SkillProgression.option_vanilla,
+ BuildingProgression.internal_name: BuildingProgression.option_vanilla,
+ ElevatorProgression.internal_name: ElevatorProgression.option_vanilla,
+ ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled,
+ HelpWantedLocations.internal_name: 0,
+ Fishsanity.internal_name: Fishsanity.option_none,
+ Museumsanity.internal_name: Museumsanity.option_none,
+ Friendsanity.internal_name: Friendsanity.option_none,
+ NumberOfMovementBuffs.internal_name: 12,
+ NumberOfLuckBuffs.internal_name: 12,
+ }
+ return min_max_options
+
+
+@cache_argsless
+def allsanity_options_without_mods():
+ allsanity = {
+ Goal.internal_name: Goal.option_perfection,
+ BundleRandomization.internal_name: BundleRandomization.option_shuffled,
+ BundlePrice.internal_name: BundlePrice.option_expensive,
+ SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
+ Cropsanity.internal_name: Cropsanity.option_shuffled,
+ BackpackProgression.internal_name: BackpackProgression.option_progressive,
+ ToolProgression.internal_name: ToolProgression.option_progressive,
+ SkillProgression.internal_name: SkillProgression.option_progressive,
+ BuildingProgression.internal_name: BuildingProgression.option_progressive,
+ FestivalLocations.internal_name: FestivalLocations.option_hard,
+ ElevatorProgression.internal_name: ElevatorProgression.option_progressive,
+ ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
+ HelpWantedLocations.internal_name: 56,
+ Fishsanity.internal_name: Fishsanity.option_all,
+ Museumsanity.internal_name: Museumsanity.option_all,
+ Friendsanity.internal_name: Friendsanity.option_all_with_marriage,
+ FriendsanityHeartSize.internal_name: 1,
+ NumberOfMovementBuffs.internal_name: 12,
+ NumberOfLuckBuffs.internal_name: 12,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
+ TrapItems.internal_name: TrapItems.option_nightmare,
+ }
+ return allsanity
+
+
+@cache_argsless
+def allsanity_options_with_mods():
+ allsanity = {}
+ allsanity.update(allsanity_options_without_mods())
+ all_mods = (
+ ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
+ ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
+ ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
+ ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
+ ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
+ ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator
+ )
+ allsanity.update({Mods.internal_name: all_mods})
+ return allsanity
+
pre_generated_worlds = {}
diff --git a/worlds/stardew_valley/test/checks/world_checks.py b/worlds/stardew_valley/test/checks/world_checks.py
index 2cdb0534d40a..9bd9fd614c26 100644
--- a/worlds/stardew_valley/test/checks/world_checks.py
+++ b/worlds/stardew_valley/test/checks/world_checks.py
@@ -1,8 +1,8 @@
+import unittest
from typing import List
from BaseClasses import MultiWorld, ItemClassification
from ... import StardewItem
-from .. import SVTestBase
def get_all_item_names(multiworld: MultiWorld) -> List[str]:
@@ -13,21 +13,21 @@ def get_all_location_names(multiworld: MultiWorld) -> List[str]:
return [location.name for location in multiworld.get_locations() if not location.event]
-def assert_victory_exists(tester: SVTestBase, multiworld: MultiWorld):
+def assert_victory_exists(tester: unittest.TestCase, multiworld: MultiWorld):
tester.assertIn(StardewItem("Victory", ItemClassification.progression, None, 1), multiworld.get_items())
-def collect_all_then_assert_can_win(tester: SVTestBase, multiworld: MultiWorld):
+def collect_all_then_assert_can_win(tester: unittest.TestCase, multiworld: MultiWorld):
for item in multiworld.get_items():
multiworld.state.collect(item)
tester.assertTrue(multiworld.find_item("Victory", 1).can_reach(multiworld.state))
-def assert_can_win(tester: SVTestBase, multiworld: MultiWorld):
+def assert_can_win(tester: unittest.TestCase, multiworld: MultiWorld):
assert_victory_exists(tester, multiworld)
collect_all_then_assert_can_win(tester, multiworld)
-def assert_same_number_items_locations(tester: SVTestBase, multiworld: MultiWorld):
+def assert_same_number_items_locations(tester: unittest.TestCase, multiworld: MultiWorld):
non_event_locations = [location for location in multiworld.get_locations() if not location.event]
tester.assertEqual(len(multiworld.itempool), len(non_event_locations))
\ No newline at end of file
diff --git a/worlds/stardew_valley/test/long/TestModsLong.py b/worlds/stardew_valley/test/long/TestModsLong.py
index b3ec6f1420ad..36a59ae854e5 100644
--- a/worlds/stardew_valley/test/long/TestModsLong.py
+++ b/worlds/stardew_valley/test/long/TestModsLong.py
@@ -1,23 +1,17 @@
+import unittest
from typing import List, Union
from BaseClasses import MultiWorld
-from worlds.stardew_valley.mods.mod_data import ModNames
+from worlds.stardew_valley.mods.mod_data import all_mods
from worlds.stardew_valley.test import setup_solo_multiworld
-from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestBase
+from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestCase
from worlds.stardew_valley.items import item_table
from worlds.stardew_valley.locations import location_table
from worlds.stardew_valley.options import Mods
from .option_names import options_to_include
-all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
- ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
- ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
- ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
- ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
- ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator})
-
-def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld):
+def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: unittest.TestCase, multiworld: MultiWorld):
if isinstance(chosen_mods, str):
chosen_mods = [chosen_mods]
for multiworld_item in multiworld.get_items():
@@ -30,7 +24,7 @@ def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase
tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods)
-class TestGenerateModsOptions(SVTestBase):
+class TestGenerateModsOptions(SVTestCase):
def test_given_mod_pairs_when_generate_then_basic_checks(self):
if self.skip_long_tests:
diff --git a/worlds/stardew_valley/test/long/TestOptionsLong.py b/worlds/stardew_valley/test/long/TestOptionsLong.py
index 23ac6125e64f..3634dc5fd169 100644
--- a/worlds/stardew_valley/test/long/TestOptionsLong.py
+++ b/worlds/stardew_valley/test/long/TestOptionsLong.py
@@ -1,13 +1,14 @@
+import unittest
from typing import Dict
from BaseClasses import MultiWorld
from Options import SpecialRange
from .option_names import options_to_include
from worlds.stardew_valley.test.checks.world_checks import assert_can_win, assert_same_number_items_locations
-from .. import setup_solo_multiworld, SVTestBase
+from .. import setup_solo_multiworld, SVTestCase
-def basic_checks(tester: SVTestBase, multiworld: MultiWorld):
+def basic_checks(tester: unittest.TestCase, multiworld: MultiWorld):
assert_can_win(tester, multiworld)
assert_same_number_items_locations(tester, multiworld)
@@ -20,7 +21,7 @@ def get_option_choices(option) -> Dict[str, int]:
return {}
-class TestGenerateDynamicOptions(SVTestBase):
+class TestGenerateDynamicOptions(SVTestCase):
def test_given_option_pair_when_generate_then_basic_checks(self):
if self.skip_long_tests:
return
diff --git a/worlds/stardew_valley/test/long/TestRandomWorlds.py b/worlds/stardew_valley/test/long/TestRandomWorlds.py
index 0145f471d100..e22c6c3564e5 100644
--- a/worlds/stardew_valley/test/long/TestRandomWorlds.py
+++ b/worlds/stardew_valley/test/long/TestRandomWorlds.py
@@ -4,7 +4,7 @@
from BaseClasses import MultiWorld
from Options import SpecialRange, Range
from .option_names import options_to_include
-from .. import setup_solo_multiworld, SVTestBase
+from .. import setup_solo_multiworld, SVTestCase
from ..checks.goal_checks import assert_perfection_world_is_valid, assert_goal_world_is_valid
from ..checks.option_checks import assert_can_reach_island_if_should, assert_cropsanity_same_number_items_and_locations, \
assert_festivals_give_access_to_deluxe_scarecrow
@@ -72,14 +72,14 @@ def generate_many_worlds(number_worlds: int, start_index: int) -> Dict[int, Mult
return multiworlds
-def check_every_multiworld_is_valid(tester: SVTestBase, multiworlds: Dict[int, MultiWorld]):
+def check_every_multiworld_is_valid(tester: SVTestCase, multiworlds: Dict[int, MultiWorld]):
for multiworld_id in multiworlds:
multiworld = multiworlds[multiworld_id]
with tester.subTest(f"Checking validity of world {multiworld_id}"):
check_multiworld_is_valid(tester, multiworld_id, multiworld)
-def check_multiworld_is_valid(tester: SVTestBase, multiworld_id: int, multiworld: MultiWorld):
+def check_multiworld_is_valid(tester: SVTestCase, multiworld_id: int, multiworld: MultiWorld):
assert_victory_exists(tester, multiworld)
assert_same_number_items_locations(tester, multiworld)
assert_goal_world_is_valid(tester, multiworld)
@@ -88,7 +88,7 @@ def check_multiworld_is_valid(tester: SVTestBase, multiworld_id: int, multiworld
assert_festivals_give_access_to_deluxe_scarecrow(tester, multiworld)
-class TestGenerateManyWorlds(SVTestBase):
+class TestGenerateManyWorlds(SVTestCase):
def test_generate_many_worlds_then_check_results(self):
if self.skip_long_tests:
return
diff --git a/worlds/stardew_valley/test/mods/TestBiggerBackpack.py b/worlds/stardew_valley/test/mods/TestBiggerBackpack.py
index 0265f61731c5..bc81f21963d8 100644
--- a/worlds/stardew_valley/test/mods/TestBiggerBackpack.py
+++ b/worlds/stardew_valley/test/mods/TestBiggerBackpack.py
@@ -7,45 +7,40 @@ class TestBiggerBackpackVanilla(SVTestBase):
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla,
options.Mods.internal_name: ModNames.big_backpack}
- def test_no_backpack_in_pool(self):
- item_names = {item.name for item in self.multiworld.get_items()}
- self.assertNotIn("Progressive Backpack", item_names)
+ def test_no_backpack(self):
+ with self.subTest(check="no items"):
+ item_names = {item.name for item in self.multiworld.get_items()}
+ self.assertNotIn("Progressive Backpack", item_names)
- def test_no_backpack_locations(self):
- location_names = {location.name for location in self.multiworld.get_locations()}
- self.assertNotIn("Large Pack", location_names)
- self.assertNotIn("Deluxe Pack", location_names)
- self.assertNotIn("Premium Pack", location_names)
+ with self.subTest(check="no locations"):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertNotIn("Large Pack", location_names)
+ self.assertNotIn("Deluxe Pack", location_names)
+ self.assertNotIn("Premium Pack", location_names)
class TestBiggerBackpackProgressive(SVTestBase):
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive,
options.Mods.internal_name: ModNames.big_backpack}
- def test_backpack_is_in_pool_3_times(self):
- item_names = [item.name for item in self.multiworld.get_items()]
- self.assertEqual(item_names.count("Progressive Backpack"), 3)
+ def test_backpack(self):
+ with self.subTest(check="has items"):
+ item_names = [item.name for item in self.multiworld.get_items()]
+ self.assertEqual(item_names.count("Progressive Backpack"), 3)
- def test_3_backpack_locations(self):
- location_names = {location.name for location in self.multiworld.get_locations()}
- self.assertIn("Large Pack", location_names)
- self.assertIn("Deluxe Pack", location_names)
- self.assertIn("Premium Pack", location_names)
+ with self.subTest(check="has locations"):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertIn("Large Pack", location_names)
+ self.assertIn("Deluxe Pack", location_names)
+ self.assertIn("Premium Pack", location_names)
-class TestBiggerBackpackEarlyProgressive(SVTestBase):
+class TestBiggerBackpackEarlyProgressive(TestBiggerBackpackProgressive):
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive,
options.Mods.internal_name: ModNames.big_backpack}
- def test_backpack_is_in_pool_3_times(self):
- item_names = [item.name for item in self.multiworld.get_items()]
- self.assertEqual(item_names.count("Progressive Backpack"), 3)
+ def test_backpack(self):
+ super().test_backpack()
- def test_3_backpack_locations(self):
- location_names = {location.name for location in self.multiworld.get_locations()}
- self.assertIn("Large Pack", location_names)
- self.assertIn("Deluxe Pack", location_names)
- self.assertIn("Premium Pack", location_names)
-
- def test_progressive_backpack_is_in_early_pool(self):
- self.assertIn("Progressive Backpack", self.multiworld.early_items[1])
+ with self.subTest(check="is early"):
+ self.assertIn("Progressive Backpack", self.multiworld.early_items[1])
diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py
index 02fd30a6b11f..9bdabaf73f14 100644
--- a/worlds/stardew_valley/test/mods/TestMods.py
+++ b/worlds/stardew_valley/test/mods/TestMods.py
@@ -4,24 +4,17 @@
import sys
from BaseClasses import MultiWorld
-from ...mods.mod_data import ModNames
-from .. import setup_solo_multiworld
-from ..TestOptions import basic_checks, SVTestBase
+from ...mods.mod_data import all_mods
+from .. import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods
+from ..TestOptions import basic_checks
from ... import items, Group, ItemClassification
from ...regions import RandomizationFlag, create_final_connections, randomize_connections, create_final_regions
from ...items import item_table, items_by_group
from ...locations import location_table
from ...options import Mods, EntranceRandomization, Friendsanity, SeasonRandomization, SpecialOrderLocations, ExcludeGingerIsland, TrapItems
-all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
- ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
- ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
- ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
- ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
- ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator})
-
-def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld):
+def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: unittest.TestCase, multiworld: MultiWorld):
if isinstance(chosen_mods, str):
chosen_mods = [chosen_mods]
for multiworld_item in multiworld.get_items():
@@ -34,7 +27,7 @@ def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase
tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods)
-class TestGenerateModsOptions(SVTestBase):
+class TestGenerateModsOptions(SVTestCase):
def test_given_single_mods_when_generate_then_basic_checks(self):
for mod in all_mods:
@@ -50,6 +43,8 @@ def test_given_mod_names_when_generate_paired_with_entrance_randomizer_then_basi
multiworld = setup_solo_multiworld({EntranceRandomization.internal_name: option, Mods: mod})
basic_checks(self, multiworld)
check_stray_mod_items(mod, self, multiworld)
+ if self.skip_extra_tests:
+ return # assume the rest will work as well
class TestBaseItemGeneration(SVTestBase):
@@ -103,7 +98,7 @@ def test_all_progression_items_except_island_are_added_to_the_pool(self):
self.assertIn(progression_item.name, all_created_items)
-class TestModEntranceRando(unittest.TestCase):
+class TestModEntranceRando(SVTestCase):
def test_mod_entrance_randomization(self):
@@ -137,12 +132,12 @@ def test_mod_entrance_randomization(self):
f"Connections are duplicated in randomization. Seed = {seed}")
-class TestModTraps(SVTestBase):
+class TestModTraps(SVTestCase):
def test_given_traps_when_generate_then_all_traps_in_pool(self):
for value in TrapItems.options:
if value == "no_traps":
continue
- world_options = self.allsanity_options_without_mods()
+ world_options = allsanity_options_without_mods()
world_options.update({TrapItems.internal_name: TrapItems.options[value], Mods: "Magic"})
multi_world = setup_solo_multiworld(world_options)
trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups]
diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py
index 7b25b61c81e5..de4f4e33dc87 100644
--- a/worlds/subnautica/__init__.py
+++ b/worlds/subnautica/__init__.py
@@ -65,22 +65,38 @@ def generate_early(self) -> None:
creature_pool, self.options.creature_scans.value)
def create_regions(self):
- self.multiworld.regions += [
- self.create_region("Menu", None, ["Lifepod 5"]),
- self.create_region("Planet 4546B",
- locations.events +
- [location["name"] for location in locations.location_table.values()] +
- [creature + creatures.suffix for creature in self.creatures_to_scan])
- ]
+ # Create Regions
+ menu_region = Region("Menu", self.player, self.multiworld)
+ planet_region = Region("Planet 4546B", self.player, self.multiworld)
+
+ # Link regions together
+ menu_region.connect(planet_region, "Lifepod 5")
+
+ # Create regular locations
+ location_names = itertools.chain((location["name"] for location in locations.location_table.values()),
+ (creature + creatures.suffix for creature in self.creatures_to_scan))
+ for location_name in location_names:
+ loc_id = self.location_name_to_id[location_name]
+ location = SubnauticaLocation(self.player, location_name, loc_id, planet_region)
+ planet_region.locations.append(location)
- # Link regions
- self.multiworld.get_entrance("Lifepod 5", self.player).connect(self.multiworld.get_region("Planet 4546B", self.player))
+ # Create events
+ goal_event_name = self.options.goal.get_event_name()
for event in locations.events:
- self.multiworld.get_location(event, self.player).place_locked_item(
+ location = SubnauticaLocation(self.player, event, None, planet_region)
+ planet_region.locations.append(location)
+ location.place_locked_item(
SubnauticaItem(event, ItemClassification.progression, None, player=self.player))
- # make the goal event the victory "item"
- self.multiworld.get_location(self.options.goal.get_event_name(), self.player).item.name = "Victory"
+ if event == goal_event_name:
+ # make the goal event the victory "item"
+ location.item.name = "Victory"
+
+ # Register regions to multiworld
+ self.multiworld.regions += [
+ menu_region,
+ planet_region
+ ]
# refer to Rules.py
set_rules = set_rules
diff --git a/worlds/terraria/Rules.dsv b/worlds/terraria/Rules.dsv
index 5f5551e465e1..b511db54de99 100644
--- a/worlds/terraria/Rules.dsv
+++ b/worlds/terraria/Rules.dsv
@@ -305,7 +305,7 @@ Hydraulic Volt Crusher; Calamity;
Life Fruit; ; (@mech_boss(1) & Wall of Flesh) | (@calamity & (Living Shard | Wall of Flesh));
Get a Life; Achievement; Life Fruit;
Topped Off; Achievement; Life Fruit;
-Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & (@mech_boss(1) | #Old One's Army Tier 3);
+Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & ((Wall of Flesh & @mech_boss(1)) | #Old One's Army Tier 3);
// Brimstone Elemental
Infernal Suevite; Calamity; @pickaxe(150) | Brimstone Elemental;
@@ -410,7 +410,7 @@ Scoria Bar; Calamity;
Seismic Hampick; Calamity | Pickaxe(210) | Hammer(95); Hardmode Anvil & Scoria Bar;
Life Alloy; Calamity; (Hardmode Anvil & Cryonic Bar & Perennial Bar & Scoria Bar) | Necromantic Geode;
Advanced Display; Calamity; Hardmode Anvil & Mysterious Circuitry & Dubious Plating & Life Alloy & Long Ranged Sensor Array;
-Old One's Army Tier 3; Location | Item; #Old One's Army Tier 1 & Golem;
+Old One's Army Tier 3; Location | Item; #Old One's Army Tier 1 & Wall of Flesh & Golem;
// Martian Madness
Martian Madness; Location | Item; Wall of Flesh & Golem;
diff --git a/worlds/terraria/docs/setup_en.md b/worlds/terraria/docs/setup_en.md
index 84744a4a337c..b69af591fa5c 100644
--- a/worlds/terraria/docs/setup_en.md
+++ b/worlds/terraria/docs/setup_en.md
@@ -31,6 +31,8 @@ highly recommended to use utility mods and features to speed up gameplay, such a
- (Can be used to break progression)
- Reduced Grinding
- Upgraded Research
+ - (WARNING: Do not use without Journey mode)
+ - (NOTE: If items you pick up aren't showing up in your inventory, check your research menu. This mod automatically researches certain items.)
## Configuring your YAML File
diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py
index de1d58e9616b..24230862bdf6 100644
--- a/worlds/timespinner/__init__.py
+++ b/worlds/timespinner/__init__.py
@@ -49,11 +49,9 @@ class TimespinnerWorld(World):
precalculated_weights: PreCalculatedWeights
- def __init__(self, world: MultiWorld, player: int):
- super().__init__(world, player)
- self.precalculated_weights = PreCalculatedWeights(world, player)
-
def generate_early(self) -> None:
+ self.precalculated_weights = PreCalculatedWeights(self.multiworld, self.player)
+
# in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly
if self.multiworld.start_inventory[self.player].value.pop('Meyef', 0) > 0:
self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true
diff --git a/worlds/tloz/docs/en_The Legend of Zelda.md b/worlds/tloz/docs/en_The Legend of Zelda.md
index e443c9b95373..7c2e6deda5bd 100644
--- a/worlds/tloz/docs/en_The Legend of Zelda.md
+++ b/worlds/tloz/docs/en_The Legend of Zelda.md
@@ -35,9 +35,17 @@ filler and useful items will cost less, and uncategorized items will be in the m
## Are there any other changes made?
-- The map and compass for each dungeon start already acquired, and other items can be found in their place.
+- The map and compass for each dungeon start already acquired, and other items can be found in their place.
- The Recorder will warp you between all eight levels regardless of Triforce count
- - It's possible for this to be your route to level 4!
+ - It's possible for this to be your route to level 4!
- Pressing Select will cycle through your inventory.
- Shop purchases are tracked within sessions, indicated by the item being elevated from its normal position.
-- What slots from a Take Any Cave have been chosen are similarly tracked.
\ No newline at end of file
+- What slots from a Take Any Cave have been chosen are similarly tracked.
+-
+
+## Local Unique Commands
+
+The following commands are only available when using the Zelda1Client to play with Archipelago.
+
+- `/nes` Check NES Connection State
+- `/toggle_msgs` Toggle displaying messages in EmuHawk
diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md
index 581e8cf7b24e..df857f16df5b 100644
--- a/worlds/tloz/docs/multiworld_en.md
+++ b/worlds/tloz/docs/multiworld_en.md
@@ -6,6 +6,7 @@
- Bundled with Archipelago: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- The BizHawk emulator. Versions 2.3.1 and higher are supported.
- [BizHawk at TASVideos](https://tasvideos.org/BizHawk)
+- Your legally acquired US v1.0 PRG0 ROM file, probably named `Legend of Zelda, The (U) (PRG0) [!].nes`
## Optional Software
@@ -44,7 +45,7 @@ them. Player settings page: [The Legend of Zelda Player Settings Page](/games/Th
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
-validator page: [YAML Validation page](/mysterycheck)
+validator page: [YAML Validation page](/check)
## Generating a Single-Player Game
diff --git a/worlds/undertale/Regions.py b/worlds/undertale/Regions.py
index ec13b249fa0e..138a6846537a 100644
--- a/worlds/undertale/Regions.py
+++ b/worlds/undertale/Regions.py
@@ -24,6 +24,7 @@ def link_undertale_areas(world: MultiWorld, player: int):
("True Lab", []),
("Core", ["Core Exit"]),
("New Home", ["New Home Exit"]),
+ ("Last Corridor", ["Last Corridor Exit"]),
("Barrier", []),
]
@@ -40,7 +41,8 @@ def link_undertale_areas(world: MultiWorld, player: int):
("News Show Entrance", "News Show"),
("Lab Elevator", "True Lab"),
("Core Exit", "New Home"),
- ("New Home Exit", "Barrier"),
+ ("New Home Exit", "Last Corridor"),
+ ("Last Corridor Exit", "Barrier"),
("Snowdin Hub", "Snowdin Forest"),
("Waterfall Hub", "Waterfall"),
("Hotland Hub", "Hotland"),
diff --git a/worlds/undertale/Rules.py b/worlds/undertale/Rules.py
index 648152c50414..897484b0508f 100644
--- a/worlds/undertale/Rules.py
+++ b/worlds/undertale/Rules.py
@@ -81,23 +81,27 @@ def set_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_entrance("New Home Exit", player),
lambda state: (state.has("Left Home Key", player) and
state.has("Right Home Key", player)) or
- state.has("Key Piece", player, state.multiworld.key_pieces[player]))
+ state.has("Key Piece", player, state.multiworld.key_pieces[player].value))
if _undertale_is_route(multiworld.state, player, 1):
set_rule(multiworld.get_entrance("Papyrus\" Home Entrance", player),
lambda state: _undertale_has_plot(state, player, "Complete Skeleton"))
set_rule(multiworld.get_entrance("Undyne\"s Home Entrance", player),
lambda state: _undertale_has_plot(state, player, "Fish") and state.has("Papyrus Date", player))
set_rule(multiworld.get_entrance("Lab Elevator", player),
- lambda state: state.has("Alphys Date", player) and _undertale_has_plot(state, player, "DT Extractor"))
+ lambda state: state.has("Alphys Date", player) and state.has("DT Extractor", player) and
+ ((state.has("Left Home Key", player) and state.has("Right Home Key", player)) or
+ state.has("Key Piece", player, state.multiworld.key_pieces[player].value)))
set_rule(multiworld.get_location("Alphys Date", player),
- lambda state: state.has("Undyne Letter EX", player) and state.has("Undyne Date", player))
+ lambda state: state.can_reach("New Home", "Region", player) and state.has("Undyne Letter EX", player)
+ and state.has("Undyne Date", player))
set_rule(multiworld.get_location("Papyrus Plot", player),
lambda state: state.can_reach("Snowdin Town", "Region", player))
set_rule(multiworld.get_location("Undyne Plot", player),
lambda state: state.can_reach("Waterfall", "Region", player))
set_rule(multiworld.get_location("True Lab Plot", player),
lambda state: state.can_reach("New Home", "Region", player)
- and state.can_reach("Letter Quest", "Location", player))
+ and state.can_reach("Letter Quest", "Location", player)
+ and state.can_reach("Alphys Date", "Location", player))
set_rule(multiworld.get_location("Chisps Machine", player),
lambda state: state.can_reach("True Lab", "Region", player))
set_rule(multiworld.get_location("Dog Sale 1", player),
@@ -113,7 +117,7 @@ def set_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_location("Hush Trade", player),
lambda state: state.can_reach("News Show", "Region", player) and state.has("Hot Dog...?", player, 1))
set_rule(multiworld.get_location("Letter Quest", player),
- lambda state: state.can_reach("New Home Exit", "Entrance", player) and state.has("Undyne Date", player))
+ lambda state: state.can_reach("Last Corridor", "Region", player) and state.has("Undyne Date", player))
if (not _undertale_is_route(multiworld.state, player, 2)) or _undertale_is_route(multiworld.state, player, 3):
set_rule(multiworld.get_location("Nicecream Punch Card", player),
lambda state: state.has("Punch Card", player, 3) and state.can_reach("Waterfall", "Region", player))
@@ -126,7 +130,7 @@ def set_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_location("Apron Hidden", player),
lambda state: state.can_reach("Cooking Show", "Region", player))
if _undertale_is_route(multiworld.state, player, 2) and \
- (multiworld.rando_love[player] or multiworld.rando_stats[player]):
+ (bool(multiworld.rando_love[player].value) or bool(multiworld.rando_stats[player].value)):
maxlv = 1
exp = 190
curarea = "Old Home"
@@ -304,7 +308,7 @@ def set_rules(multiworld: MultiWorld, player: int):
# Sets rules on completion condition
def set_completion_rules(multiworld: MultiWorld, player: int):
- completion_requirements = lambda state: state.can_reach("New Home Exit", "Entrance", player)
+ completion_requirements = lambda state: state.can_reach("Barrier", "Region", player)
if _undertale_is_route(multiworld.state, player, 1):
completion_requirements = lambda state: state.can_reach("True Lab", "Region", player)
diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py
index 5e3634470394..9e784a4a59a0 100644
--- a/worlds/undertale/__init__.py
+++ b/worlds/undertale/__init__.py
@@ -193,7 +193,7 @@ def set_rules(self):
def create_regions(self):
def UndertaleRegion(region_name: str, exits=[]):
ret = Region(region_name, self.player, self.multiworld)
- ret.locations = [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret)
+ ret.locations += [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret)
for loc_name, loc_data in advancement_table.items()
if loc_data.region == region_name and
(loc_name not in exclusion_table["NoStats"] or
diff --git a/worlds/undertale/docs/en_Undertale.md b/worlds/undertale/docs/en_Undertale.md
index 3905d3bc3ead..7ff5d55edad9 100644
--- a/worlds/undertale/docs/en_Undertale.md
+++ b/worlds/undertale/docs/en_Undertale.md
@@ -42,11 +42,22 @@ In the Pacifist run, you are not required to go to the Ruins to spare Toriel. Th
Undyne, and Mettaton EX. Just as it is in the vanilla game, you cannot kill anyone. You are also required to complete
the date/hangout with Papyrus, Undyne, and Alphys, in that order, before entering the True Lab.
-Additionally, custom items are required to hang out with Papyrus, Undyne, to enter the True Lab, and to fight
-Mettaton EX/NEO. The respective items for each interaction are `Complete Skeleton`, `Fish`, `DT Extractor`,
+Additionally, custom items are required to hang out with Papyrus, Undyne, to enter the True Lab, and to fight
+Mettaton EX/NEO. The respective items for each interaction are `Complete Skeleton`, `Fish`, `DT Extractor`,
and `Mettaton Plush`.
-The Riverperson will only take you to locations you have seen them at, meaning they will only take you to
+The Riverperson will only take you to locations you have seen them at, meaning they will only take you to
Waterfall if you have seen them at Waterfall at least once.
-If you press `W` while in the save menu, you will teleport back to the flower room, for quick access to the other areas.
\ No newline at end of file
+If you press `W` while in the save menu, you will teleport back to the flower room, for quick access to the other areas.
+
+## Unique Local Commands
+
+The following commands are only available when using the UndertaleClient to play with Archipelago.
+
+- `/resync` Manually trigger a resync.
+- `/savepath` Redirect to proper save data folder. This is necessary for Linux users to use before connecting.
+- `/auto_patch` Patch the game automatically.
+- `/patch` Patch the game. Only use this command if `/auto_patch` fails.
+- `/online` Toggles seeing other Undertale players.
+- `/deathlink` Toggles deathlink
diff --git a/worlds/wargroove/docs/en_Wargroove.md b/worlds/wargroove/docs/en_Wargroove.md
index 18474a426915..f08902535d4b 100644
--- a/worlds/wargroove/docs/en_Wargroove.md
+++ b/worlds/wargroove/docs/en_Wargroove.md
@@ -26,9 +26,16 @@ Any of the above items can be in another player's world.
## When the player receives an item, what happens?
-When the player receives an item, a message will appear in Wargroove with the item name and sender name, once an action
+When the player receives an item, a message will appear in Wargroove with the item name and sender name, once an action
is taken in game.
## What is the goal of this game when randomized?
The goal is to beat the level titled `The End` by finding the `Final Bridges`, `Final Walls`, and `Final Sickle`.
+
+## Unique Local Commands
+
+The following commands are only available when using the WargrooveClient to play with Archipelago.
+
+- `/resync` Manually trigger a resync.
+- `/commander` Set the current commander to the given commander.
diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py
index faaafd598b51..28eaba6404b6 100644
--- a/worlds/witness/__init__.py
+++ b/worlds/witness/__init__.py
@@ -66,7 +66,7 @@ def __init__(self, multiworld: "MultiWorld", player: int):
def _get_slot_data(self):
return {
- 'seed': self.multiworld.per_slot_randoms[self.player].randint(0, 1000000),
+ 'seed': self.random.randrange(0, 1000000),
'victory_location': int(self.player_logic.VICTORY_LOCATION, 16),
'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID,
'item_id_to_door_hexes': StaticWitnessItems.get_item_to_door_mappings(),
diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py
index 5d8bd5d3702c..8a9dab54bc18 100644
--- a/worlds/witness/hints.py
+++ b/worlds/witness/hints.py
@@ -228,8 +228,8 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int):
if item.player == player and item.code and item.advancement
}
loc_in_this_world = {
- location.name for location in multiworld.get_locations()
- if location.player == player and location.address
+ location.name for location in multiworld.get_locations(player)
+ if location.address
}
always_locations = [
@@ -306,7 +306,7 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int):
else:
hints.append((f"{loc} contains {item[0]}.", item[2]))
- next_random_hint_is_item = multiworld.per_slot_randoms[player].randint(0, 2)
+ next_random_hint_is_item = multiworld.per_slot_randoms[player].randrange(0, 2) # Moving this to the new system is in the bigger refactoring PR
while len(hints) < hint_amount:
if next_random_hint_is_item:
diff --git a/worlds/witness/settings/Disable_Unrandomized.txt b/worlds/witness/settings/Disable_Unrandomized.txt
index f7a0fcb7cbd6..3cd7ec1fb5eb 100644
--- a/worlds/witness/settings/Disable_Unrandomized.txt
+++ b/worlds/witness/settings/Disable_Unrandomized.txt
@@ -9,7 +9,7 @@ Requirement Changes:
0x181B3 - 0x00021 | 0x17D28 | 0x17C71
0x28B39 - True - Reflection
0x17CAB - True - True
-0x2779A - True - 0x17CFB | 0x3C12B | 0x17CF7
+0x2779A - 0x17CFB | 0x3C12B | 0x17CF7
Disabled Locations:
0x03505 (Tutorial Gate Close)
@@ -125,4 +125,4 @@ Precompleted Locations:
0x035F5
0x000D3
0x33A20
-0x03BE2
\ No newline at end of file
+0x03BE2
diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py
index 1e79f4f1338d..a5e1bfe1ad5f 100644
--- a/worlds/zillion/__init__.py
+++ b/worlds/zillion/__init__.py
@@ -329,23 +329,22 @@ def finalize_item_locations(self) -> None:
empty = zz_items[4]
multi_item = empty # a different patcher method differentiates empty from ap multi item
multi_items: Dict[str, Tuple[str, str]] = {} # zz_loc_name to (item_name, player_name)
- for loc in self.multiworld.get_locations():
- if loc.player == self.player:
- z_loc = cast(ZillionLocation, loc)
- # debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc)
- if z_loc.item is None:
- self.logger.warn("generate_output location has no item - is that ok?")
- z_loc.zz_loc.item = empty
- elif z_loc.item.player == self.player:
- z_item = cast(ZillionItem, z_loc.item)
- z_loc.zz_loc.item = z_item.zz_item
- else: # another player's item
- # print(f"put multi item in {z_loc.zz_loc.name}")
- z_loc.zz_loc.item = multi_item
- multi_items[z_loc.zz_loc.name] = (
- z_loc.item.name,
- self.multiworld.get_player_name(z_loc.item.player)
- )
+ for loc in self.multiworld.get_locations(self.player):
+ z_loc = cast(ZillionLocation, loc)
+ # debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc)
+ if z_loc.item is None:
+ self.logger.warn("generate_output location has no item - is that ok?")
+ z_loc.zz_loc.item = empty
+ elif z_loc.item.player == self.player:
+ z_item = cast(ZillionItem, z_loc.item)
+ z_loc.zz_loc.item = z_item.zz_item
+ else: # another player's item
+ # print(f"put multi item in {z_loc.zz_loc.name}")
+ z_loc.zz_loc.item = multi_item
+ multi_items[z_loc.zz_loc.name] = (
+ z_loc.item.name,
+ self.multiworld.get_player_name(z_loc.item.player)
+ )
# debug_zz_loc_ids.sort()
# for name, id_ in debug_zz_loc_ids.items():
# print(id_)
diff --git a/worlds/zillion/docs/en_Zillion.md b/worlds/zillion/docs/en_Zillion.md
index b5d37cc20209..06a11b7d7993 100644
--- a/worlds/zillion/docs/en_Zillion.md
+++ b/worlds/zillion/docs/en_Zillion.md
@@ -67,8 +67,16 @@ Note that in "restrictive" mode, Champ is the only one that can get Zillion powe
Canisters retain their original appearance, so you won't know if an item belongs to another player until you collect it.
-When you collect an item, you see the name of the player it goes to. You can see in the client log what item was collected.
+When you collect an item, you see the name of the player it goes to. You can see in the client log what item was
+collected.
## When the player receives an item, what happens?
The item collect sound is played. You can see in the client log what item was received.
+
+## Unique Local Commands
+
+The following commands are only available when using the ZillionClient to play with Archipelago.
+
+- `/sms` Tell the client that Zillion is running in RetroArch.
+- `/map` Toggle view of the map tracker.
diff --git a/worlds/zillion/docs/setup_en.md b/worlds/zillion/docs/setup_en.md
index 16000dbe3b7a..22dee5ee55e6 100644
--- a/worlds/zillion/docs/setup_en.md
+++ b/worlds/zillion/docs/setup_en.md
@@ -51,7 +51,7 @@ them.
### Verifying your config file
-If you would like to validate your config file to make sure it works, you may do so on the [YAML Validator page](/mysterycheck).
+If you would like to validate your config file to make sure it works, you may do so on the [YAML Validator page](/check).
## Generating a Single-Player Game