Merge branch 'ArchipelagoMW:main' into main

This commit is contained in:
CookieCat
2023-10-27 14:22:56 -04:00
committed by GitHub
21 changed files with 367 additions and 762 deletions

View File

@@ -181,7 +181,6 @@ class MultiWorld():
set_player_attr('plando_connections', [])
set_player_attr('game', "A Link to the Past")
set_player_attr('completion_condition', lambda state: True)
self.custom_data = {}
self.worlds = {}
self.per_slot_randoms = {}
self.plando_options = PlandoOptions.none
@@ -199,7 +198,6 @@ class MultiWorld():
new_id: int = self.players + len(self.groups) + 1
self.game[new_id] = game
self.custom_data[new_id] = {}
self.player_types[new_id] = NetUtils.SlotType.group
self._region_cache[new_id] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[game]
@@ -227,7 +225,6 @@ class MultiWorld():
def set_options(self, args: Namespace) -> None:
for player in self.player_ids:
self.custom_data[player] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
self.worlds[player] = world_type(self, player)
self.worlds[player].random = self.per_slot_randoms[player]

View File

@@ -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. Its 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. Its
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. Its also your worst-case
scenario as a modder. All thats 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. Lets look at some other examples
of game releases.
scenario as a modder. All thats 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. Lets look at some other
examples of game releases.
### Heavy Bullets
![Heavy Bullets Root Directory in Window's Explorer](./img/heavy-bullets-directory.png)
Heres 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 wont 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, its 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 youre looking at here, but I can instantly tell from this folders contents that
what we have is a game made in the Unity Engine. If you look in the sub-folders, youll 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. Well 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,
thats another dead giveaway.
![Heavy Bullets Root Directory in Window's Explorer](/docs/img/heavy-bullets-directory.png)
Heres 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 wont 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, its 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 youre looking at here, but I can instantly tell from this folders contents that
what we have is a game made in the Unity Engine. If you look in the sub-folders, youll 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 whats 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 whats 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 games 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 youll 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 games C# code, but the tool isnt 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).
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.
You'll want to open the data.win file, as this is where all the goods are kept.
Like dnSpy, you wont 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.
### Other helpful tools
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.
Depending on the games underlying engine, there may be some tools you can use either in lieu of or in addition to
existing game tools.
The tool itself is highly complex and even I have not yet charted its expanses.
#### [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.
### 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.
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:
- 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
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
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 isnt that old, its 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 isnt that old, its 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
Thats 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 consoles secrets.
Look for debugging tools, but be ready to learn assembly.
Old consoles usually have their own unique dialects of ASM youll need to get used to.
### My Game is a classic for the SNES/Sega Genesis/etc
Thats 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 consoles secrets.
Look for debugging tools, but be ready to learn assembly.
Old consoles usually have their own unique dialects of ASM youll need to get used to.
Also make sure theres a good way to interface with a running emulator, since thats 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. Its 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. Its 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 youd 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 cant 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 elses original work.
Users who have a copy of the game just need to apply the patch, and those who dont are unable to play.
Users who have a copy of the game just need to apply the patch, and those who dont 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 mods 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 players yaml file, and the code to initialize all this data.
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).
Heres 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)
### Requirements
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.
A world implementation requires a few key things from its implementation
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.
- 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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -1,6 +1,8 @@
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
@@ -31,3 +33,17 @@ class TestImplemented(unittest.TestCase):
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
with self.subTest(game_name):
multiworld = setup_solo_multiworld(world_type)
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.")

View File

@@ -89,9 +89,6 @@ components: List[Component] = [
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'),

View File

@@ -13,7 +13,6 @@ import typing
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:

View File

@@ -16,12 +16,22 @@ else:
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 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
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 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
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 @@ class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
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()))

View File

@@ -13,8 +13,8 @@ from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser,
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
@@ -133,6 +133,8 @@ 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:

View File

@@ -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

View File

@@ -1594,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")
@@ -2269,23 +2269,28 @@ def create_regions(self):
event_locations = self.multiworld.get_filled_locations(player)
def adds_reachable_entrances(entrances_copy, item):
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_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()
@@ -2302,7 +2307,7 @@ def create_regions(self):
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
@@ -2332,6 +2337,8 @@ def create_regions(self):
if multiworld.door_shuffle[player] == "full" or len(entrances) != len(reachable_entrances):
entrances.sort(key=lambda e: e.name not in entrance_only)
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":
@@ -2342,10 +2349,10 @@ def create_regions(self):
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:

View File

@@ -100,15 +100,15 @@ class StardewValleyWorld(World):
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()

View File

@@ -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[

View File

@@ -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 @@ class StardewRule:
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,28 +80,36 @@ class False_(StardewRule): # noqa
return 999999999
false_ = False_()
true_ = True_()
assert false_ is False_()
assert true_ is True_()
class Or(StardewRule):
rules: FrozenSet[StardewRule]
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)
@@ -112,11 +120,11 @@ class Or(StardewRule):
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,17 +139,17 @@ class Or(StardewRule):
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 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)
@@ -150,25 +158,26 @@ class And(StardewRule):
rules: FrozenSet[StardewRule]
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)
@@ -179,11 +188,11 @@ class And(StardewRule):
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,17 +207,17 @@ class And(StardewRule):
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 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)
@@ -218,11 +227,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 +270,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:

View File

@@ -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