Compare commits

..

117 Commits

Author SHA1 Message Date
NewSoupVi
930f627794 Release workflow as well 2025-03-10 14:32:09 +01:00
NewSoupVi
d4fc90410c Update build.yml 2025-03-10 14:09:20 +01:00
NewSoupVi
484c5f2671 Band-aid Linux Build breaking with the release of PyGObject 3.52.1 2025-03-10 14:06:59 +01:00
Silvris
04771fa4f0 Core: fix pickling plando texts (#4711) 2025-03-09 20:00:00 +01:00
jamesbrq
2639796255 MLSS: Add new goal + Update basepatch to standalone equivalent (#4409)
* Item groups + small changes

* Add alternate goal

* New Locations and Logic Updates + Basepatch

* Update basepatch.bsdiff

* Update Basepatch

* Update basepatch.bsdiff

* Update bowsers castle logic with emblem hunt

* Update Archipelago Unittests.run.xml

* Update Archipelago Unittests.run.xml

* Fix for overlapping ROM addresses

* Update Rom.py

* Update __init__.py

* Update basepatch.bsdiff

* Update Rom.py

* Update client with new helper function

* Update basepatch.bsdiff

* Update worlds/mlss/__init__.py

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

* Update worlds/mlss/__init__.py

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

* Review Refactor

* Review Refactor

---------

Co-authored-by: qwint <qwint.42@gmail.com>
2025-03-09 11:37:15 -04:00
Jérémie Bolduc
4ebabc1208 Stardew Valley: Move filler pool generation out of the world class (#4372)
* merge group options so specific handling is not needed when generating filler pool

* fix

* remove unneeded imports

* self review

* remove unneeded imports

* looks like typing was missing woopsi
2025-03-08 12:13:33 -05:00
josephwhite
ce34b60712 Super Mario 64: ItemData class and tables (#4321)
* sm64ex: use item data class

* rearrange imports

* Dict to dict

* remove optional typing

* bonus item descriptions since we can also add stuff for webworld easily

* remove item descriptions (rip) and decrease verbosity for classifications

* formatting
2025-03-08 12:07:50 -05:00
Trevor L
54094c6331 Blasphemous: Restrict right half of map start locations to hard difficulty only (#4002)
* Start locations, location name

* Fix tests
2025-03-08 11:59:35 -05:00
Bryce Wilson
3986f6f11a Pokemon Emerald: Randomize rock smash encounters (#3912)
* Pokemon Emerald: WIP add rock smash encounter randomization

* Pokemon Emerald: Refactor encounter data on maps

* Pokemon Emerald: Remove unused import

* Pokemon Emerald: Swap StrEnum for regular Enum and use .value
2025-03-08 11:57:16 -05:00
sgrunt
5662da6f7d Timespinner: Support new flags and settings from the randomizer (#4559)
* Timespinner: Add "no hell spiders" enemy rando option that is present in upstream settings

* Timespinner: Prism Break support tweaks (including tracker support)

* Timespinner: Add support for upstream Lock Key Amadeus flag

* Timespinner: Add support for upstream Risky Warps flag

* Timespinner: Add support for upstream Pyramid Start flag

* Timespinner: fix error in lab connectivity logic

* Timespinner: use has_all to simplify one check

Per PR suggestion.

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

* Timespinner: fix apparent logic error inherited from in-rando logic

* Timespinner: adjust "Origins" location logic slightly further to account for a Risky Warps case

* Timespinner: remove the backward compat options for the recent flag additions

* Timespinner: add newly added Gate Keep option from rando

* Timespinner: adjust the laser access colours in the tracker

* Timespinner: fix an item description in the tracker

* Timespinner: based on testing feedback, put Laser Access items in their own category

* Timespinner: add support for new upstream flag Royal Roadblock

* Timespinner: also ensure the new flag gets put in slot data

* Timespinner: fix bug in universal tracker support indicating castle basement is accessible at the lower Rising Tides flooding level

* Timespinner: exclude Talaria Attachment and Timespinner Wheel from pyramid start starter progression items

* Timespinner: fix region logic for the left pyramid warp

* Timespinner: fix main Gyre access logic when Risky Warps warps you behind the lasers

* Timespinner: apply suggested spacing fix

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

---------

Co-authored-by: sgrunt <sgrunt1987@gmail.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-03-08 11:54:23 -05:00
Scipio Wright
33a75fb2cb TUNIC: Breakable Shuffle (#4489)
* Starting out

* Rules for breakable regions

* make the rest of it work, it's pr ready, boom

* Make it work in not pot shuffle

* Fix after merge

* Fix item id overlap

* Move breakable, grass, and local fill options in yaml

* Fix groups getting overwritten

* Rename, add new breakables

* Rename more stuff

* Time to rename them again

* Make it actually default for breakable shuffle

* Burn the signs down

* Fix west courtyard pot regions

* Fix fortress courtyard and beneath the fortress loc groups again

* More missing loc group conversions

* Replace instances of world.player with player, same for multiworld

* Update worlds/tunic/__init__.py

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

* Remove unused import
2025-03-08 11:25:47 -05:00
Jérémie Bolduc
ee9bcb84b7 Stardew Valley: Move progressive tool options handling in features (#4374)
* create tool progression feature and unwrap option

* replace option usage with calling feature

* add comment explaining why some logic is a weird place

* replace item creation logic with feature

* self review and add unit tests

* rename test cuz I named them too long

* add a test for the trash can useful stuff cuz I thought there was a bug but turns out it works

* self review again

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

* damn it 3.11 why are you like this

* use blacksmith region when checking vanilla tools

* fix rule

* move can mine using in tool logic

* remove changes to performance test

* properly set the option I guess

* properly set options 2

* that's what happen when you code too late
2025-03-08 11:19:29 -05:00
Kaito Sinclaire
b5269e9aa4 id Tech Games: Customizable ammo capacity (#3565)
* Doom, Doom 2, Heretic: customizable ammo capacity

* Do not progression balance capacity up items

* Prog fill still doesn't agree, just go with our original idea

* Clean up the new options a bit

- Gave all options a consistent and easily readable naming scheme
  (`max_ammo_<type>` and `added_ammo_<type>`)
- Don't show the new options in the spoiler log,
  as they do not affect logic
- Fix the Doom games' Split Backpack option accidentally referring to
  Heretic's Bag of Holding

The logging change across all three games is incidental, as at some
point I did run into that condition by happenstance and it turns out
that it throws an exception due to bad formatting if it's reached

* Do the visibility change for Heretic as well

* Update required client version

* Remove spoiler log restriction on options

* Remove Visibility import now made redundant
2025-03-08 10:37:54 -05:00
Bryce Wilson
00a6ac3a52 BizHawkClient: Store seed name sent by the server for clients to check (#4702) 2025-03-08 16:14:25 +01:00
Bryce Wilson
ea8a14b003 Pokemon Emerald: Some dexsanity locations contribute evolution items (#3187)
* Pokemon Emerald: Change some dexsanity vanilla items to evo items

If a species evolves via item use (Fire Stone, Metal Coat, etc.), use that as it's vanilla item instead of a ball

* Pokemon Emerald: Remove accidentally added print

* Pokemon Emerald: Update changelog

* Pokemon Emerald: Adjust changelog

* Pokemon Emerald: Remove unnecessary else

* Pokemon Emerald: Fix changelog
2025-03-08 10:13:58 -05:00
CaitSith2
414ab86422 LttP: Fix dungeon counter options. (#4704) 2025-03-08 16:13:32 +01:00
Scipio Wright
d4e2698ae0 TUNIC: Add exception handling to deal with duplicate apworlds (#4634)
* Add exception handling to deal with duplicate apworlds

* Update worlds/tunic/__init__.py
2025-03-08 09:56:29 -05:00
JaredWeakStrike
3f8e3082c0 KH2: Client Optimizations and some QoL (#4547)
* adding qwints suggestions

* add stat increase protection and ingame yml stuff

* idk how I forgot these

* reword things

* Update worlds/kh2/Client.py

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

* 3.12 compat

* too long of a line

* why didnt I do this before lol

* reading is hard

* missed one

* forgot the self

* fix crash if you get datapackage that isnt kh2

* update to main?

* update to use 0.10 as base and fix violet's base 0 on hex values

* reverting this because I'm bad at my job

---------

Co-authored-by: qwint <qwint.42@gmail.com>
2025-03-08 08:58:59 -05:00
Justus Lind
0f738935ee Muse Dash: Update song list to Cosmic Radio. (#4554)
* MSR Anthology Vol.2 update

* Missing new line.

* Update to Cosmic Radio 2024
2025-03-08 08:58:26 -05:00
kbranch
9c57976252 LADX: Autotracker improvements (#4445)
* Expand and validate the RAM cache

* Part way through location improvement

* Fixed location tracking

* Preliminary entrance tracking support

* Actually send entrance messages

* Store found entrances on the server

* Bit of cleanup

* Added rupee count, items linked to checks

* Send Magpie a handshAck

* Got my own version wrong

* Remove the Beta name

* Only send slot_data if there's something in it

* Ask the server for entrance updates

* Small fix to stabilize Link's location when changing rooms

* Oops, server storage is shared between worlds

* Deal with null responses from the server

* Added UNUSED_KEY item
2025-03-08 13:32:45 +01:00
NewSoupVi
3e08acf381 The Witness: Move local_items code earlier #4696 2025-03-08 12:26:59 +01:00
Exempt-Medic
113259bc15 Update links (#4690)
* Update links

* Update two more
2025-03-07 20:17:45 -05:00
Natalie Weizenbaum
61afe76eae DS3: Remove the outdated French translation of the setup docs (#4700)
This was causing confusion and Discord support requests because the
instructions there are no longer compatible with the latest version of
Archipelago.

This also lists me as the primary author of the new setup guide.
2025-03-08 01:45:52 +01:00
NewSoupVi
08b3b3ecf5 The Witness: The Secret Feature (#4370)
* Secret Feature

* Fixes

* Fixes and unit tests

* renaming some variables

* Fix the thing

* unit test for elevator egg

* Docstring

* reword

* Fix duplicate locations I think?

* Remove debug thing

* Add the tests back lol

* Make it so that you can exclude an egg to disable it

* Improve hint text for easter eggs

* Update worlds/witness/options.py

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

* Update worlds/witness/player_logic.py

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

* Update worlds/witness/options.py

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

* Update worlds/witness/player_logic.py

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

* Update worlds/witness/rules.py

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

* Update test_easter_egg_shuffle.py

* This was actually not necessary, since this is the Egg requirements, nothing to do with location names

* Move one of them

* Improve logic

* Lol

* Moar

* Adjust unit tests

* option docstring adjustment

* Recommend door shuffle

* Don't overlap IDs

* Option description idk

* Change the way the difficulties work to reward playing higher modes

* Fix merge

* add some stuff to generate_data_file (this file is not imported during gen, don't review it :D)

* oop

* space

* This can be earlier than I thought, apparently.

* buffer

* Comment

* Make sure the option is VERY visible

* Some mypy stuff

* apparently ruff wants this

* .

* durinig

* Update options.py

* Explain the additional effects of each difficulty

* Fix logic of flood room secret

* Add Southern Peninsula Area

* oop

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-03-08 01:44:06 +01:00
Silent
bc61221ec6 TUNIC: Expanded hexagon quest options (#4076)
* More hex quest updates

- Implement page ability shuffle for hex quest
- Fix keys behind bosses if hex goal is less than 3
- Added check to fix conflicting hex quest options
- Add option to slot data

* Change option comparison

* Change option checking and fix some stuff

- also keep prayer first on low hex counts

* Update option defaulting

* Update option checking

* Fix option assignment again

* Show player name in option warning

* Add new option to universal tracker stuff

* Update __init__.py

* Make helper method for getting total hexagons in itempool

* Update options.py

* Update option value passthrough

* Change ability shuffle to default on

* Check for hexagons option when writing spoiler
2025-03-08 01:43:02 +01:00
threeandthreee
2f0b81e12c LADX: tarins gift improvement (#3970)
* add groups and a preset

* formatting

* pull zig's tarin's gift improvements

* typing

* alias groups for progressive items

* change tarins gift option a bit

* add bush breakers item group

* fix typo

* bush_breaker option, respect non_local_items

* review suggestions

* cleaner
thx exempt

* Update worlds/ladx/__init__.py

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

* fix gen failures for dungeon shuffle

* exclude shovel based on entrance mapping

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-03-08 01:24:58 +01:00
threeandthreee
bb9a6bcd2e LADX: more marin joke text (#3966)
* marin text

* Adds lots of Marin Flavour Text (#32)

* Updates of Splash text 24-09-18

* Re-Adds '

* use pkgutil

* Adds all community suggestions up until 20/09/2024 (#33)

* Adds all community suggestions up until 20/09/2024

* cutting deathlink jokes

---------

Co-authored-by: Alex Nordstrom <a.l.nordstrom@gmail.com>

* drop piracy-adjacent jokes

* marin text was too long

* more submissions

* no longer looking for new maintainer

---------

Co-authored-by: palex00 <32203971+palex00@users.noreply.github.com>
2025-03-08 01:19:51 +01:00
Jérémie Bolduc
c8b7ef1016 Stardew Valley: Fix a logic bug where the Tea Sapling would be considered available without having the recipe (#4703) 2025-03-08 00:14:10 +01:00
Silent
e00467c2a2 TUNIC: Update logic for chest in fortress dark area (#4691)
* Update logic for beneath the vault chest

* use helper method instead

so that it checks the lanternless option
2025-03-06 00:18:27 +01:00
Silent
0eb6150e95 TUNIC: Fix rule for some grass in West Garden (#4682) 2025-03-06 00:17:27 +01:00
Fabian Dill
91d977479d Tests: test that collect and remove have expected behaviour. (#2062)
---------

Co-authored-by: qwint <qwint.42@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-03-05 23:48:03 +01:00
BadMagic100
cd761db170 Core: Do GER speculative sweep membership checks against a set #4698 2025-02-27 19:21:48 +01:00
Aaron Wagener
026011323e The Messenger: Fix 0 Required Power Seals (#4692) 2025-02-27 11:42:41 -05:00
Silvris
adc5f3a07d MM2: Fix Shuffled Weaknesses Seed Bleed (#4689) 2025-02-27 11:13:37 -05:00
BadMagic100
69940374e1 Core: Only consider requested exits during ER placement and speculative sweep #4684 2025-02-27 17:12:35 +01:00
Scipio Wright
6dc461609b Noita: Fix bug with Traps disabled in 1-player games #4651 2025-02-23 17:27:05 +01:00
threeandthreee
58d460678e LADX: drop rupee farm condition (#4189)
* drop rupee farm condition

* cleanup

* rupee farm backup for all spending checks

* not power bracelet

* oops
2025-02-23 17:11:24 +01:00
Scipio Wright
0f7fd48cdd TUNIC: Add some more rules for Monastery connections (#4564)
* Move a couple locations to monastery

* Connect Quarry Back to Monastery

* Quarry Back -> Monastery with laurels, Monastery -> Monastery Back with wand/sword

* Add Monastery Back region

* Move a couple non-ER locations to monastery back

* Monastery front -> back with sword, wand, or laurels zip

* also laurels zip for non-ER
2025-02-23 17:02:30 +01:00
Natalie Weizenbaum
18de035b4d DS3: Update setup documentation (#4437) 2025-02-22 08:33:58 -05:00
Fabian Dill
11fa43f0a4 Factorio: prevent players from getting stuck from Teleport Traps (#4537) 2025-02-20 00:17:19 +01:00
black-sliver
91a8fc91d6 CI: fix native tests toolchain on windows (#4668)
* CI: ctest: fix trigger on CMakeLists change

* CI: ctest: update cmake version

this removes a warning
and matches gtest

* CI: ctest: remove explicit build mode for MSVC

gtest switched to dynamic libc (/MD), which is default, so this just works now
2025-02-19 13:50:25 +01:00
Fabian Dill
15bde56551 Factorio: prevent invalid starting items count (#4658) 2025-02-17 18:58:38 +01:00
NewSoupVi
d744e086ef MultiServer: Fix hinting an item that someone else already hinted in their slot not resolving correctly (#4655)
* Fix get_hint not checking for finding_player

* Fix using the wrong variable for slot lookup
2025-02-17 15:16:18 +01:00
Scipio Wright
378fa5d5c4 Fix gun missing from combat_items, add new for combat logic cache, very slight refactor of check_combat_reqs to let it do the changeover in a less complicated fashion, fix area being a boss area rather than non-boss area for a check (#4657) 2025-02-17 01:30:40 +01:00
black-sliver
8349774c5c customserver: ignore static datapackage optimization for old games (#4650) 2025-02-16 23:51:36 +01:00
qwint
34795b598a GER: Use Itempool Count for Minimal handling (#4649)
* uses itempool count vs unfilled location count instead of counting prog_items values which could have custom counters

* move unfilled location check to before can_reach

* add tests for successful minimal GER call with extra collect override prog_items in the pool to regression test issue fixed in this PR
2025-02-16 20:21:09 +01:00
JoshuaEagles
efd5004330 Docs: Update SA2B Linux and Steam Deck Setup Guide + Add Celeste 64 Linux Setup Guide (#4593)
* Update Linux and Steam Deck setup guide for sa2b

* Add Linux and Steam Deck setup guide for Celeste 64
2025-02-12 17:47:43 +01:00
Matthew Wells
c799531105 Docs: Add missing plural in faq (#4622) 2025-02-12 17:47:17 +01:00
threeandthreee
5c1ded1fe9 LADX: bomb as logical bush breaker #4636 2025-02-12 17:46:43 +01:00
qwint
b2162bb8e6 Docs: clean up create_item/event example (#4596)
* eyes

* remove line wraps where unnecessary
2025-02-12 17:46:07 +01:00
agilbert1412
f1769a8d00 Stardew Valley: Fixed Powdermelon and option inconsistencies (#4632)
* - Fixed powdermelon season

* - Improve cohesion in presets

* - Update several tooltips to be more consistent and accurate
2025-02-12 17:45:03 +01:00
qwint
f520c1d9f2 Launcher: Allow for --nogui client launches (#4549) 2025-02-10 19:34:27 +01:00
PinkSwitch
910369a7f8 Bizhawk Client: Display Err (#4532)
Co-authored-by: Bryce Wilson
2025-02-10 19:27:10 +01:00
qwint
dbf6b6f935 CC: don't try to reconnect on invalid version (#4606) 2025-02-10 19:23:58 +01:00
qwint
e9c463c897 CC: Force Text Client to always connect with empty game (#4607) 2025-02-10 19:23:09 +01:00
qwint
f4e43ca9e0 LttP: mock world.random in adjuster (#4623) 2025-02-10 19:22:06 +01:00
Fabian Dill
a298be9c41 Core: change HINT_FOUND to 40 and HINT_UNSPECIFIED to 0 (#4620) 2025-02-10 19:19:00 +01:00
Fabian Dill
18bcaa85a2 Test: ensure get_all_state() does not error in between steps (#4612) 2025-02-10 19:18:14 +01:00
Scipio Wright
359f45d50f TUNIC: Combat logic fix (#4589)
* Potential fix for attack issue

* also put the lazy version of the swamp fix in for good measure

* fix extra line

* now it is good

* Add the test, roll the other PR into this one

* Make the test exception more useful

* Remove debug print

* Combat logic fixed?

* Move a few areas to before well instead of east forest

* Put in qwint's suggestions in test

* Implement qwint's suggestions in combat_logic.py

* Implement qwint's suggestions for combat_logic.py

* Fix typo

* Remove experimental from combat logic description

* Remove copy_mixin again

* Add comment about copy_mixin

* Use a more proper random

* Some optimizations from Vi's comments
2025-02-09 19:12:17 +01:00
qwint
f5c574c37a Settings: add format handling to yaml exception marks for readability (#4531) 2025-02-09 12:11:27 +01:00
NewSoupVi
f75a1ae117 KH2: Fix lambda capture issue with weapon slot logic (#4604)
* KH2: Fix lambda capture issue with weapon slot logic

* Update Rules.py

* Improved by JaredWeakStrike (#4605)

* Apparently this wasn't meant to be indented

---------

Co-authored-by: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com>
2025-02-08 00:06:04 +01:00
Kory Dondzila
768ccffe72 Shivers: Update shivers links and guides (#4592) 2025-02-07 21:06:06 +01:00
Martmists
f6668997e6 [AHIT] Fix small options issue (#4615) 2025-02-07 21:02:37 +01:00
shananas
db11c620a7 KH2 Doc Update #4609
Mod Manager Version Number
2025-02-04 17:09:02 +01:00
Jouramie
da48af60dc Stardew Valley: add assert_can_reach_region_* for better tests (#4556)
* add assert_reach_region_*; refactor existing assert_reach_location_* to allow string

* rename asserts
2025-02-04 08:27:23 +01:00
massimilianodelliubaldini
19faaa4104 Core: Fix #4595 by using first type's docstring in a union type (#4600)
* Fix #4595: use first type's docstring in a union type.

* Reuse existing import.
2025-02-04 01:49:07 +01:00
Scipio Wright
628252896e TUNIC: Call Combat Logic experimental (#4594)
* Update options.py

* Update options.py
2025-02-03 15:53:56 +01:00
Mysteryem
f28aff6f9a Core: Replace generator creation/iteration in CollectionState methods (#4587)
* Core: Replace generator creation/iteration in CollectionState methods

Using generators in these functions incurs overhead to create the new
generator instance, call the `any`/`all`/`sum` function and have the
`any`/`all`/`sum` function iterate the generator, which in turn iterates
the iterable.

Replacing the use of generators with for loops is faster.

Getting `self.prog_items[player]` once in advance also improves
performance of iterating longer iterables.

* Add comment on the choice of for loops instead of any()/all()/sum()
2025-02-02 15:25:34 +01:00
Fabian Dill
894732be47 kvui: set home folder to non-default (#4590)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2025-02-02 02:53:16 +01:00
Jouramie
051518e72a Stardew Valley: Fix unresolved reference warning and unused imports (#4360)
* fix unresolved reference warning and unused imports

* revert stuff

* just a commit to rerun the tests cuz messenger fail
2025-02-01 22:07:08 +01:00
Spineraks
b7b78dead3 LADX: Fix generation error on minimal accessibility (#4281)
* [LADX] Fix minimal accessibility

* allow_partial for minimal accessibility

* create the correct partial_all_state

* skip our prefills rather than removing after

* dont rebuild our prefill list

---------

Co-authored-by: threeandthreee <a.l.nordstrom@gmail.com>
2025-02-01 22:03:49 +01:00
Jarno
d1167027f4 Core: Make csv options output ignore hidden options (#4539)
* Core: Make csv options output ignore hidden options

* Update Options.py

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

---------

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2025-02-01 02:26:59 +01:00
qwint
445c9b22d6 Settings: Handle empty Groups (#4576)
* export empty groups as an empty dict instead of crashing

* Update settings.py

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

* check instance values from self as well

* Apply suggestions from code review

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2025-02-01 02:11:04 +01:00
black-sliver
67e8877143 Docs: fix lower limit of valid IDs in network protocol.md (#4579) 2025-01-31 08:38:17 +01:00
agilbert1412
1fe8024b43 Stardew valley: Add Mod Recipes tests (#4580)
* `- Add Craftsanity Mod tests

* - Add the same test for cooking

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2025-01-30 09:19:06 +01:00
agilbert1412
8e14e463e4 Stardew Valley: Radioactive slot machine should be a ginger island check (#4578) 2025-01-30 09:05:51 +01:00
Jouramie
b8666b2562 Stardew Valley: Remove weird magic trap test? (#4570) 2025-01-29 13:56:50 -05:00
Felix R
57afdfda6f meritous: move completion_condition to set_rules (#4567) 2025-01-29 02:03:37 +01:00
black-sliver
738c21c625 Tests: massively improve the memory leak test performance (#4568)
* Tests: massively improve the memory leak test performance

With the growing number of worlds, GC becomes the bottleneck and slows down the test.

* Tests: fix typing in general/test_memory
2025-01-29 01:52:01 +01:00
black-sliver
41898ed640 MultiServer: implement NoText and deprecate uncompressed Websocket connections (#4540)
* MultiServer: add NoText tag and handling

* MultiServer: deprecate and warn for uncompressed connections

* MultiServer: fix missing space in no compression warning
2025-01-29 01:42:46 +01:00
agilbert1412
1ebc9e2ec0 Stardew Valley: Tests: Restructure the tests that validate Mods + ER together, improved performance (#4557)
* - Unrolled and improved the structure of the test for Mods + ER, to improve total performance and performance on individual tests for threading purposes

* Use | instead of Union[]

Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com>

* - Remove unused using

---------

Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com>
2025-01-28 23:19:20 +01:00
Silvris
9466d5274e MM2: fix plando and weakness special cases (#4561) 2025-01-28 21:45:28 +01:00
NewSoupVi
a53bcb4697 KH2: Use int(..., 0) in Client #4562 2025-01-27 23:13:10 +01:00
Exempt-Medic
8c5592e406 KH2: Fix determinism by using tuples instead of sets (#4548) 2025-01-27 11:06:10 -05:00
Bryce Wilson
41055cd963 Pokemon Emerald: Update changelog (#4551) 2025-01-27 17:01:18 +01:00
Scipio Wright
43874b1d28 Noita: Add clarification to check option descriptions (#4553) 2025-01-27 10:27:43 -05:00
Bryce Wilson
b570aa2ec6 Pokemon Emerald: Clean up free fly blacklist (#4552) 2025-01-27 10:25:31 -05:00
Bryce Wilson
c43233120a Pokemon Emerald: Clarify death link and start inventory descriptions (#4517) 2025-01-27 10:24:26 -05:00
Silvris
57a571cc11 KDL3: Fix world access on non-strict open world (#4543)
* Update rules.py

* lambda capture
2025-01-27 01:52:02 +01:00
Fabian Dill
8622cb6204 Factorio: Inventory Spill Traps (#4457) 2025-01-26 22:14:39 +01:00
qwint
90417e0022 CommonClient: Expand on make_gui docstring (#4449)
* adds docstring to make_gui describing what things you might want to change without dealing with kivy/kvui directly (there are better places to document those)

* Update CommonClient.py

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

* Update CommonClient.py

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

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2025-01-26 13:06:27 +01:00
josephwhite
96b941ed35 Super Mario 64: Add Star Costs to Spoiler (#4544) 2025-01-25 09:36:23 -05:00
Bryce Wilson
1832bac1a3 BizHawkClient: Update README for get_memory_size (#4511) 2025-01-25 09:35:42 -05:00
qwint
86641223c1 Shivers: Stop using get_all_state cache to fix timing issue #4522
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2025-01-25 00:35:54 +01:00
black-sliver
cc770418f2 MultiServer: optimize PrintJSON for !release (#4545)
* MultiServer: optimize PrintJSON for !release

* MultiServer: safer comparison

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

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2025-01-24 23:22:33 +01:00
Scipio Wright
513e361764 TUNIC: Fix UT create_item classification (#4514)
Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com>
2025-01-24 17:10:58 -05:00
Silent
ddf7fdccc7 TUNIC: Add Torch Item (#4538)
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-01-24 16:57:23 -05:00
Silent
3df2dbe051 TUNIC: Add ability shuffle information to spoiler log (#4498) 2025-01-24 16:55:49 -05:00
Jasper den Brok
3d1d6908c8 Pokemon Emerald: Add Free Fly Blacklist (#4165)
Co-authored-by: Jasper den Brok <jasper.den.brok@gmail.com>
2025-01-24 16:30:21 -05:00
qwint
7474c27372 Core: Add launch function to call launch_subprocess only if multiprocessing is actually necessary (#4237)
* skips opening a subprocess if kivy (and thus the launcher gui) hasn't been loaded so stdin can function as expected on --nogui and similar

* this exists lol

* keep old function around and use new function for CC component

* fix name=None typing
2025-01-24 19:52:12 +01:00
Scipio Wright
bb0948154d TUNIC: Make the standard entrances get made with tuples instead of sets (#4546)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-24 12:42:31 -05:00
CookieCat
fa2816822b AHIT: Fix broken link in setup guide (#4524)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-23 16:45:11 -05:00
NewSoupVi
5a42c70675 Core: Fix worlds that rely on other worlds having their Entrances connected before connect_entrances, add unit test (#4530)
* unit test that get all state is called with partial entrances before connect_entrances

* fix the two worlds doing it

* lol

* unused import

* Update test/general/test_entrances.py

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

* Update test_entrances.py

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-22 14:00:47 +01:00
JaredWeakStrike
949527f9cb KH2: Bug fixes and game update future proofing (#4075)
Co-authored-by: qwint <qwint.42@gmail.com>
2025-01-21 17:28:33 -05:00
Scipio Wright
1a1b7e9cf4 TUNIC: Reduce range end for local_fill option #4534 2025-01-21 18:39:08 +01:00
Fabian Dill
edacb17171 Factorio: remove debug print (#4533) 2025-01-21 16:12:53 +01:00
qwint
33fd9de281 Core: Add Retry to Priority Fill (#4477)
* adds a retry to priority fill in case the one item per player optimization would cause the priority fill to fail to find valid placements

* Update Fill.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>
2025-01-21 00:56:20 +01:00
qwint
a126dee068 HK: some stuff ruff and pycodestyle complained about (#4523) 2025-01-20 23:42:12 +01:00
qwint
e2b942139a HK: Save GrubHuntGoal by value (#4521) 2025-01-20 19:10:29 +01:00
Scipio Wright
823b17c386 TUNIC: Make grass go in the regular location name group too (#4504)
* Make grass go in the normal loc group too

* Make it not overwrite old groups
2025-01-20 17:44:39 +01:00
Chris J.
05d1b2129a Docs: Update ID Overlapping Docs (#4447) 2025-01-20 11:18:09 -05:00
NewSoupVi
436c0a4104 Core: Add connect_entrances world step/stage (#4420)
* Add connect_entrances

* update ER docs

* fix that test, but also ew

* Add a test that asserts the new finalization

* Rewrite test a bit

* rewrite some more

* blank line

* rewrite rewrite rewrite

* rewrite rewrite rewrite

* RE. WRITE.

* oops

* Bruh

* I guess, while we're at it

* giga oops

* It's been a long day

* Switch KH1 over to this design with permission of GICU

* Revert

* Oops

* Bc I like it

* Update locations.py
2025-01-20 16:07:15 +01:00
Scipio Wright
96f469c737 TUNIC: Fix hero relics not being prog if hex quest is on in combat logic #4509 2025-01-20 16:04:39 +01:00
Scipio Wright
4f77abac4f TUNIC: Fix failure in 1-player grass (#4520)
* Fix failure in 1-player grass

* Update worlds/tunic/__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>
2025-01-20 15:53:30 +01:00
massimilianodelliubaldini
d5cd95c7fb Docs: Clarify usage of slot data for trackers in World API doc (#3986)
* Clarify usage of slot data for trackers in world API.

* Typo.

* Update docs/world api.md

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

* Update docs/world api.md

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

* Update docs/world api.md

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

* Update docs/world api.md

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

* Keep to 120 char lines.

---------

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-20 09:01:45 +01:00
Exempt-Medic
a2fbf856ff SMZ3: Change locality options earlier (#4424) 2025-01-19 23:07:01 -05:00
Exempt-Medic
4fa8c43266 FFMQ: Fix collect_item (#4433)
* Fix FFMQ collect_item
2025-01-19 23:06:09 -05:00
224 changed files with 6339 additions and 2084 deletions

View File

@@ -132,7 +132,7 @@ jobs:
# charset-normalizer was somehow incomplete in the github runner
"${{ env.PYTHON }}" -m venv venv
source venv/bin/activate
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer
"${{ env.PYTHON }}" -m pip install --upgrade pip "PyGObject<3.51.0" charset-normalizer
python setup.py build_exe --yes bdist_appimage --yes
echo -e "setup.py build output:\n `ls build`"
echo -e "setup.py dist output:\n `ls dist`"

View File

@@ -11,7 +11,7 @@ on:
- '**.hh?'
- '**.hpp'
- '**.hxx'
- '**.CMakeLists'
- '**/CMakeLists.txt'
- '.github/workflows/ctest.yml'
pull_request:
paths:
@@ -21,7 +21,7 @@ on:
- '**.hh?'
- '**.hpp'
- '**.hxx'
- '**.CMakeLists'
- '**/CMakeLists.txt'
- '.github/workflows/ctest.yml'
jobs:

View File

@@ -64,7 +64,7 @@ jobs:
# charset-normalizer was somehow incomplete in the github runner
"${{ env.PYTHON }}" -m venv venv
source venv/bin/activate
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer
"${{ env.PYTHON }}" -m pip install --upgrade pip "PyGObject<3.51.0" charset-normalizer
python setup.py build_exe --yes bdist_appimage --yes
echo -e "setup.py build output:\n `ls build`"
echo -e "setup.py dist output:\n `ls dist`"

View File

@@ -869,21 +869,40 @@ class CollectionState():
def has(self, item: str, player: int, count: int = 1) -> bool:
return self.prog_items[player][item] >= count
# for loops are specifically used in all/any/count methods, instead of all()/any()/sum(), to avoid the overhead of
# creating and iterating generator instances. In `return all(player_prog_items[item] for item in items)`, the
# argument to all() would be a new generator instance, for example.
def has_all(self, items: Iterable[str], player: int) -> bool:
"""Returns True if each item name of items is in state at least once."""
return all(self.prog_items[player][item] for item in items)
player_prog_items = self.prog_items[player]
for item in items:
if not player_prog_items[item]:
return False
return True
def has_any(self, items: Iterable[str], player: int) -> bool:
"""Returns True if at least one item name of items is in state at least once."""
return any(self.prog_items[player][item] for item in items)
player_prog_items = self.prog_items[player]
for item in items:
if player_prog_items[item]:
return True
return False
def has_all_counts(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if each item name is in the state at least as many times as specified."""
return all(self.prog_items[player][item] >= count for item, count in item_counts.items())
player_prog_items = self.prog_items[player]
for item, count in item_counts.items():
if player_prog_items[item] < count:
return False
return True
def has_any_count(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if at least one item name is in the state at least as many times as specified."""
return any(self.prog_items[player][item] >= count for item, count in item_counts.items())
player_prog_items = self.prog_items[player]
for item, count in item_counts.items():
if player_prog_items[item] >= count:
return True
return False
def count(self, item: str, player: int) -> int:
return self.prog_items[player][item]
@@ -911,11 +930,20 @@ 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)
player_prog_items = self.prog_items[player]
total = 0
for item_name in items:
total += player_prog_items[item_name]
return total
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)
player_prog_items = self.prog_items[player]
total = 0
for item_name in items:
if player_prog_items[item_name] > 0:
total += 1
return total
# item name group related
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:

View File

@@ -709,8 +709,16 @@ class CommonContext:
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
def make_gui(self) -> typing.Type["kvui.GameManager"]:
"""To return the Kivy App class needed for run_gui so it can be overridden before being built"""
def make_gui(self) -> "type[kvui.GameManager]":
"""
To return the Kivy `App` class needed for `run_gui` so it can be overridden before being built
Common changes are changing `base_title` to update the window title of the client and
updating `logging_pairs` to automatically make new tabs that can be filled with their respective logger.
ex. `logging_pairs.append(("Foo", "Bar"))`
will add a "Bar" tab which follows the logger returned from `logging.getLogger("Foo")`
"""
from kvui import GameManager
class TextManager(GameManager):
@@ -899,6 +907,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.disconnected_intentionally = True
ctx.event_invalid_game()
elif 'IncompatibleVersion' in errors:
ctx.disconnected_intentionally = True
raise Exception('Server reported your client version as incompatible. '
'This probably means you have to update.')
elif 'InvalidItemsHandling' in errors:
@@ -1087,7 +1096,7 @@ def run_as_textclient(*args):
if password_requested and not self.password:
await super(TextContext, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
await self.send_connect(game="")
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":

View File

@@ -502,7 +502,13 @@ def distribute_items_restrictive(multiworld: MultiWorld,
# "priority fill"
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority", one_item_per_player=False)
name="Priority", one_item_per_player=True, allow_partial=True)
if prioritylocations:
# retry with one_item_per_player off because some priority fills can fail to fill with that optimization
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority Retry", one_item_per_player=False)
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations

View File

@@ -28,6 +28,7 @@ from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
from NetUtils import ClientStatus
from worlds.ladx.Common import BASE_ID as LABaseID
from worlds.ladx.GpsTracker import GpsTracker
from worlds.ladx.TrackerConsts import storage_key
from worlds.ladx.ItemTracker import ItemTracker
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
@@ -100,19 +101,23 @@ class LAClientConstants:
WRamCheckSize = 0x4
WRamSafetyValue = bytearray([0]*WRamCheckSize)
wRamStart = 0xC000
hRamStart = 0xFF80
hRamSize = 0x80
MinGameplayValue = 0x06
MaxGameplayValue = 0x1A
VictoryGameplayAndSub = 0x0102
class RAGameboy():
cache = []
cache_start = 0
cache_size = 0
last_cache_read = None
socket = None
def __init__(self, address, port) -> None:
self.cache_start = LAClientConstants.wRamStart
self.cache_size = LAClientConstants.hRamStart + LAClientConstants.hRamSize - LAClientConstants.wRamStart
self.address = address
self.port = port
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -131,9 +136,14 @@ class RAGameboy():
async def get_retroarch_status(self):
return await self.send_command("GET_STATUS")
def set_cache_limits(self, cache_start, cache_size):
self.cache_start = cache_start
self.cache_size = cache_size
def set_checks_range(self, checks_start, checks_size):
self.checks_start = checks_start
self.checks_size = checks_size
def set_location_range(self, location_start, location_size, critical_addresses):
self.location_start = location_start
self.location_size = location_size
self.critical_location_addresses = critical_addresses
def send(self, b):
if type(b) is str:
@@ -188,21 +198,57 @@ class RAGameboy():
if not await self.check_safe_gameplay():
return
cache = []
remaining_size = self.cache_size
while remaining_size:
block = await self.async_read_memory(self.cache_start + len(cache), remaining_size)
remaining_size -= len(block)
cache += block
attempts = 0
while True:
# RA doesn't let us do an atomic read of a large enough block of RAM
# Some bytes can't change in between reading location_block and hram_block
location_block = await self.read_memory_block(self.location_start, self.location_size)
hram_block = await self.read_memory_block(LAClientConstants.hRamStart, LAClientConstants.hRamSize)
verification_block = await self.read_memory_block(self.location_start, self.location_size)
valid = True
for address in self.critical_location_addresses:
if location_block[address - self.location_start] != verification_block[address - self.location_start]:
valid = False
if valid:
break
attempts += 1
# Shouldn't really happen, but keep it from choking
if attempts > 5:
return
checks_block = await self.read_memory_block(self.checks_start, self.checks_size)
if not await self.check_safe_gameplay():
return
self.cache = cache
self.cache = bytearray(self.cache_size)
start = self.checks_start - self.cache_start
self.cache[start:start + len(checks_block)] = checks_block
start = self.location_start - self.cache_start
self.cache[start:start + len(location_block)] = location_block
start = LAClientConstants.hRamStart - self.cache_start
self.cache[start:start + len(hram_block)] = hram_block
self.last_cache_read = time.time()
async def read_memory_block(self, address: int, size: int):
block = bytearray()
remaining_size = size
while remaining_size:
chunk = await self.async_read_memory(address + len(block), remaining_size)
remaining_size -= len(chunk)
block += chunk
return block
async def read_memory_cache(self, addresses):
# TODO: can we just update once per frame?
if not self.last_cache_read or self.last_cache_read + 0.1 < time.time():
await self.update_cache()
if not self.cache:
@@ -359,11 +405,12 @@ class LinksAwakeningClient():
auth = binascii.hexlify(await self.gameboy.async_read_memory(0x0134, 12)).decode()
self.auth = auth
async def wait_and_init_tracker(self):
async def wait_and_init_tracker(self, magpie: MagpieBridge):
await self.wait_for_game_ready()
self.tracker = LocationTracker(self.gameboy)
self.item_tracker = ItemTracker(self.gameboy)
self.gps_tracker = GpsTracker(self.gameboy)
magpie.gps_tracker = self.gps_tracker
async def recved_item_from_ap(self, item_id, from_player, next_index):
# Don't allow getting an item until you've got your first check
@@ -405,9 +452,11 @@ class LinksAwakeningClient():
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
async def main_tick(self, item_get_cb, win_cb, deathlink_cb):
await self.gameboy.update_cache()
await self.tracker.readChecks(item_get_cb)
await self.item_tracker.readItems()
await self.gps_tracker.read_location()
await self.gps_tracker.read_entrances()
current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth]
if self.deathlink_debounce and current_health != 0:
@@ -465,6 +514,10 @@ class LinksAwakeningContext(CommonContext):
magpie_task = None
won = False
@property
def slot_storage_key(self):
return f"{self.slot_info[self.slot].name}_{storage_key}"
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
self.client = LinksAwakeningClient()
self.slot_data = {}
@@ -507,7 +560,19 @@ class LinksAwakeningContext(CommonContext):
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def send_checks(self):
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
message = [{"cmd": "LocationChecks", "locations": self.found_checks}]
await self.send_msgs(message)
async def send_new_entrances(self, entrances: typing.Dict[str, str]):
# Store the entrances we find on the server for future sessions
message = [{
"cmd": "Set",
"key": self.slot_storage_key,
"default": {},
"want_reply": False,
"operations": [{"operation": "update", "value": entrances}],
}]
await self.send_msgs(message)
had_invalid_slot_data = None
@@ -536,6 +601,12 @@ class LinksAwakeningContext(CommonContext):
logger.info("victory!")
await self.send_msgs(message)
self.won = True
async def request_found_entrances(self):
await self.send_msgs([{"cmd": "Get", "keys": [self.slot_storage_key]}])
# Ask for updates so that players can co-op entrances in a seed
await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}])
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
if self.ENABLE_DEATHLINK:
@@ -576,6 +647,12 @@ class LinksAwakeningContext(CommonContext):
if cmd == "ReceivedItems":
for index, item in enumerate(args["items"], start=args["index"]):
self.client.recvd_checks[index] = item
if cmd == "Retrieved" and self.magpie_enabled and self.slot_storage_key in args["keys"]:
self.client.gps_tracker.receive_found_entrances(args["keys"][self.slot_storage_key])
if cmd == "SetReply" and self.magpie_enabled and args["key"] == self.slot_storage_key:
self.client.gps_tracker.receive_found_entrances(args["value"])
async def sync(self):
sync_msg = [{'cmd': 'Sync'}]
@@ -589,6 +666,12 @@ class LinksAwakeningContext(CommonContext):
checkMetadataTable[check.id])] for check in ladxr_checks]
self.new_checks(checks, [check.id for check in ladxr_checks])
for check in ladxr_checks:
if check.value and check.linkedItem:
linkedItem = check.linkedItem
if 'condition' not in linkedItem or linkedItem['condition'](self.slot_data):
self.client.item_tracker.setExtraItem(check.linkedItem['item'], check.linkedItem['qty'])
async def victory():
await self.send_victory()
@@ -622,12 +705,20 @@ class LinksAwakeningContext(CommonContext):
if not self.client.recvd_checks:
await self.sync()
await self.client.wait_and_init_tracker()
await self.client.wait_and_init_tracker(self.magpie)
min_tick_duration = 0.1
last_tick = time.time()
while True:
await self.client.main_tick(on_item_get, victory, deathlink)
await asyncio.sleep(0.1)
now = time.time()
tick_duration = now - last_tick
sleep_duration = max(min_tick_duration - tick_duration, 0)
await asyncio.sleep(sleep_duration)
last_tick = now
if self.last_resend + 5.0 < now:
self.last_resend = now
await self.send_checks()
@@ -635,8 +726,15 @@ class LinksAwakeningContext(CommonContext):
try:
self.magpie.set_checks(self.client.tracker.all_checks)
await self.magpie.set_item_tracker(self.client.item_tracker)
await self.magpie.send_gps(self.client.gps_tracker)
self.magpie.slot_data = self.slot_data
if self.client.gps_tracker.needs_found_entrances:
await self.request_found_entrances()
self.client.gps_tracker.needs_found_entrances = False
new_entrances = await self.magpie.send_gps(self.client.gps_tracker)
if new_entrances:
await self.send_new_entrances(new_entrances)
except Exception:
# Don't let magpie errors take out the client
pass

View File

@@ -33,10 +33,15 @@ WINDOW_MIN_HEIGHT = 525
WINDOW_MIN_WIDTH = 425
class AdjusterWorld(object):
class AdjusterSubWorld(object):
def __init__(self, random):
self.random = random
def __init__(self, sprite_pool):
import random
self.sprite_pool = {1: sprite_pool}
self.per_slot_randoms = {1: random}
self.worlds = {1: self.AdjusterSubWorld(random)}
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):

View File

@@ -148,7 +148,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
else:
multiworld.worlds[1].options.non_local_items.value = set()
multiworld.worlds[1].options.local_items.value = set()
AutoWorld.call_all(multiworld, "connect_entrances")
AutoWorld.call_all(multiworld, "generate_basic")
# remove starting inventory from pool items.

View File

@@ -28,9 +28,11 @@ ModuleUpdate.update()
if typing.TYPE_CHECKING:
import ssl
from NetUtils import ServerConnection
import websockets
import colorama
import websockets
from websockets.extensions.permessage_deflate import PerMessageDeflate
try:
# ponyorm is a requirement for webhost, not default server, so may not be importable
from pony.orm.dbapiprovider import OperationalError
@@ -119,13 +121,14 @@ def get_saving_second(seed_name: str, interval: int = 60) -> int:
class Client(Endpoint):
version = Version(0, 0, 0)
tags: typing.List[str] = []
tags: typing.List[str]
remote_items: bool
remote_start_inventory: bool
no_items: bool
no_locations: bool
no_text: bool
def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context):
def __init__(self, socket: "ServerConnection", ctx: Context) -> None:
super().__init__(socket)
self.auth = False
self.team = None
@@ -175,6 +178,7 @@ class Context:
"compatibility": int}
# team -> slot id -> list of clients authenticated to slot.
clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]]
endpoints: list[Client]
locations: LocationStore # typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
location_checks: typing.Dict[typing.Tuple[int, int], typing.Set[int]]
hints_used: typing.Dict[typing.Tuple[int, int], int]
@@ -364,18 +368,28 @@ class Context:
return True
def broadcast_all(self, msgs: typing.List[dict]):
msgs = self.dumper(msgs)
endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth)
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs)
data = self.dumper(msgs)
endpoints = (
endpoint
for endpoint in self.endpoints
if endpoint.auth and not (msg_is_text and endpoint.no_text)
)
async_start(self.broadcast_send_encoded_msgs(endpoints, data))
def broadcast_text_all(self, text: str, additional_arguments: dict = {}):
self.logger.info("Notice (all): %s" % text)
self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
def broadcast_team(self, team: int, msgs: typing.List[dict]):
msgs = self.dumper(msgs)
endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values()))
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs)
data = self.dumper(msgs)
endpoints = (
endpoint
for endpoint in itertools.chain.from_iterable(self.clients[team].values())
if not (msg_is_text and endpoint.no_text)
)
async_start(self.broadcast_send_encoded_msgs(endpoints, data))
def broadcast(self, endpoints: typing.Iterable[Client], msgs: typing.List[dict]):
msgs = self.dumper(msgs)
@@ -389,13 +403,13 @@ class Context:
await on_client_disconnected(self, endpoint)
def notify_client(self, client: Client, text: str, additional_arguments: dict = {}):
if not client.auth:
if not client.auth or client.no_text:
return
self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}):
if not client.auth:
if not client.auth or client.no_text:
return
async_start(self.send_msgs(client,
[{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}
@@ -760,7 +774,7 @@ class Context:
self.on_new_hint(team, slot)
for slot, hint_data in concerns.items():
if recipients is None or slot in recipients:
clients = self.clients[team].get(slot)
clients = filter(lambda c: not c.no_text, self.clients[team].get(slot, []))
if not clients:
continue
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player != slot)]
@@ -769,7 +783,7 @@ class Context:
def get_hint(self, team: int, finding_player: int, seeked_location: int) -> typing.Optional[Hint]:
for hint in self.hints[team, finding_player]:
if hint.location == seeked_location:
if hint.location == seeked_location and hint.finding_player == finding_player:
return hint
return None
@@ -819,7 +833,7 @@ def update_aliases(ctx: Context, team: int):
async_start(ctx.send_encoded_msgs(client, cmd))
async def server(websocket, path: str = "/", ctx: Context = None):
async def server(websocket: "ServerConnection", path: str = "/", ctx: Context = None) -> None:
client = Client(websocket, ctx)
ctx.endpoints.append(client)
@@ -910,6 +924,10 @@ async def on_client_joined(ctx: Context, client: Client):
"If your client supports it, "
"you may have additional local commands you can list with /help.",
{"type": "Tutorial"})
if not any(isinstance(extension, PerMessageDeflate) for extension in client.socket.extensions):
ctx.notify_client(client, "Warning: your client does not support compressed websocket connections! "
"It may stop working in the future. If you are a player, please report this to the "
"client's developer.")
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
@@ -1060,21 +1078,37 @@ def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem
def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int],
count_activity: bool = True):
slot_locations = ctx.locations[slot]
new_locations = set(locations) - ctx.location_checks[team, slot]
new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata
new_locations.intersection_update(slot_locations) # ignore location IDs unknown to this multidata
if new_locations:
if count_activity:
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
sortable: list[tuple[int, int, int, int]] = []
for location in new_locations:
item_id, target_player, flags = ctx.locations[slot][location]
# extract all fields to avoid runtime overhead in LocationStore
item_id, target_player, flags = slot_locations[location]
# sort/group by receiver and item
sortable.append((target_player, item_id, location, flags))
info_texts: list[dict[str, typing.Any]] = []
for target_player, item_id, location, flags in sorted(sortable):
new_item = NetworkItem(item_id, location, slot, flags)
send_items_to(ctx, team, target_player, new_item)
ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id],
ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location]))
info_text = json_format_send_event(new_item, target_player)
ctx.broadcast_team(team, [info_text])
if len(info_texts) >= 140:
# split into chunks that are close to compression window of 64K but not too big on the wire
# (roughly 1300-2600 bytes after compression depending on repetitiveness)
ctx.broadcast_team(team, info_texts)
info_texts.clear()
info_texts.append(json_format_send_event(new_item, target_player))
ctx.broadcast_team(team, info_texts)
del info_texts
del sortable
ctx.location_checks[team, slot] |= new_locations
send_new_items(ctx)
@@ -1101,7 +1135,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
for finding_player, location_id, item_id, receiving_player, item_flags \
in ctx.locations.find_item(slots, seeked_item_id):
prev_hint = ctx.get_hint(team, slot, location_id)
prev_hint = ctx.get_hint(team, finding_player, location_id)
if prev_hint:
hints.append(prev_hint)
else:
@@ -1787,7 +1821,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
ctx.clients[team][slot].append(client)
client.version = args['version']
client.tags = args['tags']
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
client.no_locations = "TextOnly" in client.tags or "Tracker" in client.tags
# set NoText for old PopTracker clients that predate the tag to save traffic
client.no_text = "NoText" in client.tags or ("PopTracker" in client.tags and client.version < (0, 5, 1))
connected_packet = {
"cmd": "Connected",
"team": client.team, "slot": client.slot,
@@ -1860,6 +1896,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
client.tags = args["tags"]
if set(old_tags) != set(client.tags):
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
client.no_text = "NoText" in client.tags or (
"PopTracker" in client.tags and client.version < (0, 5, 1)
)
ctx.broadcast_text_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
f"from {old_tags} to {client.tags}.",

View File

@@ -5,17 +5,18 @@ import enum
import warnings
from json import JSONEncoder, JSONDecoder
import websockets
if typing.TYPE_CHECKING:
from websockets import WebSocketServerProtocol as ServerConnection
from Utils import ByValue, Version
class HintStatus(ByValue, enum.IntEnum):
HINT_FOUND = 0
HINT_UNSPECIFIED = 1
HINT_UNSPECIFIED = 0
HINT_NO_PRIORITY = 10
HINT_AVOID = 20
HINT_PRIORITY = 30
HINT_FOUND = 40
class JSONMessagePart(typing.TypedDict, total=False):
@@ -151,7 +152,7 @@ decode = JSONDecoder(object_hook=_object_hook).decode
class Endpoint:
socket: websockets.WebSocketServerProtocol
socket: "ServerConnection"
def __init__(self, socket):
self.socket = socket

View File

@@ -1582,7 +1582,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
}
output.append(player_output)
for option_key, option in world.options_dataclass.type_hints.items():
if issubclass(Removed, option):
if option.visibility == Visibility.none:
continue
display_name = getattr(option, "display_name", option_key)
player_output[display_name] = getattr(world.options, option_key).current_option_name

View File

@@ -443,7 +443,8 @@ class RestrictedUnpickler(pickle.Unpickler):
else:
mod = importlib.import_module(module)
obj = getattr(mod, name)
if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection)):
if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection,
self.options_module.PlandoText)):
return obj
# Forbid everything else.
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")

View File

@@ -117,6 +117,7 @@ class WebHostContext(Context):
self.gamespackage = {"Archipelago": static_gamespackage.get("Archipelago", {})} # this may be modified by _load
self.item_name_groups = {"Archipelago": static_item_name_groups.get("Archipelago", {})}
self.location_name_groups = {"Archipelago": static_location_name_groups.get("Archipelago", {})}
missing_checksum = False
for game in list(multidata.get("datapackage", {})):
game_data = multidata["datapackage"][game]
@@ -132,11 +133,13 @@ class WebHostContext(Context):
continue
else:
self.logger.warning(f"Did not find game_data_package for {game}: {game_data['checksum']}")
else:
missing_checksum = True # Game rolled on old AP and will load data package from multidata
self.gamespackage[game] = static_gamespackage.get(game, {})
self.item_name_groups[game] = static_item_name_groups.get(game, {})
self.location_name_groups[game] = static_location_name_groups.get(game, {})
if not game_data_packages:
if not game_data_packages and not missing_checksum:
# all static -> use the static dicts directly
self.gamespackage = static_gamespackage
self.item_name_groups = static_item_name_groups

View File

@@ -22,7 +22,7 @@ players to rely upon each other to complete their game.
While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows
players to randomize any of the supported games, and send items between them. This allows players of different
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworld.
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds.
Here is a list of our [Supported Games](https://archipelago.gg/games).
## Can I generate a single-player game with Archipelago?

View File

@@ -75,6 +75,27 @@
#inventory-table img.acquired.green{ /*32CD32*/
filter: hue-rotate(84deg) saturate(10) brightness(0.7);
}
#inventory-table img.acquired.hotpink{ /*FF69B4*/
filter: sepia(100%) hue-rotate(300deg) saturate(10);
}
#inventory-table img.acquired.lightsalmon{ /*FFA07A*/
filter: sepia(100%) hue-rotate(347deg) saturate(10);
}
#inventory-table img.acquired.crimson{ /*DB143B*/
filter: sepia(100%) hue-rotate(318deg) saturate(10) brightness(0.86);
}
#inventory-table span{
color: #B4B4A0;
font-size: 40px;
max-width: 40px;
max-height: 40px;
filter: grayscale(100%) contrast(75%) brightness(30%);
}
#inventory-table span.acquired{
filter: none;
}
#inventory-table div.image-stack{
display: grid;

View File

@@ -99,6 +99,52 @@
{% endif %}
</div>
</div>
{% if 'PrismBreak' in options or 'LockKeyAmadeus' in options or 'GateKeep' in options %}
<div class="table-row">
{% if 'PrismBreak' in options %}
<div class="C1">
<div class="image-stack">
<div class="stack-front">
<div class="stack-top-left">
<img src="{{ icons['Laser Access'] }}" class="hotpink {{ 'acquired' if 'Laser Access A' in acquired_items }}" title="Laser Access A" />
</div>
<div class="stack-top-right">
<img src="{{ icons['Laser Access'] }}" class="lightsalmon {{ 'acquired' if 'Laser Access I' in acquired_items }}" title="Laser Access I" />
</div>
<div class="stack-bottum-left">
<img src="{{ icons['Laser Access'] }}" class="crimson {{ 'acquired' if 'Laser Access M' in acquired_items }}" title="Laser Access M" />
</div>
</div>
</div>
</div>
{% endif %}
{% if 'LockKeyAmadeus' in options %}
<div class="C2">
<div class="image-stack">
<div class="stack-front">
<div class="stack-top-left">
<img src="{{ icons['Lab Glasses'] }}" class="{{ 'acquired' if 'Lab Access Genza' in acquired_items }}" title="Lab Access Genza" />
</div>
<div class="stack-top-right">
<img src="{{ icons['Eye Orb'] }}" class="{{ 'acquired' if 'Lab Access Dynamo' in acquired_items }}" title="Lab Access Dynamo" />
</div>
<div class="stack-bottum-left">
<img src="{{ icons['Lab Coat'] }}" class="{{ 'acquired' if 'Lab Access Research' in acquired_items }}" title="Lab Access Research" />
</div>
<div class="stack-bottum-right">
<img src="{{ icons['Demon'] }}" class="{{ 'acquired' if 'Lab Access Experiment' in acquired_items }}" title="Lab Access Experiment" />
</div>
</div>
</div>
</div>
{% endif %}
{% if 'GateKeep' in options %}
<div class="C3">
<span class="{{ 'acquired' if 'Drawbridge Key' in acquired_items }}" title="Drawbridge Key">&#10070;</span>
</div>
{% endif %}
</div>
{% endif %}
</div>
<table id="location-table">

View File

@@ -1071,6 +1071,11 @@ if "Timespinner" in network_data_package["games"]:
"Plasma Orb": "https://timespinnerwiki.com/mediawiki/images/4/44/Plasma_Orb.png",
"Kobo": "https://timespinnerwiki.com/mediawiki/images/c/c6/Familiar_Kobo.png",
"Merchant Crow": "https://timespinnerwiki.com/mediawiki/images/4/4e/Familiar_Crow.png",
"Laser Access": "https://timespinnerwiki.com/mediawiki/images/9/99/Historical_Documents.png",
"Lab Glasses": "https://timespinnerwiki.com/mediawiki/images/4/4a/Lab_Glasses.png",
"Eye Orb": "https://timespinnerwiki.com/mediawiki/images/a/a4/Eye_Orb.png",
"Lab Coat": "https://timespinnerwiki.com/mediawiki/images/5/51/Lab_Coat.png",
"Demon": "https://timespinnerwiki.com/mediawiki/images/f/f8/Familiar_Demon.png",
}
timespinner_location_ids = {
@@ -1118,6 +1123,9 @@ if "Timespinner" in network_data_package["games"]:
timespinner_location_ids["Ancient Pyramid"] += [
1337237, 1337238, 1337239,
1337240, 1337241, 1337242, 1337243, 1337244, 1337245]
if (slot_data["PyramidStart"]):
timespinner_location_ids["Ancient Pyramid"] += [
1337233, 1337234, 1337235]
display_data = {}

View File

@@ -370,19 +370,13 @@ target_group_lookup = bake_target_group_lookup(world, get_target_groups)
#### When to call `randomize_entrances`
The short answer is that you will almost always want to do ER in `pre_fill`. For more information why, continue reading.
The correct step for this is `World.connect_entrances`.
ER begins by collecting the entire item pool and then uses your access rules to try and prevent some kinds of failures.
This means 2 things about when you can call ER:
1. You must supply your item pool before calling ER, or call ER before setting any rules which require items.
2. If you have rules dependent on anything other than items (e.g. `Entrance`s or events), you must set your rules
and create your events before you call ER if you want to guarantee a correct output.
If the conditions above are met, you could theoretically do ER as early as `create_regions`. However, plando is also
a consideration. Since item plando happens between `set_rules` and `pre_fill` and modifies the item pool, doing ER
in `pre_fill` is the only way to account for placements made by item plando, otherwise you risk impossible seeds or
generation failures. Obviously, if your world implements entrance plando, you will likely want to do that before ER as
well.
Currently, you could theoretically do it as early as `World.create_regions` or as late as `pre_fill`.
However, there are upcoming changes to Item Plando and Generic Entrance Randomizer to make the two features work better
together.
These changes necessitate that entrance randomization is done exactly in `World.connect_entrances`.
It is fine for your Entrances to be connected differently or not at all before this step.
#### Informing your client about randomized entrances

View File

@@ -47,6 +47,9 @@ Packets are simple JSON lists in which any number of ordered network commands ca
An object can contain the "class" key, which will tell the content data type, such as "Version" in the following example.
Websocket connections should support per-message compression. Uncompressed connections are deprecated and may stop
working in the future.
Example:
```javascript
[{"cmd": "RoomInfo", "version": {"major": 0, "minor": 1, "build": 3, "class": "Version"}, "tags": ["WebHost"], ... }]
@@ -360,11 +363,11 @@ An enumeration containing the possible hint states.
```python
import enum
class HintStatus(enum.IntEnum):
HINT_FOUND = 0 # The location has been collected. Status cannot be changed once found.
HINT_UNSPECIFIED = 1 # The receiving player has not specified any status
HINT_UNSPECIFIED = 0 # The receiving player has not specified any status
HINT_NO_PRIORITY = 10 # The receiving player has specified that the item is unneeded
HINT_AVOID = 20 # The receiving player has specified that the item is detrimental
HINT_PRIORITY = 30 # The receiving player has specified that the item is needed
HINT_FOUND = 40 # The location has been collected. Status cannot be changed once found.
```
- Hints for items with `ItemClassification.trap` default to `HINT_AVOID`.
- Hints created with `LocationScouts`, `!hint_location`, or similar (hinting a location) default to `HINT_UNSPECIFIED`.
@@ -530,9 +533,9 @@ In JSON this may look like:
{"item": 3, "location": 3, "player": 3, "flags": 0}
]
```
`item` is the item id of the item. Item ids are only supported in the range of [-2<sup>53</sup>, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
`item` is the item id of the item. Item ids are only supported in the range of [-2<sup>53</sup> + 1, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
`location` is the location id of the item inside the world. Location ids are only supported in the range of [-2<sup>53</sup>, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
`location` is the location id of the item inside the world. Location ids are only supported in the range of [-2<sup>53</sup> + 1, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
`player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item
@@ -745,6 +748,7 @@ Tags are represented as a list of strings, the common client tags follow:
| 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.² |
| NoText | Indicates the client does not want to receive text messages, improving performance if not needed. |
¹: 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.

View File

@@ -73,11 +73,11 @@ When tests are run, this class will create a multiworld with a single player hav
generic tests, as well as the new custom test. Each test method definition will create its own separate solo multiworld
that will be cleaned up after. If you don't want to run the generic tests on a base, `run_default_tests` can be
overridden. For more information on what methods are available to your class, check the
[WorldTestBase definition](/test/bases.py#L104).
[WorldTestBase definition](/test/bases.py#L106).
#### Alternatives to WorldTestBase
Unit tests can also be created using [TestBase](/test/bases.py#L14) or
Unit tests can also be created using [TestBase](/test/bases.py#L16) or
[unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) depending on your use case. These
may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for
testing portions of your code that can be tested without relying on a multiworld to be created first.

View File

@@ -222,8 +222,8 @@ could also be progress in a research tree, or even something more abstract like
Each location has a `name` and an `address` (hereafter referred to as an `id`), is placed in a Region, has access rules,
and has a classification. The name needs to be unique within each game and must not be numeric (must contain least 1
letter or symbol). The ID needs to be unique across all games, and is best kept in the same range as the item IDs.
Locations and items can share IDs, so typically a game's locations and items start at the same ID.
letter or symbol). The ID needs to be unique across all locations within the game.
Locations and items can share IDs, and locations can share IDs with other games' locations.
World-specific IDs must be in the range 1 to 2<sup>53</sup>-1; IDs ≤ 0 are global and reserved.
@@ -243,7 +243,9 @@ progression. Progression items will be assigned to locations with higher priorit
and satisfy progression balancing.
The name needs to be unique within each game, meaning if you need to create multiple items with the same name, they
will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol).
will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol).
The ID thus also needs to be unique across all items with different names within the game.
Items and locations can share IDs, and items can share IDs with other games' items.
Other classifications include:
@@ -289,7 +291,7 @@ like entrance randomization in logic.
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L295-L296)),
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)),
from which the logic unfolds. AP assumes that a player will always be able to return to this starting region by resetting the game ("Save and quit").
### Entrances
@@ -329,7 +331,7 @@ Even doing `state.can_reach_location` or `state.can_reach_entrance` is problemat
You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
Alternatively, you can set [world.explicit_indirect_conditions = False](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L301),
Alternatively, you can set [world.explicit_indirect_conditions = False](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L301-L304),
avoiding the need for indirect conditions at the expense of performance.
### Item Rules
@@ -490,6 +492,9 @@ In addition, the following methods can be implemented and are called in this ord
after this step. Locations cannot be moved to different regions after this step.
* `set_rules(self)`
called to set access and item rules on locations and entrances.
* `connect_entrances(self)`
by the end of this step, all entrances must exist and be connected to their source and target regions.
Entrance randomization should be done here.
* `generate_basic(self)`
player-specific randomization that does not affect logic can be done here.
* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)`
@@ -557,17 +562,13 @@ from .items import is_progression # this is just a dummy
def create_item(self, item: str) -> MyGameItem:
# this is called when AP wants to create an item by name (for plando) or when you call it from your own code
classification = ItemClassification.progression if is_progression(item) else
ItemClassification.filler
return MyGameItem(item, classification, self.item_name_to_id[item],
self.player)
classification = ItemClassification.progression if is_progression(item) else ItemClassification.filler
return MyGameItem(item, classification, self.item_name_to_id[item], self.player)
def create_event(self, event: str) -> MyGameItem:
# while we are at it, we can also add a helper to create events
return MyGameItem(event, True, None, self.player)
return MyGameItem(event, ItemClassification.progression, None, self.player)
```
#### create_items
@@ -835,14 +836,16 @@ def generate_output(self, output_directory: str) -> None:
### Slot Data
If the game client needs to know information about the generated seed, a preferred method of transferring the data
is through the slot data. This is filled with the `fill_slot_data` method of your world by returning
a `dict` with `str` keys that can be serialized with json.
But, to not waste resources, it should be limited to data that is absolutely necessary. Slot data is sent to your client
once it has successfully [connected](network%20protocol.md#connected).
If a client or tracker needs to know information about the generated seed, a preferred method of transferring the data
is through the slot data. This is filled with the `fill_slot_data` method of your world by returning a `dict` with
`str` keys that can be serialized with json. However, to not waste resources, it should be limited to data that is
absolutely necessary. Slot data is sent to your client once it has successfully
[connected](network%20protocol.md#connected).
If you need to know information about locations in your world, instead of propagating the slot data, it is preferable
to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. The most
common usage of slot data is sending option results that the client needs to be aware of.
to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. Adding
item/location pairs is unnecessary since the AP server already retains and freely gives that information to clients
that request it. The most common usage of slot data is sending option results that the client needs to be aware of.
```python
def fill_slot_data(self) -> Dict[str, Any]:

View File

@@ -157,17 +157,16 @@ class ERPlacementState:
def placed_regions(self) -> set[Region]:
return self.collection_state.reachable_regions[self.world.player]
def find_placeable_exits(self, check_validity: bool) -> list[Entrance]:
def find_placeable_exits(self, check_validity: bool, usable_exits: list[Entrance]) -> list[Entrance]:
if check_validity:
blocked_connections = self.collection_state.blocked_connections[self.world.player]
blocked_connections = sorted(blocked_connections, key=lambda x: x.name)
placeable_randomized_exits = [connection for connection in blocked_connections
if not connection.connected_region
and connection.is_valid_source_transition(self)]
placeable_randomized_exits = [ex for ex in usable_exits
if not ex.connected_region
and ex in blocked_connections
and ex.is_valid_source_transition(self)]
else:
# this is on a beaten minimal attempt, so any exit anywhere is fair game
placeable_randomized_exits = [ex for region in self.world.multiworld.get_regions(self.world.player)
for ex in region.exits if not ex.connected_region]
placeable_randomized_exits = [ex for ex in usable_exits if not ex.connected_region]
self.world.random.shuffle(placeable_randomized_exits)
return placeable_randomized_exits
@@ -181,7 +180,8 @@ class ERPlacementState:
self.placements.append(source_exit)
self.pairings.append((source_exit.name, target_entrance.name))
def test_speculative_connection(self, source_exit: Entrance, target_entrance: Entrance) -> bool:
def test_speculative_connection(self, source_exit: Entrance, target_entrance: Entrance,
usable_exits: set[Entrance]) -> bool:
copied_state = self.collection_state.copy()
# simulated connection. A real connection is unsafe because the region graph is shallow-copied and would
# propagate back to the real multiworld.
@@ -198,6 +198,9 @@ class ERPlacementState:
# ignore the source exit, and, if coupled, the reverse exit. They're not actually new
if _exit.name == source_exit.name or (self.coupled and _exit.name == target_entrance.name):
continue
# make sure we are only paying attention to usable exits
if _exit not in usable_exits:
continue
# technically this should be is_valid_source_transition, but that may rely on side effects from
# on_connect, which have not happened here (because we didn't do a real connection, and if we did, we would
# not want them to persist). can_reach is a close enough approximation most of the time.
@@ -326,6 +329,24 @@ def randomize_entrances(
# similar to fill, skip validity checks on entrances if the game is beatable on minimal accessibility
perform_validity_check = True
if not er_targets:
er_targets = sorted([entrance for region in world.multiworld.get_regions(world.player)
for entrance in region.entrances if not entrance.parent_region], key=lambda x: x.name)
if not exits:
exits = sorted([ex for region in world.multiworld.get_regions(world.player)
for ex in region.exits if not ex.connected_region], key=lambda x: x.name)
if len(er_targets) != len(exits):
raise EntranceRandomizationError(f"Unable to randomize entrances due to a mismatched count of "
f"entrances ({len(er_targets)}) and exits ({len(exits)}.")
# used when membership checks are needed on the exit list, e.g. speculative sweep
exits_set = set(exits)
for entrance in er_targets:
entrance_lookup.add(entrance)
# place the menu region and connected start region(s)
er_state.collection_state.update_reachable_regions(world.player)
def do_placement(source_exit: Entrance, target_entrance: Entrance) -> None:
placed_exits, removed_entrances = er_state.connect(source_exit, target_entrance)
# remove the placed targets from consideration
@@ -339,7 +360,7 @@ def randomize_entrances(
def find_pairing(dead_end: bool, require_new_exits: bool) -> bool:
nonlocal perform_validity_check
placeable_exits = er_state.find_placeable_exits(perform_validity_check)
placeable_exits = er_state.find_placeable_exits(perform_validity_check, exits)
for source_exit in placeable_exits:
target_groups = target_group_lookup[source_exit.randomization_group]
for target_entrance in entrance_lookup.get_targets(target_groups, dead_end, preserve_group_order):
@@ -355,7 +376,7 @@ def randomize_entrances(
and len(placeable_exits) == 1)
if exit_requirement_satisfied and source_exit.can_connect_to(target_entrance, dead_end, er_state):
if (needs_speculative_sweep
and not er_state.test_speculative_connection(source_exit, target_entrance)):
and not er_state.test_speculative_connection(source_exit, target_entrance, exits_set)):
continue
do_placement(source_exit, target_entrance)
return True
@@ -378,13 +399,14 @@ def randomize_entrances(
and world.multiworld.has_beaten_game(er_state.collection_state, world.player):
# ensure that we have enough locations to place our progression
accessible_location_count = 0
prog_item_count = sum(er_state.collection_state.prog_items[world.player].values())
prog_item_count = len([item for item in world.multiworld.itempool if item.advancement and item.player == world.player])
# short-circuit location checking in this case
if prog_item_count == 0:
return True
for region in er_state.placed_regions:
for loc in region.locations:
if loc.can_reach(er_state.collection_state):
if not loc.item and loc.can_reach(er_state.collection_state):
# don't count locations with preplaced items
accessible_location_count += 1
if accessible_location_count >= prog_item_count:
perform_validity_check = False
@@ -406,21 +428,6 @@ def randomize_entrances(
f"All unplaced entrances: {unplaced_entrances}\n"
f"All unplaced exits: {unplaced_exits}")
if not er_targets:
er_targets = sorted([entrance for region in world.multiworld.get_regions(world.player)
for entrance in region.entrances if not entrance.parent_region], key=lambda x: x.name)
if not exits:
exits = sorted([ex for region in world.multiworld.get_regions(world.player)
for ex in region.exits if not ex.connected_region], key=lambda x: x.name)
if len(er_targets) != len(exits):
raise EntranceRandomizationError(f"Unable to randomize entrances due to a mismatched count of "
f"entrances ({len(er_targets)}) and exits ({len(exits)}.")
for entrance in er_targets:
entrance_lookup.add(entrance)
# place the menu region and connected start region(s)
er_state.collection_state.update_reachable_regions(world.player)
# stage 1 - try to place all the non-dead-end entrances
while entrance_lookup.others:
if not find_pairing(dead_end=False, require_new_exits=True):

19
kvui.py
View File

@@ -26,6 +26,10 @@ import Utils
if Utils.is_frozen():
os.environ["KIVY_DATA_DIR"] = Utils.local_path("data")
import platformdirs
os.environ["KIVY_HOME"] = os.path.join(platformdirs.user_config_dir("Archipelago", False), "kivy")
os.makedirs(os.environ["KIVY_HOME"], exist_ok=True)
from kivy.config import Config
Config.set("input", "mouse", "mouse,disable_multitouch")
@@ -440,8 +444,11 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
if child.collide_point(*touch.pos):
key = child.sort_key
if key == "status":
parent.hint_sorter = lambda element: element["status"]["hint"]["status"]
else: parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower()
parent.hint_sorter = lambda element: status_sort_weights[element["status"]["hint"]["status"]]
else:
parent.hint_sorter = lambda element: (
remove_between_brackets.sub("", element[key]["text"]).lower()
)
if key == parent.sort_key:
# second click reverses order
parent.reversed = not parent.reversed
@@ -825,7 +832,13 @@ status_colors: typing.Dict[HintStatus, str] = {
HintStatus.HINT_AVOID: "salmon",
HintStatus.HINT_PRIORITY: "plum",
}
status_sort_weights: dict[HintStatus, int] = {
HintStatus.HINT_FOUND: 0,
HintStatus.HINT_UNSPECIFIED: 1,
HintStatus.HINT_NO_PRIORITY: 2,
HintStatus.HINT_AVOID: 3,
HintStatus.HINT_PRIORITY: 4,
}
class HintLog(RecycleView):

View File

@@ -109,7 +109,7 @@ class Group:
def get_type_hints(cls) -> Dict[str, Any]:
"""Returns resolved type hints for the class"""
if cls._type_cache is None:
if not isinstance(next(iter(cls.__annotations__.values())), str):
if not cls.__annotations__ or not isinstance(next(iter(cls.__annotations__.values())), str):
# non-str: assume already resolved
cls._type_cache = cls.__annotations__
else:
@@ -270,15 +270,20 @@ class Group:
# fetch class to avoid going through getattr
cls = self.__class__
type_hints = cls.get_type_hints()
entries = [e for e in self]
if not entries:
# write empty dict for empty Group with no instance values
cls._dump_value({}, f, indent=" " * level)
# validate group
for name in cls.__annotations__.keys():
assert hasattr(cls, name), f"{cls}.{name} is missing a default value"
# dump ordered members
for name in self:
for name in entries:
attr = cast(object, getattr(self, name))
attr_cls = type_hints[name] if name in type_hints else attr.__class__
attr_cls_origin = typing.get_origin(attr_cls)
while attr_cls_origin is Union: # resolve to first type for doc string
# resolve to first type for doc string
while attr_cls_origin is Union or attr_cls_origin is types.UnionType:
attr_cls = typing.get_args(attr_cls)[0]
attr_cls_origin = typing.get_origin(attr_cls)
if attr_cls.__doc__ and attr_cls.__module__ != "builtins":
@@ -787,7 +792,17 @@ class Settings(Group):
if location:
from Utils import parse_yaml
with open(location, encoding="utf-8-sig") as f:
options = parse_yaml(f.read())
from yaml.error import MarkedYAMLError
try:
options = parse_yaml(f.read())
except MarkedYAMLError as ex:
if ex.problem_mark:
f.seek(0)
lines = f.readlines()
problem_line = lines[ex.problem_mark.line]
error_line = " " * ex.problem_mark.column + "^"
raise Exception(f"{ex.context} {ex.problem}\n{problem_line}{error_line}")
raise ex
# TODO: detect if upgrade is required
# TODO: once we have a cache for _world_settings_name_cache, detect if any game section is missing
self.update(options or {})

View File

@@ -18,7 +18,15 @@ def run_locations_benchmark():
class BenchmarkRunner:
gen_steps: typing.Tuple[str, ...] = (
"generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
"generate_early",
"create_regions",
"create_items",
"set_rules",
"connect_entrances",
"generate_basic",
"pre_fill",
)
rule_iterations: int = 100_000
if sys.version_info >= (3, 9):

View File

@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.5)
cmake_minimum_required(VERSION 3.16)
project(ap-cpp-tests)
enable_testing()
@@ -7,8 +7,8 @@ find_package(GTest REQUIRED)
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
add_definitions("/source-charset:utf-8")
set(CMAKE_CXX_FLAGS_DEBUG "/MTd")
set(CMAKE_CXX_FLAGS_RELEASE "/MT")
# set(CMAKE_CXX_FLAGS_DEBUG "/MDd") # this is the default
# set(CMAKE_CXX_FLAGS_RELEASE "/MD") # this is the default
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# enable static analysis for gcc
add_compile_options(-fanalyzer -Werror)

View File

@@ -5,7 +5,15 @@ from BaseClasses import CollectionState, Item, ItemClassification, Location, Mul
from worlds import network_data_package
from worlds.AutoWorld import World, call_all
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
gen_steps = (
"generate_early",
"create_regions",
"create_items",
"set_rules",
"connect_entrances",
"generate_basic",
"pre_fill",
)
def setup_solo_multiworld(

View File

@@ -311,6 +311,37 @@ class TestRandomizeEntrances(unittest.TestCase):
self.assertEqual([], [exit_ for region in multiworld.get_regions()
for exit_ in region.exits if not exit_.connected_region])
def test_minimal_entrance_rando_with_collect_override(self):
"""
tests that entrance randomization can complete with minimal accessibility and unreachable exits
when the world defines a collect override that add extra values to prog_items
"""
multiworld = generate_test_multiworld()
multiworld.worlds[1].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
multiworld.completion_condition[1] = lambda state: state.can_reach("region24", player=1)
generate_disconnected_region_grid(multiworld, 5, 1)
prog_items = generate_items(10, 1, True)
multiworld.itempool += prog_items
filler_items = generate_items(15, 1, False)
multiworld.itempool += filler_items
e = multiworld.get_entrance("region1_right", 1)
set_rule(e, lambda state: False)
old_collect = multiworld.worlds[1].collect
def new_collect(state, item):
old_collect(state, item)
state.prog_items[item.player]["counter"] += 300
multiworld.worlds[1].collect = new_collect
randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
self.assertEqual([], [entrance for region in multiworld.get_regions()
for entrance in region.entrances if not entrance.parent_region])
self.assertEqual([], [exit_ for region in multiworld.get_regions()
for exit_ in region.exits if not exit_.connected_region])
def test_restrictive_region_requirement_does_not_fail(self):
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 2, 1)

View File

@@ -0,0 +1,63 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister, call_all, World
from . import setup_solo_multiworld
class TestBase(unittest.TestCase):
def test_entrance_connection_steps(self):
"""Tests that Entrances are connected and not changed after connect_entrances."""
def get_entrance_name_to_source_and_target_dict(world: World):
return [
(entrance.name, entrance.parent_region, entrance.connected_region)
for entrance in world.get_entrances()
]
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances")
additional_steps = ("generate_basic", "pre_fill")
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
original_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])
self.assertTrue(
all(entrance[1] is not None and entrance[2] is not None for entrance in original_entrances),
f"{game_name} had unconnected entrances after connect_entrances"
)
for step in additional_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)
step_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])
self.assertEqual(
original_entrances, step_entrances, f"{game_name} modified entrances during {step}"
)
def test_all_state_before_connect_entrances(self):
"""Before connect_entrances, Entrance objects may be unconnected.
Thus, we test that get_all_state is performed with allow_partial_entrances if used before or during
connect_entrances."""
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances")
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, ())
original_get_all_state = multiworld.get_all_state
def patched_get_all_state(use_cache: bool, allow_partial_entrances: bool = False):
self.assertTrue(allow_partial_entrances, (
"Before the connect_entrances step finishes, other worlds might still have partial entrances. "
"As such, any call to get_all_state must use allow_partial_entrances = True."
))
return original_get_all_state(use_cache, allow_partial_entrances)
multiworld.get_all_state = patched_get_all_state
for step in gen_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)

View File

@@ -1,5 +1,6 @@
import unittest
from BaseClasses import CollectionState
from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_solo_multiworld
@@ -8,12 +9,31 @@ class TestBase(unittest.TestCase):
def test_create_item(self):
"""Test that a world can successfully create all items in its datapackage"""
for game_name, world_type in AutoWorldRegister.world_types.items():
proxy_world = setup_solo_multiworld(world_type, ()).worlds[1]
multiworld = setup_solo_multiworld(world_type, steps=("generate_early", "create_regions", "create_items"))
proxy_world = multiworld.worlds[1]
for item_name in world_type.item_name_to_id:
test_state = CollectionState(multiworld)
with self.subTest("Create Item", item_name=item_name, game_name=game_name):
item = proxy_world.create_item(item_name)
with self.subTest("Item Name", item_name=item_name, game_name=game_name):
self.assertEqual(item.name, item_name)
if item.advancement:
with self.subTest("Item State Collect", item_name=item_name, game_name=game_name):
test_state.collect(item, True)
with self.subTest("Item State Remove", item_name=item_name, game_name=game_name):
test_state.remove(item)
self.assertEqual(test_state.prog_items, multiworld.state.prog_items,
"Item Collect -> Remove should restore empty state.")
else:
with self.subTest("Item State Collect No Change", item_name=item_name, game_name=game_name):
# Non-Advancement should not modify state.
test_state.collect(item)
self.assertEqual(test_state.prog_items, multiworld.state.prog_items)
def test_item_name_group_has_valid_item(self):
"""Test that all item name groups contain valid items. """
# This cannot test for Event names that you may have declared for logic, only sendable Items.
@@ -67,7 +87,7 @@ class TestBase(unittest.TestCase):
def test_itempool_not_modified(self):
"""Test that worlds don't modify the itempool after `create_items`"""
gen_steps = ("generate_early", "create_regions", "create_items")
additional_steps = ("set_rules", "generate_basic", "pre_fill")
additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill")
excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3")
worlds_to_test = {game: world
for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games}
@@ -84,7 +104,7 @@ class TestBase(unittest.TestCase):
def test_locality_not_modified(self):
"""Test that worlds don't modify the locality of items after duplicates are resolved"""
gen_steps = ("generate_early", "create_regions", "create_items")
additional_steps = ("set_rules", "generate_basic", "pre_fill")
additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill")
worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items()}
for game_name, world_type in worlds_to_test.items():
with self.subTest("Game", game=game_name):

View File

@@ -45,6 +45,12 @@ class TestBase(unittest.TestCase):
self.assertEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during rule creation")
call_all(multiworld, "connect_entrances")
self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during rule creation")
self.assertEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during rule creation")
call_all(multiworld, "generate_basic")
self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during generate_basic")

View File

@@ -1,5 +1,6 @@
import unittest
from BaseClasses import MultiWorld
from worlds.AutoWorld import AutoWorldRegister
from . import setup_solo_multiworld
@@ -9,8 +10,12 @@ class TestWorldMemory(unittest.TestCase):
"""Tests that worlds don't leak references to MultiWorld or themselves with default options."""
import gc
import weakref
refs: dict[str, weakref.ReferenceType[MultiWorld]] = {}
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
with self.subTest("Game creation", game_name=game_name):
weak = weakref.ref(setup_solo_multiworld(world_type))
gc.collect()
refs[game_name] = weak
gc.collect()
for game_name, weak in refs.items():
with self.subTest("Game cleanup", game_name=game_name):
self.assertFalse(weak(), "World leaked a reference")

View File

@@ -2,11 +2,11 @@ import unittest
from BaseClasses import CollectionState
from worlds.AutoWorld import AutoWorldRegister
from . import setup_solo_multiworld
from . import setup_solo_multiworld, gen_steps
class TestBase(unittest.TestCase):
gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
gen_steps = gen_steps
default_settings_unreachable_regions = {
"A Link to the Past": {

View File

@@ -0,0 +1,29 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_solo_multiworld
class TestBase(unittest.TestCase):
gen_steps = (
"generate_early",
"create_regions",
)
test_steps = (
"create_items",
"set_rules",
"connect_entrances",
"generate_basic",
"pre_fill",
)
def test_all_state_is_available(self):
"""Ensure all_state can be created at certain steps."""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type, self.gen_steps)
for step in self.test_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)
self.assertTrue(multiworld.get_all_state(False, True))

View File

@@ -378,6 +378,10 @@ class World(metaclass=AutoWorldRegister):
"""Method for setting the rules on the World's regions and locations."""
pass
def connect_entrances(self) -> None:
"""Method to finalize the source and target regions of the World's entrances"""
pass
def generate_basic(self) -> None:
"""
Useful for randomizing things that don't affect logic but are better to be determined before the output stage.

View File

@@ -87,7 +87,7 @@ class Component:
processes = weakref.WeakSet()
def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None:
def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
global processes
import multiprocessing
process = multiprocessing.Process(target=func, name=name, args=args)
@@ -95,6 +95,14 @@ def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] =
processes.add(process)
def launch(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
from Utils import is_kivy_running
if is_kivy_running():
launch_subprocess(func, name, args)
else:
func(*args)
class SuffixIdentifier:
suffixes: Iterable[str]
@@ -111,7 +119,7 @@ class SuffixIdentifier:
def launch_textclient(*args):
import CommonClient
launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args)
launch(CommonClient.run_as_textclient, name="TextClient", args=args)
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:

View File

@@ -55,6 +55,7 @@ async def lock(ctx) -> None
async def unlock(ctx) -> None
async def get_hash(ctx) -> str
async def get_memory_size(ctx, domain: str) -> int
async def get_system(ctx) -> str
async def get_cores(ctx) -> dict[str, str]
async def ping(ctx) -> None
@@ -168,9 +169,10 @@ select dialog and they will be associated with BizHawkClient. This does not affe
associate the file extension with Archipelago.
`validate_rom` is called to figure out whether a given ROM belongs to your client. It will only be called when a ROM is
running on a system you specified in your `system` class variable. In most cases, that will be a single system and you
can be sure that you're not about to try to read from nonexistent domains or out of bounds. If you decide to claim this
ROM as yours, this is where you should do setup for things like `items_handling`.
running on a system you specified in your `system` class variable. Take extra care here, because your code will run
against ROMs that you have no control over. If you're reading an address deep in ROM, you might want to check the size
of ROM before you attempt to read it using `get_memory_size`. If you decide to claim this ROM as yours, this is where
you should do setup for things like `items_handling`.
`game_watcher` is the "main loop" of your client where you should be checking memory and sending new items to the ROM.
`BizHawkClient` will make sure that your `game_watcher` only runs when your client has validated the ROM, and will do
@@ -268,6 +270,8 @@ server connection before trying to interact with it.
- By default, the player will be asked to provide their slot name after connecting to the server and validating, and
that input will be used to authenticate with the `Connect` command. You can override `set_auth` in your own client to
set it automatically based on data in the ROM or on your client instance.
- Use `get_memory_size` inside `validate_rom` if you need to read at large addresses, in case some other game has a
smaller ROM size.
- You can override `on_package` in your client to watch raw packages, but don't forget you also have access to a
subclass of `CommonContext` and its API.
- You can import `BizHawkClientContext` for type hints using `typing.TYPE_CHECKING`. Importing it without conditions at

View File

@@ -7,7 +7,7 @@ from __future__ import annotations
import abc
from typing import TYPE_CHECKING, Any, ClassVar
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch as launch_component
if TYPE_CHECKING:
from .context import BizHawkClientContext
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
def launch_client(*args) -> None:
from .context import launch
launch_subprocess(launch, name="BizHawkClient", args=args)
launch_component(launch, name="BizHawkClient", args=args)
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,

View File

@@ -41,6 +41,7 @@ class BizHawkClientCommandProcessor(ClientCommandProcessor):
class BizHawkClientContext(CommonContext):
command_processor = BizHawkClientCommandProcessor
server_seed_name: str | None = None
auth_status: AuthStatus
password_requested: bool
client_handler: BizHawkClient | None
@@ -68,6 +69,8 @@ class BizHawkClientContext(CommonContext):
if cmd == "Connected":
self.slot_data = args.get("slot_data", None)
self.auth_status = AuthStatus.AUTHENTICATED
elif cmd == "RoomInfo":
self.server_seed_name = args.get("seed_name", None)
if self.client_handler is not None:
self.client_handler.on_package(self, cmd, args)
@@ -100,6 +103,7 @@ class BizHawkClientContext(CommonContext):
async def disconnect(self, allow_autoreconnect: bool=False):
self.auth_status = AuthStatus.NOT_AUTHENTICATED
self.server_seed_name = None
await super().disconnect(allow_autoreconnect)
@@ -238,6 +242,7 @@ def _patch_and_run_game(patch_file: str):
return metadata
except Exception as exc:
logger.exception(exc)
Utils.messagebox("Error Patching Game", str(exc), True)
return {}

View File

@@ -338,7 +338,7 @@ class MinExtraYarn(Range):
There must be at least this much more yarn over the total number of yarn needed to craft all hats.
For example, if this option's value is 10, and the total yarn needed to craft all hats is 40,
there must be at least 50 yarn in the pool."""
display_name = "Max Extra Yarn"
display_name = "Min Extra Yarn"
range_start = 5
range_end = 15
default = 10

View File

@@ -12,13 +12,13 @@ from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses
from worlds.AutoWorld import World, WebWorld, CollectionState
from worlds.generic.Rules import add_rule
from typing import List, Dict, TextIO
from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type
from worlds.LauncherComponents import Component, components, icon_paths, launch as launch_component, Type
from Utils import local_path
def launch_client():
from .Client import launch
launch_subprocess(launch, name="AHITClient")
launch_component(launch, name="AHITClient")
components.append(Component("A Hat in Time Client", "AHITClient", func=launch_client,

View File

@@ -21,7 +21,7 @@
3. Click the **Betas** tab. In the **Beta Participation** dropdown, select `tcplink`.
While it downloads, you can subscribe to the [Archipelago workshop mod.]((https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601))
While it downloads, you can subscribe to the [Archipelago workshop mod](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601).
4. Once the game finishes downloading, start it up.
@@ -62,4 +62,4 @@ The level that the relic set unlocked will stay unlocked.
### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work!
There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly
if you have too many save files. Delete them and it should fix the problem.
if you have too many save files. Delete them and it should fix the problem.

View File

@@ -1547,9 +1547,9 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x18003B, 0x01 if world.map_shuffle[player] else 0x00) # maps showing crystals on overworld
# compasses showing dungeon count
if local_world.clock_mode or not world.dungeon_counters[player]:
if local_world.clock_mode or world.dungeon_counters[player] == 'off':
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
elif world.dungeon_counters[player] is True:
elif world.dungeon_counters[player] == 'on':
rom.write_byte(0x18003C, 0x02) # always on
elif world.compass_shuffle[player] or world.dungeon_counters[player] == 'pickup':
rom.write_byte(0x18003C, 0x01) # show on pickup

View File

@@ -1125,7 +1125,7 @@ def set_trock_key_rules(world, player):
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']:
set_rule(world.get_entrance(entrance, player), lambda state: False)
all_state = world.get_all_state(use_cache=False)
all_state = world.get_all_state(use_cache=False, allow_partial_entrances=True)
all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work
all_state.stale[player] = True

View File

@@ -89,7 +89,7 @@ location_names: Dict[str, str] = {
"RESCUED_CHERUB_15": "DC: Top of elevator Child of Moonlight",
"Lady[D01Z05S22]": "DC: Lady of the Six Sorrows, from MD",
"QI75": "DC: Chalice room",
"Sword[D01Z05S24]": "DC: Mea culpa altar",
"Sword[D01Z05S24]": "DC: Mea Culpa altar",
"CO44": "DC: Elevator shaft ledge",
"RESCUED_CHERUB_22": "DC: Elevator shaft Child of Moonlight",
"Lady[D01Z05S26]": "DC: Lady of the Six Sorrows, elevator shaft",

View File

@@ -67,7 +67,8 @@ class BlasphemousWorld(World):
def generate_early(self):
if not self.options.starting_location.randomized:
if self.options.starting_location == "mourning_havoc" and self.options.difficulty < 2:
if (self.options.starting_location == "knot_of_words" or self.options.starting_location == "rooftops" \
or 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.")
@@ -83,6 +84,8 @@ class BlasphemousWorld(World):
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
if self.options.difficulty < 2:
locations.remove(4)
locations.remove(5)
locations.remove(6)
if self.options.dash_shuffle:

View File

@@ -85,20 +85,7 @@ class TestGrievanceHard(BlasphemousTestBase):
}
class TestKnotOfWordsEasy(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
"difficulty": "easy"
}
class TestKnotOfWordsNormal(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
"difficulty": "normal"
}
# knot of the three words, rooftops, and mourning and havoc can't be selected on easy or normal. hard only
class TestKnotOfWordsHard(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
@@ -106,20 +93,6 @@ class TestKnotOfWordsHard(BlasphemousTestBase):
}
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",
@@ -127,7 +100,6 @@ class TestRooftopsHard(BlasphemousTestBase):
}
# mourning and havoc can't be selected on easy or normal. hard only
class TestMourningHavocHard(BlasphemousTestBase):
options = {
"starting_location": "mourning_havoc",

View File

@@ -12,6 +12,12 @@
1. Download the above release and extract it.
## Installation Procedures (Linux and Steam Deck)
1. Download the above release and extract it.
2. Add Celeste64.exe to Steam as a Non-Steam Game. In the properties for it on Steam, set it to use Proton as the compatibility tool. Launch the game through Steam in order to run it.
## Joining a MultiWorld Game
1. Before launching the game, edit the `AP.json` file in the root of the Celeste 64 install.
@@ -33,5 +39,3 @@ An Example `AP.json` file:
"Password": ""
}
```

View File

@@ -25,19 +25,10 @@ class DarkSouls3Web(WebWorld):
"English",
"setup_en.md",
"setup/en",
["Marech"]
["Natalie", "Marech"]
)
setup_fr = Tutorial(
setup_en.tutorial_name,
setup_en.description,
"Français",
"setup_fr.md",
"setup/fr",
["Marech"]
)
tutorials = [setup_en, setup_fr]
tutorials = [setup_en]
option_groups = option_groups
item_descriptions = item_descriptions
rich_text_options_doc = True

View File

@@ -3,11 +3,13 @@
## Required Software
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
- [Dark Souls III AP Client](https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest)
- [Dark Souls III AP Client]
[Dark Souls III AP Client]: https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest
## Optional Software
- Map tracker not yet updated for 3.0.0
- [Map tracker](https://github.com/TVV1GK/DS3_AP_Maptracker)
## Setting Up
@@ -73,3 +75,65 @@ things to keep in mind:
[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/8.0
[WINE]: https://www.winehq.org/
## Troubleshooting
### Enemy randomizer issues
The DS3 Archipelago randomizer uses [thefifthmatt's DS3 enemy randomizer],
essentially unchanged. Unfortunately, this randomizer has a few known issues,
including enemy AI not working, enemies spawning in places they can't be killed,
and, in a few rare cases, enemies spawning in ways that crash the game when they
load. These bugs should be [reported upstream], but unfortunately the
Archipelago devs can't help much with them.
[thefifthmatt's DS3 enemy randomizer]: https://www.nexusmods.com/darksouls3/mods/484
[reported upstream]: https://github.com/thefifthmatt/SoulsRandomizers/issues
Because in rare cases the enemy randomizer can cause seeds to be impossible to
complete, we recommend disabling it for large async multiworlds for safety
purposes.
### `launchmod_darksouls3.bat` isn't working
Sometimes `launchmod_darksouls3.bat` will briefly flash a terminal on your
screen and then terminate without actually starting the game. This is usually
caused by some issue communicating with Steam either to find `DarkSoulsIII.exe`
or to launch it properly. If this is happening to you, make sure:
* You have DS3 1.15.2 installed. This is the latest patch as of January 2025.
(Note that older versions of Archipelago required an older patch, but that
_will not work_ with the current version.)
* You own the DS3 DLC if your randomizer config has DLC enabled. (It's possible,
but unconfirmed, that you need the DLC even when it's disabled in your config).
* Steam is not running in administrator mode. To fix this, right-click
`steam.exe` (by default this is in `C:\Program Files\Steam`), select
"Properties", open the "Compatiblity" tab, and uncheck "Run this program as an
administrator".
* There is no `dinput8.dll` file in your DS3 game directory. This is the old way
of installing mods, and it can interfere with the new ModEngine2 workflow.
If you've checked all of these, you can also try:
* Running `launchmod_darksouls3.bat` as an administrator.
* Reinstalling DS3 or even reinstalling Steam itself.
* Making sure DS3 is installed on the same drive as Steam and as the randomizer.
(A number of users are able to run these on different drives, but this has
helped some users.)
If none of this works, unfortunately there's not much we can do. We use
ModEngine2 to launch DS3 with the Archipelago mod enabled, but unfortunately
it's no longer maintained and its successor, ModEngine3, isn't usable yet.
### `DS3Randomizer.exe` isn't working
This is almost always caused by using a version of the randomizer client that's
not compatible with the version used to generate the multiworld. If you're
generating your multiworld on archipelago.gg, you *must* use the latest [Dark
Souls III AP Client]. If you want to use a different client version, you *must*
generate the multiworld locally using the apworld bundled with the client.

View File

@@ -1,33 +0,0 @@
# Guide d'installation de Dark Souls III Randomizer
## Logiciels requis
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
- [Client AP de Dark Souls III](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases)
## Concept général
Le client Archipelago de Dark Souls III est un fichier dinput8.dll. Cette .dll va lancer une invite de commande Windows
permettant de lire des informations de la partie et écrire des commandes pour intéragir avec le serveur Archipelago.
## Procédures d'installation
<span style="color:#ff7800">
**Il y a des risques de bannissement permanent des serveurs FromSoftware si ce mod est utilisé en ligne.**
</span>
Ce client a été testé sur la version Steam officielle du jeu (v1.15/1.35), peu importe les DLCs actuellement installés.
Télécharger le fichier dinput8.dll disponible dans le [Client AP de Dark Souls III](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) et
placez-le à la racine du jeu (ex: "SteamLibrary\steamapps\common\DARK SOULS III\Game")
## Rejoindre une partie Multiworld
1. Lancer DarkSoulsIII.exe ou lancer le jeu depuis Steam
2. Ecrire "/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}" dans l'invite de commande Windows ouverte au lancement du jeu
3. Une fois connecté, créez une nouvelle partie, choisissez une classe et attendez que les autres soient prêts avant de lancer
4. Vous pouvez quitter et lancer le jeu n'importe quand pendant une partie
## Où trouver le fichier de configuration ?
La [Page de configuration](/games/Dark%20Souls%20III/player-options) sur le site vous permez de configurer vos
paramètres et de les exporter sous la forme d'un fichier.

View File

@@ -650,8 +650,8 @@ item_table: Dict[int, ItemDict] = {
'doom_type': 2006,
'episode': -1,
'map': -1},
350106: {'classification': ItemClassification.progression,
'count': 1,
350106: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Backpack',
'doom_type': 8,
'episode': -1,
@@ -1160,6 +1160,30 @@ item_table: Dict[int, ItemDict] = {
'doom_type': 2026,
'episode': 4,
'map': 9},
350191: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Bullet capacity',
'doom_type': 65001,
'episode': -1,
'map': -1},
350192: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Shell capacity',
'doom_type': 65002,
'episode': -1,
'map': -1},
350193: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Energy cell capacity',
'doom_type': 65003,
'episode': -1,
'map': -1},
350194: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Rocket capacity',
'doom_type': 65004,
'episode': -1,
'map': -1},
}

View File

@@ -1,4 +1,4 @@
from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
from Options import PerGameCommonOptions, Range, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
from dataclasses import dataclass
@@ -144,6 +144,84 @@ class Episode4(Toggle):
display_name = "Episode 4"
class SplitBackpack(Toggle):
"""Split the Backpack into four individual items, each one increasing ammo capacity for one type of weapon only."""
display_name = "Split Backpack"
class BackpackCount(Range):
"""How many Backpacks will be available.
If Split Backpack is set, this will be the number of each capacity upgrade available."""
display_name = "Backpack Count"
range_start = 0
range_end = 10
default = 1
class MaxAmmoBullets(Range):
"""Set the starting ammo capacity for bullets."""
display_name = "Max Ammo - Bullets"
range_start = 200
range_end = 999
default = 200
class MaxAmmoShells(Range):
"""Set the starting ammo capacity for shotgun shells."""
display_name = "Max Ammo - Shells"
range_start = 50
range_end = 999
default = 50
class MaxAmmoRockets(Range):
"""Set the starting ammo capacity for rockets."""
display_name = "Max Ammo - Rockets"
range_start = 50
range_end = 999
default = 50
class MaxAmmoEnergyCells(Range):
"""Set the starting ammo capacity for energy cells."""
display_name = "Max Ammo - Energy Cells"
range_start = 300
range_end = 999
default = 300
class AddedAmmoBullets(Range):
"""Set the amount of bullet capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Bullets"
range_start = 20
range_end = 999
default = 200
class AddedAmmoShells(Range):
"""Set the amount of shotgun shell capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Shells"
range_start = 5
range_end = 999
default = 50
class AddedAmmoRockets(Range):
"""Set the amount of rocket capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Rockets"
range_start = 5
range_end = 999
default = 50
class AddedAmmoEnergyCells(Range):
"""Set the amount of energy cell capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Energy Cells"
range_start = 30
range_end = 999
default = 300
@dataclass
class DOOM1993Options(PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
@@ -163,3 +241,14 @@ class DOOM1993Options(PerGameCommonOptions):
episode3: Episode3
episode4: Episode4
split_backpack: SplitBackpack
backpack_count: BackpackCount
max_ammo_bullets: MaxAmmoBullets
max_ammo_shells: MaxAmmoShells
max_ammo_rockets: MaxAmmoRockets
max_ammo_energy_cells: MaxAmmoEnergyCells
added_ammo_bullets: AddedAmmoBullets
added_ammo_shells: AddedAmmoShells
added_ammo_rockets: AddedAmmoRockets
added_ammo_energy_cells: AddedAmmoEnergyCells

View File

@@ -42,7 +42,7 @@ class DOOM1993World(World):
options: DOOM1993Options
game = "DOOM 1993"
web = DOOM1993Web()
required_client_version = (0, 3, 9)
required_client_version = (0, 5, 0) # 1.2.0-prerelease or higher
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
item_name_groups = Items.item_name_groups
@@ -204,6 +204,15 @@ class DOOM1993World(World):
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
itempool += [self.create_item(item["name"]) for _ in range(count)]
# Backpack(s) based on options
if self.options.split_backpack.value:
itempool += [self.create_item("Bullet capacity") for _ in range(self.options.backpack_count.value)]
itempool += [self.create_item("Shell capacity") for _ in range(self.options.backpack_count.value)]
itempool += [self.create_item("Energy cell capacity") for _ in range(self.options.backpack_count.value)]
itempool += [self.create_item("Rocket capacity") for _ in range(self.options.backpack_count.value)]
else:
itempool += [self.create_item("Backpack") for _ in range(self.options.backpack_count.value)]
# Place end level items in locked locations
for map_name in Maps.map_names:
loc_name = map_name + " - Exit"
@@ -265,7 +274,7 @@ class DOOM1993World(World):
# Was balanced for 3 episodes (We added 4th episode, but keep same ratio)
count = min(remaining_loc, max(1, int(round(self.items_ratio[item_name] * ep_count / 3))))
if count == 0:
logger.warning("Warning, no ", item_name, " will be placed.")
logger.warning(f"Warning, no {item_name} will be placed.")
return
for i in range(count):
@@ -281,4 +290,14 @@ class DOOM1993World(World):
# an older version, the player would end up stuck.
slot_data["two_ways_keydoors"] = True
# Send slot data for ammo capacity values; this must be generic because Heretic uses it too
slot_data["ammo1start"] = self.options.max_ammo_bullets.value
slot_data["ammo2start"] = self.options.max_ammo_shells.value
slot_data["ammo3start"] = self.options.max_ammo_energy_cells.value
slot_data["ammo4start"] = self.options.max_ammo_rockets.value
slot_data["ammo1add"] = self.options.added_ammo_bullets.value
slot_data["ammo2add"] = self.options.added_ammo_shells.value
slot_data["ammo3add"] = self.options.added_ammo_energy_cells.value
slot_data["ammo4add"] = self.options.added_ammo_rockets.value
return slot_data

View File

@@ -56,8 +56,8 @@ item_table: Dict[int, ItemDict] = {
'doom_type': 82,
'episode': -1,
'map': -1},
360007: {'classification': ItemClassification.progression,
'count': 1,
360007: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Backpack',
'doom_type': 8,
'episode': -1,
@@ -1058,6 +1058,30 @@ item_table: Dict[int, ItemDict] = {
'doom_type': 2026,
'episode': 4,
'map': 2},
360600: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Bullet capacity',
'doom_type': 65001,
'episode': -1,
'map': -1},
360601: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Shell capacity',
'doom_type': 65002,
'episode': -1,
'map': -1},
360602: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Energy cell capacity',
'doom_type': 65003,
'episode': -1,
'map': -1},
360603: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Rocket capacity',
'doom_type': 65004,
'episode': -1,
'map': -1},
}

View File

@@ -1,6 +1,6 @@
import typing
from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
from Options import PerGameCommonOptions, Range, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
from dataclasses import dataclass
@@ -136,6 +136,84 @@ class SecretLevels(Toggle):
display_name = "Secret Levels"
class SplitBackpack(Toggle):
"""Split the Backpack into four individual items, each one increasing ammo capacity for one type of weapon only."""
display_name = "Split Backpack"
class BackpackCount(Range):
"""How many Backpacks will be available.
If Split Backpack is set, this will be the number of each capacity upgrade available."""
display_name = "Backpack Count"
range_start = 0
range_end = 10
default = 1
class MaxAmmoBullets(Range):
"""Set the starting ammo capacity for bullets."""
display_name = "Max Ammo - Bullets"
range_start = 200
range_end = 999
default = 200
class MaxAmmoShells(Range):
"""Set the starting ammo capacity for shotgun shells."""
display_name = "Max Ammo - Shells"
range_start = 50
range_end = 999
default = 50
class MaxAmmoRockets(Range):
"""Set the starting ammo capacity for rockets."""
display_name = "Max Ammo - Rockets"
range_start = 50
range_end = 999
default = 50
class MaxAmmoEnergyCells(Range):
"""Set the starting ammo capacity for energy cells."""
display_name = "Max Ammo - Energy Cells"
range_start = 300
range_end = 999
default = 300
class AddedAmmoBullets(Range):
"""Set the amount of bullet capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Bullets"
range_start = 20
range_end = 999
default = 200
class AddedAmmoShells(Range):
"""Set the amount of shotgun shell capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Shells"
range_start = 5
range_end = 999
default = 50
class AddedAmmoRockets(Range):
"""Set the amount of rocket capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Rockets"
range_start = 5
range_end = 999
default = 50
class AddedAmmoEnergyCells(Range):
"""Set the amount of energy cell capacity added when collecting a backpack or capacity upgrade."""
display_name = "Added Ammo - Energy Cells"
range_start = 30
range_end = 999
default = 300
@dataclass
class DOOM2Options(PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
@@ -153,3 +231,14 @@ class DOOM2Options(PerGameCommonOptions):
episode2: Episode2
episode3: Episode3
episode4: SecretLevels
split_backpack: SplitBackpack
backpack_count: BackpackCount
max_ammo_bullets: MaxAmmoBullets
max_ammo_shells: MaxAmmoShells
max_ammo_rockets: MaxAmmoRockets
max_ammo_energy_cells: MaxAmmoEnergyCells
added_ammo_bullets: AddedAmmoBullets
added_ammo_shells: AddedAmmoShells
added_ammo_rockets: AddedAmmoRockets
added_ammo_energy_cells: AddedAmmoEnergyCells

View File

@@ -43,7 +43,7 @@ class DOOM2World(World):
options: DOOM2Options
game = "DOOM II"
web = DOOM2Web()
required_client_version = (0, 3, 9)
required_client_version = (0, 5, 0) # 1.2.0-prerelease or higher
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
item_name_groups = Items.item_name_groups
@@ -196,6 +196,15 @@ class DOOM2World(World):
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
itempool += [self.create_item(item["name"]) for _ in range(count)]
# Backpack(s) based on options
if self.options.split_backpack.value:
itempool += [self.create_item("Bullet capacity") for _ in range(self.options.backpack_count.value)]
itempool += [self.create_item("Shell capacity") for _ in range(self.options.backpack_count.value)]
itempool += [self.create_item("Energy cell capacity") for _ in range(self.options.backpack_count.value)]
itempool += [self.create_item("Rocket capacity") for _ in range(self.options.backpack_count.value)]
else:
itempool += [self.create_item("Backpack") for _ in range(self.options.backpack_count.value)]
# Place end level items in locked locations
for map_name in Maps.map_names:
loc_name = map_name + " - Exit"
@@ -258,11 +267,23 @@ class DOOM2World(World):
# Was balanced based on DOOM 1993's first 3 episodes
count = min(remaining_loc, max(1, int(round(self.items_ratio[item_name] * ep_count / 3))))
if count == 0:
logger.warning("Warning, no ", item_name, " will be placed.")
logger.warning(f"Warning, no {item_name} will be placed.")
return
for i in range(count):
itempool.append(self.create_item(item_name))
def fill_slot_data(self) -> Dict[str, Any]:
return self.options.as_dict("difficulty", "random_monsters", "random_pickups", "random_music", "flip_levels", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "episode1", "episode2", "episode3", "episode4")
slot_data = self.options.as_dict("difficulty", "random_monsters", "random_pickups", "random_music", "flip_levels", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "episode1", "episode2", "episode3", "episode4")
# Send slot data for ammo capacity values; this must be generic because Heretic uses it too
slot_data["ammo1start"] = self.options.max_ammo_bullets.value
slot_data["ammo2start"] = self.options.max_ammo_shells.value
slot_data["ammo3start"] = self.options.max_ammo_energy_cells.value
slot_data["ammo4start"] = self.options.max_ammo_rockets.value
slot_data["ammo1add"] = self.options.added_ammo_bullets.value
slot_data["ammo2add"] = self.options.added_ammo_shells.value
slot_data["ammo3add"] = self.options.added_ammo_energy_cells.value
slot_data["ammo4add"] = self.options.added_ammo_rockets.value
return slot_data

View File

@@ -235,6 +235,12 @@ class FactorioStartItems(OptionDict):
"""Mapping of Factorio internal item-name to amount granted on start."""
display_name = "Starting Items"
default = {"burner-mining-drill": 4, "stone-furnace": 4, "raw-fish": 50}
schema = Schema(
{
str: And(int, lambda n: n > 0,
error="amount of starting items has to be a positive integer"),
}
)
class FactorioFreeSampleBlacklist(OptionSet):
@@ -257,7 +263,8 @@ class AttackTrapCount(TrapCount):
class TeleportTrapCount(TrapCount):
"""Trap items that when received trigger a random teleport."""
"""Trap items that when received trigger a random teleport.
It is ensured the player can walk back to where they got teleported from."""
display_name = "Teleport Traps"
@@ -304,6 +311,11 @@ class EvolutionTrapIncrease(Range):
range_end = 100
class InventorySpillTrapCount(TrapCount):
"""Trap items that when received trigger dropping your main inventory and trash inventory onto the ground."""
display_name = "Inventory Spill Traps"
class FactorioWorldGen(OptionDict):
"""World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator,
with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings"""
@@ -484,6 +496,7 @@ class FactorioOptions(PerGameCommonOptions):
artillery_traps: ArtilleryTrapCount
atomic_rocket_traps: AtomicRocketTrapCount
atomic_cliff_remover_traps: AtomicCliffRemoverTrapCount
inventory_spill_traps: InventorySpillTrapCount
attack_traps: AttackTrapCount
evolution_traps: EvolutionTrapCount
evolution_trap_increase: EvolutionTrapIncrease
@@ -518,6 +531,7 @@ option_groups: list[OptionGroup] = [
ArtilleryTrapCount,
AtomicRocketTrapCount,
AtomicCliffRemoverTrapCount,
InventorySpillTrapCount,
],
start_collapsed=True
),

View File

@@ -8,7 +8,7 @@ import Utils
import settings
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
from worlds.LauncherComponents import Component, components, Type, launch as launch_component
from worlds.generic import Rules
from .Locations import location_pools, location_table
from .Mod import generate_mod
@@ -24,7 +24,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
def launch_client():
from .Client import launch
launch_subprocess(launch, name="FactorioClient")
launch_component(launch, name="FactorioClient")
components.append(Component("Factorio Client", "FactorioClient", func=launch_client, component_type=Type.CLIENT))
@@ -78,6 +78,7 @@ all_items["Cluster Grenade Trap"] = factorio_base_id - 5
all_items["Artillery Trap"] = factorio_base_id - 6
all_items["Atomic Rocket Trap"] = factorio_base_id - 7
all_items["Atomic Cliff Remover Trap"] = factorio_base_id - 8
all_items["Inventory Spill Trap"] = factorio_base_id - 9
class Factorio(World):
@@ -112,6 +113,8 @@ class Factorio(World):
science_locations: typing.List[FactorioScienceLocation]
removed_technologies: typing.Set[str]
settings: typing.ClassVar[FactorioSettings]
trap_names: tuple[str] = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery",
"Atomic Rocket", "Atomic Cliff Remover", "Inventory Spill")
def __init__(self, world, player: int):
super(Factorio, self).__init__(world, player)
@@ -136,15 +139,11 @@ class Factorio(World):
random = self.random
nauvis = Region("Nauvis", player, self.multiworld)
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
self.options.evolution_traps + \
self.options.attack_traps + \
self.options.teleport_traps + \
self.options.grenade_traps + \
self.options.cluster_grenade_traps + \
self.options.atomic_rocket_traps + \
self.options.atomic_cliff_remover_traps + \
self.options.artillery_traps
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo
for name in self.trap_names:
name = name.replace(" ", "_").lower()+"_traps"
location_count += getattr(self.options, name)
location_pool = []
@@ -196,9 +195,8 @@ class Factorio(World):
def create_items(self) -> None:
self.custom_technologies = self.set_custom_technologies()
self.set_custom_recipes()
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket",
"Atomic Cliff Remover")
for trap_name in traps:
for trap_name in self.trap_names:
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
range(getattr(self.options,
f"{trap_name.lower().replace(' ', '_')}_traps")))
@@ -280,9 +278,6 @@ class Factorio(World):
self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names)
for tech_name in victory_tech_names:
if not self.multiworld.get_all_state(True).has(tech_name, player):
print(tech_name)
self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player)
def get_recipe(self, name: str) -> Recipe:

View File

@@ -48,3 +48,107 @@ function fire_entity_at_entities(entity_name, entities, speed)
target=target, speed=speed}
end
end
local teleport_requests = {}
local teleport_attempts = {}
local max_attempts = 100
function attempt_teleport_player(player, attempt)
-- global attempt storage as metadata can't be stored
if attempt == nil then
attempt = teleport_attempts[player.index]
else
teleport_attempts[player.index] = attempt
end
if attempt > max_attempts then
player.print("Teleport failed: No valid position found after " .. max_attempts .. " attempts!")
teleport_attempts[player.index] = 0
return
end
local surface = player.character.surface
local prototype_name = player.character.prototype.name
local original_position = player.character.position
local candidate_position = random_offset_position(original_position, 1024)
local non_colliding_position = surface.find_non_colliding_position(
prototype_name, candidate_position, 0, 1
)
if non_colliding_position then
-- Request pathfinding asynchronously
local path_id = surface.request_path{
bounding_box = player.character.prototype.collision_box,
collision_mask = { layers = { ["player"] = true } },
start = original_position,
goal = non_colliding_position,
force = player.force.name,
radius = 1,
pathfind_flags = {cache = true, low_priority = true, allow_paths_through_own_entities = true},
}
-- Store the request with the player index as the key
teleport_requests[player.index] = path_id
else
attempt_teleport_player(player, attempt + 1)
end
end
function handle_teleport_attempt(event)
for player_index, path_id in pairs(teleport_requests) do
-- Check if the event matches the stored path_id
if path_id == event.id then
local player = game.players[player_index]
if event.path then
if player.character then
player.character.teleport(event.path[#event.path].position) -- Teleport to the last point in the path
-- Clear the attempts for this player
teleport_attempts[player_index] = 0
return
end
return
end
attempt_teleport_player(player, nil)
break
end
end
end
function spill_character_inventory(character)
if not (character and character.valid) then
return false
end
-- grab attrs once pre-loop
local position = character.position
local surface = character.surface
local inventories_to_spill = {
defines.inventory.character_main, -- Main inventory
defines.inventory.character_trash, -- Logistic trash slots
}
for _, inventory_type in pairs(inventories_to_spill) do
local inventory = character.get_inventory(inventory_type)
if inventory and inventory.valid then
-- Spill each item stack onto the ground
for i = 1, #inventory do
local stack = inventory[i]
if stack and stack.valid_for_read then
local spilled_items = surface.spill_item_stack{
position = position,
stack = stack,
enable_looted = false, -- do not mark for auto-pickup
force = nil, -- do not mark for auto-deconstruction
allow_belts = true, -- do mark for putting it onto belts
}
if #spilled_items > 0 then
stack.clear() -- only delete if spilled successfully
end
end
end
end
end
end

View File

@@ -134,6 +134,9 @@ end
script.on_event(defines.events.on_player_changed_position, on_player_changed_position)
{% endif %}
-- Handle the pathfinding result of teleport traps
script.on_event(defines.events.on_script_path_request_finished, handle_teleport_attempt)
function count_energy_bridges()
local count = 0
for i, bridge in pairs(storage.energy_link_bridges) do
@@ -143,9 +146,11 @@ function count_energy_bridges()
end
return count
end
function get_energy_increment(bridge)
return ENERGY_INCREMENT + (ENERGY_INCREMENT * 0.3 * bridge.quality.level)
end
function on_check_energy_link(event)
--- assuming 1 MJ increment and 5MJ battery:
--- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing
@@ -722,12 +727,10 @@ end,
game.forces["enemy"].set_evolution_factor(new_factor, "nauvis")
game.print({"", "New evolution factor:", new_factor})
end,
["Teleport Trap"] = function ()
["Teleport Trap"] = function()
for _, player in ipairs(game.forces["player"].players) do
current_character = player.character
if current_character ~= nil then
current_character.teleport(current_character.surface.find_non_colliding_position(
current_character.prototype.name, random_offset_position(current_character.position, 1024), 0, 1))
if player.character then
attempt_teleport_player(player, 1)
end
end
end,
@@ -750,6 +753,11 @@ end,
fire_entity_at_entities("atomic-rocket", {cliffs[math.random(#cliffs)]}, 0.1)
end
end,
["Inventory Spill Trap"] = function ()
for _, player in ipairs(game.forces["player"].players) do
spill_character_inventory(player.character)
end
end,
}
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)

View File

@@ -152,14 +152,23 @@ class FFMQWorld(World):
return FFMQItem(name, self.player)
def collect_item(self, state, item, remove=False):
if not item.advancement:
return None
if "Progressive" in item.name:
i = item.code - 256
if remove:
if state.has(self.item_id_to_name[i+1], self.player):
if state.has(self.item_id_to_name[i+2], self.player):
return self.item_id_to_name[i+2]
return self.item_id_to_name[i+1]
return self.item_id_to_name[i]
if state.has(self.item_id_to_name[i], self.player):
if state.has(self.item_id_to_name[i+1], self.player):
return self.item_id_to_name[i+2]
return self.item_id_to_name[i+1]
return self.item_id_to_name[i]
return item.name if item.advancement else None
return item.name
def modify_multidata(self, multidata):
# wait for self.rom_name to be available.

View File

@@ -50,8 +50,8 @@ item_table: Dict[int, ItemDict] = {
'doom_type': 2004,
'episode': -1,
'map': -1},
370006: {'classification': ItemClassification.progression,
'count': 1,
370006: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Bag of Holding',
'doom_type': 8,
'episode': -1,
@@ -1592,6 +1592,42 @@ item_table: Dict[int, ItemDict] = {
'doom_type': 35,
'episode': 5,
'map': 9},
370600: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Crystal Capacity',
'doom_type': 65001,
'episode': -1,
'map': -1},
370601: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Ethereal Arrow Capacity',
'doom_type': 65002,
'episode': -1,
'map': -1},
370602: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Claw Orb Capacity',
'doom_type': 65003,
'episode': -1,
'map': -1},
370603: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Rune Capacity',
'doom_type': 65004,
'episode': -1,
'map': -1},
370604: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Flame Orb Capacity',
'doom_type': 65005,
'episode': -1,
'map': -1},
370605: {'classification': ItemClassification.useful,
'count': 0,
'name': 'Mace Sphere Capacity',
'doom_type': 65006,
'episode': -1,
'map': -1},
}

View File

@@ -1,4 +1,4 @@
from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
from Options import PerGameCommonOptions, Range, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
from dataclasses import dataclass
@@ -144,6 +144,116 @@ class Episode5(Toggle):
display_name = "Episode 5"
class SplitBagOfHolding(Toggle):
"""Split the Bag of Holding into six individual items, each one increasing ammo capacity for one type of weapon only."""
display_name = "Split Bag of Holding"
class BagOfHoldingCount(Range):
"""How many Bags of Holding will be available.
If Split Bag of Holding is set, this will be the number of each capacity upgrade available."""
display_name = "Bag of Holding Count"
range_start = 0
range_end = 10
default = 1
class MaxAmmoCrystals(Range):
"""Set the starting ammo capacity for crystals (Elven Wand ammo)."""
display_name = "Max Ammo - Crystals"
range_start = 100
range_end = 999
default = 100
class MaxAmmoArrows(Range):
"""Set the starting ammo capacity for arrows (Ethereal Crossbow ammo)."""
display_name = "Max Ammo - Arrows"
range_start = 50
range_end = 999
default = 50
class MaxAmmoClawOrbs(Range):
"""Set the starting ammo capacity for claw orbs (Dragon Claw ammo)."""
display_name = "Max Ammo - Claw Orbs"
range_start = 200
range_end = 999
default = 200
class MaxAmmoRunes(Range):
"""Set the starting ammo capacity for runes (Hellstaff ammo)."""
display_name = "Max Ammo - Runes"
range_start = 200
range_end = 999
default = 200
class MaxAmmoFlameOrbs(Range):
"""Set the starting ammo capacity for flame orbs (Phoenix Rod ammo)."""
display_name = "Max Ammo - Flame Orbs"
range_start = 20
range_end = 999
default = 20
class MaxAmmoSpheres(Range):
"""Set the starting ammo capacity for spheres (Firemace ammo)."""
display_name = "Max Ammo - Spheres"
range_start = 150
range_end = 999
default = 150
class AddedAmmoCrystals(Range):
"""Set the amount of crystal capacity gained when collecting a bag of holding or a capacity upgrade."""
display_name = "Added Ammo - Crystals"
range_start = 10
range_end = 999
default = 100
class AddedAmmoArrows(Range):
"""Set the amount of arrow capacity gained when collecting a bag of holding or a capacity upgrade."""
display_name = "Added Ammo - Arrows"
range_start = 5
range_end = 999
default = 50
class AddedAmmoClawOrbs(Range):
"""Set the amount of claw orb capacity gained when collecting a bag of holding or a capacity upgrade."""
display_name = "Added Ammo - Claw Orbs"
range_start = 20
range_end = 999
default = 200
class AddedAmmoRunes(Range):
"""Set the amount of rune capacity gained when collecting a bag of holding or a capacity upgrade."""
display_name = "Added Ammo - Runes"
range_start = 20
range_end = 999
default = 200
class AddedAmmoFlameOrbs(Range):
"""Set the amount of flame orb capacity gained when collecting a bag of holding or a capacity upgrade."""
display_name = "Added Ammo - Flame Orbs"
range_start = 2
range_end = 999
default = 20
class AddedAmmoSpheres(Range):
"""Set the amount of sphere capacity gained when collecting a bag of holding or a capacity upgrade."""
display_name = "Added Ammo - Spheres"
range_start = 15
range_end = 999
default = 150
@dataclass
class HereticOptions(PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
@@ -163,3 +273,18 @@ class HereticOptions(PerGameCommonOptions):
episode3: Episode3
episode4: Episode4
episode5: Episode5
split_bag_of_holding: SplitBagOfHolding
bag_of_holding_count: BagOfHoldingCount
max_ammo_crystals: MaxAmmoCrystals
max_ammo_arrows: MaxAmmoArrows
max_ammo_claw_orbs: MaxAmmoClawOrbs
max_ammo_runes: MaxAmmoRunes
max_ammo_flame_orbs: MaxAmmoFlameOrbs
max_ammo_spheres: MaxAmmoSpheres
added_ammo_crystals: AddedAmmoCrystals
added_ammo_arrows: AddedAmmoArrows
added_ammo_claw_orbs: AddedAmmoClawOrbs
added_ammo_runes: AddedAmmoRunes
added_ammo_flame_orbs: AddedAmmoFlameOrbs
added_ammo_spheres: AddedAmmoSpheres

View File

@@ -695,13 +695,11 @@ def set_episode5_rules(player, multiworld, pro):
state.has("Phoenix Rod", player, 1) and
state.has("Firemace", player, 1) and
state.has("Hellstaff", player, 1) and
state.has("Gauntlets of the Necromancer", player, 1) and
state.has("Bag of Holding", player, 1))
state.has("Gauntlets of the Necromancer", player, 1))
# Skein of D'Sparil (E5M9)
set_rule(multiworld.get_entrance("Hub -> Skein of D'Sparil (E5M9) Main", player), lambda state:
state.has("Skein of D'Sparil (E5M9)", player, 1) and
state.has("Bag of Holding", player, 1) and
state.has("Hellstaff", player, 1) and
state.has("Phoenix Rod", player, 1) and
state.has("Dragon Claw", player, 1) and

View File

@@ -41,7 +41,7 @@ class HereticWorld(World):
options: HereticOptions
game = "Heretic"
web = HereticWeb()
required_client_version = (0, 3, 9)
required_client_version = (0, 5, 0) # 1.2.0-prerelease or higher
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
item_name_groups = Items.item_name_groups
@@ -206,6 +206,17 @@ class HereticWorld(World):
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
itempool += [self.create_item(item["name"]) for _ in range(count)]
# Bag(s) of Holding based on options
if self.options.split_bag_of_holding.value:
itempool += [self.create_item("Crystal Capacity") for _ in range(self.options.bag_of_holding_count.value)]
itempool += [self.create_item("Ethereal Arrow Capacity") for _ in range(self.options.bag_of_holding_count.value)]
itempool += [self.create_item("Claw Orb Capacity") for _ in range(self.options.bag_of_holding_count.value)]
itempool += [self.create_item("Rune Capacity") for _ in range(self.options.bag_of_holding_count.value)]
itempool += [self.create_item("Flame Orb Capacity") for _ in range(self.options.bag_of_holding_count.value)]
itempool += [self.create_item("Mace Sphere Capacity") for _ in range(self.options.bag_of_holding_count.value)]
else:
itempool += [self.create_item("Bag of Holding") for _ in range(self.options.bag_of_holding_count.value)]
# Place end level items in locked locations
for map_name in Maps.map_names:
loc_name = map_name + " - Exit"
@@ -274,7 +285,7 @@ class HereticWorld(World):
episode_count = self.get_episode_count()
count = min(remaining_loc, max(1, self.items_ratio[item_name] * episode_count))
if count == 0:
logger.warning("Warning, no " + item_name + " will be placed.")
logger.warning(f"Warning, no {item_name} will be placed.")
return
for i in range(count):
@@ -290,4 +301,18 @@ class HereticWorld(World):
slot_data["episode4"] = self.included_episodes[3]
slot_data["episode5"] = self.included_episodes[4]
# Send slot data for ammo capacity values; this must be generic because Doom uses it too
slot_data["ammo1start"] = self.options.max_ammo_crystals.value
slot_data["ammo2start"] = self.options.max_ammo_arrows.value
slot_data["ammo3start"] = self.options.max_ammo_claw_orbs.value
slot_data["ammo4start"] = self.options.max_ammo_runes.value
slot_data["ammo5start"] = self.options.max_ammo_flame_orbs.value
slot_data["ammo6start"] = self.options.max_ammo_spheres.value
slot_data["ammo1add"] = self.options.added_ammo_crystals.value
slot_data["ammo2add"] = self.options.added_ammo_arrows.value
slot_data["ammo3add"] = self.options.added_ammo_claw_orbs.value
slot_data["ammo4add"] = self.options.added_ammo_runes.value
slot_data["ammo5add"] = self.options.added_ammo_flame_orbs.value
slot_data["ammo6add"] = self.options.added_ammo_spheres.value
return slot_data

View File

@@ -333,7 +333,7 @@ class PlandoCharmCosts(OptionDict):
continue
try:
self.value[key] = CharmCost.from_any(data).value
except ValueError as ex:
except ValueError:
# will fail schema afterwords
self.value[key] = data

View File

@@ -7,22 +7,22 @@ import itertools
import operator
from collections import defaultdict, Counter
logger = logging.getLogger("Hollow Knight")
from .Items import item_table, lookup_type_to_names, item_name_groups
from .Regions import create_regions
from .Items import item_table, item_name_groups
from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance
from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \
shop_to_option, HKOptions, GrubHuntGoal
from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \
event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs
from .ExtractedData import locations, starts, multi_locations, event_names, item_effects, connectors, \
vanilla_shop_costs, vanilla_location_costs
from .Charms import names as charm_names
from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState
from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, \
CollectionState
from worlds.AutoWorld import World, LogicMixin, WebWorld
from settings import Group, Bool
logger = logging.getLogger("Hollow Knight")
class HollowKnightSettings(Group):
class DisableMapModSpoilers(Bool):
@@ -160,7 +160,7 @@ class HKWeb(WebWorld):
class HKWorld(World):
"""Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface,
"""Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface,
searching for riches, or glory, or answers to old secrets.
As the enigmatic Knight, youll traverse the depths, unravel its mysteries and conquer its evils.
@@ -209,7 +209,7 @@ class HKWorld(World):
# defaulting so completion condition isn't incorrect before pre_fill
self.grub_count = (
46 if options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]
else options.GrubHuntGoal
else options.GrubHuntGoal.value
)
self.grub_player_count = {self.player: self.grub_count}
@@ -231,7 +231,6 @@ class HKWorld(World):
def create_regions(self):
menu_region: Region = create_region(self.multiworld, self.player, 'Menu')
self.multiworld.regions.append(menu_region)
# wp_exclusions = self.white_palace_exclusions()
# check for any goal that godhome events are relevant to
all_event_names = event_names.copy()
@@ -241,21 +240,17 @@ class HKWorld(World):
# Link regions
for event_name in sorted(all_event_names):
#if event_name in wp_exclusions:
# continue
loc = HKLocation(self.player, event_name, None, menu_region)
loc.place_locked_item(HKItem(event_name,
True, #event_name not in wp_exclusions,
True,
None, "Event", self.player))
menu_region.locations.append(loc)
for entry_transition, exit_transition in connectors.items():
#if entry_transition in wp_exclusions:
# continue
if exit_transition:
# if door logic fulfilled -> award vanilla target as event
loc = HKLocation(self.player, entry_transition, None, menu_region)
loc.place_locked_item(HKItem(exit_transition,
True, #exit_transition not in wp_exclusions,
True,
None, "Event", self.player))
menu_region.locations.append(loc)
@@ -292,7 +287,10 @@ class HKWorld(World):
if item_name in junk_replace:
item_name = self.get_filler_item_name()
item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations else self.create_event(item_name)
item = (self.create_item(item_name)
if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations
else self.create_event(item_name)
)
if location_name == "Start":
if item_name in randomized_starting_items:
@@ -347,8 +345,8 @@ class HKWorld(World):
randomized = True
_add("Elevator_Pass", "Elevator_Pass", randomized)
for shop, locations in self.created_multi_locations.items():
for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value):
for shop, shop_locations in self.created_multi_locations.items():
for _ in range(len(shop_locations), getattr(self.options, shop_to_option[shop]).value):
self.create_location(shop)
unfilled_locations += 1
@@ -358,7 +356,7 @@ class HKWorld(World):
# Add additional shop items, as needed.
if additional_shop_items > 0:
shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16)
shops = [shop for shop, shop_locations in self.created_multi_locations.items() if len(shop_locations) < 16]
if not self.options.EggShopSlots: # No eggshop, so don't place items there
shops.remove('Egg_Shop')
@@ -380,8 +378,8 @@ class HKWorld(World):
self.sort_shops_by_cost()
def sort_shops_by_cost(self):
for shop, locations in self.created_multi_locations.items():
randomized_locations = list(loc for loc in locations if not loc.vanilla)
for shop, shop_locations in self.created_multi_locations.items():
randomized_locations = [loc for loc in shop_locations if not loc.vanilla]
prices = sorted(
(loc.costs for loc in randomized_locations),
key=lambda costs: (len(costs),) + tuple(costs.values())
@@ -405,7 +403,7 @@ class HKWorld(World):
return {k: v for k, v in weights.items() if v}
random = self.random
hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value
hybrid_chance = getattr(self.options, "CostSanityHybridChance").value
weights = {
data.term: getattr(self.options, f"CostSanity{data.option}Weight").value
for data in cost_terms.values()
@@ -493,7 +491,11 @@ 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 worlds 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)
@@ -668,8 +670,8 @@ class HKWorld(World):
):
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
else:
for shop_name, locations in hk_world.created_multi_locations.items():
for loc in locations:
for shop_name, shop_locations in hk_world.created_multi_locations.items():
for loc in shop_locations:
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
def get_multi_location_name(self, base: str, i: typing.Optional[int]) -> str:

View File

@@ -2,7 +2,6 @@ import typing
from argparse import Namespace
from BaseClasses import CollectionState, MultiWorld
from Options import ItemLinks
from test.bases import WorldTestBase
from worlds.AutoWorld import AutoWorldRegister, call_all
from .. import HKWorld

View File

@@ -1,5 +1,6 @@
from . import linkedTestHK, WorldTestBase
from test.bases import WorldTestBase
from Options import ItemLinks
from . import linkedTestHK
class test_grubcount_limited(linkedTestHK, WorldTestBase):

View File

@@ -206,19 +206,19 @@ def set_rules(world: "KDL3World") -> None:
lambda state: can_reach_needle(state, world.player))
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u2, world.player),
lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u3, world.player),
lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u4, world.player),
lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
set_rule(world.multiworld.get_location(location_name.cloudy_park_6_u1, world.player),
lambda state: can_reach_cutter(state, world.player))
@@ -248,9 +248,9 @@ def set_rules(world: "KDL3World") -> None:
for i in range(12, 18):
set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player),
lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player)))
for i in range(21, 23):
set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player),
lambda state: can_reach_chuchu(state, world.player))
@@ -307,7 +307,7 @@ def set_rules(world: "KDL3World") -> None:
lambda state: can_reach_coo(state, world.player) and can_reach_burning(state, world.player))
set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a3, world.player),
lambda state: can_reach_chuchu(state, world.player) and can_reach_coo(state, world.player)
and can_reach_burning(state, world.player))
and can_reach_burning(state, world.player))
for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified",
"Level 3 Boss - Purified", "Level 4 Boss - Purified",
@@ -329,6 +329,14 @@ def set_rules(world: "KDL3World") -> None:
world.options.ow_boss_requirement.value,
world.player_levels)))
if world.options.open_world:
for boss_flag, level in zip(["Level 1 Boss - Defeated", "Level 2 Boss - Defeated", "Level 3 Boss - Defeated",
"Level 4 Boss - Defeated", "Level 5 Boss - Defeated"],
location_name.level_names.keys()):
set_rule(world.get_location(boss_flag),
lambda state, lvl=level: state.has(f"{lvl} - Stage Completion", world.player,
world.options.ow_boss_requirement.value))
set_rule(world.multiworld.get_entrance("To Level 6", world.player),
lambda state: state.has("Heart Star", world.player, world.required_heart_stars))

View File

@@ -483,6 +483,8 @@ def create_regions(multiworld: MultiWorld, player: int, options):
for name, data in regions.items():
multiworld.regions.append(create_region(multiworld, player, name, data))
def connect_entrances(multiworld: MultiWorld, player: int):
multiworld.get_entrance("Awakening", player).connect(multiworld.get_region("Awakening", player))
multiworld.get_entrance("Destiny Islands", player).connect(multiworld.get_region("Destiny Islands", player))
multiworld.get_entrance("Traverse Town", player).connect(multiworld.get_region("Traverse Town", player))
@@ -500,6 +502,7 @@ def create_regions(multiworld: MultiWorld, player: int, options):
multiworld.get_entrance("World Map", player).connect(multiworld.get_region("World Map", player))
multiworld.get_entrance("Levels", player).connect(multiworld.get_region("Levels", player))
def create_region(multiworld: MultiWorld, player: int, name: str, data: KH1RegionData):
region = Region(name, player, multiworld)
if data.locations:

View File

@@ -6,15 +6,15 @@ from worlds.AutoWorld import WebWorld, World
from .Items import KH1Item, KH1ItemData, event_item_table, get_items_by_category, item_table, item_name_groups
from .Locations import KH1Location, location_table, get_locations_by_category, location_name_groups
from .Options import KH1Options, kh1_option_groups
from .Regions import create_regions
from .Regions import connect_entrances, create_regions
from .Rules import set_rules
from .Presets import kh1_option_presets
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
from worlds.LauncherComponents import Component, components, Type, launch as launch_component
def launch_client():
from .Client import launch
launch_subprocess(launch, name="KH1 Client")
launch_component(launch, name="KH1 Client")
components.append(Component("KH1 Client", "KH1Client", func=launch_client, component_type=Type.CLIENT))
@@ -242,6 +242,9 @@ class KH1World(World):
def create_regions(self):
create_regions(self.multiworld, self.player, self.options)
def connect_entrances(self):
connect_entrances(self.multiworld, self.player)
def generate_early(self):
value_names = ["Reports to Open End of the World", "Reports to Open Final Rest Door", "Reports in Pool"]

View File

@@ -1,12 +1,15 @@
import ModuleUpdate
import Utils
ModuleUpdate.update()
import os
import asyncio
import json
import requests
from pymem import pymem
from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, SupportAbility_Table, ActionAbility_Table, all_weapon_slot
from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, \
SupportAbility_Table, ActionAbility_Table, all_weapon_slot
from .Names import ItemName
from .WorldLocations import *
@@ -21,6 +24,7 @@ class KH2Context(CommonContext):
def __init__(self, server_address, password):
super(KH2Context, self).__init__(server_address, password)
self.goofy_ability_to_slot = dict()
self.donald_ability_to_slot = dict()
self.all_weapon_location_id = None
@@ -33,6 +37,7 @@ class KH2Context(CommonContext):
self.serverconneced = False
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
self.kh2_data_package = {}
self.kh2_loc_name_to_id = None
self.kh2_item_name_to_id = None
self.lookup_id_to_item = None
@@ -81,7 +86,10 @@ class KH2Context(CommonContext):
},
}
self.kh2seedname = None
self.kh2_seed_save_path_join = None
self.kh2slotdata = None
self.mem_json = None
self.itemamount = {}
if "localappdata" in os.environ:
self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP")
@@ -111,26 +119,18 @@ class KH2Context(CommonContext):
# 255: {}, # starting screen
}
self.last_world_int = -1
# 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room
# self.sveroom = 0x2A09C00 + 0x41
# 0 not in battle 1 in yellow battle 2 red battle #short
# self.inBattle = 0x2A0EAC4 + 0x40
# self.onDeath = 0xAB9078
# PC Address anchors
# self.Now = 0x0714DB8 old address
# epic addresses
# epic .10 addresses
self.Now = 0x0716DF8
self.Save = 0x09A92F0
self.Save = 0x9A9330
self.Journal = 0x743260
self.Shop = 0x743350
self.Slot1 = 0x2A22FD8
# self.Sys3 = 0x2A59DF0
# self.Bt10 = 0x2A74880
# self.BtlEnd = 0x2A0D3E0
# self.Slot1 = 0x2A20C98 old address
self.Slot1 = 0x2A23018
self.kh2_game_version = None # can be egs or steam
self.kh2_seed_save_path = None
self.chest_set = set(exclusion_table["Chests"])
self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"])
self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"])
@@ -178,7 +178,8 @@ class KH2Context(CommonContext):
self.base_accessory_slots = 1
self.base_armor_slots = 1
self.base_item_slots = 3
self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, 0x2770, 0x2772]
self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E,
0x2770, 0x2772]
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
@@ -190,8 +191,7 @@ class KH2Context(CommonContext):
self.kh2connected = False
self.serverconneced = False
if self.kh2seedname is not None and self.auth is not None:
with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
'w') as f:
with open(self.kh2_seed_save_path_join, 'w') as f:
f.write(json.dumps(self.kh2_seed_save, indent=4))
await super(KH2Context, self).connection_closed()
@@ -199,8 +199,7 @@ class KH2Context(CommonContext):
self.kh2connected = False
self.serverconneced = False
if self.kh2seedname not in {None} and self.auth not in {None}:
with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
'w') as f:
with open(self.kh2_seed_save_path_join, 'w') as f:
f.write(json.dumps(self.kh2_seed_save, indent=4))
await super(KH2Context, self).disconnect()
@@ -213,8 +212,7 @@ class KH2Context(CommonContext):
async def shutdown(self):
if self.kh2seedname not in {None} and self.auth not in {None}:
with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
'w') as f:
with open(self.kh2_seed_save_path_join, 'w') as f:
f.write(json.dumps(self.kh2_seed_save, indent=4))
await super(KH2Context, self).shutdown()
@@ -228,7 +226,7 @@ class KH2Context(CommonContext):
return self.kh2.write_bytes(self.kh2.base_address + address, value.to_bytes(1, 'big'), 1)
def kh2_read_byte(self, address):
return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1), "big")
return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1))
def kh2_read_int(self, address):
return self.kh2.read_int(self.kh2.base_address + address)
@@ -240,11 +238,14 @@ class KH2Context(CommonContext):
return self.kh2.read_string(self.kh2.base_address + address, length)
def on_package(self, cmd: str, args: dict):
if cmd in {"RoomInfo"}:
if cmd == "RoomInfo":
self.kh2seedname = args['seed_name']
self.kh2_seed_save_path = f"kh2save2{self.kh2seedname}{self.auth}.json"
self.kh2_seed_save_path_join = os.path.join(self.game_communication_path, self.kh2_seed_save_path)
if not os.path.exists(self.game_communication_path):
os.makedirs(self.game_communication_path)
if not os.path.exists(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json"):
if not os.path.exists(self.kh2_seed_save_path_join):
self.kh2_seed_save = {
"Levels": {
"SoraLevel": 0,
@@ -257,12 +258,11 @@ class KH2Context(CommonContext):
},
"SoldEquipment": [],
}
with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
'wt') as f:
with open(self.kh2_seed_save_path_join, 'wt') as f:
pass
# self.locations_checked = set()
elif os.path.exists(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json"):
with open(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json", 'r') as f:
elif os.path.exists(self.kh2_seed_save_path_join):
with open(self.kh2_seed_save_path_join) as f:
self.kh2_seed_save = json.load(f)
if self.kh2_seed_save is None:
self.kh2_seed_save = {
@@ -280,13 +280,22 @@ class KH2Context(CommonContext):
# self.locations_checked = set(self.kh2_seed_save_cache["LocationsChecked"])
# self.serverconneced = True
if cmd in {"Connected"}:
asyncio.create_task(self.send_msgs([{"cmd": "GetDataPackage", "games": ["Kingdom Hearts 2"]}]))
if cmd == "Connected":
self.kh2slotdata = args['slot_data']
# self.kh2_local_items = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()}
self.kh2_data_package = Utils.load_data_package_for_checksum(
"Kingdom Hearts 2", self.checksums["Kingdom Hearts 2"])
if "location_name_to_id" in self.kh2_data_package:
self.data_package_kh2_cache(
self.kh2_data_package["location_name_to_id"], self.kh2_data_package["item_name_to_id"])
self.connect_to_game()
else:
asyncio.create_task(self.send_msgs([{"cmd": "GetDataPackage", "games": ["Kingdom Hearts 2"]}]))
self.locations_checked = set(args["checked_locations"])
if cmd in {"ReceivedItems"}:
if cmd == "ReceivedItems":
# 0x2546
# 0x2658
# 0x276A
@@ -334,56 +343,46 @@ class KH2Context(CommonContext):
for item in args['items']:
asyncio.create_task(self.give_item(item.item, item.location))
if cmd in {"RoomUpdate"}:
if cmd == "RoomUpdate":
if "checked_locations" in args:
new_locations = set(args["checked_locations"])
self.locations_checked |= new_locations
if cmd in {"DataPackage"}:
self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"]
self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()}
self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"]
self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()}
self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]]
if cmd == "DataPackage":
if "Kingdom Hearts 2" in args["data"]["games"]:
self.data_package_kh2_cache(
args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"],
args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"])
self.connect_to_game()
asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}]))
if "KeybladeAbilities" in self.kh2slotdata.keys():
# sora ability to slot
self.AbilityQuantityDict.update(self.kh2slotdata["KeybladeAbilities"])
# itemid:[slots that are available for that item]
self.AbilityQuantityDict.update(self.kh2slotdata["StaffAbilities"])
self.AbilityQuantityDict.update(self.kh2slotdata["ShieldAbilities"])
def connect_to_game(self):
if "KeybladeAbilities" in self.kh2slotdata.keys():
# sora ability to slot
self.AbilityQuantityDict.update(self.kh2slotdata["KeybladeAbilities"])
# itemid:[slots that are available for that item]
self.AbilityQuantityDict.update(self.kh2slotdata["StaffAbilities"])
self.AbilityQuantityDict.update(self.kh2slotdata["ShieldAbilities"])
all_weapon_location_id = []
for weapon_location in all_weapon_slot:
all_weapon_location_id.append(self.kh2_loc_name_to_id[weapon_location])
self.all_weapon_location_id = set(all_weapon_location_id)
self.all_weapon_location_id = {self.kh2_loc_name_to_id[loc] for loc in all_weapon_slot}
try:
try:
if not self.kh2:
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
if self.kh2_game_version is None:
if self.kh2_read_string(0x09A9830, 4) == "KH2J":
self.kh2_game_version = "STEAM"
self.Now = 0x0717008
self.Save = 0x09A9830
self.Slot1 = 0x2A23518
self.Journal = 0x7434E0
self.Shop = 0x7435D0
self.get_addresses()
elif self.kh2_read_string(0x09A92F0, 4) == "KH2J":
self.kh2_game_version = "EGS"
else:
self.kh2_game_version = None
logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.")
if self.kh2_game_version is not None:
logger.info(f"You are now auto-tracking. {self.kh2_game_version}")
self.kh2connected = True
except Exception as e:
if self.kh2connected:
self.kh2connected = False
logger.info("Game is not open.")
self.serverconneced = True
except Exception as e:
if self.kh2connected:
self.kh2connected = False
logger.info("Game is not open.")
self.serverconneced = True
asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}]))
def data_package_kh2_cache(self, loc_to_id, item_to_id):
self.kh2_loc_name_to_id = loc_to_id
self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()}
self.kh2_item_name_to_id = item_to_id
self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()}
self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]]
async def checkWorldLocations(self):
try:
@@ -425,7 +424,6 @@ class KH2Context(CommonContext):
0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels],
3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels], 5: ["SummonLevel", SummonLevels]
}
# TODO: remove formDict[i][0] in self.kh2_seed_save_cache["Levels"].keys() after 4.3
for i in range(6):
for location, data in formDict[i][1].items():
formlevel = self.kh2_read_byte(self.Save + data.addrObtained)
@@ -469,9 +467,11 @@ class KH2Context(CommonContext):
if locationName in self.chest_set:
if locationName in self.location_name_to_worlddata.keys():
locationData = self.location_name_to_worlddata[locationName]
if self.kh2_read_byte(self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0:
if self.kh2_read_byte(
self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0:
roomData = self.kh2_read_byte(self.Save + locationData.addrObtained)
self.kh2_write_byte(self.Save + locationData.addrObtained, roomData | 0x01 << locationData.bitIndex)
self.kh2_write_byte(self.Save + locationData.addrObtained,
roomData | 0x01 << locationData.bitIndex)
except Exception as e:
if self.kh2connected:
@@ -494,6 +494,9 @@ class KH2Context(CommonContext):
async def give_item(self, item, location):
try:
# todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites
#sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts
while not self.lookup_id_to_item:
await asyncio.sleep(0.5)
itemname = self.lookup_id_to_item[item]
itemdata = self.item_name_to_data[itemname]
# itemcode = self.kh2_item_name_to_id[itemname]
@@ -637,7 +640,8 @@ class KH2Context(CommonContext):
item_data = self.item_name_to_data[item_name]
# if the inventory slot for that keyblade is less than the amount they should have,
# and they are not in stt
if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte(self.Save + 0x1CFF) != 13:
if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte(
self.Save + 0x1CFF) != 13:
# Checking form anchors for the keyblade to remove extra keyblades
if self.kh2_read_short(self.Save + 0x24F0) == item_data.kh2id \
or self.kh2_read_short(self.Save + 0x32F4) == item_data.kh2id \
@@ -738,13 +742,15 @@ class KH2Context(CommonContext):
item_data = self.item_name_to_data[item_name]
amount_of_items = 0
amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Magic"][item_name]
if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(self.Shop) in {10, 8}:
if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(
self.Shop) in {10, 8}:
self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
for item_name in master_stat:
amount_of_items = 0
amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][item_name]
if self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5:
# checking if they talked to the computer to give them these
if self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and (self.kh2_read_byte(self.Save + 0x1D27) & 0x1 << 3) > 0:
if item_name == ItemName.MaxHPUp:
if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical
Bonus = 5
@@ -797,7 +803,8 @@ class KH2Context(CommonContext):
# self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
if "PoptrackerVersionCheck" in self.kh2slotdata:
if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3
if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(
self.Save + 0x3607) != 1: # telling the goa they are on version 4.3
self.kh2_write_byte(self.Save + 0x3607, 1)
except Exception as e:
@@ -806,10 +813,58 @@ class KH2Context(CommonContext):
logger.info(e)
logger.info("line 840")
def get_addresses(self):
if not self.kh2connected and self.kh2 is not None:
if self.kh2_game_version is None:
# current verions is .10 then runs the get from github stuff
if self.kh2_read_string(0x9A98B0, 4) == "KH2J":
self.kh2_game_version = "STEAM"
self.Now = 0x0717008
self.Save = 0x09A98B0
self.Slot1 = 0x2A23598
self.Journal = 0x7434E0
self.Shop = 0x7435D0
elif self.kh2_read_string(0x9A9330, 4) == "KH2J":
self.kh2_game_version = "EGS"
else:
if self.game_communication_path:
logger.info("Checking with most up to date addresses from the addresses json.")
#if mem addresses file is found then check version and if old get new one
kh2memaddresses_path = os.path.join(self.game_communication_path, "kh2memaddresses.json")
if not os.path.exists(kh2memaddresses_path):
logger.info("File is not found. Downloading json with memory addresses. This might take a moment")
mem_resp = requests.get("https://raw.githubusercontent.com/JaredWeakStrike/KH2APMemoryValues/master/kh2memaddresses.json")
if mem_resp.status_code == 200:
self.mem_json = json.loads(mem_resp.content)
with open(kh2memaddresses_path, 'w') as f:
f.write(json.dumps(self.mem_json, indent=4))
else:
with open(kh2memaddresses_path) as f:
self.mem_json = json.load(f)
if self.mem_json:
for key in self.mem_json.keys():
if self.kh2_read_string(int(self.mem_json[key]["GameVersionCheck"], 0), 4) == "KH2J":
self.Now = int(self.mem_json[key]["Now"], 0)
self.Save = int(self.mem_json[key]["Save"], 0)
self.Slot1 = int(self.mem_json[key]["Slot1"], 0)
self.Journal = int(self.mem_json[key]["Journal"], 0)
self.Shop = int(self.mem_json[key]["Shop"], 0)
self.kh2_game_version = key
if self.kh2_game_version is not None:
logger.info(f"You are now auto-tracking {self.kh2_game_version}")
self.kh2connected = True
else:
logger.info("Your game version does not match what the client requires. Check in the "
"kingdom-hearts-2-final-mix channel for more information on correcting the game "
"version.")
self.kh2connected = False
def finishedGame(ctx: KH2Context):
if ctx.kh2slotdata['FinalXemnas'] == 1:
if not ctx.final_xemnas and ctx.kh2_read_byte(ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \
if not ctx.final_xemnas and ctx.kh2_read_byte(
ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \
& 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0:
ctx.final_xemnas = True
# three proofs
@@ -843,7 +898,8 @@ def finishedGame(ctx: KH2Context):
for boss in ctx.kh2slotdata["hitlist"]:
if boss in locations:
ctx.hitlist_bounties += 1
if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"]["Bounty"] >= ctx.kh2slotdata["BountyRequired"]:
if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"][
"Bounty"] >= ctx.kh2slotdata["BountyRequired"]:
if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1:
ctx.kh2_write_byte(ctx.Save + 0x36B2, 1)
ctx.kh2_write_byte(ctx.Save + 0x36B3, 1)
@@ -894,24 +950,7 @@ async def kh2_watcher(ctx: KH2Context):
while not ctx.kh2connected and ctx.serverconneced:
await asyncio.sleep(15)
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
if ctx.kh2 is not None:
if ctx.kh2_game_version is None:
if ctx.kh2_read_string(0x09A9830, 4) == "KH2J":
ctx.kh2_game_version = "STEAM"
ctx.Now = 0x0717008
ctx.Save = 0x09A9830
ctx.Slot1 = 0x2A23518
ctx.Journal = 0x7434E0
ctx.Shop = 0x7435D0
elif ctx.kh2_read_string(0x09A92F0, 4) == "KH2J":
ctx.kh2_game_version = "EGS"
else:
ctx.kh2_game_version = None
logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.")
if ctx.kh2_game_version is not None:
logger.info(f"You are now auto-tracking {ctx.kh2_game_version}")
ctx.kh2connected = True
ctx.get_addresses()
except Exception as e:
if ctx.kh2connected:
ctx.kh2connected = False

View File

@@ -368,6 +368,37 @@ def patch_kh2(self, output_directory):
}
]
},
{
'name': 'msg/us/he.bar',
'multi': [
{
'name': 'msg/fr/he.bar'
},
{
'name': 'msg/gr/he.bar'
},
{
'name': 'msg/it/he.bar'
},
{
'name': 'msg/sp/he.bar'
}
],
'method': 'binarc',
'source': [
{
'name': 'he',
'type': 'list',
'method': 'kh2msg',
'source': [
{
'name': 'he.yml',
'language': 'en'
}
]
}
]
},
],
'title': 'Randomizer Seed'
}
@@ -411,6 +442,34 @@ def patch_kh2(self, output_directory):
'en': f"Your Level Depth is {self.options.LevelDepth.current_option_name}"
}
]
self.fight_and_form_text = [
{
'id': 15121, # poster name
'en': f"Game Options"
},
{
'id': 15122,
'en': f"Fight Logic is {self.options.FightLogic.current_option_name}\n"
f"Auto Form Logic is {self.options.AutoFormLogic.current_option_name}\n"
f"Final Form Logic is {self.options.FinalFormLogic.current_option_name}"
}
]
self.cups_text = [
{
'id': 4043,
'en': f"CupsToggle: {self.options.Cups.current_option_name}"
},
{
'id': 4044,
'en': f"CupsToggle: {self.options.Cups.current_option_name}"
},
{
'id': 4045,
'en': f"CupsToggle: {self.options.Cups.current_option_name}"
},
]
mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__)
self.mod_yml["title"] = f"Randomizer Seed {mod_name}"
@@ -423,7 +482,8 @@ def patch_kh2(self, output_directory):
"FmlvList.yml": yaml.dump(self.formattedFmlv, line_break="\n"),
"mod.yml": yaml.dump(self.mod_yml, line_break="\n"),
"po.yml": yaml.dump(self.pooh_text, line_break="\n"),
"sys.yml": yaml.dump(self.level_depth_text, line_break="\n"),
"sys.yml": yaml.dump(self.level_depth_text + self.fight_and_form_text, line_break="\n"),
"he.yml": yaml.dump(self.cups_text, line_break="\n")
}
mod = KH2Container(openkhmod, mod_dir, output_directory, self.player,

View File

@@ -540,7 +540,7 @@ KH2REGIONS: typing.Dict[str, typing.List[str]] = {
LocationName.SephirothFenrir,
LocationName.SephiEventLocation
],
RegionName.CoR: [
RegionName.CoR: [ #todo: make logic for getting these checks.
LocationName.CoRDepthsAPBoost,
LocationName.CoRDepthsPowerCrystal,
LocationName.CoRDepthsFrostCrystal,
@@ -1032,99 +1032,99 @@ def connect_regions(self):
multiworld = self.multiworld
player = self.player
# connecting every first visit to the GoA
KH2RegionConnections: typing.Dict[str, typing.Set[str]] = {
"Menu": {RegionName.GoA},
RegionName.GoA: {RegionName.Sp, RegionName.Pr, RegionName.Tt, RegionName.Oc, RegionName.Ht,
KH2RegionConnections: typing.Dict[str, typing.Tuple[str]] = {
"Menu": (RegionName.GoA,),
RegionName.GoA: (RegionName.Sp, RegionName.Pr, RegionName.Tt, RegionName.Oc, RegionName.Ht,
RegionName.LoD,
RegionName.Twtnw, RegionName.Bc, RegionName.Ag, RegionName.Pl, RegionName.Hb,
RegionName.Dc, RegionName.Stt,
RegionName.Ha1, RegionName.Keyblade, RegionName.LevelsVS1,
RegionName.Valor, RegionName.Wisdom, RegionName.Limit, RegionName.Master,
RegionName.Final, RegionName.Summon, RegionName.AtlanticaSongOne},
RegionName.LoD: {RegionName.ShanYu},
RegionName.ShanYu: {RegionName.LoD2},
RegionName.LoD2: {RegionName.AnsemRiku},
RegionName.AnsemRiku: {RegionName.StormRider},
RegionName.StormRider: {RegionName.DataXigbar},
RegionName.Ag: {RegionName.TwinLords},
RegionName.TwinLords: {RegionName.Ag2},
RegionName.Ag2: {RegionName.GenieJafar},
RegionName.GenieJafar: {RegionName.DataLexaeus},
RegionName.Dc: {RegionName.Tr},
RegionName.Tr: {RegionName.OldPete},
RegionName.OldPete: {RegionName.FuturePete},
RegionName.FuturePete: {RegionName.Terra, RegionName.DataMarluxia},
RegionName.Ha1: {RegionName.Ha2},
RegionName.Ha2: {RegionName.Ha3},
RegionName.Ha3: {RegionName.Ha4},
RegionName.Ha4: {RegionName.Ha5},
RegionName.Ha5: {RegionName.Ha6},
RegionName.Pr: {RegionName.Barbosa},
RegionName.Barbosa: {RegionName.Pr2},
RegionName.Pr2: {RegionName.GrimReaper1},
RegionName.GrimReaper1: {RegionName.GrimReaper2},
RegionName.GrimReaper2: {RegionName.DataLuxord},
RegionName.Oc: {RegionName.Cerberus},
RegionName.Cerberus: {RegionName.OlympusPete},
RegionName.OlympusPete: {RegionName.Hydra},
RegionName.Hydra: {RegionName.OcPainAndPanicCup, RegionName.OcCerberusCup, RegionName.Oc2},
RegionName.Oc2: {RegionName.Hades},
RegionName.Hades: {RegionName.Oc2TitanCup, RegionName.Oc2GofCup, RegionName.DataZexion},
RegionName.Oc2GofCup: {RegionName.HadesCups},
RegionName.Bc: {RegionName.Thresholder},
RegionName.Thresholder: {RegionName.Beast},
RegionName.Beast: {RegionName.DarkThorn},
RegionName.DarkThorn: {RegionName.Bc2},
RegionName.Bc2: {RegionName.Xaldin},
RegionName.Xaldin: {RegionName.DataXaldin},
RegionName.Sp: {RegionName.HostileProgram},
RegionName.HostileProgram: {RegionName.Sp2},
RegionName.Sp2: {RegionName.Mcp},
RegionName.Mcp: {RegionName.DataLarxene},
RegionName.Ht: {RegionName.PrisonKeeper},
RegionName.PrisonKeeper: {RegionName.OogieBoogie},
RegionName.OogieBoogie: {RegionName.Ht2},
RegionName.Ht2: {RegionName.Experiment},
RegionName.Experiment: {RegionName.DataVexen},
RegionName.Hb: {RegionName.Hb2},
RegionName.Hb2: {RegionName.CoR, RegionName.HBDemyx},
RegionName.HBDemyx: {RegionName.ThousandHeartless},
RegionName.ThousandHeartless: {RegionName.Mushroom13, RegionName.DataDemyx, RegionName.Sephi},
RegionName.CoR: {RegionName.CorFirstFight},
RegionName.CorFirstFight: {RegionName.CorSecondFight},
RegionName.CorSecondFight: {RegionName.Transport},
RegionName.Pl: {RegionName.Scar},
RegionName.Scar: {RegionName.Pl2},
RegionName.Pl2: {RegionName.GroundShaker},
RegionName.GroundShaker: {RegionName.DataSaix},
RegionName.Stt: {RegionName.TwilightThorn},
RegionName.TwilightThorn: {RegionName.Axel1},
RegionName.Axel1: {RegionName.Axel2},
RegionName.Axel2: {RegionName.DataRoxas},
RegionName.Tt: {RegionName.Tt2},
RegionName.Tt2: {RegionName.Tt3},
RegionName.Tt3: {RegionName.DataAxel},
RegionName.Twtnw: {RegionName.Roxas},
RegionName.Roxas: {RegionName.Xigbar},
RegionName.Xigbar: {RegionName.Luxord},
RegionName.Luxord: {RegionName.Saix},
RegionName.Saix: {RegionName.Twtnw2},
RegionName.Twtnw2: {RegionName.Xemnas},
RegionName.Xemnas: {RegionName.ArmoredXemnas, RegionName.DataXemnas},
RegionName.ArmoredXemnas: {RegionName.ArmoredXemnas2},
RegionName.ArmoredXemnas2: {RegionName.FinalXemnas},
RegionName.LevelsVS1: {RegionName.LevelsVS3},
RegionName.LevelsVS3: {RegionName.LevelsVS6},
RegionName.LevelsVS6: {RegionName.LevelsVS9},
RegionName.LevelsVS9: {RegionName.LevelsVS12},
RegionName.LevelsVS12: {RegionName.LevelsVS15},
RegionName.LevelsVS15: {RegionName.LevelsVS18},
RegionName.LevelsVS18: {RegionName.LevelsVS21},
RegionName.LevelsVS21: {RegionName.LevelsVS24},
RegionName.LevelsVS24: {RegionName.LevelsVS26},
RegionName.AtlanticaSongOne: {RegionName.AtlanticaSongTwo},
RegionName.AtlanticaSongTwo: {RegionName.AtlanticaSongThree},
RegionName.AtlanticaSongThree: {RegionName.AtlanticaSongFour},
RegionName.Final, RegionName.Summon, RegionName.AtlanticaSongOne),
RegionName.LoD: (RegionName.ShanYu,),
RegionName.ShanYu: (RegionName.LoD2,),
RegionName.LoD2: (RegionName.AnsemRiku,),
RegionName.AnsemRiku: (RegionName.StormRider,),
RegionName.StormRider: (RegionName.DataXigbar,),
RegionName.Ag: (RegionName.TwinLords,),
RegionName.TwinLords: (RegionName.Ag2,),
RegionName.Ag2: (RegionName.GenieJafar,),
RegionName.GenieJafar: (RegionName.DataLexaeus,),
RegionName.Dc: (RegionName.Tr,),
RegionName.Tr: (RegionName.OldPete,),
RegionName.OldPete: (RegionName.FuturePete,),
RegionName.FuturePete: (RegionName.Terra, RegionName.DataMarluxia),
RegionName.Ha1: (RegionName.Ha2,),
RegionName.Ha2: (RegionName.Ha3,),
RegionName.Ha3: (RegionName.Ha4,),
RegionName.Ha4: (RegionName.Ha5,),
RegionName.Ha5: (RegionName.Ha6,),
RegionName.Pr: (RegionName.Barbosa,),
RegionName.Barbosa: (RegionName.Pr2,),
RegionName.Pr2: (RegionName.GrimReaper1,),
RegionName.GrimReaper1: (RegionName.GrimReaper2,),
RegionName.GrimReaper2: (RegionName.DataLuxord,),
RegionName.Oc: (RegionName.Cerberus,),
RegionName.Cerberus: (RegionName.OlympusPete,),
RegionName.OlympusPete: (RegionName.Hydra,),
RegionName.Hydra: (RegionName.OcPainAndPanicCup, RegionName.OcCerberusCup, RegionName.Oc2),
RegionName.Oc2: (RegionName.Hades,),
RegionName.Hades: (RegionName.Oc2TitanCup, RegionName.Oc2GofCup, RegionName.DataZexion),
RegionName.Oc2GofCup: (RegionName.HadesCups,),
RegionName.Bc: (RegionName.Thresholder,),
RegionName.Thresholder: (RegionName.Beast,),
RegionName.Beast: (RegionName.DarkThorn,),
RegionName.DarkThorn: (RegionName.Bc2,),
RegionName.Bc2: (RegionName.Xaldin,),
RegionName.Xaldin: (RegionName.DataXaldin,),
RegionName.Sp: (RegionName.HostileProgram,),
RegionName.HostileProgram: (RegionName.Sp2,),
RegionName.Sp2: (RegionName.Mcp,),
RegionName.Mcp: (RegionName.DataLarxene,),
RegionName.Ht: (RegionName.PrisonKeeper,),
RegionName.PrisonKeeper: (RegionName.OogieBoogie,),
RegionName.OogieBoogie: (RegionName.Ht2,),
RegionName.Ht2: (RegionName.Experiment,),
RegionName.Experiment: (RegionName.DataVexen,),
RegionName.Hb: (RegionName.Hb2,),
RegionName.Hb2: (RegionName.CoR, RegionName.HBDemyx),
RegionName.HBDemyx: (RegionName.ThousandHeartless,),
RegionName.ThousandHeartless: (RegionName.Mushroom13, RegionName.DataDemyx, RegionName.Sephi),
RegionName.CoR: (RegionName.CorFirstFight,),
RegionName.CorFirstFight: (RegionName.CorSecondFight,),
RegionName.CorSecondFight: (RegionName.Transport,),
RegionName.Pl: (RegionName.Scar,),
RegionName.Scar: (RegionName.Pl2,),
RegionName.Pl2: (RegionName.GroundShaker,),
RegionName.GroundShaker: (RegionName.DataSaix,),
RegionName.Stt: (RegionName.TwilightThorn,),
RegionName.TwilightThorn: (RegionName.Axel1,),
RegionName.Axel1: (RegionName.Axel2,),
RegionName.Axel2: (RegionName.DataRoxas,),
RegionName.Tt: (RegionName.Tt2,),
RegionName.Tt2: (RegionName.Tt3,),
RegionName.Tt3: (RegionName.DataAxel,),
RegionName.Twtnw: (RegionName.Roxas,),
RegionName.Roxas: (RegionName.Xigbar,),
RegionName.Xigbar: (RegionName.Luxord,),
RegionName.Luxord: (RegionName.Saix,),
RegionName.Saix: (RegionName.Twtnw2,),
RegionName.Twtnw2: (RegionName.Xemnas,),
RegionName.Xemnas: (RegionName.ArmoredXemnas, RegionName.DataXemnas),
RegionName.ArmoredXemnas: (RegionName.ArmoredXemnas2,),
RegionName.ArmoredXemnas2: (RegionName.FinalXemnas,),
RegionName.LevelsVS1: (RegionName.LevelsVS3,),
RegionName.LevelsVS3: (RegionName.LevelsVS6,),
RegionName.LevelsVS6: (RegionName.LevelsVS9,),
RegionName.LevelsVS9: (RegionName.LevelsVS12,),
RegionName.LevelsVS12: (RegionName.LevelsVS15,),
RegionName.LevelsVS15: (RegionName.LevelsVS18,),
RegionName.LevelsVS18: (RegionName.LevelsVS21,),
RegionName.LevelsVS21: (RegionName.LevelsVS24,),
RegionName.LevelsVS24: (RegionName.LevelsVS26,),
RegionName.AtlanticaSongOne: (RegionName.AtlanticaSongTwo,),
RegionName.AtlanticaSongTwo: (RegionName.AtlanticaSongThree,),
RegionName.AtlanticaSongThree: (RegionName.AtlanticaSongFour,),
}
for source, target in KH2RegionConnections.items():

View File

@@ -194,8 +194,8 @@ class KH2WorldRules(KH2Rules):
RegionName.Oc: lambda state: self.oc_unlocked(state, 1),
RegionName.Oc2: lambda state: self.oc_unlocked(state, 2),
#twtnw1 is actually the roxas fight region thus roxas requires 1 way to the dawn
RegionName.Twtnw2: lambda state: self.twtnw_unlocked(state, 2),
# These will be swapped and First Visit lock for twtnw is in development.
# RegionName.Twtnw1: lambda state: self.lod_unlocked(state, 2),
RegionName.Ht: lambda state: self.ht_unlocked(state, 1),
@@ -263,7 +263,10 @@ class KH2WorldRules(KH2Rules):
weapon_region = self.multiworld.get_region(RegionName.Keyblade, self.player)
for location in weapon_region.locations:
add_rule(location, lambda state: state.has(exclusion_table["WeaponSlots"][location.name], self.player))
if location.name in exclusion_table["WeaponSlots"]: # shop items and starting items are not in this list
exclusion_item = exclusion_table["WeaponSlots"][location.name]
add_rule(location, lambda state, e_item=exclusion_item: state.has(e_item, self.player))
if location.name in Goofy_Checks:
add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys())
elif location.name in Donald_Checks:
@@ -919,8 +922,8 @@ class KH2FightRules(KH2Rules):
# normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus
# hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus
sephiroth_rules = {
"easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1,
"normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2,
"easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state),
"normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([gap_closer], state) >= 1,
"hard": self.kh2_dict_count(hard_sephiroth_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2,
}
return sephiroth_rules[self.fight_logic]

View File

@@ -3,7 +3,7 @@ from typing import List
from BaseClasses import Tutorial, ItemClassification
from Fill import fast_fill
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
from worlds.LauncherComponents import Component, components, Type, launch as launch_component
from worlds.AutoWorld import World, WebWorld
from .Items import *
from .Locations import *
@@ -17,7 +17,7 @@ from .Subclasses import KH2Item
def launch_client():
from .Client import launch
launch_subprocess(launch, name="KH2Client")
launch_component(launch, name="KH2Client")
components.append(Component("KH2 Client", "KH2Client", func=launch_client, component_type=Type.CLIENT))

View File

@@ -10,7 +10,7 @@
Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
1. Version 3.4.0 or greater OpenKH Mod Manager with Panacea
1. Version 25.01.26.0 or greater OpenKH Mod Manager with Panacea
2. Lua Backend from the OpenKH Mod Manager
3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager
- Needed for Archipelago
@@ -52,7 +52,7 @@ After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot
<h2 style="text-transform:none";>What the Mod Manager Should Look Like.</h2>
![image](https://i.imgur.com/Si4oZ8w.png)
![image](https://i.imgur.com/N0WJ8Qn.png)
<h2 style="text-transform:none";>Using the KH2 Client</h2>

View File

@@ -1,92 +1,266 @@
import json
roomAddress = 0xFFF6
mapIdAddress = 0xFFF7
indoorFlagAddress = 0xDBA5
entranceRoomOffset = 0xD800
screenCoordAddress = 0xFFFA
import typing
from websockets import WebSocketServerProtocol
mapMap = {
0x00: 0x01,
0x01: 0x01,
0x02: 0x01,
0x03: 0x01,
0x04: 0x01,
0x05: 0x01,
0x06: 0x02,
0x07: 0x02,
0x08: 0x02,
0x09: 0x02,
0x0A: 0x02,
0x0B: 0x02,
0x0C: 0x02,
0x0D: 0x02,
0x0E: 0x02,
0x0F: 0x02,
0x10: 0x02,
0x11: 0x02,
0x12: 0x02,
0x13: 0x02,
0x14: 0x02,
0x15: 0x02,
0x16: 0x02,
0x17: 0x02,
0x18: 0x02,
0x19: 0x02,
0x1D: 0x01,
0x1E: 0x01,
0x1F: 0x01,
0xFF: 0x03,
}
from . import TrackerConsts as Consts
from .TrackerConsts import EntranceCoord
from .LADXR.entranceInfo import ENTRANCE_INFO
class Entrance:
outdoor_room: int
indoor_map: int
indoor_address: int
name: str
other_side_name: str = None
changed: bool = False
known_to_server: bool = False
def __init__(self, outdoor: int, indoor: int, name: str, indoor_address: int=None):
self.outdoor_room = outdoor
self.indoor_map = indoor
self.indoor_address = indoor_address
self.name = name
def map(self, other_side: str, known_to_server: bool = False):
if other_side != self.other_side_name:
self.changed = True
self.known_to_server = known_to_server
self.other_side_name = other_side
class GpsTracker:
room = None
location_changed = False
screenX = 0
screenY = 0
indoors = None
room: int = None
last_room: int = None
last_different_room: int = None
room_same_for: int = 0
room_changed: bool = False
screen_x: int = 0
screen_y: int = 0
spawn_x: int = 0
spawn_y: int = 0
indoors: int = None
indoors_changed: bool = False
spawn_map: int = None
spawn_room: int = None
spawn_changed: bool = False
spawn_same_for: int = 0
entrance_mapping: typing.Dict[str, str] = None
entrances_by_name: typing.Dict[str, Entrance] = {}
needs_found_entrances: bool = False
needs_slot_data: bool = True
def __init__(self, gameboy) -> None:
self.gameboy = gameboy
async def read_byte(self, b):
return (await self.gameboy.async_read_memory(b))[0]
self.gameboy.set_location_range(
Consts.link_motion_state,
Consts.transition_sequence - Consts.link_motion_state + 1,
[Consts.transition_state]
)
async def read_byte(self, b: int):
return (await self.gameboy.read_memory_cache([b]))[b]
def load_slot_data(self, slot_data: typing.Dict[str, typing.Any]):
if 'entrance_mapping' not in slot_data:
return
# We need to know how entrances were mapped at generation before we can autotrack them
self.entrance_mapping = {}
# Convert to upstream's newer format
for outside, inside in slot_data['entrance_mapping'].items():
new_inside = f"{inside}:inside"
self.entrance_mapping[outside] = new_inside
self.entrance_mapping[new_inside] = outside
self.entrances_by_name = {}
for name, info in ENTRANCE_INFO.items():
alternate_address = (
Consts.entrance_address_overrides[info.target]
if info.target in Consts.entrance_address_overrides
else None
)
entrance = Entrance(info.room, info.target, name, alternate_address)
self.entrances_by_name[name] = entrance
inside_entrance = Entrance(info.target, info.room, f"{name}:inside", alternate_address)
self.entrances_by_name[f"{name}:inside"] = inside_entrance
self.needs_slot_data = False
self.needs_found_entrances = True
async def read_location(self):
indoors = await self.read_byte(indoorFlagAddress)
# We need to wait for screen transitions to finish
transition_state = await self.read_byte(Consts.transition_state)
transition_target_x = await self.read_byte(Consts.transition_target_x)
transition_target_y = await self.read_byte(Consts.transition_target_y)
transition_scroll_x = await self.read_byte(Consts.transition_scroll_x)
transition_scroll_y = await self.read_byte(Consts.transition_scroll_y)
transition_sequence = await self.read_byte(Consts.transition_sequence)
motion_state = await self.read_byte(Consts.link_motion_state)
if (transition_state != 0
or transition_target_x != transition_scroll_x
or transition_target_y != transition_scroll_y
or transition_sequence != 0x04):
return
indoors = await self.read_byte(Consts.indoor_flag)
if indoors != self.indoors and self.indoors != None:
self.indoorsChanged = True
self.indoors_changed = True
self.indoors = indoors
mapId = await self.read_byte(mapIdAddress)
if mapId not in mapMap:
print(f'Unknown map ID {hex(mapId)}')
# We use the spawn point to know which entrance was most recently entered
spawn_map = await self.read_byte(Consts.spawn_map)
map_digit = Consts.map_map[spawn_map] << 8 if self.spawn_map else 0
spawn_room = await self.read_byte(Consts.spawn_room) + map_digit
spawn_x = await self.read_byte(Consts.spawn_x)
spawn_y = await self.read_byte(Consts.spawn_y)
# The spawn point needs to be settled before we can trust location data
if ((spawn_room != self.spawn_room and self.spawn_room != None)
or (spawn_map != self.spawn_map and self.spawn_map != None)
or (spawn_x != self.spawn_x and self.spawn_x != None)
or (spawn_y != self.spawn_y and self.spawn_y != None)):
self.spawn_changed = True
self.spawn_same_for = 0
else:
self.spawn_same_for += 1
self.spawn_map = spawn_map
self.spawn_room = spawn_room
self.spawn_x = spawn_x
self.spawn_y = spawn_y
# Spawn point is preferred, but doesn't work for the sidescroller entrances
# Those can be addressed by keeping track of which room we're in
# Also used to validate that we came from the right room for what the spawn point is mapped to
map_id = await self.read_byte(Consts.map_id)
if map_id not in Consts.map_map:
print(f'Unknown map ID {hex(map_id)}')
return
mapDigit = mapMap[mapId] << 8 if indoors else 0
last_room = self.room
self.room = await self.read_byte(roomAddress) + mapDigit
map_digit = Consts.map_map[map_id] << 8 if indoors else 0
self.last_room = self.room
self.room = await self.read_byte(Consts.room) + map_digit
coords = await self.read_byte(screenCoordAddress)
self.screenX = coords & 0x0F
self.screenY = (coords & 0xF0) >> 4
# Again, the room needs to settle before we can trust location data
if self.last_room != self.room:
self.room_same_for = 0
self.room_changed = True
self.last_different_room = self.last_room
else:
self.room_same_for += 1
if (self.room != last_room):
self.location_changed = True
last_message = {}
async def send_location(self, socket, diff=False):
if self.room is None:
# Only update Link's location when he's not in the air to avoid weirdness
if motion_state in [0, 1]:
coords = await self.read_byte(Consts.screen_coord)
self.screen_x = coords & 0x0F
self.screen_y = (coords & 0xF0) >> 4
async def read_entrances(self):
if not self.last_different_room or not self.entrance_mapping:
return
if self.spawn_changed and self.spawn_same_for > 0 and self.room_same_for > 0:
# Use the spawn location, last room, and entrance mapping at generation to map the right entrance
# A bit overkill for simple ER, but necessary for upstream's advanced ER
spawn_coord = EntranceCoord(None, self.spawn_room, self.spawn_x, self.spawn_y)
if str(spawn_coord) in Consts.entrance_lookup:
valid_sources = {x.name for x in Consts.entrance_coords if x.room == self.last_different_room}
dest_entrance = Consts.entrance_lookup[str(spawn_coord)].name
source_entrance = [
x for x in self.entrance_mapping
if self.entrance_mapping[x] == dest_entrance and x in valid_sources
]
if source_entrance:
self.entrances_by_name[source_entrance[0]].map(dest_entrance)
self.spawn_changed = False
elif self.room_changed and self.room_same_for > 0:
# Check for the stupid sidescroller rooms that don't set your spawn point
if self.last_different_room in Consts.sidescroller_rooms:
source_entrance = Consts.sidescroller_rooms[self.last_different_room]
if source_entrance in self.entrance_mapping:
dest_entrance = self.entrance_mapping[source_entrance]
expected_room = self.entrances_by_name[dest_entrance].outdoor_room
if dest_entrance.endswith(":indoor"):
expected_room = self.entrances_by_name[dest_entrance].indoor_map
if expected_room == self.room:
self.entrances_by_name[source_entrance].map(dest_entrance)
if self.room in Consts.sidescroller_rooms:
valid_sources = {x.name for x in Consts.entrance_coords if x.room == self.last_different_room}
dest_entrance = Consts.sidescroller_rooms[self.room]
source_entrance = [
x for x in self.entrance_mapping
if self.entrance_mapping[x] == dest_entrance and x in valid_sources
]
if source_entrance:
self.entrances_by_name[source_entrance[0]].map(dest_entrance)
self.room_changed = False
last_location_message = {}
async def send_location(self, socket: WebSocketServerProtocol) -> None:
if self.room is None or self.room_same_for < 1:
return
message = {
"type":"location",
"refresh": True,
"version":"1.0",
"room": f'0x{self.room:02X}',
"x": self.screenX,
"y": self.screenY,
"x": self.screen_x,
"y": self.screen_y,
"drawFine": True,
}
if message != self.last_message:
self.last_message = message
if message != self.last_location_message:
self.last_location_message = message
await socket.send(json.dumps(message))
async def send_entrances(self, socket: WebSocketServerProtocol, diff: bool=True) -> typing.Dict[str, str]:
if not self.entrance_mapping:
return
new_entrances = [x for x in self.entrances_by_name.values() if x.changed or (not diff and x.other_side_name)]
if not new_entrances:
return
message = {
"type":"entrance",
"refresh": True,
"diff": True,
"entranceMap": {},
}
for entrance in new_entrances:
message['entranceMap'][entrance.name] = entrance.other_side_name
entrance.changed = False
await socket.send(json.dumps(message))
new_to_server = {
entrance.name: entrance.other_side_name
for entrance in new_entrances
if not entrance.known_to_server
}
return new_to_server
def receive_found_entrances(self, found_entrances: typing.Dict[str, str]):
if not found_entrances:
return
for entrance, destination in found_entrances.items():
if entrance in self.entrances_by_name:
self.entrances_by_name[entrance].map(destination, known_to_server=True)

View File

@@ -1,12 +1,16 @@
import json
gameStateAddress = 0xDB95
validGameStates = {0x0B, 0x0C}
gameStateResetThreshold = 0x06
inventorySlotCount = 16
inventoryStartAddress = 0xDB00
inventoryEndAddress = inventoryStartAddress + inventorySlotCount
rupeesHigh = 0xDB5D
rupeesLow = 0xDB5E
addRupeesHigh = 0xDB8F
addRupeesLow = 0xDB90
removeRupeesHigh = 0xDB91
removeRupeesLow = 0xDB92
inventoryItemIds = {
0x02: 'BOMB',
0x05: 'BOW',
@@ -98,10 +102,11 @@ dungeonItemOffsets = {
'STONE_BEAK{}': 2,
'NIGHTMARE_KEY{}': 3,
'KEY{}': 4,
'UNUSED_KEY{}': 4,
}
class Item:
def __init__(self, id, address, threshold=0, mask=None, increaseOnly=False, count=False, max=None):
def __init__(self, id, address, threshold=0, mask=None, increaseOnly=False, count=False, max=None, encodedCount=True):
self.id = id
self.address = address
self.threshold = threshold
@@ -112,6 +117,7 @@ class Item:
self.rawValue = 0
self.diff = 0
self.max = max
self.encodedCount = encodedCount
def set(self, byte, extra):
oldValue = self.value
@@ -121,7 +127,7 @@ class Item:
if not self.count:
byte = int(byte > self.threshold)
else:
elif self.encodedCount:
# LADX seems to store one decimal digit per nibble
byte = byte - (byte // 16 * 6)
@@ -165,6 +171,7 @@ class ItemTracker:
Item('BOOMERANG', None),
Item('TOADSTOOL', None),
Item('ROOSTER', None),
Item('RUPEE_COUNT', None, count=True, encodedCount=False),
Item('SWORD', 0xDB4E, count=True),
Item('POWER_BRACELET', 0xDB43, count=True),
Item('SHIELD', 0xDB44, count=True),
@@ -219,9 +226,9 @@ class ItemTracker:
self.itemDict = {item.id: item for item in self.items}
async def readItems(state):
extraItems = state.extraItems
missingItems = {x for x in state.items if x.address == None}
async def readItems(self):
extraItems = self.extraItems
missingItems = {x for x in self.items if x.address == None and x.id != 'RUPEE_COUNT'}
# Add keys for opened key doors
for i in range(len(dungeonKeyDoors)):
@@ -230,16 +237,16 @@ class ItemTracker:
for address, masks in dungeonKeyDoors[i].items():
for mask in masks:
value = await state.readRamByte(address) & mask
value = await self.readRamByte(address) & mask
if value > 0:
extraItems[item] += 1
# Main inventory items
for i in range(inventoryStartAddress, inventoryEndAddress):
value = await state.readRamByte(i)
value = await self.readRamByte(i)
if value in inventoryItemIds:
item = state.itemDict[inventoryItemIds[value]]
item = self.itemDict[inventoryItemIds[value]]
extra = extraItems[item.id] if item.id in extraItems else 0
item.set(1, extra)
missingItems.remove(item)
@@ -249,9 +256,21 @@ class ItemTracker:
item.set(0, extra)
# All other items
for item in [x for x in state.items if x.address]:
for item in [x for x in self.items if x.address]:
extra = extraItems[item.id] if item.id in extraItems else 0
item.set(await state.readRamByte(item.address), extra)
item.set(await self.readRamByte(item.address), extra)
# The current rupee count is BCD, but the add/remove values are not
currentRupees = self.calculateRupeeCount(await self.readRamByte(rupeesHigh), await self.readRamByte(rupeesLow))
addingRupees = (await self.readRamByte(addRupeesHigh) << 8) + await self.readRamByte(addRupeesLow)
removingRupees = (await self.readRamByte(removeRupeesHigh) << 8) + await self.readRamByte(removeRupeesLow)
self.itemDict['RUPEE_COUNT'].set(currentRupees + addingRupees - removingRupees, 0)
def calculateRupeeCount(self, high: int, low: int) -> int:
return (high - (high // 16 * 6)) * 100 + (low - (low // 16 * 6))
def setExtraItem(self, item: str, qty: int) -> None:
self.extraItems[item] = qty
async def sendItems(self, socket, diff=False):
if not self.items:
@@ -259,7 +278,6 @@ class ItemTracker:
message = {
"type":"item",
"refresh": True,
"version":"1.0",
"diff": diff,
"items": [],
}

View File

@@ -7,23 +7,12 @@ from ..roomEditor import RoomEditor
class StartItem(DroppedKey):
# We need to give something here that we can use to progress.
# FEATHER
OPTIONS = [SWORD, SHIELD, POWER_BRACELET, OCARINA, BOOMERANG, MAGIC_ROD, TAIL_KEY, SHOVEL, HOOKSHOT, PEGASUS_BOOTS, MAGIC_POWDER, BOMB]
MULTIWORLD = False
def __init__(self):
super().__init__(0x2A3)
self.give_bowwow = False
def configure(self, options):
if options.bowwow != 'normal':
# When we have bowwow mode, we pretend to be a sword for logic reasons
self.OPTIONS = [SWORD]
self.give_bowwow = True
if options.randomstartlocation and options.entranceshuffle != 'none':
self.OPTIONS.append(FLIPPERS)
def patch(self, rom, option, *, multiworld=None):
assert multiworld is None

View File

@@ -11,7 +11,7 @@ class World:
mabe_village = Location("Mabe Village")
Location().add(HeartPiece(0x2A4)).connect(mabe_village, r.bush) # well
Location().add(FishingMinigame()).connect(mabe_village, AND(r.bush, COUNT("RUPEES", 20))) # fishing game, heart piece is directly done by the minigame.
Location().add(FishingMinigame()).connect(mabe_village, AND(r.can_farm, COUNT("RUPEES", 20))) # fishing game, heart piece is directly done by the minigame.
Location().add(Seashell(0x0A3)).connect(mabe_village, r.bush) # bushes below the shop
Location().add(Seashell(0x0D2)).connect(mabe_village, PEGASUS_BOOTS) # smash into tree next to lv1
Location().add(Song(0x092)).connect(mabe_village, OCARINA) # Marins song
@@ -23,7 +23,7 @@ class World:
papahl_house.connect(mamasha_trade, TRADING_ITEM_YOSHI_DOLL)
trendy_shop = Location("Trendy Shop")
trendy_shop.connect(Location().add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)), FOUND("RUPEES", 50))
trendy_shop.connect(Location().add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)), AND(r.can_farm, FOUND("RUPEES", 50)))
outside_trendy = Location()
outside_trendy.connect(mabe_village, r.bush)
@@ -43,8 +43,8 @@ class World:
self._addEntrance("start_house", mabe_village, start_house, None)
shop = Location("Shop")
Location().add(ShopItem(0)).connect(shop, OR(COUNT("RUPEES", 500), SWORD))
Location().add(ShopItem(1)).connect(shop, OR(COUNT("RUPEES", 1480), SWORD))
Location().add(ShopItem(0)).connect(shop, OR(AND(r.can_farm, COUNT("RUPEES", 500)), SWORD))
Location().add(ShopItem(1)).connect(shop, OR(AND(r.can_farm, COUNT("RUPEES", 1480)), SWORD))
self._addEntrance("shop", mabe_village, shop, None)
dream_hut = Location("Dream Hut")
@@ -164,7 +164,7 @@ class World:
self._addEntrance("prairie_left_cave2", ukuku_prairie, prairie_left_cave2, BOMB)
self._addEntranceRequirementExit("prairie_left_cave2", None) # if exiting, you do not need bombs
mamu = Location().connect(Location().add(Song(0x2FB)), AND(OCARINA, COUNT("RUPEES", 1480)))
mamu = Location().connect(Location().add(Song(0x2FB)), AND(OCARINA, r.can_farm, COUNT("RUPEES", 1480)))
self._addEntrance("mamu", ukuku_prairie, mamu, AND(OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER), OR(HOOKSHOT, ROOSTER), POWER_BRACELET))
dungeon3_entrance = Location().connect(ukuku_prairie, OR(FEATHER, ROOSTER, FLIPPERS))
@@ -377,7 +377,7 @@ class World:
# Raft game.
raft_house = Location("Raft House")
Location().add(KeyLocation("RAFT")).connect(raft_house, AND(r.bush, COUNT("RUPEES", 100))) # add bush requirement for farming in case player has to try again
Location().add(KeyLocation("RAFT")).connect(raft_house, AND(r.can_farm, COUNT("RUPEES", 100)))
raft_return_upper = Location()
raft_return_lower = Location().connect(raft_return_upper, None, one_way=True)
outside_raft_house = Location().connect(below_right_taltal, HOOKSHOT).connect(below_right_taltal, FLIPPERS, one_way=True)

View File

@@ -253,7 +253,8 @@ def isConsumable(item) -> bool:
class RequirementsSettings:
def __init__(self, options):
self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG)
self.can_farm = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, BOOMERANG, BOMB, HOOKSHOT, BOW)
self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG, BOMB)
self.pit_bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, BOOMERANG, BOMB) # unique
self.attack = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG)
self.attack_hookshot = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # hinox, shrouded stalfos

View File

@@ -4,6 +4,7 @@ from ..roomEditor import RoomEditor
from .. import entityData
import os
import bsdiff4
import pkgutil
def imageTo2bpp(filename):
import PIL.Image
@@ -179,24 +180,9 @@ def noText(rom):
def reduceMessageLengths(rom, rnd):
# Into text from Marin. Got to go fast, so less text. (This intro text is very long)
rom.texts[0x01] = formatText(rnd.choice([
"Let's a go!",
"Remember, sword goes on A!",
"Avoid the heart piece of shame!",
"Marin? No, this is Zelda. Welcome to Hyrule",
"Why are you in my bed?",
"This is not a Mario game!",
"MuffinJets was here...",
"Remember, there are no bugs in LADX",
"#####, #####, you got to wake up!\nDinner is ready.",
"Go find the stepladder",
"Pizza power!",
"Eastmost penninsula is the secret",
"There is no cow level",
"You cannot lift rocks with your bear hands",
"Thank you, daid!",
"There, there now. Just relax. You've been asleep for almost nine hours now."
]))
lines = pkgutil.get_data(__name__, "marin.txt").decode("unicode_escape").splitlines()
lines = [l for l in lines if l.strip()]
rom.texts[0x01] = formatText(rnd.choice(lines).strip())
# Reduce length of a bunch of common texts
rom.texts[0xEA] = formatText("You've got a Guardian Acorn!")

View File

@@ -0,0 +1,465 @@
Let's a go!
Remember, sword goes on A!
Remember, sword goes on B!
It's pronounced Hydrocity Zone.
Avoid the heart piece of shame!
Marin? No, this is Zelda. Welcome to Hyrule
Why are you in my bed?
This is not a Mario game!
Wait, I thought Daid was French!
Is it spicefather or spaceotter?
kbranch finally took a break!
Baby seed ahead.
Abandon all hope ye who enter here...
Link... Open your eyes...\nWait, you're #####?
Remember, there are no bugs in LADX.
#####, #####, you got to wake up!\nDinner is ready.
Go find the stepladder.
Pizza power!
Eastmost peninsula is the secret.
There is no cow level.
You cannot lift rocks with your bear hands.
Don't worry, the doghouse was patched.
The carpet whale isn't real, it can't hurt you.
Isn't this a demake of Phantom Hourglass?
Go try the LAS rando!
Go try the Oracles rando!
Go try Archipelago!
Go try touching grass!
Please leave my house.
Trust me, this will be a 2 hour seed, max.
This is still better than doing Dampe dungeons.
They say that Marin can be found here.
Stalfos are such boneheads.
90 percent bug-free!
404 Marin.personality not found.
Idk man, works on my machine.
Hey guys, did you know that Vaporeon
Trans rights!
Support gay rights!\nAnd their lefts!
Snake? Snake?! SNAAAAKE!!!
Oh, you chose THESE settings?
As seen on TV!
May contain nuts.
Limited edition!
May contain RNG.
Reticulating splines!
Keyboard compatible!
Teetsuuuuoooo!
Kaaneeeedaaaa!
Learn about allyship!
This Marin text left intentionally blank.
'Autological' is!
Technoblade never dies!
Thank you, CrystalSaver!
Wait, LADX has a rando?
Wait, how many Pokemon are there now?
GOOD EMU
Good luck finding the feather.
Good luck finding the bracelets.
Good luck finding the boots.
Good luck finding your swords.
Good luck finding the flippers.
Good luck finding the rooster.
Good luck finding the hookshot.
Good luck finding the magic rod.
It's not a fire rod.\nIt's a magic rod, it shoots magic.
You should check the Seashell Mansion.
Mt. Tamaranch
WIND FISH IN NAME ONLY, FOR IT IS NEITHER.
Stuck? Try Magpie!
Ribbit! Ribbit! I'm Marin, on vocals!
Try this rando at ladxr.daid.eu!
He turned himself into a carpet whale!
Which came first, the whale or the egg?
Glan - Known Death and Taxes appreciator.
Pokemon number 591.
Would you?
Sprinkle the desert skulls.
Please don't curse in my Christian LADXR seed.
... ... ... \n... ...smash.
How was bedwetting practice?
The Oracles decomp project is going well!
#####, how do I download RAM?
Is this a delayed April Fool's Joke?
Play as if your footage will go in a\nSummoning Salt video.
I hope you prepared for our date later.
Isn't this the game where you date a seagull?
You look pretty good for a guy who probably drowned.
Remember, we race on Sundays.
This randomizer was made possible by players like you. \n \n Thank you!
Now with real fake doors!
Now with real fake floors!
You could be doing something productive right now.
No eggs were harmed in the making of this game.
I'm helping the goat, \ncatfishing Mr. Write is kinda the goal.
There are actually two LADX randomizers.
You're not gonna cheat... \n ...right?
Mamu's singing is so bad it wakes the dead.
Don't forget the Richard picture.
Are you sure you wanna do this? I kinda like this island.
SJ, BT, WW, OoB, HIJKLMNOP.
5 dollars in the swear jar. Now.
#####, I promise this seed will be better than the last one.
Want your name here? Contribute to LADXR!
Kappa
HEY! \n \n LANGUAGE!
I sell seashells on the seashore.
Hey! Are you even listening to me?
Your stay will total 10,000 rupees. I hope you have good insurance.
I have like the biggest crush on you. Will you get the hints now?
Daid watches Matty for ideas. \nBlame her if things go wrong.
'All of you are to blame.' -Daid
Batman Contingency Plan: Link. Step 1: Disguise yourself as a maiden to attract the young hero.
I have flooded Koholint with a deadly neurotoxin.
Ahh, General #####.
Finally, Link's Awakening!
Is the Wind Fish dreaming that he's sleeping in an egg? Or is he dreaming that he's you?
Save Koholint. By destroying it. Huh? Don't ask me, I'm just a kid!
There aren't enough women in this village to sustain a civilization.
So does this game take place before or after Oracles?
Have you tried the critically acclaimed MMORPG FINAL FANTASY XIV that has a free trial up to level 60 including the Heavensward expansion?
The thumbs-up sign had been used by the Galactic Federation for ages. Me, I was known for giving the thumbs-down during briefing. I had my reasons, though... Commander Adam Malkovich was normally cool and not one to joke around, but he would end all of his mission briefings by saying, 'Any objections, Lady?'
Hot hippos are near your location!
#####, get up! It's my turn in the bed! Tarin's smells too much...
Have you ever had a dream\nthat\nyo wa-\nyo had\nyo\nthat\nthat you could do anything?
Next time, try a salad.
seagull noises
I'm telling you, YOU HAVE UNO, it came free with your Xbox!
I'm telling you, YOU HAVE TRENDY, it came free with your Mabe!
LADXR - Now with even more Marin quotes!
You guys are spending more time adding Marin quotes than actually playing the game.
NASA faked the moon.
Doh, I missed!
Beginning the seed in... 100\n99\n98\n97\n96\n...\nJust Kidding.
Consider libre software!
Consider a GNU/Linux installation!
Now you're gonna tell me about how you need to get some instruments or maybe shells to hatch a whale out of an egg, right? All you boys are the same...
Oh hey #####! I made pancakes!
Oh hey #####! I made breakfast!
Alright Tarin, test subject number 142857 was a failure, give him the item and the memory drug and we'll try next time.
Betcha 100 rupees that Tarin gives you a sword.
Betcha 100 rupees that Tarin gives you the feather.
Betcha 100 rupees that Tarin gives you a bracelet.
Betcha 100 rupees that Tarin gives you the boots.
Betcha 100 rupees that Tarin gives you the hookshot.
Betcha 100 rupees that Tarin gives you the rod.
You'd think that Madam MeowMeow would be a cat person.
Look at you, with them dry lips.
You are now manually breathing. Hope that doesn't throw you off for this race.
Lemme get a number nine, a number nine large, a number six, with extra dip...
Tarin, the red-nosed deadbeat \nHad a mushroom addiction!
I'm using tilt controls!
SPLASH! \n \n \n ...Wait, you meant something else by 'splash text'?
CRACKLE-FWOOSH!
'Logic' is a strong word.
They say that the go-to way for fixing things is just to add another one of me.
gl hf
Have you considered multi-classing as a THIEF?
Don't call me Shirley
WHY are you buying CLOTHES at the SOUP STORE?
Believe it or not, this won't be the last time Link gets stranded on an island.
Is this the real life? Or is this just fantasy?
To the owner of the white sedan, your lights are on.
Now remade, in beautiful SD 2D!
Animal Village in my seed \nMarin and rabbits, loop de loop.
You seem totally entranced in Marin's appearance.
House hippoes are very timid creatures and are rarely seen, but they will defend their territory if provoked.
New goal! Close this seed, open the LADXR source code, and find the typo.
All your base are belong to us
Really? Another seed?
This seed brought to you by: the corners in the D2 boss room.
Hey, THIEF! Oh wait, you haven't done anything wrong... yet.
Hello World
With these hands, I give you life!
I heard we're a subcommunity of FFR now.
Try the Final Fantasy Randomizer!
How soon should we start calling you THIEF?
... Why do you keep doing this to yourself?
YOUR AD HERE
Did Matty give you this seed? Yeesh, good luck.
Yoooo I looked ahead into the spoiler log for this one...\n...\n...\n...good luck.
Lemme check the spoiler log...\nOkay, cool, only the normal amount of stupid.
Oh, you're alive. Dang. Guess I won't be needing THIS anymore.
Now you're gonna go talk to my dad. Gosh, boys are so predictable.
Shoot, I WAS going to steal your kidneys while you were asleep. Guess I'll have to find a moment when you don't expect me.
You caught me, mid-suavamente!
You'll be the bedwetting champion in no time.
Link, stop doing that, this is the fifth time this week I've had to change the sheets!
You mind napping in Not My Bed next time?
Why do they call it oven when you of in the cold food of out hot eat the food?
Marin sayings will never be generated by AI. Our community really is just that unfunny.
skibidi toilet\n...\nYes, that joke WILL age well
WHO DARES AWAKEN ME FROM MY THOUSAND-YEAR SLUMBER
The wind... it is... blowing...
Have I ever told you how much I hate sand?
explosion.gif
It is pronounced LADXR, not LADXR.
Stop pronouncing it lah-decks.
Someone once suggested to add all the nag messages all at once for me.
Accidentally playing Song 2? In front of the egg? It's more likely than you think.
Ladies and gentlemen? We got him.
Ladies and gentlemen? We got her.
Ladies and gentlemen? We got 'em.
What a wake up! I thought you'd never Marin! You were feeling a bit woozy and Zelda... What? Koholint? No, my name's relief! You must still be tossing. You are on turning Island!
...Zelda? Oh Marin is it? My apologies, thank you for saving me. So I'm on Koholint Island? Wait, where's my sword and shield?!
Koholint? More like kOWOlint.
What? The Wind Fish will grant my wish literally? I forsee nothing wrong happening with this.
Hey Marin! You woke me up from a fine nap! ... Thanks a lot! But now, I'll get my revenge! Are you ready?!
Why bother coming up with a funny quote? You're just gonna mash through it anyway.
something something whale something something dream something something adventure.
Some people won't be able to see this message!
If you're playing Archipelago and see this message, say hi to zig for me!
I think it may be time to stop playing LADXR seeds.
Rings do nothing unless worn!
Thank you Link, but our Instruments are in another Dungeon.
Are you sure you loaded the right seed?
Is this even randomized?
This seed brought to you by... Corners!
To this day I still don't know if we inconvenienced the Mad Batter or not.
Oh, hi #####
People forgot I was playable in Hyrule Warriors
Join our Discord. Or else.
Also try Minecraft!
I see you're finally awake...
OwO
This is Todd Howard, and today I'm pleased to announce... The Elder Scrolls V: Skyrim for the Nintendo Game Boy Color!
Hey dummy! Need a hint? The power bracelet is... !! Whoops! There I go, talking too much again.
Thank you for visiting Toronbo Shores featuring Mabe Village. Don't forget your complimentary gift on the way out.
They say that sand can be found in Yarna Desert.
I got to see a previously unreleased cut yesterday. It only cost me 200 rupees. What a deal!
Just let him sleep
LADXR is going to be renamed X now.
Did you hear this chart-topping song yet? It's called Manbo's Mambo, it's so catchy! OH!
YOU DARE BRING LIGHT INTO MY LAIR?!?! You must DIE!
But enough talk! Have at you!
Please input your age for optimal meme-text delivery.
So the bear is just calling the walrus fat beecause he's projecting, right?
Please help, #####! The Nightmare has shuffled all the items around!
One does not simply Wake the Wind Fish.
Nothing unusual here, just a completely normal LADX game, Mister Nintendo.
Remember:\n1) Play Vanilla\n2) Play Solo Rando\n3) Play Multi
Is :) a good item?
What version do we have anyway? 0.6.9?
So, what &newgames are coming in the next AP version?
Is !remaining fixed yet?
Remember the APocalypse. Never forget the rooms we lost that day.
Have you heard of Berserker's Multiworld?
MILF. Man I love Fangames.
How big can the Big Async be anyway? A hundred worlds?
Have you heard of the After Dark server?
Try Adventure!
Try Aquaria!
Try Blasphemous!
Try Bomb Rush Cyberfunk!
Try Bumper Stickers!
Try Castlevania 64!
Try Celeste 64!
Try ChecksFinder!
Try Clique!
Try Dark Souls III!
Try DLCQuest!
Try Donkey Kong Country 3!
Try DOOM 1993!
Try DOOM II!
Try Factorio!
Try Final Fantasy!
Try Final Fantasy Mystic Quest!
Try A Hat in Time!
Try Heretic!
Try Hollow Knight!
Try Hylics 2!
Try Kingdom Hearts 2!
Try Kirby's Dream Land 3!
Try Landstalker - The Treasures of King Nole!
Try The Legend of Zelda!
Try Lingo!
Try A Link to the Past!
Try Links Awakening DX!
Try Lufia II Ancient Cave!
Try Mario & Luigi Superstar Saga!
Try MegaMan Battle Network 3!
Try Meritous!
Try The Messenger!
Try Minecraft!
Try Muse Dash!
Try Noita!
Try Ocarina of Time!
Try Overcooked! 2!
Try Pokemon Emerald!
Try Pokemon Red and Blue!
Try Raft!
Try Risk of Rain 2!
Try Rogue Legacy!
Try Secret of Evermore!
Try Shivers!
Try A Short Hike!
Try Slay the Spire!
Try SMZ3!
Try Sonic Adventure 2 Battle!
Try Starcraft 2!
Try Stardew Valley!
Try Subnautica!
Try Sudoku!
Try Super Mario 64!
Try Super Mario World!
Try Super Metroid!
Try Terraria!
Try Timespinner!
Try TUNIC!
Try Undertale!
Try VVVVVV!
Try Wargroove!
Try The Witness!
Try Yoshi's Island!
Try Yu-Gi-Oh! 2006!
Try Zillion!
Try Zork Grand Inquisitor!
Try Old School Runescape!
Try Kingdom Hearts!
Try Mega Man 2!
Try Yacht Dice!
VVVVVVVVVVVVVV this should be enough V right?
If you see this message, please open a #bug-report about it\n\n\nDon't actually though.
This YAML is going in the bucket, isn't it?
Oh, this is a terrible seed for a Sync
Oh, this is a terrible seed for an Async
What does BK stand for anyway?
Check out the #future-game-design forum
This is actually a Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward and Stormblood expansions up to level 70 with no restrictions on playtime!
Is it April yet? Can I play ArchipIDLE again?
https://archipelago.gg/datapackage
Hello, Link! (Disregard message if your player sprite is not Link.)
Go back to sleep, Outer Wilds isn't supported yet.
:)\nWelcome back!
Don't forget about Aginah!
Remind your Undertale player not to warp before Mad Dummy.
You need\n9 instruments\nor maybe not. I wouldn't know.
Try !\n\nIt makes the game easier.
Have you tried The Witness? If you're a fan of games about waking up on an unfamiliar island, give it a shot!
Have you tried turning it off and on again?
Its about time. Now go and check
This dream is a lie. Or is it a cake?
Don't live your dream. Dream your live.
Only 5 more minutes. zzzZ
Tell me, for whom do you fight?\nHmmph. How very glib. And do you believe in Koholint?
I wonder when Undertale will be merged?\nOh wait it already has.
Hit me up if you get stuck -\nwe could go to Burger King together.
Post this message to delay Silksong.
Sorry #####, but your princess is in another castle!
You've been met with a terrible fate, haven't you?
Hey!\nListen!\nHey! Hey!\nListen!
I bet there's a progression item at the 980 Rupee shop check.
Lamp oil? Rope? Bombs? You want it? It's yours, my friend. As long as you have enough rubies.
One day I happened to be occupied with the subject of generation of waves by wind.
(nuzzles you) uwu
why do they call it links awakening when links awake and IN links asleep OUT the wind fish
For many years I have been looking, searching for, but never finding, the builder of this house...
What the heck is a Quatro?
Have you tried The Binding of Isaac yet?
Have you played Pong? \n I hear it's still popular nowadays.
Five Nights at Freddy's... \n That's where I wanna be
Setting Coinsanity to -1...
Your Feather can be found at Mask-Shard_Grey_Mourner
Your Sword can be found in Ganon's Tower
Your Rooster can be found in HylemXylem
Your Bracelet can be found at Giant Floor Puzzle
Your Flippers can be found in Valley of Bowser
Your Magic Rod can be found in Victory Road
Your Hookshot can be found in Bowser in the Sky
Have they added Among Us to AP yet?
Every copy of LADX is personalized, David.
Looks like you're going on A Short Hike. Bring back feathers please?
Functioning Brain is at...\nWait. This isn't Witness. Wrong game, sorry.
Don't forget to check your Clique!\nIf, y'know, you have one. No pressure...
:3
Sorry ######, but your progression item is in another world.
&newgames\n&oldgames
Do arrows come with turners? I'm stuck in my Bumper Stickers world.
This seed has dexsanity enabled. Don't get stuck in Dewford!
Please purchase the Dialogue Pack for DLC Quest: Link's Adventure to read the rest of this text.
No hints here. Maybe ask BK Sudoku for some?
KILNS (Yellow Middle, 5) \n REVELATION (White Low, 9)
Push the button! When someone lets you...
You won't believe the WEIRD thing Tarin found at the beach! Go on, ask him about it!
When's door randomizer getting added to AP?
Can you get my Morph Ball?
Shoutouts to Simpleflips
Remember, Sword goes on C!\n...you have a C button, right?
Ask Berserker for your Progressive Power Bracelets!
I will be taking your Burger King order now to save you some time when you inevitably need it.
Welcome to KOHOLINT ISLAND.\nNo, we do not have a BURGER KING.
Welcome to Burger King, may I take your order?
Rise and shine, #####. Rise and shine.
Well, this is\nLITTLEROOT TOWN.\nHow do you like it?
My boy, this peace is what all true warriors strive for!
#####, you can do it!\nSave the Princess...\nZelda is your... ... ...
Dear Mario:\nPlease come to the castle, I've baked a cake for you. Yours truly--\nPrincess Toadstool\nPeach
Grass-sanity mode activated. Have fun!
Don't forget to bring rupees to the signpost maze this time.
UP UP DOWN DOWN LEFT RIGHT LEFT RIGHT B A START
Try LADX!\nWait a minute...
ERROR! Unable to verify player. Please drink a verification can.
We have been trying to reach you about your raft's extended warranty
Are you ready for the easiest BK of your life?
Hello, welcome to the world of Pokemon!\nMy name is Marin, and I'm--
Alright, this is very important, I need you to listen to what I'm about to tell you--\nHey, wait, where are you going?!
Cheques?\nSorry we don't accept cheques here
Hi! \nMarin. \nWho...? \nHow...? \nWait... \nWhy??? \nSorry... \n...\nThanks. \nBye!
AHHH WHY IS THERE SO MUCH GRASS? \nHOLY SH*T GRASS SNAKE AHHHH
Could you buy some strawberries on your way home? \nHuh it's out of logic??? What??
I heard you sleeptalking about skeletons and genocide... Your past must have been full of misery (mire)
It's time to let go... \nIt wasn't your fault... \nYou couldn't have known your first check was going to be hardmode...
They say that your progression is in another castle...
A minute of silence for the failed generations due to the Fitness Gram Pacer test.
Save an Ice Trap for me, please?
maren
ERROR DETECTED IN YAML\nOHKO MODE FORCED ON
she awaken my link (extremely loud incorrect buzzer)
Is deathlink on? If so, be careful!
Sorry, but you're about to be BK'd.
Did you set up cheesetracker yet?
I've got a hint I need you to get...
You aren't planning to destroy this island and kill everyone on it are you?
Have you ever had a dream, that, that you um you had you'd you would you could you'd do you wi you wants you you could do so you you'd do you could you you want you want him to do you so much you could do anything?
R R R U L L U L U R U R D R D R U U
I'm not sure how, but I am pretty sure this is Phar's fault.
Oh, look at that. Link's Awakened.\nYou did it, you beat the game.
Excellent armaments, #####. Please return - \nCOVERED IN BLOOD -\n...safe and sound.
Pray return to the Link's Awakening Sands.
This Marin dialogue was inspired by The Witness's audiologs.
You're awake!\n....\nYou were warned.\nI'm now going to say every word beginning with Z!\nZA\nZABAGLIONE\nZABAGLIONES\nZABAIONE\nZABAIONES\nZABAJONE\nZABAJONES\nZABETA\nZABETAS\nZABRA\nZABRAS\nZABTIEH\nZABTIEHS\nZACATON\nZACATONS\nZACK\nZACKS\nZADDICK\nZADDIK\nZADDIKIM\nZADDIKS\nZAFFAR\nzAFFARS\nZAFFER\nZAFFERS\nZAFFIR\n....\n....\n....\nI'll let you off easy.\nThis time.
Leave me alone, I'm Marinating.
praise be to the tungsten cube
If you play multiple seeds in a row, you can pretend that each run is the dream you awaken from in the next.
If this is a competitive race,\n\nyour time has already started.
If anything goes wrong, remember.\n Blame Phar.
Better hope your Hookshot didn't land on the Sick Kid.
One time, I accidentally said Konoliht instead of Koholint...
Sometimes, you must become best girl yourself...
You just woke up! My name's #####!\nYou must be Marin, right?
I just had the strangest dream, I was a seagull!\nI sung many songs for everybody to hear!\nHave you ever had a strange dream before?
If you think about it, Koholint sounds suspiciously similar to Coherent...
All I kin remember is biting into a juicy toadstool. Then I had the strangest dream... I was a Marin! Yeah, it sounds strange, but it sure was fun!
Prepare for a 100% run!
Prediction: 1 hour
Prediction: 4 hours
Prediction: 6 hours
Prediction: 12 hours
Prediction: Impossible seed
Oak's parcel has arrived.
Don't forget to like and subscribe!
Don't BK, eat healthy!
No omega symbols broke this seed gen? Good!
#####...\nYou're lucky.\nLooks like my summer vacation is...\nover.
Are you ready to send nukes to someone's Factorio game?
You're late... Is this a Cmario game?
At least you don't have to fight Ganon... What?
PRAISE THE SUN!
I'd recommend more sleep before heading out there.
You Must Construct Additional Pylons
#####, you lazy bum. I knew that I'd find you snoozing down here.
This is it, #####.\nJust breathe.\nWhy are you so nervous?
Hey, you. You're finally awake.\nYou were trying to cross the border, huh?
Hey, you. You're finally awake.\nYou were trying to leave the island, huh?\nSwam straight into that whirlpool, same as us, and that thief over there.
Is my Triforce locked behind your Wind Fish?

View File

@@ -110,15 +110,6 @@ class LinksAwakeningLocation(Location):
add_item_rule(self, filter_item)
def has_free_weapon(state: CollectionState, player: int) -> bool:
return state.has("Progressive Sword", player) or state.has("Magic Rod", player) or state.has("Boomerang", player) or state.has("Hookshot", player)
# If the player has access to farm enough rupees to afford a game, we assume that they can keep beating the game
def can_farm_rupees(state: CollectionState, player: int) -> bool:
return has_free_weapon(state, player) and (state.has("Can Play Trendy Game", player=player) or state.has("RAFT", player=player))
class LinksAwakeningRegion(Region):
dungeon_index = None
ladxr_region = None
@@ -154,9 +145,7 @@ class GameStateAdapater:
def get(self, item, default):
# Don't allow any money usage if you can't get back wasted rupees
if item == "RUPEES":
if can_farm_rupees(self.state, self.player):
return self.state.prog_items[self.player]["RUPEES"]
return 0
return self.state.prog_items[self.player]["RUPEES"]
elif item.endswith("_USED"):
return 0
else:

View File

@@ -527,6 +527,20 @@ class InGameHints(DefaultOnToggle):
display_name = "In-game Hints"
class TarinsGift(Choice):
"""
[Local Progression] Forces Tarin's gift to be an item that immediately opens up local checks.
Has little effect in single player games, and isn't always necessary with randomized entrances.
[Bush Breaker] Forces Tarin's gift to be an item that can destroy bushes.
[Any Item] Tarin's gift can be any item for any world
"""
display_name = "Tarin's Gift"
option_local_progression = 0
option_bush_breaker = 1
option_any_item = 2
default = option_local_progression
class StabilizeItemPool(DefaultOffToggle):
"""
By default, rupees in the item pool may be randomly swapped with bombs, arrows, powders, or capacity upgrades. This option disables that swapping, which is useful for plando.
@@ -565,6 +579,7 @@ ladx_option_groups = [
OptionGroup("Miscellaneous", [
TradeQuest,
Rooster,
TarinsGift,
Overworld,
TrendyGame,
InGameHints,
@@ -638,6 +653,7 @@ class LinksAwakeningOptions(PerGameCommonOptions):
text_mode: TextMode
no_flash: NoFlash
in_game_hints: InGameHints
tarins_gift: TarinsGift
overworld: Overworld
stabilize_item_pool: StabilizeItemPool

View File

@@ -1,3 +1,6 @@
import typing
from worlds.ladx.GpsTracker import GpsTracker
from .LADXR.checkMetadata import checkMetadataTable
import json
import logging
@@ -10,13 +13,14 @@ logger = logging.getLogger("Tracker")
# kbranch you're a hero
# https://github.com/kbranch/Magpie/blob/master/autotracking/checks.py
class Check:
def __init__(self, id, address, mask, alternateAddress=None):
def __init__(self, id, address, mask, alternateAddress=None, linkedItem=None):
self.id = id
self.address = address
self.alternateAddress = alternateAddress
self.mask = mask
self.value = None
self.diff = 0
self.linkedItem = linkedItem
def set(self, bytes):
oldValue = self.value
@@ -86,6 +90,27 @@ class LocationTracker:
blacklist = {'None', '0x2A1-2'}
def seashellCondition(slot_data):
return 'goal' not in slot_data or slot_data['goal'] != 'seashells'
linkedCheckItems = {
'0x2E9': {'item': 'SEASHELL', 'qty': 20, 'condition': seashellCondition},
'0x2A2': {'item': 'TOADSTOOL', 'qty': 1},
'0x2A6-Trade': {'item': 'TRADING_ITEM_YOSHI_DOLL', 'qty': 1},
'0x2B2-Trade': {'item': 'TRADING_ITEM_RIBBON', 'qty': 1},
'0x2FE-Trade': {'item': 'TRADING_ITEM_DOG_FOOD', 'qty': 1},
'0x07B-Trade': {'item': 'TRADING_ITEM_BANANAS', 'qty': 1},
'0x087-Trade': {'item': 'TRADING_ITEM_STICK', 'qty': 1},
'0x2D7-Trade': {'item': 'TRADING_ITEM_HONEYCOMB', 'qty': 1},
'0x019-Trade': {'item': 'TRADING_ITEM_PINEAPPLE', 'qty': 1},
'0x2D9-Trade': {'item': 'TRADING_ITEM_HIBISCUS', 'qty': 1},
'0x2A8-Trade': {'item': 'TRADING_ITEM_LETTER', 'qty': 1},
'0x0CD-Trade': {'item': 'TRADING_ITEM_BROOM', 'qty': 1},
'0x2F5-Trade': {'item': 'TRADING_ITEM_FISHING_HOOK', 'qty': 1},
'0x0C9-Trade': {'item': 'TRADING_ITEM_NECKLACE', 'qty': 1},
'0x297-Trade': {'item': 'TRADING_ITEM_SCALE', 'qty': 1},
}
# in no dungeons boss shuffle, the d3 boss in d7 set 0x20 in fascade's room (0x1BC)
# after beating evil eagile in D6, 0x1BC is now 0xAC (other things may have happened in between)
# entered d3, slime eye flag had already been set (0x15A 0x20). after killing angler fish, bits 0x0C were set
@@ -98,6 +123,8 @@ class LocationTracker:
address = addressOverrides[check_id] if check_id in addressOverrides else 0xD800 + int(
room, 16)
linkedItem = linkedCheckItems[check_id] if check_id in linkedCheckItems else None
if 'Trade' in check_id or 'Owl' in check_id:
mask = 0x20
@@ -111,13 +138,19 @@ class LocationTracker:
highest_check = max(
highest_check, alternateAddresses[check_id])
check = Check(check_id, address, mask,
alternateAddresses[check_id] if check_id in alternateAddresses else None)
check = Check(
check_id,
address,
mask,
(alternateAddresses[check_id] if check_id in alternateAddresses else None),
linkedItem,
)
if check_id == '0x2A3':
self.start_check = check
self.all_checks.append(check)
self.remaining_checks = [check for check in self.all_checks]
self.gameboy.set_cache_limits(
self.gameboy.set_checks_range(
lowest_check, highest_check - lowest_check + 1)
def has_start_item(self):
@@ -147,10 +180,17 @@ class MagpieBridge:
server = None
checks = None
item_tracker = None
gps_tracker: GpsTracker = None
ws = None
features = []
slot_data = {}
def use_entrance_tracker(self):
return "entrances" in self.features \
and self.slot_data \
and "entrance_mapping" in self.slot_data \
and any([k != v for k, v in self.slot_data["entrance_mapping"].items()])
async def handler(self, websocket):
self.ws = websocket
while True:
@@ -159,14 +199,18 @@ class MagpieBridge:
logger.info(
f"Connected, supported features: {message['features']}")
self.features = message["features"]
await self.send_handshAck()
if message["type"] in ("handshake", "sendFull"):
if message["type"] == "sendFull":
if "items" in self.features:
await self.send_all_inventory()
if "checks" in self.features:
await self.send_all_checks()
if "slot_data" in self.features:
if "slot_data" in self.features and self.slot_data:
await self.send_slot_data(self.slot_data)
if self.use_entrance_tracker():
await self.send_gps(diff=False)
# Translate renamed IDs back to LADXR IDs
@staticmethod
@@ -176,6 +220,18 @@ class MagpieBridge:
if the_id == "0x2A7":
return "0x2A1-1"
return the_id
async def send_handshAck(self):
if not self.ws:
return
message = {
"type": "handshAck",
"version": "1.32",
"name": "archipelago-ladx-client",
}
await self.ws.send(json.dumps(message))
async def send_all_checks(self):
while self.checks == None:
@@ -185,7 +241,6 @@ class MagpieBridge:
message = {
"type": "check",
"refresh": True,
"version": "1.0",
"diff": False,
"checks": [{"id": self.fixup_id(check.id), "checked": check.value} for check in self.checks]
}
@@ -200,7 +255,6 @@ class MagpieBridge:
message = {
"type": "check",
"refresh": True,
"version": "1.0",
"diff": True,
"checks": [{"id": self.fixup_id(check), "checked": True} for check in checks]
}
@@ -222,10 +276,17 @@ class MagpieBridge:
return
await self.item_tracker.sendItems(self.ws, diff=True)
async def send_gps(self, gps):
async def send_gps(self, diff: bool=True) -> typing.Dict[str, str]:
if not self.ws:
return
await gps.send_location(self.ws)
await self.gps_tracker.send_location(self.ws)
if self.use_entrance_tracker():
if self.slot_data and self.gps_tracker.needs_slot_data:
self.gps_tracker.load_slot_data(self.slot_data)
return await self.gps_tracker.send_entrances(self.ws, diff)
async def send_slot_data(self, slot_data):
if not self.ws:

View File

@@ -0,0 +1,291 @@
class EntranceCoord:
name: str
room: int
x: int
y: int
def __init__(self, name: str, room: int, x: int, y: int):
self.name = name
self.room = room
self.x = x
self.y = y
def __repr__(self):
return EntranceCoord.coordString(self.room, self.x, self.y)
def coordString(room: int, x: int, y: int):
return f"{room:#05x}, {x}, {y}"
storage_key = "found_entrances"
room = 0xFFF6
map_id = 0xFFF7
indoor_flag = 0xDBA5
spawn_map = 0xDB60
spawn_room = 0xDB61
spawn_x = 0xDB62
spawn_y = 0xDB63
entrance_room_offset = 0xD800
transition_state = 0xC124
transition_target_x = 0xC12C
transition_target_y = 0xC12D
transition_scroll_x = 0xFF96
transition_scroll_y = 0xFF97
link_motion_state = 0xC11C
transition_sequence = 0xC16B
screen_coord = 0xFFFA
entrance_address_overrides = {
0x312: 0xDDF2,
}
map_map = {
0x00: 0x01,
0x01: 0x01,
0x02: 0x01,
0x03: 0x01,
0x04: 0x01,
0x05: 0x01,
0x06: 0x02,
0x07: 0x02,
0x08: 0x02,
0x09: 0x02,
0x0A: 0x02,
0x0B: 0x02,
0x0C: 0x02,
0x0D: 0x02,
0x0E: 0x02,
0x0F: 0x02,
0x10: 0x02,
0x11: 0x02,
0x12: 0x02,
0x13: 0x02,
0x14: 0x02,
0x15: 0x02,
0x16: 0x02,
0x17: 0x02,
0x18: 0x02,
0x19: 0x02,
0x1D: 0x01,
0x1E: 0x01,
0x1F: 0x01,
0xFF: 0x03,
}
sidescroller_rooms = {
0x2e9: "seashell_mansion:inside",
0x08a: "seashell_mansion",
0x2fd: "mambo:inside",
0x02a: "mambo",
0x1eb: "castle_secret_exit:inside",
0x049: "castle_secret_exit",
0x1ec: "castle_secret_entrance:inside",
0x04a: "castle_secret_entrance",
0x117: "d1:inside", # not a sidescroller, but acts weird
}
entrance_coords = [
EntranceCoord("writes_house:inside", 0x2a8, 80, 124),
EntranceCoord("rooster_grave", 0x92, 88, 82),
EntranceCoord("start_house:inside", 0x2a3, 80, 124),
EntranceCoord("dream_hut", 0x83, 40, 66),
EntranceCoord("papahl_house_right:inside", 0x2a6, 80, 124),
EntranceCoord("papahl_house_right", 0x82, 120, 82),
EntranceCoord("papahl_house_left:inside", 0x2a5, 80, 124),
EntranceCoord("papahl_house_left", 0x82, 88, 82),
EntranceCoord("d2:inside", 0x136, 80, 124),
EntranceCoord("shop", 0x93, 72, 98),
EntranceCoord("armos_maze_cave:inside", 0x2fc, 104, 96),
EntranceCoord("start_house", 0xa2, 88, 82),
EntranceCoord("animal_house3:inside", 0x2d9, 80, 124),
EntranceCoord("trendy_shop", 0xb3, 88, 82),
EntranceCoord("mabe_phone:inside", 0x2cb, 80, 124),
EntranceCoord("mabe_phone", 0xb2, 88, 82),
EntranceCoord("ulrira:inside", 0x2a9, 80, 124),
EntranceCoord("ulrira", 0xb1, 72, 98),
EntranceCoord("moblin_cave:inside", 0x2f0, 80, 124),
EntranceCoord("kennel", 0xa1, 88, 66),
EntranceCoord("madambowwow:inside", 0x2a7, 80, 124),
EntranceCoord("madambowwow", 0xa1, 56, 66),
EntranceCoord("library:inside", 0x1fa, 80, 124),
EntranceCoord("library", 0xb0, 56, 50),
EntranceCoord("d5:inside", 0x1a1, 80, 124),
EntranceCoord("d1", 0xd3, 104, 34),
EntranceCoord("d1:inside", 0x117, 80, 124),
EntranceCoord("d3:inside", 0x152, 80, 124),
EntranceCoord("d3", 0xb5, 104, 32),
EntranceCoord("banana_seller", 0xe3, 72, 48),
EntranceCoord("armos_temple:inside", 0x28f, 80, 124),
EntranceCoord("boomerang_cave", 0xf4, 24, 32),
EntranceCoord("forest_madbatter:inside", 0x1e1, 136, 80),
EntranceCoord("ghost_house", 0xf6, 88, 66),
EntranceCoord("prairie_low_phone:inside", 0x29d, 80, 124),
EntranceCoord("prairie_low_phone", 0xe8, 56, 98),
EntranceCoord("prairie_madbatter_connector_entrance:inside", 0x1f6, 136, 112),
EntranceCoord("prairie_madbatter_connector_entrance", 0xf9, 120, 80),
EntranceCoord("prairie_madbatter_connector_exit", 0xe7, 104, 32),
EntranceCoord("prairie_madbatter_connector_exit:inside", 0x1e5, 40, 48),
EntranceCoord("ghost_house:inside", 0x1e3, 80, 124),
EntranceCoord("prairie_madbatter", 0xe6, 72, 64),
EntranceCoord("d4:inside", 0x17a, 80, 124),
EntranceCoord("d5", 0xd9, 88, 64),
EntranceCoord("prairie_right_cave_bottom:inside", 0x293, 48, 124),
EntranceCoord("prairie_right_cave_bottom", 0xc8, 40, 80),
EntranceCoord("prairie_right_cave_high", 0xb8, 88, 48),
EntranceCoord("prairie_right_cave_high:inside", 0x295, 112, 124),
EntranceCoord("prairie_right_cave_top", 0xb8, 120, 96),
EntranceCoord("prairie_right_cave_top:inside", 0x292, 48, 124),
EntranceCoord("prairie_to_animal_connector:inside", 0x2d0, 40, 64),
EntranceCoord("prairie_to_animal_connector", 0xaa, 136, 64),
EntranceCoord("animal_to_prairie_connector", 0xab, 120, 80),
EntranceCoord("animal_to_prairie_connector:inside", 0x2d1, 120, 64),
EntranceCoord("animal_phone:inside", 0x2e3, 80, 124),
EntranceCoord("animal_phone", 0xdb, 120, 82),
EntranceCoord("animal_house1:inside", 0x2db, 80, 124),
EntranceCoord("animal_house1", 0xcc, 40, 80),
EntranceCoord("animal_house2:inside", 0x2dd, 80, 124),
EntranceCoord("animal_house2", 0xcc, 120, 80),
EntranceCoord("hookshot_cave:inside", 0x2b3, 80, 124),
EntranceCoord("animal_house3", 0xcd, 40, 80),
EntranceCoord("animal_house4:inside", 0x2da, 80, 124),
EntranceCoord("animal_house4", 0xcd, 88, 80),
EntranceCoord("banana_seller:inside", 0x2fe, 80, 124),
EntranceCoord("animal_house5", 0xdd, 88, 66),
EntranceCoord("animal_cave:inside", 0x2f7, 96, 124),
EntranceCoord("animal_cave", 0xcd, 136, 32),
EntranceCoord("d6", 0x8c, 56, 64),
EntranceCoord("madbatter_taltal:inside", 0x1e2, 136, 80),
EntranceCoord("desert_cave", 0xcf, 88, 16),
EntranceCoord("dream_hut:inside", 0x2aa, 80, 124),
EntranceCoord("armos_maze_cave", 0xae, 72, 112),
EntranceCoord("shop:inside", 0x2a1, 80, 124),
EntranceCoord("armos_temple", 0xac, 88, 64),
EntranceCoord("d6_connector_exit:inside", 0x1f0, 56, 16),
EntranceCoord("d6_connector_exit", 0x9c, 88, 16),
EntranceCoord("desert_cave:inside", 0x1f9, 120, 96),
EntranceCoord("d6_connector_entrance:inside", 0x1f1, 136, 96),
EntranceCoord("d6_connector_entrance", 0x9d, 56, 48),
EntranceCoord("armos_fairy:inside", 0x1ac, 80, 124),
EntranceCoord("armos_fairy", 0x8d, 56, 32),
EntranceCoord("raft_return_enter:inside", 0x1f7, 136, 96),
EntranceCoord("raft_return_enter", 0x8f, 8, 32),
EntranceCoord("raft_return_exit", 0x2f, 24, 112),
EntranceCoord("raft_return_exit:inside", 0x1e7, 72, 16),
EntranceCoord("raft_house:inside", 0x2b0, 80, 124),
EntranceCoord("raft_house", 0x3f, 40, 34),
EntranceCoord("heartpiece_swim_cave:inside", 0x1f2, 72, 124),
EntranceCoord("heartpiece_swim_cave", 0x2e, 88, 32),
EntranceCoord("rooster_grave:inside", 0x1f4, 88, 112),
EntranceCoord("d4", 0x2b, 72, 34),
EntranceCoord("castle_phone:inside", 0x2cc, 80, 124),
EntranceCoord("castle_phone", 0x4b, 72, 34),
EntranceCoord("castle_main_entrance:inside", 0x2d3, 80, 124),
EntranceCoord("castle_main_entrance", 0x69, 88, 64),
EntranceCoord("castle_upper_left", 0x59, 24, 48),
EntranceCoord("castle_upper_left:inside", 0x2d5, 80, 124),
EntranceCoord("witch:inside", 0x2a2, 80, 124),
EntranceCoord("castle_upper_right", 0x59, 88, 64),
EntranceCoord("prairie_left_cave2:inside", 0x2f4, 64, 124),
EntranceCoord("castle_jump_cave", 0x78, 40, 112),
EntranceCoord("prairie_left_cave1:inside", 0x2cd, 80, 124),
EntranceCoord("seashell_mansion", 0x8a, 88, 64),
EntranceCoord("prairie_right_phone:inside", 0x29c, 80, 124),
EntranceCoord("prairie_right_phone", 0x88, 88, 82),
EntranceCoord("prairie_left_fairy:inside", 0x1f3, 80, 124),
EntranceCoord("prairie_left_fairy", 0x87, 40, 16),
EntranceCoord("bird_cave:inside", 0x27e, 96, 124),
EntranceCoord("prairie_left_cave2", 0x86, 24, 64),
EntranceCoord("prairie_left_cave1", 0x84, 152, 98),
EntranceCoord("prairie_left_phone:inside", 0x2b4, 80, 124),
EntranceCoord("prairie_left_phone", 0xa4, 56, 66),
EntranceCoord("mamu:inside", 0x2fb, 136, 112),
EntranceCoord("mamu", 0xd4, 136, 48),
EntranceCoord("richard_house:inside", 0x2c7, 80, 124),
EntranceCoord("richard_house", 0xd6, 72, 80),
EntranceCoord("richard_maze:inside", 0x2c9, 128, 124),
EntranceCoord("richard_maze", 0xc6, 56, 80),
EntranceCoord("graveyard_cave_left:inside", 0x2de, 56, 64),
EntranceCoord("graveyard_cave_left", 0x75, 56, 64),
EntranceCoord("graveyard_cave_right:inside", 0x2df, 56, 48),
EntranceCoord("graveyard_cave_right", 0x76, 104, 80),
EntranceCoord("trendy_shop:inside", 0x2a0, 80, 124),
EntranceCoord("d0", 0x77, 120, 46),
EntranceCoord("boomerang_cave:inside", 0x1f5, 72, 124),
EntranceCoord("witch", 0x65, 72, 50),
EntranceCoord("toadstool_entrance:inside", 0x2bd, 80, 124),
EntranceCoord("toadstool_entrance", 0x62, 120, 66),
EntranceCoord("toadstool_exit", 0x50, 136, 50),
EntranceCoord("toadstool_exit:inside", 0x2ab, 80, 124),
EntranceCoord("prairie_madbatter:inside", 0x1e0, 136, 112),
EntranceCoord("hookshot_cave", 0x42, 56, 66),
EntranceCoord("castle_upper_right:inside", 0x2d6, 80, 124),
EntranceCoord("forest_madbatter", 0x52, 104, 48),
EntranceCoord("writes_phone:inside", 0x29b, 80, 124),
EntranceCoord("writes_phone", 0x31, 104, 82),
EntranceCoord("d0:inside", 0x312, 80, 92),
EntranceCoord("writes_house", 0x30, 120, 50),
EntranceCoord("writes_cave_left:inside", 0x2ae, 80, 124),
EntranceCoord("writes_cave_left", 0x20, 136, 50),
EntranceCoord("writes_cave_right:inside", 0x2af, 80, 124),
EntranceCoord("writes_cave_right", 0x21, 24, 50),
EntranceCoord("d6:inside", 0x1d4, 80, 124),
EntranceCoord("d2", 0x24, 56, 34),
EntranceCoord("animal_house5:inside", 0x2d7, 80, 124),
EntranceCoord("moblin_cave", 0x35, 104, 80),
EntranceCoord("crazy_tracy:inside", 0x2ad, 80, 124),
EntranceCoord("crazy_tracy", 0x45, 136, 66),
EntranceCoord("photo_house:inside", 0x2b5, 80, 124),
EntranceCoord("photo_house", 0x37, 72, 66),
EntranceCoord("obstacle_cave_entrance:inside", 0x2b6, 80, 124),
EntranceCoord("obstacle_cave_entrance", 0x17, 56, 50),
EntranceCoord("left_to_right_taltalentrance:inside", 0x2ee, 120, 48),
EntranceCoord("left_to_right_taltalentrance", 0x7, 56, 80),
EntranceCoord("obstacle_cave_outside_chest:inside", 0x2bb, 80, 124),
EntranceCoord("obstacle_cave_outside_chest", 0x18, 104, 18),
EntranceCoord("obstacle_cave_exit:inside", 0x2bc, 48, 124),
EntranceCoord("obstacle_cave_exit", 0x18, 136, 18),
EntranceCoord("papahl_entrance:inside", 0x289, 64, 124),
EntranceCoord("papahl_entrance", 0x19, 136, 64),
EntranceCoord("papahl_exit:inside", 0x28b, 80, 124),
EntranceCoord("papahl_exit", 0xa, 24, 112),
EntranceCoord("rooster_house:inside", 0x29f, 80, 124),
EntranceCoord("rooster_house", 0xa, 72, 34),
EntranceCoord("d7:inside", 0x20e, 80, 124),
EntranceCoord("bird_cave", 0xa, 120, 112),
EntranceCoord("multichest_top:inside", 0x2f2, 80, 124),
EntranceCoord("multichest_top", 0xd, 24, 112),
EntranceCoord("multichest_left:inside", 0x2f9, 32, 124),
EntranceCoord("multichest_left", 0x1d, 24, 48),
EntranceCoord("multichest_right:inside", 0x2fa, 112, 124),
EntranceCoord("multichest_right", 0x1d, 120, 80),
EntranceCoord("right_taltal_connector1:inside", 0x280, 32, 124),
EntranceCoord("right_taltal_connector1", 0x1e, 56, 16),
EntranceCoord("right_taltal_connector3:inside", 0x283, 128, 124),
EntranceCoord("right_taltal_connector3", 0x1e, 120, 16),
EntranceCoord("right_taltal_connector2:inside", 0x282, 112, 124),
EntranceCoord("right_taltal_connector2", 0x1f, 40, 16),
EntranceCoord("right_fairy:inside", 0x1fb, 80, 124),
EntranceCoord("right_fairy", 0x1f, 56, 80),
EntranceCoord("right_taltal_connector4:inside", 0x287, 96, 124),
EntranceCoord("right_taltal_connector4", 0x1f, 88, 64),
EntranceCoord("right_taltal_connector5:inside", 0x28c, 96, 124),
EntranceCoord("right_taltal_connector5", 0x1f, 120, 16),
EntranceCoord("right_taltal_connector6:inside", 0x28e, 112, 124),
EntranceCoord("right_taltal_connector6", 0xf, 72, 80),
EntranceCoord("d7", 0x0e, 88, 48),
EntranceCoord("left_taltal_entrance:inside", 0x2ea, 80, 124),
EntranceCoord("left_taltal_entrance", 0x15, 136, 64),
EntranceCoord("castle_jump_cave:inside", 0x1fd, 88, 80),
EntranceCoord("madbatter_taltal", 0x4, 120, 112),
EntranceCoord("fire_cave_exit:inside", 0x1ee, 24, 64),
EntranceCoord("fire_cave_exit", 0x3, 72, 80),
EntranceCoord("fire_cave_entrance:inside", 0x1fe, 112, 124),
EntranceCoord("fire_cave_entrance", 0x13, 88, 16),
EntranceCoord("phone_d8:inside", 0x299, 80, 124),
EntranceCoord("phone_d8", 0x11, 104, 50),
EntranceCoord("kennel:inside", 0x2b2, 80, 124),
EntranceCoord("d8", 0x10, 88, 16),
EntranceCoord("d8:inside", 0x25d, 80, 124),
]
entrance_lookup = {str(coord): coord for coord in entrance_coords}

View File

@@ -4,12 +4,13 @@ import os
import pkgutil
import tempfile
import typing
import logging
import re
import bsdiff4
import settings
from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial, MultiWorld
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Tutorial, MultiWorld
from Fill import fill_restrictive
from worlds.AutoWorld import WebWorld, World
from .Common import *
@@ -178,10 +179,10 @@ class LinksAwakeningWorld(World):
assert(start)
menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld)
menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld)
menu_region.exits = [Entrance(self.player, "Start Game", menu_region)]
menu_region.exits[0].connect(start)
self.multiworld.regions.append(menu_region)
# Place RAFT, other access events
@@ -189,14 +190,14 @@ class LinksAwakeningWorld(World):
for loc in region.locations:
if loc.address is None:
loc.place_locked_item(self.create_event(loc.ladxr_item.event))
# Connect Windfish -> Victory
windfish = self.multiworld.get_region("Windfish", self.player)
l = Location(self.player, "Windfish", parent=windfish)
windfish.locations = [l]
l.place_locked_item(self.create_event("An Alarm Clock"))
self.multiworld.completion_condition[self.player] = lambda state: state.has("An Alarm Clock", player=self.player)
def create_item(self, item_name: str):
@@ -206,6 +207,8 @@ class LinksAwakeningWorld(World):
return Item(event, ItemClassification.progression, None, self.player)
def create_items(self) -> None:
itempool = []
exclude = [item.name for item in self.multiworld.precollected_items[self.player]]
self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ]
@@ -265,9 +268,9 @@ class LinksAwakeningWorld(World):
self.prefill_own_dungeons.append(item)
self.pre_fill_items.append(item)
else:
self.multiworld.itempool.append(item)
itempool.append(item)
else:
self.multiworld.itempool.append(item)
itempool.append(item)
self.multi_key = self.generate_multi_key()
@@ -276,8 +279,8 @@ class LinksAwakeningWorld(World):
event_location = Location(self.player, "Can Play Trendy Game", parent=trendy_region)
trendy_region.locations.insert(0, event_location)
event_location.place_locked_item(self.create_event("Can Play Trendy Game"))
self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []]
self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []]
for r in self.multiworld.get_regions(self.player):
# Set aside dungeon locations
if r.dungeon_index:
@@ -290,21 +293,52 @@ class LinksAwakeningWorld(World):
# Properly fill locations within dungeon
location.dungeon = r.dungeon_index
# For now, special case first item
FORCE_START_ITEM = True
if FORCE_START_ITEM:
self.force_start_item()
if self.options.tarins_gift != "any_item":
self.force_start_item(itempool)
def force_start_item(self):
self.multiworld.itempool += itempool
def force_start_item(self, itempool):
start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player)
if not start_loc.item:
possible_start_items = [index for index, item in enumerate(self.multiworld.itempool)
if item.player == self.player
and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS and not item.location]
if possible_start_items:
index = self.random.choice(possible_start_items)
start_item = self.multiworld.itempool.pop(index)
"""
Find an item that forces progression or a bush breaker for the player, depending on settings.
"""
def is_possible_start_item(item):
return item.advancement and item.name not in self.options.non_local_items
def opens_new_regions(item):
collection_state = base_collection_state.copy()
collection_state.collect(item)
return len(collection_state.reachable_regions[self.player]) > reachable_count
start_items = [item for item in itempool if is_possible_start_item(item)]
self.random.shuffle(start_items)
if self.options.tarins_gift == "bush_breaker":
start_item = next((item for item in start_items if item.name in links_awakening_item_name_groups["Bush Breakers"]), None)
else: # local_progression
entrance_mapping = self.ladxr_logic.world_setup.entrance_mapping
# Tail key opens a region but not a location if d1 entrance is not mapped to d1 or d4
# exclude it in these cases to avoid fill errors
if entrance_mapping['d1'] not in ['d1', 'd4']:
start_items = [item for item in start_items if item.name != 'Tail Key']
# Exclude shovel unless starting in Mabe Village
if entrance_mapping['start_house'] not in ['start_house', 'shop']:
start_items = [item for item in start_items if item.name != 'Shovel']
base_collection_state = CollectionState(self.multiworld)
base_collection_state.update_reachable_regions(self.player)
reachable_count = len(base_collection_state.reachable_regions[self.player])
start_item = next((item for item in start_items if opens_new_regions(item)), None)
if start_item:
itempool.remove(start_item)
start_loc.place_locked_item(start_item)
else:
logging.getLogger("Link's Awakening Logger").warning(f"No {self.options.tarins_gift.current_option_name} available for Tarin's Gift.")
def get_pre_fill_items(self):
return self.pre_fill_items
@@ -315,11 +349,9 @@ class LinksAwakeningWorld(World):
# Set up filter rules
# The list of items we will pass to fill_restrictive, contains at first the items that go to all dungeons
all_dungeon_items_to_fill = list(self.prefill_own_dungeons)
# set containing the list of all possible dungeon locations for the player
all_dungeon_locs = set()
# Do dungeon specific things
for dungeon_index in range(0, 9):
# set up allow-list for dungeon specific items
@@ -327,15 +359,12 @@ class LinksAwakeningWorld(World):
for item in self.prefill_original_dungeon[dungeon_index]:
allowed_locations_by_item[item] = locs
# put the items for this dungeon in the list to fill
all_dungeon_items_to_fill.extend(self.prefill_original_dungeon[dungeon_index])
# ...and gather the list of all dungeon locations
all_dungeon_locs |= locs
# ...also set the rules for the dungeon
for location in locs:
orig_rule = location.item_rule
# If an item is about to be placed on a dungeon location, it can go there iff
# If an item is about to be placed on a dungeon location, it can go there iff
# 1. it fits the general rules for that location (probably 'return True' for most places)
# 2. Either
# 2a. it's not a restricted dungeon item
@@ -369,16 +398,27 @@ class LinksAwakeningWorld(World):
if allowed_locations_by_item[item] is all_dungeon_locs:
i += 3
return i
all_dungeon_items_to_fill = self.get_pre_fill_items()
all_dungeon_items_to_fill.sort(key=priority)
# Set up state
all_state = self.multiworld.get_all_state(use_cache=False)
# Remove dungeon items we are about to put in from the state so that we don't double count
for item in all_dungeon_items_to_fill:
all_state.remove(item)
# Finally, fill!
fill_restrictive(self.multiworld, all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False)
partial_all_state = CollectionState(self.multiworld)
# Collect every item from the item pool and every pre-fill item like MultiWorld.get_all_state, except not our own pre-fill items.
for item in self.multiworld.itempool:
partial_all_state.collect(item, prevent_sweep=True)
for player in self.multiworld.player_ids:
if player == self.player:
# Don't collect the items we're about to place.
continue
subworld = self.multiworld.worlds[player]
for item in subworld.get_pre_fill_items():
partial_all_state.collect(item, prevent_sweep=True)
# Sweep to pick up already placed items that are reachable with everything but the dungeon items.
partial_all_state.sweep_for_advancements()
fill_restrictive(self.multiworld, partial_all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False)
name_cache = {}
# Tries to associate an icon from another game with an icon we have
@@ -415,7 +455,7 @@ class LinksAwakeningWorld(World):
for name in possibles:
if name in self.name_cache:
return self.name_cache[name]
return "TRADING_ITEM_LETTER"
@classmethod
@@ -430,7 +470,7 @@ class LinksAwakeningWorld(World):
for loc in r.locations:
if isinstance(loc, LinksAwakeningLocation):
assert(loc.item)
# If we're a links awakening item, just use the item
if isinstance(loc.item, LinksAwakeningItem):
loc.ladxr_item.item = loc.item.item_data.ladxr_id
@@ -464,7 +504,7 @@ class LinksAwakeningWorld(World):
args = parser.parse_args([rom_name, "-o", out_name, "--dump"])
rom = generator.generateRom(args, self)
with open(out_path, "wb") as handle:
rom.save(handle, name="LADXR")
@@ -472,7 +512,7 @@ class LinksAwakeningWorld(World):
if self.options.ap_title_screen:
with tempfile.NamedTemporaryFile(delete=False) as title_patch:
title_patch.write(pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4"))
bsdiff4.file_patch_inplace(out_path, title_patch.name)
os.unlink(title_patch.name)

View File

@@ -136,6 +136,12 @@ class MeritousWorld(World):
def set_rules(self):
set_rules(self.multiworld, self.player)
if self.goal == 0:
self.multiworld.completion_condition[self.player] = lambda state: state.has_any(
["Victory", "Full Victory"], self.player)
else:
self.multiworld.completion_condition[self.player] = lambda state: state.has(
"Full Victory", self.player)
def generate_basic(self):
self.multiworld.get_location("Place of Power", self.player).place_locked_item(
@@ -166,13 +172,6 @@ class MeritousWorld(World):
self.multiworld.get_location(boss, self.player).place_locked_item(
self.create_item("Evolution Trap"))
if self.goal == 0:
self.multiworld.completion_condition[self.player] = lambda state: state.has_any(
["Victory", "Full Victory"], self.player)
else:
self.multiworld.completion_condition[self.player] = lambda state: state.has(
"Full Victory", self.player)
def fill_slot_data(self) -> dict:
return {
"goal": self.goal,

View File

@@ -228,7 +228,7 @@ class MessengerWorld(World):
f"({self.options.total_seals}). Adjusting to {total_seals}"
)
self.total_seals = total_seals
self.required_seals = int(self.options.percent_seals_required.value / 100 * self.total_seals)
self.required_seals = max(1, int(self.options.percent_seals_required.value / 100 * self.total_seals))
seals = [self.create_item("Power Seal") for _ in range(self.total_seals)]
itempool += seals

View File

@@ -26,7 +26,7 @@ class MessengerRules:
maximum_price = (world.multiworld.get_location("The Shop - Demon's Bane", self.player).cost +
world.multiworld.get_location("The Shop - Focused Power Sense", self.player).cost)
self.maximum_price = min(maximum_price, world.total_shards)
self.required_seals = max(1, world.required_seals)
self.required_seals = world.required_seals
# dict of connection names and requirements to traverse the exit
self.connection_rules = {
@@ -34,7 +34,7 @@ class MessengerRules:
"Artificer's Portal":
lambda state: state.has_all({"Demon King Crown", "Magic Firefly"}, self.player),
"Shrink Down":
lambda state: state.has_all(NOTES, self.player) or self.has_enough_seals(state),
lambda state: state.has_all(NOTES, self.player),
# the shop
"Money Sink":
lambda state: state.has("Money Wrench", self.player) and self.can_shop(state),
@@ -314,6 +314,9 @@ class MessengerRules:
self.has_dart,
}
if self.required_seals:
self.connection_rules["Shrink Down"] = self.has_enough_seals
def has_wingsuit(self, state: CollectionState) -> bool:
return state.has("Wingsuit", self.player)

View File

@@ -1,4 +1,4 @@
from BaseClasses import ItemClassification, CollectionState
from BaseClasses import CollectionState, ItemClassification
from . import MessengerTestBase
@@ -10,8 +10,9 @@ class AllSealsRequired(MessengerTestBase):
def test_chest_access(self) -> None:
"""Defaults to a total of 45 power seals in the pool and required."""
with self.subTest("Access Dependency"):
self.assertEqual(len([seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]),
self.world.options.total_seals)
self.assertEqual(
len([seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]),
self.world.options.total_seals)
locations = ["Rescue Phantom"]
items = [["Power Seal"]]
self.assertAccessDependency(locations, items)
@@ -93,3 +94,22 @@ class MaxSealsWithShards(MessengerTestBase):
if seal.classification == ItemClassification.progression_skip_balancing]
self.assertEqual(len(total_seals), 85)
self.assertEqual(len(required_seals), 85)
class NoSealsRequired(MessengerTestBase):
options = {
"goal": "power_seal_hunt",
"total_seals": 1,
"percent_seals_required": 10, # percentage
}
def test_seals_amount(self) -> None:
"""Should be 1 seal and it should be progression."""
self.assertEqual(self.world.options.total_seals, 1)
self.assertEqual(self.world.total_seals, 1)
self.assertEqual(self.world.required_seals, 1)
total_seals = [item for item in self.multiworld.itempool if item.name == "Power Seal"]
required_seals = [item for item in self.multiworld.itempool if
item.advancement and item.name == "Power Seal"]
self.assertEqual(len(total_seals), 1)
self.assertEqual(len(required_seals), 1)

View File

@@ -269,7 +269,7 @@ class MLSSClient(BizHawkClient):
self.local_checked_locations = locs_to_send
if locs_to_send is not None:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": list(locs_to_send)}])
await ctx.check_locations(locs_to_send)
except bizhawk.RequestFailedError:
# Exit handler and return to main loop to reconnect.

View File

@@ -153,7 +153,6 @@ enemies = [
0x50458C,
0x5045AC,
0x50468C,
# 0x5046CC, 6 enemy formation
0x5046EC,
0x50470C
]
@@ -166,6 +165,7 @@ bosses = [
0x50360C,
0x5037AC,
0x5037CC,
0x50396C,
0x503A8C,
0x503D6C,
0x503F0C,

View File

@@ -160,6 +160,7 @@ itemList: typing.List[ItemData] = [
ItemData(77771142, "Game Boy Horror SP", ItemClassification.useful, 0xFE),
ItemData(77771143, "Woo Bean", ItemClassification.skip_balancing, 0x1C),
ItemData(77771144, "Hee Bean", ItemClassification.skip_balancing, 0x1F),
ItemData(77771145, "Beanstar Emblem", ItemClassification.progression, 0x3E),
]
item_frequencies: typing.Dict[str, int] = {
@@ -186,5 +187,12 @@ item_frequencies: typing.Dict[str, int] = {
"Hammers": 3,
}
mlss_item_name_groups = {
"Beanstar Piece": { "Beanstar Piece 1", "Beanstar Piece 2", "Beanstar Piece 3", "Beanstar Piece 4"},
"Beanfruit": { "Bean Fruit 1", "Bean Fruit 2", "Bean Fruit 3", "Bean Fruit 4", "Bean Fruit 5", "Bean Fruit 6", "Bean Fruit 7"},
"Neon Egg": { "Blue Neon Egg", "Red Neon Egg", "Green Neon Egg", "Yellow Neon Egg", "Purple Neon Egg", "Orange Neon Egg", "Azure Neon Egg"},
"Chuckola Fruit": { "Red Chuckola Fruit", "Purple Chuckola Fruit", "White Chuckola Fruit"}
}
item_table: typing.Dict[str, ItemData] = {item.itemName: item for item in itemList}
items_by_id: typing.Dict[int, ItemData] = {item.code: item for item in itemList}

View File

@@ -251,9 +251,9 @@ coins: typing.List[LocationData] = [
LocationData("Hoohoo Village North Cave Room 1 Coin Block", 0x39DAA0, 0),
LocationData("Hoohoo Village South Cave Coin Block 1", 0x39DAC5, 0),
LocationData("Hoohoo Village South Cave Coin Block 2", 0x39DAD5, 0),
LocationData("Hoohoo Mountain Base Boo Statue Cave Coin Block 1", 0x39DAE2, 0),
LocationData("Hoohoo Mountain Base Boo Statue Cave Coin Block 2", 0x39DAF2, 0),
LocationData("Hoohoo Mountain Base Boo Statue Cave Coin Block 3", 0x39DAFA, 0),
LocationData("Hoohoo Mountain Base Boostatue Cave Coin Block 1", 0x39DAE2, 0),
LocationData("Hoohoo Mountain Base Boostatue Cave Coin Block 2", 0x39DAF2, 0),
LocationData("Hoohoo Mountain Base Boostatue Cave Coin Block 3", 0x39DAFA, 0),
LocationData("Beanbean Outskirts NW Coin Block", 0x39DB8F, 0),
LocationData("Beanbean Outskirts S Room 1 Coin Block", 0x39DC18, 0),
LocationData("Beanbean Outskirts S Room 2 Coin Block", 0x39DC3D, 0),
@@ -262,6 +262,8 @@ coins: typing.List[LocationData] = [
LocationData("Chucklehuck Woods Cave Room 1 Coin Block", 0x39DD7A, 0),
LocationData("Chucklehuck Woods Cave Room 2 Coin Block", 0x39DD97, 0),
LocationData("Chucklehuck Woods Cave Room 3 Coin Block", 0x39DDB4, 0),
LocationData("Chucklehuck Woods Solo Luigi Cave Room 1 Coin Block 1", 0x39DB48, 0),
LocationData("Chucklehuck Woods Solo Luigi Cave Room 1 Coin Block 2", 0x39DB50, 0),
LocationData("Chucklehuck Woods Pipe 5 Room Coin Block", 0x39DDE6, 0),
LocationData("Chucklehuck Woods Room 7 Coin Block", 0x39DE31, 0),
LocationData("Chucklehuck Woods Past Chuckleroot Coin Block", 0x39DF14, 0),
@@ -289,6 +291,7 @@ baseUltraRocks: typing.List[LocationData] = [
LocationData("Teehee Valley Upper Maze Room 1 Block", 0x39E5E0, 0),
LocationData("Teehee Valley Upper Maze Room 2 Digspot 1", 0x39E5C8, 0),
LocationData("Teehee Valley Upper Maze Room 2 Digspot 2", 0x39E5D0, 0),
LocationData("Guffawha Ruins Block", 0x39E6A3, 0),
LocationData("Hoohoo Mountain Base Guffawha Ruins Entrance Digspot", 0x39DA0B, 0),
LocationData("Hoohoo Mountain Base Teehee Valley Entrance Digspot", 0x39DA20, 0),
LocationData("Hoohoo Mountain Base Teehee Valley Entrance Block", 0x39DA18, 0),
@@ -298,7 +301,7 @@ booStatue: typing.List[LocationData] = [
LocationData("Beanbean Outskirts Before Harhall Digspot 1", 0x39E951, 0),
LocationData("Beanbean Outskirts Before Harhall Digspot 2", 0x39E959, 0),
LocationData("Beanstar Piece Harhall", 0x1E9441, 2),
LocationData("Beanbean Outskirts Boo Statue Mole", 0x1E9434, 2),
LocationData("Beanbean Outskirts Boostatue Mole", 0x1E9434, 2),
LocationData("Harhall's Pants", 0x1E9444, 2),
LocationData("Beanbean Outskirts S Room 2 Digspot 1", 0x39DC65, 0),
LocationData("Beanbean Outskirts S Room 2 Digspot 2", 0x39DC5D, 0),
@@ -317,6 +320,9 @@ chucklehuck: typing.List[LocationData] = [
LocationData("Chucklehuck Woods Cave Room 1 Block 2", 0x39DD8A, 0),
LocationData("Chucklehuck Woods Cave Room 2 Block", 0x39DD9F, 0),
LocationData("Chucklehuck Woods Cave Room 3 Block", 0x39DDAC, 0),
LocationData("Chucklehuck Woods Solo Luigi Cave Room 2 Block", 0x39DB72, 0),
LocationData("Chucklehuck Woods Solo Luigi Cave Room 3 Block 1", 0x39DB5D, 0),
LocationData("Chucklehuck Woods Solo Luigi Cave Room 3 Block 2", 0x39DB65, 0),
LocationData("Chucklehuck Woods Room 2 Block", 0x39DDC1, 0),
LocationData("Chucklehuck Woods Room 2 Digspot", 0x39DDC9, 0),
LocationData("Chucklehuck Woods Pipe Room Block 1", 0x39DDD6, 0),
@@ -786,7 +792,7 @@ nonBlock = [
(0x4373, 0x10, 0x277A45), # Teehee Valley Mole
(0x434D, 0x8, 0x1E9444), # Harhall's Pants
(0x432E, 0x10, 0x1E9441), # Harhall Beanstar Piece
(0x434B, 0x8, 0x1E9434), # Outskirts Boo Statue Mole
(0x434B, 0x8, 0x1E9434), # Outskirts Boostatue Mole
(0x42FE, 0x2, 0x1E943E), # Red Goblet
(0x42FE, 0x4, 0x24E628), # Green Goblet
(0x4301, 0x10, 0x250621), # Red Chuckola Fruit

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