Compare commits

...

159 Commits

Author SHA1 Message Date
Fabian Dill
62b3fd4d37 WebHost: invert multitracker control back to webhost 2023-08-28 17:18:13 +02:00
Fabian Dill
e2f7153312 Factorio: convert multitracker to Template once, instead of per render 2023-08-28 13:52:56 +02:00
Fabian Dill
96d4143030 WebHost: move new API hooks to WebWorld 2023-08-28 13:49:14 +02:00
Fabian Dill
a1dcaf52e3 WebHost: offer API to modify WebHost 2023-08-28 01:37:50 +02:00
Fabian Dill
aab8f31345 Factorio: fix website multitracker 2023-08-28 01:08:19 +02:00
Fabian Dill
9ad0032eb4 Windows: fix ArchipelagoServer.exe not being installed. 2023-08-25 22:29:56 +02:00
Seldom
09fd65209c Terraria: Fix Soul of Sight requiring Post-The Twins flag instead of access to The Twins (#2115) 2023-08-25 22:27:28 +02:00
Aaron Wagener
d8d9a49564 The Messenger: Fix a typo preventing a location from being created (#2110)
* The Messenger: Fix a typo preventing a location from being created

* Add a unit test that locations are created
2023-08-25 22:25:56 +02:00
zig-for
41a34b140c LttP: Fixes patching on a fresh AP install (#2118) 2023-08-25 22:25:02 +02:00
kindasneaki
b235ba2c52 RoR2: Move filler item creation to get_filler_item_name (#2075) 2023-08-16 09:21:07 -05:00
agilbert1412
7ce9f20bc7 Stardew Valley: Added rules requiring museum access to make donations (#2107) 2023-08-16 09:18:50 -05:00
Trevor L
6c7a7d2be5 Blasphemous: Set rules for event items later + misc logic fixes (#2084)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-08-16 09:09:49 -05:00
Alchav
8af4fda7b6 Pokemon R/B: locations accessibility fixes, etc (#2104) 2023-08-16 09:04:44 -05:00
blastron
e30f364bbd Witness: Fix for Witness plando crashes. (#2092) 2023-08-16 09:03:41 -05:00
Bryce Wilson
be07634b15 Docs: Update generate_output docstring (#2098) 2023-08-16 09:02:43 -05:00
Mewlif
5cd837256f Undertale: Key placement fix (#2030)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-08-16 09:02:01 -05:00
Doug Hoskisson
26b4ff1df2 Zillion: Python 3.11 compatibility fix (#2105) 2023-08-16 09:00:10 -05:00
Seldom
61ff94259a Terraria: Update Terraria Docs mod recommendations (#2095) 2023-08-16 08:57:27 -05:00
lordlou
4cb4c254dc SM: fix location counters preventing some goal completion (#2108) 2023-08-15 10:11:46 +02:00
Zach Parks
3a4b157363 Adjustments to player-settings.css for better UI on small view-widths. (#2019)
Also removes "portrait" media query as it forces this display method for large monitors.
2023-08-12 23:06:28 -04:00
Alchav
7a494d637b Pokémon RB: Route 3 Guard fix (#2077)
* Pokémon RB: Route 3 Guard fix

* Change dexsanity option names

* Diglett's Cave warp fix
2023-08-11 11:04:21 +02:00
David St-Louis
ca06a4b836 DOOM 1993: Fixed rule for red region in E3M9 (#2079) 2023-08-11 11:03:23 +02:00
Justus Lind
3643b1de2c Muse Dash: Make item_name_to_id and location_name_to_id ordering deterministic (#2086)
* Fix up non-deterministic order of item_name_to_id and location_name_to_id.

* Remove debug line.

* Change to use a Chainmap instead and simplify logic a bit.

* Add the forgotten music sheet item.
2023-08-11 11:02:35 +02:00
digiholic
d0c6eaf239 MMBN3: Fixes hint spam when receiving a hint (#2087)
* Only sends hints whenever the list changes

* Further reduces hint spam by not re-sending the entire list when one new thing is added
2023-08-11 11:01:24 +02:00
N00byKing
64d1722acd sm64ex: Fix possible inaccessible region 2023-08-11 10:59:24 +02:00
agilbert1412
01e8e9576c Stardew Valley: Fixed Help wanted rules, added missing coffee bean to cropsanity (#2089)
* - Added missing coffee bean to cropsanity

* - Fix an issue with the seed having the same name as the crop

* - Fix a recently discovered bug with help wanted rules when using a number not divisible by 7
2023-08-11 10:58:27 +02:00
Seldom
d5514c4635 Terraria: Fix Necromantic Scroll requiring Post-Mourning Wood instead of access to Mourning Wood #2094 2023-08-11 10:57:30 +02:00
zig-for
d5474128e3 LADX: Fix hints generation for longer location names #2099 2023-08-11 10:56:36 +02:00
t3hf1gm3nt
8d6b2dfc9c [TLOZ] Fix filepath error for base patch
using os.path.join was causing duplicate parts of the filepath in certain environments. turns out it's not needed when loading the basepatch in our current world structure. this should hopefully fix genning issues on the RC beta site (and presumably the main site once the RC turns into the release)
2023-08-11 10:54:42 +02:00
t3hf1gm3nt
c9404d75b0 [TLOZ] MD5 validation update (#2080)
* - Use proper MD5 validation

The method TLoZ was trying to validate it's baserom was different from basically every other ROM game. Almost all the other ROM games use the same method as each other (except for the external patchers like FF1 and SoE, and OoT has its own special handling that's vastly different), so updating TloZ to match.

Also got rid of the checksum attribute for the TLoZDeltaPatch as it didn't seem to be used anywhere, so felt it was unnecessary and partially confusing to have it right next to the hash attribute that is actually used.

* change error message to reference MD5
2023-08-07 09:31:43 +02:00
black-sliver
eb50e0781e MultiServer: exit console task when console thread dies (#2068) 2023-08-04 10:01:51 +02:00
Alchav
6864f28f3e Pokémon Red and Blue: Progressive Card Key and auto hint bug fixes (#2076)
* Fix Progressive Card Key bug

* Fix auto hint spam
2023-08-02 19:51:53 +02:00
Aaron Wagener
6befc91773 The Messenger: actually implement get_filler_item_name (#2070) 2023-08-01 19:43:10 +02:00
PoryGone
1d6a2bff4f SA2B: Fix generate_filler_item_name (#2074) 2023-08-01 08:15:28 +02:00
PoryGone
898558b121 SMW & DKC3: Add get_filler_item_name override (#2071) 2023-08-01 07:54:05 +02:00
Silvris
a9fb7e2ace Plando: fix automatic locations only working for the first world (#2063)
* copy location_names for each iteration

* remove copy, just set the list
2023-07-31 23:16:42 +02:00
StripesOO7
f29d5c8cae ALTTP: Add fill_slot_data for external trackers (#1919)
* __init__.py: Add fill_slot_data function

Add fill_slot_data function. 
Used by StripesOO7's pop-tracker pack to auto populate settings as convenience for the user

* LTTP__init__.py added race condition to fill_slot_data

* added missing self to multiworl.is_race

* changed filling of slot_data to fill from static list instead of pulling all alttp_options.
additional options needed to be done separately cause they are not stored the same way as the rest. "mode", "goal", etc. are simple values as the rest are key:value pairs so `.value` is not supported and I didn't want to introduce an if-statement.

* changed filling of slot_data to fill from static list instead of pulling all alttp_options.
additional options needed to be done separately cause they are not stored the same way as the rest. "mode", "goal", etc. are simple values as the rest are key:value pairs so `.value` is not supported and I didn't want to introduce an if-statement.

* added a comment to describe the use for the option added to slot_data

---------

Co-authored-by: StripesOO7 <54711792+StripeesOO7@users.noreply.github.com>
2023-07-31 01:37:12 +02:00
Aaron Wagener
cacfd4ffae Core: Add dict functionality to OptionDict (#2036)
* Options: Add support for `items()` and `__getitem__` to OptionDict

* Options: have OptionDict inherit from Mapping

* add typing to __getitem__

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-07-31 01:01:21 +02:00
t3hf1gm3nt
62315e304a TLoZ: Setup doc update (#2045)
- add section about configuring lua core (shamelessly taken from the OoT setup guide) on bizhawk version 2.8 and below
- fix wrong reference to the ff1 connector lua to correctly reference the tloz connector lua
- remove reference to recommended bizhawk version. it's unnecessary
2023-07-30 20:18:15 +02:00
Aaron Wagener
cc39eec646 Stardew Valley: Import base multiworld setup in tests and use it (#2006) 2023-07-30 20:17:12 +02:00
Daivuk
de1ec4a18f DOOM 1993: Include only regions/rules for selected episodes 2023-07-30 20:14:02 +02:00
axe-y
40c9287eba DLCQuest: Add missing gun rule. (#2058) 2023-07-30 20:12:42 +02:00
el-u
5869f78ea7 core: remove the correct item from the item_pool in fill_restrictive 2023-07-30 20:11:29 +02:00
el-u
6c908de13f core: add a test to verify that fill_restrictive removes the exact same item from the item_pool that it has used to fill 2023-07-30 20:11:29 +02:00
black-sliver
29d67ac456 Speedups: ignore warning C4551 for pyximport+MSVC (#2054)
The cython-generated code triggers C4551 on MSVC,
which does not get silenced by pyximport's build flags.
So we silence it on source code level instead.
2023-07-30 10:33:00 +02:00
black-sliver
6d93a6234e MultiServer: fix wrong missing for empty state w/o speedups and add/fix tests (#2052)
* MultiServer: fix wrong missing for empty state w/o speedups

* Tests: fix some tests not being run

* Tests: add test for set intersection with LocationStore
2023-07-29 19:44:10 +02:00
zig-for
b579dbfdf8 LADX: Add rooster option (#2021) 2023-07-29 19:17:50 +02:00
zig-for
6ad33bb16e LADX: Fix hints crash (#2050) 2023-07-29 19:16:39 +02:00
black-sliver
7b8f8918fc Settings: change/fix tests behavior (#2053)
* Settings: disable saving and gui during tests

* Tests: create a fresh host.yaml for TestHostYAML

Now that host.yaml is .gitignored, testing the local host.yaml makes no sense anymore
2023-07-29 18:50:21 +02:00
Justus Lind
a90825eac3 Muse Dash: Add songs from Cosmic Radio Update (#2047) 2023-07-29 18:35:32 +02:00
agilbert1412
280ebf9c34 Stardew valley: Supported Mods documentation and changes required for legal reasons (#2038)
## What is this fixing or adding?
It was pointed out that distributing an archive with copies of all the supported mods could lead to legal problems down the line. So we are moving away from this approach.
This also means that, in the event that a mod gets updated and the previous version is no longer available, we need the ability to update the mod's supported version at any point in time, and cannot rely on AP's release schedule for such updates that will, in most cases, be only changing the string for the required version.

Changes:
- Scrub all references to the support mods zip file from documentation
- Create dedicated "Supported Mods" documentation page, external to AP so we can keep it updated with mod versions regardless of their release schedule
- Remove mod version validation from the AP backend, and manage that in the mod itself, for the same reason.
2023-07-29 04:08:22 +02:00
Zach Parks
672a97c9ae Core: Set locality rules after set_rules stage. (#2044)
* Core: Set locality rules after `generate_basic`.

* Move locality rules to before `generate_basic`.
2023-07-28 21:06:43 -05:00
Fabian Dill
b684ba4822 CommonClient: fix 0.4.2 EnergyLink datastore key 2023-07-29 02:28:13 +02:00
el-u
0d28eeb3c5 alttp: remove excess Blue Mail from hard item pool 2023-07-28 14:08:41 +02:00
NewSoupVi
cf37a69e53 Witness: Fix 2 generation crashes (#2043)
* Fix for error in get_early_items when removing plandoed items.

* Fix Early Caves

* Remove unnecessary list() call

* Update worlds/witness/items.py

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

---------

Co-authored-by: blastron <blastron@mac.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-07-28 09:39:56 +02:00
Silvris
a99a407c41 MultiServer: fix unclosed parenthesis in connection message (#2035) 2023-07-28 03:31:48 +02:00
NewSoupVi
8f447487fb Witness: Add Exempt-Medic to the in-game credits hint 2023-07-28 03:30:45 +02:00
Alchav
eb8855afb9 Pokemon RB: apworld fixes (#2042) 2023-07-27 21:43:37 +02:00
Zach Parks
09c3a99be8 Docs: Create CODEOWNERS document for tracking world maintainers. (#1901)
* Meta: Create code owners document for tracking and notifying owners of world changes.

* Removing @dewiniaid as maintainer for Hollow Knight.

2023-07-11 - Finalization Date for Vote

https://discord.com/channels/731205301247803413/1123286507390767267/1128482720218099812

@ThePhar - Vote to Remove (2023-06-27)
@black-sliver - Vote to Remove (2023-06-27)
@KonoTyran - Vote to Remove (2023-06-27)
@Berserker66 - Vote to Remove (2023-07-09)

Passed with majority to remove maintainer status.

* Adding @BadMagic100 and @ThePhar as maintainers for Hollow Knight.

@BadMagic100 to primarily handle client-side maintenance/updates.
@ThePhar to primarily handle Archipelago-side maintenance/updates.

https://discord.com/channels/731205301247803413/1131762415021858907

@ThePhar - Approved @BadMagic100 (2023-07-20) and @ThePhar (2023-07-24) as Maintainers
@LegendaryLinux - Approved @BadMagic100 (2023-07-20) as Maintainer
@Berserker66 - Approved @BadMagic100 (2023-07-26) and @ThePhar (2023-07-26) as Maintainers
@black-sliver - Approved @BadMagic100 (2023-07-26) and @ThePhar (2023-07-26) as Maintainers
@KonoTyran - Approved @BadMagic100 (2023-07-27) and @ThePhar (2023-07-27) as Maintainers

Passed with a majority to set maintainer status for Hollow Knight.
2023-07-27 09:12:06 -05:00
zig-for
3bf86cd8f0 LADX: Fix getting old items over and over again in Bizhawk (#2011)
There was a bug that randomly after opening and closing the menu, some players on Bizhawk would get old items again. Tracking this down took multiple hours over the course of several weeks. The root cause turned out to be reading from the System Bus domain while an DMA copy was happening. Doing so is undefined behavior on GBC (though I'm sure some game relies on it). On Gambatte, you end up reading some garbage byte no matter what the read is (unsure what the providence of the byte is - some garbage, some register, the actual DMA data, who knows?). Normally, this isn't an issue, as Bizhawk callbacks only happen during vblank/halt, which is generally a state where we have valid WRAM to read from. However - a setting is being passed around the community for Bizhawk that changes the frame counter to go from "only when Vblank happens" to "whenever some number of audio samples have happened" which causes the bizhawk callbacks to happen....nearly whenever. Including during a DMA. You can tell this is happening if you print the `PC` register when reading memory - if it matches `FFXX` then you are executing in a routine in HRAM and likely doing a DMA.

Additionally, the check items counter specifically is in WRAM Bank 1 which could be swapped out of - will have to keep an eye on this - generally LADX lives in Bank 1, but there are a few things that use the other banks (swap space for some objects??). This could be a problem on any platform - if we get more reports of bad items gets, that's probably why.

Also, fixes some logging that was never getting reenabled.
2023-07-27 16:08:14 +02:00
NewSoupVi
2333ddeaf7 The Witness: Make the path behind Keep Pressure Plates 2 logical in Vanilla and Normal (#2013) 2023-07-25 17:58:28 +02:00
NewSoupVi
0e8ad7b9bc Witness: Fixing a world bleed issue with multiple Witness seeds, preventing generation (#2031)
Co-authored-by: blastron <blastron@mac.com>
2023-07-24 22:54:23 -05:00
BadMagic100
9d1a31004f HK: Fix typo in LEFTSLASH (#2027) 2023-07-25 05:32:57 +02:00
Aaron Wagener
f2d0d1e895 The Messenger: Improve the shopping experience (#2029)
* The Messenger: Don't generate Figurines

* The Messenger: add prerequisite shop cost requirements

* The Messenger: don't double the cost anymore

* The Messenger: remove centered mind prereq instead of checking for it

* The Messenger: use cost as a property to cache it and gain back speed

* The Messenger: hardcode the prereqs for more speed

* make the linter and mypy happier

* use cached_property
2023-07-25 02:41:20 +02:00
Fabian Dill
6a96f33ad2 Core: trace error to player, if possible. (#2023) 2023-07-25 02:18:39 +02:00
agilbert1412
bb069443a4 Stardew valley: backpack fix, enum fix (#2028)
* - Reorganised tests for better backpack coverage
- Added a test for backpack locations being absent on vanilla

* - Fix backpack locations on vanilla

* - Fixed a typo in documentation

* - Added missing parenthesis after enum.auto so that Python 3.11 still works

* - Added Blank lines at the end of the backpack test files

* - cleaned whitespace
2023-07-25 01:52:15 +02:00
Zach Parks
fa3d69cf48 CI: Update workflows to use Python 3.11 (#1949)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-07-23 20:15:13 -05:00
black-sliver
6107749cbe CI: remove cython3 beta testing (#2024)
* CI: remove cython3 beta testing

Cython 3.0.0 was released: https://cython.readthedocs.io/en/latest/src/changes.html

* CI: remove duplicate run
2023-07-24 02:53:53 +02:00
Fabian Dill
60289666dc Core: update modules 2023-07-24 02:31:17 +02:00
Fabian Dill
5b8c3425c8 Setup: package the entire websockets module 2023-07-24 00:54:14 +02:00
Alchav
85b92e2696 Pokémon Red and Blue: Version 4 update (#1963)
## What is this fixing or adding?
Adds a large number of new options, including:

- Door Shuffle
- Sphere-based level scaling
- Key Item and Pokedex requirement options to reach the Elite Four
- Split Card Key option
- Dexsanity option can be set to a percentage of Pokémon that will be checks
- Stonesanity: remove the stones from the Celadon Department Store and shuffle them into the item pool, replacing 4 of the 5 Moon Stone items
- Sleep Trap items option
- Randomize Move Types option
- Town Map Fly Location option, to unlock a flight location when finding/receiving the Town Map

Many enhancements have been made, including:
- Game allows you to continue your save file _from Pallet Town_ as a way to save warp back to the beginning of the game. The one-way drop from Diglett's Cave to north Route 2 that had been added to the randomizer has been removed.
- Client auto-hints some locations when you are able to see the item before you can obtain it (but would only show AP Item if it is for another player), including Bike Shop, Oak's Aides, Celadon Prize Corner, and the unchosen Fossil location.

Various bugs have been fixed, including:
- Route 13 wild Pokémon not correctly logically requiring Cut
- Vanilla tm/hm compatibility options giving compatibility for many TMs/HMs erroneously 
- If an item that exists in multiple quantities in the item pool is chosen for one of the locations that are pre-filled with local items, it will continue placing that same item in the remaining locations as long as more of that item exist
- `start_with` option for `randomize_pokedex` still shuffling a Pokédex into the item pool
- The obedience threshold levels being incorrect with 0-2 badges, with Pokémon up to level 30 obeying with 0-1 badges and up to 10 with 2 badges
- Receiving a DeathLink trigger in the Safari Zone causing issues. Now, you will have your steps remaining set to 0 instead of blacking out when you're in the Safari Zone.

Many location names have been changed, as location names are automatically prepended using the Region name and a large number of areas have been split into new regions as part of the overhaul to add Door Shuffle.
2023-07-24 00:46:54 +02:00
Brooty Johnson
cf8ac49f76 DS3: move an items location from RC -> DH (#2017)
* moves items location from RC -> DH

"Ring of Steel Protection+3" actually belongs in DH instead of RC. this will shift the item ID's for the last 3 items in RC, and should not shift any ids in DH

* updated data_version to 7
2023-07-24 00:20:45 +02:00
Fabian Dill
d9594b049c Setup: allow user to auto launch the launcher, so they can conveniently launch things when the setup is done. (#2020) 2023-07-24 00:15:47 +02:00
black-sliver
caa8d478f5 Factorio: update min_client_version (#2018)
Ensure that people don't use an old client that is known to be incompatible.
2023-07-24 00:09:47 +02:00
Daivuk
7279de0605 DOOM 1993: Added Episode 4. Game is now complete
And some bug fixes, balance and small features.
2023-07-23 22:24:54 +02:00
black-sliver
d49860fbeb Fill: fix cleanup-after-swapping performance (#2016)
#1800 introduced a cleanup pass to "eject" unreachable items to hopefully get better error reporting of what could not be placed. The code to do so has an unnecessary amount of sweeps from pool.
2023-07-23 17:57:33 +02:00
Witchybun
591661ca79 Stardew Valley: Fix typo with woods obelisk item (#2015)
Co-authored-by: Witchybun <elnendil@gmail.com>
2023-07-22 23:23:03 -05:00
David St-Louis
e1374492de DOOM 1993: Fixed bad level exit regions (#2007) 2023-07-22 11:02:02 -05:00
el-u
5843f71447 docs: mention all item classifications (#1961)
* docs: mention all item classifications

* docs: mention all item classifications: reword skip_balancing and progression_skip_balancing
2023-07-22 09:56:00 -05:00
KonoTyran
9b1de8fea8 StS: Update location table and move item creation to create_items from generate_basic. (#1938) 2023-07-22 00:51:13 -05:00
el-u
86a55c7837 lufia2ac: code cleanup (#1971) 2023-07-22 00:49:23 -05:00
Aaron Wagener
8405b35a94 The Messenger: use the new region helpers (#1687)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-07-22 00:45:46 -05:00
Mewlif
889a4f4db9 Undertale: Doc updates and client bug fixes. (#1996) 2023-07-22 00:38:21 -05:00
Scipio Wright
191dcb505c Generate: Change yaml is destroyed to yaml is invalid (#1954) 2023-07-21 20:13:50 -05:00
Zach Parks
ecb1e0b74b Core: Add display name for item_links Option. (#1952) 2023-07-21 19:31:23 -05:00
Seldom
f8e2d7f503 Terraria: Fix Lunatic Cultist goal immediately awarded (#1995) 2023-07-21 19:24:06 -05:00
David St-Louis
8015734fcf DOOM 1993: Better region logics/rules, balancing, level exits (#1973) 2023-07-21 19:22:24 -05:00
Aaron Wagener
21228f9c63 The Messenger: Update Docs for latest release (#2005) 2023-07-21 14:43:23 -05:00
agilbert1412
57c1bc800c Stardew Valley: Turned the Treehouse into an unlockable item (#2004) 2023-07-21 12:56:03 -05:00
Fabian Dill
7f180a6d5a Factorio: fix multitracker ID misalignment 2023-07-21 19:11:05 +02:00
digiholic
9839164817 MMBN3: Fixes crash when checking certain locations (#2003) 2023-07-21 12:00:44 -05:00
Bryce Wilson
3c1950dd40 DS3: Accessibility error fix (#1983) 2023-07-21 11:59:17 -05:00
Aaron Wagener
e8bf471dcd Webhost: Fix failing gen for players > 1 (#1998) 2023-07-20 22:40:31 +02:00
Seldom
210d6f81eb Terraria: Fix Python 3.8 compat (#1994) 2023-07-20 10:49:52 +02:00
NewSoupVi
6797216eb8 Witness: Fix type hints being incompatible with 3.8 (#1991)
* Fixed 3.8 typing in init

* Fixed 3.8 typing in items

* Fixed 3.8 typing in player logic

* Fixed 3.8 typing in static_logic

* Fix 3.8 typing in utils

* Fixed fault import
2023-07-20 02:10:48 +02:00
Fabian Dill
1e72851b28 HK: apworld support on 3.10+ 2023-07-20 02:01:13 +02:00
NewSoupVi
75463193ab Witness: Yeah let's not sort the entire multiworld's itempool lol (#1993)
* Witness: Yeah let's not sort the entire multiworld's itempool lol

* Non-stupid dict sorting

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>

* Even less stupid dict sorting

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-07-20 01:20:59 +02:00
agilbert1412
257774c31b Stardew Valley: Fixed Leo's Treehouse being randomized too aggressively (#1992)
* - Fixed Leo's Treehouse being randomized too aggressively

* - Added an automated test to catch badly tagged Non-progression entrances

* - Fixed a logic issue with Void Mayonnaise not being fishable

* - Removed unused import
2023-07-20 01:20:52 +02:00
Zach Parks
ca46a64abc Clique: Refactors and Additional Features supported by v1.5 (#1989) 2023-07-19 17:16:03 -05:00
NewSoupVi
1a29caffcb Witness: (Fatal) Fix incorrect reference due to a faulty merge conflict (#1990) 2023-07-19 17:08:25 -05:00
Witchybun
8fd805235d Undertale: Change Save Data Folder Location (#1966)
Co-authored-by: Witchybun <elnendil@gmail.com>
2023-07-19 16:39:57 -05:00
agilbert1412
62657df3fb Stardew Valley: 4.x.x - The Ginger Update (#1931)
## What is this fixing or adding?
Major content update for Stardew Valley

## How was this tested?
One large-scale public Beta on the archipelago server, plus several smaller private asyncs and test runs

You can go to https://github.com/agilbert1412/StardewArchipelago/releases to grab the mod (latest 4.x.x version), the supported mods and the apworld, to test this PR

## New Features:
- Festival Checks [Easy mode or Hard Mode]
- Special Orders [Both Board and Qi]
- Willy's Boat
- Ginger Island Parrots
- TV Channels
- Trap Items [Available in various difficulty levels]
- Entrance Randomizer: Buildings and Chaos
- New Fishsanity options: Exclude Legendaries, Exclude Hard fish, Only easy fish
- Resource Pack overhaul [Resource packs are now more enjoyable and varied]
- Goal: Greatest Walnut Hunter [Find every single Golden Walnut]
- Goal: Perfection [Achieve Perfection]
- Option: Profit Margin [Multiplier over all earnings]
- Option: Friendsanity Heart Size [Reduce clutter from friendsanity hearts]
- Option: Exclude Ginger Island - will exclude many locations and items to generate a playthrough that does not go to the island
- Mod Support [Curated list of mods]

## New Contributors:
@Witchybun for the mod support

---------

Co-authored-by: Witchybun <embenham05@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-07-19 20:26:38 +02:00
blastron
1f6db12797 The Witness: Item loading refactor. (#1953)
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-07-18 22:02:57 -05:00
Aaron Wagener
18c9779815 The Messenger: Fix location access for Figurine Shop Locations (#1975) 2023-07-18 22:01:44 -05:00
Justus Lind
09f4b7ec38 Muse Dash: Update code to use some newer API (#1980) 2023-07-18 22:00:52 -05:00
Silvris
d14131c3be MMBN3, Pokemon RB: Fix Incorrect Import (#1988) 2023-07-18 21:59:38 -05:00
Scipio Wright
8360435607 Noita: Implement Extra Orbs, Shop Price Reduction, and some slight region tweaks (#1972)
Co-authored-by: Adam Heinermann <aheinerm@gmail.com>
2023-07-18 21:51:01 -05:00
Seldom
83387da6a4 Terraria: Implement New Game (#1405)
Co-authored-by: Zach Parks <zach@alliware.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-07-18 21:37:26 -05:00
NewSoupVi
f318ca8886 Witness: Add junk hints for new games (#1962) 2023-07-18 21:19:58 -05:00
FlySniper
1630529d58 Wargroove (#1982)
## What is this fixing or adding?

Adjusted some map terrain. Made Ambushed in the Middle's HQ more exposed. Made Deep Thicket's AI spawn extra units. Adjusted some terrain in Rebel Village.
Moved item creation from generate_basic to create_items for (https://github.com/ArchipelagoMW/Archipelago/pull/1460)
2023-07-19 01:59:41 +02:00
Scipio Wright
60b8daa3af Docs: Slight update regarding apworld yamls (#1987) 2023-07-18 21:12:04 +02:00
black-sliver
a77739ba18 Settings: implement saving of dict and sequence, add graceful crashing (#1981)
* settings: don't crash when loading an outdated host.yaml

* settings: use temp file to not destroy host.yaml on error

* settings: implement saving of dicts

* settings: simplify dump of dict

* settings: add support for sequences

also a few more comments

* settings: reformat a bit
2023-07-18 20:59:52 +02:00
Fabian Dill
60586aa284 Tests: ensure unreachable_regions is correctly set 2023-07-18 12:41:26 +02:00
Silvris
f1d09d2282 TLoZ: Fix Incorrect Import (#1986) 2023-07-18 10:22:39 +02:00
NewSoupVi
48746f6c62 Witness: Fix Python 3.11 crash and fix Desert Laser hint (#1970) 2023-07-18 10:18:42 +02:00
PoryGone
8c5688e5e2 Add link to location guide into Game Page (#1974) 2023-07-18 09:58:38 +02:00
NewSoupVi
bad79ee11a Witness: Fix excluded EPs not being precompleted anymore (#1979)
One of the recent PRs accidentally removed all ability for the client to see which EPs are precompleted (due to settings)

This is pretty bad, as the client now thinks these EPs need to be completed for "Obelisk Side" locations, when the generator does not. This would lead to impossible seeds.
2023-07-18 09:56:04 +02:00
NewSoupVi
afed1dc558 Witness: Logic Fix (Incorrect Symbols expected for Quarry Lower Row 1 in Expert) (#1984)
Doesn't currently lead to any broken seeds or anything, it just makes seeds unnecessarily restrictive.
2023-07-18 09:34:31 +02:00
Bryce Wilson
8df08b53d9 WebHost: Fix as_dict attribute error (#1977)
* WebHost: Fix as_dict attribute error

Introduced in 827444f5a4

* WebHost: Add assertion that baked_server_options is a dict

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-07-15 22:52:52 +02:00
Silvris
dfe08298ef Installer: Add desktop and start menu shortcuts for Launcher and LADX Client (#1947) 2023-07-14 03:48:38 +02:00
zig-for
48ffad867a LADX: Add Hints (#1932) 2023-07-14 03:14:04 +02:00
PoryGone
a88e75f3a1 DKC3: Move item creation earlier (#1941) 2023-07-14 03:11:49 +02:00
PoryGone
087cc334f4 SMW: Move item creation earlier (#1942) 2023-07-14 03:11:19 +02:00
Ludwig
11278d0e61 SM64: Verbosify SM64 documentation (#1967) 2023-07-14 03:08:09 +02:00
black-sliver
bff2b80acf MultiServer: fix loading of default hint_cost (#1964) 2023-07-11 20:38:09 +02:00
Exempt-Medic
5b606e53fc HK: Fix bugs and update setup guide for Scarab+ and XBox Game Pass support (#1955)
Fixing three bugs:
1. Made Salubra Charm Shop Slots use the actual options value and not the Iselda Shop Slots value.
2. Made Mask Shards no longer considered Filler but instead Progression so they can actually be used for access requirements that require damage boosts.
3. Fixed goal requirements to account for Focus being required for The Hollow Knight and Sealed Siblings endings and adjusted the Radiance and Any goal requirements accordingly.

Updated Setup Guide:
Changed to mention Scarab+ instead of Scarab (it is better maintained and has several quality of life improvements and fixes a bug for XBox Game Pass).
Added info about how to use Scarab+ with XBox Game Pass.
2023-07-11 11:49:40 +02:00
Zach Parks
9af56ec0dd WebHost: Update copyright year in footer. (#1951) 2023-07-09 13:23:50 -05:00
el-u
ab22b11bac Docs: clean up world api.md a bit (#1958) 2023-07-09 18:04:24 +02:00
Aaron Wagener
07d74ac186 Core: Region connection helpers (#1923)
* Region.create_exit and Region.connect helpers

* reduce code duplication and better naming in Region.connect

* thank you tests

* reorder class definition

* define entrance_type on Region

* document helpers

* drop __class_getitem__ for now

* review changes
2023-07-09 17:52:20 +02:00
zig-for
36474c3ccc LADX: Client Fixes (#1934) 2023-07-09 15:17:24 +02:00
espeon65536
736945658a OoT: Python 3.11 Compatibility fix and Minor Bug fixes (#1948)
* OoT: biggoron's sword and giant's knife now considered progression in non-glitchless

* OoT: fixed seeding the random module with the Random object
2023-07-09 14:30:05 +02:00
NewSoupVi
cfe14aec76 The Witness: Add Quarry Stoneworks Control Room Left to the hint pool (#1957) 2023-07-09 14:21:05 +02:00
Remy Jette
feaa30d808 Docs, StS: Document setup for Slay the Spire GOG/Game Pass installations (#1913)
Co-authored-by: KonoTyran <Kono.Tyran@gmail.com>
2023-07-07 09:47:11 -05:00
Trevor L
1338d7a968 Blasphemous: Randomizer 2.0 (#1883)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-07-05 23:39:26 -05:00
kindasneaki
f2117be7d9 RoR2: fix event exits for dlc stages (#1946) 2023-07-05 22:45:43 -05:00
lordlou
5f2c226b43 SMZ3: update to upstream version 11.3.1 and item link fix (#1950) 2023-07-05 22:44:59 -05:00
ScorelessPine
81b956408e SMZ3: Fixed Ganon sign text on AllDungeonsDefeatMotherBrain goal (#1617) 2023-07-05 20:49:36 -05:00
Remy Jette
354a182859 WebHost: Fix docs generation from a .apworld (#1862)
zfile.filename is the full path within the archive, so by default
zf.extract will maintain that directory structure when extracting.

This causes the docs to be placed in the wrong place, as the Javascript
code expects them to be placed directly in the game folder.
2023-07-05 23:36:46 +02:00
black-sliver
827444f5a4 Core: Add settings API ("auto settings") for host.yaml (#1871)
* Add settings API ("auto settings") for host.yaml

* settings: no BOM when saving

* settings: fix saving / groups resetting themselves

* settings: fix AutoWorldRegister import

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>

* Lufia2: settings: clean up imports

* settings: more consistent class naming

* Docs: update world api for settings api refactor

* settings: fix access from World instance

* settings: update migration timeline

* Docs: Apply suggestions from code review

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

* Settings: correctly resolve .exe in UserPath and LocalPath

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: Zach Parks <zach@alliware.com>
2023-07-05 22:39:35 +02:00
Fabian Dill
d8a8997684 Core: remove "names" from multidata (#1928) 2023-07-05 21:51:38 +02:00
Samuel Thayer
e920692ec3 DS3, Docs: Add downpatching instructions to Dark Souls III setup guide (#1874)
* add links to downpatching instructions

* renumber properly

* Update setup_en.md

* Update setup_en.md

* DS3, Docs: Avoid having to update the guide for steam updates

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-07-05 20:21:32 +02:00
black-sliver
6fd16ecced MultiServer: Allow games with no locations, add checks to pure python implementation. (#1944)
* Server: allow games with no locations again

* Server: validate locations in pure python implementation

and rework tests

* Server: fix tests for py<3.11
2023-07-05 10:35:03 +02:00
black-sliver
50537a9161 Setup: don't build speedups in-place, copy isntead (#1945) 2023-07-05 02:53:34 +02:00
Sunny Bat
cbb7616f03 Raft: Only modify itempool during create_items (#1939) 2023-07-04 14:28:09 -05:00
Ziktofel
85a2193f35 SC2: Move itempool generation logic from generate_basic to create_items. (#1940) 2023-07-04 14:27:04 -05:00
Mewlif
857364fa78 Undertale: Bug fix pr (#1937) 2023-07-04 13:09:17 -05:00
zig-for
153125a5ea LADX: Fix being forced to farm for money, fix set iteration (#1924) 2023-07-04 19:33:33 +02:00
black-sliver
b6e78bd1a3 MultiServer: speed up location commands (#1926)
* MultiServer: speed up location commands

Adds optimized pure python wrapper around locations dict
Adds optimized cython implementation of the wrapper, saving cpu time and 80% memory use

* Speedups: auto-build on import and build during setup

* Speedups: add requirements

* CI: don't break with build_ext

* Speedups: use C++ compiler for pyximport

* Speedups: cleanup and more validation

* Speedups: add tests for LocationStore

* Setup: delete temp in-place build modules

* Speedups: more tests and safer indices

The change has no security implications, but ensures that entries[IndexEntry.start] is always valid.

* Speedups: add cython3 compatibility

* Speedups: remove unused import

* Speedups: reformat

* Speedup: fix empty set in test

* Speedups: use regular dict in Locations.get_for_player

* CI: run unittests with beta cython

now with 2x nicer names
2023-07-04 19:12:43 +02:00
Bryce Wilson
d35d3b629e DS3: Dark Souls 3 Major Update/Refactor (#1864)
* fix estus shard/bone shard numbers

there are only 11 estus shards in the pool, so fixed number of estus shards so everything can be collected, and upped the number of bone shards in the pool to fix datapackage numbers. new counts went from: 15(estus) + 5(bones) = 20(total) TO 11(estus) + 9(bones) = 20(total)

* Update locations_data.py

changed estus shard/bone shard counts to match the counts in items_data.py. same reasoning as the commit for that, only 11 estus in game

* added new options "Late DLC"
revampled "Late Basin of Vows" option
added the Fire Demon location in Undead Sanctuary

* first file dump

added new settings for customizing pool options
sorted all the item pools
code clean up
fixed estus/bone shard counts
still need to figure out location excluding

* bunch of changes

added new locations
put locations into specific lists
made DarkSouls3Locations for each list of items
still need to figure out how to exclude

* excluded locations from generating without options, created gotthard_region, update how the pool fills additional items, update location/item tables, create more tables

* code cleanup, remove extra tables, add grave key/eyes of a firekeeper back to key pool

* fixed some logging

* add more detailed options descriptions

* forgot to update progressive locations updates too whoops

* remove irina's tower key from items/location list. the current ID's dont work to shuffle

* fixed item-to-locations, added new weapons, added new armors, added new rings, added "eyes of a fire keeper" to key locations list to balance, adjusted tables

* added HWL: broken straight sword location, moved Greirat's ashes to  NPC items

* remove hwl: broken short sword location/item from pool (does not exist), fix item/location counts in options, general code clean up

* more code cleanup, fix Havels Ring +3 location/properly renamed item, changed Estus/Bone Shard names to not include a +| added a missing undead bone shard

* fixed npc rule, added a bunch of ring locations, fixed ring tables

* updated options

* cleaned up more code, edited some option names

* start of new items system

* DS3: Major refactor (allows for defining more items than those in vanilla locations)

* DS3: Repair changes overwritten by refactor

* DS3: Re-implement new options for location categories

* DS3: Make replacement item lists for most unique item types

* DS3: Remove accidentally added apworld

* DS3: Make option names more consistent

* DS3: Fix Pyromancer's Parting Flame location category

* DS3: Add new items

* DS3: Fix access rule for DLC/Contraption Key

* DS3: Only replace unrandomized progression items with events

Also fix some location names/categories

* DS3: Change some location names to be in line with their items

* DS3: Add randomized infusion code (only works for Broadsword)

* DS3: Make varied item pool an option

* added remaining weapons, shields, armors, rings, spells, dlc equivalents | added remaining dlc ring locations (2 in dreg heap, 5 in ringed city)

* adjusted 'Progressive Locations' counts and added new table

* added more souls + upgrade gems

* added the rest of consumables

* reverted adding an additional 'progressive location 4' table and added bulk progression locations to prog. location 3 table

* DS3: Add infusion categories and some cleanup of items

* DS3: Fix item ordering

* DS3: Fix infusion/upgrade code extra if

* DS3: Disable some unmarked cut content items

* DS3: Rename blessed red and white shield+1

* DS3: Implement guaranteed_items option

* DS3: Remove print statement

* DS3: Add extra check for trying to remove items from an empty list

* add unused content item id's

* DS3: Move cut content to its own list

* DS3: Classify spells and healing upgrades as useful

* DS3: Implement get_filler_item_name

* DS3: Change lower bounds for upgrades from +1 to +0

* DS3: Move Ancient Dragon Greatshield back to vanilla and recategorize some useful consumables

* DS3: Guaranteed items checks for number of existing items before replacing

* added remaining progressive items, fixed npc rules, adjusted option location counts

* delete extra items, add rule for dancer/late basin

* seperate PW into two parts (can access first half w/o contraption key | SKIP more unused items

* DS3: Minor linting changes

* DS3: Update required_client_version

* DS3: Remove rule for bell tower access

The key can always be purchased from the shop

* DS3: Move location category option checks to generate_early

* added "Boss Soul" option to pool

* DS3: Fix rules for boss souls and update misc location count

* DS3: Address minor review comments

* DS3: Change category enums to IntEnum

* DS3: Make apworld

---------

Co-authored-by: Brooty Johnson <83629348+Br00ty@users.noreply.github.com>
2023-07-04 08:46:18 +02:00
Fabian Dill
532c4c068f Subnautica: revamp filler item pool 2023-07-04 08:29:46 +02:00
lordlou
b077b2aeef SM: save and quit escape restriction and bad EscapeTrigger code fix (#1929)
* prevent using save and reload when escaping Zebes

fixed wrong code when EscapeTrigger is required (X.item.advancement isnt defined for ItemLocation)
2023-07-03 06:40:32 -05:00
David St-Louis
e9e18054cf Docs: Added DOOM 1993 to the list of games in the root README.md (#1930) 2023-07-02 16:15:27 -05:00
Bryce Wilson
d94bee20d0 Core: Import random for type hint on World.random (#1927) 2023-07-02 18:23:47 +02:00
David St-Louis
c321c5d256 DOOM 1993: implement new game (#1759)
* DOOM 1993: implement new game

* DOOM 1993 - Phar's cleanup to __init__.py
2023-07-02 10:34:55 -05:00
Fabian Dill
ee40312384 LttP: free core of checks_in_area (#1798) 2023-07-02 13:00:05 +02:00
Aaron Wagener
a6ba185c55 Core: Attribute per slot random directly to the World and discourage using MultiWorld's random (#1649)
* add a random object to the World

* use it in The Messenger

* the worlds don't exist until the end of set options

* set seed in lttp tests

* use world.random for shop shuffle
2023-07-02 05:50:14 -05:00
Fabian Dill
6a88d5aa79 Core: update modules 2023-07-02 09:51:01 +02:00
Justus Lind
4a60d8a4c1 Muse Dash: Fix Rare Test Failure (#1920) 2023-07-02 02:48:17 -05:00
Aaron Wagener
9b15278de8 Core: Add support for non dictionary iterables for Region.add_exits (#1698)
* Core: Add support for non dictionary iterables for `Region.add_exits`

* some cleanup and duplicate code removal

* add unit test for non dict iterable

* use more consistent naming

* sometimes i just make stuff harder on myself :)
2023-06-30 20:37:44 -05:00
329 changed files with 43211 additions and 14051 deletions

View File

@@ -38,12 +38,13 @@ jobs:
run: |
python -m pip install --upgrade pip
python setup.py build_exe --yes
$NAME="$(ls build)".Split('.',2)[1]
$NAME="$(ls build | Select-String -Pattern 'exe')".Split('.',2)[1]
$ZIP_NAME="Archipelago_$NAME.7z"
echo "$NAME -> $ZIP_NAME"
echo "ZIP_NAME=$ZIP_NAME" >> $Env:GITHUB_ENV
New-Item -Path dist -ItemType Directory -Force
cd build
Rename-Item exe.$NAME Archipelago
Rename-Item "exe.$NAME" Archipelago
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
- name: Store 7z
uses: actions/upload-artifact@v3
@@ -65,10 +66,10 @@ jobs:
- name: Get a recent python
uses: actions/setup-python@v4
with:
python-version: '3.9'
python-version: '3.11'
- name: Install build-time dependencies
run: |
echo "PYTHON=python3.9" >> $GITHUB_ENV
echo "PYTHON=python3.11" >> $GITHUB_ENV
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract

View File

@@ -44,10 +44,10 @@ jobs:
- name: Get a recent python
uses: actions/setup-python@v4
with:
python-version: '3.9'
python-version: '3.11'
- name: Install build-time dependencies
run: |
echo "PYTHON=python3.9" >> $GITHUB_ENV
echo "PYTHON=python3.11" >> $GITHUB_ENV
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract

View File

@@ -36,12 +36,13 @@ jobs:
- {version: '3.8'}
- {version: '3.9'}
- {version: '3.10'}
- {version: '3.11'}
include:
- python: {version: '3.8'} # win7 compat
os: windows-latest
- python: {version: '3.10'} # current
- python: {version: '3.11'} # current
os: windows-latest
- python: {version: '3.10'} # current
- python: {version: '3.11'} # current
os: macos-latest
steps:
@@ -55,6 +56,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-subtests
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
python Launcher.py --update_settings # make sure host.yaml exists for tests
- name: Unittests
run: |
pytest

5
.gitignore vendored
View File

@@ -37,6 +37,7 @@ README.html
EnemizerCLI/
/Players/
/SNI/
/host.yaml
/options.yaml
/config.yaml
/logs/
@@ -168,6 +169,10 @@ dmypy.json
# Cython debug symbols
cython_debug/
# Cython intermediates
_speedups.cpp
_speedups.html
# minecraft server stuff
jdk*/
minecraft*/

View File

@@ -9,7 +9,8 @@ import typing # this can go away when Python 3.8 support is dropped
from argparse import Namespace
from collections import ChainMap, Counter, deque
from enum import IntEnum, IntFlag
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \
Type, ClassVar
import NetUtils
import Options
@@ -81,6 +82,7 @@ class MultiWorld():
random: random.Random
per_slot_randoms: Dict[int, random.Random]
"""Deprecated. Please use `self.random` instead."""
class AttributeProxy():
def __init__(self, rule):
@@ -242,6 +244,7 @@ class MultiWorld():
setattr(self, option_key, getattr(args, option_key, {}))
self.worlds[player] = world_type(self, player)
self.worlds[player].random = self.per_slot_randoms[player]
def set_item_links(self):
item_links = {}
@@ -484,8 +487,10 @@ class MultiWorld():
def get_unfilled_locations_for_players(self, location_names: List[str], players: Iterable[int]):
for player in players:
if not location_names:
location_names = [location.name for location in self.get_unfilled_locations(player)]
for location_name in location_names:
valid_locations = [location.name for location in self.get_unfilled_locations(player)]
else:
valid_locations = location_names
for location_name in valid_locations:
location = self._location_cache.get((location_name, player), None)
if location is not None and location.item is None:
yield location
@@ -786,78 +791,6 @@ class CollectionState():
self.stale[item.player] = True
class Region:
name: str
_hint_text: str
player: int
multiworld: Optional[MultiWorld]
entrances: List[Entrance]
exits: List[Entrance]
locations: List[Location]
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
self.name = name
self.entrances = []
self.exits = []
self.locations = []
self.multiworld = multiworld
self._hint_text = hint
self.player = player
def can_reach(self, state: CollectionState) -> bool:
if state.stale[self.player]:
state.update_reachable_regions(self.player)
return self in state.reachable_regions[self.player]
def can_reach_private(self, state: CollectionState) -> bool:
for entrance in self.entrances:
if entrance.can_reach(state):
if not self in state.path:
state.path[self] = (self.name, state.path.get(entrance, None))
return True
return False
@property
def hint_text(self) -> str:
return self._hint_text if self._hint_text else self.name
def get_connecting_entrance(self, is_main_entrance: typing.Callable[[Entrance], bool]) -> Entrance:
for entrance in self.entrances:
if is_main_entrance(entrance):
return entrance
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
def add_locations(self, locations: Dict[str, Optional[int]], location_type: Optional[typing.Type[Location]] = None) -> None:
"""Adds locations to the Region object, where location_type is your Location class and locations is a dict of
location names to address."""
if location_type is None:
location_type = Location
for location, address in locations.items():
self.locations.append(location_type(self.player, location, address, self))
def add_exits(self, exits: Dict[str, Optional[str]], rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None:
"""
Connects current region to regions in exit dictionary. Passed region names must exist first.
:param exits: exits from the region. format is {"connecting_region", "exit_name"}
:param rules: rules for the exits from this region. format is {"connecting_region", rule}
"""
for exiting_region, name in exits.items():
ret = Entrance(self.player, name, self) if name \
else Entrance(self.player, f"{self.name} -> {exiting_region}", self)
if rules and exiting_region in rules:
ret.access_rule = rules[exiting_region]
self.exits.append(ret)
ret.connect(self.multiworld.get_region(exiting_region, self.player))
def __repr__(self):
return self.__str__()
def __str__(self):
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
class Entrance:
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
hide_path: bool = False
@@ -896,6 +829,108 @@ class Entrance:
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
class Region:
name: str
_hint_text: str
player: int
multiworld: Optional[MultiWorld]
entrances: List[Entrance]
exits: List[Entrance]
locations: List[Location]
entrance_type: ClassVar[Type[Entrance]] = Entrance
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
self.name = name
self.entrances = []
self.exits = []
self.locations = []
self.multiworld = multiworld
self._hint_text = hint
self.player = player
def can_reach(self, state: CollectionState) -> bool:
if state.stale[self.player]:
state.update_reachable_regions(self.player)
return self in state.reachable_regions[self.player]
def can_reach_private(self, state: CollectionState) -> bool:
for entrance in self.entrances:
if entrance.can_reach(state):
if not self in state.path:
state.path[self] = (self.name, state.path.get(entrance, None))
return True
return False
@property
def hint_text(self) -> str:
return self._hint_text if self._hint_text else self.name
def get_connecting_entrance(self, is_main_entrance: Callable[[Entrance], bool]) -> Entrance:
for entrance in self.entrances:
if is_main_entrance(entrance):
return entrance
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
def add_locations(self, locations: Dict[str, Optional[int]],
location_type: Optional[Type[Location]] = None) -> None:
"""
Adds locations to the Region object, where location_type is your Location class and locations is a dict of
location names to address.
:param locations: dictionary of locations to be created and added to this Region `{name: ID}`
:param location_type: Location class to be used to create the locations with"""
if location_type is None:
location_type = Location
for location, address in locations.items():
self.locations.append(location_type(self.player, location, address, self))
def connect(self, connecting_region: Region, name: Optional[str] = None,
rule: Optional[Callable[[CollectionState], bool]] = None) -> None:
"""
Connects this Region to another Region, placing the provided rule on the connection.
:param connecting_region: Region object to connect to path is `self -> exiting_region`
:param name: name of the connection being created
:param rule: callable to determine access of this connection to go from self to the exiting_region"""
exit_ = self.create_exit(name if name else f"{self.name} -> {connecting_region.name}")
if rule:
exit_.access_rule = rule
exit_.connect(connecting_region)
def create_exit(self, name: str) -> Entrance:
"""
Creates and returns an Entrance object as an exit of this region.
:param name: name of the Entrance being created
"""
exit_ = self.entrance_type(self.player, name, self)
self.exits.append(exit_)
return exit_
def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None:
"""
Connects current region to regions in exit dictionary. Passed region names must exist first.
:param exits: exits from the region. format is {"connecting_region": "exit_name"}. if a non dict is provided,
created entrances will be named "self.name -> connecting_region"
:param rules: rules for the exits from this region. format is {"connecting_region", rule}
"""
if not isinstance(exits, Dict):
exits = dict.fromkeys(exits)
for connecting_region, name in exits.items():
self.connect(self.multiworld.get_region(connecting_region, self.player),
name,
rules[connecting_region] if rules and connecting_region in rules else None)
def __repr__(self):
return self.__str__()
def __str__(self):
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
class LocationProgressType(IntEnum):
DEFAULT = 1
PRIORITY = 2

View File

@@ -833,7 +833,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
elif cmd == "SetReply":
ctx.stored_data[args["key"]] = args["value"]
if args["key"] == "EnergyLink":
if args["key"].startswith("EnergyLink"):
ctx.current_energy_link_value = args["value"]
if ctx.ui:
ctx.ui.set_new_energy_link_value()

View File

@@ -51,7 +51,10 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
items_to_place = [items.pop()
for items in reachable_items.values() if items]
for item in items_to_place:
item_pool.remove(item)
for p, pool_item in enumerate(item_pool):
if pool_item is item:
item_pool.pop(p)
break
maximum_exploration_state = sweep_from_pool(
base_state, item_pool + unplaced_items)
@@ -152,8 +155,8 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
if cleanup_required:
# validate all placements and remove invalid ones
state = sweep_from_pool(base_state, [])
for placement in placements:
state = sweep_from_pool(base_state, [])
if world.accessibility[placement.item.player] != "minimal" and not placement.can_reach(state):
placement.item.location = None
unplaced_items.append(placement.item)

View File

@@ -14,44 +14,42 @@ import ModuleUpdate
ModuleUpdate.update()
import copy
import Utils
from worlds.alttp import Options as LttPOptions
from worlds.generic import PlandoConnection
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, get_options, user_path
from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from BaseClasses import seeddigits, get_seed, PlandoOptions
import Options
from BaseClasses import seeddigits, get_seed, PlandoOptions
from Main import main as ERmain
from settings import get_settings
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, user_path
from worlds.alttp import Options as LttPOptions
from worlds.alttp.EntranceRandomizer import parse_arguments
from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister
import copy
from worlds.generic import PlandoConnection
def mystery_argparse():
options = get_options()
defaults = options["generator"]
def resolve_path(path: str, resolver: Callable[[str], str]) -> str:
return path if os.path.isabs(path) else resolver(path)
options = get_settings()
defaults = options.generator
parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
parser.add_argument('--weights_file_path', default=defaults["weights_file_path"],
parser.add_argument('--weights_file_path', default=defaults.weights_file_path,
help='Path to the weights file to use for rolling game settings, urls are also valid')
parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player',
action='store_true')
parser.add_argument('--player_files_path', default=resolve_path(defaults["player_files_path"], user_path),
parser.add_argument('--player_files_path', default=defaults.player_files_path,
help="Input directory for player files.")
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
parser.add_argument('--outputpath', default=resolve_path(options["general_options"]["output_path"], user_path),
parser.add_argument('--multi', default=defaults.players, type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults.spoiler)
parser.add_argument('--outputpath', default=options.general_options.output_path,
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
parser.add_argument('--race', action='store_true', default=defaults["race"])
parser.add_argument('--meta_file_path', default=defaults["meta_file_path"])
parser.add_argument('--race', action='store_true', default=defaults.race)
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
parser.add_argument('--log_level', default='info', help='Sets log level')
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
parser.add_argument('--plando', default=defaults["plando_options"],
parser.add_argument('--plando', default=defaults.plando_options,
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
parser.add_argument("--skip_prog_balancing", action="store_true",
help="Skip progression balancing step during generation.")
@@ -71,6 +69,8 @@ def get_seed_name(random_source) -> str:
def main(args=None, callback=ERmain):
if not args:
args, options = mystery_argparse()
else:
options = get_settings()
seed = get_seed(args.seed)
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
@@ -86,7 +86,7 @@ def main(args=None, callback=ERmain):
try:
weights_cache[args.weights_file_path] = read_weights_yamls(args.weights_file_path)
except Exception as e:
raise ValueError(f"File {args.weights_file_path} is destroyed. Please fix your yaml.") from e
raise ValueError(f"File {args.weights_file_path} is invalid. Please fix your yaml.") from e
logging.info(f"Weights: {args.weights_file_path} >> "
f"{get_choice('description', weights_cache[args.weights_file_path][-1], 'No description specified')}")
@@ -94,7 +94,7 @@ def main(args=None, callback=ERmain):
try:
meta_weights = read_weights_yamls(args.meta_file_path)[-1]
except Exception as e:
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
raise ValueError(f"File {args.meta_file_path} is invalid. Please fix your yaml.") from e
logging.info(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
del(meta_weights["meta_description"])
@@ -114,7 +114,7 @@ def main(args=None, callback=ERmain):
try:
weights_cache[fname] = read_weights_yamls(path)
except Exception as e:
raise ValueError(f"File {fname} is destroyed. Please fix your yaml.") from e
raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e
# sort dict for consistent results across platforms:
weights_cache = {key: value for key, value in sorted(weights_cache.items())}
@@ -137,7 +137,7 @@ def main(args=None, callback=ERmain):
erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed
erargs.plando_options = args.plando
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
erargs.glitch_triforce = options.generator.glitch_triforce_room
erargs.spoiler = args.spoiler
erargs.race = args.race
erargs.outputname = seed_name
@@ -195,7 +195,7 @@ def main(args=None, callback=ERmain):
player += 1
except Exception as e:
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
raise ValueError(f"File {path} is invalid. Please fix your yaml.") from e
else:
raise RuntimeError(f'No weights specified for player {player}')
@@ -374,7 +374,7 @@ def roll_linked_options(weights: dict) -> dict:
else:
logging.debug(f"linked option {option_set['name']} skipped.")
except Exception as e:
raise ValueError(f"Linked option {option_set['name']} is destroyed. "
raise ValueError(f"Linked option {option_set['name']} is invalid. "
f"Please fix your linked option.") from e
return weights
@@ -404,7 +404,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
update_weights(currently_targeted_weights, category_options, "Triggered", option_set["option_name"])
except Exception as e:
raise ValueError(f"Your trigger number {i + 1} is destroyed. "
raise ValueError(f"Your trigger number {i + 1} is invalid. "
f"Please fix your triggers.") from e
return weights

View File

@@ -22,6 +22,7 @@ from shutil import which
from typing import Sequence, Union, Optional
import Utils
import settings
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths
if __name__ == "__main__":
@@ -33,7 +34,8 @@ from Utils import is_frozen, user_path, local_path, init_logging, open_filename,
def open_host_yaml():
file = user_path('host.yaml')
file = settings.get_settings().filename
assert file, "host.yaml missing"
if is_linux:
exe = which('sensible-editor') or which('gedit') or \
which('xdg-open') or which('gnome-open') or which('kde-open')
@@ -84,6 +86,11 @@ def open_folder(folder_path):
webbrowser.open(folder_path)
def update_settings():
from settings import get_settings
get_settings().save()
components.extend([
# Functions
Component("Open host.yaml", func=open_host_yaml),
@@ -256,11 +263,13 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
if not component:
logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
if args["update_settings"]:
update_settings()
if 'file' in args:
run_component(args["component"], args["file"], *args["args"])
elif 'component' in args:
run_component(args["component"], *args["args"])
else:
elif not args["update_settings"]:
run_gui()
@@ -269,9 +278,13 @@ if __name__ == '__main__':
Utils.freeze_support()
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
parser = argparse.ArgumentParser(description='Archipelago Launcher')
parser.add_argument('Patch|Game|Component', type=str, nargs='?',
help="Pass either a patch file, a generated game or the name of a component to run.")
parser.add_argument('args', nargs="*", help="Arguments to pass to component.")
run_group = parser.add_argument_group("Run")
run_group.add_argument("--update_settings", action="store_true",
help="Update host.yaml and exit.")
run_group.add_argument("Patch|Game|Component", type=str, nargs="?",
help="Pass either a patch file, a generated game or the name of a component to run.")
run_group.add_argument("args", nargs="*",
help="Arguments to pass to component.")
main(parser.parse_args())
from worlds.LauncherComponents import processes

View File

@@ -9,16 +9,19 @@ if __name__ == "__main__":
import asyncio
import base64
import binascii
import colorama
import io
import logging
import os
import re
import select
import shlex
import socket
import struct
import sys
import subprocess
import time
import typing
import urllib
import colorama
import struct
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
server_loop)
@@ -30,6 +33,7 @@ from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
from worlds.ladx.Tracker import LocationTracker, MagpieBridge
class GameboyException(Exception):
pass
@@ -115,17 +119,17 @@ class RAGameboy():
assert (self.socket)
self.socket.setblocking(False)
def get_retroarch_version(self):
self.send(b'VERSION\n')
select.select([self.socket], [], [])
response_str, addr = self.socket.recvfrom(16)
async def send_command(self, command, timeout=1.0):
self.send(f'{command}\n')
response_str = await self.async_recv()
self.check_command_response(command, response_str)
return response_str.rstrip()
def get_retroarch_status(self, timeout):
self.send(b'GET_STATUS\n')
select.select([self.socket], [], [], timeout)
response_str, addr = self.socket.recvfrom(1000, )
return response_str.rstrip()
async def get_retroarch_version(self):
return await self.send_command("VERSION")
async def get_retroarch_status(self):
return await self.send_command("GET_STATUS")
def set_cache_limits(self, cache_start, cache_size):
self.cache_start = cache_start
@@ -141,8 +145,8 @@ class RAGameboy():
response, _ = self.socket.recvfrom(4096)
return response
async def async_recv(self):
response = await asyncio.get_event_loop().sock_recv(self.socket, 4096)
async def async_recv(self, timeout=1.0):
response = await asyncio.wait_for(asyncio.get_event_loop().sock_recv(self.socket, 4096), timeout)
return response
async def check_safe_gameplay(self, throw=True):
@@ -169,6 +173,8 @@ class RAGameboy():
raise InvalidEmulatorStateError()
return False
if not await check_wram():
if throw:
raise InvalidEmulatorStateError()
return False
return True
@@ -227,20 +233,30 @@ class RAGameboy():
return r
def check_command_response(self, command: str, response: bytes):
if command == "VERSION":
ok = re.match("\d+\.\d+\.\d+", response.decode('ascii')) is not None
else:
ok = response.startswith(command.encode())
if not ok:
logger.warning(f"Bad response to command {command} - {response}")
raise BadRetroArchResponse()
def read_memory(self, address, size=1):
command = "READ_CORE_MEMORY"
self.send(f'{command} {hex(address)} {size}\n')
response = self.recv()
self.check_command_response(command, response)
splits = response.decode().split(" ", 2)
assert (splits[0] == command)
# Ignore the address for now
# TODO: transform to bytes
if splits[2][:2] == "-1" or splits[0] != "READ_CORE_MEMORY":
if splits[2][:2] == "-1":
raise BadRetroArchResponse()
# TODO: check response address, check hex behavior between RA and BH
return bytearray.fromhex(splits[2])
async def async_read_memory(self, address, size=1):
@@ -248,14 +264,21 @@ class RAGameboy():
self.send(f'{command} {hex(address)} {size}\n')
response = await self.async_recv()
self.check_command_response(command, response)
response = response[:-1]
splits = response.decode().split(" ", 2)
try:
response_addr = int(splits[1], 16)
except ValueError:
raise BadRetroArchResponse()
assert (splits[0] == command)
# Ignore the address for now
if response_addr != address:
raise BadRetroArchResponse()
# TODO: transform to bytes
return bytearray.fromhex(splits[2])
ret = bytearray.fromhex(splits[2])
if len(ret) > size:
raise BadRetroArchResponse()
return ret
def write_memory(self, address, bytes):
command = "WRITE_CORE_MEMORY"
@@ -263,7 +286,7 @@ class RAGameboy():
self.send(f'{command} {hex(address)} {" ".join(hex(b) for b in bytes)}')
select.select([self.socket], [], [])
response, _ = self.socket.recvfrom(4096)
self.check_command_response(command, response)
splits = response.decode().split(" ", 3)
assert (splits[0] == command)
@@ -281,6 +304,9 @@ class LinksAwakeningClient():
pending_deathlink = False
deathlink_debounce = True
recvd_checks = {}
retroarch_address = None
retroarch_port = None
gameboy = None
def msg(self, m):
logger.info(m)
@@ -288,50 +314,48 @@ class LinksAwakeningClient():
self.gameboy.send(s)
def __init__(self, retroarch_address="127.0.0.1", retroarch_port=55355):
self.gameboy = RAGameboy(retroarch_address, retroarch_port)
self.retroarch_address = retroarch_address
self.retroarch_port = retroarch_port
pass
stop_bizhawk_spam = False
async def wait_for_retroarch_connection(self):
logger.info("Waiting on connection to Retroarch...")
if not self.stop_bizhawk_spam:
logger.info("Waiting on connection to Retroarch...")
self.stop_bizhawk_spam = True
self.gameboy = RAGameboy(self.retroarch_address, self.retroarch_port)
while True:
try:
version = self.gameboy.get_retroarch_version()
version = await self.gameboy.get_retroarch_version()
NO_CONTENT = b"GET_STATUS CONTENTLESS"
status = NO_CONTENT
core_type = None
GAME_BOY = b"game_boy"
while status == NO_CONTENT or core_type != GAME_BOY:
try:
status = self.gameboy.get_retroarch_status(0.1)
if status.count(b" ") < 2:
await asyncio.sleep(1.0)
continue
GET_STATUS, PLAYING, info = status.split(b" ", 2)
if status.count(b",") < 2:
await asyncio.sleep(1.0)
continue
core_type, rom_name, self.game_crc = info.split(b",", 2)
if core_type != GAME_BOY:
logger.info(
f"Core type should be '{GAME_BOY}', found {core_type} instead - wrong type of ROM?")
await asyncio.sleep(1.0)
continue
except (BlockingIOError, TimeoutError) as e:
await asyncio.sleep(0.1)
pass
logger.info(f"Connected to Retroarch {version} {info}")
self.gameboy.read_memory(0x1000)
status = await self.gameboy.get_retroarch_status()
if status.count(b" ") < 2:
await asyncio.sleep(1.0)
continue
GET_STATUS, PLAYING, info = status.split(b" ", 2)
if status.count(b",") < 2:
await asyncio.sleep(1.0)
continue
core_type, rom_name, self.game_crc = info.split(b",", 2)
if core_type != GAME_BOY:
logger.info(
f"Core type should be '{GAME_BOY}', found {core_type} instead - wrong type of ROM?")
await asyncio.sleep(1.0)
continue
self.stop_bizhawk_spam = False
logger.info(f"Connected to Retroarch {version.decode('ascii')} running {rom_name.decode('ascii')}")
return
except ConnectionResetError:
except (BlockingIOError, TimeoutError, ConnectionResetError):
await asyncio.sleep(1.0)
pass
def reset_auth(self):
auth = binascii.hexlify(self.gameboy.read_memory(0x0134, 12)).decode()
if self.auth:
assert (auth == self.auth)
async def reset_auth(self):
auth = binascii.hexlify(await self.gameboy.async_read_memory(0x0134, 12)).decode()
self.auth = auth
async def wait_and_init_tracker(self):
@@ -367,11 +391,14 @@ class LinksAwakeningClient():
status = self.gameboy.write_memory(LAClientConstants.wLinkStatusBits, [status])
self.gameboy.write_memory(LAClientConstants.wRecvIndex, struct.pack(">H", next_index))
should_reset_auth = False
async def wait_for_game_ready(self):
logger.info("Waiting on game to be in valid state...")
while not await self.gameboy.check_safe_gameplay(throw=False):
pass
logger.info("Ready!")
if self.should_reset_auth:
self.should_reset_auth = False
raise GameboyException("Resetting due to wrong archipelago server")
logger.info("Game connection ready!")
async def is_victory(self):
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
@@ -398,7 +425,7 @@ class LinksAwakeningClient():
if await self.is_victory():
await win_cb()
recv_index = struct.unpack(">H", self.gameboy.read_memory(LAClientConstants.wRecvIndex, 2))[0]
recv_index = struct.unpack(">H", await self.gameboy.async_read_memory(LAClientConstants.wRecvIndex, 2))[0]
# Play back one at a time
if recv_index in self.recvd_checks:
@@ -480,6 +507,15 @@ class LinksAwakeningContext(CommonContext):
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
await self.send_msgs(message)
had_invalid_slot_data = None
def event_invalid_slot(self):
# The next time we try to connect, reset the game loop for new auth
self.had_invalid_slot_data = True
self.auth = None
# Don't try to autoreconnect, it will just fail
self.disconnected_intentionally = True
CommonContext.event_invalid_slot(self)
ENABLE_DEATHLINK = False
async def send_deathlink(self):
if self.ENABLE_DEATHLINK:
@@ -511,8 +547,17 @@ class LinksAwakeningContext(CommonContext):
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(LinksAwakeningContext, self).server_auth(password_requested)
if self.had_invalid_slot_data:
# We are connecting when previously we had the wrong ROM or server - just in case
# re-read the ROM so that if the user had the correct address but wrong ROM, we
# allow a successful reconnect
self.client.should_reset_auth = True
self.had_invalid_slot_data = False
while self.client.auth == None:
await asyncio.sleep(0.1)
self.auth = self.client.auth
await self.get_username()
await self.send_connect()
def on_package(self, cmd: str, args: dict):
@@ -520,9 +565,13 @@ class LinksAwakeningContext(CommonContext):
self.game = self.slot_info[self.slot].game
# TODO - use watcher_event
if cmd == "ReceivedItems":
for index, item in enumerate(args["items"], args["index"]):
for index, item in enumerate(args["items"], start=args["index"]):
self.client.recvd_checks[index] = item
async def sync(self):
sync_msg = [{'cmd': 'Sync'}]
await self.send_msgs(sync_msg)
item_id_lookup = get_locations_to_id()
async def run_game_loop(self):
@@ -539,17 +588,31 @@ class LinksAwakeningContext(CommonContext):
if self.magpie_enabled:
self.magpie_task = asyncio.create_task(self.magpie.serve())
# yield to allow UI to start
await asyncio.sleep(0)
while True:
try:
# TODO: cancel all client tasks
logger.info("(Re)Starting game loop")
if not self.client.stop_bizhawk_spam:
logger.info("(Re)Starting game loop")
self.found_checks.clear()
# On restart of game loop, clear all checks, just in case we swapped ROMs
# this isn't totally neccessary, but is extra safety against cross-ROM contamination
self.client.recvd_checks.clear()
await self.client.wait_for_retroarch_connection()
self.client.reset_auth()
await self.client.reset_auth()
# If we find ourselves with new auth after the reset, reconnect
if self.auth and self.client.auth != self.auth:
# It would be neat to reconnect here, but connection needs this loop to be running
logger.info("Detected new ROM, disconnecting...")
await self.disconnect()
continue
if not self.client.recvd_checks:
await self.sync()
await self.client.wait_and_init_tracker()
while True:
@@ -560,39 +623,59 @@ class LinksAwakeningContext(CommonContext):
self.last_resend = now
await self.send_checks()
if self.magpie_enabled:
self.magpie.set_checks(self.client.tracker.all_checks)
await self.magpie.set_item_tracker(self.client.item_tracker)
await self.magpie.send_gps(self.client.gps_tracker)
try:
self.magpie.set_checks(self.client.tracker.all_checks)
await self.magpie.set_item_tracker(self.client.item_tracker)
await self.magpie.send_gps(self.client.gps_tracker)
except Exception:
# Don't let magpie errors take out the client
pass
if self.client.should_reset_auth:
self.client.should_reset_auth = False
raise GameboyException("Resetting due to wrong archipelago server")
except (GameboyException, asyncio.TimeoutError, TimeoutError, ConnectionResetError):
await asyncio.sleep(1.0)
except GameboyException:
time.sleep(1.0)
pass
def run_game(romfile: str) -> None:
auto_start = typing.cast(typing.Union[bool, str],
Utils.get_options()["ladx_options"].get("rom_start", True))
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
elif isinstance(auto_start, str):
args = shlex.split(auto_start)
# Specify full path to ROM as we are going to cd in popen
full_rom_path = os.path.realpath(romfile)
args.append(full_rom_path)
try:
# set cwd so that paths to lua scripts are always relative to our client
if getattr(sys, 'frozen', False):
# The application is frozen
script_dir = os.path.dirname(sys.executable)
else:
script_dir = os.path.dirname(os.path.realpath(__file__))
subprocess.Popen(args, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=script_dir)
except FileNotFoundError:
logger.error(f"Couldn't launch ROM, {args[0]} is missing")
async def main():
parser = get_base_parser(description="Link's Awakening Client.")
parser.add_argument("--url", help="Archipelago connection url")
parser.add_argument("--no-magpie", dest='magpie', default=True, action='store_false', help="Disable magpie bridge")
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a .apladx Archipelago Binary Patch file')
args = parser.parse_args()
logger.info(args)
if args.diff_file:
import Patch
logger.info("patch file was supplied - creating rom...")
meta, rom_file = Patch.create_rom_file(args.diff_file)
if "server" in meta:
args.url = meta["server"]
if "server" in meta and not args.connect:
args.connect = meta["server"]
logger.info(f"wrote rom file to {rom_file}")
if args.url:
url = urllib.parse.urlparse(args.url)
args.connect = url.netloc
if url.password:
args.password = urllib.parse.unquote(url.password)
ctx = LinksAwakeningContext(args.connect, args.password, args.magpie)
@@ -604,6 +687,10 @@ async def main():
ctx.run_gui()
ctx.run_cli()
# Down below run_gui so that we get errors out of the process
if args.diff_file:
run_game(rom_file)
await ctx.exit_event.wait()
await ctx.shutdown()

View File

@@ -23,9 +23,10 @@ from urllib.request import urlopen
import ModuleUpdate
ModuleUpdate.update()
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
from worlds.alttp.Rom import LocalRom, apply_rom_settings, get_base_rom_bytes
from worlds.alttp.Sprites import Sprite
from Utils import output_path, local_path, user_path, open_file, get_cert_none_ssl_context, persistent_store, \
get_adjuster_settings, tkinter_center_window, init_logging
get_adjuster_settings, get_adjuster_settings_no_defaults, tkinter_center_window, init_logging
GAME_ALTTP = "A Link to the Past"
@@ -43,6 +44,47 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def _get_help_string(self, action):
return textwrap.dedent(action.help)
# See argparse.BooleanOptionalAction
class BooleanOptionalActionWithDisable(argparse.Action):
def __init__(self,
option_strings,
dest,
default=None,
type=None,
choices=None,
required=False,
help=None,
metavar=None):
_option_strings = []
for option_string in option_strings:
_option_strings.append(option_string)
if option_string.startswith('--'):
option_string = '--disable' + option_string[2:]
_option_strings.append(option_string)
if help is not None and default is not None:
help += " (default: %(default)s)"
super().__init__(
option_strings=_option_strings,
dest=dest,
nargs=0,
default=default,
type=type,
choices=choices,
required=required,
help=help,
metavar=metavar)
def __call__(self, parser, namespace, values, option_string=None):
if option_string in self.option_strings:
setattr(namespace, self.dest, not option_string.startswith('--disable'))
def format_usage(self):
return ' | '.join(self.option_strings)
def get_argparser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
@@ -52,6 +94,8 @@ def get_argparser() -> argparse.ArgumentParser:
help='Path to an ALttP Japan(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default='info', const='info', nargs='?',
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--auto_apply', default='ask',
choices=['ask', 'always', 'never'], help='Whether or not to apply settings automatically in the future.')
parser.add_argument('--menuspeed', default='normal', const='normal', nargs='?',
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\
@@ -61,7 +105,7 @@ def get_argparser() -> argparse.ArgumentParser:
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--deathlink', help='Enable DeathLink system.', action='store_true')
parser.add_argument('--allowcollect', help='Allow collection of other player items', action='store_true')
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
parser.add_argument('--music', default=True, help='Enables/Disables game music.', action=BooleanOptionalActionWithDisable)
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?',
choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
help='''\
@@ -104,21 +148,23 @@ def get_argparser() -> argparse.ArgumentParser:
Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted.
''')
parser.add_argument('--sprite_pool', nargs='+', default=[], help='''
A list of sprites to pull from.
''')
parser.add_argument('--oof', help='''\
Path to a sound effect to replace Link's "oof" sound.
Needs to be in a .brr format and have a length of no
more than 2673 bytes, created from a 16-bit signed PCM
.wav at 12khz. https://github.com/boldowa/snesbrr
''')
parser.add_argument('--names', default='', type=str)
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
return parser
def main():
parser = get_argparser()
args = parser.parse_args()
args.music = not args.disablemusic
args = parser.parse_args(namespace=get_adjuster_settings_no_defaults(GAME_ALTTP))
# set up logger
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
args.loglevel]
@@ -530,9 +576,6 @@ class AttachTooltip(object):
def get_rom_frame(parent=None):
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
if not adjuster_settings:
adjuster_settings = Namespace()
adjuster_settings.baserom = "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
romFrame = Frame(parent)
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
@@ -560,33 +603,8 @@ def get_rom_frame(parent=None):
return romFrame, romVar
def get_rom_options_frame(parent=None):
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
defaults = {
"auto_apply": 'ask',
"music": True,
"reduceflashing": True,
"deathlink": False,
"sprite": None,
"oof": None,
"quickswap": True,
"menuspeed": 'normal',
"heartcolor": 'red',
"heartbeep": 'normal',
"ow_palettes": 'default',
"uw_palettes": 'default',
"hud_palettes": 'default',
"sword_palettes": 'default',
"shield_palettes": 'default',
"sprite_pool": [],
"allowcollect": False,
}
if not adjuster_settings:
adjuster_settings = Namespace()
for key, defaultvalue in defaults.items():
if not hasattr(adjuster_settings, key):
setattr(adjuster_settings, key, defaultvalue)
romOptionsFrame = LabelFrame(parent, text="Rom options")
romOptionsFrame.columnconfigure(0, weight=1)

View File

@@ -71,6 +71,7 @@ class MMBN3Context(CommonContext):
self.auth_name = None
self.slot_data = dict()
self.patching_error = False
self.sent_hints = []
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
@@ -175,13 +176,16 @@ async def parse_payload(payload: dict, ctx: MMBN3Context, force: bool):
# If trade hinting is enabled, send scout checks
if ctx.slot_data.get("trade_quest_hinting", 0) == 2:
scouted_locs = [loc.id for loc in scoutable_locations
trade_bits = [loc.id for loc in scoutable_locations
if check_location_scouted(loc, payload["locations"])]
await ctx.send_msgs([{
"cmd": "LocationScouts",
"locations": scouted_locs,
"create_as_hint": 2
}])
scouted_locs = [loc for loc in trade_bits if loc not in ctx.sent_hints]
if len(scouted_locs) > 0:
ctx.sent_hints.extend(scouted_locs)
await ctx.send_msgs([{
"cmd": "LocationScouts",
"locations": scouted_locs,
"create_as_hint": 2
}])
def check_location_packet(location, memory):

62
Main.py
View File

@@ -7,31 +7,24 @@ import tempfile
import time
import zipfile
import zlib
from typing import Dict, List, Optional, Set, Tuple
from typing import Dict, List, Optional, Set, Tuple, Union
import worlds
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
from Options import StartInventoryPool
from Utils import __version__, get_options, output_path, version_tuple
from settings import get_settings
from Utils import __version__, output_path, version_tuple
from worlds import AutoWorld
from worlds.alttp.Regions import is_main_entrance
from worlds.alttp.Shops import FillDisabledShopSlots
from worlds.alttp.SubClasses import LTTPRegionType
from worlds.generic.Rules import exclusion_rules, locality_rules
__all__ = ["main"]
ordered_areas = (
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total"
)
def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None):
if not baked_server_options:
baked_server_options = get_options()["server_options"]
baked_server_options = get_settings().server_options.as_dict()
assert isinstance(baked_server_options, dict)
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
output_path.cached_path = args.outputpath
@@ -140,12 +133,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.non_local_items[player].value -= world.local_items[player].value
world.non_local_items[player].value -= set(world.local_early_items[player])
if world.players > 1:
locality_rules(world)
else:
world.non_local_items[1].value = set()
world.local_items[1].value = set()
AutoWorld.call_all(world, "set_rules")
for player in world.player_ids:
@@ -154,6 +141,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for location_name in world.priority_locations[player].value:
world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY
# Set local and non-local item rules.
if world.players > 1:
locality_rules(world)
else:
world.non_local_items[1].value = set()
world.local_items[1].value = set()
AutoWorld.call_all(world, "generate_basic")
# remove starting inventory from pool items.
@@ -313,35 +307,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
er_hint_data: Dict[int, Dict[int, str]] = {}
AutoWorld.call_all(world, 'extend_hint_information', er_hint_data)
checks_in_area = {player: {area: list() for area in ordered_areas}
for player in range(1, world.players + 1)}
for player in range(1, world.players + 1):
checks_in_area[player]["Total"] = 0
for location in world.get_filled_locations():
if type(location.address) is int:
if location.game != "A Link to the Past":
checks_in_area[location.player]["Light World"].append(location.address)
else:
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)
checks_in_area[location.player]["Total"] += 1
FillDisabledShopSlots(world)
def write_multidata():
import NetUtils
slot_data = {}
@@ -401,10 +366,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for game_world in world.worlds.values()
}
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}
multidata = {
"slot_data": slot_data,
"slot_info": slot_info,
"names": names, # TODO: remove after 0.3.9
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
"locations": locations_data,
"checks_in_area": checks_in_area,

View File

@@ -299,7 +299,7 @@ if __name__ == '__main__':
versions = get_minecraft_versions(data_version, channel)
forge_dir = Utils.user_path(options["minecraft_options"]["forge_directory"])
forge_dir = options["minecraft_options"]["forge_directory"]
max_heap = options["minecraft_options"]["max_heap_size"]
forge_version = args.forge or versions["forge"]
java_version = args.java or versions["java"]

View File

@@ -38,7 +38,7 @@ import NetUtils
import Utils
from Utils import version_tuple, restricted_loads, Version, async_start
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
SlotType
SlotType, LocationStore
min_client_version = Version(0, 1, 6)
colorama.init()
@@ -152,7 +152,9 @@ class Context:
"compatibility": int}
# team -> slot id -> list of clients authenticated to slot.
clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]]
locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
locations: LocationStore # typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
location_checks: typing.Dict[typing.Tuple[int, int], typing.Set[int]]
hints_used: typing.Dict[typing.Tuple[int, int], int]
groups: typing.Dict[int, typing.Set[int]]
save_version = 2
stored_data: typing.Dict[str, object]
@@ -187,8 +189,6 @@ class Context:
self.player_name_lookup: typing.Dict[str, team_slot] = {}
self.connect_names = {} # names of slots clients can connect to
self.allow_releases = {}
# player location_id item_id target_player_id
self.locations = {}
self.host = host
self.port = port
self.server_password = server_password
@@ -284,6 +284,7 @@ class Context:
except websockets.ConnectionClosed:
logging.exception(f"Exception during send_msgs, could not send {msg}")
await self.disconnect(endpoint)
return False
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
@@ -297,6 +298,7 @@ class Context:
except websockets.ConnectionClosed:
logging.exception("Exception during send_encoded_msgs")
await self.disconnect(endpoint)
return False
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
@@ -311,6 +313,7 @@ class Context:
websockets.broadcast(sockets, msg)
except RuntimeError:
logging.exception("Exception during broadcast_send_encoded_msgs")
return False
else:
if self.log_network:
logging.info(f"Outgoing broadcast: {msg}")
@@ -413,7 +416,7 @@ class Context:
self.seed_name = decoded_obj["seed_name"]
self.random.seed(self.seed_name)
self.connect_names = decoded_obj['connect_names']
self.locations = decoded_obj['locations']
self.locations = LocationStore(decoded_obj.pop("locations")) # pre-emptively free memory
self.slot_data = decoded_obj['slot_data']
for slot, data in self.slot_data.items():
self.read_data[f"slot_data_{slot}"] = lambda data=data: data
@@ -792,7 +795,7 @@ async def on_client_joined(ctx: Context, client: Client):
ctx.broadcast_text_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
f"{verb} {ctx.games[client.slot]} has joined. "
f"Client({version_str}), {client.tags}).",
f"Client({version_str}), {client.tags}.",
{"type": "Join", "team": client.team, "slot": client.slot, "tags": client.tags})
ctx.notify_client(client, "Now that you are connected, "
"you can use !help to list commands to run via the server. "
@@ -902,11 +905,7 @@ def release_player(ctx: Context, team: int, slot: int):
def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
"""register any locations that are in the multidata, pointing towards this player"""
all_locations = collections.defaultdict(set)
for source_slot, location_data in ctx.locations.items():
for location_id, values in location_data.items():
if values[1] == slot:
all_locations[source_slot].add(location_id)
all_locations = ctx.locations.get_for_player(slot)
ctx.broadcast_text_all("%s (Team #%d) has collected their items from other worlds."
% (ctx.player_names[(team, slot)], team + 1),
@@ -925,11 +924,7 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
items = []
for location_id in ctx.locations[slot]:
if location_id not in ctx.location_checks[team, slot]:
items.append(ctx.locations[slot][location_id][0]) # item ID
return sorted(items)
return ctx.locations.get_remaining(ctx.location_checks, team, slot)
def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem):
@@ -977,13 +972,12 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
slots.add(group_id)
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
for finding_player, check_data in ctx.locations.items():
for location_id, (item_id, receiving_player, item_flags) in check_data.items():
if receiving_player in slots and item_id == seeked_item_id:
found = location_id in ctx.location_checks[team, finding_player]
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
item_flags))
for finding_player, location_id, item_id, receiving_player, item_flags \
in ctx.locations.find_item(slots, seeked_item_id):
found = location_id in ctx.location_checks[team, finding_player]
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
item_flags))
return hints
@@ -1555,15 +1549,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
def get_checked_checks(ctx: Context, team: int, slot: int) -> typing.List[int]:
return [location_id for
location_id in ctx.locations[slot] if
location_id in ctx.location_checks[team, slot]]
return ctx.locations.get_checked(ctx.location_checks, team, slot)
def get_missing_checks(ctx: Context, team: int, slot: int) -> typing.List[int]:
return [location_id for
location_id in ctx.locations[slot] if
location_id not in ctx.location_checks[team, slot]]
return ctx.locations.get_missing(ctx.location_checks, team, slot)
def get_client_points(ctx: Context, client: Client) -> int:
@@ -2128,13 +2118,15 @@ class ServerCommandProcessor(CommonCommandProcessor):
async def console(ctx: Context):
import sys
queue = asyncio.Queue()
Utils.stream_input(sys.stdin, queue)
worker = Utils.stream_input(sys.stdin, queue)
while not ctx.exit_event.is_set():
try:
# I don't get why this while loop is needed. Works fine without it on clients,
# but the queue.get() for server never fulfills if the queue is empty when entering the await.
while queue.qsize() == 0:
await asyncio.sleep(0.05)
if not worker.is_alive():
return
input_text = await queue.get()
queue.task_done()
ctx.commandprocessor(input_text)
@@ -2145,7 +2137,7 @@ async def console(ctx: Context):
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
defaults = Utils.get_options()["server_options"]
defaults = Utils.get_options()["server_options"].as_dict()
parser.add_argument('multidata', nargs="?", default=defaults["multidata"])
parser.add_argument('--host', default=defaults["host"])
parser.add_argument('--port', default=defaults["port"], type=int)
@@ -2254,12 +2246,15 @@ async def main(args: argparse.Namespace):
if not isinstance(e, ImportError):
logging.error(f"Failed to load tkinter ({e})")
logging.info("Pass a multidata filename on command line to run headless.")
exit(1)
# when cx_Freeze'd the built-in exit is not available, so we import sys.exit instead
import sys
sys.exit(1)
raise
if not data_filename:
logging.info("No file selected. Exiting.")
exit(1)
import sys
sys.exit(1)
try:
ctx.load(data_filename, args.use_embedded_options)

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import typing
import enum
import warnings
from json import JSONEncoder, JSONDecoder
import websockets
@@ -343,3 +344,77 @@ class Hint(typing.NamedTuple):
@property
def local(self):
return self.receiving_player == self.finding_player
class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tuple[int, int, int]]]):
def __init__(self, values: typing.MutableMapping[int, typing.Dict[int, typing.Tuple[int, int, int]]]):
super().__init__(values)
if not self:
raise ValueError(f"Rejecting game with 0 players")
if len(self) != max(self):
raise ValueError("Player IDs not continuous")
if len(self.get(0, {})):
raise ValueError("Invalid player id 0 for location")
def find_item(self, slots: typing.Set[int], seeked_item_id: int
) -> typing.Generator[typing.Tuple[int, int, int, int, int], None, None]:
for finding_player, check_data in self.items():
for location_id, (item_id, receiving_player, item_flags) in check_data.items():
if receiving_player in slots and item_id == seeked_item_id:
yield finding_player, location_id, item_id, receiving_player, item_flags
def get_for_player(self, slot: int) -> typing.Dict[int, typing.Set[int]]:
import collections
all_locations: typing.Dict[int, typing.Set[int]] = collections.defaultdict(set)
for source_slot, location_data in self.items():
for location_id, values in location_data.items():
if values[1] == slot:
all_locations[source_slot].add(location_id)
return all_locations
def get_checked(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int
) -> typing.List[int]:
checked = state[team, slot]
if not checked:
# This optimizes the case where everyone connects to a fresh game at the same time.
return []
return [location_id for
location_id in self[slot] if
location_id in checked]
def get_missing(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int
) -> typing.List[int]:
checked = state[team, slot]
if not checked:
# This optimizes the case where everyone connects to a fresh game at the same time.
return list(self[slot])
return [location_id for
location_id in self[slot] if
location_id not in checked]
def get_remaining(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int
) -> typing.List[int]:
checked = state[team, slot]
player_locations = self[slot]
return sorted([player_locations[location_id][0] for
location_id in player_locations if
location_id not in checked])
if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub
LocationStore = _LocationStore
else:
try:
import pyximport
pyximport.install()
except ImportError:
pyximport = None
try:
from _speedups import LocationStore
except ImportError:
warnings.warn("_speedups not available. Falling back to pure python LocationStore. "
"Install a matching C++ compiler for your platform to compile _speedups.")
LocationStore = _LocationStore

View File

@@ -296,8 +296,6 @@ async def patch_and_run_game(apz5_file):
comp_path = base_name + '.z64'
# Load vanilla ROM, patch file, compress ROM
rom_file_name = Utils.get_options()["oot_options"]["rom_file"]
if not os.path.exists(rom_file_name):
rom_file_name = Utils.user_path(rom_file_name)
rom = Rom(rom_file_name)
sub_file = None

View File

@@ -1,13 +1,15 @@
from __future__ import annotations
import abc
import logging
from copy import deepcopy
import math
import numbers
import typing
import random
import typing
from copy import deepcopy
from schema import And, Optional, Or, Schema
from schema import Schema, And, Or, Optional
from Utils import get_fuzzy_results
if typing.TYPE_CHECKING:
@@ -769,7 +771,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)")
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys):
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
default: typing.Dict[str, typing.Any] = {}
supports_weighting = False
@@ -787,8 +789,14 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys):
def get_option_name(self, value):
return ", ".join(f"{key}: {v}" for key, v in value.items())
def __contains__(self, item):
return item in self.value
def __getitem__(self, item: str) -> typing.Any:
return self.value.__getitem__(item)
def __iter__(self) -> typing.Iterator[str]:
return self.value.__iter__()
def __len__(self) -> int:
return self.value.__len__()
class ItemDict(OptionDict):
@@ -949,6 +957,7 @@ class DeathLink(Toggle):
class ItemLinks(OptionList):
"""Share part of your item pool with other players."""
display_name = "Item Links"
default = []
schema = Schema([
{

View File

@@ -29,6 +29,9 @@ for location in location_data:
location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address
location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit}
location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"
and location.address is not None}
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart pkmn_rb.lua"
@@ -72,6 +75,7 @@ class GBContext(CommonContext):
self.items_handling = 0b001
self.sent_release = False
self.sent_collect = False
self.auto_hints = set()
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
@@ -153,6 +157,33 @@ async def parse_locations(data: List, ctx: GBContext):
locations.append(loc_id)
elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']:
locations.append(loc_id)
hints = []
if flags["EventFlag"][280] & 16:
hints.append("Cerulean Bicycle Shop")
if flags["EventFlag"][280] & 32:
hints.append("Route 2 Gate - Oak's Aide")
if flags["EventFlag"][280] & 64:
hints.append("Route 11 Gate 2F - Oak's Aide")
if flags["EventFlag"][280] & 128:
hints.append("Route 15 Gate 2F - Oak's Aide")
if flags["EventFlag"][281] & 1:
hints += ["Celadon Prize Corner - Item Prize 1", "Celadon Prize Corner - Item Prize 2",
"Celadon Prize Corner - Item Prize 3"]
if (location_name_to_id["Fossil - Choice A"] in ctx.checked_locations and location_name_to_id["Fossil - Choice B"]
not in ctx.checked_locations):
hints.append("Fossil - Choice B")
elif (location_name_to_id["Fossil - Choice B"] in ctx.checked_locations and location_name_to_id["Fossil - Choice A"]
not in ctx.checked_locations):
hints.append("Fossil - Choice A")
hints = [
location_name_to_id[loc] for loc in hints if location_name_to_id[loc] not in ctx.auto_hints and
location_name_to_id[loc] in ctx.missing_locations and location_name_to_id[loc] not in ctx.locations_checked
]
if hints:
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": hints, "create_as_hint": 2}])
ctx.auto_hints.update(hints)
if flags["EventFlag"][280] & 1 and not ctx.finished_game:
await ctx.send_msgs([
{"cmd": "StatusUpdate",

View File

@@ -49,6 +49,8 @@ Currently, the following games are supported:
* Bumper Stickers
* Mega Man Battle Network 3: Blue Version
* Muse Dash
* DOOM 1993
* Terraria
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import os
import sys
import asyncio
import typing
import bsdiff4
@@ -11,7 +12,7 @@ from NetUtils import NetworkItem, ClientStatus
from worlds import undertale
from MultiServer import mark_raw
from CommonClient import CommonContext, server_loop, \
gui_enabled, ClientCommandProcessor, get_base_parser
gui_enabled, ClientCommandProcessor, logger, get_base_parser
from Utils import async_start
@@ -32,6 +33,12 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
self.ctx.patch_game()
self.output("Patched.")
def _cmd_savepath(self, directory: str):
"""Redirect to proper save data folder. (Use before connecting!)"""
if isinstance(self.ctx, UndertaleContext):
UndertaleContext.save_game_folder = directory
self.output("Changed to the following directory: " + directory)
@mark_raw
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
"""Patch the game automatically."""
@@ -92,6 +99,7 @@ class UndertaleContext(CommonContext):
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.pieces_needed = 0
self.finished_game = False
self.game = "Undertale"
self.got_deathlink = False
self.syncing = False
@@ -99,6 +107,8 @@ class UndertaleContext(CommonContext):
self.tem_armor = False
self.completed_count = 0
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
# self.save_game_folder: files go in this path to pass data between us and the actual game
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
def patch_game(self):
with open(os.getcwd() + "/Undertale/data.win", "rb") as f:
@@ -227,7 +237,7 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
f.close()
filename = f"check.spot"
with open(os.path.join(ctx.save_game_folder, filename), "a") as f:
for ss in ctx.checked_locations:
for ss in set(args["checked_locations"]):
f.write(str(ss-12000)+"\n")
f.close()
elif cmd == "LocationInfo":
@@ -353,14 +363,14 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
if "checked_locations" in args:
filename = f"check.spot"
with open(os.path.join(ctx.save_game_folder, filename), "a") as f:
for ss in ctx.checked_locations:
for ss in set(args["checked_locations"]):
f.write(str(ss-12000)+"\n")
f.close()
elif cmd == "Bounced":
tags = args.get("tags", [])
if "Online" in tags:
data = args.get("worlds/undertale/data", {})
data = args.get("data", {})
if data["player"] != ctx.slot and data["player"] is not None:
filename = f"FRISK" + str(data["player"]) + ".playerspot"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
@@ -406,34 +416,38 @@ async def game_watcher(ctx: UndertaleContext):
ctx.syncing = False
if ctx.got_deathlink:
ctx.got_deathlink = False
with open(os.path.join(ctx.save_game_folder, "/WelcomeToTheDead.youDied"), "w") as f:
with open(os.path.join(ctx.save_game_folder, "WelcomeToTheDead.youDied"), "w") as f:
f.close()
sending = []
victory = False
found_routes = 0
for root, dirs, files in os.walk(path):
for file in files:
if "DontBeMad.mad" in file and "DeathLink" in ctx.tags:
if "DontBeMad.mad" in file:
os.remove(root+"/"+file)
await ctx.send_death()
if "DeathLink" in ctx.tags:
await ctx.send_death()
if "scout" == file:
sending = []
with open(root+"/"+file, "r") as f:
lines = f.readlines()
try:
with open(root+"/"+file, "r") as f:
lines = f.readlines()
for l in lines:
if ctx.server_locations.__contains__(int(l)+12000):
sending = sending + [int(l)+12000]
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending,
"create_as_hint": int(2)}])
os.remove(root+"/"+file)
sending = sending + [int(l.rstrip('\n'))+12000]
finally:
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending,
"create_as_hint": int(2)}])
os.remove(root+"/"+file)
if "check.spot" in file:
sending = []
with open(root+"/"+file, "r") as f:
lines = f.readlines()
try:
with open(root+"/"+file, "r") as f:
lines = f.readlines()
for l in lines:
sending = sending+[(int(l))+12000]
message = [{"cmd": "LocationChecks", "locations": sending}]
await ctx.send_msgs(message)
sending = sending+[(int(l.rstrip('\n')))+12000]
finally:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": sending}])
if "victory" in file and str(ctx.route) in file:
victory = True
if ".playerspot" in file and "Online" not in ctx.tags:

228
Utils.py
View File

@@ -13,8 +13,10 @@ import io
import collections
import importlib
import logging
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
from argparse import Namespace
from settings import Settings, get_settings
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
from yaml import load, load_all, dump, SafeLoader
try:
@@ -138,13 +140,16 @@ def user_path(*path: str) -> str:
user_path.cached_path = local_path()
else:
user_path.cached_path = home_path()
# populate home from local - TODO: upgrade feature
if user_path.cached_path != local_path() and not os.path.exists(user_path("host.yaml")):
import shutil
for dn in ("Players", "data/sprites"):
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
for fn in ("manifest.json", "host.yaml"):
shutil.copy2(local_path(fn), user_path(fn))
# populate home from local
if user_path.cached_path != local_path():
import filecmp
if not os.path.exists(user_path("manifest.json")) or \
not filecmp.cmp(local_path("manifest.json"), user_path("manifest.json"), shallow=True):
import shutil
for dn in ("Players", "data/sprites"):
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
for fn in ("manifest.json",):
shutil.copy2(local_path(fn), user_path(fn))
return os.path.join(user_path.cached_path, *path)
@@ -238,155 +243,15 @@ def get_public_ipv6() -> str:
return ip
OptionsType = typing.Dict[str, typing.Dict[str, typing.Any]]
OptionsType = Settings # TODO: remove ~2 versions after 0.4.1
@cache_argsless
def get_default_options() -> OptionsType:
# Refer to host.yaml for comments as to what all these options mean.
options = {
"general_options": {
"output_path": "output",
},
"factorio_options": {
"executable": os.path.join("factorio", "bin", "x64", "factorio"),
"filter_item_sends": False,
"bridge_chat_out": True,
},
"sni_options": {
"sni_path": "SNI",
"snes_rom_start": True,
},
"sm_options": {
"rom_file": "Super Metroid (JU).sfc",
},
"soe_options": {
"rom_file": "Secret of Evermore (USA).sfc",
},
"lttp_options": {
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
},
"ladx_options": {
"rom_file": "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc",
},
"server_options": {
"host": None,
"port": 38281,
"password": None,
"multidata": None,
"savefile": None,
"disable_save": False,
"loglevel": "info",
"server_password": None,
"disable_item_cheat": False,
"location_check_points": 1,
"hint_cost": 10,
"release_mode": "goal",
"collect_mode": "disabled",
"remaining_mode": "goal",
"auto_shutdown": 0,
"compatibility": 2,
"log_network": 0
},
"generator": {
"enemizer_path": os.path.join("EnemizerCLI", "EnemizerCLI.Core"),
"player_files_path": "Players",
"players": 0,
"weights_file_path": "weights.yaml",
"meta_file_path": "meta.yaml",
"spoiler": 3,
"glitch_triforce_room": 1,
"race": 0,
"plando_options": "bosses",
},
"minecraft_options": {
"forge_directory": "Minecraft Forge server",
"max_heap_size": "2G",
"release_channel": "release"
},
"oot_options": {
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
"rom_start": True
},
"dkc3_options": {
"rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc",
},
"smw_options": {
"rom_file": "Super Mario World (USA).sfc",
},
"zillion_options": {
"rom_file": "Zillion (UE) [!].sms",
# RetroArch doesn't make it easy to launch a game from the command line.
# You have to know the path to the emulator core library on the user's computer.
"rom_start": "retroarch",
},
"pokemon_rb_options": {
"red_rom_file": "Pokemon Red (UE) [S][!].gb",
"blue_rom_file": "Pokemon Blue (UE) [S][!].gb",
"rom_start": True
},
"ffr_options": {
"display_msgs": True,
},
"lufia2ac_options": {
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
},
"tloz_options": {
"rom_file": "Legend of Zelda, The (U) (PRG0) [!].nes",
"rom_start": True,
"display_msgs": True,
},
"wargroove_options": {
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
},
"mmbn3_options": {
"rom_file": "Mega Man Battle Network 3 - Blue Version (USA).gba",
"rom_start": True
},
"adventure_options": {
"rom_file": "ADVNTURE.BIN",
"display_msgs": True,
"rom_start": True,
"rom_args": ""
},
}
return options
def get_default_options() -> Settings: # TODO: remove ~2 versions after 0.4.1
return Settings(None)
def update_options(src: dict, dest: dict, filename: str, keys: list) -> OptionsType:
for key, value in src.items():
new_keys = keys.copy()
new_keys.append(key)
option_name = '.'.join(new_keys)
if key not in dest:
dest[key] = value
if filename.endswith("options.yaml"):
logging.info(f"Warning: {filename} is missing {option_name}")
elif isinstance(value, dict):
if not isinstance(dest.get(key, None), dict):
if filename.endswith("options.yaml"):
logging.info(f"Warning: {filename} has {option_name}, but it is not a dictionary. overwriting.")
dest[key] = value
else:
dest[key] = update_options(value, dest[key], filename, new_keys)
return dest
@cache_argsless
def get_options() -> OptionsType:
filenames = ("options.yaml", "host.yaml")
locations: typing.List[str] = []
if os.path.join(os.getcwd()) != local_path():
locations += filenames # use files from cwd only if it's not the local_path
locations += [user_path(filename) for filename in filenames]
for location in locations:
if os.path.exists(location):
with open(location) as f:
options = parse_yaml(f.read())
return update_options(get_default_options(), options, location, list())
raise FileNotFoundError(f"Could not find {filenames[1]} to load options.")
get_options = get_settings # TODO: add a warning ~2 versions after 0.4.1 and remove once all games are ported
def persistent_store(category: str, key: typing.Any, value: typing.Any):
@@ -454,12 +319,27 @@ def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> N
except Exception as e:
logging.debug(f"Could not store data package: {e}")
def get_default_adjuster_settings(game_name: str) -> Namespace:
import LttPAdjuster
adjuster_settings = Namespace()
if game_name == LttPAdjuster.GAME_ALTTP:
return LttPAdjuster.get_argparser().parse_known_args(args=[])[0]
def get_adjuster_settings(game_name: str) -> typing.Dict[str, typing.Any]:
adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {})
return adjuster_settings
def get_adjuster_settings_no_defaults(game_name: str) -> Namespace:
return persistent_load().get("adjuster", {}).get(game_name, Namespace())
def get_adjuster_settings(game_name: str) -> Namespace:
adjuster_settings = get_adjuster_settings_no_defaults(game_name)
default_settings = get_default_adjuster_settings(game_name)
# Fill in any arguments from the argparser that we haven't seen before
return Namespace(**vars(adjuster_settings), **{k:v for k,v in vars(default_settings).items() if k not in vars(adjuster_settings)})
@cache_argsless
def get_unique_identifier():
uuid = persistent_load().get("client", {}).get("uuid", None)
@@ -677,7 +557,7 @@ def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: ty
)
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]) \
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]], suggest: str = "") \
-> typing.Optional[str]:
def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
@@ -688,11 +568,12 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
kdialog = which("kdialog")
if kdialog:
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
return run(kdialog, f"--title={title}", "--getopenfilename", ".", k_filters)
return run(kdialog, f"--title={title}", "--getopenfilename", suggest or ".", k_filters)
zenity = which("zenity")
if zenity:
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
return run(zenity, f"--title={title}", "--file-selection", *z_filters)
selection = (f'--filename="{suggest}',) if suggest else ()
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
# fall back to tk
try:
@@ -705,7 +586,38 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
else:
root = tkinter.Tk()
root.withdraw()
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes))
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
initialfile=suggest or None)
def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
if is_linux:
# prefer native dialog
from shutil import which
kdialog = None#which("kdialog")
if kdialog:
return run(kdialog, f"--title={title}", "--getexistingdirectory", suggest or ".")
zenity = None#which("zenity")
if zenity:
z_filters = ("--directory",)
selection = (f'--filename="{suggest}',) if suggest else ()
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
# fall back to tk
try:
import tkinter
import tkinter.filedialog
except Exception as e:
logging.error('Could not load tkinter, which is likely not installed. '
f'This attempt was made because open_filename was used for "{title}".')
raise e
else:
root = tkinter.Tk()
root.withdraw()
return tkinter.filedialog.askdirectory(title=title, mustexist=True, initialdir=suggest or None)
def messagebox(title: str, text: str, error: bool = False) -> None:

View File

@@ -10,6 +10,7 @@ ModuleUpdate.update()
# in case app gets imported by something like gunicorn
import Utils
import settings
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
@@ -18,9 +19,10 @@ from waitress import serve
from WebHostLib.models import db
from WebHostLib.autolauncher import autohost, autogen
from WebHostLib.lttpsprites import update_sprites_lttp
from WebHostLib.options import create as create_options_files
import worlds
settings.no_gui = True
configpath = os.path.abspath("config.yaml")
if not os.path.exists(configpath): # fall back to config.yaml in home
configpath = os.path.abspath(Utils.user_path('config.yaml'))
@@ -40,6 +42,13 @@ def get_app():
db.bind(**app.config["PONY"])
db.generate_mapping(create_tables=True)
for world in worlds.AutoWorldRegister.world_types.values():
try:
world.web.run_webhost_app_setup(app)
except Exception as e:
logging.exception(e)
return app
@@ -72,6 +81,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
with zipfile.ZipFile(zipfile_path) as zf:
for zfile in zf.infolist():
if not zfile.is_dir() and "/docs/" in zfile.filename:
zfile.filename = os.path.basename(zfile.filename)
zf.extract(zfile, target_path)
else:
source_path = Utils.local_path(os.path.dirname(world.__file__), "docs")
@@ -117,12 +127,17 @@ if __name__ == "__main__":
multiprocessing.freeze_support()
multiprocessing.set_start_method('spawn')
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
try:
update_sprites_lttp()
except Exception as e:
logging.exception(e)
logging.warning("Could not update LttP sprites.")
for world in worlds.AutoWorldRegister.world_types.values():
try:
world.web.run_webhost_setup()
except Exception as e:
logging.exception(e)
app = get_app()
del world, worlds
create_options_files()
create_ordered_tutorials_file()
if app.config["SELFLAUNCH"]:

View File

@@ -130,6 +130,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
erargs.teams = 1
erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options",
{"bosses", "items", "connections", "texts"}))
erargs.skip_prog_balancing = False
name_counter = Counter()
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):

View File

@@ -1,50 +0,0 @@
import os
import threading
import json
from Utils import local_path, user_path
from worlds.alttp.Rom import Sprite
def update_sprites_lttp():
from tkinter import Tk
from LttPAdjuster import get_image_for_sprite
from LttPAdjuster import BackgroundTaskProgress
from LttPAdjuster import BackgroundTaskProgressNullWindow
from LttPAdjuster import update_sprites
# Target directories
input_dir = user_path("data", "sprites", "alttpr")
output_dir = local_path("WebHostLib", "static", "generated") # TODO: move to user_path
os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True)
# update sprites through gui.py's functions
done = threading.Event()
try:
top = Tk()
except:
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
else:
top.withdraw()
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.isSet():
task.do_events()
spriteData = []
for file in (file for file in os.listdir(input_dir) if not file.startswith(".")):
sprite = Sprite(os.path.join(input_dir, file))
if not sprite.name:
print("Warning:", file, "has no name.")
sprite.name = file.split(".", 1)[0]
if sprite.valid:
with open(os.path.join(output_dir, "sprites", f"{os.path.splitext(file)[0]}.gif"), 'wb') as image:
image.write(get_image_for_sprite(sprite, True))
spriteData.append({"file": file, "author": sprite.author_name, "name": sprite.name})
else:
print(file, "dropped, as it has no valid sprite data.")
spriteData.sort(key=lambda entry: entry["name"])
with open(f'{output_dir}/spriteData.json', 'w') as file:
json.dump({"sprites": spriteData}, file, indent=1)
return spriteData

View File

@@ -5,7 +5,8 @@ html{
}
#player-settings{
max-width: 1000px;
box-sizing: border-box;
max-width: 1024px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
@@ -163,6 +164,11 @@ html{
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
}
#player-settings table .randomize-button[data-tooltip]::after {
left: unset;
right: 0;
}
#player-settings table label{
display: block;
min-width: 200px;
@@ -177,18 +183,31 @@ html{
vertical-align: top;
}
@media all and (max-width: 1000px), all and (orientation: portrait){
@media all and (max-width: 1024px) {
#player-settings {
border-radius: 0;
}
#player-settings #game-options{
justify-content: flex-start;
flex-wrap: wrap;
}
#player-settings .left, #player-settings .right{
flex-grow: unset;
#player-settings .left,
#player-settings .right {
margin: 0;
}
#game-options table {
margin-bottom: 0;
}
#game-options table label{
display: block;
min-width: 200px;
}
#game-options table tr td {
width: 50%;
}
}

View File

@@ -0,0 +1,28 @@
{% for team, hints in hints.items() %}
<div class="table-wrapper">
<table id="hints-table" class="table non-unique-item-table" data-order='[[5, "asc"], [0, "asc"]]'>
<thead>
<tr>
<th>Finder</th>
<th>Receiver</th>
<th>Item</th>
<th>Location</th>
<th>Entrance</th>
<th>Found</th>
</tr>
</thead>
<tbody>
{%- for hint in hints -%}
<tr>
<td>{{ long_player_names[team, hint.finding_player] }}</td>
<td>{{ long_player_names[team, hint.receiving_player] }}</td>
<td>{{ hint.item|item_name }}</td>
<td>{{ hint.location|location_name }}</td>
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
<td>{% if hint.found %}✔{% endif %}</td>
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
{% endfor %}

View File

@@ -1,6 +1,6 @@
{% block footer %}
<footer id="island-footer">
<div id="copyright-notice">Copyright 2022 Archipelago</div>
<div id="copyright-notice">Copyright 2023 Archipelago</div>
<div id="links">
<a href="/sitemap">Site Map</a>
-

View File

@@ -128,20 +128,30 @@
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
<td>{{ player_names[(team, loop.index)]|e }}</td>
{%- for area in ordered_areas -%}
{%- set checks_done = checks[area] -%}
{%- set checks_total = checks_in_area[player][area] -%}
{%- if checks_done == checks_total -%}
<td class="item-acquired center-column">
{{ checks_done }}/{{ checks_total }}</td>
{%- else -%}
<td class="center-column">{{ checks_done }}/{{ checks_total }}</td>
{%- endif -%}
{%- if area in key_locations -%}
<td class="center-column">{{ inventory[team][player][small_key_ids[area]] }}</td>
{%- endif -%}
{%- if area in big_key_locations -%}
<td class="center-column">{% if inventory[team][player][big_key_ids[area]] %}✔️{% endif %}</td>
{%- endif -%}
{% if player in checks_in_area and area in checks_in_area[player] %}
{%- set checks_done = checks[area] -%}
{%- set checks_total = checks_in_area[player][area] -%}
{%- if checks_done == checks_total -%}
<td class="item-acquired center-column">
{{ checks_done }}/{{ checks_total }}</td>
{%- else -%}
<td class="center-column">{{ checks_done }}/{{ checks_total }}</td>
{%- endif -%}
{%- if area in key_locations -%}
<td class="center-column">{{ inventory[team][player][small_key_ids[area]] }}</td>
{%- endif -%}
{%- if area in big_key_locations -%}
<td class="center-column">{% if inventory[team][player][big_key_ids[area]] %}✔️{% endif %}</td>
{%- endif -%}
{% else %}
<td class="center-column"></td>
{%- if area in key_locations -%}
<td class="center-column"></td>
{%- endif -%}
{%- if area in big_key_locations -%}
<td class="center-column"></td>
{%- endif -%}
{% endif %}
{%- endfor -%}
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
{%- if activity_timers[(team, player)] -%}
@@ -155,34 +165,7 @@
</table>
</div>
{% endfor %}
{% for team, hints in hints.items() %}
<div class="table-wrapper">
<table id="hints-table" class="table non-unique-item-table" data-order='[[5, "asc"], [0, "asc"]]'>
<thead>
<tr>
<th>Finder</th>
<th>Receiver</th>
<th>Item</th>
<th>Location</th>
<th>Entrance</th>
<th>Found</th>
</tr>
</thead>
<tbody>
{%- for hint in hints -%}
<tr>
<td>{{ long_player_names[team, hint.finding_player] }}</td>
<td>{{ long_player_names[team, hint.receiving_player] }}</td>
<td>{{ hint.item|item_name }}</td>
<td>{{ hint.location|location_name }}</td>
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
<td>{% if hint.found %}✔{% endif %}</td>
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
{% endfor %}
{% include "hintTable.html" with context %}
</div>
</div>
{% endblock %}

View File

@@ -53,7 +53,7 @@
{# implement this block in game-specific multi trackers #}
{% endblock %}
<td class="center-column" data-sort="{{ checks["Total"] }}">
{{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }}
{{ checks["Total"] }}/{{ locations[player] | length }}
</td>
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
{%- if activity_timers[team, player] -%}
@@ -67,34 +67,7 @@
</table>
</div>
{% endfor %}
{% for team, hints in hints.items() %}
<div class="table-wrapper">
<table id="hints-table" class="table non-unique-item-table" data-order='[[5, "asc"], [0, "asc"]]'>
<thead>
<tr>
<th>Finder</th>
<th>Receiver</th>
<th>Item</th>
<th>Location</th>
<th>Entrance</th>
<th>Found</th>
</tr>
</thead>
<tbody>
{%- for hint in hints -%}
<tr>
<td>{{ long_player_names[team, hint.finding_player] }}</td>
<td>{{ long_player_names[team, hint.receiving_player] }}</td>
<td>{{ hint.item|item_name }}</td>
<td>{{ hint.location|location_name }}</td>
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
<td>{% if hint.found %}✔{% endif %}</td>
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
{% endfor %}
{% include "hintTable.html" with context %}
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,7 @@
{%- if enabled_multiworld_trackers|length > 1 -%}
<div id="tracker-navigation">
{% for enabled_tracker in enabled_multiworld_trackers %}
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %}
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker, game=enabled_tracker.name) %}
<a class="tracker-navigation-button{% if enabled_tracker.current %} selected{% endif %}"
href="{{ tracker_url }}">{{ enabled_tracker.name }}</a>
{% endfor %}

View File

@@ -1,17 +1,18 @@
import collections
import datetime
import typing
from typing import Counter, Optional, Dict, Any, Tuple
import pkgutil
from typing import Counter, Optional, Dict, Any, Tuple, List
from uuid import UUID
from flask import render_template
from jinja2 import pass_context, runtime
from jinja2 import pass_context, runtime, Template
from werkzeug.exceptions import abort
from MultiServer import Context, get_saving_second
from NetUtils import SlotType
from NetUtils import SlotType, NetworkSlot
from Utils import restricted_loads
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package, AutoWorldRegister
from worlds.alttp import Items
from . import app, cache
from .models import GameDataPackage, Room
@@ -264,16 +265,17 @@ def get_static_room_data(room: Room):
multidata = Context.decompress(room.seed.multidata)
# in > 100 players this can take a bit of time and is the main reason for the cache
locations: Dict[int, Dict[int, Tuple[int, int, int]]] = multidata['locations']
names: Dict[int, Dict[int, str]] = multidata["names"]
games = {}
names: List[List[str]] = multidata.get("names", [])
games = multidata.get("games", {})
groups = {}
custom_locations = {}
custom_items = {}
if "slot_info" in multidata:
games = {slot: slot_info.game for slot, slot_info in multidata["slot_info"].items()}
groups = {slot: slot_info.group_members for slot, slot_info in multidata["slot_info"].items()
slot_info_dict: Dict[int, NetworkSlot] = multidata["slot_info"]
games = {slot: slot_info.game for slot, slot_info in slot_info_dict.items()}
groups = {slot: slot_info.group_members for slot, slot_info in slot_info_dict.items()
if slot_info.type == SlotType.group}
names = [[slot_info.name for slot, slot_info in sorted(slot_info_dict.items())]]
for game in games.values():
if game not in multidata["datapackage"]:
continue
@@ -290,8 +292,7 @@ def get_static_room_data(room: Room):
{id_: name for name, id_ in game_data["location_name_to_id"].items()})
custom_items.update(
{id_: name for name, id_ in game_data["item_name_to_id"].items()})
elif "games" in multidata:
games = multidata["games"]
seed_checks_in_area = checks_in_area.copy()
use_door_tracker = False
@@ -302,14 +303,17 @@ def get_static_room_data(room: Room):
seed_checks_in_area[area] += len(checks)
seed_checks_in_area["Total"] = 249
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][playernumber][areaname])
if areaname != "Total" else multidata["checks_in_area"][playernumber]["Total"]
for areaname in ordered_areas}
for playernumber in range(1, len(names[0]) + 1)
if playernumber not in groups}
player_checks_in_area = {
playernumber: {
areaname: len(multidata["checks_in_area"][playernumber][areaname]) if areaname != "Total" else
multidata["checks_in_area"][playernumber]["Total"]
for areaname in ordered_areas
}
for playernumber in multidata["checks_in_area"]
}
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber])
for playernumber in range(1, len(names[0]) + 1)
if playernumber not in groups}
for playernumber in multidata["checks_in_area"]}
saving_second = get_saving_second(multidata["seed_name"])
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
multidata["precollected_items"], games, multidata["slot_data"], groups, saving_second, \
@@ -343,7 +347,7 @@ def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, w
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
get_static_room_data(room)
player_name = names[tracked_team][tracked_player - 1]
location_to_area = player_location_to_area[tracked_player]
location_to_area = player_location_to_area.get(tracked_player, {})
inventory = collections.Counter()
checks_done = {loc_name: 0 for loc_name in default_locations}
@@ -375,15 +379,18 @@ def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, w
if recipient in slots_aimed_at_player: # a check done for the tracked player
attribute_item_solo(inventory, item)
if ms_player == tracked_player: # a check done by the tracked player
checks_done[location_to_area[location]] += 1
area_name = location_to_area.get(location, None)
if area_name:
checks_done[area_name] += 1
checks_done["Total"] += 1
specific_tracker = game_specific_trackers.get(games[tracked_player], None)
if specific_tracker and not want_generic:
tracker = specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second)
seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second)
else:
tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
seed_checks_in_area, checks_done, saving_second, custom_locations, custom_items)
tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player,
player_name, seed_checks_in_area, checks_done, saving_second,
custom_locations, custom_items)
return (saving_second - datetime.datetime.now().second) % 60 or 60, tracker
@@ -1325,87 +1332,6 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic
custom_items=custom_items, custom_locations=custom_locations)
def get_enabled_multiworld_trackers(room: Room, current: str):
enabled = [
{
"name": "Generic",
"endpoint": "get_multiworld_tracker",
"current": current == "Generic"
}
]
for game_name, endpoint in multi_trackers.items():
if any(slot.game == game_name for slot in room.seed.slots) or current == game_name:
enabled.append({
"name": game_name,
"endpoint": endpoint.__name__,
"current": current == game_name}
)
return enabled
def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]:
room: Room = Room.get(tracker=tracker)
if not room:
return None
locations, names, use_door_tracker, checks_in_area, player_location_to_area, \
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
get_static_room_data(room)
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)}
percent_total_checks_done = {teamnumber: {playernumber: 0
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)}
hints = {team: set() for team in range(len(names))}
if room.multisave:
multisave = restricted_loads(room.multisave)
else:
multisave = {}
if "hints" in multisave:
for (team, slot), slot_hints in multisave["hints"].items():
hints[team] |= set(slot_hints)
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
if player in groups:
continue
player_locations = locations[player]
checks_done[team][player]["Total"] = sum(1 for loc in locations_checked if loc in player_locations)
percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] /
checks_in_area[player]["Total"] * 100) \
if checks_in_area[player]["Total"] else 100
activity_timers = {}
now = datetime.datetime.utcnow()
for (team, player), timestamp in multisave.get("client_activity_timers", []):
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
player_names = {}
states: typing.Dict[typing.Tuple[int, int], int] = {}
for team, names in enumerate(names):
for player, name in enumerate(names, 1):
player_names[team, player] = name
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0)
long_player_names = player_names.copy()
for (team, player), alias in multisave.get("name_aliases", {}).items():
player_names[team, player] = alias
long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})"
video = {}
for (team, player), data in multisave.get("video", []):
video[team, player] = data
return dict(player_names=player_names, room=room, checks_done=checks_done,
percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area,
activity_timers=activity_timers, video=video, hints=hints,
long_player_names=long_player_names,
multisave=multisave, precollected_items=precollected_items, groups=groups,
locations=locations, games=games, states=states)
def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]:
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
for teamnumber, team_data in data["checks_done"].items()}
@@ -1426,32 +1352,6 @@ def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int,
inventory[team][recipient][item_id] += 1
return inventory
@app.route('/tracker/<suuid:tracker>')
@cache.memoize(timeout=60) # multisave is currently created at most every minute
def get_multiworld_tracker(tracker: UUID):
data = _get_multiworld_tracker_data(tracker)
if not data:
abort(404)
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic")
return render_template("multiTracker.html", **data)
@app.route('/tracker/<suuid:tracker>/Factorio')
@cache.memoize(timeout=60) # multisave is currently created at most every minute
def get_Factorio_multiworld_tracker(tracker: UUID):
data = _get_multiworld_tracker_data(tracker)
if not data:
abort(404)
data["inventory"] = _get_inventory_data(data)
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio")
return render_template("multiFactorioTracker.html", **data)
@app.route('/tracker/<suuid:tracker>/A Link to the Past')
@cache.memoize(timeout=60) # multisave is currently created at most every minute
def get_LttP_multiworld_tracker(tracker: UUID):
@@ -1509,8 +1409,8 @@ def get_LttP_multiworld_tracker(tracker: UUID):
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"] / seed_checks_in_area[player]["Total"] * 100) if \
seed_checks_in_area[player]["Total"] else 100
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:
@@ -1577,7 +1477,142 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = {
"Starcraft 2 Wings of Liberty": __renderSC2WoLTracker
}
# MultiTrackers
@app.route('/tracker/<suuid:tracker>')
@cache.memoize(timeout=60) # multisave is currently created at most every minute
def get_multiworld_tracker(tracker: UUID) -> str:
data = _get_multiworld_tracker_data(tracker)
if not data:
abort(404)
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic")
return render_template("multiTracker.html", **data)
def get_enabled_multiworld_trackers(room: Room, current: str) -> typing.List[typing.Dict[str, typing.Any]]:
enabled = [
{
"name": "Generic",
"endpoint": "get_multiworld_tracker",
"current": current == "Generic"
}
]
for game_name, endpoint in multi_trackers.items():
if any(slot.game == game_name for slot in room.seed.slots) or current == game_name:
enabled.append({
"name": game_name,
"endpoint": endpoint.__name__,
"current": current == game_name}
)
return enabled
def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]:
room: Room = Room.get(tracker=tracker)
if not room:
return None
locations, names, use_door_tracker, checks_in_area, player_location_to_area, \
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
get_static_room_data(room)
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)}
percent_total_checks_done = {teamnumber: {playernumber: 0
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)}
hints = {team: set() for team in range(len(names))}
if room.multisave:
multisave = restricted_loads(room.multisave)
else:
multisave = {}
if "hints" in multisave:
for (team, slot), slot_hints in multisave["hints"].items():
hints[team] |= set(slot_hints)
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
if player in groups:
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
activity_timers = {}
now = datetime.datetime.utcnow()
for (team, player), timestamp in multisave.get("client_activity_timers", []):
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
player_names = {}
states: typing.Dict[typing.Tuple[int, int], int] = {}
for team, names in enumerate(names):
for player, name in enumerate(names, 1):
player_names[team, player] = name
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0)
long_player_names = player_names.copy()
for (team, player), alias in multisave.get("name_aliases", {}).items():
player_names[team, player] = alias
long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})"
video = {}
for (team, player), data in multisave.get("video", []):
video[team, player] = data
return dict(
player_names=player_names, room=room, checks_done=checks_done,
percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area,
activity_timers=activity_timers, video=video, hints=hints,
long_player_names=long_player_names,
multisave=multisave, precollected_items=precollected_items, groups=groups,
locations=locations, games=games, states=states,
custom_locations=custom_locations, custom_items=custom_items,
)
multi_trackers: typing.Dict[str, typing.Callable] = {
"A Link to the Past": get_LttP_multiworld_tracker,
"Factorio": get_Factorio_multiworld_tracker,
}
class MultiTrackerData(typing.NamedTuple):
template: Template
item_name_to_id: typing.Dict[str, int]
location_name_to_id: typing.Dict[str, int]
multi_tracker_data: typing.Dict[str, MultiTrackerData] = {}
@app.route("/tracker/<suuid:tracker>/<game>")
@cache.memoize(timeout=60) # multisave is currently created up to every minute
def get_game_multiworld_tracker(tracker: UUID, game: str) -> str:
current_multi_tracker_data = multi_tracker_data.get(game, None)
if not current_multi_tracker_data:
abort(404)
data = _get_multiworld_tracker_data(tracker)
if not data:
abort(404)
data["inventory"] = _get_inventory_data(data)
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], game)
data["item_name_to_id"] = current_multi_tracker_data.item_name_to_id
data["location_name_to_id"] = current_multi_tracker_data.location_name_to_id
return render_template(current_multi_tracker_data.template, **data)
def register_multitrackers() -> None:
for world in AutoWorldRegister.world_types.values():
multitracker = world.web.multitracker_template
if multitracker:
multitracker_template = pkgutil.get_data(world.__module__, multitracker).decode()
multitracker_template = app.jinja_env.from_string(multitracker_template)
multi_trackers[world.game] = get_game_multiworld_tracker
multi_tracker_data[world.game] = MultiTrackerData(
multitracker_template,
world.item_name_to_id,
world.location_name_to_id,
)
register_multitrackers()

View File

@@ -423,9 +423,9 @@ async def zillion_sync_task(ctx: ZillionContext) -> None:
async_start(ctx.send_connect())
log_no_spam("logging in to server...")
await asyncio.wait((
ctx.got_slot_data.wait(),
ctx.exit_event.wait(),
asyncio.sleep(6)
asyncio.create_task(ctx.got_slot_data.wait()),
asyncio.create_task(ctx.exit_event.wait()),
asyncio.create_task(asyncio.sleep(6))
), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets
else: # not correct seed name
log_no_spam("incorrect seed - did you mix up roms?")
@@ -447,9 +447,9 @@ async def zillion_sync_task(ctx: ZillionContext) -> None:
ctx.known_name = name
async_start(ctx.connect())
await asyncio.wait((
ctx.got_room_info.wait(),
ctx.exit_event.wait(),
asyncio.sleep(6)
asyncio.create_task(ctx.got_room_info.wait()),
asyncio.create_task(ctx.exit_event.wait()),
asyncio.create_task(asyncio.sleep(6))
), return_when=asyncio.FIRST_COMPLETED)
else: # no name found in game
if not help_message_shown:

347
_speedups.pyx Normal file
View File

@@ -0,0 +1,347 @@
#cython: language_level=3
#distutils: language = c++
"""
Provides faster implementation of some core parts.
This is deliberately .pyx because using a non-compiled "pure python" may be slower.
"""
# pip install cython cymem
import cython
import warnings
from cpython cimport PyObject
from typing import Any, Dict, Iterable, Iterator, Generator, Sequence, Tuple, TypeVar, Union, Set, List, TYPE_CHECKING
from cymem.cymem cimport Pool
from libc.stdint cimport int64_t, uint32_t
from libcpp.set cimport set as std_set
from collections import defaultdict
cdef extern from *:
"""
// avoid warning from cython-generated code with MSVC + pyximport
#ifdef _MSC_VER
#pragma warning( disable: 4551 )
#endif
"""
ctypedef uint32_t ap_player_t # on AMD64 this is faster (and smaller) than 64bit ints
ctypedef uint32_t ap_flags_t
ctypedef int64_t ap_id_t
cdef ap_player_t MAX_PLAYER_ID = 1000000 # limit the size of indexing array
cdef size_t INVALID_SIZE = <size_t>(-1) # this is all 0xff... adding 1 results in 0, but it's not negative
cdef struct LocationEntry:
# layout is so that
# 64bit player: location+sender and item+receiver 128bit comparisons, if supported
# 32bit player: aligned to 32/64bit with no unused space
ap_id_t location
ap_player_t sender
ap_player_t receiver
ap_id_t item
ap_flags_t flags
cdef struct IndexEntry:
size_t start
size_t count
cdef class LocationStore:
"""Compact store for locations and their items in a MultiServer"""
# The original implementation uses Dict[int, Dict[int, Tuple(int, int, int]]
# with sender, location, (item, receiver, flags).
# This implementation is a flat list of (sender, location, item, receiver, flags) using native integers
# as well as some mapping arrays used to speed up stuff, saving a lot of memory while speeding up hints.
# Using std::map might be worth investigating, but memory overhead would be ~100% compared to arrays.
cdef Pool _mem
cdef object _len
cdef LocationEntry* entries # 3.2MB/100k items
cdef size_t entry_count
cdef IndexEntry* sender_index # 16KB/1000 players
cdef size_t sender_index_size
cdef list _keys # ~36KB/1000 players, speed up iter (28 per int + 8 per list entry)
cdef list _items # ~64KB/1000 players, speed up items (56 per tuple + 8 per list entry)
cdef list _proxies # ~92KB/1000 players, speed up self[player] (56 per struct + 28 per len + 8 per list entry)
cdef PyObject** _raw_proxies # 8K/1000 players, faster access to _proxies, but does not keep a ref
def get_size(self):
from sys import getsizeof
size = getsizeof(self) + getsizeof(self._mem) + getsizeof(self._len) \
+ sizeof(LocationEntry) * self.entry_count + sizeof(IndexEntry) * self.sender_index_size
size += getsizeof(self._keys) + getsizeof(self._items) + getsizeof(self._proxies)
size += sum(sizeof(key) for key in self._keys)
size += sum(sizeof(item) for item in self._items)
size += sum(sizeof(proxy) for proxy in self._proxies)
size += sizeof(self._raw_proxies[0]) * self.sender_index_size
return size
def __cinit__(self, locations_dict: Dict[int, Dict[int, Sequence[int]]]) -> None:
self._mem = None
self._keys = None
self._items = None
self._proxies = None
self._len = 0
self.entries = NULL
self.entry_count = 0
self.sender_index = NULL
self.sender_index_size = 0
self._raw_proxies = NULL
def __init__(self, locations_dict: Dict[int, Dict[int, Sequence[int]]]) -> None:
self._mem = Pool()
cdef object key
self._keys = []
self._items = []
self._proxies = []
# iterate over everything to get all maxima and validate everything
cdef size_t max_sender = INVALID_SIZE # keep track of highest used player id for indexing
cdef size_t sender_count = 0
cdef size_t count = 0
for sender, locations in locations_dict.items():
# we don't require the dict to be sorted here
if not isinstance(sender, int) or sender < 1 or sender > MAX_PLAYER_ID:
raise ValueError(f"Invalid player id {sender} for location")
if max_sender == INVALID_SIZE:
max_sender = sender
else:
max_sender = max(max_sender, sender)
for location, data in locations.items():
receiver = data[1]
if receiver < 1 or receiver > MAX_PLAYER_ID:
raise ValueError(f"Invalid player id {receiver} for item")
count += 1
sender_count += 1
if not sender_count:
raise ValueError(f"Rejecting game with 0 players")
if sender_count != max_sender:
# we assume player 0 will never have locations
raise ValueError("Player IDs not continuous")
if not count:
warnings.warn("Game has no locations")
# allocate the arrays and invalidate index (0xff...)
self.entries = <LocationEntry*>self._mem.alloc(count, sizeof(LocationEntry))
self.sender_index = <IndexEntry*>self._mem.alloc(max_sender + 1, sizeof(IndexEntry))
self._raw_proxies = <PyObject**>self._mem.alloc(max_sender + 1, sizeof(PyObject*))
# build entries and index
cdef size_t i = 0
for sender, locations in sorted(locations_dict.items()):
self.sender_index[sender].start = i
self.sender_index[sender].count = 0
# Sorting locations here makes it possible to write a faster lookup without an additional index.
for location, data in sorted(locations.items()):
self.entries[i].sender = sender
self.entries[i].location = location
self.entries[i].item = data[0]
self.entries[i].receiver = data[1]
if len(data) > 2:
self.entries[i].flags = data[2] # initialized to 0 during alloc
# Ignoring extra data. warn?
self.sender_index[sender].count += 1
i += 1
# build pyobject caches
self._proxies.append(None) # player 0
assert self.sender_index[0].count == 0
for i in range(1, max_sender + 1):
assert self.sender_index[i].count == 0 or (
self.sender_index[i].start < count and
self.sender_index[i].start + self.sender_index[i].count <= count)
key = i # allocate python integer
proxy = PlayerLocationProxy(self, i)
self._keys.append(key)
self._items.append((key, proxy))
self._proxies.append(proxy)
self._raw_proxies[i] = <PyObject*>proxy
self.sender_index_size = max_sender + 1
self.entry_count = count
self._len = sender_count
# fake dict access
def __len__(self) -> int:
return self._len
def __iter__(self) -> Iterator[int]:
return self._keys.__iter__()
def __getitem__(self, key: int) -> Any:
# figure out if player actually exists in the multidata and return a proxy
cdef size_t i = key # NOTE: this may raise TypeError
if i < 1 or i >= self.sender_index_size:
raise KeyError(key)
return <object>self._raw_proxies[key]
T = TypeVar('T')
def get(self, key: int, default: T) -> Union[PlayerLocationProxy, T]:
# calling into self.__getitem__ here is slow, but this is not used in MultiServer
try:
return self[key]
except KeyError:
return default
def items(self) -> Iterable[Tuple[int, PlayerLocationProxy]]:
return self._items
# specialized accessors
def find_item(self, slots: Set[int], seeked_item_id: int) -> Generator[Tuple[int, int, int, int, int], None, None]:
cdef ap_id_t item = seeked_item_id
cdef ap_player_t receiver
cdef std_set[ap_player_t] receivers
cdef size_t slot_count = len(slots)
if slot_count == 1:
# specialized implementation for single slot
receiver = list(slots)[0]
with nogil:
for entry in self.entries[:self.entry_count]:
if entry.item == item and entry.receiver == receiver:
with gil:
yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags
elif slot_count:
# generic implementation with lookup in set
for receiver in slots:
receivers.insert(receiver)
with nogil:
for entry in self.entries[:self.entry_count]:
if entry.item == item and receivers.count(entry.receiver):
with gil:
yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags
def get_for_player(self, slot: int) -> Dict[int, Set[int]]:
cdef ap_player_t receiver = slot
all_locations: Dict[int, Set[int]] = {}
with nogil:
for entry in self.entries[:self.entry_count]:
if entry.receiver == receiver:
with gil:
sender: int = entry.sender
if sender not in all_locations:
all_locations[sender] = set()
all_locations[sender].add(entry.location)
return all_locations
if TYPE_CHECKING:
State = Dict[Tuple[int, int], Set[int]]
else:
State = Union[Tuple[int, int], Set[int], defaultdict]
def get_checked(self, state: State, team: int, slot: int) -> List[int]:
# This used to validate checks actually exist. A remnant from the past.
# If the order of locations becomes relevant at some point, we could not do sorted(set), so leaving it.
cdef set checked = state[team, slot]
if not len(checked):
# Skips loop if none have been checked.
# This optimizes the case where everyone connects to a fresh game at the same time.
return []
# Unless the set is close to empty, it's cheaper to use the python set directly, so we do that.
cdef LocationEntry* entry
cdef ap_player_t sender = slot
cdef size_t start = self.sender_index[sender].start
cdef size_t count = self.sender_index[sender].count
return [entry.location for
entry in self.entries[start:start+count] if
entry.location in checked]
def get_missing(self, state: State, team: int, slot: int) -> List[int]:
cdef LocationEntry* entry
cdef ap_player_t sender = slot
cdef size_t start = self.sender_index[sender].start
cdef size_t count = self.sender_index[sender].count
cdef set checked = state[team, slot]
if not len(checked):
# Skip `in` if none have been checked.
# This optimizes the case where everyone connects to a fresh game at the same time.
return [entry.location for
entry in self.entries[start:start + count]]
else:
# Unless the set is close to empty, it's cheaper to use the python set directly, so we do that.
return [entry.location for
entry in self.entries[start:start + count] if
entry.location not in checked]
def get_remaining(self, state: State, team: int, slot: int) -> List[int]:
cdef LocationEntry* entry
cdef ap_player_t sender = slot
cdef size_t start = self.sender_index[sender].start
cdef size_t count = self.sender_index[sender].count
cdef set checked = state[team, slot]
return sorted([entry.item for
entry in self.entries[start:start+count] if
entry.location not in checked])
@cython.internal # unsafe. disable direct import
cdef class PlayerLocationProxy:
cdef LocationStore _store
cdef size_t _player
cdef object _len
def __init__(self, store: LocationStore, player: int) -> None:
self._store = store
self._player = player
self._len = self._store.sender_index[self._player].count
def __len__(self) -> int:
return self._store.sender_index[self._player].count
def __iter__(self) -> Generator[int, None, None]:
cdef LocationEntry* entry
cdef size_t i
cdef size_t off = self._store.sender_index[self._player].start
for i in range(self._store.sender_index[self._player].count):
entry = self._store.entries + off + i
yield entry.location
cdef LocationEntry* _get(self, ap_id_t loc):
# This requires locations to be sorted.
# This is always going to be slower than a pure python dict, because constructing the result tuple takes as long
# as the search in a python dict, which stores a pointer to an existing tuple.
cdef LocationEntry* entry = NULL
# binary search
cdef size_t l = self._store.sender_index[self._player].start
cdef size_t r = l + self._store.sender_index[self._player].count
cdef size_t m
while l < r:
m = (l + r) // 2
entry = self._store.entries + m
if entry.location < loc:
l = m + 1
else:
r = m
if entry: # count != 0
entry = self._store.entries + l
if entry.location == loc:
return entry
return NULL
def __getitem__(self, key: int) -> Tuple[int, int, int]:
cdef LocationEntry* entry = self._get(key)
if entry:
return entry.item, entry.receiver, entry.flags
raise KeyError(f"No location {key} for player {self._player}")
T = TypeVar('T')
def get(self, key: int, default: T) -> Union[Tuple[int, int, int], T]:
cdef LocationEntry* entry = self._get(key)
if entry:
return entry.item, entry.receiver, entry.flags
return default
def items(self) -> Generator[Tuple[int, Tuple[int, int, int]], None, None]:
cdef LocationEntry* entry
start = self._store.sender_index[self._player].start
count = self._store.sender_index[self._player].count
for entry in self._store.entries[start:start+count]:
yield entry.location, (entry.item, entry.receiver, entry.flags)

8
_speedups.pyxbld Normal file
View File

@@ -0,0 +1,8 @@
# This file is required to get pyximport to work with C++.
# Switching from std::set to a pure C implementation is still on the table to simplify everything.
def make_ext(modname, pyxfilename):
from distutils.extension import Extension
return Extension(name=modname,
sources=[pyxfilename],
language='c++')

View File

@@ -43,13 +43,13 @@
local socket = require("socket")
local udp = socket.socket.udp()
udp = socket.socket.udp()
require('common')
udp:setsockname('127.0.0.1', 55355)
udp:settimeout(0)
while true do
function on_vblank()
-- Attempt to lessen the CPU load by only polling the UDP socket every x frames.
-- x = 10 is entirely arbitrary, very little thought went into it.
-- We could try to make use of client.get_approx_framerate() here, but the values returned
@@ -112,6 +112,7 @@ while true do
for _, v in ipairs(mem) do
hex_string = hex_string .. string.format("%02X ", v)
end
hex_string = hex_string:sub(1, -2) -- Hang head in shame, remove last " "
local reply = string.format("%s %02x %s\n", command, address, hex_string)
udp:sendto(reply, msg_or_ip, port_or_nil)
@@ -135,6 +136,10 @@ while true do
udp:sendto(reply, msg_or_ip, port_or_nil)
end
end
emu.frameadvance()
end
event.onmemoryexecute(on_vblank, 0x40, "ap_connector_vblank")
while true do
emu.yield()
end

View File

@@ -46,10 +46,10 @@ function get_socket_path()
local pwd = (io.popen and io.popen("cd"):read'*l') or "."
return pwd .. "/" .. arch .. "/socket-" .. the_os .. "-" .. get_lua_version() .. "." .. ext
end
local lua_version = get_lua_version()
local socket_path = get_socket_path()
local socket = assert(package.loadlib(socket_path, "luaopen_socket_core"))()
local event = event
-- http://lua-users.org/wiki/ModulesTutorial
local M = {}
if setfenv then
@@ -59,6 +59,20 @@ else
end
M.socket = socket
-- Bizhawk <= 2.8 has an issue where resetting the lua doesn't close the socket
-- ...to get around this, we register an exit handler to close the socket first
if lua_version == '5-1' then
local old_udp = socket.udp
function udp(self)
s = old_udp(self)
function close_socket(self)
s:close()
end
event.onexit(close_socket)
return s
end
socket.udp = udp
end
-----------------------------------------------------------------------------
-- Exported auxiliar functions

166
docs/CODEOWNERS Normal file
View File

@@ -0,0 +1,166 @@
# Archipelago World Code Owners / Maintainers Document
#
# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder. For any pull
# requests that modify these worlds, a code owner must approve the PR in addition to a core maintainer. This is not to
# be used for files/folders outside the /worlds folder, those will always need sign off from a core maintainer.
#
# All usernames must be GitHub usernames (and are case sensitive).
###################
## Active Worlds ##
###################
# Adventure
/worlds/adventure/ @JusticePS
# A Link to the Past
/worlds/alttp/ @Berserker66
# ArchipIDLE
/worlds/archipidle/ @LegendaryLinux
# Sudoku (BK Sudoku)
/worlds/bk_sudoku/ @Jarno458
# Blasphemous
/worlds/blasphemous/ @TRPG0
# Bumper Stickers
/worlds/bumpstik/ @FelicitusNeko
# ChecksFinder
/worlds/checksfinder/ @jonloveslegos
# Clique
/worlds/clique/ @ThePhar
# Dark Souls III
/worlds/dark_souls_3/ @Marechal-L
# Donkey Kong Country 3
/worlds/dkc3/ @PoryGone
# DLCQuest
/worlds/dlcquest/ @axe-y @agilbert1412
# DOOM 1993
/worlds/doom_1993/ @Daivuk
# Factorio
/worlds/factorio/ @Berserker66
# Final Fantasy
/worlds/ff1/ @jtoyoda
# Hollow Knight
/worlds/hk/ @BadMagic100 @ThePhar
# Hylics 2
/worlds/hylics2/ @TRPG0
# Kingdom Hearts 2
/worlds/kh2/ @JaredWeakStrike
# Links Awakening DX
/worlds/ladx/ @zig-for
# Lufia II Ancient Cave
/worlds/lufia2ac/ @el-u
/worlds/lufia2ac/docs/ @wordfcuk @el-u
# Meritous
/worlds/meritous/ @FelicitusNeko
# The Messenger
/worlds/messenger/ @alwaysintreble
# Minecraft
/worlds/minecraft/ @KonoTyran @espeon65536
# MegaMan Battle Network 3
/worlds/mmbn3/ @digiholic
# Muse Dash
/worlds/musedash/ @DeamonHunter
# Noita
/worlds/noita/ @ScipioWright @heinermann
# Ocarina of Time
/worlds/oot/ @espeon65536
# Overcooked! 2
/worlds/overcooked2/ @toasterparty
# Pokemon Red and Blue
/worlds/pokemon_rb/ @Alchav
# Raft
/worlds/raft/ @SunnyBat
# Rogue Legacy
/worlds/rogue_legacy/ @ThePhar
# Risk of Rain 2
/worlds/ror2/ @kindasneaki
# Sonic Adventure 2 Battle
/worlds/sa2b/ @PoryGone @RaspberrySpace
# Starcraft 2 Wings of Liberty
/worlds/sc2wol/ @Ziktofel
# Super Metroid
/worlds/sm/ @lordlou
# Super Mario 64
/worlds/sm64ex/ @N00byKing
# Super Mario World
/worlds/smw/ @PoryGone
# SMZ3
/worlds/smz3/ @lordlou
# Secret of Evermore
/worlds/soe/ @black-sliver
# Slay the Spire
/worlds/spire/ @KonoTyran
# Stardew Valley
/worlds/stardew_valley/ @agilbert1412
# Subnautica
/worlds/subnautica/ @Berserker66
# Terraria
/worlds/terraria/ @Seldom-SE
# Timespinner
/worlds/timespinner/ @Jarno458
# The Legend of Zelda (1)
/worlds/tloz/ @Rosalie-A @t3hf1gm3nt
# Undertale
/worlds/undertale/ @jonloveslegos
# VVVVVV
/worlds/v6/ @N00byKing
# Wargroove
/worlds/wargroove/ @FlySniper
# The Witness
/worlds/witness/ @NewSoupVi @blastron
# Zillion
/worlds/zillion/ @beauxq
##################################
## Disabled Unmaintained Worlds ##
##################################
# Ori and the Blind Forest
# /worlds_disabled/oribf/ <Unmaintained>

View File

@@ -1,7 +1,7 @@
# apworld Specification
Archipelago depends on worlds to provide game-specific details like items, locations and output generation.
Those are located in the `worlds/` folder (source) or `<insall dir>/lib/worlds/` (when installed).
Those are located in the `worlds/` folder (source) or `<install dir>/lib/worlds/` (when installed).
See [world api.md](world%20api.md) for details.
apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld`

187
docs/settings api.md Normal file
View File

@@ -0,0 +1,187 @@
# Archipelago Settings API
The settings API describes how to use installation-wide config and let the user configure them, like paths, etc. using
host.yaml. For the player settings / player yamls see [options api.md](options api.md).
The settings API replaces `Utils.get_options()` and `Utils.get_default_options()`
as well as the predefined `host.yaml` in the repository.
For backwards compatibility with APWorlds, some interfaces are kept for now and will produce a warning when being used.
## Config File
Settings use options.yaml (manual override), if that exists, or host.yaml (the default) otherwise.
The files are searched for in the current working directory, if different from install directory, and in `user_path`,
which either points to the installation directory, if writable, or to %home%/Archipelago otherwise.
**Examples:**
* C:\Program Data\Archipelago\options.yaml
* C:\Program Data\Archipelago\host.yaml
* path\to\code\repository\host.yaml
* ~/Archipelago/host.yaml
Using the settings API, AP can update the config file or create a new one with default values and comments,
if it does not exist.
## Global Settings
All non-world-specific settings are defined directly in settings.py.
Each value needs to have a default. If the default should be `None`, define it as `typing.Optional` and assign `None`.
To access a "global" config value, with correct typing, use one of
```python
from settings import get_settings, GeneralOptions, FolderPath
from typing import cast
x = get_settings().general_options.output_path
y = cast(GeneralOptions, get_settings()["general_options"]).output_path
z = cast(FolderPath, get_settings()["general_options"]["output_path"])
```
## World Settings
Worlds can define the top level key to use by defining `settings_key: ClassVar[str]` in their World class.
It defaults to `{folder_name}_options` if undefined, i.e. `worlds/factorio/...` defaults to `factorio_options`.
Worlds define the layout of their config section using type annotation of the variable `settings` in the class.
The type has to inherit from `settings.Group`. Each value in the config can have a comment by subclassing a built-in
type. Some helper types are defined in `settings.py`, see [Types](#Types) for a list.```
Inside the class code, you can then simply use `self.settings.rom_file` to get the value.
In case of paths they will automatically be read as absolute file paths. No need to use user_path or local_path.
```python
import settings
from worlds.AutoWorld import World
class MyGameSettings(settings.Group):
class RomFile(settings.SNESRomPath):
"""Description that is put into host.yaml"""
description = "My Game US v1.0 ROM File" # displayed in the file browser
copy_to = "MyGame.sfc" # instead of storing the path, copy to AP dir
md5s = ["..."]
rom_file: RomFile = RomFile("MyGame.sfc") # definition and default value
class MyGameWorld(World):
...
settings: MyGameSettings
...
def something(self):
pass # use self.settings.rom_file here
```
## Types
When writing the host.yaml, the code will down cast the values to builtins.
When reading the host.yaml, the code will upcast the values to what is defined in the type annotations.
E.g. an IntEnum becomes int when saving and will construct the IntEnum when loading.
Types that can not be down cast to / up cast from a builtin can not be used except for Group, which will be converted
to/from a dict.
`bool` is a special case, see settings.py: ServerOptions.disable_item_cheat for an example.
Below are some predefined types that can be used if they match your requirements:
### Group
A section / dict in the config file. Behaves similar to a dataclass.
Type annotation and default assignment define how loading, saving and default values behave.
It can be accessed using attributes or as a dict: `group["a"]` is equivalent to `group.a`.
In worlds, this should only be used for the top level to avoid issues when upgrading/migrating.
### Bool
Since `bool` can not be subclassed, use the `settings.Bool` helper in a `typing.Union` to get a comment in host.yaml.
```python
import settings
import typing
class MySettings(settings.Group):
class MyBool(settings.Bool):
"""Doc string"""
my_value: typing.Union[MyBool, bool] = True
```
### UserFilePath
Path to a single file. Automatically resolves as user_path:
Source folder or AP install path on Windows. ~/Archipelago for the AppImage.
Will open a file browser if the file is missing when in GUI mode.
#### class method validate(cls, path: str)
Override this and raise ValueError if validation fails.
Checks the file against [md5s](#md5s) by default.
#### is_exe: bool
Resolves to an executable (varying file extension based on platform)
#### description: Optional\[str\]
Human-readable name to use in file browser
#### copy_to: Optional\[str\]
Instead of storing the path, copy the file.
#### md5s: List[Union[str, bytes]]
Provide md5 hashes as hex digests or raw bytes for automatic validation.
### UserFolderPath
Same as [UserFilePath](#UserFilePath), but for a folder instead of a file.
### LocalFilePath
Same as [UserFilePath](#UserFilePath), but resolves as local_path:
path inside the AP dir or Appimage even if read-only.
### LocalFolderPath
Same as [LocalFilePath](#LocalFilePath), but for a folder instead of a file.
### OptionalUserFilePath, OptionalUserFolderPath, OptionalLocalFilePath, OptionalLocalFolderPath
Same as UserFilePath, UserFolderPath, LocalFilePath, LocalFolderPath but does not open a file browser if missing.
### SNESRomPath
Specialized [UserFilePath](#UserFilePath) that ignores an optional 512 byte header when validating.
## Caveats
### Circular Imports
Because the settings are defined on import, code that runs on import can not use settings since that would result in
circular / partial imports. Instead, the code should fetch from settings on demand during generation.
"Global" settings are populated immediately, while worlds settings are lazy loaded, so if really necessary,
"global" settings could be used in global scope of worlds.
### APWorld Backwards Compatibility
APWorlds that want to be compatible with both stable and dev versions, have two options:
1. use the old Utils.get_options() API until Archipelago 0.4.2 is out
2. add some sort of compatibility code to your world that mimics the new API

View File

@@ -22,8 +22,8 @@ allows using WebSockets.
## Coding style
AP follows all the PEPs. When in doubt use an IDE with coding style
linter, for example PyCharm Community Edition.
AP follows [style.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/style.md).
When in doubt use an IDE with coding style linter, for example PyCharm Community Edition.
## Docstrings
@@ -44,7 +44,7 @@ class MyGameWorld(World):
## Definitions
This section will cover various classes and objects you can use for your world.
While some of the attributes and methods are mentioned here not all of them are,
While some of the attributes and methods are mentioned here, not all of them are,
but you can find them in `BaseClasses.py`.
### World Class
@@ -56,11 +56,12 @@ game.
### WebWorld Class
A `WebWorld` class contains specific attributes and methods that can be modified
for your world specifically on the webhost.
for your world specifically on the webhost:
`settings_page` which can be changed to a link instead of an AP generated settings page.
`settings_page`, which can be changed to a link instead of an AP generated settings page.
`theme` to be used for your game specific AP pages. Available themes:
| dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone |
|---|---|---|---|---|---|---|---|
| <img src="img/theme_dirt.JPG" width="100"> | <img src="img/theme_grass.JPG" width="100"> | <img src="img/theme_grassFlowers.JPG" width="100"> | <img src="img/theme_ice.JPG" width="100"> | <img src="img/theme_jungle.JPG" width="100"> | <img src="img/theme_ocean.JPG" width="100"> | <img src="img/theme_partyTime.JPG" width="100"> | <img src="img/theme_stone.JPG" width="100"> |
@@ -75,26 +76,30 @@ prefixed with the same string as defined here. Default already has 'en'.
### MultiWorld Object
The `MultiWorld` object references the whole multiworld (all items and locations
for all players) and is accessible through `self.world` inside a `World` object.
for all players) and is accessible through `self.multiworld` inside a `World` object.
### Player
The player is just an integer in AP and is accessible through `self.player`
inside a World object.
inside a `World` object.
### Player Options
Players provide customized settings for their World in the form of yamls.
Those are accessible through `self.world.<option_name>[self.player]`. A dict
Those are accessible through `self.multiworld.<option_name>[self.player]`. A dict
of valid options has to be provided in `self.option_definitions`. Options are automatically
added to the `World` object for easy access.
### World Options
### World Settings
Any AP installation can provide settings for a world, for example a ROM file,
accessible through `Utils.get_options()['<world>_options']['<option>']`.
Any AP installation can provide settings for a world, for example a ROM file, accessible through
`self.settings.<setting_name>` or `cls.settings.<setting_name>` (new API)
or `Utils.get_options()["<world>_options"]["<setting_name>"]` (deprecated).
Users can set those in their `host.yaml` file.
Users can set those in their `host.yaml` file. Some settings may automatically open a file browser if a file is missing.
Refer to [settings api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/settings%20api.md)
for details.
### Locations
@@ -132,10 +137,13 @@ same ID. Name must not be numeric (has to contain at least 1 letter or symbol).
Special items with ID `None` can mark events (read below).
Other classifications include
* filler: a regular item or trash item
* useful: generally quite useful, but not required for anything logical
* trap: negative impact on the player
* skip_balancing: add to progression to skip balancing; e.g. currency or tokens
* `filler`: a regular item or trash item
* `useful`: generally quite useful, but not required for anything logical
* `trap`: negative impact on the player
* `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be
combined with `progression`; see below)
* `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that
will not be moved around by progression balancing; used, e.g., for currency or tokens
### Events
@@ -159,10 +167,10 @@ or more event locations based on player options.
Regions are logical groups of locations that share some common access rules. If
location logic is written from scratch, using regions greatly simplifies the
definition and allow to somewhat easily implement things like entrance
definition and allows to somewhat easily implement things like entrance
randomizer in logic.
Regions have a list called `exits` which are `Entrance` objects representing
Regions have a list called `exits`, which are `Entrance` objects representing
transitions to other regions.
There has to be one special region "Menu" from which the logic unfolds. AP
@@ -179,7 +187,7 @@ They can be static (regular logic) or be defined/connected during generation
### Access Rules
An access rule is a function that returns `True` or `False` for a `Location` or
`Entrance` based on the the current `state` (items that can be collected).
`Entrance` based on the current `state` (items that can be collected).
### Item Rules
@@ -196,14 +204,14 @@ the `/worlds` directory. The starting point for the package is `__init__.py`.
Conventionally, your world class is placed in that file.
World classes must inherit from the `World` class in `/worlds/AutoWorld.py`,
which can be imported as `worlds.AutoWorld.World` from your package.
which can be imported as `from worlds.AutoWorld import World` from your package.
AP will pick up your world automatically due to the `AutoWorld` implementation.
### Requirements
If your world needs specific python packages, they can be listed in
`world/[world_name]/requirements.txt`. ModuleUpdate.py will automatically
`worlds/<world_name>/requirements.txt`. ModuleUpdate.py will automatically
pick up and install them.
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format).
@@ -214,7 +222,7 @@ AP will only import the `__init__.py`. Depending on code size it makes sense to
use multiple files and use relative imports to access them.
e.g. `from .Options import mygame_options` from your `__init__.py` will load
`world/[world_name]/Options.py` and make its `mygame_options` accesible.
`worlds/<world_name>/Options.py` and make its `mygame_options` accessible.
When imported names pile up it may be easier to use `from . import Options`
and access the variable as `Options.mygame_options`.
@@ -225,12 +233,12 @@ function, see [apworld specification.md](apworld%20specification.md).
### Your Item Type
Each world uses its own subclass of `BaseClasses.Item`. The constuctor can be
Each world uses its own subclass of `BaseClasses.Item`. The constructor can be
overridden to attach additional data to it, e.g. "price in shop".
Since the constructor is only ever called from your code, you can add whatever
arguments you like to the constructor.
In its simplest form we only set the game name and use the default constuctor
In its simplest form we only set the game name and use the default constructor
```python
from BaseClasses import Item
@@ -265,7 +273,7 @@ Each option has its own class, inherits from a base option type, has a docstring
to describe it and a `display_name` property for display on the website and in
spoiler logs.
The actual name as used in the yaml is defined in a `dict[str, Option]`, that is
The actual name as used in the yaml is defined in a `Dict[str, AssembleOptions]`, that is
assigned to the world under `self.option_definitions`.
Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`.
@@ -326,10 +334,10 @@ class FixXYZGlitch(Toggle):
display_name = "Fix XYZ Glitch"
# By convention we call the options dict variable `<world>_options`.
mygame_options: typing.Dict[str, type(Option)] = {
mygame_options: typing.Dict[str, AssembleOptions] = {
"difficulty": Difficulty,
"final_boss_hp": FinalBossHP,
"fix_xyz_glitch": FixXYZGlitch
"fix_xyz_glitch": FixXYZGlitch,
}
```
```python
@@ -349,27 +357,39 @@ class MyGameWorld(World):
```python
# world/mygame/__init__.py
import settings
import typing
from .Options import mygame_options # the options we defined earlier
from .Items import mygame_items # data used below to add items to the World
from .Locations import mygame_locations # same as above
from worlds.AutoWorld import World
from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification
from Utils import get_options, output_path
class MyGameItem(Item): # or from Items import MyGameItem
game = "My Game" # name of the game/world this item is from
class MyGameLocation(Location): # or from Locations import MyGameLocation
game = "My Game" # name of the game/world this location is in
class MyGameSettings(settings.Group):
class RomFile(settings.SNESRomPath):
"""Insert help text for host.yaml here."""
rom_file: RomFile = RomFile("MyGame.sfc")
class MyGameWorld(World):
"""Insert description of the world/game here."""
game = "My Game" # name of the game/world
option_definitions = mygame_options # options the player can set
settings: typing.ClassVar[MyGameSettings] # will be automatically assigned from type hint
topology_present = True # show path to required location checks in spoiler
# ID of first item and location, could be hard-coded but code may be easier
# to read with this as a propery.
# to read with this as a property.
base_id = 1234
# Instead of dynamic numbering, IDs could be part of data.
@@ -384,7 +404,7 @@ class MyGameWorld(World):
# Items can be grouped using their names to allow easy checking if any item
# from that group has been collected. Group names can also be used for !hint
item_name_groups = {
"weapons": {"sword", "lance"}
"weapons": {"sword", "lance"},
}
```
@@ -398,7 +418,7 @@ The world has to provide the following things for generation
* locations placed inside those regions
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
* applying `self.multiworld.push_precollected` for start inventory
* `required_client_version: Tuple(int, int, int)`
* `required_client_version: Tuple[int, int, int]`
Optional client version as tuple of 3 ints to make sure the client is compatible to
this world (e.g. implements all required features) when connecting.
@@ -496,30 +516,28 @@ def create_items(self) -> None:
def create_regions(self) -> None:
# Add regions to the multiworld. "Menu" is the required starting point.
# Arguments to Region() are name, player, world, and optionally hint_text
r = Region("Menu", self.player, self.multiworld)
# Set Region.exits to a list of entrances that are reachable from region
r.exits = [Entrance(self.player, "New game", r)] # or use r.exits.append
# Append region to MultiWorld's regions
self.multiworld.regions.append(r) # or use += [r...]
menu_region = Region("Menu", self.player, self.multiworld)
self.multiworld.regions.append(menu_region) # or use += [menu_region...]
r = Region("Main Area", self.player, self.multiworld)
main_region = Region("Main Area", self.player, self.multiworld)
# Add main area's locations to main area (all but final boss)
r.locations = [MyGameLocation(self.player, location.name,
self.location_name_to_id[location.name], r)]
r.exits = [Entrance(self.player, "Boss Door", r)]
self.multiworld.regions.append(r)
main_region.add_locations(main_region_locations, MyGameLocation)
# or
# main_region.locations = \
# [MyGameLocation(self.player, location_name, self.location_name_to_id[location_name], main_region]
self.multiworld.regions.append(main_region)
r = Region("Boss Room", self.player, self.multiworld)
# add event to Boss Room
r.locations = [MyGameLocation(self.player, "Final Boss", None, r)]
self.multiworld.regions.append(r)
# If entrances are not randomized, they should be connected here, otherwise
# they can also be connected at a later stage.
self.multiworld.get_entrance("New Game", self.player)
.connect(self.multiworld.get_region("Main Area", self.player))
self.multiworld.get_entrance("Boss Door", self.player)
.connect(self.multiworld.get_region("Boss Room", self.player))
boss_region = Region("Boss Room", self.player, self.multiworld)
# Add event to Boss Room
boss_region.locations.append(MyGameLocation(self.player, "Final Boss", None, boss_region))
# If entrances are not randomized, they should be connected here,
# otherwise they can also be connected at a later stage.
# Create Entrances and connect the Regions
menu_region.connect(main_region) # connects the "Menu" and "Main Area", can also pass a rule
# or
main_region.add_exits({"Boss Room": "Boss Door"}, {"Boss Room": lambda state: state.has("Sword", self.player)})
# Connects the "Main Area" and "Boss Room" regions, and places a rule requiring the "Sword" item to traverse
# If setting location access rules from data is easier here, set_rules can
# possibly omitted.
@@ -573,7 +591,7 @@ def set_rules(self) -> None:
# require one item from an item group
add_rule(self.multiworld.get_location("Chest3", self.player),
lambda state: state.has_group("weapons", self.player))
# state also has .item_count() for items, .has_any() and.has_all() for sets
# state also has .item_count() for items, .has_any() and .has_all() for sets
# and .count_group() for groups
# set_rule is likely to be a bit faster than add_rule
@@ -611,7 +629,7 @@ public members with `mygame_`.
More advanced uses could be to add additional variables to the state object,
override `World.collect(self, state, item)` and `remove(self, state, item)`
to update the state object, and check those added variables in added methods.
Please do this with caution and only when neccessary.
Please do this with caution and only when necessary.
#### Sample
@@ -623,7 +641,7 @@ from worlds.AutoWorld import LogicMixin
class MyGameLogic(LogicMixin):
def mygame_has_key(self, player: int):
# Arguments above are free to choose
# MultiWorld can be accessed through self.world, explicitly passing in
# MultiWorld can be accessed through self.multiworld, explicitly passing in
# MyGameWorld instance for easy options access is also a valid approach
return self.has("key", player) # or whatever
```
@@ -636,8 +654,8 @@ import .Logic # apply the mixin by importing its file
class MyGameWorld(World):
# ...
def set_rules(self):
set_rule(self.world.get_location("A Door", self.player),
lamda state: state.mygame_has_key(self.player))
set_rule(self.multiworld.get_location("A Door", self.player),
lambda state: state.mygame_has_key(self.player))
```
### Generate Output
@@ -665,14 +683,14 @@ def generate_output(self, output_directory: str):
# store option name "easy", "normal" or "hard" for difficuly
"difficulty": self.multiworld.difficulty[self.player].current_key,
# store option value True or False for fixing a glitch
"fix_xyz_glitch": self.multiworld.fix_xyz_glitch[self.player].value
"fix_xyz_glitch": self.multiworld.fix_xyz_glitch[self.player].value,
}
# point to a ROM specified by the installation
src = Utils.get_options()["mygame_options"]["rom_file"]
src = self.settings.rom_file
# or point to worlds/mygame/data/mod_template
src = os.path.join(os.path.dirname(__file__), "data", "mod_template")
# generate output path
mod_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.player_name[self.player]}"
mod_name = self.multiworld.get_out_file_name_base(self.player)
out_file = os.path.join(output_directory, mod_name + ".zip")
# generate the file
generate_mod(src, out_file, data)
@@ -721,14 +739,14 @@ from . import MyGameTestBase
class TestChestAccess(MyGameTestBase):
def testSwordChests(self):
def test_sword_chests(self):
"""Test locations that require a sword"""
locations = ["Chest1", "Chest2"]
items = [["Sword"]]
# this will test that each location can't be accessed without the "Sword", but can be accessed once obtained.
self.assertAccessDependency(locations, items)
def testAnyWeaponChests(self):
def test_any_weapon_chests(self):
"""Test locations that require any weapon"""
locations = [f"Chest{i}" for i in range(3, 6)]
items = [["Sword"], ["Axe"], ["Spear"]]

View File

@@ -5,6 +5,7 @@ A world maintainer is a person responsible for a world or part of a world in Arc
If a world author does not want to take on the responsibilities of a world maintainer, they can release their world as
an unofficial [APWorld](/docs/apworld%20specification.md) or maintain their own fork instead.
All current world maintainers are listed in the [CODEOWNERS](/docs/CODEOWNERS) document.
## Responsibilities
@@ -18,15 +19,15 @@ Unless these are shared between multiple people, we expect the following from ea
pull requests. Core maintainers may also ping you if a pull request concerns your world.
* Test (or have tested) the world on the main branch from time to time, especially during RC (release candidate) phases
of development.
* Let us know of long unavailabilities.
* Let us know of long periods of unavailability.
## Becoming a World Maintainer
### Adding a World
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you
nominate someone else (i.e. there are multiple devs).
nominate someone else (i.e. there are multiple devs). You can define who is allowed to approve changes to your world
in the [CODEOWNERS](/docs/CODEOWNERS) document.
### Getting Voted
@@ -36,12 +37,12 @@ For a vote to pass, the majority of participating core maintainers must vote in
The time limit is 1 week, but can end early if the majority is reached earlier.
Voting shall be conducted on Discord in #archipelago-dev.
## Dropping out
### Resigning
A world maintainer can resign. If no new maintainer steps up and gets voted, the world becomes unmaintained.
A world maintainer can resign and have their username removed from the [CODEOWNERS](/docs/CODEOWNERS) document. If no
new maintainer takes over management of the world, the world becomes unmaintained.
### Getting Voted out
@@ -53,7 +54,6 @@ made their case or was pinged and has been unreachable for more than 2 weeks alr
Voting shall be conducted on Discord in #archipelago-dev. Commits that are a direct result of the voting shall include
date, voting members and final result in the commit message.
## Handling of Unmaintained Worlds
As long as worlds are known to work for the most part, they can stay included. Once a world becomes broken it shall be

190
host.yaml
View File

@@ -1,190 +0,0 @@
general_options:
# Where to place output files
output_path: "output"
# Options for MultiServer
# Null means nothing, for the server this means to default the value
# These overwrite command line arguments!
server_options:
host: null
port: 38281
password: null
multidata: null
savefile: null
disable_save: false
loglevel: "info"
# Allows for clients to log on and manage the server. If this is null, no remote administration is possible.
server_password: null
# Disallow !getitem.
disable_item_cheat: false
# Client hint system
# Points given to a player for each acquired item in their world
location_check_points: 1
# Relative point cost to receive a hint via !hint for players
# so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint, for a total of 5
hint_cost: 10 # Set to 0 if you want free hints
# Release modes
# A Release sends out the remaining items *from* a world that releases
# "disabled" -> clients can't release,
# "enabled" -> clients can always release
# "auto" -> automatic release on goal completion
# "auto-enabled" -> automatic release on goal completion and manual release is also enabled
# "goal" -> release is allowed after goal completion
release_mode: "goal"
# Collect modes
# A Collect sends the remaining items *to* a world that collects
# "disabled" -> clients can't collect,
# "enabled" -> clients can always collect
# "auto" -> automatic collect on goal completion
# "auto-enabled" -> automatic collect on goal completion and manual collect is also enabled
# "goal" -> collect is allowed after goal completion
collect_mode: "goal"
# Remaining modes
# !remaining handling, that tells a client which items remain in their pool
# "enabled" -> Client can always ask for remaining items
# "disabled" -> Client can never ask for remaining items
# "goal" -> Client can ask for remaining items after goal completion
remaining_mode: "goal"
# Automatically shut down the server after this many seconds without new location checks, 0 to keep running
auto_shutdown: 0
# Compatibility handling
# 2 -> Recommended for casual/cooperative play, attempt to be compatible with everything across all versions
# 1 -> No longer in use, kept reserved in case of future use
# 0 -> Recommended for tournaments to force a level playing field, only allow an exact version match
compatibility: 2
# log all server traffic, mostly for dev use
log_network: 0
# Options for Generation
generator:
# Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases
enemizer_path: "EnemizerCLI/EnemizerCLI.Core" # + ".exe" is implied on Windows
# Folder from which the player yaml files are pulled from
player_files_path: "Players"
#amount of players, 0 to infer from player files
players: 0
# general weights file, within the stated player_files_path location
# gets used if players is higher than the amount of per-player files found to fill remaining slots
weights_file_path: "weights.yaml"
# Meta file name, within the stated player_files_path location
meta_file_path: "meta.yaml"
# Create a spoiler file
# 0 -> None
# 1 -> Spoiler without playthrough or paths to playthrough required items
# 2 -> Spoiler with playthrough (viable solution to goals)
# 3 -> Spoiler with playthrough and traversal paths towards items
spoiler: 3
# Glitch to Triforce room from Ganon
# When disabled, you have to have a weapon that can hurt ganon (master sword or swordless/easy item functionality + hammer)
# and have completed the goal required for killing ganon to be able to access the triforce room.
# 1 -> Enabled.
# 0 -> Disabled (except in no-logic)
glitch_triforce_room: 1
# Create encrypted race roms and flag games as race mode
race: 0
# List of options that can be plando'd. Can be combined, for example "bosses, items"
# Available options: bosses, items, texts, connections
plando_options: "bosses"
sni_options:
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
sni_path: "SNI"
# Set this to false to never autostart a rom (such as after patching)
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with
snes_rom_start: true
lttp_options:
# File name of the v1.0 J rom
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
ladx_options:
# File name of the Link's Awakening DX rom
rom_file: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"
lufia2ac_options:
# File name of the US rom
rom_file: "Lufia II - Rise of the Sinistrals (USA).sfc"
sm_options:
# File name of the v1.0 J rom
rom_file: "Super Metroid (JU).sfc"
factorio_options:
executable: "factorio/bin/x64/factorio"
# by default, no settings are loaded if this file does not exist. If this file does exist, then it will be used.
# server_settings: "factorio\\data\\server-settings.json"
# Whether to filter item send messages displayed in-game to only those that involve you.
filter_item_sends: false
# Whether to send chat messages from players on the Factorio server to Archipelago.
bridge_chat_out: true
minecraft_options:
forge_directory: "Minecraft Forge server"
max_heap_size: "2G"
# release channel, currently "release", or "beta"
# any games played on the "beta" channel have a high likelihood of no longer working on the "release" channel.
release_channel: "release"
oot_options:
# File name of the OoT v1.0 ROM
rom_file: "The Legend of Zelda - Ocarina of Time.z64"
# Set this to false to never autostart a rom (such as after patching)
# true for operating system default program
# Alternatively, a path to a program to open the .z64 file with
rom_start: true
soe_options:
# File name of the SoE US ROM
rom_file: "Secret of Evermore (USA).sfc"
ffr_options:
display_msgs: true
tloz_options:
# File name of the Zelda 1
rom_file: "Legend of Zelda, The (U) (PRG0) [!].nes"
# Set this to false to never autostart a rom (such as after patching)
# true for operating system default program
# Alternatively, a path to a program to open the .nes file with
rom_start: true
# Display message inside of EmuHawk
display_msgs: true
dkc3_options:
# File name of the DKC3 US rom
rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
smw_options:
# File name of the SMW US rom
rom_file: "Super Mario World (USA).sfc"
pokemon_rb_options:
# File names of the Pokemon Red and Blue roms
red_rom_file: "Pokemon Red (UE) [S][!].gb"
blue_rom_file: "Pokemon Blue (UE) [S][!].gb"
# Set this to false to never autostart a rom (such as after patching)
# True for operating system default program
# Alternatively, a path to a program to open the .gb file with
rom_start: true
wargroove_options:
# Locate the Wargroove root directory on your system.
# This is used by the Wargroove client, so it knows where to send communication files to
root_directory: "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
zillion_options:
# File name of the Zillion US rom
rom_file: "Zillion (UE) [!].sms"
# Set this to false to never autostart a rom (such as after patching)
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with
# RetroArch doesn't make it easy to launch a game from the command line.
# You have to know the path to the emulator core library on the user's computer.
rom_start: "retroarch"
mmbn3_options:
# File name of the MMBN3 Blue US rom
rom_file: "Mega Man Battle Network 3 - Blue Version (USA).gba"
rom_start: true
adventure_options:
# File name of the standard NTSC Adventure rom.
# The licensed "The 80 Classic Games" CD-ROM contains this.
# It may also have a .a26 extension
rom_file: "ADVNTURE.BIN"
# Set this to false to never autostart a rom (such as after patching)
# True for operating system default program for '.a26'
# Alternatively, a path to a program to open the .a26 file with (generally EmuHawk for multiworld)
rom_start: true
# Optional, additional args passed into rom_start before the .bin file
# For example, this can be used to autoload the connector script in EmuHawk
# (see EmuHawk --lua= option)
# Windows example:
# rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/connector_adventure.lua"
rom_args: " "
# Set this to true to display item received messages in Emuhawk
display_msgs: true

View File

@@ -2,10 +2,10 @@
#define min_windows ReadIni(SourcePath + "\setup.ini", "Data", "min_windows")
#define MyAppName "Archipelago"
#define MyAppExeName "ArchipelagoServer.exe"
#define MyAppExeName "ArchipelagoLauncher.exe"
#define MyAppIcon "data/icon.ico"
#dim VersionTuple[4]
#define MyAppVersion GetVersionComponents(source_path + '\ArchipelagoServer.exe', VersionTuple[0], VersionTuple[1], VersionTuple[2], VersionTuple[3])
#define MyAppVersion GetVersionComponents(source_path + '\ArchipelagoLauncher.exe', VersionTuple[0], VersionTuple[1], VersionTuple[2], VersionTuple[3])
#define MyAppVersionText Str(VersionTuple[0])+"."+Str(VersionTuple[1])+"."+Str(VersionTuple[2])
@@ -141,7 +141,8 @@ Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
[Icons]
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server
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} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
@@ -155,12 +156,14 @@ Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoSta
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} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
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} 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
@@ -174,6 +177,7 @@ Name: "{commondesktop}\{#MyAppName} MegaMan Battle Network 3 Client"; Filename:
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
@@ -182,6 +186,8 @@ Name: "{commondesktop}\{#MyAppName} Undertale Client"; Filename: "{app}\Archipel
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}\ArchipelagoLauncher"; Parameters: "--update_settings"; StatusMsg: "Updating host.yaml..."; Flags: runasoriginaluser runhidden
Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringChange('Launcher', '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[UninstallDelete]
Type: dirifempty; Name: "{app}"

View File

@@ -1,10 +1,12 @@
colorama>=0.4.5
websockets>=11.0.3
PyYAML>=6.0
jellyfish>=0.11.2
PyYAML>=6.0.1
jellyfish>=1.0.0
jinja2>=3.1.2
schema>=0.7.5
kivy>=2.2.0
bsdiff4>=1.2.3
platformdirs>=3.5.1
certifi>=2023.5.7
platformdirs>=3.9.1
certifi>=2023.7.22
cython>=0.29.35
cymem>=2.0.7

836
settings.py Normal file
View File

@@ -0,0 +1,836 @@
"""
Application settings / host.yaml interface using type hints.
This is different from player settings.
"""
import os.path
import shutil
import sys
import typing
import warnings
from enum import IntEnum
from threading import Lock
from typing import cast, Any, BinaryIO, ClassVar, Dict, Iterator, List, Optional, TextIO, Tuple, Union, TypeVar
import os
__all__ = [
"get_settings", "fmt_doc", "no_gui",
"Group", "Bool", "Path", "UserFilePath", "UserFolderPath", "LocalFilePath", "LocalFolderPath",
"OptionalUserFilePath", "OptionalUserFolderPath", "OptionalLocalFilePath", "OptionalLocalFolderPath",
"GeneralOptions", "ServerOptions", "GeneratorOptions", "SNIOptions", "Settings"
]
no_gui = False
skip_autosave = False
_world_settings_name_cache: Dict[str, str] = {} # TODO: cache on disk and update when worlds change
_world_settings_name_cache_updated = False
_lock = Lock()
def _update_cache() -> None:
"""Load all worlds and update world_settings_name_cache"""
global _world_settings_name_cache_updated
if _world_settings_name_cache_updated:
return
try:
from worlds.AutoWorld import AutoWorldRegister
for world in AutoWorldRegister.world_types.values():
annotation = world.__annotations__.get("settings", None)
if annotation is None or annotation == "ClassVar[Optional['Group']]":
continue
_world_settings_name_cache[world.settings_key] = f"{world.__module__}.{world.__name__}"
finally:
_world_settings_name_cache_updated = True
def fmt_doc(cls: type, level: int) -> str:
comment = cls.__doc__
assert comment, f"{cls} has no __doc__"
indent = level * 2 * " "
return "\n".join(map(lambda s: f"{indent}# {s}", filter(None, map(lambda s: s.strip(), comment.split("\n")))))
class Group:
_type_cache: ClassVar[Optional[Dict[str, Any]]] = None
_dumping: bool = False
_has_attr: bool = False
_changed: bool = False
_dumper: ClassVar[type]
def __getitem__(self, key: str) -> Any:
try:
return getattr(self, key)
except NameError:
raise KeyError(key)
def __iter__(self) -> Iterator[str]:
cls_members = dir(self.__class__)
members = filter(lambda k: not k.startswith("_") and (k not in cls_members or k in self.__annotations__),
list(self.__annotations__) +
[name for name in dir(self) if name not in self.__annotations__])
return members.__iter__()
def __contains__(self, key: str) -> bool:
try:
self._has_attr = True
return hasattr(self, key)
finally:
self._has_attr = False
def __setitem__(self, key: str, value: Any) -> None:
setattr(self, key, value)
def __getattribute__(self, item: str) -> Any:
attr = super().__getattribute__(item)
if isinstance(attr, Path) and not super().__getattribute__("_dumping"):
if attr.required and not attr.exists() and not super().__getattribute__("_has_attr"):
# if a file is required, and the one from settings does not exist, ask the user to provide it
# unless we are dumping the settings, because that would ask for each entry
with _lock: # lock to avoid opening multiple
new = None if no_gui else attr.browse()
if new is None:
raise FileNotFoundError(f"{attr} does not exist, but "
f"{self.__class__.__name__}.{item} is required")
setattr(self, item, new)
self._changed = True
attr = new
# resolve the path immediately when accessing it
return attr.__class__(attr.resolve())
return attr
@property
def changed(self) -> bool:
return self._changed or any(map(lambda v: isinstance(v, Group) and v.changed,
self.__dict__.values()))
@classmethod
def get_type_hints(cls) -> Dict[str, Any]:
"""Returns resolved type hints for the class"""
if cls._type_cache is None:
if not isinstance(next(iter(cls.__annotations__.values())), str):
# non-str: assume already resolved
cls._type_cache = cls.__annotations__
else:
# str: build dicts and resolve with eval
mod = sys.modules[cls.__module__] # assume the module wasn't deleted
mod_dict = {k: getattr(mod, k) for k in dir(mod)}
cls._type_cache = typing.get_type_hints(cls, globalns=mod_dict, localns=cls.__dict__)
return cls._type_cache
def get(self, key: str, default: Any) -> Any:
if key in self:
return self[key]
return default
def items(self) -> List[Tuple[str, Any]]:
return [(key, getattr(self, key)) for key in self]
def update(self, dct: Dict[str, Any]) -> None:
assert isinstance(dct, dict), f"{self.__class__.__name__}.update called with " \
f"{dct.__class__.__name__} instead of dict."
for k in self.__annotations__:
if not k.startswith("_") and k not in dct:
self._changed = True # key missing from host.yaml
for k, v in dct.items():
# don't do getattr to stay lazy with world group init/loading
# instead we assign unknown groups as dicts and a later getattr will upcast them
attr = self.__dict__[k] if k in self.__dict__ else \
self.__class__.__dict__[k] if k in self.__class__.__dict__ else None
if isinstance(attr, Group):
# update group
if k not in self.__dict__:
attr = attr.__class__() # make a copy of default
setattr(self, k, attr)
if isinstance(v, dict):
attr.update(v)
else:
warnings.warn(f"{self.__class__.__name__}.{k} "
f"tried to update Group from {type(v)}")
elif isinstance(attr, dict):
# update dict
if k not in self.__dict__:
attr = attr.copy() # make a copy of default
setattr(self, k, attr)
if isinstance(v, dict):
attr.update(v)
else:
warnings.warn(f"{self.__class__.__name__}.{k} "
f"tried to update dict from {type(v)}")
else:
# assign value, try to upcast to type hint
annotation = self.get_type_hints().get(k, None)
candidates = [] if annotation is None else \
typing.get_args(annotation) if typing.get_origin(annotation) is Union else [annotation]
none_type = type(None)
for cls in candidates:
assert isinstance(cls, type), f"{self.__class__.__name__}.{k}: type {cls} not supported in settings"
if v is None and cls is none_type:
# assign None, i.e. from Optional
setattr(self, k, v)
break
if cls is bool and isinstance(v, bool):
# assign bool - special handling because issubclass(int, bool) is True
setattr(self, k, v)
break
if cls is not bool and issubclass(cls, type(v)):
# upcast, i.e. int -> IntEnum, str -> Path
setattr(self, k, cls.__call__(v))
break
if issubclass(cls, (tuple, set)) and isinstance(v, list):
# convert or upcast from list
setattr(self, k, cls.__call__(v))
break
else:
# assign scalar and hope for the best
setattr(self, k, v)
if annotation:
warnings.warn(f"{self.__class__.__name__}.{k} "
f"assigned from incompatible type {type(v).__name__}")
def as_dict(self, *args: str, downcast: bool = True) -> Dict[str, Any]:
return {
name: _to_builtin(cast(object, getattr(self, name))) if downcast else getattr(self, name)
for name in self if not args or name in args
}
@classmethod
def _dump_value(cls, value: Any, f: TextIO, indent: str) -> None:
"""Write a single yaml line to f"""
from Utils import dump, Dumper as BaseDumper
yaml_line: str = dump(value, Dumper=cast(BaseDumper, cls._dumper))
assert yaml_line.count("\n") == 1, f"Unexpected input for yaml dumper: {value}"
f.write(f"{indent}{yaml_line}")
@classmethod
def _dump_item(cls, name: Optional[str], attr: object, f: TextIO, level: int) -> None:
"""Write a group, dict or sequence item to f, where attr can be a scalar or a collection"""
# lazy construction of yaml Dumper to avoid loading Utils early
from Utils import Dumper as BaseDumper
from yaml import ScalarNode, MappingNode
if not hasattr(cls, "_dumper"):
if cls is Group or not hasattr(Group, "_dumper"):
class Dumper(BaseDumper):
def represent_mapping(self, tag: str, mapping: Any, flow_style: Any = None) -> MappingNode:
from yaml import ScalarNode
res: MappingNode = super().represent_mapping(tag, mapping, flow_style)
pairs = cast(List[Tuple[ScalarNode, Any]], res.value)
for k, v in pairs:
k.style = None # remove quotes from keys
return res
def represent_str(self, data: str) -> ScalarNode:
# default double quote all strings
return self.represent_scalar("tag:yaml.org,2002:str", data, style='"')
Dumper.add_representer(str, Dumper.represent_str)
Group._dumper = Dumper
if cls is not Group:
cls._dumper = Group._dumper
indent = " " * level
start = f"{indent}-\n" if name is None else f"{indent}{name}:\n"
if isinstance(attr, Group):
# handle group
f.write(start)
attr.dump(f, level=level+1)
elif isinstance(attr, (list, tuple, set)) and attr:
# handle non-empty sequence; empty use one-line [] syntax
f.write(start)
for value in attr:
cls._dump_item(None, value, f, level=level + 1)
elif isinstance(attr, dict) and attr:
# handle non-empty dict; empty use one-line {} syntax
f.write(start)
for dict_key, value in attr.items():
# not dumping doc string here, since there is no way to upcast it after dumping
assert dict_key is not None, "Key None is reserved for sequences"
cls._dump_item(dict_key, value, f, level=level + 1)
else:
# dump scalar or empty sequence or mapping item
line = [_to_builtin(attr)] if name is None else {name: _to_builtin(attr)}
cls._dump_value(line, f, indent=indent)
def dump(self, f: TextIO, level: int = 0) -> None:
"""Dump Group to stream f at given indentation level"""
# There is no easy way to generate extra lines into default yaml output,
# so we format part of it by hand using an odd recursion here and in _dump_*.
self._dumping = True
try:
# fetch class to avoid going through getattr
cls = self.__class__
type_hints = cls.get_type_hints()
# validate group
for name in cls.__annotations__.keys():
assert hasattr(cls, name), f"{cls}.{name} is missing a default value"
# dump ordered members
for name in self:
attr = cast(object, getattr(self, name))
attr_cls = type_hints[name] if name in type_hints else attr.__class__
attr_cls_origin = typing.get_origin(attr_cls)
while attr_cls_origin is Union: # resolve to first type for doc string
attr_cls = typing.get_args(attr_cls)[0]
attr_cls_origin = typing.get_origin(attr_cls)
if attr_cls.__doc__ and attr_cls.__module__ != "builtins":
f.write(fmt_doc(attr_cls, level=level) + "\n")
self._dump_item(name, attr, f, level=level)
self._changed = False
finally:
self._dumping = False
class Bool:
# can't subclass bool, so we use this and Union or type: ignore
def __bool__(self) -> bool:
raise NotImplementedError()
# Types for generic settings
T = TypeVar("T", bound="Path")
def _resolve_exe(s: str) -> str:
"""Append exe file extension if the file is an executable"""
if isinstance(s, Path):
from Utils import is_windows
if s.is_exe and is_windows and not s.lower().endswith(".exe"):
return str(s + ".exe")
return str(s)
def _to_builtin(o: object) -> Any:
"""Downcast object to a builtin type for output"""
if o is None:
return None
c = o.__class__
while c.__module__ != "builtins":
c = c.__base__
return c.__call__(o)
class Path(str):
# paths in host.yaml are str
required: bool = True
"""Marks the file as required and opens a file browser when missing"""
is_exe: bool = False
"""Special cross-platform handling for executables"""
description: Optional[str] = None
"""Title to display when browsing for the file"""
copy_to: Optional[str] = None
"""If not None, copy to AP folder instead of linking it"""
@classmethod
def validate(cls, path: str) -> None:
"""Overload and raise to validate input files from browse"""
pass
def browse(self: T, **kwargs: Any) -> Optional[T]:
"""Opens a file browser to search for the file"""
raise NotImplementedError(f"Please use a subclass of Path for {self.__class__.__name__}")
def resolve(self) -> str:
return _resolve_exe(self)
def exists(self) -> bool:
return os.path.exists(self.resolve())
class _UserPath(str):
def resolve(self) -> str:
if os.path.isabs(self):
return str(self)
from Utils import user_path
return user_path(_resolve_exe(self))
class _LocalPath(str):
def resolve(self) -> str:
if os.path.isabs(self):
return str(self)
from Utils import local_path
return local_path(_resolve_exe(self))
class FilePath(Path):
# path to a file
md5s: ClassVar[List[Union[str, bytes]]] = []
"""MD5 hashes for default validator."""
def browse(self: T,
filetypes: Optional[typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]] = None, **kwargs: Any)\
-> Optional[T]:
from Utils import open_filename, is_windows
if not filetypes:
if self.is_exe:
name, ext = "Program", ".exe" if is_windows else ""
else:
ext = os.path.splitext(self)[1]
name = ext[1:] if ext else "File"
filetypes = [(name, [ext])]
res = open_filename(f"Select {self.description or self.__class__.__name__}", filetypes, self)
if res:
self.validate(res)
if self.copy_to:
# instead of linking the file, copy it
dst = self.__class__(self.copy_to).resolve()
shutil.copy(res, dst, follow_symlinks=True)
res = dst
try:
rel = os.path.relpath(res, self.__class__("").resolve())
if not rel.startswith(".."):
res = rel
except ValueError:
pass
return self.__class__(res)
return None
@classmethod
def _validate_stream_hashes(cls, f: BinaryIO) -> None:
"""Helper to efficiently validate stream against hashes"""
if not cls.md5s:
return # no hashes to validate against
pos = f.tell()
try:
from hashlib import md5
file_md5 = md5()
block = bytearray(64*1024)
view = memoryview(block)
while n := f.readinto(view): # type: ignore
file_md5.update(view[:n])
file_md5_hex = file_md5.hexdigest()
for valid_md5 in cls.md5s:
if isinstance(valid_md5, str):
if valid_md5.lower() == file_md5_hex:
break
elif valid_md5 == file_md5.digest():
break
else:
raise ValueError(f"Hashes do not match for {cls.__name__}")
finally:
f.seek(pos)
@classmethod
def validate(cls, path: str) -> None:
"""Try to open and validate file against hashes"""
with open(path, "rb", buffering=0) as f:
try:
cls._validate_stream_hashes(f)
except ValueError:
raise ValueError(f"File hash does not match for {path}")
class FolderPath(Path):
# path to a folder
def browse(self: T, **kwargs: Any) -> Optional[T]:
from Utils import open_directory
res = open_directory(f"Select {self.description or self.__class__.__name__}", self)
if res:
try:
rel = os.path.relpath(res, self.__class__("").resolve())
if not rel.startswith(".."):
res = rel
except ValueError:
pass
return self.__class__(res)
return None
class UserFilePath(_UserPath, FilePath):
pass
class UserFolderPath(_UserPath, FolderPath):
pass
class OptionalUserFilePath(UserFilePath):
required = False
class OptionalUserFolderPath(UserFolderPath):
required = False
class LocalFilePath(_LocalPath, FilePath):
pass
class LocalFolderPath(_LocalPath, FolderPath):
pass
class OptionalLocalFilePath(LocalFilePath):
required = False
class OptionalLocalFolderPath(LocalFolderPath):
required = False
class SNESRomPath(UserFilePath):
# Special UserFilePath that ignores an optional header when validating
@classmethod
def validate(cls, path: str) -> None:
"""Try to open and validate file against hashes"""
with open(path, "rb", buffering=0) as f:
f.seek(0, os.SEEK_END)
size = f.tell()
if size % 1024 == 512:
f.seek(512) # skip header
elif size % 1024 == 0:
f.seek(0) # header-less
else:
raise ValueError(f"Unexpected file size for {path}")
try:
cls._validate_stream_hashes(f)
except ValueError:
raise ValueError(f"File hash does not match for {path}")
# World-independent setting groups
class GeneralOptions(Group):
class OutputPath(OptionalUserFolderPath):
"""
Where to place output files
"""
# created on demand, so marked as optional
output_path: OutputPath = OutputPath("output")
class ServerOptions(Group):
"""
Options for MultiServer
Null means nothing, for the server this means to default the value
These overwrite command line arguments!
"""
class ServerPassword(str):
"""
Allows for clients to log on and manage the server. If this is null, no remote administration is possible.
"""
class DisableItemCheat(Bool):
"""Disallow !getitem"""
class LocationCheckPoints(int):
"""
Client hint system
Points given to a player for each acquired item in their world
"""
class HintCost(int):
"""
Relative point cost to receive a hint via !hint for players
so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint,
for a total of 5
"""
class ReleaseMode(str):
"""
Release modes
A Release sends out the remaining items *from* a world that releases
"disabled" -> clients can't release,
"enabled" -> clients can always release
"auto" -> automatic release on goal completion
"auto-enabled" -> automatic release on goal completion and manual release is also enabled
"goal" -> release is allowed after goal completion
"""
class CollectMode(str):
"""
Collect modes
A Collect sends the remaining items *to* a world that collects
"disabled" -> clients can't collect,
"enabled" -> clients can always collect
"auto" -> automatic collect on goal completion
"auto-enabled" -> automatic collect on goal completion and manual collect is also enabled
"goal" -> collect is allowed after goal completion
"""
class RemainingMode(str):
"""
Remaining modes
!remaining handling, that tells a client which items remain in their pool
"enabled" -> Client can always ask for remaining items
"disabled" -> Client can never ask for remaining items
"goal" -> Client can ask for remaining items after goal completion
"""
class AutoShutdown(int):
"""Automatically shut down the server after this many seconds without new location checks, 0 to keep running"""
class Compatibility(IntEnum):
"""
Compatibility handling
2 -> Recommended for casual/cooperative play, attempt to be compatible with everything across all versions
1 -> No longer in use, kept reserved in case of future use
0 -> Recommended for tournaments to force a level playing field, only allow an exact version match
"""
OFF = 0
ON = 1
FULL = 2
class LogNetwork(IntEnum):
"""log all server traffic, mostly for dev use"""
OFF = 0
ON = 1
host: Optional[str] = None
port: int = 38281
password: Optional[str] = None
multidata: Optional[str] = None
savefile: Optional[str] = None
disable_save: bool = False
loglevel: str = "info"
server_password: Optional[ServerPassword] = None
disable_item_cheat: Union[DisableItemCheat, bool] = False
location_check_points: LocationCheckPoints = LocationCheckPoints(1)
hint_cost: HintCost = HintCost(10)
release_mode: ReleaseMode = ReleaseMode("goal")
collect_mode: CollectMode = CollectMode("goal")
remaining_mode: RemainingMode = RemainingMode("goal")
auto_shutdown: AutoShutdown = AutoShutdown(0)
compatibility: Compatibility = Compatibility(2)
log_network: LogNetwork = LogNetwork(0)
class GeneratorOptions(Group):
"""Options for Generation"""
class EnemizerPath(LocalFilePath):
"""Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases"""
is_exe = True
class PlayerFilesPath(OptionalUserFolderPath):
"""Folder from which the player yaml files are pulled from"""
# created on demand, so marked as optional
class Players(int):
"""amount of players, 0 to infer from player files"""
class WeightsFilePath(str):
"""
general weights file, within the stated player_files_path location
gets used if players is higher than the amount of per-player files found to fill remaining slots
"""
# this is special because the path is relative to player_files_path
class MetaFilePath(str):
"""Meta file name, within the stated player_files_path location"""
# this is special because the path is relative to player_files_path
class Spoiler(IntEnum):
"""
Create a spoiler file
0 -> None
1 -> Spoiler without playthrough or paths to playthrough required items
2 -> Spoiler with playthrough (viable solution to goals)
3 -> Spoiler with playthrough and traversal paths towards items
"""
NONE = 0
BASIC = 1
PLAYTHROUGH = 2
FULL = 3
class GlitchTriforceRoom(IntEnum):
"""
Glitch to Triforce room from Ganon
When disabled, you have to have a weapon that can hurt ganon (master sword or swordless/easy item functionality
+ hammer) and have completed the goal required for killing ganon to be able to access the triforce room.
1 -> Enabled.
0 -> Disabled (except in no-logic)
"""
OFF = 0
ON = 1
class PlandoOptions(str):
"""
List of options that can be plando'd. Can be combined, for example "bosses, items"
Available options: bosses, items, texts, connections
"""
class Race(IntEnum):
"""Create encrypted race roms and flag games as race mode"""
OFF = 0
ON = 1
enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows
player_files_path: PlayerFilesPath = PlayerFilesPath("Players")
players: Players = Players(0)
weights_file_path: WeightsFilePath = WeightsFilePath("weights.yaml")
meta_file_path: MetaFilePath = MetaFilePath("meta.yaml")
spoiler: Spoiler = Spoiler(3)
glitch_triforce_room: GlitchTriforceRoom = GlitchTriforceRoom(1) # why is this here?
race: Race = Race(0)
plando_options: PlandoOptions = PlandoOptions("bosses")
class SNIOptions(Group):
class SNIPath(LocalFolderPath):
"""
Set this to your SNI folder location if you want the MultiClient to attempt an auto start, \
does nothing if not found
"""
class SnesRomStart(str):
"""
Set this to false to never autostart a rom (such as after patching)
True for operating system default program
Alternatively, a path to a program to open the .sfc file with
"""
sni_path: SNIPath = SNIPath("SNI")
snes_rom_start: Union[SnesRomStart, bool] = True
# Top-level group with lazy loading of worlds
class Settings(Group):
general_options: GeneralOptions = GeneralOptions()
server_options: ServerOptions = ServerOptions()
generator: GeneratorOptions = GeneratorOptions()
sni_options: SNIOptions = SNIOptions()
_filename: Optional[str] = None
def __getattribute__(self, key: str) -> Any:
if key.startswith("_") or key in self.__class__.__dict__:
# not a group or a hard-coded group
pass
elif key not in dir(self) or isinstance(super().__getattribute__(key), dict):
# settings class not loaded yet
if key not in _world_settings_name_cache:
# find world that provides the settings class
_update_cache()
# check for missing keys to update _changed
for world_settings_name in _world_settings_name_cache:
if world_settings_name not in dir(self):
self._changed = True
if key not in _world_settings_name_cache:
# not a world group
return super().__getattribute__(key)
# directly import world and grab settings class
world_mod, world_cls_name = _world_settings_name_cache[key].rsplit(".", 1)
world = cast(type, getattr(__import__(world_mod, fromlist=[world_cls_name]), world_cls_name))
assert getattr(world, "settings_key") == key
try:
cls_or_name = world.__annotations__["settings"]
except KeyError:
import warnings
warnings.warn(f"World {world_cls_name} does not define settings. Please consider upgrading the world.")
return super().__getattribute__(key)
if isinstance(cls_or_name, str):
# Try to resolve type. Sadly we can't use get_type_hints, see https://bugs.python.org/issue43463
cls_name = cls_or_name
if "[" in cls_name: # resolve ClassVar[]
cls_name = cls_name.split("[", 1)[1].rsplit("]", 1)[0]
cls = cast(type, getattr(__import__(world_mod, fromlist=[cls_name]), cls_name))
else:
type_args = typing.get_args(cls_or_name) # resolve ClassVar[]
cls = type_args[0] if type_args else cast(type, cls_or_name)
impl: Group = cast(Group, cls())
assert isinstance(impl, Group), f"{world_cls_name}.settings has to inherit from settings.Group. " \
"If that's already the case, please avoid recursive partial imports."
# above assert fails for recursive partial imports
# upcast loaded data to settings class
try:
dct = super().__getattribute__(key)
if isinstance(dct, dict):
impl.update(dct)
else:
self._changed = True # key is a class var -> new section
except AttributeError:
self._changed = True # key is unknown -> new section
setattr(self, key, impl)
return super().__getattribute__(key)
def __init__(self, location: Optional[str]): # change to PathLike[str] once we drop 3.8?
super().__init__()
if location:
from Utils import parse_yaml
with open(location, encoding="utf-8-sig") as f:
options = parse_yaml(f.read())
# TODO: detect if upgrade is required
# TODO: once we have a cache for _world_settings_name_cache, detect if any game section is missing
self.update(options or {})
self._filename = location
def autosave() -> None:
if __debug__:
import __main__
main_file = getattr(__main__, "__file__", "")
assert "pytest" not in main_file and "unittest" not in main_file, \
f"Auto-saving {self._filename} during unittests"
if self._filename and self.changed and not skip_autosave:
self.save()
if not skip_autosave:
import atexit
atexit.register(autosave)
def save(self, location: Optional[str] = None) -> None: # as above
location = location or self._filename
assert location, "No file specified"
temp_location = location + ".tmp" # not using tempfile to test expected file access
# remove old temps
if os.path.exists(temp_location):
os.unlink(temp_location)
# can't use utf-8-sig because it breaks backward compat: pyyaml on Windows with bytes does not strip the BOM
with open(temp_location, "w", encoding="utf-8") as f:
self.dump(f)
# replace old with new
if os.path.exists(location):
os.unlink(location)
os.rename(temp_location, location)
self._filename = location
def dump(self, f: TextIO, level: int = 0) -> None:
# load all world setting classes
_update_cache()
for key in _world_settings_name_cache:
self.__getattribute__(key) # load all worlds
super().dump(f, level)
@property
def filename(self) -> Optional[str]:
return self._filename
# host.yaml loader
def get_settings() -> Settings:
"""Returns settings from the default host.yaml"""
with _lock: # make sure we only have one instance
res = getattr(get_settings, "_cache", None)
if not res:
import os
from Utils import user_path, local_path
filenames = ("options.yaml", "host.yaml")
locations: List[str] = []
if os.path.join(os.getcwd()) != local_path():
locations += filenames # use files from cwd only if it's not the local_path
locations += [user_path(filename) for filename in filenames]
for location in locations:
try:
res = Settings(location)
break
except FileNotFoundError:
continue
else:
warnings.warn(f"Could not find {filenames[1]} to load options. Creating a new one.")
res = Settings(None)
res.save(user_path(filenames[1]))
setattr(get_settings, "_cache", res)
return res

View File

@@ -6,6 +6,7 @@ import shutil
import sys
import sysconfig
import typing
import warnings
import zipfile
import urllib.request
import io
@@ -20,7 +21,7 @@ from pathlib import Path
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
try:
requirement = 'cx-Freeze==6.14.9'
requirement = 'cx-Freeze>=6.15.2'
import pkg_resources
try:
pkg_resources.require(requirement)
@@ -57,6 +58,7 @@ if __name__ == "__main__":
from worlds.LauncherComponents import components, icon_paths
from Utils import version_tuple, is_windows, is_linux
from Cython.Build import cythonize
# On Python < 3.10 LogicMixin is not currently supported.
@@ -65,20 +67,16 @@ non_apworlds: set = {
"Adventure",
"ArchipIDLE",
"Archipelago",
"Blasphemous",
"ChecksFinder",
"Clique",
"DLCQuest",
"Dark Souls III",
"Final Fantasy",
"Hollow Knight",
"Hylics 2",
"Kingdom Hearts 2",
"Lufia II Ancient Cave",
"Meritous",
"Ocarina of Time",
"Overcooked! 2",
"Pokemon Red and Blue",
"Raft",
"Secret of Evermore",
"Slay the Spire",
@@ -90,6 +88,9 @@ non_apworlds: set = {
"Zillion",
}
# LogicMixin is broken before 3.10 import revamp
if sys.version_info < (3,10):
non_apworlds.add("Hollow Knight")
def download_SNI():
print("Updating SNI")
@@ -191,7 +192,7 @@ exes = [
) for c in components if c.script_name and c.frozen_name
]
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI"]
extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
@@ -293,17 +294,38 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader")
sni_thread.start()
# pre build steps
# pre-build steps
print(f"Outputting to: {self.buildfolder}")
os.makedirs(self.buildfolder, exist_ok=True)
import ModuleUpdate
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
ModuleUpdate.update(yes=self.yes)
# auto-build cython modules
build_ext = self.distribution.get_command_obj("build_ext")
build_ext.inplace = False
self.run_command("build_ext")
# find remains of previous in-place builds, try to delete and warn otherwise
for path in build_ext.get_outputs():
parts = os.path.split(path)[-1].split(".")
pattern = parts[0] + ".*." + parts[-1]
for match in Path().glob(pattern):
try:
match.unlink()
print(f"Removed {match}")
except Exception as ex:
warnings.warn(f"Could not delete old build output: {match}\n"
f"{ex}\nPlease close all AP instances and delete manually.")
# regular cx build
self.buildtime = datetime.datetime.utcnow()
super().run()
# manually copy built modules to lib folder. cx_Freeze does not know they exist.
for src in build_ext.get_outputs():
print(f"copying {src} -> {self.libfolder}")
shutil.copy(src, self.libfolder, follow_symlinks=False)
# need to finish download before copying
sni_thread.join()
@@ -396,14 +418,6 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
for extra_exe in extra_exes:
if extra_exe.is_file():
extra_exe.chmod(0o755)
# rewrite windows-specific things in host.yaml
host_yaml = self.buildfolder / 'host.yaml'
with host_yaml.open('r+b') as f:
data = f.read()
data = data.replace(b'factorio\\\\bin\\\\x64\\\\factorio', b'factorio/bin/x64/factorio')
f.seek(0, os.SEEK_SET)
f.write(data)
f.truncate()
class AppImageCommand(setuptools.Command):
@@ -586,10 +600,10 @@ cx_Freeze.setup(
version=f"{version_tuple.major}.{version_tuple.minor}.{version_tuple.build}",
description="Archipelago",
executables=exes,
ext_modules=[], # required to disable auto-discovery with setuptools>=61
ext_modules=cythonize("_speedups.pyx"),
options={
"build_exe": {
"packages": ["websockets", "worlds", "kivy"],
"packages": ["worlds", "kivy", "cymem", "websockets"],
"includes": [],
"excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas"],

View File

@@ -237,7 +237,8 @@ class WorldTestBase(unittest.TestCase):
for location in self.multiworld.get_locations():
if location.name not in excluded:
with self.subTest("Location should be reached", location=location):
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
reachable = location.can_reach(state)
self.assertTrue(reachable, f"{location.name} unreachable")
with self.subTest("Beatable"):
self.multiworld.state = state
self.assertBeatable(True)

View File

@@ -1,3 +1,7 @@
import warnings
warnings.simplefilter("always")
import settings
warnings.simplefilter("always")
settings.no_gui = True
settings.skip_autosave = True

View File

@@ -433,6 +433,20 @@ class TestFillRestrictive(unittest.TestCase):
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")
def test_correct_item_instance_removed_from_pool(self):
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2)
player1.prog_items[0].name = "Different_item_instance_but_same_item_name"
player1.prog_items[1].name = "Different_item_instance_but_same_item_name"
loc0 = player1.locations[0]
fill_restrictive(multi_world, multi_world.state,
[loc0], player1.prog_items)
self.assertEqual(1, len(player1.prog_items))
self.assertIsNot(loc0.item, player1.prog_items[0], "Filled item was still present in item pool")
class TestDistributeItemsRestrictive(unittest.TestCase):
def test_basic_distribute(self):

View File

@@ -19,6 +19,7 @@ class TestHelpers(unittest.TestCase):
regions: Dict[str, str] = {
"TestRegion1": "I'm an apple",
"TestRegion2": "I'm a banana",
"TestRegion3": "Empty Region",
}
locations: Dict[str, Dict[str, Optional[int]]] = {
@@ -38,6 +39,10 @@ class TestHelpers(unittest.TestCase):
"TestRegion2": {"TestRegion1": None},
}
reg_exit_set: Dict[str, set[str]] = {
"TestRegion1": {"TestRegion3"}
}
exit_rules: Dict[str, Callable[[CollectionState], bool]] = {
"TestRegion1": lambda state: state.has("test_item", self.player)
}
@@ -68,3 +73,10 @@ class TestHelpers(unittest.TestCase):
entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}"
self.assertEqual(exit_rules[exit_reg],
self.multiworld.get_entrance(entrance_name, self.player).access_rule)
for region in reg_exit_set:
current_region = self.multiworld.get_region(region, self.player)
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}")

View File

@@ -1,22 +1,27 @@
import os
import unittest
from tempfile import TemporaryFile
from settings import Settings
import Utils
class TestIDs(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
with open(Utils.local_path("host.yaml")) as f:
with TemporaryFile("w+", encoding="utf-8") as f:
Settings(None).dump(f)
f.seek(0, os.SEEK_SET)
cls.yaml_options = Utils.parse_yaml(f.read())
def testUtilsHasHost(self):
def test_utils_in_yaml(self) -> None:
for option_key, option_set in Utils.get_default_options().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 testHostHasUtils(self):
def test_yaml_in_utils(self) -> None:
utils_options = Utils.get_default_options()
for option_key, option_set in self.yaml_options.items():
with self.subTest(option_key):

View File

@@ -44,7 +44,10 @@ class TestBase(unittest.TestCase):
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
for region in world.get_regions():
if region.name not in unreachable_regions:
if region.name in unreachable_regions:
with self.subTest("Region should be unreachable", region=region):
self.assertFalse(region.can_reach(state))
else:
with self.subTest("Region should be reached", region=region):
self.assertTrue(region.can_reach(state))

View File

@@ -0,0 +1,238 @@
# Tests for _speedups.LocationStore and NetUtils._LocationStore
import typing
import unittest
import warnings
from NetUtils import LocationStore, _LocationStore
State = typing.Dict[typing.Tuple[int, int], typing.Set[int]]
RawLocations = typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
sample_data: RawLocations = {
1: {
11: (21, 2, 7),
12: (22, 2, 0),
13: (13, 1, 0),
},
2: {
23: (11, 1, 0),
22: (12, 1, 0),
21: (23, 2, 0),
},
4: {
9: (99, 3, 0),
},
3: {
9: (99, 4, 0),
},
}
empty_state: State = {
(0, slot): set() for slot in sample_data
}
full_state: State = {
(0, slot): set(locations) for (slot, locations) in sample_data.items()
}
one_state: State = {
(0, 1): {12}
}
class Base:
class TestLocationStore(unittest.TestCase):
"""Test method calls on a loaded store."""
store: typing.Union[LocationStore, _LocationStore]
def test_len(self) -> None:
self.assertEqual(len(self.store), 4)
self.assertEqual(len(self.store[1]), 3)
def test_key_error(self) -> None:
with self.assertRaises(KeyError):
_ = self.store[0]
with self.assertRaises(KeyError):
_ = self.store[5]
locations = self.store[1] # no Exception
with self.assertRaises(KeyError):
_ = locations[7]
_ = locations[11] # no Exception
def test_getitem(self) -> None:
self.assertEqual(self.store[1][11], (21, 2, 7))
self.assertEqual(self.store[1][13], (13, 1, 0))
self.assertEqual(self.store[2][22], (12, 1, 0))
self.assertEqual(self.store[4][9], (99, 3, 0))
def test_get(self) -> None:
self.assertEqual(self.store.get(1, None), self.store[1])
self.assertEqual(self.store.get(0, None), None)
self.assertEqual(self.store[1].get(11, (None, None, None)), self.store[1][11])
self.assertEqual(self.store[1].get(10, (None, None, None)), (None, None, None))
def test_iter(self) -> None:
self.assertEqual(sorted(self.store), [1, 2, 3, 4])
self.assertEqual(len(self.store), len(sample_data))
self.assertEqual(list(self.store[1]), [11, 12, 13])
self.assertEqual(len(self.store[1]), len(sample_data[1]))
def test_items(self) -> None:
self.assertEqual(sorted(p for p, _ in self.store.items()), sorted(self.store))
self.assertEqual(sorted(p for p, _ in self.store[1].items()), sorted(self.store[1]))
self.assertEqual(sorted(self.store.items())[0][0], 1)
self.assertEqual(sorted(self.store.items())[0][1], self.store[1])
self.assertEqual(sorted(self.store[1].items())[0][0], 11)
self.assertEqual(sorted(self.store[1].items())[0][1], self.store[1][11])
def test_find_item(self) -> None:
self.assertEqual(sorted(self.store.find_item(set(), 99)), [])
self.assertEqual(sorted(self.store.find_item({3}, 1)), [])
self.assertEqual(sorted(self.store.find_item({5}, 99)), [])
self.assertEqual(sorted(self.store.find_item({3}, 99)),
[(4, 9, 99, 3, 0)])
self.assertEqual(sorted(self.store.find_item({3, 4}, 99)),
[(3, 9, 99, 4, 0), (4, 9, 99, 3, 0)])
def test_get_for_player(self) -> None:
self.assertEqual(self.store.get_for_player(3), {4: {9}})
self.assertEqual(self.store.get_for_player(1), {1: {13}, 2: {22, 23}})
def test_get_checked(self) -> None:
self.assertEqual(self.store.get_checked(full_state, 0, 1), [11, 12, 13])
self.assertEqual(self.store.get_checked(one_state, 0, 1), [12])
self.assertEqual(self.store.get_checked(empty_state, 0, 1), [])
self.assertEqual(self.store.get_checked(full_state, 0, 3), [9])
def test_get_missing(self) -> None:
self.assertEqual(self.store.get_missing(full_state, 0, 1), [])
self.assertEqual(self.store.get_missing(one_state, 0, 1), [11, 13])
self.assertEqual(self.store.get_missing(empty_state, 0, 1), [11, 12, 13])
self.assertEqual(self.store.get_missing(empty_state, 0, 3), [9])
def test_get_remaining(self) -> None:
self.assertEqual(self.store.get_remaining(full_state, 0, 1), [])
self.assertEqual(self.store.get_remaining(one_state, 0, 1), [13, 21])
self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [13, 21, 22])
self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [99])
def test_location_set_intersection(self) -> None:
locations = {10, 11, 12}
locations.intersection_update(self.store[1])
self.assertEqual(locations, {11, 12})
class TestLocationStoreConstructor(unittest.TestCase):
"""Test constructors for a given store type."""
type: type
def test_hole(self) -> None:
with self.assertRaises(Exception):
self.type({
1: {1: (1, 1, 1)},
3: {1: (1, 1, 1)},
})
def test_no_slot1(self) -> None:
with self.assertRaises(Exception):
self.type({
2: {1: (1, 1, 1)},
3: {1: (1, 1, 1)},
})
def test_slot0(self) -> None:
with self.assertRaises(ValueError):
self.type({
0: {1: (1, 1, 1)},
1: {1: (1, 1, 1)},
})
with self.assertRaises(ValueError):
self.type({
0: {1: (1, 1, 1)},
2: {1: (1, 1, 1)},
})
def test_no_players(self) -> None:
with self.assertRaises(Exception):
_ = self.type({})
def test_no_locations(self) -> None:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
store = self.type({
1: {},
})
self.assertEqual(len(store), 1)
self.assertEqual(len(store[1]), 0)
def test_no_locations_for_1(self) -> None:
store = self.type({
1: {},
2: {1: (1, 2, 3)},
})
self.assertEqual(len(store), 2)
self.assertEqual(len(store[1]), 0)
self.assertEqual(len(store[2]), 1)
def test_no_locations_for_last(self) -> None:
store = self.type({
1: {1: (1, 2, 3)},
2: {},
})
self.assertEqual(len(store), 2)
self.assertEqual(len(store[1]), 1)
self.assertEqual(len(store[2]), 0)
class TestPurePythonLocationStore(Base.TestLocationStore):
"""Run base method tests for pure python implementation."""
def setUp(self) -> None:
self.store = _LocationStore(sample_data)
super().setUp()
class TestPurePythonLocationStoreConstructor(Base.TestLocationStoreConstructor):
"""Run base constructor tests for the pure python implementation."""
def setUp(self) -> None:
self.type = _LocationStore
super().setUp()
@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
class TestSpeedupsLocationStore(Base.TestLocationStore):
"""Run base method tests for cython implementation."""
def setUp(self) -> None:
self.store = LocationStore(sample_data)
super().setUp()
@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
class TestSpeedupsLocationStoreConstructor(Base.TestLocationStoreConstructor):
"""Run base constructor tests and tests the additional constraints for cython implementation."""
def setUp(self) -> None:
self.type = LocationStore
super().setUp()
def test_float_key(self) -> None:
with self.assertRaises(Exception):
self.type({
1: {1: (1, 1, 1)},
1.1: {1: (1, 1, 1)},
3: {1: (1, 1, 1)}
})
def test_string_key(self) -> None:
with self.assertRaises(Exception):
self.type({
"1": {1: (1, 1, 1)},
})
def test_high_player_number(self) -> None:
with self.assertRaises(Exception):
self.type({
1 << 32: {1: (1, 1, 1)},
})
def test_not_a_tuple(self) -> None:
with self.assertRaises(Exception):
self.type({
1: {1: None},
})

View File

View File

@@ -73,13 +73,21 @@ class TestGenerateMain(unittest.TestCase):
def test_generate_yaml(self):
# override host.yaml
defaults = Generate.Utils.get_options()["generator"]
defaults["player_files_path"] = str(self.yaml_input_dir)
defaults["players"] = 0
sys.argv = [sys.argv[0], '--seed', '0',
'--outputpath', self.output_tempdir.name]
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}, player_files_path={self.yaml_input_dir}')
Generate.main()
from settings import get_settings
from Utils import user_path, local_path
settings = get_settings()
# NOTE: until/unless we override settings.Group's setattr, we have to upcast the input dir here
settings.generator.player_files_path = settings.generator.PlayerFilesPath(self.yaml_input_dir)
settings.generator.players = 0
settings._filename = None # don't write to disk
user_path_backup = user_path.cached_path
user_path.cached_path = local_path() # test yaml is actually in local_path
try:
sys.argv = [sys.argv[0], '--seed', '0',
'--outputpath', self.output_tempdir.name]
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}, player_files_path={self.yaml_input_dir}')
Generate.main()
finally:
user_path.cached_path = user_path_backup
self.assertOutput(self.output_tempdir.name)

View File

@@ -11,14 +11,27 @@ from BaseClasses import CollectionState
from Options import AssembleOptions
if TYPE_CHECKING:
import random
from BaseClasses import MultiWorld, Item, Location, Tutorial
from . import GamesPackage
from settings import Group
from flask import Flask
class AutoWorldRegister(type):
world_types: Dict[str, Type[World]] = {}
__file__: str
zip_path: Optional[str]
settings_key: str
__settings: Any
@property
def settings(cls) -> Any: # actual type is defined in World
# lazy loading + caching to minimize runtime cost
if cls.__settings is None:
from settings import get_settings
cls.__settings = get_settings()[cls.settings_key]
return cls.__settings
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
if "web" in dct:
@@ -60,6 +73,11 @@ class AutoWorldRegister(type):
new_class.__file__ = sys.modules[new_class.__module__].__file__
if ".apworld" in new_class.__file__:
new_class.zip_path = pathlib.Path(new_class.__file__).parents[1]
if "settings_key" not in dct:
mod_name = new_class.__module__
world_folder_name = mod_name[7:].lower() if mod_name.startswith("worlds.") else mod_name.lower()
new_class.settings_key = world_folder_name + "_options"
new_class.__settings = None
return new_class
@@ -81,7 +99,17 @@ class AutoLogicRegister(type):
def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
method = getattr(multiworld.worlds[player], method_name)
return method(*args)
try:
ret = method(*args)
except Exception as e:
message = f"Exception in {method} for player {player}, named {multiworld.player_name[player]}."
if sys.version_info >= (3, 11, 0):
e.add_note(message) # PEP 678
else:
logging.error(message)
raise e
else:
return ret
def call_all(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
@@ -128,9 +156,22 @@ class WebWorld:
"""Choose a theme for you /game/* pages.
Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone"""
bug_report_page: Optional[str]
bug_report_page: Optional[str] = None
"""display a link to a bug report page, most likely a link to a GitHub issue page."""
multitracker_template: Optional[str] = None
"""relative path with /-seperator to a MultiTracker Template file."""
# allows modification of webhost during startup, this is run once
@classmethod
def run_webhost_setup(cls):
pass
# allows modification of webhost during startup,
# this is run whenever a Flask app is created (per-thread/per-process)
@classmethod
def run_webhost_app_setup(cls, app: "Flask"):
pass
class World(metaclass=AutoWorldRegister):
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
@@ -203,6 +244,14 @@ class World(metaclass=AutoWorldRegister):
location_names: ClassVar[Set[str]]
"""set of all potential location names"""
random: random.Random
"""This world's random object. Should be used for any randomization needed in world for this player slot."""
settings_key: ClassVar[str]
"""name of the section in host.yaml for world-specific settings, will default to {folder}_options"""
settings: ClassVar[Optional["Group"]]
"""loaded settings from host.yaml"""
zip_path: ClassVar[Optional[pathlib.Path]] = None
"""If loaded from a .apworld, this is the Path to it."""
__file__: ClassVar[str]
@@ -212,6 +261,11 @@ class World(metaclass=AutoWorldRegister):
self.multiworld = multiworld
self.player = player
def __getattr__(self, item: str) -> Any:
if item == "settings":
return self.__class__.settings
raise AttributeError
# overridable methods that get called by Main.py, sorted by execution order
# can also be implemented as a classmethod and called "stage_<original_name>",
# in that case the MultiWorld object is passed as an argument and it gets called once for the entire multiworld.
@@ -269,8 +323,8 @@ class World(metaclass=AutoWorldRegister):
This happens before progression balancing, so the items may not be in their final locations yet."""
def generate_output(self, output_directory: str) -> None:
"""This method gets called from a threadpool, do not use world.random here.
If you need any last-second randomization, use MultiWorld.per_slot_randoms[slot] instead."""
"""This method gets called from a threadpool, do not use multiworld.random here.
If you need any last-second randomization, use self.random instead."""
pass
def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot
@@ -372,7 +426,6 @@ class World(metaclass=AutoWorldRegister):
res["checksum"] = data_package_checksum(res)
return res
# any methods attached to this can be used as part of CollectionState,
# please use a prefix as all of them get clobbered together
class LogicMixin(metaclass=AutoLogicRegister):

View File

@@ -3,6 +3,8 @@ import copy
import itertools
import math
import os
import settings
import typing
from enum import IntFlag
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple
@@ -31,6 +33,42 @@ from worlds.LauncherComponents import Component, components, SuffixIdentifier
components.append(Component('Adventure Client', 'AdventureClient', file_identifier=SuffixIdentifier('.apadvn')))
class AdventureSettings(settings.Group):
class RomFile(settings.UserFilePath):
"""
File name of the standard NTSC Adventure rom.
The licensed "The 80 Classic Games" CD-ROM contains this.
It may also have a .a26 extension
"""
copy_to = "ADVNTURE.BIN"
description = "Adventure ROM File"
md5s = [AdventureDeltaPatch.hash]
class RomStart(str):
"""
Set this to false to never autostart a rom (such as after patching)
True for operating system default program for '.a26'
Alternatively, a path to a program to open the .a26 file with (generally EmuHawk for multiworld)
"""
class RomArgs(str):
"""
Optional, additional args passed into rom_start before the .bin file
For example, this can be used to autoload the connector script in BizHawk
(see BizHawk --lua= option)
Windows example:
rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/connector_adventure.lua"
"""
class DisplayMsgs(settings.Bool):
"""Set this to true to display item received messages in EmuHawk"""
rom_file: RomFile = RomFile(RomFile.copy_to)
rom_start: typing.Union[RomStart, bool] = True
rom_args: Optional[RomArgs] = " "
display_msgs: typing.Union[DisplayMsgs, bool] = True
class AdventureWeb(WebWorld):
theme = "dirt"
@@ -53,7 +91,6 @@ class AdventureWeb(WebWorld):
)
tutorials = [setup, setup_fr]
def get_item_position_data_start(table_index: int):
@@ -73,6 +110,7 @@ class AdventureWorld(World):
web: ClassVar[WebWorld] = AdventureWeb()
option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions
settings: ClassVar[AdventureSettings]
item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()}
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
data_version: ClassVar[int] = 1

View File

@@ -581,31 +581,25 @@ class ALTTPSNIClient(SNIClient):
def get_alttp_settings(romfile: str):
import LttPAdjuster
last_settings = Utils.get_adjuster_settings(GAME_ALTTP)
base_settings = LttPAdjuster.get_argparser().parse_known_args(args=[])[0]
allow_list = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
"uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes",
"reduceflashing", "deathlink", "allowcollect", "oof"}
for option_name in allow_list:
# set new defaults since last_settings were created
if not hasattr(last_settings, option_name):
setattr(last_settings, option_name, getattr(base_settings, option_name))
adjustedromfile = ''
if last_settings:
if vars(Utils.get_adjuster_settings_no_defaults(GAME_ALTTP)):
last_settings = Utils.get_adjuster_settings(GAME_ALTTP)
allow_list = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
"uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes",
"reduceflashing", "deathlink", "allowcollect", "oof"}
choice = 'no'
if not hasattr(last_settings, 'auto_apply') or 'ask' in last_settings.auto_apply:
if 'ask' in last_settings.auto_apply:
printed_options = {name: value for name, value in vars(last_settings).items() if name in allow_list}
if hasattr(last_settings, "sprite_pool"):
sprite_pool = {}
for sprite in last_settings.sprite_pool:
if sprite in sprite_pool:
sprite_pool[sprite] += 1
else:
sprite_pool[sprite] = 1
if sprite_pool:
printed_options["sprite_pool"] = sprite_pool
sprite_pool = {}
for sprite in last_settings.sprite_pool:
if sprite in sprite_pool:
sprite_pool[sprite] += 1
else:
sprite_pool[sprite] = 1
if sprite_pool:
printed_options["sprite_pool"] = sprite_pool
import pprint
from CommonClient import gui_enabled
@@ -685,17 +679,17 @@ def get_alttp_settings(romfile: str):
choice = 'yes'
if 'yes' in choice:
import LttPAdjuster
from worlds.alttp.Rom import get_base_rom_path
last_settings.rom = romfile
last_settings.baserom = get_base_rom_path()
last_settings.world = None
if hasattr(last_settings, "sprite_pool"):
if last_settings.sprite_pool:
from LttPAdjuster import AdjusterWorld
last_settings.world = AdjusterWorld(getattr(last_settings, "sprite_pool"))
adjusted = True
import LttPAdjuster
_, adjustedromfile = LttPAdjuster.adjust(last_settings)
if hasattr(last_settings, "world"):

View File

@@ -130,7 +130,7 @@ difficulties = {
progressiveshield=['Progressive Shield'] * 3,
basicshield=['Blue Shield', 'Red Shield', 'Red Shield'],
progressivearmor=['Progressive Mail'] * 2,
basicarmor=['Blue Mail', 'Blue Mail'] * 2,
basicarmor=['Blue Mail'] * 2,
swordless=['Rupees (20)'] * 4,
progressivemagic=['Magic Upgrade (1/2)', 'Rupees (300)'],
basicmagic=['Magic Upgrade (1/2)', 'Rupees (300)'],

View File

@@ -7,21 +7,19 @@ LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173"
RANDOMIZERBASEHASH: str = "9952c2a3ec1b421e408df0d20c8f0c7f"
ROM_PLAYER_LIMIT: int = 255
import io
import json
import hashlib
import logging
import os
import random
import struct
import subprocess
import threading
import concurrent.futures
import bsdiff4
from typing import Optional, List
from typing import List
from BaseClasses import CollectionState, Region, Location, MultiWorld
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, read_snes_rom
from .Shops import ShopType, ShopPriceType
from .Dungeons import dungeon_music_addresses
@@ -37,6 +35,7 @@ from .Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmith
from .Items import ItemFactory, item_table, item_name_groups, progression_items
from .EntranceShuffle import door_addresses
from .Options import smallkey_shuffle
from .Sprites import apply_random_sprite_on_event
try:
from maseya import z3pr
@@ -212,73 +211,6 @@ def check_enemizer(enemizercli):
check_enemizer.done = True
def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_random_on_event, sprite_pool):
userandomsprites = False
if sprite and not isinstance(sprite, Sprite):
sprite = sprite.lower()
userandomsprites = sprite.startswith('randomon')
racerom = rom.read_byte(0x180213)
if allow_random_on_event or not racerom:
# Changes to this byte for race rom seeds are only permitted on initial rolling of the seed.
# However, if the seed is not a racerom seed, then it is always allowed.
rom.write_byte(0x186381, 0x00 if userandomsprites else 0x01)
onevent = 0
if sprite == 'randomonall':
onevent = 0xFFFF # Support all current and future events that can cause random sprite changes.
elif sprite == 'randomonnone':
# Allows for opting into random on events on race rom seeds, without actually enabling any of the events initially.
onevent = 0x0000
elif sprite == 'randomonrandom':
# Allows random to take the wheel on which events apply. (at least one event will be applied.)
onevent = local_random.randint(0x0001, 0x003F)
elif userandomsprites:
onevent = 0x01 if 'hit' in sprite else 0x00
onevent += 0x02 if 'enter' in sprite else 0x00
onevent += 0x04 if 'exit' in sprite else 0x00
onevent += 0x08 if 'slash' in sprite else 0x00
onevent += 0x10 if 'item' in sprite else 0x00
onevent += 0x20 if 'bonk' in sprite else 0x00
rom.write_int16(0x18637F, onevent)
sprite = Sprite(sprite) if os.path.isfile(sprite) else Sprite.get_sprite_from_name(sprite, local_random)
# write link sprite if required
if sprite:
sprites = list()
sprite.write_to_rom(rom)
_populate_sprite_table()
if userandomsprites:
if sprite_pool:
if isinstance(sprite_pool, str):
sprite_pool = sprite_pool.split(':')
for spritename in sprite_pool:
sprite = Sprite(spritename) if os.path.isfile(spritename) else Sprite.get_sprite_from_name(
spritename, local_random)
if sprite:
sprites.append(sprite)
else:
logging.info(f"Sprite {spritename} was not found.")
else:
sprites = list(set(_sprite_table.values())) # convert to list and remove dupes
else:
sprites.append(sprite)
if sprites:
while len(sprites) < 32:
sprites.extend(sprites)
local_random.shuffle(sprites)
for i, sprite in enumerate(sprites[:32]):
if not i and not userandomsprites:
continue
rom.write_bytes(0x300000 + (i * 0x8000), sprite.sprite)
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
player = world.player
multiworld = world.multiworld
@@ -487,271 +419,6 @@ class TileSet:
return localrandom.choice(tile_sets)
sprite_list_lock = threading.Lock()
_sprite_table = {}
def _populate_sprite_table():
with sprite_list_lock:
if not _sprite_table:
def load_sprite_from_file(file):
sprite = Sprite(file)
if sprite.valid:
_sprite_table[sprite.name.lower()] = sprite
_sprite_table[os.path.basename(file).split(".")[0].lower()] = sprite # alias for filename base
else:
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
with concurrent.futures.ThreadPoolExecutor() as pool:
for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]:
for file in os.listdir(dir):
pool.submit(load_sprite_from_file, os.path.join(dir, file))
class Sprite():
sprite_size = 28672
palette_size = 120
glove_size = 4
author_name: Optional[str] = None
base_data: bytes
def __init__(self, filename):
if not hasattr(Sprite, "base_data"):
self.get_vanilla_sprite_data()
with open(filename, 'rb') as file:
filedata = file.read()
self.name = os.path.basename(filename)
self.valid = True
if filename.endswith(".apsprite"):
self.from_ap_sprite(filedata)
elif len(filedata) == 0x7000:
# sprite file with graphics and without palette data
self.sprite = filedata[:0x7000]
elif len(filedata) == 0x7078:
# sprite file with graphics and palette data
self.sprite = filedata[:0x7000]
self.palette = filedata[0x7000:]
self.glove_palette = filedata[0x7036:0x7038] + filedata[0x7054:0x7056]
elif len(filedata) == 0x707C:
# sprite file with graphics and palette data including gloves
self.sprite = filedata[:0x7000]
self.palette = filedata[0x7000:0x7078]
self.glove_palette = filedata[0x7078:]
elif len(filedata) in [0x100000, 0x200000, 0x400000]:
# full rom with patched sprite, extract it
self.sprite = filedata[0x80000:0x87000]
self.palette = filedata[0xDD308:0xDD380]
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
elif filedata.startswith(b'ZSPR'):
self.from_zspr(filedata, filename)
else:
self.valid = False
def get_vanilla_sprite_data(self):
file_name = get_base_rom_path()
base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb")))
Sprite.sprite = base_rom_bytes[0x80000:0x87000]
Sprite.palette = base_rom_bytes[0xDD308:0xDD380]
Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9]
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
def from_ap_sprite(self, filedata):
# noinspection PyBroadException
try:
obj = parse_yaml(filedata.decode("utf-8-sig"))
if obj["min_format_version"] > 1:
raise Exception("Sprite file requires an updated reader.")
self.author_name = obj["author"]
self.name = obj["name"]
if obj["data"]: # skip patching for vanilla content
data = bsdiff4.patch(Sprite.base_data, obj["data"])
self.sprite = data[:self.sprite_size]
self.palette = data[self.sprite_size:self.palette_size]
self.glove_palette = data[self.sprite_size + self.palette_size:]
except Exception:
logger = logging.getLogger("apsprite")
logger.exception("Error parsing apsprite file")
self.valid = False
@property
def author_game_display(self) -> str:
name = getattr(self, "_author_game_display", "")
if not name:
name = self.author_name
# At this point, may need some filtering to displayable characters
return name
def to_ap_sprite(self, path):
import yaml
payload = {"format_version": 1,
"min_format_version": 1,
"sprite_version": 1,
"name": self.name,
"author": self.author_name,
"game": "A Link to the Past",
"data": self.get_delta()}
with open(path, "w") as f:
f.write(yaml.safe_dump(payload))
def get_delta(self):
modified_data = self.sprite + self.palette + self.glove_palette
return bsdiff4.diff(Sprite.base_data, modified_data)
def from_zspr(self, filedata, filename):
result = self.parse_zspr(filedata, 1)
if result is None:
self.valid = False
return
(sprite, palette, self.name, self.author_name, self._author_game_display) = result
if self.name == "":
self.name = os.path.split(filename)[1].split(".")[0]
if len(sprite) != 0x7000:
self.valid = False
return
self.sprite = sprite
if len(palette) == 0:
pass
elif len(palette) == 0x78:
self.palette = palette
elif len(palette) == 0x7C:
self.palette = palette[:0x78]
self.glove_palette = palette[0x78:]
else:
self.valid = False
@staticmethod
def get_sprite_from_name(name: str, local_random=random) -> Optional[Sprite]:
_populate_sprite_table()
name = name.lower()
if name.startswith('random'):
sprites = list(set(_sprite_table.values()))
sprites.sort(key=lambda x: x.name)
return local_random.choice(sprites)
return _sprite_table.get(name, None)
@staticmethod
def default_link_sprite():
return Sprite(local_path('data', 'default.apsprite'))
def decode8(self, pos):
arr = [[0 for _ in range(8)] for _ in range(8)]
for y in range(8):
for x in range(8):
position = 1 << (7 - x)
val = 0
if self.sprite[pos + 2 * y] & position:
val += 1
if self.sprite[pos + 2 * y + 1] & position:
val += 2
if self.sprite[pos + 2 * y + 16] & position:
val += 4
if self.sprite[pos + 2 * y + 17] & position:
val += 8
arr[y][x] = val
return arr
def decode16(self, pos):
arr = [[0 for _ in range(16)] for _ in range(16)]
top_left = self.decode8(pos)
top_right = self.decode8(pos + 0x20)
bottom_left = self.decode8(pos + 0x200)
bottom_right = self.decode8(pos + 0x220)
for x in range(8):
for y in range(8):
arr[y][x] = top_left[y][x]
arr[y][x + 8] = top_right[y][x]
arr[y + 8][x] = bottom_left[y][x]
arr[y + 8][x + 8] = bottom_right[y][x]
return arr
@staticmethod
def parse_zspr(filedata, expected_kind):
logger = logging.getLogger("ZSPR")
headerstr = "<4xBHHIHIHH6x"
headersize = struct.calcsize(headerstr)
if len(filedata) < headersize:
return None
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
headerstr, filedata)
if version not in [1]:
logger.error("Error parsing ZSPR file: Version %g not supported", version)
return None
if kind != expected_kind:
return None
stream = io.BytesIO(filedata)
stream.seek(headersize)
def read_utf16le(stream):
"""Decodes a null-terminated UTF-16_LE string of unknown size from a stream"""
raw = bytearray()
while True:
char = stream.read(2)
if char in [b"", b"\x00\x00"]:
break
raw += char
return raw.decode("utf-16_le")
# noinspection PyBroadException
try:
sprite_name = read_utf16le(stream)
author_name = read_utf16le(stream)
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
# Ignoring the Author Rom name for the time being.
real_csum = sum(filedata) % 0x10000
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
logger.warning("ZSPR file has incorrect checksum. It may be corrupted.")
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
palette = filedata[palette_offset:palette_offset + palette_size]
if len(sprite) != sprite_size or len(palette) != palette_size:
logger.error("Error parsing ZSPR file: Unexpected end of file")
return None
return sprite, palette, sprite_name, author_name, author_credits_name
except Exception:
logger.exception("Error parsing ZSPR file")
return None
def decode_palette(self):
"""Returns the palettes as an array of arrays of 15 colors"""
def array_chunk(arr, size):
return list(zip(*[iter(arr)] * size))
def make_int16(pair):
return pair[1] << 8 | pair[0]
def expand_color(i):
return (i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8
# turn palette data into a list of RGB tuples with 8 bit values
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)]
# split into palettes of 15 colors
return array_chunk(palette_as_colors, 15)
def __hash__(self):
return hash(self.name)
def write_to_rom(self, rom: LocalRom):
if not self.valid:
logging.warning("Tried writing invalid sprite to rom, skipping.")
return
rom.write_bytes(0x80000, self.sprite)
rom.write_bytes(0xDD308, self.palette)
rom.write_bytes(0xDEDF5, self.glove_palette)
rom.write_bytes(0x300000, self.sprite)
rom.write_bytes(0x307000, self.palette)
rom.write_bytes(0x307078, self.glove_palette)
bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A,
0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD,
0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D,

View File

@@ -172,6 +172,7 @@ def FillDisabledShopSlots(world):
shop: Shop = location.parent_region.shop
location.item = ItemFactory(shop.inventory[location.shop_slot]['item'], location.player)
location.item_rule = lambda item: item.name == location.item.name and item.player == location.player
location.locked = True
def ShopSlotFill(multiworld):
@@ -278,6 +279,8 @@ def ShopSlotFill(multiworld):
if 'P' in multiworld.shop_shuffle[location.player]:
price_to_funny_price(multiworld, shop.inventory[location.shop_slot], location.player)
FillDisabledShopSlots(multiworld)
def create_shops(world, player: int):
option = world.shop_shuffle[player]

393
worlds/alttp/Sprites.py Normal file
View File

@@ -0,0 +1,393 @@
from __future__ import annotations
import concurrent.futures
import io
import json
import logging
import os
import random
import struct
import threading
from typing import Optional, TYPE_CHECKING
import bsdiff4
from Utils import user_path, read_snes_rom, parse_yaml, local_path
if TYPE_CHECKING:
from .Rom import LocalRom
sprite_list_lock = threading.Lock()
_sprite_table = {}
def _populate_sprite_table():
with sprite_list_lock:
if not _sprite_table:
def load_sprite_from_file(file):
sprite = Sprite(file)
if sprite.valid:
_sprite_table[sprite.name.lower()] = sprite
_sprite_table[os.path.basename(file).split(".")[0].lower()] = sprite # alias for filename base
else:
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
with concurrent.futures.ThreadPoolExecutor() as pool:
for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]:
for file in os.listdir(dir):
pool.submit(load_sprite_from_file, os.path.join(dir, file))
class Sprite():
sprite_size = 28672
palette_size = 120
glove_size = 4
author_name: Optional[str] = None
base_data: bytes
def __init__(self, filename):
if not hasattr(Sprite, "base_data"):
self.get_vanilla_sprite_data()
with open(filename, 'rb') as file:
filedata = file.read()
self.name = os.path.basename(filename)
self.valid = True
if filename.endswith(".apsprite"):
self.from_ap_sprite(filedata)
elif len(filedata) == 0x7000:
# sprite file with graphics and without palette data
self.sprite = filedata[:0x7000]
elif len(filedata) == 0x7078:
# sprite file with graphics and palette data
self.sprite = filedata[:0x7000]
self.palette = filedata[0x7000:]
self.glove_palette = filedata[0x7036:0x7038] + filedata[0x7054:0x7056]
elif len(filedata) == 0x707C:
# sprite file with graphics and palette data including gloves
self.sprite = filedata[:0x7000]
self.palette = filedata[0x7000:0x7078]
self.glove_palette = filedata[0x7078:]
elif len(filedata) in [0x100000, 0x200000, 0x400000]:
# full rom with patched sprite, extract it
self.sprite = filedata[0x80000:0x87000]
self.palette = filedata[0xDD308:0xDD380]
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
elif filedata.startswith(b'ZSPR'):
self.from_zspr(filedata, filename)
else:
self.valid = False
def get_vanilla_sprite_data(self):
from .Rom import get_base_rom_path
file_name = get_base_rom_path()
base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb")))
Sprite.sprite = base_rom_bytes[0x80000:0x87000]
Sprite.palette = base_rom_bytes[0xDD308:0xDD380]
Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9]
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
def from_ap_sprite(self, filedata):
# noinspection PyBroadException
try:
obj = parse_yaml(filedata.decode("utf-8-sig"))
if obj["min_format_version"] > 1:
raise Exception("Sprite file requires an updated reader.")
self.author_name = obj["author"]
self.name = obj["name"]
if obj["data"]: # skip patching for vanilla content
data = bsdiff4.patch(Sprite.base_data, obj["data"])
self.sprite = data[:self.sprite_size]
self.palette = data[self.sprite_size:self.palette_size]
self.glove_palette = data[self.sprite_size + self.palette_size:]
except Exception:
logger = logging.getLogger("apsprite")
logger.exception("Error parsing apsprite file")
self.valid = False
@property
def author_game_display(self) -> str:
name = getattr(self, "_author_game_display", "")
if not name:
name = self.author_name
# At this point, may need some filtering to displayable characters
return name
def to_ap_sprite(self, path):
import yaml
payload = {"format_version": 1,
"min_format_version": 1,
"sprite_version": 1,
"name": self.name,
"author": self.author_name,
"game": "A Link to the Past",
"data": self.get_delta()}
with open(path, "w") as f:
f.write(yaml.safe_dump(payload))
def get_delta(self):
modified_data = self.sprite + self.palette + self.glove_palette
return bsdiff4.diff(Sprite.base_data, modified_data)
def from_zspr(self, filedata, filename):
result = self.parse_zspr(filedata, 1)
if result is None:
self.valid = False
return
(sprite, palette, self.name, self.author_name, self._author_game_display) = result
if self.name == "":
self.name = os.path.split(filename)[1].split(".")[0]
if len(sprite) != 0x7000:
self.valid = False
return
self.sprite = sprite
if len(palette) == 0:
pass
elif len(palette) == 0x78:
self.palette = palette
elif len(palette) == 0x7C:
self.palette = palette[:0x78]
self.glove_palette = palette[0x78:]
else:
self.valid = False
@staticmethod
def get_sprite_from_name(name: str, local_random=random) -> Optional[Sprite]:
_populate_sprite_table()
name = name.lower()
if name.startswith('random'):
sprites = list(set(_sprite_table.values()))
sprites.sort(key=lambda x: x.name)
return local_random.choice(sprites)
return _sprite_table.get(name, None)
@staticmethod
def default_link_sprite():
return Sprite(local_path('data', 'default.apsprite'))
def decode8(self, pos):
arr = [[0 for _ in range(8)] for _ in range(8)]
for y in range(8):
for x in range(8):
position = 1 << (7 - x)
val = 0
if self.sprite[pos + 2 * y] & position:
val += 1
if self.sprite[pos + 2 * y + 1] & position:
val += 2
if self.sprite[pos + 2 * y + 16] & position:
val += 4
if self.sprite[pos + 2 * y + 17] & position:
val += 8
arr[y][x] = val
return arr
def decode16(self, pos):
arr = [[0 for _ in range(16)] for _ in range(16)]
top_left = self.decode8(pos)
top_right = self.decode8(pos + 0x20)
bottom_left = self.decode8(pos + 0x200)
bottom_right = self.decode8(pos + 0x220)
for x in range(8):
for y in range(8):
arr[y][x] = top_left[y][x]
arr[y][x + 8] = top_right[y][x]
arr[y + 8][x] = bottom_left[y][x]
arr[y + 8][x + 8] = bottom_right[y][x]
return arr
@staticmethod
def parse_zspr(filedata, expected_kind):
logger = logging.getLogger("ZSPR")
headerstr = "<4xBHHIHIHH6x"
headersize = struct.calcsize(headerstr)
if len(filedata) < headersize:
return None
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
headerstr, filedata)
if version not in [1]:
logger.error("Error parsing ZSPR file: Version %g not supported", version)
return None
if kind != expected_kind:
return None
stream = io.BytesIO(filedata)
stream.seek(headersize)
def read_utf16le(stream):
"""Decodes a null-terminated UTF-16_LE string of unknown size from a stream"""
raw = bytearray()
while True:
char = stream.read(2)
if char in [b"", b"\x00\x00"]:
break
raw += char
return raw.decode("utf-16_le")
# noinspection PyBroadException
try:
sprite_name = read_utf16le(stream)
author_name = read_utf16le(stream)
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
# Ignoring the Author Rom name for the time being.
real_csum = sum(filedata) % 0x10000
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
logger.warning("ZSPR file has incorrect checksum. It may be corrupted.")
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
palette = filedata[palette_offset:palette_offset + palette_size]
if len(sprite) != sprite_size or len(palette) != palette_size:
logger.error("Error parsing ZSPR file: Unexpected end of file")
return None
return sprite, palette, sprite_name, author_name, author_credits_name
except Exception:
logger.exception("Error parsing ZSPR file")
return None
def decode_palette(self):
"""Returns the palettes as an array of arrays of 15 colors"""
def array_chunk(arr, size):
return list(zip(*[iter(arr)] * size))
def make_int16(pair):
return pair[1] << 8 | pair[0]
def expand_color(i):
return (i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8
# turn palette data into a list of RGB tuples with 8 bit values
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)]
# split into palettes of 15 colors
return array_chunk(palette_as_colors, 15)
def __hash__(self):
return hash(self.name)
def write_to_rom(self, rom: "LocalRom"):
if not self.valid:
logging.warning("Tried writing invalid sprite to rom, skipping.")
return
rom.write_bytes(0x80000, self.sprite)
rom.write_bytes(0xDD308, self.palette)
rom.write_bytes(0xDEDF5, self.glove_palette)
rom.write_bytes(0x300000, self.sprite)
rom.write_bytes(0x307000, self.palette)
rom.write_bytes(0x307078, self.glove_palette)
def update_sprites():
from tkinter import Tk
from LttPAdjuster import get_image_for_sprite
from LttPAdjuster import BackgroundTaskProgress
from LttPAdjuster import BackgroundTaskProgressNullWindow
from LttPAdjuster import update_sprites
# Target directories
input_dir = user_path("data", "sprites", "alttpr")
output_dir = local_path("WebHostLib", "static", "generated") # TODO: move to user_path
os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True)
# update sprites through gui.py's functions
done = threading.Event()
try:
top = Tk()
except:
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
else:
top.withdraw()
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.isSet():
task.do_events()
spriteData = []
for file in (file for file in os.listdir(input_dir) if not file.startswith(".")):
sprite = Sprite(os.path.join(input_dir, file))
if not sprite.name:
print("Warning:", file, "has no name.")
sprite.name = file.split(".", 1)[0]
if sprite.valid:
with open(os.path.join(output_dir, "sprites", f"{os.path.splitext(file)[0]}.gif"), 'wb') as image:
image.write(get_image_for_sprite(sprite, True))
spriteData.append({"file": file, "author": sprite.author_name, "name": sprite.name})
else:
print(file, "dropped, as it has no valid sprite data.")
spriteData.sort(key=lambda entry: entry["name"])
with open(f'{output_dir}/spriteData.json', 'w') as file:
json.dump({"sprites": spriteData}, file, indent=1)
return spriteData
def apply_random_sprite_on_event(rom: "LocalRom", sprite, local_random, allow_random_on_event, sprite_pool):
userandomsprites = False
if sprite and not isinstance(sprite, Sprite):
sprite = sprite.lower()
userandomsprites = sprite.startswith('randomon')
racerom = rom.read_byte(0x180213)
if allow_random_on_event or not racerom:
# Changes to this byte for race rom seeds are only permitted on initial rolling of the seed.
# However, if the seed is not a racerom seed, then it is always allowed.
rom.write_byte(0x186381, 0x00 if userandomsprites else 0x01)
onevent = 0
if sprite == 'randomonall':
onevent = 0xFFFF # Support all current and future events that can cause random sprite changes.
elif sprite == 'randomonnone':
# Allows for opting into random on events on race rom seeds, without actually enabling any of the events initially.
onevent = 0x0000
elif sprite == 'randomonrandom':
# Allows random to take the wheel on which events apply. (at least one event will be applied.)
onevent = local_random.randint(0x0001, 0x003F)
elif userandomsprites:
onevent = 0x01 if 'hit' in sprite else 0x00
onevent += 0x02 if 'enter' in sprite else 0x00
onevent += 0x04 if 'exit' in sprite else 0x00
onevent += 0x08 if 'slash' in sprite else 0x00
onevent += 0x10 if 'item' in sprite else 0x00
onevent += 0x20 if 'bonk' in sprite else 0x00
rom.write_int16(0x18637F, onevent)
sprite = Sprite(sprite) if os.path.isfile(sprite) else Sprite.get_sprite_from_name(sprite, local_random)
# write link sprite if required
if sprite:
sprites = list()
sprite.write_to_rom(rom)
_populate_sprite_table()
if userandomsprites:
if sprite_pool:
if isinstance(sprite_pool, str):
sprite_pool = sprite_pool.split(':')
for spritename in sprite_pool:
sprite = Sprite(spritename) if os.path.isfile(spritename) else Sprite.get_sprite_from_name(
spritename, local_random)
if sprite:
sprites.append(sprite)
else:
logging.info(f"Sprite {spritename} was not found.")
else:
sprites = list(set(_sprite_table.values())) # convert to list and remove dupes
else:
sprites.append(sprite)
if sprites:
while len(sprites) < 32:
sprites.extend(sprites)
local_random.shuffle(sprites)
for i, sprite in enumerate(sprites[:32]):
if not i and not userandomsprites:
continue
rom.write_bytes(0x300000 + (i * 0x8000), sprite.sprite)
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)

View File

@@ -1,6 +1,7 @@
import logging
import os
import random
import settings
import threading
import typing
@@ -29,6 +30,16 @@ lttp_logger = logging.getLogger("A Link to the Past")
extras_list = sum(difficulties['normal'].extras[0:5], [])
class ALTTPSettings(settings.Group):
class RomFile(settings.SNESRomPath):
"""File name of the v1.0 J rom"""
description = "ALTTP v1.0 J ROM File"
copy_to = "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
md5s = [LttPDeltaPatch.hash]
rom_file: RomFile = RomFile(RomFile.copy_to)
class ALTTPWeb(WebWorld):
setup_en = Tutorial(
"Multiworld Setup Tutorial",
@@ -113,6 +124,14 @@ class ALTTPWeb(WebWorld):
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
@classmethod
def run_webhost_setup(cls):
rom_file = get_base_rom_path()
if os.path.exists(rom_file):
from .Sprites import update_sprites
update_sprites()
else:
logging.warning("Could not update LttP sprites.")
class ALTTPWorld(World):
"""
@@ -123,6 +142,8 @@ class ALTTPWorld(World):
"""
game = "A Link to the Past"
option_definitions = alttp_options
settings_key = "lttp_options"
settings: typing.ClassVar[ALTTPSettings]
topology_present = True
item_name_groups = item_name_groups
location_name_groups = {
@@ -219,9 +240,16 @@ class ALTTPWorld(World):
create_items = generate_itempool
enemizer_path: str = Utils.get_options()["generator"]["enemizer_path"] \
if os.path.isabs(Utils.get_options()["generator"]["enemizer_path"]) \
else Utils.local_path(Utils.get_options()["generator"]["enemizer_path"])
_enemizer_path: typing.ClassVar[typing.Optional[str]] = None
@property
def enemizer_path(self) -> str:
# TODO: directly use settings
cls = self.__class__
if cls._enemizer_path is None:
cls._enemizer_path = settings.get_settings().generator.enemizer_path
assert isinstance(cls._enemizer_path, str)
return cls._enemizer_path
# custom instance vars
dungeon_local_item_names: typing.Set[str]
@@ -544,6 +572,44 @@ class ALTTPWorld(World):
er_hint_data[region.player][location.address] = main_entrance.name
hint_data.update(er_hint_data)
@classmethod
def stage_modify_multidata(cls, multiworld, multidata: dict):
ordered_areas = (
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total"
)
checks_in_area = {player: {area: list() for area in ordered_areas}
for player in multiworld.get_game_players(cls.game)}
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
multidata["checks_in_area"].update(checks_in_area)
def modify_multidata(self, multidata: dict):
import base64
# wait for self.rom_name to be available.
@@ -724,6 +790,31 @@ class ALTTPWorld(World):
res.append(item)
return res
def fill_slot_data(self):
slot_data = {}
if not self.multiworld.is_race:
# all of these option are NOT used by the SNI- or Text-Client.
# they are used by the alttp-poptracker pack (https://github.com/StripesOO7/alttp-ap-poptracker-pack)
# for convenient auto-tracking of the generated settings and adjusting the tracker accordingly
slot_options = ["crystals_needed_for_gt", "crystals_needed_for_ganon", "open_pyramid",
"bigkey_shuffle", "smallkey_shuffle", "compass_shuffle", "map_shuffle",
"progressive", "swordless", "retro_bow", "retro_caves", "shop_item_slots",
"boss_shuffle", "pot_shuffle", "enemy_shuffle"]
slot_data = {option_name: getattr(self.multiworld, option_name)[self.player].value for option_name in slot_options}
slot_data.update({
'mode': self.multiworld.mode[self.player],
'goal': self.multiworld.goal[self.player],
'dark_room_logic': self.multiworld.dark_room_logic[self.player],
'mm_medalion': self.multiworld.required_medallions[self.player][0],
'tr_medalion': self.multiworld.required_medallions[self.player][1],
'shop_shuffle': self.multiworld.shop_shuffle[self.player],
'entrance_shuffle': self.multiworld.shuffle[self.player]
}
)
return slot_data
def get_same_seed(world, seed_def: tuple) -> str:
seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {})

View File

@@ -14,6 +14,7 @@ from worlds import AutoWorld
class TestDungeon(unittest.TestCase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})

View File

@@ -15,6 +15,7 @@ from worlds import AutoWorld
class TestInverted(TestBase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})

View File

@@ -15,6 +15,7 @@ class TestInvertedBombRules(unittest.TestCase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
self.multiworld.mode[1] = "inverted"
args = Namespace
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():

View File

@@ -16,6 +16,7 @@ from worlds import AutoWorld
class TestInvertedMinor(TestBase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})

View File

@@ -17,6 +17,7 @@ from worlds import AutoWorld
class TestInvertedOWG(TestBase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})

View File

@@ -16,6 +16,7 @@ from worlds import AutoWorld
class TestMinor(TestBase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})

View File

@@ -1,14 +1,10 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_entrances
from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.InvertedRegions import mark_dark_world_regions
from worlds.alttp.ItemPool import difficulties, generate_itempool
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase
from worlds import AutoWorld
@@ -17,6 +13,7 @@ from worlds import AutoWorld
class TestVanillaOWG(TestBase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})

View File

@@ -1,20 +1,17 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_entrances
from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.InvertedRegions import mark_dark_world_regions
from worlds.alttp.ItemPool import difficulties, generate_itempool
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase
from worlds import AutoWorld
class TestVanilla(TestBase):
def setUp(self):
self.multiworld = MultiWorld(1)
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)})

View File

@@ -1,135 +0,0 @@
from typing import List, Dict
region_exit_table: Dict[str, List[str]] = {
"menu" : ["New Game"],
"albero" : ["To The Holy Line",
"To Desecrated Cistern",
"To Wasteland of the Buried Churches",
"To Dungeons"],
"attots" : ["To Mother of Mothers"],
"ar" : ["To Mother of Mothers",
"To Wall of the Holy Prohibitions",
"To Deambulatory of His Holiness"],
"bottc" : ["To Wasteland of the Buried Churches",
"To Ferrous Tree"],
"botss" : ["To The Holy Line",
"To Mountains of the Endless Dusk"],
"coolotcv" : ["To Graveyard of the Peaks",
"To Wall of the Holy Prohibitions"],
"dohh" : ["To Archcathedral Rooftops"],
"dc" : ["To Albero",
"To Mercy Dreams",
"To Mountains of the Endless Dusk",
"To Echoes of Salt",
"To Grievance Ascends"],
"eos" : ["To Jondo",
"To Mountains of the Endless Dusk",
"To Desecrated Cistern",
"To The Resting Place of the Sister",
"To Mourning and Havoc"],
"ft" : ["To Bridge of the Three Cavalries",
"To Hall of the Dawning",
"To Patio of the Silent Steps"],
"gotp" : ["To Where Olive Trees Wither",
"To Convent of Our Lady of the Charred Visage"],
"ga" : ["To Jondo",
"To Desecrated Cistern"],
"hotd" : ["To Ferrous Tree"],
"jondo" : ["To Mountains of the Endless Dusk",
"To Grievance Ascends"],
"kottw" : ["To Mother of Mothers"],
"lotnw" : ["To Mother of Mothers",
"To The Sleeping Canvases"],
"md" : ["To Wasteland of the Buried Churches",
"To Desecrated Cistern",
"To The Sleeping Canvases"],
"mom" : ["To Patio of the Silent Steps",
"To Archcathedral Rooftops",
"To Knot of the Three Words",
"To Library of the Negated Words",
"To All the Tears of the Sea"],
"moted" : ["To Brotherhood of the Silent Sorrow",
"To Jondo",
"To Desecrated Cistern"],
"mah" : ["To Echoes of Salt",
"To Mother of Mothers"],
"potss" : ["To Ferrous Tree",
"To Mother of Mothers",
"To Wall of the Holy Prohibitions"],
"petrous" : ["To The Holy Line"],
"thl" : ["To Brotherhood of the Silent Sorrow",
"To Petrous",
"To Albero"],
"trpots" : ["To Echoes of Salt"],
"tsc" : ["To Library of the Negated Words",
"To Mercy Dreams"],
"wothp" : ["To Archcathedral Rooftops",
"To Convent of Our Lady of the Charred Visage"],
"wotbc" : ["To Albero",
"To Where Olive Trees Wither",
"To Mercy Dreams"],
"wotw" : ["To Wasteland of the Buried Churches",
"To Graveyard of the Peaks"]
}
exit_lookup_table: Dict[str, str] = {
"New Game": "botss",
"To Albero": "albero",
"To All the Tears of the Sea": "attots",
"To Archcathedral Rooftops": "ar",
"To Bridge of the Three Cavalries": "bottc",
"To Brotherhood of the Silent Sorrow": "botss",
"To Convent of Our Lady of the Charred Visage": "coolotcv",
"To Deambulatory of His Holiness": "dohh",
"To Desecrated Cistern": "dc",
"To Echoes of Salt": "eos",
"To Ferrous Tree": "ft",
"To Graveyard of the Peaks": "gotp",
"To Grievance Ascends": "ga",
"To Hall of the Dawning": "hotd",
"To Jondo": "jondo",
"To Knot of the Three Words": "kottw",
"To Library of the Negated Words": "lotnw",
"To Mercy Dreams": "md",
"To Mother of Mothers": "mom",
"To Mountains of the Endless Dusk": "moted",
"To Mourning and Havoc": "mah",
"To Patio of the Silent Steps": "potss",
"To Petrous": "petrous",
"To The Holy Line": "thl",
"To The Resting Place of the Sister": "trpots",
"To The Sleeping Canvases": "tsc",
"To Wall of the Holy Prohibitions": "wothp",
"To Wasteland of the Buried Churches": "wotbc",
"To Where Olive Trees Wither": "wotw",
"To Dungeons": "dungeon"
}

View File

@@ -219,6 +219,12 @@ item_table: List[ItemDict] = [
{'name': "Three Gnarled Tongues",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Boots of Pleading",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Purified Hand of the Nun",
'count': 1,
'classification': ItemClassification.progression},
# Mea Culpa Hearts
{'name': "Smoking Heart of Incense",
@@ -372,7 +378,7 @@ item_table: List[ItemDict] = [
'classification': ItemClassification.progression},
{'name': "Quicksilver",
'count': 5,
'classification': ItemClassification.useful},
'classification': ItemClassification.progression},
{'name': "Petrified Bell",
'count': 1,
'classification': ItemClassification.progression},
@@ -398,7 +404,7 @@ item_table: List[ItemDict] = [
# Skills
{'name': "Combo Skill",
'count': 3,
'classification': ItemClassification.useful},
'classification': ItemClassification.progression},
{'name': "Charged Skill",
'count': 3,
'classification': ItemClassification.progression},
@@ -410,7 +416,13 @@ item_table: List[ItemDict] = [
'classification': ItemClassification.progression},
{'name': "Lunge Skill",
'count': 3,
'classification': ItemClassification.useful},
'classification': ItemClassification.progression},
{'name': "Dash Ability",
'count': 1,
'classification': ItemClassification.progression},
{'name': "Wall Climb Ability",
'count': 1,
'classification': ItemClassification.progression},
# Other
{'name': "Parietal bone of Lasser, the Inquisitor",
@@ -625,6 +637,23 @@ item_table: List[ItemDict] = [
'classification': ItemClassification.filler}
]
event_table: Dict[str, str] = {
"OpenedDCGateW": "D01Z05S24",
"OpenedDCGateE": "D01Z05S12",
"OpenedDCLadder": "D01Z05S20",
"OpenedWOTWCave": "D02Z01S06",
"RodeGOTPElevator": "D02Z02S11",
"OpenedConventLadder": "D02Z03S11",
"BrokeJondoBellW": "D03Z02S09",
"BrokeJondoBellE": "D03Z02S05",
"OpenedMOMLadder": "D04Z02S06",
"OpenedTSCGate": "D05Z02S11",
"OpenedARLadder": "D06Z01S23",
"BrokeBOTTCStatue": "D08Z01S02",
"OpenedWOTHPGate": "D09Z01S05",
"OpenedBOTSSLadder": "D17Z01S04"
}
group_table: Dict[str, Set[str]] = {
"wounds" : ["Holy Wound of Attrition",
"Holy Wound of Contrition",
@@ -634,6 +663,10 @@ group_table: Dict[str, Set[str]] = {
"Mirrored Mask of Dolphos",
"Embossed Mask of Crescente"],
"marks" : ["Mark of the First Refuge",
"Mark of the Second Refuge",
"Mark of the Third Refuge"],
"tirso" : ["Bouquet of Rosemary",
"Incense Garlic",
"Olive Seeds",

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,22 @@
from Options import Choice, Toggle, DefaultOnToggle, DeathLink
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool
import random
class ChoiceIsRandom(Choice):
randomized: bool = False
@classmethod
def from_text(cls, text: str) -> Choice:
text = text.lower()
if text == "random":
cls.randomized = True
return cls(random.choice(list(cls.name_lookup)))
for option_name, value in cls.options.items():
if option_name == text:
return cls(value)
raise KeyError(
f'Could not find option "{text}" for "{cls.__name__}", '
f'known options are {", ".join(f"{option}" for option in cls.name_lookup.values())}')
class PrieDieuWarp(DefaultOnToggle):
@@ -17,13 +35,12 @@ class CorpseHints(DefaultOnToggle):
class Difficulty(Choice):
"""Adjusts the logic required to defeat bosses.
Impossible: Removes all logic requirements for bosses. Good luck."""
"""Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses
and advanced movement tricks or glitches."""
display_name = "Difficulty"
option_easy = 0
option_normal = 1
option_hard = 2
option_impossible = 3
default = 1
@@ -32,9 +49,20 @@ class Penitence(Toggle):
display_name = "Penitence"
class ExpertLogic(Toggle):
"""Expands the logic used by the randomizer to allow for some difficult and/or lesser known tricks."""
display_name = "Expert Logic"
class StartingLocation(ChoiceIsRandom):
"""Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain
other options.
Specifically, Brotherhood and Mourning And Havoc cannot be chosen if Shuffle Dash is enabled, and Grievance Ascends
cannot be chosen if Shuffle Wall Climb is enabled."""
display_name = "Starting Location"
option_brotherhood = 0
option_albero = 1
option_convent = 2
option_grievance = 3
option_knot_of_words = 4
option_rooftops = 5
option_mourning_havoc = 6
default = 0
class Ending(Choice):
@@ -48,6 +76,13 @@ class Ending(Choice):
default = 0
class SkipLongQuests(Toggle):
"""Ensures that the rewards for long quests will be filler items.
Affected locations: \"Albero: Donate 50000 Tears\", \"Ossuary: 11th reward\", \"AtTotS: Miriam's gift\",
\"TSC: Jocinero's final reward\""""
display_name = "Skip Long Quests"
class ThornShuffle(Choice):
"""Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool."""
display_name = "Shuffle Thorn"
@@ -57,124 +92,33 @@ class ThornShuffle(Choice):
default = 0
class DashShuffle(Toggle):
"""Turns the ability to dash into an item that must be found in the multiworld."""
display_name = "Shuffle Dash"
class WallClimbShuffle(Toggle):
"""Turns the ability to climb walls with your sword into an item that must be found in the multiworld."""
display_name = "Shuffle Wall Climb"
class ReliquaryShuffle(DefaultOnToggle):
"""Adds the True Torment exclusive Reliquary rosary beads into the item pool."""
display_name = "Shuffle Penitence Rewards"
class CherubShuffle(DefaultOnToggle):
"""Shuffles Children of Moonlight into the item pool."""
display_name = "Shuffle Children of Moonlight"
class CustomItem1(Toggle):
"""Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes
and survive.
Must have the \"Blasphemous-Boots-of-Pleading\" mod installed to connect to a multiworld."""
display_name = "Boots of Pleading"
class LifeShuffle(DefaultOnToggle):
"""Shuffles life upgrades from the Lady of the Six Sorrows into the item pool."""
display_name = "Shuffle Life Upgrades"
class FervourShuffle(DefaultOnToggle):
"""Shuffles fervour upgrades from the Oil of the Pilgrims into the item pool."""
display_name = "Shuffle Fervour Upgrades"
class SwordShuffle(DefaultOnToggle):
"""Shuffles Mea Culpa upgrades from the Mea Culpa Altars into the item pool."""
display_name = "Shuffle Mea Culpa Upgrades"
class BlessingShuffle(DefaultOnToggle):
"""Shuffles blessings from the Lake of Silent Pilgrims into the item pool."""
display_name = "Shuffle Blessings"
class DungeonShuffle(DefaultOnToggle):
"""Shuffles rewards from completing Confessor Dungeons into the item pool."""
display_name = "Shuffle Dungeon Rewards"
class TirsoShuffle(DefaultOnToggle):
"""Shuffles rewards from delivering herbs to Tirso into the item pool."""
display_name = "Shuffle Tirso's Rewards"
class MiriamShuffle(DefaultOnToggle):
"""Shuffles the prayer given by Miriam into the item pool."""
display_name = "Shuffle Miriram's Reward"
class RedentoShuffle(DefaultOnToggle):
"""Shuffles rewards from assisting Redento into the item pool."""
display_name = "Shuffle Redento's Rewards"
class JocineroShuffle(DefaultOnToggle):
"""Shuffles rewards from rescuing 20 and 38 Children of Moonlight into the item pool."""
display_name = "Shuffle Jocinero's Rewards"
class AltasgraciasShuffle(DefaultOnToggle):
"""Shuffles the reward given by Altasgracias and the item left behind by them into the item pool."""
display_name = "Shuffle Altasgracias' Rewards"
class TentudiaShuffle(DefaultOnToggle):
"""Shuffles the rewards from delivering Tentudia's remains to Lvdovico into the item pool."""
display_name = "Shuffle Lvdovico's Rewards"
class GeminoShuffle(DefaultOnToggle):
"""Shuffles the rewards from Gemino's quest and the hidden tomb into the item pool."""
display_name = "Shuffle Gemino's Rewards"
class GuiltShuffle(DefaultOnToggle):
"""Shuffles the Weight of True Guilt into the item pool."""
display_name = "Shuffle Immaculate Bead"
class OssuaryShuffle(DefaultOnToggle):
"""Shuffles the rewards from delivering bones to the Ossuary into the item pool."""
display_name = "Shuffle Ossuary Rewards"
class BossShuffle(DefaultOnToggle):
"""Shuffles the Tears of Atonement from defeating bosses into the item pool."""
display_name = "Shuffle Boss Tears"
class WoundShuffle(DefaultOnToggle):
"""Shuffles the Holy Wounds required to pass the Bridge of the Three Cavalries into the item pool."""
display_name = "Shuffle Holy Wounds"
class MaskShuffle(DefaultOnToggle):
"""Shuffles the masks required to use the elevator in Archcathedral Rooftops into the item pool."""
display_name = "Shuffle Masks"
class EyeShuffle(DefaultOnToggle):
"""Shuffles the Eyes of the Traitor from defeating Isidora and Sierpes into the item pool."""
display_name = "Shuffle Traitor's Eyes"
class HerbShuffle(DefaultOnToggle):
"""Shuffles the herbs required for Tirso's quest into the item pool."""
display_name = "Shuffle Herbs"
class ChurchShuffle(DefaultOnToggle):
"""Shuffles the rewards from donating 5,000 and 50,000 Tears of Atonement to the Church in Albero into the item pool."""
display_name = "Shuffle Donation Rewards"
class ShopShuffle(DefaultOnToggle):
"""Shuffles the items sold in Candelaria's shops into the item pool."""
display_name = "Shuffle Shop Items"
class CandleShuffle(DefaultOnToggle):
"""Shuffles the Beads of Wax and their upgrades into the item pool."""
display_name = "Shuffle Candles"
class CustomItem2(Toggle):
"""Adds the custom relic Purified Hand of the Nun into the item pool, which grants the ability to jump
a second time in mid-air.
Must have the \"Blasphemous-Double-Jump\" mod installed to connect to a multiworld."""
display_name = "Purified Hand of the Nun"
class StartWheel(Toggle):
@@ -189,7 +133,8 @@ class SkillRando(Toggle):
class EnemyRando(Choice):
"""Randomizes the enemies that appear in each room.
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in a standard game.
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in
a standard game.
Randomized: Every enemy is completely random, and can appear any number of times.
Some enemies will never be randomized."""
display_name = "Enemy Randomizer"
@@ -223,37 +168,20 @@ blasphemous_options = {
"corpse_hints": CorpseHints,
"difficulty": Difficulty,
"penitence": Penitence,
"expert_logic": ExpertLogic,
"starting_location": StartingLocation,
"ending": Ending,
"skip_long_quests": SkipLongQuests,
"thorn_shuffle" : ThornShuffle,
"dash_shuffle": DashShuffle,
"wall_climb_shuffle": WallClimbShuffle,
"reliquary_shuffle": ReliquaryShuffle,
"cherub_shuffle" : CherubShuffle,
"life_shuffle" : LifeShuffle,
"fervour_shuffle" : FervourShuffle,
"sword_shuffle" : SwordShuffle,
"blessing_shuffle" : BlessingShuffle,
"dungeon_shuffle" : DungeonShuffle,
"tirso_shuffle" : TirsoShuffle,
"miriam_shuffle" : MiriamShuffle,
"redento_shuffle" : RedentoShuffle,
"jocinero_shuffle" : JocineroShuffle,
"altasgracias_shuffle" : AltasgraciasShuffle,
"tentudia_shuffle" : TentudiaShuffle,
"gemino_shuffle" : GeminoShuffle,
"guilt_shuffle" : GuiltShuffle,
"ossuary_shuffle" : OssuaryShuffle,
"boss_shuffle" : BossShuffle,
"wound_shuffle" : WoundShuffle,
"mask_shuffle" : MaskShuffle,
"eye_shuffle": EyeShuffle,
"herb_shuffle" : HerbShuffle,
"church_shuffle" : ChurchShuffle,
"shop_shuffle" : ShopShuffle,
"candle_shuffle" : CandleShuffle,
"boots_of_pleading": CustomItem1,
"purified_hand": CustomItem2,
"start_wheel": StartWheel,
"skill_randomizer": SkillRando,
"enemy_randomizer": EnemyRando,
"enemy_groups": EnemyGroups,
"enemy_scaling": EnemyScaling,
"death_link": BlasphemousDeathLink
"death_link": BlasphemousDeathLink,
"start_inventory": StartInventoryPool
}

5405
worlds/blasphemous/Rooms.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,205 +7,14 @@ unrandomized_dict: Dict[str, str] = {
"DC: Chalice room": "Chalice of Inverted Verses"
}
cherub_set: Set[str] = [
"Albero: Child of Moonlight",
"AR: Upper west shaft Child of Moonlight",
"BotSS: Starting room Child of Moonlight",
"DC: Child of Moonlight, above water",
"DC: Upper east Child of Moonlight",
"DC: Child of Moonlight, miasma room",
"DC: Child of Moonlight, behind pillar",
"DC: Top of elevator Child of Moonlight",
"DC: Elevator shaft Child of Moonlight",
"GotP: Shop cave Child of Moonlight",
"GotP: Elevator shaft Child of Moonlight",
"GotP: West shaft Child of Moonlight",
"GotP: Center shaft Child of Moonlight",
"GA: Miasma room Child of Moonlight",
"GA: Blood bridge Child of Moonlight",
"GA: Lower east Child of Moonlight",
"Jondo: Upper east Child of Moonlight",
"Jondo: Spike tunnel Child of Moonlight",
"Jondo: Upper west Child of Moonlight",
"LotNW: Platform room Child of Moonlight",
"LotNW: Lowest west Child of Moonlight",
"LotNW: Elevator Child of Moonlight",
"MD: Second area Child of Moonlight",
"MD: Cave Child of Moonlight",
"MoM: Lower west Child of Moonlight",
"MoM: Upper center Child of Moonlight",
"MotED: Child of Moonlight, above chasm",
"PotSS: First area Child of Moonlight",
"PotSS: Third area Child of Moonlight",
"THL: Child of Moonlight",
"WotHP: Upper east room, top bronze cell",
"WotHP: Upper west room, top silver cell",
"WotHP: Lower east room, bottom silver cell",
"WotHP: Outside Child of Moonlight",
"WotBC: Outside Child of Moonlight",
"WotBC: Cliffside Child of Moonlight",
"WOTW: Underground Child of Moonlight",
"WOTW: Upper east Child of Moonlight",
junk_locations: Set[str] = [
"Albero: Donate 50000 Tears",
"Ossuary: 11th reward",
"AtTotS: Miriam's gift",
"TSC: Jocinero's final reward"
]
life_set: Set[str] = [
"AR: Lady of the Six Sorrows",
"CoOLotCV: Lady of the Six Sorrows",
"DC: Lady of the Six Sorrows, from MD",
"DC: Lady of the Six Sorrows, elevator shaft",
"GotP: Lady of the Six Sorrows",
"LotNW: Lady of the Six Sorrows"
]
fervour_set: Set[str] = [
"DC: Oil of the Pilgrims",
"GotP: Oil of the Pilgrims",
"GA: Oil of the Pilgrims",
"LotNW: Oil of the Pilgrims",
"MoM: Oil of the Pilgrims",
"WotHP: Oil of the Pilgrims"
]
sword_set: Set[str] = [
"Albero: Mea Culpa altar",
"AR: Mea Culpa altar",
"BotSS: Mea Culpa altar",
"CoOLotCV: Mea Culpa altar",
"DC: Mea Culpa altar",
"LotNW: Mea Culpa altar",
"MoM: Mea Culpa altar"
]
blessing_dict: Dict[str, str] = {
"Albero: Bless Severed Hand": "Incorrupt Hand of the Fraternal Master",
"Albero: Bless Linen Cloth": "Shroud of Dreamt Sins",
"Albero: Bless Hatched Egg": "Three Gnarled Tongues"
}
dungeon_dict: Dict[str, str] = {
"Confessor Dungeon 1 extra": "Tears of Atonement (1000)",
"Confessor Dungeon 2 extra": "Heart of the Single Tone",
"Confessor Dungeon 3 extra": "Tears of Atonement (3000)",
"Confessor Dungeon 4 extra": "Embers of a Broken Star",
"Confessor Dungeon 5 extra": "Tears of Atonement (5000)",
"Confessor Dungeon 6 extra": "Scaly Coin",
"Confessor Dungeon 7 extra": "Seashell of the Inverted Spiral"
}
tirso_dict: Dict[str, str] = {
"Albero: Tirso's 1st reward": "Linen Cloth",
"Albero: Tirso's 2nd reward": "Tears of Atonement (500)",
"Albero: Tirso's 3rd reward": "Tears of Atonement (1000)",
"Albero: Tirso's 4th reward": "Tears of Atonement (2000)",
"Albero: Tirso's 5th reward": "Tears of Atonement (5000)",
"Albero: Tirso's 6th reward": "Tears of Atonement (10000)",
"Albero: Tirso's final reward": "Knot of Rosary Rope"
}
redento_dict: Dict[str, str] = {
"MoM: Redento's treasure": "Nail Uprooted from Dirt",
"MoM: Final meeting with Redento": "Knot of Rosary Rope",
"MotED: 1st meeting with Redento": "Fourth Toe made of Limestone",
"PotSS: 4th meeting with Redento": "Big Toe made of Limestone",
"WotBC: 3rd meeting with Redento": "Little Toe made of Limestone"
}
jocinero_dict: Dict[str, str] = {
"TSC: Jocinero's 1st reward": "Linen of Golden Thread",
"TSC: Jocinero's final reward": "Campanillero to the Sons of the Aurora"
}
altasgracias_dict: Dict[str, str] = {
"GA: Altasgracias' gift": "Egg of Deformity",
"GA: Empty giant egg": "Knot of Hair"
}
tentudia_dict: Dict[str, str] = {
"Albero: Lvdovico's 1st reward": "Tears of Atonement (500)",
"Albero: Lvdovico's 2nd reward": "Tears of Atonement (1000)",
"Albero: Lvdovico's 3rd reward": "Debla of the Lights"
}
gemino_dict: Dict[str, str] = {
"WOTW: Gift for the tomb": "Dried Flowers bathed in Tears",
"WOTW: Underground tomb": "Saeta Dolorosa",
"WOTW: Gemino's gift": "Empty Golden Thimble",
"WOTW: Gemino's reward": "Frozen Olive"
}
ossuary_dict: Dict[str, str] = {
"Ossuary: 1st reward": "Tears of Atonement (250)",
"Ossuary: 2nd reward": "Tears of Atonement (500)",
"Ossuary: 3rd reward": "Tears of Atonement (750)",
"Ossuary: 4th reward": "Tears of Atonement (1000)",
"Ossuary: 5th reward": "Tears of Atonement (1250)",
"Ossuary: 6th reward": "Tears of Atonement (1500)",
"Ossuary: 7th reward": "Tears of Atonement (1750)",
"Ossuary: 8th reward": "Tears of Atonement (2000)",
"Ossuary: 9th reward": "Tears of Atonement (2500)",
"Ossuary: 10th reward": "Tears of Atonement (3000)",
"Ossuary: 11th reward": "Tears of Atonement (5000)",
}
boss_dict: Dict[str, str] = {
"BotTC: Esdras, of the Anointed Legion": "Tears of Atonement (4300)",
"BotSS: Warden of the Silent Sorrow": "Tears of Atonement (300)",
"CoOLotCV: Our Lady of the Charred Visage": "Tears of Atonement (2600)",
"HotD: Laudes, the First of the Amanecidas": "Tears of Atonement (30000)",
"GotP: Amanecida of the Bejeweled Arrow": "Tears of Atonement (18000)",
"GA: Tres Angustias": "Tears of Atonement (2100)",
"MD: Ten Piedad": "Tears of Atonement (625)",
"MoM: Melquiades, The Exhumed Archbishop": "Tears of Atonement (5500)",
"MotED: Amanecida of the Golden Blades": "Tears of Atonement (18000)",
"MaH: Sierpes": "Tears of Atonement (5000)",
"PotSS: Amanecida of the Chiselled Steel": "Tears of Atonement (18000)",
"TSC: Exposito, Scion of Abjuration": "Tears of Atonement (9000)",
"WotHP: Quirce, Returned By The Flames": "Tears of Atonement (11250)",
"WotHP: Amanecida of the Molten Thorn": "Tears of Atonement (18000)"
}
wound_dict: Dict[str, str] = {
"CoOLotCV: Visage of Compunction": "Holy Wound of Compunction",
"GA: Visage of Contrition": "Holy Wound of Contrition",
"MD: Visage of Attrition": "Holy Wound of Attrition"
}
mask_dict: Dict[str, str] = {
"CoOLotCV: Mask room": "Mirrored Mask of Dolphos",
"LotNW: Mask room": "Embossed Mask of Crescente",
"MoM: Mask room": "Deformed Mask of Orestes"
}
eye_dict: Dict[str, str] = {
"Ossuary: Isidora, Voice of the Dead": "Severed Right Eye of the Traitor",
"MaH: Sierpes' eye": "Broken Left Eye of the Traitor"
}
herb_dict: Dict[str, str] = {
"Albero: Gate of Travel room": "Bouquet of Thyme",
"Jondo: Lower east bell trap": "Bouquet of Rosemary",
"MotED: Blood platform alcove": "Dried Clove",
"PotSS: Third area lower ledge": "Olive Seeds",
"TSC: Painting ladder ledge": "Sooty Garlic",
"WOTW: Entrance to tomb": "Incense Garlic"
}
church_dict: Dict[str, str] = {
"Albero: Donate 5000 Tears": "Token of Appreciation",
"Albero: Donate 50000 Tears": "Cloistered Ruby"
}
shop_dict: Dict[str, str] = {
"GotP: Shop item 1": "Torn Bridal Ribbon",
"GotP: Shop item 2": "Calcified Eye of Erudition",
"GotP: Shop item 3": "Ember of the Holy Cremation",
"MD: Shop item 1": "Key to the Chamber of the Eldest Brother",
"MD: Shop item 2": "Hollow Pearl",
"MD: Shop item 3": "Moss Preserved in Glass",
"TSC: Shop item 1": "Wicker Knot",
"TSC: Shop item 2": "Empty Bile Vessel",
"TSC: Shop item 3": "Key of the Inquisitor"
}
thorn_set: Set[str] = {
"THL: Deogracias' gift",
@@ -218,14 +27,6 @@ thorn_set: Set[str] = {
"Confessor Dungeon 7 main",
}
candle_dict: Dict[str, str] = {
"CoOLotCV: Red candle": "Bead of Red Wax",
"LotNW: Red candle": "Bead of Red Wax",
"MD: Red candle": "Bead of Red Wax",
"BotSS: Blue candle": "Bead of Blue Wax",
"CoOLotCV: Blue candle": "Bead of Blue Wax",
"MD: Blue candle": "Bead of Blue Wax"
}
skill_dict: Dict[str, str] = {
"Skill 1, Tier 1": "Combo Skill",

View File

@@ -1,14 +1,14 @@
from typing import Dict, Set, List, Any
from typing import Dict, List, Set, Any
from collections import Counter
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
from ..AutoWorld import World, WebWorld
from .Items import base_id, item_table, group_table, tears_set, reliquary_set, skill_set
from .Locations import location_table, shop_set
from .Exits import region_exit_table, exit_lookup_table
from worlds.AutoWorld import World, WebWorld
from .Items import base_id, item_table, group_table, tears_set, reliquary_set, event_table
from .Locations import location_table
from .Rooms import room_table, door_table
from .Rules import rules
from worlds.generic.Rules import set_rule
from worlds.generic.Rules import set_rule, add_rule
from .Options import blasphemous_options
from . import Vanilla
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
class BlasphemousWeb(WebWorld):
@@ -32,7 +32,7 @@ class BlasphemousWorld(World):
game: str = "Blasphemous"
web = BlasphemousWeb()
data_version = 1
data_version = 2
item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)}
location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)}
@@ -41,9 +41,20 @@ class BlasphemousWorld(World):
item_name_groups = group_table
option_definitions = blasphemous_options
required_client_version = (0, 4, 2)
def __init__(self, multiworld, player):
super(BlasphemousWorld, self).__init__(multiworld, player)
self.start_room: str = "D17Z01S01"
self.door_connections: Dict[str, str] = {}
def set_rules(self):
rules(self)
for door in door_table:
add_rule(self.multiworld.get_location(door["Id"], self.player),
lambda state: state.can_reach(self.get_connected_door(door["Id"])), self.player)
def create_item(self, name: str) -> "BlasphemousItem":
@@ -61,102 +72,134 @@ class BlasphemousWorld(World):
return self.multiworld.random.choice(tears_set)
def create_items(self):
placed_items = []
def generate_early(self):
world = self.multiworld
player = self.player
placed_items.extend(Vanilla.unrandomized_dict.values())
if not world.starting_location[player].randomized:
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
" cannot be chosen if Difficulty is lower than Hard.")
if not self.multiworld.reliquary_shuffle[self.player]:
placed_items.extend(reliquary_set)
elif self.multiworld.reliquary_shuffle[self.player]:
placed_items.append("Tears of Atonement (250)")
placed_items.append("Tears of Atonement (300)")
placed_items.append("Tears of Atonement (500)")
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
and world.dash_shuffle[player]:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
" cannot be chosen if Shuffle Dash is enabled.")
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
" cannot be chosen if Shuffle Wall Climb is enabled.")
else:
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
invalid: bool = False
if not self.multiworld.cherub_shuffle[self.player]:
for i in range(38):
placed_items.append("Child of Moonlight")
if world.difficulty[player].value < 2:
locations.remove(6)
if not self.multiworld.life_shuffle[self.player]:
for i in range(6):
placed_items.append("Life Upgrade")
if world.dash_shuffle[player]:
locations.remove(0)
if 6 in locations:
locations.remove(6)
if not self.multiworld.fervour_shuffle[self.player]:
for i in range(6):
placed_items.append("Fervour Upgrade")
if world.wall_climb_shuffle[player]:
locations.remove(3)
if not self.multiworld.sword_shuffle[self.player]:
for i in range(7):
placed_items.append("Mea Culpa Upgrade")
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
invalid = True
if not self.multiworld.blessing_shuffle[self.player]:
placed_items.extend(Vanilla.blessing_dict.values())
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
and world.dash_shuffle[player]:
invalid = True
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
invalid = True
if not self.multiworld.dungeon_shuffle[self.player]:
placed_items.extend(Vanilla.dungeon_dict.values())
if not self.multiworld.tirso_shuffle[self.player]:
placed_items.extend(Vanilla.tirso_dict.values())
if not self.multiworld.miriam_shuffle[self.player]:
placed_items.append("Cantina of the Blue Rose")
if not self.multiworld.redento_shuffle[self.player]:
placed_items.extend(Vanilla.redento_dict.values())
if not self.multiworld.jocinero_shuffle[self.player]:
placed_items.extend(Vanilla.jocinero_dict.values())
if invalid:
world.starting_location[player].value = world.random.choice(locations)
if not self.multiworld.altasgracias_shuffle[self.player]:
placed_items.extend(Vanilla.altasgracias_dict.values())
if not world.dash_shuffle[player]:
world.push_precollected(self.create_item("Dash Ability"))
if not self.multiworld.tentudia_shuffle[self.player]:
placed_items.extend(Vanilla.tentudia_dict.values())
if not world.wall_climb_shuffle[player]:
world.push_precollected(self.create_item("Wall Climb Ability"))
if not self.multiworld.gemino_shuffle[self.player]:
placed_items.extend(Vanilla.gemino_dict.values())
if world.skip_long_quests[player]:
for loc in junk_locations:
world.exclude_locations[player].value.add(loc)
if not self.multiworld.guilt_shuffle[self.player]:
placed_items.append("Weight of True Guilt")
start_rooms: Dict[int, str] = {
0: "D17Z01S01",
1: "D01Z02S01",
2: "D02Z03S09",
3: "D03Z03S11",
4: "D04Z03S01",
5: "D06Z01S09",
6: "D20Z02S09"
}
if not self.multiworld.ossuary_shuffle[self.player]:
placed_items.extend(Vanilla.ossuary_dict.values())
self.start_room = start_rooms[world.starting_location[player].value]
if not self.multiworld.boss_shuffle[self.player]:
placed_items.extend(Vanilla.boss_dict.values())
if not self.multiworld.wound_shuffle[self.player]:
placed_items.extend(Vanilla.wound_dict.values())
def create_items(self):
world = self.multiworld
player = self.player
if not self.multiworld.mask_shuffle[self.player]:
placed_items.extend(Vanilla.mask_dict.values())
removed: int = 0
to_remove: List[str] = [
"Tears of Atonement (250)",
"Tears of Atonement (300)",
"Tears of Atonement (500)",
"Tears of Atonement (500)",
"Tears of Atonement (500)"
]
if not self.multiworld.eye_shuffle[self.player]:
placed_items.extend(Vanilla.eye_dict.values())
skipped_items = []
junk: int = 0
if not self.multiworld.herb_shuffle[self.player]:
placed_items.extend(Vanilla.herb_dict.values())
for item, count in world.start_inventory[player].value.items():
for _ in range(count):
skipped_items.append(item)
junk += 1
if not self.multiworld.church_shuffle[self.player]:
placed_items.extend(Vanilla.church_dict.values())
skipped_items.extend(unrandomized_dict.values())
if not self.multiworld.shop_shuffle[self.player]:
placed_items.extend(Vanilla.shop_dict.values())
if self.multiworld.thorn_shuffle[self.player] == 2:
if world.thorn_shuffle[player] == 2:
for i in range(8):
placed_items.append("Thorn Upgrade")
skipped_items.append("Thorn Upgrade")
if not self.multiworld.candle_shuffle[self.player]:
placed_items.extend(Vanilla.candle_dict.values())
if world.dash_shuffle[player]:
skipped_items.append(to_remove[removed])
removed += 1
elif not world.dash_shuffle[player]:
skipped_items.append("Dash Ability")
if self.multiworld.start_wheel[self.player]:
placed_items.append("The Young Mason's Wheel")
if world.wall_climb_shuffle[player]:
skipped_items.append(to_remove[removed])
removed += 1
elif not world.wall_climb_shuffle[player]:
skipped_items.append("Wall Climb Ability")
if not self.multiworld.skill_randomizer[self.player]:
placed_items.extend(Vanilla.skill_dict.values())
if not world.reliquary_shuffle[player]:
skipped_items.extend(reliquary_set)
elif world.reliquary_shuffle[player]:
for i in range(3):
skipped_items.append(to_remove[removed])
removed += 1
counter = Counter(placed_items)
if not world.boots_of_pleading[player]:
skipped_items.append("Boots of Pleading")
if not world.purified_hand[player]:
skipped_items.append("Purified Hand of the Nun")
if world.start_wheel[player]:
skipped_items.append("The Young Mason's Wheel")
if not world.skill_randomizer[player]:
skipped_items.extend(skill_dict.values())
counter = Counter(skipped_items)
pool = []
@@ -169,95 +212,30 @@ class BlasphemousWorld(World):
for i in range(count):
pool.append(self.create_item(item["name"]))
self.multiworld.itempool += pool
for _ in range(junk):
pool.append(self.create_item(self.get_filler_item_name()))
world.itempool += pool
def pre_fill(self):
self.place_items_from_dict(Vanilla.unrandomized_dict)
world = self.multiworld
player = self.player
if not self.multiworld.cherub_shuffle[self.player]:
self.place_items_from_set(Vanilla.cherub_set, "Child of Moonlight")
self.place_items_from_dict(unrandomized_dict)
if not self.multiworld.life_shuffle[self.player]:
self.place_items_from_set(Vanilla.life_set, "Life Upgrade")
if world.thorn_shuffle[player] == 2:
self.place_items_from_set(thorn_set, "Thorn Upgrade")
if not self.multiworld.fervour_shuffle[self.player]:
self.place_items_from_set(Vanilla.fervour_set, "Fervour Upgrade")
if not self.multiworld.sword_shuffle[self.player]:
self.place_items_from_set(Vanilla.sword_set, "Mea Culpa Upgrade")
if not self.multiworld.blessing_shuffle[self.player]:
self.place_items_from_dict(Vanilla.blessing_dict)
if not self.multiworld.dungeon_shuffle[self.player]:
self.place_items_from_dict(Vanilla.dungeon_dict)
if not self.multiworld.tirso_shuffle[self.player]:
self.place_items_from_dict(Vanilla.tirso_dict)
if not self.multiworld.miriam_shuffle[self.player]:
self.multiworld.get_location("AtTotS: Miriam's gift", self.player)\
.place_locked_item(self.create_item("Cantina of the Blue Rose"))
if not self.multiworld.redento_shuffle[self.player]:
self.place_items_from_dict(Vanilla.redento_dict)
if not self.multiworld.jocinero_shuffle[self.player]:
self.place_items_from_dict(Vanilla.jocinero_dict)
if not self.multiworld.altasgracias_shuffle[self.player]:
self.place_items_from_dict(Vanilla.altasgracias_dict)
if not self.multiworld.tentudia_shuffle[self.player]:
self.place_items_from_dict(Vanilla.tentudia_dict)
if not self.multiworld.gemino_shuffle[self.player]:
self.place_items_from_dict(Vanilla.gemino_dict)
if not self.multiworld.guilt_shuffle[self.player]:
self.multiworld.get_location("GotP: Confessor Dungeon room", self.player)\
.place_locked_item(self.create_item("Weight of True Guilt"))
if not self.multiworld.ossuary_shuffle[self.player]:
self.place_items_from_dict(Vanilla.ossuary_dict)
if not self.multiworld.boss_shuffle[self.player]:
self.place_items_from_dict(Vanilla.boss_dict)
if not self.multiworld.wound_shuffle[self.player]:
self.place_items_from_dict(Vanilla.wound_dict)
if not self.multiworld.mask_shuffle[self.player]:
self.place_items_from_dict(Vanilla.mask_dict)
if not self.multiworld.eye_shuffle[self.player]:
self.place_items_from_dict(Vanilla.eye_dict)
if not self.multiworld.herb_shuffle[self.player]:
self.place_items_from_dict(Vanilla.herb_dict)
if not self.multiworld.church_shuffle[self.player]:
self.place_items_from_dict(Vanilla.church_dict)
if not self.multiworld.shop_shuffle[self.player]:
self.place_items_from_dict(Vanilla.shop_dict)
if self.multiworld.thorn_shuffle[self.player] == 2:
self.place_items_from_set(Vanilla.thorn_set, "Thorn Upgrade")
if not self.multiworld.candle_shuffle[self.player]:
self.place_items_from_dict(Vanilla.candle_dict)
if self.multiworld.start_wheel[self.player]:
self.multiworld.get_location("BotSS: Beginning gift", self.player)\
if world.start_wheel[player]:
world.get_location("Beginning gift", player)\
.place_locked_item(self.create_item("The Young Mason's Wheel"))
if not self.multiworld.skill_randomizer[self.player]:
self.place_items_from_dict(Vanilla.skill_dict)
if not world.skill_randomizer[player]:
self.place_items_from_dict(skill_dict)
if self.multiworld.thorn_shuffle[self.player] == 1:
self.multiworld.local_items[self.player].value.add("Thorn Upgrade")
if world.thorn_shuffle[player] == 1:
world.local_items[player].value.add("Thorn Upgrade")
def place_items_from_set(self, location_set: Set[str], name: str):
@@ -273,133 +251,142 @@ class BlasphemousWorld(World):
def create_regions(self) -> None:
player = self.player
world = self.multiworld
region_table: Dict[str, Region] = {
"menu" : Region("Menu", player, world),
"albero" : Region("Albero", player, world),
"attots" : Region("All the Tears of the Sea", player, world),
"ar" : Region("Archcathedral Rooftops", player, world),
"bottc" : Region("Bridge of the Three Cavalries", player, world),
"botss" : Region("Brotherhood of the Silent Sorrow", player, world),
"coolotcv": Region("Convent of Our Lady of the Charred Visage", player, world),
"dohh" : Region("Deambulatory of His Holiness", player, world),
"dc" : Region("Desecrated Cistern", player, world),
"eos" : Region("Echoes of Salt", player, world),
"ft" : Region("Ferrous Tree", player, world),
"gotp" : Region("Graveyard of the Peaks", player, world),
"ga" : Region("Grievance Ascends", player, world),
"hotd" : Region("Hall of the Dawning", player, world),
"jondo" : Region("Jondo", player, world),
"kottw" : Region("Knot of the Three Words", player, world),
"lotnw" : Region("Library of the Negated Words", player, world),
"md" : Region("Mercy Dreams", player, world),
"mom" : Region("Mother of Mothers", player, world),
"moted" : Region("Mountains of the Endless Dusk", player, world),
"mah" : Region("Mourning and Havoc", player, world),
"potss" : Region("Patio of the Silent Steps", player, world),
"petrous" : Region("Petrous", player, world),
"thl" : Region("The Holy Line", player, world),
"trpots" : Region("The Resting Place of the Sister", player, world),
"tsc" : Region("The Sleeping Canvases", player, world),
"wothp" : Region("Wall of the Holy Prohibitions", player, world),
"wotbc" : Region("Wasteland of the Buried Churches", player, world),
"wotw" : Region("Where Olive Trees Wither", player, world),
"dungeon" : Region("Dungeons", player, world)
}
for rname, reg in region_table.items():
world.regions.append(reg)
for ename, exits in region_exit_table.items():
if ename == rname:
for i in exits:
ent = Entrance(player, i, reg)
reg.exits.append(ent)
for e, r in exit_lookup_table.items():
if i == e:
ent.connect(region_table[r])
for loc in location_table:
id = base_id + location_table.index(loc)
region_table[loc["region"]].locations\
.append(BlasphemousLocation(self.player, loc["name"], id, region_table[loc["region"]]))
victory = Location(self.player, "His Holiness Escribar", None, self.multiworld.get_region("Deambulatory of His Holiness", self.player))
victory.place_locked_item(self.create_event("Victory"))
self.multiworld.get_region("Deambulatory of His Holiness", self.player).locations.append(victory)
menu_region = Region("Menu", player, world)
misc_region = Region("Misc", player, world)
world.regions += [menu_region, misc_region]
if self.multiworld.ending[self.player].value == 1:
for room in room_table:
region = Region(room, player, world)
world.regions.append(region)
menu_region.add_exits({self.start_room: "New Game"})
world.get_region(self.start_room, player).add_exits({"Misc": "Misc"})
for door in door_table:
if door.get("OriginalDoor") is None:
continue
else:
if not door["Id"] in self.door_connections.keys():
self.door_connections[door["Id"]] = door["OriginalDoor"]
self.door_connections[door["OriginalDoor"]] = door["Id"]
parent_region: Region = self.get_room_from_door(door["Id"])
target_region: Region = self.get_room_from_door(door["OriginalDoor"])
parent_region.add_exits({
target_region.name: door["Id"]
}, {
target_region.name: lambda x: door.get("VisibilityFlags") != 1
})
for index, loc in enumerate(location_table):
if not world.boots_of_pleading[player] and loc["name"] == "BotSS: 2nd meeting with Redento":
continue
if not world.purified_hand[player] and loc["name"] == "MoM: Western room ledge":
continue
region: Region = world.get_region(loc["room"], player)
region.add_locations({loc["name"]: base_id + index})
#id = base_id + location_table.index(loc)
#reg.locations.append(BlasphemousLocation(player, loc["name"], id, reg))
for e, r in event_table.items():
region: Region = world.get_region(r, player)
event = BlasphemousLocation(player, e, None, region)
event.show_in_spoiler = False
event.place_locked_item(self.create_event(e))
region.locations.append(event)
for door in door_table:
region: Region = self.get_room_from_door(self.door_connections[door["Id"]])
event = BlasphemousLocation(player, door["Id"], None, region)
event.show_in_spoiler = False
event.place_locked_item(self.create_event(door["Id"]))
region.locations.append(event)
victory = Location(player, "His Holiness Escribar", None, world.get_region("D07Z01S03", player))
victory.place_locked_item(self.create_event("Victory"))
world.get_region("D07Z01S03", player).locations.append(victory)
if world.ending[self.player].value == 1:
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
elif self.multiworld.ending[self.player].value == 2:
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and \
elif world.ending[self.player].value == 2:
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and
state.has("Holy Wound of Abnegation", player))
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
world.completion_condition[self.player] = lambda state: state.has("Victory", player)
def get_room_from_door(self, door: str) -> Region:
return self.multiworld.get_region(door.split("[")[0], self.player)
def get_connected_door(self, door: str) -> Entrance:
return self.multiworld.get_entrance(self.door_connections[door], self.player)
def fill_slot_data(self) -> Dict[str, Any]:
slot_data: Dict[str, Any] = {}
locations = []
doors: Dict[str, str] = {}
for loc in self.multiworld.get_filled_locations(self.player):
if loc.name == "His Holiness Escribar":
world = self.multiworld
player = self.player
thorns: bool = True
if world.thorn_shuffle[player].value == 2:
thorns = False
for loc in world.get_filled_locations(player):
if loc.item.code == None:
continue
else:
data = {
"id": self.location_name_to_game_id[loc.name],
"ap_id": loc.address,
"name": loc.item.name,
"player_name": self.multiworld.player_name[loc.item.player]
"player_name": world.player_name[loc.item.player],
"type": int(loc.item.classification)
}
if loc.name in shop_set:
data["type"] = loc.item.classification.name
locations.append(data)
config = {
"versionCreated": "AP",
"general": {
"teleportationAlwaysUnlocked": bool(self.multiworld.prie_dieu_warp[self.player].value),
"skipCutscenes": bool(self.multiworld.skip_cutscenes[self.player].value),
"enablePenitence": bool(self.multiworld.penitence[self.player].value),
"hardMode": False,
"customSeed": 0,
"allowHints": bool(self.multiworld.corpse_hints[self.player].value)
},
"items": {
"type": 1,
"lungDamage": False,
"disableNPCDeath": True,
"startWithWheel": bool(self.multiworld.start_wheel[self.player].value),
"shuffleReliquaries": bool(self.multiworld.reliquary_shuffle[self.player].value)
},
"enemies": {
"type": self.multiworld.enemy_randomizer[self.player].value,
"maintainClass": bool(self.multiworld.enemy_groups[self.player].value),
"areaScaling": bool(self.multiworld.enemy_scaling[self.player].value)
},
"prayers": {
"type": 0,
"removeMirabis": False
},
"doors": {
"type": 0
},
"debug": {
"type": 0
}
"LogicDifficulty": world.difficulty[player].value,
"StartingLocation": world.starting_location[player].value,
"VersionCreated": "AP",
"UnlockTeleportation": bool(world.prie_dieu_warp[player].value),
"AllowHints": bool(world.corpse_hints[player].value),
"AllowPenitence": bool(world.penitence[player].value),
"ShuffleReliquaries": bool(world.reliquary_shuffle[player].value),
"ShuffleBootsOfPleading": bool(world.boots_of_pleading[player].value),
"ShufflePurifiedHand": bool(world.purified_hand[player].value),
"ShuffleDash": bool(world.dash_shuffle[player].value),
"ShuffleWallClimb": bool(world.wall_climb_shuffle[player].value),
"ShuffleSwordSkills": bool(world.skill_randomizer[player].value),
"ShuffleThorns": thorns,
"JunkLongQuests": bool(world.skip_long_quests[player].value),
"StartWithWheel": bool(world.start_wheel[player].value),
"EnemyShuffleType": world.enemy_randomizer[player].value,
"MaintainClass": bool(world.enemy_groups[player].value),
"AreaScaling": bool(world.enemy_scaling[player].value),
"BossShuffleType": 0,
"DoorShuffleType": 0
}
slot_data = {
"locations": locations,
"doors": doors,
"cfg": config,
"ending": self.multiworld.ending[self.player].value,
"death_link": bool(self.multiworld.death_link[self.player].value)
"ending": world.ending[self.player].value,
"death_link": bool(world.death_link[self.player].value)
}
return slot_data

View File

@@ -1,24 +1,49 @@
# Blasphemous Multiworld Setup Guide
## Required Software
## Useful Links
- Blasphemous from: [Steam](https://store.steampowered.com/app/774361/Blasphemous/)
- Blasphemous Modding API from: [GitHub](https://github.com/BrandenEK/Blasphemous-Modding-API)
- Blasphemous Randomizer from: [GitHub](https://github.com/BrandenEK/Blasphemous-Randomizer)
- Blasphemous Multiworld from: [GitHub](https://github.com/BrandenEK/Blasphemous-Multiworld)
- (*Optional*) PopTracker Pack from: [GitHub](https://github.com/sassyvania/Blasphemous-Randomizer-Maptracker)
Required:
- Blasphemous: [Steam](https://store.steampowered.com/app/774361/Blasphemous/)
- The GOG version of Blasphemous will also work.
- Blasphemous Mod Installer: [GitHub](https://github.com/BrandenEK/Blasphemous-Mod-Installer)
- Blasphemous Modding API: [GitHub](https://github.com/BrandenEK/Blasphemous-Modding-API)
- Blasphemous Randomizer: [GitHub](https://github.com/BrandenEK/Blasphemous-Randomizer)
- Blasphemous Multiworld: [GitHub](https://github.com/BrandenEK/Blasphemous-Multiworld)
## Instructions (Windows)
Optional:
- In-game map tracker: [GitHub](https://github.com/BrandenEK/Blasphemous-Rando-Map)
- Quick Prie Dieu warp mod: [GitHub](https://github.com/BadMagic100/Blasphemous-PrieWarp)
- Boots of Pleading mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Boots-of-Pleading)
- Double Jump mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Double-Jump)
- PopTracker pack: [GitHub](https://github.com/sassyvania/Blasphemous-Randomizer-Maptracker)
1. Download the [Modding API](https://github.com/BrandenEK/Blasphemous-Modding-API/releases), and follow the [installation instructions](https://github.com/BrandenEK/Blasphemous-Modding-API#installation) on the GitHub page.
## Mod Installer (Recommended)
2. After the Modding API has been installed, download the [Randomizer](https://github.com/BrandenEK/Blasphemous-Randomizer/releases) and [Multiworld](https://github.com/BrandenEK/Blasphemous-Multiworld/releases) archives, and extract the contents of both into the `Modding` folder.
1. Download the [Mod Installer](https://github.com/BrandenEK/Blasphemous-Mod-Installer),
and point it to your install directory for Blasphemous.
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both the Randomizer and Multiworld on the title screen.
2. Install the `Modding API`, `Randomizer`, and `Multiworld` mods. Optionally, you can also install the
`Rando Map`, `PrieWarp`, `Boots of Pleading`, and `Double Jump` mods, and set up the PopTracker pack if desired.
4. (*Optional*) Add the Blasphemous pack to PopTracker. In game, open the console by pressing backslash `\` and type `randomizer autotracker on` to automatically connect the game to PopTracker.
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both
the Randomizer and Multiworld on the title screen.
## Manual Installation
1. Download the [Modding API](https://github.com/BrandenEK/Blasphemous-Modding-API/releases), and follow
the [installation instructions](https://github.com/BrandenEK/Blasphemous-Modding-API#installation) on the GitHub page.
2. After the Modding API has been installed, download the
[Randomizer](https://github.com/BrandenEK/Blasphemous-Randomizer/releases) and
[Multiworld](https://github.com/BrandenEK/Blasphemous-Multiworld/releases) archives, and extract the contents of both
into the `Modding` folder. Then, add any desired additional mods.
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both
the Randomizer and Multiworld on the title screen.
## Connecting
To connect to an Archipelago server, open the in-game console by pressing backslash `\` and use the command `multiworld connect [address:port] [name] [password]`. The port and password are both optional - if no port is provided then the default port of 38281 is used.
To connect to an Archipelago server, open the in-game console by pressing backslash `\` and use
the command `multiworld connect [address:port] [name] [password]`.
The port and password are both optional - if no port is provided then the default port of 38281 is used.
**Make sure to connect to the server before attempting to start a new save file.**

35
worlds/clique/Items.py Normal file
View File

@@ -0,0 +1,35 @@
from typing import Callable, Dict, NamedTuple, Optional
from BaseClasses import Item, ItemClassification, MultiWorld
class CliqueItem(Item):
game = "Clique"
class CliqueItemData(NamedTuple):
code: Optional[int] = None
type: ItemClassification = ItemClassification.filler
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True
item_data_table: Dict[str, CliqueItemData] = {
"Feeling of Satisfaction": CliqueItemData(
code=69696969,
type=ItemClassification.progression,
),
"Button Activation": CliqueItemData(
code=69696968,
type=ItemClassification.progression,
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]),
),
"A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData(
code=69696967,
can_create=lambda multiworld, player: False # Only created from `get_filler_item_name`.
),
"The Urge to Push": CliqueItemData(
type=ItemClassification.progression,
),
}
item_table = {name: data.code for name, data in item_data_table.items() if data.code is not None}

View File

@@ -0,0 +1,34 @@
from typing import Callable, Dict, NamedTuple, Optional
from BaseClasses import Location, MultiWorld
class CliqueLocation(Location):
game = "Clique"
class CliqueLocationData(NamedTuple):
region: str
address: Optional[int] = None
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True
locked_item: Optional[str] = None
location_data_table: Dict[str, CliqueLocationData] = {
"The Big Red Button": CliqueLocationData(
region="The Button Realm",
address=69696969,
),
"The Item on the Desk": CliqueLocationData(
region="The Button Realm",
address=69696968,
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]),
),
"In the Player's Mind": CliqueLocationData(
region="The Button Realm",
locked_item="The Urge to Push",
),
}
location_table = {name: data.address for name, data in location_data_table.items() if data.address is not None}
locked_locations = {name: data for name, data in location_data_table.items() if data.locked_item}

View File

@@ -1,6 +1,6 @@
from typing import Dict
from Options import Option, Toggle
from Options import Choice, Option, Toggle
class HardMode(Toggle):
@@ -8,6 +8,27 @@ class HardMode(Toggle):
display_name = "Hard Mode"
class ButtonColor(Choice):
"""Customize your button! Now available in 12 unique colors."""
display_name = "Button Color"
option_red = 0
option_orange = 1
option_yellow = 2
option_green = 3
option_cyan = 4
option_blue = 5
option_magenta = 6
option_purple = 7
option_pink = 8
option_brown = 9
option_white = 10
option_black = 11
clique_options: Dict[str, type(Option)] = {
"hard_mode": HardMode
"color": ButtonColor,
"hard_mode": HardMode,
# DeathLink is always on. Always.
# "death_link": DeathLink,
}

11
worlds/clique/Regions.py Normal file
View File

@@ -0,0 +1,11 @@
from typing import Dict, List, NamedTuple
class CliqueRegionData(NamedTuple):
connecting_regions: List[str] = []
region_data_table: Dict[str, CliqueRegionData] = {
"Menu": CliqueRegionData(["The Button Realm"]),
"The Button Realm": CliqueRegionData(),
}

10
worlds/clique/Rules.py Normal file
View File

@@ -0,0 +1,10 @@
from typing import Callable
from BaseClasses import CollectionState, MultiWorld
def get_button_rule(multiworld: MultiWorld, player: int) -> Callable[[CollectionState], bool]:
if getattr(multiworld, "hard_mode")[player]:
return lambda state: state.has("Button Activation", player)
return lambda state: True

View File

@@ -1,15 +1,12 @@
from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial
from typing import List
from BaseClasses import Region, Tutorial
from worlds.AutoWorld import WebWorld, World
from worlds.generic.Rules import set_rule
from .Items import CliqueItem, item_data_table, item_table
from .Locations import CliqueLocation, location_data_table, location_table, locked_locations
from .Options import clique_options
class CliqueItem(Item):
game = "Clique"
class CliqueLocation(Location):
game = "Clique"
from .Regions import region_data_table
from .Rules import get_button_rule
class CliqueWebWorld(WebWorld):
@@ -27,71 +24,69 @@ class CliqueWebWorld(WebWorld):
class CliqueWorld(World):
"""The greatest game ever designed. Full of exciting gameplay!"""
"""The greatest game of all time."""
game = "Clique"
data_version = 2
data_version = 3
web = CliqueWebWorld()
option_definitions = clique_options
# Yes, I'm like 12 for this.
location_name_to_id = {
"The Big Red Button": 69696969,
"The Item on the Desk": 69696968,
}
item_name_to_id = {
"Feeling of Satisfaction": 69696969,
"Button Activation": 69696968,
}
location_name_to_id = location_table
item_name_to_id = item_table
def create_item(self, name: str) -> CliqueItem:
return CliqueItem(name, ItemClassification.progression, self.item_name_to_id[name], self.player)
return CliqueItem(name, item_data_table[name].type, item_data_table[name].code, self.player)
def create_items(self) -> None:
self.multiworld.itempool.append(self.create_item("Feeling of Satisfaction"))
self.multiworld.priority_locations[self.player].value.add("The Big Red Button")
item_pool: List[CliqueItem] = []
for name, item in item_data_table.items():
if item.code and item.can_create(self.multiworld, self.player):
item_pool.append(self.create_item(name))
if self.multiworld.hard_mode[self.player]:
self.multiworld.itempool.append(self.create_item("Button Activation"))
self.multiworld.itempool += item_pool
def create_regions(self) -> None:
if self.multiworld.hard_mode[self.player]:
self.multiworld.regions += [
create_region(self.multiworld, self.player, "Menu", None, ["The entrance to the button."]),
create_region(self.multiworld, self.player, "The realm of the button.", self.location_name_to_id)
]
else:
self.multiworld.regions += [
create_region(self.multiworld, self.player, "Menu", None, ["The entrance to the button."]),
create_region(self.multiworld, self.player, "The realm of the button.", {
"The Big Red Button": 69696969
})]
# Create regions.
for region_name in region_data_table.keys():
region = Region(region_name, self.player, self.multiworld)
self.multiworld.regions.append(region)
self.multiworld.get_entrance("The entrance to the button.", self.player) \
.connect(self.multiworld.get_region("The realm of the button.", self.player))
# Create locations.
for region_name, region_data in region_data_table.items():
region = self.multiworld.get_region(region_name, self.player)
region.add_locations({
location_name: location_data.address for location_name, location_data in location_data_table.items()
if location_data.region == region_name and location_data.can_create(self.multiworld, self.player)
}, CliqueLocation)
region.add_exits(region_data_table[region_name].connecting_regions)
# Place locked locations.
for location_name, location_data in locked_locations.items():
# Ignore locations we never created.
if not location_data.can_create(self.multiworld, self.player):
continue
locked_item = self.create_item(location_data_table[location_name].locked_item)
self.multiworld.get_location(location_name, self.player).place_locked_item(locked_item)
# Set priority location for the Big Red Button!
self.multiworld.priority_locations[self.player].value.add("The Big Red Button")
def get_filler_item_name(self) -> str:
return self.multiworld.random.choice(self.item_name_to_id)
return "A Cool Filler Item (No Satisfaction Guaranteed)"
def set_rules(self) -> None:
if self.multiworld.hard_mode[self.player]:
set_rule(
self.multiworld.get_location("The Big Red Button", self.player),
lambda state: state.has("Button Activation", self.player))
button_rule = get_button_rule(self.multiworld, self.player)
self.multiworld.get_location("The Big Red Button", self.player).access_rule = button_rule
self.multiworld.get_location("In the Player's Mind", self.player).access_rule = button_rule
self.multiworld.completion_condition[self.player] = lambda state: \
state.has("Feeling of Satisfaction", self.player)
# Do not allow button activations on buttons.
self.multiworld.get_location("The Big Red Button", self.player).item_rule =\
lambda item: item.name != "Button Activation"
# Completion condition.
self.multiworld.completion_condition[self.player] = lambda state: state.has("The Urge to Push", self.player)
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
region = Region(name, player, world)
if locations:
for location_name in locations.keys():
region.locations.append(CliqueLocation(player, location_name, locations[location_name], region))
if exits:
for _exit in exits:
region.exits.append(Entrance(player, _exit, region))
return region
def fill_slot_data(self):
return {
"color": getattr(self.multiworld, "color")[self.player].current_key
}

View File

@@ -8,7 +8,7 @@ Clique is a joke game developed for Archipelago in March 2023 to showcase how ea
Archipelago. The objective of the game is to press the big red button. If a player is playing on `hard_mode`, they must
wait for someone else in the multiworld to "activate" their button before they can press it.
Clique can be played on any HTML5-capable browser.
Clique can be played on most modern HTML5-capable browsers.
## Where is the settings page?

View File

@@ -6,7 +6,7 @@ slot name, and a room password if one is required. Then click "Connect".
If you're playing on "easy mode", just click the button and receive "Satisfaction".
If you're playing on "hard mode", you may need to wait for activation before you can complete your objective. Luckily,
Clique runs in all the major browsers that support HTML5, so you can load Clique on your phone and be productive while
Clique runs in most major browsers that support HTML5, so you can load Clique on your phone and be productive while
you wait!
If you need some ideas for what to do while waiting for button activation, give the following a try:
@@ -19,4 +19,4 @@ If you need some ideas for what to do while waiting for button activation, give
- Do your school work.
~~If you run into any issues with this game, definitely do not contact Phar#4444 on discord. *wink* *wink*~~
~~If you run into any issues with this game, definitely do not contact **thephar** on discord. *wink* *wink*~~

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,691 @@
import sys
from enum import IntEnum
from typing import Optional, NamedTuple, Dict
from BaseClasses import Location
from worlds.dark_souls_3.data.locations_data import location_tables, painted_world_table, dreg_heap_table, \
ringed_city_table
from BaseClasses import Location, Region
class DS3LocationCategory(IntEnum):
WEAPON = 0
SHIELD = 1
ARMOR = 2
RING = 3
SPELL = 4
NPC = 5
KEY = 6
BOSS = 7
MISC = 8
HEALTH = 9
PROGRESSIVE_ITEM = 10
EVENT = 11
class DS3LocationData(NamedTuple):
name: str
default_item: str
category: DS3LocationCategory
class DarkSouls3Location(Location):
game: str = "Dark Souls III"
category: DS3LocationCategory
default_item_name: str
def __init__(
self,
player: int,
name: str,
category: DS3LocationCategory,
default_item_name: str,
address: Optional[int] = None,
parent: Optional[Region] = None):
super().__init__(player, name, address, parent)
self.default_item_name = default_item_name
self.category = category
@staticmethod
def get_name_to_id() -> dict:
base_id = 100000
table_offset = 100
table_order = [
"Firelink Shrine",
"Firelink Shrine Bell Tower",
"High Wall of Lothric",
"Undead Settlement",
"Road of Sacrifices",
"Cathedral of the Deep",
"Farron Keep",
"Catacombs of Carthus",
"Smouldering Lake",
"Irithyll of the Boreal Valley",
"Irithyll Dungeon",
"Profaned Capital",
"Anor Londo",
"Lothric Castle",
"Consumed King's Garden",
"Grand Archives",
"Untended Graves",
"Archdragon Peak",
"Painted World of Ariandel 1",
"Painted World of Ariandel 2",
"Dreg Heap",
"Ringed City",
"Progressive Items 1",
"Progressive Items 2",
"Progressive Items 3",
"Progressive Items 4",
"Progressive Items DLC",
]
output = {}
for i, table in enumerate(location_tables):
if len(table) > table_offset:
raise Exception("A location table has {} entries, that is more than {} entries (table #{})".format(len(table), table_offset, i))
output.update({name: id for id, name in enumerate(table, base_id + (table_offset * i))})
for i, region_name in enumerate(table_order):
if len(location_tables[region_name]) > table_offset:
raise Exception("A location table has {} entries, that is more than {} entries (table #{})".format(len(location_tables[region_name]), table_offset, i))
output.update({location_data.name: id for id, location_data in enumerate(location_tables[region_name], base_id + (table_offset * i))})
return output
location_tables = {
"Firelink Shrine": [
DS3LocationData("FS: Broken Straight Sword", "Broken Straight Sword", DS3LocationCategory.WEAPON),
DS3LocationData("FS: East-West Shield", "East-West Shield", DS3LocationCategory.SHIELD),
DS3LocationData("FS: Uchigatana", "Uchigatana", DS3LocationCategory.WEAPON),
DS3LocationData("FS: Master's Attire", "Master's Attire", DS3LocationCategory.ARMOR),
DS3LocationData("FS: Master's Gloves", "Master's Gloves", DS3LocationCategory.ARMOR),
],
"Firelink Shrine Bell Tower": [
DS3LocationData("FSBT: Covetous Silver Serpent Ring", "Covetous Silver Serpent Ring", DS3LocationCategory.RING),
DS3LocationData("FSBT: Fire Keeper Robe", "Fire Keeper Robe", DS3LocationCategory.ARMOR),
DS3LocationData("FSBT: Fire Keeper Gloves", "Fire Keeper Gloves", DS3LocationCategory.ARMOR),
DS3LocationData("FSBT: Fire Keeper Skirt", "Fire Keeper Skirt", DS3LocationCategory.ARMOR),
DS3LocationData("FSBT: Estus Ring", "Estus Ring", DS3LocationCategory.RING),
DS3LocationData("FSBT: Fire Keeper Soul", "Fire Keeper Soul", DS3LocationCategory.MISC),
],
"High Wall of Lothric": [
DS3LocationData("HWL: Deep Battle Axe", "Deep Battle Axe", DS3LocationCategory.WEAPON),
DS3LocationData("HWL: Club", "Club", DS3LocationCategory.WEAPON),
DS3LocationData("HWL: Claymore", "Claymore", DS3LocationCategory.WEAPON),
DS3LocationData("HWL: Binoculars", "Binoculars", DS3LocationCategory.MISC),
DS3LocationData("HWL: Longbow", "Longbow", DS3LocationCategory.WEAPON),
DS3LocationData("HWL: Mail Breaker", "Mail Breaker", DS3LocationCategory.WEAPON),
DS3LocationData("HWL: Broadsword", "Broadsword", DS3LocationCategory.WEAPON),
DS3LocationData("HWL: Silver Eagle Kite Shield", "Silver Eagle Kite Shield", DS3LocationCategory.SHIELD),
DS3LocationData("HWL: Astora's Straight Sword", "Astora's Straight Sword", DS3LocationCategory.WEAPON),
DS3LocationData("HWL: Cell Key", "Cell Key", DS3LocationCategory.KEY),
DS3LocationData("HWL: Rapier", "Rapier", DS3LocationCategory.WEAPON),
DS3LocationData("HWL: Lucerne", "Lucerne", DS3LocationCategory.WEAPON),
DS3LocationData("HWL: Small Lothric Banner", "Small Lothric Banner", DS3LocationCategory.KEY),
DS3LocationData("HWL: Basin of Vows", "Basin of Vows", DS3LocationCategory.KEY),
DS3LocationData("HWL: Soul of Boreal Valley Vordt", "Soul of Boreal Valley Vordt", DS3LocationCategory.BOSS),
DS3LocationData("HWL: Soul of the Dancer", "Soul of the Dancer", DS3LocationCategory.BOSS),
DS3LocationData("HWL: Way of Blue", "Way of Blue", DS3LocationCategory.MISC),
DS3LocationData("HWL: Greirat's Ashes", "Greirat's Ashes", DS3LocationCategory.NPC),
DS3LocationData("HWL: Blue Tearstone Ring", "Blue Tearstone Ring", DS3LocationCategory.NPC),
],
"Undead Settlement": [
DS3LocationData("US: Small Leather Shield", "Small Leather Shield", DS3LocationCategory.SHIELD),
DS3LocationData("US: Whip", "Whip", DS3LocationCategory.WEAPON),
DS3LocationData("US: Reinforced Club", "Reinforced Club", DS3LocationCategory.WEAPON),
DS3LocationData("US: Blue Wooden Shield", "Blue Wooden Shield", DS3LocationCategory.SHIELD),
DS3LocationData("US: Cleric Hat", "Cleric Hat", DS3LocationCategory.ARMOR),
DS3LocationData("US: Cleric Blue Robe", "Cleric Blue Robe", DS3LocationCategory.ARMOR),
DS3LocationData("US: Cleric Gloves", "Cleric Gloves", DS3LocationCategory.ARMOR),
DS3LocationData("US: Cleric Trousers", "Cleric Trousers", DS3LocationCategory.ARMOR),
DS3LocationData("US: Mortician's Ashes", "Mortician's Ashes", DS3LocationCategory.KEY),
DS3LocationData("US: Caestus", "Caestus", DS3LocationCategory.WEAPON),
DS3LocationData("US: Plank Shield", "Plank Shield", DS3LocationCategory.SHIELD),
DS3LocationData("US: Flame Stoneplate Ring", "Flame Stoneplate Ring", DS3LocationCategory.RING),
DS3LocationData("US: Caduceus Round Shield", "Caduceus Round Shield", DS3LocationCategory.SHIELD),
DS3LocationData("US: Fire Clutch Ring", "Fire Clutch Ring", DS3LocationCategory.RING),
DS3LocationData("US: Partizan", "Partizan", DS3LocationCategory.WEAPON),
DS3LocationData("US: Bloodbite Ring", "Bloodbite Ring", DS3LocationCategory.RING),
DS3LocationData("US: Red Hilted Halberd", "Red Hilted Halberd", DS3LocationCategory.WEAPON),
DS3LocationData("US: Saint's Talisman", "Saint's Talisman", DS3LocationCategory.WEAPON),
DS3LocationData("US: Irithyll Straight Sword", "Irithyll Straight Sword", DS3LocationCategory.WEAPON),
DS3LocationData("US: Large Club", "Large Club", DS3LocationCategory.WEAPON),
DS3LocationData("US: Northern Helm", "Northern Helm", DS3LocationCategory.ARMOR),
DS3LocationData("US: Northern Armor", "Northern Armor", DS3LocationCategory.ARMOR),
DS3LocationData("US: Northern Gloves", "Northern Gloves", DS3LocationCategory.ARMOR),
DS3LocationData("US: Northern Trousers", "Northern Trousers", DS3LocationCategory.ARMOR),
DS3LocationData("US: Flynn's Ring", "Flynn's Ring", DS3LocationCategory.RING),
DS3LocationData("US: Mirrah Vest", "Mirrah Vest", DS3LocationCategory.ARMOR),
DS3LocationData("US: Mirrah Gloves", "Mirrah Gloves", DS3LocationCategory.ARMOR),
DS3LocationData("US: Mirrah Trousers", "Mirrah Trousers", DS3LocationCategory.ARMOR),
DS3LocationData("US: Chloranthy Ring", "Chloranthy Ring", DS3LocationCategory.RING),
DS3LocationData("US: Loincloth", "Loincloth", DS3LocationCategory.ARMOR),
DS3LocationData("US: Wargod Wooden Shield", "Wargod Wooden Shield", DS3LocationCategory.SHIELD),
DS3LocationData("US: Loretta's Bone", "Loretta's Bone", DS3LocationCategory.KEY),
DS3LocationData("US: Hand Axe", "Hand Axe", DS3LocationCategory.WEAPON),
DS3LocationData("US: Great Scythe", "Great Scythe", DS3LocationCategory.WEAPON),
DS3LocationData("US: Soul of the Rotted Greatwood", "Soul of the Rotted Greatwood", DS3LocationCategory.BOSS),
DS3LocationData("US: Hawk Ring", "Hawk Ring", DS3LocationCategory.RING),
DS3LocationData("US: Warrior of Sunlight", "Warrior of Sunlight", DS3LocationCategory.MISC),
DS3LocationData("US: Blessed Red and White Shield+1", "Blessed Red and White Shield+1", DS3LocationCategory.SHIELD),
DS3LocationData("US: Irina's Ashes", "Irina's Ashes", DS3LocationCategory.NPC),
DS3LocationData("US: Cornyx's Ashes", "Cornyx's Ashes", DS3LocationCategory.NPC),
DS3LocationData("US: Cornyx's Wrap", "Cornyx's Wrap", DS3LocationCategory.NPC),
DS3LocationData("US: Cornyx's Garb", "Cornyx's Garb", DS3LocationCategory.NPC),
DS3LocationData("US: Cornyx's Skirt", "Cornyx's Skirt", DS3LocationCategory.NPC),
DS3LocationData("US: Pyromancy Flame", "Pyromancy Flame", DS3LocationCategory.NPC),
DS3LocationData("US: Transposing Kiln", "Transposing Kiln", DS3LocationCategory.MISC),
DS3LocationData("US: Tower Key", "Tower Key", DS3LocationCategory.NPC),
],
"Road of Sacrifices": [
DS3LocationData("RS: Brigand Twindaggers", "Brigand Twindaggers", DS3LocationCategory.WEAPON),
DS3LocationData("RS: Brigand Hood", "Brigand Hood", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Brigand Armor", "Brigand Armor", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Brigand Gauntlets", "Brigand Gauntlets", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Brigand Trousers", "Brigand Trousers", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Butcher Knife", "Butcher Knife", DS3LocationCategory.WEAPON),
DS3LocationData("RS: Brigand Axe", "Brigand Axe", DS3LocationCategory.WEAPON),
DS3LocationData("RS: Braille Divine Tome of Carim", "Braille Divine Tome of Carim", DS3LocationCategory.MISC),
DS3LocationData("RS: Morne's Ring", "Morne's Ring", DS3LocationCategory.RING),
DS3LocationData("RS: Twin Dragon Greatshield", "Twin Dragon Greatshield", DS3LocationCategory.SHIELD),
DS3LocationData("RS: Heretic's Staff", "Heretic's Staff", DS3LocationCategory.WEAPON),
DS3LocationData("RS: Sorcerer Hood", "Sorcerer Hood", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Sorcerer Robe", "Sorcerer Robe", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Sorcerer Gloves", "Sorcerer Gloves", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Sorcerer Trousers", "Sorcerer Trousers", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Sage Ring", "Sage Ring", DS3LocationCategory.RING),
DS3LocationData("RS: Fallen Knight Helm", "Fallen Knight Helm", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Fallen Knight Armor", "Fallen Knight Armor", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Fallen Knight Gauntlets", "Fallen Knight Gauntlets", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Fallen Knight Trousers", "Fallen Knight Trousers", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Conjurator Hood", "Conjurator Hood", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Conjurator Robe", "Conjurator Robe", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Conjurator Manchettes", "Conjurator Manchettes", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Conjurator Boots", "Conjurator Boots", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Great Swamp Pyromancy Tome", "Great Swamp Pyromancy Tome", DS3LocationCategory.MISC),
DS3LocationData("RS: Great Club", "Great Club", DS3LocationCategory.WEAPON),
DS3LocationData("RS: Exile Greatsword", "Exile Greatsword", DS3LocationCategory.WEAPON),
DS3LocationData("RS: Farron Coal", "Farron Coal", DS3LocationCategory.MISC),
DS3LocationData("RS: Sellsword Twinblades", "Sellsword Twinblades", DS3LocationCategory.WEAPON),
DS3LocationData("RS: Sellsword Helm", "Sellsword Helm", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Sellsword Armor", "Sellsword Armor", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Sellsword Gauntlet", "Sellsword Gauntlet", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Sellsword Trousers", "Sellsword Trousers", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Golden Falcon Shield", "Golden Falcon Shield", DS3LocationCategory.SHIELD),
DS3LocationData("RS: Herald Helm", "Herald Helm", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Herald Armor", "Herald Armor", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Herald Gloves", "Herald Gloves", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Herald Trousers", "Herald Trousers", DS3LocationCategory.ARMOR),
DS3LocationData("RS: Grass Crest Shield", "Grass Crest Shield", DS3LocationCategory.SHIELD),
DS3LocationData("RS: Soul of a Crystal Sage", "Soul of a Crystal Sage", DS3LocationCategory.BOSS),
DS3LocationData("RS: Great Swamp Ring", "Great Swamp Ring", DS3LocationCategory.RING),
DS3LocationData("RS: Orbeck's Ashes", "Orbeck's Ashes", DS3LocationCategory.NPC),
],
"Cathedral of the Deep": [
DS3LocationData("CD: Paladin's Ashes", "Paladin's Ashes", DS3LocationCategory.MISC),
DS3LocationData("CD: Spider Shield", "Spider Shield", DS3LocationCategory.SHIELD),
DS3LocationData("CD: Crest Shield", "Crest Shield", DS3LocationCategory.SHIELD),
DS3LocationData("CD: Notched Whip", "Notched Whip", DS3LocationCategory.WEAPON),
DS3LocationData("CD: Astora Greatsword", "Astora Greatsword", DS3LocationCategory.WEAPON),
DS3LocationData("CD: Executioner's Greatsword", "Executioner's Greatsword", DS3LocationCategory.WEAPON),
DS3LocationData("CD: Curse Ward Greatshield", "Curse Ward Greatshield", DS3LocationCategory.SHIELD),
DS3LocationData("CD: Saint-tree Bellvine", "Saint-tree Bellvine", DS3LocationCategory.WEAPON),
DS3LocationData("CD: Poisonbite Ring", "Poisonbite Ring", DS3LocationCategory.RING),
DS3LocationData("CD: Lloyd's Sword Ring", "Lloyd's Sword Ring", DS3LocationCategory.RING),
DS3LocationData("CD: Seek Guidance", "Seek Guidance", DS3LocationCategory.SPELL),
DS3LocationData("CD: Aldrich's Sapphire", "Aldrich's Sapphire", DS3LocationCategory.RING),
DS3LocationData("CD: Deep Braille Divine Tome", "Deep Braille Divine Tome", DS3LocationCategory.MISC),
DS3LocationData("CD: Saint Bident", "Saint Bident", DS3LocationCategory.WEAPON),
DS3LocationData("CD: Maiden Hood", "Maiden Hood", DS3LocationCategory.ARMOR),
DS3LocationData("CD: Maiden Robe", "Maiden Robe", DS3LocationCategory.ARMOR),
DS3LocationData("CD: Maiden Gloves", "Maiden Gloves", DS3LocationCategory.ARMOR),
DS3LocationData("CD: Maiden Skirt", "Maiden Skirt", DS3LocationCategory.ARMOR),
DS3LocationData("CD: Drang Armor", "Drang Armor", DS3LocationCategory.ARMOR),
DS3LocationData("CD: Drang Gauntlets", "Drang Gauntlets", DS3LocationCategory.ARMOR),
DS3LocationData("CD: Drang Shoes", "Drang Shoes", DS3LocationCategory.ARMOR),
DS3LocationData("CD: Drang Hammers", "Drang Hammers", DS3LocationCategory.WEAPON),
DS3LocationData("CD: Deep Ring", "Deep Ring", DS3LocationCategory.RING),
DS3LocationData("CD: Archdeacon White Crown", "Archdeacon White Crown", DS3LocationCategory.ARMOR),
DS3LocationData("CD: Archdeacon Holy Garb", "Archdeacon Holy Garb", DS3LocationCategory.ARMOR),
DS3LocationData("CD: Archdeacon Skirt", "Archdeacon Skirt", DS3LocationCategory.ARMOR),
DS3LocationData("CD: Arbalest", "Arbalest", DS3LocationCategory.WEAPON),
DS3LocationData("CD: Small Doll", "Small Doll", DS3LocationCategory.KEY),
DS3LocationData("CD: Soul of the Deacons of the Deep", "Soul of the Deacons of the Deep", DS3LocationCategory.BOSS),
DS3LocationData("CD: Rosaria's Fingers", "Rosaria's Fingers", DS3LocationCategory.MISC)
],
"Farron Keep": [
DS3LocationData("FK: Ragged Mask", "Ragged Mask", DS3LocationCategory.ARMOR),
DS3LocationData("FK: Iron Flesh", "Iron Flesh", DS3LocationCategory.SPELL),
DS3LocationData("FK: Golden Scroll", "Golden Scroll", DS3LocationCategory.MISC),
DS3LocationData("FK: Antiquated Dress", "Antiquated Dress", DS3LocationCategory.ARMOR),
DS3LocationData("FK: Antiquated Gloves", "Antiquated Gloves", DS3LocationCategory.ARMOR),
DS3LocationData("FK: Antiquated Skirt", "Antiquated Skirt", DS3LocationCategory.ARMOR),
DS3LocationData("FK: Nameless Knight Helm", "Nameless Knight Helm", DS3LocationCategory.ARMOR),
DS3LocationData("FK: Nameless Knight Armor", "Nameless Knight Armor", DS3LocationCategory.ARMOR),
DS3LocationData("FK: Nameless Knight Gauntlets", "Nameless Knight Gauntlets", DS3LocationCategory.ARMOR),
DS3LocationData("FK: Nameless Knight Leggings", "Nameless Knight Leggings", DS3LocationCategory.ARMOR),
DS3LocationData("FK: Sunlight Talisman", "Sunlight Talisman", DS3LocationCategory.WEAPON),
DS3LocationData("FK: Wolf's Blood Swordgrass", "Wolf's Blood Swordgrass", DS3LocationCategory.MISC),
DS3LocationData("FK: Greatsword", "Greatsword", DS3LocationCategory.WEAPON),
DS3LocationData("FK: Sage's Coal", "Sage's Coal", DS3LocationCategory.MISC),
DS3LocationData("FK: Stone Parma", "Stone Parma", DS3LocationCategory.SHIELD),
DS3LocationData("FK: Sage's Scroll", "Sage's Scroll", DS3LocationCategory.MISC),
DS3LocationData("FK: Crown of Dusk", "Crown of Dusk", DS3LocationCategory.ARMOR),
DS3LocationData("FK: Lingering Dragoncrest Ring", "Lingering Dragoncrest Ring", DS3LocationCategory.RING),
DS3LocationData("FK: Pharis's Hat", "Pharis's Hat", DS3LocationCategory.ARMOR),
DS3LocationData("FK: Black Bow of Pharis", "Black Bow of Pharis", DS3LocationCategory.WEAPON),
DS3LocationData("FK: Dreamchaser's Ashes", "Dreamchaser's Ashes", DS3LocationCategory.MISC),
DS3LocationData("FK: Great Axe", "Great Axe", DS3LocationCategory.WEAPON),
DS3LocationData("FK: Dragon Crest Shield", "Dragon Crest Shield", DS3LocationCategory.SHIELD),
DS3LocationData("FK: Lightning Spear", "Lightning Spear", DS3LocationCategory.SPELL),
DS3LocationData("FK: Atonement", "Atonement", DS3LocationCategory.SPELL),
DS3LocationData("FK: Great Magic Weapon", "Great Magic Weapon", DS3LocationCategory.SPELL),
DS3LocationData("FK: Cinders of a Lord - Abyss Watcher", "Cinders of a Lord - Abyss Watcher", DS3LocationCategory.KEY),
DS3LocationData("FK: Soul of the Blood of the Wolf", "Soul of the Blood of the Wolf", DS3LocationCategory.BOSS),
DS3LocationData("FK: Soul of a Stray Demon", "Soul of a Stray Demon", DS3LocationCategory.BOSS),
DS3LocationData("FK: Watchdogs of Farron", "Watchdogs of Farron", DS3LocationCategory.MISC),
],
"Catacombs of Carthus": [
DS3LocationData("CC: Carthus Pyromancy Tome", "Carthus Pyromancy Tome", DS3LocationCategory.MISC),
DS3LocationData("CC: Carthus Milkring", "Carthus Milkring", DS3LocationCategory.RING),
DS3LocationData("CC: Grave Warden's Ashes", "Grave Warden's Ashes", DS3LocationCategory.MISC),
DS3LocationData("CC: Carthus Bloodring", "Carthus Bloodring", DS3LocationCategory.RING),
DS3LocationData("CC: Grave Warden Pyromancy Tome", "Grave Warden Pyromancy Tome", DS3LocationCategory.MISC),
DS3LocationData("CC: Old Sage's Blindfold", "Old Sage's Blindfold", DS3LocationCategory.ARMOR),
DS3LocationData("CC: Witch's Ring", "Witch's Ring", DS3LocationCategory.RING),
DS3LocationData("CC: Black Blade", "Black Blade", DS3LocationCategory.WEAPON),
DS3LocationData("CC: Soul of High Lord Wolnir", "Soul of High Lord Wolnir", DS3LocationCategory.BOSS),
DS3LocationData("CC: Soul of a Demon", "Soul of a Demon", DS3LocationCategory.BOSS),
],
"Smouldering Lake": [
DS3LocationData("SL: Shield of Want", "Shield of Want", DS3LocationCategory.SHIELD),
DS3LocationData("SL: Speckled Stoneplate Ring", "Speckled Stoneplate Ring", DS3LocationCategory.RING),
DS3LocationData("SL: Dragonrider Bow", "Dragonrider Bow", DS3LocationCategory.WEAPON),
DS3LocationData("SL: Lightning Stake", "Lightning Stake", DS3LocationCategory.SPELL),
DS3LocationData("SL: Izalith Pyromancy Tome", "Izalith Pyromancy Tome", DS3LocationCategory.MISC),
DS3LocationData("SL: Black Knight Sword", "Black Knight Sword", DS3LocationCategory.WEAPON),
DS3LocationData("SL: Quelana Pyromancy Tome", "Quelana Pyromancy Tome", DS3LocationCategory.MISC),
DS3LocationData("SL: Toxic Mist", "Toxic Mist", DS3LocationCategory.SPELL),
DS3LocationData("SL: White Hair Talisman", "White Hair Talisman", DS3LocationCategory.WEAPON),
DS3LocationData("SL: Izalith Staff", "Izalith Staff", DS3LocationCategory.WEAPON),
DS3LocationData("SL: Sacred Flame", "Sacred Flame", DS3LocationCategory.SPELL),
DS3LocationData("SL: Fume Ultra Greatsword", "Fume Ultra Greatsword", DS3LocationCategory.WEAPON),
DS3LocationData("SL: Black Iron Greatshield", "Black Iron Greatshield", DS3LocationCategory.SHIELD),
DS3LocationData("SL: Soul of the Old Demon King", "Soul of the Old Demon King", DS3LocationCategory.BOSS),
DS3LocationData("SL: Knight Slayer's Ring", "Knight Slayer's Ring", DS3LocationCategory.RING),
],
"Irithyll of the Boreal Valley": [
DS3LocationData("IBV: Dorhys' Gnawing", "Dorhys' Gnawing", DS3LocationCategory.SPELL),
DS3LocationData("IBV: Witchtree Branch", "Witchtree Branch", DS3LocationCategory.WEAPON),
DS3LocationData("IBV: Magic Clutch Ring", "Magic Clutch Ring", DS3LocationCategory.RING),
DS3LocationData("IBV: Ring of the Sun's First Born", "Ring of the Sun's First Born", DS3LocationCategory.RING),
DS3LocationData("IBV: Roster of Knights", "Roster of Knights", DS3LocationCategory.MISC),
DS3LocationData("IBV: Pontiff's Right Eye", "Pontiff's Right Eye", DS3LocationCategory.RING),
DS3LocationData("IBV: Yorshka's Spear", "Yorshka's Spear", DS3LocationCategory.WEAPON),
DS3LocationData("IBV: Great Heal", "Great Heal", DS3LocationCategory.SPELL),
DS3LocationData("IBV: Smough's Great Hammer", "Smough's Great Hammer", DS3LocationCategory.WEAPON),
DS3LocationData("IBV: Leo Ring", "Leo Ring", DS3LocationCategory.RING),
DS3LocationData("IBV: Excrement-covered Ashes", "Excrement-covered Ashes", DS3LocationCategory.MISC),
DS3LocationData("IBV: Dark Stoneplate Ring", "Dark Stoneplate Ring", DS3LocationCategory.RING),
DS3LocationData("IBV: Easterner's Ashes", "Easterner's Ashes", DS3LocationCategory.MISC),
DS3LocationData("IBV: Painting Guardian's Curved Sword", "Painting Guardian's Curved Sword", DS3LocationCategory.WEAPON),
DS3LocationData("IBV: Painting Guardian Hood", "Painting Guardian Hood", DS3LocationCategory.ARMOR),
DS3LocationData("IBV: Painting Guardian Gown", "Painting Guardian Gown", DS3LocationCategory.ARMOR),
DS3LocationData("IBV: Painting Guardian Gloves", "Painting Guardian Gloves", DS3LocationCategory.ARMOR),
DS3LocationData("IBV: Painting Guardian Waistcloth", "Painting Guardian Waistcloth", DS3LocationCategory.ARMOR),
DS3LocationData("IBV: Dragonslayer Greatbow", "Dragonslayer Greatbow", DS3LocationCategory.WEAPON),
DS3LocationData("IBV: Reversal Ring", "Reversal Ring", DS3LocationCategory.RING),
DS3LocationData("IBV: Brass Helm", "Brass Helm", DS3LocationCategory.ARMOR),
DS3LocationData("IBV: Brass Armor", "Brass Armor", DS3LocationCategory.ARMOR),
DS3LocationData("IBV: Brass Gauntlets", "Brass Gauntlets", DS3LocationCategory.ARMOR),
DS3LocationData("IBV: Brass Leggings", "Brass Leggings", DS3LocationCategory.ARMOR),
DS3LocationData("IBV: Ring of Favor", "Ring of Favor", DS3LocationCategory.RING),
DS3LocationData("IBV: Golden Ritual Spear", "Golden Ritual Spear", DS3LocationCategory.WEAPON),
DS3LocationData("IBV: Soul of Pontiff Sulyvahn", "Soul of Pontiff Sulyvahn", DS3LocationCategory.BOSS),
DS3LocationData("IBV: Aldrich Faithful", "Aldrich Faithful", DS3LocationCategory.MISC),
DS3LocationData("IBV: Drang Twinspears", "Drang Twinspears", DS3LocationCategory.WEAPON),
],
"Irithyll Dungeon": [
DS3LocationData("ID: Bellowing Dragoncrest Ring", "Bellowing Dragoncrest Ring", DS3LocationCategory.RING),
DS3LocationData("ID: Jailbreaker's Key", "Jailbreaker's Key", DS3LocationCategory.KEY),
DS3LocationData("ID: Prisoner Chief's Ashes", "Prisoner Chief's Ashes", DS3LocationCategory.KEY),
DS3LocationData("ID: Old Sorcerer Hat", "Old Sorcerer Hat", DS3LocationCategory.ARMOR),
DS3LocationData("ID: Old Sorcerer Coat", "Old Sorcerer Coat", DS3LocationCategory.ARMOR),
DS3LocationData("ID: Old Sorcerer Gauntlets", "Old Sorcerer Gauntlets", DS3LocationCategory.ARMOR),
DS3LocationData("ID: Old Sorcerer Boots", "Old Sorcerer Boots", DS3LocationCategory.ARMOR),
DS3LocationData("ID: Great Magic Shield", "Great Magic Shield", DS3LocationCategory.SPELL),
DS3LocationData("ID: Dragon Torso Stone", "Dragon Torso Stone", DS3LocationCategory.MISC),
DS3LocationData("ID: Lightning Blade", "Lightning Blade", DS3LocationCategory.SPELL),
DS3LocationData("ID: Profaned Coal", "Profaned Coal", DS3LocationCategory.MISC),
DS3LocationData("ID: Xanthous Ashes", "Xanthous Ashes", DS3LocationCategory.MISC),
DS3LocationData("ID: Old Cell Key", "Old Cell Key", DS3LocationCategory.KEY),
DS3LocationData("ID: Pickaxe", "Pickaxe", DS3LocationCategory.WEAPON),
DS3LocationData("ID: Profaned Flame", "Profaned Flame", DS3LocationCategory.SPELL),
DS3LocationData("ID: Covetous Gold Serpent Ring", "Covetous Gold Serpent Ring", DS3LocationCategory.RING),
DS3LocationData("ID: Jailer's Key Ring", "Jailer's Key Ring", DS3LocationCategory.KEY),
DS3LocationData("ID: Dusk Crown Ring", "Dusk Crown Ring", DS3LocationCategory.RING),
DS3LocationData("ID: Dark Clutch Ring", "Dark Clutch Ring", DS3LocationCategory.RING),
DS3LocationData("ID: Karla's Ashes", "Karla's Ashes", DS3LocationCategory.NPC),
DS3LocationData("ID: Karla's Pointed Hat", "Karla's Pointed Hat", DS3LocationCategory.NPC),
DS3LocationData("ID: Karla's Coat", "Karla's Coat", DS3LocationCategory.NPC),
DS3LocationData("ID: Karla's Gloves", "Karla's Gloves", DS3LocationCategory.NPC),
DS3LocationData("ID: Karla's Trousers", "Karla's Trousers", DS3LocationCategory.NPC),
],
"Profaned Capital": [
DS3LocationData("PC: Cursebite Ring", "Cursebite Ring", DS3LocationCategory.RING),
DS3LocationData("PC: Court Sorcerer Hood", "Court Sorcerer Hood", DS3LocationCategory.ARMOR),
DS3LocationData("PC: Court Sorcerer Robe", "Court Sorcerer Robe", DS3LocationCategory.ARMOR),
DS3LocationData("PC: Court Sorcerer Gloves", "Court Sorcerer Gloves", DS3LocationCategory.ARMOR),
DS3LocationData("PC: Court Sorcerer Trousers", "Court Sorcerer Trousers", DS3LocationCategory.ARMOR),
DS3LocationData("PC: Wrath of the Gods", "Wrath of the Gods", DS3LocationCategory.SPELL),
DS3LocationData("PC: Logan's Scroll", "Logan's Scroll", DS3LocationCategory.MISC),
DS3LocationData("PC: Eleonora", "Eleonora", DS3LocationCategory.WEAPON),
DS3LocationData("PC: Court Sorcerer's Staff", "Court Sorcerer's Staff", DS3LocationCategory.WEAPON),
DS3LocationData("PC: Greatshield of Glory", "Greatshield of Glory", DS3LocationCategory.SHIELD),
DS3LocationData("PC: Storm Ruler", "Storm Ruler", DS3LocationCategory.KEY),
DS3LocationData("PC: Cinders of a Lord - Yhorm the Giant", "Cinders of a Lord - Yhorm the Giant", DS3LocationCategory.KEY),
DS3LocationData("PC: Soul of Yhorm the Giant", "Soul of Yhorm the Giant", DS3LocationCategory.BOSS),
],
"Anor Londo": [
DS3LocationData("AL: Giant's Coal", "Giant's Coal", DS3LocationCategory.MISC),
DS3LocationData("AL: Sun Princess Ring", "Sun Princess Ring", DS3LocationCategory.RING),
DS3LocationData("AL: Aldrich's Ruby", "Aldrich's Ruby", DS3LocationCategory.RING),
DS3LocationData("AL: Cinders of a Lord - Aldrich", "Cinders of a Lord - Aldrich", DS3LocationCategory.KEY),
DS3LocationData("AL: Soul of Aldrich", "Soul of Aldrich", DS3LocationCategory.BOSS),
],
"Lothric Castle": [
DS3LocationData("LC: Hood of Prayer", "Hood of Prayer", DS3LocationCategory.ARMOR),
DS3LocationData("LC: Robe of Prayer", "Robe of Prayer", DS3LocationCategory.ARMOR),
DS3LocationData("LC: Skirt of Prayer", "Skirt of Prayer", DS3LocationCategory.ARMOR),
DS3LocationData("LC: Sacred Bloom Shield", "Sacred Bloom Shield", DS3LocationCategory.SHIELD),
DS3LocationData("LC: Winged Knight Helm", "Winged Knight Helm", DS3LocationCategory.ARMOR),
DS3LocationData("LC: Winged Knight Armor", "Winged Knight Armor", DS3LocationCategory.ARMOR),
DS3LocationData("LC: Winged Knight Gauntlets", "Winged Knight Gauntlets", DS3LocationCategory.ARMOR),
DS3LocationData("LC: Winged Knight Leggings", "Winged Knight Leggings", DS3LocationCategory.ARMOR),
DS3LocationData("LC: Greatlance", "Greatlance", DS3LocationCategory.WEAPON),
DS3LocationData("LC: Sniper Crossbow", "Sniper Crossbow", DS3LocationCategory.WEAPON),
DS3LocationData("LC: Spirit Tree Crest Shield", "Spirit Tree Crest Shield", DS3LocationCategory.SHIELD),
DS3LocationData("LC: Red Tearstone Ring", "Red Tearstone Ring", DS3LocationCategory.RING),
DS3LocationData("LC: Caitha's Chime", "Caitha's Chime", DS3LocationCategory.WEAPON),
DS3LocationData("LC: Braille Divine Tome of Lothric", "Braille Divine Tome of Lothric", DS3LocationCategory.MISC),
DS3LocationData("LC: Knight's Ring", "Knight's Ring", DS3LocationCategory.RING),
DS3LocationData("LC: Irithyll Rapier", "Irithyll Rapier", DS3LocationCategory.WEAPON),
DS3LocationData("LC: Sunlight Straight Sword", "Sunlight Straight Sword", DS3LocationCategory.WEAPON),
DS3LocationData("LC: Soul of Dragonslayer Armour", "Soul of Dragonslayer Armour", DS3LocationCategory.BOSS),
DS3LocationData("LC: Grand Archives Key", "Grand Archives Key", DS3LocationCategory.KEY),
DS3LocationData("LC: Gotthard Twinswords", "Gotthard Twinswords", DS3LocationCategory.WEAPON),
],
"Consumed King's Garden": [
DS3LocationData("CKG: Dragonscale Ring", "Dragonscale Ring", DS3LocationCategory.RING),
DS3LocationData("CKG: Shadow Mask", "Shadow Mask", DS3LocationCategory.ARMOR),
DS3LocationData("CKG: Shadow Garb", "Shadow Garb", DS3LocationCategory.ARMOR),
DS3LocationData("CKG: Shadow Gauntlets", "Shadow Gauntlets", DS3LocationCategory.ARMOR),
DS3LocationData("CKG: Shadow Leggings", "Shadow Leggings", DS3LocationCategory.ARMOR),
DS3LocationData("CKG: Claw", "Claw", DS3LocationCategory.WEAPON),
DS3LocationData("CKG: Soul of Consumed Oceiros", "Soul of Consumed Oceiros", DS3LocationCategory.BOSS),
DS3LocationData("CKG: Magic Stoneplate Ring", "Magic Stoneplate Ring", DS3LocationCategory.RING),
],
"Grand Archives": [
DS3LocationData("GA: Avelyn", "Avelyn", DS3LocationCategory.WEAPON),
DS3LocationData("GA: Witch's Locks", "Witch's Locks", DS3LocationCategory.WEAPON),
DS3LocationData("GA: Power Within", "Power Within", DS3LocationCategory.SPELL),
DS3LocationData("GA: Scholar Ring", "Scholar Ring", DS3LocationCategory.RING),
DS3LocationData("GA: Soul Stream", "Soul Stream", DS3LocationCategory.SPELL),
DS3LocationData("GA: Fleshbite Ring", "Fleshbite Ring", DS3LocationCategory.RING),
DS3LocationData("GA: Crystal Chime", "Crystal Chime", DS3LocationCategory.WEAPON),
DS3LocationData("GA: Golden Wing Crest Shield", "Golden Wing Crest Shield", DS3LocationCategory.SHIELD),
DS3LocationData("GA: Onikiri and Ubadachi", "Onikiri and Ubadachi", DS3LocationCategory.WEAPON),
DS3LocationData("GA: Hunter's Ring", "Hunter's Ring", DS3LocationCategory.RING),
DS3LocationData("GA: Divine Pillars of Light", "Divine Pillars of Light", DS3LocationCategory.SPELL),
DS3LocationData("GA: Cinders of a Lord - Lothric Prince", "Cinders of a Lord - Lothric Prince", DS3LocationCategory.KEY),
DS3LocationData("GA: Soul of the Twin Princes", "Soul of the Twin Princes", DS3LocationCategory.BOSS),
DS3LocationData("GA: Sage's Crystal Staff", "Sage's Crystal Staff", DS3LocationCategory.WEAPON),
DS3LocationData("GA: Outrider Knight Helm", "Outrider Knight Helm", DS3LocationCategory.ARMOR),
DS3LocationData("GA: Outrider Knight Armor", "Outrider Knight Armor", DS3LocationCategory.ARMOR),
DS3LocationData("GA: Outrider Knight Gauntlets", "Outrider Knight Gauntlets", DS3LocationCategory.ARMOR),
DS3LocationData("GA: Outrider Knight Leggings", "Outrider Knight Leggings", DS3LocationCategory.ARMOR),
DS3LocationData("GA: Crystal Scroll", "Crystal Scroll", DS3LocationCategory.MISC),
],
"Untended Graves": [
DS3LocationData("UG: Ashen Estus Ring", "Ashen Estus Ring", DS3LocationCategory.RING),
DS3LocationData("UG: Black Knight Glaive", "Black Knight Glaive", DS3LocationCategory.WEAPON),
DS3LocationData("UG: Hornet Ring", "Hornet Ring", DS3LocationCategory.RING),
DS3LocationData("UG: Chaos Blade", "Chaos Blade", DS3LocationCategory.WEAPON),
DS3LocationData("UG: Blacksmith Hammer", "Blacksmith Hammer", DS3LocationCategory.WEAPON),
DS3LocationData("UG: Eyes of a Fire Keeper", "Eyes of a Fire Keeper", DS3LocationCategory.KEY),
DS3LocationData("UG: Coiled Sword Fragment", "Coiled Sword Fragment", DS3LocationCategory.MISC),
DS3LocationData("UG: Soul of Champion Gundyr", "Soul of Champion Gundyr", DS3LocationCategory.BOSS),
],
"Archdragon Peak": [
DS3LocationData("AP: Lightning Clutch Ring", "Lightning Clutch Ring", DS3LocationCategory.RING),
DS3LocationData("AP: Ancient Dragon Greatshield", "Ancient Dragon Greatshield", DS3LocationCategory.SHIELD),
DS3LocationData("AP: Ring of Steel Protection", "Ring of Steel Protection", DS3LocationCategory.RING),
DS3LocationData("AP: Calamity Ring", "Calamity Ring", DS3LocationCategory.RING),
DS3LocationData("AP: Drakeblood Greatsword", "Drakeblood Greatsword", DS3LocationCategory.WEAPON),
DS3LocationData("AP: Dragonslayer Spear", "Dragonslayer Spear", DS3LocationCategory.WEAPON),
DS3LocationData("AP: Thunder Stoneplate Ring", "Thunder Stoneplate Ring", DS3LocationCategory.RING),
DS3LocationData("AP: Great Magic Barrier", "Great Magic Barrier", DS3LocationCategory.SPELL),
DS3LocationData("AP: Dragon Chaser's Ashes", "Dragon Chaser's Ashes", DS3LocationCategory.MISC),
DS3LocationData("AP: Twinkling Dragon Torso Stone", "Twinkling Dragon Torso Stone", DS3LocationCategory.MISC),
DS3LocationData("AP: Dragonslayer Helm", "Dragonslayer Helm", DS3LocationCategory.ARMOR),
DS3LocationData("AP: Dragonslayer Armor", "Dragonslayer Armor", DS3LocationCategory.ARMOR),
DS3LocationData("AP: Dragonslayer Gauntlets", "Dragonslayer Gauntlets", DS3LocationCategory.ARMOR),
DS3LocationData("AP: Dragonslayer Leggings", "Dragonslayer Leggings", DS3LocationCategory.ARMOR),
DS3LocationData("AP: Ricard's Rapier", "Ricard's Rapier", DS3LocationCategory.WEAPON),
DS3LocationData("AP: Soul of the Nameless King", "Soul of the Nameless King", DS3LocationCategory.BOSS),
DS3LocationData("AP: Dragon Tooth", "Dragon Tooth", DS3LocationCategory.WEAPON),
DS3LocationData("AP: Havel's Greatshield", "Havel's Greatshield", DS3LocationCategory.SHIELD),
],
"Kiln of the First Flame": [],
# DLC
"Painted World of Ariandel 1": [
DS3LocationData("PW: Follower Javelin", "Follower Javelin", DS3LocationCategory.WEAPON),
DS3LocationData("PW: Frozen Weapon", "Frozen Weapon", DS3LocationCategory.SPELL),
DS3LocationData("PW: Millwood Greatbow", "Millwood Greatbow", DS3LocationCategory.WEAPON),
DS3LocationData("PW: Captain's Ashes", "Captain's Ashes", DS3LocationCategory.MISC),
DS3LocationData("PW: Millwood Battle Axe", "Millwood Battle Axe", DS3LocationCategory.WEAPON),
DS3LocationData("PW: Ethereal Oak Shield", "Ethereal Oak Shield", DS3LocationCategory.SHIELD),
DS3LocationData("PW: Crow Quills", "Crow Quills", DS3LocationCategory.WEAPON),
DS3LocationData("PW: Slave Knight Hood", "Slave Knight Hood", DS3LocationCategory.ARMOR),
DS3LocationData("PW: Slave Knight Armor", "Slave Knight Armor", DS3LocationCategory.ARMOR),
DS3LocationData("PW: Slave Knight Gauntlets", "Slave Knight Gauntlets", DS3LocationCategory.ARMOR),
DS3LocationData("PW: Slave Knight Leggings", "Slave Knight Leggings", DS3LocationCategory.ARMOR),
DS3LocationData("PW: Way of White Corona", "Way of White Corona", DS3LocationCategory.SPELL),
DS3LocationData("PW: Crow Talons", "Crow Talons", DS3LocationCategory.WEAPON),
DS3LocationData("PW: Onyx Blade", "Onyx Blade", DS3LocationCategory.WEAPON),
DS3LocationData("PW: Contraption Key", "Contraption Key", DS3LocationCategory.KEY),
],
"Painted World of Ariandel 2": [
DS3LocationData("PW: Quakestone Hammer", "Quakestone Hammer", DS3LocationCategory.WEAPON),
DS3LocationData("PW: Earth Seeker", "Earth Seeker", DS3LocationCategory.WEAPON),
DS3LocationData("PW: Follower Torch", "Follower Torch", DS3LocationCategory.SHIELD),
DS3LocationData("PW: Follower Shield", "Follower Shield", DS3LocationCategory.SHIELD),
DS3LocationData("PW: Follower Sabre", "Follower Sabre", DS3LocationCategory.WEAPON),
DS3LocationData("PW: Snap Freeze", "Snap Freeze", DS3LocationCategory.SPELL),
DS3LocationData("PW: Floating Chaos", "Floating Chaos", DS3LocationCategory.SPELL),
DS3LocationData("PW: Pyromancer's Parting Flame", "Pyromancer's Parting Flame", DS3LocationCategory.WEAPON),
DS3LocationData("PW: Vilhelm's Helm", "Vilhelm's Helm", DS3LocationCategory.ARMOR),
DS3LocationData("PW: Vilhelm's Armor", "Vilhelm's Armor", DS3LocationCategory.ARMOR),
DS3LocationData("PW: Vilhelm's Gauntlets", "Vilhelm's Gauntlets", DS3LocationCategory.ARMOR),
DS3LocationData("PW: Vilhelm's Leggings", "Vilhelm's Leggings", DS3LocationCategory.ARMOR),
DS3LocationData("PW: Valorheart", "Valorheart", DS3LocationCategory.WEAPON),
DS3LocationData("PW: Champion's Bones", "Champion's Bones", DS3LocationCategory.MISC),
DS3LocationData("PW: Soul of Sister Friede", "Soul of Sister Friede", DS3LocationCategory.BOSS),
DS3LocationData("PW: Chillbite Ring", "Chillbite Ring", DS3LocationCategory.RING),
],
"Dreg Heap": [
DS3LocationData("DH: Loincloth", "Loincloth", DS3LocationCategory.ARMOR),
DS3LocationData("DH: Aquamarine Dagger", "Aquamarine Dagger", DS3LocationCategory.WEAPON),
DS3LocationData("DH: Murky Hand Scythe", "Murky Hand Scythe", DS3LocationCategory.WEAPON),
DS3LocationData("DH: Murky Longstaff", "Murky Longstaff", DS3LocationCategory.WEAPON),
DS3LocationData("DH: Great Soul Dregs", "Great Soul Dregs", DS3LocationCategory.SPELL),
DS3LocationData("DH: Lothric War Banner", "Lothric War Banner", DS3LocationCategory.WEAPON),
DS3LocationData("DH: Projected Heal", "Projected Heal", DS3LocationCategory.SPELL),
DS3LocationData("DH: Desert Pyromancer Hood", "Desert Pyromancer Hood", DS3LocationCategory.ARMOR),
DS3LocationData("DH: Desert Pyromancer Garb", "Desert Pyromancer Garb", DS3LocationCategory.ARMOR),
DS3LocationData("DH: Desert Pyromancer Gloves", "Desert Pyromancer Gloves", DS3LocationCategory.ARMOR),
DS3LocationData("DH: Desert Pyromancer Skirt", "Desert Pyromancer Skirt", DS3LocationCategory.ARMOR),
DS3LocationData("DH: Giant Door Shield", "Giant Door Shield", DS3LocationCategory.SHIELD),
DS3LocationData("DH: Herald Curved Greatsword", "Herald Curved Greatsword", DS3LocationCategory.WEAPON),
DS3LocationData("DH: Flame Fan", "Flame Fan", DS3LocationCategory.SPELL),
DS3LocationData("DH: Soul of the Demon Prince", "Soul of the Demon Prince", DS3LocationCategory.BOSS),
DS3LocationData("DH: Small Envoy Banner", "Small Envoy Banner", DS3LocationCategory.KEY),
DS3LocationData("DH: Ring of Favor+3", "Ring of Favor+3", DS3LocationCategory.RING),
DS3LocationData("DH: Covetous Silver Serpent Ring+3", "Covetous Silver Serpent Ring+3", DS3LocationCategory.RING),
DS3LocationData("DH: Ring of Steel Protection+3", "Ring of Steel Protection+3", DS3LocationCategory.RING),
],
"Ringed City": [
DS3LocationData("RC: Ruin Sentinel Helm", "Ruin Sentinel Helm", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Ruin Sentinel Armor", "Ruin Sentinel Armor", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Ruin Sentinel Gauntlets", "Ruin Sentinel Gauntlets", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Ruin Sentinel Leggings", "Ruin Sentinel Leggings", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Black Witch Veil", "Black Witch Veil", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Black Witch Hat", "Black Witch Hat", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Black Witch Garb", "Black Witch Garb", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Black Witch Wrappings", "Black Witch Wrappings", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Black Witch Trousers", "Black Witch Trousers", DS3LocationCategory.ARMOR),
DS3LocationData("RC: White Preacher Head", "White Preacher Head", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Havel's Ring+3", "Havel's Ring+3", DS3LocationCategory.RING),
DS3LocationData("RC: Ringed Knight Spear", "Ringed Knight Spear", DS3LocationCategory.WEAPON),
DS3LocationData("RC: Dragonhead Shield", "Dragonhead Shield", DS3LocationCategory.SHIELD),
DS3LocationData("RC: Ringed Knight Straight Sword", "Ringed Knight Straight Sword", DS3LocationCategory.WEAPON),
DS3LocationData("RC: Preacher's Right Arm", "Preacher's Right Arm", DS3LocationCategory.WEAPON),
DS3LocationData("RC: White Birch Bow", "White Birch Bow", DS3LocationCategory.WEAPON),
DS3LocationData("RC: Church Guardian Shiv", "Church Guardian Shiv", DS3LocationCategory.MISC),
DS3LocationData("RC: Dragonhead Greatshield", "Dragonhead Greatshield", DS3LocationCategory.SHIELD),
DS3LocationData("RC: Ringed Knight Paired Greatswords", "Ringed Knight Paired Greatswords", DS3LocationCategory.WEAPON),
DS3LocationData("RC: Shira's Crown", "Shira's Crown", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Shira's Armor", "Shira's Armor", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Shira's Gloves", "Shira's Gloves", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Shira's Trousers", "Shira's Trousers", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Crucifix of the Mad King", "Crucifix of the Mad King", DS3LocationCategory.WEAPON),
DS3LocationData("RC: Sacred Chime of Filianore", "Sacred Chime of Filianore", DS3LocationCategory.WEAPON),
DS3LocationData("RC: Iron Dragonslayer Helm", "Iron Dragonslayer Helm", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Iron Dragonslayer Armor", "Iron Dragonslayer Armor", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Iron Dragonslayer Gauntlets", "Iron Dragonslayer Gauntlets", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Iron Dragonslayer Leggings", "Iron Dragonslayer Leggings", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Lightning Arrow", "Lightning Arrow", DS3LocationCategory.SPELL),
DS3LocationData("RC: Ritual Spear Fragment", "Ritual Spear Fragment", DS3LocationCategory.MISC),
DS3LocationData("RC: Antiquated Plain Garb", "Antiquated Plain Garb", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Violet Wrappings", "Violet Wrappings", DS3LocationCategory.ARMOR),
DS3LocationData("RC: Soul of Darkeater Midir", "Soul of Darkeater Midir", DS3LocationCategory.BOSS),
DS3LocationData("RC: Soul of Slave Knight Gael", "Soul of Slave Knight Gael", DS3LocationCategory.BOSS),
DS3LocationData("RC: Blood of the Dark Soul", "Blood of the Dark Soul", DS3LocationCategory.KEY),
DS3LocationData("RC: Chloranthy Ring+3", "Chloranthy Ring+3", DS3LocationCategory.RING),
DS3LocationData("RC: Covetous Gold Serpent Ring+3", "Covetous Gold Serpent Ring+3", DS3LocationCategory.RING),
DS3LocationData("RC: Ring of the Evil Eye+3", "Ring of the Evil Eye+3", DS3LocationCategory.RING),
DS3LocationData("RC: Wolf Ring+3", "Wolf Ring+3", DS3LocationCategory.RING),
],
# Progressive
"Progressive Items 1": [] +
# Upgrade materials
[DS3LocationData(f"Titanite Shard #{i + 1}", "Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(26)] +
[DS3LocationData(f"Large Titanite Shard #{i + 1}", "Large Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(28)] +
[DS3LocationData(f"Titanite Slab #{i + 1}", "Titanite Slab", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Twinkling Titanite #{i + 1}", "Twinkling Titanite", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(15)] +
# Healing
[DS3LocationData(f"Estus Shard #{i + 1}", "Estus Shard", DS3LocationCategory.HEALTH) for i in range(11)] +
[DS3LocationData(f"Undead Bone Shard #{i + 1}", "Undead Bone Shard", DS3LocationCategory.HEALTH) for i in range(10)],
"Progressive Items 2": [] +
# Items
[DS3LocationData(f"Green Blossom #{i + 1}", "Green Blossom", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] +
[DS3LocationData(f"Firebomb #{i + 1}", "Firebomb", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] +
[DS3LocationData(f"Alluring Skull #{i + 1}", "Alluring Skull", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Undead Hunter Charm #{i + 1}", "Undead Hunter Charm", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Duel Charm #{i + 1}", "Duel Charm", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Throwing Knife #{i + 1}", "Throwing Knife", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Gold Pine Resin #{i + 1}", "Gold Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
[DS3LocationData(f"Charcoal Pine Resin #{i + 1}", "Charcoal Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
[DS3LocationData(f"Human Pine Resin #{i + 1}", "Human Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Carthus Rouge #{i + 1}", "Carthus Rouge", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
[DS3LocationData(f"Pale Pine Resin #{i + 1}", "Pale Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
[DS3LocationData(f"Charcoal Pine Bundle #{i + 1}", "Charcoal Pine Bundle", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Rotten Pine Resin #{i + 1}", "Rotten Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Homeward Bone #{i + 1}", "Homeward Bone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(16)] +
[DS3LocationData(f"Pale Tongue #{i + 1}", "Pale Tongue", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Rusted Coin #{i + 1}", "Rusted Coin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Rusted Gold Coin #{i + 1}", "Rusted Gold Coin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Ember #{i + 1}", "Ember", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(45)],
"Progressive Items 3": [] +
# Souls & Bulk Upgrade Materials
[DS3LocationData(f"Fading Soul #{i + 1}", "Fading Soul", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
[DS3LocationData(f"Soul of a Deserted Corpse #{i + 1}", "Soul of a Deserted Corpse", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
[DS3LocationData(f"Large Soul of a Deserted Corpse #{i + 1}", "Large Soul of a Deserted Corpse", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
[DS3LocationData(f"Soul of an Unknown Traveler #{i + 1}", "Soul of an Unknown Traveler", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
[DS3LocationData(f"Large Soul of an Unknown Traveler #{i + 1}", "Large Soul of an Unknown Traveler", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
[DS3LocationData(f"Soul of a Nameless Soldier #{i + 1}", "Soul of a Nameless Soldier", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] +
[DS3LocationData(f"Large Soul of a Nameless Soldier #{i + 1}", "Large Soul of a Nameless Soldier", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] +
[DS3LocationData(f"Soul of a Weary Warrior #{i + 1}", "Soul of a Weary Warrior", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] +
[DS3LocationData(f"Soul of a Crestfallen Knight #{i + 1}", "Soul of a Crestfallen Knight", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Titanite Chunk #{i + 1}", "Titanite Chunk", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(22)] +
[DS3LocationData(f"Titanite Scale #{i + 1}", "Titanite Scale", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(29)],
"Progressive Items 4": [] +
# Gems & Random Consumables
[DS3LocationData(f"Ring of Sacrifice #{i + 1}", "Ring of Sacrifice", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] +
[DS3LocationData(f"Divine Blessing #{i + 1}", "Divine Blessing", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Hidden Blessing #{i + 1}", "Hidden Blessing", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
[DS3LocationData(f"Budding Green Blossom #{i + 1}", "Budding Green Blossom", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
[DS3LocationData(f"Bloodred Moss Clump #{i + 1}", "Bloodred Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
[DS3LocationData(f"Purple Moss Clump #{i + 1}", "Purple Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Blooming Purple Moss Clump #{i + 1}", "Blooming Purple Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
[DS3LocationData(f"Purging Stone #{i + 1}", "Purging Stone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Rime-blue Moss Clump #{i + 1}", "Rime-blue Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Repair Powder #{i + 1}", "Repair Powder", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Kukri #{i + 1}", "Kukri", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Lightning Urn #{i + 1}", "Lightning Urn", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Rubbish #{i + 1}", "Rubbish", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
[DS3LocationData(f"Blue Bug Pellet #{i + 1}", "Blue Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Red Bug Pellet #{i + 1}", "Red Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Yellow Bug Pellet #{i + 1}", "Yellow Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Black Bug Pellet #{i + 1}", "Black Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Heavy Gem #{i + 1}", "Heavy Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Sharp Gem #{i + 1}", "Sharp Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Refined Gem #{i + 1}", "Refined Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Crystal Gem #{i + 1}", "Crystal Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Simple Gem #{i + 1}", "Simple Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Fire Gem #{i + 1}", "Fire Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Chaos Gem #{i + 1}", "Chaos Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Lightning Gem #{i + 1}", "Lightning Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Deep Gem #{i + 1}", "Deep Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Dark Gem #{i + 1}", "Dark Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Poison Gem #{i + 1}", "Poison Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Blood Gem #{i + 1}", "Blood Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
[DS3LocationData(f"Raw Gem #{i + 1}", "Raw Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Blessed Gem #{i + 1}", "Blessed Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Hollow Gem #{i + 1}", "Hollow Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Shriving Stone #{i + 1}", "Shriving Stone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)],
"Progressive Items DLC": [] +
# Upgrade materials
[DS3LocationData(f"Large Titanite Shard ${i + 1}", "Large Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Titanite Chunk ${i + 1}", "Titanite Chunk", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(15)] +
[DS3LocationData(f"Titanite Slab ${i + 1}", "Titanite Slab", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Twinkling Titanite ${i + 1}", "Twinkling Titanite", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
[DS3LocationData(f"Titanite Scale ${i + 1}", "Titanite Scale", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(11)] +
# Items
[DS3LocationData(f"Homeward Bone ${i + 1}", "Homeward Bone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] +
[DS3LocationData(f"Rusted Coin ${i + 1}", "Rusted Coin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
[DS3LocationData(f"Ember ${i + 1}", "Ember", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(10)] +
# Souls
[DS3LocationData(f"Large Soul of an Unknown Traveler ${i + 1}", "Large Soul of an Unknown Traveler", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(9)] +
[DS3LocationData(f"Soul of a Weary Warrior ${i + 1}", "Soul of a Weary Warrior", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
[DS3LocationData(f"Large Soul of a Weary Warrior ${i + 1}", "Large Soul of a Weary Warrior", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] +
[DS3LocationData(f"Soul of a Crestfallen Knight ${i + 1}", "Soul of a Crestfallen Knight", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] +
[DS3LocationData(f"Large Soul of a Crestfallen Knight ${i + 1}", "Large Soul of a Crestfallen Knight", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
# Gems
[DS3LocationData(f"Dark Gem ${i + 1}", "Dark Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Blood Gem ${i + 1}", "Blood Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
[DS3LocationData(f"Blessed Gem ${i + 1}", "Blessed Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Hollow Gem ${i + 1}", "Hollow Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)]
}
location_dictionary: Dict[str, DS3LocationData] = {}
for location_table in location_tables.values():
location_dictionary.update({location_data.name: location_data for location_data in location_table})

View File

@@ -1,16 +1,91 @@
import typing
from Options import Toggle, Option, Range, Choice, DeathLink
from Options import Toggle, DefaultOnToggle, Option, Range, Choice, ItemDict, DeathLink
class RandomizeWeaponLocations(DefaultOnToggle):
"""Randomizes weapons (+76 locations)"""
display_name = "Randomize Weapon Locations"
class RandomizeShieldLocations(DefaultOnToggle):
"""Randomizes shields (+24 locations)"""
display_name = "Randomize Shield Locations"
class RandomizeArmorLocations(DefaultOnToggle):
"""Randomizes armor pieces (+97 locations)"""
display_name = "Randomize Armor Locations"
class RandomizeRingLocations(DefaultOnToggle):
"""Randomizes rings (+49 locations)"""
display_name = "Randomize Ring Locations"
class RandomizeSpellLocations(DefaultOnToggle):
"""Randomizes spells (+18 locations)"""
display_name = "Randomize Spell Locations"
class RandomizeKeyLocations(DefaultOnToggle):
"""Randomizes items which unlock doors or bypass barriers"""
display_name = "Randomize Key Locations"
class RandomizeBossSoulLocations(DefaultOnToggle):
"""Randomizes Boss Souls (+18 Locations)"""
display_name = "Randomize Boss Soul Locations"
class RandomizeNPCLocations(Toggle):
"""Randomizes friendly NPC drops (meaning you will probably have to kill them) (+14 locations)"""
display_name = "Randomize NPC Locations"
class RandomizeMiscLocations(Toggle):
"""Randomizes miscellaneous items (ashes, tomes, scrolls, etc.) to the pool. (+36 locations)"""
display_name = "Randomize Miscellaneous Locations"
class RandomizeHealthLocations(Toggle):
"""Randomizes health upgrade items. (+21 locations)"""
display_name = "Randomize Health Upgrade Locations"
class RandomizeProgressiveLocationsOption(Toggle):
"""Randomizes upgrade materials and consumables such as the titanite shards, firebombs, resin, etc...
Instead of specific locations, these are progressive, so Titanite Shard #1 is the first titanite shard
you pick up, regardless of whether it's from an enemy drop late in the game or an item on the ground in the
first 5 minutes."""
display_name = "Randomize Progressive Locations"
class PoolTypeOption(Choice):
"""Changes which non-progression items you add to the pool
Shuffle: Items are picked from the locations being randomized
Various: Items are picked from a list of all items in the game, but are the same type of item they replace"""
display_name = "Pool Type"
option_shuffle = 0
option_various = 1
class GuaranteedItemsOption(ItemDict):
"""Guarantees that the specified items will be in the item pool"""
display_name = "Guaranteed Items"
class AutoEquipOption(Toggle):
"""Automatically equips any received armor or left/right weapons."""
display_name = "Auto-equip"
display_name = "Auto-Equip"
class LockEquipOption(Toggle):
"""Lock the equipment slots so you cannot change your armor or your left/right weapons. Works great with the
Auto-equip option."""
display_name = "Lock Equipement Slots"
display_name = "Lock Equipment Slots"
class NoWeaponRequirementsOption(Toggle):
@@ -26,93 +101,124 @@ class NoSpellRequirementsOption(Toggle):
class NoEquipLoadOption(Toggle):
"""Disable the equip load constraint from the game"""
display_name = "No Equip load"
display_name = "No Equip Load"
class RandomizeWeaponsLevelOption(Choice):
class RandomizeInfusionOption(Toggle):
"""Enable this option to infuse a percentage of the pool of weapons and shields."""
display_name = "Randomize Infusion"
class RandomizeInfusionPercentageOption(Range):
"""The percentage of weapons/shields in the pool to be infused if Randomize Infusion is toggled"""
display_name = "Percentage of Infused Weapons"
range_start = 0
range_end = 100
default = 33
class RandomizeWeaponLevelOption(Choice):
"""Enable this option to upgrade a percentage of the pool of weapons to a random value between the minimum and
maximum levels defined.
all: All weapons are eligible, both basic and epic
basic: Only weapons that can be upgraded to +10
epic: Only weapons that can be upgraded to +5"""
display_name = "Randomize weapons level"
maximum levels defined.
All: All weapons are eligible, both basic and epic
Basic: Only weapons that can be upgraded to +10
Epic: Only weapons that can be upgraded to +5"""
display_name = "Randomize Weapon Level"
option_none = 0
option_all = 1
option_basic = 2
option_epic = 3
class RandomizeWeaponsLevelPercentageOption(Range):
class RandomizeWeaponLevelPercentageOption(Range):
"""The percentage of weapons in the pool to be upgraded if randomize weapons level is toggled"""
display_name = "Percentage of randomized weapons"
range_start = 1
display_name = "Percentage of Randomized Weapons"
range_start = 0
range_end = 100
default = 33
class MinLevelsIn5WeaponPoolOption(Range):
"""The minimum upgraded value of a weapon in the pool of weapons that can only reach +5"""
display_name = "Minimum level of +5 weapons"
range_start = 1
display_name = "Minimum Level of +5 Weapons"
range_start = 0
range_end = 5
default = 1
class MaxLevelsIn5WeaponPoolOption(Range):
"""The maximum upgraded value of a weapon in the pool of weapons that can only reach +5"""
display_name = "Maximum level of +5 weapons"
range_start = 1
display_name = "Maximum Level of +5 Weapons"
range_start = 0
range_end = 5
default = 5
class MinLevelsIn10WeaponPoolOption(Range):
"""The minimum upgraded value of a weapon in the pool of weapons that can reach +10"""
display_name = "Minimum level of +10 weapons"
range_start = 1
display_name = "Minimum Level of +10 Weapons"
range_start = 0
range_end = 10
default = 1
class MaxLevelsIn10WeaponPoolOption(Range):
"""The maximum upgraded value of a weapon in the pool of weapons that can reach +10"""
display_name = "Maximum level of +10 weapons"
range_start = 1
display_name = "Maximum Level of +10 Weapons"
range_start = 0
range_end = 10
default = 10
class LateBasinOfVowsOption(Toggle):
"""Force the Basin of Vows to be located as a reward of defeating Pontiff Sulyvahn. It permits to ease the
progression by preventing having to kill the Dancer of the Boreal Valley as the first boss"""
"""This option makes it so the Basin of Vows is still randomized, but guarantees you that you wont have to venture into
Lothric Castle to find your Small Lothric Banner to get out of High Wall of Lothric. So you may find Basin of Vows early,
but you wont have to fight Dancer to find your Small Lothric Banner."""
display_name = "Late Basin of Vows"
class EnableProgressiveLocationsOption(Toggle):
"""Randomize upgrade materials such as the titanite shards, the estus shards and the consumables"""
display_name = "Randomize materials, Estus shards and consumables (+196 checks/items)"
class LateDLCOption(Toggle):
"""This option makes it so you are guaranteed to find your Small Doll without having to venture off into the DLC,
effectively putting anything in the DLC in logic after finding both Contraption Key and Small Doll,
and being able to get into Irithyll of the Boreal Valley."""
display_name = "Late DLC"
class EnableDLCOption(Toggle):
"""To use this option, you must own both the ASHES OF ARIANDEL and the RINGED CITY DLC"""
display_name = "Add the DLC Items and Locations to the pool (+81 checks/items)"
display_name = "Enable DLC"
dark_souls_options: typing.Dict[str, type(Option)] = {
dark_souls_options: typing.Dict[str, Option] = {
"enable_weapon_locations": RandomizeWeaponLocations,
"enable_shield_locations": RandomizeShieldLocations,
"enable_armor_locations": RandomizeArmorLocations,
"enable_ring_locations": RandomizeRingLocations,
"enable_spell_locations": RandomizeSpellLocations,
"enable_key_locations": RandomizeKeyLocations,
"enable_boss_locations": RandomizeBossSoulLocations,
"enable_npc_locations": RandomizeNPCLocations,
"enable_misc_locations": RandomizeMiscLocations,
"enable_health_upgrade_locations": RandomizeHealthLocations,
"enable_progressive_locations": RandomizeProgressiveLocationsOption,
"pool_type": PoolTypeOption,
"guaranteed_items": GuaranteedItemsOption,
"auto_equip": AutoEquipOption,
"lock_equip": LockEquipOption,
"no_weapon_requirements": NoWeaponRequirementsOption,
"randomize_weapons_level": RandomizeWeaponsLevelOption,
"randomize_weapons_percentage": RandomizeWeaponsLevelPercentageOption,
"randomize_infusion": RandomizeInfusionOption,
"randomize_infusion_percentage": RandomizeInfusionPercentageOption,
"randomize_weapon_level": RandomizeWeaponLevelOption,
"randomize_weapon_level_percentage": RandomizeWeaponLevelPercentageOption,
"min_levels_in_5": MinLevelsIn5WeaponPoolOption,
"max_levels_in_5": MaxLevelsIn5WeaponPoolOption,
"min_levels_in_10": MinLevelsIn10WeaponPoolOption,
"max_levels_in_10": MaxLevelsIn10WeaponPoolOption,
"late_basin_of_vows": LateBasinOfVowsOption,
"late_dlc": LateDLCOption,
"no_spell_requirements": NoSpellRequirementsOption,
"no_equip_load": NoEquipLoadOption,
"death_link": DeathLink,
"enable_progressive_locations": EnableProgressiveLocationsOption,
"enable_dlc": EnableDLCOption,
}

View File

@@ -1,21 +1,15 @@
# world/dark_souls_3/__init__.py
from typing import Dict
from typing import Dict, Set, List
from .Items import DarkSouls3Item
from .Locations import DarkSouls3Location
from .Options import dark_souls_options
from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary, key_items_list, \
dlc_weapons_upgrade_5_table, dlc_weapons_upgrade_10_table
from .data.locations_data import location_dictionary, fire_link_shrine_table, \
high_wall_of_lothric, \
undead_settlement_table, road_of_sacrifice_table, consumed_king_garden_table, cathedral_of_the_deep_table, \
farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, \
irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, grand_archives_table, \
untended_graves_table, archdragon_peak_table, firelink_shrine_bell_tower_table, progressive_locations, \
progressive_locations_2, progressive_locations_3, painted_world_table, dreg_heap_table, ringed_city_table, dlc_progressive_locations
from ..AutoWorld import World, WebWorld
from BaseClasses import MultiWorld, Region, Item, Entrance, Tutorial, ItemClassification
from ..generic.Rules import set_rule, add_item_rule
from Options import Toggle
from worlds.AutoWorld import World, WebWorld
from worlds.generic.Rules import set_rule, add_rule, add_item_rule
from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names
from .Locations import DarkSouls3Location, DS3LocationCategory, location_tables, location_dictionary
from .Options import RandomizeWeaponLevelOption, PoolTypeOption, dark_souls_options
class DarkSouls3Web(WebWorld):
@@ -52,212 +46,399 @@ class DarkSouls3World(World):
option_definitions = dark_souls_options
topology_present: bool = True
web = DarkSouls3Web()
data_version = 5
data_version = 7
base_id = 100000
required_client_version = (0, 3, 7)
enabled_location_categories: Set[DS3LocationCategory]
required_client_version = (0, 4, 2)
item_name_to_id = DarkSouls3Item.get_name_to_id()
location_name_to_id = DarkSouls3Location.get_name_to_id()
def __init__(self, world: MultiWorld, player: int):
super().__init__(world, player)
def __init__(self, multiworld: MultiWorld, player: int):
super().__init__(multiworld, player)
self.locked_items = []
self.locked_locations = []
self.main_path_locations = []
self.enabled_location_categories = set()
def generate_early(self):
if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.WEAPON)
if self.multiworld.enable_shield_locations[self.player] == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.SHIELD)
if self.multiworld.enable_armor_locations[self.player] == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.ARMOR)
if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.RING)
if self.multiworld.enable_spell_locations[self.player] == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.SPELL)
if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.NPC)
if self.multiworld.enable_key_locations[self.player] == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.KEY)
if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.BOSS)
if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.MISC)
if self.multiworld.enable_health_upgrade_locations[self.player] == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.HEALTH)
if self.multiworld.enable_progressive_locations[self.player] == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.PROGRESSIVE_ITEM)
def create_regions(self):
progressive_location_table = []
if self.multiworld.enable_progressive_locations[self.player].value:
progressive_location_table = [] + \
location_tables["Progressive Items 1"] + \
location_tables["Progressive Items 2"] + \
location_tables["Progressive Items 3"] + \
location_tables["Progressive Items 4"]
if self.multiworld.enable_dlc[self.player].value:
progressive_location_table += location_tables["Progressive Items DLC"]
# Create Vanilla Regions
regions: Dict[str, Region] = {}
regions["Menu"] = self.create_region("Menu", progressive_location_table)
regions.update({region_name: self.create_region(region_name, location_tables[region_name]) for region_name in [
"Firelink Shrine",
"Firelink Shrine Bell Tower",
"High Wall of Lothric",
"Undead Settlement",
"Road of Sacrifices",
"Cathedral of the Deep",
"Farron Keep",
"Catacombs of Carthus",
"Smouldering Lake",
"Irithyll of the Boreal Valley",
"Irithyll Dungeon",
"Profaned Capital",
"Anor Londo",
"Lothric Castle",
"Consumed King's Garden",
"Grand Archives",
"Untended Graves",
"Archdragon Peak",
"Kiln of the First Flame",
]})
# Adds Path of the Dragon as an event item for Archdragon Peak access
potd_location = DarkSouls3Location(self.player, "CKG: Path of the Dragon", DS3LocationCategory.EVENT, "Path of the Dragon", None, regions["Consumed King's Garden"])
potd_location.place_locked_item(Item("Path of the Dragon", ItemClassification.progression, None, self.player))
regions["Consumed King's Garden"].locations.append(potd_location)
# Create DLC Regions
if self.multiworld.enable_dlc[self.player]:
regions.update({region_name: self.create_region(region_name, location_tables[region_name]) for region_name in [
"Painted World of Ariandel 1",
"Painted World of Ariandel 2",
"Dreg Heap",
"Ringed City",
]})
# Connect Regions
def create_connection(from_region: str, to_region: str):
connection = Entrance(self.player, f"Go To {to_region}", regions[from_region])
regions[from_region].exits.append(connection)
connection.connect(regions[to_region])
regions["Menu"].exits.append(Entrance(self.player, "New Game", regions["Menu"]))
self.multiworld.get_entrance("New Game", self.player).connect(regions["Firelink Shrine"])
create_connection("Firelink Shrine", "High Wall of Lothric")
create_connection("Firelink Shrine", "Firelink Shrine Bell Tower")
create_connection("Firelink Shrine", "Kiln of the First Flame")
create_connection("High Wall of Lothric", "Undead Settlement")
create_connection("High Wall of Lothric", "Lothric Castle")
create_connection("Undead Settlement", "Road of Sacrifices")
create_connection("Road of Sacrifices", "Cathedral of the Deep")
create_connection("Road of Sacrifices", "Farron Keep")
create_connection("Farron Keep", "Catacombs of Carthus")
create_connection("Catacombs of Carthus", "Irithyll of the Boreal Valley")
create_connection("Catacombs of Carthus", "Smouldering Lake")
create_connection("Irithyll of the Boreal Valley", "Irithyll Dungeon")
create_connection("Irithyll of the Boreal Valley", "Anor Londo")
create_connection("Irithyll Dungeon", "Archdragon Peak")
create_connection("Irithyll Dungeon", "Profaned Capital")
create_connection("Lothric Castle", "Consumed King's Garden")
create_connection("Lothric Castle", "Grand Archives")
create_connection("Consumed King's Garden", "Untended Graves")
# Connect DLC Regions
if self.multiworld.enable_dlc[self.player]:
create_connection("Cathedral of the Deep", "Painted World of Ariandel 1")
create_connection("Painted World of Ariandel 1", "Painted World of Ariandel 2")
create_connection("Painted World of Ariandel 2", "Dreg Heap")
create_connection("Dreg Heap", "Ringed City")
# For each region, add the associated locations retrieved from the corresponding location_table
def create_region(self, region_name, location_table) -> Region:
new_region = Region(region_name, self.player, self.multiworld)
for location in location_table:
if location.category in self.enabled_location_categories:
new_location = DarkSouls3Location(
self.player,
location.name,
location.category,
location.default_item,
self.location_name_to_id[location.name],
new_region
)
else:
# Replace non-randomized progression items with events
event_item = self.create_item(location.default_item)
if event_item.classification != ItemClassification.progression:
continue
new_location = DarkSouls3Location(
self.player,
location.name,
location.category,
location.default_item,
None,
new_region
)
event_item.code = None
new_location.place_locked_item(event_item)
if region_name == "Menu":
add_item_rule(new_location, lambda item: not item.advancement)
new_region.locations.append(new_location)
self.multiworld.regions.append(new_region)
return new_region
def create_items(self):
dlc_enabled = self.multiworld.enable_dlc[self.player] == Toggle.option_true
itempool_by_category = {category: [] for category in self.enabled_location_categories}
# Gather all default items on randomized locations
num_required_extra_items = 0
for location in self.multiworld.get_locations(self.player):
if location.category in itempool_by_category:
if item_dictionary[location.default_item_name].category == DS3ItemCategory.SKIP:
num_required_extra_items += 1
else:
itempool_by_category[location.category].append(location.default_item_name)
# Replace each item category with a random sample of items of those types
if self.multiworld.pool_type[self.player] == PoolTypeOption.option_various:
def create_random_replacement_list(item_categories: Set[DS3ItemCategory], num_items: int):
candidates = [
item.name for item
in item_dictionary.values()
if (item.category in item_categories and (not item.is_dlc or dlc_enabled))
]
return self.multiworld.random.sample(candidates, num_items)
if DS3LocationCategory.WEAPON in self.enabled_location_categories:
itempool_by_category[DS3LocationCategory.WEAPON] = create_random_replacement_list(
{
DS3ItemCategory.WEAPON_UPGRADE_5,
DS3ItemCategory.WEAPON_UPGRADE_10,
DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE
},
len(itempool_by_category[DS3LocationCategory.WEAPON])
)
if DS3LocationCategory.SHIELD in self.enabled_location_categories:
itempool_by_category[DS3LocationCategory.SHIELD] = create_random_replacement_list(
{DS3ItemCategory.SHIELD, DS3ItemCategory.SHIELD_INFUSIBLE},
len(itempool_by_category[DS3LocationCategory.SHIELD])
)
if DS3LocationCategory.ARMOR in self.enabled_location_categories:
itempool_by_category[DS3LocationCategory.ARMOR] = create_random_replacement_list(
{DS3ItemCategory.ARMOR},
len(itempool_by_category[DS3LocationCategory.ARMOR])
)
if DS3LocationCategory.RING in self.enabled_location_categories:
itempool_by_category[DS3LocationCategory.RING] = create_random_replacement_list(
{DS3ItemCategory.RING},
len(itempool_by_category[DS3LocationCategory.RING])
)
if DS3LocationCategory.SPELL in self.enabled_location_categories:
itempool_by_category[DS3LocationCategory.SPELL] = create_random_replacement_list(
{DS3ItemCategory.SPELL},
len(itempool_by_category[DS3LocationCategory.SPELL])
)
itempool: List[DarkSouls3Item] = []
for category in self.enabled_location_categories:
itempool += [self.create_item(name) for name in itempool_by_category[category]]
# A list of items we can replace
removable_items = [item for item in itempool if item.classification != ItemClassification.progression]
guaranteed_items = self.multiworld.guaranteed_items[self.player].value
for item_name in guaranteed_items:
# Break early just in case nothing is removable (if user is trying to guarantee more
# items than the pool can hold, for example)
if len(removable_items) == 0:
break
num_existing_copies = len([item for item in itempool if item.name == item_name])
for _ in range(guaranteed_items[item_name]):
if num_existing_copies > 0:
num_existing_copies -= 1
continue
if num_required_extra_items > 0:
# We can just add them instead of using "Soul of an Intrepid Hero" later
num_required_extra_items -= 1
else:
if len(removable_items) == 0:
break
# Try to construct a list of items with the same category that can be removed
# If none exist, just remove something at random
removable_shortlist = [
item for item
in removable_items
if item_dictionary[item.name].category == item_dictionary[item_name].category
]
if len(removable_shortlist) == 0:
removable_shortlist = removable_items
removed_item = self.multiworld.random.choice(removable_shortlist)
removable_items.remove(removed_item) # To avoid trying to replace the same item twice
itempool.remove(removed_item)
itempool.append(self.create_item(item_name))
# Extra filler items for locations containing SKIP items
itempool += [self.create_filler() for _ in range(num_required_extra_items)]
# Add items to itempool
self.multiworld.itempool += itempool
def create_item(self, name: str) -> Item:
useful_categories = {
DS3ItemCategory.WEAPON_UPGRADE_5,
DS3ItemCategory.WEAPON_UPGRADE_10,
DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE,
DS3ItemCategory.SPELL,
}
data = self.item_name_to_id[name]
if name in key_items_list:
if name in key_item_names:
item_classification = ItemClassification.progression
elif name in weapons_upgrade_5_table or name in weapons_upgrade_10_table:
elif item_dictionary[name].category in useful_categories or name in {"Estus Shard", "Undead Bone Shard"}:
item_classification = ItemClassification.useful
else:
item_classification = ItemClassification.filler
return DarkSouls3Item(name, item_classification, data, self.player)
def create_regions(self):
if self.multiworld.enable_progressive_locations[self.player].value and self.multiworld.enable_dlc[self.player].value:
menu_region = self.create_region("Menu", {**progressive_locations, **progressive_locations_2,
**progressive_locations_3, **dlc_progressive_locations})
elif self.multiworld.enable_progressive_locations[self.player].value:
menu_region = self.create_region("Menu", {**progressive_locations, **progressive_locations_2,
**progressive_locations_3})
else:
menu_region = self.create_region("Menu", None)
def get_filler_item_name(self) -> str:
return "Soul of an Intrepid Hero"
# Create all Vanilla regions of Dark Souls III
firelink_shrine_region = self.create_region("Firelink Shrine", fire_link_shrine_table)
firelink_shrine_bell_tower_region = self.create_region("Firelink Shrine Bell Tower",
firelink_shrine_bell_tower_table)
high_wall_of_lothric_region = self.create_region("High Wall of Lothric", high_wall_of_lothric)
undead_settlement_region = self.create_region("Undead Settlement", undead_settlement_table)
road_of_sacrifices_region = self.create_region("Road of Sacrifices", road_of_sacrifice_table)
consumed_king_garden_region = self.create_region("Consumed King's Garden", consumed_king_garden_table)
cathedral_of_the_deep_region = self.create_region("Cathedral of the Deep", cathedral_of_the_deep_table)
farron_keep_region = self.create_region("Farron Keep", farron_keep_table)
catacombs_of_carthus_region = self.create_region("Catacombs of Carthus", catacombs_of_carthus_table)
smouldering_lake_region = self.create_region("Smouldering Lake", smouldering_lake_table)
irithyll_of_the_boreal_valley_region = self.create_region("Irithyll of the Boreal Valley",
irithyll_of_the_boreal_valley_table)
irithyll_dungeon_region = self.create_region("Irithyll Dungeon", irithyll_dungeon_table)
profaned_capital_region = self.create_region("Profaned Capital", profaned_capital_table)
anor_londo_region = self.create_region("Anor Londo", anor_londo_table)
lothric_castle_region = self.create_region("Lothric Castle", lothric_castle_table)
grand_archives_region = self.create_region("Grand Archives", grand_archives_table)
untended_graves_region = self.create_region("Untended Graves", untended_graves_table)
archdragon_peak_region = self.create_region("Archdragon Peak", archdragon_peak_table)
kiln_of_the_first_flame_region = self.create_region("Kiln Of The First Flame", None)
# DLC Down here
if self.multiworld.enable_dlc[self.player]:
painted_world_of_ariandel_region = self.create_region("Painted World of Ariandel", painted_world_table)
dreg_heap_region = self.create_region("Dreg Heap", dreg_heap_table)
ringed_city_region = self.create_region("Ringed City", ringed_city_table)
# Create the entrance to connect those regions
menu_region.exits.append(Entrance(self.player, "New Game", menu_region))
self.multiworld.get_entrance("New Game", self.player).connect(firelink_shrine_region)
firelink_shrine_region.exits.append(Entrance(self.player, "Goto High Wall of Lothric",
firelink_shrine_region))
firelink_shrine_region.exits.append(Entrance(self.player, "Goto Kiln Of The First Flame",
firelink_shrine_region))
firelink_shrine_region.exits.append(Entrance(self.player, "Goto Bell Tower",
firelink_shrine_region))
self.multiworld.get_entrance("Goto High Wall of Lothric", self.player).connect(high_wall_of_lothric_region)
self.multiworld.get_entrance("Goto Kiln Of The First Flame", self.player).connect(
kiln_of_the_first_flame_region)
self.multiworld.get_entrance("Goto Bell Tower", self.player).connect(firelink_shrine_bell_tower_region)
high_wall_of_lothric_region.exits.append(Entrance(self.player, "Goto Undead Settlement",
high_wall_of_lothric_region))
high_wall_of_lothric_region.exits.append(Entrance(self.player, "Goto Lothric Castle",
high_wall_of_lothric_region))
self.multiworld.get_entrance("Goto Undead Settlement", self.player).connect(undead_settlement_region)
self.multiworld.get_entrance("Goto Lothric Castle", self.player).connect(lothric_castle_region)
undead_settlement_region.exits.append(Entrance(self.player, "Goto Road Of Sacrifices",
undead_settlement_region))
self.multiworld.get_entrance("Goto Road Of Sacrifices", self.player).connect(road_of_sacrifices_region)
road_of_sacrifices_region.exits.append(Entrance(self.player, "Goto Cathedral", road_of_sacrifices_region))
road_of_sacrifices_region.exits.append(Entrance(self.player, "Goto Farron keep", road_of_sacrifices_region))
self.multiworld.get_entrance("Goto Cathedral", self.player).connect(cathedral_of_the_deep_region)
self.multiworld.get_entrance("Goto Farron keep", self.player).connect(farron_keep_region)
farron_keep_region.exits.append(Entrance(self.player, "Goto Carthus catacombs", farron_keep_region))
self.multiworld.get_entrance("Goto Carthus catacombs", self.player).connect(catacombs_of_carthus_region)
catacombs_of_carthus_region.exits.append(Entrance(self.player, "Goto Irithyll of the boreal",
catacombs_of_carthus_region))
catacombs_of_carthus_region.exits.append(Entrance(self.player, "Goto Smouldering Lake",
catacombs_of_carthus_region))
self.multiworld.get_entrance("Goto Irithyll of the boreal", self.player). \
connect(irithyll_of_the_boreal_valley_region)
self.multiworld.get_entrance("Goto Smouldering Lake", self.player).connect(smouldering_lake_region)
irithyll_of_the_boreal_valley_region.exits.append(Entrance(self.player, "Goto Irithyll dungeon",
irithyll_of_the_boreal_valley_region))
irithyll_of_the_boreal_valley_region.exits.append(Entrance(self.player, "Goto Anor Londo",
irithyll_of_the_boreal_valley_region))
self.multiworld.get_entrance("Goto Irithyll dungeon", self.player).connect(irithyll_dungeon_region)
self.multiworld.get_entrance("Goto Anor Londo", self.player).connect(anor_londo_region)
irithyll_dungeon_region.exits.append(Entrance(self.player, "Goto Archdragon peak", irithyll_dungeon_region))
irithyll_dungeon_region.exits.append(Entrance(self.player, "Goto Profaned capital", irithyll_dungeon_region))
self.multiworld.get_entrance("Goto Archdragon peak", self.player).connect(archdragon_peak_region)
self.multiworld.get_entrance("Goto Profaned capital", self.player).connect(profaned_capital_region)
lothric_castle_region.exits.append(Entrance(self.player, "Goto Consumed King Garden", lothric_castle_region))
lothric_castle_region.exits.append(Entrance(self.player, "Goto Grand Archives", lothric_castle_region))
self.multiworld.get_entrance("Goto Consumed King Garden", self.player).connect(consumed_king_garden_region)
self.multiworld.get_entrance("Goto Grand Archives", self.player).connect(grand_archives_region)
consumed_king_garden_region.exits.append(Entrance(self.player, "Goto Untended Graves",
consumed_king_garden_region))
self.multiworld.get_entrance("Goto Untended Graves", self.player).connect(untended_graves_region)
# DLC Connectors Below
if self.multiworld.enable_dlc[self.player]:
cathedral_of_the_deep_region.exits.append(Entrance(self.player, "Goto Painted World of Ariandel",
cathedral_of_the_deep_region))
self.multiworld.get_entrance("Goto Painted World of Ariandel", self.player).connect(painted_world_of_ariandel_region)
painted_world_of_ariandel_region.exits.append(Entrance(self.player, "Goto Dreg Heap",
painted_world_of_ariandel_region))
self.multiworld.get_entrance("Goto Dreg Heap", self.player).connect(dreg_heap_region)
dreg_heap_region.exits.append(Entrance(self.player, "Goto Ringed City", dreg_heap_region))
self.multiworld.get_entrance("Goto Ringed City", self.player).connect(ringed_city_region)
# For each region, add the associated locations retrieved from the corresponding location_table
def create_region(self, region_name, location_table) -> Region:
new_region = Region(region_name, self.player, self.multiworld)
if location_table:
for name, address in location_table.items():
location = DarkSouls3Location(self.player, name, self.location_name_to_id[name], new_region)
if region_name == "Menu":
add_item_rule(location, lambda item: not item.advancement)
new_region.locations.append(location)
self.multiworld.regions.append(new_region)
return new_region
def create_items(self):
for name, address in self.item_name_to_id.items():
# Specific items will be included in the item pool under certain conditions. See generate_basic
if name == "Basin of Vows":
continue
# Do not add progressive_items ( containing "#" ) to the itempool if the option is disabled
if (not self.multiworld.enable_progressive_locations[self.player]) and "#" in name:
continue
# Do not add DLC items if the option is disabled
if (not self.multiworld.enable_dlc[self.player]) and DarkSouls3Item.is_dlc_item(name):
continue
# Do not add DLC Progressives if both options are disabled
if ((not self.multiworld.enable_progressive_locations[self.player]) or (not self.multiworld.enable_dlc[self.player])) and DarkSouls3Item.is_dlc_progressive(name):
continue
self.multiworld.itempool += [self.create_item(name)]
def generate_early(self):
pass
def set_rules(self) -> None:
# Define the access rules to the entrances
set_rule(self.multiworld.get_entrance("Goto Bell Tower", self.player),
lambda state: state.has("Tower Key", self.player))
set_rule(self.multiworld.get_entrance("Goto Undead Settlement", self.player),
set_rule(self.multiworld.get_entrance("Go To Undead Settlement", self.player),
lambda state: state.has("Small Lothric Banner", self.player))
set_rule(self.multiworld.get_entrance("Goto Lothric Castle", self.player),
set_rule(self.multiworld.get_entrance("Go To Lothric Castle", self.player),
lambda state: state.has("Basin of Vows", self.player))
set_rule(self.multiworld.get_entrance("Goto Irithyll of the boreal", self.player),
set_rule(self.multiworld.get_entrance("Go To Irithyll of the Boreal Valley", self.player),
lambda state: state.has("Small Doll", self.player))
set_rule(self.multiworld.get_entrance("Goto Archdragon peak", self.player),
lambda state: state.can_reach("CKG: Soul of Consumed Oceiros", "Location", self.player))
set_rule(self.multiworld.get_entrance("Goto Profaned capital", self.player),
lambda state: state.has("Storm Ruler", self.player))
set_rule(self.multiworld.get_entrance("Goto Grand Archives", self.player),
set_rule(self.multiworld.get_entrance("Go To Archdragon Peak", self.player),
lambda state: state.has("Path of the Dragon", self.player))
set_rule(self.multiworld.get_entrance("Go To Grand Archives", self.player),
lambda state: state.has("Grand Archives Key", self.player))
set_rule(self.multiworld.get_entrance("Goto Kiln Of The First Flame", self.player),
set_rule(self.multiworld.get_entrance("Go To Kiln of the First Flame", self.player),
lambda state: state.has("Cinders of a Lord - Abyss Watcher", self.player) and
state.has("Cinders of a Lord - Yhorm the Giant", self.player) and
state.has("Cinders of a Lord - Aldrich", self.player) and
state.has("Cinders of a Lord - Lothric Prince", self.player))
if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true:
add_rule(self.multiworld.get_entrance("Go To Lothric Castle", self.player),
lambda state: state.has("Small Lothric Banner", self.player))
# DLC Access Rules Below
if self.multiworld.enable_dlc[self.player]:
set_rule(self.multiworld.get_entrance("Goto Painted World of Ariandel", self.player),
lambda state: state.has("Contraption Key", self.player))
set_rule(self.multiworld.get_entrance("Goto Ringed City", self.player),
set_rule(self.multiworld.get_entrance("Go To Ringed City", self.player),
lambda state: state.has("Small Envoy Banner", self.player))
# If key items are randomized, must have contraption key to enter second half of Ashes DLC
# If key items are not randomized, Contraption Key is guaranteed to be accessible before it is needed
if self.multiworld.enable_key_locations[self.player] == Toggle.option_true:
add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 2", self.player),
lambda state: state.has("Contraption Key", self.player))
if self.multiworld.late_dlc[self.player] == Toggle.option_true:
add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 1", self.player),
lambda state: state.has("Small Doll", self.player))
# Define the access rules to some specific locations
set_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
lambda state: state.has("Basin of Vows", self.player))
set_rule(self.multiworld.get_location("HWL: Greirat's Ashes", self.player),
lambda state: state.has("Cell Key", self.player))
set_rule(self.multiworld.get_location("HWL: Blue Tearstone Ring", self.player),
lambda state: state.has("Cell Key", self.player))
set_rule(self.multiworld.get_location("ID: Bellowing Dragoncrest Ring", self.player),
lambda state: state.has("Jailbreaker's Key", self.player))
set_rule(self.multiworld.get_location("ID: Prisoner Chief's Ashes", self.player),
lambda state: state.has("Jailer's Key Ring", self.player))
set_rule(self.multiworld.get_location("ID: Covetous Gold Serpent Ring", self.player),
lambda state: state.has("Old Cell Key", self.player))
set_rule(self.multiworld.get_location("ID: Karla's Ashes", self.player),
lambda state: state.has("Jailer's Key Ring", self.player))
black_hand_gotthard_corpse_rule = lambda state: \
set_rule(self.multiworld.get_location("PC: Cinders of a Lord - Yhorm the Giant", self.player),
lambda state: state.has("Storm Ruler", self.player))
if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true:
set_rule(self.multiworld.get_location("ID: Bellowing Dragoncrest Ring", self.player),
lambda state: state.has("Jailbreaker's Key", self.player))
set_rule(self.multiworld.get_location("ID: Covetous Gold Serpent Ring", self.player),
lambda state: state.has("Old Cell Key", self.player))
set_rule(self.multiworld.get_location("UG: Hornet Ring", self.player),
lambda state: state.has("Small Lothric Banner", self.player))
if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true:
set_rule(self.multiworld.get_location("HWL: Greirat's Ashes", self.player),
lambda state: state.has("Cell Key", self.player))
set_rule(self.multiworld.get_location("HWL: Blue Tearstone Ring", self.player),
lambda state: state.has("Cell Key", self.player))
set_rule(self.multiworld.get_location("ID: Karla's Ashes", self.player),
lambda state: state.has("Jailer's Key Ring", self.player))
set_rule(self.multiworld.get_location("ID: Karla's Pointed Hat", self.player),
lambda state: state.has("Jailer's Key Ring", self.player))
set_rule(self.multiworld.get_location("ID: Karla's Coat", self.player),
lambda state: state.has("Jailer's Key Ring", self.player))
set_rule(self.multiworld.get_location("ID: Karla's Gloves", self.player),
lambda state: state.has("Jailer's Key Ring", self.player))
set_rule(self.multiworld.get_location("ID: Karla's Trousers", self.player),
lambda state: state.has("Jailer's Key Ring", self.player))
if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true:
set_rule(self.multiworld.get_location("ID: Prisoner Chief's Ashes", self.player),
lambda state: state.has("Jailer's Key Ring", self.player))
if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true:
set_rule(self.multiworld.get_location("PC: Soul of Yhorm the Giant", self.player),
lambda state: state.has("Storm Ruler", self.player))
set_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
lambda state: state.has("Basin of Vows", self.player))
# Lump Soul of the Dancer in with LC for locations that should not be reachable
# before having access to US. (Prevents requiring getting Basin to fight Dancer to get SLB to go to US)
if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true:
add_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
lambda state: state.has("Small Lothric Banner", self.player))
gotthard_corpse_rule = lambda state: \
(state.can_reach("AL: Cinders of a Lord - Aldrich", "Location", self.player) and
state.can_reach("PC: Cinders of a Lord - Yhorm the Giant", "Location", self.player))
set_rule(self.multiworld.get_location("LC: Grand Archives Key", self.player), black_hand_gotthard_corpse_rule)
set_rule(self.multiworld.get_location("LC: Gotthard Twinswords", self.player), black_hand_gotthard_corpse_rule)
set_rule(self.multiworld.get_location("LC: Grand Archives Key", self.player), gotthard_corpse_rule)
if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true:
set_rule(self.multiworld.get_location("LC: Gotthard Twinswords", self.player), gotthard_corpse_rule)
self.multiworld.completion_condition[self.player] = lambda state: \
state.has("Cinders of a Lord - Abyss Watcher", self.player) and \
@@ -265,57 +446,36 @@ class DarkSouls3World(World):
state.has("Cinders of a Lord - Aldrich", self.player) and \
state.has("Cinders of a Lord - Lothric Prince", self.player)
def generate_basic(self):
# Depending on the specified option, add the Basin of Vows to a specific location or to the item pool
item = self.create_item("Basin of Vows")
if self.multiworld.late_basin_of_vows[self.player]:
self.multiworld.get_location("IBV: Soul of Pontiff Sulyvahn", self.player).place_locked_item(item)
else:
self.multiworld.itempool += [item]
# Fill item pool with additional items
item_pool_len = self.item_name_to_id.__len__()
total_required_locations = self.location_name_to_id.__len__()
for i in range(item_pool_len, total_required_locations):
self.multiworld.itempool += [self.create_item("Soul of an Intrepid Hero")]
def fill_slot_data(self) -> Dict[str, object]:
slot_data: Dict[str, object] = {}
# Depending on the specified option, modify items hexadecimal value to add an upgrade level
item_dictionary_copy = item_dictionary.copy()
if self.multiworld.randomize_weapons_level[self.player] > 0:
# Depending on the specified option, modify items hexadecimal value to add an upgrade level or infusion
name_to_ds3_code = {item.name: item.ds3_code for item in item_dictionary.values()}
# Randomize some weapon upgrades
if self.multiworld.randomize_weapon_level[self.player] != RandomizeWeaponLevelOption.option_none:
# if the user made an error and set a min higher than the max we default to the max
max_5 = self.multiworld.max_levels_in_5[self.player]
min_5 = min(self.multiworld.min_levels_in_5[self.player], max_5)
max_10 = self.multiworld.max_levels_in_10[self.player]
min_10 = min(self.multiworld.min_levels_in_10[self.player], max_10)
weapons_percentage = self.multiworld.randomize_weapons_percentage[self.player]
weapon_level_percentage = self.multiworld.randomize_weapon_level_percentage[self.player]
# Randomize some weapons upgrades
if self.multiworld.randomize_weapons_level[self.player] in [1, 3]: # Options are either all or +5
for name in weapons_upgrade_5_table.keys():
if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage:
value = self.multiworld.per_slot_randoms[self.player].randint(min_5, max_5)
item_dictionary_copy[name] += value
for item in item_dictionary.values():
if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < weapon_level_percentage:
if item.category == DS3ItemCategory.WEAPON_UPGRADE_5:
name_to_ds3_code[item.name] += self.multiworld.per_slot_randoms[self.player].randint(min_5, max_5)
elif item.category in {DS3ItemCategory.WEAPON_UPGRADE_10, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE}:
name_to_ds3_code[item.name] += self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10)
if self.multiworld.randomize_weapons_level[self.player] in [1, 2]: # Options are either all or +10
for name in weapons_upgrade_10_table.keys():
if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage:
value = self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10)
item_dictionary_copy[name] += value
if self.multiworld.randomize_weapons_level[self.player] in [1, 3]:
for name in dlc_weapons_upgrade_5_table.keys():
if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage:
value = self.multiworld.per_slot_randoms[self.player].randint(min_5, max_5)
item_dictionary_copy[name] += value
if self.multiworld.randomize_weapons_level[self.player] in [1, 2]:
for name in dlc_weapons_upgrade_10_table.keys():
if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage:
value = self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10)
item_dictionary_copy[name] += value
# Randomize some weapon infusions
if self.multiworld.randomize_infusion[self.player] == Toggle.option_true:
infusion_percentage = self.multiworld.randomize_infusion_percentage[self.player]
for item in item_dictionary.values():
if item.category in {DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, DS3ItemCategory.SHIELD_INFUSIBLE}:
if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < infusion_percentage:
name_to_ds3_code[item.name] += 100 * self.multiworld.per_slot_randoms[self.player].randint(0, 15)
# Create the mandatory lists to generate the player's output file
items_id = []
@@ -324,15 +484,19 @@ class DarkSouls3World(World):
locations_address = []
locations_target = []
for location in self.multiworld.get_filled_locations():
# Skip events
if location.item.code is None:
continue
if location.item.player == self.player:
items_id.append(location.item.code)
items_address.append(item_dictionary_copy[location.item.name])
items_address.append(name_to_ds3_code[location.item.name])
if location.player == self.player:
locations_address.append(location_dictionary[location.name])
locations_address.append(item_dictionary[location_dictionary[location.name].default_item].ds3_code)
locations_id.append(location.address)
if location.item.player == self.player:
locations_target.append(item_dictionary_copy[location.item.name])
locations_target.append(name_to_ds3_code[location.item.name])
else:
locations_target.append(0)

View File

@@ -1,600 +0,0 @@
"""
Tools used to create this list :
List of all items https://docs.google.com/spreadsheets/d/1nK2g7g6XJ-qphFAk1tjP3jZtlXWDQY-ItKLa_sniawo/edit#gid=1551945791
Regular expression parser https://regex101.com/r/XdtiLR/2
List of locations https://darksouls3.wiki.fextralife.com/Locations
"""
weapons_upgrade_5_table = {
"Irithyll Straight Sword": 0x0020A760,
"Chaos Blade": 0x004C9960,
"Dragonrider Bow": 0x00D6B0F0,
"White Hair Talisman": 0x00CAF120,
"Izalith Staff": 0x00C96A80,
"Fume Ultra Greatsword": 0x0060E4B0,
"Black Knight Sword": 0x005F5E10,
"Yorshka's Spear": 0x008C3A70,
"Smough's Great Hammer": 0x007E30B0,
"Dragonslayer Greatbow": 0x00CF8500,
"Golden Ritual Spear": 0x00C83200,
"Eleonora": 0x006CCB90,
"Witch's Locks": 0x00B7B740,
"Crystal Chime": 0x00CA2DD0,
"Black Knight Glaive": 0x009AE070,
"Dragonslayer Spear": 0x008CAFA0,
"Caitha's Chime": 0x00CA06C0,
"Sunlight Straight Sword": 0x00203230,
"Firelink Greatsword": 0x0060BDA0,
"Hollowslayer Greatsword": 0x00604870,
"Arstor's Spear": 0x008BEC50,
"Vordt's Great Hammer": 0x007CD120,
"Crystal Sage's Rapier": 0x002E6300,
"Farron Greatsword": 0x005E9AC0,
"Wolf Knight's Greatsword": 0x00602160,
"Dancer's Enchanted Swords": 0x00F4C040,
"Wolnir's Holy Sword": 0x005FFA50,
"Demon's Greataxe": 0x006CA480,
"Demon's Fist": 0x00A84DF0,
"Old King's Great Hammer": 0x007CF830,
"Greatsword of Judgment": 0x005E2590,
"Profaned Greatsword": 0x005E4CA0,
"Yhorm's Great Machete": 0x005F0FF0,
"Cleric's Candlestick": 0x0020F580,
"Dragonslayer Greataxe": 0x006C7D70,
"Moonlight Greatsword": 0x00606F80,
"Gundyr's Halberd": 0x009A1D20,
"Lothric's Holy Sword": 0x005FD340,
"Lorian's Greatsword": 0x005F8520,
"Twin Princes' Greatsword": 0x005FAC30,
"Storm Curved Sword": 0x003E4180,
"Dragonslayer Swordspear": 0x008BC540,
"Sage's Crystal Staff": 0x00C8CE40,
"Irithyll Rapier": 0x002E8A10
}
dlc_weapons_upgrade_5_table = {
"Friede's Great Scythe": 0x009B55A0,
"Rose of Ariandel": 0x00B82C70,
"Demon's Scar": 0x003F04D0, # Assigned to "RC: Church Guardian Shiv"
"Frayed Blade": 0x004D35A0, # Assigned to "RC: Ritual Spear Fragment"
"Gael's Greatsword": 0x00227C20, # Assigned to "RC: Violet Wrappings"
"Repeating Crossbow": 0x00D885B0, # Assigned to "RC: Blood of the Dark Souls"
"Onyx Blade": 0x00222E00, # VILHELM FIGHT
"Earth Seeker": 0x006D8EE0,
"Quakestone Hammer": 0x007ECCF0,
"Millwood Greatbow": 0x00D85EA0,
"Valorheart": 0x00F646E0,
"Aquamarine Dagger": 0x00116520,
"Ringed Knight Straight Sword": 0x00225510,
"Ledo's Great Hammer": 0x007EF400, # INVADER FIGHT
"Ringed Knight Spear": 0x008CFDC0,
"Crucifix of the Mad King": 0x008D4BE0,
"Sacred Chime of Filianore": 0x00CCECF0,
"Preacher's Right Arm": 0x00CD1400,
"White Birch Bow": 0x00D77440,
"Ringed Knight Paired Greatswords": 0x00F69500
}
weapons_upgrade_10_table = {
"Broken Straight Sword": 0x001EF9B0,
"Deep Battle Axe": 0x0006AFA54,
"Club": 0x007A1200,
"Claymore": 0x005BDBA0,
"Longbow": 0x00D689E0,
"Mail Breaker": 0x002DEDD0,
"Broadsword": 0x001ED2A0,
"Astora's Straight Sword": 0x002191C0,
"Rapier": 0x002E14E0,
"Lucerne": 0x0098BD90,
"Whip": 0x00B71B00,
"Reinforced Club": 0x007A8730,
"Caestus": 0x00A7FFD0,
"Partizan": 0x0089C970,
"Red Hilted Halberd": 0x009AB960,
"Saint's Talisman": 0x00CACA10,
"Large Club": 0x007AFC60,
"Brigand Twindaggers": 0x00F50E60,
"Butcher Knife": 0x006BE130,
"Brigand Axe": 0x006B1DE0,
"Heretic's Staff": 0x00C8F550,
"Great Club": 0x007B4A80,
"Exile Greatsword": 0x005DD770,
"Sellsword Twinblades": 0x00F42400,
"Notched Whip": 0x00B7DE50,
"Astora Greatsword": 0x005C9EF0,
"Executioner's Greatsword": 0x0021DFE0,
"Saint-tree Bellvine": 0x00C9DFB0,
"Saint Bident": 0x008C1360,
"Drang Hammers": 0x00F61FD0,
"Arbalest": 0x00D662D0,
"Sunlight Talisman": 0x00CA54E0,
"Greatsword": 0x005C50D0,
"Black Bow of Pharis": 0x00D7E970,
"Great Axe": 0x006B9310,
"Black Blade": 0x004CC070,
"Blacksmith Hammer": 0x007E57C0,
"Witchtree Branch": 0x00C94370,
"Painting Guardian's Curved Sword": 0x003E6890,
"Pickaxe": 0x007DE290,
"Court Sorcerer's Staff": 0x00C91C60,
"Avelyn": 0x00D6FF10,
"Onikiri and Ubadachi": 0x00F58390,
"Ricard's Rapier": 0x002E3BF0,
"Drakeblood Greatsword": 0x00609690,
"Greatlance": 0x008A8CC0,
"Sniper Crossbow": 0x00D83790,
"Claw": 0x00A7D8C0,
"Drang Twinspears": 0x00F5AAA0,
"Pyromancy Flame": 0x00CC77C0 #given/dropped by Cornyx
}
dlc_weapons_upgrade_10_table = {
"Follower Sabre": 0x003EDDC0,
"Millwood Battle Axe": 0x006D67D0,
"Follower Javelin": 0x008CD6B0,
"Crow Talons": 0x00A89C10,
"Pyromancer's Parting Flame": 0x00CC9ED0,
"Crow Quills": 0x00F66DF0,
"Follower Torch": 0x015F1AD0,
"Murky Hand Scythe": 0x00118C30,
"Herald Curved Greatsword": 0x006159E0,
"Lothric War Banner": 0x008D24D0,
"Splitleaf Greatsword": 0x009B2E90, # SHOP ITEM
"Murky Longstaff": 0x00CCC5E0,
}
shields_table = {
"East-West Shield": 0x0142B930,
"Silver Eagle Kite Shield": 0x014418C0,
"Small Leather Shield": 0x01315410,
"Blue Wooden Shield": 0x0143F1B0,
"Plank Shield": 0x01346150,
"Caduceus Round Shield": 0x01341330,
"Wargod Wooden Shield": 0x0144DC10,
"Grass Crest Shield": 0x01437C80,
"Golden Falcon Shield": 0x01354BB0,
"Twin Dragon Greatshield": 0x01513820,
"Spider Shield": 0x01435570,
"Crest Shield": 0x01430750,
"Curse Ward Greatshield": 0x01518640,
"Stone Parma": 0x01443FD0,
"Dragon Crest Shield": 0x01432E60,
"Shield of Want": 0x0144B500,
"Black Iron Greatshield": 0x0150EA00,
"Greatshield of Glory": 0x01515F30,
"Sacred Bloom Shield": 0x013572C0,
"Golden Wing Crest Shield": 0x0143CAA0,
"Ancient Dragon Greatshield": 0x013599D0,
"Spirit Tree Crest Shield": 0x014466E0,
"Blessed Red and White Shield": 0x01343FB9
}
dlc_shields_table = {
"Followers Shield": 0x0135C0E0,
"Ethereal Oak Shield": 0x01450320,
"Giant Door Shield": 0x00F5F8C0,
"Dragonhead Shield": 0x0135E7F0,
"Dragonhead Greatshield": 0x01452A30
}
goods_table = {
"Soul of an Intrepid Hero": 0x4000019D,
"Soul of the Nameless King": 0x400002D2,
"Soul of Champion Gundyr": 0x400002C8,
"Soul of the Twin Princes": 0x400002DB,
"Soul of Consumed Oceiros": 0x400002CE,
"Soul of Aldrich": 0x400002D5,
"Soul of Yhorm the Giant": 0x400002DC,
"Soul of Pontiff Sulyvahn": 0x400002D4,
"Soul of the Old Demon King": 0x400002D0,
"Soul of High Lord Wolnir": 0x400002D6,
"Soul of the Blood of the Wolf": 0x400002CD,
"Soul of the Deacons of the Deep": 0x400002D9,
"Soul of a Crystal Sage": 0x400002CB,
"Soul of Boreal Valley Vordt": 0x400002CF,
"Soul of a Stray Demon": 0x400002E7,
"Soul of a Demon": 0x400002E3,
# Upgrade materials
**{"Titanite Shard #"+str(i): 0x400003E8 for i in range(1, 11)},
**{"Large Titanite Shard #"+str(i): 0x400003E9 for i in range(1, 11)},
**{"Titanite Chunk #"+str(i): 0x400003EA for i in range(1, 6)},
**{"Titanite Slab #"+str(i): 0x400003EB for i in range(1, 4)},
# Healing
**{"Estus Shard #"+str(i): 0x4000085D for i in range(1, 16)},
**{"Undead Bone Shard #"+str(i): 0x4000085F for i in range(1, 6)},
# Souls
**{"Soul of a Great Champion #"+str(i): 0x400001A4 for i in range(1, 3)},
**{"Soul of a Champion #"+str(i): 0x400001A3 for i in range(1, 5)},
**{"Soul of a Venerable Old Hand #"+str(i): 0x400001A2 for i in range(1, 5)},
**{"Soul of a Crestfallen Knight #"+str(i): 0x40000199 for i in range(1, 11)}
}
goods_2_table = { # Added by Br00ty
"HWL: Gold Pine Resin #": 0x4000014B,
"US: Charcoal Pine Resin #": 0x4000014A,
"FK: Gold Pine Bundle #": 0x40000155,
"CC: Carthus Rouge #": 0x4000014F,
"ID: Pale Pine Resin #": 0x40000150,
**{"Ember #"+str(i): 0x400001F4 for i in range(1, 45)},
**{"Titanite Shard #"+str(i): 0x400003E8 for i in range(11, 16)},
**{"Large Titanite Shard #"+str(i): 0x400003E9 for i in range(11, 16)},
**{"Titanite Scale #" + str(i): 0x400003FC for i in range(1, 25)}
}
goods_3_table = { # Added by Br00ty
**{"Fading Soul #" + str(i): 0x40000190 for i in range(1, 4)},
**{"Ring of Sacrifice #"+str(i): 0x20004EF2 for i in range(1, 5)},
**{"Homeward Bone #"+str(i): 0x4000015E for i in range(1, 17)},
**{"Green Blossom #"+str(i): 0x40000104 for i in range(1, 7)},
**{"Human Pine Resin #"+str(i): 0x4000014E for i in range(1, 3)},
**{"Charcoal Pine Bundle #"+str(i): 0x40000154 for i in range(1, 3)},
**{"Rotten Pine Resin #"+str(i): 0x40000157 for i in range(1, 3)},
**{"Alluring Skull #"+str(i): 0x40000126 for i in range(1, 9)},
**{"Rusted Coin #"+str(i): 0x400001C7 for i in range(1, 3)},
**{"Rusted Gold Coin #"+str(i): 0x400001C9 for i in range(1, 3)},
**{"Titanite Chunk #"+str(i): 0x400003EA for i in range(1, 17)},
**{"Twinkling Titanite #"+str(i): 0x40000406 for i in range(1, 8)}
}
dlc_goods_table = {
"Soul of Sister Friede": 0x400002E8,
"Soul of the Demon Prince": 0x400002EA,
"Soul of Darkeater Midir": 0x400002EB,
"Soul of Slave Knight Gael": 0x400002E9
}
dlc_goods_2_table = { #71
**{"Large Soul of an Unknown Traveler $"+str(i): 0x40000194 for i in range(1, 10)},
**{"Soul of a Weary Warrior $"+str(i): 0x40000197 for i in range(1, 6)},
**{"Large Soul of a Weary Warrior $"+str(i): 0x40000198 for i in range(1, 7)},
**{"Soul of a Crestfallen Knight $"+str(i): 0x40000199 for i in range(1, 7)},
**{"Large Soul of a Crestfallen Knight $"+str(i): 0x4000019A for i in range(1, 4)},
**{"Homeward Bone $"+str(i): 0x4000015E for i in range(1, 7)},
**{"Large Titanite Shard $"+str(i): 0x400003E9 for i in range(1, 4)},
**{"Titanite Chunk $"+str(i): 0x400003EA for i in range(1, 16)},
**{"Twinkling Titanite $"+str(i): 0x40000406 for i in range(1, 6)},
**{"Rusted Coin $"+str(i): 0x400001C7 for i in range(1, 4)},
**{"Ember $"+str(i): 0x400001F4 for i in range(1, 11)}
}
armor_table = {
"Fire Keeper Robe": 0x140D9CE8,
"Fire Keeper Gloves": 0x140DA0D0,
"Fire Keeper Skirt": 0x140DA4B8,
"Deserter Trousers": 0x126265B8,
"Cleric Hat": 0x11D905C0,
"Cleric Blue Robe": 0x11D909A8,
"Cleric Gloves": 0x11D90D90,
"Cleric Trousers": 0x11D91178,
"Northern Helm": 0x116E3600,
"Northern Armor": 0x116E39E8,
"Northern Gloves": 0x116E3DD0,
"Northern Trousers": 0x116E41B8,
"Loincloth": 0x148F57D8,
"Brigand Hood": 0x148009E0,
"Brigand Armor": 0x14800DC8,
"Brigand Gauntlets": 0x148011B0,
"Brigand Trousers": 0x14801598,
"Sorcerer Hood": 0x11C9C380,
"Sorcerer Robe": 0x11C9C768,
"Sorcerer Gloves": 0x11C9CB50,
"Sorcerer Trousers": 0x11C9CF38,
"Fallen Knight Helm": 0x1121EAC0,
"Fallen Knight Armor": 0x1121EEA8,
"Fallen Knight Gauntlets": 0x1121F290,
"Fallen Knight Trousers": 0x1121F678,
"Conjurator Hood": 0x149E8E60,
"Conjurator Robe": 0x149E9248,
"Conjurator Manchettes": 0x149E9630,
"Conjurator Boots": 0x149E9A18,
"Sellsword Helm": 0x11481060,
"Sellsword Armor": 0x11481448,
"Sellsword Gauntlet": 0x11481830,
"Sellsword Trousers": 0x11481C18,
"Herald Helm": 0x114FB180,
"Herald Armor": 0x114FB568,
"Herald Gloves": 0x114FB950,
"Herald Trousers": 0x114FBD38,
"Maiden Hood": 0x14BD12E0,
"Maiden Robe": 0x14BD16C8,
"Maiden Gloves": 0x14BD1AB0,
"Maiden Skirt": 0x14BD1E98,
"Drang Armor": 0x154E0C28,
"Drang Gauntlets": 0x154E1010,
"Drang Shoes": 0x154E13F8,
"Archdeacon White Crown": 0x13EF1480,
"Archdeacon Holy Garb": 0x13EF1868,
"Archdeacon Skirt": 0x13EF2038,
"Antiquated Dress": 0x15D76068,
"Antiquated Gloves": 0x15D76450,
"Antiquated Skirt": 0x15D76838,
"Ragged Mask": 0x148F4C20,
"Crown of Dusk": 0x15D75C80,
"Pharis's Hat": 0x1487AB00,
"Old Sage's Blindfold": 0x11945BA0,
"Painting Guardian Hood": 0x156C8CC0,
"Painting Guardian Gown": 0x156C90A8,
"Painting Guardian Gloves": 0x156C9490,
"Painting Guardian Waistcloth": 0x156C9878,
"Brass Helm": 0x1501BD00,
"Brass Armor": 0x1501C0E8,
"Brass Gauntlets": 0x1501C4D0,
"Brass Leggings": 0x1501C8B8,
"Old Sorcerer Hat": 0x1496ED40,
"Old Sorcerer Coat": 0x1496F128,
"Old Sorcerer Gauntlets": 0x1496F510,
"Old Sorcerer Boots": 0x1496F8F8,
"Court Sorcerer Hood": 0x11BA8140,
"Court Sorcerer Robe": 0x11BA8528,
"Court Sorcerer Gloves": 0x11BA8910,
"Court Sorcerer Trousers": 0x11BA8CF8,
"Dragonslayer Helm": 0x158B1140,
"Dragonslayer Armor": 0x158B1528,
"Dragonslayer Gauntlets": 0x158B1910,
"Dragonslayer Leggings": 0x158B1CF8,
"Hood of Prayer": 0x13AA6A60,
"Robe of Prayer": 0x13AA6E48,
"Skirt of Prayer": 0x13AA7618,
"Winged Knight Helm": 0x12EBAE40,
"Winged Knight Armor": 0x12EBB228,
"Winged Knight Gauntlets": 0x12EBB610,
"Winged Knight Leggings": 0x12EBB9F8,
"Shadow Mask": 0x14D3F640,
"Shadow Garb": 0x14D3FA28,
"Shadow Gauntlets": 0x14D3FE10,
"Shadow Leggings": 0x14D401F8,
"Outrider Knight Helm": 0x1328B740,
"Outrider Knight Armor": 0x1328BB28,
"Outrider Knight Gauntlets": 0x1328BF10,
"Outrider Knight Leggings": 0x1328C2F8,
"Cornyx's Wrap": 0x11946370,
"Cornyx's Garb": 0x11945F88,
"Cornyx's Skirt": 0x11946758
}
dlc_armor_table = {
"Slave Knight Hood": 0x134EDCE0,
"Slave Knight Armor": 0x134EE0C8,
"Slave Knight Gauntlets": 0x134EE4B0,
"Slave Knight Leggings": 0x134EE898,
"Vilhelm's Helm": 0x11312D00,
"Vilhelm's Armor": 0x113130E8,
"Vilhelm's Gauntlets": 0x113134D0,
"Vilhelm's Leggings": 0x113138B8,
#"Millwood Knight Helm": 0x139B2820, # SHOP ITEM
#"Millwood Knight Armor": 0x139B2C08, # SHOP ITEM
#"Millwood Knight Gauntlets": 0x139B2FF0, # SHOP ITEM
#"Millwood Knight Leggings": 0x139B33D8, # SHOP ITEM
"Shira's Crown": 0x11C22260,
"Shira's Armor": 0x11C22648,
"Shira's Gloves": 0x11C22A30,
"Shira's Trousers": 0x11C22E18,
#"Lapp's Helm": 0x11E84800, # SHOP ITEM
#"Lapp's Armor": 0x11E84BE8, # SHOP ITEM
#"Lapp's Gauntlets": 0x11E84FD0, # SHOP ITEM
#"Lapp's Leggings": 0x11E853B8, # SHOP ITEM
#"Ringed Knight Hood": 0x13C8EEE0, # RANDOM ENEMY DROP
#"Ringed Knight Armor": 0x13C8F2C8, # RANDOM ENEMY DROP
#"Ringed Knight Gauntlets": 0x13C8F6B0, # RANDOM ENEMY DROP
#"Ringed Knight Leggings": 0x13C8FA98, # RANDOM ENEMY DROP
#"Harald Legion Armor": 0x13D83508, # RANDOM ENEMY DROP
#"Harald Legion Gauntlets": 0x13D838F0, # RANDOM ENEMY DROP
#"Harald Legion Leggings": 0x13D83CD8, # RANDOM ENEMY DROP
"Iron Dragonslayer Helm": 0x1405F7E0,
"Iron Dragonslayer Armor": 0x1405FBC8,
"Iron Dragonslayer Gauntlets": 0x1405FFB0,
"Iron Dragonslayer Leggings": 0x14060398,
"Ruin Sentinel Helm": 0x14CC5520,
"Ruin Sentinel Armor": 0x14CC5908,
"Ruin Sentinel Gauntlets": 0x14CC5CF0,
"Ruin Sentinel Leggings": 0x14CC60D8,
"Desert Pyromancer Hood": 0x14DB9760,
"Desert Pyromancer Garb": 0x14DB9B48,
"Desert Pyromancer Gloves": 0x14DB9F30,
"Desert Pyromancer Skirt": 0x14DBA318,
#"Follower Helm": 0x137CA3A0, # RANDOM ENEMY DROP
#"Follower Armor": 0x137CA788, # RANDOM ENEMY DROP
#"Follower Gloves": 0x137CAB70, # RANDOM ENEMY DROP
#"Follower Boots": 0x137CAF58, # RANDOM ENEMY DROP
#"Ordained Hood": 0x135E1F20, # SHOP ITEM
#"Ordained Dress": 0x135E2308, # SHOP ITEM
#"Ordained Trousers": 0x135E2AD8, # SHOP ITEM
"Black Witch Veil": 0x14FA1BE0,
"Black Witch Hat": 0x14EAD9A0,
"Black Witch Garb": 0x14EADD88,
"Black Witch Wrappings": 0x14EAE170,
"Black Witch Trousers": 0x14EAE558,
"White Preacher Head": 0x14153A20,
"Antiquated Plain Garb": 0x11B2E408
}
rings_table = {
"Estus Ring": 0x200050DC,
"Covetous Silver Serpent Ring": 0x20004FB0,
"Fire Clutch Ring": 0x2000501E,
"Flame Stoneplate Ring": 0x20004E52,
"Flynn's Ring": 0x2000503C,
"Chloranthy Ring": 0x20004E2A,
"Morne's Ring": 0x20004F1A,
"Sage Ring": 0x20004F38,
"Aldrich's Sapphire": 0x20005096,
"Lloyd's Sword Ring": 0x200050B4,
"Poisonbite Ring": 0x20004E8E,
"Deep Ring": 0x20004F60,
"Lingering Dragoncrest Ring": 0x20004F2E,
"Carthus Milkring": 0x20004FE2,
"Witch's Ring": 0x20004F11,
"Carthus Bloodring": 0x200050FA,
"Speckled Stoneplate Ring": 0x20004E7A,
"Magic Clutch Ring": 0x2000500A,
"Ring of the Sun's First Born": 0x20004F1B,
"Pontiff's Right Eye": 0x2000510E, "Leo Ring": 0x20004EE8,
"Dark Stoneplate Ring": 0x20004E70,
"Reversal Ring": 0x20005104,
"Ring of Favor": 0x20004E3E,
"Bellowing Dragoncrest Ring": 0x20004F07,
"Covetous Gold Serpent Ring": 0x20004FA6,
"Dusk Crown Ring": 0x20004F4C,
"Dark Clutch Ring": 0x20005028,
"Cursebite Ring": 0x20004E98,
"Sun Princess Ring": 0x20004FBA,
"Aldrich's Ruby": 0x2000508C,
"Scholar Ring": 0x20004EB6,
"Fleshbite Ring": 0x20004EA2,
"Hunter's Ring": 0x20004FF6,
"Ashen Estus Ring": 0x200050E6,
"Hornet Ring": 0x20004F9C,
"Lightning Clutch Ring": 0x20005014,
"Ring of Steel Protection": 0x20004E48,
"Calamity Ring": 0x20005078,
"Thunder Stoneplate Ring": 0x20004E5C,
"Knight's Ring": 0x20004FEC,
"Red Tearstone Ring": 0x20004ECA,
"Dragonscale Ring": 0x2000515E,
"Knight Slayer's Ring": 0x20005000,
"Magic Stoneplate Ring": 0x20004E66,
"Blue Tearstone Ring": 0x20004ED4 #given/dropped by Greirat
}
dlc_ring_table = {
"Havel's Ring": 0x20004E34,
"Chillbite Ring": 0x20005208
}
spells_table = {
"Seek Guidance": 0x40360420,
"Lightning Spear": 0x40362B30,
"Atonement": 0x4039ADA0,
"Great Magic Weapon": 0x40140118,
"Iron Flesh": 0x40251430,
"Lightning Stake": 0x40389C30,
"Toxic Mist": 0x4024F108,
"Sacred Flame": 0x40284880,
"Dorhys' Gnawing": 0x40363EB8,
"Great Heal": 0x40356FB0,
"Lightning Blade": 0x4036C770,
"Profaned Flame": 0x402575D8,
"Wrath of the Gods": 0x4035E0F8,
"Power Within": 0x40253B40,
"Soul Stream": 0x4018B820,
"Divine Pillars of Light": 0x4038C340,
"Great Magic Barrier": 0x40365628,
"Great Magic Shield": 0x40144F38,
"Crystal Scroll": 0x40000856
}
dlc_spells_table = {
#"Boulder Heave": 0x40282170, # KILN STRAY DEMON
#"Seething Chaos": 0x402896A0, # KILN DEMON PRINCES
#"Old Moonlight": 0x4014FF00, # KILN MIDIR
"Frozen Weapon": 0x401408E8,
"Snap Freeze": 0x401A90C8,
"Great Soul Dregs": 0x401879A0,
"Flame Fan": 0x40258190,
"Lightning Arrow": 0x40358B08,
"Way of White Corona": 0x403642A0,
"Projected Heal": 0x40364688,
"Floating Chaos": 0x40257DA8
}
misc_items_table = {
"Tower Key": 0x400007DF,
"Grave Key": 0x400007D9,
"Cell Key": 0x400007DA,
"Small Lothric Banner": 0x40000836,
"Mortician's Ashes": 0x4000083B,
"Braille Divine Tome of Carim": 0x40000847, # Shop
"Great Swamp Pyromancy Tome": 0x4000084F, # Shop
"Farron Coal ": 0x40000837, # Shop
"Paladin's Ashes": 0x4000083D, # Shop
"Deep Braille Divine Tome": 0x40000860, # Shop
"Small Doll": 0x400007D5,
"Golden Scroll": 0x4000085C,
"Sage's Coal": 0x40000838, # Shop #Unique
"Sage's Scroll": 0x40000854,
"Dreamchaser's Ashes": 0x4000083C, # Shop #Unique
"Cinders of a Lord - Abyss Watcher": 0x4000084B,
"Cinders of a Lord - Yhorm the Giant": 0x4000084D,
"Cinders of a Lord - Aldrich": 0x4000084C,
"Grand Archives Key": 0x400007DE,
"Basin of Vows": 0x40000845,
"Cinders of a Lord - Lothric Prince": 0x4000084E,
"Carthus Pyromancy Tome": 0x40000850,
"Grave Warden's Ashes": 0x4000083E,
"Grave Warden Pyromancy Tome": 0x40000853,
"Quelana Pyromancy Tome": 0x40000852,
"Izalith Pyromancy Tome": 0x40000851,
"Greirat's Ashes": 0x4000083F,
"Excrement-covered Ashes": 0x40000862,
"Easterner's Ashes": 0x40000868,
"Prisoner Chief's Ashes": 0x40000863,
"Jailbreaker's Key": 0x400007D7,
"Dragon Torso Stone": 0x4000017A,
"Profaned Coal": 0x4000083A,
"Xanthous Ashes": 0x40000864,
"Old Cell Key": 0x400007DC,
"Jailer's Key Ring": 0x400007D8,
"Logan's Scroll": 0x40000855,
"Storm Ruler": 0x006132D0,
"Giant's Coal": 0x40000839,
"Coiled Sword Fragment": 0x4000015F,
"Dragon Chaser's Ashes": 0x40000867,
"Twinkling Dragon Torso Stone": 0x40000184,
"Braille Divine Tome of Lothric": 0x40000848,
"Irina's Ashes": 0x40000843,
"Karla's Ashes": 0x40000842,
"Cornyx's Ashes": 0x40000841,
"Orbeck's Ashes": 0x40000840
}
dlc_misc_table = {
"Captains Ashes": 0x4000086A,
"Contraption Key": 0x4000086B, # Needed for Painted World
"Small Envoy Banner": 0x4000086C # Needed to get to Ringed City from Dreg Heap
}
key_items_list = {
"Small Lothric Banner",
"Basin of Vows",
"Small Doll",
"Storm Ruler",
"Grand Archives Key",
"Cinders of a Lord - Abyss Watcher",
"Cinders of a Lord - Yhorm the Giant",
"Cinders of a Lord - Aldrich",
"Cinders of a Lord - Lothric Prince",
"Mortician's Ashes",
"Cell Key",
"Tower Key",
"Jailbreaker's Key",
"Prisoner Chief's Ashes",
"Old Cell Key",
"Jailer's Key Ring",
"Contraption Key",
"Small Envoy Banner"
}
item_tables = [weapons_upgrade_5_table, weapons_upgrade_10_table, shields_table,
armor_table, rings_table, spells_table, misc_items_table, goods_table, goods_2_table, goods_3_table,
dlc_weapons_upgrade_5_table, dlc_weapons_upgrade_10_table, dlc_shields_table, dlc_goods_table,
dlc_armor_table, dlc_spells_table, dlc_ring_table, dlc_misc_table, dlc_goods_2_table]
item_dictionary = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table,
**armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table, **goods_2_table,
**goods_3_table, **dlc_weapons_upgrade_5_table, **dlc_weapons_upgrade_10_table, **dlc_shields_table,
**dlc_goods_table, **dlc_armor_table, **dlc_spells_table, **dlc_ring_table, **dlc_misc_table, **dlc_goods_2_table}

View File

@@ -1,614 +0,0 @@
"""
Tools used to create this list :
List of all items https://docs.google.com/spreadsheets/d/1nK2g7g6XJ-qphFAk1tjP3jZtlXWDQY-ItKLa_sniawo/edit#gid=1551945791
Regular expression parser https://regex101.com/r/XdtiLR/2
List of locations https://darksouls3.wiki.fextralife.com/Locations
"""
fire_link_shrine_table = {
# "FS: Coiled Sword": 0x40000859, You can still light the Firelink Shrine fire whether you have it or not, useless
"FS: Broken Straight Sword": 0x001EF9B0,
"FS: East-West Shield": 0x0142B930,
"FS: Uchigatana": 0x004C4B40,
"FS: Master's Attire": 0x148F5008,
"FS: Master's Gloves": 0x148F53F0,
}
firelink_shrine_bell_tower_table = {
"FSBT: Covetous Silver Serpent Ring": 0x20004FB0,
"FSBT: Fire Keeper Robe": 0x140D9CE8,
"FSBT: Fire Keeper Gloves": 0x140DA0D0,
"FSBT: Fire Keeper Skirt": 0x140DA4B8,
"FSBT: Estus Ring": 0x200050DC,
"FSBT: Fire Keeper Soul": 0x40000186
}
high_wall_of_lothric = {
"HWL: Deep Battle Axe": 0x0006AFA54,
"HWL: Club": 0x007A1200,
"HWL: Claymore": 0x005BDBA0,
"HWL: Binoculars": 0x40000173,
"HWL: Longbow": 0x00D689E0,
"HWL: Mail Breaker": 0x002DEDD0,
"HWL: Broadsword": 0x001ED2A0,
"HWL: Silver Eagle Kite Shield": 0x014418C0,
"HWL: Astora's Straight Sword": 0x002191C0,
"HWL: Cell Key": 0x400007DA,
"HWL: Rapier": 0x002E14E0,
"HWL: Lucerne": 0x0098BD90,
"HWL: Small Lothric Banner": 0x40000836,
"HWL: Basin of Vows": 0x40000845,
"HWL: Soul of Boreal Valley Vordt": 0x400002CF,
"HWL: Soul of the Dancer": 0x400002CA,
"HWL: Way of Blue Covenant": 0x2000274C,
"HWL: Greirat's Ashes": 0x4000083F,
"HWL: Blue Tearstone Ring": 0x20004ED4 #given/dropped by Greirat
}
undead_settlement_table = {
"US: Small Leather Shield": 0x01315410,
"US: Whip": 0x00B71B00,
"US: Reinforced Club": 0x007A8730,
"US: Blue Wooden Shield": 0x0143F1B0,
"US: Cleric Hat": 0x11D905C0,
"US: Cleric Blue Robe": 0x11D909A8,
"US: Cleric Gloves": 0x11D90D90,
"US: Cleric Trousers": 0x11D91178,
"US: Mortician's Ashes": 0x4000083B,
"US: Caestus": 0x00A7FFD0,
"US: Plank Shield": 0x01346150,
"US: Flame Stoneplate Ring": 0x20004E52,
"US: Caduceus Round Shield": 0x01341330,
"US: Fire Clutch Ring": 0x2000501E,
"US: Partizan": 0x0089C970,
"US: Bloodbite Ring": 0x20004E84,
"US: Red Hilted Halberd": 0x009AB960,
"US: Saint's Talisman": 0x00CACA10,
"US: Irithyll Straight Sword": 0x0020A760,
"US: Large Club": 0x007AFC60,
"US: Northern Helm": 0x116E3600,
"US: Northern Armor": 0x116E39E8,
"US: Northern Gloves": 0x116E3DD0,
"US: Northern Trousers": 0x116E41B8,
"US: Flynn's Ring": 0x2000503C,
"US: Mirrah Vest": 0x15204568,
"US: Mirrah Gloves": 0x15204950,
"US: Mirrah Trousers": 0x15204D38,
"US: Chloranthy Ring": 0x20004E2A,
"US: Loincloth": 0x148F57D8,
"US: Wargod Wooden Shield": 0x0144DC10,
"US: Loretta's Bone": 0x40000846,
"US: Hand Axe": 0x006ACFC0,
"US: Great Scythe": 0x00989680,
"US: Soul of the Rotted Greatwood": 0x400002D7,
"US: Hawk Ring": 0x20004F92,
"US: Warrior of Sunlight Covenant": 0x20002738,
"US: Blessed Red and White Shield": 0x01343FB9,
"US: Irina's Ashes": 0x40000843,
"US: Cornyx's Ashes": 0x40000841,
"US: Cornyx's Wrap": 0x11946370,
"US: Cornyx's Garb": 0x11945F88,
"US: Cornyx's Skirt": 0x11946758,
"US: Pyromancy Flame": 0x00CC77C0 #given/dropped by Cornyx
}
road_of_sacrifice_table = {
"RS: Brigand Twindaggers": 0x00F50E60,
"RS: Brigand Hood": 0x148009E0,
"RS: Brigand Armor": 0x14800DC8,
"RS: Brigand Gauntlets": 0x148011B0,
"RS: Brigand Trousers": 0x14801598,
"RS: Butcher Knife": 0x006BE130,
"RS: Brigand Axe": 0x006B1DE0,
"RS: Braille Divine Tome of Carim": 0x40000847,
"RS: Morne's Ring": 0x20004F1A,
"RS: Twin Dragon Greatshield": 0x01513820,
"RS: Heretic's Staff": 0x00C8F550,
"RS: Sorcerer Hood": 0x11C9C380,
"RS: Sorcerer Robe": 0x11C9C768,
"RS: Sorcerer Gloves": 0x11C9CB50,
"RS: Sorcerer Trousers": 0x11C9CF38,
"RS: Sage Ring": 0x20004F38,
"RS: Fallen Knight Helm": 0x1121EAC0,
"RS: Fallen Knight Armor": 0x1121EEA8,
"RS: Fallen Knight Gauntlets": 0x1121F290,
"RS: Fallen Knight Trousers": 0x1121F678,
"RS: Conjurator Hood": 0x149E8E60,
"RS: Conjurator Robe": 0x149E9248,
"RS: Conjurator Manchettes": 0x149E9630,
"RS: Conjurator Boots": 0x149E9A18,
"RS: Great Swamp Pyromancy Tome": 0x4000084F,
"RS: Great Club": 0x007B4A80,
"RS: Exile Greatsword": 0x005DD770,
"RS: Farron Coal ": 0x40000837,
"RS: Sellsword Twinblades": 0x00F42400,
"RS: Sellsword Helm": 0x11481060,
"RS: Sellsword Armor": 0x11481448,
"RS: Sellsword Gauntlet": 0x11481830,
"RS: Sellsword Trousers": 0x11481C18,
"RS: Golden Falcon Shield": 0x01354BB0,
"RS: Herald Helm": 0x114FB180,
"RS: Herald Armor": 0x114FB568,
"RS: Herald Gloves": 0x114FB950,
"RS: Herald Trousers": 0x114FBD38,
"RS: Grass Crest Shield": 0x01437C80,
"RS: Soul of a Crystal Sage": 0x400002CB,
"RS: Great Swamp Ring": 0x20004F10,
"RS: Orbeck's Ashes": 0x40000840
}
cathedral_of_the_deep_table = {
"CD: Paladin's Ashes": 0x4000083D,
"CD: Spider Shield": 0x01435570,
"CD: Crest Shield": 0x01430750,
"CD: Notched Whip": 0x00B7DE50,
"CD: Astora Greatsword": 0x005C9EF0,
"CD: Executioner's Greatsword": 0x0021DFE0,
"CD: Curse Ward Greatshield": 0x01518640,
"CD: Saint-tree Bellvine": 0x00C9DFB0,
"CD: Poisonbite Ring": 0x20004E8E,
"CD: Lloyd's Sword Ring": 0x200050B4,
"CD: Seek Guidance": 0x40360420,
"CD: Aldrich's Sapphire": 0x20005096,
"CD: Deep Braille Divine Tome": 0x40000860,
"CD: Saint Bident": 0x008C1360,
"CD: Maiden Hood": 0x14BD12E0,
"CD: Maiden Robe": 0x14BD16C8,
"CD: Maiden Gloves": 0x14BD1AB0,
"CD: Maiden Skirt": 0x14BD1E98,
"CD: Drang Armor": 0x154E0C28,
"CD: Drang Gauntlets": 0x154E1010,
"CD: Drang Shoes": 0x154E13F8,
"CD: Drang Hammers": 0x00F61FD0,
"CD: Deep Ring": 0x20004F60,
"CD: Archdeacon White Crown": 0x13EF1480,
"CD: Archdeacon Holy Garb": 0x13EF1868,
"CD: Archdeacon Skirt": 0x13EF2038,
"CD: Arbalest": 0x00D662D0,
"CD: Small Doll": 0x400007D5,
"CD: Soul of the Deacons of the Deep": 0x400002D9,
"CD: Rosaria's Fingers Covenant": 0x20002760,
}
farron_keep_table = {
"FK: Ragged Mask": 0x148F4C20,
"FK: Iron Flesh": 0x40251430,
"FK: Golden Scroll": 0x4000085C,
"FK: Antiquated Dress": 0x15D76068,
"FK: Antiquated Gloves": 0x15D76450,
"FK: Antiquated Skirt": 0x15D76838,
"FK: Nameless Knight Helm": 0x143B5FC0,
"FK: Nameless Knight Armor": 0x143B63A8,
"FK: Nameless Knight Gauntlets": 0x143B6790,
"FK: Nameless Knight Leggings": 0x143B6B78,
"FK: Sunlight Talisman": 0x00CA54E0,
"FK: Wolf's Blood Swordgrass": 0x4000016E,
"FK: Greatsword": 0x005C50D0,
"FK: Sage's Coal": 0x40000838,
"FK: Stone Parma": 0x01443FD0,
"FK: Sage's Scroll": 0x40000854,
"FK: Crown of Dusk": 0x15D75C80,
"FK: Lingering Dragoncrest Ring": 0x20004F2E,
"FK: Pharis's Hat": 0x1487AB00,
"FK: Black Bow of Pharis": 0x00D7E970,
"FK: Dreamchaser's Ashes": 0x4000083C,
"FK: Great Axe": 0x006B9310,
"FK: Dragon Crest Shield": 0x01432E60,
"FK: Lightning Spear": 0x40362B30,
"FK: Atonement": 0x4039ADA0,
"FK: Great Magic Weapon": 0x40140118,
"FK: Cinders of a Lord - Abyss Watcher": 0x4000084B,
"FK: Soul of the Blood of the Wolf": 0x400002CD,
"FK: Soul of a Stray Demon": 0x400002E7,
"FK: Watchdogs of Farron Covenant": 0x20002724,
}
catacombs_of_carthus_table = {
"CC: Carthus Pyromancy Tome": 0x40000850,
"CC: Carthus Milkring": 0x20004FE2,
"CC: Grave Warden's Ashes": 0x4000083E,
"CC: Carthus Bloodring": 0x200050FA,
"CC: Grave Warden Pyromancy Tome": 0x40000853,
"CC: Old Sage's Blindfold": 0x11945BA0,
"CC: Witch's Ring": 0x20004F11,
"CC: Black Blade": 0x004CC070,
"CC: Soul of High Lord Wolnir": 0x400002D6,
"CC: Soul of a Demon": 0x400002E3,
}
smouldering_lake_table = {
"SL: Shield of Want": 0x0144B500,
"SL: Speckled Stoneplate Ring": 0x20004E7A,
"SL: Dragonrider Bow": 0x00D6B0F0,
"SL: Lightning Stake": 0x40389C30,
"SL: Izalith Pyromancy Tome": 0x40000851,
"SL: Black Knight Sword": 0x005F5E10,
"SL: Quelana Pyromancy Tome": 0x40000852,
"SL: Toxic Mist": 0x4024F108,
"SL: White Hair Talisman": 0x00CAF120,
"SL: Izalith Staff": 0x00C96A80,
"SL: Sacred Flame": 0x40284880,
"SL: Fume Ultra Greatsword": 0x0060E4B0,
"SL: Black Iron Greatshield": 0x0150EA00,
"SL: Soul of the Old Demon King": 0x400002D0,
"SL: Knight Slayer's Ring": 0x20005000,
}
irithyll_of_the_boreal_valley_table = {
"IBV: Dorhys' Gnawing": 0x40363EB8,
"IBV: Witchtree Branch": 0x00C94370,
"IBV: Magic Clutch Ring": 0x2000500A,
"IBV: Ring of the Sun's First Born": 0x20004F1B,
"IBV: Roster of Knights": 0x4000006C,
"IBV: Pontiff's Right Eye": 0x2000510E,
"IBV: Yorshka's Spear": 0x008C3A70,
"IBV: Great Heal": 0x40356FB0,
"IBV: Smough's Great Hammer": 0x007E30B0,
"IBV: Leo Ring": 0x20004EE8,
"IBV: Excrement-covered Ashes": 0x40000862,
"IBV: Dark Stoneplate Ring": 0x20004E70,
"IBV: Easterner's Ashes": 0x40000868,
"IBV: Painting Guardian's Curved Sword": 0x003E6890,
"IBV: Painting Guardian Hood": 0x156C8CC0,
"IBV: Painting Guardian Gown": 0x156C90A8,
"IBV: Painting Guardian Gloves": 0x156C9490,
"IBV: Painting Guardian Waistcloth": 0x156C9878,
"IBV: Dragonslayer Greatbow": 0x00CF8500,
"IBV: Reversal Ring": 0x20005104,
"IBV: Brass Helm": 0x1501BD00,
"IBV: Brass Armor": 0x1501C0E8,
"IBV: Brass Gauntlets": 0x1501C4D0,
"IBV: Brass Leggings": 0x1501C8B8,
"IBV: Ring of Favor": 0x20004E3E,
"IBV: Golden Ritual Spear": 0x00C83200,
"IBV: Soul of Pontiff Sulyvahn": 0x400002D4,
"IBV: Aldrich Faithful Covenant": 0x2000272E,
"IBV: Drang Twinspears": 0x00F5AAA0,
}
irithyll_dungeon_table = {
"ID: Bellowing Dragoncrest Ring": 0x20004F07,
"ID: Jailbreaker's Key": 0x400007D7,
"ID: Prisoner Chief's Ashes": 0x40000863,
"ID: Old Sorcerer Hat": 0x1496ED40,
"ID: Old Sorcerer Coat": 0x1496F128,
"ID: Old Sorcerer Gauntlets": 0x1496F510,
"ID: Old Sorcerer Boots": 0x1496F8F8,
"ID: Great Magic Shield": 0x40144F38,
"ID: Dragon Torso Stone": 0x4000017A,
"ID: Lightning Blade": 0x4036C770,
"ID: Profaned Coal": 0x4000083A,
"ID: Xanthous Ashes": 0x40000864,
"ID: Old Cell Key": 0x400007DC,
"ID: Pickaxe": 0x007DE290,
"ID: Profaned Flame": 0x402575D8,
"ID: Covetous Gold Serpent Ring": 0x20004FA6,
"ID: Jailer's Key Ring": 0x400007D8,
"ID: Dusk Crown Ring": 0x20004F4C,
"ID: Dark Clutch Ring": 0x20005028,
"ID: Karla's Ashes": 0x40000842
}
profaned_capital_table = {
"PC: Cursebite Ring": 0x20004E98,
"PC: Court Sorcerer Hood": 0x11BA8140,
"PC: Court Sorcerer Robe": 0x11BA8528,
"PC: Court Sorcerer Gloves": 0x11BA8910,
"PC: Court Sorcerer Trousers": 0x11BA8CF8,
"PC: Wrath of the Gods": 0x4035E0F8,
"PC: Logan's Scroll": 0x40000855,
"PC: Eleonora": 0x006CCB90,
"PC: Court Sorcerer's Staff": 0x00C91C60,
"PC: Greatshield of Glory": 0x01515F30,
"PC: Storm Ruler": 0x006132D0,
"PC: Cinders of a Lord - Yhorm the Giant": 0x4000084D,
"PC: Soul of Yhorm the Giant": 0x400002DC,
}
anor_londo_table = {
"AL: Giant's Coal": 0x40000839,
"AL: Sun Princess Ring": 0x20004FBA,
"AL: Aldrich's Ruby": 0x2000508C,
"AL: Cinders of a Lord - Aldrich": 0x4000084C,
"AL: Soul of Aldrich": 0x400002D5,
}
lothric_castle_table = {
"LC: Hood of Prayer": 0x13AA6A60,
"LC: Robe of Prayer": 0x13AA6E48,
"LC: Skirt of Prayer": 0x13AA7618,
"LC: Sacred Bloom Shield": 0x013572C0,
"LC: Winged Knight Helm": 0x12EBAE40,
"LC: Winged Knight Armor": 0x12EBB228,
"LC: Winged Knight Gauntlets": 0x12EBB610,
"LC: Winged Knight Leggings": 0x12EBB9F8,
"LC: Greatlance": 0x008A8CC0,
"LC: Sniper Crossbow": 0x00D83790,
"LC: Spirit Tree Crest Shield": 0x014466E0,
"LC: Red Tearstone Ring": 0x20004ECA,
"LC: Caitha's Chime": 0x00CA06C0,
"LC: Braille Divine Tome of Lothric": 0x40000848,
"LC: Knight's Ring": 0x20004FEC,
"LC: Irithyll Rapier": 0x002E8A10,
"LC: Sunlight Straight Sword": 0x00203230,
"LC: Soul of Dragonslayer Armour": 0x400002D1,
# The Black Hand Gotthard corpse appears when you have defeated Yhorm and Aldrich and triggered the cutscene
"LC: Grand Archives Key": 0x400007DE, # On Black Hand Gotthard corpse
"LC: Gotthard Twinswords": 0x00F53570 # On Black Hand Gotthard corpse
}
consumed_king_garden_table = {
"CKG: Dragonscale Ring": 0x2000515E,
"CKG: Shadow Mask": 0x14D3F640,
"CKG: Shadow Garb": 0x14D3FA28,
"CKG: Shadow Gauntlets": 0x14D3FE10,
"CKG: Shadow Leggings": 0x14D401F8,
"CKG: Claw": 0x00A7D8C0,
"CKG: Soul of Consumed Oceiros": 0x400002CE,
"CKG: Magic Stoneplate Ring": 0x20004E66,
# "CKG: Path of the Dragon Gesture": 0x40002346, I can't technically randomize it as it is a gesture and not an item
}
grand_archives_table = {
"GA: Avelyn": 0x00D6FF10,
"GA: Witch's Locks": 0x00B7B740,
"GA: Power Within": 0x40253B40,
"GA: Scholar Ring": 0x20004EB6,
"GA: Soul Stream": 0x4018B820,
"GA: Fleshbite Ring": 0x20004EA2,
"GA: Crystal Chime": 0x00CA2DD0,
"GA: Golden Wing Crest Shield": 0x0143CAA0,
"GA: Onikiri and Ubadachi": 0x00F58390,
"GA: Hunter's Ring": 0x20004FF6,
"GA: Divine Pillars of Light": 0x4038C340,
"GA: Cinders of a Lord - Lothric Prince": 0x4000084E,
"GA: Soul of the Twin Princes": 0x400002DB,
"GA: Sage's Crystal Staff": 0x00C8CE40,
"GA: Outrider Knight Helm": 0x1328B740,
"GA: Outrider Knight Armor": 0x1328BB28,
"GA: Outrider Knight Gauntlets": 0x1328BF10,
"GA: Outrider Knight Leggings": 0x1328C2F8,
"GA: Crystal Scroll": 0x40000856,
}
untended_graves_table = {
"UG: Ashen Estus Ring": 0x200050E6,
"UG: Black Knight Glaive": 0x009AE070,
"UG: Hornet Ring": 0x20004F9C,
"UG: Chaos Blade": 0x004C9960,
"UG: Blacksmith Hammer": 0x007E57C0,
"UG: Eyes of a Fire Keeper": 0x4000085A,
"UG: Coiled Sword Fragment": 0x4000015F,
"UG: Soul of Champion Gundyr": 0x400002C8,
}
archdragon_peak_table = {
"AP: Lightning Clutch Ring": 0x20005014,
"AP: Ancient Dragon Greatshield": 0x013599D0,
"AP: Ring of Steel Protection": 0x20004E48,
"AP: Calamity Ring": 0x20005078,
"AP: Drakeblood Greatsword": 0x00609690,
"AP: Dragonslayer Spear": 0x008CAFA0,
"AP: Thunder Stoneplate Ring": 0x20004E5C,
"AP: Great Magic Barrier": 0x40365628,
"AP: Dragon Chaser's Ashes": 0x40000867,
"AP: Twinkling Dragon Torso Stone": 0x40000184,
"AP: Dragonslayer Helm": 0x158B1140,
"AP: Dragonslayer Armor": 0x158B1528,
"AP: Dragonslayer Gauntlets": 0x158B1910,
"AP: Dragonslayer Leggings": 0x158B1CF8,
"AP: Ricard's Rapier": 0x002E3BF0,
"AP: Soul of the Nameless King": 0x400002D2,
"AP: Dragon Tooth": 0x007E09A0,
"AP: Havel's Greatshield": 0x013376F0,
}
painted_world_table = { # DLC
"PW: Follower Javelin": 0x008CD6B0,
"PW: Frozen Weapon": 0x401408E8,
"PW: Millwood Greatbow": 0x00D85EA0,
"PW: Captains Ashes": 0x4000086A,
"PW: Millwood Battle Axe": 0x006D67D0,
"PW: Ethereal Oak Shield": 0x01450320,
"PW: Crow Quills": 0x00F66DF0,
"PW: Slave Knight Hood": 0x134EDCE0,
"PW: Slave Knight Armor": 0x134EE0C8,
"PW: Slave Knight Gauntlets": 0x134EE4B0,
"PW: Slave Knight Leggings": 0x134EE898,
"PW: Way of White Corona": 0x403642A0,
"PW: Crow Talons": 0x00A89C10,
"PW: Quakestone Hammer": 0x007ECCF0,
"PW: Earth Seeker": 0x006D8EE0,
"PW: Follower Torch": 0x015F1AD0,
"PW: Follower Shield": 0x0135C0E0,
"PW: Follower Sabre": 0x003EDDC0,
"PW: Snap Freeze": 0x401A90C8,
"PW: Floating Chaos": 0x40257DA8,
"PW: Pyromancer's Parting Flame": 0x00CC9ED0,
"PW: Vilhelm's Helm": 0x11312D00,
"PW: Vilhelm's Armor": 0x113130E8,
"PW: Vilhelm's Gauntlets": 0x113134D0,
"PW: Vilhelm's Leggings": 0x113138B8,
"PW: Valorheart": 0x00F646E0, # GRAVETENDER FIGHT
"PW: Champions Bones": 0x40000869, # GRAVETENDER FIGHT
"PW: Onyx Blade": 0x00222E00, # VILHELM FIGHT
"PW: Soul of Sister Friede": 0x400002E8,
"PW: Titanite Slab": 0x400003EB,
"PW: Chillbite Ring": 0x20005208,
"PW: Contraption Key": 0x4000086B # VILHELM FIGHT/NEEDED TO PROGRESS THROUGH PW
}
dreg_heap_table = { # DLC
"DH: Loincloth": 0x11B2EBD8,
"DH: Aquamarine Dagger": 0x00116520,
"DH: Murky Hand Scythe": 0x00118C30,
"DH: Murky Longstaff": 0x00CCC5E0,
"DH: Great Soul Dregs": 0x401879A0,
"DH: Lothric War Banner": 0x00CCC5E0,
"DH: Projected Heal": 0x40364688,
"DH: Desert Pyromancer Hood": 0x14DB9760,
"DH: Desert Pyromancer Garb": 0x14DB9B48,
"DH: Desert Pyromancer Gloves": 0x14DB9F30,
"DH: Desert Pyromancer Skirt": 0x14DBA318,
"DH: Giant Door Shield": 0x00F5F8C0,
"DH: Herald Curved Greatsword": 0x006159E0,
"DH: Flame Fan": 0x40258190,
"DH: Soul of the Demon Prince": 0x400002EA,
"DH: Small Envoy Banner": 0x4000086C # NEEDED TO TRAVEL TO RINGED CITY
}
ringed_city_table = { # DLC
"RC: Ruin Sentinel Helm": 0x14CC5520,
"RC: Ruin Sentinel Armor": 0x14CC5908,
"RC: Ruin Sentinel Gauntlets": 0x14CC5CF0,
"RC: Ruin Sentinel Leggings": 0x14CC60D8,
"RC: Black Witch Veil": 0x14FA1BE0,
"RC: Black Witch Hat": 0x14EAD9A0,
"RC: Black Witch Garb": 0x14EADD88,
"RC: Black Witch Wrappings": 0x14EAE170,
"RC: Black Witch Trousers": 0x14EAE558,
"RC: White Preacher Head": 0x14153A20,
"RC: Havel's Ring": 0x20004E34,
"RC: Ringed Knight Spear": 0x008CFDC0,
"RC: Dragonhead Shield": 0x0135E7F0,
"RC: Ringed Knight Straight Sword": 0x00225510,
"RC: Preacher's Right Arm": 0x00CD1400,
"RC: White Birch Bow": 0x00D77440,
"RC: Church Guardian Shiv": 0x4000013B, # Assigned to "Demon's Scar"
"RC: Dragonhead Greatshield": 0x01452A30,
"RC: Ringed Knight Paired Greatswords": 0x00F69500,
"RC: Shira's Crown": 0x11C22260,
"RC: Shira's Armor": 0x11C22648,
"RC: Shira's Gloves": 0x11C22A30,
"RC: Shira's Trousers": 0x11C22E18,
"RC: Titanite Slab": 0x400003EB, # SHIRA DROP
"RC: Crucifix of the Mad King": 0x008D4BE0, # SHIRA DROP
"RC: Sacred Chime of Filianore": 0x00CCECF0, # SHIRA DROP
"RC: Iron Dragonslayer Helm": 0x1405F7E0,
"RC: Iron Dragonslayer Armor": 0x1405FBC8,
"RC: Iron Dragonslayer Gauntlets": 0x1405FFB0,
"RC: Iron Dragonslayer Leggings": 0x14060398,
"RC: Lightning Arrow": 0x40358B08,
"RC: Ritual Spear Fragment": 0x4000028A, # Assigned to "Frayed Blade"
"RC: Antiquated Plain Garb": 0x11B2E408,
"RC: Violet Wrappings": 0x11B2E7F0, # Assigned to "Gael's Greatsword"
"RC: Soul of Darkeater Midir": 0x400002EB,
"RC: Soul of Slave Knight Gael": 0x400002E9,
"RC: Blood of the Dark Souls": 0x4000086E, # Assigned to "Repeating Crossbow"
}
progressive_locations = {
# Upgrade materials
**{"Titanite Shard #"+str(i): 0x400003E8 for i in range(1, 11)},
**{"Large Titanite Shard #"+str(i): 0x400003E9 for i in range(1, 11)},
**{"Titanite Chunk #"+str(i): 0x400003EA for i in range(1, 6)},
**{"Titanite Slab #"+str(i): 0x400003EB for i in range(1, 4)},
# Healing
**{"Estus Shard #"+str(i): 0x4000085D for i in range(1, 16)},
**{"Undead Bone Shard #"+str(i): 0x4000085F for i in range(1, 6)},
# Items
**{"Firebomb #"+str(i): 0x40000124 for i in range(1, 5)},
**{"Throwing Knife #"+str(i): 0x40000136 for i in range(1, 3)},
# Souls
**{"Soul of a Deserted Corpse #" + str(i): 0x40000191 for i in range(1, 6)},
**{"Large Soul of a Deserted Corpse #" + str(i): 0x40000192 for i in range(1, 6)},
**{"Soul of an Unknown Traveler #" + str(i): 0x40000193 for i in range(1, 6)},
**{"Large Soul of an Unknown Traveler #" + str(i): 0x40000194 for i in range(1, 6)}
}
progressive_locations_2 = {
##Added by Br00ty
"HWL: Gold Pine Resin #": 0x4000014B,
"US: Charcoal Pine Resin #": 0x4000014A,
"FK: Gold Pine Bundle #": 0x40000155,
"CC: Carthus Rouge #": 0x4000014F,
"ID: Pale Pine Resin #": 0x40000150,
**{"Titanite Scale #" + str(i): 0x400003FC for i in range(1, 27)},
**{"Fading Soul #" + str(i): 0x40000190 for i in range(1, 4)},
**{"Ring of Sacrifice #"+str(i): 0x20004EF2 for i in range(1, 5)},
**{"Homeward Bone #"+str(i): 0x4000015E for i in range(1, 17)},
**{"Ember #"+str(i): 0x400001F4 for i in range(1, 46)},
}
progressive_locations_3 = {
**{"Green Blossom #" + str(i): 0x40000104 for i in range(1, 7)},
**{"Human Pine Resin #" + str(i): 0x4000014E for i in range(1, 3)},
**{"Charcoal Pine Bundle #" + str(i): 0x40000154 for i in range(1, 3)},
**{"Rotten Pine Resin #" + str(i): 0x40000157 for i in range(1, 3)},
**{"Pale Tongue #" + str(i): 0x40000175 for i in range(1, 3)},
**{"Alluring Skull #" + str(i): 0x40000126 for i in range(1, 3)},
**{"Undead Hunter Charm #" + str(i): 0x40000128 for i in range(1, 3)},
**{"Duel Charm #" + str(i): 0x40000130 for i in range(1, 3)},
**{"Rusted Coin #" + str(i): 0x400001C7 for i in range(1, 3)},
**{"Rusted Gold Coin #" + str(i): 0x400001C9 for i in range(1, 4)},
**{"Titanite Chunk #"+str(i): 0x400003EA for i in range(1, 17)},
**{"Twinkling Titanite #"+str(i): 0x40000406 for i in range(1, 8)}
}
dlc_progressive_locations = { #71
**{"Large Soul of an Unknown Traveler $"+str(i): 0x40000194 for i in range(1, 10)},
**{"Soul of a Weary Warrior $"+str(i): 0x40000197 for i in range(1, 6)},
**{"Large Soul of a Weary Warrior $"+str(i): 0x40000198 for i in range(1, 7)},
**{"Soul of a Crestfallen Knight $"+str(i): 0x40000199 for i in range(1, 7)},
**{"Large Soul of a Crestfallen Knight $"+str(i): 0x4000019A for i in range(1, 4)},
**{"Homeward Bone $"+str(i): 0x4000015E for i in range(1, 7)},
**{"Large Titanite Shard $"+str(i): 0x400003E9 for i in range(1, 4)},
**{"Titanite Chunk $"+str(i): 0x400003EA for i in range(1, 16)},
**{"Twinkling Titanite $"+str(i): 0x40000406 for i in range(1, 6)},
**{"Rusted Coin $"+str(i): 0x400001C7 for i in range(1, 4)},
**{"Ember $"+str(i): 0x400001F4 for i in range(1, 11)}
}
location_tables = [fire_link_shrine_table, firelink_shrine_bell_tower_table, high_wall_of_lothric, undead_settlement_table, road_of_sacrifice_table,
cathedral_of_the_deep_table, farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table,
irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, consumed_king_garden_table,
grand_archives_table, untended_graves_table, archdragon_peak_table, progressive_locations, progressive_locations_2, progressive_locations_3,
painted_world_table, dreg_heap_table, ringed_city_table, dlc_progressive_locations]
location_dictionary = {**fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table,
**cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table,
**irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table,
**grand_archives_table, **untended_graves_table, **archdragon_peak_table, **progressive_locations, **progressive_locations_2, **progressive_locations_3,
**painted_world_table, **dreg_heap_table, **ringed_city_table, **dlc_progressive_locations}

View File

@@ -11,27 +11,32 @@
## General Concept
<span style="color:tomato">
**This mod can ban you permanently from the FromSoftware servers if used online.**
</span>
The Dark Souls III AP Client is a dinput8.dll triggered when launching Dark Souls III. This .dll file will launch a command
prompt where you can read information about your run and write any command to interact with the Archipelago server.
## Installation Procedures
This client has only been tested with the Official Steam version of the game at version 1.15. It does not matter which DLCs are installed. However, you will have to downpatch your Dark Souls III installation from current patch.
<span style="color:tomato">
**This mod can ban you permanently from the FromSoftware servers if used online.**
</span>
This client has only been tested with the Official Steam version of the game (v1.15/1.35) not matter which DLCs are installed.
## Downpatching Dark Souls III
Follow instructions from the [speedsouls wiki](https://wiki.speedsouls.com/darksouls3:Downpatching) to download version 1.15. Your download command, including the correct depot and manifest ids, will be "download_depot 374320 374321 4471176929659548333"
## Installing the Archipelago mod
Get the dinput8.dll from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) and
add it at the root folder of your game (e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game")
## Joining a MultiWorld Game
1. Run DarkSoulsIII.exe or run the game through Steam
2. Type in "/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}" in the "Windows Command Prompt" that opened
3. Once connected, create a new game, choose a class and wait for the others before starting
4. You can quit and launch at anytime during a game
1. Run Steam in offline mode, both to avoid being banned and to prevent Steam from updating the game files
2. Launch Dark Souls III
3. Type in "/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}" in the "Windows Command Prompt" that opened
4. Once connected, create a new game, choose a class and wait for the others before starting
5. You can quit and launch at anytime during a game
## Where do I get a config file?
The [Player Settings](/games/Dark%20Souls%20III/player-settings) page on the website allows you to
configure your personal settings and export them into a config file
configure your personal settings and export them into a config file.

View File

@@ -3,6 +3,7 @@ import typing
import math
import threading
import settings
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table
from .Locations import DKC3Location, all_locations, setup_locations
@@ -17,6 +18,16 @@ from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch
import Patch
class DK3Settings(settings.Group):
class RomFile(settings.UserFilePath):
"""File name of the DKC3 US rom"""
copy_to = "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
description = "DKC3 (US) ROM File"
md5s = [DKC3DeltaPatch.hash]
rom_file: RomFile = RomFile(RomFile.copy_to)
class DKC3Web(WebWorld):
theme = "jungle"
@@ -40,6 +51,7 @@ class DKC3World(World):
"""
game: str = "Donkey Kong Country 3"
option_definitions = dkc3_options
settings: typing.ClassVar[DK3Settings]
topology_present = False
data_version = 2
#hint_blacklist = {LocationName.rocket_rush_flag}
@@ -74,7 +86,11 @@ class DKC3World(World):
return slot_data
def generate_basic(self):
def create_regions(self):
location_table = setup_locations(self.multiworld, self.player)
create_regions(self.multiworld, self.player, location_table)
# Not generate basic
self.topology_present = self.multiworld.level_shuffle[self.player].value
itempool: typing.List[DKC3Item] = []
@@ -186,10 +202,6 @@ class DKC3World(World):
er_hint_data[location.address] = world_names[world_index]
multidata['er_hint_data'][self.player] = er_hint_data
def create_regions(self):
location_table = setup_locations(self.multiworld, self.player)
create_regions(self.multiworld, self.player, location_table)
def create_item(self, name: str, force_non_progression=False) -> Item:
data = item_table[name]
@@ -204,5 +216,8 @@ class DKC3World(World):
return created_item
def get_filler_item_name(self) -> str:
return self.multiworld.random.choice(list(junk_table.keys()))
def set_rules(self):
set_rules(self.multiworld, self.player)

View File

@@ -105,6 +105,8 @@ def set_basic_shuffled_items_rules(World_Options, player, world):
lambda state: state.has("Sword", player) or state.has("Gun", player))
set_rule(world.get_location("West Cave Sheep", player),
lambda state: state.has("Sword", player) or state.has("Gun", player))
set_rule(world.get_location("Gun", player),
lambda state: state.has("Gun Pack", player))
if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required:
set_rule(world.get_location("Sword", player),

1172
worlds/doom_1993/Items.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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