Compare commits

...

97 Commits

Author SHA1 Message Date
NewSoupVi
af4b312f54 Update BaseClasses.py
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2024-09-05 18:11:02 +02:00
NewSoupVi
b388572379 Update BaseClasses.py 2024-09-05 13:54:57 +02:00
NewSoupVi
6ee907e631 Core: Make region.add_exits return the created Entrances 2024-09-05 13:53:03 +02:00
Bryce Wilson
d65863ffa2 Pokemon Emerald: Fix wrong place for initialization (#3870) 2024-09-04 20:00:47 +02:00
Aaron Wagener
b8d7ef24f7 The Messenger: remove an invalid entrance (#3873) 2024-09-04 15:21:02 +02:00
Silvris
b2949dfbe8 KDL3: Account for additional animal in pool #3874 2024-09-04 15:19:00 +02:00
black-sliver
2aa0653b6d WebHost: update dependencies (#3871) 2024-09-03 02:31:42 +02:00
black-sliver
d63efa5846 Core: update dependencies (#3869) 2024-09-03 02:22:48 +02:00
Fabian Dill
765721888a WebHost: config override (#3701) 2024-09-03 01:26:46 +02:00
black-sliver
73701292b5 Core, CI: Add Python 3.12 support (#3290)
* Core, CI: add py3.12 compat

* Stardew Valley: Fix tests for Py3.12

* ModuleUpdate: always install pkg_resources

* Docs: update supported python versions

* WebHost: update pony to upstream 0.7.18

* CI: test hosting update to py3.12

* Update docs/running from source.md
2024-09-02 10:08:16 +02:00
Fabian Dill
3ab71daa8d MultiServer: put some limits in place (#3858)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-09-01 21:59:37 +02:00
NewSoupVi
6f46397185 Rogue Legacy: Crash generation when there are overlapping IDs (#3865)
Client literally does not work when there are overlapping IDs.

Phar is not currently intending to fix it.

https://discord.com/channels/731205301247803413/929585237695029268/1269684436853723156
2024-09-01 21:41:55 +02:00
black-sliver
1a41e1acc8 customserver: fix memory leak (#3864) 2024-09-01 20:34:50 +02:00
Scipio Wright
34a3b5f058 TUNIC: Add alias for Ladders in Overworld Town #3862 2024-08-31 23:37:18 +02:00
Exempt-Medic
456b4adaa1 ALttP/Docs: Correcting the plando docs (#3835)
* Correcting some text

* Reword sentence
2024-08-31 23:36:29 +02:00
NewSoupVi
fc8462f4e9 The Witness: Add Beginner Mode option preset #3691 2024-08-31 22:51:41 +02:00
Mysteryem
499dad53b1 AHIT: Fix thug shops having 0 items after the first shop rolls 0 items (#3799)
Once a thug shop rolled 0 as the number of items it should have, all
remaining iterations would do nothing because neither the `count == -1`
condition nor the `count >= 1` condition would be met. This caused all
remaining thug shops to have zero items. This also caused the item
counts of remaining thug shops to be absent from slot data, which was
how this issue was found.

I found the old code confusing and, rather than try to figure out how to
fix it, I opted to rewrite it. With the new code, a local variable
dictionary tracks the number of created locations for each thug and no
more locations are created for a thug once their number of locations
equals the number of shop items that thug rolled.
2024-08-31 21:00:19 +02:00
agilbert1412
8a809be67a Stardew Valley - Prize Ticket and Mystery Box grinding requires the abilty to redeem them #3728 2024-08-31 20:57:43 +02:00
lordlou
7e0219c214 SM and SMZ3 option_definitions deprecation fix (#3372)
* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* Fixed multiworld support patch not working with VariaRandomizer's

Added stage_fill_hook to set morph first in progitempool
Added back VariaRandomizer's standard patches

* + added missing files from variaRandomizer project

* + added missing variaRandomizer files (custom sprites)

+ started integrating VariaRandomizer options (WIP)

* Some fixes for player and server name display

- fixed player name of 16 characters reading too far in SM client
- fixed 12 bytes SM player name limit (now 16)
- fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO)
- request: temporarly changed default seed names displayed in SM main menu to OWTCH

* Fixed Goal completion not triggering in smClient

* integrated VariaRandomizer's options into AP (WIP)

- startAP is working
- door rando is working
- skillset is working

* - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off")

* skillset are now instanced per player instead of being a singleton class

* RomPatches are now instanced per player instead of being a singleton class

* DoorManager is now instanced per player instead of being a singleton class

* - fixed the last bugs that prevented generation of >1 SM world

* fixed crash when no skillset preset is specified in randoPreset (default to "casual")

* maxDifficulty support and itemsounds removal

- added support for maxDifficulty
- removed itemsounds patch as its always applied from multiworld patch for now

* Fixed bad merge

* Post merge adaptation

* fixed player name length fix that got lost with the merge

* fixed generation with other game type than SM

* added default randoPreset json for SM in playerSettings.yaml

* fixed broken SM client following merge

* beautified json skillset presets

* Fixed ArchipelagoSmClient not building

* Fixed conflict between mutliworld patch and beam_doors_plms patch

- doorsColorsRando now working

* SM generation now outputs APBP

- Fixed paths for patches and presets when frozen

* added missing file and fixed multithreading issue

* temporarily set data_version = 0

* more work

- added support for AP starting items
- fixed client crash with gamemode being None
- patch.py "compatible_version" is now 3

* commited missing asm files

fixed start item reserve breaking game (was using bad write offset when patching)

* Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it).

fixed crash in SMClient when loosing connection to SNI

* fixed No Energy Item missing its ID

fixed Plando

* merge post fixes

* fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color)

* fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses)

* fixed start item x-ray HUD display

* Fixed start items being sent by the server (is all handled in ROM)

Start items are now not removed from itempool anymore
Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though.
Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified

* fixed settings that could be applied to any SM players

* fixed auth to server only using player name (now does as ALTTP to authenticate)

* - fixed End Credits broken text

* added non SM item name display

* added all supported SM options in playerSettings.yaml

* fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region

did some cleaning (mainly reverts on unnecessary core classes

* minor setting fixes and tweaks

- merged Area and lightArea settings
- made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating
- fixed inverted layoutPatch setting

* added option start_inventory_removes_from_pool

fixed option names formatting
fixed lint errors
small code and repo cleanup

* Hopefully fixed ROR2 that could not send any items

* - fixed missing required change to ROR2

* fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum)

* fixed typo with doors_colors_rando

* fixed checksum

* added custom sprites for off-world items (progression or not)

the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu

* - added missing change following upstream merge

- changed patch filename extension from apbp to apm3 so patch can be used with the new client

* added morph placement options: early means local and sphere 1

* fixed failing unit tests

* - fixed broken custom_preset options

* - big cleanup to remove unnecessary or unsupported features

* - more cleanup

* - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips

- small cleanup

* - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch)

* fixed g4_skip patch that can be not applied if hud is enabled

* - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette)

* - updated basepatch to reflect g4_skip removal

- moved more asm files to SMBasepatch project

* - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed)

* fixed wrong path if using built as exe

* - cleaned exposed maxDifficulty options

- removed always enabled Knows

* Merged LttPClient and SMClient into SNIClient

* added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service

* small doc precision

* - added death_link support

- fixed broken Goal Completion
- post merge fix

* - removed now useless presets

* - fixed bad internal mapping with maxDiff

- increases maxDiff if only Bosses is preventing beating the game

* - added support for lowercase custom preset sections (knows, settings and controller)

- fixed controller settings not applying to ROM

* - fixed death loop when dying with Door rando, bomb or speed booster as starting items

- varia's backup save should now be usable (automatically enabled when doing door rando)

* -added docstring for generated yaml

* fixed bad merge

* fixed broken infinity max difficulty

* commented debug prints

* adjusted credits to mark progression speed and difficulty as Non Available

* added support for more than 255 players (will print Archipelago for higher player number)

* fixed missing cleanup

* added support for 65535 different player names in ROM

* fixed generations failing when only bosses are unreachable

* - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish

* fixed failling generations when using 'fun' settings

Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings

* fixed debug logger

* removed unsupported "suits_restriction" option

* fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP)

* - fixed deathlink emptying reserves

- added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves

* - merged death_link and death_link_survive options

* fixed death_link

* added a fallback default starting location instead of failing generation if an invalid one was chosen

* added Nothing and NoEnergy as hint blacklist

added missing NoEnergy as local items and removed it from progression

* replaced deprecated use of option_definitions for SM and SMZ3 by options_dataclass

* fixed missed references to option_definitions

* Update worlds/sm/variaRandomizer/utils/utils.py

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

* fixed conflicts and made SMZ3 accessibility related code more future proof

* Update worlds/smz3/Options.py

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

* Update worlds/smz3/__init__.py

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

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-08-31 13:49:33 +02:00
Exempt-Medic
b37bb60891 DS3: Prevent prioritized+excluded locations (#3855) 2024-08-31 13:44:48 +02:00
Natalie Weizenbaum
f81335d614 DS3: Don't return early in the location loop (#3856)
This caused behavior errors when some locations in a group were
excluded and others were not.
2024-08-31 13:44:09 +02:00
Kory Dondzila
8ed466bf24 Shivers: Add collect behavior option. (#3854)
* Add collect behavior option.

* Add comma

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

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-08-31 13:30:42 +02:00
Silvris
920cffda2d KDL3: Version 2.0.0 (#3323)
* initial work on procedure patch

* more flexibility

load default procedure for version 5 patches
add args for procedure
add default extension for tokens and bsdiff
allow specifying additional required extensions for generation

* pushing current changes to go fix tloz bug

* move tokens into a separate inheritable class

* forgot the commit to remove token from ProcedurePatch

* further cleaning from bad commit

* start on docstrings

* further work on docstrings and typing

* improve docstrings

* fix incorrect docstring

* cleanup

* clean defaults and docstring

* define interface that has only the bare minimum required
for `Patch.create_rom_file`

* change to dictionary.get

* remove unnecessary if statement

* update to explicitly check for procedure, restore compatible version and manual override

* Update Files.py

* remove struct uses

* Update Rom.py

* convert KDL3 to APPP

* change class variables to instance variables

* Update worlds/Files.py

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

* Update worlds/Files.py

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

* move required_extensions to tuple

* fix missing tuple ellipsis

* fix classvar mixup

* rename tokens to _tokens. use hasattr

* type hint cleanup

* Update Files.py

* initial base for local items, need to finish

* coo not clean

* handle local items for real, appp cleanup

* actually make bosses send their locations

* fix cloudy park 4 rule, zero deathlink message

* remove redundant door_shuffle bool

when generic ER gets in, this whole function gets rewritten. So just clean it a little now.

* properly fix deathlink messages, fix fill error

* update docs

* add prefill items

* fix kine fill error

* Update Rom.py

* Update Files.py

* mypy and softlock fix

* Update Gifting.py

* mypy phase 1

* fix rare async client bug

* Update __init__.py

* typing cleanup

* fix stone softlock

because of the way Kine's Stone works, you can't clear the stone blocks before clearing the burning blocks, so we have to bring Burning from outside

* Update Rom.py

* Add option groups

* Rename to lowercase

* finish rename

* whoops broke the world

* fix animal duplication bug

* overhaul filler generation

* add Miku flavor

* Update gifting.py

* fix issues related to max_hs increase

* Update test_locations.py

* fix boss shuffle not working if level shuffle is disabled

* fix bleeding default levels

* Update options.py

* thought this would print seed

* yay bad merges

* forgot options too

* yeah lets just break generation while at it

* this is probably a problem

* cap required heart stars

* Revert "cap required heart stars"

This reverts commit 759efd3e2b.

* fix duplication removal placement, deprecated test option

* forgot that we need to account for what we place

* move location ids

* rewrite trap handling

* further stage renumber fixes

* forgot one more

* basic UT support

* fix local heart star checks

* fix pattern

---------

Co-authored-by: beauxq <beauxq@yahoo.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-08-31 13:15:00 +02:00
Natalie Weizenbaum
b1be597451 DS3: Explicitly track item equality by name when sending IDs (#3853)
We had been keeping a set of items and defining item equality, but
item equality really only makes sense if you consider distinct IDs to
be distinct items. But that means the set ends up having multiple
copies of the same item, causing a bug where some items had the wrong
upgrade level in the game.

This also removes the equality definition, which was only used by this
one set.
2024-08-30 12:26:49 +02:00
Scipio Wright
08dc7e522e TUNIC: Add note about plando items to ER hint-creation failure error message (#3825)
* Add note about plando items to entrance rando option description

* Update error text to specifically call out plando items

* Remove option description change
2024-08-29 09:42:46 +02:00
Exempt-Medic
0f64bd08e1 ChecksFinder: itempool naming/typing (#3797)
* Rename itempool

* Update comment
2024-08-29 08:43:13 +02:00
agilbert1412
d52827ebd2 Stardew Valley: Fix Crimsonfish region (#3687)
* - Add Unit test for all the fish that require a specific region to be reachable

* - Move the crimsonfish to the tide pools region

* - Improved the unit test to be more thorough, add extended family fish to the test

* - Moved the son of crimsonfish to the correct region as well

* FFMQ: Fix reset protection (#3710)

* Revert reset protection

* Fix reset protection

---------

Co-authored-by: alchav <alchav@jalchavware.com>

* - Take shipsanity moss out of shipsanity crops (#3709)

* sc2: Removing unused dependency in requirements.txt (#3697)

* sc2: Removing unused dependency in requirements.txt

* sc2: Add missing newline in requirements.txt

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

---------

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

* WebHost: Fix NamedRange values clamping to the range (#3613)

If a NamedRange has a `special_range_names` entry outside the
`range_start` and `range_end`, the HTML5 range input will clamp the
submitted value to the closest value in the range.

These means that, for example, Pokemon RB's "HM Compatibility" option's
"Vanilla (-1)" option would instead get posted as "0" rather than "-1".

This change updates NamedRange to behave like TextChoice, where the
select element has a `name` attribute matching the option, and there is
an additional element to be able to provide an option other than the
select element's choices.

This uses a different suffix of `-range` rather than `-custom` that
TextChoice uses. The reason is we need some way to decide whether to use
the custom value or the select value, and that method needs to work
without JavaScript. For TextChoice this is easy, if the custom field is
empty use the select element. For NamedRange this is more difficult as
the browser will always submit *something*. My choice was to only use
the value from the range if the select box is set to "custom". Since
this only happens with JS as "custom' is hidden, I made the range hidden
under no-JS. If it's preferred, I could make the select box hidden
instead. Let me know.

This PR also makes the `js-required` class set `display: none` with
`!important` as otherwise the class wouldn't work on any rule that
had `display: flex` with more specificity than a single class.

* Timespinner: migrate to new options api and correct random (#2485)

* Implemented new options system into Timespinner

* Fixed typo

* Fixed typo

* Fixed slotdata maybe

* Fixes

* more fixes

* Fixed failing unit tests

* Implemented options backwards comnpatibility

* Fixed option fallbacks

* Implemented review results

* Fixed logic bug

* Fixed python 3.8/3.9 compatibility

* Replaced one more multiworld option usage

* Update worlds/timespinner/Options.py

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

* Updated logging of options replacement to include player name and also write it to spoiler
Fixed generation bug
Implemented review results

---------

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

* Core: migrate item links out of main (#2914)

* Core: move item linking out of main

* add a test that item link option correctly validates

* remove unused fluff

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>

* Core: Rework accessibility (#1481)

* rename locations accessibility to "full" and make old locations accessibility debug only

* fix a bug in oot

* reorder lttp tests to not override its overrides

* changed the wrong word in the dict

* :forehead:

* update the manual lttp yaml

* use __debug__

* update pokemon and messenger

* fix conflicts from 993

* fix stardew presets

* add that locations may be inaccessible to description

* use reST format and make the items description one line so that it renders correctly on webhost

* forgot i renamed that

* add aliases for back compat

* some cleanup

* fix imports

* fix test failure

* only check "items" players when the item is progression

* Revert "only check "items" players when the item is progression"

This reverts commit ecbf986145.

* remove some unnecessary diffs

* CV64: Add ItemsAccessibility

* put items description at the bottom of the docstring since that's it's visual order

* :

* rename accessibility reference in pokemon rb dexsanity

* make the rendered tooltips look nicer

* Shivers: New features and removes two missed options using the old options API (#3287)

* Adds an option to have pot pieces placed local/non-local/anywhere

Shivers nearly always finishes last in multiworld games due to the fact you need all 20 pot pieces to win and the pot pieces open very few location checks. This option allows the pieces to be placed locally. This should allow Shivers to be finished earlier.

* New option: Choose how many ixupi captures are needed for goal completion

New option: Choose how many ixupi captures are needed for goal completion

* Fixes rule logic for location 'puzzle solved three floor elevator'

Fixes rule logic for location 'puzzle solved three floor elevator'. Missing a parenthesis caused only the key requirement to be checked for the blue maze region.

* Merge branch 'main' of https://github.com/GodlFire/Shivers

* Revert "Merge branch 'main' of https://github.com/GodlFire/Shivers"

This reverts commit bb08c3f0c2.

* Fixes issue with office elevator rule logic.

* Bug fix, missing logic requirement for location 'Final Riddle: Guillotine Dropped'

Bug fix, missing logic requirement for location 'Final Riddle: Guillotine Dropped'

* Moves plaque location to front for better tracker referencing.

* Tiki should be Shaman.

* Hanging should be Gallows.

* Merrick spelling.

* Clarity change.

* Changes new option to use new option API

Changes new option to use new option API

* Added sub regions for Ixupi

-Added sub regions for Ixupi and moved ixupi capture checks into the sub region.
-Added missing wax capture possible spot in Shaman room

* Adds option for ixupi captures to be priority locations

Adds option for ixupi captures to be priority locations

* Consistency

Consistency

* Changes ixupi captures priority to default on toggle

Changes ixupi captures priority to default on toggle

* Docs update

-Updated link to randomizer
-Update some text to reflect the latest functionality
-Replaced 'setting' with 'option'

* New features/bug fixes

-Adds an option to have completed pots in the item pool
-Moved subterranean world information plaque to maze staircase

* Cleanup

Cleanup

* Fixed name for moved location

When moving a location and renaming it I forgot to fix the name in a second spot.

* Squashed commit of the following:

commit 630a3bdfb9
Merge: 8477d3c8 5e579200
Author: GodlFire <46984098+GodlFire@users.noreply.github.com>
Date:   Mon Apr 1 19:08:48 2024 -0600

    Merge pull request #10 from ArchipelagoMW/main

    Merge main into branch

commit 5e5792009c
Author: Alchav <59858495+Alchav@users.noreply.github.com>
Date:   Mon Apr 1 12:08:21 2024 -0500

    LttP: delete playerSettings.yaml (#3062)

commit 9aeeeb077a
Author: CaitSith2 <d_good@caitsith2.com>
Date:   Mon Apr 1 06:07:56 2024 -0700

    ALttP: Re-mark light/dark world regions after applying plando connections (#2964)

commit 35458380e6
Author: Bryce Wilson <gyroscope15@gmail.com>
Date:   Mon Apr 1 07:07:11 2024 -0600

    Pokemon Emerald: Fix wonder trade race condition (#2983)

commit 4ac1866689
Author: Alchav <59858495+Alchav@users.noreply.github.com>
Date:   Mon Apr 1 08:06:31 2024 -0500

    ALTTP: Skull Woods Inverted fix (#2980)

commit 4aa03da66e
Author: Fabian Dill <Berserker66@users.noreply.github.com>
Date:   Mon Apr 1 15:06:02 2024 +0200

    Factorio: fix attempting to create savegame with not filename safe characters (#2842)

commit 24a03bc8b6
Author: Silvris <58583688+Silvris@users.noreply.github.com>
Date:   Mon Apr 1 08:02:26 2024 -0500

    KDL3: fix shuffled animals not actually being random (#3060)

commit f813a7005f
Author: Aaron Wagener <mmmcheese158@gmail.com>
Date:   Sun Mar 31 11:11:10 2024 -0500

    The Messenger: update docs formatting and fix outdated info (#3033)

    * The Messenger: update docs formatting and fix outdated info

    * address review feedback

    * 120 chars

commit 2a0b7e0def
Author: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com>
Date:   Sun Mar 31 09:55:55 2024 -0600

    CV64: A couple of very small docs corrections. (#3057)

commit 03d47e460e
Author: Ixrec <ericrhitchcock@gmail.com>
Date:   Sun Mar 31 16:55:08 2024 +0100

    A Short Hike: Clarify installation instructions (#3058)

    * Clarify installation instructions

    * don't mention 'config' folder since it isn't created until the game starts

commit e546c0f7ff
Author: Silvris <58583688+Silvris@users.noreply.github.com>
Date:   Sun Mar 31 10:50:31 2024 -0500

    Yoshi's Island: add patch suffix (#3061)

commit 2ec93ba82a
Author: Bryce Wilson <gyroscope15@gmail.com>
Date:   Sun Mar 31 09:48:59 2024 -0600

    Pokemon Emerald: Fix inconsistent location name (#3065)

commit 4e3d396394
Author: Aaron Wagener <mmmcheese158@gmail.com>
Date:   Sun Mar 31 10:47:11 2024 -0500

    The Messenger: Fix precollected notes not being removed from the itempool (#3066)

    * The Messenger: fix precollected notes not being properly removed from pool

    * The Messenger: bump required client version

commit 72c53513f8
Author: Fabian Dill <Berserker66@users.noreply.github.com>
Date:   Sun Mar 31 03:57:59 2024 +0200

    WebHost: fix /check creating broken yaml files if files don't end with a newline (#3063)

commit b7ac6a4cbd
Author: Aaron Wagener <mmmcheese158@gmail.com>
Date:   Fri Mar 29 20:14:53 2024 -0500

    The Messenger: Fix various portal shuffle issues (#2976)

    * put constants in a bit more sensical order

    * fix accidental incorrect scoping

    * fix plando rules not being respected

    * add docstrings for the plando functions

    * fix the portal output pools being overwritten

    * use shuffle and pop instead of removing by content so plando can go to the same area twice

    * move portal pool rebuilding outside mapping creation

    * remove plando_connection cleansing since it isn't shared with transition shuffle

commit 5f0112e783
Author: Zach Parks <zach@alliware.com>
Date:   Fri Mar 29 19:13:51 2024 -0500

    Tracker: Add starting inventory to trackers and received items table. (#3051)

commit bb481256de
Author: Aaron Wagener <mmmcheese158@gmail.com>
Date:   Thu Mar 28 21:48:40 2024 -0500

    Core: Make fill failure error more human parseable (#3023)

commit 301d9de975
Author: Aaron Wagener <mmmcheese158@gmail.com>
Date:   Thu Mar 28 19:31:59 2024 -0500

    Docs: adding games rework (#2892)

    * Docs: complete adding games.md rework

    * remove all the now unused images

    * review changes

    * address medic's review

    * address more comments

commit 9dc708978b
Author: Trevor L <80716066+TRPG0@users.noreply.github.com>
Date:   Thu Mar 28 18:26:58 2024 -0600

    Hylics 2: Fix invalid multiworld data, use `self.random` instead of `self.multiworld.random` (#3001)

    * Hylics 2: Fixes

    * Rewrite loop

commit 4391d1f4c1
Author: Bryce Wilson <gyroscope15@gmail.com>
Date:   Thu Mar 28 18:05:39 2024 -0600

    Pokemon Emerald: Fix opponents learning non-randomized TMs (#3025)

commit 5d9d4ed9f1
Author: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date:   Fri Mar 29 01:01:31 2024 +0100

    SoE: update to pyevermizer v0.48.0 (#3050)

commit c97215e0e7
Author: Scipio Wright <scipiowright@gmail.com>
Date:   Thu Mar 28 17:23:37 2024 -0400

    TUNIC: Minor refactor of the vanilla_portals function (#3009)

    * Remove unused, change an if to an elif

    * Remove unused import

commit eb66886a90
Author: Alchav <59858495+Alchav@users.noreply.github.com>
Date:   Thu Mar 28 16:23:01 2024 -0500

    SC2: Don't Filter Excluded Victory Locations (#3018)

commit de860623d1
Author: Fabian Dill <Berserker66@users.noreply.github.com>
Date:   Thu Mar 28 22:21:56 2024 +0100

    Core: differentiate between unknown worlds and broken worlds in error message (#2903)

commit 74b2bf5161
Author: Bryce Wilson <gyroscope15@gmail.com>
Date:   Thu Mar 28 15:20:55 2024 -0600

    Pokemon Emerald: Exclude norman trainer location during norman goal (#3038)

commit 74ac66b032
Author: BadMagic100 <dempsey.sean@outlook.com>
Date:   Thu Mar 28 08:49:19 2024 -0700

    Hollow Knight: 0.4.5 doc revamp and default options tweaks (#2982)

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

commit 80d7ac4164
Author: Silvris <58583688+Silvris@users.noreply.github.com>
Date:   Thu Mar 28 09:41:32 2024 -0500

    KDL3: RC1 Fixes and Enhancement (#3022)

    * fix cloudy park 4 rule, zero deathlink message

    * remove redundant door_shuffle bool

    when generic ER gets in, this whole function gets rewritten. So just clean it a little now.

    * properly fix deathlink messages, fix fill error

    * update docs

commit 77311719fa
Author: Ziktofel <ziktofel@gmail.com>
Date:   Thu Mar 28 15:38:34 2024 +0100

    SC2: Fix HERC upgrades (#3044)

commit cfc1541be9
Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Date:   Thu Mar 28 15:19:32 2024 +0100

    Docs: Mention the "last received item index" paradigm in the network protocol docs (#2989)

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

commit 4d954afd9b
Author: Scipio Wright <scipiowright@gmail.com>
Date:   Thu Mar 28 10:11:20 2024 -0400

    TUNIC: Add link to AP plando guide to connection plando section of game page (#2993)

commit 17748a4bf1
Author: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Date:   Thu Mar 28 10:00:10 2024 -0400

    Launcher, Docs: Update UI and Set-Up Guide to Reference Options  (#2950)

commit 9182fe563f
Author: Entropynines <163603868+Entropynines@users.noreply.github.com>
Date:   Thu Mar 28 06:56:35 2024 -0700

    README: Remove outdated information about launchers (#2966)

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

commit bcf223081f
Author: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com>
Date:   Thu Mar 28 09:54:56 2024 -0400

    TLOZ: Fix markdown issue with game info page (#2985)

commit fa93488f3f
Author: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Date:   Thu Mar 28 09:46:00 2024 -0400

    Docs: Consistent naming for "connection plando" (#2994)

commit db15dd4bde
Author: chandler05 <66492208+chandler05@users.noreply.github.com>
Date:   Thu Mar 28 08:45:19 2024 -0500

    A Short Hike: Fix incorrect info in docs (#3016)

commit 01cdb0d761
Author: PoryGone <98504756+PoryGone@users.noreply.github.com>
Date:   Thu Mar 28 09:44:23 2024 -0400

    SMW: Update World Doc for v2.0 Features (#3034)

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

commit d0ac2b744e
Author: panicbit <panicbit@users.noreply.github.com>
Date:   Thu Mar 28 10:11:26 2024 +0100

    LADX: fix local and non-local instrument placement (#2987)

    * LADX: fix local and non-local instrument placement

    * change confusing variable name

commit 14f5f0127e
Author: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>
Date:   Thu Mar 28 04:42:35 2024 -0400

    Stardew Valley: Fix potential soft lock with vanilla tools and entrance randomizer + Performance improvement for vanilla tool/skills (#3002)

    * fix vanilla tool fishing rod requiring metal bars
    fix vanilla skill requiring previous level (it's always the same rule or more restrictive)

    * add test to ensure fishing rod need fish shop

    * fishing rod should be indexed from 0 like a mentally sane person would do.

    * fishing rod 0 isn't real, but it definitely can hurt you.

    * reeeeeeeee

commit cf133dde72
Author: Bryce Wilson <gyroscope15@gmail.com>
Date:   Thu Mar 28 02:32:27 2024 -0600

    Pokemon Emerald: Fix typo (#3020)

commit ca18121811
Author: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>
Date:   Thu Mar 28 04:27:49 2024 -0400

    Stardew Valley: Fix generation fail with SVE and entrance rando when Wizard Tower is in place of Sprite Spring (#2970)

commit 1d4512590e
Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Date:   Wed Mar 27 21:09:09 2024 +0100

    requirements.txt: _ instead of - to make PyCharm happy (#3043)

commit f7b415dab0
Author: agilbert1412 <alexgilbert@yahoo.com>
Date:   Tue Mar 26 19:40:58 2024 +0300

    Stardew valley: Game version documentation (#2990)

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

commit 702f006c84
Author: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com>
Date:   Tue Mar 26 07:31:36 2024 -0600

    CV64: Change all mentions of "settings" to "options" and fix a broken link (#3015)

commit 98ce8f8844
Author: Yussur Mustafa Oraji <N00byKing@hotmail.de>
Date:   Tue Mar 26 14:29:25 2024 +0100

    sm64ex: New Options API and WebHost fix (#2979)

commit ea47b90367
Author: Scipio Wright <scipiowright@gmail.com>
Date:   Tue Mar 26 09:25:41 2024 -0400

    TUNIC: You can grapple down here without the ladder, neat (#3019)

commit bf3856866c
Author: agilbert1412 <alexgilbert@yahoo.com>
Date:   Sun Mar 24 23:53:49 2024 +0300

    Stardew Valley: presets with some of the new available values for existing settings to make them more accurate (#3014)

commit c0368ae0d4
Author: Phaneros <31861583+MatthewMarinets@users.noreply.github.com>
Date:   Sun Mar 24 13:53:20 2024 -0700

    SC2: Fixed missing upgrade from custom tracker (#3013)

commit 36c83073ad
Author: Salzkorn <salzkitty@gmail.com>
Date:   Sun Mar 24 21:52:41 2024 +0100

    SC2 Tracker: Fix grouped items pointing at wrong item IDs (#2992)

commit 2b24539ea5
Author: Ziktofel <ziktofel@gmail.com>
Date:   Sun Mar 24 21:52:16 2024 +0100

    SC2 Tracker: Use level tinting to let the player know which level he has of Replenishable Magazine (#2986)

commit 7e904a1c78
Author: Ziktofel <ziktofel@gmail.com>
Date:   Sun Mar 24 21:51:46 2024 +0100

    SC2: Fix Kerrigan presence resolving when deciding which races should be used (#2978)

commit bdd498db23
Author: Alchav <59858495+Alchav@users.noreply.github.com>
Date:   Fri Mar 22 15:36:27 2024 -0500

    ALTTP: Fix #2290's crashes (#2973)

commit 355223b8f0
Author: PinkSwitch <52474902+PinkSwitch@users.noreply.github.com>
Date:   Fri Mar 22 15:35:00 2024 -0500

    Yoshi's Island: Implement New Game (#2141)

    Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
    Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com>
    Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
    Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

commit aaa3472d5d
Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Date:   Fri Mar 22 21:30:51 2024 +0100

    The Witness: Fix seed bleed issue (#3008)

commit 96d93c1ae3
Author: chandler05 <66492208+chandler05@users.noreply.github.com>
Date:   Fri Mar 22 15:30:23 2024 -0500

    A Short Hike: Add option to customize filler coin count (#3004)

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

commit ca549df20a
Author: Silvris <58583688+Silvris@users.noreply.github.com>
Date:   Fri Mar 22 15:29:24 2024 -0500

    CommonClient: fix hint tab overlapping (#2957)

    Co-authored-by: Remy Jette <remy@remyjette.com>

commit 44988d430d
Author: Star Rauchenberger <fefferburbia@gmail.com>
Date:   Fri Mar 22 15:28:41 2024 -0500

    Lingo: Add trap weights option (#2837)

commit 11b32f17ab
Author: Danaël V <104455676+ReverM@users.noreply.github.com>
Date:   Fri Mar 22 12:46:14 2024 -0400

    Docs: replacing "setting" to "option" in world docs  (#2622)

    * Update contributing.md

    * Update contributing.md

    * Update contributing.md

    * Update contributing.md

    * Update contributing.md

    * Update contributing.md

    Added non-AP World specific information

    * Update contributing.md

    Fixed broken link

    * Some minor touchups

    * Update Contributing.md

    Draft for version with picture

    * Update contributing.md

    Small word change

    * Minor updates for conciseness, mostly

    * Changed all instances of settings to options in info and setup guides

    I combed through all world docs and swapped "setting" to "option" when this was refering to yaml options.
    I also changed a leftover "setting" in option.py

    * Update contributing.md

    * Update contributing.md

    * Update setup_en.md

    Woops I forgot one

    * Update Options.py

    Reverted changes regarding options.py

    * Update worlds/noita/docs/en_Noita.md

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

    * Update worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md

    revert change waiting for that page to be updated

    * Update worlds/witness/docs/setup_en.md

    * Update worlds/witness/docs/en_The Witness.md

    * Update worlds/soe/docs/multiworld_en.md

    Fixed Typo

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

    * Update worlds/witness/docs/en_The Witness.md

    * Update worlds/adventure/docs/en_Adventure.md

    * Update worlds/witness/docs/setup_en.md

    * Updated Stardew valley to hopefully get rid of the merge conflicts

    * Didn't work :dismay:

    * Delete worlds/sc2wol/docs/setup_en.md

    I think this will fix the merge issue

    * Now it should work

    * Woops

    ---------

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

commit 218cd45844
Author: Silvris <58583688+Silvris@users.noreply.github.com>
Date:   Fri Mar 22 03:02:38 2024 -0500

    APProcedurePatch: fix RLE/COPY incorrect sizing (#3006)

    * change class variables to instance variables

    * Update worlds/Files.py

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

    * Update worlds/Files.py

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

    * move required_extensions to tuple

    * fix missing tuple ellipsis

    * fix classvar mixup

    * rename tokens to _tokens. use hasattr

    * type hint cleanup

    * Update Files.py

    * check using isinstance instead

    * Update Files.py

    ---------

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

commit 4196bde597
Author: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Date:   Thu Mar 21 16:38:36 2024 -0400

    Docs: Fixing special_range_names example (#3005)

commit 40f843f54d
Author: Star Rauchenberger <fefferburbia@gmail.com>
Date:   Thu Mar 21 11:00:53 2024 -0500

    Lingo: Minor game data fixes (#3003)

commit da333fbb0c
Author: GodlFire <46984098+GodlFire@users.noreply.github.com>
Date:   Thu Mar 21 09:52:16 2024 -0600

    Shivers: Adds missing logic rule for skull dial door location (#2997)

commit 43084da23c
Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Date:   Thu Mar 21 16:51:29 2024 +0100

    The Witness: Fix newlines in Witness option tooltips (#2971)

commit 14816743fc
Author: Scipio Wright <scipiowright@gmail.com>
Date:   Thu Mar 21 11:50:07 2024 -0400

    TUNIC: Shuffle Ladders option (#2919)

commit 30a0aa2c85
Author: Star Rauchenberger <fefferburbia@gmail.com>
Date:   Thu Mar 21 10:46:53 2024 -0500

    Lingo: Add item/location groups (#2789)

commit f4b7c28a33
Author: Silvris <58583688+Silvris@users.noreply.github.com>
Date:   Wed Mar 20 17:45:32 2024 -0500

    APProcedurePatch: hotfix changing class variables to instance variables (#2996)

    * change class variables to instance variables

    * Update worlds/Files.py

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

    * Update worlds/Files.py

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

    * move required_extensions to tuple

    * fix missing tuple ellipsis

    * fix classvar mixup

    * rename tokens to _tokens. use hasattr

    * type hint cleanup

    * Update Files.py

    * check using isinstance instead

    ---------

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

commit 12864f7b24
Author: chandler05 <66492208+chandler05@users.noreply.github.com>
Date:   Wed Mar 20 22:44:09 2024 +0100

    A Short Hike: Implement New Game (#2577)

commit db02e9d2aa
Author: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com>
Date:   Wed Mar 20 15:03:25 2024 -0600

    Castlevania 64: Implement New Game (#2472)

commit 32315776ac
Author: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>
Date:   Wed Mar 20 16:57:45 2024 -0400

    Stardew Valley: Fix extended family legendary fishes being locations with fishsanity set to exclude legendary (#2967)

commit e9620bea77
Author: Magnemania <89949176+Magnemania@users.noreply.github.com>
Date:   Wed Mar 20 16:56:00 2024 -0400

    SM64: Goal Logic and Hint Bugfixes (#2886)

commit 183ca35bba
Author: qwint <qwint.42@gmail.com>
Date:   Wed Mar 20 08:39:37 2024 -0500

    CommonClient: Port Casting Bug (#2975)

commit fcaaa197a1
Author: TheLX5 <luisyuregi@gmail.com>
Date:   Wed Mar 20 05:56:19 2024 -0700

    SMW: Fixes for Bowser being defeatable on Egg Hunt and CI2 DC room access (#2981)

commit 8f7b63a787
Author: TheLX5 <luisyuregi@gmail.com>
Date:   Wed Mar 20 05:56:04 2024 -0700

    SMW: Blocksanity logic fixes (#2988)

commit 6f64bb9869
Author: Scipio Wright <scipiowright@gmail.com>
Date:   Wed Mar 20 08:46:31 2024 -0400

    Noita: Remove newline from option description so it doesn't look bad on webhost (#2969)

commit d0a9d0e2d1
Author: Bryce Wilson <gyroscope15@gmail.com>
Date:   Wed Mar 20 06:43:13 2024 -0600

    Pokemon Emerald: Bump required client version (#2963)

commit 94650a02de
Author: Silvris <58583688+Silvris@users.noreply.github.com>
Date:   Tue Mar 19 17:08:29 2024 -0500

    Core: implement APProcedurePatch and APTokenMixin (#2536)

    * initial work on procedure patch

    * more flexibility

    load default procedure for version 5 patches
    add args for procedure
    add default extension for tokens and bsdiff
    allow specifying additional required extensions for generation

    * pushing current changes to go fix tloz bug

    * move tokens into a separate inheritable class

    * forgot the commit to remove token from ProcedurePatch

    * further cleaning from bad commit

    * start on docstrings

    * further work on docstrings and typing

    * improve docstrings

    * fix incorrect docstring

    * cleanup

    * clean defaults and docstring

    * define interface that has only the bare minimum required
    for `Patch.create_rom_file`

    * change to dictionary.get

    * remove unnecessary if statement

    * update to explicitly check for procedure, restore compatible version and manual override

    * Update Files.py

    * remove struct uses

    * ensure returning bytes, add token type checking

    * Apply suggestions from code review

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

    * pep8

    ---------

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

* Changes pot_completed_list to a instance variable instead of global.

Changes pot_completed_list to a instance variable instead of global. The global variable was unintentional and was causing missmatch in pre_fill which would cause generation error.

* Removing deprecated options getter

* Adds back fix from main branch

Adds back fix from main branch

* Removing messenger changes that somehow got on my branch?

Removing messenger changes that somehow got on my branch?

* Removing messenger changes that are somehow on the Shivers branch

Removing messenger changes that are somehow on the Shivers branch

* Still trying to remove Messenger changes on Shivers branch

Still trying to remove Messenger changes on Shivers branch

* Review comments addressed. Early lobby access set as default.

Review comments addressed. Early lobby access set as default.

* Review comments addressed

Review comments addressed

* Review comments addressed. Option for priority locations removed.

Option to have ixupi captures a priority has been removed and can be added again if Priority Fill is changed. See Issues #3467.

* Minor Change

Minor Change

* Fixed ID 10 T Error

Fixed ID 10 T Error

* Front door option added to slot data

Front door option added to slot data

* Add missing .value on slot data

Add missing .value on slot data

* Small change to slot data

Small change to slot data

* Small change to slot data

Why didn't this change get pushed github...

* Forgot list

Forgot list

---------

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

* Bomb Rush Cyberfunk: Fix Coil quest being in glitched logic too early (#3720)

* Update Rules.py

* Update Rules.py

* Options: Always verify keys for VerifyKeys options (#3280)

* Options: Always verify keys for VerifyKeys options

* fix PlandoTexts

* use OptionError and give a slightly better error message for which option it is

* add the player name to the error

* don't create an unnecessary list

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>

* Docs: Add FFMQ French Setup Guide + Minor fixes to English Guide (#3590)

* Add docs

* Fix character

* Configuration

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* ajuster

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* inclure

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* doublon

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* remplissage

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* autre

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* pouvoir

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* mappemonde

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* apostrophes

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* virgule

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* fournir

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* apostrophes 2

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* snes9x

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* apostrophes 3

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* options

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* lien

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* de laquelle

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* Étape de génération

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* apostrophes 4

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* également

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* guillemets

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* guillemets 2

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* adresse

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* Connect

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* seed

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>

* Changer fichier yaml pour de configuration

* Fix capitalization

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* Fix capitalization 2

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* Fix typo+Add link to fr/en info page

---------

Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* Spire: Convert options, clean up random calls, and add DeathLink (#3704)

* Convert StS options

* probably a bad idea

* Update worlds/spire/Options.py

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

---------

Co-authored-by: Kono Tyran <Kono@koifysh.dev>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Core: fix missing import for `MultiWorld.link_items()` (#3731)

* Pokemon R/B: Removing Floats from NamedRange #3717

* Docs: Missed Full Accessibility mention/conversion #3734

* ChecksFinder: Refactor/Cleaning (#3725)

* Update ChecksFinder

* minor cleanup

* Check for compatible name

* Enable APWorld

* Update setup_en.md

* Update en_ChecksFinder.md

* The client is getting updated instead

* Qwint suggestions, ' -> ", streamline fill_slot_data

* Oops, too many refactors

---------

Co-authored-by: SunCat <suncat.game@ya.ru>

* OSRS: Implement New Game (#1976)

* MMBN3: Press program now has proper color index when received remotely

* Initial commit of OSRS untangled from MMBN3 branch

* Fixes some broken region connections

* Removes some locations

* Rearranges locations to fill in slots left by removed locations

* Adds starting area rando

* Moves Oak and Willow trees to resource regions

* Fixes various PEP8 violations

* Refactor of regions

* Fixes variable capture issue with region rules

* Partial completion of brutal grind logic

* Finishes can_reach_skill function

* Adds skill requirements to location rules, fixes regions rules

* Adds documentation for OSRS

* Removes match statement

* Updates Data Version to test mode to prevent item name caching

* Fixes starting spawn logic for east varrock

* Fixes river lum crossing logic to not assume you can phase across water

* Prevents equipping items when you haven't unlocked them

* Changes canoe logic to not require huge levels

* Skeletoning out some data I'll need for variable task system

* Adds csvs and parser for logic

* Adds Items parsing

* Fixes the spawning logic to not default to Chunksanity when you didn't pick it

* Begins adding generation rules for data-driven logic

* Moves region handling and location creating to different methods

* Adds logic limits to Options

* Begun the location generation has

* Randomly generates tasks for each skill until populated

* Mopping up improper names, adding custom logic, and fixes location rolling

* Drastically cleans up the location rolling loop

* Modifies generation to properly use local variables and pass unit tests

* Game is now generating, but rules don't seem to work

* Lambda capture, my old nemesis. We meet again

* Fixes issue with Corsair Cove item requirement causing logic loop

* Okay one more fix, another variable capture

* On second thought lets not have skull sceptre tasks. 'Tis a silly place

* Removes QP from item pool (they're events not items)

* Removes Stronghold floor tasks, no varbit to track them

* Loads CSV with pkutil so it can be used in apworld

* Fixes logic of skill tasks and adds QP requirements to long grinds

* Fixes pathing in pkgutil call

* Better handling for empty task categories, no longer throws errors

* Fixes order for progressive tasks, removes un-checkable spider task

* Fixes logic issues related to stew and the Blurite caves

* Fixes issues generating causing tests to sporadically fail

* Adds missing task that caused off-by-one error

* Updates to new Options API

* Updates generation to function properly with the Universal Tracker (Thanks Faris)

* Replaces runtime CSV parsing with pre-made python files generated from CSVs

* Switches to self.random and uses random.choice instead of doing it manually

* Fixes to typing, variable names, iterators, and continue conditions

* Replaces Name classes with Enums

* Fixes parse error on region special rules

* Skill requirements check now returns an accessrule instead of being one that checks options

* Updates documentation and setup guide

* Adjusts maximum numbers for combat and general tasks

* Fixes region names so dictionary lookup works for chunksanity

* Update worlds/osrs/docs/en_Old School Runescape.md

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* Update worlds/osrs/docs/en_Old School Runescape.md

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* Updates readme.md and codeowners doc

* Removes erroneous East Varrock -> Al Kharid connection

* Changes to canoe logic to account for woodcutting level options

* Fixes embarassing typo on 'Edgeville'

* Moves Logic CSVs to separate repository, addresses suggested changes on PR

* Fixes logic error in east/west lumbridge regions. Fixes incorrect List typing in main

* Removes task types with weight 0 from the list of rollable tasks

* Missed another place that the task type had to be removed if 0 weight

* Prevents adding an empty task weight if levels are too restrictive for tasks to be added

* Removes giant blank space in error message

* Adds player name to error for not having enough available tasks

---------

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* TUNIC: Fix missing traversal req #3740

* TUNIC: Sort entrances in the spoiler log (#3733)

* Sort entrances in spoiler log

* Rearrange portal list to closer match the vanilla game order, for better spoiler and because I already did this mod-side

* Add break (thanks vi)

* KH2: Update the docs to support steam in the setup guide (#3711)

* doc updates

* add steam link

* Update worlds/kh2/docs/setup_en.md

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

* Update setup_en.md

* Forgot to include these

* Consistent styling

* :)

* version 3.3.0

---------

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

* RoR2: Remove recursion from explore mode access rules (#3681)

The access rules for "<Environment name> Chest n", "<Environment name>
Shrine n" etc. locations recursively called state.can_reach() for the
n-1 location name, with the n=1 location being the only location to have
the actual access rule set.

This patch removes the recursion, instead setting the actual access rule
directly on each location, increasing the performance of checking
accessibility of n>1 locations.

Risk of Rain 2 was already quite fast to generate despite the recursion
in the access rules, but with this patch, generating a multiworld with
200 copies of the template RoR2 yaml (and progression balancing
disabled through a meta.yaml) goes from about 18s to about 6s for me.

From generating the same seed before and after this patch, the same
result is produced.

* Aquaria: Logic bug fixes (#3679)

* Fixing logic bugs

* Require energy attack in the cathedral and energy form in the body

* King Jelly can be beaten easily with only the Dual Form

* I think that I have a problem with my left and right...

* There is a monster that is blocking the path, soo need attack to pass

* The Li cage is not accessible without the Sunken city boss

* Removing useless space.

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

* Two more minors logic modification

* Adapting tests to af9b6cd

* Reformat the Region file

---------

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

* HK: add grub hunt goal (#3203)

* makes grub hunt goal option that calculates the total available grubs (including item link replacements) and requires all of them to be gathered for goal completion

* update slot data name for grub count

* add option to set number needed for grub hub

* updates to grub hunt goal based on review

* copy/paste fix

* account for 'any' goal and fix overriding non-grub goals

* making sure godhome is in logic for any and removing redundancy on completion condition

* fix typing

* i hate typing

* move to stage_pre_fill

* modify "any" goal so all goals are in logic under minimal settings

* rewrite grub counting to create lookups for grubs and groups that can be reused

* use generator instead of list comprehension

* fix whitespace merging wrong

* minor code cleanup

* DS3: Version 3.0.0 (#3128)

* Update worlds/dark_souls_3/Locations.py

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

* Fix Covetous Silver Serpent Ring location

* Update location groups

This should cover pretty much all of the seriously hidden items. It
also splits out miniboss drops, mimic drops, and hostile NPC drops.

* Remove the "Guarded by Keys" group

On reflection, I don't think this is actually that useful. It'll also
get a lot muddier once we can randomize shops and ashes become
pseudo-"keys".

* Restore Knight Slayer's Ring classification

* Support infusions/upgrades in the new DS3 mod system

* Support random starting loadouts

* Make an item's NPC status orthogonal to its category

* Track location groups with flags

* Track Archipelago/Offline mismatches on the server

Also fix a few incorrect item names.

* Add additional locations that are now randomizable

* Don't put soul and multiple items in shops

* Add an option to enable whether NG+ items/locations are included

* Clean up useful item categorization

There are so many weapons in the game now, it doesn't make sense to
treat them all as useful

* Add more variety to filler items

* Iron out a few bugs and incompatibilities

* Fix more silly bugs

* Get tests passing

* Update options to cover new item types

Also recategorize some items.

* Verify the default values of `Option`s.

Since `Option.verify()` can handle normalization of option names, this allows options  to define defaults which rely on that normalization. For example, it allows a world to exclude certain locations by default.

This also makes it easier to catch errors if a world author accidentally sets an invalid default.

* Make a few more improvements and fixes

* Randomize Path of the Dragon

* Mark items that unlock checks as useful

These items all unlock missable checks, but they're still good to ahve in the game for variety's sake.

* Guarantee more NPC quests are completable

* Fix a syntax error

* Fix rule definition

* Support enemy randomization

* Support online Yhorm randomization

* Remove a completed TODO

* Fix tests

* Fix force_unique

* Add an option to smooth out upgrade item progression

* Add helpers for setting location/entrance rules

* Support smoother soul item progression

* Fill extra smoothing items into conditional locations as well as other worlds

* Add health item smoothing

* Handle infusions at item generation time

* Handle item upgrades at genreation time

* Fix Grave Warden's Ashes

* Don't overwrite old rules

* Randomize items based on spheres instead of DS3 locations

* Add a smoothing option for weapon upgrades

* Add rules for crow trades

* Small fixes

* Fix a few more bugs

* Fix more bugs

* Try to prevent Path of the Dragon from going somewhere it doesn't work

* Add the ability to provide enemy presets

* Various fixes and features

* Bug fixes

* Better Coiled Sword placement

* Structure DarkSouls3Location more like DarkSouls3Item

* Add events to make DS3's spheres more even

* Restructure locations to work like items do now

* Add rules for more missable locations

* Don't add two Storm Rulers

* Place Hawk Ring in Farron Keep

* Mark the Grass Crest Shield as useful

* Mark new progression items

* Fix a bug

* Support newer better Path of the Dragon code

* Don't lock the player out of Coiled Sword

* Don't create events for missable locations

* Don't throw strings

* Don't smooth event items

* Properly categorize Butcher Knife

* Be more careful about placing Yhorm in low-randomization scenarios

* Don't try to smooth DLC items with DLC disabled

* Fix another Yhorm bug

* Fix upgrade/infusion logic

* Remove the PoolType option

This distinction is no longer meaningful now that every location in
the game of each type is randomized

* Categorize HWL: Red Eye Orb as an NPC location

* Don't place Storm Ruler on CA: Coiled Sword

* Define flatten() locally to make this APWorld capable

* Fix some more Leonhard weirdness

* Fix unique item randomization

* Don't double Twin Dragon Greatshield

* Remove debugging print

* Don't add double Storm Ruler

Also remove now-redundant item sorting by category in create_items.

* Don't add double Storm Ruler

Also remove now-redundant item sorting by category in create_items.

* Add a missing dlc_enabled check

* Use nicer options syntax

* Bump data_version

* Mention where Yhorm is in which world

* Better handle excluded events

* Add a newline to Yhorm location

* Better way of handling excluded unradomized progression locations

* Fix a squidge of nondeterminism

* Only smooth items from this world

* Don't smooth progression weapons

* Remove a location that doesn't actually exist in-game

* Classify Power Within as useful

* Clarify location names

* Fix location requirements

* Clean up randomization options

* Properly name Coiled Sword location

* Add an option for configuring how missable items are handled

* Fix some bugs from location name updates

* Fix location guide link

* Fix a couple locations that were busted offline

* Update detailed location descriptions

* Fix some bugs when generating for a multiworld

* Inject Large Leather Shield

* Fix a few location issues

* Don't allow progression_skip_balancing for unnecessary locs

* Update some location info

* Don't uniquify the wrong items

* Fix some more location issues

* More location fixes

* Use hyphens instead of parens for location descriptions

* Update and fix more locations

* Fix Soul of Cinder boss name

* Fix some logic issues

* Add item groups and document item/location groups

* Fix the display name for "Impatient Mimics"

* Properly handle Transposing Kiln and Pyromancer's Flame

* Testing

* Some fixes to NPC quests, late basin, and transposing kiln

* Improve a couple location names

* Split out and improve missable NPC item logic

* Don't allow crow trades to have foreign items

* Fix a variable capture bug

* Make sure early items are accessible early even with early Castle

* Mark ID giant slave drops as missable

* Make sure late basin means that early items aren't behind it

* Make is_location_available explicitly private

* Add an _add_item_rule utility that checks availability

* Clear excluded items if excluded_locations == "unnecessary"

* Don't allow upgrades/infusions in crow trades

* Fix the documentation for deprecated options

* Create events for all excluded locations

This allows `can_reach` logic to work even if the locations are
randomized.

* Fix up Patches' and Siegward's logic based on some manual testing

* Factor out more sub-methods for setting location rules

* Oops, left these in

* Fixing name

* Left that in too

* Changing to NamedRange to support special_range_names

* Alphabetizing

* Don't call _is_location_available on foreign locations

* Add missing Leonhard items

* Changing late basin to have a post-small-doll option

* Update basin option, add logic for some of Leonhard Hawkwood and Orbeck

* Simplifying an option, fixing a copy-paste error

* Removing trailing whitespace

* Changing lost items to go into start inventory

* Revert Basin changes

* Oops

* Update Options.py

* Reverting small doll changes

* Farron Keep boss requirement logic

* Add Scroll for late_dlc

* Fixing excluded unnecessary locations

* Adding Priestess Ring as being after UG boss

* Removing missable from Corvian Titanite Slab

* Adding KFF Yhorm boss locks

* Screams about Creighton

* Elite Knight Set isn't permanently missable

* Adding Kiln requirement to KFF

* fixing valid_keys and item groups

* Fixing an option-checker

* Throwing unplaceable Storm Ruler into start inventory

* Update locations

* Refactor item injection

* Update setup doc

* Small fixes

* Fix another location name

* Fix injection calculation

* Inject guaranteed items along with progression items

* Mark boss souls as required for access to regions

This allows us to set quest requirements for boss souls and have them
automatically propagated to regions, means we need less machinery for
Yhorm bosses, and allows us to get rid of a few region-transition
events.

* Make sure Sirris's quest can be completed before Pontiff

* Removing unused list

* Changing dict to list

* Removing unused test

* Update __init__.py

* self.multiworld.random -> self.random (#9)

* Fix some miscellaneous location issues

* Rewrite the DS3 intro page/FAQ

* Removing modifying the itempool after fill (#7)

Co-authored-by: Natalie Weizenbaum <nweiz@google.com>

* Small fixes to the setup guide (#10)

Small fixes, adding an example for connecting

* Expanded Late Basin of Vows and Late DLC (#6)

* Add proper requirements for CD: Black Eye Orb

* Fix Aldrich's name

* Document the differences with the 2.x.x branch

* Don't crash if there are more items than locations in smoothing

* Apply suggestions from code review

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* Code review

* Fix _replace_with_filler

* Don't use the shared flatten function in SM

* Track local items separately rather than iterating the multiworld

* Various formatting/docs changes suggested by PyCharm (#12)

* Drop deprecated options

* Rename "offline randomizer" to "static randomizer" which is clearer

* Move `enable_*_locations` under removed options.

* Avoid excluded locations for locally-filled items

* Adding Removed options to error (#14)

* Changes for WebHost options display and the options overhaul

* unpack iterators in item list (#13)

* Allow worlds to add options to prebuilt groups

Previously, this crashed because `typing.NamedTuple` fields such as
`group.name` aren't assignable. Now it will only fail for group names
that are actually incorrectly cased, and will fail with a better error
message.

* Style changes, rename exclude behavior options, remove guaranteed items option

* Spacing/Formatting (#18)

* Various Fixes (#19)

* Universally Track Yhorm (#20)

* Account for excluded and missable

* These are behaviors now

* This is singular, apparently

* Oops

* Fleshing out the priority process

* Missable Titanite Lizards and excluded locations (#22)

* Small style/efficiency changes

* Final passthrough fixes (#24)

* Use rich option formatting

* Make the behavior option values actual behaviors (#25)

* Use !=

* Remove unused flatten utility

* Some changes from review (#28)

* Fixing determinism and making smooth faster (#29)

* Style change

* PyCharm and Mypy fixes (#26)

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

* Change yhorm default (#30)

* Add indirect condition (#27)

* Update worlds/dark_souls_3/docs/locations_en.md

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* Ship all item IDs to the client

This avoids issues where items might get skipped if, for instance,
they're only in the starting inventory.

* Make sure to send AP IDs for infused/upgraded weapons

* Make `RandomEnemyPresetOption` compatible with ArchipelagoMW/Archipelago#3280 (#31)

* Fix cast

* More typing and small fixes (#32)

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Exempt-Medic <ExemptMedic@Gmail.com>
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>

* Core: Check parent_region.can_reach first in Location.can_reach (#3724)

* Core: Check parent_region.can_reach first in Location.can_reach

The comment about self.access_rule computing faster on average appears
to no longer be correct with the current caching system for region
accessibility, resulting in self.parent_region.can_reach computing
faster on average.

Generation of template yamls for each game that does not require a rom
to generate, generated with `python -O .\Generate.py --seed 1`
(all durations averaged over at 4 or 5 generations):

Full generation with `spoiler: 1` and no progression balancing:
89.9s -> 72.6s
Only output from above case:
2.6s -> 2.2s

Full generation with `spoiler: 3` and no progression balancing:
769.9s -> 627.1s
Only playthrough calculation + paths from above case:
680.5s -> 555.3s

Full generation with `spoiler: 1` with default progression balancing:
123.5s -> 98.3s
Only progression balancing from above case:
11.3s -> 9.6s

* Update BaseClasses.py

* Update BaseClasses.py

* Update BaseClasses.py

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>

* Core: Speed up CollectionState.copy() using built-in copy methods (#3678)

All the types being copied are built-in types with their own `copy()`
methods, so using the `copy` module was a bit overkill and also slower.

This patch replaces the use of the `copy` module in
`CollectionState.copy()` with using the built-in `.copy()` methods.

The copying of `reachable_regions` and `blocked_connections` was also
iterating the keys of each dictionary and then looking up the value in
the dictionary for that key. It is faster, and I think more readable, to
iterate the dictionary's `.items()` instead.

For me, when generating a multiworld including the template yaml of
every world with `python -O .\Generate.py --skip_output`, this patch
saves about 2.1s. The overall generation duration for these yamls varies
quite a lot, but averages around 160s for me, so on average this patch
reduced overall generation duration (excluding output duration) by
around 1.3%.

Timing comparisons were made by calling time.perf_counter() at the start
and end of `CollectionState.copy()`'s body, and summing the differences
between the starts and ends of the method body into a global variable
that was printed at the end of generation.

Additional timing comparisons were made, using the `timeit` module, of
the individual function calls or dictionary comprehensions used to
perform the copying.

The main performance cost was `copy.deepcopy()`, which gets slow as the
number of keys multiplied by the number of values within the
sets/Counters gets large, e.g., to deepcopy a `dict[int, Counter[str]]`
with 100 keys and where each Counter contains 100 keys was 30x slower
than most other tested copying methods. Increasing the number of dict
keys or Counter keys only makes it slower.

* HK: fix iterating all worlds instead of only HK worlds in stage_pre_fill (#3750)

Would cause generation to fail when generating with HK and another game.

Mistake in 6803c373e5.

* DOOM, DOOM II: Update steam URLs (#3746)

* TLOZ: world: multiworld (#3752)

* SoE: fix determinism (#3745)

Fixes randomly placed ingredients not being deterministic (depending on settings)
and in turn also fixes logic not being deterministic if they get replaced by fragments.

* Core: fix invalid __package__ of zipped worlds (#3686)

* fix invalid package fix

* add comment describing fix

* Clique: Update to new options API (#3759)

* Timespinner: Fix eels check logic #3777

* TUNIC: Add note to Universal Tracker stuff #3772

* Core: change start inventory from pool to warn when nothing to remove (#3158)

* makes start inventory from pool warn and fixes the itempool to match when it can not find a matching item to remove

* calc the difference correctly

* save new filler and non-removed items differently so we don't remove existing items at random

* Undertale: Fix slot_data and options.as_dict() (#3774)

* Undertale: Fixing slot_data

* Booleans were difficult

* Core: Error on empty options.as_dict (#3773)

* Error on empty options.as_dict

* ValueError instead

* Apply suggestions from code review

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

---------

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>

* Core: Remove broken unused code from Options.py (#3781)

"Unused" is a baseless assertion, but this code path has been crashing on the first statement for 6 months and noone's complained

* Core: Two Small Fixes (#3782)

* Core: recontextualize `CollectionState.collect` (#3723)

* Core: renamed `CollectionState.collect` arg from `event` to `prevent_sweep` and remove forced collection

* Update TestDungeon.py

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>

* Core: dump all item placements for generation failures. (#3237)

* Core: dump all item placements for generation failures

* pass the multiworld from remaining fill

* change how the args get handled to fix formatting

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>

* Tests: fix the all games multiworld test (#3788)

* TUNIC: Swap from multiworld.get to world.get for applicable things (#3789)

* Swap from multiworld.get to world.get for applicable things

* Why was this even here in the first place?

* I have no idea (#3791)

* TUNIC: Add off and on aliases for the Entrance Rando option #3794

* Stardew Valley: Add Quality Bobber in the logic rules for fish quality gold and above #3792

* Core: Require excluded locations to be reachable with full/locations accessibility (#3802)

* Make excludeds reachable

* Update all_state tests

* Lingo: Fixed Initiated-side Eight Door not opening (#3793)

* TUNIC: Give the fox a gun (in logic) (very small PR) (#3790)

* Add bomb wall logic

* Remove option call from can_shop

* Gun for the envoy blocking Quarry

* has_sword -> can_shop on cube cave entrance region

* TLOZ: Fix non-deterministic item pool generation (#3779)

* TLOZ: Fix non-deterministic item pool generation

The way the item pool was constructed involved iterating unions of sets.
Sets are unordered, so the order of iteration of these combined sets
would be non-deterministic, resulting in the items in the item pool
being generated in a different order with the same seed.

Rather than creating unions of sets at all, the original code has been
replaced with using Counter objects. As a dict subclass, Counter
maintains insertion order, and its update() method makes it simple to
combine the separate item dictionaries into a single dictionary with the
total count of each item across each of the separate item dictionaries.

Fixes #3664 - After investigating more deeply, the only differences I
could find between generations of the same seed was the order of items
created by TLOZ, so this patch appears to fix the non-deterministic
generation issue. I did manage to reproduce the non-deterministic
behaviour with just TLOZ in the end, but it was very rare. I'm not
entirely sure why generating with SMZ3 specifically would cause the
non-deterministic behaviour in TLOZ to be frequently present, whereas
generating with other games or multiple TLOZ yamls would not.

* Change import order

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>

* Docs: Update 'tag' documentation (#3632)

* Add tag docs for HintGame

* Apply suggestions from code review

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

* Make Tracker/TextOnly consistent with previous commit

* Apply suggestion

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

* fix spacing

* Apply suggestion

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

* apply suggestion correcting footnotes

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

---------

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

* [OSRS] Fixes Incorrect filler item names causing failures on tests. (#3768)

* Updates filler item names to match the actual item names

* Adds more descriptive error message in case this error comes back

* Properly raises exception instead of just text

* Replaces exception with assert

* Fix !remaining for cross-world items (#3732)

* Fix !remaining for other worlds

* Typing fixes for the previous change

* Update LocationStore test to match what get_remaining now returns

* Core: early_local != local_early #3780

* Pokemon Emerald: Ensure dig tutor is always usable (#3660)

* Pokemon Emerald: Ensure dig tutor is always usable

* Pokemon Emerald: Clarify comment

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

---------

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

* Core: type for `CommonContext.ui` (#3796)

* Core: type for `CommonContext.ui`

* use `Optional`

* VVVVVV: Make unnecessary Trinkets filler (#3806)

* Make unnecessary trinkets filler

* Proper syntax

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

---------

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

* Kingdom Hearts: Implement New Game (#3201)

* Added Final Ansem Goal

* Update __init__.py

* Update Rules.py

* New EotW logic

* Update __init__.py

* Update __init__.py

* Update Items.py

* Update Rules.py

* Rename Location to be more meaningful, logic fixes

* Removed Aerith locations

* Change to allow randomized keyblade stats

* Fixed incorrect option description.  Fixed victory locations for alternative win condition settings

* Commit

* Lots of changes

* Fixes

* Fixes

* Update Rules.py

* Update Rules.py

* Update Rules.py

* Update Rules.py

* Fixes

* Update Rules.py

* Update Rules.py

* Update Options.py

* Old Book is not required

* Added Jungle Slider

* Add Cid Check

* Add Wonderland Book Check

* Add OC Green Trinity

* Add Inferno Band Event

* Add Kurt Zisa Zantetsuken and Unknown EXP Necklace checks

* Update Locations.py

* Fix Final Ansem Goal

* Update __init__.py

* Update __init__.py

* Add options to exclude super bosses and 100 acre wood

* Fix puppies trp, remove cid check

* Fix 100 Acre Wood Option

* Material to Empty Bottle

* Fixed rules, location names, etc

* Fix super bosses

* Add item + location groups, level sanity

* Fix location and item group names

* Add Bad Starting Weapons Option

* Logic Error for 100 Acre Wood

* Update Rules.py

* Update __init__.py

* Fixes related to randomized keyblade stats and super bosses

* Credits and Fixes

* Logic fixes, location name group changes

* Update Options.py

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/docs/kh1_en.md

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

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update .gitignore

* Update CODEOWNERS

* Update docs/CODEOWNERS

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

* Fixed Atlantica item group name

* Update CODEOWNERS

* Update Client.py

* Update Items.py

* Update __init__.py

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

* Update Rules.py

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

* Update Rules.py

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

* Update Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Fixed report group name

* Fixes for PR

* Update Options.py

* Push changes for making the Final Rest Door appear, few option fixes

* Update Rules.py

* Website formatting, 0 min for reports, option description typo

* Create KH1Client.py

* Update worlds/kh1/docs/kh1_en.md

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

* Update Options.py

* Update Options.py

* Update Rules.py

* Update Rules.py

* Update Rules.py

* Add Donald and Goofy Death Link

* Add fight logic for optional bosses

* Update __init__.py

* Update Options.py

* Update worlds/kh1/Options.py

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

* Update Client.py

* Update kh1_en.md

* Update __init__.py

* Cleaning up for PR

* Update Client.py

* Added event locations for vanilla items

* Add proper location groups and auto hint synth shop items when entering

* so many changes

* Update Rules.py

* fixed oathkeeper and crabclaw logic

* Update Rules.py

* Update Rules.py

* Update Rules.py

* Update Rules.py

* Update en_Kingdom Hearts.md

* Update en_Kingdom Hearts.md

* fixing text

* Update kh1_en.md

* Addition of new key items

* Update Regions.py

* Push for start item from pool test

* Update worlds/kh1/Options.py

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

* Document update

* Update Rules.py

* Added starting world range and final rest goal option

* Update kh1_en.md

* Update en_Kingdom Hearts.md

* Update __init__.py

* Update __init__.py

* Clean up options descriptions

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Client.py

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

* Fix grammar in document

* Update __init__.py

* Update worlds/kh1/__init__.py

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

* Removed return type

* Update __init__.py

* Update __init__.py

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update __init__.py

* Fix missing i replacement, rework set rules to use "self" instead of a million arguments

* Update KH1Client.py

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

* Reformat rules, fix bug with exp mult, add to readme

* Clean up regions, fix client

* Fix item send prompt

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update worlds/kh1/docs/kh1_en.md

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

* Update worlds/kh1/docs/kh1_en.md

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

* Update worlds/kh1/docs/kh1_en.md

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

* Update worlds/kh1/docs/kh1_en.md

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

* Update worlds/kh1/test/test_goal.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Items.py

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

* Update worlds/kh1/Locations.py

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

* Update worlds/kh1/Regions.py

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

* Update worlds/kh1/Locations.py

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

* Update worlds/kh1/Locations.py

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

* Update worlds/kh1/Items.py

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

* Update worlds/kh1/Regions.py

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

* Update worlds/kh1/Regions.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Fix so many suggestions

* removed junk in missable locations option

* Update __init__.py

* Change credits order

* Update en_Kingdom Hearts.md

* Standardize punctuation

* Update en_Kingdom Hearts.md

* Update en_Kingdom Hearts.md

* Update Regions.py

* Removed "disclude" options in generation fillers

* Update Rules.py

* Update __init__.py

* Fix cemetery typo

* Update worlds/kh1/Options.py

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

* Add option groups and option presets

* Update worlds/kh1/__init__.py

That's a good idea!

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Presets.py

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

* fixed HB rule and formatting on a line in Items.py

* Fix logic bug with Geppetto's House postcard

* Update Rules.py

* Update Options.py

* Update __init__.py

* Update __init__.py

* Huge under-the-hood update for PR

* More updates for PR

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/Rules.py

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

* Update __init__.py

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Core: Fix incorrect default state checked in MultiWorld.can_beat_game (#3813)

`MultiWorld.can_beat_game()` with no arguments would initially check if
`self.state` is beatable, but then would create an empty state,
`state = CollectionState(self)`, to sweep spheres from to determine if
the game is beatable. The issue was that `self.state` and the new empty
state could be different.

Currently, it seems that everywhere in Archipelago's codebase that calls
`MultiWorld.can_beat_game()` with no arguments or `starting_state=None`
has a `self.state` that only contains precollected items, so the new
empty state happens to result in an equivalent state, but this should
not be relied upon to always be the case.

This patch changes `can_beat_game()` to initially check if the new empty
state is beatable instead of `self.state`.

This appears to be a bug introduced way back in 27b6dd8bd7

Fixes #3742

* The Witness: Fix Tunnels Theater Flower EP Access Logic + Add Unit Test for it (and Expert PP2) (#3807)

* Tunnels Theater Flowers fix + Flowers&PP2 Unit Tests

* copypaste

* Can just do it like this

* This is even better probably

* Also do some cleanup :3

* God damnit

* Docs: `NetworkItem.player` (#3811)

* Docs: `NetworkItem.player`

In many contexts, it's difficult to tell whether this is the sending player or the receiving player.

* correct player info

* Update NetUtils.py

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

---------

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

* Minecraft: Update to new options system. (#3765)

* Move to new options system.
switch to using self.random
reformat rules file.

* further reformats

* fix tests to use new options system.

* fix slot data to not use self.multiworld

* I hate python

* new starting_items docstring to prepare for 1.20.5+ item components.
fix invalid json being output to starting_items

* more typing fixes.

* stupid quotes around type declarations

* removed unused variable in ItemPool.py
change null check in Structures.py

* update rules "self" variable to a "world: MinecraftWorld" variable

* get key, and not value for required bosses.

* The Witness: Panel Hunt Mode (#3265)

* Add panel hunt options

* Make sure all panels are either solvable or disabled in panel hunt

* Pick huntable panels

* Discards in disable non randomized

* Set up panel hunt requirement

* Panel hunt functional

* Make it so an event can have multiple names

* Panel hunt with events

* Add hunt entities to slot data

* ruff

* add to hint data, no client sneding yet

* encode panel hunt amount in compact hint data

* Remove print statement

* my b

* consistent

* meh

* additions for lcient

* Nah

* Victory panels ineligible for panel hunt

* Panel Hunt Postgame option

* cleanup

* Add data generation file

* pull out set

* always disable gate ep in panel hunt

* Disallow certain challenge panels from being panel hunt panels

* Make panelhuntpostgame its own function, so it can be called even if normal postgame is enabled

* disallow PP resets from panel hunt

* Disable challenge timer and elevetor start respectively in disable hunt postgame

* Fix panelhunt postgame

* lol

* When you test that the bug is fixed but not that the non-bug is not unfixed

* Prevent Obelisks from being panel hunt panels

* Make picking panels for panel hunt a bit more sophisticated, if less random

* Better function maybe ig

* Ok maybe that was a bit too much

* Give advanced players some control over panel hunt

* lint

* correct the logic for amount to pick

* decided the jingle thing was dumb, I'll figure sth out client side. Same area discouragement is now a configurable factor, and the logic has been significantly rewritten

* comment

* Make the option visible

* Safety

* Change assert slightly

* We do a little logging

* number tweak & we do a lil logging

* we do a little more logging

* Ruff

* Panel Hunt Option Group

* Idk how that got here

* Update worlds/witness/options.py

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

* Update worlds/witness/__init__.py

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

* remove merge error

* Update worlds/witness/player_logic.py

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

* True

* Don't have underwater sliding bridge when you have above water sliding bridge

* These are not actually connected lol

* get rid of unnecessary variable

* Refactor compact hint function again

* lint

* Pull out Entity Hunt Picking into its own class, split it into many functions. Kept a lot of the comments tho

* forgot to actually add the new file

* some more refactoring & docstrings

* consistent naming

* flip elif change

* Comment about naming

* Make static eligible panels a constant I can refer back to

* slight formatting change

* pull out options-based eligibility into its own function

* better text and stuff

* lint

* this is not necessary

* capitalisation

* Fix same area discouragement 0

* Simplify data file generation

* Simplify data file generation

* prevent div 0

* Add Vault Boxes -> Vault Panels to replacements

* Update options.py

* Update worlds/witness/entity_hunt.py

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

* Update entity_hunt.py

* Fix some events not working

* assert

* remove now unused function

* lint

* Lasers Activate, Lasers don't Solve

* lint

* oops

* mypy

* lint

* Add simple panel hunt unit test

* Add Panel Hunt Tests

* Add more Panel Hunt Tests

* Disallow Box Short for normal panel hunt

---------

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

* The Witness: Add "vague" hints making use of other games' region names and location groups (#2921)

* Vague hints work! But, the client will probably reveal some of the info through scouts atm

* Fall back on Everywhere if necessary

* Some of these failsafes are not necessary now

* Limit region size to 100 as well

* Actually... like this.

* Nutmeg

* Lol

* -1 for own player but don't scout

* Still make always/priority ITEM hints

* fix

* uwu notices your bug

* The hints should, like, actually work, you know?

* Make it a Toggle

* Update worlds/witness/hints.py

Co-authored-by: Bryce Wilson <gyroscope15@gmail.com>

* Update worlds/witness/hints.py

Co-authored-by: Bryce Wilson <gyroscope15@gmail.com>

* Make some suggested changes

* Make that ungodly equation a bit clearer in terms of formatting

* make that not sorted

* Add a warning about the feature in the option tooltip

* Make using region names experimental

* reword option tooltip

* Note about singleplayer

* Slight rewording again

* Reorder the order of priority a bit

* this condition is unnecessary now

* comment

* No wait the order has to be like this

* Okay now I think it's correct

* Another comment

* Align option tooltip with new behavior

* slight rewording again

* reword reword reword reword

* -

* ethics

* Update worlds/witness/options.py

Co-authored-by: Bryce Wilson <gyroscope15@gmail.com>

* Rename and slight behavior change for local hints

* I think I overengineered this system before. Make it more consistent and clear now

* oops I used checks by accident

* oops

* OMEGA OOPS

* Accidentally commited a print statemetn

* Vi don't commit nonsense challenge difficulty impossible

* This isn't always true but it's good enough

* Update options.py

* Update worlds/witness/options.py

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

* Scipio :3

* switch to is_event instead of checking against location.address

* oop

* Update test_roll_other_options.py

* Fix that unit test problem lol

* Oh is this not fixed in the apworld?

---------

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

* Mega Man 2: Implement New Game (#3256)

* initial (broken) commit

* small work on init

* Update Items.py

* beginning work, some rom patches

* commit progress from bh branch

* deathlink, fix soft-reset kill, e-tank loss

* begin work on targeting new bhclient

* write font

* definitely didn't forget to add the other two hashes no

* update to modern options, begin colors

* fix 6th letter bug

* palette shuffle + logic rewrite

* fix a bunch of pointers

* fix color changes, deathlink, and add wily 5 req

* adjust weapon weakness generation

* Update Rules.py

* attempt wily 5 softlock fix

* add explicit test for rbm weaknesses

* fix difficulty and hard reset

* fix connect deathlink and off by one item color

* fix atomic fire again

* de-jank deathlink

* rewrite wily5 rule

* fix rare solo-gen fill issue, hopefully

* Update Client.py

* fix wily 5 requirements

* undo fill hook

* fix picopico-kun rules

* for real this time

* update minimum damage requirement

* begin move to procedure patch

* finish move to APPP, allow rando boobeam, color updates

* fix color bug, UT support?

* what do you mean I forgot the procedure

* fix UT?

* plando weakness and fixes

* sfx when item received, more time stopper edge cases

* Update test_weakness.py

* fix rules and color bug

* fix color bug, support reduced flashing

* major world overhaul

* Update Locations.py

* fix first found bugs

* mypy cleanup

* headerless roms

* Update Rom.py

* further cleanup

* work on energylink

* el fixes

* update to energylink 2.0 packet

* energylink balancing

* potentially break other clients, more balancing

* Update Items.py

* remove startup change from basepatch

we write that in patch, since we also need to clean the area before applying

* el balancing and feedback

* hopefully less test failures?

* implement world version check

* add weapon/health option

* Update Rom.py

* x/x2

* specials

* Update Color.py

* Update Options.py

* finally apply location groups

* bump minor version number instead

* fix duplicate stage sends

* validate wily 5, tests

* see if renaming fixes

* add shuffled weakness

* remove passwords

* refresh rbm select, fix wily 5 validation

* forgot we can't check 0

* oops I broke the basepatch (remove failing test later)

* fix solo gen fill error?

* fix webhost patch recognition

* fix imports, basepatch

* move to flexibility metric for boss validation

* special case boobeam trap

* block strobe on stage select init

* more energylink balancing

* bump world version

* wily HP inaccurate in validation

* fix validation edge case

* save last completed wily to data storage

* mypy and pep8 cleanup

* fix file browse validation

* fix test failure, add enemy weakness

* remove test seed

* update enemy damage

* inno setup

* Update en_Mega Man 2.md

* setup guide

* Update en_Mega Man 2.md

* finish plando weakness section

* starting rbm edge case

* remove * imports

* properly wrap later weakness additions in regen playthrough

* fix import

* forgot readme

* remove time stopper special casing

since we moved to proper wily 5 validation, this special casing is no longer important

* properly type added locations

* Update CODEOWNERS

* add animation reduction

* deprioritize Time Stopper in rush checks

* special case wily phase 1

* fix key error

* forgot the test

* music and general cleanup

* the great rename

* fix import

* thanks pycharm

* reorder palette shuffle

* account for alien on shuffled weakness

* apply suggestions

* fix seedbleed

* fix invalid buster passthrough

* fix weakness landing beneath required amount

* fix failsafe

* finish music

* fix Time Stopper on Flash/Alien

* asar pls

* Apply suggestions from code review

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

* world helpers

* init cleanup

* apostrophes

* clearer wording

* mypy and cleanup

* options doc cleanup

* Update rom.py

* rules cleanup

* Update __init__.py

* Update __init__.py

* move to defaultdict

* cleanup world helpers

* Update __init__.py

* remove unnecessary line from fill hook

* forgot the other one

* apply code review

* remove collect

* Update rules.py

* forgot another

---------

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

* Blasphemous: Total overhaul (#3355)

* Blasphemous: WIP overhaul

* Entrance rule mistake

* stuff

* Getting closer

* Real?? Maybe??

* Don't fail me now 🙏

* Add starting location tests

* More tests (it still doesn't work actually 😔)

* REAL

* Add unreachable regions to test_reachability.py

* PR ready

- Remove unused functions from init
- Use group exclusive functions in rules
- Style changes

* Bump required client version

* Clean up unused imports

* Change slot data

* Review fixes

- Prevent strength calculations from including excess items
- Add new lines to ends of files
- Fix missed deprecated option and random usage in init

* Update option docstrings, add groups

* Add preprocessor files

* Update option docstrings again actually

* Update player strength calculation

* Rename group methods

* Fix missing logic for RESCUED_CHERUB_06

* Register indirect conditions

* Register indirect conditions (part 2)

* Update extracted logic, change slot data key

* Add region to excluded list

* A capital letter

* Use camelCase keys in preprocessor

* Write some of new setup guide

* Remove indents before list points

* Change locationinfo to list of dictonaries

* Finish docs, update extractor config and data

* Mark region_data.py as generated

* Suggested changes

* More suggested changes

* Suggested changes again

- Use OptionError
- Create list of disabled locations before looping
- Check if options are equal to str instead of int
- Clean up start location override
- Reword some of setup guide
- Organize location list
- Remove unnecessary escaped quotes from option docstrings
- Add world type to test base

* C# moment

* Requested changes

* Update .gitattributes

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>

* MM2: fix Wily 5 Time Stopper rule (#3824)

* fix time stopper rule

* that was the entirely wrong rule actually

* YachtDice: implement new game (#3482)

* Add the yacht dice (from other git) world to the yacht dice fork

* Update .gitignore

* Removed zillion because it doesn't work

* Update .gitignore

* added zillion again...

* Now you can have 0 extra fragments

* Added alt categories, also options

* Added item categories

* Extra categories are now working! 🐶

* changed options and added exceptions

* Testing if I change the generate.py

* Revert "Testing if I change the generate.py"

This reverts commit 7c2b3df617.

* ignore gitignore

* Delete .gitignore

* Update .gitignore

* Update .gitignore

* Update logic, added multiplicative categories

* Changed difficulties

* Update offline mode so that it works again

* Adjusted difficulty

* New version of the apworld, with 1000 as final score, always

Will still need to check difficulty and weights of adding items.
Website is not ready yet, so this version is not usable yet :)

* Changed yaml and small bug fixes

Fix when goal and max are same
Options: changed chance to weight

* no changes, just whitespaces

* changed how logic works

Now you put an array of mults and the cpu gets a couple of tries

* Changed logic, tweaked a bit too

* Preparation for 2.0

* logic tweak

* Logic for alt categories properly now

* Update setup_en.md

* Update en_YachtDice.md

* Improve performance of add_distributions

* Formatting style

* restore gitignore to APMW

* Tweaked generation parameters and methods

* Version 2.0.3

manual input option
max score in logic always 2.0.3
faster gen

* Comments and editing

* Renamed setup guide

* Improved create_items code

* init of locations: remove self.event line

* Moved setting early items to generate_early

* Add my name to CODEOWNERS

* Added Yacht Dice to the readme in list of games

* Improve performance of Yacht Dice

* newline

* Improve typing

* This is actually just slower lol

* Update worlds/yachtdice/Items.py

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

* Apply suggestions from code review

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

* Update Options.py

* Styling

* finished text whichstory option

* removed roll and rollfragments; not used

* import; worlds not world :)

* Option groups!

* ruff styling, fix

* ruff format styling!

* styling and capitalization of options

* small comment

* Cleaned up the "state_is_a_list" a little bit

* RUFF 🐶

* Changed filling the itempool for efficiency

Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?).
And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points.

* 🐶

* Removed plando "fix"

* Changed indent of score multiplier

* faster location function

* Comments to docstrings

* fixed making location closest to goal_score be goal_score

* options format

* iterate keys and values of a dict together

* small optimization ListState

* faster collection of categories

* return arguments instead of making a list (will 🐶 later)

* Instead of turning it into a tuple, you can just make a tuple literal

* remove .keys()

* change .random and used enumerate

* some readability improvements

* Remove location "0", we don't use that one

* Remove lookup_id_to_name entirely

I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id.

* .append instead of += for single items, percentile function changed

Also an extra comment for location ids.

* remove ) too many

* Removed sorted from category list

* Hash categories (which makes it slower :( )

Maybe I messed up or misunderstood...
I'll revert this right away since it is 2x slower, probably because of sorted instead of sort?

* Revert "Hash categories (which makes it slower :( )"

This reverts commit 34f2c1aed8.

* temporary push: 40% faster generation test

Small changes in logic make the generation 40% faster.
I'll have to think about how big the changes are. I suspect they are rather limited.
If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here.

* Add Points item category

* Reverse changes of bad idea :)

* ruff 🐶

* Use numpy and pmf function to speed up gen

Numpy has a built-in way to sum probability mass functions (pmf).
This shaves of 60% of the generation time :D

* Revert "Use numpy and pmf function to speed up gen"

This reverts commit 9290191cb3.

* Step inbetween to change the weights

* Changed the weights to make it faster

135 -> 81 seconds on 100 random yamls

* Adjusted max_dist, split dice_simulation function

* Removed nonlocal and pass arguments instead

* Change "weight-lists" to Dict[str, float]

* Removed the return from ini_locations.

Also added explanations to cat_weights

* Choice options; dont'use .value (will ruff later)

* Only put important options in slotdata

* 🐶

* Add Dict import

* Split the cache per player, limit size to 400.

* 🐶

* added , because of style

* Update apworld version to 2.0.6

2.0.5 is the apworld I released on github to be tested
I never separately released 2.0.4.

* Multiple smaller code improvements

- changed names in YachtWeights so we don't need to translate them in Rules anymore
- we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore
-

* 🐶 ruff

* Mostly minimize_extra_items improvements

- Change logic, generation is now even faster (0.6s per default yaml).
- Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now:
 - you start with 2 dice and 2 rolls
 - there will be less locations/items at the start of you game

* ruff 🐶

* Removed printing options

* Reworded some option descriptions

---------

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

* Yacht Dice: setup: change release-link to latest (#3827)

On the installation page, link to the latest release, instead of the page with all releases

* ALTTP: Minor Tweaks to the Adjuster UI (#2533)

* Tweak ALTTP Adjuster padding/size to accommodate resizing

  - Set minsize so the actions buttons on bottom are always visible.
  - Added a minor amount of padding around the top level objects.
  - Increased the size of the entry fields for roms to match general
    button size.
  - Updated layout calls so vertical spacing doesn't increase
    between fields when maximizing the window
  - Added a little bit of spacing on the rom label so it more closely
    lines up with the other rom selection field

* Tweak ALTTP Adjuster padding/size to accommodate resizing

  - Set minsize so the actions buttons on bottom are always visible.
  - Added a minor amount of padding around the top level objects.
  - Increased the size of the entry fields for roms to match general
    button size.
  - Updated layout calls so vertical spacing doesn't increase
    between fields when maximizing the window
  - Added a little bit of spacing on the rom label so it more closely
    lines up with the other rom selection field

* LTTP: Fix a bug in Triforce Pieces Mode: Extra (#3784)

When triforce_pieces_mode is set to "extra", the number of Triforce pieces in the pool should be equal to the number required plus the number extra. The number available was being used in this calculation, instead of the number required.

* The Witness: Ban Excluded Panels from Panel Hunt (#3818)

* excluded panels should not be picked by panel hunt

* ban excluded panels from panel hunt

* Get rid of an unused variable

* Purge the world: multiworld evil from osrs (#3751)

* Core, some worlds: Rename sweep_for_events to sweep_for_advancements (#3571)

* Rename sweep_for_events to sweep_for_advancements

* more event->advancement renames

* oops accidentally deleted the deprecation thing in the force push

* Update TestDungeon.py

* Update BaseClasses.py

* Update BaseClasses.py

* oops

* utils.deprecate

* treble, you had no idea how right you were

* Update test_panel_hunt.py

* Update BaseClasses.py

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

---------

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

* Core: some typing and cleaning in `BaseClasses.py` (#3391)

* Core: some typing and cleaning in `BaseClasses.py`

* more backwards `__repr__`

* double-quote string

* remove some end-of-line whitespace

* Celeste 64: Typo #3840

oops

* Kingdom Hearts: Make Ceiling Division Human-Readable #3839

* The Witness: Shuffle Dog (#3425)

* Town Pet the Dog

* Add shuffle dog to options presets

* I cri evritim

* I guess it's as good a time as any

* :(

* fix the soft conflict

* add all the shuffle dog options to some of the unit tests bc why not

* Laser Panels are just 'General' now, I'm pretty sure

* Could I really call it allsanity?

* The Witness: Switch to world.player_name (#3693)

* lint

* player_name

* oops lmao

* shorten

* Launcher: Update message that displays when installing a custom apworld for a game in main (#3607)

* kvui: assert kivy is not imported before kvui (#3823)

* Pokemon Emerald: Send current map to trackers (#3726)

---------

Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com>
Co-authored-by: alchav <alchav@jalchavware.com>
Co-authored-by: Phaneros <31861583+MatthewMarinets@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Remy Jette <remy@remyjette.com>
Co-authored-by: Jarno <jarnowesthof@gmail.com>
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Co-authored-by: GodlFire <46984098+GodlFire@users.noreply.github.com>
Co-authored-by: Kory Dondzila <korydondzila@gmail.com>
Co-authored-by: Trevor L <80716066+TRPG0@users.noreply.github.com>
Co-authored-by: wildham <64616385+wildham0@users.noreply.github.com>
Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Co-authored-by: Kono Tyran <Kono@koifysh.dev>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: SunCat <suncat.game@ya.ru>
Co-authored-by: digiholic <digikun@gmail.com>
Co-authored-by: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com>
Co-authored-by: Mysteryem <Mysteryem@users.noreply.github.com>
Co-authored-by: Louis M <prog@tioui.com>
Co-authored-by: qwint <qwint.42@gmail.com>
Co-authored-by: Natalie Weizenbaum <nweiz@google.com>
Co-authored-by: Exempt-Medic <ExemptMedic@Gmail.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Kaito Sinclaire <ks@rosenthalcastle.org>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
Co-authored-by: Star Rauchenberger <fefferburbia@gmail.com>
Co-authored-by: Emily <35015090+EmilyV99@users.noreply.github.com>
Co-authored-by: Bryce Wilson <gyroscope15@gmail.com>
Co-authored-by: Scrungip <95324612+Scrungip@users.noreply.github.com>
Co-authored-by: gaithern <36639398+gaithern@users.noreply.github.com>
Co-authored-by: KonoTyran <Kono.Tyran@gmail.com>
Co-authored-by: Spineraks <markvanderboor@hotmail.com>
Co-authored-by: B1t <christopher.j.wallis@gmail.com>
Co-authored-by: Kappatechy <jmdewar@shaw.ca>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: PoryGone <98504756+PoryGone@users.noreply.github.com>
2024-08-29 08:41:57 +02:00
Scipio Wright
0e55ddc7cf LADX: Filter braces out of player names for hint text (#3831)
* Filter braces out of player names for hint text

* Filter out another spot
2024-08-29 08:15:49 +02:00
Bryce Wilson
ab5b986716 Pokemon Emerald: Move magma grunt (#3836) 2024-08-29 08:14:08 +02:00
Emily
97c313c1c4 APSudoku: Update setup guide, remove extraneous options page link (#3849)
* APSudoku: Update setup guide, remove extraneous options page link

* Apply suggestions from code review

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

* Apply suggestions from code review

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

* clean up instructions

* IP -> address

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-08-29 08:12:58 +02:00
Mysteryem
701a7faa71 AHIT: Fix Time Rift - Alpine Skyline entrance logic (#3851)
The `Time Rift - Alpine Skyline` region was incorrectly accessible from
Alpine Free Roam without Hookshot Badge or Umbrella.

One of the two regions that connects to the `Time Rift - Alpine Skyline`
region is `Alpine Free Roam`. The problem here is that
`Alpine Free Roam` corresponds to the intro section of Alpine Free Roam,
but the Time Rift is actually found in-game in what equates to the
`Alpine Skyline Area` region.

The entrance connecting `Alpine Free Roam` to `Alpine Skyline Area`
(`AFR -> Alpine Skyline Area`) requires the Hookshot Badge (and Umbrella
if umbrella logic is enabled), but because the entrance to
`Time Rift - Alpine Skyline` is placed in `Alpine Free Roam` instead, it
was missing the hookshot/umbrella requirements.

The missing Hookshot Badge and Umbrella requirements have been added to
`Rules.set_rift_rules()` and `Rules.set_default_rift_rules()`.

The entrances to the `Time Rift - Curly Tail Trail` and `Time Rift - The
Twilight Bell` regions are also in the `Alpine Free Roam` region, but
the logic for both of those entrances require event items that are only
accessible from the `Alpine Skyline Area` region.
2024-08-29 08:11:42 +02:00
Mysteryem
9a4e84efdc AHIT: Fix moderate logic rules using add_rule instead of set_rule (#3850)
The moderate logic for the Mafia Town Clock Tower Chest and Top of
Ruined Tower with nothing, and for clearing Rock the Boat without Ice
Hat were mistakenly using `add_rule` instead of `set_rule`, which was
adding the condition of `and True` which had no effect.

This patch corrects these moderate logic rules to use `set_rule`
instead.
2024-08-29 08:11:02 +02:00
NewSoupVi
906b23088c The Witness: Rules Optimisation (#3617)
* Attempt at optimizing rules

* docstrings

* Python 3.8

* Lasers optimisation

* Simplify conversion code and make it even faster

* mypy

* ruff

* Neat

* Add redirect to the other two modes

* Update WitnessLogic.txt

* Update WitnessLogicExpert.txt

* Update WitnessLogicVanilla.txt

* Use NamedTuple

* Ruff

* mypy thing

* Mypy stuff

* Move Redirect Event to Desert Region so it has a better name
2024-08-28 18:31:49 +02:00
Bryce Wilson
0fb69dce33 Pokemon Emerald: Fix map update sending to all trackers (#3846) 2024-08-25 04:08:27 +02:00
Justus Lind
e99f027b42 Muse Dash: Update to 4.7.0 - Let's Rhythm Jam! (#3837)
* Update to Muse Dash 4.7.0 Muse Dash - Let's Rhythm Jam!

* Add the replaced song to the removed list.

* Oops add the other secret song to this list.

* Add trailing comma

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

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2024-08-24 18:19:42 +02:00
Aaron Wagener
dddffa1660 LTTP: fix own_dungeon setting from not being placed in the player's own world (#3816) 2024-08-24 10:54:33 +02:00
Exempt-Medic
83367c6946 ALttP: Fix accessibility (locations -> full) (#3801) 2024-08-24 10:53:56 +02:00
Silvris
0fcca25870 Core: deepcopy plando items #3841 2024-08-24 05:41:00 +02:00
Bryce Wilson
d1a7fd7da1 Pokemon Emerald: Send current map to trackers (#3726) 2024-08-24 02:51:52 +02:00
qwint
5c5f2ffc94 kvui: assert kivy is not imported before kvui (#3823) 2024-08-24 02:12:01 +02:00
Scipio Wright
6f617e302d Launcher: Update message that displays when installing a custom apworld for a game in main (#3607) 2024-08-24 02:09:50 +02:00
NewSoupVi
35c9061c9c The Witness: Switch to world.player_name (#3693)
* lint

* player_name

* oops lmao

* shorten
2024-08-24 02:08:46 +02:00
NewSoupVi
e61d521ba8 The Witness: Shuffle Dog (#3425)
* Town Pet the Dog

* Add shuffle dog to options presets

* I cri evritim

* I guess it's as good a time as any

* :(

* fix the soft conflict

* add all the shuffle dog options to some of the unit tests bc why not

* Laser Panels are just 'General' now, I'm pretty sure

* Could I really call it allsanity?
2024-08-24 02:08:04 +02:00
gaithern
6efa065867 Kingdom Hearts: Make Ceiling Division Human-Readable #3839 2024-08-24 02:06:08 +02:00
PoryGone
56dbba6a31 Celeste 64: Typo #3840
oops
2024-08-24 02:05:42 +02:00
Doug Hoskisson
43cb9611fb Core: some typing and cleaning in BaseClasses.py (#3391)
* Core: some typing and cleaning in `BaseClasses.py`

* more backwards `__repr__`

* double-quote string

* remove some end-of-line whitespace
2024-08-24 02:05:30 +02:00
NewSoupVi
64b654d42e Core, some worlds: Rename sweep_for_events to sweep_for_advancements (#3571)
* Rename sweep_for_events to sweep_for_advancements

* more event->advancement renames

* oops accidentally deleted the deprecation thing in the force push

* Update TestDungeon.py

* Update BaseClasses.py

* Update BaseClasses.py

* oops

* utils.deprecate

* treble, you had no idea how right you were

* Update test_panel_hunt.py

* Update BaseClasses.py

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

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2024-08-23 01:15:05 +02:00
NewSoupVi
74aab81f79 Purge the world: multiworld evil from osrs (#3751) 2024-08-23 00:23:22 +02:00
NewSoupVi
f390b33c17 The Witness: Ban Excluded Panels from Panel Hunt (#3818)
* excluded panels should not be picked by panel hunt

* ban excluded panels from panel hunt

* Get rid of an unused variable
2024-08-23 00:23:05 +02:00
Kappatechy
31852801c9 LTTP: Fix a bug in Triforce Pieces Mode: Extra (#3784)
When triforce_pieces_mode is set to "extra", the number of Triforce pieces in the pool should be equal to the number required plus the number extra. The number available was being used in this calculation, instead of the number required.
2024-08-22 23:35:29 +02:00
B1t
e35addf5b2 ALTTP: Minor Tweaks to the Adjuster UI (#2533)
* Tweak ALTTP Adjuster padding/size to accommodate resizing

  - Set minsize so the actions buttons on bottom are always visible.
  - Added a minor amount of padding around the top level objects.
  - Increased the size of the entry fields for roms to match general
    button size.
  - Updated layout calls so vertical spacing doesn't increase
    between fields when maximizing the window
  - Added a little bit of spacing on the rom label so it more closely
    lines up with the other rom selection field

* Tweak ALTTP Adjuster padding/size to accommodate resizing

  - Set minsize so the actions buttons on bottom are always visible.
  - Added a minor amount of padding around the top level objects.
  - Increased the size of the entry fields for roms to match general
    button size.
  - Updated layout calls so vertical spacing doesn't increase
    between fields when maximizing the window
  - Added a little bit of spacing on the rom label so it more closely
    lines up with the other rom selection field
2024-08-22 19:59:11 +02:00
Spineraks
3cdcb8c455 Yacht Dice: setup: change release-link to latest (#3827)
On the installation page, link to the latest release, instead of the page with all releases
2024-08-21 21:40:40 +02:00
Spineraks
48c6a6fb4c YachtDice: implement new game (#3482)
* Add the yacht dice (from other git) world to the yacht dice fork

* Update .gitignore

* Removed zillion because it doesn't work

* Update .gitignore

* added zillion again...

* Now you can have 0 extra fragments

* Added alt categories, also options

* Added item categories

* Extra categories are now working! 🐶

* changed options and added exceptions

* Testing if I change the generate.py

* Revert "Testing if I change the generate.py"

This reverts commit 7c2b3df617.

* ignore gitignore

* Delete .gitignore

* Update .gitignore

* Update .gitignore

* Update logic, added multiplicative categories

* Changed difficulties

* Update offline mode so that it works again

* Adjusted difficulty

* New version of the apworld, with 1000 as final score, always

Will still need to check difficulty and weights of adding items.
Website is not ready yet, so this version is not usable yet :)

* Changed yaml and small bug fixes

Fix when goal and max are same
Options: changed chance to weight

* no changes, just whitespaces

* changed how logic works

Now you put an array of mults and the cpu gets a couple of tries

* Changed logic, tweaked a bit too

* Preparation for 2.0

* logic tweak

* Logic for alt categories properly now

* Update setup_en.md

* Update en_YachtDice.md

* Improve performance of add_distributions

* Formatting style

* restore gitignore to APMW

* Tweaked generation parameters and methods

* Version 2.0.3

manual input option
max score in logic always 2.0.3
faster gen

* Comments and editing

* Renamed setup guide

* Improved create_items code

* init of locations: remove self.event line

* Moved setting early items to generate_early

* Add my name to CODEOWNERS

* Added Yacht Dice to the readme in list of games

* Improve performance of Yacht Dice

* newline

* Improve typing

* This is actually just slower lol

* Update worlds/yachtdice/Items.py

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

* Apply suggestions from code review

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

* Update Options.py

* Styling

* finished text whichstory option

* removed roll and rollfragments; not used

* import; worlds not world :)

* Option groups!

* ruff styling, fix

* ruff format styling!

* styling and capitalization of options

* small comment

* Cleaned up the "state_is_a_list" a little bit

* RUFF 🐶

* Changed filling the itempool for efficiency

Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?).
And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points.

* 🐶

* Removed plando "fix"

* Changed indent of score multiplier

* faster location function

* Comments to docstrings

* fixed making location closest to goal_score be goal_score

* options format

* iterate keys and values of a dict together

* small optimization ListState

* faster collection of categories

* return arguments instead of making a list (will 🐶 later)

* Instead of turning it into a tuple, you can just make a tuple literal

* remove .keys()

* change .random and used enumerate

* some readability improvements

* Remove location "0", we don't use that one

* Remove lookup_id_to_name entirely

I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id.

* .append instead of += for single items, percentile function changed

Also an extra comment for location ids.

* remove ) too many

* Removed sorted from category list

* Hash categories (which makes it slower :( )

Maybe I messed up or misunderstood...
I'll revert this right away since it is 2x slower, probably because of sorted instead of sort?

* Revert "Hash categories (which makes it slower :( )"

This reverts commit 34f2c1aed8.

* temporary push: 40% faster generation test

Small changes in logic make the generation 40% faster.
I'll have to think about how big the changes are. I suspect they are rather limited.
If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here.

* Add Points item category

* Reverse changes of bad idea :)

* ruff 🐶

* Use numpy and pmf function to speed up gen

Numpy has a built-in way to sum probability mass functions (pmf).
This shaves of 60% of the generation time :D

* Revert "Use numpy and pmf function to speed up gen"

This reverts commit 9290191cb3.

* Step inbetween to change the weights

* Changed the weights to make it faster

135 -> 81 seconds on 100 random yamls

* Adjusted max_dist, split dice_simulation function

* Removed nonlocal and pass arguments instead

* Change "weight-lists" to Dict[str, float]

* Removed the return from ini_locations.

Also added explanations to cat_weights

* Choice options; dont'use .value (will ruff later)

* Only put important options in slotdata

* 🐶

* Add Dict import

* Split the cache per player, limit size to 400.

* 🐶

* added , because of style

* Update apworld version to 2.0.6

2.0.5 is the apworld I released on github to be tested
I never separately released 2.0.4.

* Multiple smaller code improvements

- changed names in YachtWeights so we don't need to translate them in Rules anymore
- we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore
-

* 🐶 ruff

* Mostly minimize_extra_items improvements

- Change logic, generation is now even faster (0.6s per default yaml).
- Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now:
 - you start with 2 dice and 2 rolls
 - there will be less locations/items at the start of you game

* ruff 🐶

* Removed printing options

* Reworded some option descriptions

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-08-21 19:59:21 +02:00
Silvris
eaa8156061 MM2: fix Wily 5 Time Stopper rule (#3824)
* fix time stopper rule

* that was the entirely wrong rule actually
2024-08-21 16:20:16 +02:00
Trevor L
54a7bb5664 Blasphemous: Total overhaul (#3355)
* Blasphemous: WIP overhaul

* Entrance rule mistake

* stuff

* Getting closer

* Real?? Maybe??

* Don't fail me now 🙏

* Add starting location tests

* More tests (it still doesn't work actually 😔)

* REAL

* Add unreachable regions to test_reachability.py

* PR ready

- Remove unused functions from init
- Use group exclusive functions in rules
- Style changes

* Bump required client version

* Clean up unused imports

* Change slot data

* Review fixes

- Prevent strength calculations from including excess items
- Add new lines to ends of files
- Fix missed deprecated option and random usage in init

* Update option docstrings, add groups

* Add preprocessor files

* Update option docstrings again actually

* Update player strength calculation

* Rename group methods

* Fix missing logic for RESCUED_CHERUB_06

* Register indirect conditions

* Register indirect conditions (part 2)

* Update extracted logic, change slot data key

* Add region to excluded list

* A capital letter

* Use camelCase keys in preprocessor

* Write some of new setup guide

* Remove indents before list points

* Change locationinfo to list of dictonaries

* Finish docs, update extractor config and data

* Mark region_data.py as generated

* Suggested changes

* More suggested changes

* Suggested changes again

- Use OptionError
- Create list of disabled locations before looping
- Check if options are equal to str instead of int
- Clean up start location override
- Reword some of setup guide
- Organize location list
- Remove unnecessary escaped quotes from option docstrings
- Add world type to test base

* C# moment

* Requested changes

* Update .gitattributes

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-08-21 01:18:28 +02:00
Silvris
0e6e359747 Mega Man 2: Implement New Game (#3256)
* initial (broken) commit

* small work on init

* Update Items.py

* beginning work, some rom patches

* commit progress from bh branch

* deathlink, fix soft-reset kill, e-tank loss

* begin work on targeting new bhclient

* write font

* definitely didn't forget to add the other two hashes no

* update to modern options, begin colors

* fix 6th letter bug

* palette shuffle + logic rewrite

* fix a bunch of pointers

* fix color changes, deathlink, and add wily 5 req

* adjust weapon weakness generation

* Update Rules.py

* attempt wily 5 softlock fix

* add explicit test for rbm weaknesses

* fix difficulty and hard reset

* fix connect deathlink and off by one item color

* fix atomic fire again

* de-jank deathlink

* rewrite wily5 rule

* fix rare solo-gen fill issue, hopefully

* Update Client.py

* fix wily 5 requirements

* undo fill hook

* fix picopico-kun rules

* for real this time

* update minimum damage requirement

* begin move to procedure patch

* finish move to APPP, allow rando boobeam, color updates

* fix color bug, UT support?

* what do you mean I forgot the procedure

* fix UT?

* plando weakness and fixes

* sfx when item received, more time stopper edge cases

* Update test_weakness.py

* fix rules and color bug

* fix color bug, support reduced flashing

* major world overhaul

* Update Locations.py

* fix first found bugs

* mypy cleanup

* headerless roms

* Update Rom.py

* further cleanup

* work on energylink

* el fixes

* update to energylink 2.0 packet

* energylink balancing

* potentially break other clients, more balancing

* Update Items.py

* remove startup change from basepatch

we write that in patch, since we also need to clean the area before applying

* el balancing and feedback

* hopefully less test failures?

* implement world version check

* add weapon/health option

* Update Rom.py

* x/x2

* specials

* Update Color.py

* Update Options.py

* finally apply location groups

* bump minor version number instead

* fix duplicate stage sends

* validate wily 5, tests

* see if renaming fixes

* add shuffled weakness

* remove passwords

* refresh rbm select, fix wily 5 validation

* forgot we can't check 0

* oops I broke the basepatch (remove failing test later)

* fix solo gen fill error?

* fix webhost patch recognition

* fix imports, basepatch

* move to flexibility metric for boss validation

* special case boobeam trap

* block strobe on stage select init

* more energylink balancing

* bump world version

* wily HP inaccurate in validation

* fix validation edge case

* save last completed wily to data storage

* mypy and pep8 cleanup

* fix file browse validation

* fix test failure, add enemy weakness

* remove test seed

* update enemy damage

* inno setup

* Update en_Mega Man 2.md

* setup guide

* Update en_Mega Man 2.md

* finish plando weakness section

* starting rbm edge case

* remove * imports

* properly wrap later weakness additions in regen playthrough

* fix import

* forgot readme

* remove time stopper special casing

since we moved to proper wily 5 validation, this special casing is no longer important

* properly type added locations

* Update CODEOWNERS

* add animation reduction

* deprioritize Time Stopper in rush checks

* special case wily phase 1

* fix key error

* forgot the test

* music and general cleanup

* the great rename

* fix import

* thanks pycharm

* reorder palette shuffle

* account for alien on shuffled weakness

* apply suggestions

* fix seedbleed

* fix invalid buster passthrough

* fix weakness landing beneath required amount

* fix failsafe

* finish music

* fix Time Stopper on Flash/Alien

* asar pls

* Apply suggestions from code review

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

* world helpers

* init cleanup

* apostrophes

* clearer wording

* mypy and cleanup

* options doc cleanup

* Update rom.py

* rules cleanup

* Update __init__.py

* Update __init__.py

* move to defaultdict

* cleanup world helpers

* Update __init__.py

* remove unnecessary line from fill hook

* forgot the other one

* apply code review

* remove collect

* Update rules.py

* forgot another

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-08-20 04:59:29 +02:00
NewSoupVi
c4e7b6ca82 The Witness: Add "vague" hints making use of other games' region names and location groups (#2921)
* Vague hints work! But, the client will probably reveal some of the info through scouts atm

* Fall back on Everywhere if necessary

* Some of these failsafes are not necessary now

* Limit region size to 100 as well

* Actually... like this.

* Nutmeg

* Lol

* -1 for own player but don't scout

* Still make always/priority ITEM hints

* fix

* uwu notices your bug

* The hints should, like, actually work, you know?

* Make it a Toggle

* Update worlds/witness/hints.py

Co-authored-by: Bryce Wilson <gyroscope15@gmail.com>

* Update worlds/witness/hints.py

Co-authored-by: Bryce Wilson <gyroscope15@gmail.com>

* Make some suggested changes

* Make that ungodly equation a bit clearer in terms of formatting

* make that not sorted

* Add a warning about the feature in the option tooltip

* Make using region names experimental

* reword option tooltip

* Note about singleplayer

* Slight rewording again

* Reorder the order of priority a bit

* this condition is unnecessary now

* comment

* No wait the order has to be like this

* Okay now I think it's correct

* Another comment

* Align option tooltip with new behavior

* slight rewording again

* reword reword reword reword

* -

* ethics

* Update worlds/witness/options.py

Co-authored-by: Bryce Wilson <gyroscope15@gmail.com>

* Rename and slight behavior change for local hints

* I think I overengineered this system before. Make it more consistent and clear now

* oops I used checks by accident

* oops

* OMEGA OOPS

* Accidentally commited a print statemetn

* Vi don't commit nonsense challenge difficulty impossible

* This isn't always true but it's good enough

* Update options.py

* Update worlds/witness/options.py

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

* Scipio :3

* switch to is_event instead of checking against location.address

* oop

* Update test_roll_other_options.py

* Fix that unit test problem lol

* Oh is this not fixed in the apworld?

---------

Co-authored-by: Bryce Wilson <gyroscope15@gmail.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-08-20 01:34:40 +02:00
NewSoupVi
f253dffc07 The Witness: Panel Hunt Mode (#3265)
* Add panel hunt options

* Make sure all panels are either solvable or disabled in panel hunt

* Pick huntable panels

* Discards in disable non randomized

* Set up panel hunt requirement

* Panel hunt functional

* Make it so an event can have multiple names

* Panel hunt with events

* Add hunt entities to slot data

* ruff

* add to hint data, no client sneding yet

* encode panel hunt amount in compact hint data

* Remove print statement

* my b

* consistent

* meh

* additions for lcient

* Nah

* Victory panels ineligible for panel hunt

* Panel Hunt Postgame option

* cleanup

* Add data generation file

* pull out set

* always disable gate ep in panel hunt

* Disallow certain challenge panels from being panel hunt panels

* Make panelhuntpostgame its own function, so it can be called even if normal postgame is enabled

* disallow PP resets from panel hunt

* Disable challenge timer and elevetor start respectively in disable hunt postgame

* Fix panelhunt postgame

* lol

* When you test that the bug is fixed but not that the non-bug is not unfixed

* Prevent Obelisks from being panel hunt panels

* Make picking panels for panel hunt a bit more sophisticated, if less random

* Better function maybe ig

* Ok maybe that was a bit too much

* Give advanced players some control over panel hunt

* lint

* correct the logic for amount to pick

* decided the jingle thing was dumb, I'll figure sth out client side. Same area discouragement is now a configurable factor, and the logic has been significantly rewritten

* comment

* Make the option visible

* Safety

* Change assert slightly

* We do a little logging

* number tweak & we do a lil logging

* we do a little more logging

* Ruff

* Panel Hunt Option Group

* Idk how that got here

* Update worlds/witness/options.py

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

* Update worlds/witness/__init__.py

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

* remove merge error

* Update worlds/witness/player_logic.py

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

* True

* Don't have underwater sliding bridge when you have above water sliding bridge

* These are not actually connected lol

* get rid of unnecessary variable

* Refactor compact hint function again

* lint

* Pull out Entity Hunt Picking into its own class, split it into many functions. Kept a lot of the comments tho

* forgot to actually add the new file

* some more refactoring & docstrings

* consistent naming

* flip elif change

* Comment about naming

* Make static eligible panels a constant I can refer back to

* slight formatting change

* pull out options-based eligibility into its own function

* better text and stuff

* lint

* this is not necessary

* capitalisation

* Fix same area discouragement 0

* Simplify data file generation

* Simplify data file generation

* prevent div 0

* Add Vault Boxes -> Vault Panels to replacements

* Update options.py

* Update worlds/witness/entity_hunt.py

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

* Update entity_hunt.py

* Fix some events not working

* assert

* remove now unused function

* lint

* Lasers Activate, Lasers don't Solve

* lint

* oops

* mypy

* lint

* Add simple panel hunt unit test

* Add Panel Hunt Tests

* Add more Panel Hunt Tests

* Disallow Box Short for normal panel hunt

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-08-20 01:16:35 +02:00
KonoTyran
c010c8c938 Minecraft: Update to new options system. (#3765)
* Move to new options system.
switch to using self.random
reformat rules file.

* further reformats

* fix tests to use new options system.

* fix slot data to not use self.multiworld

* I hate python

* new starting_items docstring to prepare for 1.20.5+ item components.
fix invalid json being output to starting_items

* more typing fixes.

* stupid quotes around type declarations

* removed unused variable in ItemPool.py
change null check in Structures.py

* update rules "self" variable to a "world: MinecraftWorld" variable

* get key, and not value for required bosses.
2024-08-20 00:58:30 +02:00
Doug Hoskisson
1e8a8e7482 Docs: NetworkItem.player (#3811)
* Docs: `NetworkItem.player`

In many contexts, it's difficult to tell whether this is the sending player or the receiving player.

* correct player info

* Update NetUtils.py

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

---------

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2024-08-19 20:37:36 +02:00
NewSoupVi
182f7e24e5 The Witness: Fix Tunnels Theater Flower EP Access Logic + Add Unit Test for it (and Expert PP2) (#3807)
* Tunnels Theater Flowers fix + Flowers&PP2 Unit Tests

* copypaste

* Can just do it like this

* This is even better probably

* Also do some cleanup :3

* God damnit
2024-08-19 07:49:06 +02:00
Mysteryem
9277cb39ef Core: Fix incorrect default state checked in MultiWorld.can_beat_game (#3813)
`MultiWorld.can_beat_game()` with no arguments would initially check if
`self.state` is beatable, but then would create an empty state,
`state = CollectionState(self)`, to sweep spheres from to determine if
the game is beatable. The issue was that `self.state` and the new empty
state could be different.

Currently, it seems that everywhere in Archipelago's codebase that calls
`MultiWorld.can_beat_game()` with no arguments or `starting_state=None`
has a `self.state` that only contains precollected items, so the new
empty state happens to result in an equivalent state, but this should
not be relied upon to always be the case.

This patch changes `can_beat_game()` to initially check if the new empty
state is beatable instead of `self.state`.

This appears to be a bug introduced way back in 27b6dd8bd7

Fixes #3742
2024-08-19 06:44:06 +02:00
gaithern
28a9709516 Kingdom Hearts: Implement New Game (#3201)
* Added Final Ansem Goal

* Update __init__.py

* Update Rules.py

* New EotW logic

* Update __init__.py

* Update __init__.py

* Update Items.py

* Update Rules.py

* Rename Location to be more meaningful, logic fixes

* Removed Aerith locations

* Change to allow randomized keyblade stats

* Fixed incorrect option description.  Fixed victory locations for alternative win condition settings

* Commit

* Lots of changes

* Fixes

* Fixes

* Update Rules.py

* Update Rules.py

* Update Rules.py

* Update Rules.py

* Fixes

* Update Rules.py

* Update Rules.py

* Update Options.py

* Old Book is not required

* Added Jungle Slider

* Add Cid Check

* Add Wonderland Book Check

* Add OC Green Trinity

* Add Inferno Band Event

* Add Kurt Zisa Zantetsuken and Unknown EXP Necklace checks

* Update Locations.py

* Fix Final Ansem Goal

* Update __init__.py

* Update __init__.py

* Add options to exclude super bosses and 100 acre wood

* Fix puppies trp, remove cid check

* Fix 100 Acre Wood Option

* Material to Empty Bottle

* Fixed rules, location names, etc

* Fix super bosses

* Add item + location groups, level sanity

* Fix location and item group names

* Add Bad Starting Weapons Option

* Logic Error for 100 Acre Wood

* Update Rules.py

* Update __init__.py

* Fixes related to randomized keyblade stats and super bosses

* Credits and Fixes

* Logic fixes, location name group changes

* Update Options.py

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/docs/kh1_en.md

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

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update .gitignore

* Update CODEOWNERS

* Update docs/CODEOWNERS

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

* Fixed Atlantica item group name

* Update CODEOWNERS

* Update Client.py

* Update Items.py

* Update __init__.py

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

* Update Rules.py

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

* Update Rules.py

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

* Update Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Fixed report group name

* Fixes for PR

* Update Options.py

* Push changes for making the Final Rest Door appear, few option fixes

* Update Rules.py

* Website formatting, 0 min for reports, option description typo

* Create KH1Client.py

* Update worlds/kh1/docs/kh1_en.md

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

* Update Options.py

* Update Options.py

* Update Rules.py

* Update Rules.py

* Update Rules.py

* Add Donald and Goofy Death Link

* Add fight logic for optional bosses

* Update __init__.py

* Update Options.py

* Update worlds/kh1/Options.py

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

* Update Client.py

* Update kh1_en.md

* Update __init__.py

* Cleaning up for PR

* Update Client.py

* Added event locations for vanilla items

* Add proper location groups and auto hint synth shop items when entering

* so many changes

* Update Rules.py

* fixed oathkeeper and crabclaw logic

* Update Rules.py

* Update Rules.py

* Update Rules.py

* Update Rules.py

* Update en_Kingdom Hearts.md

* Update en_Kingdom Hearts.md

* fixing text

* Update kh1_en.md

* Addition of new key items

* Update Regions.py

* Push for start item from pool test

* Update worlds/kh1/Options.py

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

* Document update

* Update Rules.py

* Added starting world range and final rest goal option

* Update kh1_en.md

* Update en_Kingdom Hearts.md

* Update __init__.py

* Update __init__.py

* Clean up options descriptions

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Client.py

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

* Fix grammar in document

* Update __init__.py

* Update worlds/kh1/__init__.py

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

* Removed return type

* Update __init__.py

* Update __init__.py

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update __init__.py

* Fix missing i replacement, rework set rules to use "self" instead of a million arguments

* Update KH1Client.py

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

* Reformat rules, fix bug with exp mult, add to readme

* Clean up regions, fix client

* Fix item send prompt

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update worlds/kh1/docs/en_Kingdom Hearts.md

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

* Update worlds/kh1/docs/kh1_en.md

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

* Update worlds/kh1/docs/kh1_en.md

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

* Update worlds/kh1/docs/kh1_en.md

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

* Update worlds/kh1/docs/kh1_en.md

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

* Update worlds/kh1/test/test_goal.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Items.py

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

* Update worlds/kh1/Locations.py

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

* Update worlds/kh1/Regions.py

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

* Update worlds/kh1/Locations.py

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

* Update worlds/kh1/Locations.py

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

* Update worlds/kh1/Items.py

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

* Update worlds/kh1/Regions.py

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

* Update worlds/kh1/Regions.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/Rules.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/__init__.py

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

* Fix so many suggestions

* removed junk in missable locations option

* Update __init__.py

* Change credits order

* Update en_Kingdom Hearts.md

* Standardize punctuation

* Update en_Kingdom Hearts.md

* Update en_Kingdom Hearts.md

* Update Regions.py

* Removed "disclude" options in generation fillers

* Update Rules.py

* Update __init__.py

* Fix cemetery typo

* Update worlds/kh1/Options.py

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

* Add option groups and option presets

* Update worlds/kh1/__init__.py

That's a good idea!

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Options.py

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

* Update worlds/kh1/Presets.py

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

* fixed HB rule and formatting on a line in Items.py

* Fix logic bug with Geppetto's House postcard

* Update Rules.py

* Update Options.py

* Update __init__.py

* Update __init__.py

* Huge under-the-hood update for PR

* More updates for PR

* Update worlds/kh1/__init__.py

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

* Update worlds/kh1/Rules.py

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

* Update __init__.py

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-08-19 00:39:37 +02:00
Scrungip
49a5b52774 VVVVVV: Make unnecessary Trinkets filler (#3806)
* Make unnecessary trinkets filler

* Proper syntax

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

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-08-18 23:03:57 +02:00
Doug Hoskisson
2b1802ccee Core: type for CommonContext.ui (#3796)
* Core: type for `CommonContext.ui`

* use `Optional`
2024-08-17 03:19:16 +02:00
Bryce Wilson
f5218faea7 Pokemon Emerald: Ensure dig tutor is always usable (#3660)
* Pokemon Emerald: Ensure dig tutor is always usable

* Pokemon Emerald: Clarify comment

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

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-08-16 22:23:47 +02:00
Exempt-Medic
81092247c6 Core: early_local != local_early #3780 2024-08-16 22:20:20 +02:00
Kaito Sinclaire
ca96e7e294 Fix !remaining for cross-world items (#3732)
* Fix !remaining for other worlds

* Typing fixes for the previous change

* Update LocationStore test to match what get_remaining now returns
2024-08-16 22:20:02 +02:00
digiholic
c014c5a54a [OSRS] Fixes Incorrect filler item names causing failures on tests. (#3768)
* Updates filler item names to match the actual item names

* Adds more descriptive error message in case this error comes back

* Properly raises exception instead of just text

* Replaces exception with assert
2024-08-16 22:10:30 +02:00
Emily
e9c863dffd Docs: Update 'tag' documentation (#3632)
* Add tag docs for HintGame

* Apply suggestions from code review

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

* Make Tracker/TextOnly consistent with previous commit

* Apply suggestion

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

* fix spacing

* Apply suggestion

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

* apply suggestion correcting footnotes

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-08-16 21:04:23 +02:00
Mysteryem
7eda4c47f8 TLOZ: Fix non-deterministic item pool generation (#3779)
* TLOZ: Fix non-deterministic item pool generation

The way the item pool was constructed involved iterating unions of sets.
Sets are unordered, so the order of iteration of these combined sets
would be non-deterministic, resulting in the items in the item pool
being generated in a different order with the same seed.

Rather than creating unions of sets at all, the original code has been
replaced with using Counter objects. As a dict subclass, Counter
maintains insertion order, and its update() method makes it simple to
combine the separate item dictionaries into a single dictionary with the
total count of each item across each of the separate item dictionaries.

Fixes #3664 - After investigating more deeply, the only differences I
could find between generations of the same seed was the order of items
created by TLOZ, so this patch appears to fix the non-deterministic
generation issue. I did manage to reproduce the non-deterministic
behaviour with just TLOZ in the end, but it was very rare. I'm not
entirely sure why generating with SMZ3 specifically would cause the
non-deterministic behaviour in TLOZ to be frequently present, whereas
generating with other games or multiple TLOZ yamls would not.

* Change import order

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-08-16 20:57:04 +02:00
Scipio Wright
474a3181c6 TUNIC: Give the fox a gun (in logic) (very small PR) (#3790)
* Add bomb wall logic

* Remove option call from can_shop

* Gun for the envoy blocking Quarry

* has_sword -> can_shop on cube cave entrance region
2024-08-16 20:53:54 +02:00
Star Rauchenberger
4af6927e23 Lingo: Fixed Initiated-side Eight Door not opening (#3793) 2024-08-16 20:52:16 +02:00
Exempt-Medic
06df072095 Core: Require excluded locations to be reachable with full/locations accessibility (#3802)
* Make excludeds reachable

* Update all_state tests
2024-08-16 20:49:37 +02:00
agilbert1412
56aabe51b8 Stardew Valley: Add Quality Bobber in the logic rules for fish quality gold and above #3792 2024-08-14 17:07:06 +02:00
Scipio Wright
5e5f24cdd2 TUNIC: Add off and on aliases for the Entrance Rando option #3794 2024-08-14 16:55:02 +02:00
Exempt-Medic
9fbaa6050f I have no idea (#3791) 2024-08-14 00:21:42 -04:00
Scipio Wright
0af31c71e0 TUNIC: Swap from multiworld.get to world.get for applicable things (#3789)
* Swap from multiworld.get to world.get for applicable things

* Why was this even here in the first place?
2024-08-14 02:35:08 +02:00
Aaron Wagener
169da1b1e0 Tests: fix the all games multiworld test (#3788) 2024-08-14 00:31:26 +02:00
Aaron Wagener
8e7ea06f39 Core: dump all item placements for generation failures. (#3237)
* Core: dump all item placements for generation failures

* pass the multiworld from remaining fill

* change how the args get handled to fix formatting

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-08-14 00:17:42 +02:00
Aaron Wagener
96d48a923a Core: recontextualize CollectionState.collect (#3723)
* Core: renamed `CollectionState.collect` arg from `event` to `prevent_sweep` and remove forced collection

* Update TestDungeon.py

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-08-13 22:28:05 +02:00
Exempt-Medic
dcaa2f7b97 Core: Two Small Fixes (#3782) 2024-08-13 18:02:09 +02:00
NewSoupVi
50330cf32f Core: Remove broken unused code from Options.py (#3781)
"Unused" is a baseless assertion, but this code path has been crashing on the first statement for 6 months and noone's complained
2024-08-12 19:32:14 +02:00
Exempt-Medic
67520adcea Core: Error on empty options.as_dict (#3773)
* Error on empty options.as_dict

* ValueError instead

* Apply suggestions from code review

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

---------

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-08-12 02:13:45 +02:00
Exempt-Medic
a3e54a951f Undertale: Fix slot_data and options.as_dict() (#3774)
* Undertale: Fixing slot_data

* Booleans were difficult
2024-08-12 01:53:40 +02:00
qwint
ae0abd3821 Core: change start inventory from pool to warn when nothing to remove (#3158)
* makes start inventory from pool warn and fixes the itempool to match when it can not find a matching item to remove

* calc the difference correctly

* save new filler and non-removed items differently so we don't remove existing items at random
2024-08-12 00:57:59 +02:00
Scipio Wright
21bbf5fb95 TUNIC: Add note to Universal Tracker stuff #3772 2024-08-12 00:24:30 +02:00
Jarno
09e052c750 Timespinner: Fix eels check logic #3777 2024-08-12 00:24:09 +02:00
Scipio Wright
68a92b0c6f Clique: Update to new options API (#3759) 2024-08-11 14:47:17 +02:00
Silvris
8e06ab4f68 Core: fix invalid __package__ of zipped worlds (#3686)
* fix invalid package fix

* add comment describing fix
2024-08-10 13:49:32 +02:00
black-sliver
9dba39b606 SoE: fix determinism (#3745)
Fixes randomly placed ingredients not being deterministic (depending on settings)
and in turn also fixes logic not being deterministic if they get replaced by fragments.
2024-08-10 13:08:24 +02:00
Exempt-Medic
a6f376b02e TLOZ: world: multiworld (#3752) 2024-08-09 22:38:42 +02:00
Kaito Sinclaire
c66a8605da DOOM, DOOM II: Update steam URLs (#3746) 2024-08-09 17:04:59 +02:00
Mysteryem
ac7590e621 HK: fix iterating all worlds instead of only HK worlds in stage_pre_fill (#3750)
Would cause generation to fail when generating with HK and another game.

Mistake in 6803c373e5.
2024-08-09 17:02:41 +02:00
Mysteryem
30f97dd7de Core: Speed up CollectionState.copy() using built-in copy methods (#3678)
All the types being copied are built-in types with their own `copy()`
methods, so using the `copy` module was a bit overkill and also slower.

This patch replaces the use of the `copy` module in
`CollectionState.copy()` with using the built-in `.copy()` methods.

The copying of `reachable_regions` and `blocked_connections` was also
iterating the keys of each dictionary and then looking up the value in
the dictionary for that key. It is faster, and I think more readable, to
iterate the dictionary's `.items()` instead.

For me, when generating a multiworld including the template yaml of
every world with `python -O .\Generate.py --skip_output`, this patch
saves about 2.1s. The overall generation duration for these yamls varies
quite a lot, but averages around 160s for me, so on average this patch
reduced overall generation duration (excluding output duration) by
around 1.3%.

Timing comparisons were made by calling time.perf_counter() at the start
and end of `CollectionState.copy()`'s body, and summing the differences
between the starts and ends of the method body into a global variable
that was printed at the end of generation.

Additional timing comparisons were made, using the `timeit` module, of
the individual function calls or dictionary comprehensions used to
perform the copying.

The main performance cost was `copy.deepcopy()`, which gets slow as the
number of keys multiplied by the number of values within the
sets/Counters gets large, e.g., to deepcopy a `dict[int, Counter[str]]`
with 100 keys and where each Counter contains 100 keys was 30x slower
than most other tested copying methods. Increasing the number of dict
keys or Counter keys only makes it slower.
2024-08-09 14:25:39 +02:00
Mysteryem
6e41c60672 Core: Check parent_region.can_reach first in Location.can_reach (#3724)
* Core: Check parent_region.can_reach first in Location.can_reach

The comment about self.access_rule computing faster on average appears
to no longer be correct with the current caching system for region
accessibility, resulting in self.parent_region.can_reach computing
faster on average.

Generation of template yamls for each game that does not require a rom
to generate, generated with `python -O .\Generate.py --seed 1`
(all durations averaged over at 4 or 5 generations):

Full generation with `spoiler: 1` and no progression balancing:
89.9s -> 72.6s
Only output from above case:
2.6s -> 2.2s

Full generation with `spoiler: 3` and no progression balancing:
769.9s -> 627.1s
Only playthrough calculation + paths from above case:
680.5s -> 555.3s

Full generation with `spoiler: 1` with default progression balancing:
123.5s -> 98.3s
Only progression balancing from above case:
11.3s -> 9.6s

* Update BaseClasses.py

* Update BaseClasses.py

* Update BaseClasses.py

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-08-09 14:13:01 +02:00
Natalie Weizenbaum
5efb3fd2b0 DS3: Version 3.0.0 (#3128)
* Update worlds/dark_souls_3/Locations.py

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

* Fix Covetous Silver Serpent Ring location

* Update location groups

This should cover pretty much all of the seriously hidden items. It
also splits out miniboss drops, mimic drops, and hostile NPC drops.

* Remove the "Guarded by Keys" group

On reflection, I don't think this is actually that useful. It'll also
get a lot muddier once we can randomize shops and ashes become
pseudo-"keys".

* Restore Knight Slayer's Ring classification

* Support infusions/upgrades in the new DS3 mod system

* Support random starting loadouts

* Make an item's NPC status orthogonal to its category

* Track location groups with flags

* Track Archipelago/Offline mismatches on the server

Also fix a few incorrect item names.

* Add additional locations that are now randomizable

* Don't put soul and multiple items in shops

* Add an option to enable whether NG+ items/locations are included

* Clean up useful item categorization

There are so many weapons in the game now, it doesn't make sense to
treat them all as useful

* Add more variety to filler items

* Iron out a few bugs and incompatibilities

* Fix more silly bugs

* Get tests passing

* Update options to cover new item types

Also recategorize some items.

* Verify the default values of `Option`s.

Since `Option.verify()` can handle normalization of option names, this allows options  to define defaults which rely on that normalization. For example, it allows a world to exclude certain locations by default.

This also makes it easier to catch errors if a world author accidentally sets an invalid default.

* Make a few more improvements and fixes

* Randomize Path of the Dragon

* Mark items that unlock checks as useful

These items all unlock missable checks, but they're still good to ahve in the game for variety's sake.

* Guarantee more NPC quests are completable

* Fix a syntax error

* Fix rule definition

* Support enemy randomization

* Support online Yhorm randomization

* Remove a completed TODO

* Fix tests

* Fix force_unique

* Add an option to smooth out upgrade item progression

* Add helpers for setting location/entrance rules

* Support smoother soul item progression

* Fill extra smoothing items into conditional locations as well as other worlds

* Add health item smoothing

* Handle infusions at item generation time

* Handle item upgrades at genreation time

* Fix Grave Warden's Ashes

* Don't overwrite old rules

* Randomize items based on spheres instead of DS3 locations

* Add a smoothing option for weapon upgrades

* Add rules for crow trades

* Small fixes

* Fix a few more bugs

* Fix more bugs

* Try to prevent Path of the Dragon from going somewhere it doesn't work

* Add the ability to provide enemy presets

* Various fixes and features

* Bug fixes

* Better Coiled Sword placement

* Structure DarkSouls3Location more like DarkSouls3Item

* Add events to make DS3's spheres more even

* Restructure locations to work like items do now

* Add rules for more missable locations

* Don't add two Storm Rulers

* Place Hawk Ring in Farron Keep

* Mark the Grass Crest Shield as useful

* Mark new progression items

* Fix a bug

* Support newer better Path of the Dragon code

* Don't lock the player out of Coiled Sword

* Don't create events for missable locations

* Don't throw strings

* Don't smooth event items

* Properly categorize Butcher Knife

* Be more careful about placing Yhorm in low-randomization scenarios

* Don't try to smooth DLC items with DLC disabled

* Fix another Yhorm bug

* Fix upgrade/infusion logic

* Remove the PoolType option

This distinction is no longer meaningful now that every location in
the game of each type is randomized

* Categorize HWL: Red Eye Orb as an NPC location

* Don't place Storm Ruler on CA: Coiled Sword

* Define flatten() locally to make this APWorld capable

* Fix some more Leonhard weirdness

* Fix unique item randomization

* Don't double Twin Dragon Greatshield

* Remove debugging print

* Don't add double Storm Ruler

Also remove now-redundant item sorting by category in create_items.

* Don't add double Storm Ruler

Also remove now-redundant item sorting by category in create_items.

* Add a missing dlc_enabled check

* Use nicer options syntax

* Bump data_version

* Mention where Yhorm is in which world

* Better handle excluded events

* Add a newline to Yhorm location

* Better way of handling excluded unradomized progression locations

* Fix a squidge of nondeterminism

* Only smooth items from this world

* Don't smooth progression weapons

* Remove a location that doesn't actually exist in-game

* Classify Power Within as useful

* Clarify location names

* Fix location requirements

* Clean up randomization options

* Properly name Coiled Sword location

* Add an option for configuring how missable items are handled

* Fix some bugs from location name updates

* Fix location guide link

* Fix a couple locations that were busted offline

* Update detailed location descriptions

* Fix some bugs when generating for a multiworld

* Inject Large Leather Shield

* Fix a few location issues

* Don't allow progression_skip_balancing for unnecessary locs

* Update some location info

* Don't uniquify the wrong items

* Fix some more location issues

* More location fixes

* Use hyphens instead of parens for location descriptions

* Update and fix more locations

* Fix Soul of Cinder boss name

* Fix some logic issues

* Add item groups and document item/location groups

* Fix the display name for "Impatient Mimics"

* Properly handle Transposing Kiln and Pyromancer's Flame

* Testing

* Some fixes to NPC quests, late basin, and transposing kiln

* Improve a couple location names

* Split out and improve missable NPC item logic

* Don't allow crow trades to have foreign items

* Fix a variable capture bug

* Make sure early items are accessible early even with early Castle

* Mark ID giant slave drops as missable

* Make sure late basin means that early items aren't behind it

* Make is_location_available explicitly private

* Add an _add_item_rule utility that checks availability

* Clear excluded items if excluded_locations == "unnecessary"

* Don't allow upgrades/infusions in crow trades

* Fix the documentation for deprecated options

* Create events for all excluded locations

This allows `can_reach` logic to work even if the locations are
randomized.

* Fix up Patches' and Siegward's logic based on some manual testing

* Factor out more sub-methods for setting location rules

* Oops, left these in

* Fixing name

* Left that in too

* Changing to NamedRange to support special_range_names

* Alphabetizing

* Don't call _is_location_available on foreign locations

* Add missing Leonhard items

* Changing late basin to have a post-small-doll option

* Update basin option, add logic for some of Leonhard Hawkwood and Orbeck

* Simplifying an option, fixing a copy-paste error

* Removing trailing whitespace

* Changing lost items to go into start inventory

* Revert Basin changes

* Oops

* Update Options.py

* Reverting small doll changes

* Farron Keep boss requirement logic

* Add Scroll for late_dlc

* Fixing excluded unnecessary locations

* Adding Priestess Ring as being after UG boss

* Removing missable from Corvian Titanite Slab

* Adding KFF Yhorm boss locks

* Screams about Creighton

* Elite Knight Set isn't permanently missable

* Adding Kiln requirement to KFF

* fixing valid_keys and item groups

* Fixing an option-checker

* Throwing unplaceable Storm Ruler into start inventory

* Update locations

* Refactor item injection

* Update setup doc

* Small fixes

* Fix another location name

* Fix injection calculation

* Inject guaranteed items along with progression items

* Mark boss souls as required for access to regions

This allows us to set quest requirements for boss souls and have them
automatically propagated to regions, means we need less machinery for
Yhorm bosses, and allows us to get rid of a few region-transition
events.

* Make sure Sirris's quest can be completed before Pontiff

* Removing unused list

* Changing dict to list

* Removing unused test

* Update __init__.py

* self.multiworld.random -> self.random (#9)

* Fix some miscellaneous location issues

* Rewrite the DS3 intro page/FAQ

* Removing modifying the itempool after fill (#7)

Co-authored-by: Natalie Weizenbaum <nweiz@google.com>

* Small fixes to the setup guide (#10)

Small fixes, adding an example for connecting

* Expanded Late Basin of Vows and Late DLC (#6)

* Add proper requirements for CD: Black Eye Orb

* Fix Aldrich's name

* Document the differences with the 2.x.x branch

* Don't crash if there are more items than locations in smoothing

* Apply suggestions from code review

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* Code review

* Fix _replace_with_filler

* Don't use the shared flatten function in SM

* Track local items separately rather than iterating the multiworld

* Various formatting/docs changes suggested by PyCharm (#12)

* Drop deprecated options

* Rename "offline randomizer" to "static randomizer" which is clearer

* Move `enable_*_locations` under removed options.

* Avoid excluded locations for locally-filled items

* Adding Removed options to error (#14)

* Changes for WebHost options display and the options overhaul

* unpack iterators in item list (#13)

* Allow worlds to add options to prebuilt groups

Previously, this crashed because `typing.NamedTuple` fields such as
`group.name` aren't assignable. Now it will only fail for group names
that are actually incorrectly cased, and will fail with a better error
message.

* Style changes, rename exclude behavior options, remove guaranteed items option

* Spacing/Formatting (#18)

* Various Fixes (#19)

* Universally Track Yhorm (#20)

* Account for excluded and missable

* These are behaviors now

* This is singular, apparently

* Oops

* Fleshing out the priority process

* Missable Titanite Lizards and excluded locations (#22)

* Small style/efficiency changes

* Final passthrough fixes (#24)

* Use rich option formatting

* Make the behavior option values actual behaviors (#25)

* Use !=

* Remove unused flatten utility

* Some changes from review (#28)

* Fixing determinism and making smooth faster (#29)

* Style change

* PyCharm and Mypy fixes (#26)

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

* Change yhorm default (#30)

* Add indirect condition (#27)

* Update worlds/dark_souls_3/docs/locations_en.md

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* Ship all item IDs to the client

This avoids issues where items might get skipped if, for instance,
they're only in the starting inventory.

* Make sure to send AP IDs for infused/upgraded weapons

* Make `RandomEnemyPresetOption` compatible with ArchipelagoMW/Archipelago#3280 (#31)

* Fix cast

* More typing and small fixes (#32)

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Exempt-Medic <ExemptMedic@Gmail.com>
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-08-09 12:14:26 +02:00
234 changed files with 79185 additions and 17038 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
worlds/blasphemous/region_data.py linguist-generated=true

View File

@@ -37,12 +37,13 @@ jobs:
- {version: '3.9'}
- {version: '3.10'}
- {version: '3.11'}
- {version: '3.12'}
include:
- python: {version: '3.8'} # win7 compat
os: windows-latest
- python: {version: '3.11'} # current
- python: {version: '3.12'} # current
os: windows-latest
- python: {version: '3.11'} # current
- python: {version: '3.12'} # current
os: macos-latest
steps:
@@ -70,7 +71,7 @@ jobs:
os:
- ubuntu-latest
python:
- {version: '3.11'} # current
- {version: '3.12'} # current
steps:
- uses: actions/checkout@v4

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
import collections
import copy
import itertools
import functools
import logging
@@ -12,8 +11,10 @@ from argparse import Namespace
from collections import Counter, deque
from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag
from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \
TypedDict, Union, Type, ClassVar
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
Optional, Protocol, Set, Tuple, Union, Type)
from typing_extensions import NotRequired, TypedDict
import NetUtils
import Options
@@ -23,16 +24,16 @@ if typing.TYPE_CHECKING:
from worlds import AutoWorld
class Group(TypedDict, total=False):
class Group(TypedDict):
name: str
game: str
world: "AutoWorld.World"
players: Set[int]
item_pool: Set[str]
replacement_items: Dict[int, Optional[str]]
local_items: Set[str]
non_local_items: Set[str]
link_replacement: bool
players: AbstractSet[int]
item_pool: NotRequired[Set[str]]
replacement_items: NotRequired[Dict[int, Optional[str]]]
local_items: NotRequired[Set[str]]
non_local_items: NotRequired[Set[str]]
link_replacement: NotRequired[bool]
class ThreadBarrierProxy:
@@ -49,6 +50,11 @@ class ThreadBarrierProxy:
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")
class HasNameAndPlayer(Protocol):
name: str
player: int
class MultiWorld():
debug_types = False
player_name: Dict[int, str]
@@ -157,7 +163,7 @@ class MultiWorld():
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
for player in range(1, players + 1):
def set_player_attr(attr, val):
def set_player_attr(attr: str, val) -> None:
self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('plando_items', [])
set_player_attr('plando_texts', {})
@@ -166,13 +172,13 @@ class MultiWorld():
set_player_attr('completion_condition', lambda state: True)
self.worlds = {}
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
"world's random object instead (usually self.random)")
"world's random object instead (usually self.random)")
self.plando_options = PlandoOptions.none
def get_all_ids(self) -> Tuple[int, ...]:
return self.player_ids + tuple(self.groups)
def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset()) -> Tuple[int, Group]:
"""Create a group with name and return the assigned player ID and group.
If a group of this name already exists, the set of players is extended instead of creating a new one."""
from worlds import AutoWorld
@@ -196,7 +202,7 @@ class MultiWorld():
return new_id, new_group
def get_player_groups(self, player) -> Set[int]:
def get_player_groups(self, player: int) -> Set[int]:
return {group_id for group_id, group in self.groups.items() if player in group["players"]}
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
@@ -259,7 +265,7 @@ class MultiWorld():
"link_replacement": replacement_prio.index(item_link["link_replacement"]),
}
for name, item_link in item_links.items():
for _name, item_link in item_links.items():
current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups
pool = set()
local_items = set()
@@ -389,7 +395,7 @@ class MultiWorld():
return tuple(world for player, world in self.worlds.items() if
player not in self.groups and self.game[player] == game_name)
def get_name_string_for_object(self, obj) -> str:
def get_name_string_for_object(self, obj: HasNameAndPlayer) -> str:
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
def get_player_name(self, player: int) -> str:
@@ -431,7 +437,7 @@ class MultiWorld():
subworld = self.worlds[player]
for item in subworld.get_pre_fill_items():
subworld.collect(ret, item)
ret.sweep_for_events()
ret.sweep_for_advancements()
if use_cache:
self._all_state = ret
@@ -440,7 +446,7 @@ class MultiWorld():
def get_items(self) -> List[Item]:
return [loc.item for loc in self.get_filled_locations()] + self.itempool
def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]:
def find_item_locations(self, item: str, player: int, resolve_group_locations: bool = False) -> List[Location]:
if resolve_group_locations:
player_groups = self.get_player_groups(player)
return [location for location in self.get_locations() if
@@ -449,7 +455,7 @@ class MultiWorld():
return [location for location in self.get_locations() if
location.item and location.item.name == item and location.item.player == player]
def find_item(self, item, player: int) -> Location:
def find_item(self, item: str, player: int) -> Location:
return next(location for location in self.get_locations() if
location.item and location.item.name == item and location.item.player == player)
@@ -542,9 +548,9 @@ class MultiWorld():
return True
state = starting_state.copy()
else:
if self.has_beaten_game(self.state):
return True
state = CollectionState(self)
if self.has_beaten_game(state):
return True
prog_locations = {location for location in self.get_locations() if location.item
and location.item.advancement and location not in state.locations_checked}
@@ -617,8 +623,7 @@ class MultiWorld():
def location_relevant(location: Location) -> bool:
"""Determine if this location is relevant to sweep."""
return location.progress_type != LocationProgressType.EXCLUDED \
and (location.player in players["full"] or location.advancement)
return location.player in players["full"] or location.advancement
def all_done() -> bool:
"""Check if all access rules are fulfilled"""
@@ -663,7 +668,7 @@ class CollectionState():
multiworld: MultiWorld
reachable_regions: Dict[int, Set[Region]]
blocked_connections: Dict[int, Set[Entrance]]
events: Set[Location]
advancements: Set[Location]
path: Dict[Union[Region, Entrance], PathValue]
locations_checked: Set[Location]
stale: Dict[int, bool]
@@ -675,7 +680,7 @@ class CollectionState():
self.multiworld = parent
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
self.blocked_connections = {player: set() for player in parent.get_all_ids()}
self.events = set()
self.advancements = set()
self.path = {}
self.locations_checked = set()
self.stale = {player: True for player in parent.get_all_ids()}
@@ -719,14 +724,14 @@ class CollectionState():
def copy(self) -> CollectionState:
ret = CollectionState(self.multiworld)
ret.prog_items = copy.deepcopy(self.prog_items)
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in
self.reachable_regions}
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in
self.blocked_connections}
ret.events = copy.copy(self.events)
ret.path = copy.copy(self.path)
ret.locations_checked = copy.copy(self.locations_checked)
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}
ret.reachable_regions = {player: region_set.copy() for player, region_set in
self.reachable_regions.items()}
ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in
self.blocked_connections.items()}
ret.advancements = self.advancements.copy()
ret.path = self.path.copy()
ret.locations_checked = self.locations_checked.copy()
for function in self.additional_copy_functions:
ret = function(self, ret)
return ret
@@ -757,19 +762,24 @@ class CollectionState():
return self.multiworld.get_region(spot, player).can_reach(self)
def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None:
Utils.deprecate("sweep_for_events has been renamed to sweep_for_advancements. The functionality is the same. "
"Please switch over to sweep_for_advancements.")
return self.sweep_for_advancements(locations)
def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None) -> None:
if locations is None:
locations = self.multiworld.get_filled_locations()
reachable_events = True
# since the loop has a good chance to run more than once, only filter the events once
locations = {location for location in locations if location.advancement and location not in self.events}
reachable_advancements = True
# since the loop has a good chance to run more than once, only filter the advancements once
locations = {location for location in locations if location.advancement and location not in self.advancements}
while reachable_events:
reachable_events = {location for location in locations if location.can_reach(self)}
locations -= reachable_events
for event in reachable_events:
self.events.add(event)
assert isinstance(event.item, Item), "tried to collect Event with no Item"
self.collect(event.item, True, event)
while reachable_advancements:
reachable_advancements = {location for location in locations if location.can_reach(self)}
locations -= reachable_advancements
for advancement in reachable_advancements:
self.advancements.add(advancement)
assert isinstance(advancement.item, Item), "tried to collect Event with no Item"
self.collect(advancement.item, True, advancement)
# item name related
def has(self, item: str, player: int, count: int = 1) -> bool:
@@ -803,7 +813,7 @@ class CollectionState():
if found >= count:
return True
return False
def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
Ignores duplicates of the same item."""
@@ -818,7 +828,7 @@ class CollectionState():
def count_from_list(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state."""
return sum(self.prog_items[player][item_name] for item_name in items)
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
@@ -864,20 +874,16 @@ class CollectionState():
)
# Item related
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
def collect(self, item: Item, prevent_sweep: bool = False, location: Optional[Location] = None) -> bool:
if location:
self.locations_checked.add(location)
changed = self.multiworld.worlds[item.player].collect(self, item)
if not changed and event:
self.prog_items[item.player][item.name] += 1
changed = True
self.stale[item.player] = True
if changed and not event:
self.sweep_for_events()
if changed and not prevent_sweep:
self.sweep_for_advancements()
return changed
@@ -901,7 +907,7 @@ class Entrance:
addresses = None
target = None
def __init__(self, player: int, name: str = '', parent: Region = None):
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None:
self.name = name
self.parent_region = parent
self.player = player
@@ -921,9 +927,6 @@ class Entrance:
region.entrances.append(self)
def __repr__(self):
return self.__str__()
def __str__(self):
multiworld = self.parent_region.multiworld if self.parent_region else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
@@ -1049,7 +1052,7 @@ class Region:
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) -> entrance_type:
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
"""
Connects this Region to another Region, placing the provided rule on the connection.
@@ -1073,7 +1076,7 @@ class Region:
return exit_
def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None:
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]:
"""
Connects current region to regions in exit dictionary. Passed region names must exist first.
@@ -1083,15 +1086,16 @@ class Region:
"""
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)
return [
self.connect(
self.multiworld.get_region(connecting_region, self.player),
name,
rules[connecting_region] if rules and connecting_region in rules else None,
)
for connecting_region, name in exits.items()
]
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})'
@@ -1110,9 +1114,9 @@ class Location:
locked: bool = False
show_in_spoiler: bool = True
progress_type: LocationProgressType = LocationProgressType.DEFAULT
always_allow = staticmethod(lambda state, item: False)
always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False)
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
item_rule = staticmethod(lambda item: True)
item_rule: Callable[[Item], bool] = staticmethod(lambda item: True)
item: Optional[Item] = None
def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
@@ -1121,16 +1125,20 @@ class Location:
self.address = address
self.parent_region = parent
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items)
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
and self.item_rule(item)
and (not check_access or self.can_reach(state))))
def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool:
return ((
self.always_allow(state, item)
and item.name not in state.multiworld.worlds[item.player].options.non_local_items
) or (
(self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
and self.item_rule(item)
and (not check_access or self.can_reach(state))
))
def can_reach(self, state: CollectionState) -> bool:
# self.access_rule computes faster on average, so placing it first for faster abort
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
assert self.parent_region, "Can't reach location without region"
return self.access_rule(state) and self.parent_region.can_reach(state)
return self.parent_region.can_reach(state) and self.access_rule(state)
def place_locked_item(self, item: Item):
if self.item:
@@ -1140,9 +1148,6 @@ class Location:
self.locked = True
def __repr__(self):
return self.__str__()
def __str__(self):
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
@@ -1164,7 +1169,7 @@ class Location:
@property
def native_item(self) -> bool:
"""Returns True if the item in this location matches game."""
return self.item and self.item.game == self.game
return self.item is not None and self.item.game == self.game
@property
def hint_text(self) -> str:
@@ -1247,9 +1252,6 @@ class Item:
return hash((self.name, self.player))
def __repr__(self) -> str:
return self.__str__()
def __str__(self) -> str:
if self.location and self.location.parent_region and self.location.parent_region.multiworld:
return self.location.parent_region.multiworld.get_name_string_for_object(self)
return f"{self.name} (Player {self.player})"
@@ -1327,9 +1329,9 @@ class Spoiler:
# in the second phase, we cull each sphere such that the game is still beatable,
# reducing each range of influence to the bare minimum required inside it
restore_later = {}
restore_later: Dict[Location, Item] = {}
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
to_delete = set()
to_delete: Set[Location] = set()
for location in sphere:
# we remove the item at location and check if game is still beatable
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
@@ -1347,7 +1349,7 @@ class Spoiler:
sphere -= to_delete
# second phase, sphere 0
removed_precollected = []
removed_precollected: List[Item] = []
for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement):
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
multiworld.precollected_items[item.player].remove(item)
@@ -1428,7 +1430,7 @@ class Spoiler:
# Maybe move the big bomb over to the Event system instead?
if any(exit_path == 'Pyramid Fairy' for path in self.paths.values()
for (_, exit_path) in path):
if multiworld.mode[player] != 'inverted':
if multiworld.worlds[player].options.mode != 'inverted':
self.paths[str(multiworld.get_region('Big Bomb Shop', player))] = \
get_path(state, multiworld.get_region('Big Bomb Shop', player))
else:
@@ -1500,9 +1502,9 @@ class Spoiler:
if self.paths:
outfile.write('\n\nPaths:\n\n')
path_listings = []
path_listings: List[str] = []
for location, path in sorted(self.paths.items()):
path_lines = []
path_lines: List[str] = []
for region, exit in path:
if exit is not None:
path_lines.append("{} -> {}".format(region, exit))

View File

@@ -252,7 +252,7 @@ class CommonContext:
starting_reconnect_delay: int = 5
current_reconnect_delay: int = starting_reconnect_delay
command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor
ui = None
ui: typing.Optional["kvui.GameManager"] = None
ui_task: typing.Optional["asyncio.Task[None]"] = None
input_task: typing.Optional["asyncio.Task[None]"] = None
keep_alive_task: typing.Optional["asyncio.Task[None]"] = None

33
Fill.py
View File

@@ -12,7 +12,12 @@ from worlds.generic.Rules import add_item_rule
class FillError(RuntimeError):
pass
def __init__(self, *args: typing.Union[str, typing.Any], **kwargs) -> None:
if "multiworld" in kwargs and isinstance(args[0], str):
placements = (args[0] + f"\nAll Placements:\n" +
f"{[(loc, loc.item) for loc in kwargs['multiworld'].get_filled_locations()]}")
args = (placements, *args[1:])
super().__init__(*args)
def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
@@ -24,7 +29,7 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
new_state = base_state.copy()
for item in itempool:
new_state.collect(item, True)
new_state.sweep_for_events(locations=locations)
new_state.sweep_for_advancements(locations=locations)
return new_state
@@ -212,7 +217,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
f"Unfilled locations:\n"
f"{', '.join(str(location) for location in locations)}\n"
f"Already placed {len(placements)}:\n"
f"{', '.join(str(place) for place in placements)}")
f"{', '.join(str(place) for place in placements)}", multiworld=multiworld)
item_pool.extend(unplaced_items)
@@ -299,7 +304,7 @@ def remaining_fill(multiworld: MultiWorld,
f"Unfilled locations:\n"
f"{', '.join(str(location) for location in locations)}\n"
f"Already placed {len(placements)}:\n"
f"{', '.join(str(place) for place in placements)}")
f"{', '.join(str(place) for place in placements)}", multiworld=multiworld)
itempool.extend(unplaced_items)
@@ -324,8 +329,8 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
pool.append(location.item)
state.remove(location.item)
location.item = None
if location in state.events:
state.events.remove(location)
if location in state.advancements:
state.advancements.remove(location)
locations.append(location)
if pool and locations:
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
@@ -358,7 +363,7 @@ def distribute_early_items(multiworld: MultiWorld,
early_priority_locations: typing.List[Location] = []
loc_indexes_to_remove: typing.Set[int] = set()
base_state = multiworld.state.copy()
base_state.sweep_for_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
base_state.sweep_for_advancements(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
for i, loc in enumerate(fill_locations):
if loc.can_reach(base_state):
if loc.progress_type == LocationProgressType.PRIORITY:
@@ -506,7 +511,8 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if progitempool:
raise FillError(
f"Not enough locations for progression items. "
f"There are {len(progitempool)} more progression items than there are available locations."
f"There are {len(progitempool)} more progression items than there are available locations.",
multiworld=multiworld,
)
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
@@ -523,7 +529,8 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if excludedlocations:
raise FillError(
f"Not enough filler items for excluded locations. "
f"There are {len(excludedlocations)} more excluded locations than filler or trap items."
f"There are {len(excludedlocations)} more excluded locations than filler or trap items.",
multiworld=multiworld,
)
restitempool = filleritempool + usefulitempool
@@ -551,7 +558,7 @@ def flood_items(multiworld: MultiWorld) -> None:
progress_done = False
# sweep once to pick up preplaced items
multiworld.state.sweep_for_events()
multiworld.state.sweep_for_advancements()
# fill multiworld from top of itempool while we can
while not progress_done:
@@ -589,7 +596,7 @@ def flood_items(multiworld: MultiWorld) -> None:
if candidate_item_to_place is not None:
item_to_place = candidate_item_to_place
else:
raise FillError('No more progress items left to place.')
raise FillError('No more progress items left to place.', multiworld=multiworld)
# find item to replace with progress item
location_list = multiworld.get_reachable_locations()
@@ -739,7 +746,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
), items_to_test):
reducing_state.collect(location.item, True, location)
reducing_state.sweep_for_events(locations=locations_to_test)
reducing_state.sweep_for_advancements(locations=locations_to_test)
if multiworld.has_beaten_game(balancing_state):
if not multiworld.has_beaten_game(reducing_state):
@@ -822,7 +829,7 @@ def distribute_planned(multiworld: MultiWorld) -> None:
warn(warning, force)
swept_state = multiworld.state.copy()
swept_state.sweep_for_events()
swept_state.sweep_for_advancements()
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)

View File

@@ -511,7 +511,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
continue
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
if PlandoOptions.items in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
if ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights)

9
KH1Client.py Normal file
View File

@@ -0,0 +1,9 @@
if __name__ == '__main__':
import ModuleUpdate
ModuleUpdate.update()
import Utils
Utils.init_logging("KH1Client", exception_logger="Client")
from worlds.kh1.Client import launch
launch()

View File

@@ -266,7 +266,7 @@ def run_gui():
if file and component:
run_component(component, file)
else:
logging.warning(f"unable to identify component for {filename}")
logging.warning(f"unable to identify component for {file}")
def _stop(self, *largs):
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.

View File

@@ -14,7 +14,7 @@ import tkinter as tk
from argparse import Namespace
from concurrent.futures import as_completed, ThreadPoolExecutor
from glob import glob
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, TOP, LabelFrame, \
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, BOTH, TOP, LabelFrame, \
IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
from tkinter.constants import DISABLED, NORMAL
from urllib.parse import urlparse
@@ -29,7 +29,8 @@ from Utils import output_path, local_path, user_path, open_file, get_cert_none_s
GAME_ALTTP = "A Link to the Past"
WINDOW_MIN_HEIGHT = 525
WINDOW_MIN_WIDTH = 425
class AdjusterWorld(object):
def __init__(self, sprite_pool):
@@ -242,16 +243,17 @@ def adjustGUI():
from argparse import Namespace
from Utils import __version__ as MWVersion
adjustWindow = Tk()
adjustWindow.minsize(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT)
adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion)
set_icon(adjustWindow)
rom_options_frame, rom_vars, set_sprite = get_rom_options_frame(adjustWindow)
bottomFrame2 = Frame(adjustWindow)
bottomFrame2 = Frame(adjustWindow, padx=8, pady=2)
romFrame, romVar = get_rom_frame(adjustWindow)
romDialogFrame = Frame(adjustWindow)
romDialogFrame = Frame(adjustWindow, padx=8, pady=2)
baseRomLabel2 = Label(romDialogFrame, text='Rom to adjust')
romVar2 = StringVar()
romEntry2 = Entry(romDialogFrame, textvariable=romVar2)
@@ -261,9 +263,9 @@ def adjustGUI():
romVar2.set(rom)
romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2)
romDialogFrame.pack(side=TOP, expand=True, fill=X)
baseRomLabel2.pack(side=LEFT)
romEntry2.pack(side=LEFT, expand=True, fill=X)
romDialogFrame.pack(side=TOP, expand=False, fill=X)
baseRomLabel2.pack(side=LEFT, expand=False, fill=X, padx=(0, 8))
romEntry2.pack(side=LEFT, expand=True, fill=BOTH, pady=1)
romSelectButton2.pack(side=LEFT)
def adjustRom():
@@ -331,12 +333,11 @@ def adjustGUI():
messagebox.showinfo(title="Success", message="Settings saved to persistent storage")
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
rom_options_frame.pack(side=TOP)
rom_options_frame.pack(side=TOP, padx=8, pady=8, fill=BOTH, expand=True)
adjustButton.pack(side=LEFT, padx=(5,5))
saveButton = Button(bottomFrame2, text='Save Settings', command=saveGUISettings)
saveButton.pack(side=LEFT, padx=(5,5))
bottomFrame2.pack(side=TOP, pady=(5,5))
tkinter_center_window(adjustWindow)
@@ -576,7 +577,7 @@ class AttachTooltip(object):
def get_rom_frame(parent=None):
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
romFrame = Frame(parent)
romFrame = Frame(parent, padx=8, pady=8)
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
romVar = StringVar(value=adjuster_settings.baserom)
romEntry = Entry(romFrame, textvariable=romVar)
@@ -596,20 +597,19 @@ def get_rom_frame(parent=None):
romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect)
baseRomLabel.pack(side=LEFT)
romEntry.pack(side=LEFT, expand=True, fill=X)
romEntry.pack(side=LEFT, expand=True, fill=BOTH, pady=1)
romSelectButton.pack(side=LEFT)
romFrame.pack(side=TOP, expand=True, fill=X)
romFrame.pack(side=TOP, fill=X)
return romFrame, romVar
def get_rom_options_frame(parent=None):
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
romOptionsFrame = LabelFrame(parent, text="Rom options")
romOptionsFrame.columnconfigure(0, weight=1)
romOptionsFrame.columnconfigure(1, weight=1)
romOptionsFrame = LabelFrame(parent, text="Rom options", padx=8, pady=8)
for i in range(5):
romOptionsFrame.rowconfigure(i, weight=1)
romOptionsFrame.rowconfigure(i, weight=0, pad=4)
vars = Namespace()
vars.MusicVar = IntVar()
@@ -660,7 +660,7 @@ def get_rom_options_frame(parent=None):
spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect)
baseSpriteLabel.pack(side=LEFT)
spriteEntry.pack(side=LEFT)
spriteEntry.pack(side=LEFT, expand=True, fill=X)
spriteSelectButton.pack(side=LEFT)
oofDialogFrame = Frame(romOptionsFrame)

22
Main.py
View File

@@ -11,7 +11,8 @@ 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 Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, distribute_planned, \
flood_items
from Options import StartInventoryPool
from Utils import __version__, output_path, version_tuple, get_settings
from settings import get_settings
@@ -100,7 +101,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
multiworld.early_items[player][item_name] = max(0, early-count)
remaining_count = count-early
if remaining_count > 0:
local_early = multiworld.early_local_items[player].get(item_name, 0)
local_early = multiworld.local_early_items[player].get(item_name, 0)
if local_early:
multiworld.early_items[player][item_name] = max(0, local_early - remaining_count)
del local_early
@@ -151,6 +152,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
# Because some worlds don't actually create items during create_items this has to be as late as possible.
if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids):
new_items: List[Item] = []
old_items: List[Item] = []
depletion_pool: Dict[int, Dict[str, int]] = {
player: getattr(multiworld.worlds[player].options,
"start_inventory_from_pool",
@@ -169,20 +171,24 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
depletion_pool[item.player][item.name] -= 1
# quick abort if we have found all items
if not target:
new_items.extend(multiworld.itempool[i+1:])
old_items.extend(multiworld.itempool[i+1:])
break
else:
new_items.append(item)
old_items.append(item)
# leftovers?
if target:
for player, remaining_items in depletion_pool.items():
remaining_items = {name: count for name, count in remaining_items.items() if count}
if remaining_items:
raise Exception(f"{multiworld.get_player_name(player)}"
logger.warning(f"{multiworld.get_player_name(player)}"
f" is trying to remove items from their pool that don't exist: {remaining_items}")
assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change."
multiworld.itempool[:] = new_items
# find all filler we generated for the current player and remove until it matches
removables = [item for item in new_items if item.player == player]
for _ in range(sum(remaining_items.values())):
new_items.remove(removables.pop())
assert len(multiworld.itempool) == len(new_items + old_items), "Item Pool amounts should not change."
multiworld.itempool[:] = new_items + old_items
multiworld.link_items()
@@ -341,7 +347,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
output_file_futures.append(pool.submit(write_multidata))
if not check_accessibility_task.result():
if not multiworld.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.")
raise FillError("Game appears as unbeatable. Aborting.", multiworld=multiworld)
else:
logger.warning("Location Accessibility requirements not fulfilled.")

View File

@@ -75,13 +75,13 @@ def update(yes: bool = False, force: bool = False) -> None:
if not update_ran:
update_ran = True
install_pkg_resources(yes=yes)
import pkg_resources
if force:
update_command()
return
install_pkg_resources(yes=yes)
import pkg_resources
prev = "" # if a line ends in \ we store here and merge later
for req_file in requirements_files:
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)

View File

@@ -67,6 +67,21 @@ def update_dict(dictionary, entries):
return dictionary
def queue_gc():
import gc
from threading import Thread
gc_thread: typing.Optional[Thread] = getattr(queue_gc, "_thread", None)
def async_collect():
time.sleep(2)
setattr(queue_gc, "_thread", None)
gc.collect()
if not gc_thread:
gc_thread = Thread(target=async_collect)
setattr(queue_gc, "_thread", gc_thread)
gc_thread.start()
# functions callable on storable data on the server by clients
modify_functions = {
# generic:
@@ -551,6 +566,9 @@ class Context:
self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
else:
self.save_dirty = False
if not atexit_save: # if atexit is used, that keeps a reference anyway
queue_gc()
self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
self.auto_saver_thread.start()
@@ -991,7 +1009,7 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
collect_player(ctx, team, group, True)
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[typing.Tuple[int, int]]:
return ctx.locations.get_remaining(ctx.location_checks, team, slot)
@@ -1203,6 +1221,10 @@ class CommonCommandProcessor(CommandProcessor):
timer = int(seconds, 10)
except ValueError:
timer = 10
else:
if timer > 60 * 60:
raise ValueError(f"{timer} is invalid. Maximum is 1 hour.")
async_start(countdown(self.ctx, timer))
return True
@@ -1350,10 +1372,10 @@ class ClientMessageProcessor(CommonCommandProcessor):
def _cmd_remaining(self) -> bool:
"""List remaining items in your game, but not their location or recipient"""
if self.ctx.remaining_mode == "enabled":
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
if remaining_item_ids:
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id]
for item_id in remaining_item_ids))
rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot)
if rest_locations:
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id]
for slot, item_id in rest_locations))
else:
self.output("No remaining items found.")
return True
@@ -1363,10 +1385,10 @@ class ClientMessageProcessor(CommonCommandProcessor):
return False
else: # is goal
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
if remaining_item_ids:
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id]
for item_id in remaining_item_ids))
rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot)
if rest_locations:
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id]
for slot, item_id in rest_locations))
else:
self.output("No remaining items found.")
return True
@@ -2039,6 +2061,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
item_name, usable, response = get_intended_text(item_name, names)
if usable:
amount: int = int(amount)
if amount > 100:
raise ValueError(f"{amount} is invalid. Maximum is 100.")
new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))]
send_items_to(self.ctx, team, slot, *new_items)

View File

@@ -79,6 +79,7 @@ class NetworkItem(typing.NamedTuple):
item: int
location: int
player: int
""" Sending player, except in LocationInfo (from LocationScouts), where it is the receiving player. """
flags: int = 0
@@ -397,12 +398,12 @@ class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tu
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]:
) -> typing.List[typing.Tuple[int, 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])
return sorted([(player_locations[location_id][1], 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

View File

@@ -1236,6 +1236,7 @@ class CommonOptions(metaclass=OptionsMetaProperty):
:param option_names: names of the options to return
:param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`
"""
assert option_names, "options.as_dict() was used without any option names."
option_results = {}
for option_name in option_names:
if option_name in type(self).type_hints:
@@ -1517,31 +1518,3 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f:
f.write(res)
if __name__ == "__main__":
from worlds.alttp.Options import Logic
import argparse
map_shuffle = Toggle
compass_shuffle = Toggle
key_shuffle = Toggle
big_key_shuffle = Toggle
hints = Toggle
test = argparse.Namespace()
test.logic = Logic.from_text("no_logic")
test.map_shuffle = map_shuffle.from_text("ON")
test.hints = hints.from_text('OFF')
try:
test.logic = Logic.from_text("overworld_glitches_typo")
except KeyError as e:
print(e)
try:
test.logic_owg = Logic.from_text("owg")
except KeyError as e:
print(e)
if test.map_shuffle:
print("map_shuffle is on")
print(f"Hints are {bool(test.hints)}")
print(test)

View File

@@ -73,6 +73,9 @@ Currently, the following games are supported:
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
* A Hat in Time
* Old School Runescape
* Kingdom Hearts 1
* Mega Man 2
* Yacht Dice
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,3 +1,4 @@
import argparse
import os
import multiprocessing
import logging
@@ -31,6 +32,15 @@ def get_app() -> "Flask":
import yaml
app.config.from_file(configpath, yaml.safe_load)
logging.info(f"Updated config from {configpath}")
# inside get_app() so it's usable in systems like gunicorn, which do not run WebHost.py, but import it.
parser = argparse.ArgumentParser()
parser.add_argument('--config_override', default=None,
help="Path to yaml config file that overrules config.yaml.")
args = parser.parse_known_args()[0]
if args.config_override:
import yaml
app.config.from_file(os.path.abspath(args.config_override), yaml.safe_load)
logging.info(f"Updated config from {args.config_override}")
if not app.config["HOST_ADDRESS"]:
logging.info("Getting public IP, as HOST_ADDRESS is empty.")
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()

View File

@@ -72,6 +72,14 @@ class WebHostContext(Context):
self.video = {}
self.tags = ["AP", "WebHost"]
def __del__(self):
try:
import psutil
from Utils import format_SI_prefix
self.logger.debug(f"Context destroyed, Mem: {format_SI_prefix(psutil.Process().memory_info().rss, 1024)}iB")
except ImportError:
self.logger.debug("Context destroyed")
def _load_game_data(self):
for key, value in self.static_server_data.items():
# NOTE: attributes are mutable and shared, so they will have to be copied before being modified
@@ -249,6 +257,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
ctx = WebHostContext(static_server_data, logger)
ctx.load(room_id)
ctx.init_save()
assert ctx.server is None
try:
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
@@ -279,6 +288,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
ctx.auto_shutdown = Room.get(id=room_id).timeout
if ctx.saving:
setattr(asyncio.current_task(), "save", lambda: ctx._save(True))
assert ctx.shutdown_task is None
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
await ctx.shutdown_task
@@ -325,7 +335,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
def run(self):
while 1:
next_room = rooms_to_run.get(block=True, timeout=None)
gc.collect(0)
gc.collect()
task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
self._tasks.append(task)
task.add_done_callback(self._done)

View File

@@ -1,10 +1,11 @@
flask>=3.0.3
werkzeug>=3.0.3
pony>=0.7.17
werkzeug>=3.0.4
pony>=0.7.19
waitress>=3.0.0
Flask-Caching>=2.3.0
Flask-Compress>=1.15
Flask-Limiter>=3.7.0
Flask-Limiter>=3.8.0
bokeh>=3.1.1; python_version <= '3.8'
bokeh>=3.4.1; python_version >= '3.9'
bokeh>=3.4.3; python_version == '3.9'
bokeh>=3.5.2; python_version >= '3.10'
markupsafe>=2.1.5

View File

@@ -138,7 +138,7 @@
id="{{ option_name }}-{{ key }}"
name="{{ option_name }}||{{ key }}"
value="1"
checked="{{ "checked" if key in option.default else "" }}"
{{ "checked" if key in option.default }}
/>
<label for="{{ option_name }}-{{ key }}">
{{ key }}

View File

@@ -287,15 +287,15 @@ cdef class LocationStore:
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]:
def get_remaining(self, state: State, team: int, slot: int) -> List[Tuple[int, 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])
return sorted([(entry.receiver, entry.item) for
entry in self.entries[start:start+count] if
entry.location not in checked])
@cython.auto_pickle(False)

View File

@@ -78,6 +78,9 @@
# Kirby's Dream Land 3
/worlds/kdl3/ @Silvris
# Kingdom Hearts
/worlds/kh1/ @gaithern
# Kingdom Hearts 2
/worlds/kh2/ @JaredWeakStrike
@@ -103,6 +106,9 @@
# Minecraft
/worlds/minecraft/ @KonoTyran @espeon65536
# Mega Man 2
/worlds/mm2/ @Silvris
# MegaMan Battle Network 3
/worlds/mmbn3/ @digiholic
@@ -196,6 +202,9 @@
# The Witness
/worlds/witness/ @NewSoupVi @blastron
# Yacht Dice
/worlds/yachtdice/ @spinerak
# Yoshi's Island
/worlds/yoshisisland/ @PinkSwitch

View File

@@ -702,14 +702,18 @@ GameData is a **dict** but contains these keys and values. It's broken out into
| checksum | str | A checksum hash of this game's data. |
### Tags
Tags are represented as a list of strings, the common Client tags follow:
Tags are represented as a list of strings, the common client tags follow:
| Name | Notes |
|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. |
| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets |
| Tracker | Tells the server that this client will not send locations and is actually a Tracker. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
| TextOnly | Tells the server that this client will not send locations and is intended for chat. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
| Name | Notes |
|-----------|--------------------------------------------------------------------------------------------------------------------------------------|
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. |
| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets. |
| HintGame | Indicates the client is a hint game, made to send hints instead of locations. Special join/leave message,¹ `game` is optional.² |
| Tracker | Indicates the client is a tracker, made to track instead of sending locations. Special join/leave message,¹ `game` is optional.² |
| TextOnly | Indicates the client is a basic client, made to chat instead of sending locations. Special join/leave message,¹ `game` is optional.² |
¹: When connecting or disconnecting, the chat message shows e.g. "tracking".\
²: Allows `game` to be empty or null in [Connect](#connect). Game and version validation will then be skipped.
### DeathLink
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:

View File

@@ -8,7 +8,7 @@ use that version. These steps are for developers or platforms without compiled r
What you'll need:
* [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
* **Python 3.12 is currently unsupported**
* Python 3.12.x is currently the newest supported version
* pip: included in downloads from python.org, separate in many Linux distributions
* Matching C compiler
* possibly optional, read operating system specific sections
@@ -31,7 +31,7 @@ After this, you should be able to run the programs.
Recommended steps
* Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads)
* **Python 3.12 is currently unsupported**
* [read above](#General) which versions are supported
* **Optional**: Download and install Visual Studio Build Tools from
[Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/).

View File

@@ -186,6 +186,11 @@ Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Arc
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apmm2"; ValueData: "{#MyAppName}mm2patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mm2patch"; ValueData: "Archipelago Mega Man 2 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mm2patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mm2patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";

View File

@@ -5,6 +5,8 @@ import typing
import re
from collections import deque
assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility"
if sys.platform == "win32":
import ctypes

View File

@@ -1,14 +1,14 @@
colorama>=0.4.6
websockets>=12.0
PyYAML>=6.0.1
jellyfish>=1.0.3
websockets>=13.0.1
PyYAML>=6.0.2
jellyfish>=1.1.0
jinja2>=3.1.4
schema>=0.7.7
kivy>=2.3.0
bsdiff4>=1.2.4
platformdirs>=4.2.2
certifi>=2024.6.2
cython>=3.0.10
certifi>=2024.8.30
cython>=3.0.11
cymem>=2.0.8
orjson>=3.10.3
typing_extensions>=4.12.1
orjson>=3.10.7
typing_extensions>=4.12.2

View File

@@ -23,8 +23,8 @@ class TestBase(unittest.TestCase):
state = CollectionState(self.multiworld)
for item in items:
item.classification = ItemClassification.progression
state.collect(item, event=True)
state.sweep_for_events()
state.collect(item, prevent_sweep=True)
state.sweep_for_advancements()
state.update_reachable_regions(1)
self._state_cache[self.multiworld, tuple(items)] = state
return state
@@ -221,8 +221,8 @@ class WorldTestBase(unittest.TestCase):
if isinstance(items, Item):
items = (items,)
for item in items:
if item.location and item.advancement and item.location in self.multiworld.state.events:
self.multiworld.state.events.remove(item.location)
if item.location and item.advancement and item.location in self.multiworld.state.advancements:
self.multiworld.state.advancements.remove(item.location)
self.multiworld.state.remove(item)
def can_reach_location(self, location: str) -> bool:
@@ -293,13 +293,11 @@ class WorldTestBase(unittest.TestCase):
if not (self.run_default_tests and self.constructed):
return
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
excluded = self.multiworld.worlds[self.player].options.exclude_locations.value
state = self.multiworld.get_all_state(False)
for location in self.multiworld.get_locations():
if location.name not in excluded:
with self.subTest("Location should be reached", location=location.name):
reachable = location.can_reach(state)
self.assertTrue(reachable, f"{location.name} unreachable")
with self.subTest("Location should be reached", location=location.name):
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

@@ -192,7 +192,7 @@ class TestFillRestrictive(unittest.TestCase):
location_pool = player1.locations[1:] + player2.locations
item_pool = player1.prog_items[:-1] + player2.prog_items
fill_restrictive(multiworld, multiworld.state, location_pool, item_pool)
multiworld.state.sweep_for_events() # collect everything
multiworld.state.sweep_for_advancements() # collect everything
# all of player2's locations and items should be accessible (not all of player1's)
for item in player2.prog_items:
@@ -443,8 +443,8 @@ class TestFillRestrictive(unittest.TestCase):
item = player1.prog_items[0]
item.code = None
location.place_locked_item(item)
multiworld.state.sweep_for_events()
multiworld.state.sweep_for_events()
multiworld.state.sweep_for_advancements()
multiworld.state.sweep_for_advancements()
self.assertTrue(multiworld.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed")
self.assertEqual(multiworld.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times")

View File

@@ -14,6 +14,18 @@ class TestBase(unittest.TestCase):
"Desert Northern Cliffs", # on top of mountain, only reachable via OWG
"Dark Death Mountain Bunny Descent Area" # OWG Mountain descent
},
# These Blasphemous regions are not reachable with default options
"Blasphemous": {
"D01Z04S13[SE]", # difficulty must be hard
"D01Z05S25[E]", # difficulty must be hard
"D02Z02S05[W]", # difficulty must be hard and purified_hand must be true
"D04Z01S06[E]", # purified_hand must be true
"D04Z02S02[NE]", # difficulty must be hard and purified_hand must be true
"D05Z01S11[SW]", # difficulty must be hard
"D06Z01S08[N]", # difficulty must be hard and purified_hand must be true
"D20Z02S11[NW]", # difficulty must be hard
"D20Z02S11[E]", # difficulty must be hard
},
"Ocarina of Time": {
"Prelude of Light Warp", # Prelude is not progression by default
"Serenade of Water Warp", # Serenade is not progression by default
@@ -37,12 +49,10 @@ class TestBase(unittest.TestCase):
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type)
excluded = multiworld.worlds[1].options.exclude_locations.value
state = multiworld.get_all_state(False)
for location in multiworld.get_locations():
if location.name not in excluded:
with self.subTest("Location should be reached", location=location.name):
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
with self.subTest("Location should be reached", location=location.name):
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
for region in multiworld.get_regions():
if region.name in unreachable_regions:

View File

@@ -55,7 +55,7 @@ class TestAllGamesMultiworld(MultiworldTestBase):
all_worlds = list(AutoWorldRegister.world_types.values())
self.multiworld = setup_multiworld(all_worlds, ())
for world in self.multiworld.worlds.values():
world.options.accessibility.value = Accessibility.option_locations
world.options.accessibility.value = Accessibility.option_full
self.assertSteps(gen_steps)
with self.subTest("filling multiworld", seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
@@ -66,8 +66,8 @@ class TestAllGamesMultiworld(MultiworldTestBase):
class TestTwoPlayerMulti(MultiworldTestBase):
def test_two_player_single_game_fills(self) -> None:
"""Tests that a multiworld of two players for each registered game world can generate."""
for world in AutoWorldRegister.world_types.values():
self.multiworld = setup_multiworld([world, world], ())
for world_type in AutoWorldRegister.world_types.values():
self.multiworld = setup_multiworld([world_type, world_type], ())
for world in self.multiworld.worlds.values():
world.options.accessibility.value = Accessibility.option_full
self.assertSteps(gen_steps)

View File

@@ -130,9 +130,9 @@ class Base:
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])
self.assertEqual(self.store.get_remaining(one_state, 0, 1), [(1, 13), (2, 21)])
self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [(1, 13), (2, 21), (2, 22)])
self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [(4, 99)])
def test_location_set_intersection(self) -> None:
locations = {10, 11, 12}

View File

@@ -132,7 +132,8 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path
break
if found_already_loaded:
raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded,\n"
"so a Launcher restart is required to use the new installation.")
"so a Launcher restart is required to use the new installation.\n"
"If the Launcher is not open, no action needs to be taken.")
world_source = worlds.WorldSource(str(target), is_zip=True)
bisect.insort(worlds.world_sources, world_source)
world_source.load()

View File

@@ -73,7 +73,12 @@ class WorldSource:
else: # TODO: remove with 3.8 support
mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0])
mod.__package__ = f"worlds.{mod.__package__}"
if mod.__package__ is not None:
mod.__package__ = f"worlds.{mod.__package__}"
else:
# load_module does not populate package, we'll have to assume mod.__name__ is correct here
# probably safe to remove with 3.8 support
mod.__package__ = f"worlds.{mod.__name__}"
mod.__name__ = f"worlds.{mod.__name__}"
sys.modules[mod.__name__] = mod
with warnings.catch_warnings():

View File

@@ -968,40 +968,35 @@ def get_act_by_number(world: "HatInTimeWorld", chapter_name: str, num: int) -> R
def create_thug_shops(world: "HatInTimeWorld"):
min_items: int = world.options.NyakuzaThugMinShopItems.value
max_items: int = world.options.NyakuzaThugMaxShopItems.value
count = -1
step = 0
old_name = ""
thug_location_counts: Dict[str, int] = {}
for key, data in shop_locations.items():
if data.nyakuza_thug == "":
thug_name = data.nyakuza_thug
if thug_name == "":
# Different shop type.
continue
if old_name != "" and old_name == data.nyakuza_thug:
if thug_name not in world.nyakuza_thug_items:
shop_item_count = world.random.randint(min_items, max_items)
world.nyakuza_thug_items[thug_name] = shop_item_count
else:
shop_item_count = world.nyakuza_thug_items[thug_name]
if shop_item_count <= 0:
continue
try:
if world.nyakuza_thug_items[data.nyakuza_thug] <= 0:
continue
except KeyError:
pass
location_count = thug_location_counts.setdefault(thug_name, 0)
if location_count >= shop_item_count:
# Already created all the locations for this thug.
continue
if count == -1:
count = world.random.randint(min_items, max_items)
world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count)
if count <= 0:
continue
if count >= 1:
region = world.multiworld.get_region(data.region, world.player)
loc = HatInTimeLocation(world.player, key, data.id, region)
region.locations.append(loc)
world.shop_locs.append(loc.name)
step += 1
if step >= count:
old_name = data.nyakuza_thug
step = 0
count = -1
# Create the shop location.
region = world.multiworld.get_region(data.region, world.player)
loc = HatInTimeLocation(world.player, key, data.id, region)
region.locations.append(loc)
world.shop_locs.append(loc.name)
thug_location_counts[thug_name] = location_count + 1
def create_events(world: "HatInTimeWorld") -> int:

View File

@@ -381,8 +381,8 @@ def set_moderate_rules(world: "HatInTimeWorld"):
lambda state: can_use_hat(state, world, HatType.ICE), "or")
# Moderate: Clock Tower Chest + Ruined Tower with nothing
add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
set_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
set_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
# Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell
for loc in world.multiworld.get_region("The Subcon Well", world.player).locations:
@@ -432,8 +432,8 @@ def set_moderate_rules(world: "HatInTimeWorld"):
if world.is_dlc1():
# Moderate: clear Rock the Boat without Ice Hat
add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
set_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
set_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
# Moderate: clear Deep Sea without Ice Hat
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player),
@@ -855,6 +855,9 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]):
for entrance in regions["Time Rift - Alpine Skyline"].entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
if entrance.parent_region.name == "Alpine Free Roam":
add_rule(entrance,
lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True))
if world.is_dlc1():
for entrance in regions["Time Rift - Balcony"].entrances:
@@ -933,6 +936,9 @@ def set_default_rift_rules(world: "HatInTimeWorld"):
for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
if entrance.parent_region.name == "Alpine Free Roam":
add_rule(entrance,
lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True))
if world.is_dlc1():
for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances:

View File

@@ -248,7 +248,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
pass
for item in pre_fill_items:
multiworld.worlds[item.player].collect(all_state_base, item)
all_state_base.sweep_for_events()
all_state_base.sweep_for_advancements()
# Remove completion condition so that minimal-accessibility worlds place keys properly
for player in {item.player for item in in_dungeon_items}:
@@ -262,8 +262,8 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
all_state_base.remove(item_factory(key_data[3], multiworld.worlds[player]))
loc = multiworld.get_location(key_loc, player)
if loc in all_state_base.events:
all_state_base.events.remove(loc)
if loc in all_state_base.advancements:
all_state_base.advancements.remove(loc)
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, lock=True, allow_excluded=True,
name="LttP Dungeon Items")

View File

@@ -682,7 +682,7 @@ def get_pool_core(world, player: int):
if 'triforce_hunt' in goal:
if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra:
treasure_hunt_total = (world.triforce_pieces_available[player].value
treasure_hunt_total = (world.triforce_pieces_required[player].value
+ world.triforce_pieces_extra[player].value)
elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage:
percentage = float(world.triforce_pieces_percentage[player].value) / 100

View File

@@ -412,7 +412,7 @@ def global_rules(multiworld: MultiWorld, player: int):
lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player),
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
if multiworld.accessibility[player] != 'locations' and not multiworld.key_drop_shuffle[player]:
if multiworld.accessibility[player] != 'full' and not multiworld.key_drop_shuffle[player]:
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
@@ -547,7 +547,7 @@ def global_rules(multiworld: MultiWorld, player: int):
location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
# this seemed to be causing generation failure, disable for now
# if world.accessibility[player] != 'locations':
# if world.accessibility[player] != 'full':
# set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
# It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.

View File

@@ -356,6 +356,8 @@ class ALTTPWorld(World):
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
if option == "original_dungeon":
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
else:
self.options.local_items.value |= self.dungeon_local_item_names
self.difficulty_requirements = difficulties[multiworld.item_pool[player].current_key]

View File

@@ -2,8 +2,8 @@
## Configuration
1. Plando features have to be enabled first, before they can be used (opt-in).
2. To do so, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml
1. All plando options are enabled by default, except for "items plando" which has to be enabled before it can be used (opt-in).
2. To enable it, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml
file with a text editor.
3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the value
to `bosses, items, texts, connections`
@@ -66,6 +66,7 @@ boss_shuffle:
- ignored if only one world is generated
- can be a number, to target that slot in the multiworld
- can be a name, to target that player's world
- can be a list of names, to target those players' worlds
- can be true, to target any other player's world
- can be false, to target own world and is the default
- can be null, to target a random world
@@ -132,17 +133,15 @@ plando_items:
### Texts
- This module is disabled by default.
- Has the options `text`, `at`, and `percentage`
- All of these options support subweights
- percentage is the percentage chance for this text to be placed, can be omitted entirely for 100%
- text is the text to be placed.
- can be weighted.
- `\n` is a newline.
- `@` is the entered player's name.
- Warning: Text Mapper does not support full unicode.
- [Alphabet](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L758)
- at is the location within the game to attach the text to.
- can be weighted.
- [List of targets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L1499)
#### Example
@@ -162,7 +161,6 @@ and `uncle_dying_sewer`, then places the text "This is a plando. You've been war
### Connections
- This module is disabled by default.
- Has the options `percentage`, `entrance`, `exit` and `direction`.
- All options support subweights
- percentage is the percentage chance for this to be connected, can be omitted entirely for 100%

View File

@@ -54,7 +54,7 @@ class TestDungeon(LTTPTestBase):
for item in items:
item.classification = ItemClassification.progression
state.collect(item, event=True) # event=True prevents running sweep_for_events() and picking up
state.sweep_for_events() # key drop keys repeatedly
state.collect(item, prevent_sweep=True) # prevent_sweep=True prevents running sweep_for_advancements() and picking up
state.sweep_for_advancements() # key drop keys repeatedly
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")

View File

@@ -0,0 +1,60 @@
from unittest import TestCase
from BaseClasses import MultiWorld
from test.general import gen_steps, setup_multiworld
from worlds.AutoWorld import call_all
from worlds.generic.Rules import locality_rules
from ... import ALTTPWorld
from ...Options import DungeonItem
class DungeonFillTestBase(TestCase):
multiworld: MultiWorld
world_1: ALTTPWorld
world_2: ALTTPWorld
options = (
"big_key_shuffle",
"small_key_shuffle",
"key_drop_shuffle",
"compass_shuffle",
"map_shuffle",
)
def setUp(self):
self.multiworld = setup_multiworld([ALTTPWorld, ALTTPWorld], ())
self.world_1 = self.multiworld.worlds[1]
self.world_2 = self.multiworld.worlds[2]
def generate_with_options(self, option_value: int):
for option in self.options:
getattr(self.world_1.options, option).value = getattr(self.world_2.options, option).value = option_value
for step in gen_steps:
call_all(self.multiworld, step)
# this is where locality rules are set in normal generation which we need to verify this test
if step == "set_rules":
locality_rules(self.multiworld)
def test_original_dungeons(self):
self.generate_with_options(DungeonItem.option_original_dungeon)
for location in self.multiworld.get_filled_locations():
with (self.subTest(location=location)):
if location.parent_region.dungeon is None:
self.assertIs(location.item.dungeon, None)
else:
self.assertEqual(location.player, location.item.player,
f"{location.item} does not belong to {location}'s player")
if location.item.dungeon is None:
continue
self.assertIs(location.item.dungeon, location.parent_region.dungeon,
f"{location.item} was not placed in its original dungeon.")
def test_own_dungeons(self):
self.generate_with_options(DungeonItem.option_own_dungeons)
for location in self.multiworld.get_filled_locations():
with self.subTest(location=location):
if location.parent_region.dungeon is None:
self.assertIs(location.item.dungeon, None)
else:
self.assertEqual(location.player, location.item.player,
f"{location.item} does not belong to {location}'s player")

View File

@@ -4,7 +4,7 @@ from BaseClasses import Tutorial
from ..AutoWorld import WebWorld, World
class AP_SudokuWebWorld(WebWorld):
options_page = "games/Sudoku/info/en"
options_page = False
theme = 'partyTime'
setup_en = Tutorial(

View File

@@ -1,9 +1,7 @@
# APSudoku Setup Guide
## Required Software
- [APSudoku](https://github.com/EmilyV99/APSudoku)
- Windows (most tested on Win10)
- Other platforms might be able to build from source themselves; and may be included in the future.
- [APSudoku](https://github.com/APSudoku/APSudoku)
## General Concept
@@ -13,25 +11,33 @@ Does not need to be added at the start of a seed, as it does not create any slot
## Installation Procedures
Go to the latest release from the [APSudoku Releases page](https://github.com/EmilyV99/APSudoku/releases). Download and extract the `APSudoku.zip` file.
Go to the latest release from the [APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform.
## Joining a MultiWorld Game
1. Run APSudoku.exe
2. Under the 'Archipelago' tab at the top-right:
- Enter the server url & port number
1. Run the APSudoku executable.
2. Under `Settings` &rarr; `Connection` at the top-right:
- Enter the server address and port number
- Enter the name of the slot you wish to connect to
- Enter the room password (optional)
- Select DeathLink related settings (optional)
- Press connect
3. Go back to the 'Sudoku' tab
- Click the various '?' buttons for information on how to play / control
4. Choose puzzle difficulty
5. Try to solve the Sudoku. Click 'Check' when done.
- Press `Connect`
4. Under the `Sudoku` tab
- Choose puzzle difficulty
- Click `Start` to generate a puzzle
5. Try to solve the Sudoku. Click `Check` when done
- A correct solution rewards you with 1 hint for a location in the world you are connected to
- An incorrect solution has no penalty, unless DeathLink is enabled (see below)
Info:
- You can set various settings under `Settings` &rarr; `Sudoku`, and can change the colors used under `Settings` &rarr; `Theme`.
- While connected, you can view the `Console` and `Hints` tabs for standard TextClient-like features
- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md)
- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted)
- Click the various `?` buttons for information on controls/how to play
## DeathLink Support
If 'DeathLink' is enabled when you click 'Connect':
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or quit a puzzle without solving it (including disconnecting).
- Life count customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
If `DeathLink` is enabled when you click `Connect`:
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or if you quit a puzzle without solving it (including disconnecting).
- Your life count is customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
- On receiving a DeathLink from another player, your puzzle resets.

View File

@@ -0,0 +1,19 @@
{
"type": "WorldDefinition",
"configuration": "./output/StringWorldDefinition.json",
"emptyRegionsToKeep": [
"D17Z01S01",
"D01Z02S01",
"D02Z03S09",
"D03Z03S11",
"D04Z03S01",
"D06Z01S09",
"D20Z02S09",
"D09Z01S09[Cell24]",
"D09Z01S08[Cell7]",
"D09Z01S08[Cell18]",
"D09BZ01S01[Cell24]",
"D09BZ01S01[Cell17]",
"D09BZ01S01[Cell19]"
]
}

View File

@@ -637,52 +637,35 @@ 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",
"wounds" : {"Holy Wound of Attrition",
"Holy Wound of Contrition",
"Holy Wound of Compunction"],
"Holy Wound of Compunction"},
"masks" : ["Deformed Mask of Orestes",
"masks" : {"Deformed Mask of Orestes",
"Mirrored Mask of Dolphos",
"Embossed Mask of Crescente"],
"Embossed Mask of Crescente"},
"marks" : ["Mark of the First Refuge",
"marks" : {"Mark of the First Refuge",
"Mark of the Second Refuge",
"Mark of the Third Refuge"],
"Mark of the Third Refuge"},
"tirso" : ["Bouquet of Rosemary",
"tirso" : {"Bouquet of Rosemary",
"Incense Garlic",
"Olive Seeds",
"Dried Clove",
"Sooty Garlic",
"Bouquet of Thyme"],
"Bouquet of Thyme"},
"tentudia": ["Tentudia's Carnal Remains",
"tentudia": {"Tentudia's Carnal Remains",
"Remains of Tentudia's Hair",
"Tentudia's Skeletal Remains"],
"Tentudia's Skeletal Remains"},
"egg" : ["Melted Golden Coins",
"egg" : {"Melted Golden Coins",
"Torn Bridal Ribbon",
"Black Grieving Veil"],
"Black Grieving Veil"},
"bones" : ["Parietal bone of Lasser, the Inquisitor",
"bones" : {"Parietal bone of Lasser, the Inquisitor",
"Jaw of Ashgan, the Inquisitor",
"Cervical vertebra of Zicher, the Brewmaster",
"Clavicle of Dalhuisen, the Schoolchild",
@@ -725,14 +708,14 @@ group_table: Dict[str, Set[str]] = {
"Scaphoid of Fierce, the Leper",
"Anklebone of Weston, the Pilgrim",
"Calcaneum of Persian, the Bandit",
"Navicular of Kahnnyhoo, the Murderer"],
"Navicular of Kahnnyhoo, the Murderer"},
"power" : ["Life Upgrade",
"power" : {"Life Upgrade",
"Fervour Upgrade",
"Empty Bile Vessel",
"Quicksilver"],
"Quicksilver"},
"prayer" : ["Seguiriya to your Eyes like Stars",
"prayer" : {"Seguiriya to your Eyes like Stars",
"Debla of the Lights",
"Saeta Dolorosa",
"Campanillero to the Sons of the Aurora",
@@ -746,10 +729,17 @@ group_table: Dict[str, Set[str]] = {
"Romance to the Crimson Mist",
"Zambra to the Resplendent Crown",
"Cantina of the Blue Rose",
"Mirabras of the Return to Port"]
"Mirabras of the Return to Port"},
"toe" : {"Little Toe made of Limestone",
"Big Toe made of Limestone",
"Fourth Toe made of Limestone"},
"eye" : {"Severed Right Eye of the Traitor",
"Broken Left Eye of the Traitor"}
}
tears_set: Set[str] = [
tears_list: List[str] = [
"Tears of Atonement (500)",
"Tears of Atonement (625)",
"Tears of Atonement (750)",
@@ -772,16 +762,16 @@ tears_set: Set[str] = [
"Tears of Atonement (30000)"
]
reliquary_set: Set[str] = [
reliquary_set: Set[str] = {
"Reliquary of the Fervent Heart",
"Reliquary of the Suffering Heart",
"Reliquary of the Sorrowful Heart"
]
}
skill_set: Set[str] = [
skill_set: Set[str] = {
"Combo Skill",
"Charged Skill",
"Ranged Skill",
"Dive Skill",
"Lunge Skill"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool
from dataclasses import dataclass
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, OptionGroup
import random
@@ -20,23 +21,30 @@ class ChoiceIsRandom(Choice):
class PrieDieuWarp(DefaultOnToggle):
"""Automatically unlocks the ability to warp between Prie Dieu shrines."""
"""
Automatically unlocks the ability to warp between Prie Dieu shrines.
"""
display_name = "Unlock Fast Travel"
class SkipCutscenes(DefaultOnToggle):
"""Automatically skips most cutscenes."""
"""
Automatically skips most cutscenes.
"""
display_name = "Auto Skip Cutscenes"
class CorpseHints(DefaultOnToggle):
"""Changes the 34 corpses in game to give various hints about item locations."""
"""
Changes the 34 corpses in game to give various hints about item locations.
"""
display_name = "Corpse Hints"
class Difficulty(Choice):
"""Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses
and advanced movement tricks or glitches."""
"""
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
@@ -45,15 +53,18 @@ class Difficulty(Choice):
class Penitence(Toggle):
"""Allows one of the three Penitences to be chosen at the beginning of the game."""
"""
Allows one of the three Penitences to be chosen at the beginning of the game.
"""
display_name = "Penitence"
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."""
"""
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
@@ -66,10 +77,15 @@ class StartingLocation(ChoiceIsRandom):
class Ending(Choice):
"""Choose which ending is required to complete the game.
"""
Choose which ending is required to complete the game.
Talking to Tirso in Albero will tell you the selected ending for the current game.
Ending A: Collect all thorn upgrades.
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation."""
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation.
"""
display_name = "Ending"
option_any_ending = 0
option_ending_a = 1
@@ -78,14 +94,18 @@ class Ending(Choice):
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\""""
"""
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."""
"""
Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool.
"""
display_name = "Shuffle Thorn"
option_anywhere = 0
option_local_only = 1
@@ -94,50 +114,68 @@ class ThornShuffle(Choice):
class DashShuffle(Toggle):
"""Turns the ability to dash into an item that must be found in the multiworld."""
"""
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."""
"""
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."""
"""
Adds the True Torment exclusive Reliquary rosary beads into the item pool.
"""
display_name = "Shuffle Penitence Rewards"
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."""
"""
Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes and survive.
Must have the "Boots of Pleading" mod installed to connect to a multiworld.
"""
display_name = "Boots of Pleading"
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."""
"""
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 "Double Jump" mod installed to connect to a multiworld.
"""
display_name = "Purified Hand of the Nun"
class StartWheel(Toggle):
"""Changes the beginning gift to The Young Mason's Wheel."""
"""
Changes the beginning gift to The Young Mason's Wheel.
"""
display_name = "Start with Wheel"
class SkillRando(Toggle):
"""Randomizes the abilities from the skill tree into the item pool."""
"""
Randomizes the abilities from the skill tree into the item pool.
"""
display_name = "Skill Randomizer"
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.
"""
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.
Randomized: Every enemy is completely random, and can appear any number of times.
Some enemies will never be randomized."""
Some enemies will never be randomized.
"""
display_name = "Enemy Randomizer"
option_disabled = 0
option_shuffled = 1
@@ -146,43 +184,75 @@ class EnemyRando(Choice):
class EnemyGroups(DefaultOnToggle):
"""Randomized enemies will chosen from sets of specific groups.
"""
Randomized enemies will be chosen from sets of specific groups.
(Weak, normal, large, flying)
Has no effect if Enemy Randomizer is disabled."""
Has no effect if Enemy Randomizer is disabled.
"""
display_name = "Enemy Groups"
class EnemyScaling(DefaultOnToggle):
"""Randomized enemies will have their stats increased or decreased depending on the area they appear in.
Has no effect if Enemy Randomizer is disabled."""
"""
Randomized enemies will have their stats increased or decreased depending on the area they appear in.
Has no effect if Enemy Randomizer is disabled.
"""
display_name = "Enemy Scaling"
class BlasphemousDeathLink(DeathLink):
"""When you die, everyone dies. The reverse is also true.
Note that Guilt Fragments will not appear when killed by Death Link."""
"""
When you die, everyone dies. The reverse is also true.
Note that Guilt Fragments will not appear when killed by Death Link.
"""
blasphemous_options = {
"prie_dieu_warp": PrieDieuWarp,
"skip_cutscenes": SkipCutscenes,
"corpse_hints": CorpseHints,
"difficulty": Difficulty,
"penitence": Penitence,
"starting_location": StartingLocation,
"ending": Ending,
"skip_long_quests": SkipLongQuests,
"thorn_shuffle" : ThornShuffle,
"dash_shuffle": DashShuffle,
"wall_climb_shuffle": WallClimbShuffle,
"reliquary_shuffle": ReliquaryShuffle,
"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,
"start_inventory": StartInventoryPool
}
@dataclass
class BlasphemousOptions(PerGameCommonOptions):
prie_dieu_warp: PrieDieuWarp
skip_cutscenes: SkipCutscenes
corpse_hints: CorpseHints
difficulty: Difficulty
penitence: Penitence
starting_location: StartingLocation
ending: Ending
skip_long_quests: SkipLongQuests
thorn_shuffle: ThornShuffle
dash_shuffle: DashShuffle
wall_climb_shuffle: WallClimbShuffle
reliquary_shuffle: ReliquaryShuffle
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
blas_option_groups = [
OptionGroup("Quality of Life", [
PrieDieuWarp,
SkipCutscenes,
CorpseHints,
SkipLongQuests,
StartWheel
]),
OptionGroup("Moveset", [
DashShuffle,
WallClimbShuffle,
SkillRando,
CustomItem1,
CustomItem2
]),
OptionGroup("Enemy Randomizer", [
EnemyRando,
EnemyGroups,
EnemyScaling
])
]

View File

@@ -0,0 +1,582 @@
# Preprocessor to convert Blasphemous Randomizer logic into a StringWorldDefinition for use with APHKLogicExtractor
# https://github.com/BrandenEK/Blasphemous.Randomizer
# https://github.com/ArchipelagoMW-HollowKnight/APHKLogicExtractor
import json, requests, argparse
from typing import List, Dict, Any
def load_resource_local(file: str) -> List[Dict[str, Any]]:
print(f"Reading from {file}")
loaded = []
with open(file, encoding="utf-8") as f:
loaded = read_json(f.readlines())
f.close()
return loaded
def load_resource_from_web(url: str) -> List[Dict[str, Any]]:
req = requests.get(url, timeout=1)
print(f"Reading from {url}")
req.encoding = "utf-8"
lines: List[str] = []
for line in req.text.splitlines():
while "\t" in line:
line = line[1::]
if line != "":
lines.append(line)
return read_json(lines)
def read_json(lines: List[str]) -> List[Dict[str, Any]]:
loaded = []
creating_object: bool = False
obj: str = ""
for line in lines:
stripped = line.strip()
if "{" in stripped:
creating_object = True
obj += stripped
continue
elif "}," in stripped or "}" in stripped and "]" in lines[lines.index(line)+1]:
creating_object = False
obj += "}"
#print(f"obj = {obj}")
loaded.append(json.loads(obj))
obj = ""
continue
if not creating_object:
continue
else:
try:
if "}," in lines[lines.index(line)+1] and stripped[-1] == ",":
obj += stripped[:-1]
else:
obj += stripped
except IndexError:
obj += stripped
return loaded
def get_room_from_door(door: str) -> str:
return door[:door.find("[")]
def preprocess_logic(is_door: bool, id: str, logic: str) -> str:
if id in logic and not is_door:
index: int = logic.find(id)
logic = logic[:index] + logic[index+len(id)+4:]
while ">=" in logic:
index: int = logic.find(">=")
logic = logic[:index-1] + logic[index+3:]
while ">" in logic:
index: int = logic.find(">")
count = int(logic[index+2])
count += 1
logic = logic[:index-1] + str(count) + logic[index+3:]
while "<=" in logic:
index: int = logic.find("<=")
logic = logic[:index-1] + logic[index+3:]
while "<" in logic:
index: int = logic.find("<")
count = int(logic[index+2])
count += 1
logic = logic[:index-1] + str(count) + logic[index+3:]
#print(logic)
return logic
def build_logic_conditions(logic: str) -> List[List[str]]:
all_conditions: List[List[str]] = []
parts = logic.split()
sub_part: str = ""
current_index: int = 0
parens: int = -1
current_condition: List[str] = []
parens_conditions: List[List[List[str]]] = []
for index, part in enumerate(parts):
#print(current_index, index, parens, part)
# skip parts that have already been handled
if index < current_index:
continue
# break loop if reached final part
try:
parts[index+1]
except IndexError:
#print("INDEXERROR", part)
if parens < 0:
current_condition.append(part)
if len(parens_conditions) > 0:
for i in parens_conditions:
for j in i:
all_conditions.append(j + current_condition)
else:
all_conditions.append(current_condition)
break
#print(current_condition, parens, sub_part)
# prepare for subcondition
if "(" in part:
# keep track of nested parentheses
if parens == -1:
parens = 0
for char in part:
if char == "(":
parens += 1
# add to sub part
if sub_part == "":
sub_part = part
else:
sub_part += f" {part}"
#if not ")" in part:
continue
# end of subcondition
if ")" in part:
# read every character in case of multiple closing parentheses
for char in part:
if char == ")":
parens -= 1
sub_part += f" {part}"
# if reached end of parentheses, handle subcondition
if parens == 0:
#print(current_condition, sub_part)
parens = -1
try:
parts[index+1]
except IndexError:
#print("END OF LOGIC")
if len(parens_conditions) > 0:
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
#print("PARENS:", parens_conditions)
temp_conditions: List[List[str]] = []
for i in parens_conditions[0]:
for j in parens_conditions[1]:
temp_conditions.append(i + j)
parens_conditions.pop(0)
parens_conditions.pop(0)
while len(parens_conditions) > 0:
temp_conditions2 = temp_conditions
temp_conditions = []
for k in temp_conditions2:
for l in parens_conditions[0]:
temp_conditions.append(k + l)
parens_conditions.pop(0)
#print("TEMP:", remove_duplicates(temp_conditions))
all_conditions += temp_conditions
else:
all_conditions += build_logic_subconditions(current_condition, sub_part)
else:
#print("NEXT PARTS:", parts[index+1], parts[index+2])
if parts[index+1] == "&&":
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
#print("PARENS:", parens_conditions)
else:
if len(parens_conditions) > 0:
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
#print("PARENS:", parens_conditions)
temp_conditions: List[List[str]] = []
for i in parens_conditions[0]:
for j in parens_conditions[1]:
temp_conditions.append(i + j)
parens_conditions.pop(0)
parens_conditions.pop(0)
while len(parens_conditions) > 0:
temp_conditions2 = temp_conditions
temp_conditions = []
for k in temp_conditions2:
for l in parens_conditions[0]:
temp_conditions.append(k + l)
parens_conditions.pop(0)
#print("TEMP:", remove_duplicates(temp_conditions))
all_conditions += temp_conditions
else:
all_conditions += build_logic_subconditions(current_condition, sub_part)
current_index = index+2
current_condition = []
sub_part = ""
continue
# collect all parts until reaching end of parentheses
if parens > 0:
sub_part += f" {part}"
continue
current_condition.append(part)
# continue with current condition
if parts[index+1] == "&&":
current_index = index+2
continue
# add condition to list and start new one
elif parts[index+1] == "||":
if len(parens_conditions) > 0:
for i in parens_conditions:
for j in i:
all_conditions.append(j + current_condition)
parens_conditions = []
else:
all_conditions.append(current_condition)
current_condition = []
current_index = index+2
continue
return remove_duplicates(all_conditions)
def build_logic_subconditions(current_condition: List[str], subcondition: str) -> List[List[str]]:
#print("STARTED SUBCONDITION", current_condition, subcondition)
subconditions = build_logic_conditions(subcondition[1:-1])
final_conditions = []
for condition in subconditions:
final_condition = current_condition + condition
final_conditions.append(final_condition)
#print("ENDED SUBCONDITION")
#print(final_conditions)
return final_conditions
def remove_duplicates(conditions: List[List[str]]) -> List[List[str]]:
final_conditions: List[List[str]] = []
for condition in conditions:
final_conditions.append(list(dict.fromkeys(condition)))
return final_conditions
def handle_door_visibility(door: Dict[str, Any]) -> Dict[str, Any]:
if door.get("visibilityFlags") == None:
return door
else:
flags: List[str] = str(door.get("visibilityFlags")).split(", ")
#print(flags)
temp_flags: List[str] = []
this_door: bool = False
#required_doors: str = ""
if "ThisDoor" in flags:
this_door = True
#if "requiredDoors" in flags:
# required_doors: str = " || ".join(door.get("requiredDoors"))
if "DoubleJump" in flags:
temp_flags.append("DoubleJump")
if "NormalLogic" in flags:
temp_flags.append("NormalLogic")
if "NormalLogicAndDoubleJump" in flags:
temp_flags.append("NormalLogicAndDoubleJump")
if "HardLogic" in flags:
temp_flags.append("HardLogic")
if "HardLogicAndDoubleJump" in flags:
temp_flags.append("HardLogicAndDoubleJump")
if "EnemySkips" in flags:
temp_flags.append("EnemySkips")
if "EnemySkipsAndDoubleJump" in flags:
temp_flags.append("EnemySkipsAndDoubleJump")
# remove duplicates
temp_flags = list(dict.fromkeys(temp_flags))
original_logic: str = door.get("logic")
temp_logic: str = ""
if this_door:
temp_logic = door.get("id")
if temp_flags != []:
if temp_logic != "":
temp_logic += " || "
temp_logic += ' && '.join(temp_flags)
if temp_logic != "" and original_logic != None:
if len(original_logic.split()) == 1:
if len(temp_logic.split()) == 1:
door["logic"] = f"{temp_logic} && {original_logic}"
else:
door["logic"] = f"({temp_logic}) && {original_logic}"
else:
if len(temp_logic.split()) == 1:
door["logic"] = f"{temp_logic} && ({original_logic})"
else:
door["logic"] = f"({temp_logic}) && ({original_logic})"
elif temp_logic != "" and original_logic == None:
door["logic"] = temp_logic
return door
def get_state_provider_for_condition(condition: List[str]) -> str:
for item in condition:
if (item[0] == "D" and item[3] == "Z" and item[6] == "S")\
or (item[0] == "D" and item[3] == "B" and item[4] == "Z" and item[7] == "S"):
return item
return None
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument('-l', '--local', action="store_true", help="Use local files in the same directory instead of reading resource files from the BrandenEK/Blasphemous-Randomizer repository.")
args = parser.parse_args()
return args
def main(args: argparse.Namespace):
doors = []
locations = []
if (args.local):
doors = load_resource_local("doors.json")
locations = load_resource_local("locations_items.json")
else:
doors = load_resource_from_web("https://raw.githubusercontent.com/BrandenEK/Blasphemous-Randomizer/main/resources/data/Randomizer/doors.json")
locations = load_resource_from_web("https://raw.githubusercontent.com/BrandenEK/Blasphemous-Randomizer/main/resources/data/Randomizer/locations_items.json")
original_connections: Dict[str, str] = {}
rooms: Dict[str, List[str]] = {}
output: Dict[str, Any] = {}
logic_objects: List[Dict[str, Any]] = []
for door in doors:
if door.get("originalDoor") != None:
if not door.get("id") in original_connections:
original_connections[door.get("id")] = door.get("originalDoor")
original_connections[door.get("originalDoor")] = door.get("id")
room: str = get_room_from_door(door.get("originalDoor"))
if not room in rooms.keys():
rooms[room] = [door.get("id")]
else:
rooms[room].append(door.get("id"))
def flip_doors_in_condition(condition: List[str]) -> List[str]:
new_condition = []
for item in condition:
if item in original_connections:
new_condition.append(original_connections[item])
else:
new_condition.append(item)
return new_condition
for room in rooms.keys():
obj = {
"Name": room,
"Logic": [],
"Handling": "Default"
}
for door in rooms[room]:
logic = {
"StateProvider": door,
"Conditions": [],
"StateModifiers": []
}
obj["Logic"].append(logic)
logic_objects.append(obj)
for door in doors:
if door.get("direction") == 5:
continue
handling: str = "Transition"
if "Cell" in door.get("id"):
handling = "Default"
obj = {
"Name": door.get("id"),
"Logic": [],
"Handling": handling
}
visibility_flags: List[str] = []
if door.get("visibilityFlags") != None:
visibility_flags = str(door.get("visibilityFlags")).split(", ")
if "1" in visibility_flags:
visibility_flags.remove("1")
visibility_flags.append("ThisDoor")
required_doors: List[str] = []
if door.get("requiredDoors"):
required_doors = door.get("requiredDoors")
if len(visibility_flags) > 0:
for flag in visibility_flags:
if flag == "RequiredDoors":
continue
if flag == "ThisDoor":
flag = original_connections[door.get("id")]
if door.get("logic") != None:
logic: str = door.get("logic")
logic = f"{flag} && ({logic})"
logic = preprocess_logic(True, door.get("id"), logic)
conditions = build_logic_conditions(logic)
for condition in conditions:
condition = flip_doors_in_condition(condition)
state_provider: str = get_room_from_door(door.get("id"))
if get_state_provider_for_condition(condition) != None:
state_provider = get_state_provider_for_condition(condition)
condition.remove(state_provider)
logic = {
"StateProvider": state_provider,
"Conditions": condition,
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
logic = {
"StateProvider": get_room_from_door(door.get("id")),
"Conditions": [flag],
"StateModifiers": []
}
obj["Logic"].append(logic)
if "RequiredDoors" in visibility_flags:
for d in required_doors:
flipped = original_connections[d]
if door.get("logic") != None:
logic: str = preprocess_logic(True, door.get("id"), door.get("logic"))
conditions = build_logic_conditions(logic)
for condition in conditions:
condition = flip_doors_in_condition(condition)
state_provider: str = flipped
if flipped in condition:
condition.remove(flipped)
logic = {
"StateProvider": state_provider,
"Conditions": condition,
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
logic = {
"StateProvider": flipped,
"Conditions": [],
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
if door.get("logic") != None:
logic: str = preprocess_logic(True, door.get("id"), door.get("logic"))
conditions = build_logic_conditions(logic)
for condition in conditions:
condition = flip_doors_in_condition(condition)
stateProvider: str = get_room_from_door(door.get("id"))
if get_state_provider_for_condition(condition) != None:
stateProvider = get_state_provider_for_condition(condition)
condition.remove(stateProvider)
logic = {
"StateProvider": stateProvider,
"Conditions": condition,
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
logic = {
"StateProvider": get_room_from_door(door.get("id")),
"Conditions": [],
"StateModifiers": []
}
obj["Logic"].append(logic)
logic_objects.append(obj)
for location in locations:
obj = {
"Name": location.get("id"),
"Logic": [],
"Handling": "Location"
}
if location.get("logic") != None:
for condition in build_logic_conditions(preprocess_logic(False, location.get("id"), location.get("logic"))):
condition = flip_doors_in_condition(condition)
stateProvider: str = location.get("room")
if get_state_provider_for_condition(condition) != None:
stateProvider = get_state_provider_for_condition(condition)
condition.remove(stateProvider)
if stateProvider == "Initial":
stateProvider = None
logic = {
"StateProvider": stateProvider,
"Conditions": condition,
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
stateProvider: str = location.get("room")
if stateProvider == "Initial":
stateProvider = None
logic = {
"StateProvider": stateProvider,
"Conditions": [],
"StateModifiers": []
}
obj["Logic"].append(logic)
logic_objects.append(obj)
output["LogicObjects"] = logic_objects
with open("StringWorldDefinition.json", "w") as file:
print("Writing to StringWorldDefinition.json")
file.write(json.dumps(output, indent=4))
if __name__ == "__main__":
main(parse_args())

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,12 @@ unrandomized_dict: Dict[str, str] = {
}
junk_locations: Set[str] = [
junk_locations: Set[str] = {
"Albero: Donate 50000 Tears",
"Ossuary: 11th reward",
"AtTotS: Miriam's gift",
"TSC: Jocinero's final reward"
]
}
thorn_set: Set[str] = {
@@ -44,4 +44,4 @@ skill_dict: Dict[str, str] = {
"Skill 5, Tier 1": "Lunge Skill",
"Skill 5, Tier 2": "Lunge Skill",
"Skill 5, Tier 3": "Lunge Skill",
}
}

View File

@@ -1,15 +1,15 @@
from typing import Dict, List, Set, Any
from collections import Counter
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from Options import OptionError
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, add_rule
from .Options import blasphemous_options
from .Items import base_id, item_table, group_table, tears_list, reliquary_set
from .Locations import location_names
from .Rules import BlasRules
from worlds.generic.Rules import set_rule
from .Options import BlasphemousOptions, blas_option_groups
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
from .region_data import regions, locations
class BlasphemousWeb(WebWorld):
theme = "stone"
@@ -21,39 +21,33 @@ class BlasphemousWeb(WebWorld):
"setup/en",
["TRPG"]
)]
option_groups = blas_option_groups
class BlasphemousWorld(World):
"""
Blasphemous is a challenging Metroidvania set in the cursed land of Cvstodia. Play as the Penitent One, trapped
in an endless cycle of death and rebirth, and free the world from it's terrible fate in your quest to break
in an endless cycle of death and rebirth, and free the world from its terrible fate in your quest to break
your eternal damnation!
"""
game: str = "Blasphemous"
game = "Blasphemous"
web = BlasphemousWeb()
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)}
location_name_to_game_id = {loc["name"]: loc["game_id"] for loc in location_table}
location_name_to_id = {loc: (base_id + index) for index, loc in enumerate(location_names.values())}
item_name_groups = group_table
option_definitions = blasphemous_options
options_dataclass = BlasphemousOptions
options: BlasphemousOptions
required_client_version = (0, 4, 2)
required_client_version = (0, 4, 7)
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)
self.disabled_locations: List[str] = []
def create_item(self, name: str) -> "BlasphemousItem":
@@ -68,64 +62,56 @@ class BlasphemousWorld(World):
def get_filler_item_name(self) -> str:
return self.multiworld.random.choice(tears_set)
return self.random.choice(tears_list)
def generate_early(self):
world = self.multiworld
player = self.player
if not self.options.starting_location.randomized:
if self.options.starting_location == "mourning_havoc" and self.options.difficulty < 2:
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
f"{self.options.starting_location} cannot be chosen if Difficulty is lower than Hard.")
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 (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 (self.options.starting_location == "brotherhood" or self.options.starting_location == "mourning_havoc") \
and self.options.dash_shuffle:
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
f"{self.options.starting_location} 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.")
if self.options.starting_location == "grievance" and self.options.wall_climb_shuffle:
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
f"{self.options.starting_location} cannot be chosen if Shuffle Wall Climb is enabled.")
else:
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
invalid: bool = False
if world.difficulty[player].value < 2:
if self.options.difficulty < 2:
locations.remove(6)
if world.dash_shuffle[player]:
if self.options.dash_shuffle:
locations.remove(0)
if 6 in locations:
locations.remove(6)
if world.wall_climb_shuffle[player]:
if self.options.wall_climb_shuffle:
locations.remove(3)
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
invalid = True
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 invalid:
world.starting_location[player].value = world.random.choice(locations)
if self.options.starting_location.value not in locations:
self.options.starting_location.value = self.random.choice(locations)
if not world.dash_shuffle[player]:
world.push_precollected(self.create_item("Dash Ability"))
if not self.options.dash_shuffle:
self.multiworld.push_precollected(self.create_item("Dash Ability"))
if not world.wall_climb_shuffle[player]:
world.push_precollected(self.create_item("Wall Climb Ability"))
if not self.options.wall_climb_shuffle:
self.multiworld.push_precollected(self.create_item("Wall Climb Ability"))
if world.skip_long_quests[player]:
if not self.options.boots_of_pleading:
self.disabled_locations.append("RE401")
if not self.options.purified_hand:
self.disabled_locations.append("RE402")
if self.options.skip_long_quests:
for loc in junk_locations:
world.exclude_locations[player].value.add(loc)
self.options.exclude_locations.value.add(loc)
start_rooms: Dict[int, str] = {
0: "D17Z01S01",
@@ -137,13 +123,10 @@ class BlasphemousWorld(World):
6: "D20Z02S09"
}
self.start_room = start_rooms[world.starting_location[player].value]
self.start_room = start_rooms[self.options.starting_location.value]
def create_items(self):
world = self.multiworld
player = self.player
removed: int = 0
to_remove: List[str] = [
"Tears of Atonement (250)",
@@ -156,46 +139,46 @@ class BlasphemousWorld(World):
skipped_items = []
junk: int = 0
for item, count in world.start_inventory[player].value.items():
for item, count in self.options.start_inventory.value.items():
for _ in range(count):
skipped_items.append(item)
junk += 1
skipped_items.extend(unrandomized_dict.values())
if world.thorn_shuffle[player] == 2:
for i in range(8):
if self.options.thorn_shuffle == "vanilla":
for _ in range(8):
skipped_items.append("Thorn Upgrade")
if world.dash_shuffle[player]:
if self.options.dash_shuffle:
skipped_items.append(to_remove[removed])
removed += 1
elif not world.dash_shuffle[player]:
elif not self.options.dash_shuffle:
skipped_items.append("Dash Ability")
if world.wall_climb_shuffle[player]:
if self.options.wall_climb_shuffle:
skipped_items.append(to_remove[removed])
removed += 1
elif not world.wall_climb_shuffle[player]:
elif not self.options.wall_climb_shuffle:
skipped_items.append("Wall Climb Ability")
if not world.reliquary_shuffle[player]:
if not self.options.reliquary_shuffle:
skipped_items.extend(reliquary_set)
elif world.reliquary_shuffle[player]:
for i in range(3):
elif self.options.reliquary_shuffle:
for _ in range(3):
skipped_items.append(to_remove[removed])
removed += 1
if not world.boots_of_pleading[player]:
if not self.options.boots_of_pleading:
skipped_items.append("Boots of Pleading")
if not world.purified_hand[player]:
if not self.options.purified_hand:
skipped_items.append("Purified Hand of the Nun")
if world.start_wheel[player]:
if self.options.start_wheel:
skipped_items.append("The Young Mason's Wheel")
if not world.skill_randomizer[player]:
if not self.options.skill_randomizer:
skipped_items.extend(skill_dict.values())
counter = Counter(skipped_items)
@@ -208,184 +191,140 @@ class BlasphemousWorld(World):
if count <= 0:
continue
else:
for i in range(count):
for _ in range(count):
pool.append(self.create_item(item["name"]))
for _ in range(junk):
pool.append(self.create_item(self.get_filler_item_name()))
world.itempool += pool
self.multiworld.itempool += pool
def pre_fill(self):
world = self.multiworld
player = self.player
self.place_items_from_dict(unrandomized_dict)
if world.thorn_shuffle[player] == 2:
if self.options.thorn_shuffle == "vanilla":
self.place_items_from_set(thorn_set, "Thorn Upgrade")
if world.start_wheel[player]:
world.get_location("Beginning gift", player)\
.place_locked_item(self.create_item("The Young Mason's Wheel"))
if self.options.start_wheel:
self.get_location("Beginning gift").place_locked_item(self.create_item("The Young Mason's Wheel"))
if not world.skill_randomizer[player]:
if not self.options.skill_randomizer:
self.place_items_from_dict(skill_dict)
if world.thorn_shuffle[player] == 1:
world.local_items[player].value.add("Thorn Upgrade")
if self.options.thorn_shuffle == "local_only":
self.options.local_items.value.add("Thorn Upgrade")
def place_items_from_set(self, location_set: Set[str], name: str):
for loc in location_set:
self.multiworld.get_location(loc, self.player)\
.place_locked_item(self.create_item(name))
self.get_location(loc).place_locked_item(self.create_item(name))
def place_items_from_dict(self, option_dict: Dict[str, str]):
for loc, item in option_dict.items():
self.multiworld.get_location(loc, self.player)\
.place_locked_item(self.create_item(item))
self.get_location(loc).place_locked_item(self.create_item(item))
def create_regions(self) -> None:
multiworld = self.multiworld
player = self.player
world = self.multiworld
created_regions: List[str] = []
for r in regions:
multiworld.regions.append(Region(r["name"], player, multiworld))
created_regions.append(r["name"])
self.get_region("Menu").add_exits({self.start_room: "New Game"})
blas_logic = BlasRules(self)
for r in regions:
region = self.get_region(r["name"])
for e in r["exits"]:
region.add_exits({e["target"]}, {e["target"]: blas_logic.load_rule(True, r["name"], e)})
for l in [l for l in r["locations"] if l not in self.disabled_locations]:
region.add_locations({location_names[l]: self.location_name_to_id[location_names[l]]}, BlasphemousLocation)
for t in r["transitions"]:
if t == r["name"]:
continue
if t in created_regions:
region.add_exits({t})
else:
multiworld.regions.append(Region(t, player, multiworld))
created_regions.append(t)
region.add_exits({t})
for l in [l for l in locations if l["name"] not in self.disabled_locations]:
location = self.get_location(location_names[l["name"]])
set_rule(location, blas_logic.load_rule(False, l["name"], l))
for rname, ename in blas_logic.indirect_conditions:
self.multiworld.register_indirect_condition(self.get_region(rname), self.get_entrance(ename))
#from Utils import visualize_regions
#visualize_regions(self.get_region("Menu"), "blasphemous_regions.puml")
menu_region = Region("Menu", player, world)
misc_region = Region("Misc", player, world)
world.regions += [menu_region, misc_region]
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 = Location(player, "His Holiness Escribar", None, self.get_region("D07Z01S03[W]"))
victory.place_locked_item(self.create_event("Victory"))
world.get_region("D07Z01S03", player).locations.append(victory)
self.get_region("D07Z01S03[W]").locations.append(victory)
if world.ending[self.player].value == 1:
if self.options.ending == "ending_a":
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
elif world.ending[self.player].value == 2:
elif self.options.ending == "ending_c":
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and
state.has("Holy Wound of Abnegation", 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)
multiworld.completion_condition[self.player] = lambda state: state.has("Victory", player)
def fill_slot_data(self) -> Dict[str, Any]:
slot_data: Dict[str, Any] = {}
locations = []
doors: Dict[str, str] = {}
world = self.multiworld
player = self.player
thorns: bool = True
if world.thorn_shuffle[player].value == 2:
if self.options.thorn_shuffle == "vanilla":
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": world.player_name[loc.item.player],
"type": int(loc.item.classification)
}
locations.append(data)
config = {
"LogicDifficulty": world.difficulty[player].value,
"StartingLocation": world.starting_location[player].value,
"LogicDifficulty": self.options.difficulty.value,
"StartingLocation": self.options.starting_location.value,
"VersionCreated": "AP",
"UnlockTeleportation": bool(world.prie_dieu_warp[player].value),
"AllowHints": bool(world.corpse_hints[player].value),
"AllowPenitence": bool(world.penitence[player].value),
"UnlockTeleportation": bool(self.options.prie_dieu_warp.value),
"AllowHints": bool(self.options.corpse_hints.value),
"AllowPenitence": bool(self.options.penitence.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),
"ShuffleReliquaries": bool(self.options.reliquary_shuffle.value),
"ShuffleBootsOfPleading": bool(self.options.boots_of_pleading.value),
"ShufflePurifiedHand": bool(self.options.purified_hand.value),
"ShuffleDash": bool(self.options.dash_shuffle.value),
"ShuffleWallClimb": bool(self.options.wall_climb_shuffle.value),
"ShuffleSwordSkills": bool(world.skill_randomizer[player].value),
"ShuffleSwordSkills": bool(self.options.wall_climb_shuffle.value),
"ShuffleThorns": thorns,
"JunkLongQuests": bool(world.skip_long_quests[player].value),
"StartWithWheel": bool(world.start_wheel[player].value),
"JunkLongQuests": bool(self.options.skip_long_quests.value),
"StartWithWheel": bool(self.options.start_wheel.value),
"EnemyShuffleType": world.enemy_randomizer[player].value,
"MaintainClass": bool(world.enemy_groups[player].value),
"AreaScaling": bool(world.enemy_scaling[player].value),
"EnemyShuffleType": self.options.enemy_randomizer.value,
"MaintainClass": bool(self.options.enemy_groups.value),
"AreaScaling": bool(self.options.enemy_scaling.value),
"BossShuffleType": 0,
"DoorShuffleType": 0
}
slot_data = {
"locations": locations,
"locationinfo": [{"gameId": loc, "apId": (base_id + index)} for index, loc in enumerate(location_names)],
"doors": doors,
"cfg": config,
"ending": world.ending[self.player].value,
"death_link": bool(world.death_link[self.player].value)
"ending": self.options.ending.value,
"death_link": bool(self.options.death_link.value)
}
return slot_data

View File

@@ -1,48 +1,17 @@
# Blasphemous Multiworld Setup Guide
## Useful Links
It is recommended to use the [Mod Installer](https://github.com/BrandenEK/Blasphemous.Modding.Installer) to handle installing and updating mods. If you would prefer to install mods manually, instructions can also be found at the Mod Installer repository.
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)
You will need the [Multiworld](https://github.com/BrandenEK/Blasphemous.Randomizer.Multiworld) mod to play an Archipelago randomizer.
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)
Some optional mods are also recommended:
- [Rando Map](https://github.com/BrandenEK/Blasphemous.Randomizer.MapTracker)
- [Boots of Pleading](https://github.com/BrandenEK/Blasphemous.BootsOfPleading) (Required if the "Boots of Pleading" option is enabled)
- [Double Jump](https://github.com/BrandenEK/Blasphemous.DoubleJump) (Required if the "Purified Hand of the Nun" option is enabled)
## Mod Installer (Recommended)
To connect to a multiworld: Choose a save file and enter the address, your name, and the password (if the server has one) into the menu.
1. Download the [Mod Installer](https://github.com/BrandenEK/Blasphemous-Mod-Installer),
and point it to your install directory for Blasphemous.
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.
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.
**Make sure to connect to the server before attempting to start a new save file.**
After connecting, there are some commands you can use in the console, which can be opened by pressing backslash `\`:
- `ap status` - Display connection status.
- `ap say [message]` - Send a message to the server.
- `ap hint [item]` - Request a hint for an item from the server.

48070
worlds/blasphemous/region_data.py generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
from test.bases import WorldTestBase
from .. import BlasphemousWorld
class BlasphemousTestBase(WorldTestBase):
game = "Blasphemous"
world: BlasphemousWorld

View File

@@ -0,0 +1,56 @@
from . import BlasphemousTestBase
from ..Locations import location_names
class BotSSGauntletTest(BlasphemousTestBase):
options = {
"starting_location": "albero",
"wall_climb_shuffle": True,
"dash_shuffle": True
}
@property
def run_default_tests(self) -> bool:
return False
def test_botss_gauntlet(self) -> None:
self.assertAccessDependency([location_names["CO25"]], [["Dash Ability", "Wall Climb Ability"]], True)
class BackgroundZonesTest(BlasphemousTestBase):
@property
def run_default_tests(self) -> bool:
return False
def test_dc_shroud(self) -> None:
self.assertAccessDependency([location_names["RB03"]], [["Shroud of Dreamt Sins"]], True)
def test_wothp_bronze_cells(self) -> None:
bronze_locations = [
location_names["QI70"],
location_names["RESCUED_CHERUB_03"]
]
self.assertAccessDependency(bronze_locations, [["Key of the Secular"]], True)
def test_wothp_silver_cells(self) -> None:
silver_locations = [
location_names["CO24"],
location_names["RESCUED_CHERUB_34"],
location_names["CO37"],
location_names["RESCUED_CHERUB_04"]
]
self.assertAccessDependency(silver_locations, [["Key of the Scribe"]], True)
def test_wothp_gold_cells(self) -> None:
gold_locations = [
location_names["QI51"],
location_names["CO26"],
location_names["CO02"]
]
self.assertAccessDependency(gold_locations, [["Key of the Inquisitor"]], True)
def test_wothp_quirce(self) -> None:
self.assertAccessDependency([location_names["BS14"]], [["Key of the Secular", "Key of the Scribe", "Key of the Inquisitor"]], True)

View File

@@ -0,0 +1,135 @@
from . import BlasphemousTestBase
class TestBrotherhoodEasy(BlasphemousTestBase):
options = {
"starting_location": "brotherhood",
"difficulty": "easy"
}
class TestBrotherhoodNormal(BlasphemousTestBase):
options = {
"starting_location": "brotherhood",
"difficulty": "normal"
}
class TestBrotherhoodHard(BlasphemousTestBase):
options = {
"starting_location": "brotherhood",
"difficulty": "hard"
}
class TestAlberoEasy(BlasphemousTestBase):
options = {
"starting_location": "albero",
"difficulty": "easy"
}
class TestAlberoNormal(BlasphemousTestBase):
options = {
"starting_location": "albero",
"difficulty": "normal"
}
class TestAlberoHard(BlasphemousTestBase):
options = {
"starting_location": "albero",
"difficulty": "hard"
}
class TestConventEasy(BlasphemousTestBase):
options = {
"starting_location": "convent",
"difficulty": "easy"
}
class TestConventNormal(BlasphemousTestBase):
options = {
"starting_location": "convent",
"difficulty": "normal"
}
class TestConventHard(BlasphemousTestBase):
options = {
"starting_location": "convent",
"difficulty": "hard"
}
class TestGrievanceEasy(BlasphemousTestBase):
options = {
"starting_location": "grievance",
"difficulty": "easy"
}
class TestGrievanceNormal(BlasphemousTestBase):
options = {
"starting_location": "grievance",
"difficulty": "normal"
}
class TestGrievanceHard(BlasphemousTestBase):
options = {
"starting_location": "grievance",
"difficulty": "hard"
}
class TestKnotOfWordsEasy(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
"difficulty": "easy"
}
class TestKnotOfWordsNormal(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
"difficulty": "normal"
}
class TestKnotOfWordsHard(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
"difficulty": "hard"
}
class TestRooftopsEasy(BlasphemousTestBase):
options = {
"starting_location": "rooftops",
"difficulty": "easy"
}
class TestRooftopsNormal(BlasphemousTestBase):
options = {
"starting_location": "rooftops",
"difficulty": "normal"
}
class TestRooftopsHard(BlasphemousTestBase):
options = {
"starting_location": "rooftops",
"difficulty": "hard"
}
# mourning and havoc can't be selected on easy or normal. hard only
class TestMourningHavocHard(BlasphemousTestBase):
options = {
"starting_location": "mourning_havoc",
"difficulty": "hard"
}

View File

@@ -28,7 +28,7 @@ An Example `AP.json` file:
```
{
"Url": "archipelago:12345",
"Url": "archipelago.gg:12345",
"SlotName": "Maddy",
"Password": ""
}

View File

@@ -44,15 +44,15 @@ class ChecksFinderWorld(World):
self.multiworld.regions += [menu, board]
def create_items(self):
# Generate item pool
itempool = []
# Generate list of items
items_to_create = []
# Add the map width and height stuff
itempool += ["Map Width"] * 5 # 10 - 5
itempool += ["Map Height"] * 5 # 10 - 5
items_to_create += ["Map Width"] * 5 # 10 - 5
items_to_create += ["Map Height"] * 5 # 10 - 5
# Add the map bombs
itempool += ["Map Bombs"] * 15 # 20 - 5
# Convert itempool into real items
itempool = [self.create_item(item) for item in itempool]
items_to_create += ["Map Bombs"] * 15 # 20 - 5
# Convert list into real items
itempool = [self.create_item(item) for item in items_to_create]
self.multiworld.itempool += itempool

View File

@@ -1,6 +1,9 @@
from typing import Callable, Dict, NamedTuple, Optional
from typing import Callable, Dict, NamedTuple, Optional, TYPE_CHECKING
from BaseClasses import Item, ItemClassification, MultiWorld
from BaseClasses import Item, ItemClassification
if TYPE_CHECKING:
from . import CliqueWorld
class CliqueItem(Item):
@@ -10,7 +13,7 @@ class CliqueItem(Item):
class CliqueItemData(NamedTuple):
code: Optional[int] = None
type: ItemClassification = ItemClassification.filler
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True
can_create: Callable[["CliqueWorld"], bool] = lambda world: True
item_data_table: Dict[str, CliqueItemData] = {
@@ -21,11 +24,11 @@ item_data_table: Dict[str, CliqueItemData] = {
"Button Activation": CliqueItemData(
code=69696968,
type=ItemClassification.progression,
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]),
can_create=lambda world: world.options.hard_mode,
),
"A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData(
code=69696967,
can_create=lambda multiworld, player: False # Only created from `get_filler_item_name`.
can_create=lambda world: False # Only created from `get_filler_item_name`.
),
"The Urge to Push": CliqueItemData(
type=ItemClassification.progression,

View File

@@ -1,6 +1,9 @@
from typing import Callable, Dict, NamedTuple, Optional
from typing import Callable, Dict, NamedTuple, Optional, TYPE_CHECKING
from BaseClasses import Location, MultiWorld
from BaseClasses import Location
if TYPE_CHECKING:
from . import CliqueWorld
class CliqueLocation(Location):
@@ -10,7 +13,7 @@ class CliqueLocation(Location):
class CliqueLocationData(NamedTuple):
region: str
address: Optional[int] = None
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True
can_create: Callable[["CliqueWorld"], bool] = lambda world: True
locked_item: Optional[str] = None
@@ -22,7 +25,7 @@ location_data_table: Dict[str, CliqueLocationData] = {
"The Item on the Desk": CliqueLocationData(
region="The Button Realm",
address=69696968,
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]),
can_create=lambda world: world.options.hard_mode,
),
"In the Player's Mind": CliqueLocationData(
region="The Button Realm",

View File

@@ -1,6 +1,5 @@
from typing import Dict
from Options import Choice, Option, Toggle
from dataclasses import dataclass
from Options import Choice, Toggle, PerGameCommonOptions, StartInventoryPool
class HardMode(Toggle):
@@ -25,10 +24,11 @@ class ButtonColor(Choice):
option_black = 11
clique_options: Dict[str, type(Option)] = {
"color": ButtonColor,
"hard_mode": HardMode,
@dataclass
class CliqueOptions(PerGameCommonOptions):
color: ButtonColor
hard_mode: HardMode
start_inventory_from_pool: StartInventoryPool
# DeathLink is always on. Always.
# "death_link": DeathLink,
}
# death_link: DeathLink

View File

@@ -1,10 +1,13 @@
from typing import Callable
from typing import Callable, TYPE_CHECKING
from BaseClasses import CollectionState, MultiWorld
from BaseClasses import CollectionState
if TYPE_CHECKING:
from . import CliqueWorld
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)
def get_button_rule(world: "CliqueWorld") -> Callable[[CollectionState], bool]:
if world.options.hard_mode:
return lambda state: state.has("Button Activation", world.player)
return lambda state: True

View File

@@ -1,10 +1,10 @@
from typing import List
from typing import List, Dict, Any
from BaseClasses import Region, Tutorial
from worlds.AutoWorld import WebWorld, World
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
from .Options import CliqueOptions
from .Regions import region_data_table
from .Rules import get_button_rule
@@ -38,7 +38,8 @@ class CliqueWorld(World):
game = "Clique"
web = CliqueWebWorld()
option_definitions = clique_options
options: CliqueOptions
options_dataclass = CliqueOptions
location_name_to_id = location_table
item_name_to_id = item_table
@@ -48,7 +49,7 @@ class CliqueWorld(World):
def create_items(self) -> None:
item_pool: List[CliqueItem] = []
for name, item in item_data_table.items():
if item.code and item.can_create(self.multiworld, self.player):
if item.code and item.can_create(self):
item_pool.append(self.create_item(name))
self.multiworld.itempool += item_pool
@@ -61,41 +62,40 @@ class CliqueWorld(World):
# Create locations.
for region_name, region_data in region_data_table.items():
region = self.multiworld.get_region(region_name, self.player)
region = self.get_region(region_name)
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)
if location_data.region == region_name and location_data.can_create(self)
}, 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):
if not location_data.can_create(self):
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)
self.get_location(location_name).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")
self.options.priority_locations.value.add("The Big Red Button")
def get_filler_item_name(self) -> str:
return "A Cool Filler Item (No Satisfaction Guaranteed)"
def set_rules(self) -> None:
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
button_rule = get_button_rule(self)
self.get_location("The Big Red Button").access_rule = button_rule
self.get_location("In the Player's Mind").access_rule = button_rule
# 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"
self.get_location("The Big Red Button").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 fill_slot_data(self):
def fill_slot_data(self) -> Dict[str, Any]:
return {
"color": getattr(self.multiworld, "color")[self.player].current_key
"color": self.options.color.current_key
}

View File

@@ -0,0 +1,264 @@
# In almost all cases, we leave boss and enemy randomization up to the static randomizer. But for
# Yhorm specifically we need to know where he ends up in order to ensure that the Storm Ruler is
# available before his fight.
from dataclasses import dataclass, field
from typing import Set
@dataclass
class DS3BossInfo:
"""The set of locations a given boss location blocks access to."""
name: str
"""The boss's name."""
id: int
"""The game's ID for this particular boss."""
dlc: bool = False
"""This boss appears in one of the game's DLCs."""
before_storm_ruler: bool = False
"""Whether this location appears before it's possible to get Storm Ruler in vanilla.
This is used to determine whether it's safe to place Yhorm here if weapons
aren't randomized.
"""
locations: Set[str] = field(default_factory=set)
"""Additional individual locations that can't be accessed until the boss is dead."""
# Note: the static randomizer splits up some bosses into separate fights for separate phases, each
# of which can be individually replaced by Yhorm.
all_bosses = [
DS3BossInfo("Iudex Gundyr", 4000800, before_storm_ruler = True, locations = {
"CA: Coiled Sword - boss drop"
}),
DS3BossInfo("Vordt of the Boreal Valley", 3000800, before_storm_ruler = True, locations = {
"HWL: Soul of Boreal Valley Vordt"
}),
DS3BossInfo("Curse-rotted Greatwood", 3100800, locations = {
"US: Soul of the Rotted Greatwood",
"US: Transposing Kiln - boss drop",
"US: Wargod Wooden Shield - Pit of Hollows",
"FS: Hawkwood's Shield - gravestone after Hawkwood leaves",
"FS: Sunset Shield - by grave after killing Hodrick w/Sirris",
"US: Sunset Helm - Pit of Hollows after killing Hodrick w/Sirris",
"US: Sunset Armor - pit of hollows after killing Hodrick w/Sirris",
"US: Sunset Gauntlets - pit of hollows after killing Hodrick w/Sirris",
"US: Sunset Leggings - pit of hollows after killing Hodrick w/Sirris",
"FS: Sunless Talisman - Sirris, kill GA boss",
"FS: Sunless Veil - shop, Sirris quest, kill GA boss",
"FS: Sunless Armor - shop, Sirris quest, kill GA boss",
"FS: Sunless Gauntlets - shop, Sirris quest, kill GA boss",
"FS: Sunless Leggings - shop, Sirris quest, kill GA boss",
}),
DS3BossInfo("Crystal Sage", 3300850, locations = {
"RS: Soul of a Crystal Sage",
"FS: Sage's Big Hat - shop after killing RS boss",
"FS: Hawkwood's Shield - gravestone after Hawkwood leaves",
}),
DS3BossInfo("Deacons of the Deep", 3500800, locations = {
"CD: Soul of the Deacons of the Deep",
"CD: Small Doll - boss drop",
"FS: Hawkwood's Shield - gravestone after Hawkwood leaves",
}),
DS3BossInfo("Abyss Watchers", 3300801, before_storm_ruler = True, locations = {
"FK: Soul of the Blood of the Wolf",
"FK: Cinders of a Lord - Abyss Watcher",
"FS: Undead Legion Helm - shop after killing FK boss",
"FS: Undead Legion Armor - shop after killing FK boss",
"FS: Undead Legion Gauntlet - shop after killing FK boss",
"FS: Undead Legion Leggings - shop after killing FK boss",
"FS: Farron Ring - Hawkwood",
"FS: Hawkwood's Shield - gravestone after Hawkwood leaves",
}),
DS3BossInfo("High Lord Wolnir", 3800800, before_storm_ruler = True, locations = {
"CC: Soul of High Lord Wolnir",
"FS: Wolnir's Crown - shop after killing CC boss",
"CC: Homeward Bone - Irithyll bridge",
"CC: Pontiff's Right Eye - Irithyll bridge, miniboss drop",
}),
DS3BossInfo("Pontiff Sulyvahn", 3700850, locations = {
"IBV: Soul of Pontiff Sulyvahn",
}),
DS3BossInfo("Old Demon King", 3800830, locations = {
"SL: Soul of the Old Demon King",
}),
DS3BossInfo("Aldrich, Devourer of Gods", 3700800, locations = {
"AL: Soul of Aldrich",
"AL: Cinders of a Lord - Aldrich",
"FS: Smough's Helm - shop after killing AL boss",
"FS: Smough's Armor - shop after killing AL boss",
"FS: Smough's Gauntlets - shop after killing AL boss",
"FS: Smough's Leggings - shop after killing AL boss",
"AL: Sun Princess Ring - dark cathedral, after boss",
"FS: Leonhard's Garb - shop after killing Leonhard",
"FS: Leonhard's Gauntlets - shop after killing Leonhard",
"FS: Leonhard's Trousers - shop after killing Leonhard",
}),
DS3BossInfo("Dancer of the Boreal Valley", 3000899, locations = {
"HWL: Soul of the Dancer",
"FS: Dancer's Crown - shop after killing LC entry boss",
"FS: Dancer's Armor - shop after killing LC entry boss",
"FS: Dancer's Gauntlets - shop after killing LC entry boss",
"FS: Dancer's Leggings - shop after killing LC entry boss",
}),
DS3BossInfo("Dragonslayer Armour", 3010800, locations = {
"LC: Soul of Dragonslayer Armour",
"FS: Morne's Helm - shop after killing Eygon or LC boss",
"FS: Morne's Armor - shop after killing Eygon or LC boss",
"FS: Morne's Gauntlets - shop after killing Eygon or LC boss",
"FS: Morne's Leggings - shop after killing Eygon or LC boss",
"LC: Titanite Chunk - down stairs after boss",
}),
DS3BossInfo("Consumed King Oceiros", 3000830, locations = {
"CKG: Soul of Consumed Oceiros",
"CKG: Titanite Scale - tomb, chest #1",
"CKG: Titanite Scale - tomb, chest #2",
"CKG: Drakeblood Helm - tomb, after killing AP mausoleum NPC",
"CKG: Drakeblood Armor - tomb, after killing AP mausoleum NPC",
"CKG: Drakeblood Gauntlets - tomb, after killing AP mausoleum NPC",
"CKG: Drakeblood Leggings - tomb, after killing AP mausoleum NPC",
}),
DS3BossInfo("Champion Gundyr", 4000830, locations = {
"UG: Soul of Champion Gundyr",
"FS: Gundyr's Helm - shop after killing UG boss",
"FS: Gundyr's Armor - shop after killing UG boss",
"FS: Gundyr's Gauntlets - shop after killing UG boss",
"FS: Gundyr's Leggings - shop after killing UG boss",
"UG: Hornet Ring - environs, right of main path after killing FK boss",
"UG: Chaos Blade - environs, left of shrine",
"UG: Blacksmith Hammer - shrine, Andre's room",
"UG: Eyes of a Fire Keeper - shrine, Irina's room",
"UG: Coiled Sword Fragment - shrine, dead bonfire",
"UG: Soul of a Crestfallen Knight - environs, above shrine entrance",
"UG: Life Ring+3 - shrine, behind big throne",
"UG: Ring of Steel Protection+1 - environs, behind bell tower",
"FS: Ring of Sacrifice - Yuria shop",
"UG: Ember - shop",
"UG: Priestess Ring - shop",
"UG: Wolf Knight Helm - shop after killing FK boss",
"UG: Wolf Knight Armor - shop after killing FK boss",
"UG: Wolf Knight Gauntlets - shop after killing FK boss",
"UG: Wolf Knight Leggings - shop after killing FK boss",
}),
DS3BossInfo("Ancient Wyvern", 3200800),
DS3BossInfo("King of the Storm", 3200850, locations = {
"AP: Soul of the Nameless King",
"FS: Golden Crown - shop after killing AP boss",
"FS: Dragonscale Armor - shop after killing AP boss",
"FS: Golden Bracelets - shop after killing AP boss",
"FS: Dragonscale Waistcloth - shop after killing AP boss",
"AP: Titanite Slab - plaza",
"AP: Covetous Gold Serpent Ring+2 - plaza",
"AP: Dragonslayer Helm - plaza",
"AP: Dragonslayer Armor - plaza",
"AP: Dragonslayer Gauntlets - plaza",
"AP: Dragonslayer Leggings - plaza",
}),
DS3BossInfo("Nameless King", 3200851, locations = {
"AP: Soul of the Nameless King",
"FS: Golden Crown - shop after killing AP boss",
"FS: Dragonscale Armor - shop after killing AP boss",
"FS: Golden Bracelets - shop after killing AP boss",
"FS: Dragonscale Waistcloth - shop after killing AP boss",
"AP: Titanite Slab - plaza",
"AP: Covetous Gold Serpent Ring+2 - plaza",
"AP: Dragonslayer Helm - plaza",
"AP: Dragonslayer Armor - plaza",
"AP: Dragonslayer Gauntlets - plaza",
"AP: Dragonslayer Leggings - plaza",
}),
DS3BossInfo("Lothric, Younger Prince", 3410830, locations = {
"GA: Soul of the Twin Princes",
"GA: Cinders of a Lord - Lothric Prince",
}),
DS3BossInfo("Lorian, Elder Prince", 3410832, locations = {
"GA: Soul of the Twin Princes",
"GA: Cinders of a Lord - Lothric Prince",
"FS: Lorian's Helm - shop after killing GA boss",
"FS: Lorian's Armor - shop after killing GA boss",
"FS: Lorian's Gauntlets - shop after killing GA boss",
"FS: Lorian's Leggings - shop after killing GA boss",
}),
DS3BossInfo("Champion's Gravetender and Gravetender Greatwolf", 4500860, dlc = True,
locations = {"PW1: Valorheart - boss drop"}),
DS3BossInfo("Sister Friede", 4500801, dlc = True, locations = {
"PW2: Soul of Sister Friede",
"PW2: Titanite Slab - boss drop",
"PW1: Titanite Slab - Corvian",
"FS: Ordained Hood - shop after killing PW2 boss",
"FS: Ordained Dress - shop after killing PW2 boss",
"FS: Ordained Trousers - shop after killing PW2 boss",
}),
DS3BossInfo("Blackflame Friede", 4500800, dlc = True, locations = {
"PW2: Soul of Sister Friede",
"PW1: Titanite Slab - Corvian",
"FS: Ordained Hood - shop after killing PW2 boss",
"FS: Ordained Dress - shop after killing PW2 boss",
"FS: Ordained Trousers - shop after killing PW2 boss",
}),
DS3BossInfo("Demon Prince", 5000801, dlc = True, locations = {
"DH: Soul of the Demon Prince",
"DH: Small Envoy Banner - boss drop",
}),
DS3BossInfo("Halflight, Spear of the Church", 5100800, dlc = True, locations = {
"RC: Titanite Slab - mid boss drop",
"RC: Titanite Slab - ashes, NPC drop",
"RC: Titanite Slab - ashes, mob drop",
"RC: Filianore's Spear Ornament - mid boss drop",
"RC: Crucifix of the Mad King - ashes, NPC drop",
"RC: Shira's Crown - Shira's room after killing ashes NPC",
"RC: Shira's Armor - Shira's room after killing ashes NPC",
"RC: Shira's Gloves - Shira's room after killing ashes NPC",
"RC: Shira's Trousers - Shira's room after killing ashes NPC",
}),
DS3BossInfo("Darkeater Midir", 5100850, dlc = True, locations = {
"RC: Soul of Darkeater Midir",
"RC: Spears of the Church - hidden boss drop",
}),
DS3BossInfo("Slave Knight Gael 1", 5110801, dlc = True, locations = {
"RC: Soul of Slave Knight Gael",
"RC: Blood of the Dark Soul - end boss drop",
# These are accessible before you trigger the boss, but once you do you
# have to beat it before getting them.
"RC: Titanite Slab - ashes, mob drop",
"RC: Titanite Slab - ashes, NPC drop",
"RC: Sacred Chime of Filianore - ashes, NPC drop",
"RC: Crucifix of the Mad King - ashes, NPC drop",
"RC: Shira's Crown - Shira's room after killing ashes NPC",
"RC: Shira's Armor - Shira's room after killing ashes NPC",
"RC: Shira's Gloves - Shira's room after killing ashes NPC",
"RC: Shira's Trousers - Shira's room after killing ashes NPC",
}),
DS3BossInfo("Slave Knight Gael 2", 5110800, dlc = True, locations = {
"RC: Soul of Slave Knight Gael",
"RC: Blood of the Dark Soul - end boss drop",
# These are accessible before you trigger the boss, but once you do you
# have to beat it before getting them.
"RC: Titanite Slab - ashes, mob drop",
"RC: Titanite Slab - ashes, NPC drop",
"RC: Sacred Chime of Filianore - ashes, NPC drop",
"RC: Crucifix of the Mad King - ashes, NPC drop",
"RC: Shira's Crown - Shira's room after killing ashes NPC",
"RC: Shira's Armor - Shira's room after killing ashes NPC",
"RC: Shira's Gloves - Shira's room after killing ashes NPC",
"RC: Shira's Trousers - Shira's room after killing ashes NPC",
}),
DS3BossInfo("Lords of Cinder", 4100800, locations = {
"KFF: Soul of the Lords",
"FS: Billed Mask - Yuria after killing KFF boss",
"FS: Black Dress - Yuria after killing KFF boss",
"FS: Black Gauntlets - Yuria after killing KFF boss",
"FS: Black Leggings - Yuria after killing KFF boss"
}),
]
default_yhorm_location = DS3BossInfo("Yhorm the Giant", 3900800, locations = {
"PC: Soul of Yhorm the Giant",
"PC: Cinders of a Lord - Yhorm the Giant",
"PC: Siegbräu - Siegward after killing boss",
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,80 +1,78 @@
import typing
from dataclasses import dataclass
import json
from typing import Any, Dict
from Options import Toggle, DefaultOnToggle, Option, Range, Choice, ItemDict, DeathLink
from Options import Choice, DeathLink, DefaultOnToggle, ExcludeLocations, NamedRange, OptionDict, \
OptionGroup, PerGameCommonOptions, Range, Removed, Toggle
## Game Options
class RandomizeWeaponLocations(DefaultOnToggle):
"""Randomizes weapons (+76 locations)"""
display_name = "Randomize Weapon Locations"
class EarlySmallLothricBanner(Choice):
"""Force Small Lothric Banner into an early sphere in your world or across all worlds."""
display_name = "Early Small Lothric Banner"
option_off = 0
option_early_global = 1
option_early_local = 2
default = option_off
class RandomizeShieldLocations(DefaultOnToggle):
"""Randomizes shields (+24 locations)"""
display_name = "Randomize Shield Locations"
class LateBasinOfVowsOption(Choice):
"""Guarantee that you don't need to enter Lothric Castle until later in the run.
- **Off:** You may have to enter Lothric Castle and the areas beyond it immediately after High
Wall of Lothric.
- **After Small Lothric Banner:** You may have to enter Lothric Castle after Catacombs of
Carthus.
- **After Small Doll:** You won't have to enter Lothric Castle until after Irithyll of the
Boreal Valley.
"""
display_name = "Late Basin of Vows"
option_off = 0
alias_false = 0
option_after_small_lothric_banner = 1
alias_true = 1
option_after_small_doll = 2
class RandomizeArmorLocations(DefaultOnToggle):
"""Randomizes armor pieces (+97 locations)"""
display_name = "Randomize Armor Locations"
class LateDLCOption(Choice):
"""Guarantee that you don't need to enter the DLC until later in the run.
- **Off:** You may have to enter the DLC after Catacombs of Carthus.
- **After Small Doll:** You may have to enter the DLC after Irithyll of the Boreal Valley.
- **After Basin:** You won't have to enter the DLC until after Lothric Castle.
"""
display_name = "Late DLC"
option_off = 0
alias_false = 0
option_after_small_doll = 1
alias_true = 1
option_after_basin = 2
class RandomizeRingLocations(DefaultOnToggle):
"""Randomizes rings (+49 locations)"""
display_name = "Randomize Ring Locations"
class EnableDLCOption(Toggle):
"""Include DLC locations, items, and enemies in the randomized pools.
To use this option, you must own both the "Ashes of Ariandel" and the "Ringed City" DLCs.
"""
display_name = "Enable DLC"
class RandomizeSpellLocations(DefaultOnToggle):
"""Randomizes spells (+18 locations)"""
display_name = "Randomize Spell Locations"
class EnableNGPOption(Toggle):
"""Include items and locations exclusive to NG+ cycles."""
display_name = "Enable NG+"
class RandomizeKeyLocations(DefaultOnToggle):
"""Randomizes items which unlock doors or bypass barriers"""
display_name = "Randomize Key Locations"
## Equipment
class RandomizeStartingLoadout(DefaultOnToggle):
"""Randomizes the equipment characters begin with."""
display_name = "Randomize Starting Loadout"
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 RequireOneHandedStartingWeapons(DefaultOnToggle):
"""Require starting equipment to be usable one-handed."""
display_name = "Require One-Handed Starting Weapons"
class AutoEquipOption(Toggle):
@@ -83,47 +81,56 @@ class AutoEquipOption(Toggle):
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."""
"""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 Equipment Slots"
class NoEquipLoadOption(Toggle):
"""Disable the equip load constraint from the game."""
display_name = "No Equip Load"
class NoWeaponRequirementsOption(Toggle):
"""Disable the weapon requirements by removing any movement or damage penalties.
Permitting you to use any weapon early"""
"""Disable the weapon requirements by removing any movement or damage penalties, permitting you
to use any weapon early.
"""
display_name = "No Weapon Requirements"
class NoSpellRequirementsOption(Toggle):
"""Disable the spell requirements permitting you to use any spell"""
"""Disable the spell requirements permitting you to use any spell."""
display_name = "No Spell Requirements"
class NoEquipLoadOption(Toggle):
"""Disable the equip load constraint from the game"""
display_name = "No Equip Load"
## Weapons
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"""
class RandomizeInfusionPercentageOption(NamedRange):
"""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
# 3/155 weapons are infused in the base game, or about 2%
special_range_names = {"similar to base game": 2}
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.
"""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"""
- **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
@@ -132,7 +139,7 @@ class RandomizeWeaponLevelOption(Choice):
class RandomizeWeaponLevelPercentageOption(Range):
"""The percentage of weapons in the pool to be upgraded if randomize weapons level is toggled"""
"""The percentage of weapons in the pool to be upgraded if randomize weapons level is toggled."""
display_name = "Percentage of Randomized Weapons"
range_start = 0
range_end = 100
@@ -140,7 +147,7 @@ class RandomizeWeaponLevelPercentageOption(Range):
class MinLevelsIn5WeaponPoolOption(Range):
"""The minimum upgraded value of a weapon in the pool of weapons that can only reach +5"""
"""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 = 0
range_end = 5
@@ -148,7 +155,7 @@ class MinLevelsIn5WeaponPoolOption(Range):
class MaxLevelsIn5WeaponPoolOption(Range):
"""The maximum upgraded value of a weapon in the pool of weapons that can only reach +5"""
"""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 = 0
range_end = 5
@@ -156,7 +163,7 @@ class MaxLevelsIn5WeaponPoolOption(Range):
class MinLevelsIn10WeaponPoolOption(Range):
"""The minimum upgraded value of a weapon in the pool of weapons that can reach +10"""
"""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 = 0
range_end = 10
@@ -164,72 +171,308 @@ class MinLevelsIn10WeaponPoolOption(Range):
class MaxLevelsIn10WeaponPoolOption(Range):
"""The maximum upgraded value of a weapon in the pool of weapons that can reach +10"""
"""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 = 0
range_end = 10
default = 10
class EarlySmallLothricBanner(Choice):
"""This option makes it so the user can choose to force the Small Lothric Banner into an early sphere in their world or
into an early sphere across all worlds."""
display_name = "Early Small Lothric Banner"
option_off = 0
option_early_global = 1
option_early_local = 2
default = option_off
## Item Smoothing
class SmoothSoulItemsOption(DefaultOnToggle):
"""Distribute soul items in a similar order as the base game.
By default, soul items will be distributed totally randomly. If this is set, less valuable soul
items will generally appear in earlier spheres and more valuable ones will generally appear
later.
"""
display_name = "Smooth Soul Items"
class LateBasinOfVowsOption(Toggle):
"""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 SmoothUpgradeItemsOption(DefaultOnToggle):
"""Distribute upgrade items in a similar order as the base game.
By default, upgrade items will be distributed totally randomly. If this is set, lower-level
upgrade items will generally appear in earlier spheres and higher-level ones will generally
appear later.
"""
display_name = "Smooth Upgrade 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 SmoothUpgradedWeaponsOption(DefaultOnToggle):
"""Distribute upgraded weapons in a similar order as the base game.
By default, upgraded weapons will be distributed totally randomly. If this is set, lower-level
weapons will generally appear in earlier spheres and higher-level ones will generally appear
later.
"""
display_name = "Smooth Upgraded Weapons"
class EnableDLCOption(Toggle):
"""To use this option, you must own both the ASHES OF ARIANDEL and the RINGED CITY DLC"""
display_name = "Enable DLC"
### Enemies
class RandomizeEnemiesOption(DefaultOnToggle):
"""Randomize enemy and boss placements."""
display_name = "Randomize Enemies"
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_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,
"early_banner": EarlySmallLothricBanner,
"late_basin_of_vows": LateBasinOfVowsOption,
"late_dlc": LateDLCOption,
"no_spell_requirements": NoSpellRequirementsOption,
"no_equip_load": NoEquipLoadOption,
"death_link": DeathLink,
"enable_dlc": EnableDLCOption,
}
class SimpleEarlyBossesOption(DefaultOnToggle):
"""Avoid replacing Iudex Gundyr and Vordt with late bosses.
This excludes all bosses after Dancer of the Boreal Valley from these two boss fights. Disable
it for a chance at a much harder early game.
This is ignored unless enemies are randomized.
"""
display_name = "Simple Early Bosses"
class ScaleEnemiesOption(DefaultOnToggle):
"""Scale randomized enemy stats to match the areas in which they appear.
Disabling this will tend to make the early game much more difficult and the late game much
easier.
This is ignored unless enemies are randomized.
"""
display_name = "Scale Enemies"
class RandomizeMimicsWithEnemiesOption(Toggle):
"""Mix Mimics into the main enemy pool.
If this is enabled, Mimics will be replaced by normal enemies who drop the Mimic rewards on
death, and Mimics will be placed randomly in place of normal enemies. It's recommended to enable
Impatient Mimics as well if you enable this.
This is ignored unless enemies are randomized.
"""
display_name = "Randomize Mimics With Enemies"
class RandomizeSmallCrystalLizardsWithEnemiesOption(Toggle):
"""Mix small Crystal Lizards into the main enemy pool.
If this is enabled, Crystal Lizards will be replaced by normal enemies who drop the Crystal
Lizard rewards on death, and Crystal Lizards will be placed randomly in place of normal enemies.
This is ignored unless enemies are randomized.
"""
display_name = "Randomize Small Crystal Lizards With Enemies"
class ReduceHarmlessEnemiesOption(Toggle):
"""Reduce the frequency that "harmless" enemies appear.
Enable this to add a bit of extra challenge. This severely limits the number of enemies that are
slow to aggro, slow to attack, and do very little damage that appear in the enemy pool.
This is ignored unless enemies are randomized.
"""
display_name = "Reduce Harmless Enemies"
class AllChestsAreMimicsOption(Toggle):
"""Replace all chests with mimics that drop the same items.
If "Randomize Mimics With Enemies" is set, these chests will instead be replaced with random
enemies that drop the same items.
This is ignored unless enemies are randomized.
"""
display_name = "All Chests Are Mimics"
class ImpatientMimicsOption(Toggle):
"""Mimics attack as soon as you get close instead of waiting for you to open them.
This is ignored unless enemies are randomized.
"""
display_name = "Impatient Mimics"
class RandomEnemyPresetOption(OptionDict):
"""The YAML preset for the static enemy randomizer.
See the static randomizer documentation in `randomizer\\presets\\README.txt` for details.
Include this as nested YAML. For example:
.. code-block:: YAML
random_enemy_preset:
RemoveSource: Ancient Wyvern; Darkeater Midir
DontRandomize: Iudex Gundyr
"""
display_name = "Random Enemy Preset"
supports_weighting = False
default = {}
valid_keys = ["Description", "RecommendFullRandomization", "RecommendNoEnemyProgression",
"OopsAll", "Boss", "Miniboss", "Basic", "BuffBasicEnemiesAsBosses",
"DontRandomize", "RemoveSource", "Enemies"]
@classmethod
def get_option_name(cls, value: Dict[str, Any]) -> str:
return json.dumps(value)
## Item & Location
class DS3ExcludeLocations(ExcludeLocations):
"""Prevent these locations from having an important item."""
default = frozenset({"Hidden", "Small Crystal Lizards", "Upgrade", "Small Souls", "Miscellaneous"})
class ExcludedLocationBehaviorOption(Choice):
"""How to choose items for excluded locations in DS3.
- **Allow Useful:** Excluded locations can't have progression items, but they can have useful
items.
- **Forbid Useful:** Neither progression items nor useful items can be placed in excluded
locations.
- **Do Not Randomize:** Excluded locations always contain the same item as in vanilla Dark Souls
III.
A "progression item" is anything that's required to unlock another location in some game. A
"useful item" is something each game defines individually, usually items that are quite
desirable but not strictly necessary.
"""
display_name = "Excluded Locations Behavior"
option_allow_useful = 1
option_forbid_useful = 2
option_do_not_randomize = 3
default = 2
class MissableLocationBehaviorOption(Choice):
"""Which items can be placed in locations that can be permanently missed.
- **Allow Useful:** Missable locations can't have progression items, but they can have useful
items.
- **Forbid Useful:** Neither progression items nor useful items can be placed in missable
locations.
- **Do Not Randomize:** Missable locations always contain the same item as in vanilla Dark Souls
III.
A "progression item" is anything that's required to unlock another location in some game. A
"useful item" is something each game defines individually, usually items that are quite
desirable but not strictly necessary.
"""
display_name = "Missable Locations Behavior"
option_allow_useful = 1
option_forbid_useful = 2
option_do_not_randomize = 3
default = 2
@dataclass
class DarkSouls3Options(PerGameCommonOptions):
# Game Options
early_banner: EarlySmallLothricBanner
late_basin_of_vows: LateBasinOfVowsOption
late_dlc: LateDLCOption
death_link: DeathLink
enable_dlc: EnableDLCOption
enable_ngp: EnableNGPOption
# Equipment
random_starting_loadout: RandomizeStartingLoadout
require_one_handed_starting_weapons: RequireOneHandedStartingWeapons
auto_equip: AutoEquipOption
lock_equip: LockEquipOption
no_equip_load: NoEquipLoadOption
no_weapon_requirements: NoWeaponRequirementsOption
no_spell_requirements: NoSpellRequirementsOption
# Weapons
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
# Item Smoothing
smooth_soul_items: SmoothSoulItemsOption
smooth_upgrade_items: SmoothUpgradeItemsOption
smooth_upgraded_weapons: SmoothUpgradedWeaponsOption
# Enemies
randomize_enemies: RandomizeEnemiesOption
simple_early_bosses: SimpleEarlyBossesOption
scale_enemies: ScaleEnemiesOption
randomize_mimics_with_enemies: RandomizeMimicsWithEnemiesOption
randomize_small_crystal_lizards_with_enemies: RandomizeSmallCrystalLizardsWithEnemiesOption
reduce_harmless_enemies: ReduceHarmlessEnemiesOption
all_chests_are_mimics: AllChestsAreMimicsOption
impatient_mimics: ImpatientMimicsOption
random_enemy_preset: RandomEnemyPresetOption
# Item & Location
exclude_locations: DS3ExcludeLocations
excluded_location_behavior: ExcludedLocationBehaviorOption
missable_location_behavior: MissableLocationBehaviorOption
# Removed
pool_type: Removed
enable_weapon_locations: Removed
enable_shield_locations: Removed
enable_armor_locations: Removed
enable_ring_locations: Removed
enable_spell_locations: Removed
enable_key_locations: Removed
enable_boss_locations: Removed
enable_npc_locations: Removed
enable_misc_locations: Removed
enable_health_upgrade_locations: Removed
enable_progressive_locations: Removed
guaranteed_items: Removed
excluded_locations: Removed
missable_locations: Removed
option_groups = [
OptionGroup("Equipment", [
RandomizeStartingLoadout,
RequireOneHandedStartingWeapons,
AutoEquipOption,
LockEquipOption,
NoEquipLoadOption,
NoWeaponRequirementsOption,
NoSpellRequirementsOption,
]),
OptionGroup("Weapons", [
RandomizeInfusionOption,
RandomizeInfusionPercentageOption,
RandomizeWeaponLevelOption,
RandomizeWeaponLevelPercentageOption,
MinLevelsIn5WeaponPoolOption,
MaxLevelsIn5WeaponPoolOption,
MinLevelsIn10WeaponPoolOption,
MaxLevelsIn10WeaponPoolOption,
]),
OptionGroup("Item Smoothing", [
SmoothSoulItemsOption,
SmoothUpgradeItemsOption,
SmoothUpgradedWeaponsOption,
]),
OptionGroup("Enemies", [
RandomizeEnemiesOption,
SimpleEarlyBossesOption,
ScaleEnemiesOption,
RandomizeMimicsWithEnemiesOption,
RandomizeSmallCrystalLizardsWithEnemiesOption,
ReduceHarmlessEnemiesOption,
AllChestsAreMimicsOption,
ImpatientMimicsOption,
RandomEnemyPresetOption,
]),
OptionGroup("Item & Location Options", [
DS3ExcludeLocations,
ExcludedLocationBehaviorOption,
MissableLocationBehaviorOption,
])
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,97 @@
# python -m worlds.dark_souls_3.detailed_location_descriptions \
# worlds/dark_souls_3/detailed_location_descriptions.py
#
# This script downloads the static randomizer's descriptions for each location and adds them to
# the location documentation.
from collections import defaultdict
import html
import os
import re
import requests
import yaml
from .Locations import location_dictionary
location_re = re.compile(r'^([A-Z0-9]+): (.*?)(?:$| - )')
if __name__ == '__main__':
# TODO: update this to the main branch of the main randomizer once Archipelago support is merged
url = 'https://raw.githubusercontent.com/nex3/SoulsRandomizers/archipelago-server/dist/Base/annotations.txt'
response = requests.get(url)
if response.status_code != 200:
raise Exception(f"Got {response.status_code} when downloading static randomizer locations")
annotations = yaml.load(response.text, Loader=yaml.Loader)
static_to_archi_regions = {
area['Name']: area['Archipelago']
for area in annotations['Areas']
}
descriptions_by_key = {slot['Key']: slot['Text'] for slot in annotations['Slots']}
# A map from (region, item name) pairs to all the descriptions that match those pairs.
descriptions_by_location = defaultdict(list)
# A map from item names to all the descriptions for those item names.
descriptions_by_item = defaultdict(list)
for slot in annotations['Slots']:
region = static_to_archi_regions[slot['Area']]
for item in slot['DebugText']:
name = item.split(" - ")[0]
descriptions_by_location[(region, name)].append(slot['Text'])
descriptions_by_item[name].append(slot['Text'])
counts_by_location = {
location: len(descriptions) for (location, descriptions) in descriptions_by_location.items()
}
location_names_to_descriptions = {}
for location in location_dictionary.values():
if location.ap_code is None: continue
if location.static:
location_names_to_descriptions[location.name] = descriptions_by_key[location.static]
continue
match = location_re.match(location.name)
if not match:
raise Exception(f"Location name \"{location.name}\" doesn't match expected format.")
item_candidates = descriptions_by_item[match[2]]
if len(item_candidates) == 1:
location_names_to_descriptions[location.name] = item_candidates[0]
continue
key = (match[1], match[2])
if key not in descriptions_by_location:
raise Exception(f'No static randomizer location found matching "{match[1]}: {match[2]}".')
candidates = descriptions_by_location[key]
if len(candidates) == 0:
raise Exception(
f'There are only {counts_by_location[key]} locations in the static randomizer ' +
f'matching "{match[1]}: {match[2]}", but there are more in Archipelago.'
)
location_names_to_descriptions[location.name] = candidates.pop(0)
table = "<table><tr><th>Location name</th><th>Detailed description</th>\n"
for (name, description) in sorted(
location_names_to_descriptions.items(),
key = lambda pair: pair[0]
):
table += f"<tr><td>{html.escape(name)}</td><td>{html.escape(description)}</td></tr>\n"
table += "</table>\n"
with open(os.path.join(os.path.dirname(__file__), 'docs/locations_en.md'), 'r+') as f:
original = f.read()
start_flag = "<!-- begin location table -->\n"
start = original.index(start_flag) + len(start_flag)
end = original.index("<!-- end location table -->")
f.seek(0)
f.write(original[:start] + table + original[end:])
f.truncate()
print("Updated docs/locations_en.md!")

View File

@@ -1,28 +1,201 @@
# Dark Souls III
Game Page | [Items] | [Locations]
[Items]: /tutorial/Dark%20Souls%20III/items/en
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
## What do I need to do to randomize DS3?
See full instructions on [the setup page].
[the setup page]: /tutorial/Dark%20Souls%20III/setup/en
## Where is the options page?
The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
The [player options page for this game][options] contains all the options you
need to configure and export a config file.
[options]: ../player-options
## What does randomization do to this game?
Items that can be picked up from static corpses, taken from chests, or earned from defeating enemies or NPCs can be
randomized. Common pickups like titanite shards or firebombs can be randomized as "progressive" items. That is, the
location "Titanite Shard #5" is the fifth titanite shard you pick up, no matter where it was from. This is also what
happens when you randomize Estus Shards and Undead Bone Shards.
1. All item locations are randomized, including those in the overworld, in
shops, and dropped by enemies. Most locations can contain games from other
worlds, and any items from your world can appear in other players' worlds.
It's also possible to randomize the upgrade level of weapons and shields as well as their infusions (if they can have
one). Additionally, there are options that can make the randomized experience more convenient or more interesting, such as
removing weapon requirements or auto-equipping whatever equipment you most recently received.
2. By default, all enemies and bosses are randomized. This can be disabled by
setting "Randomize Enemies" to false.
The goal is to find the four "Cinders of a Lord" items randomized into the multiworld and defeat the Soul of Cinder.
3. By default, the starting equipment for each class is randomized. This can be
disabled by setting "Randomize Starting Loadout" to false.
## What Dark Souls III items can appear in other players' worlds?
4. By setting the "Randomize Weapon Level" or "Randomize Infusion" options, you
can randomize whether the weapons you find will be upgraded or infused.
Practically anything can be found in other worlds including pieces of armor, upgraded weapons, key items, consumables,
spells, upgrade materials, etc...
There are also options that can make playing the game more convenient or
bring a new experience, like removing equip loads or auto-equipping weapons as
you pick them up. Check out [the options page][options] for more!
## What does another world's item look like in Dark Souls III?
## What's the goal?
In Dark Souls III, items which are sent to other worlds appear as Prism Stones.
Your goal is to find the four "Cinders of a Lord" items randomized into the
multiworld and defeat the boss in the Kiln of the First Flame.
## Do I have to check every item in every area?
Dark Souls III has about 1500 item locations, which is a lot of checks for a
single run! But you don't necessarily need to check all of them. Locations that
you can potentially miss, such as rewards for failable quests or soul
transposition items, will _never_ have items required for any game to progress.
The following types of locations are also guaranteed not to contain progression
items by default:
* **Hidden:** Locations that are particularly difficult to find, such as behind
illusory walls, down hidden drops, and so on. Does not include large locations
like Untended Graves or Archdragon Peak.
* **Small Crystal Lizards:** Drops from small crystal lizards.
* **Upgrade:** Locations that contain upgrade items in vanilla, including
titanite, gems, and Shriving Stones.
* **Small Souls:** Locations that contain soul items in vanilla, not including
boss souls.
* **Miscellaneous:** Locations that contain generic stackable items in vanilla,
such as arrows, firebombs, buffs, and so on.
You can customize which locations are guaranteed not to contain progression
items by setting the `exclude_locations` field in your YAML to the [location
groups] you want to omit. For example, this is the default setting but without
"Hidden" so that hidden locations can contain progression items:
[location groups]: /tutorial/Dark%20Souls%20III/locations/en#location-groups
```yaml
Dark Souls III:
exclude_locations:
- Small Crystal Lizards
- Upgrade
- Small Souls
- Miscellaneous
```
This allows _all_ non-missable locations to have progression items, if you're in
for the long haul:
```yaml
Dark Souls III:
exclude_locations: []
```
## What if I don't want to do the whole game?
If you want a shorter DS3 randomizer experience, you can exclude entire regions
from containing progression items. The items and enemies from those regions will
still be included in the randomization pool, but none of them will be mandatory.
For example, the following configuration just requires you to play the game
through Irithyll of the Boreal Valley:
```yaml
Dark Souls III:
# Enable the DLC so it's included in the randomization pool
enable_dlc: true
exclude_locations:
# Exclude late-game and DLC regions
- Anor Londo
- Lothric Castle
- Consumed King's Garden
- Untended Graves
- Grand Archives
- Archdragon Peak
- Painted World of Ariandel
- Dreg Heap
- Ringed City
# Default exclusions
- Hidden
- Small Crystal Lizards
- Upgrade
- Small Souls
- Miscellaneous
```
## Where can I learn more about Dark Souls III locations?
Location names have to pack a lot of information into very little space. To
better understand them, check out the [location guide], which explains all the
names used in locations and provides more detailed descriptions for each
individual location.
[location guide]: /tutorial/Dark%20Souls%20III/locations/en
## Where can I learn more about Dark Souls III items?
Check out the [item guide], which explains the named groups available for items.
[item guide]: /tutorial/Dark%20Souls%20III/items/en
## What's new from 2.x.x?
Version 3.0.0 of the Dark Souls III Archipelago client has a number of
substantial differences with the older 2.x.x versions. Improvements include:
* Support for randomizing all item locations, not just unique items.
* Support for randomizing items in shops, starting loadouts, Path of the Dragon,
and more.
* Built-in integration with the enemy randomizer, including consistent seeding
for races.
* Support for the latest patch for Dark Souls III, 1.15.2. Older patches are
*not* supported.
* Optional smooth distribution for upgrade items, upgraded weapons, and soul
items so you're more likely to see weaker items earlier and more powerful
items later.
* More detailed location names that indicate where a location is, not just what
it replaces.
* Other players' item names are visible in DS3.
* If you pick up items while static, they'll still send once you reconnect.
However, 2.x.x YAMLs are not compatible with 3.0.0. You'll need to [generate a
new YAML configuration] for use with 3.x.x.
[generating a new YAML configuration]: /games/Dark%20Souls%20III/player-options
The following options have been removed:
* `enable_boss_locations` is now controlled by the `soul_locations` option.
* `enable_progressive_locations` was removed because all locations are now
individually randomized rather than replaced with a progressive list.
* `pool_type` has been removed. Since there are no longer any non-randomized
items in randomized categories, there's not a meaningful distinction between
"shuffle" and "various" mode.
* `enable_*_locations` options have all been removed. Instead, you can now add
[location group names] to the `exclude_locations` option to prevent them from
containing important items.
[location group names]: /tutorial/Dark%20Souls%20III/locations/en#location-groups
By default, the Hidden, Small Crystal Lizards, Upgrade, Small Souls, and
Miscellaneous groups are in `exclude_locations`. Once you've chosen your
excluded locations, you can set `excluded_locations: unrandomized` to preserve
the default vanilla item placements for all excluded locations.
* `guaranteed_items`: In almost all cases, all items from the base game are now
included somewhere in the multiworld.
In addition, the following options have changed:
* The location names used in options like `exclude_locations` have changed. See
the [location guide] for a full description.

View File

@@ -0,0 +1,24 @@
# Dark Souls III Items
[Game Page] | Items | [Locations]
[Game Page]: /games/Dark%20Souls%20III/info/en
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
## Item Groups
The Dark Souls III randomizer supports a number of item group names, which can
be used in YAML options like `local_items` to refer to many items at once:
* **Progression:** Items which unlock locations.
* **Cinders:** All four Cinders of a Lord. Once you have these four, you can
fight Soul of Cinder and win the game.
* **Miscellaneous:** Generic stackable items, such as arrows, firebombs, buffs,
and so on.
* **Unique:** Items that are unique per NG cycle, such as scrolls, keys, ashes,
and so on. Doesn't include equipment, spells, or souls.
* **Boss Souls:** Souls that can be traded with Ludleth, including Soul of
Rosaria.
* **Small Souls:** Soul items, not including boss souls.
* **Upgrade:** Upgrade items, including titanite, gems, and Shriving Stones.
* **Healing:** Undead Bone Shards and Estus Shards.

File diff suppressed because it is too large Load Diff

View File

@@ -7,48 +7,49 @@
## Optional Software
- [Dark Souls III Maptracker Pack](https://github.com/Br00ty/DS3_AP_Maptracker/releases/latest), for use with [Poptracker](https://github.com/black-sliver/PopTracker/releases)
- Map tracker not yet updated for 3.0.0
## General Concept
## Setting Up
<span style="color:#ff7800">
**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.
First, download the client from the link above. It doesn't need to go into any particular directory;
it'll automatically locate _Dark Souls III_ in your Steam installation folder.
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.
Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This
is the latest version, so you don't need to do any downpatching! However, if you've already
downpatched your game to use an older version of the randomizer, you'll need to reinstall the latest
version before using this version.
## Downpatching Dark Souls III
### One-Time Setup
To downpatch DS3 for use with Archipelago, use the following instructions from the speedsouls wiki database.
Before you first connect to a multiworld, you need to generate the local data files for your world's
randomized item and (optionally) enemy locations. You only need to do this once per multiworld.
1. Launch Steam (in online mode).
2. Press the Windows Key + R. This will open the Run window.
3. Open the Steam console by typing the following string: `steam://open/console`. Steam should now open in Console Mode.
4. Insert the string of the depot you wish to download. For the AP-supported v1.15, you will want to use: `download_depot 374320 374321 4471176929659548333`.
5. Steam will now download the depot. Note: There is no progress bar for the download in Steam, but it is still downloading in the background.
6. Back up your existing game executable (`DarkSoulsIII.exe`) found in `\Steam\steamapps\common\DARK SOULS III\Game`. Easiest way to do this is to move it to another directory. If you have file extensions enabled, you can instead rename the executable to `DarkSoulsIII.exe.bak`.
7. Return to the Steam console. Once the download is complete, it should say so along with the temporary local directory in which the depot has been stored. This is usually something like `\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX`.
8. Take the `DarkSoulsIII.exe` from that folder and place it in `\Steam\steamapps\common\DARK SOULS III\Game`.
9. Back up and delete your save file (`DS30000.sl2`) in AppData. AppData is hidden by default. To locate it, press Windows Key + R, type `%appdata%` and hit enter. Alternatively: open File Explorer > View > Hidden Items and follow `C:\Users\<your_username>\AppData\Roaming\DarkSoulsIII\<numbers>`.
10. If you did all these steps correctly, you should be able to confirm your game version in the upper-left corner after launching Dark Souls III.
1. Before you first connect to a multiworld, run `randomizer\DS3Randomizer.exe`.
2. Put in your Archipelago room address (usually something like `archipelago.gg:12345`), your player
name (also known as your "slot name"), and your password if you have one.
## Installing the Archipelago mod
3. Click "Load" and wait a minute or two.
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`)
### Running and Connecting the Game
## Joining a MultiWorld Game
To run _Dark Souls III_ in Archipelago mode:
1. Run Steam in offline mode to avoid being banned.
2. Launch Dark Souls III.
3. Type in `/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME} password:{PASSWORD}` in the "Windows Command Prompt" that opened. For example: `/connect archipelago.gg:38281 "Example Name" password:"Example Password"`. The password parameter is only necessary if your game requires one.
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.
1. Start Steam. **Do not run in offline mode.** The mod will make sure you don't connect to the
DS3 servers, and running Steam in offline mode will make certain scripted invaders fail to spawn.
## Where do I get a config file?
2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that
you can use to interact with the Archipelago server.
3. Type `/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}` into the command prompt, with the
appropriate values filled in. For example: `/connect archipelago.gg:24242 PlayerName`.
4. Start playing as normal. An "Archipelago connected" message will appear onscreen once you have
control of your character and the connection is established.
## Frequently Asked Questions
### Where do I get a config file?
The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to
configure your personal options and export them into a config file.

View File

@@ -0,0 +1,27 @@
from test.TestBase import WorldTestBase
from worlds.dark_souls_3.Items import item_dictionary
from worlds.dark_souls_3.Locations import location_tables
from worlds.dark_souls_3.Bosses import all_bosses
class DarkSouls3Test(WorldTestBase):
game = "Dark Souls III"
def testLocationDefaultItems(self):
for locations in location_tables.values():
for location in locations:
if location.default_item_name:
self.assertIn(location.default_item_name, item_dictionary)
def testLocationsUnique(self):
names = set()
for locations in location_tables.values():
for location in locations:
self.assertNotIn(location.name, names)
names.add(location.name)
def testBossLocations(self):
all_locations = {location.name for locations in location_tables.values() for location in locations}
for boss in all_bosses:
for location in boss.locations:
self.assertIn(location, all_locations)

View File

@@ -2,7 +2,7 @@
## Required Software
- [DOOM 1993 (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM_1993/)
- [DOOM 1993 (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM__DOOM_II/)
- [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases)
## Optional Software

View File

@@ -2,7 +2,7 @@
## Required Software
- [DOOM II (e.g. Steam version)](https://store.steampowered.com/app/2300/DOOM_II/)
- [DOOM II (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM__DOOM_II/)
- [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases)
## Optional Software

View File

@@ -467,7 +467,7 @@ class HKWorld(World):
worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]]
if worlds:
grubs = [item for item in multiworld.get_items() if item.name == "Grub"]
all_grub_players = [world.player for world in multiworld.worlds.values() if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]]
all_grub_players = [world.player for world in worlds if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]]
if all_grub_players:
group_lookup = defaultdict(set)

View File

@@ -1,940 +0,0 @@
import typing
from BaseClasses import Location, Region
from .Names import LocationName
if typing.TYPE_CHECKING:
from .Room import KDL3Room
class KDL3Location(Location):
game: str = "Kirby's Dream Land 3"
room: typing.Optional["KDL3Room"] = None
def __init__(self, player: int, name: str, address: typing.Optional[int], parent: typing.Union[Region, None]):
super().__init__(player, name, address, parent)
if not address:
self.show_in_spoiler = False
stage_locations = {
0x770001: LocationName.grass_land_1,
0x770002: LocationName.grass_land_2,
0x770003: LocationName.grass_land_3,
0x770004: LocationName.grass_land_4,
0x770005: LocationName.grass_land_5,
0x770006: LocationName.grass_land_6,
0x770007: LocationName.ripple_field_1,
0x770008: LocationName.ripple_field_2,
0x770009: LocationName.ripple_field_3,
0x77000A: LocationName.ripple_field_4,
0x77000B: LocationName.ripple_field_5,
0x77000C: LocationName.ripple_field_6,
0x77000D: LocationName.sand_canyon_1,
0x77000E: LocationName.sand_canyon_2,
0x77000F: LocationName.sand_canyon_3,
0x770010: LocationName.sand_canyon_4,
0x770011: LocationName.sand_canyon_5,
0x770012: LocationName.sand_canyon_6,
0x770013: LocationName.cloudy_park_1,
0x770014: LocationName.cloudy_park_2,
0x770015: LocationName.cloudy_park_3,
0x770016: LocationName.cloudy_park_4,
0x770017: LocationName.cloudy_park_5,
0x770018: LocationName.cloudy_park_6,
0x770019: LocationName.iceberg_1,
0x77001A: LocationName.iceberg_2,
0x77001B: LocationName.iceberg_3,
0x77001C: LocationName.iceberg_4,
0x77001D: LocationName.iceberg_5,
0x77001E: LocationName.iceberg_6,
}
heart_star_locations = {
0x770101: LocationName.grass_land_tulip,
0x770102: LocationName.grass_land_muchi,
0x770103: LocationName.grass_land_pitcherman,
0x770104: LocationName.grass_land_chao,
0x770105: LocationName.grass_land_mine,
0x770106: LocationName.grass_land_pierre,
0x770107: LocationName.ripple_field_kamuribana,
0x770108: LocationName.ripple_field_bakasa,
0x770109: LocationName.ripple_field_elieel,
0x77010A: LocationName.ripple_field_toad,
0x77010B: LocationName.ripple_field_mama_pitch,
0x77010C: LocationName.ripple_field_hb002,
0x77010D: LocationName.sand_canyon_mushrooms,
0x77010E: LocationName.sand_canyon_auntie,
0x77010F: LocationName.sand_canyon_caramello,
0x770110: LocationName.sand_canyon_hikari,
0x770111: LocationName.sand_canyon_nyupun,
0x770112: LocationName.sand_canyon_rob,
0x770113: LocationName.cloudy_park_hibanamodoki,
0x770114: LocationName.cloudy_park_piyokeko,
0x770115: LocationName.cloudy_park_mrball,
0x770116: LocationName.cloudy_park_mikarin,
0x770117: LocationName.cloudy_park_pick,
0x770118: LocationName.cloudy_park_hb007,
0x770119: LocationName.iceberg_kogoesou,
0x77011A: LocationName.iceberg_samus,
0x77011B: LocationName.iceberg_kawasaki,
0x77011C: LocationName.iceberg_name,
0x77011D: LocationName.iceberg_shiro,
0x77011E: LocationName.iceberg_angel,
}
boss_locations = {
0x770200: LocationName.grass_land_whispy,
0x770201: LocationName.ripple_field_acro,
0x770202: LocationName.sand_canyon_poncon,
0x770203: LocationName.cloudy_park_ado,
0x770204: LocationName.iceberg_dedede,
}
consumable_locations = {
0x770300: LocationName.grass_land_1_u1,
0x770301: LocationName.grass_land_1_m1,
0x770302: LocationName.grass_land_2_u1,
0x770303: LocationName.grass_land_3_u1,
0x770304: LocationName.grass_land_3_m1,
0x770305: LocationName.grass_land_4_m1,
0x770306: LocationName.grass_land_4_u1,
0x770307: LocationName.grass_land_4_m2,
0x770308: LocationName.grass_land_4_m3,
0x770309: LocationName.grass_land_6_u1,
0x77030A: LocationName.grass_land_6_u2,
0x77030B: LocationName.ripple_field_2_u1,
0x77030C: LocationName.ripple_field_2_m1,
0x77030D: LocationName.ripple_field_3_m1,
0x77030E: LocationName.ripple_field_3_u1,
0x77030F: LocationName.ripple_field_4_m2,
0x770310: LocationName.ripple_field_4_u1,
0x770311: LocationName.ripple_field_4_m1,
0x770312: LocationName.ripple_field_5_u1,
0x770313: LocationName.ripple_field_5_m2,
0x770314: LocationName.ripple_field_5_m1,
0x770315: LocationName.sand_canyon_1_u1,
0x770316: LocationName.sand_canyon_2_u1,
0x770317: LocationName.sand_canyon_2_m1,
0x770318: LocationName.sand_canyon_4_m1,
0x770319: LocationName.sand_canyon_4_u1,
0x77031A: LocationName.sand_canyon_4_m2,
0x77031B: LocationName.sand_canyon_5_u1,
0x77031C: LocationName.sand_canyon_5_u3,
0x77031D: LocationName.sand_canyon_5_m1,
0x77031E: LocationName.sand_canyon_5_u4,
0x77031F: LocationName.sand_canyon_5_u2,
0x770320: LocationName.cloudy_park_1_m1,
0x770321: LocationName.cloudy_park_1_u1,
0x770322: LocationName.cloudy_park_4_u1,
0x770323: LocationName.cloudy_park_4_m1,
0x770324: LocationName.cloudy_park_5_m1,
0x770325: LocationName.cloudy_park_6_u1,
0x770326: LocationName.iceberg_3_m1,
0x770327: LocationName.iceberg_5_u1,
0x770328: LocationName.iceberg_5_u2,
0x770329: LocationName.iceberg_5_u3,
0x77032A: LocationName.iceberg_6_m1,
0x77032B: LocationName.iceberg_6_u1,
}
level_consumables = {
1: [0, 1],
2: [2],
3: [3, 4],
4: [5, 6, 7, 8],
6: [9, 10],
8: [11, 12],
9: [13, 14],
10: [15, 16, 17],
11: [18, 19, 20],
13: [21],
14: [22, 23],
16: [24, 25, 26],
17: [27, 28, 29, 30, 31],
19: [32, 33],
22: [34, 35],
23: [36],
24: [37],
27: [38],
29: [39, 40, 41],
30: [42, 43],
}
star_locations = {
0x770401: LocationName.grass_land_1_s1,
0x770402: LocationName.grass_land_1_s2,
0x770403: LocationName.grass_land_1_s3,
0x770404: LocationName.grass_land_1_s4,
0x770405: LocationName.grass_land_1_s5,
0x770406: LocationName.grass_land_1_s6,
0x770407: LocationName.grass_land_1_s7,
0x770408: LocationName.grass_land_1_s8,
0x770409: LocationName.grass_land_1_s9,
0x77040a: LocationName.grass_land_1_s10,
0x77040b: LocationName.grass_land_1_s11,
0x77040c: LocationName.grass_land_1_s12,
0x77040d: LocationName.grass_land_1_s13,
0x77040e: LocationName.grass_land_1_s14,
0x77040f: LocationName.grass_land_1_s15,
0x770410: LocationName.grass_land_1_s16,
0x770411: LocationName.grass_land_1_s17,
0x770412: LocationName.grass_land_1_s18,
0x770413: LocationName.grass_land_1_s19,
0x770414: LocationName.grass_land_1_s20,
0x770415: LocationName.grass_land_1_s21,
0x770416: LocationName.grass_land_1_s22,
0x770417: LocationName.grass_land_1_s23,
0x770418: LocationName.grass_land_2_s1,
0x770419: LocationName.grass_land_2_s2,
0x77041a: LocationName.grass_land_2_s3,
0x77041b: LocationName.grass_land_2_s4,
0x77041c: LocationName.grass_land_2_s5,
0x77041d: LocationName.grass_land_2_s6,
0x77041e: LocationName.grass_land_2_s7,
0x77041f: LocationName.grass_land_2_s8,
0x770420: LocationName.grass_land_2_s9,
0x770421: LocationName.grass_land_2_s10,
0x770422: LocationName.grass_land_2_s11,
0x770423: LocationName.grass_land_2_s12,
0x770424: LocationName.grass_land_2_s13,
0x770425: LocationName.grass_land_2_s14,
0x770426: LocationName.grass_land_2_s15,
0x770427: LocationName.grass_land_2_s16,
0x770428: LocationName.grass_land_2_s17,
0x770429: LocationName.grass_land_2_s18,
0x77042a: LocationName.grass_land_2_s19,
0x77042b: LocationName.grass_land_2_s20,
0x77042c: LocationName.grass_land_2_s21,
0x77042d: LocationName.grass_land_3_s1,
0x77042e: LocationName.grass_land_3_s2,
0x77042f: LocationName.grass_land_3_s3,
0x770430: LocationName.grass_land_3_s4,
0x770431: LocationName.grass_land_3_s5,
0x770432: LocationName.grass_land_3_s6,
0x770433: LocationName.grass_land_3_s7,
0x770434: LocationName.grass_land_3_s8,
0x770435: LocationName.grass_land_3_s9,
0x770436: LocationName.grass_land_3_s10,
0x770437: LocationName.grass_land_3_s11,
0x770438: LocationName.grass_land_3_s12,
0x770439: LocationName.grass_land_3_s13,
0x77043a: LocationName.grass_land_3_s14,
0x77043b: LocationName.grass_land_3_s15,
0x77043c: LocationName.grass_land_3_s16,
0x77043d: LocationName.grass_land_3_s17,
0x77043e: LocationName.grass_land_3_s18,
0x77043f: LocationName.grass_land_3_s19,
0x770440: LocationName.grass_land_3_s20,
0x770441: LocationName.grass_land_3_s21,
0x770442: LocationName.grass_land_3_s22,
0x770443: LocationName.grass_land_3_s23,
0x770444: LocationName.grass_land_3_s24,
0x770445: LocationName.grass_land_3_s25,
0x770446: LocationName.grass_land_3_s26,
0x770447: LocationName.grass_land_3_s27,
0x770448: LocationName.grass_land_3_s28,
0x770449: LocationName.grass_land_3_s29,
0x77044a: LocationName.grass_land_3_s30,
0x77044b: LocationName.grass_land_3_s31,
0x77044c: LocationName.grass_land_4_s1,
0x77044d: LocationName.grass_land_4_s2,
0x77044e: LocationName.grass_land_4_s3,
0x77044f: LocationName.grass_land_4_s4,
0x770450: LocationName.grass_land_4_s5,
0x770451: LocationName.grass_land_4_s6,
0x770452: LocationName.grass_land_4_s7,
0x770453: LocationName.grass_land_4_s8,
0x770454: LocationName.grass_land_4_s9,
0x770455: LocationName.grass_land_4_s10,
0x770456: LocationName.grass_land_4_s11,
0x770457: LocationName.grass_land_4_s12,
0x770458: LocationName.grass_land_4_s13,
0x770459: LocationName.grass_land_4_s14,
0x77045a: LocationName.grass_land_4_s15,
0x77045b: LocationName.grass_land_4_s16,
0x77045c: LocationName.grass_land_4_s17,
0x77045d: LocationName.grass_land_4_s18,
0x77045e: LocationName.grass_land_4_s19,
0x77045f: LocationName.grass_land_4_s20,
0x770460: LocationName.grass_land_4_s21,
0x770461: LocationName.grass_land_4_s22,
0x770462: LocationName.grass_land_4_s23,
0x770463: LocationName.grass_land_4_s24,
0x770464: LocationName.grass_land_4_s25,
0x770465: LocationName.grass_land_4_s26,
0x770466: LocationName.grass_land_4_s27,
0x770467: LocationName.grass_land_4_s28,
0x770468: LocationName.grass_land_4_s29,
0x770469: LocationName.grass_land_4_s30,
0x77046a: LocationName.grass_land_4_s31,
0x77046b: LocationName.grass_land_4_s32,
0x77046c: LocationName.grass_land_4_s33,
0x77046d: LocationName.grass_land_4_s34,
0x77046e: LocationName.grass_land_4_s35,
0x77046f: LocationName.grass_land_4_s36,
0x770470: LocationName.grass_land_4_s37,
0x770471: LocationName.grass_land_5_s1,
0x770472: LocationName.grass_land_5_s2,
0x770473: LocationName.grass_land_5_s3,
0x770474: LocationName.grass_land_5_s4,
0x770475: LocationName.grass_land_5_s5,
0x770476: LocationName.grass_land_5_s6,
0x770477: LocationName.grass_land_5_s7,
0x770478: LocationName.grass_land_5_s8,
0x770479: LocationName.grass_land_5_s9,
0x77047a: LocationName.grass_land_5_s10,
0x77047b: LocationName.grass_land_5_s11,
0x77047c: LocationName.grass_land_5_s12,
0x77047d: LocationName.grass_land_5_s13,
0x77047e: LocationName.grass_land_5_s14,
0x77047f: LocationName.grass_land_5_s15,
0x770480: LocationName.grass_land_5_s16,
0x770481: LocationName.grass_land_5_s17,
0x770482: LocationName.grass_land_5_s18,
0x770483: LocationName.grass_land_5_s19,
0x770484: LocationName.grass_land_5_s20,
0x770485: LocationName.grass_land_5_s21,
0x770486: LocationName.grass_land_5_s22,
0x770487: LocationName.grass_land_5_s23,
0x770488: LocationName.grass_land_5_s24,
0x770489: LocationName.grass_land_5_s25,
0x77048a: LocationName.grass_land_5_s26,
0x77048b: LocationName.grass_land_5_s27,
0x77048c: LocationName.grass_land_5_s28,
0x77048d: LocationName.grass_land_5_s29,
0x77048e: LocationName.grass_land_6_s1,
0x77048f: LocationName.grass_land_6_s2,
0x770490: LocationName.grass_land_6_s3,
0x770491: LocationName.grass_land_6_s4,
0x770492: LocationName.grass_land_6_s5,
0x770493: LocationName.grass_land_6_s6,
0x770494: LocationName.grass_land_6_s7,
0x770495: LocationName.grass_land_6_s8,
0x770496: LocationName.grass_land_6_s9,
0x770497: LocationName.grass_land_6_s10,
0x770498: LocationName.grass_land_6_s11,
0x770499: LocationName.grass_land_6_s12,
0x77049a: LocationName.grass_land_6_s13,
0x77049b: LocationName.grass_land_6_s14,
0x77049c: LocationName.grass_land_6_s15,
0x77049d: LocationName.grass_land_6_s16,
0x77049e: LocationName.grass_land_6_s17,
0x77049f: LocationName.grass_land_6_s18,
0x7704a0: LocationName.grass_land_6_s19,
0x7704a1: LocationName.grass_land_6_s20,
0x7704a2: LocationName.grass_land_6_s21,
0x7704a3: LocationName.grass_land_6_s22,
0x7704a4: LocationName.grass_land_6_s23,
0x7704a5: LocationName.grass_land_6_s24,
0x7704a6: LocationName.grass_land_6_s25,
0x7704a7: LocationName.grass_land_6_s26,
0x7704a8: LocationName.grass_land_6_s27,
0x7704a9: LocationName.grass_land_6_s28,
0x7704aa: LocationName.grass_land_6_s29,
0x7704ab: LocationName.ripple_field_1_s1,
0x7704ac: LocationName.ripple_field_1_s2,
0x7704ad: LocationName.ripple_field_1_s3,
0x7704ae: LocationName.ripple_field_1_s4,
0x7704af: LocationName.ripple_field_1_s5,
0x7704b0: LocationName.ripple_field_1_s6,
0x7704b1: LocationName.ripple_field_1_s7,
0x7704b2: LocationName.ripple_field_1_s8,
0x7704b3: LocationName.ripple_field_1_s9,
0x7704b4: LocationName.ripple_field_1_s10,
0x7704b5: LocationName.ripple_field_1_s11,
0x7704b6: LocationName.ripple_field_1_s12,
0x7704b7: LocationName.ripple_field_1_s13,
0x7704b8: LocationName.ripple_field_1_s14,
0x7704b9: LocationName.ripple_field_1_s15,
0x7704ba: LocationName.ripple_field_1_s16,
0x7704bb: LocationName.ripple_field_1_s17,
0x7704bc: LocationName.ripple_field_1_s18,
0x7704bd: LocationName.ripple_field_1_s19,
0x7704be: LocationName.ripple_field_2_s1,
0x7704bf: LocationName.ripple_field_2_s2,
0x7704c0: LocationName.ripple_field_2_s3,
0x7704c1: LocationName.ripple_field_2_s4,
0x7704c2: LocationName.ripple_field_2_s5,
0x7704c3: LocationName.ripple_field_2_s6,
0x7704c4: LocationName.ripple_field_2_s7,
0x7704c5: LocationName.ripple_field_2_s8,
0x7704c6: LocationName.ripple_field_2_s9,
0x7704c7: LocationName.ripple_field_2_s10,
0x7704c8: LocationName.ripple_field_2_s11,
0x7704c9: LocationName.ripple_field_2_s12,
0x7704ca: LocationName.ripple_field_2_s13,
0x7704cb: LocationName.ripple_field_2_s14,
0x7704cc: LocationName.ripple_field_2_s15,
0x7704cd: LocationName.ripple_field_2_s16,
0x7704ce: LocationName.ripple_field_2_s17,
0x7704cf: LocationName.ripple_field_3_s1,
0x7704d0: LocationName.ripple_field_3_s2,
0x7704d1: LocationName.ripple_field_3_s3,
0x7704d2: LocationName.ripple_field_3_s4,
0x7704d3: LocationName.ripple_field_3_s5,
0x7704d4: LocationName.ripple_field_3_s6,
0x7704d5: LocationName.ripple_field_3_s7,
0x7704d6: LocationName.ripple_field_3_s8,
0x7704d7: LocationName.ripple_field_3_s9,
0x7704d8: LocationName.ripple_field_3_s10,
0x7704d9: LocationName.ripple_field_3_s11,
0x7704da: LocationName.ripple_field_3_s12,
0x7704db: LocationName.ripple_field_3_s13,
0x7704dc: LocationName.ripple_field_3_s14,
0x7704dd: LocationName.ripple_field_3_s15,
0x7704de: LocationName.ripple_field_3_s16,
0x7704df: LocationName.ripple_field_3_s17,
0x7704e0: LocationName.ripple_field_3_s18,
0x7704e1: LocationName.ripple_field_3_s19,
0x7704e2: LocationName.ripple_field_3_s20,
0x7704e3: LocationName.ripple_field_3_s21,
0x7704e4: LocationName.ripple_field_4_s1,
0x7704e5: LocationName.ripple_field_4_s2,
0x7704e6: LocationName.ripple_field_4_s3,
0x7704e7: LocationName.ripple_field_4_s4,
0x7704e8: LocationName.ripple_field_4_s5,
0x7704e9: LocationName.ripple_field_4_s6,
0x7704ea: LocationName.ripple_field_4_s7,
0x7704eb: LocationName.ripple_field_4_s8,
0x7704ec: LocationName.ripple_field_4_s9,
0x7704ed: LocationName.ripple_field_4_s10,
0x7704ee: LocationName.ripple_field_4_s11,
0x7704ef: LocationName.ripple_field_4_s12,
0x7704f0: LocationName.ripple_field_4_s13,
0x7704f1: LocationName.ripple_field_4_s14,
0x7704f2: LocationName.ripple_field_4_s15,
0x7704f3: LocationName.ripple_field_4_s16,
0x7704f4: LocationName.ripple_field_4_s17,
0x7704f5: LocationName.ripple_field_4_s18,
0x7704f6: LocationName.ripple_field_4_s19,
0x7704f7: LocationName.ripple_field_4_s20,
0x7704f8: LocationName.ripple_field_4_s21,
0x7704f9: LocationName.ripple_field_4_s22,
0x7704fa: LocationName.ripple_field_4_s23,
0x7704fb: LocationName.ripple_field_4_s24,
0x7704fc: LocationName.ripple_field_4_s25,
0x7704fd: LocationName.ripple_field_4_s26,
0x7704fe: LocationName.ripple_field_4_s27,
0x7704ff: LocationName.ripple_field_4_s28,
0x770500: LocationName.ripple_field_4_s29,
0x770501: LocationName.ripple_field_4_s30,
0x770502: LocationName.ripple_field_4_s31,
0x770503: LocationName.ripple_field_4_s32,
0x770504: LocationName.ripple_field_4_s33,
0x770505: LocationName.ripple_field_4_s34,
0x770506: LocationName.ripple_field_4_s35,
0x770507: LocationName.ripple_field_4_s36,
0x770508: LocationName.ripple_field_4_s37,
0x770509: LocationName.ripple_field_4_s38,
0x77050a: LocationName.ripple_field_4_s39,
0x77050b: LocationName.ripple_field_4_s40,
0x77050c: LocationName.ripple_field_4_s41,
0x77050d: LocationName.ripple_field_4_s42,
0x77050e: LocationName.ripple_field_4_s43,
0x77050f: LocationName.ripple_field_4_s44,
0x770510: LocationName.ripple_field_4_s45,
0x770511: LocationName.ripple_field_4_s46,
0x770512: LocationName.ripple_field_4_s47,
0x770513: LocationName.ripple_field_4_s48,
0x770514: LocationName.ripple_field_4_s49,
0x770515: LocationName.ripple_field_4_s50,
0x770516: LocationName.ripple_field_4_s51,
0x770517: LocationName.ripple_field_5_s1,
0x770518: LocationName.ripple_field_5_s2,
0x770519: LocationName.ripple_field_5_s3,
0x77051a: LocationName.ripple_field_5_s4,
0x77051b: LocationName.ripple_field_5_s5,
0x77051c: LocationName.ripple_field_5_s6,
0x77051d: LocationName.ripple_field_5_s7,
0x77051e: LocationName.ripple_field_5_s8,
0x77051f: LocationName.ripple_field_5_s9,
0x770520: LocationName.ripple_field_5_s10,
0x770521: LocationName.ripple_field_5_s11,
0x770522: LocationName.ripple_field_5_s12,
0x770523: LocationName.ripple_field_5_s13,
0x770524: LocationName.ripple_field_5_s14,
0x770525: LocationName.ripple_field_5_s15,
0x770526: LocationName.ripple_field_5_s16,
0x770527: LocationName.ripple_field_5_s17,
0x770528: LocationName.ripple_field_5_s18,
0x770529: LocationName.ripple_field_5_s19,
0x77052a: LocationName.ripple_field_5_s20,
0x77052b: LocationName.ripple_field_5_s21,
0x77052c: LocationName.ripple_field_5_s22,
0x77052d: LocationName.ripple_field_5_s23,
0x77052e: LocationName.ripple_field_5_s24,
0x77052f: LocationName.ripple_field_5_s25,
0x770530: LocationName.ripple_field_5_s26,
0x770531: LocationName.ripple_field_5_s27,
0x770532: LocationName.ripple_field_5_s28,
0x770533: LocationName.ripple_field_5_s29,
0x770534: LocationName.ripple_field_5_s30,
0x770535: LocationName.ripple_field_5_s31,
0x770536: LocationName.ripple_field_5_s32,
0x770537: LocationName.ripple_field_5_s33,
0x770538: LocationName.ripple_field_5_s34,
0x770539: LocationName.ripple_field_5_s35,
0x77053a: LocationName.ripple_field_5_s36,
0x77053b: LocationName.ripple_field_5_s37,
0x77053c: LocationName.ripple_field_5_s38,
0x77053d: LocationName.ripple_field_5_s39,
0x77053e: LocationName.ripple_field_5_s40,
0x77053f: LocationName.ripple_field_5_s41,
0x770540: LocationName.ripple_field_5_s42,
0x770541: LocationName.ripple_field_5_s43,
0x770542: LocationName.ripple_field_5_s44,
0x770543: LocationName.ripple_field_5_s45,
0x770544: LocationName.ripple_field_5_s46,
0x770545: LocationName.ripple_field_5_s47,
0x770546: LocationName.ripple_field_5_s48,
0x770547: LocationName.ripple_field_5_s49,
0x770548: LocationName.ripple_field_5_s50,
0x770549: LocationName.ripple_field_5_s51,
0x77054a: LocationName.ripple_field_6_s1,
0x77054b: LocationName.ripple_field_6_s2,
0x77054c: LocationName.ripple_field_6_s3,
0x77054d: LocationName.ripple_field_6_s4,
0x77054e: LocationName.ripple_field_6_s5,
0x77054f: LocationName.ripple_field_6_s6,
0x770550: LocationName.ripple_field_6_s7,
0x770551: LocationName.ripple_field_6_s8,
0x770552: LocationName.ripple_field_6_s9,
0x770553: LocationName.ripple_field_6_s10,
0x770554: LocationName.ripple_field_6_s11,
0x770555: LocationName.ripple_field_6_s12,
0x770556: LocationName.ripple_field_6_s13,
0x770557: LocationName.ripple_field_6_s14,
0x770558: LocationName.ripple_field_6_s15,
0x770559: LocationName.ripple_field_6_s16,
0x77055a: LocationName.ripple_field_6_s17,
0x77055b: LocationName.ripple_field_6_s18,
0x77055c: LocationName.ripple_field_6_s19,
0x77055d: LocationName.ripple_field_6_s20,
0x77055e: LocationName.ripple_field_6_s21,
0x77055f: LocationName.ripple_field_6_s22,
0x770560: LocationName.ripple_field_6_s23,
0x770561: LocationName.sand_canyon_1_s1,
0x770562: LocationName.sand_canyon_1_s2,
0x770563: LocationName.sand_canyon_1_s3,
0x770564: LocationName.sand_canyon_1_s4,
0x770565: LocationName.sand_canyon_1_s5,
0x770566: LocationName.sand_canyon_1_s6,
0x770567: LocationName.sand_canyon_1_s7,
0x770568: LocationName.sand_canyon_1_s8,
0x770569: LocationName.sand_canyon_1_s9,
0x77056a: LocationName.sand_canyon_1_s10,
0x77056b: LocationName.sand_canyon_1_s11,
0x77056c: LocationName.sand_canyon_1_s12,
0x77056d: LocationName.sand_canyon_1_s13,
0x77056e: LocationName.sand_canyon_1_s14,
0x77056f: LocationName.sand_canyon_1_s15,
0x770570: LocationName.sand_canyon_1_s16,
0x770571: LocationName.sand_canyon_1_s17,
0x770572: LocationName.sand_canyon_1_s18,
0x770573: LocationName.sand_canyon_1_s19,
0x770574: LocationName.sand_canyon_1_s20,
0x770575: LocationName.sand_canyon_1_s21,
0x770576: LocationName.sand_canyon_1_s22,
0x770577: LocationName.sand_canyon_2_s1,
0x770578: LocationName.sand_canyon_2_s2,
0x770579: LocationName.sand_canyon_2_s3,
0x77057a: LocationName.sand_canyon_2_s4,
0x77057b: LocationName.sand_canyon_2_s5,
0x77057c: LocationName.sand_canyon_2_s6,
0x77057d: LocationName.sand_canyon_2_s7,
0x77057e: LocationName.sand_canyon_2_s8,
0x77057f: LocationName.sand_canyon_2_s9,
0x770580: LocationName.sand_canyon_2_s10,
0x770581: LocationName.sand_canyon_2_s11,
0x770582: LocationName.sand_canyon_2_s12,
0x770583: LocationName.sand_canyon_2_s13,
0x770584: LocationName.sand_canyon_2_s14,
0x770585: LocationName.sand_canyon_2_s15,
0x770586: LocationName.sand_canyon_2_s16,
0x770587: LocationName.sand_canyon_2_s17,
0x770588: LocationName.sand_canyon_2_s18,
0x770589: LocationName.sand_canyon_2_s19,
0x77058a: LocationName.sand_canyon_2_s20,
0x77058b: LocationName.sand_canyon_2_s21,
0x77058c: LocationName.sand_canyon_2_s22,
0x77058d: LocationName.sand_canyon_2_s23,
0x77058e: LocationName.sand_canyon_2_s24,
0x77058f: LocationName.sand_canyon_2_s25,
0x770590: LocationName.sand_canyon_2_s26,
0x770591: LocationName.sand_canyon_2_s27,
0x770592: LocationName.sand_canyon_2_s28,
0x770593: LocationName.sand_canyon_2_s29,
0x770594: LocationName.sand_canyon_2_s30,
0x770595: LocationName.sand_canyon_2_s31,
0x770596: LocationName.sand_canyon_2_s32,
0x770597: LocationName.sand_canyon_2_s33,
0x770598: LocationName.sand_canyon_2_s34,
0x770599: LocationName.sand_canyon_2_s35,
0x77059a: LocationName.sand_canyon_2_s36,
0x77059b: LocationName.sand_canyon_2_s37,
0x77059c: LocationName.sand_canyon_2_s38,
0x77059d: LocationName.sand_canyon_2_s39,
0x77059e: LocationName.sand_canyon_2_s40,
0x77059f: LocationName.sand_canyon_2_s41,
0x7705a0: LocationName.sand_canyon_2_s42,
0x7705a1: LocationName.sand_canyon_2_s43,
0x7705a2: LocationName.sand_canyon_2_s44,
0x7705a3: LocationName.sand_canyon_2_s45,
0x7705a4: LocationName.sand_canyon_2_s46,
0x7705a5: LocationName.sand_canyon_2_s47,
0x7705a6: LocationName.sand_canyon_2_s48,
0x7705a7: LocationName.sand_canyon_3_s1,
0x7705a8: LocationName.sand_canyon_3_s2,
0x7705a9: LocationName.sand_canyon_3_s3,
0x7705aa: LocationName.sand_canyon_3_s4,
0x7705ab: LocationName.sand_canyon_3_s5,
0x7705ac: LocationName.sand_canyon_3_s6,
0x7705ad: LocationName.sand_canyon_3_s7,
0x7705ae: LocationName.sand_canyon_3_s8,
0x7705af: LocationName.sand_canyon_3_s9,
0x7705b0: LocationName.sand_canyon_3_s10,
0x7705b1: LocationName.sand_canyon_4_s1,
0x7705b2: LocationName.sand_canyon_4_s2,
0x7705b3: LocationName.sand_canyon_4_s3,
0x7705b4: LocationName.sand_canyon_4_s4,
0x7705b5: LocationName.sand_canyon_4_s5,
0x7705b6: LocationName.sand_canyon_4_s6,
0x7705b7: LocationName.sand_canyon_4_s7,
0x7705b8: LocationName.sand_canyon_4_s8,
0x7705b9: LocationName.sand_canyon_4_s9,
0x7705ba: LocationName.sand_canyon_4_s10,
0x7705bb: LocationName.sand_canyon_4_s11,
0x7705bc: LocationName.sand_canyon_4_s12,
0x7705bd: LocationName.sand_canyon_4_s13,
0x7705be: LocationName.sand_canyon_4_s14,
0x7705bf: LocationName.sand_canyon_4_s15,
0x7705c0: LocationName.sand_canyon_4_s16,
0x7705c1: LocationName.sand_canyon_4_s17,
0x7705c2: LocationName.sand_canyon_4_s18,
0x7705c3: LocationName.sand_canyon_4_s19,
0x7705c4: LocationName.sand_canyon_4_s20,
0x7705c5: LocationName.sand_canyon_4_s21,
0x7705c6: LocationName.sand_canyon_4_s22,
0x7705c7: LocationName.sand_canyon_4_s23,
0x7705c8: LocationName.sand_canyon_5_s1,
0x7705c9: LocationName.sand_canyon_5_s2,
0x7705ca: LocationName.sand_canyon_5_s3,
0x7705cb: LocationName.sand_canyon_5_s4,
0x7705cc: LocationName.sand_canyon_5_s5,
0x7705cd: LocationName.sand_canyon_5_s6,
0x7705ce: LocationName.sand_canyon_5_s7,
0x7705cf: LocationName.sand_canyon_5_s8,
0x7705d0: LocationName.sand_canyon_5_s9,
0x7705d1: LocationName.sand_canyon_5_s10,
0x7705d2: LocationName.sand_canyon_5_s11,
0x7705d3: LocationName.sand_canyon_5_s12,
0x7705d4: LocationName.sand_canyon_5_s13,
0x7705d5: LocationName.sand_canyon_5_s14,
0x7705d6: LocationName.sand_canyon_5_s15,
0x7705d7: LocationName.sand_canyon_5_s16,
0x7705d8: LocationName.sand_canyon_5_s17,
0x7705d9: LocationName.sand_canyon_5_s18,
0x7705da: LocationName.sand_canyon_5_s19,
0x7705db: LocationName.sand_canyon_5_s20,
0x7705dc: LocationName.sand_canyon_5_s21,
0x7705dd: LocationName.sand_canyon_5_s22,
0x7705de: LocationName.sand_canyon_5_s23,
0x7705df: LocationName.sand_canyon_5_s24,
0x7705e0: LocationName.sand_canyon_5_s25,
0x7705e1: LocationName.sand_canyon_5_s26,
0x7705e2: LocationName.sand_canyon_5_s27,
0x7705e3: LocationName.sand_canyon_5_s28,
0x7705e4: LocationName.sand_canyon_5_s29,
0x7705e5: LocationName.sand_canyon_5_s30,
0x7705e6: LocationName.sand_canyon_5_s31,
0x7705e7: LocationName.sand_canyon_5_s32,
0x7705e8: LocationName.sand_canyon_5_s33,
0x7705e9: LocationName.sand_canyon_5_s34,
0x7705ea: LocationName.sand_canyon_5_s35,
0x7705eb: LocationName.sand_canyon_5_s36,
0x7705ec: LocationName.sand_canyon_5_s37,
0x7705ed: LocationName.sand_canyon_5_s38,
0x7705ee: LocationName.sand_canyon_5_s39,
0x7705ef: LocationName.sand_canyon_5_s40,
0x7705f0: LocationName.cloudy_park_1_s1,
0x7705f1: LocationName.cloudy_park_1_s2,
0x7705f2: LocationName.cloudy_park_1_s3,
0x7705f3: LocationName.cloudy_park_1_s4,
0x7705f4: LocationName.cloudy_park_1_s5,
0x7705f5: LocationName.cloudy_park_1_s6,
0x7705f6: LocationName.cloudy_park_1_s7,
0x7705f7: LocationName.cloudy_park_1_s8,
0x7705f8: LocationName.cloudy_park_1_s9,
0x7705f9: LocationName.cloudy_park_1_s10,
0x7705fa: LocationName.cloudy_park_1_s11,
0x7705fb: LocationName.cloudy_park_1_s12,
0x7705fc: LocationName.cloudy_park_1_s13,
0x7705fd: LocationName.cloudy_park_1_s14,
0x7705fe: LocationName.cloudy_park_1_s15,
0x7705ff: LocationName.cloudy_park_1_s16,
0x770600: LocationName.cloudy_park_1_s17,
0x770601: LocationName.cloudy_park_1_s18,
0x770602: LocationName.cloudy_park_1_s19,
0x770603: LocationName.cloudy_park_1_s20,
0x770604: LocationName.cloudy_park_1_s21,
0x770605: LocationName.cloudy_park_1_s22,
0x770606: LocationName.cloudy_park_1_s23,
0x770607: LocationName.cloudy_park_2_s1,
0x770608: LocationName.cloudy_park_2_s2,
0x770609: LocationName.cloudy_park_2_s3,
0x77060a: LocationName.cloudy_park_2_s4,
0x77060b: LocationName.cloudy_park_2_s5,
0x77060c: LocationName.cloudy_park_2_s6,
0x77060d: LocationName.cloudy_park_2_s7,
0x77060e: LocationName.cloudy_park_2_s8,
0x77060f: LocationName.cloudy_park_2_s9,
0x770610: LocationName.cloudy_park_2_s10,
0x770611: LocationName.cloudy_park_2_s11,
0x770612: LocationName.cloudy_park_2_s12,
0x770613: LocationName.cloudy_park_2_s13,
0x770614: LocationName.cloudy_park_2_s14,
0x770615: LocationName.cloudy_park_2_s15,
0x770616: LocationName.cloudy_park_2_s16,
0x770617: LocationName.cloudy_park_2_s17,
0x770618: LocationName.cloudy_park_2_s18,
0x770619: LocationName.cloudy_park_2_s19,
0x77061a: LocationName.cloudy_park_2_s20,
0x77061b: LocationName.cloudy_park_2_s21,
0x77061c: LocationName.cloudy_park_2_s22,
0x77061d: LocationName.cloudy_park_2_s23,
0x77061e: LocationName.cloudy_park_2_s24,
0x77061f: LocationName.cloudy_park_2_s25,
0x770620: LocationName.cloudy_park_2_s26,
0x770621: LocationName.cloudy_park_2_s27,
0x770622: LocationName.cloudy_park_2_s28,
0x770623: LocationName.cloudy_park_2_s29,
0x770624: LocationName.cloudy_park_2_s30,
0x770625: LocationName.cloudy_park_2_s31,
0x770626: LocationName.cloudy_park_2_s32,
0x770627: LocationName.cloudy_park_2_s33,
0x770628: LocationName.cloudy_park_2_s34,
0x770629: LocationName.cloudy_park_2_s35,
0x77062a: LocationName.cloudy_park_2_s36,
0x77062b: LocationName.cloudy_park_2_s37,
0x77062c: LocationName.cloudy_park_2_s38,
0x77062d: LocationName.cloudy_park_2_s39,
0x77062e: LocationName.cloudy_park_2_s40,
0x77062f: LocationName.cloudy_park_2_s41,
0x770630: LocationName.cloudy_park_2_s42,
0x770631: LocationName.cloudy_park_2_s43,
0x770632: LocationName.cloudy_park_2_s44,
0x770633: LocationName.cloudy_park_2_s45,
0x770634: LocationName.cloudy_park_2_s46,
0x770635: LocationName.cloudy_park_2_s47,
0x770636: LocationName.cloudy_park_2_s48,
0x770637: LocationName.cloudy_park_2_s49,
0x770638: LocationName.cloudy_park_2_s50,
0x770639: LocationName.cloudy_park_2_s51,
0x77063a: LocationName.cloudy_park_2_s52,
0x77063b: LocationName.cloudy_park_2_s53,
0x77063c: LocationName.cloudy_park_2_s54,
0x77063d: LocationName.cloudy_park_3_s1,
0x77063e: LocationName.cloudy_park_3_s2,
0x77063f: LocationName.cloudy_park_3_s3,
0x770640: LocationName.cloudy_park_3_s4,
0x770641: LocationName.cloudy_park_3_s5,
0x770642: LocationName.cloudy_park_3_s6,
0x770643: LocationName.cloudy_park_3_s7,
0x770644: LocationName.cloudy_park_3_s8,
0x770645: LocationName.cloudy_park_3_s9,
0x770646: LocationName.cloudy_park_3_s10,
0x770647: LocationName.cloudy_park_3_s11,
0x770648: LocationName.cloudy_park_3_s12,
0x770649: LocationName.cloudy_park_3_s13,
0x77064a: LocationName.cloudy_park_3_s14,
0x77064b: LocationName.cloudy_park_3_s15,
0x77064c: LocationName.cloudy_park_3_s16,
0x77064d: LocationName.cloudy_park_3_s17,
0x77064e: LocationName.cloudy_park_3_s18,
0x77064f: LocationName.cloudy_park_3_s19,
0x770650: LocationName.cloudy_park_3_s20,
0x770651: LocationName.cloudy_park_3_s21,
0x770652: LocationName.cloudy_park_3_s22,
0x770653: LocationName.cloudy_park_4_s1,
0x770654: LocationName.cloudy_park_4_s2,
0x770655: LocationName.cloudy_park_4_s3,
0x770656: LocationName.cloudy_park_4_s4,
0x770657: LocationName.cloudy_park_4_s5,
0x770658: LocationName.cloudy_park_4_s6,
0x770659: LocationName.cloudy_park_4_s7,
0x77065a: LocationName.cloudy_park_4_s8,
0x77065b: LocationName.cloudy_park_4_s9,
0x77065c: LocationName.cloudy_park_4_s10,
0x77065d: LocationName.cloudy_park_4_s11,
0x77065e: LocationName.cloudy_park_4_s12,
0x77065f: LocationName.cloudy_park_4_s13,
0x770660: LocationName.cloudy_park_4_s14,
0x770661: LocationName.cloudy_park_4_s15,
0x770662: LocationName.cloudy_park_4_s16,
0x770663: LocationName.cloudy_park_4_s17,
0x770664: LocationName.cloudy_park_4_s18,
0x770665: LocationName.cloudy_park_4_s19,
0x770666: LocationName.cloudy_park_4_s20,
0x770667: LocationName.cloudy_park_4_s21,
0x770668: LocationName.cloudy_park_4_s22,
0x770669: LocationName.cloudy_park_4_s23,
0x77066a: LocationName.cloudy_park_4_s24,
0x77066b: LocationName.cloudy_park_4_s25,
0x77066c: LocationName.cloudy_park_4_s26,
0x77066d: LocationName.cloudy_park_4_s27,
0x77066e: LocationName.cloudy_park_4_s28,
0x77066f: LocationName.cloudy_park_4_s29,
0x770670: LocationName.cloudy_park_4_s30,
0x770671: LocationName.cloudy_park_4_s31,
0x770672: LocationName.cloudy_park_4_s32,
0x770673: LocationName.cloudy_park_4_s33,
0x770674: LocationName.cloudy_park_4_s34,
0x770675: LocationName.cloudy_park_4_s35,
0x770676: LocationName.cloudy_park_4_s36,
0x770677: LocationName.cloudy_park_4_s37,
0x770678: LocationName.cloudy_park_4_s38,
0x770679: LocationName.cloudy_park_4_s39,
0x77067a: LocationName.cloudy_park_4_s40,
0x77067b: LocationName.cloudy_park_4_s41,
0x77067c: LocationName.cloudy_park_4_s42,
0x77067d: LocationName.cloudy_park_4_s43,
0x77067e: LocationName.cloudy_park_4_s44,
0x77067f: LocationName.cloudy_park_4_s45,
0x770680: LocationName.cloudy_park_4_s46,
0x770681: LocationName.cloudy_park_4_s47,
0x770682: LocationName.cloudy_park_4_s48,
0x770683: LocationName.cloudy_park_4_s49,
0x770684: LocationName.cloudy_park_4_s50,
0x770685: LocationName.cloudy_park_5_s1,
0x770686: LocationName.cloudy_park_5_s2,
0x770687: LocationName.cloudy_park_5_s3,
0x770688: LocationName.cloudy_park_5_s4,
0x770689: LocationName.cloudy_park_5_s5,
0x77068a: LocationName.cloudy_park_5_s6,
0x77068b: LocationName.cloudy_park_6_s1,
0x77068c: LocationName.cloudy_park_6_s2,
0x77068d: LocationName.cloudy_park_6_s3,
0x77068e: LocationName.cloudy_park_6_s4,
0x77068f: LocationName.cloudy_park_6_s5,
0x770690: LocationName.cloudy_park_6_s6,
0x770691: LocationName.cloudy_park_6_s7,
0x770692: LocationName.cloudy_park_6_s8,
0x770693: LocationName.cloudy_park_6_s9,
0x770694: LocationName.cloudy_park_6_s10,
0x770695: LocationName.cloudy_park_6_s11,
0x770696: LocationName.cloudy_park_6_s12,
0x770697: LocationName.cloudy_park_6_s13,
0x770698: LocationName.cloudy_park_6_s14,
0x770699: LocationName.cloudy_park_6_s15,
0x77069a: LocationName.cloudy_park_6_s16,
0x77069b: LocationName.cloudy_park_6_s17,
0x77069c: LocationName.cloudy_park_6_s18,
0x77069d: LocationName.cloudy_park_6_s19,
0x77069e: LocationName.cloudy_park_6_s20,
0x77069f: LocationName.cloudy_park_6_s21,
0x7706a0: LocationName.cloudy_park_6_s22,
0x7706a1: LocationName.cloudy_park_6_s23,
0x7706a2: LocationName.cloudy_park_6_s24,
0x7706a3: LocationName.cloudy_park_6_s25,
0x7706a4: LocationName.cloudy_park_6_s26,
0x7706a5: LocationName.cloudy_park_6_s27,
0x7706a6: LocationName.cloudy_park_6_s28,
0x7706a7: LocationName.cloudy_park_6_s29,
0x7706a8: LocationName.cloudy_park_6_s30,
0x7706a9: LocationName.cloudy_park_6_s31,
0x7706aa: LocationName.cloudy_park_6_s32,
0x7706ab: LocationName.cloudy_park_6_s33,
0x7706ac: LocationName.iceberg_1_s1,
0x7706ad: LocationName.iceberg_1_s2,
0x7706ae: LocationName.iceberg_1_s3,
0x7706af: LocationName.iceberg_1_s4,
0x7706b0: LocationName.iceberg_1_s5,
0x7706b1: LocationName.iceberg_1_s6,
0x7706b2: LocationName.iceberg_2_s1,
0x7706b3: LocationName.iceberg_2_s2,
0x7706b4: LocationName.iceberg_2_s3,
0x7706b5: LocationName.iceberg_2_s4,
0x7706b6: LocationName.iceberg_2_s5,
0x7706b7: LocationName.iceberg_2_s6,
0x7706b8: LocationName.iceberg_2_s7,
0x7706b9: LocationName.iceberg_2_s8,
0x7706ba: LocationName.iceberg_2_s9,
0x7706bb: LocationName.iceberg_2_s10,
0x7706bc: LocationName.iceberg_2_s11,
0x7706bd: LocationName.iceberg_2_s12,
0x7706be: LocationName.iceberg_2_s13,
0x7706bf: LocationName.iceberg_2_s14,
0x7706c0: LocationName.iceberg_2_s15,
0x7706c1: LocationName.iceberg_2_s16,
0x7706c2: LocationName.iceberg_2_s17,
0x7706c3: LocationName.iceberg_2_s18,
0x7706c4: LocationName.iceberg_2_s19,
0x7706c5: LocationName.iceberg_3_s1,
0x7706c6: LocationName.iceberg_3_s2,
0x7706c7: LocationName.iceberg_3_s3,
0x7706c8: LocationName.iceberg_3_s4,
0x7706c9: LocationName.iceberg_3_s5,
0x7706ca: LocationName.iceberg_3_s6,
0x7706cb: LocationName.iceberg_3_s7,
0x7706cc: LocationName.iceberg_3_s8,
0x7706cd: LocationName.iceberg_3_s9,
0x7706ce: LocationName.iceberg_3_s10,
0x7706cf: LocationName.iceberg_3_s11,
0x7706d0: LocationName.iceberg_3_s12,
0x7706d1: LocationName.iceberg_3_s13,
0x7706d2: LocationName.iceberg_3_s14,
0x7706d3: LocationName.iceberg_3_s15,
0x7706d4: LocationName.iceberg_3_s16,
0x7706d5: LocationName.iceberg_3_s17,
0x7706d6: LocationName.iceberg_3_s18,
0x7706d7: LocationName.iceberg_3_s19,
0x7706d8: LocationName.iceberg_3_s20,
0x7706d9: LocationName.iceberg_3_s21,
0x7706da: LocationName.iceberg_4_s1,
0x7706db: LocationName.iceberg_4_s2,
0x7706dc: LocationName.iceberg_4_s3,
0x7706dd: LocationName.iceberg_5_s1,
0x7706de: LocationName.iceberg_5_s2,
0x7706df: LocationName.iceberg_5_s3,
0x7706e0: LocationName.iceberg_5_s4,
0x7706e1: LocationName.iceberg_5_s5,
0x7706e2: LocationName.iceberg_5_s6,
0x7706e3: LocationName.iceberg_5_s7,
0x7706e4: LocationName.iceberg_5_s8,
0x7706e5: LocationName.iceberg_5_s9,
0x7706e6: LocationName.iceberg_5_s10,
0x7706e7: LocationName.iceberg_5_s11,
0x7706e8: LocationName.iceberg_5_s12,
0x7706e9: LocationName.iceberg_5_s13,
0x7706ea: LocationName.iceberg_5_s14,
0x7706eb: LocationName.iceberg_5_s15,
0x7706ec: LocationName.iceberg_5_s16,
0x7706ed: LocationName.iceberg_5_s17,
0x7706ee: LocationName.iceberg_5_s18,
0x7706ef: LocationName.iceberg_5_s19,
0x7706f0: LocationName.iceberg_5_s20,
0x7706f1: LocationName.iceberg_5_s21,
0x7706f2: LocationName.iceberg_5_s22,
0x7706f3: LocationName.iceberg_5_s23,
0x7706f4: LocationName.iceberg_5_s24,
0x7706f5: LocationName.iceberg_5_s25,
0x7706f6: LocationName.iceberg_5_s26,
0x7706f7: LocationName.iceberg_5_s27,
0x7706f8: LocationName.iceberg_5_s28,
0x7706f9: LocationName.iceberg_5_s29,
0x7706fa: LocationName.iceberg_5_s30,
0x7706fb: LocationName.iceberg_5_s31,
0x7706fc: LocationName.iceberg_5_s32,
0x7706fd: LocationName.iceberg_5_s33,
0x7706fe: LocationName.iceberg_5_s34,
0x7706ff: LocationName.iceberg_6_s1,
}
location_table = {
**stage_locations,
**heart_star_locations,
**boss_locations,
**consumable_locations,
**star_locations
}

View File

@@ -1,577 +0,0 @@
import typing
from pkgutil import get_data
import Utils
from typing import Optional, TYPE_CHECKING
import hashlib
import os
import struct
import settings
from worlds.Files import APDeltaPatch
from .Aesthetics import get_palette_bytes, kirby_target_palettes, get_kirby_palette, gooey_target_palettes, \
get_gooey_palette
from .Compression import hal_decompress
import bsdiff4
if TYPE_CHECKING:
from . import KDL3World
KDL3UHASH = "201e7658f6194458a3869dde36bf8ec2"
KDL3JHASH = "b2f2d004ea640c3db66df958fce122b2"
level_pointers = {
0x770001: 0x0084,
0x770002: 0x009C,
0x770003: 0x00B8,
0x770004: 0x00D8,
0x770005: 0x0104,
0x770006: 0x0124,
0x770007: 0x014C,
0x770008: 0x0170,
0x770009: 0x0190,
0x77000A: 0x01B0,
0x77000B: 0x01E8,
0x77000C: 0x0218,
0x77000D: 0x024C,
0x77000E: 0x0270,
0x77000F: 0x02A0,
0x770010: 0x02C4,
0x770011: 0x02EC,
0x770012: 0x0314,
0x770013: 0x03CC,
0x770014: 0x0404,
0x770015: 0x042C,
0x770016: 0x044C,
0x770017: 0x0478,
0x770018: 0x049C,
0x770019: 0x04E4,
0x77001A: 0x0504,
0x77001B: 0x0530,
0x77001C: 0x0554,
0x77001D: 0x05A8,
0x77001E: 0x0640,
0x770200: 0x0148,
0x770201: 0x0248,
0x770202: 0x03C8,
0x770203: 0x04E0,
0x770204: 0x06A4,
0x770205: 0x06A8,
}
bb_bosses = {
0x770200: 0xED85F1,
0x770201: 0xF01360,
0x770202: 0xEDA3DF,
0x770203: 0xEDC2B9,
0x770204: 0xED7C3F,
0x770205: 0xEC29D2,
}
level_sprites = {
0x19B2C6: 1827,
0x1A195C: 1584,
0x19F6F3: 1679,
0x19DC8B: 1717,
0x197900: 1872
}
stage_tiles = {
0: [
0, 1, 2,
16, 17, 18,
32, 33, 34,
48, 49, 50
],
1: [
3, 4, 5,
19, 20, 21,
35, 36, 37,
51, 52, 53
],
2: [
6, 7, 8,
22, 23, 24,
38, 39, 40,
54, 55, 56
],
3: [
9, 10, 11,
25, 26, 27,
41, 42, 43,
57, 58, 59,
],
4: [
12, 13, 64,
28, 29, 65,
44, 45, 66,
60, 61, 67
],
5: [
14, 15, 68,
30, 31, 69,
46, 47, 70,
62, 63, 71
]
}
heart_star_address = 0x2D0000
heart_star_size = 456
consumable_address = 0x2F91DD
consumable_size = 698
stage_palettes = [0x60964, 0x60B64, 0x60D64, 0x60F64, 0x61164]
music_choices = [
2, # Boss 1
3, # Boss 2 (Unused)
4, # Boss 3 (Miniboss)
7, # Dedede
9, # Event 2 (used once)
10, # Field 1
11, # Field 2
12, # Field 3
13, # Field 4
14, # Field 5
15, # Field 6
16, # Field 7
17, # Field 8
18, # Field 9
19, # Field 10
20, # Field 11
21, # Field 12 (Gourmet Race)
23, # Dark Matter in the Hyper Zone
24, # Zero
25, # Level 1
26, # Level 2
27, # Level 4
28, # Level 3
29, # Heart Star Failed
30, # Level 5
31, # Minigame
38, # Animal Friend 1
39, # Animal Friend 2
40, # Animal Friend 3
]
# extra room pointers we don't want to track other than for music
room_pointers = [
3079990, # Zero
2983409, # BB Whispy
3150688, # BB Acro
2991071, # BB PonCon
2998969, # BB Ado
2980927, # BB Dedede
2894290 # BB Zero
]
enemy_remap = {
"Waddle Dee": 0,
"Bronto Burt": 2,
"Rocky": 3,
"Bobo": 5,
"Chilly": 6,
"Poppy Bros Jr.": 7,
"Sparky": 8,
"Polof": 9,
"Broom Hatter": 11,
"Cappy": 12,
"Bouncy": 13,
"Nruff": 15,
"Glunk": 16,
"Togezo": 18,
"Kabu": 19,
"Mony": 20,
"Blipper": 21,
"Squishy": 22,
"Gabon": 24,
"Oro": 25,
"Galbo": 26,
"Sir Kibble": 27,
"Nidoo": 28,
"Kany": 29,
"Sasuke": 30,
"Yaban": 32,
"Boten": 33,
"Coconut": 34,
"Doka": 35,
"Icicle": 36,
"Pteran": 39,
"Loud": 40,
"Como": 41,
"Klinko": 42,
"Babut": 43,
"Wappa": 44,
"Mariel": 45,
"Tick": 48,
"Apolo": 49,
"Popon Ball": 50,
"KeKe": 51,
"Magoo": 53,
"Raft Waddle Dee": 57,
"Madoo": 58,
"Corori": 60,
"Kapar": 67,
"Batamon": 68,
"Peran": 72,
"Bobin": 73,
"Mopoo": 74,
"Gansan": 75,
"Bukiset (Burning)": 76,
"Bukiset (Stone)": 77,
"Bukiset (Ice)": 78,
"Bukiset (Needle)": 79,
"Bukiset (Clean)": 80,
"Bukiset (Parasol)": 81,
"Bukiset (Spark)": 82,
"Bukiset (Cutter)": 83,
"Waddle Dee Drawing": 84,
"Bronto Burt Drawing": 85,
"Bouncy Drawing": 86,
"Kabu (Dekabu)": 87,
"Wapod": 88,
"Propeller": 89,
"Dogon": 90,
"Joe": 91
}
miniboss_remap = {
"Captain Stitch": 0,
"Yuki": 1,
"Blocky": 2,
"Jumper Shoot": 3,
"Boboo": 4,
"Haboki": 5
}
ability_remap = {
"No Ability": 0,
"Burning Ability": 1,
"Stone Ability": 2,
"Ice Ability": 3,
"Needle Ability": 4,
"Clean Ability": 5,
"Parasol Ability": 6,
"Spark Ability": 7,
"Cutter Ability": 8,
}
class RomData:
def __init__(self, file: str, name: typing.Optional[str] = None):
self.file = bytearray()
self.read_from_file(file)
self.name = name
def read_byte(self, offset: int):
return self.file[offset]
def read_bytes(self, offset: int, length: int):
return self.file[offset:offset + length]
def write_byte(self, offset: int, value: int):
self.file[offset] = value
def write_bytes(self, offset: int, values: typing.Sequence) -> None:
self.file[offset:offset + len(values)] = values
def write_to_file(self, file: str):
with open(file, 'wb') as outfile:
outfile.write(self.file)
def read_from_file(self, file: str):
with open(file, 'rb') as stream:
self.file = bytearray(stream.read())
def apply_patch(self, patch: bytes):
self.file = bytearray(bsdiff4.patch(bytes(self.file), patch))
def write_crc(self):
crc = (sum(self.file[:0x7FDC] + self.file[0x7FE0:]) + 0x01FE) & 0xFFFF
inv = crc ^ 0xFFFF
self.write_bytes(0x7FDC, [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF])
def handle_level_sprites(stages, sprites, palettes):
palette_by_level = list()
for palette in palettes:
palette_by_level.extend(palette[10:16])
for i in range(5):
for j in range(6):
palettes[i][10 + j] = palette_by_level[stages[i][j] - 1]
palettes[i] = [x for palette in palettes[i] for x in palette]
tiles_by_level = list()
for spritesheet in sprites:
decompressed = hal_decompress(spritesheet)
tiles = [decompressed[i:i + 32] for i in range(0, 2304, 32)]
tiles_by_level.extend([[tiles[x] for x in stage_tiles[stage]] for stage in stage_tiles])
for world in range(5):
levels = [stages[world][x] - 1 for x in range(6)]
world_tiles: typing.List[typing.Optional[bytes]] = [None for _ in range(72)]
for i in range(6):
for x in range(12):
world_tiles[stage_tiles[i][x]] = tiles_by_level[levels[i]][x]
sprites[world] = list()
for tile in world_tiles:
sprites[world].extend(tile)
# insert our fake compression
sprites[world][0:0] = [0xe3, 0xff]
sprites[world][1026:1026] = [0xe3, 0xff]
sprites[world][2052:2052] = [0xe0, 0xff]
sprites[world].append(0xff)
return sprites, palettes
def write_heart_star_sprites(rom: RomData):
compressed = rom.read_bytes(heart_star_address, heart_star_size)
decompressed = hal_decompress(compressed)
patch = get_data(__name__, os.path.join("data", "APHeartStar.bsdiff4"))
patched = bytearray(bsdiff4.patch(decompressed, patch))
rom.write_bytes(0x1AF7DF, patched)
patched[0:0] = [0xE3, 0xFF]
patched.append(0xFF)
rom.write_bytes(0x1CD000, patched)
rom.write_bytes(0x3F0EBF, [0x00, 0xD0, 0x39])
def write_consumable_sprites(rom: RomData, consumables: bool, stars: bool):
compressed = rom.read_bytes(consumable_address, consumable_size)
decompressed = hal_decompress(compressed)
patched = bytearray(decompressed)
if consumables:
patch = get_data(__name__, os.path.join("data", "APConsumable.bsdiff4"))
patched = bytearray(bsdiff4.patch(bytes(patched), patch))
if stars:
patch = get_data(__name__, os.path.join("data", "APStars.bsdiff4"))
patched = bytearray(bsdiff4.patch(bytes(patched), patch))
patched[0:0] = [0xE3, 0xFF]
patched.append(0xFF)
rom.write_bytes(0x1CD500, patched)
rom.write_bytes(0x3F0DAE, [0x00, 0xD5, 0x39])
class KDL3DeltaPatch(APDeltaPatch):
hash = [KDL3UHASH, KDL3JHASH]
game = "Kirby's Dream Land 3"
patch_file_ending = ".apkdl3"
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes()
def patch(self, target: str):
super().patch(target)
rom = RomData(target)
target_language = rom.read_byte(0x3C020)
rom.write_byte(0x7FD9, target_language)
write_heart_star_sprites(rom)
if rom.read_bytes(0x3D014, 1)[0] > 0:
stages = [struct.unpack("HHHHHHH", rom.read_bytes(0x3D020 + x * 14, 14)) for x in range(5)]
palettes = [rom.read_bytes(full_pal, 512) for full_pal in stage_palettes]
palettes = [[palette[i:i + 32] for i in range(0, 512, 32)] for palette in palettes]
sprites = [rom.read_bytes(offset, level_sprites[offset]) for offset in level_sprites]
sprites, palettes = handle_level_sprites(stages, sprites, palettes)
for addr, palette in zip(stage_palettes, palettes):
rom.write_bytes(addr, palette)
for addr, level_sprite in zip([0x1CA000, 0x1CA920, 0x1CB230, 0x1CBB40, 0x1CC450], sprites):
rom.write_bytes(addr, level_sprite)
rom.write_bytes(0x460A, [0x00, 0xA0, 0x39, 0x20, 0xA9, 0x39, 0x30, 0xB2, 0x39, 0x40, 0xBB, 0x39,
0x50, 0xC4, 0x39])
write_consumable_sprites(rom, rom.read_byte(0x3D018) > 0, rom.read_byte(0x3D01A) > 0)
rom_name = rom.read_bytes(0x3C000, 21)
rom.write_bytes(0x7FC0, rom_name)
rom.write_crc()
rom.write_to_file(target)
def patch_rom(world: "KDL3World", rom: RomData):
rom.apply_patch(get_data(__name__, os.path.join("data", "kdl3_basepatch.bsdiff4")))
tiles = get_data(__name__, os.path.join("data", "APPauseIcons.dat"))
rom.write_bytes(0x3F000, tiles)
# Write open world patch
if world.options.open_world:
rom.write_bytes(0x143C7, [0xAD, 0xC1, 0x5A, 0xCD, 0xC1, 0x5A, ])
# changes the stage flag function to compare $5AC1 to $5AC1,
# always running the "new stage" function
# This has further checks present for bosses already, so we just
# need to handle regular stages
# write check for boss to be unlocked
if world.options.consumables:
# reroute maxim tomatoes to use the 1-UP function, then null out the function
rom.write_bytes(0x3002F, [0x37, 0x00])
rom.write_bytes(0x30037, [0xA9, 0x26, 0x00, # LDA #$0026
0x22, 0x27, 0xD9, 0x00, # JSL $00D927
0xA4, 0xD2, # LDY $D2
0x6B, # RTL
0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, # NOP #10
])
# stars handling is built into the rom, so no changes there
rooms = world.rooms
if world.options.music_shuffle > 0:
if world.options.music_shuffle == 1:
shuffled_music = music_choices.copy()
world.random.shuffle(shuffled_music)
music_map = dict(zip(music_choices, shuffled_music))
# Avoid putting star twinkle in the pool
music_map[5] = world.random.choice(music_choices)
# Heart Star music doesn't work on regular stages
music_map[8] = world.random.choice(music_choices)
for room in rooms:
room.music = music_map[room.music]
for room in room_pointers:
old_music = rom.read_byte(room + 2)
rom.write_byte(room + 2, music_map[old_music])
for i in range(5):
# level themes
old_music = rom.read_byte(0x133F2 + i)
rom.write_byte(0x133F2 + i, music_map[old_music])
# Zero
rom.write_byte(0x9AE79, music_map[0x18])
# Heart Star success and fail
rom.write_byte(0x4A388, music_map[0x08])
rom.write_byte(0x4A38D, music_map[0x1D])
elif world.options.music_shuffle == 2:
for room in rooms:
room.music = world.random.choice(music_choices)
for room in room_pointers:
rom.write_byte(room + 2, world.random.choice(music_choices))
for i in range(5):
# level themes
rom.write_byte(0x133F2 + i, world.random.choice(music_choices))
# Zero
rom.write_byte(0x9AE79, world.random.choice(music_choices))
# Heart Star success and fail
rom.write_byte(0x4A388, world.random.choice(music_choices))
rom.write_byte(0x4A38D, world.random.choice(music_choices))
for room in rooms:
room.patch(rom)
if world.options.virtual_console in [1, 3]:
# Flash Reduction
rom.write_byte(0x9AE68, 0x10)
rom.write_bytes(0x9AE8E, [0x08, 0x00, 0x22, 0x5D, 0xF7, 0x00, 0xA2, 0x08, ])
rom.write_byte(0x9AEA1, 0x08)
rom.write_byte(0x9AEC9, 0x01)
rom.write_bytes(0x9AED2, [0xA9, 0x1F])
rom.write_byte(0x9AEE1, 0x08)
if world.options.virtual_console in [2, 3]:
# Hyper Zone BB colors
rom.write_bytes(0x2C5E16, [0xEE, 0x1B, 0x18, 0x5B, 0xD3, 0x4A, 0xF4, 0x3B, ])
rom.write_bytes(0x2C8217, [0xFF, 0x1E, ])
# boss requirements
rom.write_bytes(0x3D000, struct.pack("HHHHH", world.boss_requirements[0], world.boss_requirements[1],
world.boss_requirements[2], world.boss_requirements[3],
world.boss_requirements[4]))
rom.write_bytes(0x3D00A, struct.pack("H", world.required_heart_stars if world.options.goal_speed == 1 else 0xFFFF))
rom.write_byte(0x3D00C, world.options.goal_speed.value)
rom.write_byte(0x3D00E, world.options.open_world.value)
rom.write_byte(0x3D010, world.options.death_link.value)
rom.write_byte(0x3D012, world.options.goal.value)
rom.write_byte(0x3D014, world.options.stage_shuffle.value)
rom.write_byte(0x3D016, world.options.ow_boss_requirement.value)
rom.write_byte(0x3D018, world.options.consumables.value)
rom.write_byte(0x3D01A, world.options.starsanity.value)
rom.write_byte(0x3D01C, world.options.gifting.value if world.multiworld.players > 1 else 0)
rom.write_byte(0x3D01E, world.options.strict_bosses.value)
# don't write gifting for solo game, since there's no one to send anything to
for level in world.player_levels:
for i in range(len(world.player_levels[level])):
rom.write_bytes(0x3F002E + ((level - 1) * 14) + (i * 2),
struct.pack("H", level_pointers[world.player_levels[level][i]]))
rom.write_bytes(0x3D020 + (level - 1) * 14 + (i * 2),
struct.pack("H", world.player_levels[level][i] & 0x00FFFF))
if (i == 0) or (i > 0 and i % 6 != 0):
rom.write_bytes(0x3D080 + (level - 1) * 12 + (i * 2),
struct.pack("H", (world.player_levels[level][i] & 0x00FFFF) % 6))
for i in range(6):
if world.boss_butch_bosses[i]:
rom.write_bytes(0x3F0000 + (level_pointers[0x770200 + i]), struct.pack("I", bb_bosses[0x770200 + i]))
# copy ability shuffle
if world.options.copy_ability_randomization.value > 0:
for enemy in world.copy_abilities:
if enemy in miniboss_remap:
rom.write_bytes(0xB417E + (miniboss_remap[enemy] << 1),
struct.pack("H", ability_remap[world.copy_abilities[enemy]]))
else:
rom.write_bytes(0xB3CAC + (enemy_remap[enemy] << 1),
struct.pack("H", ability_remap[world.copy_abilities[enemy]]))
# following only needs done on non-door rando
# incredibly lucky this follows the same order (including 5E == star block)
rom.write_byte(0x2F77EA, 0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1))
rom.write_byte(0x2F7811, 0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1))
rom.write_byte(0x2F9BC4, 0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1))
rom.write_byte(0x2F9BEB, 0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1))
rom.write_byte(0x2FAC06, 0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1))
rom.write_byte(0x2FAC2D, 0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1))
rom.write_byte(0x2F9E7B, 0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1))
rom.write_byte(0x2F9EA2, 0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1))
rom.write_byte(0x2FA951, 0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1))
rom.write_byte(0x2FA978, 0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1))
rom.write_byte(0x2FA132, 0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1))
rom.write_byte(0x2FA159, 0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1))
rom.write_byte(0x2FA3E8, 0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1))
rom.write_byte(0x2FA40F, 0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1))
rom.write_byte(0x2F90E2, 0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1))
rom.write_byte(0x2F9109, 0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1))
if world.options.copy_ability_randomization == 2:
for enemy in enemy_remap:
# we just won't include it for minibosses
rom.write_bytes(0xB3E40 + (enemy_remap[enemy] << 1), struct.pack("h", world.random.randint(-1, 2)))
# write jumping goal
rom.write_bytes(0x94F8, struct.pack("H", world.options.jumping_target))
rom.write_bytes(0x944E, struct.pack("H", world.options.jumping_target))
from Utils import __version__
rom.name = bytearray(
f'KDL3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x3C000, rom.name)
rom.write_byte(0x3C020, world.options.game_language.value)
# handle palette
if world.options.kirby_flavor_preset.value != 0:
for addr in kirby_target_palettes:
target = kirby_target_palettes[addr]
palette = get_kirby_palette(world)
rom.write_bytes(addr, get_palette_bytes(palette, target[0], target[1], target[2]))
if world.options.gooey_flavor_preset.value != 0:
for addr in gooey_target_palettes:
target = gooey_target_palettes[addr]
palette = get_gooey_palette(world)
rom.write_bytes(addr, get_palette_bytes(palette, target[0], target[1], target[2]))
def get_base_rom_bytes() -> bytes:
rom_file: str = get_base_rom_path()
base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:
base_rom_bytes = bytes(Utils.read_snes_rom(open(rom_file, "rb")))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if basemd5.hexdigest() not in {KDL3UHASH, KDL3JHASH}:
raise Exception("Supplied Base Rom does not match known MD5 for US or JP release. "
"Get the correct game and version, then dump it")
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
return base_rom_bytes
def get_base_rom_path(file_name: str = "") -> str:
options: settings.Settings = settings.get_settings()
if not file_name:
file_name = options["kdl3_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.user_path(file_name)
return file_name

View File

@@ -1,95 +0,0 @@
import struct
import typing
from BaseClasses import Region, ItemClassification
if typing.TYPE_CHECKING:
from .Rom import RomData
animal_map = {
"Rick Spawn": 0,
"Kine Spawn": 1,
"Coo Spawn": 2,
"Nago Spawn": 3,
"ChuChu Spawn": 4,
"Pitch Spawn": 5
}
class KDL3Room(Region):
pointer: int = 0
level: int = 0
stage: int = 0
room: int = 0
music: int = 0
default_exits: typing.List[typing.Dict[str, typing.Union[int, typing.List[str]]]]
animal_pointers: typing.List[int]
enemies: typing.List[str]
entity_load: typing.List[typing.List[int]]
consumables: typing.List[typing.Dict[str, typing.Union[int, str]]]
def __init__(self, name, player, multiworld, hint, level, stage, room, pointer, music, default_exits,
animal_pointers, enemies, entity_load, consumables, consumable_pointer):
super().__init__(name, player, multiworld, hint)
self.level = level
self.stage = stage
self.room = room
self.pointer = pointer
self.music = music
self.default_exits = default_exits
self.animal_pointers = animal_pointers
self.enemies = enemies
self.entity_load = entity_load
self.consumables = consumables
self.consumable_pointer = consumable_pointer
def patch(self, rom: "RomData"):
rom.write_byte(self.pointer + 2, self.music)
animals = [x.item.name for x in self.locations if "Animal" in x.name]
if len(animals) > 0:
for current_animal, address in zip(animals, self.animal_pointers):
rom.write_byte(self.pointer + address + 7, animal_map[current_animal])
if self.multiworld.worlds[self.player].options.consumables:
load_len = len(self.entity_load)
for consumable in self.consumables:
location = next(x for x in self.locations if x.name == consumable["name"])
assert location.item
is_progression = location.item.classification & ItemClassification.progression
if load_len == 8:
# edge case, there is exactly 1 room with 8 entities and only 1 consumable among them
if not (any(x in self.entity_load for x in [[0, 22], [1, 22]])
and any(x in self.entity_load for x in [[2, 22], [3, 22]])):
replacement_target = self.entity_load.index(
next(x for x in self.entity_load if x in [[0, 22], [1, 22], [2, 22], [3, 22]]))
if is_progression:
vtype = 0
else:
vtype = 2
rom.write_byte(self.pointer + 88 + (replacement_target * 2), vtype)
self.entity_load[replacement_target] = [vtype, 22]
else:
if is_progression:
# we need to see if 1-ups are in our load list
if any(x not in self.entity_load for x in [[0, 22], [1, 22]]):
self.entity_load.append([0, 22])
else:
if any(x not in self.entity_load for x in [[2, 22], [3, 22]]):
# edge case: if (1, 22) is in, we need to load (3, 22) instead
if [1, 22] in self.entity_load:
self.entity_load.append([3, 22])
else:
self.entity_load.append([2, 22])
if load_len < len(self.entity_load):
rom.write_bytes(self.pointer + 88 + (load_len * 2), bytes(self.entity_load[load_len]))
rom.write_bytes(self.pointer + 104 + (load_len * 2),
bytes(struct.pack("H", self.consumable_pointer)))
if is_progression:
if [1, 22] in self.entity_load:
vtype = 1
else:
vtype = 0
else:
if [3, 22] in self.entity_load:
vtype = 3
else:
vtype = 2
rom.write_byte(self.pointer + consumable["pointer"] + 7, vtype)

View File

@@ -1,25 +1,25 @@
import logging
import typing
from BaseClasses import Tutorial, ItemClassification, MultiWorld
from BaseClasses import Tutorial, ItemClassification, MultiWorld, CollectionState, Item
from Fill import fill_restrictive
from Options import PerGameCommonOptions
from worlds.AutoWorld import World, WebWorld
from .Items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \
trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights
from .Locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations
from .Names.AnimalFriendSpawns import animal_friend_spawns
from .Names.EnemyAbilities import vanilla_enemies, enemy_mapping, enemy_restrictive
from .Regions import create_levels, default_levels
from .Options import KDL3Options
from .Presets import kdl3_options_presets
from .Names import LocationName
from .Room import KDL3Room
from .Rules import set_rules
from .Rom import KDL3DeltaPatch, get_base_rom_path, RomData, patch_rom, KDL3JHASH, KDL3UHASH
from .Client import KDL3SNIClient
from .items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \
trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights, animal_friend_spawn_table,\
lookup_item_to_id
from .locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations
from .names.animal_friend_spawns import animal_friend_spawns, problematic_sets
from .names.enemy_abilities import vanilla_enemies, enemy_mapping, enemy_restrictive
from .regions import create_levels, default_levels
from .options import KDL3Options, kdl3_option_groups
from .presets import kdl3_options_presets
from .names import location_name
from .room import KDL3Room
from .rules import set_rules
from .rom import KDL3ProcedurePatch, get_base_rom_path, patch_rom, KDL3JHASH, KDL3UHASH
from .client import KDL3SNIClient
from typing import Dict, TextIO, Optional, List
from typing import Dict, TextIO, Optional, List, Any, Mapping, ClassVar, Type
import os
import math
import threading
@@ -53,6 +53,7 @@ class KDL3WebWorld(WebWorld):
)
]
options_presets = kdl3_options_presets
option_groups = kdl3_option_groups
class KDL3World(World):
@@ -61,35 +62,35 @@ class KDL3World(World):
"""
game = "Kirby's Dream Land 3"
options_dataclass: typing.ClassVar[typing.Type[PerGameCommonOptions]] = KDL3Options
options_dataclass: ClassVar[Type[PerGameCommonOptions]] = KDL3Options
options: KDL3Options
item_name_to_id = {item: item_table[item].code for item in item_table}
item_name_to_id = lookup_item_to_id
location_name_to_id = {location_table[location]: location for location in location_table}
item_name_groups = item_names
web = KDL3WebWorld()
settings: typing.ClassVar[KDL3Settings]
settings: ClassVar[KDL3Settings]
def __init__(self, multiworld: MultiWorld, player: int):
self.rom_name = None
self.rom_name: bytes = bytes()
self.rom_name_available_event = threading.Event()
super().__init__(multiworld, player)
self.copy_abilities: Dict[str, str] = vanilla_enemies.copy()
self.required_heart_stars: int = 0 # we fill this during create_items
self.boss_requirements: Dict[int, int] = dict()
self.boss_requirements: List[int] = []
self.player_levels = default_levels.copy()
self.stage_shuffle_enabled = False
self.boss_butch_bosses: List[Optional[bool]] = list()
self.rooms: Optional[List[KDL3Room]] = None
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
rom_file: str = get_base_rom_path()
if not os.path.exists(rom_file):
raise FileNotFoundError(f"Could not find base ROM for {cls.game}: {rom_file}")
self.boss_butch_bosses: List[Optional[bool]] = []
self.rooms: List[KDL3Room] = []
create_regions = create_levels
def create_item(self, name: str, force_non_progression=False) -> KDL3Item:
def generate_early(self) -> None:
if self.options.total_heart_stars != -1:
logger.warning(f"Kirby's Dream Land 3 ({self.player_name}): Use of \"total_heart_stars\" is deprecated. "
f"Please use \"max_heart_stars\" instead.")
self.options.max_heart_stars.value = self.options.total_heart_stars.value
def create_item(self, name: str, force_non_progression: bool = False) -> KDL3Item:
item = item_table[name]
classification = ItemClassification.filler
if item.progression and not force_non_progression:
@@ -99,7 +100,7 @@ class KDL3World(World):
classification = ItemClassification.trap
return KDL3Item(name, classification, item.code, self.player)
def get_filler_item_name(self, include_stars=True) -> str:
def get_filler_item_name(self, include_stars: bool = True) -> str:
if include_stars:
return self.random.choices(list(total_filler_weights.keys()),
weights=list(total_filler_weights.values()))[0]
@@ -112,8 +113,8 @@ class KDL3World(World):
self.options.slow_trap_weight.value,
self.options.ability_trap_weight.value])[0]
def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: typing.List[str],
level: int, stage: int):
def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: List[str],
level: int, stage: int) -> Optional[str]:
valid_rooms = [room for room in self.rooms if (room.level < level)
or (room.level == level and room.stage < stage)] # leave out the stage in question to avoid edge
valid_enemies = set()
@@ -124,6 +125,10 @@ class KDL3World(World):
return None # a valid enemy got placed by a more restrictive placement
return self.random.choice(sorted([enemy for enemy in valid_enemies if enemy not in placed_enemies]))
def get_pre_fill_items(self) -> List[Item]:
return [self.create_item(item)
for item in [*copy_ability_access_table.keys(), *animal_friend_spawn_table.keys()]]
def pre_fill(self) -> None:
if self.options.copy_ability_randomization:
# randomize copy abilities
@@ -196,21 +201,40 @@ class KDL3World(World):
else:
animal_base = ["Rick Spawn", "Kine Spawn", "Coo Spawn", "Nago Spawn", "ChuChu Spawn", "Pitch Spawn"]
animal_pool = [self.random.choice(animal_base)
for _ in range(len(animal_friend_spawns) - 9)]
for _ in range(len(animal_friend_spawns) - 10)]
# have to guarantee one of each animal
animal_pool.extend(animal_base)
if guaranteed_animal == "Kine Spawn":
animal_pool.append("Coo Spawn")
else:
animal_pool.append("Kine Spawn")
# Weird fill hack, this forces ChuChu to be the last animal friend placed
# If Kine is ever the last animal friend placed, he will cause fill errors on closed world
animal_pool.sort()
locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns]
items = [self.create_item(animal) for animal in animal_pool]
allstate = self.multiworld.get_all_state(False)
items: List[Item] = [self.create_item(animal) for animal in animal_pool]
allstate = CollectionState(self.multiworld)
for item in [*copy_ability_table, *animal_friend_table, *["Heart Star" for _ in range(99)]]:
self.collect(allstate, self.create_item(item))
self.random.shuffle(locations)
fill_restrictive(self.multiworld, allstate, locations, items, True, True)
# Need to ensure all of these are unique items, and replace them if they aren't
for spawns in problematic_sets:
placed = [self.get_location(spawn).item for spawn in spawns]
placed_names = set([item.name for item in placed])
if len(placed_names) != len(placed):
# have a duplicate
animals = []
for spawn in spawns:
spawn_location = self.get_location(spawn)
if spawn_location.item.name not in animals:
animals.append(spawn_location.item.name)
else:
new_animal = self.random.choice([x for x in ["Rick Spawn", "Coo Spawn", "Kine Spawn",
"ChuChu Spawn", "Nago Spawn", "Pitch Spawn"]
if x not in placed_names and x not in animals])
spawn_location.item = None
spawn_location.place_locked_item(self.create_item(new_animal))
animals.append(new_animal)
# logically, this should be sound pre-ER. May need to adjust around it with ER in the future
else:
animal_friends = animal_friend_spawns.copy()
for animal in animal_friends:
@@ -225,21 +249,20 @@ class KDL3World(World):
remaining_items = len(location_table) - len(itempool)
if not self.options.consumables:
remaining_items -= len(consumable_locations)
remaining_items -= len(star_locations)
if self.options.starsanity:
# star fill, keep consumable pool locked to consumable and fill 767 stars specifically
star_items = list(star_item_weights.keys())
star_weights = list(star_item_weights.values())
itempool.extend([self.create_item(item) for item in self.random.choices(star_items, weights=star_weights,
k=767)])
total_heart_stars = self.options.total_heart_stars
if not self.options.starsanity:
remaining_items -= len(star_locations)
max_heart_stars = self.options.max_heart_stars.value
if max_heart_stars > remaining_items:
max_heart_stars = remaining_items
# ensure at least 1 heart star required per world
required_heart_stars = max(int(total_heart_stars * required_percentage), 5)
filler_items = total_heart_stars - required_heart_stars
filler_amount = math.floor(filler_items * (self.options.filler_percentage / 100.0))
trap_amount = math.floor(filler_amount * (self.options.trap_percentage / 100.0))
filler_amount -= trap_amount
non_required_heart_stars = filler_items - filler_amount - trap_amount
required_heart_stars = min(max(int(max_heart_stars * required_percentage), 5), 99)
filler_items = remaining_items - required_heart_stars
converted_heart_stars = math.floor((max_heart_stars - required_heart_stars) * (self.options.filler_percentage / 100.0))
non_required_heart_stars = max_heart_stars - converted_heart_stars - required_heart_stars
filler_items -= non_required_heart_stars
trap_amount = math.floor(filler_items * (self.options.trap_percentage / 100.0))
filler_items -= trap_amount
self.required_heart_stars = required_heart_stars
# handle boss requirements here
requirements = [required_heart_stars]
@@ -261,8 +284,8 @@ class KDL3World(World):
requirements.insert(i - 1, quotient * i)
self.boss_requirements = requirements
itempool.extend([self.create_item("Heart Star") for _ in range(required_heart_stars)])
itempool.extend([self.create_item(self.get_filler_item_name(False))
for _ in range(filler_amount + (remaining_items - total_heart_stars))])
itempool.extend([self.create_item(self.get_filler_item_name(bool(self.options.starsanity.value)))
for _ in range(filler_items)])
itempool.extend([self.create_item(self.get_trap_item_name())
for _ in range(trap_amount)])
itempool.extend([self.create_item("Heart Star", True) for _ in range(non_required_heart_stars)])
@@ -273,15 +296,15 @@ class KDL3World(World):
self.multiworld.get_location(location_table[self.player_levels[level][stage]]
.replace("Complete", "Stage Completion"), self.player) \
.place_locked_item(KDL3Item(
f"{LocationName.level_names_inverse[level]} - Stage Completion",
f"{location_name.level_names_inverse[level]} - Stage Completion",
ItemClassification.progression, None, self.player))
set_rules = set_rules
def generate_basic(self) -> None:
self.stage_shuffle_enabled = self.options.stage_shuffle > 0
goal = self.options.goal
goal_location = self.multiworld.get_location(LocationName.goals[goal], self.player)
goal = self.options.goal.value
goal_location = self.multiworld.get_location(location_name.goals[goal], self.player)
goal_location.place_locked_item(KDL3Item("Love-Love Rod", ItemClassification.progression, None, self.player))
for level in range(1, 6):
self.multiworld.get_location(f"Level {level} Boss - Defeated", self.player) \
@@ -300,60 +323,65 @@ class KDL3World(World):
else:
self.boss_butch_bosses = [False for _ in range(6)]
def generate_output(self, output_directory: str):
rom_path = ""
def generate_output(self, output_directory: str) -> None:
try:
rom = RomData(get_base_rom_path())
patch_rom(self, rom)
patch = KDL3ProcedurePatch()
patch_rom(self, patch)
rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")
rom.write_to_file(rom_path)
self.rom_name = rom.name
self.rom_name = patch.name
patch = KDL3DeltaPatch(os.path.splitext(rom_path)[0] + KDL3DeltaPatch.patch_file_ending, player=self.player,
player_name=self.multiworld.player_name[self.player], patched_path=rom_path)
patch.write()
patch.write(os.path.join(output_directory,
f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}"))
except Exception:
raise
finally:
self.rom_name_available_event.set() # make sure threading continues and errors are collected
if os.path.exists(rom_path):
os.unlink(rom_path)
def modify_multidata(self, multidata: dict):
def modify_multidata(self, multidata: Dict[str, Any]) -> None:
# wait for self.rom_name to be available.
self.rom_name_available_event.wait()
assert isinstance(self.rom_name, bytes)
rom_name = getattr(self, "rom_name", None)
# we skip in case of error, so that the original error in the output thread is the one that gets raised
if rom_name:
new_name = base64.b64encode(bytes(self.rom_name)).decode()
new_name = base64.b64encode(self.rom_name).decode()
multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]
def fill_slot_data(self) -> Mapping[str, Any]:
# UT support
return {"player_levels": self.player_levels}
def interpret_slot_data(self, slot_data: Mapping[str, Any]):
# UT support
player_levels = {int(key): value for key, value in slot_data["player_levels"].items()}
return {"player_levels": player_levels}
def write_spoiler(self, spoiler_handle: TextIO) -> None:
if self.stage_shuffle_enabled:
spoiler_handle.write(f"\nLevel Layout ({self.multiworld.get_player_name(self.player)}):\n")
for level in LocationName.level_names:
for stage, i in zip(self.player_levels[LocationName.level_names[level]], range(1, 7)):
for level in location_name.level_names:
for stage, i in zip(self.player_levels[location_name.level_names[level]], range(1, 7)):
spoiler_handle.write(f"{level} {i}: {location_table[stage].replace(' - Complete', '')}\n")
if self.options.animal_randomization:
spoiler_handle.write(f"\nAnimal Friends ({self.multiworld.get_player_name(self.player)}):\n")
for level in self.player_levels:
for lvl in self.player_levels:
for stage in range(6):
rooms = [room for room in self.rooms if room.level == level and room.stage == stage]
rooms = [room for room in self.rooms if room.level == lvl and room.stage == stage]
animals = []
for room in rooms:
animals.extend([location.item.name.replace(" Spawn", "")
for location in room.locations if "Animal" in location.name])
spoiler_handle.write(f"{location_table[self.player_levels[level][stage]].replace(' - Complete','')}"
for location in room.locations if "Animal" in location.name
and location.item is not None])
spoiler_handle.write(f"{location_table[self.player_levels[lvl][stage]].replace(' - Complete','')}"
f": {', '.join(animals)}\n")
if self.options.copy_ability_randomization:
spoiler_handle.write(f"\nCopy Abilities ({self.multiworld.get_player_name(self.player)}):\n")
for enemy in self.copy_abilities:
spoiler_handle.write(f"{enemy}: {self.copy_abilities[enemy].replace('No Ability', 'None').replace(' Ability', '')}\n")
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
if self.stage_shuffle_enabled:
regions = {LocationName.level_names[level]: level for level in LocationName.level_names}
regions = {location_name.level_names[level]: level for level in location_name.level_names}
level_hint_data = {}
for level in regions:
for stage in range(7):
@@ -361,6 +389,6 @@ class KDL3World(World):
self.player).name.replace(" - Complete", "")
stage_regions = [room for room in self.rooms if stage_name in room.name]
for region in stage_regions:
for location in [location for location in region.locations if location.address]:
for location in [location for location in list(region.get_locations()) if location.address]:
level_hint_data[location.address] = f"{regions[level]} {stage + 1 if stage < 6 else 'Boss'}"
hint_data[self.player] = level_hint_data

View File

@@ -1,5 +1,9 @@
import struct
from .Options import KirbyFlavorPreset, GooeyFlavorPreset
from .options import KirbyFlavorPreset, GooeyFlavorPreset
from typing import TYPE_CHECKING, Optional, Dict, List, Tuple
if TYPE_CHECKING:
from . import KDL3World
kirby_flavor_presets = {
1: {
@@ -223,6 +227,23 @@ kirby_flavor_presets = {
"14": "E6E6FA",
"15": "976FBD",
},
14: {
"1": "373B3E",
"2": "98d5d3",
"3": "1aa5ab",
"4": "168f95",
"5": "4f5559",
"6": "1dbac2",
"7": "137a7f",
"8": "093a3c",
"9": "86cecb",
"10": "a0afbc",
"11": "62bfbb",
"12": "50b8b4",
"13": "bec8d1",
"14": "bce4e2",
"15": "91a2b1",
}
}
gooey_flavor_presets = {
@@ -398,21 +419,21 @@ gooey_target_palettes = {
}
def get_kirby_palette(world):
def get_kirby_palette(world: "KDL3World") -> Optional[Dict[str, str]]:
palette = world.options.kirby_flavor_preset.value
if palette == KirbyFlavorPreset.option_custom:
return world.options.kirby_flavor.value
return kirby_flavor_presets.get(palette, None)
def get_gooey_palette(world):
def get_gooey_palette(world: "KDL3World") -> Optional[Dict[str, str]]:
palette = world.options.gooey_flavor_preset.value
if palette == GooeyFlavorPreset.option_custom:
return world.options.gooey_flavor.value
return gooey_flavor_presets.get(palette, None)
def rgb888_to_bgr555(red, green, blue) -> bytes:
def rgb888_to_bgr555(red: int, green: int, blue: int) -> bytes:
red = red >> 3
green = green >> 3
blue = blue >> 3
@@ -420,15 +441,15 @@ def rgb888_to_bgr555(red, green, blue) -> bytes:
return struct.pack("H", outcol)
def get_palette_bytes(palette, target, offset, factor):
def get_palette_bytes(palette: Dict[str, str], target: List[str], offset: int, factor: float) -> bytes:
output_data = bytearray()
for color in target:
hexcol = palette[color]
if hexcol.startswith("#"):
hexcol = hexcol.replace("#", "")
colint = int(hexcol, 16)
col = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF)
col: Tuple[int, ...] = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF)
col = tuple(int(int(factor*x) + offset) for x in col)
byte_data = rgb888_to_bgr555(col[0], col[1], col[2])
output_data.extend(bytearray(byte_data))
return output_data
return bytes(output_data)

View File

@@ -11,13 +11,13 @@ from MultiServer import mark_raw
from NetUtils import ClientStatus, color
from Utils import async_start
from worlds.AutoSNIClient import SNIClient
from .Locations import boss_locations
from .Gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes
from .ClientAddrs import consumable_addrs, star_addrs
from .locations import boss_locations
from .gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes
from .client_addrs import consumable_addrs, star_addrs
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from SNIClient import SNIClientCommandProcessor
from SNIClient import SNIClientCommandProcessor, SNIContext
snes_logger = logging.getLogger("SNES")
@@ -81,17 +81,16 @@ deathlink_messages = defaultdict(lambda: " was defeated.", {
@mark_raw
def cmd_gift(self: "SNIClientCommandProcessor"):
def cmd_gift(self: "SNIClientCommandProcessor") -> None:
"""Toggles gifting for the current game."""
if not getattr(self.ctx, "gifting", None):
self.ctx.gifting = True
else:
self.ctx.gifting = not self.ctx.gifting
self.output(f"Gifting set to {self.ctx.gifting}")
handler = self.ctx.client_handler
assert isinstance(handler, KDL3SNIClient)
handler.gifting = not handler.gifting
self.output(f"Gifting set to {handler.gifting}")
async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", {
f"{self.ctx.slot}":
{
"IsOpen": self.ctx.gifting,
"IsOpen": handler.gifting,
**kdl3_gifting_options
}
}))
@@ -100,16 +99,17 @@ def cmd_gift(self: "SNIClientCommandProcessor"):
class KDL3SNIClient(SNIClient):
game = "Kirby's Dream Land 3"
patch_suffix = ".apkdl3"
levels = None
consumables = None
stars = None
item_queue: typing.List = []
initialize_gifting = False
levels: typing.Dict[int, typing.List[int]] = {}
consumables: typing.Optional[bool] = None
stars: typing.Optional[bool] = None
item_queue: typing.List[int] = []
initialize_gifting: bool = False
gifting: bool = False
giftbox_key: str = ""
motherbox_key: str = ""
client_random: random.Random = random.Random()
async def deathlink_kill_player(self, ctx) -> None:
async def deathlink_kill_player(self, ctx: "SNIContext") -> None:
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
game_state = await snes_read(ctx, KDL3_GAME_STATE, 1)
if game_state[0] == 0xFF:
@@ -131,7 +131,7 @@ class KDL3SNIClient(SNIClient):
ctx.death_state = DeathState.dead
ctx.last_death_link = time.time()
async def validate_rom(self, ctx) -> bool:
async def validate_rom(self, ctx: "SNIContext") -> bool:
from SNIClient import snes_read
rom_name = await snes_read(ctx, KDL3_ROMNAME, 0x15)
if rom_name is None or rom_name == bytes([0] * 0x15) or rom_name[:4] != b"KDL3":
@@ -141,7 +141,7 @@ class KDL3SNIClient(SNIClient):
ctx.game = self.game
ctx.rom = rom_name
ctx.items_handling = 0b111 # always remote items
ctx.items_handling = 0b101 # default local items with remote start inventory
ctx.allow_collect = True
if "gift" not in ctx.command_processor.commands:
ctx.command_processor.commands["gift"] = cmd_gift
@@ -149,9 +149,10 @@ class KDL3SNIClient(SNIClient):
death_link = await snes_read(ctx, KDL3_DEATH_LINK_ADDR, 1)
if death_link:
await ctx.update_death_link(bool(death_link[0] & 0b1))
ctx.items_handling |= (death_link[0] & 0b10) # set local items if enabled
return True
async def pop_item(self, ctx, in_stage):
async def pop_item(self, ctx: "SNIContext", in_stage: bool) -> None:
from SNIClient import snes_buffered_write, snes_read
if len(self.item_queue) > 0:
item = self.item_queue.pop()
@@ -168,8 +169,8 @@ class KDL3SNIClient(SNIClient):
else:
self.item_queue.append(item) # no more slots, get it next go around
async def pop_gift(self, ctx):
if ctx.stored_data[self.giftbox_key]:
async def pop_gift(self, ctx: "SNIContext") -> None:
if self.giftbox_key in ctx.stored_data and ctx.stored_data[self.giftbox_key]:
from SNIClient import snes_read, snes_buffered_write
key, gift = ctx.stored_data[self.giftbox_key].popitem()
await pop_object(ctx, self.giftbox_key, key)
@@ -214,7 +215,7 @@ class KDL3SNIClient(SNIClient):
quality = min(10, quality * 2)
else:
# it's not really edible, but he'll eat it anyway
quality = self.client_random.choices(range(0, 2), {0: 75, 1: 25})[0]
quality = self.client_random.choices(range(0, 2), [75, 25])[0]
kirby_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1)
gooey_hp = await snes_read(ctx, KDL3_KIRBY_HP + 2, 1)
snes_buffered_write(ctx, KDL3_SOUND_FX, bytes([0x26]))
@@ -224,7 +225,8 @@ class KDL3SNIClient(SNIClient):
else:
snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality, 10)))
async def pick_gift_recipient(self, ctx, gift):
async def pick_gift_recipient(self, ctx: "SNIContext", gift: int) -> None:
assert ctx.slot
if gift != 4:
gift_base = kdl3_gifts[gift]
else:
@@ -238,7 +240,7 @@ class KDL3SNIClient(SNIClient):
if desire > most_applicable:
most_applicable = desire
most_applicable_slot = int(slot)
elif most_applicable_slot == ctx.slot and info["AcceptsAnyGift"]:
elif most_applicable_slot != ctx.slot and most_applicable == -1 and info["AcceptsAnyGift"]:
# only send to ourselves if no one else will take it
most_applicable_slot = int(slot)
# print(most_applicable, most_applicable_slot)
@@ -257,7 +259,7 @@ class KDL3SNIClient(SNIClient):
item_uuid: item,
})
async def game_watcher(self, ctx) -> None:
async def game_watcher(self, ctx: "SNIContext") -> None:
try:
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
rom = await snes_read(ctx, KDL3_ROMNAME, 0x15)
@@ -278,11 +280,12 @@ class KDL3SNIClient(SNIClient):
await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key, bool(enable_gifting[0]))
self.initialize_gifting = True
# can't check debug anymore, without going and copying the value. might be important later.
if self.levels is None:
if not self.levels:
self.levels = dict()
for i in range(5):
level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14)
self.levels[i] = unpack("HHHHHHH", level_data)
self.levels[i] = [int.from_bytes(level_data[idx:idx+1], "little")
for idx in range(0, len(level_data), 2)]
self.levels[5] = [0x0205, # Hyper Zone
0, # MG-5, can't send from here
0x0300, # Boss Butch
@@ -371,7 +374,7 @@ class KDL3SNIClient(SNIClient):
stages_raw = await snes_read(ctx, KDL3_COMPLETED_STAGES, 60)
stages = struct.unpack("HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", stages_raw)
for i in range(30):
loc_id = 0x770000 + i + 1
loc_id = 0x770000 + i
if stages[i] == 1 and loc_id not in ctx.checked_locations:
new_checks.append(loc_id)
elif loc_id in ctx.checked_locations:
@@ -381,8 +384,8 @@ class KDL3SNIClient(SNIClient):
heart_stars = await snes_read(ctx, KDL3_HEART_STARS, 35)
for i in range(5):
start_ind = i * 7
for j in range(1, 7):
level_ind = start_ind + j - 1
for j in range(6):
level_ind = start_ind + j
loc_id = 0x770100 + (6 * i) + j
if heart_stars[level_ind] and loc_id not in ctx.checked_locations:
new_checks.append(loc_id)
@@ -401,6 +404,9 @@ class KDL3SNIClient(SNIClient):
if star not in ctx.checked_locations and stars[star_addrs[star]] == 0x01:
new_checks.append(star)
if not game_state:
return
if game_state[0] != 0xFF:
await self.pop_gift(ctx)
await self.pop_item(ctx, game_state[0] != 0xFF)
@@ -408,7 +414,7 @@ class KDL3SNIClient(SNIClient):
# boss status
boss_flag_bytes = await snes_read(ctx, KDL3_BOSS_STATUS, 2)
boss_flag = unpack("H", boss_flag_bytes)[0]
boss_flag = int.from_bytes(boss_flag_bytes, "little")
for bitmask, boss in zip(range(1, 11, 2), boss_locations.keys()):
if boss_flag & (1 << bitmask) > 0 and boss not in ctx.checked_locations:
new_checks.append(boss)

View File

@@ -1,8 +1,11 @@
# Small subfile to handle gifting info such as desired traits and giftbox management
import typing
if typing.TYPE_CHECKING:
from SNIClient import SNIContext
async def update_object(ctx, key: str, value: typing.Dict):
async def update_object(ctx: "SNIContext", key: str, value: typing.Dict[str, typing.Any]) -> None:
await ctx.send_msgs([
{
"cmd": "Set",
@@ -16,7 +19,7 @@ async def update_object(ctx, key: str, value: typing.Dict):
])
async def pop_object(ctx, key: str, value: str):
async def pop_object(ctx: "SNIContext", key: str, value: str) -> None:
await ctx.send_msgs([
{
"cmd": "Set",
@@ -30,14 +33,14 @@ async def pop_object(ctx, key: str, value: str):
])
async def initialize_giftboxes(ctx, giftbox_key: str, motherbox_key: str, is_open: bool):
async def initialize_giftboxes(ctx: "SNIContext", giftbox_key: str, motherbox_key: str, is_open: bool) -> None:
ctx.set_notify(motherbox_key, giftbox_key)
await update_object(ctx, f"Giftboxes;{ctx.team}", {f"{ctx.slot}":
{
"IsOpen": is_open,
**kdl3_gifting_options
}})
ctx.gifting = is_open
{
"IsOpen": is_open,
**kdl3_gifting_options
}})
ctx.client_handler.gifting = is_open
kdl3_gifting_options = {

View File

@@ -77,9 +77,9 @@ filler_item_weights = {
}
star_item_weights = {
"Little Star": 4,
"Medium Star": 2,
"Big Star": 1
"Little Star": 16,
"Medium Star": 8,
"Big Star": 4
}
total_filler_weights = {
@@ -102,4 +102,4 @@ item_names = {
"Animal Friend": set(animal_friend_table),
}
lookup_name_to_id: typing.Dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code}
lookup_item_to_id: typing.Dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code}

940
worlds/kdl3/locations.py Normal file
View File

@@ -0,0 +1,940 @@
import typing
from BaseClasses import Location, Region
from .names import location_name
if typing.TYPE_CHECKING:
from .room import KDL3Room
class KDL3Location(Location):
game: str = "Kirby's Dream Land 3"
room: typing.Optional["KDL3Room"] = None
def __init__(self, player: int, name: str, address: typing.Optional[int], parent: typing.Union[Region, None]):
super().__init__(player, name, address, parent)
if not address:
self.show_in_spoiler = False
stage_locations = {
0x770000: location_name.grass_land_1,
0x770001: location_name.grass_land_2,
0x770002: location_name.grass_land_3,
0x770003: location_name.grass_land_4,
0x770004: location_name.grass_land_5,
0x770005: location_name.grass_land_6,
0x770006: location_name.ripple_field_1,
0x770007: location_name.ripple_field_2,
0x770008: location_name.ripple_field_3,
0x770009: location_name.ripple_field_4,
0x77000A: location_name.ripple_field_5,
0x77000B: location_name.ripple_field_6,
0x77000C: location_name.sand_canyon_1,
0x77000D: location_name.sand_canyon_2,
0x77000E: location_name.sand_canyon_3,
0x77000F: location_name.sand_canyon_4,
0x770010: location_name.sand_canyon_5,
0x770011: location_name.sand_canyon_6,
0x770012: location_name.cloudy_park_1,
0x770013: location_name.cloudy_park_2,
0x770014: location_name.cloudy_park_3,
0x770015: location_name.cloudy_park_4,
0x770016: location_name.cloudy_park_5,
0x770017: location_name.cloudy_park_6,
0x770018: location_name.iceberg_1,
0x770019: location_name.iceberg_2,
0x77001A: location_name.iceberg_3,
0x77001B: location_name.iceberg_4,
0x77001C: location_name.iceberg_5,
0x77001D: location_name.iceberg_6,
}
heart_star_locations = {
0x770100: location_name.grass_land_tulip,
0x770101: location_name.grass_land_muchi,
0x770102: location_name.grass_land_pitcherman,
0x770103: location_name.grass_land_chao,
0x770104: location_name.grass_land_mine,
0x770105: location_name.grass_land_pierre,
0x770106: location_name.ripple_field_kamuribana,
0x770107: location_name.ripple_field_bakasa,
0x770108: location_name.ripple_field_elieel,
0x770109: location_name.ripple_field_toad,
0x77010A: location_name.ripple_field_mama_pitch,
0x77010B: location_name.ripple_field_hb002,
0x77010C: location_name.sand_canyon_mushrooms,
0x77010D: location_name.sand_canyon_auntie,
0x77010E: location_name.sand_canyon_caramello,
0x77010F: location_name.sand_canyon_hikari,
0x770110: location_name.sand_canyon_nyupun,
0x770111: location_name.sand_canyon_rob,
0x770112: location_name.cloudy_park_hibanamodoki,
0x770113: location_name.cloudy_park_piyokeko,
0x770114: location_name.cloudy_park_mrball,
0x770115: location_name.cloudy_park_mikarin,
0x770116: location_name.cloudy_park_pick,
0x770117: location_name.cloudy_park_hb007,
0x770118: location_name.iceberg_kogoesou,
0x770119: location_name.iceberg_samus,
0x77011A: location_name.iceberg_kawasaki,
0x77011B: location_name.iceberg_name,
0x77011C: location_name.iceberg_shiro,
0x77011D: location_name.iceberg_angel,
}
boss_locations = {
0x770200: location_name.grass_land_whispy,
0x770201: location_name.ripple_field_acro,
0x770202: location_name.sand_canyon_poncon,
0x770203: location_name.cloudy_park_ado,
0x770204: location_name.iceberg_dedede,
}
consumable_locations = {
0x770300: location_name.grass_land_1_u1,
0x770301: location_name.grass_land_1_m1,
0x770302: location_name.grass_land_2_u1,
0x770303: location_name.grass_land_3_u1,
0x770304: location_name.grass_land_3_m1,
0x770305: location_name.grass_land_4_m1,
0x770306: location_name.grass_land_4_u1,
0x770307: location_name.grass_land_4_m2,
0x770308: location_name.grass_land_4_m3,
0x770309: location_name.grass_land_6_u1,
0x77030A: location_name.grass_land_6_u2,
0x77030B: location_name.ripple_field_2_u1,
0x77030C: location_name.ripple_field_2_m1,
0x77030D: location_name.ripple_field_3_m1,
0x77030E: location_name.ripple_field_3_u1,
0x77030F: location_name.ripple_field_4_m2,
0x770310: location_name.ripple_field_4_u1,
0x770311: location_name.ripple_field_4_m1,
0x770312: location_name.ripple_field_5_u1,
0x770313: location_name.ripple_field_5_m2,
0x770314: location_name.ripple_field_5_m1,
0x770315: location_name.sand_canyon_1_u1,
0x770316: location_name.sand_canyon_2_u1,
0x770317: location_name.sand_canyon_2_m1,
0x770318: location_name.sand_canyon_4_m1,
0x770319: location_name.sand_canyon_4_u1,
0x77031A: location_name.sand_canyon_4_m2,
0x77031B: location_name.sand_canyon_5_u1,
0x77031C: location_name.sand_canyon_5_u3,
0x77031D: location_name.sand_canyon_5_m1,
0x77031E: location_name.sand_canyon_5_u4,
0x77031F: location_name.sand_canyon_5_u2,
0x770320: location_name.cloudy_park_1_m1,
0x770321: location_name.cloudy_park_1_u1,
0x770322: location_name.cloudy_park_4_u1,
0x770323: location_name.cloudy_park_4_m1,
0x770324: location_name.cloudy_park_5_m1,
0x770325: location_name.cloudy_park_6_u1,
0x770326: location_name.iceberg_3_m1,
0x770327: location_name.iceberg_5_u1,
0x770328: location_name.iceberg_5_u2,
0x770329: location_name.iceberg_5_u3,
0x77032A: location_name.iceberg_6_m1,
0x77032B: location_name.iceberg_6_u1,
}
level_consumables = {
1: [0, 1],
2: [2],
3: [3, 4],
4: [5, 6, 7, 8],
6: [9, 10],
8: [11, 12],
9: [13, 14],
10: [15, 16, 17],
11: [18, 19, 20],
13: [21],
14: [22, 23],
16: [24, 25, 26],
17: [27, 28, 29, 30, 31],
19: [32, 33],
22: [34, 35],
23: [36],
24: [37],
27: [38],
29: [39, 40, 41],
30: [42, 43],
}
star_locations = {
0x770401: location_name.grass_land_1_s1,
0x770402: location_name.grass_land_1_s2,
0x770403: location_name.grass_land_1_s3,
0x770404: location_name.grass_land_1_s4,
0x770405: location_name.grass_land_1_s5,
0x770406: location_name.grass_land_1_s6,
0x770407: location_name.grass_land_1_s7,
0x770408: location_name.grass_land_1_s8,
0x770409: location_name.grass_land_1_s9,
0x77040a: location_name.grass_land_1_s10,
0x77040b: location_name.grass_land_1_s11,
0x77040c: location_name.grass_land_1_s12,
0x77040d: location_name.grass_land_1_s13,
0x77040e: location_name.grass_land_1_s14,
0x77040f: location_name.grass_land_1_s15,
0x770410: location_name.grass_land_1_s16,
0x770411: location_name.grass_land_1_s17,
0x770412: location_name.grass_land_1_s18,
0x770413: location_name.grass_land_1_s19,
0x770414: location_name.grass_land_1_s20,
0x770415: location_name.grass_land_1_s21,
0x770416: location_name.grass_land_1_s22,
0x770417: location_name.grass_land_1_s23,
0x770418: location_name.grass_land_2_s1,
0x770419: location_name.grass_land_2_s2,
0x77041a: location_name.grass_land_2_s3,
0x77041b: location_name.grass_land_2_s4,
0x77041c: location_name.grass_land_2_s5,
0x77041d: location_name.grass_land_2_s6,
0x77041e: location_name.grass_land_2_s7,
0x77041f: location_name.grass_land_2_s8,
0x770420: location_name.grass_land_2_s9,
0x770421: location_name.grass_land_2_s10,
0x770422: location_name.grass_land_2_s11,
0x770423: location_name.grass_land_2_s12,
0x770424: location_name.grass_land_2_s13,
0x770425: location_name.grass_land_2_s14,
0x770426: location_name.grass_land_2_s15,
0x770427: location_name.grass_land_2_s16,
0x770428: location_name.grass_land_2_s17,
0x770429: location_name.grass_land_2_s18,
0x77042a: location_name.grass_land_2_s19,
0x77042b: location_name.grass_land_2_s20,
0x77042c: location_name.grass_land_2_s21,
0x77042d: location_name.grass_land_3_s1,
0x77042e: location_name.grass_land_3_s2,
0x77042f: location_name.grass_land_3_s3,
0x770430: location_name.grass_land_3_s4,
0x770431: location_name.grass_land_3_s5,
0x770432: location_name.grass_land_3_s6,
0x770433: location_name.grass_land_3_s7,
0x770434: location_name.grass_land_3_s8,
0x770435: location_name.grass_land_3_s9,
0x770436: location_name.grass_land_3_s10,
0x770437: location_name.grass_land_3_s11,
0x770438: location_name.grass_land_3_s12,
0x770439: location_name.grass_land_3_s13,
0x77043a: location_name.grass_land_3_s14,
0x77043b: location_name.grass_land_3_s15,
0x77043c: location_name.grass_land_3_s16,
0x77043d: location_name.grass_land_3_s17,
0x77043e: location_name.grass_land_3_s18,
0x77043f: location_name.grass_land_3_s19,
0x770440: location_name.grass_land_3_s20,
0x770441: location_name.grass_land_3_s21,
0x770442: location_name.grass_land_3_s22,
0x770443: location_name.grass_land_3_s23,
0x770444: location_name.grass_land_3_s24,
0x770445: location_name.grass_land_3_s25,
0x770446: location_name.grass_land_3_s26,
0x770447: location_name.grass_land_3_s27,
0x770448: location_name.grass_land_3_s28,
0x770449: location_name.grass_land_3_s29,
0x77044a: location_name.grass_land_3_s30,
0x77044b: location_name.grass_land_3_s31,
0x77044c: location_name.grass_land_4_s1,
0x77044d: location_name.grass_land_4_s2,
0x77044e: location_name.grass_land_4_s3,
0x77044f: location_name.grass_land_4_s4,
0x770450: location_name.grass_land_4_s5,
0x770451: location_name.grass_land_4_s6,
0x770452: location_name.grass_land_4_s7,
0x770453: location_name.grass_land_4_s8,
0x770454: location_name.grass_land_4_s9,
0x770455: location_name.grass_land_4_s10,
0x770456: location_name.grass_land_4_s11,
0x770457: location_name.grass_land_4_s12,
0x770458: location_name.grass_land_4_s13,
0x770459: location_name.grass_land_4_s14,
0x77045a: location_name.grass_land_4_s15,
0x77045b: location_name.grass_land_4_s16,
0x77045c: location_name.grass_land_4_s17,
0x77045d: location_name.grass_land_4_s18,
0x77045e: location_name.grass_land_4_s19,
0x77045f: location_name.grass_land_4_s20,
0x770460: location_name.grass_land_4_s21,
0x770461: location_name.grass_land_4_s22,
0x770462: location_name.grass_land_4_s23,
0x770463: location_name.grass_land_4_s24,
0x770464: location_name.grass_land_4_s25,
0x770465: location_name.grass_land_4_s26,
0x770466: location_name.grass_land_4_s27,
0x770467: location_name.grass_land_4_s28,
0x770468: location_name.grass_land_4_s29,
0x770469: location_name.grass_land_4_s30,
0x77046a: location_name.grass_land_4_s31,
0x77046b: location_name.grass_land_4_s32,
0x77046c: location_name.grass_land_4_s33,
0x77046d: location_name.grass_land_4_s34,
0x77046e: location_name.grass_land_4_s35,
0x77046f: location_name.grass_land_4_s36,
0x770470: location_name.grass_land_4_s37,
0x770471: location_name.grass_land_5_s1,
0x770472: location_name.grass_land_5_s2,
0x770473: location_name.grass_land_5_s3,
0x770474: location_name.grass_land_5_s4,
0x770475: location_name.grass_land_5_s5,
0x770476: location_name.grass_land_5_s6,
0x770477: location_name.grass_land_5_s7,
0x770478: location_name.grass_land_5_s8,
0x770479: location_name.grass_land_5_s9,
0x77047a: location_name.grass_land_5_s10,
0x77047b: location_name.grass_land_5_s11,
0x77047c: location_name.grass_land_5_s12,
0x77047d: location_name.grass_land_5_s13,
0x77047e: location_name.grass_land_5_s14,
0x77047f: location_name.grass_land_5_s15,
0x770480: location_name.grass_land_5_s16,
0x770481: location_name.grass_land_5_s17,
0x770482: location_name.grass_land_5_s18,
0x770483: location_name.grass_land_5_s19,
0x770484: location_name.grass_land_5_s20,
0x770485: location_name.grass_land_5_s21,
0x770486: location_name.grass_land_5_s22,
0x770487: location_name.grass_land_5_s23,
0x770488: location_name.grass_land_5_s24,
0x770489: location_name.grass_land_5_s25,
0x77048a: location_name.grass_land_5_s26,
0x77048b: location_name.grass_land_5_s27,
0x77048c: location_name.grass_land_5_s28,
0x77048d: location_name.grass_land_5_s29,
0x77048e: location_name.grass_land_6_s1,
0x77048f: location_name.grass_land_6_s2,
0x770490: location_name.grass_land_6_s3,
0x770491: location_name.grass_land_6_s4,
0x770492: location_name.grass_land_6_s5,
0x770493: location_name.grass_land_6_s6,
0x770494: location_name.grass_land_6_s7,
0x770495: location_name.grass_land_6_s8,
0x770496: location_name.grass_land_6_s9,
0x770497: location_name.grass_land_6_s10,
0x770498: location_name.grass_land_6_s11,
0x770499: location_name.grass_land_6_s12,
0x77049a: location_name.grass_land_6_s13,
0x77049b: location_name.grass_land_6_s14,
0x77049c: location_name.grass_land_6_s15,
0x77049d: location_name.grass_land_6_s16,
0x77049e: location_name.grass_land_6_s17,
0x77049f: location_name.grass_land_6_s18,
0x7704a0: location_name.grass_land_6_s19,
0x7704a1: location_name.grass_land_6_s20,
0x7704a2: location_name.grass_land_6_s21,
0x7704a3: location_name.grass_land_6_s22,
0x7704a4: location_name.grass_land_6_s23,
0x7704a5: location_name.grass_land_6_s24,
0x7704a6: location_name.grass_land_6_s25,
0x7704a7: location_name.grass_land_6_s26,
0x7704a8: location_name.grass_land_6_s27,
0x7704a9: location_name.grass_land_6_s28,
0x7704aa: location_name.grass_land_6_s29,
0x7704ab: location_name.ripple_field_1_s1,
0x7704ac: location_name.ripple_field_1_s2,
0x7704ad: location_name.ripple_field_1_s3,
0x7704ae: location_name.ripple_field_1_s4,
0x7704af: location_name.ripple_field_1_s5,
0x7704b0: location_name.ripple_field_1_s6,
0x7704b1: location_name.ripple_field_1_s7,
0x7704b2: location_name.ripple_field_1_s8,
0x7704b3: location_name.ripple_field_1_s9,
0x7704b4: location_name.ripple_field_1_s10,
0x7704b5: location_name.ripple_field_1_s11,
0x7704b6: location_name.ripple_field_1_s12,
0x7704b7: location_name.ripple_field_1_s13,
0x7704b8: location_name.ripple_field_1_s14,
0x7704b9: location_name.ripple_field_1_s15,
0x7704ba: location_name.ripple_field_1_s16,
0x7704bb: location_name.ripple_field_1_s17,
0x7704bc: location_name.ripple_field_1_s18,
0x7704bd: location_name.ripple_field_1_s19,
0x7704be: location_name.ripple_field_2_s1,
0x7704bf: location_name.ripple_field_2_s2,
0x7704c0: location_name.ripple_field_2_s3,
0x7704c1: location_name.ripple_field_2_s4,
0x7704c2: location_name.ripple_field_2_s5,
0x7704c3: location_name.ripple_field_2_s6,
0x7704c4: location_name.ripple_field_2_s7,
0x7704c5: location_name.ripple_field_2_s8,
0x7704c6: location_name.ripple_field_2_s9,
0x7704c7: location_name.ripple_field_2_s10,
0x7704c8: location_name.ripple_field_2_s11,
0x7704c9: location_name.ripple_field_2_s12,
0x7704ca: location_name.ripple_field_2_s13,
0x7704cb: location_name.ripple_field_2_s14,
0x7704cc: location_name.ripple_field_2_s15,
0x7704cd: location_name.ripple_field_2_s16,
0x7704ce: location_name.ripple_field_2_s17,
0x7704cf: location_name.ripple_field_3_s1,
0x7704d0: location_name.ripple_field_3_s2,
0x7704d1: location_name.ripple_field_3_s3,
0x7704d2: location_name.ripple_field_3_s4,
0x7704d3: location_name.ripple_field_3_s5,
0x7704d4: location_name.ripple_field_3_s6,
0x7704d5: location_name.ripple_field_3_s7,
0x7704d6: location_name.ripple_field_3_s8,
0x7704d7: location_name.ripple_field_3_s9,
0x7704d8: location_name.ripple_field_3_s10,
0x7704d9: location_name.ripple_field_3_s11,
0x7704da: location_name.ripple_field_3_s12,
0x7704db: location_name.ripple_field_3_s13,
0x7704dc: location_name.ripple_field_3_s14,
0x7704dd: location_name.ripple_field_3_s15,
0x7704de: location_name.ripple_field_3_s16,
0x7704df: location_name.ripple_field_3_s17,
0x7704e0: location_name.ripple_field_3_s18,
0x7704e1: location_name.ripple_field_3_s19,
0x7704e2: location_name.ripple_field_3_s20,
0x7704e3: location_name.ripple_field_3_s21,
0x7704e4: location_name.ripple_field_4_s1,
0x7704e5: location_name.ripple_field_4_s2,
0x7704e6: location_name.ripple_field_4_s3,
0x7704e7: location_name.ripple_field_4_s4,
0x7704e8: location_name.ripple_field_4_s5,
0x7704e9: location_name.ripple_field_4_s6,
0x7704ea: location_name.ripple_field_4_s7,
0x7704eb: location_name.ripple_field_4_s8,
0x7704ec: location_name.ripple_field_4_s9,
0x7704ed: location_name.ripple_field_4_s10,
0x7704ee: location_name.ripple_field_4_s11,
0x7704ef: location_name.ripple_field_4_s12,
0x7704f0: location_name.ripple_field_4_s13,
0x7704f1: location_name.ripple_field_4_s14,
0x7704f2: location_name.ripple_field_4_s15,
0x7704f3: location_name.ripple_field_4_s16,
0x7704f4: location_name.ripple_field_4_s17,
0x7704f5: location_name.ripple_field_4_s18,
0x7704f6: location_name.ripple_field_4_s19,
0x7704f7: location_name.ripple_field_4_s20,
0x7704f8: location_name.ripple_field_4_s21,
0x7704f9: location_name.ripple_field_4_s22,
0x7704fa: location_name.ripple_field_4_s23,
0x7704fb: location_name.ripple_field_4_s24,
0x7704fc: location_name.ripple_field_4_s25,
0x7704fd: location_name.ripple_field_4_s26,
0x7704fe: location_name.ripple_field_4_s27,
0x7704ff: location_name.ripple_field_4_s28,
0x770500: location_name.ripple_field_4_s29,
0x770501: location_name.ripple_field_4_s30,
0x770502: location_name.ripple_field_4_s31,
0x770503: location_name.ripple_field_4_s32,
0x770504: location_name.ripple_field_4_s33,
0x770505: location_name.ripple_field_4_s34,
0x770506: location_name.ripple_field_4_s35,
0x770507: location_name.ripple_field_4_s36,
0x770508: location_name.ripple_field_4_s37,
0x770509: location_name.ripple_field_4_s38,
0x77050a: location_name.ripple_field_4_s39,
0x77050b: location_name.ripple_field_4_s40,
0x77050c: location_name.ripple_field_4_s41,
0x77050d: location_name.ripple_field_4_s42,
0x77050e: location_name.ripple_field_4_s43,
0x77050f: location_name.ripple_field_4_s44,
0x770510: location_name.ripple_field_4_s45,
0x770511: location_name.ripple_field_4_s46,
0x770512: location_name.ripple_field_4_s47,
0x770513: location_name.ripple_field_4_s48,
0x770514: location_name.ripple_field_4_s49,
0x770515: location_name.ripple_field_4_s50,
0x770516: location_name.ripple_field_4_s51,
0x770517: location_name.ripple_field_5_s1,
0x770518: location_name.ripple_field_5_s2,
0x770519: location_name.ripple_field_5_s3,
0x77051a: location_name.ripple_field_5_s4,
0x77051b: location_name.ripple_field_5_s5,
0x77051c: location_name.ripple_field_5_s6,
0x77051d: location_name.ripple_field_5_s7,
0x77051e: location_name.ripple_field_5_s8,
0x77051f: location_name.ripple_field_5_s9,
0x770520: location_name.ripple_field_5_s10,
0x770521: location_name.ripple_field_5_s11,
0x770522: location_name.ripple_field_5_s12,
0x770523: location_name.ripple_field_5_s13,
0x770524: location_name.ripple_field_5_s14,
0x770525: location_name.ripple_field_5_s15,
0x770526: location_name.ripple_field_5_s16,
0x770527: location_name.ripple_field_5_s17,
0x770528: location_name.ripple_field_5_s18,
0x770529: location_name.ripple_field_5_s19,
0x77052a: location_name.ripple_field_5_s20,
0x77052b: location_name.ripple_field_5_s21,
0x77052c: location_name.ripple_field_5_s22,
0x77052d: location_name.ripple_field_5_s23,
0x77052e: location_name.ripple_field_5_s24,
0x77052f: location_name.ripple_field_5_s25,
0x770530: location_name.ripple_field_5_s26,
0x770531: location_name.ripple_field_5_s27,
0x770532: location_name.ripple_field_5_s28,
0x770533: location_name.ripple_field_5_s29,
0x770534: location_name.ripple_field_5_s30,
0x770535: location_name.ripple_field_5_s31,
0x770536: location_name.ripple_field_5_s32,
0x770537: location_name.ripple_field_5_s33,
0x770538: location_name.ripple_field_5_s34,
0x770539: location_name.ripple_field_5_s35,
0x77053a: location_name.ripple_field_5_s36,
0x77053b: location_name.ripple_field_5_s37,
0x77053c: location_name.ripple_field_5_s38,
0x77053d: location_name.ripple_field_5_s39,
0x77053e: location_name.ripple_field_5_s40,
0x77053f: location_name.ripple_field_5_s41,
0x770540: location_name.ripple_field_5_s42,
0x770541: location_name.ripple_field_5_s43,
0x770542: location_name.ripple_field_5_s44,
0x770543: location_name.ripple_field_5_s45,
0x770544: location_name.ripple_field_5_s46,
0x770545: location_name.ripple_field_5_s47,
0x770546: location_name.ripple_field_5_s48,
0x770547: location_name.ripple_field_5_s49,
0x770548: location_name.ripple_field_5_s50,
0x770549: location_name.ripple_field_5_s51,
0x77054a: location_name.ripple_field_6_s1,
0x77054b: location_name.ripple_field_6_s2,
0x77054c: location_name.ripple_field_6_s3,
0x77054d: location_name.ripple_field_6_s4,
0x77054e: location_name.ripple_field_6_s5,
0x77054f: location_name.ripple_field_6_s6,
0x770550: location_name.ripple_field_6_s7,
0x770551: location_name.ripple_field_6_s8,
0x770552: location_name.ripple_field_6_s9,
0x770553: location_name.ripple_field_6_s10,
0x770554: location_name.ripple_field_6_s11,
0x770555: location_name.ripple_field_6_s12,
0x770556: location_name.ripple_field_6_s13,
0x770557: location_name.ripple_field_6_s14,
0x770558: location_name.ripple_field_6_s15,
0x770559: location_name.ripple_field_6_s16,
0x77055a: location_name.ripple_field_6_s17,
0x77055b: location_name.ripple_field_6_s18,
0x77055c: location_name.ripple_field_6_s19,
0x77055d: location_name.ripple_field_6_s20,
0x77055e: location_name.ripple_field_6_s21,
0x77055f: location_name.ripple_field_6_s22,
0x770560: location_name.ripple_field_6_s23,
0x770561: location_name.sand_canyon_1_s1,
0x770562: location_name.sand_canyon_1_s2,
0x770563: location_name.sand_canyon_1_s3,
0x770564: location_name.sand_canyon_1_s4,
0x770565: location_name.sand_canyon_1_s5,
0x770566: location_name.sand_canyon_1_s6,
0x770567: location_name.sand_canyon_1_s7,
0x770568: location_name.sand_canyon_1_s8,
0x770569: location_name.sand_canyon_1_s9,
0x77056a: location_name.sand_canyon_1_s10,
0x77056b: location_name.sand_canyon_1_s11,
0x77056c: location_name.sand_canyon_1_s12,
0x77056d: location_name.sand_canyon_1_s13,
0x77056e: location_name.sand_canyon_1_s14,
0x77056f: location_name.sand_canyon_1_s15,
0x770570: location_name.sand_canyon_1_s16,
0x770571: location_name.sand_canyon_1_s17,
0x770572: location_name.sand_canyon_1_s18,
0x770573: location_name.sand_canyon_1_s19,
0x770574: location_name.sand_canyon_1_s20,
0x770575: location_name.sand_canyon_1_s21,
0x770576: location_name.sand_canyon_1_s22,
0x770577: location_name.sand_canyon_2_s1,
0x770578: location_name.sand_canyon_2_s2,
0x770579: location_name.sand_canyon_2_s3,
0x77057a: location_name.sand_canyon_2_s4,
0x77057b: location_name.sand_canyon_2_s5,
0x77057c: location_name.sand_canyon_2_s6,
0x77057d: location_name.sand_canyon_2_s7,
0x77057e: location_name.sand_canyon_2_s8,
0x77057f: location_name.sand_canyon_2_s9,
0x770580: location_name.sand_canyon_2_s10,
0x770581: location_name.sand_canyon_2_s11,
0x770582: location_name.sand_canyon_2_s12,
0x770583: location_name.sand_canyon_2_s13,
0x770584: location_name.sand_canyon_2_s14,
0x770585: location_name.sand_canyon_2_s15,
0x770586: location_name.sand_canyon_2_s16,
0x770587: location_name.sand_canyon_2_s17,
0x770588: location_name.sand_canyon_2_s18,
0x770589: location_name.sand_canyon_2_s19,
0x77058a: location_name.sand_canyon_2_s20,
0x77058b: location_name.sand_canyon_2_s21,
0x77058c: location_name.sand_canyon_2_s22,
0x77058d: location_name.sand_canyon_2_s23,
0x77058e: location_name.sand_canyon_2_s24,
0x77058f: location_name.sand_canyon_2_s25,
0x770590: location_name.sand_canyon_2_s26,
0x770591: location_name.sand_canyon_2_s27,
0x770592: location_name.sand_canyon_2_s28,
0x770593: location_name.sand_canyon_2_s29,
0x770594: location_name.sand_canyon_2_s30,
0x770595: location_name.sand_canyon_2_s31,
0x770596: location_name.sand_canyon_2_s32,
0x770597: location_name.sand_canyon_2_s33,
0x770598: location_name.sand_canyon_2_s34,
0x770599: location_name.sand_canyon_2_s35,
0x77059a: location_name.sand_canyon_2_s36,
0x77059b: location_name.sand_canyon_2_s37,
0x77059c: location_name.sand_canyon_2_s38,
0x77059d: location_name.sand_canyon_2_s39,
0x77059e: location_name.sand_canyon_2_s40,
0x77059f: location_name.sand_canyon_2_s41,
0x7705a0: location_name.sand_canyon_2_s42,
0x7705a1: location_name.sand_canyon_2_s43,
0x7705a2: location_name.sand_canyon_2_s44,
0x7705a3: location_name.sand_canyon_2_s45,
0x7705a4: location_name.sand_canyon_2_s46,
0x7705a5: location_name.sand_canyon_2_s47,
0x7705a6: location_name.sand_canyon_2_s48,
0x7705a7: location_name.sand_canyon_3_s1,
0x7705a8: location_name.sand_canyon_3_s2,
0x7705a9: location_name.sand_canyon_3_s3,
0x7705aa: location_name.sand_canyon_3_s4,
0x7705ab: location_name.sand_canyon_3_s5,
0x7705ac: location_name.sand_canyon_3_s6,
0x7705ad: location_name.sand_canyon_3_s7,
0x7705ae: location_name.sand_canyon_3_s8,
0x7705af: location_name.sand_canyon_3_s9,
0x7705b0: location_name.sand_canyon_3_s10,
0x7705b1: location_name.sand_canyon_4_s1,
0x7705b2: location_name.sand_canyon_4_s2,
0x7705b3: location_name.sand_canyon_4_s3,
0x7705b4: location_name.sand_canyon_4_s4,
0x7705b5: location_name.sand_canyon_4_s5,
0x7705b6: location_name.sand_canyon_4_s6,
0x7705b7: location_name.sand_canyon_4_s7,
0x7705b8: location_name.sand_canyon_4_s8,
0x7705b9: location_name.sand_canyon_4_s9,
0x7705ba: location_name.sand_canyon_4_s10,
0x7705bb: location_name.sand_canyon_4_s11,
0x7705bc: location_name.sand_canyon_4_s12,
0x7705bd: location_name.sand_canyon_4_s13,
0x7705be: location_name.sand_canyon_4_s14,
0x7705bf: location_name.sand_canyon_4_s15,
0x7705c0: location_name.sand_canyon_4_s16,
0x7705c1: location_name.sand_canyon_4_s17,
0x7705c2: location_name.sand_canyon_4_s18,
0x7705c3: location_name.sand_canyon_4_s19,
0x7705c4: location_name.sand_canyon_4_s20,
0x7705c5: location_name.sand_canyon_4_s21,
0x7705c6: location_name.sand_canyon_4_s22,
0x7705c7: location_name.sand_canyon_4_s23,
0x7705c8: location_name.sand_canyon_5_s1,
0x7705c9: location_name.sand_canyon_5_s2,
0x7705ca: location_name.sand_canyon_5_s3,
0x7705cb: location_name.sand_canyon_5_s4,
0x7705cc: location_name.sand_canyon_5_s5,
0x7705cd: location_name.sand_canyon_5_s6,
0x7705ce: location_name.sand_canyon_5_s7,
0x7705cf: location_name.sand_canyon_5_s8,
0x7705d0: location_name.sand_canyon_5_s9,
0x7705d1: location_name.sand_canyon_5_s10,
0x7705d2: location_name.sand_canyon_5_s11,
0x7705d3: location_name.sand_canyon_5_s12,
0x7705d4: location_name.sand_canyon_5_s13,
0x7705d5: location_name.sand_canyon_5_s14,
0x7705d6: location_name.sand_canyon_5_s15,
0x7705d7: location_name.sand_canyon_5_s16,
0x7705d8: location_name.sand_canyon_5_s17,
0x7705d9: location_name.sand_canyon_5_s18,
0x7705da: location_name.sand_canyon_5_s19,
0x7705db: location_name.sand_canyon_5_s20,
0x7705dc: location_name.sand_canyon_5_s21,
0x7705dd: location_name.sand_canyon_5_s22,
0x7705de: location_name.sand_canyon_5_s23,
0x7705df: location_name.sand_canyon_5_s24,
0x7705e0: location_name.sand_canyon_5_s25,
0x7705e1: location_name.sand_canyon_5_s26,
0x7705e2: location_name.sand_canyon_5_s27,
0x7705e3: location_name.sand_canyon_5_s28,
0x7705e4: location_name.sand_canyon_5_s29,
0x7705e5: location_name.sand_canyon_5_s30,
0x7705e6: location_name.sand_canyon_5_s31,
0x7705e7: location_name.sand_canyon_5_s32,
0x7705e8: location_name.sand_canyon_5_s33,
0x7705e9: location_name.sand_canyon_5_s34,
0x7705ea: location_name.sand_canyon_5_s35,
0x7705eb: location_name.sand_canyon_5_s36,
0x7705ec: location_name.sand_canyon_5_s37,
0x7705ed: location_name.sand_canyon_5_s38,
0x7705ee: location_name.sand_canyon_5_s39,
0x7705ef: location_name.sand_canyon_5_s40,
0x7705f0: location_name.cloudy_park_1_s1,
0x7705f1: location_name.cloudy_park_1_s2,
0x7705f2: location_name.cloudy_park_1_s3,
0x7705f3: location_name.cloudy_park_1_s4,
0x7705f4: location_name.cloudy_park_1_s5,
0x7705f5: location_name.cloudy_park_1_s6,
0x7705f6: location_name.cloudy_park_1_s7,
0x7705f7: location_name.cloudy_park_1_s8,
0x7705f8: location_name.cloudy_park_1_s9,
0x7705f9: location_name.cloudy_park_1_s10,
0x7705fa: location_name.cloudy_park_1_s11,
0x7705fb: location_name.cloudy_park_1_s12,
0x7705fc: location_name.cloudy_park_1_s13,
0x7705fd: location_name.cloudy_park_1_s14,
0x7705fe: location_name.cloudy_park_1_s15,
0x7705ff: location_name.cloudy_park_1_s16,
0x770600: location_name.cloudy_park_1_s17,
0x770601: location_name.cloudy_park_1_s18,
0x770602: location_name.cloudy_park_1_s19,
0x770603: location_name.cloudy_park_1_s20,
0x770604: location_name.cloudy_park_1_s21,
0x770605: location_name.cloudy_park_1_s22,
0x770606: location_name.cloudy_park_1_s23,
0x770607: location_name.cloudy_park_2_s1,
0x770608: location_name.cloudy_park_2_s2,
0x770609: location_name.cloudy_park_2_s3,
0x77060a: location_name.cloudy_park_2_s4,
0x77060b: location_name.cloudy_park_2_s5,
0x77060c: location_name.cloudy_park_2_s6,
0x77060d: location_name.cloudy_park_2_s7,
0x77060e: location_name.cloudy_park_2_s8,
0x77060f: location_name.cloudy_park_2_s9,
0x770610: location_name.cloudy_park_2_s10,
0x770611: location_name.cloudy_park_2_s11,
0x770612: location_name.cloudy_park_2_s12,
0x770613: location_name.cloudy_park_2_s13,
0x770614: location_name.cloudy_park_2_s14,
0x770615: location_name.cloudy_park_2_s15,
0x770616: location_name.cloudy_park_2_s16,
0x770617: location_name.cloudy_park_2_s17,
0x770618: location_name.cloudy_park_2_s18,
0x770619: location_name.cloudy_park_2_s19,
0x77061a: location_name.cloudy_park_2_s20,
0x77061b: location_name.cloudy_park_2_s21,
0x77061c: location_name.cloudy_park_2_s22,
0x77061d: location_name.cloudy_park_2_s23,
0x77061e: location_name.cloudy_park_2_s24,
0x77061f: location_name.cloudy_park_2_s25,
0x770620: location_name.cloudy_park_2_s26,
0x770621: location_name.cloudy_park_2_s27,
0x770622: location_name.cloudy_park_2_s28,
0x770623: location_name.cloudy_park_2_s29,
0x770624: location_name.cloudy_park_2_s30,
0x770625: location_name.cloudy_park_2_s31,
0x770626: location_name.cloudy_park_2_s32,
0x770627: location_name.cloudy_park_2_s33,
0x770628: location_name.cloudy_park_2_s34,
0x770629: location_name.cloudy_park_2_s35,
0x77062a: location_name.cloudy_park_2_s36,
0x77062b: location_name.cloudy_park_2_s37,
0x77062c: location_name.cloudy_park_2_s38,
0x77062d: location_name.cloudy_park_2_s39,
0x77062e: location_name.cloudy_park_2_s40,
0x77062f: location_name.cloudy_park_2_s41,
0x770630: location_name.cloudy_park_2_s42,
0x770631: location_name.cloudy_park_2_s43,
0x770632: location_name.cloudy_park_2_s44,
0x770633: location_name.cloudy_park_2_s45,
0x770634: location_name.cloudy_park_2_s46,
0x770635: location_name.cloudy_park_2_s47,
0x770636: location_name.cloudy_park_2_s48,
0x770637: location_name.cloudy_park_2_s49,
0x770638: location_name.cloudy_park_2_s50,
0x770639: location_name.cloudy_park_2_s51,
0x77063a: location_name.cloudy_park_2_s52,
0x77063b: location_name.cloudy_park_2_s53,
0x77063c: location_name.cloudy_park_2_s54,
0x77063d: location_name.cloudy_park_3_s1,
0x77063e: location_name.cloudy_park_3_s2,
0x77063f: location_name.cloudy_park_3_s3,
0x770640: location_name.cloudy_park_3_s4,
0x770641: location_name.cloudy_park_3_s5,
0x770642: location_name.cloudy_park_3_s6,
0x770643: location_name.cloudy_park_3_s7,
0x770644: location_name.cloudy_park_3_s8,
0x770645: location_name.cloudy_park_3_s9,
0x770646: location_name.cloudy_park_3_s10,
0x770647: location_name.cloudy_park_3_s11,
0x770648: location_name.cloudy_park_3_s12,
0x770649: location_name.cloudy_park_3_s13,
0x77064a: location_name.cloudy_park_3_s14,
0x77064b: location_name.cloudy_park_3_s15,
0x77064c: location_name.cloudy_park_3_s16,
0x77064d: location_name.cloudy_park_3_s17,
0x77064e: location_name.cloudy_park_3_s18,
0x77064f: location_name.cloudy_park_3_s19,
0x770650: location_name.cloudy_park_3_s20,
0x770651: location_name.cloudy_park_3_s21,
0x770652: location_name.cloudy_park_3_s22,
0x770653: location_name.cloudy_park_4_s1,
0x770654: location_name.cloudy_park_4_s2,
0x770655: location_name.cloudy_park_4_s3,
0x770656: location_name.cloudy_park_4_s4,
0x770657: location_name.cloudy_park_4_s5,
0x770658: location_name.cloudy_park_4_s6,
0x770659: location_name.cloudy_park_4_s7,
0x77065a: location_name.cloudy_park_4_s8,
0x77065b: location_name.cloudy_park_4_s9,
0x77065c: location_name.cloudy_park_4_s10,
0x77065d: location_name.cloudy_park_4_s11,
0x77065e: location_name.cloudy_park_4_s12,
0x77065f: location_name.cloudy_park_4_s13,
0x770660: location_name.cloudy_park_4_s14,
0x770661: location_name.cloudy_park_4_s15,
0x770662: location_name.cloudy_park_4_s16,
0x770663: location_name.cloudy_park_4_s17,
0x770664: location_name.cloudy_park_4_s18,
0x770665: location_name.cloudy_park_4_s19,
0x770666: location_name.cloudy_park_4_s20,
0x770667: location_name.cloudy_park_4_s21,
0x770668: location_name.cloudy_park_4_s22,
0x770669: location_name.cloudy_park_4_s23,
0x77066a: location_name.cloudy_park_4_s24,
0x77066b: location_name.cloudy_park_4_s25,
0x77066c: location_name.cloudy_park_4_s26,
0x77066d: location_name.cloudy_park_4_s27,
0x77066e: location_name.cloudy_park_4_s28,
0x77066f: location_name.cloudy_park_4_s29,
0x770670: location_name.cloudy_park_4_s30,
0x770671: location_name.cloudy_park_4_s31,
0x770672: location_name.cloudy_park_4_s32,
0x770673: location_name.cloudy_park_4_s33,
0x770674: location_name.cloudy_park_4_s34,
0x770675: location_name.cloudy_park_4_s35,
0x770676: location_name.cloudy_park_4_s36,
0x770677: location_name.cloudy_park_4_s37,
0x770678: location_name.cloudy_park_4_s38,
0x770679: location_name.cloudy_park_4_s39,
0x77067a: location_name.cloudy_park_4_s40,
0x77067b: location_name.cloudy_park_4_s41,
0x77067c: location_name.cloudy_park_4_s42,
0x77067d: location_name.cloudy_park_4_s43,
0x77067e: location_name.cloudy_park_4_s44,
0x77067f: location_name.cloudy_park_4_s45,
0x770680: location_name.cloudy_park_4_s46,
0x770681: location_name.cloudy_park_4_s47,
0x770682: location_name.cloudy_park_4_s48,
0x770683: location_name.cloudy_park_4_s49,
0x770684: location_name.cloudy_park_4_s50,
0x770685: location_name.cloudy_park_5_s1,
0x770686: location_name.cloudy_park_5_s2,
0x770687: location_name.cloudy_park_5_s3,
0x770688: location_name.cloudy_park_5_s4,
0x770689: location_name.cloudy_park_5_s5,
0x77068a: location_name.cloudy_park_5_s6,
0x77068b: location_name.cloudy_park_6_s1,
0x77068c: location_name.cloudy_park_6_s2,
0x77068d: location_name.cloudy_park_6_s3,
0x77068e: location_name.cloudy_park_6_s4,
0x77068f: location_name.cloudy_park_6_s5,
0x770690: location_name.cloudy_park_6_s6,
0x770691: location_name.cloudy_park_6_s7,
0x770692: location_name.cloudy_park_6_s8,
0x770693: location_name.cloudy_park_6_s9,
0x770694: location_name.cloudy_park_6_s10,
0x770695: location_name.cloudy_park_6_s11,
0x770696: location_name.cloudy_park_6_s12,
0x770697: location_name.cloudy_park_6_s13,
0x770698: location_name.cloudy_park_6_s14,
0x770699: location_name.cloudy_park_6_s15,
0x77069a: location_name.cloudy_park_6_s16,
0x77069b: location_name.cloudy_park_6_s17,
0x77069c: location_name.cloudy_park_6_s18,
0x77069d: location_name.cloudy_park_6_s19,
0x77069e: location_name.cloudy_park_6_s20,
0x77069f: location_name.cloudy_park_6_s21,
0x7706a0: location_name.cloudy_park_6_s22,
0x7706a1: location_name.cloudy_park_6_s23,
0x7706a2: location_name.cloudy_park_6_s24,
0x7706a3: location_name.cloudy_park_6_s25,
0x7706a4: location_name.cloudy_park_6_s26,
0x7706a5: location_name.cloudy_park_6_s27,
0x7706a6: location_name.cloudy_park_6_s28,
0x7706a7: location_name.cloudy_park_6_s29,
0x7706a8: location_name.cloudy_park_6_s30,
0x7706a9: location_name.cloudy_park_6_s31,
0x7706aa: location_name.cloudy_park_6_s32,
0x7706ab: location_name.cloudy_park_6_s33,
0x7706ac: location_name.iceberg_1_s1,
0x7706ad: location_name.iceberg_1_s2,
0x7706ae: location_name.iceberg_1_s3,
0x7706af: location_name.iceberg_1_s4,
0x7706b0: location_name.iceberg_1_s5,
0x7706b1: location_name.iceberg_1_s6,
0x7706b2: location_name.iceberg_2_s1,
0x7706b3: location_name.iceberg_2_s2,
0x7706b4: location_name.iceberg_2_s3,
0x7706b5: location_name.iceberg_2_s4,
0x7706b6: location_name.iceberg_2_s5,
0x7706b7: location_name.iceberg_2_s6,
0x7706b8: location_name.iceberg_2_s7,
0x7706b9: location_name.iceberg_2_s8,
0x7706ba: location_name.iceberg_2_s9,
0x7706bb: location_name.iceberg_2_s10,
0x7706bc: location_name.iceberg_2_s11,
0x7706bd: location_name.iceberg_2_s12,
0x7706be: location_name.iceberg_2_s13,
0x7706bf: location_name.iceberg_2_s14,
0x7706c0: location_name.iceberg_2_s15,
0x7706c1: location_name.iceberg_2_s16,
0x7706c2: location_name.iceberg_2_s17,
0x7706c3: location_name.iceberg_2_s18,
0x7706c4: location_name.iceberg_2_s19,
0x7706c5: location_name.iceberg_3_s1,
0x7706c6: location_name.iceberg_3_s2,
0x7706c7: location_name.iceberg_3_s3,
0x7706c8: location_name.iceberg_3_s4,
0x7706c9: location_name.iceberg_3_s5,
0x7706ca: location_name.iceberg_3_s6,
0x7706cb: location_name.iceberg_3_s7,
0x7706cc: location_name.iceberg_3_s8,
0x7706cd: location_name.iceberg_3_s9,
0x7706ce: location_name.iceberg_3_s10,
0x7706cf: location_name.iceberg_3_s11,
0x7706d0: location_name.iceberg_3_s12,
0x7706d1: location_name.iceberg_3_s13,
0x7706d2: location_name.iceberg_3_s14,
0x7706d3: location_name.iceberg_3_s15,
0x7706d4: location_name.iceberg_3_s16,
0x7706d5: location_name.iceberg_3_s17,
0x7706d6: location_name.iceberg_3_s18,
0x7706d7: location_name.iceberg_3_s19,
0x7706d8: location_name.iceberg_3_s20,
0x7706d9: location_name.iceberg_3_s21,
0x7706da: location_name.iceberg_4_s1,
0x7706db: location_name.iceberg_4_s2,
0x7706dc: location_name.iceberg_4_s3,
0x7706dd: location_name.iceberg_5_s1,
0x7706de: location_name.iceberg_5_s2,
0x7706df: location_name.iceberg_5_s3,
0x7706e0: location_name.iceberg_5_s4,
0x7706e1: location_name.iceberg_5_s5,
0x7706e2: location_name.iceberg_5_s6,
0x7706e3: location_name.iceberg_5_s7,
0x7706e4: location_name.iceberg_5_s8,
0x7706e5: location_name.iceberg_5_s9,
0x7706e6: location_name.iceberg_5_s10,
0x7706e7: location_name.iceberg_5_s11,
0x7706e8: location_name.iceberg_5_s12,
0x7706e9: location_name.iceberg_5_s13,
0x7706ea: location_name.iceberg_5_s14,
0x7706eb: location_name.iceberg_5_s15,
0x7706ec: location_name.iceberg_5_s16,
0x7706ed: location_name.iceberg_5_s17,
0x7706ee: location_name.iceberg_5_s18,
0x7706ef: location_name.iceberg_5_s19,
0x7706f0: location_name.iceberg_5_s20,
0x7706f1: location_name.iceberg_5_s21,
0x7706f2: location_name.iceberg_5_s22,
0x7706f3: location_name.iceberg_5_s23,
0x7706f4: location_name.iceberg_5_s24,
0x7706f5: location_name.iceberg_5_s25,
0x7706f6: location_name.iceberg_5_s26,
0x7706f7: location_name.iceberg_5_s27,
0x7706f8: location_name.iceberg_5_s28,
0x7706f9: location_name.iceberg_5_s29,
0x7706fa: location_name.iceberg_5_s30,
0x7706fb: location_name.iceberg_5_s31,
0x7706fc: location_name.iceberg_5_s32,
0x7706fd: location_name.iceberg_5_s33,
0x7706fe: location_name.iceberg_5_s34,
0x7706ff: location_name.iceberg_6_s1,
}
location_table = {
**stage_locations,
**heart_star_locations,
**boss_locations,
**consumable_locations,
**star_locations
}

View File

View File

@@ -1,3 +1,5 @@
from typing import List
grass_land_1_a1 = "Grass Land 1 - Animal 1" # Nago
grass_land_1_a2 = "Grass Land 1 - Animal 2" # Rick
grass_land_2_a1 = "Grass Land 2 - Animal 1" # ChuChu
@@ -197,3 +199,12 @@ animal_friend_spawns = {
iceberg_6_a5: "ChuChu Spawn",
iceberg_6_a6: "Nago Spawn",
}
problematic_sets: List[List[str]] = [
# Animal groups that must be guaranteed unique. Potential for softlocks on future-ER if not.
[ripple_field_4_a1, ripple_field_4_a2, ripple_field_4_a3],
[sand_canyon_3_a1, sand_canyon_3_a2, sand_canyon_3_a3],
[cloudy_park_6_a1, cloudy_park_6_a2, cloudy_park_6_a3],
[iceberg_6_a1, iceberg_6_a2, iceberg_6_a3],
[iceberg_6_a4, iceberg_6_a5, iceberg_6_a6]
]

View File

@@ -809,7 +809,7 @@ vanilla_enemies = {'Waddle Dee': 'No Ability',
enemy_restrictive: List[Tuple[List[str], List[str]]] = [
# abilities, enemies, set_all (False to set any)
(["Burning Ability", "Stone Ability"], ["Rocky", "Sparky", "Babut", "Squishy", ]), # Ribbon Field 5 - 7
(["Stone Ability"], ["Rocky", "Sparky", "Babut", "Squishy", ]), # Ribbon Field 5 - 7
# Sand Canyon 6
(["Parasol Ability", "Cutter Ability"], ['Bukiset (Parasol)', 'Bukiset (Cutter)']),
(["Spark Ability", "Clean Ability"], ['Bukiset (Spark)', 'Bukiset (Clean)']),

View File

@@ -1,13 +1,21 @@
import random
from dataclasses import dataclass
from typing import List
from Options import DeathLink, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \
PerGameCommonOptions, PlandoConnections
from .Names import LocationName
from Options import DeathLinkMixin, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \
PerGameCommonOptions, Visibility, NamedRange, OptionGroup, PlandoConnections
from .names import location_name
class RemoteItems(DefaultOnToggle):
"""
Enables receiving items from your own world, primarily for co-op play.
"""
display_name = "Remote Items"
class KDL3PlandoConnections(PlandoConnections):
entrances = exits = {f"{i} {j}" for i in LocationName.level_names for j in range(1, 7)}
entrances = exits = {f"{i} {j}" for i in location_name.level_names for j in range(1, 7)}
class Goal(Choice):
@@ -30,6 +38,7 @@ class Goal(Choice):
return cls.name_lookup[value].upper()
return super().get_option_name(value)
class GoalSpeed(Choice):
"""
Normal: the goal is unlocked after purifying the five bosses
@@ -40,13 +49,14 @@ class GoalSpeed(Choice):
option_fast = 1
class TotalHeartStars(Range):
class MaxHeartStars(Range):
"""
Maximum number of heart stars to include in the pool of items.
If fewer available locations exist in the pool than this number, the number of available locations will be used instead.
"""
display_name = "Max Heart Stars"
range_start = 5 # set to 5 so strict bosses does not degrade
range_end = 50 # 30 default locations + 30 stage clears + 5 bosses - 14 progression items = 51, so round down
range_end = 99 # previously set to 50, set to highest it can be should there be less locations than heart stars
default = 30
@@ -84,9 +94,9 @@ class BossShuffle(PlandoBosses):
Singularity: All (non-Zero) bosses will be replaced with a single boss
Supports plando placement.
"""
bosses = frozenset(LocationName.boss_names.keys())
bosses = frozenset(location_name.boss_names.keys())
locations = frozenset(LocationName.level_names.keys())
locations = frozenset(location_name.level_names.keys())
duplicate_bosses = True
@@ -278,7 +288,8 @@ class KirbyFlavorPreset(Choice):
option_orange = 11
option_lime = 12
option_lavender = 13
option_custom = 14
option_miku = 14
option_custom = 15
default = 0
@classmethod
@@ -296,6 +307,7 @@ class KirbyFlavor(OptionDict):
A custom color for Kirby. To use a custom color, set the preset to Custom and then define a dict of keys from "1" to
"15", with their values being an HTML hex color.
"""
display_name = "Custom Kirby Flavor"
default = {
"1": "B01810",
"2": "F0E0E8",
@@ -313,6 +325,7 @@ class KirbyFlavor(OptionDict):
"14": "F8F8F8",
"15": "B03830",
}
visibility = Visibility.template | Visibility.spoiler # likely never supported on guis
class GooeyFlavorPreset(Choice):
@@ -352,6 +365,7 @@ class GooeyFlavor(OptionDict):
A custom color for Gooey. To use a custom color, set the preset to Custom and then define a dict of keys from "1" to
"15", with their values being an HTML hex color.
"""
display_name = "Custom Gooey Flavor"
default = {
"1": "000808",
"2": "102838",
@@ -363,6 +377,7 @@ class GooeyFlavor(OptionDict):
"8": "D0C0C0",
"9": "F8F8F8",
}
visibility = Visibility.template | Visibility.spoiler # likely never supported on guis
class MusicShuffle(Choice):
@@ -402,14 +417,27 @@ class Gifting(Toggle):
display_name = "Gifting"
class TotalHeartStars(NamedRange):
"""
Deprecated. Use max_heart_stars instead. Supported for only one version.
"""
default = -1
range_start = 5
range_end = 99
special_range_names = {
"default": -1
}
visibility = Visibility.none
@dataclass
class KDL3Options(PerGameCommonOptions):
class KDL3Options(PerGameCommonOptions, DeathLinkMixin):
remote_items: RemoteItems
plando_connections: KDL3PlandoConnections
death_link: DeathLink
game_language: GameLanguage
goal: Goal
goal_speed: GoalSpeed
total_heart_stars: TotalHeartStars
max_heart_stars: MaxHeartStars
heart_stars_required: HeartStarsRequired
filler_percentage: FillerPercentage
trap_percentage: TrapPercentage
@@ -435,3 +463,17 @@ class KDL3Options(PerGameCommonOptions):
gooey_flavor: GooeyFlavor
music_shuffle: MusicShuffle
virtual_console: VirtualConsoleChanges
total_heart_stars: TotalHeartStars # remove in 2 versions
kdl3_option_groups: List[OptionGroup] = [
OptionGroup("Goal Options", [Goal, GoalSpeed, MaxHeartStars, HeartStarsRequired, JumpingTarget, ]),
OptionGroup("World Options", [RemoteItems, StrictBosses, OpenWorld, OpenWorldBossRequirement, ConsumableChecks,
StarChecks, FillerPercentage, TrapPercentage, GooeyTrapPercentage,
SlowTrapPercentage, AbilityTrapPercentage, LevelShuffle, BossShuffle,
AnimalRandomization, CopyAbilityRandomization, BossRequirementRandom,
Gifting, ]),
OptionGroup("Cosmetic Options", [GameLanguage, BossShuffleAllowBB, KirbyFlavorPreset, KirbyFlavor,
GooeyFlavorPreset, GooeyFlavor, MusicShuffle, VirtualConsoleChanges, ]),
]

View File

@@ -25,6 +25,7 @@ all_random = {
"ow_boss_requirement": "random",
"boss_requirement_random": "random",
"consumables": "random",
"starsanity": "random",
"kirby_flavor_preset": "random",
"gooey_flavor_preset": "random",
"music_shuffle": "random",

View File

@@ -1,60 +1,62 @@
import orjson
import os
from pkgutil import get_data
from copy import deepcopy
from typing import TYPE_CHECKING, List, Dict, Optional, Union
from BaseClasses import Region
from typing import TYPE_CHECKING, List, Dict, Optional, Union, Callable
from BaseClasses import Region, CollectionState
from worlds.generic.Rules import add_item_rule
from .Locations import KDL3Location
from .Names import LocationName
from .Options import BossShuffle
from .Room import KDL3Room
from .locations import KDL3Location
from .names import location_name
from .options import BossShuffle
from .room import KDL3Room
if TYPE_CHECKING:
from . import KDL3World
default_levels = {
1: [0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770006, 0x770200],
2: [0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x77000C, 0x770201],
3: [0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770012, 0x770202],
4: [0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770018, 0x770203],
5: [0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x77001E, 0x770204],
1: [0x770000, 0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770200],
2: [0x770006, 0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x770201],
3: [0x77000C, 0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770202],
4: [0x770012, 0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770203],
5: [0x770018, 0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x770204],
}
first_stage_blacklist = {
# We want to confirm that the first stage can be completed without any items
0x77000B, # 2-5 needs Kine
0x770011, # 3-5 needs Cutter
0x77001C, # 5-4 needs Burning
0x77000A, # 2-5 needs Kine
0x770010, # 3-5 needs Cutter
0x77001B, # 5-4 needs Burning
}
first_world_limit = {
# We need to limit the number of very restrictive stages in level 1 on solo gens
*first_stage_blacklist, # all three of the blacklist stages need 2+ items for both checks
0x770006,
0x770007,
0x770008,
0x770013,
0x77001E,
0x770012,
0x77001D,
}
def generate_valid_level(world: "KDL3World", level: int, stage: int,
possible_stages: List[int], placed_stages: List[int]):
possible_stages: List[int], placed_stages: List[Optional[int]]) -> int:
new_stage = world.random.choice(possible_stages)
if level == 1:
if stage == 0 and new_stage in first_stage_blacklist:
possible_stages.remove(new_stage)
return generate_valid_level(world, level, stage, possible_stages, placed_stages)
elif (not (world.multiworld.players > 1 or world.options.consumables or world.options.starsanity) and
new_stage in first_world_limit and
sum(p_stage in first_world_limit for p_stage in placed_stages)
new_stage in first_world_limit and
sum(p_stage in first_world_limit for p_stage in placed_stages)
>= (2 if world.options.open_world else 1)):
return generate_valid_level(world, level, stage, possible_stages, placed_stages)
return new_stage
def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]):
level_names = {LocationName.level_names[level]: level for level in LocationName.level_names}
def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]) -> None:
level_names = {location_name.level_names[level]: level for level in location_name.level_names}
room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json")))
rooms: Dict[str, KDL3Room] = dict()
for room_entry in room_data:
@@ -63,7 +65,7 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]):
room_entry["default_exits"], room_entry["animal_pointers"], room_entry["enemies"],
room_entry["entity_load"], room_entry["consumables"], room_entry["consumables_pointer"])
room.add_locations({location: world.location_name_to_id[location] if location in world.location_name_to_id else
None for location in room_entry["locations"]
None for location in room_entry["locations"]
if (not any(x in location for x in ["1-Up", "Maxim"]) or
world.options.consumables.value) and ("Star" not in location
or world.options.starsanity.value)},
@@ -83,8 +85,8 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]):
if room.stage == 7:
first_rooms[0x770200 + room.level - 1] = room
else:
first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room
exits = dict()
first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage - 1] = room
exits: Dict[str, Callable[[CollectionState], bool]] = dict()
for def_exit in room.default_exits:
target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}"
access_rule = tuple(def_exit["access_rule"])
@@ -115,50 +117,54 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]):
if world.options.open_world:
level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name])
else:
world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player)\
world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player) \
.parent_region.add_exits([first_rooms[0x770200 + level - 1].name])
def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_pattern: bool) -> dict:
levels: Dict[int, List[Optional[int]]] = {
1: [None] * 7,
2: [None] * 7,
3: [None] * 7,
4: [None] * 7,
5: [None] * 7,
}
def generate_valid_levels(world: "KDL3World", shuffle_mode: int) -> Dict[int, List[int]]:
if shuffle_mode:
levels: Dict[int, List[Optional[int]]] = {
1: [None] * 7,
2: [None] * 7,
3: [None] * 7,
4: [None] * 7,
5: [None] * 7,
}
possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)]
if world.options.plando_connections:
for connection in world.options.plando_connections:
try:
entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1)
stage_world, stage_stage = connection.exit.rsplit(" ", 1)
new_stage = default_levels[LocationName.level_names[stage_world.strip()]][int(stage_stage) - 1]
levels[LocationName.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage
possible_stages.remove(new_stage)
possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)]
if world.options.plando_connections:
for connection in world.options.plando_connections:
try:
entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1)
stage_world, stage_stage = connection.exit.rsplit(" ", 1)
new_stage = default_levels[location_name.level_names[stage_world.strip()]][int(stage_stage) - 1]
levels[location_name.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage
possible_stages.remove(new_stage)
except Exception:
raise Exception(
f"Invalid connection: {connection.entrance} =>"
f" {connection.exit} for player {world.player} ({world.player_name})")
except Exception:
raise Exception(
f"Invalid connection: {connection.entrance} =>"
f" {connection.exit} for player {world.player} ({world.player_name})")
for level in range(1, 6):
for stage in range(6):
# Randomize bosses separately
try:
for level in range(1, 6):
for stage in range(6):
# Randomize bosses separately
if levels[level][stage] is None:
stage_candidates = [candidate for candidate in possible_stages
if (enforce_world and candidate in default_levels[level])
or (enforce_pattern and ((candidate - 1) & 0x00FFFF) % 6 == stage)
or (enforce_pattern == enforce_world)
if (shuffle_mode == 1 and candidate in default_levels[level])
or (shuffle_mode == 2 and (candidate & 0x00FFFF) % 6 == stage)
or (shuffle_mode == 3)
]
if not stage_candidates:
raise Exception(
f"Failed to find valid stage for {level}-{stage}. Remaining Stages:{possible_stages}")
new_stage = generate_valid_level(world, level, stage, stage_candidates, levels[level])
possible_stages.remove(new_stage)
levels[level][stage] = new_stage
except Exception:
raise Exception(f"Failed to find valid stage for {level}-{stage}. Remaining Stages:{possible_stages}")
else:
levels = deepcopy(default_levels)
for level in levels:
levels[level][6] = None
# now handle bosses
boss_shuffle: Union[int, str] = world.options.boss_shuffle.value
plando_bosses = []
@@ -168,17 +174,17 @@ def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_patte
boss_shuffle = BossShuffle.options[options.pop()]
for option in options:
if "-" in option:
loc, boss = option.split("-")
loc, plando_boss = option.split("-")
loc = loc.title()
boss = boss.title()
levels[LocationName.level_names[loc]][6] = LocationName.boss_names[boss]
plando_bosses.append(LocationName.boss_names[boss])
plando_boss = plando_boss.title()
levels[location_name.level_names[loc]][6] = location_name.boss_names[plando_boss]
plando_bosses.append(location_name.boss_names[plando_boss])
else:
option = option.title()
for level in levels:
if levels[level][6] is None:
levels[level][6] = LocationName.boss_names[option]
plando_bosses.append(LocationName.boss_names[option])
levels[level][6] = location_name.boss_names[option]
plando_bosses.append(location_name.boss_names[option])
if boss_shuffle > 0:
if boss_shuffle == BossShuffle.option_full:
@@ -223,15 +229,14 @@ def create_levels(world: "KDL3World") -> None:
5: level5,
}
level_shuffle = world.options.stage_shuffle.value
if level_shuffle != 0:
world.player_levels = generate_valid_levels(
world,
level_shuffle == 1,
level_shuffle == 2)
if hasattr(world.multiworld, "re_gen_passthrough"):
world.player_levels = getattr(world.multiworld, "re_gen_passthrough")["Kirby's Dream Land 3"]["player_levels"]
else:
world.player_levels = generate_valid_levels(world, level_shuffle)
generate_rooms(world, levels)
level6.add_locations({LocationName.goals[world.options.goal]: None}, KDL3Location)
level6.add_locations({location_name.goals[world.options.goal.value]: None}, KDL3Location)
menu.connect(level1, "Start Game")
level1.connect(level2, "To Level 2")

602
worlds/kdl3/rom.py Normal file
View File

@@ -0,0 +1,602 @@
import typing
from pkgutil import get_data
import Utils
from typing import Optional, TYPE_CHECKING, Tuple, Dict, List
import hashlib
import os
import struct
import settings
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension
from .aesthetics import get_palette_bytes, kirby_target_palettes, get_kirby_palette, gooey_target_palettes, \
get_gooey_palette
from .compression import hal_decompress
import bsdiff4
if TYPE_CHECKING:
from . import KDL3World
KDL3UHASH = "201e7658f6194458a3869dde36bf8ec2"
KDL3JHASH = "b2f2d004ea640c3db66df958fce122b2"
level_pointers = {
0x770000: 0x0084,
0x770001: 0x009C,
0x770002: 0x00B8,
0x770003: 0x00D8,
0x770004: 0x0104,
0x770005: 0x0124,
0x770006: 0x014C,
0x770007: 0x0170,
0x770008: 0x0190,
0x770009: 0x01B0,
0x77000A: 0x01E8,
0x77000B: 0x0218,
0x77000C: 0x024C,
0x77000D: 0x0270,
0x77000E: 0x02A0,
0x77000F: 0x02C4,
0x770010: 0x02EC,
0x770011: 0x0314,
0x770012: 0x03CC,
0x770013: 0x0404,
0x770014: 0x042C,
0x770015: 0x044C,
0x770016: 0x0478,
0x770017: 0x049C,
0x770018: 0x04E4,
0x770019: 0x0504,
0x77001A: 0x0530,
0x77001B: 0x0554,
0x77001C: 0x05A8,
0x77001D: 0x0640,
0x770200: 0x0148,
0x770201: 0x0248,
0x770202: 0x03C8,
0x770203: 0x04E0,
0x770204: 0x06A4,
0x770205: 0x06A8,
}
bb_bosses = {
0x770200: 0xED85F1,
0x770201: 0xF01360,
0x770202: 0xEDA3DF,
0x770203: 0xEDC2B9,
0x770204: 0xED7C3F,
0x770205: 0xEC29D2,
}
level_sprites = {
0x19B2C6: 1827,
0x1A195C: 1584,
0x19F6F3: 1679,
0x19DC8B: 1717,
0x197900: 1872
}
stage_tiles = {
0: [
0, 1, 2,
16, 17, 18,
32, 33, 34,
48, 49, 50
],
1: [
3, 4, 5,
19, 20, 21,
35, 36, 37,
51, 52, 53
],
2: [
6, 7, 8,
22, 23, 24,
38, 39, 40,
54, 55, 56
],
3: [
9, 10, 11,
25, 26, 27,
41, 42, 43,
57, 58, 59,
],
4: [
12, 13, 64,
28, 29, 65,
44, 45, 66,
60, 61, 67
],
5: [
14, 15, 68,
30, 31, 69,
46, 47, 70,
62, 63, 71
]
}
heart_star_address = 0x2D0000
heart_star_size = 456
consumable_address = 0x2F91DD
consumable_size = 698
stage_palettes = [0x60964, 0x60B64, 0x60D64, 0x60F64, 0x61164]
music_choices = [
2, # Boss 1
3, # Boss 2 (Unused)
4, # Boss 3 (Miniboss)
7, # Dedede
9, # Event 2 (used once)
10, # Field 1
11, # Field 2
12, # Field 3
13, # Field 4
14, # Field 5
15, # Field 6
16, # Field 7
17, # Field 8
18, # Field 9
19, # Field 10
20, # Field 11
21, # Field 12 (Gourmet Race)
23, # Dark Matter in the Hyper Zone
24, # Zero
25, # Level 1
26, # Level 2
27, # Level 4
28, # Level 3
29, # Heart Star Failed
30, # Level 5
31, # Minigame
38, # Animal Friend 1
39, # Animal Friend 2
40, # Animal Friend 3
]
# extra room pointers we don't want to track other than for music
room_music = {
3079990: 23, # Zero
2983409: 2, # BB Whispy
3150688: 2, # BB Acro
2991071: 2, # BB PonCon
2998969: 2, # BB Ado
2980927: 7, # BB Dedede
2894290: 23 # BB Zero
}
enemy_remap = {
"Waddle Dee": 0,
"Bronto Burt": 2,
"Rocky": 3,
"Bobo": 5,
"Chilly": 6,
"Poppy Bros Jr.": 7,
"Sparky": 8,
"Polof": 9,
"Broom Hatter": 11,
"Cappy": 12,
"Bouncy": 13,
"Nruff": 15,
"Glunk": 16,
"Togezo": 18,
"Kabu": 19,
"Mony": 20,
"Blipper": 21,
"Squishy": 22,
"Gabon": 24,
"Oro": 25,
"Galbo": 26,
"Sir Kibble": 27,
"Nidoo": 28,
"Kany": 29,
"Sasuke": 30,
"Yaban": 32,
"Boten": 33,
"Coconut": 34,
"Doka": 35,
"Icicle": 36,
"Pteran": 39,
"Loud": 40,
"Como": 41,
"Klinko": 42,
"Babut": 43,
"Wappa": 44,
"Mariel": 45,
"Tick": 48,
"Apolo": 49,
"Popon Ball": 50,
"KeKe": 51,
"Magoo": 53,
"Raft Waddle Dee": 57,
"Madoo": 58,
"Corori": 60,
"Kapar": 67,
"Batamon": 68,
"Peran": 72,
"Bobin": 73,
"Mopoo": 74,
"Gansan": 75,
"Bukiset (Burning)": 76,
"Bukiset (Stone)": 77,
"Bukiset (Ice)": 78,
"Bukiset (Needle)": 79,
"Bukiset (Clean)": 80,
"Bukiset (Parasol)": 81,
"Bukiset (Spark)": 82,
"Bukiset (Cutter)": 83,
"Waddle Dee Drawing": 84,
"Bronto Burt Drawing": 85,
"Bouncy Drawing": 86,
"Kabu (Dekabu)": 87,
"Wapod": 88,
"Propeller": 89,
"Dogon": 90,
"Joe": 91
}
miniboss_remap = {
"Captain Stitch": 0,
"Yuki": 1,
"Blocky": 2,
"Jumper Shoot": 3,
"Boboo": 4,
"Haboki": 5
}
ability_remap = {
"No Ability": 0,
"Burning Ability": 1,
"Stone Ability": 2,
"Ice Ability": 3,
"Needle Ability": 4,
"Clean Ability": 5,
"Parasol Ability": 6,
"Spark Ability": 7,
"Cutter Ability": 8,
}
class RomData:
def __init__(self, file: bytes, name: typing.Optional[str] = None):
self.file = bytearray(file)
self.name = name
def read_byte(self, offset: int) -> int:
return self.file[offset]
def read_bytes(self, offset: int, length: int) -> bytearray:
return self.file[offset:offset + length]
def write_byte(self, offset: int, value: int) -> None:
self.file[offset] = value
def write_bytes(self, offset: int, values: typing.Sequence[int]) -> None:
self.file[offset:offset + len(values)] = values
def get_bytes(self) -> bytes:
return bytes(self.file)
def handle_level_sprites(stages: List[Tuple[int, ...]], sprites: List[bytearray], palettes: List[List[bytearray]]) \
-> Tuple[List[bytearray], List[bytearray]]:
palette_by_level = list()
for palette in palettes:
palette_by_level.extend(palette[10:16])
out_palettes = list()
for i in range(5):
for j in range(6):
palettes[i][10 + j] = palette_by_level[stages[i][j]]
out_palettes.append(bytearray([x for palette in palettes[i] for x in palette]))
tiles_by_level = list()
for spritesheet in sprites:
decompressed = hal_decompress(spritesheet)
tiles = [decompressed[i:i + 32] for i in range(0, 2304, 32)]
tiles_by_level.extend([[tiles[x] for x in stage_tiles[stage]] for stage in stage_tiles])
out_sprites = list()
for world in range(5):
levels = [stages[world][x] for x in range(6)]
world_tiles: typing.List[bytes] = [bytes() for _ in range(72)]
for i in range(6):
for x in range(12):
world_tiles[stage_tiles[i][x]] = tiles_by_level[levels[i]][x]
out_sprites.append(bytearray())
for tile in world_tiles:
out_sprites[world].extend(tile)
# insert our fake compression
out_sprites[world][0:0] = [0xe3, 0xff]
out_sprites[world][1026:1026] = [0xe3, 0xff]
out_sprites[world][2052:2052] = [0xe0, 0xff]
out_sprites[world].append(0xff)
return out_sprites, out_palettes
def write_heart_star_sprites(rom: RomData) -> None:
compressed = rom.read_bytes(heart_star_address, heart_star_size)
decompressed = hal_decompress(compressed)
patch = get_data(__name__, os.path.join("data", "APHeartStar.bsdiff4"))
patched = bytearray(bsdiff4.patch(decompressed, patch))
rom.write_bytes(0x1AF7DF, patched)
patched[0:0] = [0xE3, 0xFF]
patched.append(0xFF)
rom.write_bytes(0x1CD000, patched)
rom.write_bytes(0x3F0EBF, [0x00, 0xD0, 0x39])
def write_consumable_sprites(rom: RomData, consumables: bool, stars: bool) -> None:
compressed = rom.read_bytes(consumable_address, consumable_size)
decompressed = hal_decompress(compressed)
patched = bytearray(decompressed)
if consumables:
patch = get_data(__name__, os.path.join("data", "APConsumable.bsdiff4"))
patched = bytearray(bsdiff4.patch(bytes(patched), patch))
if stars:
patch = get_data(__name__, os.path.join("data", "APStars.bsdiff4"))
patched = bytearray(bsdiff4.patch(bytes(patched), patch))
patched[0:0] = [0xE3, 0xFF]
patched.append(0xFF)
rom.write_bytes(0x1CD500, patched)
rom.write_bytes(0x3F0DAE, [0x00, 0xD5, 0x39])
class KDL3PatchExtensions(APPatchExtension):
game = "Kirby's Dream Land 3"
@staticmethod
def apply_post_patch(_: APProcedurePatch, rom: bytes) -> bytes:
rom_data = RomData(rom)
write_heart_star_sprites(rom_data)
if rom_data.read_bytes(0x3D014, 1)[0] > 0:
stages = [struct.unpack("HHHHHHH", rom_data.read_bytes(0x3D020 + x * 14, 14)) for x in range(5)]
palettes = [rom_data.read_bytes(full_pal, 512) for full_pal in stage_palettes]
read_palettes = [[palette[i:i + 32] for i in range(0, 512, 32)] for palette in palettes]
sprites = [rom_data.read_bytes(offset, level_sprites[offset]) for offset in level_sprites]
sprites, palettes = handle_level_sprites(stages, sprites, read_palettes)
for addr, palette in zip(stage_palettes, palettes):
rom_data.write_bytes(addr, palette)
for addr, level_sprite in zip([0x1CA000, 0x1CA920, 0x1CB230, 0x1CBB40, 0x1CC450], sprites):
rom_data.write_bytes(addr, level_sprite)
rom_data.write_bytes(0x460A, [0x00, 0xA0, 0x39, 0x20, 0xA9, 0x39, 0x30, 0xB2, 0x39, 0x40, 0xBB, 0x39,
0x50, 0xC4, 0x39])
write_consumable_sprites(rom_data, rom_data.read_byte(0x3D018) > 0, rom_data.read_byte(0x3D01A) > 0)
return rom_data.get_bytes()
class KDL3ProcedurePatch(APProcedurePatch, APTokenMixin):
hash = [KDL3UHASH, KDL3JHASH]
game = "Kirby's Dream Land 3"
patch_file_ending = ".apkdl3"
procedure = [
("apply_bsdiff4", ["kdl3_basepatch.bsdiff4"]),
("apply_tokens", ["token_patch.bin"]),
("apply_post_patch", []),
("calc_snes_crc", [])
]
name: bytes # used to pass to __init__
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes()
def patch_rom(world: "KDL3World", patch: KDL3ProcedurePatch) -> None:
patch.write_file("kdl3_basepatch.bsdiff4",
get_data(__name__, os.path.join("data", "kdl3_basepatch.bsdiff4")))
# Write open world patch
if world.options.open_world:
patch.write_token(APTokenTypes.WRITE, 0x143C7, bytes([0xAD, 0xC1, 0x5A, 0xCD, 0xC1, 0x5A, ]))
# changes the stage flag function to compare $5AC1 to $5AC1,
# always running the "new stage" function
# This has further checks present for bosses already, so we just
# need to handle regular stages
# write check for boss to be unlocked
if world.options.consumables:
# reroute maxim tomatoes to use the 1-UP function, then null out the function
patch.write_token(APTokenTypes.WRITE, 0x3002F, bytes([0x37, 0x00]))
patch.write_token(APTokenTypes.WRITE, 0x30037, bytes([0xA9, 0x26, 0x00, # LDA #$0026
0x22, 0x27, 0xD9, 0x00, # JSL $00D927
0xA4, 0xD2, # LDY $D2
0x6B, # RTL
0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA,
0xEA, # NOP #10
]))
# stars handling is built into the rom, so no changes there
rooms = world.rooms
if world.options.music_shuffle > 0:
if world.options.music_shuffle == 1:
shuffled_music = music_choices.copy()
world.random.shuffle(shuffled_music)
music_map = dict(zip(music_choices, shuffled_music))
# Avoid putting star twinkle in the pool
music_map[5] = world.random.choice(music_choices)
# Heart Star music doesn't work on regular stages
music_map[8] = world.random.choice(music_choices)
for room in rooms:
room.music = music_map[room.music]
for room_ptr in room_music:
patch.write_token(APTokenTypes.WRITE, room_ptr + 2, bytes([music_map[room_music[room_ptr]]]))
for i, old_music in zip(range(5), [25, 26, 28, 27, 30]):
# level themes
patch.write_token(APTokenTypes.WRITE, 0x133F2 + i, bytes([music_map[old_music]]))
# Zero
patch.write_token(APTokenTypes.WRITE, 0x9AE79, music_map[0x18].to_bytes(1, "little"))
# Heart Star success and fail
patch.write_token(APTokenTypes.WRITE, 0x4A388, music_map[0x08].to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x4A38D, music_map[0x1D].to_bytes(1, "little"))
elif world.options.music_shuffle == 2:
for room in rooms:
room.music = world.random.choice(music_choices)
for room_ptr in room_music:
patch.write_token(APTokenTypes.WRITE, room_ptr + 2,
world.random.choice(music_choices).to_bytes(1, "little"))
for i in range(5):
# level themes
patch.write_token(APTokenTypes.WRITE, 0x133F2 + i,
world.random.choice(music_choices).to_bytes(1, "little"))
# Zero
patch.write_token(APTokenTypes.WRITE, 0x9AE79, world.random.choice(music_choices).to_bytes(1, "little"))
# Heart Star success and fail
patch.write_token(APTokenTypes.WRITE, 0x4A388, world.random.choice(music_choices).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x4A38D, world.random.choice(music_choices).to_bytes(1, "little"))
for room in rooms:
room.patch(patch, bool(world.options.consumables.value), not bool(world.options.remote_items.value))
if world.options.virtual_console in [1, 3]:
# Flash Reduction
patch.write_token(APTokenTypes.WRITE, 0x9AE68, b"\x10")
patch.write_token(APTokenTypes.WRITE, 0x9AE8E, bytes([0x08, 0x00, 0x22, 0x5D, 0xF7, 0x00, 0xA2, 0x08, ]))
patch.write_token(APTokenTypes.WRITE, 0x9AEA1, b"\x08")
patch.write_token(APTokenTypes.WRITE, 0x9AEC9, b"\x01")
patch.write_token(APTokenTypes.WRITE, 0x9AED2, bytes([0xA9, 0x1F]))
patch.write_token(APTokenTypes.WRITE, 0x9AEE1, b"\x08")
if world.options.virtual_console in [2, 3]:
# Hyper Zone BB colors
patch.write_token(APTokenTypes.WRITE, 0x2C5E16, bytes([0xEE, 0x1B, 0x18, 0x5B, 0xD3, 0x4A, 0xF4, 0x3B, ]))
patch.write_token(APTokenTypes.WRITE, 0x2C8217, bytes([0xFF, 0x1E, ]))
# boss requirements
patch.write_token(APTokenTypes.WRITE, 0x3D000,
struct.pack("HHHHH", world.boss_requirements[0], world.boss_requirements[1],
world.boss_requirements[2], world.boss_requirements[3],
world.boss_requirements[4]))
patch.write_token(APTokenTypes.WRITE, 0x3D00A,
struct.pack("H", world.required_heart_stars if world.options.goal_speed == 1 else 0xFFFF))
patch.write_token(APTokenTypes.WRITE, 0x3D00C, world.options.goal_speed.value.to_bytes(2, "little"))
patch.write_token(APTokenTypes.WRITE, 0x3D00E, world.options.open_world.value.to_bytes(2, "little"))
patch.write_token(APTokenTypes.WRITE, 0x3D010, ((world.options.remote_items.value << 1) +
world.options.death_link.value).to_bytes(2, "little"))
patch.write_token(APTokenTypes.WRITE, 0x3D012, world.options.goal.value.to_bytes(2, "little"))
patch.write_token(APTokenTypes.WRITE, 0x3D014, world.options.stage_shuffle.value.to_bytes(2, "little"))
patch.write_token(APTokenTypes.WRITE, 0x3D016, world.options.ow_boss_requirement.value.to_bytes(2, "little"))
patch.write_token(APTokenTypes.WRITE, 0x3D018, world.options.consumables.value.to_bytes(2, "little"))
patch.write_token(APTokenTypes.WRITE, 0x3D01A, world.options.starsanity.value.to_bytes(2, "little"))
patch.write_token(APTokenTypes.WRITE, 0x3D01C, world.options.gifting.value.to_bytes(2, "little")
if world.multiworld.players > 1 else bytes([0, 0]))
patch.write_token(APTokenTypes.WRITE, 0x3D01E, world.options.strict_bosses.value.to_bytes(2, "little"))
# don't write gifting for solo game, since there's no one to send anything to
for level in world.player_levels:
for i in range(len(world.player_levels[level])):
patch.write_token(APTokenTypes.WRITE, 0x3F002E + ((level - 1) * 14) + (i * 2),
struct.pack("H", level_pointers[world.player_levels[level][i]]))
patch.write_token(APTokenTypes.WRITE, 0x3D020 + (level - 1) * 14 + (i * 2),
struct.pack("H", world.player_levels[level][i] & 0x00FFFF))
if (i == 0) or (i > 0 and i % 6 != 0):
patch.write_token(APTokenTypes.WRITE, 0x3D080 + (level - 1) * 12 + (i * 2),
struct.pack("H", (world.player_levels[level][i] & 0x00FFFF) % 6))
for i in range(6):
if world.boss_butch_bosses[i]:
patch.write_token(APTokenTypes.WRITE, 0x3F0000 + (level_pointers[0x770200 + i]),
struct.pack("I", bb_bosses[0x770200 + i]))
# copy ability shuffle
if world.options.copy_ability_randomization.value > 0:
for enemy in world.copy_abilities:
if enemy in miniboss_remap:
patch.write_token(APTokenTypes.WRITE, 0xB417E + (miniboss_remap[enemy] << 1),
struct.pack("H", ability_remap[world.copy_abilities[enemy]]))
else:
patch.write_token(APTokenTypes.WRITE, 0xB3CAC + (enemy_remap[enemy] << 1),
struct.pack("H", ability_remap[world.copy_abilities[enemy]]))
# following only needs done on non-door rando
# incredibly lucky this follows the same order (including 5E == star block)
patch.write_token(APTokenTypes.WRITE, 0x2F77EA,
(0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2F7811,
(0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2F9BC4,
(0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2F9BEB,
(0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2FAC06,
(0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2FAC2D,
(0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2F9E7B,
(0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2F9EA2,
(0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2FA951,
(0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2FA978,
(0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2FA132,
(0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2FA159,
(0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2FA3E8,
(0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2FA40F,
(0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2F90E2,
(0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)).to_bytes(1, "little"))
patch.write_token(APTokenTypes.WRITE, 0x2F9109,
(0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)).to_bytes(1, "little"))
if world.options.copy_ability_randomization == 2:
for enemy in enemy_remap:
# we just won't include it for minibosses
patch.write_token(APTokenTypes.WRITE, 0xB3E40 + (enemy_remap[enemy] << 1),
struct.pack("h", world.random.randint(-1, 2)))
# write jumping goal
patch.write_token(APTokenTypes.WRITE, 0x94F8, struct.pack("H", world.options.jumping_target))
patch.write_token(APTokenTypes.WRITE, 0x944E, struct.pack("H", world.options.jumping_target))
from Utils import __version__
patch_name = bytearray(
f'KDL3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21]
patch_name.extend([0] * (21 - len(patch_name)))
patch.name = bytes(patch_name)
patch.write_token(APTokenTypes.WRITE, 0x3C000, patch.name)
patch.write_token(APTokenTypes.WRITE, 0x3C020, world.options.game_language.value.to_bytes(1, "little"))
patch.write_token(APTokenTypes.COPY, 0x7FC0, (21, 0x3C000))
patch.write_token(APTokenTypes.COPY, 0x7FD9, (1, 0x3C020))
# handle palette
if world.options.kirby_flavor_preset.value != 0:
for addr in kirby_target_palettes:
target = kirby_target_palettes[addr]
palette = get_kirby_palette(world)
if palette is not None:
patch.write_token(APTokenTypes.WRITE, addr, get_palette_bytes(palette, target[0], target[1], target[2]))
if world.options.gooey_flavor_preset.value != 0:
for addr in gooey_target_palettes:
target = gooey_target_palettes[addr]
palette = get_gooey_palette(world)
if palette is not None:
patch.write_token(APTokenTypes.WRITE, addr, get_palette_bytes(palette, target[0], target[1], target[2]))
patch.write_file("token_patch.bin", patch.get_token_binary())
def get_base_rom_bytes() -> bytes:
rom_file: str = get_base_rom_path()
base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:
base_rom_bytes = bytes(Utils.read_snes_rom(open(rom_file, "rb")))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if basemd5.hexdigest() not in {KDL3UHASH, KDL3JHASH}:
raise Exception("Supplied Base Rom does not match known MD5 for US or JP release. "
"Get the correct game and version, then dump it")
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
return base_rom_bytes
def get_base_rom_path(file_name: str = "") -> str:
options: settings.Settings = settings.get_settings()
if not file_name:
file_name = options["kdl3_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.user_path(file_name)
return file_name

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