Compare commits

..

1496 Commits

Author SHA1 Message Date
NewSoupVi
3bf3ce2da4 Merge branch 'main' into NewSoupVi-patch-2 2024-06-20 07:22:52 +02:00
Aaron Wagener
f515a085db The Messenger: Fix missing rules for Double Swing Saws (#3562)
* The Messenger: Fix missing rules for Double Swing Saws

* i put it in the wrong dictionary

* remove unnecessary call
2024-06-19 16:20:47 +02:00
eudaimonistic
903a0bab1a Docs: Change setup_en.md to use Latest releases page (#3543)
* Change setup_en.md to use Latest releases page

Really simple change to point users to the Latest release page instead of the Releases page.  Saw a user accidentally download 0.3.6 because it was the last item on the page (they're accustomed to scrolling down to the bottom of the page in GitHub for the Assets section), and this change prevents that outright.

* Update setup_en.md

Rewrite text and link to restore semantic compatibility.
2024-06-19 16:12:25 +02:00
Kaito Sinclaire
9bb3947d7e Doom 2, Heretic: fix missing items (Doom2 Megasphere, Heretic Torch) (#3561)
for doom 2, some of the armor and health weights were nudged down
to compensate for the addition of the megasphere

for heretic, the torch was just added without changing anything else,
as I felt doing so would negatively impact the distribution of
artifacts (and personally I already feel there's too few in a game)
2024-06-19 12:59:10 +02:00
Mrks
240d1a3bbf LADX: Adding 'Option Groups' to the player options page. (#3560)
* Adding 'Option Groups' to the LADX player options page.

* Moved 'Miscellaneous' group to the logic effecting groups.
2024-06-19 08:40:10 +02:00
Kory Dondzila
b6191ff7ca Shivers: Adds missing indirect conditions. (#3558)
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-06-18 05:10:54 +02:00
Scipio Wright
19d00547c2 TUNIC: Add note about bushes to logic section of game info page (#3555)
* Add note about bushes to logic section of readme

* Update worlds/tunic/docs/en_TUNIC.md

Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com>

---------

Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com>
2024-06-18 04:51:54 +02:00
chesslogic
67a0a04917 Tests: minor: update tests base for Options API (#2516)
* update tests for Options API

* The actual "bug"

* resolve qwint's comment from 3 months ago
2024-06-18 04:49:26 +02:00
Star Rauchenberger
af213c9e5d LADX: Converted to new options API (+other small refactors) (#3542)
* Refactored various things

* Renamed hidden variable in dungeon item shuffle block

* Fixed LADXRSettings initialization

* Rename ladxr_options -> ladxr_settings

* Remove unnecessary int cast

* Update worlds/ladx/LADXR/generator.py

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

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-06-18 04:48:15 +02:00
Zach Parks
898509e7ee CODEOWNERS: Remove @zig-for as world maintainer for LADX. (#3525)
Per request: https://discord.com/channels/731205301247803413/1214608557077700720/1250714693136547920
2024-06-16 05:38:08 -05:00
Zach Parks
1f685b4272 CommonClient: Use lookup_in_game instead of lookup_in_slot in case of own-game name lookup when disconnected from server. (#3514) 2024-06-16 05:37:05 -05:00
Scipio Wright
c622240730 Tunc: Update plando connections description (#3545) 2024-06-16 05:02:48 +02:00
Mrks
1d314374d7 LADX: Moved ROM requirement from generate_output to stage_assert_generate. (#3540)
Co-authored-by: Mrks <markus.burmeister@mburm.de>
2024-06-16 04:31:32 +02:00
palex00
753eb8683f Pokemon Red/Blue: Replaces link to R&B Poptracker with a new one (#3516)
* Update setup_en.md

* Update setup_es.md
2024-06-16 04:10:50 +02:00
Fabian Dill
e8542b8acd Generate: split ERmain out of main (#3515) 2024-06-16 03:27:06 +02:00
NewSoupVi
2a11d610b6 The Witness: Fix Shuffle Postgame always thinking it's Challenge Victory (#3504)
* Fix postgame thinking it's the wrong panel

* Also don't have a default value for it so it doesn't happen again
2024-06-16 01:56:20 +02:00
coveleski
92023a2cb5 Pokemon RB: Add new options to slot_data (#3538)
Added require_pokedex, blind_trainers, and area_1_to_1 mapping, which would be helpful to the poptracker packs to accurately reflect the checks available to players.
2024-06-16 01:55:52 +02:00
Fabian Dill
df94271d30 LttP: fix single-player no-logic generation (#3454) 2024-06-15 19:18:26 +02:00
Bryce Wilson
0354315c22 Pokemon Emerald: Remove README (#3532) 2024-06-15 04:52:01 +02:00
Star Rauchenberger
e796f0ae64 Core: Expose option aliases (#3512) 2024-06-15 04:50:26 +02:00
Natalie Weizenbaum
c61505baf6 WebHost/Core/Lingo: Render option documentation as reStructuredText in the WebView (#3511)
* Render option documentation as reStructuredText in the WebView

This means that options can use the standard Python documentation
format, while producing much nicer-looking documentation in the
WebView with things like emphasis, lists, and so on.

* Opt existing worlds out of rich option docs

This avoids breaking the rendering of existing option docs which were
written with the old plain text rendering in mind, while also allowing
new options to default to the rich text rendering instead.

* Use reStructuredText formatting for Lingo Options docstrings

* Disable raw and file insertion RST directives

* Update doc comments per code review

* Make rich text docs opt-in

* Put rich_text_options_doc on WebWorld

* Document rich text API

* Code review

* Update docs/options api.md

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

* Update Options.py

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

---------

Co-authored-by: Chris Wilson <chris@legendserver.info>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2024-06-14 18:53:42 -04:00
Fabian Dill
3972b1b257 Options: fix yaml export corner case (#3529) 2024-06-15 00:48:49 +02:00
black-sliver
1fe3d842c8 CI: Install specific inno version (#3526)
* CI: Install specific inno version

* great mobile dev experience

* maybe this

* really don't enjoy PS

* Anothet attempt

* maybe fix log

* slowly going mad

* fml

* allow downgrade
2024-06-14 08:47:47 +02:00
Fabian Dill
e9ad7cb797 WebHost: fix option doc indent (#3513)
* WebHost: fix option doc indent

* Update macros.html
2024-06-13 17:37:52 -04:00
NewSoupVi
533395d336 WebHost: Fix Named Range displays on Player Options page (#3521)
* Player Options: Fix Named Range displays

* Also add validation to the NamedRange class itself

* Don't break Stardew

* Comment

* Do replace first so title works correctly

* Bring change to Weighted Options as well
2024-06-13 17:29:39 -04:00
NewSoupVi
2ae51364d9 WebHost: Fix default values that are 2 or more words in Weighted Options (#3519)
* WeightedOptions: Fix default values that are 2 or more words

* So much simpler
2024-06-13 12:24:56 -04:00
NewSoupVi
f6e3113af6 WebHost: Fix "Add" button for custom option values causing a weird redirect (#3518)
* WebHost: Fix "Add" button for Progression Balancing causing a weird redirect

This "add" button is part of a form, which causes it to submit the form, because the default type for a button is "submit".

This PR changes the type of the button to "button", which causes it to not submit the form and just execute its normal effect.

(An alternative would be `event.preventDefault()` but that seems less clean to me, but also I'm not a HTML/JS dev)

* There's also multiple.
2024-06-13 04:39:16 -04:00
JoshuaEagles
da34800f43 Fix Incorrect Link Syntax in SA2B Linux Setup (#3524) 2024-06-13 06:53:01 +02:00
black-sliver
c108845d1f CI: more checks in build and rework compression (#3336)
* CI: build: fail fast if setup.py fails on windows

* CI: build: fail for missing uploads, rework compression

Upload-artifact allows setting compression level now.
The change speeds up both upload and extraction.

* CI: match build gz in release

* CI: build: verify worlds all load

* CI: build: generate a game

* Generate: move worlds loaded exception to allow settings to init from worlds

* CI: build: build setup before running tests
2024-06-12 18:55:48 +02:00
black-sliver
acf85eb9ab Speedups: remove dependency on c++ (#2796)
* Speedups: remove dependency on c++

* Speedups: intset: handle malloc failing

* Speedups: intset: fix corner case for int64 on 32bit systems

original idea was to only use bucket->val if int<pointer,
but we always have a union now anyway

* Speedups: add size comment to player_set bucket configuration

* test: more tests for LocationStore.find_item

* test: require _speedups in CI

This kind of tests that the build succeeds.

* test: even more tests for LocationStore.find_item

* Speedups: intset uniform comment style

* Speedups: intset: avoid memory leak when realloc fails

* Speedups: intset: make `gcc -pedantic -std=c99 -fanalyzer` without warnings

Unnamed unions are not in C99, this got fixed.
The overhead of setting count=0 is minimal or optimized-out and silences -fanalizer (see comment).

* Speedups: don't leak memory in case of exception

* Speedups: intset: validate alloc and free

This won't happen in our cython, but it's still a good addition.

* CI: add test framework for C/C++ code

* CI: ctest: fix cwd

* Speedups: intset: ignore msvc warning

* Tests: intset: revert attempt at no-asan

We solve this with env vars in ctest now, and this fails for msvc.

* Test: cpp: docs: fix typo

* Test: cpp: docs: fix another typo

* Test: intset: proper bucket count for Negative test

INTxx_MIN % 1 would not produce a negative number, so the test was flawed.
2024-06-12 18:54:59 +02:00
Fabian Dill
2daccded36 Core: don't lock progression (#3501) 2024-06-12 15:35:51 +02:00
Fabian Dill
3b9b9353b7 WebHost: delete old docs files (#3503) 2024-06-12 15:34:46 +02:00
Silvris
b9e454ab4e TS: add indirect connections (#3490) 2024-06-12 03:23:46 +02:00
Natalie Weizenbaum
7299891bdf Allow worlds to add options to prebuilt groups (#3509)
Previously, this crashed because `typing.NamedTuple` fields such as
`group.name` aren't assignable. Now it will only fail for group names
that are actually incorrectly cased, and will fail with a better error
message.
2024-06-12 03:22:14 +02:00
Fabian Dill
e755f1a0b5 SC2: don't close all SC2 instances when one quits (#3507) 2024-06-12 02:14:30 +02:00
Louis M
87d24eb38a Aquaria: Add entrance rule and fix start_inventory_from_pool (#3473) 2024-06-11 17:59:46 -05:00
Justus Lind
54531c6eba Muse Dash: Remove regions for a decent speed gain in generating worlds (#3435)
* Remove Muse Dash Regions.

* Update comments.
2024-06-11 03:11:19 +02:00
Zach Parks
ccfffa1147 CODEOWNERS: Replace @ThePhar with @qwint as Hollow Knight maintainer. (#3508) 2024-06-10 18:55:02 -05:00
Fabian Dill
75bef3ddb1 Various: fix absolute imports in worlds (#3489) 2024-06-11 00:42:57 +02:00
JusticePS
484082616f Adventure: Update to use new options api (#3326) 2024-06-11 00:42:01 +02:00
Aaron Wagener
35617bdac5 Tests: Add checksum validation to the postgen datapackage test (#3456)
* Tests: Add checksum validation to the postgen datapackage test

* add a special case for the test world datapackage rather than hidden

* add the test world to the datapackage instead of special casing around it

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-06-10 09:28:28 +02:00
Phaneros
0a912808e3 SC2: update inno_setup.iss to remove old sc2wol world folder (#3495) 2024-06-10 02:05:39 +02:00
Phaneros
84a6d50ae7 sc2: Fixed sc2 client's /received command breaking after PR 1933 merged (#3497) 2024-06-09 16:55:05 +02:00
jamesbrq
5f8a8e6dad Update Rom.py (#3498) 2024-06-09 16:54:07 +02:00
Phaneros
2198a70251 Core: CommonClient: command history and echo (#3236)
* client: Added command history access with up/down and command echo in common client

* client: Changed command echo colour to orange

* client: removed star import from typing

* client: updated code style to match style guideline

* client: adjusted ordering of calling parent constructor in command prompt input constructor

* client: Fixed issues identified by beauxq in PR; fixed some typing issues

* client: PR comments; replaced command history list with deque
2024-06-09 04:08:47 +02:00
Fabian Dill
c478e55d7a Generate: improve logging capture (#3484) 2024-06-09 03:13:27 +02:00
Fabian Dill
76804d295b Core: explicitly import importlib.util (#3224) 2024-06-08 20:04:17 +02:00
Fabian Dill
0d9fce29c6 Core: load frozen decompressed worlds (#3488) 2024-06-08 19:58:58 +02:00
black-sliver
302017c69e Test: hosting: handle writes during start_room (#3492)
Note: maybe we'd also want to add such handling to WebHost itself,
      but this is out of scope for getting hosting test to work.
2024-06-08 17:51:09 +02:00
qwint
a0653cdfe0 HK: adds split movement items to skills item group (#3462) 2024-06-08 17:31:27 +02:00
Fabian Dill
89d584e474 WebHost: allow getting checksum-specific datapackage via /api/datapackage/<checksum> (#3451)
* WebHost: allow getting checksum-specific datapackage via /api/datapackage/<checksum>

* match import style of /api/generate
2024-06-08 05:07:14 -04:00
Chris Wilson
39deef5d09 Fix Choice and TextChoice options crashing WebHost if the option's default value is "random" (#3458) 2024-06-08 04:54:14 -04:00
Exempt-Medic
b3a2473853 Docs: Fixing subject-verb agreement (#3491) 2024-06-08 05:47:02 +02:00
qwint
b053fee3e5 HK: adds schema to validate plando charm costs (#3471) 2024-06-07 19:12:10 +02:00
Trevor L
8c614865bb Bomb Rush Cyberfunk: Fix missing location (#3475) 2024-06-07 19:11:35 +02:00
Silent
d72afe7100 Update setup_en.md (#3483) 2024-06-07 17:45:22 +02:00
chandler05
223f2f5523 A Short Hike: Update installation instructions (#3474)
* A Short Hike: Update installation instructions

* Update setup_en.md

* Update setup_en.md

* Change link
2024-06-06 22:57:50 +02:00
Scipio Wright
31419c84a4 TUNIC: Remove rule for west Quarry bomb wall (#3481)
* Update west quarry bomb wall rule

* Update west quarry bomb wall rule
2024-06-06 22:56:35 +02:00
Doug Hoskisson
6bb1cce43f Core: hot reload components from installed apworld (#3480)
* Core: hot reload components from installed apworld

* address PR reviews

`Launcher` widget members default to `None` so they can be defined in `build`

`Launcher._refresh_components` is not wrapped

loaded world goes into `world_sources` so we can check if it's already loaded.
(`WorldSource` can be ordered now without trying to compare `None` and `float`)
(don't load empty directories so we don't detect them as worlds)

* clarify that the installation is successful
2024-06-06 20:36:14 +02:00
black-sliver
808f2a8ff0 Core: update dependencies (#3477) 2024-06-06 19:27:01 +02:00
Doug Hoskisson
7f1e95c04c Core: gitignore custom_worlds (#3479) 2024-06-06 09:02:29 +02:00
NewSoupVi
86da3eb52c Remove all functools lru cache (#3446) 2024-06-06 03:40:47 +02:00
black-sliver
afb6d9c4da MultiServer, customserver, CI, Test: Fix problems in room hosting and test/simulate it (#3464)
* Test: add hosting simulation test

* WebHost: add weak typing to get_app()

* MultiServer: add typing to auto_saver_thread

* MultiServer: don't cancel task, properly end it

* customserver: stop auto-save thread from saving after shutdown

and make sure it stops, another potential memory leak

* MultiServer, customserver: make datapackage small again

* customserver: collect/finish room tasks

Hopefully fixes the memory leak we are seeing

* CI: test hosting

* Test: hosting: verify autohoster saves on Ctrl+C

* customserver: save when stopping via Ctrl+C
2024-06-06 01:54:46 +02:00
black-sliver
911eba3202 WebHost: update dependencies (#3476) 2024-06-06 01:51:05 +02:00
Fabian Dill
93cd13736a Launcher: handle apworld installation (#3472) 2024-06-06 01:36:02 +02:00
chandler05
c554c3fdae A Short Hike: Add new options and option groups (#3410)
* A Short Hike: New options and stuff

* Add to slot data for poptracker

* Address concerns

* Address concerns

* Fix indentations

* Update option description

* Address all issues

* Group "or"s
2024-06-06 00:50:30 +02:00
Aaron Wagener
be03dca774 Core: add unit tests and more documentation for numeric options (#2926)
* Core: add unit tests for the numeric options

* document using a collection and the hashing quirk

* add another example for the footgun

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-06-06 00:17:52 +02:00
Fabian Dill
04ec2f3893 Setup: delete old world folders (#3469) 2024-06-05 22:26:13 +02:00
Fabian Dill
afe4b2925e Setup: rename ArchipelagoLauncher(DEBUG) to ArchipelagoLauncherDebug (#3468) 2024-06-05 21:00:53 +02:00
qwint
da2f0f94ca HK: lower max egg cost (#3463) 2024-06-05 00:01:22 -05:00
Doug Hoskisson
6a60a93092 Zillion: fix some game over bugs (#3466)
There was a bug that made lots of flashing terrain if a game over happened in certain places.
(And this could be dangerous for people sensitive to flashing lights.)

There was also a bug with a bad sound effect after a game over.
2024-06-04 21:56:32 -07:00
Doug Hoskisson
76266f25ef Core: Launcher: can drag-and-drop patch on Launcher window (#3442)
* Core: Launcher: can drag-and-drop patch on Launcher window

* doc string for `_on_drop_file`
2024-06-05 01:54:21 +02:00
Aaron Wagener
3cc391e9a1 Docs: Add detail on customizing the forced groups (#3371)
* Docs: Fix incorrect assertion in option group docs and add detail on customizing the forced groups.

* add docs for the visibility attribute

* typos

* review comments

* missed one

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

* better wording

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

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-06-04 21:52:07 +02:00
Justus Lind
133167564c Muse Dash: Option Groups and Options Rework (#3434)
* Ensure that included/starter songs only include those within enabled dlcs.

* Allow filtering traps by trap instead of by category.

* Add in the currently available limited time dlcs to the dlc list.

* Add the option group to the webhost and cleanup some errors.

* Fix trap list.

* Update tests. Add new ones to test correctness of new features.

* Remove the old Just As Planned option

* Make traps order alphabetically. Also adjust the title for traps.

* Adjust new lines to better fit the website.

* Style fixes.

* Test adjustments and a fix due to test no longer having just as planned dlc.

* Undo spacing changes as it breaks yaml generation.

* Fix indenting in webhost.

* Add the old options in as removed. Also clean up unused import.

* Remove references to the old allow_just_as_planned_dlc_songs option in Muse Dash tests.

* Add newline to end of file.

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-06-04 21:45:26 +02:00
Rjosephson
f30f2d3a3f RoR2: Add Support for New Stage (#3436)
* add support for the new stage added to RoR2

* Fix stage being unreachable

* add option groups

* reorder option groups
2024-06-04 21:24:14 +02:00
Bryce Wilson
ee1b13f219 Pokemon Emerald: Fix possible dexsanity/legendary hunt softlock (#3443)
* Pokemon Emerald: Remove mirage tower from allowed dexsanity maps

* Pokemon Emerald: Prevent placing wailord/relicanth in out of logic maps

* Pokemon Emerald: Clarify docstring

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

* Pokemon Emerald: Update changelog

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-06-04 21:21:58 +02:00
Exempt-Medic
c4572964ec KH2: Fixing Start Inventory bug, limiting CustomItemPool keys, fixing two typos (#3444)
* Fixing inclusion checking error

* Fixing typo, limiting valid keys to valid keys

* Adding space

* Add period
2024-06-04 21:20:37 +02:00
CookieCat
16ae8449f4 AHIT: Fix Death Wish location rules not being added properly (#3455)
* duh

* Fuck it

* Major fixes

* a

* b

* Even more fixes

* New option - NoFreeRoamFinale

* a

* Hat Logic Fix

* Just to be safe

* multiworld.random to world.random

* KeyError fix

* Update .gitignore

* Update __init__.py

* Zoinks Scoob

* ffs

* Ruh Roh Raggy, more r-r-r-random bugs!

* 0.9b - cleanup + expanded logic difficulty

* Update Rules.py

* Update Regions.py

* AttributeError fix

* 0.10b - New Options

* 1.0 Preparations

* Docs

* Docs 2

* Fixes

* Update __init__.py

* Fixes

* variable capture my beloathed

* Fixes

* a

* 10 Seconds logic fix

* 1.1

* 1.2

* a

* New client

* More client changes

* 1.3

* Final touch-ups for 1.3

* 1.3.1

* 1.3.3

* Zero Jumps gen error fix

* more fixes

* Formatting improvements

* typo

* Update __init__.py

* Revert "Update __init__.py"

This reverts commit e178a7c0a6.

* init

* Update to new options API

* Missed some

* Snatcher Coins fix

* Missed some more

* some slight touch ups

* rewind

* a

* fix things

* Revert "Merge branch 'main' of https://github.com/CookieCat45/Archipelago-ahit"

This reverts commit a2360fe197, reversing
changes made to b8948bc495.

* Update .gitignore

* 1.3.6

* Final touch-ups

* Fix client and leftover old options api

* Delete setup-ahitclient.py

* Update .gitignore

* old python version fix

* proper warnings for invalid act plandos

* Update worlds/ahit/docs/en_A Hat in Time.md

Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com>

* Update worlds/ahit/docs/setup_en.md

Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com>

* 120 char per line

* "settings" to "options"

* Update DeathWishRules.py

* Update worlds/ahit/docs/en_A Hat in Time.md

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

* No more loading the data package

* cleanup + act plando fixes

* almost forgot

* Update Rules.py

* a

* Update worlds/ahit/Options.py

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* Options stuff

* oop

* no unnecessary type hints

* warn about depot download length in setup guide

* Update worlds/ahit/Options.py

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* typo

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* Update worlds/ahit/Rules.py

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* review stuff

* More stuff from review

* comment

* 1.5 Update

* link fix?

* link fix 2

* Update setup_en.md

* Update setup_en.md

* Update setup_en.md

* Evil

* Good fucking lord

* Review stuff again + Logic fixes

* More review stuff

* Even more review stuff - we're almost done

* DW review stuff

* Finish up review stuff

* remove leftover stuff

* a

* assert item

* add A Hat in Time to readme/codeowners files

* Fix range options not being corrected properly

* 120 chars per line in docs

* Update worlds/ahit/Regions.py

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

* Update worlds/ahit/DeathWishLocations.py

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

* Remove some unnecessary option.class.value

* Remove data_version and more option.class.value

* Update worlds/ahit/Items.py

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

* Remove the rest of option.class.value

* Update worlds/ahit/DeathWishLocations.py

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

* review stuff

* Replace connect_regions with Region.connect

* review stuff

* Remove unnecessary Optional from LocData

* Remove HatType.NONE

* Update worlds/ahit/test/TestActs.py

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

* fix so default tests actually don't run

* Improve performance for death wish rules

* rename test file

* change test imports

* 1000 is probably unnecessary

* a

* change state.count to state.has

* stuff

* starting inventory hats fix

* shouldn't have done this lol

* make ship shape task goal equal to number of tasksanity checks if set to 0

* a

* change act shuffle starting acts + logic updates

* dumb

* option groups + lambda capture cringe + typo

* a

* b

* missing option in groups

* c

* Fix Your Contract Has Expired being placed on first level when it shouldn't

* formatting

* major logic bug fix for death wish

---------

Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com>
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Co-authored-by: Ixrec <ericrhitchcock@gmail.com>
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2024-06-04 21:15:28 +02:00
Scipio Wright
c4e0b17de3 TUNIC: Add ice grapple logic to get to gauntlet (#3459) 2024-06-04 21:14:29 +02:00
Bryce Wilson
0265f4d809 BizHawkClient: Reset finished_game if ROM changes (#3246) 2024-06-04 14:06:41 +02:00
Chris Wilson
06e65c1dc6 WebHost: weighted-options bugfixes (#3448)
* Fix improper css for word-break on player-options page

* Add default handling to weighted-options types

* Remove random-low/mid/high from Toggle, Choice, and TextChoice,

* Port key sorting for OptionList and OptionSet from player-options to weighted-options

* Ensure Choice and TextChoice values are set properly

* Remove debug line 🤦‍♂️
2024-06-03 18:43:01 -04:00
Exempt-Medic
c7eef13b33 Accounting for name change (#3449) 2024-06-03 16:36:51 +02:00
Star Rauchenberger
fb2c194e37 Lingo: Fix Basement access with THE MASTER (#3231) 2024-06-03 03:51:27 -05:00
Zach Parks
cff7327558 Utils: Fix mistake made with KeyedDefaultDict from #1933 that broke tracker functionality. (#3433) 2024-06-03 03:45:01 -05:00
Scipio Wright
70e9ccb13c TUNIC: Fix plando connections, seed groups, and UT support (#3429) 2024-06-03 03:44:37 -05:00
Exempt-Medic
d9120f0bea WebHost: Allowing options that work on WebHost to be used in presets (#3441) 2024-06-03 03:42:27 -05:00
Remy Jette
424c8b0be9 Pokemon RB: Add an item group for each HM to improve hinting (#3311)
* Pokemon RB: Add an item group for each HM

HMs are suffixed with the name of the move, e.g. "HM02 Fly". If TM
move are randomized, they do not have the move name, e.g. "TM02".

If someone hints for an HM using the just the number, the fuzzy matching
sees "TM02" as closer than "HM02 Fly", and in fact sees it as close
enough to not ask the user to confirm, leading them to waste hint points
on non-progression item that they didn't intend.

Emerald already does this for this reason, adding the same for RB.

* Add the new groups for HMs in the item_table instead
2024-06-03 04:42:15 +02:00
qwint
6432560fe5 Fix Egg_Shop typo in costsanity (#3447) 2024-06-03 04:39:34 +02:00
Emily
dedabad290 APSudoku: take over maintaining hintgame sudoku from bk_sudoku (#3432) 2024-06-02 11:45:46 -05:00
NewSoupVi
e49b1f9fbb The Witness: Automatic Postgame & Disabled Panels Calculation (#2698)
* Refactor postgame code to be more readable

* Change all references to options to strings

* oops

* Fix some outdated code related to yaml-disabled EPs

* Small fixes to short/longbox stuff (thanks Medic)

* comment

* fix duplicate

* Removed triplicate lmfao

* Better comment

* added another 'unfun' postgame consideration

* comment

* more option strings

* oops

* Remove an unnecessary comparison

* another string missed

* New classification changes (Credit: Exempt-Medic)

* Don't need to pass world

* Comments

* Replace it with another magic system because why not at this point :DDDDDD

* oops

* Oops

* Another was missed

* Make events conditions. Disable_Non_Randomized will no longer just 'have all events'

* What the fuck? Has this just always been broken?

* Don't have boolean function with 'not' in the name

* Another useful classification

* slight code refactor

* Funny haha booleans

* This would create a really bad merge error

* I can't believe this actually kind of works

* And here's the punchline. + some bugfixes

* Comment dat code

* Comments galore

* LMAO OOPS

* so nice I did it twice

* debug x2

* Careful

* Add more comments

* That comment is a bit unnecessary now

* Fix overriding region connections

* Correct a comment

* Correct again

* Rename variable

* Idk I guess this is in this branch now

* More tweaking of postgame & comments

* This is commit just exists to fix that grammar error

* I think I can just fucking delete this now???

* Forgot to reset something here

* Delete dead codepath

* Obelisk Keys were getting yote erroneously

* More comments

* Fix duplicate connections

* Oopsington III

* performance improvements & cleanup

* More rules cleanup and performance improvements

* Oh cool I can do this huh

* Okay but this is even more swag tho

* Lazy eval

* remove some implicit checks

* Is this too magical yet

* more guard magic

* Maaaaaaaagiccccccccc

* Laaaaaaaaaaaaaaaazzzzzzyyyyyyyyyyy

* Make it docstring

* Newline bc I like that better

* this is a little spooky lol

* lol

* Wait

* spoO

* Better variable name and comment

* Improved comment again

* better API

* oops I deleted a deepcopy

* lol help

* Help???

* player_regionsns lmao

* Add some comments

* Make doors disabled properly again. I hope this works

* Don't disable lasers

* Omega oops

* Make Floor 2 Exit not exist

* Make a fix that's warps compatible

* I think this was an oversight, I tested a seed and it seems to have the same result

* This is definitely less Violet than before

* Does this feel more violet lol

* Exception if a laser gets disabled, cleanup

* Ruff

* >:(

* consistent utils import

* Make autopostgame more reviewable (hopefully)

* more reviewability

* WitnessRule

* replace another instance of it

* lint

* style

* comment

* found the bug

* Move comment

* Get rid of cache and ugly allow_victory

* comments and lint
2024-06-01 23:11:28 +02:00
Fabian Dill
da33d1576a WebHost: update trackers only if they're visible. (#3407) 2024-06-01 17:07:58 +02:00
Fabian Dill
13bc121c27 Webhost: Sphere Tracker (#3412) 2024-06-01 14:43:11 +02:00
Fabian Dill
bbc79a5b99 LttP: allow Triforce Piece as start inventory item (#3292) 2024-06-01 14:38:45 +02:00
Ishigh1
3cb5452455 Core: Fix auto-fill in the text client when clicking on a hint suggestion (#3267) 2024-06-01 07:32:41 -05:00
Nicholas Saylor
8dbc8d2d41 Installer: Prevent ALTTP Sprite Download from being Interrupted (#3293) 2024-06-01 06:42:02 -05:00
Dinopony
1e205f9d73 Landstalker: Fixed rare generation issues (#3353)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2024-06-01 06:39:57 -05:00
Exempt-Medic
97c9c5310b PKMN R/B: Fixing Key Items Only + Removed Exp. All (#3420) 2024-06-01 06:35:33 -05:00
Silvris
4e5b6bb3d2 Core: move PlandoConnections and PlandoTexts to the options system (#2904)
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: beauxq <beauxq@yahoo.com>
Co-authored-by: alwaysintreble <mmmcheese158@gmail.com>
2024-06-01 06:34:41 -05:00
Bryce Wilson
f40b10dc97 Pokemon Emerald: Adjust options (#3278) 2024-06-01 06:14:40 -05:00
NewSoupVi
4cab3b6371 The Witness: Put Treehouse Both Orange Bridges EP on the normal EPs exclusion list (#3308) 2024-06-01 06:13:00 -05:00
Bryce Wilson
67cd32b37c Pokemon Emerald: Use self.player_name (#3384) 2024-06-01 06:12:37 -05:00
Rensen3
91c89604a5 YGO06: prevent multiple players affecting each others procedure patch (#3409) 2024-06-01 06:10:02 -05:00
Louis M
f2587d5d27 Aquatia: Locations name changed due to typo's, grammar, or inconsistencies (#3421) 2024-06-01 06:09:34 -05:00
Exempt-Medic
2a5de8567e Docs: Making option description more readable and accurate (#3426) 2024-06-01 06:07:43 -05:00
Zach Parks
5aa6ad63ca Core: Remove Universally Unique ID Requirements (Per-Game Data Packages) (#1933) 2024-06-01 06:07:13 -05:00
Chris Wilson
f3003ff147 Fix options pages sometimes displaying blank values in form fields (#3364) 2024-05-31 22:41:49 -04:00
Chris Wilson
15e06e1779 Fix TextChoice options sometimes creating a broken YAML (#3390)
* Fix TextChoice options with custom values improperly being included in YAML output

* Update WebHostLib/options.py

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

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2024-05-31 22:41:03 -04:00
Exempt-Medic
b055a39454 PKMN R/B: "J.r" -> "Jr." (#3423) 2024-05-31 14:48:21 -05:00
BadMagic100
7058575c95 Hollow Knight: Add missing comma (#3403) 2024-05-30 19:57:54 +02:00
Salzkorn
2fe8c43351 SC2: Fix Kerrigan Primal Form on Half Completion (#3419) 2024-05-30 18:52:01 +02:00
black-sliver
6f6bf3c62d CustomServer: properly 'inherit' Archipelago from static_server_data (#3366)
This fixes a potential exception during room spin-up.
2024-05-30 18:16:13 +02:00
Witchybun
378af4b07c Stardew Valley: Fix magic altar logic (#3417)
* Fix magic altar logic

* Force a tuple (really?)

* Fix received and force progression on all spells

* Reversing the tuple change (?yllaer)
2024-05-29 20:16:19 +02:00
Zach Parks
34f903e97a CODEOWNERS: Remove @jtoyoda as world maintainer for Final Fantasy (#3398) 2024-05-29 09:59:40 -05:00
Fabian Dill
e31a7093de WebHost: use settings defaults for /api/generate and options -> Single Player Generate (#3411) 2024-05-29 16:53:18 +02:00
neocerber
527559395c Docs, Starcraft 2: Add French documentation for setup and game page (#3031)
* Started to create the french doc

* First version of sc2 setup in french finish, created the file for the introduction of the game in french

* French-fy upgrade in setup, continue translation of game description

* Finish writing FR game page, added a link to it on the english game page. Re-read and corrected both the game page and setup page.

* Corrected a sentence in the SC2 English setup guide.

* Applied 120 carac limits for french part, applied modification for consistency.

* Added reference to website yaml checker, applied several wording correction/suggestions

* Modified link to AP page to be in relative (fr/en), uniformed SC2 and random writing (fr), applied some suggestons in writing quality(fr), added a mention to the datapackage (fr/en), enhanced prog balancing recommendation (fr)

* Correction of some grammar issues

* Removed name correction for english part since done in other PR; added mention to hotkey and language restriction

* Applied suggestions of peer review

* Applied mofications proposed by reviewer about the external website

---------

Co-authored-by: neocerber <neorcerber@gmail.com>
2024-05-29 03:48:52 +02:00
Aaron Wagener
649ee117da Docs: improve contributing sign posting (#2888)
* Docs: improve sign posting for contributing

* fix styling as per the style guide

* address review comments

* apply medic's feedback
2024-05-29 03:46:17 +02:00
qwint
5b34e06c8b adds godtuner to prog and requires it for godhome flower quest manually (#3402) 2024-05-29 03:37:44 +02:00
Seldom
04e9f5c47a Migrate Terraria to new options API (#3414) 2024-05-28 20:37:07 +02:00
Aaron Wagener
dfc347cd24 Core: add options to the list of valid names instead of deleting game weights (#3381) 2024-05-27 23:52:23 +02:00
Fabian Dill
74aa4eca9d MultiServer: make !hint prefer early sphere (#2862) 2024-05-27 18:43:25 +02:00
Justus Lind
df877a9254 Muse Dash: 4.4.0 (#3395) 2024-05-27 02:27:43 +02:00
Bryce Wilson
70d97a0eb4 BizHawkClient: Add suggestion when no handler is found (#3375) 2024-05-27 02:27:04 +02:00
black-sliver
f249c36f8b Setup: pin cx_freeze to 7.0.0 (#3406)
7.1.0 is broken on Linux when using pygobject, which we use as optional dependency for kivy.
2024-05-26 21:22:40 +02:00
NewSoupVi
61e88526cf Core: Rename "count_exclusive" methods to "count_unique" (#3386)
* rename exclusive to unique

* lint

* group as well
2024-05-25 13:14:13 +02:00
Exempt-Medic
18390ecc09 Witness: Fix option description (#3396)
* Fixing description

* Another mistake
2024-05-24 19:32:23 +02:00
Aaron Wagener
8045c8717c Webhost: Allow Option Groups to specify whether they start collapsed (#3370)
* allow option groups to specify whether they should be hidden or not

* allow worlds to override whether game options starts collapsed

* remove Game Options assert so the visibility of that group can be changed

* if "Game Options" or "Item & Location Options" groups are specified, fix casing

* don't allow item & location options to have duplicates of the auto added options

* use a generator instead of a comprehension

* use consistent naming
2024-05-24 01:18:21 -04:00
NewSoupVi
613e76689e CODEOWNERS: Actually link the correct person for Yu Gi Oh (#3389) 2024-05-23 20:25:41 -05:00
Exempt-Medic
2a47f03e72 Docs: Update trigger guide and advanced yaml guide (#3385)
* I guess these don't exist anymore

* Update worlds/generic/docs/advanced_settings_en.md

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

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-05-23 20:36:45 -04:00
Aaron Wagener
8b992cbf00 Webhost: Disallow empty option groups (#3369)
* move item_and_loc_options out of the meta class and into the Options module

* don't allow empty world specified option groups

* reuse option_group generation code instead of rewriting it

* delete the default group if it's empty

* indent
2024-05-23 18:50:40 -04:00
NewSoupVi
8fae75f577 Update Generate.py 2024-05-24 00:05:01 +02:00
NewSoupVi
8b8a95089c Update Generate.py 2024-05-24 00:04:46 +02:00
NewSoupVi
1e99625f3b Add an option docstring to roll_settings to hopefully prevent the weights fiasco from being repeated 2024-05-24 00:03:25 +02:00
Doug Hoskisson
d09b214309 Core: Utils.py typing (#3064)
* Core: Utils.py typing

`get_fuzzy_results` typing

There are places that this is called with a `word_list` that is not a `Sequence`, and it is valid (e.g., `set` or `dict`).

To decide the right type, we look at how `word_list` is used:
- the parameter to `len` - requires `__len__`
- the 2nd parameter to `map` - requires `__iter__`

Then we look at https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes and ask what is the simplest type that includes both `__len__` and `__iter__`: `Collection`

(Python 3.8 requires using the alias in `typing`, instead of `collections.abc`)

* a bit more typing and cleaning

* fine, take away my fun for something that no one is ever going to see anyway...

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-05-23 19:55:45 +02:00
Fabian Dill
860ab10b0b Generate: remove tag "-" (#3036)
* Generate: introduce Remove, similar to Merge

* make + dict behave as + for each value


---------

Co-authored-by: Zach Parks <zach@alliware.com>
2024-05-23 15:03:21 +02:00
CookieCat
3f8c348a49 AHIT: Fix Your Contract has Expired being placed on the first level when it shouldn't (#3379) 2024-05-23 09:49:17 +02:00
Zach Parks
e1ff5073b5 WebHost, Core: Move item and location descriptions to WebWorld responsibilities. (#2508)
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2024-05-23 02:08:08 -05:00
Star Rauchenberger
8b6eae0a14 Lingo: Add option groups (#3352)
* Lingo: Add option groups

* Touched up option docstrings
2024-05-23 02:22:39 +02:00
agilbert1412
89d0dae299 Stardew valley: Create Option Groups (#3376)
* - Fix link in Stardew Setup Guide

* - Create option groups for Stardew Valley

* - Cleaned up the imports

* - Fixed double quotes and trailing comma

* - Improve order in the multipliers category
2024-05-23 02:22:28 +02:00
LiquidCat64
56d01f3913 CV64: Add option groups (#3360)
* Add the option groups.

* Get rid of all mid-sentence line breaks.
2024-05-23 02:16:13 +02:00
Scipio Wright
a43e294786 TUNIC: Add option presets (#3377)
* Add option presets

* why the hell is there an s here

* entrance rando yes
2024-05-23 02:12:59 +02:00
Justus Lind
92392c0e65 Update Song List to Muse Dash 4.3.0 (#3216) 2024-05-23 02:11:27 +02:00
Star Rauchenberger
893a157b23 Lingo: Minor logic fixes (part 2) (#3250)
* Lingo: Minor logic fixes (part 2)

* Update the datafile

* Renamed Fearless Mastery

* Move Rhyme Room LEAP into upper room

* Rename Artistic achievement location

* Fix broken wondrous painting

* Added a test for the Wondrous painting thing
2024-05-23 02:09:52 +02:00
Scipio Wright
02d3fdf2a6 Update options to look better on webhost after update, also give death link a description (#3329) 2024-05-23 02:05:21 +02:00
Bryce Wilson
cd160842ba BizHawkClient: Linting/style (#3335) 2024-05-23 02:03:42 +02:00
Bryce Wilson
93f63a3e31 Pokemon Emerald: Fix broken Markdown in spanish setup guide (#3320)
* Pokemon Emerald: Fix broken Markdown in spanish setup guide

* Pokemon Emerald: Minor formatting in spanish setup guide

* oops
2024-05-23 02:01:27 +02:00
Scipio Wright
b4fec93c82 Update guide with some linux instructions (#3330) 2024-05-23 02:00:06 +02:00
Exempt-Medic
1ae0a9b76f WebHost: Fixing default values for LocationSets (#3374)
* Update macros.html

* Update macros.html
2024-05-23 01:58:06 +02:00
Fabian Dill
0ea20f3929 Core: add panic_method setting (#3261) 2024-05-22 14:02:18 +02:00
PoryGone
20134d3b1e Celeste 64: Option Groups (#3321)
* Celeste 64: Option Groups

* Retarget OptionGroup import
2024-05-21 18:22:39 -04:00
PoryGone
a1c2e8715e DKC3: Option Groups (#3322)
* DKC3: Option Groups

* Retarget OptionGroup import
2024-05-21 18:19:37 -04:00
NewSoupVi
61be79b7ea The Witness: Option Groups & Tooltip formatting (#3342)
* Add option groups

* Option tooltip formatting

* eof

* reindent, apparently I'm stupid

* lint

* oops indent
2024-05-21 18:17:12 -04:00
Scipio Wright
e7544d835c TUNIC: Add option groups, fix option descriptions (#3344)
* Add option groups, fix up option descriptions

* Change sword progression description back

* Add missed newline change, missed space after asterisk
2024-05-21 18:12:52 -04:00
PoryGone
62e68ba1cc SMW: Option Groups and Presets (#3345)
* SMW: Add Option Groups and Presets

* Fix Boss Shuffle Preset

* Tooltip formatting
2024-05-21 18:09:05 -04:00
Trevor L
9441cc31b7 Hylics 2: Update option docstrings (#3359)
* Update Options.py

* "pool" -> "item pool"
2024-05-21 17:53:00 -04:00
PoryGone
5c66681233 SA2B: Option Groups and Dataclass (#3357)
* Merge Conflicts

* SA2B: Option Groups

* Re-add erroneously-removed import
2024-05-21 17:48:23 -04:00
Trevor L
92b1f3cd19 Bomb Rush Cyberfunk: Update option docstrings (#3358)
* Update Options.py

* Update Options.py
2024-05-21 17:31:01 -04:00
jamesbrq
514ad69f44 Remove logging from validate_rom (#3362) 2024-05-21 20:57:59 +02:00
Fabian Dill
461f5db35a Customserver: only save on exit if it's in a good state. (#3351) 2024-05-21 14:08:59 +02:00
CookieCat
fe7bc8784d A Hat in Time: Implement New Game (#2640)
Adds A Hat in Time as a supported game in Archipelago.
2024-05-20 09:04:06 +02:00
Louis M
c792ae76ca Aquaria: Adding Aquaria to README and some other minors changes (#3313) 2024-05-20 08:58:44 +02:00
Chris Wilson
bfe215d5a7 Use world.web.options_presets directly instead of creating an empty dict first (#3348) 2024-05-20 01:57:07 -04:00
Chris Wilson
5910b94deb Update options pages macros to respect valid_keys for item and location options (#3347) 2024-05-20 00:26:42 -04:00
Fabian Dill
14ffd1c70c Subnautica: fix use of _valid_keys were valid_keys should be used. (#3346)
* Subnautica: fix use of _valid_keys were valid_keys should be used.

* Update Options.py
2024-05-20 00:20:01 -04:00
Scipio Wright
754fc11c1b TUNIC: ER Refactor for better plando connections, fewer shops improvement (#3075)
* Fixed shop changes

* Update option description

* Apply suggestions from Vi's review (thank you)

* Fix for plando connections on a full scene

* Plando connections should work better now for complicated paths

* Even more good plando connections yes

* Starting to move the info over

* Fixing up formatting a bit

* Remove unneeded item info

* Put in updated_reachable_regions, to replace add_dependent_regions

* Updated to match ladder shuffle

* More stuff I guess

* It functions!

* It mostly works with plando now, some slight issues still

* Fixed minor logic bug

* Fixed world leakage

* Change exception message

* Make exception message better for troubleshooting failed connections

* Merged with main

* technically a logic fix but it would never matter cause no start shuffle

* Add a couple more alias item groups cause yeah

* Rename beneath the vault front -> beneath the vault main

* Flip lantern access rule to the region

* Add missing connection to traversal reqs

* Move start_inventory_from_pool to the top so that it's next to start_inventory

* Reword the fixed shop description slightly

* Refactor per ixrec's comments

* Greatly reduced an overcomplicated block because Vi is cool and smart and also cool

* Rewrite traversal reqs thing per Vi's comments
2024-05-20 01:01:24 +02:00
Star Rauchenberger
12cde88f95 Lingo: Fixed edge case sunwarp shuffle accessibility issue (#3228)
* Lingo: Fixed edge case sunwarp shuffle accessibility issue

* Minor readability update

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-20 00:56:24 +02:00
Alchav
e0b6889634 ALTTP: Second attempt to fix Swamp Palace boss logic (#3315) 2024-05-19 22:18:41 +02:00
black-sliver
14321d6ba2 Factorio: update factorio-rcon (#3198)
2.1.1 didn't work with py3.8, 2.1.2 fixes that
2024-05-19 20:41:18 +02:00
black-sliver
e978109410 WebHost: properly stop worker threads (#3340)
* WebHost: properly stop worker threads

* Less jank

* Forgot the try-catch around the while true
2024-05-19 20:40:36 +02:00
black-sliver
019dfb8242 CustomServer: re-add missing Archipelago to data package (#3341) 2024-05-19 20:40:08 +02:00
Doug Hoskisson
8e9a050889 Zillion: "item counts" OptionGroup (#3338) 2024-05-19 14:36:47 -04:00
Fabian Dill
2801e21296 WebHost: fixup WebHostLib/options.py (#3332)
* WebHost: fixup WebHostLib/options.py

* Update WebHostLib/options.py

* Update WebHostLib/options.py

* fix visibility flag handling
2024-05-19 14:21:46 -04:00
Fabian Dill
e97eddcdaf WebHost: move atexit saving to end of room hosting function (#3339) 2024-05-19 18:25:56 +02:00
Fabian Dill
d3f4ee4994 WebHost: re-introduce per-Room Locker (#3337) 2024-05-19 16:31:35 +02:00
black-sliver
cf34f125d6 CustomServer: don't mutate static server data (#3334)
when switching to multiple rooms per process, we ended up modifying the static server data
because that's how _load works and the data is now shared between multiple rooms.
2024-05-19 15:32:11 +02:00
Fabian Dill
663b50b33e WebHost: fix AutoLauncher restarting rooms due to race condition (#3333) 2024-05-19 15:17:55 +02:00
Doug Hoskisson
230a9e620b Core: move OptionGroup definition to Options.py (#3325) 2024-05-19 04:40:41 +02:00
Silvris
1b6fb7b090 Tests: test that no worlds fail to load (#3318)
* test that no worlds fail to load

* pep8

* Update test_implemented.py

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-19 00:42:58 +02:00
jamesbrq
0e893889c7 MLSS: General bugfixes + Add patch extension to inno_setup.iss (#3286)
* Remove outdated header change for ROM verification

* Update Connections to be compatible with python ver. 3.8

* Update inno_setup.iss

* Update inno_setup.iss
2024-05-18 22:26:50 +02:00
Rensen3
2bc345504e YGO06: make sure it runs on 3.8 support (#3324)
* YGO06: make sure it runs on python 3.8

* YGO06: change merge of dict, so it runs on python 3.8
2024-05-18 13:53:17 +02:00
Chris Wilson
5e3c5dedf3 WebHost: Massive overhaul of options pages (#2614)
* Implement support for option groups. WebHost options pages still need to be updated.

* Remove debug output

* In-progress conversion of player-options to Jinja rendering

* Support "Randomize" button without JS, transpile SCSS to CSS, include map file for later editors

* Un-alphabetize options, add default group name for item/location Option classes, implement more option types

* Re-flow UI generation to avoid printing rows with unsupported or invalid option types, add support for TextChoice options

* Support all remaining option types

* Rendering improvements and CSS fixes for prettiness

* Wrap options in a form, update button styles, fix labels, disable inputs where the default is random, nuke the JS

* Minor CSS tweaks, as recommended by the designer

* Hide JS-required elements in noscript tag. Add JS reactivity to range, named-range, and randomize buttons.

* Fix labels, add JS handling for TextChoice

* Make option groups collapsable

* PEP8 current option_groups progress (#2604)

* Make the python more PEP8 and remove unneeded imports

* remove LocationSet from `Item & Location Options` group

* It's ugly, but YAML generation is working

* Stop generating JSON files for player-options pages

* Do not include ItemDict entries whose values are zero

* Properly format yaml output

* Save options when form is submitted, load options on page load

* Fix options being omitted from the page if a group has an even number of options

* Implement generate-game, escape option descriptions

* Fix "randomize" checkboxes not properly setting YAML options to "random"

* Add a separator between item/location groups and items/locations in their respective lists

* Implement option presets

* Fix docs to detail what actually ended up happening

* implement option groups on webworld to allow dev sorting (#2616)

* Force extremely long item/location/option names with no spaces to text-wrap

* Fix "randomize" button being too wide in single-column display, change page header to include game name

* Update preset select to read "custom" when updating form inputs. Show error message if the user doesn't input a name

* Un-break weighted-options, add option group names to weighted options

* Nuke weighted-options. Set up framework to rebuild it in Jinja.

* Generate styles with scss, remove styles which will be replaced, add placeholders for worlds

* Support Toggle, DefaultOnToggle, and Choice options in weighted-options

* Implement expand/collapse without JS for worlds and option groups

* Properly style set options

* Implement Range and NamedRange. Also, CSS is hard.

* Add support for remaining option types. JS and backend still forthcoming.

* Add JS functionality for collapsing game divs, populating span values on range updates. Add <noscript> tag to warn users with JS disabled.

* Support showing/hiding game divs based on range value for game

* Add support for adding/deleting range rows

* Save settings to localStorage on form submission

* Save deleted options on form submission

* Break weighted-options into a per-game page.

- Break weighted-options into a per-game page
- Add "advanced options" links to supported games page
- Use details/summary tags on supported games, player-options, and weighted-options
- Fix bug preventing previously deleted rows from being removed on page load if JS is enabled
- Move route handling for options pages to options.py
- Remove world handling from weighted-options

* Implement loading previous settings from localStorage on page load if JS is enabled

* Weighted options can now generate YAML files and single-player games

* options pages now respect option visibility settings for simple and complex pages

* Remove `/weighted-settings` redirect, fix weighted-options link on player-options page

* Fix instance of AutoWorld not having access to proper `random`

* Catch instances of frozenset along with set

* Restore word-wrap in tooltips

* Fix word wrap in player-options labels

* Add `dedent` filter to help with formatting tooltips in player-options

* Do not change the ordering of keys when printing yaml files

* Move necessary import out of conditional statement

* Expand only the first option group by default on both options pages

* Respect option visibility when generating yaml template files

* Swap to double quotes

* Replace instances of `/weighted-settings` with `/weighted-options`, swap out incomplete links

* Strip newlines and spaces after applying dedent filter

* Fix documentation for option groups

* Update site map

* Update various docs

* Sort OptionSet lists alphabetically

* Minor style tweak

* Fix extremely long text overflowing tooltips

* Convert player-options to use CSS grid instead of tables

* Do not display link to weighted-options page on supported games if the options page is an external link

* Update worlds/AutoWorld.py

Bugfix by @alwaysintreble

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

* Fix NamedRange options not being properly set if a preset it loaded

* Move option-presets route into options.py

* Include preset name in YAML if not "default" and not "custom"

* Removed macros for PlandoBosses and DefaultOnToggle, as they were handled by their parent classes

* Fix not disabling custom inputs when the randomize button is clicked

* Only sort OptionList and OptionSet valid_keys if they are unordered

* Quick style fixes for player-settings to give `select` elements `text-overflow: ellipsis` and increase base size of left-column

* Prevent showing a horizontal scroll bar on player-options if the browser width was beneath a certain threshold

* Fix a bug in weighted-options which prevented inputting a negative value for new range inputs

---------

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2024-05-18 00:11:57 -04:00
NewSoupVi
5fb0126754 Core: Player name property on world class (#3042)
* player property on world class

* Remove dat shi from overcooked

* Update worlds/AutoWorld.py

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

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2024-05-18 00:18:57 +02:00
Rensen3
b4c263fc9d YGO06: add new game yugioh06 to CODEOWNERS inno_setup and readme (#3316) 2024-05-18 00:09:03 +02:00
Bryce Wilson
013862b068 Pokemon Emerald: Update changelog (#3317)
* Pokemon Emerald: Update changelog

* Pokemon Emerald: Fix spelling error in changelog

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

---------

Co-authored-by: Remy Jette <remy@remyjette.com>
2024-05-18 00:06:30 +02:00
Doug Hoskisson
280b67f996 some worlds: some typing in LocalRom (#3090)
* some worlds: some typing in `LocalRom`

### `read_bytes`

It's not safe to return `bytearray` when we think it's `bytes`
```python
a = rom.read_bytes(8, 3)
hash(a)  # This won't crash, right?
```

### `write_bytes`

`Iterable[SupportsIndex]` is what's required for `bytearray.__setitem__(slice, values)`
We need to add `__len__` for the `len(values)` in this function.

* remove `object` inheritance
2024-05-17 21:41:57 +02:00
NewSoupVi
9ae7083bfc Fix Monastery Entry RIght righqeuotghqeougtfgas (#3213) 2024-05-17 19:29:55 +02:00
NewSoupVi
bd18018852 The Witness: Fix Mountain Floor 2 Near Row 5 Symbol Requirement (#3212) 2024-05-17 19:29:46 +02:00
Exempt-Medic
b4b79bcd78 BRCF: Small Fixes (#3314)
* Plural fix

* Update link
2024-05-17 19:24:32 +02:00
Rensen3
539ee1c5da Yu-Gi-oh! 2006: implement new game (#2795)
* Initial implementation of Yu-Gi-Oh! WC 2006

* Added Opponents and banlists

* Initial implementation of Yu-Gi-Oh! WC 2006

* Added Opponents and banlists

* Added Campaign Logic

* Added Bonuses Logic

* Added challenge logic

* fixed yugioh client

* ygo06 rom cleanup and include lua

* ygo06 patch cleanup

* ygo06 move client to world folder

* lots of small changes

* bug fixes

* implemented filler item for yugioh06

* BizHawkClient: Add client and connector

* BizHawkClient: Add launcher component and inno_setup lines

* BizHawkClient: Misc stability updates and small improvements

Bad commit organization a consequence of working with two different branches and not keeping the commits separated

* BizHawkClient: Add docstrings

* BizHawkClient: Pull in changes from other branch

* BizHawkClient: Fix no handler message not displaying after changed ROMs

* BizHawkClient: Remove extra print statement from lua

* BizHawkClient: Change version command to use raw strings

* BizHawkClient: Change script version to single integer

* YGO06: added logic for "all expect type forbidden" limited duels

* YGO06: Structure Deck choice now affects logic. Fixed a bug with tier 5 campaign opponents. Added logic for TD16 Union.

* BizHawkClient: Add newline to version for lua script

* BizHawkClient: Call send_connect from BizHawkClient's watcher loop

* BizHawkClient: Add handling for failed request getting script version

* BizHawkClient: Have base64.lua check lua version explicitly for bit operations

On 2.9, it would detect LuaJIT and flood the console with deprecation warnings

* BizHawkClient: Update connector script for slightly better errors and address Gambatte frame sync issue

* BizHawkClient: Remove accidentally added print statements

* BizHawkClient: Fix connector server not closing correctly

* BizHawkClient: Move some connector code around, some linting

* BizHawkClient: Small cleanup in lua

* BizHawkClient: Lua linting

* BizHawkClient: Remove outdated sentences in docstrings

* YGO06: Logic additions and bug fixes

* BizHawkClient: Correctly null check patch file arg

* BizHawkClient: Initialize logging

* BizHawkClient: Move code to worlds/_bizhawk

Also splits out BizHawk communication functions to their own file for use outside this client

* BizHawkClient: Add license to connector lua, add types to docs

* BizHawkClient: Add module docstrings

* YGO06: Logic additions

* BizHawkClient: Allow clients to define multiple systems

* BizHawkClient: Better logging and handling of interruptions to connection to script

* YGO06: Logic additions

* YGO06: Added text to options

* YGO06: Ported to bizhawk client

* YGO06: fix goal not being detected

* YGO06: fix access item rule for tier 5 column 1 and 2

* YGO06: docu and bug fixes

* YGO06: change name

* YGO06: some fixes

* YGO06: fix starting opponent and booster not applying

* YGO06: added option to reduce the amount of challenges and remove the no ban list from pool.

* YGO06: added rom being asked for on first use

* YGO06: fix rules for challenges

* YGO06: create proper rules for TD04 Ritual Summon

* YGO06: mark most banlists as usefull instead of progression

* YGO06: reduce the required core boosters across the board

* YGO06: fix client not loading if another game already loaded the bizhawk client

* YGO06: fix client not finding the bizhawk client.

* YGO06: fix TD08 Draw not giving out an item

* YGO06: small text changes

* YGO06: update to version 0.4.4

* YGO06: logic mixin clean-up

* YGO06: added option for campaign opponents as goal

* Pokemon Emerald add encounter table randomization

* Pokemon Emerald: Item ball randomization working

* Pokemon Emerald: Clean up code a little

* Pokemon Emerald: Partial rework of region/location creation

* Pokemon Emerald: Dedupe items and add more readable names

* Refactor region creation to manually defined regions

* Split region json

* Use new data.json with flattened constants and add HM locations

* YGO06: bug fixes

* YGO06: bug fix

* YGO06: changes default options to be more beginner friendly

* YGO06: attempt at universal tracker support. Settings are stored in slot data now.

* YGO06: fix for older python versions

* YGO06: fix slot data

* YGO06: added diiferent opponents to the campaign

* YGO06: fix small bug with opponent icons

* YGO06: fix unwanted changes

* YGO06: repair merge with main

* YGO06: map out all of the opponents

* YGO06: added opponent shuffle

* YGO06: added logic to opponent shuffle

* YGO06: added option to use ocg art

* YGO06: bug_fixes

* YGO06: removed todos, since they are not needed anymore

* YGO06: added draft mode

* YGO06: added logic to draft mode

* YGO06: Added Money multiplier when you lose

* YGO06: Fixed Unit Test errors

* YGO06: Added Random deck option

* YGO06: Bug fix with registering client

* YGO06: client clean-up

* YGO06: fixed card misspellings

* YGO06: removed unused imports and other small changes

* YGO06: small changes

* YGO06: fix generation error when the combination of starting with "No Banlist" and not adding "No Banlist" to the pool is selected

* YGO06: fix ocg art path overwriting Huge Revolution bugfix

* YGO06: added comments and other minor changes

* YGO06: fixed byte length in client for money

* YGO06: fixes for webhost and options

* YGO06: use the proper random function

* YGO06: change settings to options

* YGO06: move to procedure patch

* YGO06: fix imports

* YGO06: fix download link for patch not showing

* YGO06: remove unnecessary Optional

* YGO06: fix universal tracker stuff

* YGO06: add typings

* YGO06: small cleanup

* yugioh06:  small change to setup

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

* YGO06: remove logic mixin

* YGO06: fix create item and implement create filler and get filler item name

* YGO06: remove double lambdas

* YGO06: use pkgutil.get_data instaed pf zipFile

* YGO06: fix starting items being duplicated

* YGO06: lots of small changes

* YGO06: moved functions to match execution order

* YGO06: run ruff

* YGO06: run ruff format

* YGO06: fix ruff errors

* YGO06: undo ruff format for rules

* YGO06: move import to prevent circular dependency

* YGO06: remove unused class

* YGO06: optimizing rules

* YGO06: some optimization and small bug fix

---------

Co-authored-by: Zunawe <gyroscope15@gmail.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-05-17 19:23:05 +02:00
Exempt-Medic
5fb1d0f98a FF1: Switching Options System (#3302) 2024-05-17 19:19:55 +02:00
Louis M
89a2a3c35b Aquaria: implement new game (#3197)
This is a new world for the Aquaria game (https://www.bit-blot.com/aquaria/).
2024-05-17 12:29:00 +02:00
Fabian Dill
7900e4c9a4 WebHost: use a limited process pool to run Rooms (#3214) 2024-05-17 12:21:01 +02:00
Fabian Dill
3dbdd048cd Core: prevent "Could not find identify Component responsible for None" from being logged. (#3225) 2024-05-17 12:19:41 +02:00
Trevor L
68323b46a9 Bomb Rush Cyberfunk: Implement new game (#2925)
Adds Team Reptile's Bomb Rush Cyberfunk as a new game.
2024-05-17 12:13:40 +02:00
Aaron Wagener
2447be92d8 The Messenger: fix generation failure for no portal shuffle with 3 available portals (#3200) 2024-05-17 10:18:50 +02:00
NewSoupVi
88dd27eb3a The Witness: Use OptionError (#3258)
* Use OptionError

* ruff
2024-05-17 10:07:38 +02:00
Exempt-Medic
6d8ac5d054 Core: Remove deprecated get_current_option_name and SpecialRange (#3296)
* Removing deprecated function

* Removing SpecialRange
2024-05-17 10:02:25 +02:00
Exempt-Medic
5a2d839412 Removing deprecated item_count (#3309) 2024-05-17 09:54:57 +02:00
Doug Hoskisson
4bd4a2c541 Docs: remove obsolete yaml generation info (#3304)
* Docs: remove obsolete yaml generation info

This line was added when we didn't have the "Generate Template Options" button in the launcher.

* add information about `Launcher.py`
2024-05-17 01:26:43 +02:00
FlySniper
705cb2e816 Wargroove: Switched to options API. (#3306)
* Wargroove: Switched to options API.

* Update Options.py

* Update __init__.py

* Options is plural

* Wargroove: Options updates with some small fixes.

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-05-16 18:46:13 +02:00
NewSoupVi
467bbd7754 WebHost: Fix setup guide link not working for games with special characters (#3269)
* WebHost: Fix setup guide link not working for games with special characters

* use url_for with _anchor (#3279)
2024-05-15 21:40:40 -04:00
LiquidCat64
4da9cdd91c CV64: Fix items with weird characters landing on Renon's shop crashing (#3305) 2024-05-15 23:50:04 +02:00
chandler05
6576b069f2 Hylics 2: Remove Random Start option and replace it with Start Location option (#3289)
* Hylics 2: Remove Random Start option and replace it with Start Location option

* remove choice

* Readd random start to slot data

* newlines

* Add random_start as a Removed option
2024-05-14 20:35:32 +02:00
Scipio Wright
b78781ab3e Docs: Update advanced yaml guide wording for priority locations (#3298)
* Update advanced yaml guide wording

* Update options api as well

* Update exclude locations description slightly to use more current verbiage

* Update priority locations in options api.md to note what happens if it runs out

* Remove auto-added bullet points

* Slightly mess with wording to make it more succinct
2024-05-14 20:28:15 +02:00
Aaron Wagener
9a82edc931 World: remove ClassVar typing from topology_present (#3294) 2024-05-14 04:35:33 +02:00
Doug Hoskisson
77cce68c08 Zillion: remove deprecated Logger.warn (#3295) 2024-05-13 20:31:15 +02:00
Exempt-Medic
f38655d6b6 Bumper Stickers and Meritous: Options and world: multiworld fixes (#3281)
* Update Options.py

* Update __init__.py

* Correct case

* Correct case

* Update Meritous and actually use Options

* Oops

* Fixing world: multiworld
2024-05-12 18:52:34 +02:00
Exempt-Medic
701fbab837 Core: World: MultiWorld and another deprecated option getter (#3254)
* world: multiworld and deprecated options getting

* Oops

* Found two more
2024-05-12 18:51:20 +02:00
Aaron Wagener
af83050b75 Core: log warning for unknown options (#1385)
* throw an error for unknown options

* move the error to the end of trigger resolution and make trigger names valid

* add bad hardcoded stuff for LTTP

* use itertools.chain instead of a ChainMap

* remove accidental unused import

* make the check after both trigger resolutions so no valid keys are missed, and only check relevant game.

* log a warning instead of crashing

* delete options from the weights once it gets registered for cleaner erroring

* grammar hard

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

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-05-10 23:00:13 +02:00
Exempt-Medic
8db3e40094 Removing old option getters (#3285) 2024-05-10 16:29:07 +02:00
Trevor L
d48f2ab1b4 Core: Add list/item group exclusive methods to CollectionState (#3192)
* Group exclusive methods

* Add docstrings

* Apply suggestions from code review

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

* Put lines back with no whitespace

* Add list methods

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-08 18:34:32 +02:00
Bryce Wilson
0f1b16d640 Pokemon Emerald: Change Lilycove access logic (#3277)
* Pokemon Emerald: Change logical access to lilycove from east

* Pokemon Emerald: Add tests
2024-05-08 18:26:13 +02:00
Bryce Wilson
76962b8b3b Pokemon Emerald: Fix incorrect access to slateport water encounters (#3243) 2024-05-07 12:43:35 +02:00
NewSoupVi
e04db57dce Core: Add has_list and count_list and improve (?) count_group (#2934)
* Add has_list and count_list and improve (?) count_group

* MESSENGER STOP

* Add docstrings to has_list and count_list

* Add docstrings for has_group and count_group as well

* oops

* Rename to has_from

* docstrings improvement again

* Docstring
2024-05-07 09:23:25 +02:00
digiholic
12b8fef1aa Adds a canary byte check before sending game completion (#3217) 2024-05-07 09:22:11 +02:00
NewSoupVi
0ac8844f6f Core: Add "has_all_counts" and "has_any_count" functions to CollectionState (#2933)
* Add has_all_counts and has_any_counts

* Messenger gave me a red x and I'm mad about it

* Update BaseClasses.py

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

* Update BaseClasses.py

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

* Mapping instead of Dict

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-05-07 08:15:09 +02:00
black-sliver
23eca7d747 SoE: Docs: rework some styling (#3268)
* Docs: SoE: move header and fix header level

* Docs: SoE: be pedantic for required software
2024-05-06 10:55:25 +02:00
Alchav
1a563a14fc LTTP: Yet more LTTP logic fixes (#3270) 2024-05-06 09:36:08 +02:00
jamesbrq
5935093615 Mario & Luigi: Superstar Saga: Implement New Game (#2754)
* Commit for PR

* Commit for PR

* Update worlds/mlss/Client.py

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>

* Update worlds/mlss/__init__.py

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>

* Update worlds/mlss/__init__.py

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>

* Update worlds/mlss/docs/setup_en.md

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>

* Remove deprecated import. Updated settings and romfile syntax

* Updated Options to new system. Changed all references from MultiWorld to World

* Changed switch statements to if else

* Update en_Mario & Luigi Superstar Saga.md

* Updated client.py

* Update Client.py

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Updated logic, Updated patch implementation, Removed unused imports, Cleaned up Code

* Update __init__.py

* Changed reference from world to mlssworld

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Fix merge conflict + update prep

* v1.2

* Leftover print commands

* Update basepatch.bsdiff

* Update basepatch.bsdiff

* v1.3

* Update Rom.py

* Change tracker locations to serverside, no longer locations. Various code cleanup and logic changes.

* Event removal continuation.

* Partial Implementation of APPP (Incomplete))

* v1.4 Implemented APPP

* Docs Updated

* Update Rom.py

* Update setup_en.md

* Update Rom.py

* Update Rules.py

* Fix for APPP being broken on webhost

* Update Rom.py

* Update Rom.py

* Location name fixes + pants color fixes

* Update Rules.py

* Fix for ultra hammer cutscene

* Fixed compat. issues with python ver. 3.8

* Updated hidden block yaml option

* pre-v1.5

* Update Client.py

* Update basepatch.bsdiff

* v1.5

* Update XP multiplier to have a minimum of 0

* Update 'Beanfruit' to 'Bean Fruit'

* v1.6

* Update Rom.py

* Update basepatch.bsdiff

* Initial review refactor

* Revert state logic changes. Continuation of refactor.

* Fixed failed generations. Finished refactor.

* Reworked colors. Removed all .txt files

* Actually removed the .txt files this time

* Update Rom.py

* Update README.md

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

* Update worlds/mlss/Options.py

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

* Update worlds/mlss/Client.py

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

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Update worlds/mlss/__init__.py

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

* Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md

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

* Update worlds/mlss/Data.py

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

* Review refactor.

* Update README.md

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

* Update worlds/mlss/Rules.py

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

* Add coin blocks to LocationName

* Refactor.

* Update Items.py

* Delete mlss.apworld

* Small asm bugfix

* Update basepatch.bsdiff

* Client sends less messages to server

* Update basepatch.bsdiff

---------

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-05-06 09:15:06 +02:00
Fabian Dill
2aa3ef372d WebHost: use redirect for /room form submission (#3271) 2024-05-05 22:59:51 +02:00
Bryce Wilson
d94cf8dcb2 Pokemon Emerald: Add event ticket locations to client data store flags (#3177)
* Pokemon Emerald: Add event ticket locations to client data store flags

* Pokemon Emerald: Add regi doors event flag

* Pokemon Emerald: Add more tracker flags
2024-05-05 10:46:11 +02:00
PoryGone
5fae1c087e Celeste 64: v1.2 Content Update (#3210)
* Cleanup and new option support

* Handle new locations

* Support higher Strawberry counts

* Don't add start inventory items to the pool

* Support Move Shuffle functionality and items

* Hard and Move Shuffle Logic

* Fix Options

* Update CHANGELOG.md

* Add standard moves logic for signs 3 and 4

* Fix Option Tooltip

* Add tracker link to setup guide

* Fix unit test

* Fix option tooltips

* Missing Space

* Move option checking out of rule function

* Delete just_gen500.bat
2024-05-05 08:58:49 +02:00
Bryce Wilson
7e61211365 Pokemon Emerald: Convert to procedure patch (#2995)
* Pokemon Emerald: Convert to procedure patch

* Pokemon Emerald: Remove assertion for vanilla rom's existence

* Pokemon Emerald: Add APPP implication to changelog

* Pokemon Emerald: Move procedure patch changelog line to new version

* Pokemon Emerald: Modify changelog versions

* Pokemon Emerald: Fix patch file download not appearing

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-05 07:08:24 +02:00
Bryce Wilson
7603b4a88f Pokemon Emerald: Change dexsanity to not create locations for blacklisted wilds (#3056) 2024-05-04 21:44:38 +02:00
Aaron Wagener
005fc4e864 Fill: allow for single player fill restrictive placement and sweeping (#2415)
* Core: allow for single player state sweeping

* Fill: have distribute items use single_player fill when it can

* oop

* pass locations to sweep_for_events instead of the player

* finally found the diff that was breaking swap

* LTTP fills everyone's dungeons at once, not just a single player's

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-04 12:42:36 +02:00
Star Rauchenberger
28262a31b8 Lingo: Started using OptionError (#3251) 2024-05-04 08:40:17 +02:00
Bryce Wilson
660b068f5a Pokemon Emerald: Use OptionError (#3264) 2024-05-04 08:38:24 +02:00
Thorsten Horberth
879c3407d8 Yoshi's World: Fixed minor logic inconsistincy in Rules.py (#3241)
* Fixed Logic in Rules.py

As of easy logic of this goal is
    set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Stars", player), lambda state: logic.has_midring(state) or (state.has("Tulip", player) and logic.cansee_clouds(state)))
normal logic shouldn't need any collectable.

* Corrected Logic Rules.py
2024-05-04 04:29:12 +02:00
Scipio Wright
d5683c4326 Core: Make output when hinting something with multiple copies show up in a better order (#3245)
* Make the hint info show up in a better order

* Change how old_hints is modified/done

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-04 04:28:09 +02:00
Fabian Dill
f27d1d635b SNIClient: restore old operands header (#3242) 2024-05-03 22:00:05 +02:00
Nicholas Saylor
298c9fc159 Fixed typo and odd capitalization (#3233) 2024-05-03 12:23:08 +02:00
Scipio Wright
26188230b7 TUNIC: Better seed groups for Entrance Rando (#2998)
* Update entrance rando description to discuss seed groups

* Starting off, setting up some names

* It lives

* Some preliminary plando connection handling, probably has errors

* Add missed comma

* if -> elif

* I think this is working properly to handle plando connections

* Update comments

* Fix up shop -> shop portal stuff

* Add back comma that got removed for no reason in the ladder PR

* Remove unnecessary if else

* add back the actually necessary if but not the else

* okay they were both necessary

* Update entrance rando description

* blasphemy

Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com>

* Rename other instances of tunc -> tunic

* Update per Vi's review (thank you)

* Fix a not that shouldn't have been

* Rearrange, update per Vi's comments (thank you)

* Fix indent

* Add a .value

* Add .values

* Fix bad comparison

* Add a not that was supposed to be there

* Replace another isinstance

* Revise option description

* Fix per Kaito's comment

Co-authored-by: Kaito Sinclaire <ks@rosenthalcastle.org>

---------

Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com>
Co-authored-by: Kaito Sinclaire <ks@rosenthalcastle.org>
2024-05-03 07:21:27 +02:00
t3hf1gm3nt
b68be7360c [TLOZ]: Remove use of per_slot_randoms (#3255)
We only used it in two spots for randomizing the secret rupee cave values. Uses proper world random now.
2024-05-03 02:56:20 +02:00
t3hf1gm3nt
255e52642e TLOZ: Fix rings classification, so they are actually considered for logic (#3253) 2024-05-02 16:49:39 -05:00
qwint
49862dca1f move godhome events to create_regions with the others to not try and make them non-events when unshuffled is on (#3221) 2024-05-02 15:26:17 +02:00
Star Rauchenberger
0d586a4467 Lingo: Fix broken good item in panelsanity (#3249) 2024-05-02 15:14:30 +02:00
Ziktofel
8c8b29ae92 SC2: For non-campaign order pick one of the hardest missions as goal (#3180)
This allows End Game as the goal even if long campaigns are present
2024-05-02 12:20:57 +02:00
Natalie Weizenbaum
9d478ba2bc Rules: Verify the default values of Options. (#2403)
* Verify the default values of `Option`s.

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

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

* Update Generate.py

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

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-02 12:19:15 +02:00
ken
3cc434cd78 Core: organize files on ingest via alpha, not ascii (#3029)
* organize files on ingest via alpha, not ascii

* Change from lower() to casefold()
2024-05-02 12:14:50 +02:00
Scipio Wright
31a5696526 Noita: Add more location groups, capitalize existing ones (#3141)
* Add location groups for each region

* Capitalize existing location groups

* Capitalize new boss location group names

* Update comment with capitalization

* Capitalize location_type in reigons.py
2024-05-02 12:02:14 +02:00
palex00
7bdf9a643c Updating Poptracker-Pack-Link for Pokémon Emerald as the old one was no longer maintained and did not work with 0.4.5 (#3193)
* Replaced the outdated Tracker Pack with a new one that is also pinned in the Discord channel

* Same change but for Spanish

* Update setup_en.md

* catching the bottom link as well

* See English Setupguide
2024-05-02 11:56:35 +02:00
Scipio Wright
c64c80aac0 TUNIC: Location groups for each area of the game (#3024)
* huzzah, location groups

* scope creep pog

* Apply suggestion to the other spot it is applicable at too

* apply berserker's suggestion

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

* Remove extra location group for shops

* Fire rod for magic wand

* Capitalize itme name groups

* Update docs to capitalize item name groups, remove the little section on aliases

since the aliases bit is really more for someone misremembering the name than anything else, like "fire rod" is because you played a lot of LttP, or Orb instead of Magic Orb is clear.

* Fix rule with item group name

* Capitalization is cool

* Fix merge mistake

* Add Flask group, remove Potions group

* Update docs to detail how to find item and location groups

* Revise per Vi's comment

* Fix test

* fuzzy matching please stop

* Remove test change that was meant for a different branch

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2024-05-02 10:02:59 +02:00
Aaron Wagener
07d9d6165e Tests: Clean up some of the fill test helpers a bit (#2935)
* Tests: Clean up some of the fill test helpers a bit

* fix some formatting

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-02 10:01:59 +02:00
Star Rauchenberger
fc571ba356 Lingo: Expand sphere 1 under restrictive conditions (#3190) 2024-05-02 09:52:16 +02:00
Emily
ea6235e0d9 Core: Improve join/leave messages, add "HintGame" tag (#2859)
* Add better "verbs" on joining msg, and improve leaving msgs

* Add 'HintGame' tag, for projects like BKSudoku/APSudoku/HintMachine

* data in one place instead of 3

* Clean up 'ignore_game' loop to use any() instead

---------

Co-authored-by: beauxq <beauxq@yahoo.com>
2024-05-02 09:38:49 +02:00
Aaron Wagener
6f8b8fc9c9 Options: Add an OptionError to specify bad options caused the failure (#2343)
* Options: Add an OptionError to specify bad options caused the failure

* inherit from ValueError instead of RuntimeError since this error should be thrown via bad input

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-05-02 09:22:50 +02:00
black-sliver
0ed0de3daa Setup: update cx_freeze to 7.x (#3195) 2024-05-01 01:48:32 +02:00
Star Rauchenberger
487a067d10 Lingo: Fix world load on frozen 3.8 (#3220)
* Lingo: Fix world load on frozen 3.8

* Fixed absolute imports in unit test

* Made unpickling safer
2024-04-29 20:38:29 +02:00
Doug Hoskisson
fc4e6adff5 Core: some CommonContext typing (#3227) 2024-04-29 07:26:30 +02:00
t3hf1gm3nt
9cdc90513b [TLOZ]: Dark Rooms and Level 8 Logic Fixes (#3222)
Properly name the Book to Book of Magic in Rules.py so you can actually possibly be expected to use Magical Rod plus Book of Magic to get through dark rooms. No wonder we tend to see candles so early oops.

Also adding a rule that you need candles for access to Level 8 so you aren't required to time a Rod+Book shot against a moblin to burn the bush. Might make this a logic trick or something later.
2024-04-28 01:49:59 +02:00
Alchav
9afe45166c ALTTP: 0.4.6 fixes (#3215)
* Fix randomizer room logic

* Fix Triforce Hunt HUD always present

* Fix Circle of Pots enemy byte

* treasure_hunt_total for Murahdala text
2024-04-28 01:48:59 +02:00
LiquidCat64
9e20fa48e1 CV64: fix import that shouldn't be relative (#3223) 2024-04-28 01:41:30 +02:00
Zach Parks
e76ba928a8 WebHost: Prevent committing data packages with invalid checksums to database and prevent 500 error from invalid zip files. (#3206) 2024-04-26 22:18:12 -04:00
Aaron Wagener
4f1e696243 The Messenger: fix import that shouldn't be relative (#3219) 2024-04-26 21:29:01 +02:00
Fabian Dill
4756c76541 WebHost: remove JSON_AS_ASCII (#3209) 2024-04-24 06:36:35 +02:00
Fabian Dill
2f78860d8c Core/SNIClient/LttP/Factorio: switch to get_settings (#3208) 2024-04-24 06:24:44 +02:00
Scipio Wright
cca9778871 Delete worlds/sc2wol directory (#3202) 2024-04-23 12:58:38 -05:00
Fabian Dill
bb16fe284a Core: make open_filename log that it's asking (#3199) 2024-04-23 19:05:03 +02:00
Fabian Dill
daccb30e3d Factorio: fix client compatibility with Windows 7/Python 3.8 (#3196) 2024-04-22 14:32:31 +02:00
black-sliver
747b48183c Setup: update cx_freeze to latest 6.x and exclude 7.x (#3194)
7.x has a breaking API change for us, so that needs to be resolved separately.
2024-04-22 13:54:35 +02:00
NewSoupVi
e22ac85e15 The Witness: More door renames (#3131) 2024-04-21 11:45:39 -05:00
Kaito Sinclaire
ee69fa6a8c SMZ3: Use correct font tiles for cross-world items in SM (#3095) 2024-04-21 11:44:17 -05:00
Scipio Wright
ec18254e9e TUNIC: Minor edits to game page (#3182)
* Minor edits to game page

* in -> of
2024-04-21 11:39:37 -05:00
Zach Parks
49c0268a84 MultiServer: Fix /alias <name> not removing aliases. (#3186) 2024-04-21 11:37:24 -05:00
Scipio Wright
6aafa6ff04 TUNIC: Fix minimal Heir access in ladder shuffle (#3189) 2024-04-21 17:00:16 +02:00
Silvris
3e27b93c37 SNIClient: set SNESState to SNES_DISCONNECTED when disconnected (#3188) 2024-04-21 16:59:19 +02:00
Fabian Dill
392c47dcef MultiServer: add datastore list command to MultiServer (#3181) 2024-04-21 03:47:01 +02:00
Scipio Wright
442c7d04db Core: Silently fix invalid yaml option for Toggles (#3179) 2024-04-21 03:37:28 +02:00
Chris Wilson
ad4451276d WebHost: Add robots.txt to WebHost (#3157)
* Add a `robots.txt` file to prevent crawlers from scraping the site

* Added `ASSET_RIGHTS` entry to config.yaml to control whether `/robots.txt` is served or not

* Always import robots.py, determine config in route function

* Finish writing a comment

* Remove unnecessary redundant import and config
2024-04-20 20:58:56 -04:00
Silvris
915ad61ecf Core: fix unfilled items "Per-Player Count" (#2661) 2024-04-20 19:57:55 -05:00
Bryce Wilson
5d8aca1b4e Pokemon Emerald: Temporary fix for missing items (#3162) 2024-04-20 19:56:08 -05:00
Aaron Wagener
d4c8083be5 Docs: Mention /check in the advanced yaml guide (#3092)
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2024-04-20 19:49:48 -05:00
SunCat
a3702abe38 CODEOWNERS: Update owner for ChecksFinder (#3089) 2024-04-20 19:49:00 -05:00
Danaël V
5fcc1aa83f Launcher: Adding Unrated mention to the launcher (#3097) 2024-04-20 19:48:14 -05:00
Flore
b8e0d4c4ee DS3: Update the setup docs to be more up to date (#2932)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Co-authored-by: Moonlington <Moonlington@users.noreply.github.com>
2024-04-20 19:45:20 -05:00
Doug Hoskisson
fc18f9caf9 Zillion: Docs: add information to skill option documentation (#3118) 2024-04-20 19:43:13 -05:00
Doug Hoskisson
a50e68acd1 Docs: style: multiline brackets (#3143) 2024-04-20 19:42:27 -05:00
agilbert1412
0624ba5e81 Stardew Valley: Options page documentation improvements (#3155) 2024-04-20 19:41:00 -05:00
Zach Parks
a45fa84382 Factorio: Fix 500 error on Factorio multi-tracker. (#3184)
* Factorio: Fix 500 error on Factorio multi-tracker.

* Hopefully this also fixes the webhost test failures.
2024-04-20 19:39:33 -04:00
Zach Parks
532cff1334 ALTTP: Updates and refactors to multi-tracker and player tracker. (#3183)
* ALTTP: Massive game tracker update.

* Adds dropdowns separated by region for each location and its checked status.
* Adds Bombs for bombless start seeds.
* Adds Triforce Pieces to track.
* Update icon image URLs to match in-game closer.
* Fix issue with grouped progressive items.

* Couple missed points.

* Another edge case with details being refreshed.

* Remove old commented out CSS

* Consolidate a table and move an erroneous location in wrong region.

* ALTTP: Updates and refactors to multi-tracker and player tracker.

* Removed some missed commented out code.

* Add triforce to prepare inventory logic.
2024-04-20 18:29:41 -04:00
Zach Parks
0a1ce5b7d8 ALTTP: Updates to player-specific game tracker. (#3133) 2024-04-20 13:18:06 -05:00
David St-Louis
a4acdb6ddf DOOM II: var world->multiworld fix and minor doc fix (#3140) 2024-04-19 23:11:12 +02:00
Fabian Dill
7a004de9a0 LttP: remove glitch triforce setting (#3174) 2024-04-19 23:10:29 +02:00
Silvris
8021ec744f LttP: fix percentage Triforce Pieces and missed cleanup from #3160 (#3178) 2024-04-19 23:10:10 +02:00
Silvris
a06bca95ad CV64: Include player in APPP constructor (#3175) 2024-04-19 01:24:00 +02:00
Alchav
b372b9da20 LTTP: ToH Crystal Switch Logic (#3172) 2024-04-18 21:33:41 +02:00
black-sliver
6087ec539b Settings: disable automatic yaml line breaks (#3096)
* Settings: disable automatic yaml line breaks

* Tests: add settings formatting checks

* Tests: fix typing in test_host_yaml
2024-04-18 19:13:43 +02:00
digiholic
f89cee4b15 MMBN3: Modernizations and Minor Bugfixes (#2991) 2024-04-18 19:02:01 +02:00
JusticePS
727915040d Adventure: Remove runtime changes to location templates (#3010) 2024-04-18 19:01:12 +02:00
PoryGone
c4c4069022 SA2B: Update Setup guide for new Mod Manager (#3085) 2024-04-18 19:00:01 +02:00
Aaron Wagener
5711d2c309 The Messenger: Hotfix item links filler gen (#3078) 2024-04-18 18:59:30 +02:00
Aaron Wagener
580c9c3943 Core: fix item_name_groups unfolding in item links (#3088) 2024-04-18 18:58:18 +02:00
NewSoupVi
fbfe82f57f The Witness: Make item links work properly with the hint system (#3110) 2024-04-18 18:57:22 +02:00
el-u
233eba6681 lufia2ac: prevent double checks (#3154) 2024-04-18 18:56:32 +02:00
David St-Louis
ffff63e6f3 DOOM 1993: Update to use new Options + logic fixes + Doc fix (#3138) 2024-04-18 18:56:10 +02:00
JaredWeakStrike
2704015eef KH2: Docs updates and Excluded Locations Bugfix (#3150) 2024-04-18 18:55:27 +02:00
lordlou
3c70621f1b SM: getitem cheat fix (#3102) 2024-04-18 18:54:46 +02:00
Scipio Wright
5ec342abf4 Noita: Add Meat Realm (#3119) 2024-04-18 18:54:03 +02:00
David St-Louis
2c80a9b8f1 Heretic: Update to use new Options + logic fixes + Doc fix (#3139) 2024-04-18 18:53:09 +02:00
Silvris
6f7c2fa25f KDL3: Fix invalid animal placements and fill error (#3152) 2024-04-18 18:52:23 +02:00
Bryce Wilson
8b8df9fa33 Pokemon Emerald: Fix missing region for water encounters in Dewford (#3103) 2024-04-18 18:51:49 +02:00
Doug Hoskisson
3343d4e364 SM: clean post_fill function (#2863) 2024-04-18 18:50:17 +02:00
NewSoupVi
1faaa0d941 The Witness: Increase variety of the starting item (#3047) 2024-04-18 18:49:15 +02:00
Nicholas Brochu
fec533b65e Zork Grand Inquisitor: Fix Determinism Issues on Fixed Seeds (#3134) 2024-04-18 18:47:27 +02:00
Zach Parks
21184b59d2 MultiServer: Prevent invalid *_mode option values. (#3149) 2024-04-18 18:46:48 +02:00
qwint
444178171b Docs: adds new commonclient commands to webhost docs (#3151) 2024-04-18 18:46:00 +02:00
Star Rauchenberger
740b76ebd5 Lingo: The Pilgrim Update (#2884)
* An option was added to enable or disable the pilgrimage, and it defaults to disabled. When disabled, the client will prevent you from performing a pilgrimage (i.e. the yellow border will not appear when you enter the 1 sunwarp). The sun painting is added to the item pool when pilgrimage is disabled, as otherwise there is no way into the Pilgrim Antechamber. Inversely, the sun painting is no longer in the item pool when pilgrimage is enabled (even if door shuffle is on), requiring you to perform a pilgrimage to get to that room.
* The canonical pilgrimage has been deprecated. Instead, there is logic for determining whether a pilgrimage is possible.
* Two options were added that allow the player to decide whether paintings and/or Crossroads - Roof Access are permitted during the pilgrimage. Both default to disabled. These options apply both to logical expectations in the generator, and are also enforced by the game client.
* An option was added to control how sunwarps are accessed. The default is for them to always be accessible, like in the base game. It is also possible to disable them entirely (which is not possible when pilgrimage is enabled), or lock them behind items similar to door shuffle. It can either be one item that unlocks all sunwarps at the same time, six progressive items that unlock the sunwarps from 1 to 6, or six individual items that unlock the sunwarps in any order. This option is independent from door shuffle.
* An option was added that shuffles sunwarps. This acts similarly to painting shuffle. The 12 sunwarps are re-ordered and re-paired. Sunwarps that were previously entrances or exits do not need to stay entrances or exits. Performing a pilgrimage requires proceeding through the sunwarps in the new order, rather than the original one.
* Pilgrimage was added as a win condition. It requires you to solve the blue PILGRIM panel in the Pilgrim Antechamber.
2024-04-18 18:45:33 +02:00
Magnemania
6b50c91ce2 SM64ex: Logic and Generation Fixes (#3135) 2024-04-18 18:43:19 +02:00
wildham
a91105c958 FFMQ: Update FFMQ setup_en.md (#3091) 2024-04-18 18:42:28 +02:00
Trevor L
c3060a8b66 Hylics 2: Update to new options API (#3147) 2024-04-18 18:40:53 +02:00
PinkSwitch
5996a8163d Yoshi's Island: Minor Fixes (#3142) 2024-04-18 18:40:00 +02:00
Bryce Wilson
ee1e578201 Pokemon Emerald: Fix client crash if 0.4.6 client connects to 0.4.5 seed (#3146) 2024-04-18 18:39:28 +02:00
Bryce Wilson
2d3f3fcc2d Pokemon Emerald: Fix terra/marine caves bugged internal id (#3161) 2024-04-18 18:38:41 +02:00
LiquidCat64
437843bf53 CV64: Use AP Procedure Patch (#3159) 2024-04-18 18:37:51 +02:00
Scipio Wright
7fd7d5e492 Noita: Add the new bosses to the check pool (#3170) 2024-04-18 18:34:57 +02:00
el-u
52c1b04fc8 lufia2ac: prevent 0 byte reads (#3168) 2024-04-18 18:34:26 +02:00
Fabian Dill
6e56f31398 LttP/Core: more ripping and tearing (#3160) 2024-04-18 18:33:16 +02:00
Alchav
f19a84222e ALTTP: Triforce Pieces and Condense Items fixes (#3166) 2024-04-17 19:29:32 +02:00
Alchav
801d1223ac LTTP: Thieves Town KDS key fix (#3145) 2024-04-17 19:22:03 +02:00
Doug Hoskisson
30cdde8605 CI: pyright in github actions (#3121)
* CI: strict mypy check in github actions

mypy_files.txt is a list of files that will fail the CI if mypy finds errors in them

* don't need these

* `Any` should be a way to silence the type checker

* restrict return Any

* CI: pyright in github actions

* fix mistake in translating from mypy

* missed another change from mypy to pyright

* pin pyright version

* add more paths that should trigger check

* use Python instead of bash

* type error for testing CI

* Revert "type error for testing CI"

This reverts commit 99f65f3dad.

* oops

* don't need to redirect output
2024-04-16 23:03:30 +02:00
Fabian Dill
38c54ba393 WebHost: check: display exception chain one layer deep (#3153)
* WebHost: check: display exception chain one layer deep

* Update WebHostLib/check.py
2024-04-15 21:26:59 -04:00
Ziktofel
5da3a40964 SC2 Documentation: Fix the page titles (#3074) 2024-04-16 02:55:36 +02:00
Trevor L
a1ef25455b Hylics 2: Fix logic for medallions in vault (#3148) 2024-04-16 02:54:35 +02:00
Fabian Dill
feb62b4af2 Core: increment version (#3144) 2024-04-16 02:53:12 +02:00
Scipio Wright
6dbeb6c658 TUNIC: Fix chest in incorrect region, incorrect key requirement (#3132) 2024-04-14 21:14:16 -05:00
Fabian Dill
09abc5beaa Core: add visibility attribute to Option (#3125) 2024-04-14 20:49:43 +02:00
Fabian Dill
fb1cf26118 SNIClient/LttP: modern SNI prevents payload overflow (#2523) 2024-04-14 20:40:09 +02:00
Aaron Wagener
842a15fd3c Core: replace Location.event with advancement property (#2871) 2024-04-14 20:37:48 +02:00
Fabian Dill
f67e8497e0 kvui: use all flags in Item Class tooltip (#3011) 2024-04-14 20:36:55 +02:00
Fabian Dill
19f1b265b1 LttP: deprioritize locked locations for ingame hints (#3127) 2024-04-14 20:36:36 +02:00
Fabian Dill
1c14d1107f Subnautica: filler items distribution (#3104) 2024-04-14 20:36:25 +02:00
Fabian Dill
7b3727e945 CommonClient: set max_size to 16 MB (#3124) 2024-04-14 20:36:08 +02:00
Justus Lind
d1274c12b9 Muse Dash: Add filler items and rework generation balance (#2809) 2024-04-14 20:23:13 +02:00
NewSoupVi
7c44d749d4 MultiServer: Support location name groups in !missing and !checked commands (#2538) 2024-04-14 20:22:12 +02:00
Silvris
f1765899c4 MultiServer: add all worlds goal completion message (#2956) 2024-04-14 20:16:45 +02:00
PoryGone
87b9f4a6fa Spoiler: Display all precollected items in the Spoiler Log (#2928) 2024-04-14 20:05:16 +02:00
Scipio Wright
480c15eea0 TUNIC: Fix entrance rule for unrestricted + ladders - entrance rando (#3076) 2024-04-14 19:59:34 +02:00
Alchav
d4ec4d32f0 ALTTP: Bomb Walls Logic Fixes (#3130) 2024-04-14 17:30:40 +02:00
Bryce Wilson
50fb70d832 BizHawkClient: Add error message if patching fails (#2877) 2024-04-14 03:26:25 +02:00
zig-for
ca5c0d9eb8 LADX: Add "boots controls" option (#2085) 2024-04-14 03:21:55 +02:00
Aaron Wagener
98e2d89a1c Core: Let location name groups work with /hint_location (#2814) 2024-04-14 02:25:27 +02:00
PinkSwitch
5bda265f43 Yoshi's Island: Fix Outdated Connection Setup (#3113) 2024-04-14 02:23:59 +02:00
NewSoupVi
9ef1fa825d The Witness: Rename "Town Windmill Entry" to "Windmill Entry" (#3081) 2024-04-14 02:21:18 +02:00
Seldom
f5ff005360 Terraria: Crate logic (#2841) 2024-04-14 02:18:02 +02:00
Scipio Wright
fb3035a78b TUNIC: Some cleanup (#3115) 2024-04-14 02:06:06 +02:00
Ziktofel
3d5c21cec5 SC2: Remove the deprecated data version attribute in order to avoid cached old item names (#3040) 2024-04-14 01:44:51 +02:00
Scipio Wright
feaae1db12 Noita: Do some cleanup to make mypy happy (#3114) 2024-04-14 01:21:20 +02:00
Phaneros
56ec0e902d sc2: Fixing mission levels not counting towards the level 35 threshold to unlock primal kerrigan (#3109) 2024-04-14 01:09:01 +02:00
agilbert1412
c8fd42f938 Stardew Valley: Remove early shipping bin documentation (#3126) 2024-04-14 00:46:41 +02:00
Star Rauchenberger
e5eb54fb27 The Witness: Migrate joke hints to the client (#3049) 2024-04-14 00:46:11 +02:00
Scipio Wright
fbeba1e470 TUNIC: Error catching for logic bugs in ER (#3082) 2024-04-14 00:20:52 +02:00
Star Rauchenberger
11073dfdac Lingo: Remove unnecessary player_logic parameters (#3054)
A world's player_logic is accessible from the LingoWorld object, so it's not necessary to also pass the LingoPlayerLogic object through every function that uses both.
2024-04-14 00:20:31 +02:00
Alchav
7e660dbd23 Pokémon Red and Blue: 0.4.5 Fixes (#3106) 2024-04-13 17:58:50 +02:00
Exempt-Medic
1fc2c5ed4b Core: Getting rid of forfeit_mode (#3099) 2024-04-12 21:25:33 +02:00
Chris Wilson
242126b4b2 ArchipIDLE 2024 (#3079)
* Update item pool to include 25 jokes and videos as progression items, as well as a progression GeroCities profile

* Fix a bug in Items.py causing item names to be appended inappropriately

* Remove unnecessary import

* Change item pool to have 50 jokes and 20 motivational videos

* Adjust item pool to have 40 of both jokes and videos

* Fix imports to allow compressing for distribution as a .apworld
2024-04-12 00:32:10 -04:00
Ziktofel
30dda013de SC2: Fix Typos in location names (#3108) 2024-04-12 03:01:12 +02:00
Scipio Wright
ea4d0abb7f Webhost: Fix a typo on Start Playing page (#3122)
* add an or

* Changed the wording to account for uploading multiple files
2024-04-11 19:31:42 -04:00
PoryGone
9bbc49204d DKC3: Fix List Out of Range Error on Level Shuffle Hint extension (#3077) 2024-04-12 00:53:52 +02:00
Ziktofel
b97cee4372 SC2: Fix vanilla mission order connection (#3101) 2024-04-12 00:52:27 +02:00
Aaron Wagener
5dcafac861 Core: add Location.is_event property (#2968) 2024-04-12 00:49:22 +02:00
Ziktofel
8d28c34f95 SC2: Fix unused_items refill to respect item dependencies. (#3116) 2024-04-12 00:46:15 +02:00
Justus Lind
0f2bd0fb85 Muse Dash: Update songs to 4.2.0. Add a new trap. (#3053) 2024-04-12 00:44:16 +02:00
Bryce Wilson
cf59cfaad0 Pokemon Emerald: Change Ho-Oh capitalization (#3069) 2024-04-12 00:31:53 +02:00
Scipio Wright
c534cb79b5 TUNIC: Fix link to player options page in setup guide (#3086) 2024-04-12 00:30:31 +02:00
Silvris
8952fbdc03 KDL3: Fix boss access on open world disabled (#3120) 2024-04-12 00:29:40 +02:00
Ziktofel
9012afeb75 SC2: Fix possible non-determinism in goal selection (#3123) 2024-04-12 00:28:59 +02:00
NewSoupVi
401a6d9a42 The Witness: The big dumb refactor (#3007) 2024-04-12 00:27:42 +02:00
Aaron Wagener
5d4ed00452 Webhost: add file downloads to the room api endpoint (#2780) 2024-04-11 05:05:52 +02:00
Exempt-Medic
0ba6d90bb8 Fix typo (#3094) 2024-04-10 00:05:02 -04:00
Rjosephson
b007a42487 Ror2: Add progressive stages option (#2813) 2024-04-09 21:14:18 +02:00
qwint
32c92e03e7 Hollow Knight: Adding Godhome Goal Logic (#2952) 2024-04-09 21:12:50 +02:00
el-u
14437d653f lufia2ac: ability to swap party members mid-run and option to gain EXP while inactive (#2800) 2024-04-09 00:33:34 +02:00
Fabian Dill
1021df8b1b Core: remove now unused stuff in Generate.py (#3035) 2024-04-09 00:24:38 +02:00
Nicholas Saylor
569c37cb8e Core, Webhost, Docs: Replace all usages of player settings (#3067)
* Replace all usages of player settings

* Fixed line break error

* Attempt to fix line break again

* Finally figure out what Pycharm did to this file

* Pycharm search failed me

* Remove duplicate s

* Update ArchipIdle

* Revert random newline changes from Pycharm

* Remove player settings from fstrings and rename --samesettings to --sameoptions

* Finally get PyCharm to not auto-format my commits, randomly inserting the newlines

* Removing player-settings

* Missed one

* Remove final line break error
Co-authored-by: Exempt-Medic <60412657+exempt-medic@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Exempt-Medic <ExemptMedic@Gmail.com>
2024-04-06 19:25:26 -04:00
Robyn (Reckoner)
b296f64d3c fixing minor typos on options (#3080) 2024-04-06 19:17:48 -04:00
Alchav
8d9bd0135e LTTP: Fix Bug With Custom Resource Spending (#3105) 2024-04-06 19:53:20 +02:00
Fabian Dill
885fb4aabe LttP: fix fix fake world always applying 2024-04-06 17:40:52 +02:00
Fabian Dill
3c564d7b96 WebHost: allow deleting Rooms and Seeds, as well as their associated data (#3071) 2024-04-02 16:45:07 +02:00
Alchav
5e5792009c LttP: delete playerSettings.yaml (#3062) 2024-04-01 19:08:21 +02:00
CaitSith2
9aeeeb077a ALttP: Re-mark light/dark world regions after applying plando connections (#2964) 2024-04-01 15:07:56 +02:00
Bryce Wilson
35458380e6 Pokemon Emerald: Fix wonder trade race condition (#2983) 2024-04-01 15:07:11 +02:00
Alchav
4ac1866689 ALTTP: Skull Woods Inverted fix (#2980) 2024-04-01 15:06:31 +02:00
Fabian Dill
4aa03da66e Factorio: fix attempting to create savegame with not filename safe characters (#2842) 2024-04-01 15:06:02 +02:00
Silvris
24a03bc8b6 KDL3: fix shuffled animals not actually being random (#3060) 2024-04-01 15:02:26 +02:00
Aaron Wagener
f813a7005f The Messenger: update docs formatting and fix outdated info (#3033)
* The Messenger: update docs formatting and fix outdated info

* address review feedback

* 120 chars
2024-03-31 18:11:10 +02:00
LiquidCat64
2a0b7e0def CV64: A couple of very small docs corrections. (#3057) 2024-03-31 17:55:55 +02:00
Ixrec
03d47e460e A Short Hike: Clarify installation instructions (#3058)
* Clarify installation instructions

* don't mention 'config' folder since it isn't created until the game starts
2024-03-31 17:55:08 +02:00
Silvris
e546c0f7ff Yoshi's Island: add patch suffix (#3061) 2024-03-31 17:50:31 +02:00
Bryce Wilson
2ec93ba82a Pokemon Emerald: Fix inconsistent location name (#3065) 2024-03-31 17:48:59 +02:00
Aaron Wagener
4e3d396394 The Messenger: Fix precollected notes not being removed from the itempool (#3066)
* The Messenger: fix precollected notes not being properly removed from pool

* The Messenger: bump required client version
2024-03-31 17:47:11 +02:00
Fabian Dill
72c53513f8 WebHost: fix /check creating broken yaml files if files don't end with a newline (#3063) 2024-03-31 03:57:59 +02:00
Aaron Wagener
b7ac6a4cbd The Messenger: Fix various portal shuffle issues (#2976)
* put constants in a bit more sensical order

* fix accidental incorrect scoping

* fix plando rules not being respected

* add docstrings for the plando functions

* fix the portal output pools being overwritten

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

* move portal pool rebuilding outside mapping creation

* remove plando_connection cleansing since it isn't shared with transition shuffle
2024-03-30 02:14:53 +01:00
Zach Parks
5f0112e783 Tracker: Add starting inventory to trackers and received items table. (#3051) 2024-03-29 19:13:51 -05:00
Aaron Wagener
bb481256de Core: Make fill failure error more human parseable (#3023) 2024-03-28 21:48:40 -05:00
Aaron Wagener
301d9de975 Docs: adding games rework (#2892)
* Docs: complete adding games.md rework

* remove all the now unused images

* review changes

* address medic's review

* address more comments
2024-03-29 01:31:59 +01:00
Trevor L
9dc708978b Hylics 2: Fix invalid multiworld data, use self.random instead of self.multiworld.random (#3001)
* Hylics 2: Fixes

* Rewrite loop
2024-03-29 01:26:58 +01:00
Bryce Wilson
4391d1f4c1 Pokemon Emerald: Fix opponents learning non-randomized TMs (#3025) 2024-03-29 01:05:39 +01:00
black-sliver
5d9d4ed9f1 SoE: update to pyevermizer v0.48.0 (#3050) 2024-03-29 01:01:31 +01:00
Scipio Wright
c97215e0e7 TUNIC: Minor refactor of the vanilla_portals function (#3009)
* Remove unused, change an if to an elif

* Remove unused import
2024-03-28 22:23:37 +01:00
Alchav
eb66886a90 SC2: Don't Filter Excluded Victory Locations (#3018) 2024-03-28 22:23:01 +01:00
Fabian Dill
de860623d1 Core: differentiate between unknown worlds and broken worlds in error message (#2903) 2024-03-28 22:21:56 +01:00
Bryce Wilson
74b2bf5161 Pokemon Emerald: Exclude norman trainer location during norman goal (#3038) 2024-03-28 22:20:55 +01:00
BadMagic100
74ac66b032 Hollow Knight: 0.4.5 doc revamp and default options tweaks (#2982)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-03-28 10:49:19 -05:00
Silvris
80d7ac4164 KDL3: RC1 Fixes and Enhancement (#3022)
* fix cloudy park 4 rule, zero deathlink message

* remove redundant door_shuffle bool

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

* properly fix deathlink messages, fix fill error

* update docs
2024-03-28 15:41:32 +01:00
Ziktofel
77311719fa SC2: Fix HERC upgrades (#3044) 2024-03-28 15:38:34 +01:00
NewSoupVi
cfc1541be9 Docs: Mention the "last received item index" paradigm in the network protocol docs (#2989)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-03-28 09:19:32 -05:00
Scipio Wright
4d954afd9b TUNIC: Add link to AP plando guide to connection plando section of game page (#2993) 2024-03-28 09:11:20 -05:00
Nicholas Saylor
17748a4bf1 Launcher, Docs: Update UI and Set-Up Guide to Reference Options (#2950) 2024-03-28 09:00:10 -05:00
Entropynines
9182fe563f README: Remove outdated information about launchers (#2966)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-03-28 08:56:35 -05:00
t3hf1gm3nt
bcf223081f TLOZ: Fix markdown issue with game info page (#2985) 2024-03-28 08:54:56 -05:00
Exempt-Medic
fa93488f3f Docs: Consistent naming for "connection plando" (#2994) 2024-03-28 08:46:00 -05:00
chandler05
db15dd4bde A Short Hike: Fix incorrect info in docs (#3016) 2024-03-28 08:45:19 -05:00
PoryGone
01cdb0d761 SMW: Update World Doc for v2.0 Features (#3034)
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-03-28 08:44:23 -05:00
panicbit
d0ac2b744e LADX: fix local and non-local instrument placement (#2987)
* LADX: fix local and non-local instrument placement

* change confusing variable name
2024-03-28 10:11:26 +01:00
Jérémie Bolduc
14f5f0127e Stardew Valley: Fix potential soft lock with vanilla tools and entrance randomizer + Performance improvement for vanilla tool/skills (#3002)
* fix vanilla tool fishing rod requiring metal bars
fix vanilla skill requiring previous level (it's always the same rule or more restrictive)

* add test to ensure fishing rod need fish shop

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

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

* reeeeeeeee
2024-03-28 09:42:35 +01:00
Bryce Wilson
cf133dde72 Pokemon Emerald: Fix typo (#3020) 2024-03-28 09:32:27 +01:00
Jérémie Bolduc
ca18121811 Stardew Valley: Fix generation fail with SVE and entrance rando when Wizard Tower is in place of Sprite Spring (#2970) 2024-03-28 09:27:49 +01:00
NewSoupVi
1d4512590e requirements.txt: _ instead of - to make PyCharm happy (#3043) 2024-03-27 21:09:09 +01:00
agilbert1412
f7b415dab0 Stardew valley: Game version documentation (#2990)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-03-26 11:40:58 -05:00
LiquidCat64
702f006c84 CV64: Change all mentions of "settings" to "options" and fix a broken link (#3015) 2024-03-26 14:31:36 +01:00
Yussur Mustafa Oraji
98ce8f8844 sm64ex: New Options API and WebHost fix (#2979) 2024-03-26 14:29:25 +01:00
Scipio Wright
ea47b90367 TUNIC: You can grapple down here without the ladder, neat (#3019) 2024-03-26 14:25:41 +01:00
agilbert1412
bf3856866c Stardew Valley: presets with some of the new available values for existing settings to make them more accurate (#3014) 2024-03-24 21:53:49 +01:00
Phaneros
c0368ae0d4 SC2: Fixed missing upgrade from custom tracker (#3013) 2024-03-24 21:53:20 +01:00
Salzkorn
36c83073ad SC2 Tracker: Fix grouped items pointing at wrong item IDs (#2992) 2024-03-24 21:52:41 +01:00
Ziktofel
2b24539ea5 SC2 Tracker: Use level tinting to let the player know which level he has of Replenishable Magazine (#2986) 2024-03-24 21:52:16 +01:00
Ziktofel
7e904a1c78 SC2: Fix Kerrigan presence resolving when deciding which races should be used (#2978) 2024-03-24 21:51:46 +01:00
Alchav
bdd498db23 ALTTP: Fix #2290's crashes (#2973) 2024-03-22 21:36:27 +01:00
PinkSwitch
355223b8f0 Yoshi's Island: Implement New Game (#2141)
Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-03-22 21:35:00 +01:00
NewSoupVi
aaa3472d5d The Witness: Fix seed bleed issue (#3008) 2024-03-22 21:30:51 +01:00
chandler05
96d93c1ae3 A Short Hike: Add option to customize filler coin count (#3004)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-03-22 21:30:23 +01:00
Silvris
ca549df20a CommonClient: fix hint tab overlapping (#2957)
Co-authored-by: Remy Jette <remy@remyjette.com>
2024-03-22 21:29:24 +01:00
Star Rauchenberger
44988d430d Lingo: Add trap weights option (#2837) 2024-03-22 21:28:41 +01:00
Danaël V
11b32f17ab Docs: replacing "setting" to "option" in world docs (#2622)
* Update contributing.md

* Update contributing.md

* Update contributing.md

* Update contributing.md

* Update contributing.md

* Update contributing.md

Added non-AP World specific information

* Update contributing.md

Fixed broken link

* Some minor touchups

* Update Contributing.md

Draft for version with picture

* Update contributing.md

Small word change

* Minor updates for conciseness, mostly

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

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

* Update contributing.md

* Update contributing.md

* Update setup_en.md

Woops I forgot one

* Update Options.py

Reverted changes regarding options.py

* Update worlds/noita/docs/en_Noita.md

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

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

revert change waiting for that page to be updated

* Update worlds/witness/docs/setup_en.md

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

* Update worlds/soe/docs/multiworld_en.md

Fixed Typo

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

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

* Update worlds/adventure/docs/en_Adventure.md

* Update worlds/witness/docs/setup_en.md

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

* Didn't work :dismay:

* Delete worlds/sc2wol/docs/setup_en.md

I think this will fix the merge issue

* Now it should work

* Woops

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-03-22 17:46:14 +01:00
Silvris
218cd45844 APProcedurePatch: fix RLE/COPY incorrect sizing (#3006)
* change class variables to instance variables

* Update worlds/Files.py

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

* Update worlds/Files.py

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

* move required_extensions to tuple

* fix missing tuple ellipsis

* fix classvar mixup

* rename tokens to _tokens. use hasattr

* type hint cleanup

* Update Files.py

* check using isinstance instead

* Update Files.py

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-03-22 09:02:38 +01:00
Exempt-Medic
4196bde597 Docs: Fixing special_range_names example (#3005) 2024-03-21 21:38:36 +01:00
Star Rauchenberger
40f843f54d Lingo: Minor game data fixes (#3003) 2024-03-21 17:00:53 +01:00
GodlFire
da333fbb0c Shivers: Adds missing logic rule for skull dial door location (#2997) 2024-03-21 16:52:16 +01:00
NewSoupVi
43084da23c The Witness: Fix newlines in Witness option tooltips (#2971) 2024-03-21 16:51:29 +01:00
Scipio Wright
14816743fc TUNIC: Shuffle Ladders option (#2919) 2024-03-21 16:50:07 +01:00
Star Rauchenberger
30a0aa2c85 Lingo: Add item/location groups (#2789) 2024-03-21 16:46:53 +01:00
Silvris
f4b7c28a33 APProcedurePatch: hotfix changing class variables to instance variables (#2996)
* change class variables to instance variables

* Update worlds/Files.py

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

* Update worlds/Files.py

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

* move required_extensions to tuple

* fix missing tuple ellipsis

* fix classvar mixup

* rename tokens to _tokens. use hasattr

* type hint cleanup

* Update Files.py

* check using isinstance instead

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-03-20 23:45:32 +01:00
chandler05
12864f7b24 A Short Hike: Implement New Game (#2577) 2024-03-20 22:44:09 +01:00
LiquidCat64
db02e9d2aa Castlevania 64: Implement New Game (#2472) 2024-03-20 22:03:25 +01:00
Jérémie Bolduc
32315776ac Stardew Valley: Fix extended family legendary fishes being locations with fishsanity set to exclude legendary (#2967) 2024-03-20 21:57:45 +01:00
Magnemania
e9620bea77 SM64: Goal Logic and Hint Bugfixes (#2886) 2024-03-20 21:56:00 +01:00
qwint
183ca35bba CommonClient: Port Casting Bug (#2975) 2024-03-20 14:39:37 +01:00
TheLX5
fcaaa197a1 SMW: Fixes for Bowser being defeatable on Egg Hunt and CI2 DC room access (#2981) 2024-03-20 13:56:19 +01:00
TheLX5
8f7b63a787 SMW: Blocksanity logic fixes (#2988) 2024-03-20 13:56:04 +01:00
Scipio Wright
6f64bb9869 Noita: Remove newline from option description so it doesn't look bad on webhost (#2969) 2024-03-20 13:46:31 +01:00
Bryce Wilson
d0a9d0e2d1 Pokemon Emerald: Bump required client version (#2963) 2024-03-20 13:43:13 +01:00
Silvris
94650a02de Core: implement APProcedurePatch and APTokenMixin (#2536)
* initial work on procedure patch

* more flexibility

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

* pushing current changes to go fix tloz bug

* move tokens into a separate inheritable class

* forgot the commit to remove token from ProcedurePatch

* further cleaning from bad commit

* start on docstrings

* further work on docstrings and typing

* improve docstrings

* fix incorrect docstring

* cleanup

* clean defaults and docstring

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

* change to dictionary.get

* remove unnecessary if statement

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

* Update Files.py

* remove struct uses

* ensure returning bytes, add token type checking

* Apply suggestions from code review

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

* pep8

---------

Co-authored-by: beauxq <beauxq@yahoo.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2024-03-19 23:08:29 +01:00
PoryGone
8a8263fa61 SMW: Increment Required Client Version (#2962) 2024-03-15 18:02:25 +01:00
NewSoupVi
9efc7bae40 The Witness: Add junk hint for Zork: Grand Inquisitor (#2961) 2024-03-15 17:54:21 +01:00
Scipio Wright
e6198585c8 TUNIC: Implement support for connection plando (#2864) 2024-03-15 17:52:05 +01:00
Nicholas Brochu
2a8784ef72 Zork Grand Inquisitor: Implement New Game (#2539)
Adds Archipelago support for Zork Grand Inquisitor, the 1997 point-and-click PC adventure game.

The client (based on `CommonClient`), on top of its regular Archipelago duties, fully handles the randomization of the game and the monitoring / modification of the game state. No game modding needed at all; the player is ready to play an Archipelago seed if they can play the vanilla game through ScummVM.

The "reverse engineering" (there's likely a better term for this...) of the game is my own original work and I included an MIT license at the root of my world directory.

A PopTracker pack was also created to help people learn the game: https://github.com/SerpentAI/ZorkGrandInquisitorAPTracker
2024-03-15 17:35:37 +01:00
Ziktofel
e0e9fdd86a SC2: Multi-campaign (#2954)
Adds HotS, LotV and NCO campaigns to SC2 game.
The world's name has changed to reflect that (it's not only Wings of Liberty now)
The client was patched in a way that can still join to games generated prior this change
---------

Co-authored-by: Magnemania <magnemight@gmail.com>
Co-authored-by: EnvyDragon <138727357+EnvyDragon@users.noreply.github.com>
Co-authored-by: Matthew <matthew.marinets@gmail.com>
Co-authored-by: hopop201 <benjy.hopop201@gmail.com>
Co-authored-by: Salzkorn <salzkitty@gmail.com>
Co-authored-by: genderdruid <pallyoffail@gmail.com>
Co-authored-by: MadiMadsen <137329235+MadiMadsen@users.noreply.github.com>
Co-authored-by: neocerber <neocerber@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2024-03-15 17:33:03 +01:00
agilbert1412
ed9cbfecb9 Stardew Valley: Added a Great Combat requirement to an entrance that could block its own key (#2959) 2024-03-15 17:31:01 +01:00
espeon65536
d1ef198566 OoT: Entrance Spoiler Fixes (#2500) 2024-03-15 08:05:44 -05:00
NewSoupVi
1324bccc89 The Witness: Add newly submitted junk hints (#2949) 2024-03-15 14:00:10 +01:00
PoryGone
cafacfe754 SA2B: Add CHANGELOG.md (#2945) 2024-03-15 07:58:20 -05:00
Fabian Dill
e1038b4431 Core: increment version (#2958) 2024-03-15 13:58:08 +01:00
PoryGone
2e8a84121e DKC3: Add CHANGELOG.md (#2946) 2024-03-15 07:57:43 -05:00
PoryGone
0342c1e09d Celeste 64: Add CHANGELOG.md (#2948) 2024-03-15 07:57:05 -05:00
PoryGone
f330e5835c SMW: Add CHANGELOG.md (#2947) 2024-03-15 07:56:12 -05:00
Silent
329a2f9175 TUNIC: Updated display name for a few options (#2953) 2024-03-15 13:53:41 +01:00
Silvris
31249ec6e2 Launcher: make scrollbar more prominent (#2955) 2024-03-15 13:47:31 +01:00
agilbert1412
52e65e208e Stardew Valley: 5.x.x - The Allsanity Update (#2764)
Major Content update for Stardew Valley, including the following features

- Major performance improvements all across the Stardew Valley apworld, including a significant reduction in the test time
- Randomized Farm Type
- Bundles rework (Remixed Bundles and Missing Bundle!)
- New Settings:
  * Shipsanity - Shipping individual items
  * Monstersanity - Slaying monsters
  * Cooksanity - Cooking individual recipes
  * Chefsanity - Learning individual recipes
  * Craftsanity - Crafting individual items
- New Goals:
  * Protector of the Valley - Complete every monster slayer goal
  * Full Shipment - Ship every item
  * Craftmaster - Craft every item
  * Gourmet Chef - Cook every recipe
  * Legend - Earn 10 000 000g
  * Mystery of the Stardrops - Find every stardrop (Maguffin Hunt)
  * Allsanity - Complete every check in your slot
- Building Shuffle: Cheaper options
- Tool Shuffle: Cheaper options
- Money rework
- New traps
- New isolated checks and items, including the farm cave, the movie theater, etc
- Mod Support: SVE [Albrekka]
- Mod Support: Distant Lands [Albrekka]
- Mod Support: Hat Mouse Lacey [Albrekka]
- Mod Support: Boarding House [Albrekka]

Co-authored-by: Witchybun <elnendil@gmail.com>
Co-authored-by: Witchybun <96719127+Witchybun@users.noreply.github.com>
Co-authored-by: Jouramie <jouramie@hotmail.com>
Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com>
2024-03-15 13:05:14 +01:00
qwint
f7da833572 CommonClient: Don't retry connection when connection details are invalid (#2831) 2024-03-15 09:27:42 +01:00
Star Rauchenberger
afe7aea536 Lingo: Pre-compile datafile to improve loading time (#2829) 2024-03-15 09:26:00 +01:00
Doug Hoskisson
e534abeab0 Core: add layer for patches that don't use Patch.py (#2889)
* Core: add layer for patches that don't use `Patch.py`

* bump container version

* APAutoPatchInterface name

* mystic quest change

* OoT and Adventure changes

* missed name in docstring

* container version compatibility
2024-03-14 22:29:29 +01:00
Bryce Wilson
fa233b2583 Pokemon Emerald: v2 Update (#2918) 2024-03-14 12:37:10 +01:00
Silvris
3e3965272d KDL3: Ensure all abilities accessible on non-minimal (#2929) 2024-03-13 21:05:38 +01:00
qwint
72e6383cc7 HK: Removes Vanilla Items from ItemPool and Uses Grimmchild1 when relevant (#2898) 2024-03-13 06:45:43 -05:00
black-sliver
67ed0fdca5 CI: update actions (#2943) 2024-03-13 08:25:51 +01:00
Aaron Wagener
c4ec8682d5 Core: fix incorrect ordering on the always_allow static method (#2938) 2024-03-12 23:29:32 +01:00
Alchav
1705213353 FFMQ: Update Map Shuffle Seed description (#2658)
* Update Map Shuffle Seed description

* Update worlds/ffmq/Options.py

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

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-03-12 23:21:58 +01:00
Kory Dondzila
d953927b3a Shivers: Renaming for clarity and consistency (#2869)
* Moves plaque location to front for better tracker referencing.

* Tiki should be Shaman.

* Hanging should be Gallows.

* Merrick spelling.

* Clarity change.
2024-03-12 23:17:18 +01:00
black-sliver
ecd84fd1ca CI: build: create setup (#2936)
* CI: build: create setup

also add /DNO_SIGNTOOL to inno_setup.iss

* CI: build: trigger when changing setup-related files
2024-03-12 22:27:17 +01:00
Silvris
fb9ef19c15 Core: add list/dict merging feature to triggers (#2793)
* proof of concept

* add dict support, block top/game level merge

* prevent key error when option being merged is new

* update triggers guide

* Add documentation about add/remove/replace

* move to trailing name instead of proper tag

* update docs

* confirm types

* Update Utils.py

* Update Generate.py

* pep8

* move to + syntax

* forgot to support sets

* specify received type of type error

* Update Generate.py

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

* Apply suggestion from review

* add test for update weights

* move test to new test case

* Apply suggestions from code review

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

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-03-12 22:08:12 +01:00
Doug Hoskisson
03d403ff51 Core: typing for Option.default and a few other ClassVars (#2899)
* Core: typing for `Option.default` and a few other `Option` class variables

This is a replacement for https://github.com/ArchipelagoMW/Archipelago/pull/2173

You can read discussion there for issues we found for why we can't have more specific typing on `default`

instead of setting a default in `Option` (where we don't know the type), we check in the metaclass to make sure they have a default.

* NumericOption doesn't need the type annotation that brings out the mypy bug

* SoE default ClassVar
2024-03-12 22:03:57 +01:00
PoryGone
f8d5fe0e1e SMW: v2.0 Content Update (#2762)
Changelog:

Features:
- New optional Location Checks
  - 3-Up Moons
  - Hidden 1-Ups
  - Bonus Blocks
  - Blocksanity
    - All blocks that contain coins or items are included, with the exception of:
      - Blocks in Top Secret Area & Front Door/Bowser Castle
      - Blocks that are unreachable without glitches/unreasonable movement
- New Items
  - Special Zone Clear
  - New Filler Items
    - 1 Coin
    - 5 Coins
    - 10 Coins
    - 50 Coins
  - New Trap Items
    - Reverse Trap
    - Thwimp Trap
- SFX Shuffle
- Palette Shuffle Overhaul
  - New Curated Palette can now be used for the Overworld and Level Palette Shuffle options
  - Foreground and Background Shuffle options have been merged into a single setting
- Max possible Yoshi Egg value is 255
  - UI in-game is updated to handle 3-digits
  - New `Display Received Item Popups` option: `progression_minus_yoshi_eggs`

Quality of Life:
- In-Game Indicators are now displayed on the map screen for location checks and received items
- In-level sprites are displayed upon receiving certain items
- The Camera Scroll unlocking is now only enabled on levels where it needs to be
- SMW can now handle receiving more than 255 items
- Significant World Code cleanup
  - New Options API
  - Removal of `world: MultiWorld` across the world
- The PopTracker pack now has tabs for every level/sublevel, and can automatically swap tabs while playing if connected to the server

Bug Fixes:
- Several logic tweaks/fixes

"Major credit to @TheLX5 for being the driving force for almost all of this update. We've been collaborating on design and polish of the features for the last few months, but all of the heavy lifting was all @TheLX5."
2024-03-12 22:00:13 +01:00
Remy Jette
b6b88070be CommonClient: Fix item link group name when member slot name contains brackets (#2794) 2024-03-12 21:13:52 +01:00
Fabian Dill
a6e1ea8c48 CommonClient: use rich text for /received (#2715) 2024-03-12 20:40:58 +01:00
Alchav
2692604c09 Core: String comparison with FreeText class (#2942) 2024-03-12 20:40:16 +01:00
NewSoupVi
4bf676e588 The Witness: Obelisk Keys (#2805) 2024-03-12 20:04:13 +01:00
Fabian Dill
ae6c16bde1 MultiServer: send new read_hints datastore values on change (#2558) 2024-03-12 19:58:02 +01:00
Fabian Dill
c795c72471 kvui: allow sorting hints in the hint tab (#2684) 2024-03-12 14:52:57 +01:00
NewSoupVi
30ad2aa4a8 The Witness: Don't unnecessarily break people's 0.4.4 yamls (#2940) 2024-03-12 14:51:10 +01:00
Aaron Wagener
51243abea1 Docs: improve AutoWorld method docstrings (#2509)
* clarify some autoworld docstrings

* revert accidental change
2024-03-12 09:27:41 +01:00
BadMagic100
cb2c00f644 CI: Don't auto-remove content based labels (#2941) 2024-03-12 09:11:13 +01:00
Aaron Wagener
6d3f7865ff The Messenger: fix items accessibility reachability bug due to new rules (#2937) 2024-03-12 00:55:28 +01:00
JaredWeakStrike
a7384b4b63 KH2: Update all instances of multiworld.option_name to option.option_name (#2634)
* update the multiworld to options

* Update worlds/kh2/Rules.py

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

* does this work

* namine sketches

* wrong branch :)

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-03-12 00:52:16 +01:00
CubeSoldier
3d56f3c096 Docs: Added snes9x-nwa as recommended emulator to the setup guides for SNES games (#1778)
* Added snes9x-nwa as recommended emulator to the setup guides

* Removed snes9x-nwa from the setup guides of DKC3 and SMW

* Update worlds/alttp/docs/multiworld_en.md

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

* Removed duplicate text
Minor grammar and spelling fixes

* Unified required software for SM, SMZ3 and SoE with ALTTP

* Added instructions for usage of BSNES-Plus for ALTTP, SM and SMZ3

---------

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2024-03-12 00:42:56 +01:00
Jérémie Bolduc
a3125cb06e Core: Fix OptionList and OptionSet to allow Iterable of Iterable (#2911)
* fix, maybe

* typegard for iterable of any

* wow I'm so tired I just changed the method name without changing what it actually does...

* also exclude bytes in is_iterable_but_str

* apply pr comments

* Update Utils.py

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

* Revert "also exclude bytes in is_iterable_but_str"

This reverts commit cf087d2ee2.

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2024-03-12 00:30:14 +01:00
Silvris
d3019421de KDL3: fix invalid inno_setup components and deathlink messages (#2922)
* remove component checking

* fix missing deathlink messages

* move reads under deathlink check
2024-03-11 23:26:21 +01:00
Aaron Wagener
d20d09e682 The Messenger: content update (#2823)
* map option objects to a `World.options` dict

* convert RoR2 to options dict system for testing

* add temp behavior for lttp with notes

* copy/paste bad

* convert `set_default_common_options` to a namespace property

* reorganize test call order

* have fill_restrictive use the new options system

* update world api

* update soe tests

* fix world api

* core: auto initialize a dataclass on the World class with the option results

* core: auto initialize a dataclass on the World class with the option results: small tying improvement

* add `as_dict` method to the options dataclass

* fix namespace issues with tests

* have current option updates use `.value` instead of changing the option

* update ror2 to use the new options system again

* revert the junk pool dict since it's cased differently

* fix begin_with_loop typo

* write new and old options to spoiler

* change factorio option behavior back

* fix comparisons

* move common and per_game_common options to new system

* core: automatically create missing options_dataclass from legacy option_definitions

* remove spoiler special casing and add back the Factorio option changing but in new system

* give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly

* reimplement `inspect.get_annotations`

* move option info generation for webhost to new system

* need to include Common and PerGame common since __annotations__ doesn't include super

* use get_type_hints for the options dictionary

* typing.get_type_hints returns the bases too.

* forgot to sweep through generate

* sweep through all the tests

* swap to a metaclass property

* move remaining usages from get_type_hints to metaclass property

* move remaining usages from __annotations__ to metaclass property

* move remaining usages from legacy dictionaries to metaclass property

* remove legacy dictionaries

* cache the metaclass property

* clarify inheritance in world api

* move the messenger to new options system

* add an assert for my dumb

* update the doc

* rename o to options

* missed a spot

* update new messenger options

* comment spacing

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

* fix tests

* fix missing import

* make the documentation definition more accurate

* use options system for loc creation

* type cast MessengerWorld

* fix typo and use quotes for cast

* LTTP: set random seed in tests

* ArchipIdle: remove change here as it's default on AutoWorld

* Stardew: Need to set state because `set_default_common_options` used to

* The Messenger: update shop rando and helpers to new system; optimize imports

* Add a kwarg to `as_dict` to do the casing for you

* RoR2: use new kwarg for less code

* RoR2: revert some accidental reverts

* The Messenger: remove an unnecessary variable

* remove TypeVar that isn't used

* CommonOptions not abstract

* Docs: fix mistake in options api.md

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

* create options for item link worlds

* revert accidental doc removals

* Item Links: set default options on group

* Messenger: Limited Movement option first draft

* The Messenger: add automated setup through the launcher

* drop tomllib

* don't uselessly import launcher

* The Messenger: fix missing goal requirement for power seal hunt

* make hard mode goal harder

* make fire seal a bit more lenient

* have limited movement force minimal accessibility

* add an early meditation option

* clean up precollected notes tests a bit

* add linux support

* add steam deck support

* await monokickstart

* minor styling cleanup

* more minor styling cleanup

* Initial implementation of Generic ER

* Move ERType to Entrance.Type, fix typing imports

* updates based on testing (read: flailing)

* Updates from feedback

* Various bug fixes in ERCollectionState

* Use deque instead of queue.Queue

* Allow partial entrances in collection state earlier, doc improvements

* Prevent early loops in region graph, improve reusability of ER stage code

* Typos, grammar, PEP8, and style "fixes"

* use RuntimeError instead of bare Exceptions

* return tuples from connect since it's slightly faster for our purposes

* move the shuffle to the beginning of find_pairing

* do er_state placements within pairing lookups to remove code duplication

* requested adjustments

* Add some temporary performance logging

* Use CollectionState to track available exits and placed regions

* remove seal shuffle option

* some cleanup stuff

* portal rando progress

* pre-emptive region creation

* seals need to be in the datapackage

* put mega shards in old order

* fix typos and make it actually work

* fix more missed connections and add portal events

* fix all the portal rando code

* finish initial logic implementation

* remove/comment out debug stuff

* does not actually support plando yet

* typos and fix a crash when 3 available portals was selected

* finish initial logic for all connections and remove/rename as necessary

* fix typos and add some more leniency

* move item classification determination to its own method rather than split between two spots

* super complicated solution for handling installing the alpha builds

* fix logic bugs and add a test

* implement logic to shuffle the cutscene portals even though it's probably not possible

* just use the one list

* fix some issues with the mod checking/downloading

* Core: have webhost slot name links go through the launcher so that components can use them

* add uri support to the launcher component function

* generate output file under specific conditions

* cleanup connections.py

* set topology_present to true when portals are shuffled

* add requirement for ghost pit loc since it's pretty hard without movement

* bring hard logic back

* misc cleanup

* fix asset grabbing of latest version

* implement ER

* just use the entrances for the spoiler instead of manipulating the cache

* remove test defaults

* remove excessive comprehension

* cleanup and cater data for the client

* add elemental skylands to the shuffle pools

* initial attempts at hint text

* use network items for offline seeds

* change around the offline seed data again

* move er after portal shuffle and ensure a minimal sphere 1

* Add a method to automatically disconnect entrances in a coupled-compliant way

 Update docs and cleanup todos

* Make find_placeable_exits deterministic by sorting blocked_connections set

* add more ER transitions

* fix spoiler output of portal warps

* add path to hint_data

* rename entrance to tot to be a bit clearer

* cleanup imports and update description for hard logic

* cleanup for PR to main

* missed a spot

* cleanup monokickstart

* add location_name_groups

* update docs for new setup

* client can reconnect on its own now, no need for a button.

* fix mod download link grabbing the wrong assets

* cleanup mod pulling a bit and display version it's trying to update to

* plando support

* comment out broken steam deck support

* supports plando

* satisfy flake for currently unused file

* fix the items accessibility test

* review comments

* add searing crags portal to starting portals when disabled like option says

* address sliver comments

* rip out currently unused transition shuffle

* add aerobatics warrior requirement to fire seal

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@yahoo.com>
Co-authored-by: Sean Dempsey <dempsey.sean@outlook.com>
Co-authored-by: qwint <qwint.42@gmail.com>
2024-03-11 23:23:41 +01:00
Aaron Wagener
078d793073 Tests: add test for 2-player-multiworlds (#2386)
* Tests: add test for all games multiworld and test for two player multiworld per game

* make assertSteps behave like call_all

* review improvements

* fix stage calling and loc copying in accessibility

* add docstrings

* lttp is on the options api now

* skip the all games multiworld for now. likely needs to be modified to test specific worlds

* move skip to the class
2024-03-11 23:22:30 +01:00
Aaron Wagener
5fecb7f043 LTTP: fix some hashed string comparisons (#2927) 2024-03-11 10:00:28 +01:00
chandler05
9c920fbc53 MultiServer: Improve command response to help troubleshooting (#2833)
* Update MultiServer.py

* Improve logging again

* Add to other log as well

* Update MultiServer.py
2024-03-11 09:55:22 +01:00
wildham
fed3d04c8d FF1: Fix resending items on disconnect/connect (#2817) 2024-03-11 09:51:51 +01:00
Bryce Wilson
6badc75237 BizHawkClient: Fix error logging in python 3.8 (#2930) 2024-03-11 07:16:48 +01:00
axe-y
c7e735da15 DLCQuest: progression coin bundle update (#2785) 2024-03-10 22:12:55 +01:00
Aaron Wagener
2e1a5b0e3b Core: create the per world random object in the world constructor (#2083)
* Core: create the per world random object in the world constructor

* remove the check that multiworld exists

* add a deprecation warning to per_slot_randoms

* move random import and fix conflicts

* assert worlds don't exist before setting the multiworld seed

* fix the dlcq and sdv tests

* actually use the seed
2024-03-10 18:47:45 +01:00
Seldom
b8c24def8d Terraria: Logic fix: Witch Doctor sells Bewitching Table (#2880) 2024-03-10 16:03:44 +01:00
Kappatechy
3602ed45a4 Bumper Stickers: logic fixes for "off-by-one" errors (#2855)
* Corrected logic error.

Per discussion here: https://discord.com/channels/731205301247803413/1148330200891932742/1192138309120577646

At the moment, the logic expects Treasure Bumper 2 to require 1 bumper, Treasure Bumper 3 to require 2, etc., and for Treasure Bumper 1 to be in Sphere 1. This is incorrect, each Bumper check should require 1 Bumper item of it's type.

This corrects that. I've verified I was able to generate with it by editing my apworld locally, but I'm also not a programmer and don't know anything about tests. However, I'd think this is a simple change.

* Correct logic in Bumper Sticker unit tests

Off By One errors were rampant in the Bumper Stickers unit test logic. This should correct those errors.

* Correct use of "range" function

The function setting the access rules for Treasure and Booster Bumpers was stopping one short of being applied to all the related locations. This has been corrected.

* Restoring and clarifying designer's original level access intent

The original creator of the AP version of Bumper Stickers intentionally set the Treasure Bumper requirements to logically reach each level 1 higher than the actual game requires, and logic tests were built based on this. This design decision has now been restored.

* Revert "Restoring and clarifying designer's original level access intent"

This reverts commit 5186c5fcc3.

* Correct test logic for level 5

While 33 Treasure Bumpers are generated, only 32 are needed to reach level 5. This push corrects the unit test for the level 5 checks.

* Rename generically-named variables

Change variables from generic names (x, y, n) to more meaningful names, for ease of readability.

---------

Co-authored-by: The T <thet113@gmail.com>
2024-03-10 15:36:42 +01:00
panicbit
37add8ee59 LADX: shuffle instruments (#2804)
* ladx: shuffle instruments

* set correct default for shuffled instruments
2024-03-10 14:48:00 +01:00
qwint
5a4d88d554 Clients: add /item_groups and /location_groups (#2822) 2024-03-10 14:46:44 +01:00
Robyn (Reckoner)
a4f89396d9 Docs: Fix typos in Minecraft info page (#2686)
Fixed typos in game page
2024-03-10 13:50:25 +01:00
Zach Parks
8c11c385f3 ALTTP: Fix NotImplemented error when using non-none values for timer. (#2924) 2024-03-10 01:56:57 -06:00
Aaron Wagener
939a5ec959 LTTP: remove multiworld = None (#2290) 2024-03-10 08:18:25 +01:00
axe-y
4ce58c0240 DLC Quest: AP World Status fix (#2908) 2024-03-10 08:13:52 +01:00
Remy Jette
be802b4723 Core: Remove extra " character in /forbid_release help message (#2923)
Was pointed out in the Discord: https://discord.com/channels/731205301247803413/731205301818359821/1215882443261870190
2024-03-10 08:12:55 +01:00
NewSoupVi
3c4ebb2114 The Witness: Fix... I don't know how to explain this one (#2920)
```            
for hint in generated_hints:
    hint = generated_hints.pop(0)
```

lmao
2024-03-09 00:03:02 +01:00
Seafo
3e3b4c6732 Minecraft: Add Pickaxes to the documentation (#2688) 2024-03-08 13:15:36 -08:00
Silvris
3e0ff3f72d KDL3: Post-PR adjustments (#2917)
* logic adjustments, add patch_suffix

* forgot the other two consumables
2024-03-08 07:46:14 +01:00
Silvris
b4bb88fcf7 SNIClient: dynamically generate patch file identifier (#2870)
Co-authored-by: beauxq <beauxq@yahoo.com>
2024-03-07 10:18:22 +01:00
Exempt-Medic
862d77820d Fill: Improve clarity of remaining_fill messages (#2894)
* Update Fill.py

Make the fill message more descriptive

* Removing kwarg
2024-03-07 08:48:55 +01:00
Aaron Wagener
c6b1039e0f Core: call from_any on the options class when creating item links options (#2783) 2024-03-07 08:40:09 +01:00
BadMagic100
ac791f2999 CI: Avoid race condition in labeler workflow (#2910) 2024-03-07 08:24:08 +01:00
Alchav
e33ea0147b LTTP: Missed per_slot_random change (#2907) 2024-03-06 16:43:34 +01:00
PoryGone
db30a0116e Celeste 64: Implement New Game (#2798)
Co-authored-by: chandler05 <66492208+chandler05@users.noreply.github.com>
Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
Co-authored-by: Zach Parks <zach@alliware.com>
2024-03-05 17:55:56 -06:00
Alchav
a5a1494a96 Pokemon R/B: The Big Door Shuffle Update (#2861)
- Perhaps most critically, adds the ability for the door shuffle code to catch door shuffle exceptions and try again. Will try up to 10 times. Should mean Door Shuffle does not need to be disallowed in the big async🤞
- Door Shuffle code has been made drastically faster by searching for the first dead end instead of sorting the whole list of entrances by whether they are dead ends.
- Renames Full to Interiors, and adds a new Full door shuffle that shuffles interior-to-interior doors separately from exterior-to-interior doors.
- Adds a new Decoupled door shuffle.
- Warp Tile Shuffle now has 3 separate options, Vanilla, Shuffle, and Mixed. Shuffle shuffles the warp tiles among themselves, Mixed mixes them into the Door Shuffle pool.
- Safari Zone connections are now shuffled on Full, Insanity, and Decoupled.
- On Simple Door Shuffle, the Town Map is updated to show the new dungeon locations. The Town Map has been updated to show the locations of dungeons that previously were not shown unless you opened the map within them, and the Sea Cottage has been removed from it.
- Adds Auto Level Scaling that chooses the level scaling mode based on the Door Shuffle choice.
- Fixes issues with Flash and Fly move interventions (where it ensures an available Pokémon that can learn it is reachable depending on settings).
- Fixes a possible generation crash with type chart randomization.
- Should fix an issue where `stage_fill_hook` was able to remove the wrong item from the item pool resulting in a duplicated item reference existing.
- Adds a stage_post_fill function which searches for Pokémon in order of spheres, setting all but the first advancement Pokémon event found to `useful` so that spoiler playthrough calculation skips them. In a solo game gen test, this cut gen time from 15 seconds to 10 seconds with same seed number. Difference is likely to be much more massive in larger multiworlds.
2024-03-05 23:01:45 +01:00
Zach Parks
bfa9e7da00 Generate: Trim slot names again after 16 character limitation slice. (#2906) 2024-03-05 15:59:34 -06:00
t3hf1gm3nt
938beb34df TLOZ: use proper rule for completion condition (#2872)
Was pointed out that using `state.locations.checked` is not the best solution, even if it's for a completion condition and the Ganon event location would always have the Triforce of Power event item. So let's just check for the Triforce of Power instead. Thank you Zunawe for pointing it out and Silvris for providing the proper rule to use.
2024-03-05 22:55:59 +01:00
NewSoupVi
4ddfb7ce8b The Witness: Laser Hints (#2895) 2024-03-05 22:54:02 +01:00
NewSoupVi
b147c5bf8a The Witness: Bump required client version (#2897)
Bump required client version from 0.4.4 to 0.4.5.

The [client](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/tag/v5.0.0p14) now connects with version 0.4.5.
2024-03-05 22:53:00 +01:00
NewSoupVi
bf60e905ec The Witness: Fix absolute world import (#2905) 2024-03-05 22:51:29 +01:00
Seldom
6926f38414 Terraria: Broken Hero Sword reqs mech bosses (#2879) 2024-03-05 19:35:41 +01:00
Silvris
644f75978d Kirby's Dream Land 3: Implement New Game (#2119)
Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com>
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
Co-authored-by: Doug Hoskisson <beauxq@yahoo.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2024-03-05 09:59:55 -06:00
black-sliver
af4172f32f Docs: Add review expectations to contributing.md (#2843)
Co-authored-by: BadMagic100 <dempsey.sean@outlook.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-03-05 09:55:12 -06:00
Silent
45a15004a4 TUNIC: Update setup guide and game page docs (#2832) 2024-03-05 09:54:18 -06:00
black-sliver
ce43c52589 Doc: fix typo in commands_en.md (#2765) 2024-03-05 09:49:34 -06:00
Bryce Wilson
7384bbdf23 BizHawkClient: Add README (#2689)
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2024-03-05 09:48:37 -06:00
Nicholas Brochu
bcbb06d78d Fix usage of __new__ for SpecialRange compatibility fallback (#2513) 2024-03-05 09:46:09 -06:00
Remy Jette
26ee9fe05c Pokemon RB: Fix exceptions raised by /bank (#2836)
* If the user tried to run `/bank` with no arguments to see the current
  value while disconnected, previously it threw an exception `KeyError:
  'EnergyLinkNone'`. Now it informs the user that they must be connected
  and in-game, like `/bank deposit` and `/bank withdraw` do.

  I'm also open to adding another `if` branch to make `/bank` only check
  for `ctx.server` instead of combining it with the other bank commands,
  to allow connecting to check the bank before the game save is loaded.
  If that's preferred let me know.

* If the user tried to run `/bank` or `/bank deposit` when the EnergyLink
  hadn't been used yet, they would get a `TypeError` exception. Trying
  `/bank withdraw` would give no output and would crash the lua
  connector script. Now it treats a `None` EnergyLink as `0` and works
  properly.
2024-03-05 09:36:18 +01:00
Justus Lind
12cc930825 Muse Dash: Add Muse Dash 4.1.0 songs (#2878) 2024-03-05 09:33:15 +01:00
Exempt-Medic
b9d561ae25 Core: Update generic.Rules.py (#2896) 2024-03-05 03:55:46 +01:00
Doug Hoskisson
ecec931e9f Core: fix (typing) mistake in PR #2887 (#2891)
I made this variable for more compatible and safer type narrowing, and then I didn't use if for the type narrowing.
2024-03-04 08:26:52 +01:00
Alchav
a70b94fd62 LTTP: Open Pyramid and Shop Prog Balancing Bug Fixes (#2890) 2024-03-04 01:52:03 +01:00
Jérémie Bolduc
37a871eab1 Core: Allow common collections in OptionSet and OptionList constructors (#2874)
* allow common collection in set and list option constructors

* allow any iterable of strings

* add return None

---------

Co-authored-by: beauxq <beauxq@yahoo.com>
2024-03-03 22:30:51 +01:00
Doug Hoskisson
113c54f9be Zillion: remove rom requirement for generation (#2875)
* in the middle of work towards no rom for generation (not working)

* no rom needed for Zillion generation

* revert core changes
2024-03-03 22:10:14 +01:00
Doug Hoskisson
4e31e51d7a Core: clarify error message when reading an APContainer (#2887) 2024-03-03 20:09:06 +01:00
t3hf1gm3nt
519dffdb73 TLOZ: Fix Logic for Gleeok guarded locations (#2734)
Turns out you can't kill Gleeok with bombs or a candle as I happened to find out in a community async. While I'll be fine, a rare combination of settings could put all 4 possible weapons (the three levels of sword and the Magical Rod) to kill Gleeoks behind killing Gleeoks. This fix should prevent that from happening.

Note: Even though there are technically 5 weapons that can kill Gleeok in the pool because at the moment we have an extra copy of the base Sword, I want to future-proof this incase we make changes to the item pool later.

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-03-03 17:59:31 +01:00
Aaron Wagener
d124df72e4 Core: add specific can_reach helpers to CollectionState (#2867) 2024-03-03 17:25:21 +01:00
black-sliver
57d1fe6d79 Docs: add note for stage_assert_generate to settings api (#2885) 2024-03-03 17:00:32 +01:00
Doug Hoskisson
ef37ee81f9 Zillion: apworld-compatible package data (#2860)
* Zillion: apworld-compatible module data

* fixed `World` import
2024-03-03 16:23:02 +01:00
Aaron Wagener
526eb09089 Options: add a DeathLinkMixin dataclass to easily standardize death_link (#2355)
* Options: add a DeathLinkOption dataclass to easily standardize death_link

* rename to DeathLinkMixin

* Update worlds/messenger/options.py
2024-03-03 14:11:44 +01:00
Star Rauchenberger
b2f30d5fd0 Lingo: Add a third location to Starting Room (#2839)
Despite earlier efforts, there were still rare fill errors when door shuffle and color shuffle were on and early color hallways was off, because sphere 1 was too small. This turns "Starting Room - HI" back into a location, which should give the algorithm more room.

The "forced good item" pool has been reconsidered. The problem with the specific item that caused the recent failure (Welcome Back - Shortcut to Starting Room) is that it only provided one location when color shuffle was on, which is a net of zero considering that the GOOD LUCK check was forced. It will no longer show up as a good item unless color shuffle is off. On an opposite vein, Rhyme Room Doors will now show up even if color shuffle is on, because it gives color hallways access by itself.

A good item will only be forced onto GOOD LUCK now if there is more than one player.
2024-03-03 08:20:37 +01:00
wildham
b8bf67a166 FF1: Update Location Names (#2838) 2024-03-03 06:43:45 +01:00
Fabian Dill
2c5b2e0759 MultiServer: make !hint without further arguments only reply to the instigating player (#2339) 2024-03-03 06:34:48 +01:00
Fabian Dill
b65a3b7464 Subnautica: cleanup (#2828) 2024-03-03 06:33:48 +01:00
Silvris
01cf60f48d Launcher: make launcher scrollable (#2881) 2024-03-03 06:32:58 +01:00
Alchav
ad3ffde785 FFMQ: Remove debug print statements (#2882) 2024-03-03 06:31:22 +01:00
zig-for
f17ff15669 LADX: fix modifying item pool in pre_fill (#2060) 2024-03-03 06:28:26 +01:00
Bryce Wilson
983da12a03 Pokemon Emerald: Add exhaustive list of ROM changes (#2801) 2024-02-29 20:42:13 +01:00
NewSoupVi
564ec8c32e The Witness: Allow specifying custom trap weights (#2835)
* Trap weights

* Slightly change the way the option works

* Wording one more time

* Non optional to bring in line with Ixrec's implementation

* Be clear that it's not an absolute amount, but a weight

* E x c l a m a t i o n   p o i n t

* Update worlds/witness/items.py

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

* Wait I can just do this now lol

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-02-29 07:40:08 +01:00
Trevor L
7a85ee7ed1 Blasphemous: Remove poptracker pack from setup guide (#2759) 2024-02-29 02:56:20 +01:00
Jarno
e60a2636cd Docs: Fixed broken ClientStatus hyperlink in network protocol.md (#2844) 2024-02-29 02:40:59 +01:00
Danaël V
184dedfa69 Core: Default YAML header updates (#2723)
* Cleaning up (#4)

Cleanup

* Adressed change about spaces no longer being replaced to underscores.

Added a "that" to remove an ambiguity

* Update data/options.yaml

Combined the two sentences into one, per suggestion

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

---------

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2024-02-29 02:30:28 +01:00
Doug Hoskisson
5e06a75bf2 Core: typing: return type of fill_slot_data to Mapping (#2876)
* Core: typing: return type of `fill_slot_data` to `Mapping`

type checker be like:

"Wait a minute! If you give this mutable dict to those sussy sketchbags, they might mutate it and invalidate your more specific typing!"

Note that this doesn't mean the return value needs to be immutable. It just means the caller won't mutate it (which matches current `Main.py` implementation).

I've seen some talk of introducing ownership to the type system.

https://discuss.python.org/t/we-may-need-better-specification-for-existing-and-future-refinement-types-in-the-type-system/43955/5

Then maybe I could say: "Do whatever you want with it, because I'm giving up ownership."
But that doesn't exist in the type system currently.

* in docs too

* docs talk less about type and more about json

* keep `dict` to be safe with .net client and json
2024-02-29 02:22:42 +01:00
Hisu
3bc2c44ac3 Docs: Add Spanish Guide for Pokemon Emerald (#2696)
* Docs: Add Spanish Guide for Pokemon Emerald

* Docs: Add Spanish Guide for Pokémon Emerald

* Docs: Add Spanish Guide for Pokemon Emerald

* Docs: Add Spanish Guide for Pokemon Emerald

* Docs: Add Spanish Guide for Pokemon Emerald
2024-02-29 01:54:54 +01:00
Nicholas Saylor
7ebd5d3891 DS3: Modified theme and warning color for accessibility (#2312) 2024-02-29 01:26:52 +01:00
black-sliver
a659036e95 Docs: mention that IDs for items and locations can overlap (#2854)
* Docs: mention that IDs for items and locations can overlap

* Update docs/world api.md

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

---------

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>
2024-02-28 19:55:55 +01:00
PoryGone
36cee91a2c DKC3: Long-overdue World code cleanup (#2820)
Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
2024-02-28 04:53:13 +01:00
NewSoupVi
59a6e4a1b5 The Witness: New hint type ("area hints") (#2494)
This new type of "area hint" will instead give you general information about one of the named geographical areas in your world.
Example:

```
There are 4 progression items in the "Quarry" region.
Of them, 2 are for other players.
Also, one of them is a laser for this world.
```

This also renames some of the locations in the game to better fit into an "area", such as the "River Obelisk" being renamed to the "Mountainside Obelisk".

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-02-28 04:44:22 +01:00
black-sliver
c126418f35 Utils: YAML goes brrrt (#2868)
Also tests to validate we dont break the API.
2024-02-27 08:44:34 +01:00
Scipio Wright
738a9ebb7d TUNIC: Misc Logic Changes, Additions (#2856)
* Add nmg boss scav kill

* Add boss quick kills

* Fix name of orb

* Remove getting into zig with ice grapple

* Remove connection from quarry to zig

* Add a few missing dependent regions

* Separate the atoll statue and portal pad so that it doesn't assume you can get from one to the other without prayer
2024-02-26 08:30:20 +01:00
Alchav
5c05ab1527 LTTP: KDS Default on (#2850) 2024-02-25 22:28:33 +01:00
Scipio Wright
46fc8df36e TUNIC: Fix for incorrect Zig 3 ER rule (#2849)
* Fix for incorrect ER rule in zig 3

* Add nmg logic to this same connection
2024-02-25 22:27:19 +01:00
Aaron Wagener
8f7b0ee489 Core: don't allow region, location, or entrance with duplicate names (#2453) 2024-02-25 21:56:27 +01:00
Aaron Wagener
86a7ac466e Core: remove bad hardcoded behavior around plando_connections (#2170) 2024-02-25 04:45:23 +01:00
Ixrec
57fcd57a85 Docs: Clarify which kinds of options actually support "random" (#2845)
* Clarify which kinds of options actually support "random"

The current phrasing of this sentence made me expect "random" to work even on my OptionsDict option. After asking `#archipelago-dev` and checking the `Options.py` code, it's become clear that many option types don't (and can't) support "random". This is my best guess at a more correct wording.

* add a sentence about from_text overrides based on black-silver's suggestion
2024-02-24 17:01:54 +01:00
Scipio Wright
6bf4a94537 TUNIC: Use push_precollected for start_with_sword (#2857) 2024-02-23 19:41:59 +01:00
Fabian Dill
96163c6408 Core: provide convenience getters on World class (#2827) 2024-02-23 10:32:14 +01:00
black-sliver
afa5ce4afe CI: add static analysis for native code / cython (#2852)
* CI: add static analysis for native code / cython

* CI: scan-build: also run for requirements.txt
2024-02-23 10:11:00 +01:00
Alchav
b18641091f LTTP: Thieves' Town Big Chest fix (#2853) 2024-02-22 16:56:53 +01:00
NewSoupVi
f8981a4638 Docs: Better description for LocationScouts (#2674)
* Better description for LocationScouts

* Update network protocol.md

* typo

* Update docs/network protocol.md

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

* Update docs/network protocol.md

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

* Update docs/network protocol.md

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

* Update docs/network protocol.md

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

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-02-22 09:49:02 +01:00
Exempt-Medic
9f0d736aed Generate: Fix sphere calculation debug message (#2788) 2024-02-22 09:44:03 +01:00
Silvris
ffdcb91a13 CI: add missing core files to "affects: core" labelling (#2824)
* add missing files

* Change to wildcard
2024-02-21 09:51:22 +01:00
black-sliver
17c73916b7 Speedups: no cinit, no pickling (#2851)
* Speedups: remove unnecessary cinit

This was meant for (memory) safety, but cython docs clearly state that this
is done automatically. The code generated for cinit with args is what
triggers a 'possible null deref' in clang's static analyzer, so by removing
cinit, we can now use static analysis.

* Speedups: disable pickling ...

... of LocationStore and internal classes.
This reduces code size and avoids accidentally pickling them.
2024-02-21 08:53:54 +01:00
BootsinSoots
7fc159c881 Docs: Make all guide titles say Guide, for my sanity (and the webhost) (#2304) 2024-02-20 10:22:32 -06:00
black-sliver
38cc90efd0 TextClient: fix logging not always showing up (#2846) 2024-02-20 08:07:33 +01:00
Alchav
7a86285807 LttP: Bombless Start and Options/Shops overhaul (#2357)
## What is this fixing or adding?
Adds Bombless Start option, along with proper bomb logic. This involves updating `can_kill_most_things` to include checking how many bombs can be held. Many places where the ability to kill enemies was assumed, now have logic. This fixes some possible existing logic issues, for example: Mini Moldorm cave checks currently are always in logic despite the fact that on expert enemy health it would require 12 bombs to kill each mini moldorm.

Overhauls options, pulling them out of core and in particular making large changes to how the shop options work.


Co-authored-by: espeon65536 <81029175+espeon65536@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Bondo <38083232+BadmoonzZ@users.noreply.github.com>
Co-authored-by: espeon65536 <espeon65536@gmail.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2024-02-20 01:07:49 +01:00
Fabian Dill
933e5bacff Core: update requirements (#2716) 2024-02-20 00:25:51 +01:00
Bryce Wilson
818b0a49e1 Pokemon Emerald: Un-exclude locations that must contain progression (#2840) 2024-02-18 01:52:50 +01:00
Nikola-Em
523c7dbfad Lingo: MASTERY (Room) not require "gray" (#2792) 2024-02-17 06:50:51 +01:00
Silent
e8249d1f72 TUNIC: Rename ability item (#2834) 2024-02-16 23:25:20 +01:00
Scipio Wright
04b02f5a4a TUNIC: Add aliases to LogicRules (#2825) 2024-02-16 23:24:25 +01:00
Bryce Wilson
687af30d14 BizHawkClient: Use callbacks in connector script instead of else/ifs (#2784) 2024-02-16 08:59:57 +01:00
Scipio Wright
539307cf0b TUNIC: Universal Tracker Support Update (#2786)
Adds better support for the Universal Tracker (see its channel in the future game design section).
This does absolutely nothing regarding standard gen, just adds some checks for an attribute that only exists when UT is being used.
2024-02-16 05:03:51 +01:00
Star Rauchenberger
4d9202537c Lingo: Fix non-progressive The Colorful (#2782)
The Colorful did not actually properly split into individual doors when progressive colorful was off. This change refactors the code that handles special cases with progressive items to make things clearer (which is important because I will be introducing another one).
2024-02-16 00:19:54 +01:00
Aaron Wagener
3869a25944 Tests: assign the world to WorldTestBase, and a default player field (#2385)
* Tests: assign the World to WorldTestBase and add a player field (because I like typing self.player far more than random 1's all over the place)

* more accurate docstring for world and multiworld

* use self.player within the class
2024-02-15 23:49:52 +01:00
Fabian Dill
9805bf92e4 Core: fix comment that did more harm than good (#2826) 2024-02-15 23:34:29 +01:00
Bryce Wilson
057e372325 Pokemon Emerald: Shuffle initial TMs for diverse_balanced option (#2758) 2024-02-15 21:04:20 +01:00
Star Rauchenberger
2c38b9fd51 Lingo: Various item/location renames (#2746)
## What is this fixing or adding?
- Roof MASTERY panels are now technically in individual regions with more descriptive names, so they can be displayed better on the tracker.
- Orange Tower Seventh Floor - Mastery has been renamed to simply Mastery.
- The Optimistic is its own region now.
- The misnamed CEILING in Room Room has been fixed.
- The misnamed CHEESE in Challenge Room has been fixed.
- The misnamed SOUND in Outside the Bold has been fixed.
- "The Bearer - Shortcut to The Bold" is now "The Bearer - Entrance".
- HUB ROOM - NEAR, FAR and the Warts Straw and Leaf Feel Areas have now been semantically combined into the "Symmetry Room". They are still logically three separate regions.
- The FACTS chain in Challenge Room has been reindexed, and the full chain panel is now indicated as such.
- The Room Room floors have been reindexed.
- Panels in The Observant are now named by their questions, not answers.
- Added a (1) subscript to several panels in Orange Tower Fourth Floor, Outside The Initiated, and The Seeker.

The validate_config.rb script has also been updated to check that all items and locations have an ID.

This change should not impact generation logic at all. It is just changing item and location names.
2024-02-15 21:03:10 +01:00
Doug Hoskisson
475e803500 Core: APPatch interface (#2808)
define interface that has only the bare minimum required
for `Patch.create_rom_file`
2024-02-15 00:23:05 +01:00
Scipio Wright
f178d438b8 TUNIC: Fix duplicate entrance name in ER (#2818) 2024-02-15 00:05:48 +01:00
black-sliver
e5980ac5f5 Core: remove module level AutoWorld import (#2790)
With BaseClasses running `worlds.__init__.py` and worlds importing
`from BaseClasses`, this is likely to result in some extra code being run
because of partial recursive imports. This now lazily loads `worlds` when
needed, at which point `sys.modules` should be properly populated.
2024-02-14 22:56:21 +01:00
Star Rauchenberger
2167db5a88 Lingo: Split up Color Hunt and Champion's Rest (#2745) 2024-02-14 01:56:24 +01:00
Star Rauchenberger
2165253961 Lingo: Detach Art Gallery Exit from Progressive Art Gallery (#2739)
The final stage of Progressive Art Gallery opens up the four-way intersection between the Art Gallery, Orange Tower Fifth Floor, The Bearer, and Outside The Initiated. This is a very useful door, and it would be cool to be able to open it without having to get five progressive items. The original reason this was included in the progression was because getting into the back of Art Gallery early would cause sequence breaks. At this point, the way the client handles the Art Gallery has changed enough that it does not matter if the player can go through this door before getting all progressive art galleries.
2024-02-14 01:55:19 +01:00
Bryce Wilson
57fcdf4fbe Pokemon Emerald: Add missed locations to postgame locations group (#2654) 2024-02-13 22:47:57 +01:00
NewSoupVi
74e79bff06 The Witness: Event System & Item Classification System revamp (#2652)
Two things have been happening.

**Incorrect Events**
Spoiler logs containing events that just straight up have an incorrect name and shouldn't be there. E.g. "Symmetry Island Yellow 3 solved - Monastery Laser Activation" when playing Laser Shuffle where this event should not exist, because Laser Activations are governed by the Laser items.

Now to be clear - There are no logic issues with it. The event will be in the spoiler log, but it won't actually be used in the way that its name suggests.
Basically, every panel in the game has exactly one event name. If the panel is referenced by another panel, it will reference the event instead. So, the Symmetry Laser Panel location will reference Symmetry Island Yellow 3, and an event is created for Symmetry Island Yellow 3. The only problem is the **name**: The canonical name for the event is related to "Symmetry Island Yellow 3" is "Monastery Laser Activation", because that's another thing that panel does sometimes.

From now on, event names are tied to both the panel referencing and the panel being referenced. Only once the referincing panel actually references the dependent panel (during the dependency reduction process in generate_early), is the event actually created.

This also removes some spoiler log clutter where unused events were just in the location list.

**Item classifications**
When playing shuffle_doors, there are a lot of doors in the game that are logically useless depending on settings. When that happens, they should get downgraded from progression to useful. The previous system for this was jank and terrible. Now there is a better system for it, and many items have been added to it. :)
2024-02-13 22:47:19 +01:00
Ishigh1
3ca3417172 LADX: Added some resilience to non-ASCII player names (#2642)
* Added some resilience to non-ASCII player names or items

* Also the client, not even sure if switching to ascii is useful here

* Split a long line in two
2024-02-13 22:46:18 +01:00
BadMagic100
55455914e6 CI: Add a workflow which automates some labeling (#2812)
* Initial content-based labeling

* Improve labeling rules around docs and /worlds/generic

* Improve labeling rules around docs and webhost

* Formatting

* Update matching for webhost

* back to square 1 on is:docu

* Try a better glob for docs

* Formatting

* Manage PR state labels

* Correct syntax for conditions

* Correct syntax for conditions

* add trigger on reopening

* add trigger on closing

* keep labels in sync as pr updates

* Change edit event to sync

* Restrict only to PRs to main

* address review comments

* apply only to PRs into main
2024-02-13 07:48:33 +01:00
Magnemania
0c8f726393 SM64: Move Randomizer Content Update (#2569)
* Super Mario 64: Move Randomizer Update

Co-authored-by: RBman <139954693+RBmans@users.noreply.github.com>
Signed-off-by: Magnemania <magnemight@gmail.com>

* Fixed logic for Vanish Cap Under the Moat

Signed-off-by: Magnemania <magnemight@gmail.com>
2024-02-13 06:14:21 +01:00
Alchav
6f3bc3a7ad Core: Minimal-Items Accessibility Fix (#1888) 2024-02-13 02:45:39 +01:00
Scipio Wright
151e2c3ac2 TUNIC: Add an ER static connection, modify an nmg rule (#2802)
* Add laurels connection at monastery front

* Removed an entrance rule to prevent people from being expected to softlock themselves
2024-02-11 03:15:46 +01:00
NewSoupVi
a6deffb9f2 The Witness: Change all option name comparisons to strings instead of numeric values (#2503)
* Refactor postgame code to be more readable

* Change all references to options to strings

* oops

* Fix some outdated code related to yaml-disabled EPs

* Small fixes to short/longbox stuff (thanks Medic)

* comment

* fix duplicate

* Removed triplicate lmfao

* Better comment

* added another 'unfun' postgame consideration

* comment

* more option strings

* oops

* Remove an unnecessary comparison

* another string missed

* Another was missed

* This would create a really bad merge error
2024-02-11 02:25:03 +01:00
black-sliver
77c326cb81 FFMQ: fix __version__ import in Output.py (#2791)
Importing from Main is a recursive import during Generate, also it's not
listed in Main.__all__ (and pycharm warns about this).
2024-02-11 01:07:23 +01:00
Alchav
1a675821cf Pokémon R/B: Halve Bank Exchange Rate (#2619) 2024-02-11 00:59:15 +01:00
JaredWeakStrike
03c3ef4e72 KH2: Fix Final Form logic softlock (#2803) 2024-02-11 00:50:38 +01:00
Justus Lind
4a703c5aba Muse Dash: Add support for Muse Dash 4.0.0 Songs (#2810) 2024-02-11 00:49:58 +01:00
Exempt-Medic
59ef010842 Fill: Changing deprecated option getter (#2735) 2024-02-10 22:07:11 +01:00
Fabian Dill
4032cfb9ea WebHost: provide None password to URI so it doesn't get stripped (#2777) 2024-02-05 18:11:02 -05:00
PoryGone
281fe01c25 Core: Purge the evil (world: MultiWorld) (#2749)
* Purge the evil

* Some files didn't save

* Fix a couple of missed string references

* multi_world -> multiworld
2024-02-05 00:38:00 +01:00
Fabian Dill
6c19bc42bb Tests: add world load benchmark (#2768) 2024-02-04 09:09:07 +01:00
Alchav
33237bd5c0 LTTP: Create Hyrule Castle Big Key Rule On Universal Small Keys Option (#2787) 2024-02-03 06:45:37 +01:00
NewSoupVi
57cb971177 The Witness: Junk hints for Shivers, Mystic Quest and Heretic (#2592) 2024-01-31 21:07:07 +01:00
t3hf1gm3nt
140f802564 LTTP: Update playerSettings.yaml to require AP version 0.4.4 (#2737)
Updating yaml to make sure people have the proper required version if they ever use this template to play with KDS
2024-01-31 08:01:55 +01:00
Star Rauchenberger
3a51c035ac Lingo: Enable start_inventory_from_pool (#2781) 2024-01-31 07:56:35 +01:00
Aaron Wagener
016c1e9bb4 Docs: world api general cleanup/overhaul (#2598)
* Docs: world api general cleanup/overhaul

* add pep-0287 to style doc

* some cleanup, reorganization, and grammar improvements

* reorder item and region creation

* address review comments

* fix indent

* linter grammar

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

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-01-30 21:42:33 +01:00
Alchav
697deb98b4 Pokémon R/B: Fix Thunder Stone item groups #2740 2024-01-30 09:06:10 +01:00
Ixrec
144769a141 Tests: use strict equality in some tests # (#2778)
* Tests: replace .assertLess/GreaterEqual() with .assertEqual() in two tests where strict equality seems more correct
2024-01-30 09:00:47 +01:00
Benny D
dc49d50c2c Docs: fixed typo in Stardew Valley setup guide (#2770)
* fix typo

* fix another typo

* Update worlds/stardew_valley/docs/en_Stardew Valley.md

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

---------

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2024-01-30 08:58:31 +01:00
black-sliver
5663c21f39 Tests: test that item/location name groups are not empty (#2748)
* Tests: test that item/location name groups are not empty

* Tests: better name for test_groups TestCase

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2024-01-30 08:34:54 +01:00
black-sliver
1b188bab3c Doc: add GM libs to network protocol.md (#2744) 2024-01-30 08:21:23 +01:00
JaredWeakStrike
69c80501c4 KH2: Fix empty location groups (#2757)
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2024-01-29 18:39:45 +01:00
JusticePS
0bc9966d6f Adventure: Fix iterable copy error when freeincarnate_max is tuned low (#2774) 2024-01-29 18:37:55 +01:00
Scipio Wright
b4212d1c3e TUNIC: Fix for nmg logic bug (#2772) 2024-01-28 21:13:03 +01:00
black-sliver
aa72f671bc SoE: fix naming of atlas medallion (#2747)
In pyevermizer, it's called Atlas Medallion, not Amulet, leading to an
empty group and to code not considering them as an alchemy ingredient
when swapping out for a trap or an energy core fragment.

Also adds a test.
2024-01-21 19:34:24 +01:00
Scipio Wright
5f9ce2b7b6 Noita: Update to use new Options API (#2370)
Reworking the options to make it work with the new options API.
Also reworked stuff in several spots to use world: NoitaWorld instead of multiworld: MultiWorld
2024-01-19 21:31:45 +01:00
zig-for
1307754f02 LADX: music shuffle (#2101) 2024-01-19 21:14:26 +01:00
Bicoloursnake
ac7b707e3e OOT: Adjust the Logic Trick Keys to be an ordered object (#2736) 2024-01-18 02:18:03 +01:00
Star Rauchenberger
ec440b7785 Lingo: NORTH requires hint panels (#2732) 2024-01-18 01:58:48 +01:00
Danaël V
4c901dcfc0 TUNIC: Change Tunic to TUNIC (#2720) 2024-01-18 01:56:34 +01:00
Fabian Dill
834b6e35b4 Setup: auto update vc redist (#2502) 2024-01-18 01:52:33 +01:00
Fabian Dill
602c2966fc LttP: move _hint_text to SubClasses (#2532) 2024-01-16 17:23:18 +01:00
black-sliver
49ecd4b9c1 CI: flake8: max-complexity=14 (#2731)
The value of 10 does not really fit some of our world patterns and values
up to 15 may be acceptable. Looking at some worlds, 14 seems to be
achievable without too much work and reduces the noise in test output,
making it more usable.
2024-01-16 17:10:58 +01:00
black-sliver
de8fe21d4a Tests: create sane cov defaults (#2728) 2024-01-16 17:10:19 +01:00
NewSoupVi
4fdeec4f70 The Witness: Cleanup - Options Access, data version, snake_case for file name (#2631) 2024-01-16 15:33:34 +01:00
NewSoupVi
71a3e2230d The Witness: Allow Mountain Lasers to go up to 11 instead of 7. (#2618) 2024-01-16 15:27:09 +01:00
JaredWeakStrike
325a510ba7 KH2: Promise charm logic (#2635) 2024-01-16 15:26:18 +01:00
NewSoupVi
5dcaa6ca20 The Witness: Death Link Amnesty (#2646) 2024-01-16 15:24:10 +01:00
NewSoupVi
e15873e861 The Witness: Bonk trap support (#2645) 2024-01-16 15:23:30 +01:00
NewSoupVi
5c7bae7940 The Witness: Local Laser Shuffle + Option Presets (#2590)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-01-16 15:14:06 +01:00
NewSoupVi
e6f7ed5060 The Witness: Progressive Symmetry (#2644) 2024-01-16 15:13:04 +01:00
NewSoupVi
1c2dcb7b01 The Witness: Add Desert Control Panels (#2643) 2024-01-16 15:11:52 +01:00
Star Rauchenberger
5df7a8f686 Lingo: Disable forced good item when early color hallways is on (#2729) 2024-01-16 15:10:59 +01:00
Bryce Wilson
3a588099bd Pokemon Emerald: Automatically exclude locations based on goal (#2655) 2024-01-16 15:09:47 +01:00
Yussur Mustafa Oraji
d000b52ae0 V6: Use new options api (#2668)
* v6: Use new options API

* v6: Add display names for some options
2024-01-16 13:38:19 +01:00
NewSoupVi
fe3bc8d6be The Witness: Add Obelisk Side locations to always and priority hints (#2665) 2024-01-16 13:14:38 +01:00
NewSoupVi
7affb885ba The Witness: Add "Town Desert Laser Redirect Control (Panel)" as an item (#2669) 2024-01-16 13:13:44 +01:00
Star Rauchenberger
d390d2eff8 Lingo: Remove colors from Bearer SIXes (#2677) 2024-01-16 13:13:02 +01:00
JaredWeakStrike
0efc13fc8a KH2: Location Groups and Subclasses (#2700) 2024-01-16 13:12:33 +01:00
Star Rauchenberger
c6896c6af9 Lingo: Make The Colorful optionally progressive (#2711) 2024-01-16 13:11:20 +01:00
Star Rauchenberger
adad7b532d Lingo: Turn The Colorful into a countdown achievement (#2710)
The Colorful currently, in logic, does not expect you to solve the achievement panel until all of the doors are opened. This is not enforced by the client in complex door shuffle. It is also not typical of how achievements in Lingo usually work, and it ended up this way because of the fact that The Colorful is, uniquely, not a countdown panel. This change modifies logic so that solving each panel within The Colorful is required in order to access the achievement, rather than opening all of the doors. This will be accompanied by a change to the client that will turn the achievement panel into a countdown.
2024-01-16 13:09:54 +01:00
Held_der_Zeit
d756960a0b Worlds Docs: Translations German (Clique, BK Sudoku, OoT) (#2581)
* Sudoku German

* German OOT (+ Room Image)

* German Clique

* german translation

* translation flexibility - ff1

* german setup - oot

* Transaltion Flexibilty - SM64

* translation flexibilty - factorio

* translation flexibilty - kh2

* translation flexibility - Super Metroid

* translation flexibility - Stardew Valley

* german translation added - clique

* translation flexibility - terraria

* translation flexibilty - checksfinder

* Sudoku Setup - Grammar Fix

* Sudoku Main - Fix Grammar

* Revert "translation flexibility - ff1"

This reverts commit 6df434c682.

* Revert "Transaltion Flexibilty - SM64"

This reverts commit 754bf95d2f.

* Revert "translation flexibilty - factorio"

This reverts commit db1226a9de.

* Revert "translation flexibility - Super Metroid"

This reverts commit ca5bd9a64a.

* Revert "translation flexibilty - kh2"

This reverts commit 076534ee32.

* Revert "translation flexibility - Stardew Valley"

This reverts commit 4b13701394.

* Revert "translation flexibility - terraria"

This reverts commit a0abfc8a03.

* Revert "translation flexibilty - checksfinder"

This reverts commit a4de49961d.

* Sugesstion - Fixes in Grammar (and Typos)

One or two suggesstions need to be changed a bit further (such as an incomplete sentence)

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

* Apply suggestions from code review

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

* Apply suggestions from code review

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

* Update guide_de.md

* Update setup_de.md

* Update de_Sudoku.md

* Update __init__.py

* Update worlds/oot/docs/setup_de.md

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-01-16 06:54:48 +01:00
Alchav
30ec080449 FFMQ: Reset protection (#2727)
Bizhawk's "hard reset" option fills RAM with 0x55s. This causes game completion to be erroneously flagged, and likely many erroneous location checks with it. This fix checks for 0x55 and will not proceed to process anything if present.
2024-01-16 01:21:02 +01:00
Fabian Dill
79e2f7e357 Tests: test that World.options is not set on the class (#2725)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-01-15 20:50:16 +01:00
t3hf1gm3nt
b4077a0717 TLOZ: properly assign options (#2726)
whoops used a = instead of a :
mad that im doing a literal one character change PR :/
2024-01-15 20:19:18 +01:00
black-sliver
518b04c08e SoE: minor typing and style fixes (#2724)
* SoE: fix typing for tests

* SoE: explicitly export pyevermizer

To support loading the module from source (rather than module) we import
pyevermizer from `__init__.py` in other files. This has been an implicit export
and `mypy --strict` disables implicit exports, so we export it explicitly now.

* SoE: fix style in patch.py

* SoE: remove unused imports

* SoE: fix format mistakes

* SoE: cleaner typing in SoEOptions.flags

as suggested by beauxq
2024-01-15 09:17:46 +01:00
GodlFire
d10f8f66c7 Shivers: Fix rule logic for location 'Final Riddle: Guillotine Dropped' (#2706) 2024-01-15 04:48:44 +01:00
t3hf1gm3nt
6d393fe42b TLOZ: update to new options API (#2714) 2024-01-15 04:47:32 +01:00
agilbert1412
5b93db121f Stardew Valley: Added missing rule on the club card (#2722) 2024-01-15 04:29:30 +01:00
Fabian Dill
ad074490bc Test: add location access rule benchmark (#2433) 2024-01-14 21:30:00 +01:00
Fabian Dill
6ac3d5c651 Core: set consistent server defaults (#2566) 2024-01-14 21:24:34 +01:00
Doug Hoskisson
ed6b7b2670 Zillion: remove old option access from item link validation (#2673)
* Zillion: remove old option access from item link validation
and a little bit a cleaning in other stuff nearby

* one option access missed
2024-01-14 15:48:30 +01:00
Doug Hoskisson
6904bd5885 Typing: improve kivy type stubs (#2681) 2024-01-14 15:31:13 +01:00
black-sliver
962b9b28f0 Setup: don't install webhost dependencies (#2717)
also makes ModuleUpdate detect changed requirements for update()
2024-01-14 03:09:03 +01:00
Aaron Wagener
37b03807fd Core: Log the worlds still using the old options API (#2707) 2024-01-14 03:04:12 +01:00
Aaron Wagener
73e41cb701 Core: migrate start_inventory_from_pool to new options API (#2666)
* Core: migrate start_inventory_from_pool to new options API

* get the other spot too

* skip {}

* oops

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-01-14 02:57:53 +01:00
Aaron Wagener
cfd758168c Tests: add a test for worlds to not modify the itempool after create_items (#1460)
* Tests: add a test for worlds to only modify the itempool in `create_items`

* extend test multiworld setup instead of a new function

* cleanup the test a bit

* put more strict wording in `create_items` docstring

* list of shame

* Don't call `set_rules` before testing

* remove ChecksFinder from the list of shame

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2024-01-14 02:15:35 +01:00
Nicholas Saylor
01fb44c186 Docs: Added Disabled World information to README.md (#2705)
* Add rationale for OriBF being disabled

* Removed periods

* Added warning to README.md

* Apply suggestions from code review

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>

* Added disable date

Meant to provide context for any updates the world may need (For example, this world would need to change to the new options sstem in 0.4.4)

* Moved rationale to local README
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Apply suggestions from code review

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

---------

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-01-13 21:10:16 +01:00
Aaron Wagener
2725c0258f Docs: specify that deathlink cause should contain the player name (#2557)
* Docs: specify that the cause should contain the player name

* accidental whitespace moment

* fix table formatting
2024-01-13 19:23:14 +01:00
black-sliver
0c0adb0745 Core: update kivy (#2718) 2024-01-13 18:01:36 +01:00
Scipio Wright
4a85f21c25 TUNIC: Update game page for blurb about playing vanilla first (#2712)
* Update en_Tunic.md

* Change emphasis a bit

* Move the "haven't played before" section up

* settings -> options

* Update worlds/tunic/docs/en_Tunic.md

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

* Update setup as well with settings -> options and some recent changes to the in-game settings

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-01-13 15:12:43 +01:00
Silent
3933fd3929 TUNIC: Implement New Game (#2172) 2024-01-12 20:32:15 +01:00
Ame
b241644e54 Docs: add FR guide for DLCQuest (#2699)
* Docs: add Translate FR guide for DLCQuest

* Add Translate

* fix

* Update worlds/dlcquest/docs/fr_DLCQuest.md

Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com>

* Fix Translate

* Fix translate

* Update __init__.py

* Update worlds/dlcquest/__init__.py

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

* Update worlds/dlcquest/__init__.py

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

---------

Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-01-12 20:26:50 +01:00
black-sliver
e00b5a7d17 SoE: use new AP API and naming and make APworld (#2701)
* SoE: new file naming

also fixes test base deprecation

* SoE: use options_dataclass

* SoE: moar typing

* SoE: no more multiworld.random

* SoE: replace LogicMixin by SoEPlayerLogic object

* SoE: add test that rocket parts always exist

* SoE: Even moar typing

* SoE: can haz apworld now

* SoE: pep up test naming

* SoE: use self.options for trap chances

* SoE: remove unused import with outdated comment

* SoE: move flag and trap extraction to dataclass

as suggested by beauxq

* SoE: test trap option parsing and item generation
2024-01-12 01:07:40 +01:00
Alchav
47dd36456e Pokémon R/B: Fix move intervention (#2687) 2024-01-12 00:49:54 +01:00
Fabian Dill
4ce8a7ec4d PyCharm: ship a working unittest run config (#2694) 2024-01-12 00:49:14 +01:00
Kory Dondzila
a99c1e15ad Shivers: Fixes issue with office elevator rule logic. (#2690)
Office elevator logic was written as
can reach Underground Tunnels OR can reach Office AND have Key for Office Elevator

Meaning that key for office elevator was not required if Underground Tunnels could be reached when it should be.

Changed to
(can reach Underground Tunnels OR can reach Office) AND have Key for Office Elevator
2024-01-12 00:48:22 +01:00
Fabian Dill
44de140add SC2: run download_data via concurrent.futures (#2704) 2024-01-12 00:40:33 +01:00
Doug Hoskisson
ac2387e17c Tests: remove deprecated option access from WorldTestBase (#2671)
* remove deprecated option access from `WorldTestBase`

* one in test_reachability
2024-01-12 00:22:04 +01:00
Danaël V
2760deb5b6 Docs: Fix broken link in Landstalker setup Guide (#2680)
* Cleaning up (#4)

Cleanup

* Update landstalker_setup_en.md

Fixed Redirect
2024-01-12 00:18:11 +01:00
Remy Jette
f530895c33 WebHost: Fix /api/generate (#2693) 2024-01-11 17:44:12 -05:00
Justus Lind
b6f3ccb8c5 Touhou Mugakudan 3 song update. (#2659)
- Adds all the songs from the Touhou Mugakudan -Ⅲ- update. 
- Increases the upper limit of additional songs to 508 due to there being 512 songs now.
- Finally fixes umpopoff. As it was the only song that had less than 3 difficulties but also didn't have proper difficulty values
2024-01-11 23:13:39 +01:00
Flori
388413fcdd Hollow Knight: Fix fragile/unbreakable charm variants counting as 2 distinct charms in logic (#2683)
Deletes CHARM of the 3 unbreakable charms, adds 0.5 CHARM to Queen_fragment, King_Fragment and Void_heart
2024-01-11 23:10:25 +01:00
JaredWeakStrike
4045c6a9cf KH2: Fix relative import (#2702) 2024-01-11 00:56:43 +01:00
JaredWeakStrike
e082c83dc7 KH2: Fix plando breaking because of keyblades (#2678) 2024-01-10 18:22:54 +01:00
Doug Hoskisson
82410fd554 Zillion: client win location check (#2682) 2024-01-10 17:52:43 +01:00
JaredWeakStrike
570ba28bee KH2: Fix Terra logic (#2676) 2024-01-10 06:22:04 +01:00
Alchav
b0638b993d FFMQ: Fix starting progressive gear (#2685) 2024-01-09 03:57:38 +01:00
lordlou
89f211f31e SMZ3: 0.4.4 backward compat client fix (#2667)
fixed broken client compatibility with any seed generated before 0.4.4 introduced with the recent change to the message queue.
2024-01-07 13:13:52 +01:00
Fabian Dill
70fdd6b90d Core: clean up MultiServer.py/auto_shutdown (#2552) 2024-01-07 01:42:57 +01:00
Fabian Dill
f22daca74e CommonClient: request datapackage per-game (#2563) 2024-01-07 01:42:16 +01:00
Danaël V
064a7bf01b Docs: Update regarding new launcher (#2627)
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-01-04 21:03:39 -06:00
JaredWeakStrike
02a9430ad5 KH2: Update docs and logic sheet to reflect 4.4 (#2610) 2024-01-04 08:58:41 -06:00
JaredWeakStrike
c19afa4f4e KH2: setup guide update to reflect main rando (#2633) 2024-01-04 08:57:40 -06:00
Zach Parks
c593a960f6 WebHost: Fix 500 Server errors relating to player/multi trackers. (#2664)
* WebHost: Fix player tracker issue with items missing from data package.

 Reported in https://discord.com/channels/731205301247803413/1192202112172576819

* WebHost: Fix multi-tracker error when item links are present.

 Reported in https://discord.com/channels/731205301247803413/1192104719959724062

* Use Utils.KeyedDefaultDict instead of checking for key

* formatted revert

* import tweak
2024-01-04 08:29:42 -06:00
Zach Parks
7406a1e512 WebHost: Copyright update time. (#2660) 2024-01-03 18:43:41 -06:00
Aaron Wagener
0df0955415 Core: check if a location is an event before excluding it (#2653)
* Core: check if a location is an event before excluding it

* log a warning

* put the warning in the right spot
2024-01-02 15:03:39 +01:00
Bryce Wilson
bf17582c55 BizHawkClient: Add some handling for non-string errors (#2656) 2024-01-02 11:32:03 +01:00
JaredWeakStrike
e5c739ee31 KH2: Ability dupe fix and stat increase fix (#2621)
Makes the client make sure the player has the correct amount of stat increase instead of letting the goa mod (apcompanion) do it

abilities: checks the slot where abilities could dupe unless that slot is being used for an actual abiliity given to the player
2024-01-02 11:19:57 +01:00
GodlFire
88c7484b3a Shivers: Fixes rule logic for location 'puzzle solved three floor elevator' (#2657)
Fixes rule logic for location 'puzzle solved three floor elevator'. Missing a parenthesis caused only the key requirement to be checked for the blue maze region.
2024-01-02 11:16:45 +01:00
Doug Hoskisson
c104e81145 Zillion: move client to worlds/zillion (#2649) 2024-01-01 13:42:41 -06:00
wildham
3d1be0c468 FF1: Fix terminated_event access_rule not getting set (#2648) 2024-01-01 18:13:35 +01:00
lordlou
e674e37e08 SMZ3: optimized message queues (#2611) 2023-12-28 16:43:16 -06:00
Jarno
d1a17a350d Docs: Add missing Get location_name_groups_* to network protocol (#2550) 2023-12-28 14:41:24 +01:00
Fabian Dill
24ac3de125 Factorio: "improve" default start items (#2588)
Makes it less likely that people kill themselves via pollution and gives them some healing items they may not even know about.
2023-12-28 14:30:10 +01:00
Scipio Wright
901201f675 Noita: Don't allow impossible slot names (#2608)
* Noita: Add note about allowable slot names

* Update character list

* Update init to raise an exception if a yaml has bad characters

* Slightly adjust exception message
2023-12-28 14:21:54 +01:00
t3hf1gm3nt
c7617f92dd TLOZ: Try accounting for non_local_items with the pool of starting weapons (#2620)
It was brought up that if you attempt to non_local any of the starting weapons, there is still a chance for it to get chosen as your starting weapon if you are on a StartingPosition value lower than very_dangerous. This fix will attempt to build the starting weapons list accounting for non_local items, but if all possible weapons have been set to non_local, force one of them to be your starting weapon anyway since the player is still expecting a starting weapon in their world if they have chosen one of the lower StartingPosition values.
2023-12-28 14:17:23 +01:00
NewSoupVi
8e708f829d The Witness: Fix an instance of multiworld.random being used (#2630)
o_o
2023-12-28 14:12:37 +01:00
Fabian Dill
7af654e619 WebHost: validate uploaded datapackage and calculate own checksum (#2639) 2023-12-28 13:57:41 +01:00
TheLynk
af1f6e9113 Oot : Update setup fr (#2394)
* add new translation

* Add translation for OOT Setup in french

* Update setup_fr.md

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/minecraft/docs/minecraft_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/minecraft/docs/minecraft_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/minecraft/docs/minecraft_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/minecraft/docs/minecraft_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/minecraft/docs/minecraft_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/minecraft/docs/minecraft_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/minecraft/docs/minecraft_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/minecraft/docs/minecraft_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update setup_fr.md

Fix treu to true

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Marech <marechal-l@gmx.com>

* Update OOT Init and Update Minecraft Init

* Fix formatting errors

* Fix wrong link in stardew valley randomizer setup guide

Fix wrong link in stardew valley randomizer setup guide

* Add new translation for Adventure and Archipidle in french

Add new translation for Adventure and Archipidle in french

* Add more store in setup page subnautica for more fairness

Add more store in setup page subnautica for more fairness

* tweak update merge #1685 for lua file

tweak update merge #1685 for lua file

* fix text

fix text

* fix wrong translation

fix wrong translation

* Yes it's better

Yes it's better

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

* Update OOT Setup FR

Update OOT Setup FR

* Tweak Text

Tweak Text

---------

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-12-28 13:43:42 +01:00
Bryce Wilson
04d194db74 Pokemon Emerald: Change "settings" to "options" in docs (#2517)
* Pokemon Emerald: Change "settings" to "options" in docs

* Pokemon Emerald: Fix two more usages of "setting" instead of "option"

* Pokemon Emerald: Minor rephrase in docs

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

---------

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2023-12-28 13:33:30 +01:00
Rosalie-A
70eb2b58f5 [TLOZ] Fix bug with item drops in non-expanded item pool (#2623)
There was a bug in non-expanded item pool where due to the base patch changes to accommodate more items in dungeons, some items were transformed into glitch items that removed bombs (this also happened in expanded item pool, but the item placement would overwrite the results of this bug so it didn't appear as frequently). Being a Zelda game, losing bombs is bad. This PR fixes the base patch process to avoid this bug, by properly carrying the value of a variable through a procedure.
2023-12-28 12:16:38 +01:00
Alchav
576c705106 Pokémon R/B: Badge plando fix (#2628)
Only attempt to place badges in badge locations if they are empty. Return unplaced badges to the item pool if fewer than 8 locations are being filled.
This should fix errors that occur when items are placed into badge locations via plando, or whatever other worlds may do.
2023-12-28 12:15:48 +01:00
lordlou
b99c734954 SM: strict rom validation fix (#2632)
added a more robust ROM tag validation to free oher games to use tag starting with "SM" followed by another letter (SMW, SMZ3, SMRPG, SMMR,...)
2023-12-28 12:14:13 +01:00
Yussur Mustafa Oraji
7c70b87f29 sm64ex: Fix randomizing Courses and Secrets separately (#2637)
Backported from #2569
2023-12-28 08:01:48 +01:00
Trevor L
2512eb7501 Hylics 2: Add missing logic (#2638) 2023-12-28 06:25:41 +01:00
CaitSith2
bb0a0f2aca Factorio: Fix unbeatable seeds where a science pack needs chemical plant (#2613) 2023-12-22 20:02:49 -08:00
Fabian Dill
0d929b81e8 Factorio: fix files from mod base directory not being grabbed correctly in non-apworld (#2603) 2023-12-21 04:26:41 +01:00
Fabian Dill
8842f5d5c7 Core: make update_reachable_regions local variables more wordy (#2522) 2023-12-21 04:11:11 +01:00
Star Rauchenberger
817197c14d Lingo: Tests no longer disable forced good item (#2602)
The static class with the "disable forced good item" field is gone. Now, certain tests that want to check for specific access progression can run a method that removes the forced good item and adds it back to the pool. Tests that don't care about this will collect the forced good item like normal. This should prevent the intermittent fill failures on complex doors unit tests, since the forced good item should provide enough locations to fill in.
2023-12-18 09:46:24 -06:00
Alchav
c8adadb08b Pokémon R/B: Fix Flash learnable logic (#2615) 2023-12-18 09:39:04 -06:00
Zach Parks
a549af8304 Hollow Knight: Add additional DeathLink option and add ExtraPlatforms option. (#2545) 2023-12-17 10:11:40 -06:00
Fabian Dill
4979314825 Webhost: open graph support for /room (#2580)
* WebHost: add Open Graph metadata to /room

* WebHost: Open Graph cleanup
2023-12-17 00:08:40 -05:00
Silvris
f958af4067 Adventure: Fix KeyError on Retrieved (#2560) 2023-12-16 22:22:51 +01:00
Aaron Wagener
7dff09dc1a Options: set old options api before the world is created (#2378) 2023-12-16 22:21:05 +01:00
lordlou
c56cbd0474 SM: item link replacement fix (#2597) 2023-12-16 04:28:54 +01:00
PoryGone
6c4fdc985d SA2B: Fix Weapons Bed - Omochao 2 Logic (#2605) 2023-12-16 04:16:36 +01:00
Alchav
b500cf600c FFMQ: Actually fix the spellbook option (#2594) 2023-12-16 04:16:13 +01:00
Alchav
394633558f ALTTP: Restore allow_excluded (#2607)
Restores allow_excluded to the dungeon fill_restrictive call, which was apparently removed by mistake during merge conflict resolution
2023-12-15 20:39:09 +01:00
Alchav
3e3af385fa Pokémon R/B: client locations import (#2596) 2023-12-13 23:57:14 +01:00
Yussur Mustafa Oraji
ff556bf4cc sm64ex: Fix server (#2599) 2023-12-13 23:46:46 +01:00
Alchav
a3b0476b4b LTTP: Boss rule fix (#2600) 2023-12-13 23:34:36 +01:00
Zach Parks
0eefe9e936 WebHost: Some refactors and additional checks when uploading files. (#2549) 2023-12-12 20:12:16 -06:00
Aaron Wagener
db1d195cb0 Hollow Knight: remove unused option check (#2595) 2023-12-12 20:11:10 -06:00
Bryce Wilson
45fa9a8f9e BizHawkClient: Add SGB to systems using explicit vblank callback (#2593) 2023-12-12 05:48:20 +01:00
Alchav
e9317d4031 FFMQR: Fix Empty Kaeli Companion Event Location and Spellbook option (#2591) 2023-12-12 02:39:38 +01:00
Aaron Wagener
d9d282c925 Tests: test that the datapackage after generation is still valid (#2575) 2023-12-12 02:14:44 +01:00
Fabian Dill
13122ab466 Core: remove start_inventory_from_pool from early_items (#2579) 2023-12-10 20:42:41 +01:00
Fabian Dill
e8f96dabe8 Core: faster prog balance (#2586)
* Core: rename world to multiworld in balance_multiworld_progression

* Core: small optimization to progression balance speed
2023-12-10 20:42:07 +01:00
Fabian Dill
1a05bad612 Core: update modules (#2551) 2023-12-10 20:38:49 +01:00
NewSoupVi
8142564156 The Witness: Fix non-deterministic hints (#2514) 2023-12-10 20:36:55 +01:00
NewSoupVi
e2109dba50 The Witness: Fix Logic Error for Keep Pressure Plates 2 EP in puzzle_randomization: none (#2515) 2023-12-10 20:35:46 +01:00
Yussur Mustafa Oraji
3a09677333 sm64ex: Fix generations (#2583) 2023-12-10 20:31:43 +01:00
Star Rauchenberger
d3b09bde12 Lingo: Fix entrance checking being broken on default settings (#2506)
The most serious issue this PR addresses is that entrances that use doors without items (a small subset of doors when door shuffle is on, but *every* door when door shuffle is off, which is the default) underestimate the requirements needed to use that entrance. The logic would calculate the panels needed to open the door, but would neglect to keep track of the rooms those panels were in, meaning that doors would be considered openable if you had the colors needed to solve a panel that's in a room you have no access to.

Another issue is that, previously, logic would always consider the "ANOTHER TRY" panel accessible for the purposes of the LEVEL 2 panel hunt. This could result in seeds where the player is expected to have exactly the correct number of solves to reach LEVEL 2, but in reality is short by one because ANOTHER TRY itself is not revealed until the panel hunt is complete. This change marks ANOTHER TRY as non-counting, because even though it is technically a counting panel in-game, it can never contribute to the LEVEL 2 panel hunt. This issue could also apply to THE MASTER, since it is the only other counting panel with special access rules, although it is much less likely. This change adds special handling for counting THE MASTER. These issues were possible to manifest whenever the LEVEL 2 panel hunt was enabled, which it is by default.

Smaller logic issues also fixed in this PR:

* The Orange Tower Basement MASTERY panel was marked as requiring the mastery doors to be opened, when it was actually possible to get it without them by using a painting to get into the room.
* The Pilgrim Room painting item was incorrectly being marked as a filler item, despite it being progression.
* There has been another update to the game that adds connections between areas that were previously not connected. These changes were additive, which is why they are not critical.
* The panel stacks in the rhyme room now require both colours on each panel.
2023-12-10 19:15:42 +01:00
Rjosephson
01d0c05259 RoR2: Remove begin with loop (#2518) 2023-12-10 19:12:46 +01:00
Fabian Dill
19b8624818 Factorio: remove staging folder for mod assembly (#2519) 2023-12-10 19:11:57 +01:00
Alchav
1312884fa2 Pokémon R/B: Fix Silph Co 6F Hostage (#2524)
Fixes an issue where the Silph Co 6F hostage check becomes unavailable if Giovanni has been defeated on 11F. This is due to the NPC having separate scripts depending on whether Giovanni was defeated. The code for the check has been moved to before the branch.
2023-12-10 19:10:09 +01:00
lordlou
6cd5abdc11 SMZ3: KeyTH check fix (#2574) 2023-12-10 19:07:56 +01:00
JaredWeakStrike
6b0eb7da79 KH2: RC1 Bug Fixes (#2530)
Changes the finished_game to new variable so now it only checks the game's memory and if it has sent the finished flag before
Fixed ag2 not requiring 1 of each black magic
Fix hitlist if you exclude summon level 7 and have summon levels option turned off
2023-12-10 18:58:52 +01:00
Doug Hoskisson
b0a09f67f4 Core: some typing and documentation in BaseClasses.py (#2589) 2023-12-10 06:43:17 +01:00
Fabian Dill
c3184e7b19 Factorio: fix wrong parent class for FactorioStartItems (#2587) 2023-12-10 00:10:01 -05:00
t3hf1gm3nt
3214cef6cf TLOZ: Fix starting weapon possibly getting overwritten by triforce fragments (#2578)
As discovered by this bug report https://discord.com/channels/731205301247803413/1182522267687731220 it's currently possible to accidentally have the starting weapon of a player overwritten by a triforce fragment if TriforceLocations is set to dungeons and StartingPosition is set to dangerous. This fix makes sure to remove the location of a placed starting weapon if said location is in a dungeon from the pool of possible locations that triforce fragments can be placed in this circumstance.
2023-12-10 04:23:40 +01:00
Alchav
f10431779b ALTTP: Ensure all Hyrule Castle keys are local in Standard (#2582) 2023-12-09 19:33:51 +01:00
JaredWeakStrike
a9a6c72d2c KH2: Fix events in datapackage (#2576) 2023-12-08 22:39:24 +01:00
PoryGone
9351fb45ca SA2B: Fix KeyError on Unexpected Characters in Slot Names (#2571)
There were no safeguards on characters being used as keys into a conversion dict. Now there are.
2023-12-08 07:17:12 +01:00
beauxq
abfc2ddfed Zillion: fix retrieved packet processing 2023-12-07 22:27:46 +01:00
NewSoupVi
bf801a1efe The Witness: Fix Symmetry Island Upper Panel logic (2nd try)
I got lazy and didn't properly test the last fix.

Big apologies, I got a bit panicked with all the logic errors that were being found.
2023-12-07 20:16:22 +01:00
Bryce Wilson
5bd022138b Pokemon Emerald: Fix missing rule for 2 items on Route 120 (#2570)
Two items on Route 120 are on the other side of a pond but were considered accessible in logic without Surf.


Creates a new separate region for these two items and adds a rule for being able to Surf to get to this region. Also adds the items to the existing surf test.
2023-12-07 20:15:38 +01:00
Aaron Wagener
69ae12823a The Messenger: bump required client version (#2544)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-12-07 08:23:05 +01:00
Aaron Wagener
57001ced0f The Messenger: remove old links and update relevant ones (#2542) 2023-12-07 08:22:12 +01:00
NewSoupVi
3fa01a41cd The Witness: Fix unreachable locations on certain settings (Keep PP2 EP, Theater Flowers EP) (#2499)
Basically, the function for "checking entrances both ways" only checked one way. This resulted in unreachable locations.

This affects Expert seeds with (non-remote doors and specific types of EP Shuffle), as well as seeds with non-remote doors + specific types of disabled panels + specific types of EP Shuffle.

Also includes two changes that makes spoiler logs nicer (not creating unnecessary events).
2023-12-07 06:36:46 +01:00
Alchav
87252c14aa FFMQ: Update to FFMQR 1.5 (#2568)
FFMQR was just updated to 1.5, adding a number of new options. This brings these updates to AP.
2023-12-06 18:24:59 +01:00
Fabian Dill
56ac6573f1 WebHost: fix room shutdown (#2554)
Currently when a room shuts down while clients are connected it instantly spins back up. This fixes that behaviour categorically.
I still don't know why or when this problem started, but it's certainly wreaking havok on prod.
2023-12-06 18:24:13 +01:00
Doug Hoskisson
d8004f82ef Zillion: some typing fixes (#2534)
`colorama` has type stubs when it didn't before

`ZillionDeltaPatch.hash` annotated type could be `None` but md5s doesn't allow `None`

type of `CollectionState.prog_items` changed

`WorldTestBase` moved

all of the following are related to this issue:
https://github.com/python/typing/discussions/1486

CommonContext for `command_processor` (is invalid without specifying immutable - but I don't need it anyway)

ZillionWorld options and settings (is invalid without specifying immutable - but I do need it)
2023-12-06 18:23:43 +01:00
NewSoupVi
597f94dc22 The Witness: Add all the Challenge panels to Challenge exclusion list (#2564)
Just a small cleanup where right now, the logic still considers the entirety of the challenge "solvable" except for Challenge Vault Box
2023-12-06 18:22:11 +01:00
Aaron Wagener
49e1fd0b79 The Messenger: ease rule on key of strength a bit (#2541)
Makes the logic for accessing key of strength just a tiny bit easier since a few players said it was really difficult.
2023-12-06 18:20:18 +01:00
Yussur Mustafa Oraji
530617c9a7 sm64ex: Refactor Regions (#2546)
Refactors region code to remove references to course index.
There were bugs somewhere, but I dont know where tbh.
This fixes them but leaves logic otherwise intact, and much cleaner to look at as there's one list less to take care of.

Additionally, this fixes stopping the clock from Big Boos Haunt.
2023-12-06 18:19:03 +01:00
NewSoupVi
229a263131 The Witness: Fix logic error with Symmetry Island Upper in doors: panels (broken seed reported) (#2565)
Door entities think they can be solved without any other panels needing to be solved.

Usually, this is true, because they no longer need to be "powered on" by a previous panel.
However, there are some entities that need another entity to be powered/solved for a different reason.
In this case, Symmetry Island Lower Left set opens the latches that block your ability to solve the panel. The panel itself actually starts on. Playing doors: panels does not change this, unlike usually where dependencies like this get removed by playing that mode.

In the long term, I want to somehow be able to "mark" dependencies as "environmental" or "power based" so I can distinguish them properly.
2023-12-06 18:17:27 +01:00
NewSoupVi
a861ede8b3 The Witness: Fix various incorrect symbol requirements in Vanilla Puzzles (#2543)
* Fix Vanilla First Floor Left

* More vanilla logic fixes

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2023-12-04 16:26:00 +01:00
el-u
b7111eeccc lufia2ac: fix disappearing Ancient key (#2537)
Since the coop update, the Ancient key (which is always the reward for defeating the boss) would disappear when leaving the cave, making it impossible to open the locked door behind the Ancient Cave entrance counter. While this is basically cosmetic and has no adverse effects on the multiworld (as the door does not lead to any multiworld locations and is only accessible after defeating the final boss anyway), players may still want to enter this room as part of a ritual to celebrate their victory.

Why does this happen? The game keeps track of two different inventories, one for outside and another one for the cave dive. When entering or leaving the cave, important things such as blue chest items and Iris treasures are automatically copied to the other inventory. However, it turns out that the Ancient key doesn't participate in this mechanism. Instead, the script that runs when exiting the cave checks whether event flag 0xC3 is set, and if it is on, it calls a script action that adds the key item to the outside inventory. (Whether or not the player actually had the key item in their in-cave inventory is not checked at all; only the flag matters.)

In the unmodified game, that flag is set by the cutscene script that awards the key. It actually sets two event flags, 0xC3 and 0xD1. The latter is used by the game when trying to display the boss in the cafe basement and is used by AP as the indicator that the boss goal was completed. With the coop update, the event script method that created the key was intercepted and modified to send out a location check instead. That location always has the Ancient key as a fixed item placement; the benefit of handling it as a remote item is that in this way the key essentially serves as a signal that transmits the information of the boss' defeat to all clients cooping on the slot. When receiving the key, however, the custom ASM did only set flag 0xD1. As part of the bugfix, it is now changed to set flag 0xC3 as well.

But that alone is still not enough to make it work. The subroutine that is called by the game to create the key when exiting the cave with flag 0xC3 is the same subroutine that gets called in the cutscene that originally tried to award the key. But that's the one that has been rewritten to send the location check instead. So instead of creating the key when leaving the cave, it would just send the same location check again, effectively doing nothing. Therefore, the other part of the bugfix is to only intercept this subroutine if the player is currently on the Ancient Cave Final Floor (where the cutscene takes place), thus making it possible to recreate the key item when exiting.
2023-12-04 00:06:52 +01:00
Star Rauchenberger
39a92e98c6 Lingo: Default color shuffle to on (#2548)
* Lingo: Default color shuffle on

* Raise error if no progression in multiworld
2023-12-04 00:06:11 +01:00
zig-for
a83bf2f616 LADX: Fix bug with Webhost usage (#2556)
We were using data created in init when we never called init
2023-12-03 21:24:35 +01:00
Alchav
e8ceb12281 Pokémon RB: Fix connection names + missing connection (#2553) 2023-12-02 18:40:38 +01:00
Aaron Wagener
6e38126add Webhost: fix options page redirects (#2540) 2023-12-01 14:20:24 -06:00
Fabian Dill
5e5018dd64 WebHost: flash each message only once (#2547) 2023-12-01 21:19:41 +01:00
Aaron Wagener
c7d4c2f63c Docs: Add documentation on writing and running tests (#2348)
* Docs: Add documentation on writing and running tests

* review improvements

* sliver requests
2023-12-01 10:26:27 +01:00
agilbert1412
80fed1c6fb Stardew Valley: Fixed potential softlock with walnut purchases if Entrance Randomizer locks access to the field office (#2261)
* - Added logic rules for reaching, then completing, the field office in order to be allowed to spend significant amounts of walnuts

* - Revert moving a method for some reason
2023-11-30 09:32:32 +01:00
Brooty Johnson
b9ce2052c5 DS3: update setup guide to preserve downpatching instructions (#2531)
* update DS3 setup guide to preserve downpatching instructions

we want to preserve this on the AP site as the future of the speedsouls wiki is unknown and may disappear at any time.

* Update worlds/dark_souls_3/docs/setup_en.md

Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com>

* Update setup_en.md

---------

Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com>
2023-11-30 09:29:55 +01:00
Chris Wilson
a83501a2a0 Fix a bug in weighted-settings causing accepted range values to be exclusive of outer range (#2535) 2023-11-29 22:57:40 -05:00
t3hf1gm3nt
6c5f8250fb TLOZ: Use the proper location name lookup (#2529) 2023-11-29 00:19:42 -06:00
el-u
39969abd6a WebHostLib: fix NamedRange in options presets (#2528) 2023-11-28 17:11:17 -06:00
Bryce Wilson
737686a88d BizHawkClient: Use local_path when autolaunching BizHawk with lua script (#2526)
* BizHawkClient: Change autolaunch path to lua script to use local_path

* BizHawkClient: Remove unnecessary call to os.path.join and linting
2023-11-28 22:56:27 +01:00
Bryce Wilson
ce2f9312ca BizHawkClient: Change open_connection to use 127.0.0.1 instead of localhost (#2525)
When using localhost on mac, both ipv4 and ipv6 are tried and raise separate errors
which are combined by asyncio and difficult/inelegant to handle.

Python 3.12 adds the argument all_errors, which would make this easier.
2023-11-28 22:50:12 +01:00
Alchav
f54f8622bb Final Fantasy Mystic Quest: Implement new game (#1909)
FFMQR by @wildham0 
Uses an API created by wildham for Map Shuffle, Crest Shuffle and Battlefield Reward Shuffle, using a similar method of obtaining data from an external website to Super Metroid's Varia Preset option.
Generates a .apmq file which the user must bring to the FFMQR website https://www.ffmqrando.net/Archipelago to patch their rom. It is not an actual patch file but contains item placement and options data for the FFMQR website to generate a patched rom with for AP.
Some of the AP options may seem unusual, using Choice instead of Range where it may seem more appropriate, but these are options that are passed to FFMQR and I can only be as flexible as it is.

@wildham0 deserves the bulk of the credit for not only creating FFMQR in the first place but all the ASM work on the rom needed to make this possible, work on FFMQR to allow patching with the .apmq files, and creating the API that meant I did not have to recreate his map shuffle from scratch.
2023-11-26 17:17:59 +01:00
Justus Lind
65f47be511 Muse Dash: Presets and Song Updates (#2512) 2023-11-25 22:13:59 -06:00
Bryce Wilson
eec35ab1c3 Pokemon Emerald: Fix tracker flags being reset in menus (#2511) 2023-11-25 22:13:08 -06:00
PoryGone
7a46209259 SA2B: Add AP 0.4.4 Game Chao Names (#2510) 2023-11-25 22:12:38 -06:00
Aaron Wagener
cfe357eb71 The Messenger, LADX: use collect and remove as intended (#2093)
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-11-25 15:07:02 -06:00
Aaron Wagener
fe6a70a1de Docs: add documentation for options comparison (#2505) 2023-11-25 10:48:13 -06:00
Bryce Wilson
6718fa4e3b Installer: Add _bizhawk.apworld to installer deleted files (#2477) 2023-11-25 09:38:12 -06:00
Bryce Wilson
5475b04b90 Pokemon Emerald: Bump apworld version number (#2504) 2023-11-25 09:27:54 -06:00
Dinopony
d46e68cb5f Landstalker: implement new game (#1808)
Co-authored-by: Anthony Demarcy <anthony.demarcy@lumiplan.com>
Co-authored-by: Phar <zach@alliware.com>
2023-11-25 09:00:15 -06:00
JaredWeakStrike
2ccf11f3d7 KH2: Version 2 (#2009)
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
Co-authored-by: Joe Prochaska <prochaska.joseph@gmail.com>
2023-11-25 08:46:00 -06:00
David St-Louis
c138918400 DOOM 1993: Added various new options (#2067) 2023-11-25 08:43:14 -06:00
Fabian Dill
59ed2602bd Pokemon: delete old files (#2501) 2023-11-25 15:42:03 +01:00
Brooty Johnson
dd47790c31 DS3: Added 'Early Banner' Setting (#2199)
Co-authored-by: Zach Parks <zach@alliware.com>
2023-11-25 08:38:18 -06:00
David St-Louis
9afca87045 Heretic: implement new game (#2256) 2023-11-25 15:22:30 +01:00
el-u
ba53278147 core: make option resolution in world tests deterministic (#2471)
Co-authored-by: Zach Parks <zach@alliware.com>
2023-11-25 13:53:02 +01:00
Star Rauchenberger
6dccf36f88 Lingo: Various generation optimizations (#2479)
Almost all of the events have been eradicated, which significantly improves both generation speed and playthrough calculation.

Previously, checking for access to a location involved checking for access to each panel in the location, as well as recursively checking for access to any panels required by those panels. This potentially performed the same check multiple times. The access requirements for locations are now calculated and flattened in generate_early, so that the access function can directly check for the required rooms, doors, and colors.

These flattened access requirements are also used for Entrance checking, and register_indirect_condition is used to make sure that can_reach(Region) is safe to use.

The Mastery and Level 2 rules now just run a bunch of access rules and count the number of them that succeed, instead of relying on event items.

Finally: the Level 2 panel hunt is now enabled even when Level 2 is not the victory condition, as I feel that generation is fast enough now for that to be acceptable.
2023-11-25 13:09:08 +01:00
Alchav
8a852abdc4 Pokémon R/B: Migrate support into Bizhawk Client (#2466)
- Removes the Pokémon Client, adding support for Red and Blue to the Bizhawk Client.
- Adds `/bank` commands that mirror SDV's, allowing transferring money into and out of the EnergyLink storage.
- Adds a fix to the base patch so that the progressive card key counter will not increment beyond 10, which would lead to receiving glitch items. This value is checked against and verified that it is not > 10 as part of crash detection by the client, to prevent erroneous location checks when the game crashes, so this is relevant to the new client (although shouldn't happen unless you're using !getitem, or putting progressive card keys as item link replacement items)
2023-11-25 11:57:02 +01:00
Fabian Dill
edb62004ef LttP: remove extra default = False (#2497)
* LttP: remove extra default = False
2023-11-25 11:12:13 +01:00
GodlFire
8d41430cc8 Shivers: Implement New Game (#1836)
Co-authored-by: Mathx2 <Mathx2@gmail.com>
Co-authored-by: Zach Parks <zach@alliware.com>
2023-11-24 17:23:45 -06:00
Zach Parks
8173fd54e7 DOOM II: Add to CODEOWNERS (#2492) 2023-11-24 17:16:19 -06:00
Zach Parks
e46420f4a9 MultiServer: Create read-only data storage key for client statuses. (#2412) 2023-11-24 17:14:07 -06:00
el-u
c944ecf628 Core: Introduce new Option class NamedRange (#2330)
Co-authored-by: Chris Wilson <chris@legendserver.info>
Co-authored-by: Zach Parks <zach@alliware.com>
2023-11-24 17:10:52 -06:00
Chris Wilson
e64c7b1cbb Fix player-options and weighted-options failing to validate settings if a payer's name is entirely numeric (#2496) 2023-11-24 16:50:32 -05:00
NewSoupVi
15797175c7 The Witness: New junk hints (#2495) 2023-11-24 13:38:46 -06:00
digiholic
4641456ba2 MMBN3: Small Bug Fixes (#2282)
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: Zach Parks <zach@alliware.com>
2023-11-24 11:14:05 -06:00
Star Rauchenberger
a18fb0a14f Lingo: Move datafiles into a subdirectory (#2459) 2023-11-24 18:11:34 +01:00
David St-Louis
c5b0330223 DOOM II: implement new game (#2255) 2023-11-24 18:08:02 +01:00
Ishigh1
530e792c3c Core: Floor and ceil in datastorage (#2448) 2023-11-24 10:42:22 -06:00
Fabian Dill
d892622ab1 Plando: verify from_pool type (#2200) 2023-11-24 10:41:56 -06:00
Exempt-Medic
a8e03420ec Fill: Fix plando removing Usefuls first (#2445)
Co-authored-by: blastron <blastron@mac.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2023-11-24 10:33:59 -06:00
Star Rauchenberger
1ff8ed396b Lingo: Demote warpless painting items to filler (#2481) 2023-11-24 10:30:15 -06:00
NewSoupVi
e93842a52c The Witness: Big™ new™ content update™ (#2114)
Co-authored-by: blastron <blastron@mac.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2023-11-23 23:27:03 -06:00
el-u
205c6acb49 lufia2ac: fix client behavior at max blue chests combined with party member or capsule monster shuffle (#2478)
When option combinations at (or near) the maximum location count were used, the client could trip over a wrongly coded limit and stop sending checks.
2023-11-24 01:59:41 +01:00
Aaron Wagener
2f6b6838cd The Messenger: more optimizations (#2451)
More speed optimizations for The Messenger. Moves Figurines into their own region, so their complicated access rule only needs to be calculated once when doing a sweep. Removes a redundant loop for shop locations by just directly assigning the access rule in the class instead of retroactively. Reduces slot_data to only information that can't be derived, and removes some additional extraneous data. Removes some unused sets and lists. Removes a redundant event location, and increments the required_client_version to prevent clients that don't expect the new slot_data. Drops data version since it's going away soon anyways, to remove conflicts.
2023-11-24 00:38:57 +01:00
Fabian Dill
844481a002 Core: remove duplicate state.item_count (#2463) 2023-11-24 00:35:37 +01:00
Zach Parks
5d9896773d Generate: Add --skip_output flag to bypass assertion and output stages. (#2416) 2023-11-23 16:03:56 -06:00
JaredWeakStrike
9312ad9bfe KH2: Fix grammar to clarify which locations can have a bounty (#2488) 2023-11-23 16:02:20 -06:00
Fabian Dill
cb6467cfe6 Core: update modules, move orjson to core (#2489) 2023-11-23 21:36:20 +01:00
Fabian Dill
28ed786609 LttP: fix Ganons Tower - Compass Room - Bottom Left being listed twice in Ganons Tower location group and add missing Ganons Tower - Compass Room - Bottom Right (#2490) 2023-11-23 21:36:05 +01:00
Bryce Wilson
7efec64745 BizHawkClient: Restore use of ConnectorErrors (#2480) 2023-11-23 20:51:53 +01:00
Alchav
f840ed3a94 Pokémon R/B: Fix trainer regions (#2474)
* Fix Mt Moon B2F trainer regions

* Fix Trainer Party regions
2023-11-23 19:17:09 +01:00
Yussur Mustafa Oraji
286dfd84c0 sm64ex: Replace old launcher tutorial (#2383) 2023-11-23 12:10:32 -06:00
JaredWeakStrike
a1759ed7e1 KH2: Update Game Docs (#2188)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2023-11-23 12:06:57 -06:00
Star Rauchenberger
ae8a81c0cb Lingo: Change docs to link to the client in the Steam Workshop (#2486) 2023-11-23 11:56:55 -06:00
Bryce Wilson
a7aed71fbe Pokemon Emerald: Fix opponent trainer moves sometimes being MOVE_NONE (#2487) 2023-11-23 11:55:50 -06:00
Bryce Wilson
0d38b41540 BizHawkClient: Add support for multiple concurrent instances (#2475)
This allows multiple client/connector pairs to run at the same time. It also includes a few other miscellaneous small changes that accumulated as I went. They can be split if desired

- Whatever the `client_socket:send` line (~440) was doing with that missing operator, it's no longer doing. Don't ask me how it was working before. Lua is witchcraft.
- Removed the `settimeout(2)` which causes the infamous emulator freeze (and replaced it with a `settimeout(0)` when the server socket is created). It appears to be unnecessary to set a timeout for discovering a client. Maybe at some point in time it was useful to keep the success rate for connecting high, but it seems to not be a problem if the timeout is 0 instead.
  - Also updated the Emerald setup to remove mention of the freezing.
- Connector script now picks the first port that's not in use in a range of 5 ports.
  - To summarize why I was previously under the impression that multiple running scripts would not detect when a port was in use:
    1. Calling `socket.bind` in the existing script will first create an ipv6 socket.
    2. A second concurrent script trying to bind to the same port would I think fail to create an ipv6 socket but then succeed in creating an ipv4 socket on the same port.
    3. That second socket could never communicate with a client; extra clients would just bounce off the first script.
    4. The third concurrent script will then fail on both and actually give an `address already in use` error.  
  - I'm not _really_ sure what's going on there. But forcing one or the other by calling `socket.tcp4()` or `socket.tcp6()` means that only one script will believe it has the port while any others will give `address already in use` as you'd expect.
  - As a side note, our `socket.lua` is much wonkier than I had previously thought. I understand some parts were added for LADX and when BizHawk 2.9 came out, but as far back as the file's history in this repo, it has provided a strange, modified interface as compared to the file it was originally derived from, to no benefit as far as I can tell.
- The connector script closes `server` once it finds a client and opens a new one if the connection drops. I'm not sure this ultimately has an effect, but it seems more proper.
- If the connector script's main function returns because of some error or refusal to proceed, the script no longer tries to resume the coroutine it was part of, which would flood the log with irrelevant errors.
- Creating `SyncError`s in `guarded_read` and `guarded_write` would raise its own error because the wrong variable was being used in its message.
- A call to `_bizhawk.connect` can take a while as the client tries the possible ports. There's a modification that will wait on either the `connect` or the exit event. And if the exit event fires while still looking for a connector script, this cancels the `connect` so the window can close.
  - Related: It takes 2-3 seconds for a call to `asyncio.open_connection` to come back with any sort of response on my machine, which can be significant now that we're trying multiple ports in sequence. I guess it could fire off 5 tasks at once. Might cause some weirdness if there exist multiple scripts and multiple clients looking for each other at the same time.
  - Also related: The first time a client attempts to connect to a script, they accept each other and start communicating as expected. The second client to try that port seems to believe it connects and will then time out on the first message. And then all subsequent attempts to connect to that port by any client will be refused (as expected) until the script shuts down or restarts. I haven't been able to explain this behavior. It adds more time to a client's search for a script, but doesn't ultimately cause problems.
2023-11-23 15:00:46 +01:00
Bryce Wilson
b2e7ce2c36 Pokemon Emerald: Fix using wrong key for extracted constant (#2484) 2023-11-22 12:21:15 -06:00
Remy Jette
af0d47b444 Core: Provide a better error message if only weights.yaml is provided with players: 0 (#2227) 2023-11-22 11:13:02 -06:00
Zach Parks
ee76cce1a3 Rogue Legacy: Fix a preset including an option that prevents generation. (#2473) 2023-11-22 10:42:21 -06:00
agilbert1412
0f98cf525f Stardew Valley: Generate proper filler for item links (#2069)
Co-authored-by: Zach Parks <zach@alliware.com>
2023-11-22 10:04:33 -06:00
Zach Parks
cfd2e9c47f Core: Increment Archipelago Version (#2483) 2023-11-22 10:04:10 -06:00
digiholic
4a9d075b77 MMBN3: Adds instructions for using the Legacy Collection ROM for setup (#2120)
Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
2023-11-22 09:45:32 -06:00
Rjosephson
79406faf27 RoR2: 1.3.0 content update (#2425) 2023-11-22 09:20:32 -06:00
zig-for
01b566b798 LADX: Text shuffle (#2051) 2023-11-22 08:29:33 -06:00
Jarno
d1b22935b4 Timespinner: New options from TS Rando v1.25 + Logic fix (#2090)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-11-22 08:17:33 -06:00
agilbert1412
3b357315ee Git: Added file type .smc to gitignore (#2476) 2023-11-22 08:15:35 -06:00
BadMagic100
f959819801 Hollow Knight: Don't force mimics local (#2482) 2023-11-22 08:15:09 -06:00
agilbert1412
e916b0d6b0 Stardew Valley: Add Options presets (#2470) 2023-11-18 12:35:57 -06:00
Zach Parks
790f192ded WebHost: Refactor tracker.py, removal of dead code, and tweaks to layouts of some tracker pages. (#2438) 2023-11-18 12:29:35 -06:00
Aaron Wagener
185a519248 Core: fix item links around core changes (#2452) 2023-11-16 04:55:18 -06:00
Zach Parks
79ad54623b WebHost, Core: Developer-defined game option presets. (#2143) 2023-11-16 04:37:06 -06:00
Bryce Wilson
3619abc7ca Pokemon Emerald: Fix scorched slab missing surf requirement (#2465) 2023-11-16 04:36:38 -06:00
FlySniper
cb0412e011 Wargroove: Fixed WargrooveClient retaining victory and location information and minor doc fix (#2464) 2023-11-16 04:35:20 -06:00
Justus Lind
e66ce6c05f Muse Dash: Rename some Trap Items to match the wider community name (#2180) 2023-11-16 04:33:56 -06:00
Star Rauchenberger
a4b625c3e3 Lingo: Sync config with game update (#2447) 2023-11-16 04:12:44 -06:00
PoryGone
85d02b2dc5 SA2B: v2.3 - The Chao Update (#2277)
Changelog:

Features:
- New goal
  - Chaos Chao
    - Raise a Chaos Chao to win!
- New optional Location Checks
  - Chao Animal Parts
    - Each body part from each type of animal is a location
  - Chao Stats
    - 0-99 levels of each of the 7 Chao stats can be locations
    - The frequency of Chao Stat locations can be set (every level, every 2nd level, etc)
  - Kindergartensanity
    - Classroom lessons are locations
      - Either all lessons or any one of each category can be set as locations
  - Shopsanity
    - A specified number of locations can be placed in the Chao Black Market
    - These locations are unlocked by acquiring `Chao Coin`s
    - Ring costs for these items can be adjusted 
  - Chao Karate can now be set to one location per fight, instead of one per tournament
- Items
  - If any Chao locations are active, the following will be in the item pool:
    - Chao Eggs
    - Garden Seeds
    - Garden Fruit
    - Chao Hats
    - Chaos Drives
- The starting eggs in the garden can be a random color
- Chao World entrances can be shuffled
- Chao are given default names
- New Traps
  - Reverse Trap

Quality of Life:
- Chao Save Data is now separate per-slot in addition to per-seed
  - This allows a single player to have multiple slots in the same seed, each having separate Chao progress
- Chao Race/Karate progress is now displayed on Stage Select (when hovering over Chao World)
- All Chao can now enter the Hero and Dark races
- Chao Karate difficulty can be set separately from Chao Race difficulty
- Chao Aging can be sped up at will, up to 15×
- New mod `config` option to fine-tune Chao Stat multiplication
  - Note: This does not mix well with the Mod Manager "`Chao Stat Multiplier`" code
- Pong Traps can now activate in Chao World
- Maximum range for possible number of Emblems is now 1000
- General APWorld cleanup and optimization
  - Option access has moved to the new options system
  - An item group now exists for trap items

Bug Fixes:
- Dry Lagoon now has all 11 Animals
- Eternal Engine - 2 (Standard and Hard Logic) now requires only `Tails - Booster`
- Lost Colony - 2 (Hard Logic) now requires no upgrades
- Lost Colony - Animal 9 (Hard Logic) now requires either `Eggman - Jet Engine` or `Eggman - Large Cannon`
2023-11-16 08:08:38 +01:00
Fabian Dill
829c664304 Core: check that location address is unique per player (#2429) 2023-11-15 20:50:00 +01:00
Danaël V
28a20391ab Docs: Rework of Contributing.md (#2278)
* Update contributing.md

* Update docs/contributing.md

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

* Update contributing.md

Separated the sentence specifically for web stuff as well as slight rephrasing of the first bullet point

* Update docs/contributing.md

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

* Update docs/contributing.md

Changed the order of two words

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

* Update docs/contributing.md

Clarified "this document"

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2023-11-15 19:18:57 +01:00
Aaron Wagener
bf8432faa7 Docs: minor updates to recommend modern PEP8 (#2384)
* docs: update world api for modern PEP8 conventions

* docs: update options api for modern PEP8 styling

* missed a spot
2023-11-15 17:07:42 +01:00
el-u
2af5410301 core: fix item/location descriptions test (#2450) 2023-11-15 07:26:10 +01:00
Trevor L
41b6aef23c Hylics 2: Unique entrance names, fix APWorld on 3.8 (#2460)
* Blasphemous: Set rules for events later

* Blasphemous: More misc logic fixes

* Update worlds/blasphemous/Rules.py

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

* Update worlds/blasphemous/Rules.py

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

* Blasphemous: Some cleanup

* Hylics 2: Unique entrance names, fix APWorld on 3.8

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-11-15 07:03:40 +01:00
Fabian Dill
8ce073e355 Core: relax typing hints on has_all and has_any (#2462) 2023-11-15 06:53:37 +01:00
Fabian Dill
287a186ff6 CommonClient: make IncompatibleVersion more explicit. (#2350) 2023-11-15 06:13:41 +01:00
FaultBat
44a9bb59ec Factorio: Update icons to match vanilla style (#2449) 2023-11-15 03:15:37 +01:00
el-u
6c1ae77db4 lufia2ac: improve performance of access rules (#2456)
Modifies various access rules in the lufia2ac world with the aim of making them evaluate quicker.
Instead of having to determine the reachability of another location, they now only have to count items in state, which is faster.
(Also made it reuse the identical lambda for multiple locations, which might save a smidgen of memory.)
2023-11-15 03:11:02 +01:00
black-sliver
0239578a62 Setup: fix orjson import on frozen py3.8 (#2458)
orjson has a .py entry point that imports `from .orjson` (from the DLL), which does not work on 3.8 if the DLL is not in the same folder as the .py. The changed import system in 3.9+ seems to allow this. Excluding it from libraries.zip will put both files into the same folder.
2023-11-13 22:42:34 +01:00
Fabian Dill
81cc016267 LttP: write fairy bottle fill to spoiler and prevent fart in a bottle (#2424) 2023-11-13 06:50:45 +01:00
Fabian Dill
f63743f9a9 Core: limit perf logger to 4 post-point places (#2404) 2023-11-13 06:49:31 +01:00
Fabian Dill
b3a9d58e02 Core: update modules (#2440) 2023-11-13 06:48:50 +01:00
Fabian Dill
ef7d8a6b4f Core: limit parallel APContainer writing (#2443)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-11-13 06:46:40 +01:00
Star Rauchenberger
cc0ea6a9e9 Lingo: Made entrance names unique (#2454) 2023-11-12 19:22:05 -06:00
Remy Jette
4d711a0aa5 Installer: Fix invalid component error in inno_setup.iss (#2455) 2023-11-12 19:21:17 -06:00
Bryce Wilson
43041f7292 Pokemon Emerald: Implement New Game (#1813) 2023-11-12 15:39:34 -06:00
black-sliver
e670ca513b Fill: fix swap error found in CI (#2397)
* Fill: add test for swap error with item rules

https://discord.com/channels/731205301247803413/731214280439103580/1167195750082560121

* Fill: fix swap error found in CI

Swap now assumes the unplaced items can be placed before the to-be-swapped item.
Unsure if that is safe or unsafe.

* Test: clarify docstring and comments in fill swap test

* Test: clarify comments in fill swap test more
2023-11-11 10:54:51 +01:00
Remy Jette
df1e78c6f2 WebHost: Sort tracker last activity 'None' as maximum instead of -1 (#2446)
When managing an async, it can be useful to sort the tracker by Last
Activity to see who has potentially abandoned their slots. Today, if a
slot hasn't been started (last activity is None) then it is sorted as
if last activity is -1, that it is it has had more recent activity than
any other slot.

This change makes it so slots that haven't started are treated as if
they have last activity MAX_VALUE time ago. This way they get sorted
with slots that haven't been touched in a long time which should make
intuitive sense as the "last activity" is effectively inf time ago.
2023-11-11 01:13:32 -05:00
Natalie Weizenbaum
2dd904e758 Allow worlds to provide item and location descriptions (#2409)
These are displayed in the weighted options page as hoverable tooltips.
2023-11-11 01:06:54 -05:00
Aaron Wagener
64159a6d0f The Messenger: fix logic rule for spike darts and power seal hunt (#2414) 2023-11-11 05:49:55 +01:00
Fabian Dill
ac77666f2f Factorio: skip a bunch of file IO (#2444)
In a lot of cases, Factorio would write data to file first, then attach that file into zip. It now directly attaches the data to the zip and encapsulation was used to allow earlier GC in places (rendered templates especially).
2023-11-10 22:02:34 +01:00
Star Rauchenberger
7af7ef2dc7 Lingo: Removed "Reached" event items (#2442) 2023-11-10 13:19:05 -06:00
Star Rauchenberger
f444d570d3 Lingo: Fix edge case painting shuffle accessibility issues (#2441)
* Lingo: Fix painting shuffle logic issue in The Wise

* Lingo: More generic painting cycle prevention

* Lingo: okay how about now

* Lingo: Consider Owl Hallway blocked painting areas in vanilla doors

* Lingo: so honestly I should've seen this one coming

* Lingo: Refined req_blocked for vanilla doors

* Lingo: Orange Tower Basement is also owl-blocked

* Lingo: Rewrite randomize_paintings to eliminate rerolls

Now, mapping is done in two phases, rather than assigning everything at once and then rerolling if the mapping is non-viable.
2023-11-10 13:07:56 -06:00
NewSoupVi
b5bd95771d Raft: Use world.random instead of global random (#2439) 2023-11-09 08:47:36 +01:00
Star Rauchenberger
ea9c31392d Lingo: New game (#1806)
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: Phar <zach@alliware.com>
2023-11-08 17:35:12 -06:00
Ziktofel
154e17f4ff SC2: 0.4.3 bugfixes (#2273)
Co-authored-by: Matthew <matthew.marinets@gmail.com>
2023-11-08 12:00:55 -06:00
Mewlif
504d09daf6 Undertale: Logic fixes (#2436) 2023-11-08 11:50:29 -06:00
Aaron Wagener
03e1c45d71 Tests: log the seed fo slot_data failures (#2402) 2023-11-08 09:15:06 +01:00
Silvris
ced35c5b78 CommonClient: Add a hints tab (#2392)
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-11-07 14:51:35 -06:00
Nicholas Saylor
779a312650 Docs, Undertale: Added Suggestions Missed in #2285 (#2435)
Co-authored-by: jonloveslegos <68133186+jonloveslegos@users.noreply.github.com>
Co-authored-by: kindasneaki <ryandj67@hotmail.com>
Co-authored-by: ScootyPuffJr1 <77215594+scootypuffjr1@users.noreply.github.com>
2023-11-07 14:41:13 -06:00
Fabian Dill
72cb8b7d60 Factorio: inflate location pool (#2422) 2023-11-07 21:02:28 +01:00
TheLynk
5a7d69c8b4 ChecksFinder: Tweak link in ChecksFinder (#2353)
Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>
Co-authored-by: Marech <marechal-l@gmx.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-11-07 11:31:06 -06:00
NewSoupVi
c984b48149 The Witness: Fix Town Tower 4th Door Logic (#2421) 2023-11-07 07:39:36 +01:00
axe-y
84fb2f58fa DLC Quest Stardew: bug (#2423) 2023-11-06 06:01:49 +01:00
Fabian Dill
e1f1bf83c2 Core: Running item Plando dot (#2405) 2023-11-05 00:15:39 -05:00
black-sliver
d2e9bfb196 AppImage: allow loading apworlds from ~/Archipelago and copy scripts (#2358)
also fixes some mypy and flake8 violations in worlds/__init__.py
2023-11-04 10:26:51 +01:00
black-sliver
880326c9a5 SM: fix missed SMWorld.spheres in #2400 (#2419) 2023-11-02 21:08:36 +01:00
espeon65536
ec70cfc798 OoT: fix incorrect calls to sweep_for_events (#2417) 2023-11-02 20:02:38 +01:00
Aaron Wagener
5669579374 Core: make state.prog_items a Dict[int, Counter[str]] (#2407) 2023-11-02 06:41:20 +01:00
espeon65536
19dc0720ba OoT: fix enhanced_map_compass generation failure (#2411) 2023-11-02 06:39:29 +01:00
dennisw100
f701b81308 Docs: Terraria Setup Guide added information about the Upgraded Research Mod (#2338)
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Co-authored-by: Seldom <38388947+Seldom-SE@users.noreply.github.com>
2023-11-01 16:08:04 -05:00
kindasneaki
d7ec722aba RoR2: update options (#2391) 2023-10-31 22:34:24 +01:00
Natalie Weizenbaum
dc80f59165 WebHost: Expose name groups through the weighted-settings UI (#2327)
* Factor out a common function for building lists

* Expose name groups through the weighted-settings UI

* Fix weighted-settings page

The request for the JSON file that provides the setting data was missed during the rename in #2037, so prior to this the weighted settings page wasn't rendering at all.
2023-10-31 17:25:07 -04:00
Natalie Weizenbaum
5726d2f962 Fix weighted-settings page (#2408)
The request for the JSON file that provides the setting data was missed during the rename in #2037, so prior to this the weighted settings page wasn't rendering at all.
2023-10-31 17:22:02 -04:00
Nicholas Saylor
560c57fedd Docs, Various Games: Add Unique Local Commands to Game Page (#2285)
* Add Unique Locals Commands to ChecksFinder

* Add Unique Locals Commands to MMBN3 Game Page

* Add Unique Locals Commands to Ocarina of Time Game Page

* Add Unique Locals Commands to Undertale Game Page

* Add Unique Locals Commands to Wargroove Game Page

* Add Unique Locals Commands to The Legend of Zelda Game Page

* Add Unique Locals Commands to Zillion Game Page

* Amend Unique Locals Commands on Final Fantasy 1 Game Page

* Add Unique Locals Commands to Pokemon R/B Game Page

* Grammar fix for FF1

* Corrected sections names to match

* Added commands to Starcraft 2 Wings of Liberty game page

Co-authored-by: Bicoloursnake <60069210+bicoloursnake@users.noreply.github.com>

---------

Co-authored-by: Bicoloursnake <60069210+bicoloursnake@users.noreply.github.com>
2023-10-31 17:20:24 -04:00
Remy Jette
3bff20a3cf WebHost: Round percentage of checks, fix possible 500 error (#2270)
* WebHost: Round percentage of checks, fix possible 500 error

* Round using str.format in the template

How the percentage of checks done should be displayed is a display
concern, so it makes sense to just always do it in the template. That
way, along with using .format() instead of .round, means we always get
exactly the same presentation regardless of whether it ends in .00
(which would not round to two decimal places), is an int (which
`round(2)` wouldn't touch at all), etc.

* Round percent_total_checks_done in lttp multitracker

* Fix non-LttP games showing as 0% done in LttP MultiTracker
2023-10-31 17:20:07 -04:00
Silvris
d2c541c51c SNIClient, ALttP: expose death_text to SNI client, add message to alttp (#1793) 2023-10-31 11:11:18 +01:00
black-sliver
5f5c48e17b Core: fix some memory leak sources without removing caching (#2400)
* Core: fix some memory leak sources

* Core: run gc before detecting memory leaks

* Core: restore caching in BaseClasses.MultiWorld

* SM: move spheres cache to MultiWorld._sm_spheres to avoid memory leak

* Test: add tests for world memory leaks

* Test: limit WorldTestBase leak-check to py>=3.11

---------

Co-authored-by: Fabian Dill <fabian.dill@web.de>
2023-10-31 02:08:56 +01:00
Aaron Wagener
d4498948f2 Core: return the created entrance when connecting regions (#2406) 2023-10-30 21:14:14 +01:00
Alchav
aa56383310 Pokémon R/B: Fix incompatible option combination (#2356) 2023-10-30 21:13:02 +01:00
Fabian Dill
d743d10b2c Core: log completion time if > 1.0 seconds per step (#2345) 2023-10-30 04:06:40 +01:00
espeon65536
db978aa48a OoT Time Optimization (#2401)
- Entrance randomizer no longer grows with multiworld
- Improved ER success rate again by prioritizing Temple of Time even more
- Prefill is faster, has slightly reduced failure rate when map/compass are in dungeon but previous items in any_dungeon (which consumed all available locations), no longer removes items from the main itempool; itemlinked prefill items removed to accomodate improvements
- Now triggers only one recache after `generate_basic` instead of one per oot world
- Avoids recaches during `create_regions`
- All ER temp entrances have unique names (so the entrance cache does not break)
2023-10-30 04:05:49 +01:00
Fabian Dill
f81e72686a Core: log fill progress (#2382)
* Core: log fill progress

* Add names to common fill steps

* Update Fill.py

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

* Apply suggestions from code review

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

* cleanup default name

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-10-30 01:22:00 +01:00
Justus Lind
d5745d4051 Muse Dash: Adds the new songs in the Happy Otaku Pack Vol.18 update. (#2398) 2023-10-30 01:21:29 +01:00
Scipio Wright
36f95b0683 Noita: Fix rare item fill failure for single-player games (#2387) 2023-10-29 20:02:53 +01:00
Fabian Dill
9c80a7c4ec HK: skip for loop (#2390) 2023-10-29 19:53:57 +01:00
Fabian Dill
3e0d1d4e1c Core: change Region caching to on_change from on-miss-strategy (#2366) 2023-10-29 19:47:37 +01:00
black-sliver
d9b076a687 Stardew Valley: simplify in-place (#2393)
this allows skipping multiple simplifications of the same object, e.g. item_rules
also update the logic simplification tests to be a proper unittest.TestCase
2023-10-29 13:20:28 +01:00
Alchav
ff65de1464 Pokemon R/B: Reenable Rock tunnel location access rules (#2396) 2023-10-28 17:32:03 -05:00
Scipio Wright
b874febb1e Noita: Extra Life change (#2247)
* Item rate update, also removed unnecessary reverse region connections

* Converted sets into lists, removed empties
2023-10-28 22:27:57 +02:00
Bryce Wilson
acfc71b8c9 BizHawkClient: Add support for server passwords (#2306) 2023-10-28 21:48:31 +02:00
Trevor L
e8a7200740 Blasphemous: Include ranged attack in logic for all difficulties (#2271) 2023-10-28 21:47:14 +02:00
Yussur Mustafa Oraji
253f3e61f7 sm64ex: All Bowser Stages Goal (#2112) 2023-10-28 21:44:16 +02:00
el-u
2353346768 minecraft: avoid duplicate prefix in output file name (#2048) 2023-10-28 21:43:09 +02:00
t3hf1gm3nt
4b95065c47 TLOZ: Update setup doc to include what version of TLOZ is required (#2395) 2023-10-28 13:49:07 -05:00
eudaimonistic
f5e9fc9b34 Docs, WebHost: Update faq_en.md (#2313)
* Update faq_en.md

Reorganizing information and adding links to some of the various guides and website pages.  Even just adding the Getting Started, Supported Games, and Server Commands links seems like a hefty upgrade.  We have good resources, we should make them obvious.

I think more can probably be done here, but I already shuffled this around a lot.

* Reorganize information again, elaborate single player

Sneaki's suggestion makes way more sense organizationally.  Added more detail to the single player section to more clearly explain the easiest method.

* Usage of multi-world

Consistency

Co-authored-by: kindasneaki <ryandj67@hotmail.com>

* More multi-world

More consistency

Co-authored-by: kindasneaki <ryandj67@hotmail.com>

* Revert to multiworld

Makes more sense and is colloquially the preferred terminology.

* Rework "leaving early"

Changed the "What if a player needs to leave early" section into, "Does everyone need to be connected at the same time?"

This allows the FAQ to explain briefly what a sync multiworld and an async multiworld is.  This is probably good material for the Glossary, but it comes up so much in the Discord that we probably need to explain it here as briefly as possible.  This paragraph lends itself to the question of what to do if a player must leave early anyway.

* Grammatical, tensing, and voice updates for consistency with other pages I originally authored.

---------

Co-authored-by: kindasneaki <ryandj67@hotmail.com>
Co-authored-by: Chris Wilson <chris@legendserver.info>
2023-10-28 14:18:11 -04:00
black-sliver
bf46e0e60f Core: deprecate Utils.get_options and remove Utils.get_default_options (#2352)
* Core: deprecate Utils.get_options and remove Utils.get_default_options

* L2AC, Adventure: use settings instead of Utils.get_options
2023-10-28 19:32:12 +02:00
kindasneaki
7bddea3ee8 Hollow Knight: update item name groups (#2331)
* add missing groups

* remove set comprehensions

* fix boss essence

* reorganized them

* combine boss essence on creation instead of update

* rename to match option names

* Add missing groups

* add PoP totem
2023-10-28 13:30:18 +02:00
Alchav
bdc15186e7 Pokémon R/B: Fix cave surf bug (#2389) 2023-10-28 06:40:06 +02:00
Fabian Dill
20dd478fb5 OoT: create and copy less useless data state (#2379) 2023-10-28 03:13:08 +02:00
black-sliver
e3112e5d51 Stardew Valley: Cut tests by 3 minutes (#2375)
* Stardew Valley: Test: unify mods

* Stardew Valley: Test: don't use SVTestBase where setUp is unused

* Stardew Valley: Test: remove duplicate backpack test

* Stardew Valley: Test: remove 2,3,4 heart tests

assume the math is correct with just 2 points on the curve

* Stardew Valley: Test: reduce duplicate test/gen runs

* Stardew Valley: Test: Change 'long' tests to not use TestBase

TestBase' setUp is not being used in the changed TestCases

* Stardew Valley: Test: Use subtests and inheritance for backpacks

* Stardew Valley: Test: add flag to skip some of the extensive tests by default
2023-10-28 00:18:33 +02:00
Fabian Dill
c470849cee Core: remove custom_data (#2380) 2023-10-27 19:10:16 +02:00
black-sliver
fc2855ca6d Stardew Valley: speed up rules creation by 4% (#2371)
* Stardew Valley: speed up rules creation by 4%

No class should ever inherit from And, Or, False_ or True_ and isinstance is not free.
Sadly there is no cheap way to forbid inheritance, but it was tested using metaclass.

* Stardew Valley: save calls to type()

Local variable is a bit faster than fetching type again

* Stardew Valley: save calls to True_() and False_(), also use 'in' operator

* Stardew Valley: optimize And and Or simplification

* Stardew Valley: optimize logic constructors
2023-10-27 18:09:12 +02:00
ArashiKurobara
6a2407468a OoT: Update YAML Instructions (#1745)
Existing setup guide hard-coded in a YAML from 0.1.7
2023-10-27 15:43:36 +02:00
Aaron Wagener
9281011315 Tests: Add a unit test for slot_data (#2333)
* Tests: Add a unit test for slot_data

* use NetUtils.encode

* modern PEP8
2023-10-27 12:33:59 +02:00
Aaron Wagener
d595b1a67f Docs: slight adding games.md rework (#1192)
* begin reworking adding games.md

* make it presentable

* some doc cleanup

* style cleanup

* rework the "more on that later" section of SDV

* remove now unused images

* make the doc links consistent

* typo

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

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-10-27 12:30:32 +02:00
Aaron Wagener
16fe66721f Stardew Valley: Use the pre-existing cache rather than ignoring it (#2368) 2023-10-27 12:12:17 +02:00
Jarno
3b5f9d1758 Timespinner: Fixed generation error caused by new options system (#2374) 2023-10-27 12:01:46 +02:00
Bryce Wilson
0f7ebe389e BizHawkClient: Add better launcher component suffix handling (#2367) 2023-10-27 06:14:25 +02:00
Justus Lind
6061bffbb6 Pokemon R/B: Avoid a case of repeatedly checking of state in ER (#2376) 2023-10-27 06:12:04 +02:00
Bryce Wilson
b16804102d BizHawkClient: Add lock for communicating with lua script (#2369) 2023-10-27 03:55:46 +02:00
Fabian Dill
88d69dba97 DLCQuest: logic speed up (#2323) 2023-10-26 00:51:32 +02:00
Fabian Dill
aa73dbab2d Subnautica: avoid cache recreation in create_regions call and clean up function. (#2365) 2023-10-26 00:03:14 +02:00
Fabian Dill
dab704df55 Core/LttP: remove initialize_regions (#2362) 2023-10-25 21:23:52 +02:00
Felix R
e5ca83b5db Bumper Stickers: add location rules (#2254)
* bumpstik: treasure/booster location rules

* bumpstik: oop missed a bit

* bumpstik: apply access rule to Hazards check

* bumpstik: move completion cond. to set_rules

* bumpstik: tests?
I have literally never written these before so 🤷

* bumpstik: oops

* bumpstik: how about this?

* bumpstik: fix some logic

* bumpstik: this almost works but not quite

* bumpstik: accurate region boundaries for BBs
since we're using rules now

* bumpstik: holy heck it works now
2023-10-25 10:22:09 +02:00
Aaron Wagener
be959c05a6 The Messenger: speed up generation for large multiworlds (#2359) 2023-10-25 09:56:56 +02:00
black-sliver
e5554f8630 SoE: create regions cleanup and speedup (#2361)
* SoE: create regions cleanup and speedup

keep local reference instead of hitting multiworld cache
also technically fixes a bug where all locations are in 'menu', not 'ingame'

* SoE: somplify region connection
2023-10-25 09:34:59 +02:00
black-sliver
e87d5d5ac2 SoE: update to v0.46.1
* install via pypi, pin hashes
* add OoB logic option
* add sequence break logic option
* fix turd ball texts
* add option to fix OoB
* better textbox handling when turning in energy core fragments
2023-10-25 00:52:57 +02:00
black-sliver
58642edc17 Core: allow multi-line and --hash in requirements.txt 2023-10-25 00:52:57 +02:00
Aaron Wagener
90c5f45a1f Options: have as_dict return set values as lists to reduce JSON footprint (#2354)
* Options: return set values as lists to reduce JSON footprint

* sorted()

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-10-24 22:50:53 +02:00
black-sliver
78a4b01db5 pytest: run tests on non-windows with new names (#2349) 2023-10-24 10:59:15 +02:00
Fabian Dill
426e9d3090 LttP: make Triforce Piece progression_skip_balancing (#2351) 2023-10-24 08:16:46 +02:00
Seldom
706a2b36db Terraria Old One's Army tier 2 and 3 missing Hardmode req (#2342) 2023-10-24 07:27:57 +02:00
Aaron Wagener
764128568e WebHost: consistent naming for player options (#2037)
* WebHost: unify references to options

* it was just an extra s the whole time...

* grammar

* redirect from old pages

* redirect stuff correctly

* use url_for

* use " for modified strings

* remove redirect cache

* player_settings

* update site map
2023-10-24 02:20:08 +02:00
Justus Lind
12c73acb20 Muse Dash: Make which .net to download more explicit in setup guides. (#2328) 2023-10-23 15:39:37 -05:00
el-u
8109d4a1af lufia2ac: prevent "door stairs" and "rare stairs" (#2341) 2023-10-23 22:20:27 +02:00
Fabian Dill
e394c316f5 Setup: new setup experience (read: torch almost all of it) (#2268) 2023-10-23 22:07:24 +02:00
Alchav
195cf60e8a Pokémon R/B: Door Shuffle efficiency improvement and crash fix (#2347)
Sweep only current player's locations so that more players does not slow it down.
Fix a slight possibility of Full door shuffle crash by only sorting for outdoor dead ends only when connecting from a non-dead end.
2023-10-23 19:28:16 +02:00
espeon65536
724999fc43 Ocarina of Time: long-awaited bugfixes (#2344)
- Added location name groups, so you can make your entire Water Temple priority to annoy everyone else
- Significant improvement to ER generation success rate (~80% to >99%)
- Changed `adult_trade_start` option to a choice option instead of a list (this shouldn't actually break any YAMLs though, due to the lesser-known property of lists parsing as a uniformly-weighted choice)
- Major improvements to the option tooltips where needed. (Possibly too much text now)
- Changed default hint distribution to `async` to help people's generation times. The tooltip explains that it removes WOTH hints so people hopefully don't get tripped up.
- Makes stick and nut capacity upgrades useful items
- Added shop prices and required trials to spoiler log
- Added Cojiro to adult trade item group, because it had been forgotten previously
- Fixed size-modified chests not being moved properly due to trap appearance changing the size
- Fixed Thieves Hideout keyring not being allowed in start inventory
- Fixed hint generation not accurately flagging barren locations on certain dungeon item shuffle settings
- Fixed bug where you could plando arbitrarily-named items into the world, breaking everything
2023-10-22 18:38:47 +02:00
BootsinSoots
50244342d9 Docs: Added Note Explaining BK and fix typo in advanced settings (#2316)
* Added Note Explaining BK

Added suggested change regarding BK mode from Issue #2295

* Changed to Glossary hyperlink

* Fix minor typo in exclude_locations

* Update worlds/generic/docs/advanced_settings_en.md

Co-authored-by: kindasneaki <ryandj67@hotmail.com>

* Docs: Reformat advanced_settings_en/progression_balancing

---------

Co-authored-by: kindasneaki <ryandj67@hotmail.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-10-22 13:11:19 +02:00
Aaron Wagener
30da81c390 Tests: modern PEP8-ify core test modules and methods (#2298)
* rename modules

* rename methods

* add docstrings to the general tests

* add base import stub

* test_base -> bases

* print deprecation warning

* redo 2346
2023-10-22 13:00:27 +02:00
Aaron Wagener
6e6fa13e44 Tests: add multiworld seed to fill subtest (#2346) 2023-10-22 12:12:26 +02:00
NewSoupVi
9f126ad0d0 The Witness: Fix random events not having the correct probabilities (#2340) 2023-10-22 06:48:06 +02:00
Fabian Dill
ee31051c43 WebHost: offer combined yaml file on /check if successful (#2337) 2023-10-22 02:02:30 +02:00
agilbert1412
a5022ccfc5 - Fix Stardew valley option that was accidentally renamed in 993 (#2336) 2023-10-21 23:28:07 +02:00
el-u
1c4303cce6 lufia2ac: add shops to the cave (#2103)
This PR adds a new, optional aspect to the Ancient Cave experience:
During their run, players can have the opportunity to purchase some additional items or spells to improve their party. If enabled, a shop will appear everytime a certain (configurable) number of floors in the dungeon has been completed. The shop inventories are generated randomly (taking into account player preference as well as a system to ensure that more expensive items can only become available deeper into the run).

For customization, 3 new options are introduced: 
- `shop_interval`: Determines by how many floors the shops are separated (or keeps them turned off entirely)
- `shop_inventory`: Determines what's possible to be for sale. (Players can specify weights for general categories of things such as "weapon" or "spell" or even adjust the probabilities of individual items)
- `gold_modifier`: Determines how much gold is dropped by enemies. This is the player's only source of income and thus controls how much money they will have available to spend in shops
2023-10-21 23:27:30 +02:00
Silvris
7c2cb34b45 Plando: prevent duplicate candidate locations (#2286) 2023-10-21 12:59:53 +02:00
Fabian Dill
1a1d607b10 Core: explicitly limit threadpool (#2334) 2023-10-20 05:14:12 +02:00
black-sliver
56796b7ee8 WebHost: minor css changes to make Supported Games page usable without js (#2266)
* WebHost: minor css changes to make Supported Games page usable without js

* Update JS to use querySelectorAll, remove most id attributes from elements, use relative element selectors

* Hide content when clearing search bar

* Remove `console.log`, remove TODO

---------

Co-authored-by: Chris Wilson <chris@legendserver.info>
2023-10-19 20:58:41 -04:00
espeon65536
b82f48fe4b Core: guard against plandoing items onto event locations (#2284) 2023-10-20 02:23:32 +02:00
Justus Lind
385803eb5c Muse Dash: Add support for specifying specific DLCs (#2329) 2023-10-20 02:13:17 +02:00
Aaron Wagener
fb6b66463d OC2: fix mistakes when moving to new options api (#2332) 2023-10-20 01:36:18 +02:00
Bryce Wilson
b707619aad BizHawkClient: Add autostart setting (#2322) 2023-10-19 07:07:15 +02:00
Natalie Weizenbaum
38c9ee146d WebHost: Refactor weighted-settings.js (#2318)
* Refactor weighted-settings.js

This moves most of the infrastructure into two classes:

* WeightedSettings covers the settings page as a whole. It tracks the
  user's current settings in local storage as well as the game data
  from the server so they don't need to be manually passed around from
  function to function.

* GameSettings covers the settings for a single game, and provides a
  view of the current settings and the game data just for that game.

* Fix item count updating
2023-10-18 18:26:52 -04:00
PsyMarth
1c7c83c69e OoT: Update Utils.py (#2310)
Removed optional maxsize parameter, setting it to the default of 128.
2023-10-18 23:53:54 +02:00
Aaron Wagener
e8a48da315 SM: fix missing option import (#2326) 2023-10-18 16:04:12 -05:00
Zach Parks
45e69f3d26 Docs: Triage role expectations documentation. (#2325)
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2023-10-18 15:11:25 -05:00
Alchav
7aab9d4439 Docs: Recommend Bizhawk Version 2.9.1 for Pokémon R/B (#2320) 2023-10-18 21:55:03 +02:00
agilbert1412
5ca1ababfd DLC Quest: Fix code structure, typos, poor code quality (#2066)
"Added a bunch of tests to make sure I don't break anything during refactoring
Huge cleanup in the Regions file, extract methods, remove code duplicate, fix typos, fix variable naming conventions, etc.
Small cleanup in other places, minor stuff just what was needed for Regions"
2023-10-18 21:53:12 +02:00
Trevor L
11ebc523a9 Hylics 2: Various fixes and APWorld support (#2324)
- Fix generation failing with certain gesture shuffle options
    - Fixed passing ItemDict to multidata instead of item code
    - Don't allow CHARGE UP to be placed at Foglast: TV
- APWorld support by removing LogicMixin from Rules.py
2023-10-18 21:50:57 +02:00
Alchav
13b68ecb15 Pokémon R/B: Door Shuffle fixes (#2314)
* Door shuffle fixes

* Add Rt 23's Victory Road exit door to list of unreachable outdoor entrances
2023-10-17 07:20:34 +02:00
Exempt-Medic
e27aeac2e5 HK: Update Setup Guide to use/mention Lumafly (#2308) 2023-10-16 19:59:07 -05:00
el-u
63c7f1deae lufia2ac: switch to new options system (#2289) 2023-10-15 04:53:28 +02:00
Fabian Dill
fffbe68428 Subnautica: cleanup pass (#2293) 2023-10-15 04:51:52 +02:00
Shiny
8fc304269e Docs: add Spanish guide for Muse Dash (#2297)
* adding setup_es

* Update setup_es.md

* Update setup_es.md

* Update __init__.py

referencing setup_es on init.py

* Update __init__.py

fixing a space
2023-10-12 19:51:10 -04:00
NewSoupVi
19d649f92b The Witness: Update docs (outdated information) (#2294)
* Update Witness Game Page

* Update outdated Witness Setup Guide

* Incorporate suggestions
2023-10-12 19:46:16 -04:00
Fabian Dill
1ef3bc78dc CommonClient: inherit Context tags (#2283) 2023-10-11 19:21:02 +02:00
Aaron Wagener
e1ee08a599 FFR: create items in create_items (#2291) 2023-10-11 01:51:13 +02:00
Remy Jette
88dfbd4087 WebHost: Show error instead of 500 for unexpected files in multidata zip (#2260)
* WebHost: Show error instead of 500 for unexpected files in multidata zip

* Add filename to error message

* 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>
2023-10-10 23:20:08 +02:00
Doug Hoskisson
d7475ddd73 Zillion: remove test detection hack (#2287) 2023-10-10 23:08:19 +02:00
Aaron Wagener
7193182294 Core: move option results to the World class instead of MultiWorld (#993)
🤞 

* map option objects to a `World.options` dict

* convert RoR2 to options dict system for testing

* add temp behavior for lttp with notes

* copy/paste bad

* convert `set_default_common_options` to a namespace property

* reorganize test call order

* have fill_restrictive use the new options system

* update world api

* update soe tests

* fix world api

* core: auto initialize a dataclass on the World class with the option results

* core: auto initialize a dataclass on the World class with the option results: small tying improvement

* add `as_dict` method to the options dataclass

* fix namespace issues with tests

* have current option updates use `.value` instead of changing the option

* update ror2 to use the new options system again

* revert the junk pool dict since it's cased differently

* fix begin_with_loop typo

* write new and old options to spoiler

* change factorio option behavior back

* fix comparisons

* move common and per_game_common options to new system

* core: automatically create missing options_dataclass from legacy option_definitions

* remove spoiler special casing and add back the Factorio option changing but in new system

* give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly

* reimplement `inspect.get_annotations`

* move option info generation for webhost to new system

* need to include Common and PerGame common since __annotations__ doesn't include super

* use get_type_hints for the options dictionary

* typing.get_type_hints returns the bases too.

* forgot to sweep through generate

* sweep through all the tests

* swap to a metaclass property

* move remaining usages from get_type_hints to metaclass property

* move remaining usages from __annotations__ to metaclass property

* move remaining usages from legacy dictionaries to metaclass property

* remove legacy dictionaries

* cache the metaclass property

* clarify inheritance in world api

* move the messenger to new options system

* add an assert for my dumb

* update the doc

* rename o to options

* missed a spot

* update new messenger options

* comment spacing

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

* fix tests

* fix missing import

* make the documentation definition more accurate

* use options system for loc creation

* type cast MessengerWorld

* fix typo and use quotes for cast

* LTTP: set random seed in tests

* ArchipIdle: remove change here as it's default on AutoWorld

* Stardew: Need to set state because `set_default_common_options` used to

* The Messenger: update shop rando and helpers to new system; optimize imports

* Add a kwarg to `as_dict` to do the casing for you

* RoR2: use new kwarg for less code

* RoR2: revert some accidental reverts

* The Messenger: remove an unnecessary variable

* remove TypeVar that isn't used

* CommonOptions not abstract

* Docs: fix mistake in options api.md

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

* create options for item link worlds

* revert accidental doc removals

* Item Links: set default options on group

* change Zillion to new options dataclass

* remove unused parameter to function

* use TypeGuard for Literal narrowing

* move dlc quest to new api

* move overcooked 2 to new api

* fixed some missed code in oc2

* - Tried to be compliant with 993 (WIP?)

* - I think it all works now

* - Removed last trace of me touching core

* typo

* It now passes all tests!

* Improve options, fix all issues I hope

* - Fixed init options

* dlcquest: fix bad imports

* missed a file

* - Reduce code duplication

* add as_dict documentation

* - Use .items(), get option name more directly, fix slot data content

* - Remove generic options from the slot data

* improve slot data documentation

* remove `CommonOptions.get_value` (#21)

* better slot data description

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

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@yahoo.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-10-10 22:30:20 +02:00
Fabian Dill
a7b4914bb7 WebHost: update flask (#2250)
* WebHost: update flask

* WebHost: update flask-caching

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-10-09 10:18:41 +02:00
Fabian Dill
0d8a868ed9 Utils: support messagebox on windows without dependencies (#2224) 2023-10-08 22:14:28 +02:00
Aaron Wagener
6f9484f375 The Messenger: Make modules and tests PEP8 (#2276)
* The Messenger: PEP8 module and test names

* fix dumb

* whitespace
2023-10-08 14:33:39 +02:00
Fabian Dill
cc2247bfa0 CommonClient: fix json prints not being logged in UI mode (#2253) 2023-10-08 13:26:14 +02:00
Aaron Wagener
5eeaf834cb Tests: Add a test for fill to WorldTestBase (#2049)
* Tests: Add a test for fill to WorldTestBase

* test items and minimal accessibility, only bailing out when no reachable locations exist.

* put egg shard max/goal at sane values
114 locations - 35 always-present progression items - 25 excluded locations from settings <= 74 egg shards
past me can't do arithmetic

* f

* i'm bad at git

* make fill import local to prevent circular imports

---------

Co-authored-by: espeon65536 <espeon65536@gmail.com>
2023-10-08 12:08:47 +02:00
Aaron Wagener
fd93f6e722 Tests: add can_reach_region method to WorldTestBase (#2274) 2023-10-08 11:46:30 +02:00
Fabian Dill
5591879547 WebHost/Factorio: use "better" jinja practices in web tracker (#2257) 2023-10-08 11:30:34 +02:00
Alex Nordstrom
c3c6a7eb86 LADX: Set display names in options (#2229)
* set display_name throughout Options.py

* drop whitespace changes

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-10-07 18:36:22 +02:00
Fabian Dill
b8fe3196e0 Setup: also delete old disabled worlds (#2267) 2023-10-07 16:44:21 +02:00
el-u
6028112e0e checksfinder: create items in create_items (#2056) 2023-10-07 16:44:01 +02:00
Fabian Dill
7df1b6f496 LttP: Adjuster no longer breaks when sprite path doesn't exist. 2023-10-07 15:57:05 +02:00
Shiny
debdd4c571 Docs: Pokemon RB Spanish Setup Guide: fixed a bunch of punctuation/grammar and fixed bold format in Configuring Bizhawk section (#2228)
* added setup_es.md

setup_en 100% translated (with a bit of adaptation to spanish linguistics)

* Update __init__.py

add reference to the spanish tutorial

* Update setup_es.md

removed temporary "wip translation" header

* Update setup_es.md

formatting cleanup

* Update setup_es.md

translated "alias for" on lines 73 and 74, which I just forgot to

* Update setup_es.md

fixed a bunch of punctuation/grammar and fixed bold format in Configuring Bizhawk section

* Update worlds/pokemon_rb/docs/setup_es.md

updated bold format as per nicholassaylor's suggestion

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

---------

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2023-10-07 15:13:17 +02:00
kindasneaki
bb09811433 Docs: add info to turn on github actions (#2264)
* add info to turn on github actions

* add missing image

* add when pushing

* reduce picture size

* mention editing actions on your fork instead
2023-10-05 11:41:49 +02:00
kindasneaki
115a6b666c Webhost: random button presisted after being inactive (#2248)
* update game settings to get the proper attribute

* change to ternary operator
2023-10-04 23:53:59 -04:00
Nicholas Saylor
6c4a3959c3 Docs: Categorize Commands in Guide (#2213)
* Update commands_en.md

Commands re-ordered and put into categories

Some commands were better documented / explained more clearly

Other formatting changes

* Status command moved to General category and elaboration on getitem command

* "Multi-world" -> "Multiworld"

* Moved game-specific local commands to game pages
2023-10-04 16:52:34 -04:00
Aaron Wagener
f6e92a18de The Messenger: Fix items accessibility region rule (#2263) 2023-10-04 18:23:29 +02:00
Aaron Wagener
78057476f3 Docs: python 3.11 works now (#2258)
* Docs: python 3.11 works now

* change to py 3.12 unsupported
2023-10-03 12:19:09 +02:00
black-sliver
cdbb2cf7b7 Core: fix unittest world discovery (#2262) 2023-10-03 09:47:22 +02:00
Fabian Dill
6b48f9aac5 WebHost: link to stats from the use statistics directly on landing (#2242) 2023-10-02 20:52:58 -04:00
Bryce Wilson
bc11c9dfd4 BizHawkClient: Add BizHawkClient (#1978)
Adds a generic client that can communicate with BizHawk. Similar to SNIClient, but for arbitrary systems and doesn't have an intermediary application like SNI.
2023-10-03 02:44:19 +02:00
Bryce Wilson
24403eba1b Launcher: Allow opening patches for clients without an exe (#2176)
* Launcher: Allow opening patches for clients without an exe

* Launcher: Restore behavior for not showing patch suffixes for clients that aren't installed
2023-10-02 20:52:00 +02:00
black-sliver
e377068d1f Core: more gitignore (#2249)
gitignore versioned venvs, prof output, appimagetool and sni downloads
2023-10-02 20:17:34 +02:00
Fabian Dill
c7c94eebeb WebHost: fix indentation (#2240) 2023-10-02 20:07:31 +02:00
Fabian Dill
9d38725688 WebHost: update ponyorm (#2241) 2023-10-02 20:07:15 +02:00
Fabian Dill
18bf7425c4 WebHost: cache static misc pages (#2245) 2023-10-02 20:06:56 +02:00
Fabian Dill
17127a4117 kvui: silently fail to disable DPI awareness on Windows (#2246) 2023-10-02 20:06:29 +02:00
black-sliver
5d9b47355e CI: run tests multi-threaded (#2251) 2023-10-02 08:47:28 +02:00
black-sliver
f9761ad4e5 CI: ignore invalid hostname of some macos runners (#2252) 2023-10-02 08:34:50 +02:00
el-u
485aa23afd core: utility method for visualizing worlds as PlantUML (#1935)
* core: typing for MultiWorld.get_regions

* core: utility method for visualizing worlds as PlantUML

* core: utility method for visualizing worlds as PlantUML: update docs
2023-10-02 01:56:55 +02:00
zig-for
e08deff6f9 LADX: Implement remake style warp selection (#1587) 2023-10-02 01:16:25 +02:00
Doug Hoskisson
d5d630dcf0 Zillion: change test detection for running tests with multiprocessing (#2243) 2023-10-02 01:13:30 +02:00
Fabian Dill
58b696e986 Factorio: use orjson (#1809)
* Factorio: use orjson

* Factorio: update orjson

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-09-30 23:58:58 +02:00
lordlou
a3907e800b SMZ3: 0.4.2 non local items fix (#2212)
fixed generation failure when using non_local_items set to "Everything"
For this, GT prefill now allows non local non progression items to be placed.
2023-09-30 21:05:07 +02:00
Trevor L
5c640c6c52 Blasphemous: Fix rules for platforming room in BotSS (#2231) 2023-09-30 21:03:55 +02:00
Aaron Wagener
fe6096464c The Messenger: fix rules for two glacial peak locations (#2234)
* The Messenger: fix rules for two glacial peak locations
2023-09-30 12:35:07 +02:00
Bryce Wilson
5bf3de45f4 DS3: Add cinders item group (#2226) 2023-09-30 12:32:44 +02:00
Aaron Wagener
f33babc420 Tests: add a name removal method (#2233)
* Tests: add a name removal method, and have assertAccessDependency use and dispose its own state

* Update test/TestBase.py

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-09-30 11:53:11 +02:00
Alchav
1c9199761b LTTP: Key Drop Shuffle fix for dungeon state item removal (#2232) 2023-09-29 20:23:46 +02:00
CaitSith2
368fa64914 LttP: Update credits text for GT Big Key when key drop shuffle is on. (#2235) 2023-09-29 20:18:43 +02:00
Fabian Dill
e114ed5566 Core: offer API hook to modify Group World creation 2023-09-27 11:26:38 +02:00
Fabian Dill
5d47c5b316 WebHost: check that worlds system is not loaded in customserver (#2222) 2023-09-27 11:26:08 +02:00
Alchav
812dc413e5 LTTP: Key Drop Shuffle (#282)
Co-authored-by: espeon65536 <81029175+espeon65536@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Bondo <38083232+BadmoonzZ@users.noreply.github.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-09-27 05:24:10 +02:00
Justus Lind
4aed2be93b Muse Dash: Add mentions to Muse Plus to Docs and Options. (#2179)
* Add mentions to Muse Plus.

* Grammer fix.

* Apply Exempt-Medics Suggestion

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

* Make [Just as Planned] casing consistent. Fix grammar on Available Trap Types option.

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2023-09-25 22:21:09 -04:00
Chris Wilson
974bab2b24 [WebHost] Add search filter and collapse button to Supported Games page (#2215)
* Add search filter and collapse button to Supported Games page

* Autofocus search input, fix bug with arrow display when searching

* Add "Expand All" and "Collapse All" buttons. Buttons respect visible games.
2023-09-25 22:15:00 -04:00
Daivuk
98d61b32af DOOM 1993: Logic fixes 2023-09-26 01:08:56 +02:00
Fabian Dill
4141a50d8c Terraria: remove unused data 2023-09-25 23:35:55 +02:00
black-sliver
93c18cd9a7 Core/GUI: Better fileselection error (#2216)
* Core: better error if GUI is unavailable

* Core: enable open_directory kdialog and zenity

The native dialog helpers were disabled because there was odd behavior.
This is now fixed and was tested with latest zenity and kdialog.

* Core: fix open_filename suggestion for zenity
2023-09-24 10:30:33 +02:00
Trevor L
5af47425b0 Hylics 2: Add more missing locations (#2219)
Adds two missing locations in Sage Labyrinth and their items
2023-09-24 08:08:40 +02:00
NewSoupVi
b41a1e69b4 The Witness: Fix Itemlinks 2023-09-24 02:04:27 +02:00
Felix R
124113f3d3 bumpstik: update docs (#2198) 2023-09-23 00:54:21 -04:00
Shiny
2b69820619 Pokémon Red and Blue: Adding Spanish Setup Guide (#2205)
* added setup_es.md

setup_en 100% translated (with a bit of adaptation to spanish linguistics)

* Update __init__.py

add reference to the spanish tutorial

* Update setup_es.md

removed temporary "wip translation" header

* Update setup_es.md

formatting cleanup

* Update setup_es.md

translated "alias for" on lines 73 and 74, which I just forgot to
2023-09-23 00:45:46 -04:00
Bicoloursnake
f147f9e5a0 Docs: DKC3, Lufia2AC, SM, SMW, SMZ3: Updating documentation with the current location of SNI Connector.lua (#2203)
* Update SC2 setup guide

Removed a sentence that made sense when I included sudo in the command in the previous sentence, but does not make sense otherwise.

* Update en_Super Mario 64.md

It turns out castle has a lowercase l in it.

* Docs: SMW: Updated SNIClient Connector Lua Directory

* Docs: DKC3: Updated SNIClient Connector Lua Directory

* Docs: Lufia2AC: Updated SNIClient Connector Lua Directory

* Docs: SM: Updated SNIClient Connector Lua Directory

* Docs: SMZ3: Updated SNIClient Connector Lua Directory
2023-09-23 00:40:47 -04:00
Nicholas Saylor
db7c0c9db9 Docs: Clarify Documentation Information for Undertale, Terraria, DOOM 1993 (#2149)
* Cleaned up Undertale documentation
Standardized file names

* Outlined Terraria installation more clearly
Other minor edits to setup guide

* Minor edits to DOOM 1993 set-up guide

* Update worlds/terraria/docs/setup_en.md

Co-authored-by: kindasneaki <ryandj67@hotmail.com>

* Suggested changes from @Seldom-SE

Co-authored-by: Seldom <38388947+seldom-se@users.noreply.github.com>

* Code block to quotation change from code review

Co-authored-by: Seldom <38388947+seldom-se@users.noreply.github.com>
Co-authored-by: Chris Wilson <chris@legendserver.info>

* Code review from @LegendaryLinux

Co-authored-by: Chris Wilson <chris@legendserver.info>

---------

Co-authored-by: kindasneaki <ryandj67@hotmail.com>
Co-authored-by: Seldom <38388947+seldom-se@users.noreply.github.com>
Co-authored-by: Chris Wilson <chris@legendserver.info>
2023-09-22 20:45:52 -04:00
eudaimonistic
b40fba0840 Subnautica: Update en_Subnautica.md (#2207)
Added reference to the four goal options, and a small grammatical change.
2023-09-22 23:08:27 +02:00
black-sliver
ea799c494e Speedups: fix file date check when frozen (#2211) 2023-09-22 23:05:04 +02:00
Fabian Dill
b4b8426def Core: update jellyfish 2023-09-22 21:39:29 +02:00
black-sliver
39a50da55c Factorio: fix world generation in spoiler (#2209)
This used a set operation previously, resulting in random order of dict items.
2023-09-22 21:32:03 +02:00
Ziktofel
9931605f94 SC2 Client: Fix processing metadata from Github Releases for /download_data 2023-09-22 21:27:46 +02:00
NewSoupVi
8834ba88aa Witness: Expert Doors Logic Fix
Expert Swamp Maze currently thinks it needs Red Underwater 4 instead of the door. This could lead to an unbeatable seed in door shuffle, although it's very unlikely.
2023-09-22 02:55:44 +02:00
lordlou
5e46967b7d SM: 0.4.2 broken quick save and reload fix (#2204) 2023-09-22 02:49:27 +02:00
kindasneaki
638d6807db add invisible to locations div (#2191) 2023-09-20 16:53:00 -04:00
black-sliver
d471dcc067 Core, WebHost: lazy-load worlds in unpickler, WebHost and WebHostLib (#2156)
* Core: lazy-load worlds in unpickler

this should hopefully fix customserver's memory consumption

* WebHost: move imports around to save memory in MP

* MultiServer: prefer loading _speedups without pyximport

This saves ~15MB per MP and speeds up module import if it was built in-place.

* Tests: fix tests for changed WebHost imports

* CustomServer: run GC after setup

* CustomServer: cleanup exception handling
2023-09-20 16:05:56 +02:00
CaitSith2
4a27fae1ab Core: Allow any valid priority location in yaml even when they are not used in a given game. (#2128)
* Allow any valid priority location in yaml.

For some games, the use location group name "Everywhere", results in the generator failing no matter what,  as only a subset of the location names will actually be present.  A good example of that is Zillion.  It has 21 location names per room, of which, only at most 2 is ever used.


Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-09-20 11:22:01 +02:00
Remy Jette
794959e182 WebHost: Fix KeyError in alttp multitracker (#2194) 2023-09-20 10:20:03 +02:00
Mewlif
aff852fb45 Undertale: Various Fixes (#2146)
* Changed the pathing code to use os.path.join, instead of adding strings together, also fixed the savepath command using UndertaleContext instead of self.ctx (Credit to Wackerly for finding the self.ctx issue and fix)

* Undertale: Fixed a debug function in the game not requiring debug to be enabled.

* Undetale: Fixed a logic bug with the location "Letter Quest"
2023-09-20 10:18:53 +02:00
Remy Jette
a0eea3a650 WebHost: Don't count item links in the summary row completed worlds (#2193) 2023-09-20 01:27:49 +02:00
lordlou
0012584e51 SM: 0.4.2 percent goals fix (#2183)
fixed percent items goals that can fail generation (reported here https://discord.com/channels/731205301247803413/1147318124383850516/1147318124383850516 and here https://discord.com/channels/731205301247803413/1138137515505750108/1138137515505750108)
2023-09-20 01:26:42 +02:00
Silvris
6e02a4ca3c Plando: fix overwriting outer scope (#2196) 2023-09-20 00:43:37 +02:00
Fabian Dill
2ef05a1799 kvui: remove custom DPI scaling on windows (#2177) 2023-09-17 22:56:59 +02:00
Fabian Dill
fa2891f785 Factorio: offer error message with some more insight for lock file in use (#2187) 2023-09-17 22:47:06 +02:00
agilbert1412
d5d13a6d4d Stardew Valley: Fix two logic bugs with the wizard on Entrance Randomizer (#2192)
* - Added a rule to vault bundles that require access to the wizard
- Fixed the region required to meet the wizard

* - Updated the location count in a test due to a previous coffee bean bugfix that added a location
2023-09-17 20:20:18 +02:00
Doug Hoskisson
b24037e9d9 Zillion: ensure 1st sphere not empty (#2190) 2023-09-17 17:55:21 +02:00
Ziktofel
6d6de4a98e SC2: Typo fix in option display name 2023-09-16 23:00:35 +02:00
Fabian Dill
0e7c7bd1bf Core: update versions (#2186) 2023-09-16 19:23:22 +02:00
Fabian Dill
9312f14ffb Subnautica: add extra Laser Cutter Fragment to priority filler 2023-09-16 18:08:31 +02:00
Fabian Dill
ce8f07b347 Core: fix start_inventory_from_pool only adding one filler per item name 2023-09-16 18:07:40 +02:00
Bryce Wilson
cff6c7c4da DS3: Fix health locations setting not enabling (#2147)
* DS3: Fix health locations setting not enabling

* DS3: Move health locations to their own table

* DS3: Bump data version
2023-09-16 12:17:40 +02:00
Altiami
f9120c620f The Legend of Zelda Conntector: Make items obtained counter in save data 16 bits. (#2117)
Certain multiworld settings (bug observed with item link settings) can cause the total item count in TLoZ world to exceed 255. This causes an overflow in the loop to receive all pending items. This adds an additional byte to be used as a high byte for the items obtained counter.

This approach was taken due to the surrounding bytes being occupied, preventing a direct 16-bit number from being used without moving to a different location and leaving more empty bytes in the memory block for save data.
2023-09-15 20:18:03 +02:00
Ziktofel
44f1a93d31 Docs: Update SC2 map/mod link 2023-09-15 19:28:16 +02:00
Sunny Bat
6d61eae522 Raft: Fix test_collect_remove (#2109) 2023-09-15 09:30:46 +02:00
black-sliver
f05a9ecd2f settings: add default=None to Group.get (#2178)
This is regular dict behavior/emulation.
2023-09-15 09:18:14 +02:00
Ziktofel
648d682add SC2 WoL - Mod, Item and Location update (#2113)
Migrates SC2 WoL world to the new mod with new items and locations. The new mod has a different architecture making it more future proof (with planned adding of other campaigns). Also gets rid of several old bugs

Adds new short game formats intended for sync games (Tiny Grid, Mini Gauntlet). The final mission isn't decided by campaign length anymore but it's configurable instead. Allow excluding missions for Vanilla Shuffled, corrected some documentation.

NOTE: This is a squashed commit with Salz' HotS excluded (not ready for the release and I plan multi-campaign instead)

---------

Co-authored-by: Matthew <matthew.marinets@gmail.com>
2023-09-15 02:22:10 +02:00
BadMagic100
47cf3e06c0 Hollow Knight: Update outdated setup documentation (#2171)
* Hollow Knight: Update outdated setup documentation

* Update a reference from Scarab to Scarab+

Co-authored-by: kindasneaki <ryandj67@hotmail.com>

* Fix numbering

---------

Co-authored-by: kindasneaki <ryandj67@hotmail.com>
2023-09-14 23:57:36 +02:00
agilbert1412
fdac50523b Stardew Valley: Added missing logic rules for dating and marriage (#2160)
* - Added missing logic rules where, to earn hearts above 8 and 10, you need access to dating and marriage respectively.

* - Slight cleanup based on Black Sliver's suggestion
2023-09-14 23:56:13 +02:00
Alchav
7522a32ad6 Pokémon R/B: More tracker slot data (#2174) 2023-09-14 21:49:57 +02:00
lordlou
8ee743ac8a SM: 0.4.2 fixes (#2175)
## What is this fixing or adding?
- fixed failing generation with disabled layout patch by moving door_indicators_plms.ips to AP instead of the base patch (reported at https://discord.com/channels/731205301247803413/1149509811529072751/1149509811529072751)
(part of the fix is in the Basepatch with this commit 46bbda980c)

- fixed broken map data saving when using fast_save (reported at https://discord.com/channels/731205301247803413/1138163133089849344/1138163133089849344)
(part of the fix is in the Basepatch with this commit 54a82774c9)
2023-09-14 21:49:11 +02:00
Seldom
c3cfbf8e1c Terraria: Add the rest of the settings to slot data (#2116)
* Add the rest of the Terraria settings to slot data

* Update worlds/terraria/__init__.py

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-09-14 09:46:29 +02:00
Friðberg
1756a30acc WebHost: Clean up the exported yaml in weighted settings (#2167)
* Trim output yaml in weighted options

Remove options that have only one possible outcome as well as empty arrays, when building yaml.

* fix quotes
2023-09-11 17:17:11 -04:00
Remy Jette
57c13ff273 WebHost: Support multi-select during check/generate file upload (#2138)
* Support multi-select during check/generate file upload

This will allow the user to select multiple YAML files via Shift-Click
or Control-Click in their browser when generating a game via the site
instead of having to zip them locally first.

* Update generate.html: File -> File(s)

* Change check.html button text to "Upload File(s)" to match generate.html
2023-09-11 16:57:14 -04:00
Rob B
3d9837678c Factorio: better Technology Tree Information description (#2121)
* Fix typo in Factorio options tooltip

* Fix typo, add details

* Apply code review suggestion

It doesn't let me apply more than one change to the same line in a batch.

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

* Apply code review suggestion from @nicholassaylor

It doesn't let me apply more than one change to the same line in a batch.

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2023-09-11 00:13:39 +02:00
Trevor L
3e95ccd06c Blasphemous: Fixed Amanecidas not requiring Petrified Bell (#2166) 2023-09-11 00:04:57 +02:00
Alchav
0e21a3e121 Pokémon R/B: Fix broken options (#2162) 2023-09-10 23:38:56 +02:00
NewSoupVi
5eef7a34d3 The Witness: Fix Expert Tutorial Gate Close (#2164) 2023-09-10 23:34:20 +02:00
blastron
6c844750ae Witness: fix items being modified by other slots (#2161) 2023-09-10 23:29:42 +02:00
Trevor L
8649b15787 Blasphemous: Add missing logic (#2165)
* Blasphemous: Set rules for events later

* Blasphemous: More misc logic fixes

* Update worlds/blasphemous/Rules.py

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

* Update worlds/blasphemous/Rules.py

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

* Blasphemous: Some cleanup

* Blasphemous: Add missing logic

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-09-10 23:24:33 +02:00
Remy Jette
fbd64651e4 Pokemon RB: Fix typo on the game info page (#2142)
Thanks Shiny for pointing it out https://discord.com/channels/731205301247803413/1043592720603693167/1147300361883893790
2023-09-10 23:24:09 +02:00
Doug Hoskisson
e01eb4e00c Zillion: webhost config fix (#2145) 2023-09-10 23:03:22 +02:00
Fabian Dill
72b44be41c SNIClient: fix /snes command if tree (#791) 2023-09-10 07:19:40 +02:00
Bryce Wilson
2bdb1b2029 DS3: Update game page (#2163)
* DS3: Update game page

* DS3: Split long sentence in game page docs

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

* DS3: Minor word change

---------

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2023-09-10 04:41:52 +02:00
Bicoloursnake
bf685dc850 Docs, SM64, SC2: Minor Documentation Updates (#2008)
* Update SC2 setup guide

Removed a sentence that made sense when I included sudo in the command in the previous sentence, but does not make sense otherwise.

* Update en_Super Mario 64.md

It turns out castle has a lowercase l in it.
2023-09-10 03:51:12 +02:00
Brooty Johnson
faf4887616 DS3: add more options to slot_data for autotracking (#2148) 2023-09-10 03:33:57 +02:00
Nicholas Saylor
a1418ccb66 Docs: Small typo and proofreading edits (#2078)
* Slight rewording of DS3 game page

Lists made more concise, space added between "generated weapons" and open parenthesis

* Proofread Final Fantasy pages

Fixed minor typos and reworded sentences for conciseness.

* Edited Kingdom Hearts 2 Game Page

Refined style, capitalization, and sentence structure for clarity

* Fixed nested list in Minecraft game page

Each nest needed an additional 2 spaces

* Edited Risk of Rain 2 Game Page

Made various edits to redundancy within the page as well as omitted/unclear information

* Edited Stardew Valley game page

Small capitalization consistency edits and slight rewording for conciseness

* Update worlds/ff1/docs/multiworld_en.md

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>

* Update worlds/kh2/docs/en_Kingdom Hearts 2.md

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>

* Update worlds/kh2/docs/en_Kingdom Hearts 2.md

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>

* Add information for EXP multiplier

Include Drive Forms and Summons

* Correction for Newt Altars RoR2

Co-Authored-By: kindasneaki <19377912+kindasneaki@users.noreply.github.com>

---------

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
Co-authored-by: kindasneaki <19377912+kindasneaki@users.noreply.github.com>
2023-09-10 03:30:03 +02:00
Fabian Dill
29f8053d6e Factorio: fix website multitracker (#2126)
Co-authored-by: Remy Jette <remy@remyjette.com>
2023-09-10 00:33:36 +02:00
Fabian Dill
f6dafa2b56 Core: collect errors from generate_output at same step as multidata 2023-09-09 19:55:11 +02:00
Fabian Dill
2b9e8fa273 WebHost: flask caching doesn't do lazy init anymore (#2155) 2023-09-09 05:02:05 +02:00
Mathx2
5368451867 Timespinner: Options.py Typo (#2154)
Line 63, changed the commend from (Reccomended) to (Recommended)
2023-09-07 22:23:42 +02:00
Fabian Dill
77a349c1c6 Core/LttP: remove can_reach_private 2023-08-31 22:10:38 +02:00
agilbert1412
c4a3204af7 Stardew Valley: Add missing special order logic rules (#2136)
* - Added missing special order requirements, mostly for the regions where to place the collected items, or the NPC to talk to when done

* - Added missing requirement on being able to go see the wizard cutscene in order to interact with bundles
2023-08-31 06:45:52 +02:00
Remy Jette
9323f7d892 WebHost: Add a summary row to the Multiworld Tracker (#1965)
* WebHost: Add a summary row to the Multiworld Tracker

Implements suggestions from the generation-suggestions channel:
- https://discord.com/channels/731205301247803413/1124186131911688262
- https://discord.com/channels/731205301247803413/1109513647274856518

* Improve secondsToHours function, and remove jQuery from footerCallback function.

* Don't show the summary row on game-specific multi trackers

---------

Co-authored-by: Chris Wilson <chris@legendserver.info>
2023-08-29 17:58:49 -04:00
Fabian Dill
30e747bb4c Windows: create terminal capable Launcher (#2111) 2023-08-29 20:59:39 +02:00
Justus Lind
9d29c6d301 Muse Dash: Fix bad generations occuring due to changing item ids (#2122) 2023-08-29 20:58:34 +02:00
NewSoupVi
aa19a79d26 Witness: Fix one of the hints not being a Haiku (seriously) (#2123)
I hope this gets a prize for "Most irrelevant PR in AP history"

Explanation:
When changing the hint system on the client side to be able to auto-wrap, decisions were made about which line breaks were still explicitly important, with most of them being removed.

This hint was somewhat devalued in the process.

-. --- - .... .. -. --. translates to "Nothing", which I thought was the entirety of the joke.

However, the line breaks were actually also important, because:

dash dot, dash dash dash,
dash, dot dot dot dot, dot dot,
dash dot, dash dash dot

is a Haiku! And the hint's creator (oddGarrett I believe) said this was specifically part of the creative vision for this joke hint. They said it's fine, I don't need to change it, but I couldn't let that stand.

So, the explicit line breaks for this joke hint are back.
2023-08-29 20:56:40 +02:00
Alchav
5a34471266 Pokémon R/B: Fix fishing logic mistake (#2133) 2023-08-29 20:56:07 +02:00
eudaimonistic
ae96010ff1 [Subnautica] update subnautica/docs/setup_en.md (#2131)
At some point the client-side mod for this world started to include support for the "!" in dev console, rendering this line obsolete.  Updated to reflect current client behavior.
2023-08-29 18:05:12 +02:00
CaitSith2
944fe6cb8c Fix root cause of ALttP hardware crashes on collect. (#2132)
As it turns out, SD2SNES / FXPAK Pro has a limit as to how many bytes can be written in a single command packet.  Exceed this limit, and the hardware will crash.  That limit is 512 bytes.  Even then, I have scaled back to 256 bytes at a time for a margin of safety.
2023-08-29 06:07:31 -07:00
agilbert1412
21baa302d4 [SDV] Added a missing logic rule for talking to Leo (#2129) 2023-08-29 00:55:13 +02:00
Fabian Dill
9ad0032eb4 Windows: fix ArchipelagoServer.exe not being installed. 2023-08-25 22:29:56 +02:00
Seldom
09fd65209c Terraria: Fix Soul of Sight requiring Post-The Twins flag instead of access to The Twins (#2115) 2023-08-25 22:27:28 +02:00
Aaron Wagener
d8d9a49564 The Messenger: Fix a typo preventing a location from being created (#2110)
* The Messenger: Fix a typo preventing a location from being created

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

* Change dexsanity option names

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

* Remove debug line.

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

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

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

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

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

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

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

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

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

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

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

* LTTP__init__.py added race condition to fill_slot_data

* added missing self to multiworl.is_race

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

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

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

---------

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

* Options: have OptionDict inherit from Mapping

* add typing to __getitem__

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

---------

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

* Tests: fix some tests not being run

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

* Tests: create a fresh host.yaml for TestHostYAML

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

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

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

* Fix Early Caves

* Remove unnecessary list() call

* Update worlds/witness/items.py

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

---------

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

* Removing @dewiniaid as maintainer for Hollow Knight.

2023-07-11 - Finalization Date for Vote

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

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

Passed with majority to remove maintainer status.

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

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

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

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

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

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

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

* The Messenger: add prerequisite shop cost requirements

* The Messenger: don't double the cost anymore

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

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

* The Messenger: hardcode the prereqs for more speed

* make the linter and mypy happier

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

* - Fix backpack locations on vanilla

* - Fixed a typo in documentation

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

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

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

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

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

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

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

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

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

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

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

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

* Fixed 3.8 typing in items

* Fixed 3.8 typing in player logic

* Fixed 3.8 typing in static_logic

* Fix 3.8 typing in utils

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

* Non-stupid dict sorting

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

* Even less stupid dict sorting

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

---------

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

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

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

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

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

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

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

## New Contributors:
@Witchybun for the mod support

---------

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

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

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

* settings: implement saving of dicts

* settings: simplify dump of dict

* settings: add support for sequences

also a few more comments

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

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

Introduced in 827444f5a4

* WebHost: Add assertion that baked_server_options is a dict

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

---------

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

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

* reduce code duplication and better naming in Region.connect

* thank you tests

* reorder class definition

* define entrance_type on Region

* document helpers

* drop __class_getitem__ for now

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

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

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

* settings: no BOM when saving

* settings: fix saving / groups resetting themselves

* settings: fix AutoWorldRegister import

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

* Lufia2: settings: clean up imports

* settings: more consistent class naming

* Docs: update world api for settings api refactor

* settings: fix access from World instance

* settings: update migration timeline

* Docs: Apply suggestions from code review

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

* Settings: correctly resolve .exe in UserPath and LocalPath

---------

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

* renumber properly

* Update setup_en.md

* Update setup_en.md

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

---------

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

* Server: validate locations in pure python implementation

and rework tests

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

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

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

* Speedups: add requirements

* CI: don't break with build_ext

* Speedups: use C++ compiler for pyximport

* Speedups: cleanup and more validation

* Speedups: add tests for LocationStore

* Setup: delete temp in-place build modules

* Speedups: more tests and safer indices

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

* Speedups: add cython3 compatibility

* Speedups: remove unused import

* Speedups: reformat

* Speedup: fix empty set in test

* Speedups: use regular dict in Locations.get_for_player

* CI: run unittests with beta cython

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

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

* Update locations_data.py

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

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

* first file dump

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

* bunch of changes

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

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

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

* fixed some logging

* add more detailed options descriptions

* forgot to update progressive locations updates too whoops

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

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

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

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

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

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

* updated options

* cleaned up more code, edited some option names

* start of new items system

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

* DS3: Repair changes overwritten by refactor

* DS3: Re-implement new options for location categories

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

* DS3: Remove accidentally added apworld

* DS3: Make option names more consistent

* DS3: Fix Pyromancer's Parting Flame location category

* DS3: Add new items

* DS3: Fix access rule for DLC/Contraption Key

* DS3: Only replace unrandomized progression items with events

Also fix some location names/categories

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

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

* DS3: Make varied item pool an option

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

* adjusted 'Progressive Locations' counts and added new table

* added more souls + upgrade gems

* added the rest of consumables

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

* DS3: Add infusion categories and some cleanup of items

* DS3: Fix item ordering

* DS3: Fix infusion/upgrade code extra if

* DS3: Disable some unmarked cut content items

* DS3: Rename blessed red and white shield+1

* DS3: Implement guaranteed_items option

* DS3: Remove print statement

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

* add unused content item id's

* DS3: Move cut content to its own list

* DS3: Classify spells and healing upgrades as useful

* DS3: Implement get_filler_item_name

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

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

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

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

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

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

* DS3: Minor linting changes

* DS3: Update required_client_version

* DS3: Remove rule for bell tower access

The key can always be purchased from the shop

* DS3: Move location category option checks to generate_early

* added "Boss Soul" option to pool

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

* DS3: Address minor review comments

* DS3: Change category enums to IntEnum

* DS3: Make apworld

---------

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

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

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

* use it in The Messenger

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

* set seed in lttp tests

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

* some cleanup and duplicate code removal

* add unit test for non dict iterable

* use more consistent naming

* sometimes i just make stuff harder on myself :)
2023-06-30 20:37:44 -05:00
Trevor L
fa3c132304 Hylics 2: Add missing location (#1917)
* Hylics 2: Add missing location

* Hylics 2: Change data_version
2023-06-30 17:46:32 -05:00
digiholic
6226713c4d MMBN3: Press program now has proper color index when received remotely (#1918) 2023-06-30 17:34:53 -05:00
Justus Lind
b56da79890 Muse Dash: Add 2023 Anniversary songs and remove a hidden song (#1916)
* Remove CHAOS Glitch. Add test to check for removed songs.

* Add to game list

* Fix oversight with 0 difficulty songs. Fix naming of test.

* Add new songs and update other data.

* Fix accidental copy paste
2023-06-30 08:10:58 -05:00
PoryGone
1d6345d3a2 SA2: Add troubleshooting note about Skip Intro and Cutscene Traps (#1915) 2023-06-29 22:28:08 -05:00
el-u
51a639ceaf lufia2ac: use an appropriate dungeon sprite and battle theme for each boss (#1914) 2023-06-29 22:21:46 -05:00
digiholic
7ecb1e6d6c Docs: Adds MMBN3 to Readme.md (#1912) 2023-06-29 15:01:37 -05:00
Zach Parks
c9fb443c64 OriBF: Move Ori and the Blind Forest to worlds_disabled. (#1906)
* OriBF: Move Ori and the Blind Forest to `worlds_disabled/`

* Add readme for `worlds_disabled` folder

* fix link

* fix link 2

* Remove useless comment

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

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-06-29 13:36:48 -05:00
digiholic
325299286b Mega Man Battle Network 3: Implement New Game (#1198)
* Initializes MMBN3 world with empty files

* Adds item names to item dict

* Adds locations and names

* Adds skeleton of MMBN3Client. Mostly copy pasta from OOT

* Fixed some style and formatting

* More incremental Lua tests

* Adds all locations and checking to Lua connector

* Made class definitions for TextPet Parser

* Begun connecting item delivery system through lua and textpet

* Lua Connection can now send test items

* Item Delivery is now parameterized. Test command can send any chip

* Adds the ability to send non-chip items

* Fixes name errors in python client

* Fixes count for zenny, attempts to fix bugfrags

* Fixes an issue where you always received 255 bugfrags

* Converts zenny and bugfrag amounts to little endian bytecode

* Checks game state before sending chips

Adds debug option to display information overlayed on rom
Fixes chip indexing issue for chips with ids over 255
Minor text fixes

* Adds in some animation reset instructions during item get message

* Stores previously collected item index in save, re-sends missing items

* Adds title screen check before sending locations

Loading items from save could not be done via RAM. Had to be added in
assembly

* Adds progressive undernet check

* Added library for lzss decoding bits of rom

* More progress on parsing text events from ROM

* Adds a way to inject messages into ScriptArchive data structure and generate bytecode

* Adds Item definitions, passes to client

* Adds regions and item collection rules

* Touched up a few names and values that have changed in preparation for the final patching

* Modifying messages via item is now successful

* Added generate_output hook to generate ROM data

* Generates ROM successfully

* Fixes navi cust give index

* Whoops forgot to wrap this in brackets

* Injects extra scripts for undernet rankings

* Programs had ammount and color swapped

* Prompts the user for their username when connecting

* Adds flagClear to the list of commands to avoid overwriting

* Fixes message box crashes and several other multiworld issues

* Fixes IDs and names of several items and locations

* Added .gba to gitignore

* Fixes compatibility after recent rebase

* Fixes some locations and items that are otherwise unobtainable

* Attempts to make a working launcher in the installer

* Creates installer and fixes several inaccessible locations

* Many minor changes to items, locations, and requirements made during testing

* Adds an info page for MMBN3

* Fixes failing tests by removing duplicate IDs and properly marking progression items

* Accidentally forgot to un-remove the thing

* Whoops, changed this by accident

* Updates self.world references to self.multiworld

* Fixes imports to use from imports instead of using the namespace

* Removed some leftover merge artifacts from inno setup

* Puts back that darned signtool line again

* Adds Overworld Metro keys as items

* Adds TamaCode and puts shortcuts behind cyber passes

* Fixes Numberman code 16 check

* Fixes metro access logic and adds text to metro

* Reworks Lua to fix crashing when many items are queued

* Items for other BN3 games for different players are no longer given in the main player's ROM as well

* Fixes incorrect Item ID for ACDC Metro

* Fixes multi-box text messages

* Adds timer before sending an item

* Forgot to remove the second box of SubMems

* Updates patch and lua to prevent softlocks and crashes

* Adds options for extra undernet ranks, exclude jobs

* Extra GigFreez now gives 20 bugfrags

* Additional Progressive Undernets can no longer appear on the WWW Base

* Moves item signal byte to empty area of flags instead of end of RAM

* Adds Chocolate Shop locations and navi chips to fill them

* Fixes save crash, and added chocolates to lua

* Fixes chocolate stand selling out text, removes DrillMan cube in Undernet

* Replaces old messaging system with direct memory manipulation for receiving items

* Removes NDSPY requirements from MMBN3 by manually adapting the GBA's lz10 algorithm

* Fixes the names of Hospital-1 Locations

* Adds Canary Bit to avoid sending checks when title screen check fails

* Gaining a cybermetro pass will now open the shortcut immediately

* Randomizes the two accessible areas of Undernet 7, adds Hammer as item

* Adds new locations to connector lua

* Injects the name of the item into trade quests

* Fixes copy-paste error in docs

* Fixes merge artifacts and depracated code

* Nut-wafer stand now faces Lan the right way after buying

* Removes unused Goal Option and updates the readme to include most recent changes

* Touch-ups and formatting changes

* The Great Fillerization update. Dozens of items changed to Filler

* Replaces instances of Mega Man with MegaMan

* Update worlds/mmbn3/docs/en_MegaMan Battle Network 3.md

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

* Update worlds/mmbn3/__init__.py

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

* Apply suggestions from code review

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

* Changes code ordering to suit base class's

* assert_generate now checks for roms. Minor text fixes

* Makes player specific frequency and excluded location options

* Apply suggestions from code review

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

* Addresses suggested changes from PR review

* Replaces ndspy lz10 with MIT-compliant nlzss lz10

* apworld compatibility fix for mmbn3_options from utils

* Addressing more comments by el-u

* APworld will now pull patch from zip folder

* Apply suggestions from code review

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

* Cleaned up comments for progressive undernet ROM function, moved index list to field to avoid re-initializing

* Removes improper player-indexed location/item dicts, replaces with world member variables

* Avoids redefining list in progressive undernet ROM function

* Filler items can no longer be generated beyond their specified amounts

* Fixes list copying issue with item frequencies

* Adds BN3 Client Generation back into Launcher settings

* Fixes typos causing huge problems

* Fixed non-relative import for apworld

* Removes custom enum implementation that broke pickle

* Displays message when attempting to load an incorrect ROM, will not attempt to patch it

* Filler items can now only be placed once

* Changes path in setup doc to match Lua path changes

* Fixes file extension for MMBN3 file

* Replaces magic number with reference to value in NetUtils

* Moves victory rules to set_rules. Removes commented out code

* Rewrites Lua script to send block of memory

* Fixes off-by-one error in sending bytes for locations

* Fixes issue with invalid characters in text parsing, and WWW monitor text box parsing

* Moves trade text injection to init so it has access to options

* Attempts to split the text boxes for hinted items

* Trade checks now provide hints if the option is set for them

* Fixes escape character issue for BizHawk 2.9.1

Something in Bizhawk lua parsing changed to dislike the escaped tilde.
I'm not even entirely sure why it was escaped in the first place, but
this should fix the compatibility of it.

* Re-adds desk check that it turns out actually does exist

* Updates requirements to mention bizhawk 2.7 instead of 2.3.1

* Fixes off-by-one error in command byte counts

* Fixes program color indices

* Fixes newline PEP violations

* Reverts an accidental whitespace change made to launcher.py

* Fixes URL formatting on link to settings from setup guide

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

* Splits several lines in the readme to avoid excessive length

* Fixes formatting and (hopefully) reduces cringe of joke in setup doc

* Removes unnecessary constructor

* Changes item frequency generation to avoid reusing the same references

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

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
Co-authored-by: Zach Parks <zach@alliware.com>
2023-06-29 13:36:01 -05:00
Alchav
776b5fab7c LTTP/SM/SMZ3: Show correct item icon for cross-game items (#1112)
Co-authored-by: lordlou <87331798+lordlou@users.noreply.github.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-06-29 17:47:21 +02:00
Fabian Dill
18e0d25051 Factorio: fix resync not reconciling divergent history 2023-06-29 15:15:12 +02:00
el-u
dfb3df4a8f lufia2ac: coop support + update AP version number to 0.4.2 (#1868)
* Core: typing for async_start

* CommonClient: add a framework for clients to subscribe to data storage key notifications

* Core: update version to 0.4.2

* lufia2ac: coop support
2023-06-29 08:06:58 -05:00
lordlou
d0db728850 SM: 0.4.1 Fixes and Additional Objective Options (#1859)
* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

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

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

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

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

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

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

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

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

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

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

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

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

* first working single-world randomized SM rom patches

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

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

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

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

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

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

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

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

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

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

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

* Fixed multiworld support patch not working with VariaRandomizer's

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

* + added missing files from variaRandomizer project

* + added missing variaRandomizer files (custom sprites)

+ started integrating VariaRandomizer options (WIP)

* Some fixes for player and server name display

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

* Fixed Goal completion not triggering in smClient

* integrated VariaRandomizer's options into AP (WIP)

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

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

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

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

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

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

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

* maxDifficulty support and itemsounds removal

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

* Fixed bad merge

* Post merge adaptation

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

* fixed generation with other game type than SM

* added default randoPreset json for SM in playerSettings.yaml

* fixed broken SM client following merge

* beautified json skillset presets

* Fixed ArchipelagoSmClient not building

* Fixed conflict between mutliworld patch and beam_doors_plms patch

- doorsColorsRando now working

* SM generation now outputs APBP

- Fixed paths for patches and presets when frozen

* added missing file and fixed multithreading issue

* temporarily set data_version = 0

* more work

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

* commited missing asm files

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

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

fixed crash in SMClient when loosing connection to SNI

* fixed No Energy Item missing its ID

fixed Plando

* merge post fixes

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

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

* fixed start item x-ray HUD display

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

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

* fixed settings that could be applied to any SM players

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

* - fixed End Credits broken text

* added non SM item name display

* added all supported SM options in playerSettings.yaml

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

did some cleaning (mainly reverts on unnecessary core classes

* minor setting fixes and tweaks

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

* added option start_inventory_removes_from_pool

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

* Hopefully fixed ROR2 that could not send any items

* - fixed missing required change to ROR2

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

* fixed typo with doors_colors_rando

* fixed checksum

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

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

* - added missing change following upstream merge

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

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

* fixed failing unit tests

* - fixed broken custom_preset options

* - big cleanup to remove unnecessary or unsupported features

* - more cleanup

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

- small cleanup

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

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

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

* - updated basepatch to reflect g4_skip removal

- moved more asm files to SMBasepatch project

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

* fixed wrong path if using built as exe

* - cleaned exposed maxDifficulty options

- removed always enabled Knows

* Merged LttPClient and SMClient into SNIClient

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

* small doc precision

* - added death_link support

- fixed broken Goal Completion
- post merge fix

* - removed now useless presets

* - fixed bad internal mapping with maxDiff

- increases maxDiff if only Bosses is preventing beating the game

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

- fixed controller settings not applying to ROM

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

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

* -added docstring for generated yaml

* fixed bad merge

* fixed broken infinity max difficulty

* commented debug prints

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

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

* fixed missing cleanup

* added support for 65535 different player names in ROM

* fixed generations failing when only bosses are unreachable

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

* fixed failling generations when using 'fun' settings

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

* fixed debug logger

* removed unsupported "suits_restriction" option

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

* - fixed deathlink emptying reserves

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

* - merged death_link and death_link_survive options

* fixed death_link

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

* added Nothing and NoEnergy as hint blacklist

added missing NoEnergy as local items and removed it from progression

* fixed broken Item links

* fixed failing generation that could happen with Disabled Tourian

fixed shared Location list that could be modified for each world

* added missing force disable of EscapeRando if an escape solution cant be found

* fixed broken animal surprise patches

* prevent receiving items when in the first room of Ceres (message box in mode7 is broken)

* fixed generating with "activate chozo robots" Objective

* added soft reset that saves to initial starting location

reverted code change applied to fix softlocks from comeback checks
reverted forcing all beam local when using door rando

* replaced "save and reset" with "save and fast reload" (using same Start+Select+L+R)

* added documentation about Save and Reload

removed forgotten docstring about forcing beams as local items when using door rando

* fixed frequent failing generation on WebHost (KeyError: 'Kraid')

* added "objectiveRandom", "nbObjective", objectiveList and adapted Objective selection options to better reflect VARIA's.

fixed "collect 100% items" not being excluded when objectiveRandom is used
added Exception when VARIA initial layout fails

* fixed broken non-AP items

fixed determinism caused by the use of a set

* fixed generation failing on Webhost with string as a OptionSet (replaced default with a list of string)

cleaned doc and naming of Objective related Options
2023-06-29 07:51:09 -05:00
Justus Lind
77b0852dca Muse Dash: Add New Game (#1723)
* Alpha 1 Muse dash stuff.

* Add in an option to limit to only base game songs.

* Make all items progression instead of progression_skip_balancing.

* Add in extra_goal_song_items to help make runs less about completing every song.

* Change ID range to be in a more open area, and add some comments.

* Add in Streamer Mode and difficulty range options. Rearrange data files so its easier to get all data at once.

* Fix generation issues.

* Fix up the maximum and remove old option.

* Remove empty items and the option to make filler songs empty.

* Support emerald hunt mode. Make difficulties an option rather than 2 sliders.

* Fix DLC Song option being inverted.

* Fix item counting being broken if there was more than 1 world.

* Make compatible with .apworld specification.

* Make All item names ASCII compatible.

* Add in the additional_item_percentage option.

* Add a test to ensure the item names are within the normal ascii range.

* Add in death link.

* Remove the album from the item name. Not really needed anymore.

* Add the 2 budget is burning albums under the free songs heading. Adds a couple more songs without dlc.

* Sanitise Album names.

* Added the grade needed choice.

* Update songs to v3.1.0

* Adjust difficulty ranges. Add Expert and Master.

* Fix setup_en.md being out of date.

* Add a manual override.

* Add testing for diff ranges. Fix bugs introduced there. Limit option to 11 to not generate an impossible seed.

* Remove regions from Muse Dash.

* Some Oops...

* Attempt to make tests happy.

* Remove supports weighting false to stop webhost test failing.

* Adjusted settings

* Adjust music sheets to use percentages. Various cleanups.

* Fixes to new code.

* Add Ola Dash Album. Add support for overriding song difficulty. Other stylisation changes.

* Attempt fix tests.

* Ooops missed one.

* flake8 suggestions.

* Remove FM 17314 SUGAR RADIO as that song is a bit weird.

* Update document pages.

* Add trap support

* Lower additional song count by 10.

* Tests broke on my end. Using github to test this.

* Looks like I was accidentally adding ~.

* Fix the one song that crashes OoT hint generation

* Various documentation changes.

* Website documents fixup.

* Doc updates part 2.

* Oops. Doc updates part 3.

* Add Muse Dash to the apworld list.

* Add trailing comma.

* Add a couple plando options.

* Set data_version to 1.

* Add in some handling incase someone decides a song is both starter and included.

* Remove brackets around ifs.

* Oops. Accidentally removed a necessary bracket.

* Fix filtering crash due to me mixing up c# and python .remove().

* Add Happy Otaku Pack Vol.17. Also increment data version.

* Update links to melon loader to be the latest.

* Clean up song selection code by shuffling once then popping.

* Add UID to the Data text file, so the same file can be used client and server.

* Increment Data Version because some names have changed.

* Correct some names.

* Update data to v3.4.0 (Addition of Muse Radio FM104)

* Add support for SFX traps. Adjusted how traps were setup a bit.

* Update the docs to include a troubleshooting section.

* Small fixes.

* Remove unnecessary brackets.

* Add .net downloads to docs.

* Avoid failing generation if strict difficulty settings are applied with no dlc songs and streamer mode.

* Forgot to add the worst starting song count.

* Make minimum song count be Starting Songs + 11 instead of Starting Songs * 2 + 1.

* Fix up several issues where song count could mismatch the requested amount.

* Add a test to ensure world size doesn't grow.

* Fix some oversights.

* Remove unnecessary brackets.

* Fix up passing the tuple out when just the key would suffice.

* Adjust typing based on Phar's suggestions.

* Apply the rest of Phar's suggestions with minor tweaks to other parts to suit suggestions.

* Adjust some more stuff to fit 120 characters.

* Some more pep8 stuff and fix tests.

* Some pep8 in tests.
2023-06-29 07:36:39 -05:00
Aaron Wagener
3fba94f000 The Messenger: strip generated filler items for a sufficiently small pool (#1907)
* The Messenger: strip generated filler items for a sufficiently small remaining item pool

* rewrite the test for the small chance there's no large currency shards
2023-06-29 07:33:37 -05:00
William Quelho Ferreira
85582b9458 TLoZ: fix LauncherComponents entry (#1908) 2023-06-28 19:06:45 -05:00
Aaron Wagener
122d404145 Docs: rework main ap setup guide (#1853)
* rework main ap setup guide

* review updates

* add blurb about re-opening rooms and user-content

* more review suggestions

* remove dead links. Windows blurb
2023-06-28 19:06:18 -05:00
Aaron Wagener
07e3fbe845 Docs, LTTP: clarify not using qusb and remove redundancies (#1373)
* clarify not using qusb and remove redundancies

* SNES mini note

* review suggestions

* remove remaining repetitive text

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-28 00:11:06 -05:00
Kory Dondzila
76cace725b WebHost: Fixes multi-tracker checks sorting. (#1893) 2023-06-27 20:40:29 -05:00
NewSoupVi
99656bf059 The Witness: Utils.cache_argsless -> functools.lru_cache (#1897)
* Changed Utils.cache_argsless to functools.lru_cache

* Revmoed unused variable

* Removed remaining direct reference to a .txt outside utils

* Update worlds/witness/utils.py

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

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-06-28 02:23:50 +02:00
Aaron Wagener
332eab9569 The Messenger: Add Shop Rando (#1834)
* add shop shuffle options and items

* add logic for the shop slots

* write cost tests

* start on shop item logic

* make strike and second wind early items

* some cleanup

* remove 5 shards

* double cost requirement for really expensive items and raise the rates

* add test for shop shuffle with minimum other locations

* put power seal in front of shards

* rename locations and items

* update rules, regions, and shop

* update tests and misc fixes

* minor cleanup

* implement money wrench and figurines

* clean out now unneeded info from slot_data

* docs update and fix a failure when not shuffling shops

* remove shop shuffle option

* Finish out shop rules

* make seals generation easier to read and fix tests

* rule adjustments

* oop

* adjust the prices to be a bit more generous

* add max price to slot data for tracker

* update the hard rules a bit

* remove unnecessary test

* update data_version

* bump version and remove info for fixed issues

* remove now unneeded assert

* review updates

* minor bug fix

* add a test for minimum locations shop costing

* minor optimizations and cleanup

* remove whitespace
2023-06-28 01:39:52 +02:00
zig-for
8c2584f872 LADX: 16 bits for the check ID (#1903) 2023-06-27 16:39:57 -05:00
PoryGone
1ced726d31 SA2B: v2.2 Content Update (#1904)
* Ice Trap Support

* Support Animalsanity

* Add option for controlling number of emblems in pool

* Support Slow Trap

* Support Cutscene Traps

* Support Voice Shuffle

* Handle Boss Rush goals

* Fix create item reference to self.multiworld

* Support Ringlink

* Reduce beep frequency to 20

* Add Boss Rush Chaos Emerald Hunt Goal

* Fix Eternal Engine - Pipe 1 logic

* Add Chao voice shuffle

* Remove unused option

* Adjust wording of Required Cannon's Core Missions

* Fix incorrect region assignment

* Fix incorrect animal logics

* Fix Chao Race tooltip

* Remove Green Hill Animal Location

* Add Location Count info to tooltips

* Don't allow M4 first if animalsanity is active

* Add Iron Boots to Standard Logic Egg Quarters 5

* Make Vanilla Boss Rush actually Vanilla

* Increment Mod Version

* Increment Data Package Version

---------

Co-authored-by: RaspberrySpaceJam <tyler.summers@gmail.com>
2023-06-27 16:38:58 -05:00
Freya Arbjerg
d51e0ec0ab WebHost: Align multitracker status column to the left (#1645)
* Align multitracker status column to the left

* Move 'Status' column to after 'Game' column
2023-06-27 17:37:01 -04:00
Felix R
36b5b1207c Add Bumper Stickers (#811)
* bumpstik: initial commit

* bumpstik: fix game name in location obj

* bumpstik: specified offset

* bumpstik: forgot to call create_regions

* bumpstik: fix entrance generation

* bumpstik: fix completion definition

* bumpstik: treasure bumper, LttP text

* bumpstik: add more score-based locations

* bumpstik: adjust regions

* bumpstik: fill with Treasure Bumpers

* bumpstik: force Treasure Bumper on last location

* bumpstik: don't require Hazard Bumpers for level 4

* bumpstik: treasure bumper locations

* bumpstik: formatting

* bumpstik: refactor to 0.3.5

* bumpstik: Treasure bumpers are now progression

* bumpstik: complete reimplementation of locations

* bumpstik: implement Nothing as item

* bumpstik: level 3 and 4 locations

* bumpstik: correct a goal value

* bumpstik: region defs need one extra treasure

* bumpstik: add more starting paint cans

* bumpstik: toned down final score goal

* bumpstik: changing items, Hazards no longer traps

* bumpstik: remove item groups

* bumpstik: update self.world to self.multiworld

* bumpstik: clean up item types and classes

* bumpstik: add options
also add traps to item pool

* bumpstik: update docs

* bumpstik: oops

* bumpstik: add to master game list on readme

* bumpstik: renaming Task Skip to Task Advance
because "Task Skip" is surprisingly hard to say

* bumpstik: fill with score on item gen
instead of nothing (nothing is still the default filler)

* bumpstik: add 18 checks

* bumpstik: bump ap ver

* bumpstik: add item groups

* bumpstik: make helper items and traps configurable

* bumpstik: make Hazard Bumper progression

* bumpstik: tone final score goal down to 50K

* bumpstik: 0.4.0 region update

* bumpstik: clean up docs
also final goal is now 50K or your score + 5000, whichever is higher

* bumpstik: take datapackage out of testing mode

* bumpstik: Apply suggestions from code review

code changes for .apworld support

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

---------

Co-authored-by: Zach Parks <zach@alliware.com>
2023-06-27 15:37:17 -05:00
Fabian Dill
a4e485e297 Launcher: keep alive (#1894) 2023-06-27 09:30:54 +02:00
kindasneaki
a7bc8846cd RoR2: bug fixes (#1891)
* adding back parens that got deleted by accident.

* Void Locus and The Planetarium ids backwards

* change required client version

* beads of fealty was missing for A Moment, whole victory

* found another logic bug

* Update worlds/ror2/__init__.py

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

* Remove unnecessary comment

---------

Co-authored-by: Zach Parks <zach@alliware.com>
2023-06-26 23:47:52 -05:00
Fabian Dill
125ee8b198 WebHost: fix dict lookup exceptions 2023-06-27 04:39:21 +02:00
Mewlif
553fe0be19 Undertale for AP (#439)
Randomizes the items, and adds a new item to the pool, "Plot" which lets you go further and further in the game the more you have.

Developers: WirTheAvali (Preferred name for professional use, mewlif)
2023-06-27 04:35:41 +02:00
Zach Parks
71bfb6babd Generate: Add skip progression balancing argument. (#1876) 2023-06-26 16:14:01 -05:00
James Groom
1698c17caa Docs: Revise all docs mentioning Lua in EmuHawk (which are in English), and other misc. corrections (#1782)
* Fix links to TASVideos.org using HTTP

* Revise all docs mentioning Lua in EmuHawk which are in English

resolves TASEmulators/BizHawk#3650

* Correct capitalisation of "BizHawk"

in strings and camelCase identifiers

* Use the term "EmuHawk" when referring to the app, in English docs

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-26 08:53:44 +02:00
black-sliver
751e5cec63 Ori: fix py3.8 apworld compatibility 2023-06-26 08:16:56 +02:00
NewSoupVi
dc46e96e3f Witness: APworld compatibility, but for real this time (#1896)
* removed relative imports from outside the witness package

* Remove Witness from the apworld shame list
2023-06-26 00:38:39 +02:00
NewSoupVi
0934e5c711 The Witness: Fixed seeds not generating with vanilla logic (#1895)
Yikes, I swear I ran like 15 generations with a random yaml, I got so unlucky
2023-06-26 00:20:28 +02:00
Fabian Dill
aa8ffa247d Setup: flip apworld list (#1882)
* Setup: flip apworld list

* Update setup.py

Co-authored-by: kindasneaki <ryandj67@hotmail.com>

* Update setup.py

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

* setup: make TLoZ an apworld

This reverts commit fd026c5eb2.

---------

Co-authored-by: kindasneaki <ryandj67@hotmail.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-25 03:47:38 +02:00
black-sliver
a45e8730cb Fill: fix fill_restrictive for mixed minimal and non-minimal and test (#1800)
* Tests: add test for mixing minimal and non-minimal

* Tests: minor cleanup in test_minimal_mixed_fill

* fix fill_restrictive for mixed minimal/non-minimal

The reason why this only happens for minimal is because it would not accept the solution it found otherwise.
Tracking and releasing unreachable items would be the better solution, but that's a lot harder to do.

* fix typo in fill_restrictive

* fix pep8 in fill_restrictive

* Fill: cleanup invalid unsafe placements, better comments

* Fill: more cleanup
2023-06-25 02:55:13 +02:00
Fabian Dill
46f2f3d7cd Factorio: Client in folder, TextClient: always available (#1829)
* Factorio: move Client into world folder

* Factorio: declare Client as Client Component

* FactorioClient: use centralized launch_subprocess

* TextClient: make always available
2023-06-25 02:31:25 +02:00
black-sliver
a96ff8de16 Linux: add freeze_support, Launcher: use spawn (#1890) 2023-06-25 02:24:43 +02:00
agilbert1412
f3e2e429b8 DLC Quest: Option Documentation improvements (#1887) 2023-06-25 02:13:33 +02:00
NewSoupVi
46b13e0b53 Witness: apworld support (#1885) 2023-06-25 02:00:56 +02:00
t3hf1gm3nt
7a4e903906 TLOZ: APworld support (#1884)
- Remove a relative import in Rules.py
- Clean up a few unused imports in __init__.py
- Use pkgutil instead of open when applying base patch
- make sure rom_name is initialized correctly in modify_multidata

* use os.path.join() instead of explicit "/"
2023-06-25 01:58:54 +02:00
Aaron Wagener
f1ccf1b663 reenable ping 2023-06-25 01:24:39 +02:00
NewSoupVi
ec0822c5eb Docs: Mention Git in the "Optional" section of "Running from Source" (#1880)
* Docs: Mention Git in the "Optional" section of "Running from Source"

GIt is required to install the Zilliandomizer package.

Also, this is probably just nice to have.

* Remove mention of Zillion so the text doesn't need updating.

* Update docs/running from source.md

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

* Update docs/running from source.md

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

* Mention PyCharm's git integration

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-24 12:59:14 +02:00
Fabian Dill
78b981228a Generate: improve error message for missing game (#1857)
---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-23 10:17:35 +02:00
NewSoupVi
f3c788d0cc Witness: Fix missing location
All Pressure Plates puzzles are now always locations.

This makes a line where a Pressure Plates location gets added and a different one cause incorrect behavior.
2023-06-23 10:16:39 +02:00
Zach Parks
59ad9e97e5 WebHost: Fix special-range value setting to custom when randomization is toggled off (#1856)
* WebHost: Fix custom-range value setting to `custom` when randomization is toggled off

* Remove redundant code

* Add optional parameter default
2023-06-22 22:12:22 -04:00
StripesOO7
abd8eaf36e WebHost: Change default spoiler-option for games generated from WebHost to 3 instead of 0 (#1852)
* Change default spoiler-option in WebHostLib/generate.py to 3 instead of 0

* shifting spoiler-default to the JS calls instead of setting it in generate.py

---------

Co-authored-by: StripesOO7 <54711792+StripeesOO7@users.noreply.github.com>
2023-06-22 21:01:09 -05:00
black-sliver
f36468fc25 Docs: add info about maintaining worlds (#1838)
* Docs: add info about mainting worlds

* Docs: fix typos in world maintainer

* Docs: commit suggestions into world maintainers

Thanks Joethepic and Silvris

* Docs: fix more typos in world maintainer

* Docs: more typos

* Docs: world maintainers link to core maintainers

* Docs: world maintainers voting on discord

* Docs: add 'world maintainer' link to 'adding games'

* Docs: unmaintained worlds in 'disabled'

* Docs: world maintainer update from review

Thanks LegendaryLinux

* Doc: rephrase world maintainer voting
2023-06-22 08:51:02 +02:00
black-sliver
a939f50480 Clients: use certifi (#1879)
* Clients: use certifi for wss

On Windows, the local cert store might be outdated and refuse connection to some servers.

* Clients: lazily create ssl_context
2023-06-22 00:01:41 +02:00
Fabian Dill
b04b105bd8 LADX: use custom collect/remove to keep track of logical rupee counts instead of LogixMixin
May contain some pep8, sorry
2023-06-21 12:42:11 +02:00
NewSoupVi
845502ad39 The Witness: Hint distribution changes, added locations, misc fixes (#1785)
Changes:

* Hints should feel a lot less same-y now ("Priority hints" are no longer always hints in disguise)
* Keep Hedge Mazes 1-3 and Pressure Plates 1-3 are added as locations in all settings
* Desert Final Room Hexagonal & Desert Final Room Bent 3 are added as locations
* Entries in exclude_locations that are referring to panels are now sent through slot data. This means they can be pre-skipped on the client side.

Fixes:

* Logic error in the Stoneworks that led to more restrictive seeds than necessary
* Logic error for Theater Flowers EP that led to more restrictive seeds than necessary
* Fixed crash in plando when "item" is a dict with weights
* Spoiler log locations were in random order per region, now they are consistent
2023-06-21 00:45:26 +02:00
Sunny Bat
afe9e12ef4 Raft: Fix item prefilling (#1878) 2023-06-20 09:14:46 +02:00
Fabian Dill
a75159b57e WebHost: import Markup from markupsafe (#1848) 2023-06-20 01:01:42 +02:00
Fabian Dill
61fc80505e Core: refactor some loading mechanisms (#1753)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-20 01:01:18 +02:00
Fabian Dill
25f285b242 Launcher: deprecate FUNC Component type (#1872)
* Launcher: add hidden component type

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-19 09:57:17 +02:00
Fabian Dill
c4e28a8736 Setup: pin cx-Freeze to latest working version 2023-06-19 00:40:14 +02:00
Fabian Dill
422ccdaa4c WebHost: remove some unused imports 2023-06-18 22:56:55 +02:00
Bicoloursnake
1e7c650159 Docs: Updating the macOS guide for 'New Terminal at Folder' (#1865)
* Update mac_en.md

Added an alternate option to simply terminal navigation

* Update worlds/generic/docs/mac_en.md

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-18 13:21:12 +02:00
Aaron Wagener
ab64173600 SoE/SNIClient: auto launch SNI before browser when SNIClient patched (#1861)
* auto launch SNI before browser

* launch emulator too :)

* don't infinitely await sni connection
2023-06-18 11:27:08 +02:00
Fabian Dill
36499b8983 Setup: delete outdated Enemizer and SNI files 2023-06-17 00:56:13 +02:00
NewSoupVi
923ff033b1 The Witness: Logic Fix: Vanilla First Wooden Beam (#1867) 2023-06-15 00:30:50 +02:00
Sunny Bat
599d0ac81b Raft: Small website/code touchups (#1866)
* Remove unnecessary Set

* Ocean theme

* Use create_items instead of generate_basic
2023-06-15 00:30:14 +02:00
Ziktofel
ce2433b247 SC2: Python 3.11 compatibility (#1821)
Co-authored-by: Salzkorn
2023-06-12 07:41:53 +02:00
Scipio Wright
f6cb90daf9 Noita: Region connection edits (#1855)
Shifts the Lake region to be connected to The Laboratory, so that the Lake boss is late game instead of early game.
Shifts the Below Lava Lake region to be connected to the Snowy Depths, so instead of being early game it's early-mid game (since that's when you would be expected to be able to have decent enough digging or a Sädekivi.
2023-06-05 19:32:33 +02:00
el-u
54b200451d Docs: Fix typo in world api.md (#1854) 2023-06-01 22:56:44 -05:00
Exempt-Medic
b98080afee Docs: Update YAML planning guide (#1845)
* [Docs] Update YAML planning guide

Changed wording for items accessibility to describe how it actually works. Reordered settings such as local_items and start_location_hints to match their order in templates. Fixed some grammatical errors.

* Fix typo

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

* Update doc

Moved `accessibility`, `progression_balancing`, and `triggers` to game sections instead of root sections and reworded description accordingly. Updated version number. Fixed `progression_balancing` values in example YAMLs.

* Indented trigger to be part of ALTTP

---------

Co-authored-by: Zach Parks <zach@alliware.com>
2023-06-01 22:33:12 -05:00
Exempt-Medic
5401e485aa Blasphemous: Logic fixes for WotBC Cherub and Jondo upper west tree root (#1835) 2023-06-01 03:52:46 +02:00
Fabian Dill
58cf9783eb Tests: make names more unique 2023-06-01 01:45:24 +02:00
Fabian Dill
fad0fe16f4 Tests: sort custom loaded tests (#1851) 2023-06-01 01:44:54 +02:00
kindasneaki
c2884e9eb0 RoR2: Victory Conditions Doc Update (#1833) 2023-05-31 18:38:03 -05:00
Doug Hoskisson
1809823308 Zillion: cache key includes gun requirement (#1846)
The key for the logic cache was missing some important information, so it was yielding a cache hit when it should have been a miss.
2023-05-31 05:56:23 +02:00
lordlou
df7462efcc SMZ3 decoding fix (#1847) 2023-05-30 03:05:05 +02:00
FlySniper
00e3c44400 Wargroove: Fixed commander.json file never being closed by the mod (#1841)
The Wargroove mod didn't close the commander.json's file handle. The Wargroove mod will now close that file handle. The change for the mod can be viewed here: FlySniper/WargrooveArchipelagoMod@fc9aeb3
The change can be verified as present in this repository by viewing the binary data in the modAssets.dat file and searching for "commander.json"
2023-05-29 20:33:35 +02:00
agilbert1412
abf4b3bcbc Stardew valley: Fix package and imports for apworld linux (#1842)
- Fix csv load to use explicitly imported self package instead of keyword __package__
- Fix init.py having a relative import to outside of the apworld
2023-05-29 01:00:33 +02:00
Fabian Dill
c9f217943e LttP: fix patching crash if old always_apply adjuster settings were applied 2023-05-25 14:08:56 +02:00
Fabian Dill
e9f8b1ed28 WebHost: use Py3.11 compatible ponyorm 2023-05-25 14:07:21 +02:00
el-u
c46d8afcfa Core: clean up BaseClasses a bit (#1731) 2023-05-25 01:24:12 +02:00
ScootyPuffJr1
f4d9c294a3 [SM] Minor update to link in Options.py (#1831) 2023-05-23 16:30:39 +02:00
Exempt-Medic
42d8fb8409 [Blasphemous] Various logic fixes (#1830)
This makes a few changes to logic to better match the 1.3 rando's logic. This fixes instances where the wrong items were expected, fixes a typo of "Lorqiana", moves the expert logic on "PotSS: Second area ledge" to only apply if on expert, and adds a new route to "DC: Mea Culpa altar" via Linen of Golden Thread + Three Gnarled Tongues
2023-05-22 19:03:21 +02:00
axe-y
127d4812b5 DLCQuest: Fix Documentation Broken Link 2023-05-21 15:48:56 +02:00
Fabian Dill
527f30d91a Core: log race mode enabled 2023-05-21 05:02:14 +02:00
Fabian Dill
1d565b9aaf WebHost: add game to template export 2023-05-21 05:01:56 +02:00
Fabian Dill
6814bc158a WebHost: index columns used by landing page. 2023-05-21 05:01:29 +02:00
alwaysintreble
e80f3206b6 The Messenger: override start_inventory description (#1695)
* The Messenger: override start_inventory description

* use StartInventoryPool directly
2023-05-21 02:54:50 +02:00
el-u
54ea917c48 CI: treat all files as modified on new branches (#1826) 2023-05-20 21:57:38 +02:00
Exempt-Medic
5e9bf4b007 Docs: Update world api excluded/priority locations description (#1807)
* Update world api doc

Changed the description of excluded and priority locations to match how they appear in other places such as the options api doc

* Update world api.md
2023-05-20 20:04:26 +02:00
Fabian Dill
c8453035da LttP: extract Dungeon and Boss from core (#1787) 2023-05-20 19:57:48 +02:00
Fabian Dill
a2ddd5c9e8 LttP: deterministic shop_shuffle 2023-05-20 19:43:44 +02:00
Fabian Dill
97ba631b80 Core: update modules 2023-05-20 19:36:55 +02:00
black-sliver
be4c597c8d Logging: make sure level is applied for websockets 2023-05-20 19:27:12 +02:00
black-sliver
324d3cf042 Main: add __all__ and change wrong imports (#1824)
* Main: add __all__ and change wrong imports

* Adjusters: fix __version__ import
2023-05-20 19:21:39 +02:00
Fabian Dill
b1c5456d18 Subnautica: move mod exports to own module 2023-05-20 18:34:22 +02:00
Cybrou
f474b81f40 LADX: Add --no-magpie argument for disabling magpie bridge (#1788) 2023-05-20 15:30:33 +02:00
el-u
5255bc5cd8 CI: add a workflow to show flake8/mypy violations in modified files of a PR (#1513)
* CI: add a workflow to show flake8 violations in modified files of a PR

* modify a file to trigger the lint check

* CI: add a workflow to show mypy violations in modified files of a PR

* modify a file to trigger the type check

* Split flake8 and mypy into two parallel jobs; run a variant of the workflow on push event; modify a file to trigger the push workflow

* fail the task if there are syntax errors; remove old lint workflow
2023-05-20 14:40:51 +02:00
Exempt-Medic
18127a75f5 Blasphemous: Fixed logic errors in WotHP 2023-05-19 11:05:52 +02:00
espeon65536
899de428df ALttP: fix dungeon fill failures properly (#1812) 2023-05-18 15:31:12 +02:00
Fabian Dill
f401702e7c Core: skip ModuleUpdate in subprocess 2023-05-18 15:29:17 +02:00
JaredWeakStrike
68bfe1705d KH2: AntipointReset (#1815) 2023-05-18 15:28:35 +02:00
Fabian Dill
98b0bf7456 LttP: use local_early_items for Small Key HC in Standard Keyshuffle (#1799) 2023-05-15 09:34:56 +02:00
NewSoupVi
7674e62ba7 The Witness: Logic fix in response to broken seed (Expert Swamp) 2023-05-15 08:54:12 +02:00
zig-for
0b33c25b39 Fix pokemon lua on bizhawk 2.9 (#1794)
---------

Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com>
2023-05-11 17:52:29 +02:00
Scipio Wright
62f4e62d71 Docs: Add location count specifics to Noita (#1805)
Added specifics about the number of checks in the pool.
2023-05-10 17:49:05 +02:00
espeon65536
48add4687c alttp: remove triforce during dungeon item fill (#1801)
This ensures that even for minimal worlds, the locations will be checked appropriately.
2023-05-10 13:06:25 +02:00
JaredWeakStrike
cc08e853a0 KH2: Ability Sync Fix (#1804) 2023-05-10 13:04:43 +02:00
lordlou
7e3fa5058d SM: door color rando option doc (#1803)
Added precision in DoorsColorsRando docstring about beams being forced local items if enabled.
2023-05-09 03:12:24 +02:00
t3hf1gm3nt
c74577d708 [TLOZ] Fix start weapon locations (#1802)
* Fix starting weapon locations usage

Makes a fresh copy of starting weapon locations when get_pool_core is ran
Should fix the issue of dangerous_weapon_locations getting appended to the list for other worlds past the first world that has dangerous StartingPosition, as well as running into the error if ExpandedPool was different between players
Credit for fix goes to @Silvris in the AP Discord
2023-05-08 22:36:35 +02:00
JaredWeakStrike
a8b76b1310 KH2: Async fix and linter cleanup (#1796) 2023-05-07 04:49:37 +02:00
Zach Parks
c8ebad1dfe WebHost: Prevent dict type options with valid_keys from exporting with [] on weighted settings. (#1762)
Thanks HK.
2023-05-06 19:07:57 -05:00
Cyb3R
d3447a3983 Launcher: Fix multiprocessing in built Launcher (#1792) 2023-05-05 00:53:57 +02:00
Fabian Dill
11b2b5ed2f Core: call stages in sorted order (#1791) 2023-05-04 14:14:20 +02:00
Fabian Dill
a0464ecea1 Core: fix start_inventory_from_pool breaking if it's removing the last instance of an item from pool.
Core: fix start_inventory_from_pool removing arbitrary items from pool if quick abort branch is entered.
2023-05-04 03:10:52 +02:00
zig-for
97fd78ba1b LADX: fix bizhawk 2.9 (#1784) 2023-05-03 23:35:14 +02:00
Fabian Dill
a60f370224 Test: fix setting seed from the hash of the seed method, rather than calling it 2023-05-03 04:31:35 +02:00
Scipio Wright
0363630f61 Noita - Docs updates (#1789)
* Docs updates

* Update worlds/noita/docs/setup_en.md

Co-authored-by: Adam Heinermann <aheinerm@gmail.com>

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Adam Heinermann <aheinerm@gmail.com>
2023-05-03 00:35:50 +02:00
black-sliver
39c7c7291e Core: store multidata/multisave enums by value on py3.11 (#1783)
This is what py3.10 did and should be better for AP than the new default
2023-05-02 08:23:39 +02:00
TheLynk
a368520200 Add New Translation for Adventure and Archipidle in french (#1749)
* Add new translation for Adventure and Archipidle in french

Add new translation for Adventure and Archipidle in french

* Add more store in setup page subnautica for more fairness

Add more store in setup page subnautica for more fairness

* tweak update merge #1685 for lua file

tweak update merge #1685 for lua file

* fix text

fix text

* fix wrong translation

fix wrong translation

* Yes it's better

Yes it's better

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

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-05-01 02:03:31 +02:00
Fabian Dill
5d25f908a4 Launcher: fix loading of mcicon (#1779)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-04-30 18:10:58 +02:00
Fabian Dill
9edab76567 WebHost: recycle generator processes 2023-04-30 16:34:03 +02:00
kindasneaki
91b60f2e21 [RoR2] Classic mode logic fix (#1775) 2023-04-29 09:07:42 +02:00
Jarno
41b59488e3 [Docs] Added lua lib (#1751)
* [Docs] Added lua lib

* Update docs/network protocol.md

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-04-29 00:10:43 +02:00
Fabian Dill
42da24cb5e WebHost: offer room owner log download link 2023-04-28 05:31:19 +02:00
zig-for
28c5e9ee65 LADX: Rework dungeon item fill (#1763) 2023-04-28 05:30:13 +02:00
alwaysintreble
b55174ccdf Docs: document option alias in the options doc (#1755)
* Docs: document option alias in the options doc

* give an example of alias and move it under option creation.

* use clearer example names
2023-04-27 09:33:49 +02:00
lordlou
7bcf299412 SM: missing foreign item filter fix (#1774) 2023-04-27 02:23:52 +02:00
Abacys
a7816d186f Launcher: Correcting minor formatting error (#1768)
Reformatting comment to comply with PEP format
2023-04-26 13:43:23 +02:00
Fabian Dill
9d40471dee Subnautica: add free samples option 2023-04-26 10:50:22 +02:00
zig-for
b704070de5 LADX: Fix palettes (#1767) 2023-04-26 10:49:38 +02:00
Fabian Dill
6c459066a7 Core: add generator_version to network protocol 2023-04-26 10:48:57 +02:00
Fabian Dill
4c3eaf2996 LttP: fix that collect can bypass requirements for ganon ped goal (#1771)
LttP: more pep8
2023-04-26 10:48:08 +02:00
TheBigSalarius
bb56f7b400 FF1: Added URange fix for Bizhawk 2.9 support
URange wasn't moved to common.lua (and no longer exists in connector_ff1.lua) when the lua files were changed for Bizhawk 2.9 socket change.
2023-04-26 10:47:25 +02:00
Doug Hoskisson
22ed7ff9c3 Zillion: fix empty 1st Sphere (#1770)
There was a low probability that the Zillion 1st sphere could be empty.
caused this test failure: https://github.com/ArchipelagoMW/Archipelago/actions/runs/4791795268/jobs/8522615992
2023-04-26 07:24:47 +02:00
axe-y
173513c9f4 DLCQuest: Generation bug fix (#1757)
* Fix documentation error

* init_creation

somehow this fix bug

* item_shuffle_fix

also a count as been corrected

* Fix_early_generation

* Update __init__.py

* Update __init__.py

fix version specific bug

* fix rule set for final boss

and did some reformation
(thanks kaito)

* Update Rules.py

the sword trio can now be in itself if before or actually themself

* Core: correct typing info for item_in_locations
Core: rename item_in_locations to item_name_in_location_names
Core: add actual item_name_in_locations

* item_shuffle_fix

also a count as been corrected

* Fix_early_generation

* fix rule set for final boss

and did some reformation
(thanks kaito)

* Update Rules.py

the sword trio can now be in itself if before or actually themself

* Fix the missing []
and switch to the good function

* - Cleanup and Black Sliver's suggestions

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-04-25 09:06:58 +02:00
Bicoloursnake
c0cf35edda StarCraft 2 macOS documentation (#1747)
* Adding SC2 macOS instructions

A few hours ago, I tested whether the client would run successfully on macOS (Send/Receive items, load maps, download maps, etc.). After the successful testing, I thought adding some documentation would be nice for those who want to play Archipelago on a macOS system.

* Don't need sudo

Turns out you don't need sudo to do the download_data, oops.

* Removed Extraneous Parantheses
2023-04-25 00:53:33 +02:00
Fabian Dill
dcc628f878 Core: correct typing info for item_in_locations
Core: rename item_in_locations to item_name_in_location_names
Core: add actual item_name_in_locations
2023-04-24 23:14:13 +02:00
Fabian Dill
b950af09a6 Factorio: remove tech_tree_layout_prerequisites from core 2023-04-24 23:11:25 +02:00
Fabian Dill
58aea7ca58 Multiserver: cleaner exit (#1743) 2023-04-23 22:21:28 +02:00
JaredWeakStrike
06a25a903e KH2: New Unit Test and better keyblade fill (#1744)
__init__:
 - Added exception for if the player has too many excluded abilities on keyblades.
 - Fixed Action Abilities only on keyblades from breaking.
 - Added proper support for ability quantity's instead of 1 of the ability 
 - Moved filling the localitems slot data to init instead of generate_output so I could easily unit test it

TestSlotData:
- Checks if the "localItems" part of slot data is filled. This is used for keeping track of local items and making sure nothing dupes
2023-04-23 22:20:43 +02:00
Alchav
62a265cc31 Pokémon R/B: logic and location name fixes (#1752)
Corrects incorrect name listed for location in rules.py leading to logic rules failing to apply.
Swaps location names for incorrectly-named trainersanity checks in Viridian Gym
2023-04-23 22:17:03 +02:00
lordlou
67c3076572 SM: comeback fix6 and some refactor (#1756)
refactored and cleaned a bit SMWorld class for best practices:
- moved content of Regions.py and Rules.py in SMWorld
- moved appropiate code to their dedicated World core functions
- moved some Entrances being created in generate_basic to create_regions
more comeback check fixes:
- fixed setting progression door openers items local if doors_colors_rando is used
- enable comeback check only for filling stage as later stages (progression balancing, accessibility and spoiler playthrough) are prone to fail with it
2023-04-23 22:16:01 +02:00
agilbert1412
ab5cb7adad Stardew Valley - Add alias for renamed Fishsanity option (#1758) 2023-04-23 21:59:46 +02:00
lordlou
5e84f91d2f SM: comeback fix5 (#1746) 2023-04-22 02:57:31 +02:00
zig-for
a7a17a5a4d Fix OOT? (#1721) 2023-04-20 09:23:04 +02:00
Ziktofel
a6ea3e1953 Sc2wol logic update (#1715)
Fixes some issues with typos and oversights logic generation in SC2

Rebalancing
Adds Vultures to Advanced tactics as train killers
Drops Firebats from basic unit pool and moves Goliaths from advanced tactics to standard
2023-04-20 09:13:34 +02:00
zig-for
0515acc8fe LADX: no pre fill (#1673)
* no pre fill
* redo trade logic
2023-04-20 09:12:53 +02:00
Fabian Dill
bf5c1cbbbf WebHost: add spoiler level field to generate form 2023-04-20 09:12:07 +02:00
alwaysintreble
c8fb46a5e6 The Messenger: Throw error for invalid names and replace _ with (#1728)
* Replace '_' with ' ' and throw error for other invalid names

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-04-20 09:11:15 +02:00
lordlou
a38a2903d5 SM: comeback fix4 (#1741) 2023-04-20 09:10:21 +02:00
JaredWeakStrike
4ef7e43521 KH2: client bug fixes (#1742)
Use item index instead of location and player to determine if the player should get the item.
Fixed getting stat increases on the title screen breaking stuff.
Changed local locations list from a list organized by world-id to a set.
Fixed the inventory slots to be the actual back of inventory.
Fixed recaching when not closing the client but switching slots .
Fixed getting a ability faster than the game so it dupes.
Removed verify location since it was never used.
2023-04-20 09:08:59 +02:00
Fabian Dill
e1f17fadfc Launcher: add icons where present and add headers to table 2023-04-20 05:59:49 +02:00
Adam Heinermann
4dc934729d Noita: implement new game (#1676)
* Noita: implement new game (#1676)

---------

Co-authored-by: DaftBrit <87314354+DaftBrit@users.noreply.github.com>
Co-authored-by: l.kelsall@b4rn.org.uk <l.kelsall@b4rn.org.uk>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Scipio Wright <lightdemonjoe4@gmail.com>
Co-authored-by: Zach Parks <zach@alliware.com>
2023-04-19 22:21:56 -05:00
Zach Parks
722757e18a Clique: Better, less-obtuse, docs for Clique. (#1719) 2023-04-19 21:17:16 -05:00
kindasneaki
0ca3c5e6a2 [kivy] change the sizing for macOS (#1732) 2023-04-19 23:16:13 +02:00
agilbert1412
664bbd86bb Stardew Valley: Fix Mistake and Formatting in settings page (#1737) 2023-04-19 23:15:22 +02:00
axe-y
7a9d4272be Generation bug fix (#1740) 2023-04-19 23:14:46 +02:00
alwaysintreble
7559adbb14 LTTP: fix bad parens in logging 2023-04-19 04:32:15 +02:00
Trevor L
1a7bc4ffd4 Blasphemous: Fix logic for Laudes (for real this time) (#1727) 2023-04-18 02:03:06 +02:00
Fabian Dill
be74a4a71a LttP: update xxtea to 3.0.0 2023-04-17 22:56:54 +02:00
lordlou
cb634fa8d4 SM: failing generation fixes (#1726)
- fixed wrong condition in Collect to assign lastAP
- fixed possible infinite loop in generating output when many SM worlds are present
- fixed new VARIA code that changed a list used for every SM worlds and would throw if many SM worlds uses Aea rando and not AreaLayout
2023-04-17 05:46:19 +02:00
axe-y
f6758524d5 DLCQuest: fix loader bug (#1729) 2023-04-17 02:38:48 +02:00
Fabian Dill
f395a6d184 Docs: has_all and has_any (#1725)
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-04-16 12:59:53 +02:00
agilbert1412
ea03c90152 Stardew Valley: Fix a logic bug and a documentation typo (#1722)
Typo in the documentation
Logic error for help wanted quests
2023-04-16 11:22:33 +02:00
Trevor L
50d9ab041a Blasphemous: Fix logic for Laudes (#1724) 2023-04-16 10:53:42 +02:00
axe-y
acd3cb45bf DLCQuest: Fix documentation error (#1720) 2023-04-16 05:04:47 +02:00
JaredWeakStrike
8a78062825 KH2: fixed lucky emblem required>lucky emblem amount (#1718) 2023-04-16 01:59:01 +02:00
Fabian Dill
599cd2c82e Launcher: add Discord links and generate yamls (#1716) 2023-04-16 01:57:52 +02:00
espeon65536
89ec31708e oot: bugfixes (#1709)
* oot client: check types of tables coming from lua script for safety
There was a reported bug with corrupted (?) slot data preventing locations sending. This should safeguard against any instances of that happening in the future, if it ever happens again.

* oot: repair minor hint issues
SMW has # in some location names which breaks ootr's poor text formatting system, so those need to be filtered out.
Also replaces "[X] for [player Y]" with "[player Y]'s X" as frequently requested.

* oot: update required client version

* oot client: fix patching filename bug

* oot: fix broken poachers saw item
how was I this stupid, seriously

* oot: sanitize player, location, and item names everywhere
2023-04-16 01:45:31 +02:00
Fabian Dill
ef211da27f Core: update modules 2023-04-16 01:43:24 +02:00
JaredWeakStrike
0122eb38ab KH2: Fix validAbilitys (#1714) 2023-04-15 22:04:08 +02:00
JaredWeakStrike
3d8bc0bb67 KH2: Init Cleanup and Keyblade Fix (#1713) 2023-04-15 21:17:23 +02:00
JusticePS
d85c13ef0e Adventure: Fix error in connector lua that was fairly harmless in BizHawk 2.8 but throws in 2.9 2023-04-15 21:16:11 +02:00
Doug Hoskisson
27cb93d319 Asset: make icon 512 x 512 (#1710) 2023-04-15 10:37:31 -05:00
black-sliver
b0e8c8db6b MultiServer: fix broken ping (#1711)
Ping argument seems to have changed in an update of websockets.
This uses the lib's default of 20s now.
2023-04-15 17:10:33 +02:00
zig-for
5a7d20d393 Lua: Further centralize code, fix Bizhawk 2.9 (#1685)
* Fixes the socket library for bizhawk 2.9/lua 5.4 by including another one in parallel
* Fixes lua 5.4 support by making socket.lua into a "modern" module (the `module` keyword is gone)
* Adds the linux version and 32 bit windows socket dlls because why not
* Merges common functions into `common.lua` - the only functional change of this should be that:
  * Some things that were locals are globals now - this can be changed, I just was lazy and it likely doesn't matter
  * `drawText` now uses middle/bottom for all prints - feel free to do what you like with that change
2023-04-15 09:17:33 +02:00
zig-for
808203a50f LADX: fix egg sprite in non-patched gfx mode (#1699) 2023-04-15 07:47:45 +02:00
zig-for
d8f79b4a42 LADX: Fix useless item being marked as progression (#1700) 2023-04-15 06:48:05 +02:00
zig-for
02ef6cee47 LADX: Support magpie tracker's sendfull button (#1701) 2023-04-15 06:47:36 +02:00
JaredWeakStrike
e716b50f8c KH2: Fixed Non Deterministic Generation (#1707) 2023-04-15 06:15:04 +02:00
agilbert1412
3fdf07677c Stardew Valley: Removed Pytest Requirement from everything (#1696) 2023-04-15 05:42:02 +02:00
t3hf1gm3nt
d3baca9251 [TLOZ] Add FileNotFoundError handling for base rom (#1708) 2023-04-15 05:40:48 +02:00
alwaysintreble
f52ca2571f Tests: Add tests for location name groups (#1706) 2023-04-14 20:11:01 +02:00
Fabian Dill
469807ba01 Core: remove outdated assert on push_item 2023-04-14 20:05:12 +02:00
alwaysintreble
8ada91939c LTTP: Fix Location Name Groups (#1705) 2023-04-14 20:04:20 +02:00
axe-y
054d14baa4 Fix DLCQuest Errors Generating on Latest Source, without any DLCQuest YAMLs (#1704) 2023-04-14 07:09:50 +02:00
Magnemania
f0324e60f8 SC2: Removed extra space from location (#1697) 2023-04-11 11:50:26 -05:00
zig-for
70ff19ac8c LADX: AP egg title screen (#1683) 2023-04-11 09:18:33 +02:00
Fabian Dill
b02b329181 DLCQuest: fix data_version 2023-04-11 08:47:19 +02:00
Nyx-Edelstein
8b7ffaf671 ALTTP: Add "oof" sound customization option (#709)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: Fabian Dill <fabian.dill@web.de>
Co-authored-by: Zach Parks <zach@alliware.com>
2023-04-10 21:31:57 -05:00
toasterparty
c711d803f8 [OC2] Enabled DLC Option (#1688)
- New OC2 option `DLCOptionSet`, which is a list of DLCs whose levels should or shouldn't be used for entrance randomizer (and mention in documentation). By default, DLC owners now need to enable DLCs in weighted settings.
- Throw user-friendly exceptions when contradictory settings are enabled
- Slightly relax generation requirements for sphere 1/2 level permutations
- Write entrance randomizer info in spoiler log
- Skip adding "Dark Green Ramp" to item pool if Kevin Levels are disabled
2023-04-11 03:43:29 +02:00
Zach Parks
3c3954f5e8 Core: Band-aid fixes start_inventory_from_pool causing generation failures if any world doesn't utilize it. (#1694)
* Core: Band-aid fixes `start_inventory_from_pool` causing generation failures if any world does utilize it.

* Core: Slightly better(?) solution

* Set default so it doesn't fail on WebHost.
2023-04-10 20:18:29 -05:00
Alchav
05d398a51d Pokémon R/B: Missing game corner logic (#1693)
* Game corner logic fix

* Fix for the fix
2023-04-10 20:16:38 -05:00
agilbert1412
5eadbc9840 Major Game Update: Stardew Valley v3.x.x - The BK Update (#1686)
This is a major update for Stardew Valley, for version 3.x.x.
Changes include a large number of new features, including Seasons Randomizer, SeedShuffle, Museumsanity, Friendsanity, Complete Collection Goal, Full House Goal, friendship multiplier

Co-authored-by: Jouramie <jouramie@hotmail.com>
2023-04-11 01:44:59 +02:00
Zach Parks
0c1e3097c3 WebHost: Set defaults for lists/sets on Weighted Settings page (#1692) 2023-04-10 18:01:54 -04:00
Fabian Dill
cdf7ca1dcc Core: allow ordered valid keys (#1690)
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-04-10 23:54:56 +02:00
alwaysintreble
77fbd0eb2b MultiServer: Notify clients of hint points (#1548)
* notify clients of their amount of hint points on initial connection and when hinting

* send in connect packet instead of sending a RoomUpdate on connect

* send hint_points update in `on_new_hint`

* add to connected packet docs

* hint_points isn't a new variable on RoomUpdate now

* note roomupdate can contain connected members

* add the hint point stuff to commonclient

* only show hint points when relevant and default to 0

* Revert "note roomupdate can contain connected members"

* remove hint_points from roomupdate args list and condense explanation of possible packet args

* updates from phar's review

* Small tweak to wording in RoomUpdate

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: Phar <zach@alliware.com>
2023-04-10 14:44:20 -05:00
Fabian Dill
c7284f90d9 Core: implement start_inventory_from_pool (#1170)
* Core: implement start_inventory_from_pool

* Factorio/LttP/Subnautica: add start_inventory_from_pool Option
2023-04-10 21:13:33 +02:00
alwaysintreble
8d559daa35 LTTP: use server for counting locations instead of having 249/216 (#1648) 2023-04-10 21:12:06 +02:00
Ziktofel
e49ffc64f2 SC2: Rebalance item classes (#1512)
It's been a month, if Condor disapproves this can be reverted.
I don't really know what to do about the SC2WOL situation going forward.
2023-04-10 21:08:49 +02:00
alwaysintreble
94a02510c0 core: Region management helpers (#761) 2023-04-10 21:07:37 +02:00
Fabian Dill
9d73988030 Tests: check that options have a docstring (#823) 2023-04-10 04:33:47 +02:00
JaredWeakStrike
81411a191c KH2: init cleanup and random visit locking fix and docs update. (#1652)
Random Visit Locking wasn't copying correctly
init cleanup and moved itempool population to create_items
Updated docs due to a lot of people having issues setting it up.
2023-04-10 04:12:23 +02:00
lordlou
6059b5ef66 SM: 20221101 update (#1479)
This adds support to most of Varia's 20221101 update. Notably, added Options for:
- Objectives
- Tourian
- RelaxedRoundRobinCF

As well as previously unsupported Options:
- EscapeRando
- RemoveEscapeEnemies
- HideItems
2023-04-10 00:35:46 +02:00
black-sliver
0bc5a3bc8d Readme: add missing game DLC Quest (#1682) 2023-04-09 15:53:23 -05:00
black-sliver
11fdb29357 Doc: apworlds have to be all lower case
https://discord.com/channels/731205301247803413/731214280439103580/1094655639600508999
2023-04-09 21:48:22 +02:00
axe-y
bbef7a4cbc DLCQuest : implement new game (#1628)
adding DLC Quest as a new game
2023-04-09 21:06:59 +02:00
Fabian Dill
8e7bbb4ea8 Factorio: flatten science pack curve (#1660)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-04-09 20:58:24 +02:00
alwaysintreble
6628e8c85d Docs: Add some more details to running from source doc (#1680)
* make build tools step more obviously optional and give better directions

* review commit
2023-04-09 15:53:14 +02:00
lordlou
84402a1b55 SM and SMZ3 apworld support (#1677) 2023-04-08 22:52:34 +02:00
Fabian Dill
f4035b8621 MultiServer: remove remaining forfeit compat from network layer 2023-04-08 20:10:07 +02:00
Fabian Dill
bbf8546867 MultiServer: Flag for saving on datastore, create_as_hint scout and client state change 2023-04-08 20:09:51 +02:00
Zach Parks
67a22b8b43 Clique: Force priority location for "final location" and other minor tweaks. (#1583)
* Clique: The greatest game of all time

* Fix failing test

* Increment data_version for backwards compat

* Clique: Tweaked names and forced progression for "The Button"

* Clique: Just location/item definitions to class

* Clique: More tweaks.

* Clique: Fix derp moment.

* Clique: Fix derp moment, part dos.

* Clique: Suggested changes.

* Clique: Update domain

* Clique: Create derived classes for Item and Location

* Clique: simplify line
2023-04-07 19:05:16 -05:00
Trevor L
8e6ec85532 Blasphemous: Update docs, item creation (#1670)
* Blasphemous: Update docs, item creation

* Blasphemous: Remove get_pre_fill_items
2023-04-07 19:04:34 -05:00
Yussur Mustafa Oraji
8fc50510a0 sm64ex,v6: Use create_items for itempool modification (#1674) 2023-04-07 19:03:28 -05:00
Trevor L
aa6ad5d34f Hylics 2: Update item creation (#1671) 2023-04-07 20:01:26 +02:00
zig-for
ccb89dd65c WebHost: Fix upload of .archipelago file (#1657)
* Fix upload

* simple fix for slot data

* remove extra patch.data check

* remove extra parens

* Update WebHostLib/upload.py

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

* Update WebHostLib/upload.py

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

* parse -> process

* Update WebHostLib/upload.py

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-04-07 00:24:03 +02:00
agilbert1412
a86c0aa37d Stardew: Fix link in Setup Guide (#1672) 2023-04-06 20:07:09 +02:00
zig-for
ece6598b09 LADX: apworld (#1665) 2023-04-06 20:06:34 +02:00
alwaysintreble
eef8f7af1a The Messenger: Add Mega Time Shards and Quest 1 boss locations (#1661)
* implement mega shards

* create the option and locations, add to slot data and tests

* add boss refights as locations

* remove barma'thazel. it's apparently impossible to get to him

* remove barma'thazel again

* up max shard count to 85

* increment version

* dynamically alter the power seal pool

* revert host.yaml change

* two mega shards were missing from the maps

* add new checks to the info page

* add some more rules to skylands

* forgot to update my tests

* explicit imports, remove unnecessary typing, lower required client ver

* use generators for shard and seal creation
2023-04-06 10:48:30 +02:00
Chris Wilson
c626618221 Update ArchipIDLE's documentation and create items in create_items func. (#1669) 2023-04-06 00:08:41 -04:00
kindasneaki
47989325f8 [Webhost] header closing tag moved after mobile menu (#1650)
* Change archipelago mod download page

* Docs: change connecting to archipelago in RoR2 setup guide

* /header off by one
2023-04-05 19:27:56 -04:00
black-sliver
815e7e6b0a Core: default data_version to 0 (#1668)
* Core: default data_version to 0

This allows new (ap-)worlds to function with old clients without having to define a version.

* Blasphemous: fix data_version
2023-04-06 01:19:58 +02:00
Fabian Dill
a61a1f58c6 Network: allow filtering checked and missing by text fragment 2023-04-05 19:36:32 +02:00
black-sliver
4c24872264 CI: update ubuntu to 20.04
18.04 will not be supported starting 2023-04-01
2023-04-05 19:16:32 +02:00
Zach Parks
e778e49574 Core: Fix ZeroDivisionError if a world is contained 100% locked locations. (#1659)
* Core: Fix divide by zero error if all locations are priority

* Core: 100% makes more sense than 0% in this context

* Core: Revert a some "autoformatter" damage

* Core: Move comparisons a bit earlier.

Worlds that are 100% filled with locked locations should now be completely skipped in the progression balancing step now as we filter them out at the very beginning of the stage now. Also skips progression balancing if it turns out all locations are locked early before attempting to balance.
2023-04-05 12:11:34 -05:00
FlySniper
25f7413881 Wargroove: Client is not added to the Start Menu. (#1664)
* Wargroove - Fixed client not showing up in the windows search menu.
2023-04-05 00:07:15 -05:00
Alchav
397ce8343e Pokémon R/B: Pokédex option fixes (#1666)
* Pokémon R/B: Pokedex option fixes

* Pokémon R/B: Missing option display names
2023-04-04 23:59:59 -05:00
Trevor L
37fdc00517 Blasphemous: Small logic fixes (#1667) 2023-04-04 23:54:51 -05:00
alwaysintreble
03aa9b3604 Update AP icons (#1637) 2023-04-04 23:38:23 -05:00
JusticePS
8f52e4654f Adventure: Change user_path to local_path for locating basepatch file (#1639) 2023-04-04 19:32:03 +02:00
alwaysintreble
a86fd37860 Core: set convert_name_groups to true for LocationSet (#1663) 2023-04-04 19:29:20 +02:00
toasterparty
eb503adb13 [OC2] Only Calculate Priority Locations Once (#1662) 2023-04-04 06:53:05 +02:00
Fabian Dill
cbf72becc1 Subnautica: group items and removal of item pool option 2023-04-04 06:46:54 +02:00
alwaysintreble
cdd460ae15 The Messenger: some miscellaneous cleanup. Also found a logic bug. oops. (#1654) 2023-04-04 02:27:36 +02:00
toasterparty
ffd968d89d [OC2] Fix Overworld Access Logic for World 3 (#1655) 2023-04-04 02:26:24 +02:00
toasterparty
8d73746d5b [OC2] Spelling Mistakes (#1656) 2023-04-04 02:25:59 +02:00
zig-for
5ed56db48a LADX: Fix crash in item pick up with > 100 players (#1658) 2023-04-04 02:23:39 +02:00
PoryGone
5f447f4e6b SMW: Fix Option Tooltip (#1651) 2023-04-02 03:54:07 +02:00
Fabian Dill
f015cf4298 MultiServer: compat fix if checksum is not present (#1642) 2023-04-01 22:40:14 +02:00
Freya Arbjerg
e43bb99622 Fix broken styling of multitracker navigation (#1644) 2023-04-01 19:56:08 +02:00
JaredWeakStrike
34de5a57af KH2: updated the docs for options and faq (#1641) 2023-04-01 19:55:43 +02:00
Fabian Dill
510a460d84 Multiserver: location name groups fix (#1643) 2023-04-01 19:54:44 +02:00
Justin LaLone
6e271b643d Adventure: Added examples for automatically loading adventure_connector.lua
in BizHawk
2023-04-01 19:54:02 +02:00
2059 changed files with 301947 additions and 78821 deletions

5
.coveragerc Normal file
View File

@@ -0,0 +1,5 @@
[report]
exclude_lines =
pragma: no cover
if TYPE_CHECKING:
if typing.TYPE_CHECKING:

31
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
'is: documentation':
- changed-files:
- all-globs-to-all-files: '{**/docs/**,**/README.md}'
'affects: webhost':
- changed-files:
- all-globs-to-any-file: 'WebHost.py'
- all-globs-to-any-file: 'WebHostLib/**/*'
'affects: core':
- changed-files:
- all-globs-to-any-file:
- '!*Client.py'
- '!README.md'
- '!LICENSE'
- '!*.yml'
- '!.gitignore'
- '!**/docs/**'
- '!typings/kivy/**'
- '!test/**'
- '!data/**'
- '!.run/**'
- '!.github/**'
- '!worlds_disabled/**'
- '!worlds/**'
- '!WebHost.py'
- '!WebHostLib/**'
- any-glob-to-any-file: # exceptions to the above rules of "stuff that isn't core"
- 'worlds/generic/**/*.py'
- 'worlds/*.py'
- 'CommonClient.py'

27
.github/pyright-config.json vendored Normal file
View File

@@ -0,0 +1,27 @@
{
"include": [
"type_check.py",
"../worlds/AutoSNIClient.py",
"../Patch.py"
],
"exclude": [
"**/__pycache__"
],
"stubPath": "../typings",
"typeCheckingMode": "strict",
"reportImplicitOverride": "error",
"reportMissingImports": true,
"reportMissingTypeStubs": true,
"pythonVersion": "3.8",
"pythonPlatform": "Windows",
"executionEnvironments": [
{
"root": ".."
}
]
}

15
.github/type_check.py vendored Normal file
View File

@@ -0,0 +1,15 @@
from pathlib import Path
import subprocess
config = Path(__file__).parent / "pyright-config.json"
command = ("pyright", "-p", str(config))
print(" ".join(command))
try:
result = subprocess.run(command)
except FileNotFoundError as e:
print(f"{e} - Is pyright installed?")
exit(1)
exit(result.returncode)

View File

@@ -0,0 +1,80 @@
name: Analyze modified files
on:
pull_request:
paths:
- "**.py"
push:
paths:
- "**.py"
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
BEFORE: ${{ github.event.before }}
AFTER: ${{ github.event.after }}
jobs:
flake8-or-mypy:
strategy:
fail-fast: false
matrix:
task: [flake8, mypy]
name: ${{ matrix.task }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: "Determine modified files (pull_request)"
if: github.event_name == 'pull_request'
run: |
git fetch origin $BASE $HEAD
DIFF=$(git diff --diff-filter=d --name-only $BASE...$HEAD -- "*.py")
echo "modified files:"
echo "$DIFF"
echo "diff=${DIFF//$'\n'/$' '}" >> $GITHUB_ENV
- name: "Determine modified files (push)"
if: github.event_name == 'push' && github.event.before != '0000000000000000000000000000000000000000'
run: |
git fetch origin $BEFORE $AFTER
DIFF=$(git diff --diff-filter=d --name-only $BEFORE..$AFTER -- "*.py")
echo "modified files:"
echo "$DIFF"
echo "diff=${DIFF//$'\n'/$' '}" >> $GITHUB_ENV
- name: "Treat all files as modified (new branch)"
if: github.event_name == 'push' && github.event.before == '0000000000000000000000000000000000000000'
run: |
echo "diff=." >> $GITHUB_ENV
- uses: actions/setup-python@v5
if: env.diff != ''
with:
python-version: 3.8
- name: "Install dependencies"
if: env.diff != ''
run: |
python -m pip install --upgrade pip ${{ matrix.task }}
python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes
- name: "flake8: Stop the build if there are Python syntax errors or undefined names"
continue-on-error: false
if: env.diff != '' && matrix.task == 'flake8'
run: |
flake8 --count --select=E9,F63,F7,F82 --show-source --statistics ${{ env.diff }}
- name: "flake8: Lint modified files"
continue-on-error: true
if: env.diff != '' && matrix.task == 'flake8'
run: |
flake8 --count --max-complexity=14 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
- name: "mypy: Type check modified files"
continue-on-error: true
if: env.diff != '' && matrix.task == 'mypy'
run: |
mypy --follow-imports=silent --install-types --non-interactive --strict ${{ env.diff }}

View File

@@ -8,11 +8,13 @@ on:
- '.github/workflows/build.yml'
- 'setup.py'
- 'requirements.txt'
- '*.iss'
pull_request:
paths:
- '.github/workflows/build.yml'
- 'setup.py'
- 'requirements.txt'
- '*.iss'
workflow_dispatch:
env:
@@ -25,50 +27,94 @@ jobs:
build-win-py38: # RCs will still be built and signed by hand
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.8'
- name: Download run-time dependencies
run: |
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
choco install innosetup --version=6.2.2 --allow-downgrade
- name: Build
run: |
python -m pip install --upgrade pip
python setup.py build_exe --yes
$NAME="$(ls build)".Split('.',2)[1]
if ( $? -eq $false ) {
Write-Error "setup.py failed!"
exit 1
}
$NAME="$(ls build | Select-String -Pattern 'exe')".Split('.',2)[1]
$ZIP_NAME="Archipelago_$NAME.7z"
echo "$NAME -> $ZIP_NAME"
echo "ZIP_NAME=$ZIP_NAME" >> $Env:GITHUB_ENV
New-Item -Path dist -ItemType Directory -Force
cd build
Rename-Item exe.$NAME Archipelago
Rename-Item "exe.$NAME" Archipelago
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
Rename-Item Archipelago "exe.$NAME" # inno_setup.iss expects the original name
- name: Build Setup
run: |
& "${env:ProgramFiles(x86)}\Inno Setup 6\iscc.exe" inno_setup.iss /DNO_SIGNTOOL
if ( $? -eq $false ) {
Write-Error "Building setup failed!"
exit 1
}
$contents = Get-ChildItem -Path setups/*.exe -Force -Recurse
$SETUP_NAME=$contents[0].Name
echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV
- name: Check build loads expected worlds
shell: bash
run: |
cd build/exe*
mv Players/Templates/meta.yaml .
ls -1 Players/Templates | sort > setup-player-templates.txt
rm -R Players/Templates
timeout 30 ./ArchipelagoLauncher "Generate Template Options" || true
ls -1 Players/Templates | sort > generated-player-templates.txt
cmp setup-player-templates.txt generated-player-templates.txt \
|| diff setup-player-templates.txt generated-player-templates.txt
mv meta.yaml Players/Templates/
- name: Test Generate
shell: bash
run: |
cd build/exe*
cp Players/Templates/Clique.yaml Players/
timeout 30 ./ArchipelagoGenerate
- name: Store 7z
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ env.ZIP_NAME }}
path: dist/${{ env.ZIP_NAME }}
compression-level: 0 # .7z is incompressible by zip
if-no-files-found: error
retention-days: 7 # keep for 7 days, should be enough
- name: Store Setup
uses: actions/upload-artifact@v4
with:
name: ${{ env.SETUP_NAME }}
path: setups/${{ env.SETUP_NAME }}
if-no-files-found: error
retention-days: 7 # keep for 7 days, should be enough
build-ubuntu1804:
runs-on: ubuntu-18.04
build-ubuntu2004:
runs-on: ubuntu-20.04
steps:
# - copy code below to release.yml -
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install base dependencies
run: |
sudo apt update
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
- name: Get a recent python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.9'
python-version: '3.11'
- name: Install build-time dependencies
run: |
echo "PYTHON=python3.9" >> $GITHUB_ENV
echo "PYTHON=python3.11" >> $GITHUB_ENV
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract
@@ -90,7 +136,7 @@ jobs:
echo -e "setup.py dist output:\n `ls dist`"
cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd ..
export TAR_NAME="${APPIMAGE_NAME%.AppImage}.tar.gz"
(cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME")
(cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -cv Archipelago | gzip -8 > ../dist/$TAR_NAME && mv Archipelago "$DIR_NAME")
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
# - copy code above to release.yml -
@@ -98,15 +144,36 @@ jobs:
run: |
source venv/bin/activate
python setup.py build_exe --yes
- name: Check build loads expected worlds
shell: bash
run: |
cd build/exe*
mv Players/Templates/meta.yaml .
ls -1 Players/Templates | sort > setup-player-templates.txt
rm -R Players/Templates
timeout 30 ./ArchipelagoLauncher "Generate Template Options" || true
ls -1 Players/Templates | sort > generated-player-templates.txt
cmp setup-player-templates.txt generated-player-templates.txt \
|| diff setup-player-templates.txt generated-player-templates.txt
mv meta.yaml Players/Templates/
- name: Test Generate
shell: bash
run: |
cd build/exe*
cp Players/Templates/Clique.yaml Players/
timeout 30 ./ArchipelagoGenerate
- name: Store AppImage
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ env.APPIMAGE_NAME }}
path: dist/${{ env.APPIMAGE_NAME }}
if-no-files-found: error
retention-days: 7
- name: Store .tar.gz
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ env.TAR_NAME }}
path: dist/${{ env.TAR_NAME }}
compression-level: 0 # .gz is incompressible by zip
if-no-files-found: error
retention-days: 7

View File

@@ -43,7 +43,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

54
.github/workflows/ctest.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
# Run CMake / CTest C++ unit tests
name: ctest
on:
push:
paths:
- '**.cc?'
- '**.cpp'
- '**.cxx'
- '**.hh?'
- '**.hpp'
- '**.hxx'
- '**.CMakeLists'
- '.github/workflows/ctest.yml'
pull_request:
paths:
- '**.cc?'
- '**.cpp'
- '**.cxx'
- '**.hh?'
- '**.hpp'
- '**.hxx'
- '**.CMakeLists'
- '.github/workflows/ctest.yml'
jobs:
ctest:
runs-on: ${{ matrix.os }}
name: Test C++ ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: ilammy/msvc-dev-cmd@v1
if: startsWith(matrix.os,'windows')
- uses: Bacondish2023/setup-googletest@v1
with:
build-type: 'Release'
- name: Build tests
run: |
cd test/cpp
mkdir build
cmake -S . -B build/ -DCMAKE_BUILD_TYPE=Release
cmake --build build/ --config Release
ls
- name: Run tests
run: |
cd test/cpp
ctest --test-dir build/ -C Release --output-on-failure

View File

@@ -0,0 +1,46 @@
name: Label Pull Request
on:
pull_request_target:
types: ['opened', 'reopened', 'synchronize', 'ready_for_review', 'converted_to_draft', 'closed']
branches: ['main']
permissions:
contents: read
pull-requests: write
jobs:
labeler:
name: 'Apply content-based labels'
if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize'
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
with:
sync-labels: false
peer_review:
name: 'Apply peer review label'
needs: labeler
if: >-
(github.event.action == 'opened' || github.event.action == 'reopened' ||
github.event.action == 'ready_for_review') && !github.event.pull_request.draft
runs-on: ubuntu-latest
steps:
- name: 'Add label'
run: "gh pr edit \"$PR_URL\" --add-label 'waiting-on: peer-review'"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
unblock_draft_prs:
name: 'Remove waiting-on labels'
needs: labeler
if: github.event.action == 'converted_to_draft' || github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: 'Remove labels'
run: |-
gh pr edit "$PR_URL" --remove-label 'waiting-on: peer-review' \
--remove-label 'waiting-on: core-review' \
--remove-label 'waiting-on: world-maintainer' \
--remove-label 'waiting-on: author'
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,35 +0,0 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: lint
on:
push:
paths:
- '**.py'
pull_request:
paths:
- '**.py'
jobs:
flake8:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip wheel
pip install flake8
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

View File

@@ -18,7 +18,7 @@ jobs:
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV # tag x.y.z will become "Archipelago x.y.z"
- name: Create Release
uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
with:
draft: true # don't publish right away, especially since windows build is added by hand
prerelease: false
@@ -29,25 +29,25 @@ jobs:
# build-release-windows: # this is done by hand because of signing
# build-release-macos: # LF volunteer
build-release-ubuntu1804:
runs-on: ubuntu-18.04
build-release-ubuntu2004:
runs-on: ubuntu-20.04
steps:
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
# - code below copied from build.yml -
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install base dependencies
run: |
sudo apt update
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
- name: Get a recent python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.9'
python-version: '3.11'
- name: Install build-time dependencies
run: |
echo "PYTHON=python3.9" >> $GITHUB_ENV
echo "PYTHON=python3.11" >> $GITHUB_ENV
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract
@@ -69,12 +69,12 @@ jobs:
echo -e "setup.py dist output:\n `ls dist`"
cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd ..
export TAR_NAME="${APPIMAGE_NAME%.AppImage}.tar.gz"
(cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME")
(cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -cv Archipelago | gzip -8 > ../dist/$TAR_NAME && mv Archipelago "$DIR_NAME")
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
# - code above copied from build.yml -
- name: Add to Release
uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
with:
draft: true # see above
prerelease: false

65
.github/workflows/scan-build.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
name: Native Code Static Analysis
on:
push:
paths:
- '**.c'
- '**.cc'
- '**.cpp'
- '**.cxx'
- '**.h'
- '**.hh'
- '**.hpp'
- '**.pyx'
- 'setup.py'
- 'requirements.txt'
- '.github/workflows/scan-build.yml'
pull_request:
paths:
- '**.c'
- '**.cc'
- '**.cpp'
- '**.cxx'
- '**.h'
- '**.hh'
- '**.hpp'
- '**.pyx'
- 'setup.py'
- 'requirements.txt'
- '.github/workflows/scan-build.yml'
jobs:
scan-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install newer Clang
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x ./llvm.sh
sudo ./llvm.sh 17
- name: Install scan-build command
run: |
sudo apt install clang-tools-17
- name: Get a recent python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip -r requirements.txt
- name: scan-build
run: |
source venv/bin/activate
scan-build-17 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y
- name: Store report
if: failure()
uses: actions/upload-artifact@v4
with:
name: scan-build-reports
path: scan-build-reports

33
.github/workflows/strict-type-check.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: type check
on:
pull_request:
paths:
- "**.py"
- ".github/pyright-config.json"
- ".github/workflows/strict-type-check.yml"
- "**.pyi"
push:
paths:
- "**.py"
- ".github/pyright-config.json"
- ".github/workflows/strict-type-check.yml"
- "**.pyi"
jobs:
pyright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: "Install dependencies"
run: |
python -m pip install --upgrade pip pyright==1.1.358
python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes
- name: "pyright: strict check on specific files"
run: python .github/type_check.py

View File

@@ -24,7 +24,7 @@ on:
- '.github/workflows/unittests.yml'
jobs:
build:
unit:
runs-on: ${{ matrix.os }}
name: Test Python ${{ matrix.python.version }} ${{ matrix.os }}
@@ -36,25 +36,56 @@ jobs:
- {version: '3.8'}
- {version: '3.9'}
- {version: '3.10'}
- {version: '3.11'}
include:
- python: {version: '3.8'} # win7 compat
os: windows-latest
- python: {version: '3.10'} # current
- python: {version: '3.11'} # current
os: windows-latest
- python: {version: '3.10'} # current
- python: {version: '3.11'} # current
os: macos-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python.version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python.version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-subtests
pip install pytest pytest-subtests pytest-xdist
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
python Launcher.py --update_settings # make sure host.yaml exists for tests
- name: Unittests
run: |
pytest
pytest -n auto
hosting:
runs-on: ${{ matrix.os }}
name: Test hosting with ${{ matrix.python.version }} on ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-latest
python:
- {version: '3.11'} # current
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python.version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python.version }}
- name: Install dependencies
run: |
python -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
- name: Test hosting
run: |
source venv/bin/activate
export PYTHONPATH=$(pwd)
python test/hosting/__main__.py

18
.gitignore vendored
View File

@@ -9,12 +9,14 @@
*.apmc
*.apz5
*.aptloz
*.apemerald
*.pyc
*.pyd
*.sfc
*.z64
*.n64
*.nes
*.smc
*.sms
*.gb
*.gbc
@@ -27,15 +29,21 @@
*.archipelago
*.apsave
*.BIN
*.puml
setups
build
bundle/components.wxs
dist
/prof/
README.html
.vs/
EnemizerCLI/
/Players/
/SNI/
/sni-*/
/appimagetool*
/host.yaml
/options.yaml
/config.yaml
/logs/
@@ -54,6 +62,7 @@ Output Logs/
/installdelete.iss
/data/user.kv
/datapackage
/custom_worlds
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -137,6 +146,7 @@ ipython_config.py
.venv*
env/
venv/
/venv*/
ENV/
env.bak/
venv.bak/
@@ -167,6 +177,11 @@ dmypy.json
# Cython debug symbols
cython_debug/
# Cython intermediates
_speedups.c
_speedups.cpp
_speedups.html
# minecraft server stuff
jdk*/
minecraft*/
@@ -176,6 +191,9 @@ minecraft_versions.json
# pyenv
.python-version
#undertale stuff
/Undertale/
# OS General Files
.DS_Store
.AppleDouble

View File

@@ -0,0 +1,18 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Archipelago Unittests" type="tests" factoryName="Unittests">
<module name="Archipelago" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="_new_pattern" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/test&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" />
</configuration>
</component>

8
AHITClient.py Normal file
View File

@@ -0,0 +1,8 @@
from worlds.ahit.Client import launch
import Utils
import ModuleUpdate
ModuleUpdate.update()
if __name__ == "__main__":
Utils.init_logging("AHITClient", exception_logger="Client")
launch()

View File

@@ -25,11 +25,11 @@ from worlds.adventure.Offsets import static_item_element_size, connector_port_of
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = \
"Connection timing out. Please restart your emulator, then restart adventure_connector.lua"
"Connection timing out. Please restart your emulator, then restart connector_adventure.lua"
CONNECTION_REFUSED_STATUS = \
"Connection Refused. Please start your emulator and make sure adventure_connector.lua is running"
"Connection Refused. Please start your emulator and make sure connector_adventure.lua is running"
CONNECTION_RESET_STATUS = \
"Connection was reset. Please restart your emulator, then restart adventure_connector.lua"
"Connection was reset. Please restart your emulator, then restart connector_adventure.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
@@ -80,7 +80,7 @@ class AdventureContext(CommonContext):
self.local_item_locations = {}
self.dragon_speed_info = {}
options = Utils.get_options()
options = Utils.get_settings()
self.display_msgs = options["adventure_options"]["display_msgs"]
async def server_auth(self, password_requested: bool = False):
@@ -102,7 +102,7 @@ class AdventureContext(CommonContext):
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
self.locations_array = None
if Utils.get_options()["adventure_options"].get("death_link", False):
if Utils.get_settings()["adventure_options"].get("death_link", False):
self.set_deathlink = True
async_start(self.get_freeincarnates_used())
elif cmd == "RoomInfo":
@@ -112,14 +112,15 @@ class AdventureContext(CommonContext):
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "ReceivedItems":
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
msg = f"Received {', '.join([self.item_names.lookup_in_game(item.item) for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "Retrieved":
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
if self.freeincarnates_used is None:
self.freeincarnates_used = 0
self.freeincarnates_used += self.freeincarnate_pending
self.send_pending_freeincarnates()
if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]:
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
if self.freeincarnates_used is None:
self.freeincarnates_used = 0
self.freeincarnates_used += self.freeincarnate_pending
self.send_pending_freeincarnates()
elif cmd == "SetReply":
if args["key"] == f"adventure_{self.auth}_freeincarnates_used":
self.freeincarnates_used = args["value"]
@@ -396,7 +397,7 @@ async def atari_sync_task(ctx: AdventureContext):
ctx.atari_streams = await asyncio.wait_for(
asyncio.open_connection("localhost",
port),
timeout=10)
timeout=10)
ctx.atari_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
@@ -414,8 +415,8 @@ async def atari_sync_task(ctx: AdventureContext):
async def run_game(romfile):
auto_start = Utils.get_options()["adventure_options"].get("rom_start", True)
rom_args = Utils.get_options()["adventure_options"].get("rom_args")
auto_start = Utils.get_settings()["adventure_options"].get("rom_start", True)
rom_args = Utils.get_settings()["adventure_options"].get("rom_args")
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
@@ -436,7 +437,7 @@ async def patch_and_run_game(patch_file, ctx):
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
with open(Utils.user_path("data", "adventure_basepatch.bsdiff4"), "rb") as file:
with open(Utils.local_path("data", "adventure_basepatch.bsdiff4"), "rb") as file:
basepatch = bytes(file.read())
base_patched_rom_data = bsdiff4.patch(base_rom, basepatch)

File diff suppressed because it is too large Load Diff

9
BizHawkClient.py Normal file
View File

@@ -0,0 +1,9 @@
from __future__ import annotations
import ModuleUpdate
ModuleUpdate.update()
from worlds._bizhawk.context import launch
if __name__ == "__main__":
launch()

View File

@@ -1,4 +1,7 @@
from __future__ import annotations
import collections
import copy
import logging
import asyncio
import urllib.parse
@@ -6,6 +9,7 @@ import sys
import typing
import time
import functools
import warnings
import ModuleUpdate
ModuleUpdate.update()
@@ -18,11 +22,12 @@ if __name__ == "__main__":
Utils.init_logging("TextClient", exception_logger="Client")
from MultiServer import CommandProcessor
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \
ClientStatus, Permission, NetworkSlot, RawJSONtoTextParser
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes)
from Utils import Version, stream_input, async_start
from worlds import network_data_package, AutoWorldRegister
import os
import ssl
if typing.TYPE_CHECKING:
import kvui
@@ -33,6 +38,12 @@ logger = logging.getLogger("Client")
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
@Utils.cache_argsless
def get_ssl_context():
import certifi
return ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
class ClientCommandProcessor(CommandProcessor):
def __init__(self, ctx: CommonContext):
self.ctx = ctx
@@ -63,19 +74,29 @@ class ClientCommandProcessor(CommandProcessor):
def _cmd_received(self) -> bool:
"""List all received items"""
self.output(f'{len(self.ctx.items_received)} received items:')
item: NetworkItem
self.output(f'{len(self.ctx.items_received)} received items, sorted by time:')
for index, item in enumerate(self.ctx.items_received, 1):
self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}")
parts = []
add_json_item(parts, item.item, self.ctx.slot, item.flags)
add_json_text(parts, " from ")
add_json_location(parts, item.location, item.player)
add_json_text(parts, " by ")
add_json_text(parts, item.player, type=JSONTypes.player_id)
self.ctx.on_print_json({"data": parts, "cmd": "PrintJSON"})
return True
def _cmd_missing(self) -> bool:
"""List all missing location checks, from your local game state"""
def _cmd_missing(self, filter_text = "") -> bool:
"""List all missing location checks, from your local game state.
Can be given text, which will be used as filter."""
if not self.ctx.game:
self.output("No game set, cannot determine missing checks.")
return False
count = 0
checked_count = 0
for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items():
if filter_text and filter_text not in location:
continue
if location_id < 0:
continue
if location_id not in self.ctx.locations_checked:
@@ -103,6 +124,15 @@ class ClientCommandProcessor(CommandProcessor):
for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id:
self.output(item_name)
def _cmd_item_groups(self):
"""List all item group names for the currently running game."""
if not self.ctx.game:
self.output("No game set, cannot determine existing item groups.")
return False
self.output(f"Item Group Names for {self.ctx.game}")
for group_name in AutoWorldRegister.world_types[self.ctx.game].item_name_groups:
self.output(group_name)
def _cmd_locations(self):
"""List all location names for the currently running game."""
if not self.ctx.game:
@@ -112,6 +142,15 @@ class ClientCommandProcessor(CommandProcessor):
for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id:
self.output(location_name)
def _cmd_location_groups(self):
"""List all location group names for the currently running game."""
if not self.ctx.game:
self.output("No game set, cannot determine existing location groups.")
return False
self.output(f"Location Group Names for {self.ctx.game}")
for group_name in AutoWorldRegister.world_types[self.ctx.game].location_name_groups:
self.output(group_name)
def _cmd_ready(self):
"""Send ready status to server."""
self.ctx.ready = not self.ctx.ready
@@ -136,10 +175,77 @@ class CommonContext:
items_handling: typing.Optional[int] = None
want_slot_data: bool = True # should slot_data be retrieved via Connect
# data package
# Contents in flux until connection to server is made, to download correct data for this multiworld.
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
class NameLookupDict:
"""A specialized dict, with helper methods, for id -> name item/location data package lookups by game."""
def __init__(self, ctx: CommonContext, lookup_type: typing.Literal["item", "location"]):
self.ctx: CommonContext = ctx
self.lookup_type: typing.Literal["item", "location"] = lookup_type
self._unknown_item: typing.Callable[[int], str] = lambda key: f"Unknown {lookup_type} (ID: {key})"
self._archipelago_lookup: typing.Dict[int, str] = {}
self._flat_store: typing.Dict[int, str] = Utils.KeyedDefaultDict(self._unknown_item)
self._game_store: typing.Dict[str, typing.ChainMap[int, str]] = collections.defaultdict(
lambda: collections.ChainMap(self._archipelago_lookup, Utils.KeyedDefaultDict(self._unknown_item)))
self.warned: bool = False
# noinspection PyTypeChecker
def __getitem__(self, key: str) -> typing.Mapping[int, str]:
# TODO: In a future version (0.6.0?) this should be simplified by removing implicit id lookups support.
if isinstance(key, int):
if not self.warned:
# Use warnings instead of logger to avoid deprecation message from appearing on user side.
self.warned = True
warnings.warn(f"Implicit name lookup by id only is deprecated and only supported to maintain "
f"backwards compatibility for now. If multiple games share the same id for a "
f"{self.lookup_type}, name could be incorrect. Please use "
f"`{self.lookup_type}_names.lookup_in_game()` or "
f"`{self.lookup_type}_names.lookup_in_slot()` instead.")
return self._flat_store[key] # type: ignore
return self._game_store[key]
def __len__(self) -> int:
return len(self._game_store)
def __iter__(self) -> typing.Iterator[str]:
return iter(self._game_store)
def __repr__(self) -> str:
return self._game_store.__repr__()
def lookup_in_game(self, code: int, game_name: typing.Optional[str] = None) -> str:
"""Returns the name for an item/location id in the context of a specific game or own game if `game` is
omitted.
"""
if game_name is None:
game_name = self.ctx.game
assert game_name is not None, f"Attempted to lookup {self.lookup_type} with no game name available."
return self._game_store[game_name][code]
def lookup_in_slot(self, code: int, slot: typing.Optional[int] = None) -> str:
"""Returns the name for an item/location id in the context of a specific slot or own slot if `slot` is
omitted.
Use of `lookup_in_slot` should not be used when not connected to a server. If looking in own game, set
`ctx.game` and use `lookup_in_game` method instead.
"""
if slot is None:
slot = self.ctx.slot
assert slot is not None, f"Attempted to lookup {self.lookup_type} with no slot info available."
return self.lookup_in_game(code, self.ctx.slot_info[slot].game)
def update_game(self, game: str, name_to_id_lookup_table: typing.Dict[str, int]) -> None:
"""Overrides existing lookup tables for a particular game."""
id_to_name_lookup_table = Utils.KeyedDefaultDict(self._unknown_item)
id_to_name_lookup_table.update({code: name for name, code in name_to_id_lookup_table.items()})
self._game_store[game] = collections.ChainMap(self._archipelago_lookup, id_to_name_lookup_table)
self._flat_store.update(id_to_name_lookup_table) # Only needed for legacy lookup method.
if game == "Archipelago":
# Keep track of the Archipelago data package separately so if it gets updated in a custom datapackage,
# it updates in all chain maps automatically.
self._archipelago_lookup.clear()
self._archipelago_lookup.update(id_to_name_lookup_table)
# defaults
starting_reconnect_delay: int = 5
@@ -154,7 +260,9 @@ class CommonContext:
disconnected_intentionally: bool = False
server: typing.Optional[Endpoint] = None
server_version: Version = Version(0, 0, 0)
generator_version: Version = Version(0, 0, 0)
current_energy_link_value: typing.Optional[int] = None # to display in UI, gets set by server
max_size: int = 16*1024*1024 # 16 MB of max incoming packet size
last_death_link: float = time.time() # last send/received death link on AP layer
@@ -163,10 +271,13 @@ class CommonContext:
server_address: typing.Optional[str]
password: typing.Optional[str]
hint_cost: typing.Optional[int]
hint_points: typing.Optional[int]
player_names: typing.Dict[int, str]
finished_game: bool
ready: bool
team: typing.Optional[int]
slot: typing.Optional[int]
auth: typing.Optional[str]
seed_name: typing.Optional[str]
@@ -179,13 +290,17 @@ class CommonContext:
server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations
locations_info: typing.Dict[int, NetworkItem]
# data storage
stored_data: typing.Dict[str, typing.Any]
stored_data_notification_keys: typing.Set[str]
# internals
# current message box through kvui
_messagebox: typing.Optional["kvui.MessageBox"] = None
# message box reporting a loss of connection
_messagebox_connection_loss: typing.Optional["kvui.MessageBox"] = None
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None:
def __init__(self, server_address: typing.Optional[str] = None, password: typing.Optional[str] = None) -> None:
# server state
self.server_address = server_address
self.username = None
@@ -214,6 +329,9 @@ class CommonContext:
self.server_locations = set() # all locations the server knows of, missing_location | checked_locations
self.locations_info = {}
self.stored_data = {}
self.stored_data_notification_keys = set()
self.input_queue = asyncio.Queue()
self.input_requests = 0
@@ -222,7 +340,11 @@ class CommonContext:
self.exit_event = asyncio.Event()
self.watcher_event = asyncio.Event()
self.item_names = self.NameLookupDict(self, "item")
self.location_names = self.NameLookupDict(self, "location")
self.jsontotextparser = JSONtoTextParser(self)
self.rawjsontotextparser = RawJSONtoTextParser(self)
self.update_data_package(network_data_package)
# execution
@@ -256,6 +378,7 @@ class CommonContext:
self.items_received = []
self.locations_info = {}
self.server_version = Version(0, 0, 0)
self.generator_version = Version(0, 0, 0)
self.server = None
self.server_task = None
self.hint_cost = None
@@ -357,10 +480,13 @@ class CommonContext:
def on_print_json(self, args: dict):
if self.ui:
self.ui.print_json(args["data"])
else:
text = self.jsontotextparser(args["data"])
logger.info(text)
# send copy to UI
self.ui.print_json(copy.deepcopy(args["data"]))
logging.getLogger("FileLog").info(self.rawjsontotextparser(copy.deepcopy(args["data"])),
extra={"NoStream": True})
logging.getLogger("StreamLog").info(self.jsontotextparser(copy.deepcopy(args["data"])),
extra={"NoFile": True})
def on_package(self, cmd: str, args: dict):
"""For custom package handling in subclasses."""
@@ -370,6 +496,11 @@ class CommonContext:
"""Gets called before sending a Say to the server from the user.
Returned text is sent, or sending is aborted if None is returned."""
return text
def on_ui_command(self, text: str) -> None:
"""Gets called by kivy when the user executes a command starting with `/` or `!`.
The command processor is still called; this is just intended for command echoing."""
self.ui.print_json([{"text": text, "type": "color", "color": "orange"}])
def update_permissions(self, permissions: typing.Dict[str, int]):
for permission_name, permission_flag in permissions.items():
@@ -432,28 +563,42 @@ class CommonContext:
or remote_checksum != cache_checksum:
needed_updates.add(game)
else:
self.update_game(cached_game)
self.update_game(cached_game, game)
if needed_updates:
await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}])
await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates])
def update_game(self, game_package: dict):
for item_name, item_id in game_package["item_name_to_id"].items():
self.item_names[item_id] = item_name
for location_name, location_id in game_package["location_name_to_id"].items():
self.location_names[location_id] = location_name
def update_game(self, game_package: dict, game: str):
self.item_names.update_game(game, game_package["item_name_to_id"])
self.location_names.update_game(game, game_package["location_name_to_id"])
def update_data_package(self, data_package: dict):
for game, game_data in data_package["games"].items():
self.update_game(game_data)
self.update_game(game_data, game)
def consume_network_data_package(self, data_package: dict):
self.update_data_package(data_package)
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
current_cache.update(data_package["games"])
Utils.persistent_store("datapackage", "games", current_cache)
logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}")
for game, game_data in data_package["games"].items():
Utils.store_data_package_for_checksum(game, game_data)
# data storage
def set_notify(self, *keys: str) -> None:
"""Subscribe to be notified of changes to selected data storage keys.
The values can be accessed via the "stored_data" attribute of this context, which is a dictionary mapping the
names of the data storage keys to the latest values received from the server.
"""
if new_keys := (set(keys) - self.stored_data_notification_keys):
self.stored_data_notification_keys.update(new_keys)
async_start(self.send_msgs([{"cmd": "Get",
"keys": list(new_keys)},
{"cmd": "SetNotify",
"keys": list(new_keys)}]))
# DeathLink hooks
def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
@@ -576,14 +721,16 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
ctx.username = server_url.username
if server_url.password:
ctx.password = server_url.password
port = server_url.port or 38281
def reconnect_hint() -> str:
return ", type /connect to reconnect" if ctx.server_address else ""
logger.info(f'Connecting to Archipelago server at {address}')
try:
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
port = server_url.port or 38281 # raises ValueError if invalid
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None,
ssl=get_ssl_context() if address.startswith("wss://") else None,
max_size=ctx.max_size)
if ctx.ui is not None:
ctx.ui.update_address_bar(server_url.netloc)
ctx.server = Endpoint(socket)
@@ -598,6 +745,7 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
except websockets.InvalidMessage:
# probably encrypted
if address.startswith("ws://"):
# try wss
await server_loop(ctx, "ws" + address[1:])
else:
ctx.handle_connection_loss(f"Lost connection to the multiworld server due to InvalidMessage"
@@ -642,11 +790,16 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
logger.info('Room Information:')
logger.info('--------------------------------')
version = args["version"]
ctx.server_version = tuple(version)
version = ".".join(str(item) for item in version)
ctx.server_version = Version(*version)
logger.info(f'Server protocol version: {version}')
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
if "generator_version" in args:
ctx.generator_version = Version(*args["generator_version"])
logger.info(f'Server protocol version: {ctx.server_version.as_simple_string()}, '
f'generator version: {ctx.generator_version.as_simple_string()}, '
f'tags: {", ".join(args["tags"])}')
else:
logger.info(f'Server protocol version: {ctx.server_version.as_simple_string()}, '
f'tags: {", ".join(args["tags"])}')
if args['password']:
logger.info('Password required')
ctx.update_permissions(args.get("permissions", {}))
@@ -679,17 +832,19 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
await ctx.server_auth(args['password'])
elif cmd == 'DataPackage':
logger.info("Got new ID/Name DataPackage")
ctx.consume_network_data_package(args['data'])
elif cmd == 'ConnectionRefused':
errors = args["errors"]
if 'InvalidSlot' in errors:
ctx.disconnected_intentionally = True
ctx.event_invalid_slot()
elif 'InvalidGame' in errors:
ctx.disconnected_intentionally = True
ctx.event_invalid_game()
elif 'IncompatibleVersion' in errors:
raise Exception('Server reported your client version as incompatible')
raise Exception('Server reported your client version as incompatible. '
'This probably means you have to update.')
elif 'InvalidItemsHandling' in errors:
raise Exception('The item handling flags requested by the client are not supported')
# last to check, recoverable problem
@@ -708,7 +863,9 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.slot = args["slot"]
# int keys get lost in JSON transfer
ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()}
ctx.hint_points = args.get("hint_points", 0)
ctx.consume_players_package(args["players"])
ctx.stored_data_notification_keys.add(f"_read_hints_{ctx.team}_{ctx.slot}")
msgs = []
if ctx.locations_checked:
msgs.append({"cmd": "LocationChecks",
@@ -716,6 +873,11 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
if ctx.locations_scouted:
msgs.append({"cmd": "LocationScouts",
"locations": list(ctx.locations_scouted)})
if ctx.stored_data_notification_keys:
msgs.append({"cmd": "Get",
"keys": list(ctx.stored_data_notification_keys)})
msgs.append({"cmd": "SetNotify",
"keys": list(ctx.stored_data_notification_keys)})
if msgs:
await ctx.send_msgs(msgs)
if ctx.finished_game:
@@ -779,8 +941,17 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
# we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this
if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]:
ctx.on_deathlink(args["data"])
elif cmd == "Retrieved":
ctx.stored_data.update(args["keys"])
if ctx.ui and f"_read_hints_{ctx.team}_{ctx.slot}" in args["keys"]:
ctx.ui.update_hints()
elif cmd == "SetReply":
if args["key"] == "EnergyLink":
ctx.stored_data[args["key"]] = args["value"]
if ctx.ui and f"_read_hints_{ctx.team}_{ctx.slot}" == args["key"]:
ctx.ui.update_hints()
elif args["key"].startswith("EnergyLink"):
ctx.current_energy_link_value = args["value"]
if ctx.ui:
ctx.ui.set_new_energy_link_value()
@@ -820,11 +991,10 @@ def get_base_parser(description: typing.Optional[str] = None):
return parser
if __name__ == '__main__':
# Text Mode to use !hint and such with games that have no text entry
def run_as_textclient():
class TextContext(CommonContext):
tags = {"AP", "TextOnly"}
# Text Mode to use !hint and such with games that have no text entry
tags = CommonContext.tags | {"TextOnly"}
game = "" # empty matches any game since 0.3.2
items_handling = 0b111 # receive all items for /received
want_slot_data = False # Can't use game specific slot_data
@@ -838,12 +1008,11 @@ if __name__ == '__main__':
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.game = self.slot_info[self.slot].game
async def disconnect(self, allow_autoreconnect: bool = False):
self.game = ""
await super().disconnect(allow_autoreconnect)
async def main(args):
ctx = TextContext(args.connect, args.password)
ctx.auth = args.name
@@ -856,7 +1025,6 @@ if __name__ == '__main__':
await ctx.exit_event.wait()
await ctx.shutdown()
import colorama
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
@@ -876,3 +1044,8 @@ if __name__ == '__main__':
asyncio.run(main(args))
colorama.deinit()
if __name__ == '__main__':
logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING
run_as_textclient()

View File

@@ -13,9 +13,9 @@ from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandP
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart ff1_connector.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure ff1_connector.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart ff1_connector.lua"
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_ff1.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure connector_ff1.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_ff1.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
@@ -33,7 +33,7 @@ class FF1CommandProcessor(ClientCommandProcessor):
logger.info(f"NES Status: {self.ctx.nes_status}")
def _cmd_toggle_msgs(self):
"""Toggle displaying messages in bizhawk"""
"""Toggle displaying messages in EmuHawk"""
global DISPLAY_MSGS
DISPLAY_MSGS = not DISPLAY_MSGS
logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}")

View File

@@ -1,553 +1,12 @@
from __future__ import annotations
import os
import logging
import json
import string
import copy
import re
import subprocess
import sys
import time
import random
import typing
import ModuleUpdate
ModuleUpdate.update()
import factorio_rcon
import colorama
import asyncio
from queue import Queue
from worlds.factorio.Client import check_stdin, launch
import Utils
def check_stdin() -> None:
if Utils.is_windows and sys.stdin:
print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.")
if __name__ == "__main__":
Utils.init_logging("FactorioClient", exception_logger="Client")
check_stdin()
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, logger, gui_enabled, get_base_parser
from MultiServer import mark_raw
from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
from Utils import async_start
from worlds.factorio import Factorio
class FactorioCommandProcessor(ClientCommandProcessor):
ctx: FactorioContext
def _cmd_energy_link(self):
"""Print the status of the energy link."""
self.output(f"Energy Link: {self.ctx.energy_link_status}")
@mark_raw
def _cmd_factorio(self, text: str) -> bool:
"""Send the following command to the bound Factorio Server."""
if self.ctx.rcon_client:
# TODO: Print the command non-silently only for race seeds, or otherwise block anything but /factorio /save in race seeds.
self.ctx.print_to_game(f"/factorio {text}")
result = self.ctx.rcon_client.send_command(text)
if result:
self.output(result)
return True
return False
def _cmd_resync(self):
"""Manually trigger a resync."""
self.ctx.awaiting_bridge = True
def _cmd_toggle_send_filter(self):
"""Toggle filtering of item sends that get displayed in-game to only those that involve you."""
self.ctx.toggle_filter_item_sends()
def _cmd_toggle_chat(self):
"""Toggle sending of chat messages from players on the Factorio server to Archipelago."""
self.ctx.toggle_bridge_chat_out()
class FactorioContext(CommonContext):
command_processor = FactorioCommandProcessor
game = "Factorio"
items_handling = 0b111 # full remote
# updated by spinup server
mod_version: Utils.Version = Utils.Version(0, 0, 0)
def __init__(self, server_address, password):
super(FactorioContext, self).__init__(server_address, password)
self.send_index: int = 0
self.rcon_client = None
self.awaiting_bridge = False
self.write_data_path = None
self.death_link_tick: int = 0 # last send death link on Factorio layer
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
self.energy_link_increment = 0
self.last_deplete = 0
self.filter_item_sends: bool = False
self.multiplayer: bool = False # whether multiple different players have connected
self.bridge_chat_out: bool = True
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(FactorioContext, self).server_auth(password_requested)
if self.rcon_client:
await get_info(self, self.rcon_client) # retrieve current auth code
else:
raise Exception("Cannot connect to a server with unknown own identity, "
"bridge to Factorio first.")
await self.send_connect()
def on_print(self, args: dict):
super(FactorioContext, self).on_print(args)
if self.rcon_client:
if not args['text'].startswith(self.player_names[self.slot] + ":"):
self.print_to_game(args['text'])
def on_print_json(self, args: dict):
if self.rcon_client:
if (not self.filter_item_sends or not self.is_uninteresting_item_send(args)) \
and not self.is_echoed_chat(args):
text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
if not text.startswith(self.player_names[self.slot] + ":"): # TODO: Remove string heuristic in the future.
self.print_to_game(text)
super(FactorioContext, self).on_print_json(args)
@property
def savegame_name(self) -> str:
return f"AP_{self.seed_name}_{self.auth}_Save.zip"
def print_to_game(self, text):
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
f"{text}")
@property
def energy_link_status(self) -> str:
if not self.energy_link_increment:
return "Disabled"
elif self.current_energy_link_value is None:
return "Standby"
else:
return f"{Utils.format_SI_prefix(self.current_energy_link_value)}J"
def on_deathlink(self, data: dict):
if self.rcon_client:
self.rcon_client.send_command(f"/ap-deathlink {data['source']}")
super(FactorioContext, self).on_deathlink(data)
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected", "RoomUpdate"}:
# catch up sync anything that is already cleared.
if "checked_locations" in args and args["checked_locations"]:
self.rcon_client.send_commands({item_name: f'/ap-get-technology ap-{item_name}-\t-1' for
item_name in args["checked_locations"]})
if cmd == "Connected" and self.energy_link_increment:
async_start(self.send_msgs([{
"cmd": "SetNotify", "keys": ["EnergyLink"]
}]))
elif cmd == "SetReply":
if args["key"] == "EnergyLink":
if self.energy_link_increment and args.get("last_deplete", -1) == self.last_deplete:
# it's our deplete request
gained = int(args["original_value"] - args["value"])
gained_text = Utils.format_SI_prefix(gained) + "J"
if gained:
logger.debug(f"EnergyLink: Received {gained_text}. "
f"{Utils.format_SI_prefix(args['value'])}J remaining.")
self.rcon_client.send_command(f"/ap-energylink {gained}")
def on_user_say(self, text: str) -> typing.Optional[str]:
# Mirror chat sent from the UI to the Factorio server.
self.print_to_game(f"{self.player_names[self.slot]}: {text}")
return text
async def chat_from_factorio(self, user: str, message: str) -> None:
if not self.bridge_chat_out:
return
# Pass through commands
if message.startswith("!"):
await self.send_msgs([{"cmd": "Say", "text": message}])
return
# Omit messages that contain local coordinates
if "[gps=" in message:
return
prefix = f"({user}) " if self.multiplayer else ""
await self.send_msgs([{"cmd": "Say", "text": f"{prefix}{message}"}])
def toggle_filter_item_sends(self) -> None:
self.filter_item_sends = not self.filter_item_sends
if self.filter_item_sends:
announcement = "Item sends are now filtered."
else:
announcement = "Item sends are no longer filtered."
logger.info(announcement)
self.print_to_game(announcement)
def toggle_bridge_chat_out(self) -> None:
self.bridge_chat_out = not self.bridge_chat_out
if self.bridge_chat_out:
announcement = "Chat is now bridged to Archipelago."
else:
announcement = "Chat is no longer bridged to Archipelago."
logger.info(announcement)
self.print_to_game(announcement)
def run_gui(self):
from kvui import GameManager
class FactorioManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("FactorioServer", "Factorio Server Log"),
("FactorioWatcher", "Bridge Data Log"),
]
base_title = "Archipelago Factorio Client"
self.ui = FactorioManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def game_watcher(ctx: FactorioContext):
bridge_logger = logging.getLogger("FactorioWatcher")
next_bridge = time.perf_counter() + 1
try:
while not ctx.exit_event.is_set():
# TODO: restore on-demand refresh
if ctx.rcon_client and time.perf_counter() > next_bridge:
next_bridge = time.perf_counter() + 1
ctx.awaiting_bridge = False
data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
if not ctx.auth:
pass # auth failed, wait for new attempt
elif data["slot_name"] != ctx.auth:
bridge_logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}")
elif data["seed_name"] != ctx.seed_name:
bridge_logger.warning(
f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}")
else:
data = data["info"]
research_data = data["research_done"]
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
victory = data["victory"]
await ctx.update_death_link(data["death_link"])
ctx.multiplayer = data.get("multiplayer", False)
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if ctx.locations_checked != research_data:
bridge_logger.debug(
f"New researches done: "
f"{[ctx.location_names[rid] for rid in research_data - ctx.locations_checked]}")
ctx.locations_checked = research_data
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
death_link_tick = data.get("death_link_tick", 0)
if death_link_tick != ctx.death_link_tick:
ctx.death_link_tick = death_link_tick
if "DeathLink" in ctx.tags:
async_start(ctx.send_death())
if ctx.energy_link_increment:
in_world_bridges = data["energy_bridges"]
if in_world_bridges:
in_world_energy = data["energy"]
if in_world_energy < (ctx.energy_link_increment * in_world_bridges):
# attempt to refill
ctx.last_deplete = time.time()
async_start(ctx.send_msgs([{
"cmd": "Set", "key": "EnergyLink", "operations":
[{"operation": "add", "value": -ctx.energy_link_increment * in_world_bridges},
{"operation": "max", "value": 0}],
"last_deplete": ctx.last_deplete
}]))
# Above Capacity - (len(Bridges) * ENERGY_INCREMENT)
elif in_world_energy > (in_world_bridges * ctx.energy_link_increment * 5) - \
ctx.energy_link_increment*in_world_bridges:
value = ctx.energy_link_increment * in_world_bridges
async_start(ctx.send_msgs([{
"cmd": "Set", "key": "EnergyLink", "operations":
[{"operation": "add", "value": value}]
}]))
ctx.rcon_client.send_command(
f"/ap-energylink -{value}")
logger.debug(f"EnergyLink: Sent {Utils.format_SI_prefix(value)}J")
await asyncio.sleep(0.1)
except Exception as e:
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
def stream_factorio_output(pipe, queue, process):
pipe.reconfigure(errors="replace")
def queuer():
while process.poll() is None:
text = pipe.readline().strip()
if text:
queue.put_nowait(text)
from threading import Thread
thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True)
thread.start()
return thread
async def factorio_server_watcher(ctx: FactorioContext):
savegame_name = os.path.abspath(ctx.savegame_name)
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
subprocess.run((
executable, "--create", savegame_name, "--preset", "archipelago"
))
factorio_process = subprocess.Popen((executable, "--start-server", ctx.savegame_name,
*(str(elem) for elem in server_args)),
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL,
encoding="utf-8")
factorio_server_logger.info("Started Factorio Server")
factorio_queue = Queue()
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
try:
while not ctx.exit_event.is_set():
if factorio_process.poll() is not None:
factorio_server_logger.info("Factorio server has exited.")
ctx.exit_event.set()
while not factorio_queue.empty():
msg = factorio_queue.get()
factorio_queue.task_done()
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
if not ctx.server:
logger.info("Established bridge to Factorio Server. "
"Ready to connect to Archipelago via /connect")
check_stdin()
if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
ctx.awaiting_bridge = True
factorio_server_logger.debug(msg)
elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command energy-link$", msg):
factorio_server_logger.debug(msg)
ctx.print_to_game(f"Energy Link: {ctx.energy_link_status}")
elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command toggle-ap-send-filter$", msg):
factorio_server_logger.debug(msg)
ctx.toggle_filter_item_sends()
elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command toggle-ap-chat$", msg):
factorio_server_logger.debug(msg)
ctx.toggle_bridge_chat_out()
else:
factorio_server_logger.info(msg)
match = re.match(r"^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \[CHAT\] ([^:]+): (.*)$", msg)
if match:
await ctx.chat_from_factorio(match.group(1), match.group(2))
if ctx.rcon_client:
commands = {}
while ctx.send_index < len(ctx.items_received):
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
item_id = transfer_item.item
player_name = ctx.player_names[transfer_item.player]
if item_id not in Factorio.item_id_to_name:
factorio_server_logger.error(f"Cannot send unknown item ID: {item_id}")
else:
item_name = Factorio.item_id_to_name[item_id]
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
commands[ctx.send_index] = f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}'
ctx.send_index += 1
if commands:
ctx.rcon_client.send_commands(commands)
await asyncio.sleep(0.1)
except Exception as e:
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
ctx.exit_event.set()
finally:
if factorio_process.poll() is not None:
if ctx.rcon_client:
ctx.rcon_client.close()
ctx.rcon_client = None
return
sent_quit = False
if ctx.rcon_client:
# Attempt clean quit through RCON.
try:
ctx.rcon_client.send_command("/quit")
except factorio_rcon.RCONNetworkError:
pass
else:
sent_quit = True
ctx.rcon_client.close()
ctx.rcon_client = None
if not sent_quit:
# Attempt clean quit using SIGTERM. (Note that on Windows this kills the process instead.)
factorio_process.terminate()
try:
factorio_process.wait(10)
except subprocess.TimeoutExpired:
factorio_process.kill()
async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient):
info = json.loads(rcon_client.send_command("/ap-rcon-info"))
ctx.auth = info["slot_name"]
ctx.seed_name = info["seed_name"]
# 0.2.0 addition, not present earlier
death_link = bool(info.get("death_link", False))
ctx.energy_link_increment = info.get("energy_link", 0)
logger.debug(f"Energy Link Increment: {ctx.energy_link_increment}")
if ctx.energy_link_increment and ctx.ui:
ctx.ui.enable_energy_link()
await ctx.update_death_link(death_link)
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
savegame_name = os.path.abspath("Archipelago.zip")
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
subprocess.run((
executable, "--create", savegame_name
))
factorio_process = subprocess.Popen(
(executable, "--start-server", savegame_name, *(str(elem) for elem in server_args)),
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL,
encoding="utf-8")
factorio_server_logger.info("Started Information Exchange Factorio Server")
factorio_queue = Queue()
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
rcon_client = None
try:
while not ctx.auth:
while not factorio_queue.empty():
msg = factorio_queue.get()
factorio_server_logger.info(msg)
if "Loading mod AP-" in msg and msg.endswith("(data.lua)"):
parts = msg.split()
ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split(".")))
elif "Write data path: " in msg:
ctx.write_data_path = Utils.get_text_between(msg, "Write data path: ", " [")
if "AppData" in ctx.write_data_path:
logger.warning("It appears your mods are loaded from Appdata, "
"this can lead to problems with multiple Factorio instances. "
"If this is the case, you will get a file locked error running Factorio.")
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
if ctx.mod_version == ctx.__class__.mod_version:
raise Exception("No Archipelago mod was loaded. Aborting.")
await get_info(ctx, rcon_client)
await asyncio.sleep(0.01)
except Exception as e:
logger.exception(e, extra={"compact_gui": True})
msg = "Aborted Factorio Server Bridge"
logger.error(msg)
ctx.gui_error(msg, e)
ctx.exit_event.set()
else:
logger.info(
f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
return True
finally:
factorio_process.terminate()
factorio_process.wait(5)
return False
async def main(args):
ctx = FactorioContext(args.connect, args.password)
ctx.filter_item_sends = initial_filter_item_sends
ctx.bridge_chat_out = initial_bridge_chat_out
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
factorio_server_task = asyncio.create_task(factorio_spinup_server(ctx), name="FactorioSpinupServer")
successful_launch = await factorio_server_task
if successful_launch:
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="FactorioProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await factorio_server_task
await ctx.shutdown()
class FactorioJSONtoTextParser(JSONtoTextParser):
def _handle_color(self, node: JSONMessagePart):
colors = node["color"].split(";")
for color in colors:
if color in self.color_codes:
node["text"] = f"[color=#{self.color_codes[color]}]{node['text']}[/color]"
return self._handle_text(node)
return self._handle_text(node)
if __name__ == '__main__':
parser = get_base_parser(description="Optional arguments to FactorioClient follow. "
"Remaining arguments get passed into bound Factorio instance."
"Refer to Factorio --help for those.")
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
parser.add_argument('--server-settings', help='Factorio server settings configuration file.')
args, rest = parser.parse_known_args()
colorama.init()
rcon_port = args.rcon_port
rcon_password = args.rcon_password if args.rcon_password else ''.join(
random.choice(string.ascii_letters) for x in range(32))
factorio_server_logger = logging.getLogger("FactorioServer")
options = Utils.get_options()
executable = options["factorio_options"]["executable"]
server_settings = args.server_settings if args.server_settings else options["factorio_options"].get("server_settings", None)
if server_settings:
server_settings = os.path.abspath(server_settings)
if not isinstance(options["factorio_options"]["filter_item_sends"], bool):
logging.warning(f"Warning: Option filter_item_sends should be a bool.")
initial_filter_item_sends = bool(options["factorio_options"]["filter_item_sends"])
if not isinstance(options["factorio_options"]["bridge_chat_out"], bool):
logging.warning(f"Warning: Option bridge_chat_out should be a bool.")
initial_bridge_chat_out = bool(options["factorio_options"]["bridge_chat_out"])
if not os.path.exists(os.path.dirname(executable)):
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
if os.path.isdir(executable): # user entered a path to a directory, let's find the executable therein
executable = os.path.join(executable, "factorio")
if not os.path.isfile(executable):
if os.path.isfile(executable + ".exe"):
executable = executable + ".exe"
else:
raise FileNotFoundError(f"Path {executable} is not an executable file.")
if server_settings and os.path.isfile(server_settings):
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, "--server-settings", server_settings, *rest)
else:
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest)
asyncio.run(main(args))
colorama.deinit()
launch()

462
Fill.py
View File

@@ -1,10 +1,11 @@
import logging
import typing
import collections
import itertools
import logging
import typing
from collections import Counter, deque
from BaseClasses import CollectionState, Location, LocationProgressType, MultiWorld, Item, ItemClassification
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
from Options import Accessibility
from worlds.AutoWorld import call_all
from worlds.generic.Rules import add_item_rule
@@ -14,48 +15,62 @@ class FillError(RuntimeError):
pass
def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple()) -> CollectionState:
def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
logging.info(f"Current fill step ({name}) at {placed}/{total_items} items placed.")
def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple(),
locations: typing.Optional[typing.List[Location]] = None) -> CollectionState:
new_state = base_state.copy()
for item in itempool:
new_state.collect(item, True)
new_state.sweep_for_events()
new_state.sweep_for_events(locations=locations)
return new_state
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
allow_partial: bool = False, allow_excluded: bool = False) -> None:
allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None:
"""
:param world: Multiworld to be filled.
:param multiworld: Multiworld to be filled.
:param base_state: State assumed before fill.
:param locations: Locations to be filled with item_pool
:param item_pool: Items to fill into the locations
:param locations: Locations to be filled with item_pool, gets mutated by removing locations that get filled.
:param item_pool: Items to fill into the locations, gets mutated by removing items that get placed.
:param single_player_placement: if true, can speed up placement if everything belongs to a single player
:param lock: locations are set to locked as they are filled
:param swap: if true, swaps of already place items are done in the event of a dead end
:param on_place: callback that is called when a placement happens
:param allow_partial: only place what is possible. Remaining items will be in the item_pool list.
:param allow_excluded: if true and placement fails, it is re-attempted while ignoring excluded on Locations
:param name: name of this fill step for progress logging purposes
"""
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
cleanup_required = False
swapped_items: typing.Counter[typing.Tuple[int, str, bool]] = Counter()
reachable_items: typing.Dict[int, typing.Deque[Item]] = {}
for item in item_pool:
reachable_items.setdefault(item.player, deque()).append(item)
# for progress logging
total = min(len(item_pool), len(locations))
placed = 0
while any(reachable_items.values()) and locations:
# grab one item per player
items_to_place = [items.pop()
for items in reachable_items.values() if items]
for item in items_to_place:
item_pool.remove(item)
for p, pool_item in enumerate(item_pool):
if pool_item is item:
item_pool.pop(p)
break
maximum_exploration_state = sweep_from_pool(
base_state, item_pool + unplaced_items)
base_state, item_pool + unplaced_items, multiworld.get_filled_locations(item.player)
if single_player_placement else None)
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
has_beaten_game = multiworld.has_beaten_game(maximum_exploration_state)
while items_to_place:
# if we have run out of locations to fill,break out of this loop
@@ -67,8 +82,8 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
spot_to_fill: typing.Optional[Location] = None
# if minimal accessibility, only check whether location is reachable if game not beatable
if world.accessibility[item_to_place.player] == 'minimal':
perform_access_check = not world.has_beaten_game(maximum_exploration_state,
if multiworld.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
perform_access_check = not multiworld.has_beaten_game(maximum_exploration_state,
item_to_place.player) \
if single_player_placement else not has_beaten_game
else:
@@ -85,32 +100,37 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
else:
# we filled all reachable spots.
if swap:
# try swapping this item with previously placed items
for (i, location) in enumerate(placements):
# try swapping this item with previously placed items in a safe way then in an unsafe way
swap_attempts = ((i, location, unsafe)
for unsafe in (False, True)
for i, location in enumerate(placements))
for (i, location, unsafe) in swap_attempts:
placed_item = location.item
# Unplaceable items can sometimes be swapped infinitely. Limit the
# number of times we will swap an individual item to prevent this
swap_count = swapped_items[placed_item.player,
placed_item.name]
swap_count = swapped_items[placed_item.player, placed_item.name, unsafe]
if swap_count > 1:
continue
location.item = None
placed_item.location = None
swap_state = sweep_from_pool(base_state, [placed_item])
# swap_state assumes we can collect placed item before item_to_place
swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool,
multiworld.get_filled_locations(item.player)
if single_player_placement else None)
# unsafe means swap_state assumes we can somehow collect placed_item before item_to_place
# by continuing to swap, which is not guaranteed. This is unsafe because there is no mechanic
# to clean that up later, so there is a chance generation fails.
if (not single_player_placement or location.player == item_to_place.player) \
and location.can_fill(swap_state, item_to_place, perform_access_check):
# Verify that placing this item won't reduce available locations, which could happen with rules
# that want to not have both items. Left in until removal is proven useful.
# Verify placing this item won't reduce available locations, which would be a useless swap.
prev_state = swap_state.copy()
prev_loc_count = len(
world.get_reachable_locations(prev_state))
multiworld.get_reachable_locations(prev_state))
swap_state.collect(item_to_place, True)
new_loc_count = len(
world.get_reachable_locations(swap_state))
multiworld.get_reachable_locations(swap_state))
if new_loc_count >= prev_loc_count:
# Add this item to the existing placement, and
@@ -118,13 +138,15 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
spot_to_fill = placements.pop(i)
swap_count += 1
swapped_items[placed_item.player,
placed_item.name] = swap_count
swapped_items[placed_item.player, placed_item.name, unsafe] = swap_count
reachable_items[placed_item.player].appendleft(
placed_item)
item_pool.append(placed_item)
# cleanup at the end to hopefully get better errors
cleanup_required = True
break
# Item can't be placed here, restore original item
@@ -138,13 +160,30 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
else:
unplaced_items.append(item_to_place)
continue
world.push_item(spot_to_fill, item_to_place, False)
multiworld.push_item(spot_to_fill, item_to_place, False)
spot_to_fill.locked = lock
placements.append(spot_to_fill)
spot_to_fill.event = item_to_place.advancement
placed += 1
if not placed % 1000:
_log_fill_progress(name, placed, total)
if on_place:
on_place(spot_to_fill)
if total > 1000:
_log_fill_progress(name, placed, total)
if cleanup_required:
# validate all placements and remove invalid ones
state = sweep_from_pool(
base_state, [], multiworld.get_filled_locations(item.player)
if single_player_placement else None)
for placement in placements:
if multiworld.worlds[placement.item.player].options.accessibility != "minimal" and not placement.can_reach(state):
placement.item.location = None
unplaced_items.append(placement.item)
placement.item = None
locations.append(placement)
if allow_excluded:
# check if partial fill is the result of excluded locations, in which case retry
excluded_locations = [
@@ -154,7 +193,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
if excluded_locations:
for location in excluded_locations:
location.progress_type = location.progress_type.DEFAULT
fill_restrictive(world, base_state, excluded_locations, unplaced_items, single_player_placement, lock,
fill_restrictive(multiworld, base_state, excluded_locations, unplaced_items, single_player_placement, lock,
swap, on_place, allow_partial, False)
for location in excluded_locations:
if not location.item:
@@ -162,22 +201,32 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0:
# There are leftover unplaceable items and locations that won't accept them
if world.can_beat_game():
if multiworld.can_beat_game():
logging.warning(
f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})')
f"Not all items placed. Game beatable anyway.\nCould not place:\n"
f"{', '.join(str(item) for item in unplaced_items)}")
else:
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
f"Unplaced items:\n"
f"{', '.join(str(item) for item in unplaced_items)}\n"
f"Unfilled locations:\n"
f"{', '.join(str(location) for location in locations)}\n"
f"Already placed {len(placements)}:\n"
f"{', '.join(str(place) for place in placements)}")
item_pool.extend(unplaced_items)
def remaining_fill(world: MultiWorld,
def remaining_fill(multiworld: MultiWorld,
locations: typing.List[Location],
itempool: typing.List[Item]) -> None:
itempool: typing.List[Item],
name: str = "Remaining",
move_unplaceable_to_start_inventory: bool = False) -> None:
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
total = min(len(itempool), len(locations))
placed = 0
while locations and itempool:
item_to_place = itempool.pop()
spot_to_fill: typing.Optional[Location] = None
@@ -225,30 +274,49 @@ def remaining_fill(world: MultiWorld,
unplaced_items.append(item_to_place)
continue
world.push_item(spot_to_fill, item_to_place, False)
multiworld.push_item(spot_to_fill, item_to_place, False)
placements.append(spot_to_fill)
placed += 1
if not placed % 1000:
_log_fill_progress(name, placed, total)
if total > 1000:
_log_fill_progress(name, placed, total)
if unplaced_items and locations:
# There are leftover unplaceable items and locations that won't accept them
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
if move_unplaceable_to_start_inventory:
last_batch = []
for item in unplaced_items:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
multiworld.push_precollected(item)
last_batch.append(multiworld.worlds[item.player].create_filler())
remaining_fill(multiworld, locations, unplaced_items, name + " Start Inventory Retry")
else:
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
f"Unplaced items:\n"
f"{', '.join(str(item) for item in unplaced_items)}\n"
f"Unfilled locations:\n"
f"{', '.join(str(location) for location in locations)}\n"
f"Already placed {len(placements)}:\n"
f"{', '.join(str(place) for place in placements)}")
itempool.extend(unplaced_items)
def fast_fill(world: MultiWorld,
def fast_fill(multiworld: MultiWorld,
item_pool: typing.List[Item],
fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]:
placing = min(len(item_pool), len(fill_locations))
for item, location in zip(item_pool, fill_locations):
world.push_item(location, item, False)
multiworld.push_item(location, item, False)
return item_pool[placing:], fill_locations[placing:]
def accessibility_corrections(world: MultiWorld, state: CollectionState, locations, pool=[]):
def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, locations, pool=[]):
maximum_exploration_state = sweep_from_pool(state, pool)
minimal_players = {player for player in world.player_ids if world.accessibility[player] == "minimal"}
unreachable_locations = [location for location in world.get_locations() if location.player in minimal_players and
minimal_players = {player for player in multiworld.player_ids if multiworld.worlds[player].options.accessibility == "minimal"}
unreachable_locations = [location for location in multiworld.get_locations() if location.player in minimal_players and
not location.can_reach(maximum_exploration_state)]
for location in unreachable_locations:
if (location.item is not None and location.item.advancement and location.address is not None and not
@@ -256,42 +324,41 @@ def accessibility_corrections(world: MultiWorld, state: CollectionState, locatio
pool.append(location.item)
state.remove(location.item)
location.item = None
location.event = False
if location in state.events:
state.events.remove(location)
locations.append(location)
if pool and locations:
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
fill_restrictive(world, state, locations, pool)
fill_restrictive(multiworld, state, locations, pool, name="Accessibility Corrections")
def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations):
def inaccessible_location_rules(multiworld: MultiWorld, state: CollectionState, locations):
maximum_exploration_state = sweep_from_pool(state)
unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)]
if unreachable_locations:
def forbid_important_item_rule(item: Item):
return not ((item.classification & 0b0011) and world.accessibility[item.player] != 'minimal')
return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != 'minimal')
for location in unreachable_locations:
add_item_rule(location, forbid_important_item_rule)
def distribute_early_items(world: MultiWorld,
def distribute_early_items(multiworld: MultiWorld,
fill_locations: typing.List[Location],
itempool: typing.List[Item]) -> typing.Tuple[typing.List[Location], typing.List[Item]]:
""" returns new fill_locations and itempool """
early_items_count: typing.Dict[typing.Tuple[str, int], typing.List[int]] = {}
for player in world.player_ids:
items = itertools.chain(world.early_items[player], world.local_early_items[player])
for player in multiworld.player_ids:
items = itertools.chain(multiworld.early_items[player], multiworld.local_early_items[player])
for item in items:
early_items_count[item, player] = [world.early_items[player].get(item, 0),
world.local_early_items[player].get(item, 0)]
early_items_count[item, player] = [multiworld.early_items[player].get(item, 0),
multiworld.local_early_items[player].get(item, 0)]
if early_items_count:
early_locations: typing.List[Location] = []
early_priority_locations: typing.List[Location] = []
loc_indexes_to_remove: typing.Set[int] = set()
base_state = world.state.copy()
base_state.sweep_for_events(locations=(loc for loc in world.get_filled_locations() if loc.address is None))
base_state = multiworld.state.copy()
base_state.sweep_for_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
for i, loc in enumerate(fill_locations):
if loc.can_reach(base_state):
if loc.progress_type == LocationProgressType.PRIORITY:
@@ -303,8 +370,8 @@ def distribute_early_items(world: MultiWorld,
early_prog_items: typing.List[Item] = []
early_rest_items: typing.List[Item] = []
early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids}
early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids}
item_indexes_to_remove: typing.Set[int] = set()
for i, item in enumerate(itempool):
if (item.name, item.player) in early_items_count:
@@ -328,27 +395,29 @@ def distribute_early_items(world: MultiWorld,
if len(early_items_count) == 0:
break
itempool = [item for i, item in enumerate(itempool) if i not in item_indexes_to_remove]
for player in world.player_ids:
for player in multiworld.player_ids:
player_local = early_local_rest_items[player]
fill_restrictive(world, base_state,
fill_restrictive(multiworld, base_state,
[loc for loc in early_locations if loc.player == player],
player_local, lock=True, allow_partial=True)
player_local, lock=True, allow_partial=True, name=f"Local Early Items P{player}")
if player_local:
logging.warning(f"Could not fulfill rules of early items: {player_local}")
early_rest_items.extend(early_local_rest_items[player])
early_locations = [loc for loc in early_locations if not loc.item]
fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True)
fill_restrictive(multiworld, base_state, early_locations, early_rest_items, lock=True, allow_partial=True,
name="Early Items")
early_locations += early_priority_locations
for player in world.player_ids:
for player in multiworld.player_ids:
player_local = early_local_prog_items[player]
fill_restrictive(world, base_state,
fill_restrictive(multiworld, base_state,
[loc for loc in early_locations if loc.player == player],
player_local, lock=True, allow_partial=True)
player_local, lock=True, allow_partial=True, name=f"Local Early Progression P{player}")
if player_local:
logging.warning(f"Could not fulfill rules of early items: {player_local}")
early_prog_items.extend(player_local)
early_locations = [loc for loc in early_locations if not loc.item]
fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True)
fill_restrictive(multiworld, base_state, early_locations, early_prog_items, lock=True, allow_partial=True,
name="Early Progression")
unplaced_early_items = early_rest_items + early_prog_items
if unplaced_early_items:
logging.warning("Ran out of early locations for early items. Failed to place "
@@ -356,18 +425,19 @@ def distribute_early_items(world: MultiWorld,
itempool += unplaced_early_items
fill_locations.extend(early_locations)
world.random.shuffle(fill_locations)
multiworld.random.shuffle(fill_locations)
return fill_locations, itempool
def distribute_items_restrictive(world: MultiWorld) -> None:
fill_locations = sorted(world.get_unfilled_locations())
world.random.shuffle(fill_locations)
def distribute_items_restrictive(multiworld: MultiWorld,
panic_method: typing.Literal["swap", "raise", "start_inventory"] = "swap") -> None:
fill_locations = sorted(multiworld.get_unfilled_locations())
multiworld.random.shuffle(fill_locations)
# get items to distribute
itempool = sorted(world.itempool)
world.random.shuffle(itempool)
itempool = sorted(multiworld.itempool)
multiworld.random.shuffle(itempool)
fill_locations, itempool = distribute_early_items(world, fill_locations, itempool)
fill_locations, itempool = distribute_early_items(multiworld, fill_locations, itempool)
progitempool: typing.List[Item] = []
usefulitempool: typing.List[Item] = []
@@ -381,7 +451,7 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
else:
filleritempool.append(item)
call_all(world, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
call_all(multiworld, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
loc_type: [] for loc_type in LocationProgressType}
@@ -402,74 +472,104 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
if prioritylocations:
# "priority fill"
fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking)
accessibility_corrections(world, world.state, prioritylocations, progitempool)
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking,
name="Priority")
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations
if progitempool:
# "progression fill"
fill_restrictive(world, world.state, defaultlocations, progitempool)
# "advancement/progression fill"
if panic_method == "swap":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=True,
name="Progression", single_player_placement=multiworld.players == 1)
elif panic_method == "raise":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=False,
name="Progression", single_player_placement=multiworld.players == 1)
elif panic_method == "start_inventory":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=False, allow_partial=True,
name="Progression", single_player_placement=multiworld.players == 1)
if progitempool:
for item in progitempool:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
multiworld.push_precollected(item)
filleritempool.append(multiworld.worlds[item.player].create_filler())
logging.warning(f"{len(progitempool)} items moved to start inventory,"
f" due to failure in Progression fill step.")
progitempool[:] = []
else:
raise ValueError(f"Generator Panic Method {panic_method} not recognized.")
if progitempool:
raise FillError(
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
accessibility_corrections(world, world.state, defaultlocations)
f"Not enough locations for progression items. "
f"There are {len(progitempool)} more progression items than there are available locations."
)
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
for location in lock_later:
if location.item:
location.locked = True
del mark_for_locking, lock_later
inaccessible_location_rules(world, world.state, defaultlocations)
inaccessible_location_rules(multiworld, multiworld.state, defaultlocations)
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded",
move_unplaceable_to_start_inventory=panic_method=="start_inventory")
remaining_fill(world, excludedlocations, filleritempool)
if excludedlocations:
raise FillError(
f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
f"Not enough filler items for excluded locations. "
f"There are {len(excludedlocations)} more excluded locations than filler or trap items."
)
restitempool = usefulitempool + filleritempool
restitempool = filleritempool + usefulitempool
remaining_fill(world, defaultlocations, restitempool)
remaining_fill(multiworld, defaultlocations, restitempool,
move_unplaceable_to_start_inventory=panic_method=="start_inventory")
unplaced = restitempool
unfilled = defaultlocations
if unplaced or unfilled:
logging.warning(
f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
items_counter = Counter(location.item.player for location in world.get_locations() if location.item)
locations_counter = Counter(location.player for location in world.get_locations())
f"Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}")
items_counter = Counter(location.item.player for location in multiworld.get_filled_locations())
locations_counter = Counter(location.player for location in multiworld.get_locations())
items_counter.update(item.player for item in unplaced)
locations_counter.update(location.player for location in unfilled)
print_data = {"items": items_counter, "locations": locations_counter}
logging.info(f'Per-Player counts: {print_data})')
logging.info(f"Per-Player counts: {print_data})")
def flood_items(world: MultiWorld) -> None:
def flood_items(multiworld: MultiWorld) -> None:
# get items to distribute
world.random.shuffle(world.itempool)
itempool = world.itempool
multiworld.random.shuffle(multiworld.itempool)
itempool = multiworld.itempool
progress_done = False
# sweep once to pick up preplaced items
world.state.sweep_for_events()
multiworld.state.sweep_for_events()
# fill world from top of itempool while we can
# fill multiworld from top of itempool while we can
while not progress_done:
location_list = world.get_unfilled_locations()
world.random.shuffle(location_list)
location_list = multiworld.get_unfilled_locations()
multiworld.random.shuffle(location_list)
spot_to_fill = None
for location in location_list:
if location.can_fill(world.state, itempool[0]):
if location.can_fill(multiworld.state, itempool[0]):
spot_to_fill = location
break
if spot_to_fill:
item = itempool.pop(0)
world.push_item(spot_to_fill, item, True)
multiworld.push_item(spot_to_fill, item, True)
continue
# ran out of spots, check if we need to step in and correct things
if len(world.get_reachable_locations()) == len(world.get_locations()):
if len(multiworld.get_reachable_locations()) == len(multiworld.get_locations()):
progress_done = True
continue
@@ -479,7 +579,7 @@ def flood_items(world: MultiWorld) -> None:
for item in itempool:
if item.advancement:
candidate_item_to_place = item
if world.unlocks_new_location(item):
if multiworld.unlocks_new_location(item):
item_to_place = item
break
@@ -492,20 +592,20 @@ def flood_items(world: MultiWorld) -> None:
raise FillError('No more progress items left to place.')
# find item to replace with progress item
location_list = world.get_reachable_locations()
world.random.shuffle(location_list)
location_list = multiworld.get_reachable_locations()
multiworld.random.shuffle(location_list)
for location in location_list:
if location.item is not None and not location.item.advancement:
# safe to replace
replace_item = location.item
replace_item.location = None
itempool.append(replace_item)
world.push_item(location, item_to_place, True)
multiworld.push_item(location, item_to_place, True)
itempool.remove(item_to_place)
break
def balance_multiworld_progression(world: MultiWorld) -> None:
def balance_multiworld_progression(multiworld: MultiWorld) -> None:
# A system to reduce situations where players have no checks remaining, popularly known as "BK mode."
# Overall progression balancing algorithm:
# Gather up all locations in a sphere.
@@ -513,29 +613,29 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
# If other players are below the threshold value, swap progression in this sphere into earlier spheres,
# which gives more locations available by this sphere.
balanceable_players: typing.Dict[int, float] = {
player: world.progression_balancing[player] / 100
for player in world.player_ids
if world.progression_balancing[player] > 0
player: multiworld.worlds[player].options.progression_balancing / 100
for player in multiworld.player_ids
if multiworld.worlds[player].options.progression_balancing > 0
}
if not balanceable_players:
logging.info('Skipping multiworld progression balancing.')
else:
logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.')
logging.debug(balanceable_players)
state: CollectionState = CollectionState(world)
state: CollectionState = CollectionState(multiworld)
checked_locations: typing.Set[Location] = set()
unchecked_locations: typing.Set[Location] = set(world.get_locations())
unchecked_locations: typing.Set[Location] = set(multiworld.get_locations())
reachable_locations_count: typing.Dict[int, int] = {
player: 0
for player in world.player_ids
if len(world.get_filled_locations(player)) != 0
}
total_locations_count: typing.Counter[int] = Counter(
location.player
for location in world.get_locations()
for location in multiworld.get_locations()
if not location.locked
)
reachable_locations_count: typing.Dict[int, int] = {
player: 0
for player in multiworld.player_ids
if total_locations_count[player] and len(multiworld.get_filled_locations(player)) != 0
}
balanceable_players = {
player: balanceable_players[player]
for player in balanceable_players
@@ -552,6 +652,10 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
def item_percentage(player: int, num: int) -> float:
return num / total_locations_count[player]
# If there are no locations that aren't locked, there's no point in attempting to balance progression.
if len(total_locations_count) == 0:
return
while True:
# Gather non-locked locations.
# This ensures that only shuffled locations get counted for progression balancing,
@@ -594,7 +698,7 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
while True:
# Check locations in the current sphere and gather progression items to swap earlier
for location in balancing_sphere:
if location.event:
if location.advancement:
balancing_state.collect(location.item, True, location)
player = location.item.player
# only replace items that end up in another player's world
@@ -609,7 +713,7 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
balancing_unchecked_locations.remove(location)
if not location.locked:
balancing_reachables[location.player] += 1
if world.has_beaten_game(balancing_state) or all(
if multiworld.has_beaten_game(balancing_state) or all(
item_percentage(player, reachables) >= threshold_percentages[player]
for player, reachables in balancing_reachables.items()
if player in threshold_percentages):
@@ -626,7 +730,7 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
locations_to_test = unlocked_locations[player]
items_to_test = list(candidate_items[player])
items_to_test.sort()
world.random.shuffle(items_to_test)
multiworld.random.shuffle(items_to_test)
while items_to_test:
testing = items_to_test.pop()
reducing_state = state.copy()
@@ -638,8 +742,8 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
reducing_state.sweep_for_events(locations=locations_to_test)
if world.has_beaten_game(balancing_state):
if not world.has_beaten_game(reducing_state):
if multiworld.has_beaten_game(balancing_state):
if not multiworld.has_beaten_game(reducing_state):
items_to_replace.append(testing)
else:
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
@@ -647,33 +751,32 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
if p < threshold_percentages[player]:
items_to_replace.append(testing)
replaced_items = False
old_moved_item_count = moved_item_count
# sort then shuffle to maintain deterministic behaviour,
# while allowing use of set for better algorithm growth behaviour elsewhere
replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked)
world.random.shuffle(replacement_locations)
replacement_locations = sorted(l for l in checked_locations if not l.advancement and not l.locked)
multiworld.random.shuffle(replacement_locations)
items_to_replace.sort()
world.random.shuffle(items_to_replace)
multiworld.random.shuffle(items_to_replace)
# Start swapping items. Since we swap into earlier spheres, no need for accessibility checks.
while replacement_locations and items_to_replace:
old_location = items_to_replace.pop()
for new_location in replacement_locations:
for i, new_location in enumerate(replacement_locations):
if new_location.can_fill(state, old_location.item, False) and \
old_location.can_fill(state, new_location.item, False):
replacement_locations.remove(new_location)
replacement_locations.pop(i)
swap_location_item(old_location, new_location)
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
f"displacing {old_location.item} into {old_location}")
moved_item_count += 1
state.collect(new_location.item, True, new_location)
replaced_items = True
break
else:
logging.warning(f"Could not Progression Balance {old_location.item}")
if replaced_items:
if old_moved_item_count < moved_item_count:
logging.debug(f"Moved {moved_item_count} items so far\n")
unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]}
for location in get_sphere_locations(state, unlocked):
@@ -683,11 +786,11 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
sphere_locations.add(location)
for location in sphere_locations:
if location.event:
if location.advancement:
state.collect(location.item, True, location)
checked_locations |= sphere_locations
if world.has_beaten_game(state):
if multiworld.has_beaten_game(state):
break
elif not sphere_locations:
logging.warning("Progression Balancing ran out of paths.")
@@ -704,10 +807,9 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked:
location_2.item, location_1.item = location_1.item, location_2.item
location_1.item.location = location_1
location_2.item.location = location_2
location_1.event, location_2.event = location_2.event, location_1.event
def distribute_planned(world: MultiWorld) -> None:
def distribute_planned(multiworld: MultiWorld) -> None:
def warn(warning: str, force: typing.Union[bool, str]) -> None:
if force in [True, 'fail', 'failure', 'none', False, 'warn', 'warning']:
logging.warning(f'{warning}')
@@ -720,42 +822,43 @@ def distribute_planned(world: MultiWorld) -> None:
else:
warn(warning, force)
swept_state = world.state.copy()
swept_state = multiworld.state.copy()
swept_state.sweep_for_events()
reachable = frozenset(world.get_reachable_locations(swept_state))
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
for loc in world.get_unfilled_locations():
for loc in multiworld.get_unfilled_locations():
if loc in reachable:
early_locations[loc.player].append(loc.name)
else: # not reachable with swept state
non_early_locations[loc.player].append(loc.name)
# TODO: remove. Preferably by implementing key drop
from worlds.alttp.Regions import key_drop_data
world_name_lookup = world.world_name_lookup
world_name_lookup = multiworld.world_name_lookup
block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str]
plando_blocks: typing.List[typing.Dict[str, typing.Any]] = []
player_ids = set(world.player_ids)
player_ids = set(multiworld.player_ids)
for player in player_ids:
for block in world.plando_items[player]:
for block in multiworld.plando_items[player]:
block['player'] = player
if 'force' not in block:
block['force'] = 'silent'
if 'from_pool' not in block:
block['from_pool'] = True
elif not isinstance(block['from_pool'], bool):
from_pool_type = type(block['from_pool'])
raise Exception(f'Plando "from_pool" has to be boolean, not {from_pool_type} for player {player}.')
if 'world' not in block:
target_world = False
else:
target_world = block['world']
if target_world is False or world.players == 1: # target own world
if target_world is False or multiworld.players == 1: # target own world
worlds: typing.Set[int] = {player}
elif target_world is True: # target any worlds besides own
worlds = set(world.player_ids) - {player}
worlds = set(multiworld.player_ids) - {player}
elif target_world is None: # target all worlds
worlds = set(world.player_ids)
worlds = set(multiworld.player_ids)
elif type(target_world) == list: # list of target worlds
worlds = set()
for listed_world in target_world:
@@ -765,9 +868,9 @@ def distribute_planned(world: MultiWorld) -> None:
continue
worlds.add(world_name_lookup[listed_world])
elif type(target_world) == int: # target world by slot number
if target_world not in range(1, world.players + 1):
if target_world not in range(1, multiworld.players + 1):
failed(
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
f"Cannot place item in world {target_world} as it is not in range of (1, {multiworld.players})",
block['force'])
continue
worlds = {target_world}
@@ -795,7 +898,7 @@ def distribute_planned(world: MultiWorld) -> None:
item_list: typing.List[str] = []
for key, value in items.items():
if value is True:
value = world.itempool.count(world.worlds[player].create_item(key))
value = multiworld.itempool.count(multiworld.worlds[player].create_item(key))
item_list += [key] * value
items = item_list
if isinstance(items, str):
@@ -818,15 +921,14 @@ def distribute_planned(world: MultiWorld) -> None:
if "early_locations" in locations:
locations.remove("early_locations")
for player in worlds:
locations += early_locations[player]
for target_player in worlds:
locations += early_locations[target_player]
if "non_early_locations" in locations:
locations.remove("non_early_locations")
for player in worlds:
locations += non_early_locations[player]
for target_player in worlds:
locations += non_early_locations[target_player]
block['locations'] = locations
block['locations'] = list(dict.fromkeys(locations))
if not block['count']:
block['count'] = (min(len(block['items']), len(block['locations'])) if
@@ -846,17 +948,17 @@ def distribute_planned(world: MultiWorld) -> None:
count = block['count']
failed(f"Plando count {count} greater than locations specified", block['force'])
block['count'] = len(block['locations'])
block['count']['target'] = world.random.randint(block['count']['min'], block['count']['max'])
block['count']['target'] = multiworld.random.randint(block['count']['min'], block['count']['max'])
if block['count']['target'] > 0:
plando_blocks.append(block)
# shuffle, but then sort blocks by number of locations minus number of items,
# so less-flexible blocks get priority
world.random.shuffle(plando_blocks)
multiworld.random.shuffle(plando_blocks)
plando_blocks.sort(key=lambda block: (len(block['locations']) - block['count']['target']
if len(block['locations']) > 0
else len(world.get_unfilled_locations(player)) - block['count']['target']))
else len(multiworld.get_unfilled_locations(player)) - block['count']['target']))
for placement in plando_blocks:
player = placement['player']
@@ -867,52 +969,50 @@ def distribute_planned(world: MultiWorld) -> None:
maxcount = placement['count']['target']
from_pool = placement['from_pool']
candidates = list(world.get_unfilled_locations_for_players(locations, sorted(worlds)))
world.random.shuffle(candidates)
world.random.shuffle(items)
candidates = list(multiworld.get_unfilled_locations_for_players(locations, sorted(worlds)))
multiworld.random.shuffle(candidates)
multiworld.random.shuffle(items)
count = 0
err: typing.List[str] = []
successful_pairs: typing.List[typing.Tuple[Item, Location]] = []
for item_name in items:
item = world.worlds[player].create_item(item_name)
item = multiworld.worlds[player].create_item(item_name)
for location in reversed(candidates):
if location in key_drop_data:
warn(
f"Can't place '{item_name}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
continue
if not location.item:
if location.item_rule(item):
if location.can_fill(world.state, item, False):
successful_pairs.append((item, location))
candidates.remove(location)
count = count + 1
break
if (location.address is None) == (item.code is None): # either both None or both not None
if not location.item:
if location.item_rule(item):
if location.can_fill(multiworld.state, item, False):
successful_pairs.append((item, location))
candidates.remove(location)
count = count + 1
break
else:
err.append(f"Can't place item at {location} due to fill condition not met.")
else:
err.append(f"Can't place item at {location} due to fill condition not met.")
err.append(f"{item_name} not allowed at {location}.")
else:
err.append(f"{item_name} not allowed at {location}.")
err.append(f"Cannot place {item_name} into already filled location {location}.")
else:
err.append(f"Cannot place {item_name} into already filled location {location}.")
err.append(f"Mismatch between {item_name} and {location}, only one is an event.")
if count == maxcount:
break
if count < placement['count']['min']:
m = placement['count']['min']
failed(
f"Plando block failed to place {m - count} of {m} item(s) for {world.player_name[player]}, error(s): {' '.join(err)}",
f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}",
placement['force'])
for (item, location) in successful_pairs:
world.push_item(location, item, collect=False)
location.event = True # flag location to be checked during fill
multiworld.push_item(location, item, collect=False)
location.locked = True
logging.debug(f"Plando placed {item} at {location}")
if from_pool:
try:
world.itempool.remove(item)
multiworld.itempool.remove(item)
except ValueError:
warn(
f"Could not remove {item} from pool for {world.player_name[player]} as it's already missing from it.",
f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.",
placement['force'])
except Exception as e:
raise Exception(
f"Error running plando for player {player} ({world.player_name[player]})") from e
f"Error running plando for player {player} ({multiworld.player_name[player]})") from e

View File

@@ -1,83 +1,86 @@
from __future__ import annotations
import argparse
import copy
import logging
import os
import random
import string
import sys
import urllib.parse
import urllib.request
from collections import Counter, ChainMap
from typing import Dict, Tuple, Callable, Any, Union
from collections import Counter
from typing import Any, Dict, Tuple, Union
from itertools import chain
import ModuleUpdate
ModuleUpdate.update()
import Utils
from worlds.alttp import Options as LttPOptions
from worlds.generic import PlandoConnection
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, get_options, user_path
from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from BaseClasses import seeddigits, get_seed, PlandoOptions
import Options
from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister
import copy
from BaseClasses import seeddigits, get_seed, PlandoOptions
from Utils import parse_yamls, version_tuple, __version__, tuplize_version
def mystery_argparse():
options = get_options()
defaults = options["generator"]
def resolve_path(path: str, resolver: Callable[[str], str]) -> str:
return path if os.path.isabs(path) else resolver(path)
from settings import get_settings
settings = get_settings()
defaults = settings.generator
parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
parser.add_argument('--weights_file_path', default=defaults["weights_file_path"],
help='Path to the weights file to use for rolling game settings, urls are also valid')
parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player',
parser.add_argument('--weights_file_path', default=defaults.weights_file_path,
help='Path to the weights file to use for rolling game options, urls are also valid')
parser.add_argument('--sameoptions', help='Rolls options per weights file rather than per player',
action='store_true')
parser.add_argument('--player_files_path', default=resolve_path(defaults["player_files_path"], user_path),
parser.add_argument('--player_files_path', default=defaults.player_files_path,
help="Input directory for player files.")
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
parser.add_argument('--outputpath', default=resolve_path(options["general_options"]["output_path"], user_path),
parser.add_argument('--multi', default=defaults.players, type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults.spoiler)
parser.add_argument('--outputpath', default=settings.general_options.output_path,
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
parser.add_argument('--race', action='store_true', default=defaults["race"])
parser.add_argument('--meta_file_path', default=defaults["meta_file_path"])
parser.add_argument('--race', action='store_true', default=defaults.race)
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
parser.add_argument('--log_level', default='info', help='Sets log level')
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
parser.add_argument('--plando', default=defaults["plando_options"],
parser.add_argument('--plando', default=defaults.plando_options,
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
parser.add_argument("--skip_prog_balancing", action="store_true",
help="Skip progression balancing step during generation.")
parser.add_argument("--skip_output", action="store_true",
help="Skips generation assertion and output stages and skips multidata and spoiler output. "
"Intended for debugging and testing purposes.")
args = parser.parse_args()
if not os.path.isabs(args.weights_file_path):
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
if not os.path.isabs(args.meta_file_path):
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
return args, options
return args
def get_seed_name(random_source) -> str:
return f"{random_source.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
def main(args=None, callback=ERmain):
def main(args=None) -> Tuple[argparse.Namespace, int]:
# __name__ == "__main__" check so unittests that already imported worlds don't trip this.
if __name__ == "__main__" and "worlds" in sys.modules:
raise Exception("Worlds system should not be loaded before logging init.")
if not args:
args, options = mystery_argparse()
args = mystery_argparse()
seed = get_seed(args.seed)
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
random.seed(seed)
seed_name = get_seed_name(random)
if args.race:
logging.info("Race mode enabled. Using non-deterministic random source.")
random.seed() # reset to time-based random source
weights_cache: Dict[str, Tuple[Any, ...]] = {}
@@ -85,22 +88,22 @@ def main(args=None, callback=ERmain):
try:
weights_cache[args.weights_file_path] = read_weights_yamls(args.weights_file_path)
except Exception as e:
raise ValueError(f"File {args.weights_file_path} is destroyed. Please fix your yaml.") from e
print(f"Weights: {args.weights_file_path} >> "
f"{get_choice('description', weights_cache[args.weights_file_path][-1], 'No description specified')}")
raise ValueError(f"File {args.weights_file_path} is invalid. Please fix your yaml.") from e
logging.info(f"Weights: {args.weights_file_path} >> "
f"{get_choice('description', weights_cache[args.weights_file_path][-1], 'No description specified')}")
if args.meta_file_path and os.path.exists(args.meta_file_path):
try:
meta_weights = read_weights_yamls(args.meta_file_path)[-1]
except Exception as e:
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
raise ValueError(f"File {args.meta_file_path} is invalid. Please fix your yaml.") from e
logging.info(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
del(meta_weights["meta_description"])
except Exception as e:
raise ValueError("No meta description found for meta.yaml. Unable to verify.") from e
if args.samesettings:
raise Exception("Cannot mix --samesettings with --meta")
if args.sameoptions:
raise Exception("Cannot mix --sameoptions with --meta")
else:
meta_weights = None
player_id = 1
@@ -113,38 +116,48 @@ def main(args=None, callback=ERmain):
try:
weights_cache[fname] = read_weights_yamls(path)
except Exception as e:
raise ValueError(f"File {fname} is destroyed. Please fix your yaml.") from e
raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e
# sort dict for consistent results across platforms:
weights_cache = {key: value for key, value in sorted(weights_cache.items())}
weights_cache = {key: value for key, value in sorted(weights_cache.items(), key=lambda k: k[0].casefold())}
for filename, yaml_data in weights_cache.items():
if filename not in {args.meta_file_path, args.weights_file_path}:
for yaml in yaml_data:
print(f"P{player_id} Weights: {filename} >> "
f"{get_choice('description', yaml, 'No description specified')}")
logging.info(f"P{player_id} Weights: {filename} >> "
f"{get_choice('description', yaml, 'No description specified')}")
player_files[player_id] = filename
player_id += 1
args.multi = max(player_id - 1, args.multi)
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
f"{args.plando}")
if args.multi == 0:
raise ValueError(
"No individual player files found and number of players is 0. "
"Provide individual player files or specify the number of players via host.yaml or --multi."
)
logging.info(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, "
f"{seed_name} Seed {seed} with plando: {args.plando}")
if not weights_cache:
raise Exception(f"No weights found. Provide a general weights file ({args.weights_file_path}) or individual player files. "
raise Exception(f"No weights found. "
f"Provide a general weights file ({args.weights_file_path}) or individual player files. "
f"A mix is also permitted.")
from worlds.AutoWorld import AutoWorldRegister
from worlds.alttp.EntranceRandomizer import parse_arguments
erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed
erargs.plando_options = args.plando
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
erargs.spoiler = args.spoiler
erargs.race = args.race
erargs.outputname = seed_name
erargs.outputpath = args.outputpath
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
erargs.skip_prog_balancing = args.skip_prog_balancing
erargs.skip_output = args.skip_output
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
for fname, yamls in weights_cache.items()}
if meta_weights:
@@ -156,7 +169,8 @@ def main(args=None, callback=ERmain):
for yaml in weights_cache[path]:
if category_name is None:
for category in yaml:
if category in AutoWorldRegister.world_types and key in Options.common_options:
if category in AutoWorldRegister.world_types and \
key in Options.CommonOptions.type_hints:
yaml[category][key] = option
elif category_name not in yaml:
logging.warning(f"Meta: Category {category_name} is not present in {path}.")
@@ -167,7 +181,7 @@ def main(args=None, callback=ERmain):
for player in range(1, args.multi + 1):
player_path_cache[player] = player_files.get(player, args.weights_file_path)
name_counter = Counter()
erargs.player_settings = {}
erargs.player_options = {}
player = 1
while player <= args.multi:
@@ -194,7 +208,7 @@ def main(args=None, callback=ERmain):
player += 1
except Exception as e:
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
raise ValueError(f"File {path} is invalid. Please fix your yaml.") from e
else:
raise RuntimeError(f'No weights specified for player {player}')
@@ -223,7 +237,7 @@ def main(args=None, callback=ERmain):
with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f:
yaml.dump(important, f)
callback(erargs, seed)
return erargs, seed
def read_weights_yamls(path) -> Tuple[Any, ...]:
@@ -289,71 +303,77 @@ def handle_name(name: str, player: int, name_counter: Counter):
NUMBER=(number if number > 1 else ''),
player=player,
PLAYER=(player if player > 1 else '')))
new_name = new_name.strip()[:16]
# Run .strip twice for edge case where after the initial .slice new_name has a leading whitespace.
# Could cause issues for some clients that cannot handle the additional whitespace.
new_name = new_name.strip()[:16].strip()
if new_name == "Archipelago":
raise Exception(f"You cannot name yourself \"{new_name}\"")
return new_name
def prefer_int(input_data: str) -> Union[str, int]:
try:
return int(input_data)
except:
return input_data
goals = {
'ganon': 'ganon',
'crystals': 'crystals',
'bosses': 'bosses',
'pedestal': 'pedestal',
'ganon_pedestal': 'ganonpedestal',
'triforce_hunt': 'triforcehunt',
'local_triforce_hunt': 'localtriforcehunt',
'ganon_triforce_hunt': 'ganontriforcehunt',
'local_ganon_triforce_hunt': 'localganontriforcehunt',
'ice_rod_hunt': 'icerodhunt',
}
def roll_percentage(percentage: Union[int, float]) -> bool:
"""Roll a percentage chance.
percentage is expected to be in range [0, 100]"""
return random.random() < (float(percentage) / 100)
def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> dict:
def update_weights(weights: dict, new_weights: dict, update_type: str, name: str) -> dict:
logging.debug(f'Applying {new_weights}')
new_options = set(new_weights) - set(weights)
weights.update(new_weights)
cleaned_weights = {}
for option in new_weights:
option_name = option.lstrip("+-")
if option.startswith("+") and option_name in weights:
cleaned_value = weights[option_name]
new_value = new_weights[option]
if isinstance(new_value, set):
cleaned_value.update(new_value)
elif isinstance(new_value, list):
cleaned_value.extend(new_value)
elif isinstance(new_value, dict):
cleaned_value = dict(Counter(cleaned_value) + Counter(new_value))
else:
raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name},"
f" received {type(new_value).__name__}.")
cleaned_weights[option_name] = cleaned_value
elif option.startswith("-") and option_name in weights:
cleaned_value = weights[option_name]
new_value = new_weights[option]
if isinstance(new_value, set):
cleaned_value.difference_update(new_value)
elif isinstance(new_value, list):
for element in new_value:
cleaned_value.remove(element)
elif isinstance(new_value, dict):
cleaned_value = dict(Counter(cleaned_value) - Counter(new_value))
else:
raise Exception(f"Cannot apply remove to non-dict, set, or list type {option_name},"
f" received {type(new_value).__name__}.")
cleaned_weights[option_name] = cleaned_value
else:
cleaned_weights[option_name] = new_weights[option]
new_options = set(cleaned_weights) - set(weights)
weights.update(cleaned_weights)
if new_options:
for new_option in new_options:
logging.warning(f'{type} Suboption "{new_option}" of "{name}" did not '
logging.warning(f'{update_type} Suboption "{new_option}" of "{name}" did not '
f'overwrite a root option. '
f'This is probably in error.')
return weights
def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
from worlds import AutoWorldRegister
if not game:
return get_choice(option_key, category_dict)
if game in AutoWorldRegister.world_types:
game_world = AutoWorldRegister.world_types[game]
options = ChainMap(game_world.option_definitions, Options.per_game_common_options)
options = game_world.options_dataclass.type_hints
if option_key in options:
if options[option_key].supports_weighting:
return get_choice(option_key, category_dict)
return category_dict[option_key]
if game == "A Link to the Past": # TODO wow i hate this
if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode",
"triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra",
"triforce_pieces_required", "shop_shuffle", "mode", "item_pool", "item_functionality",
"boss_shuffle", "enemy_damage", "enemy_health", "timer", "countdown_start_time",
"red_clock_time", "blue_clock_time", "green_clock_time", "dungeon_counters", "shuffle_prizes",
"misery_mire_medallion", "turtle_rock_medallion", "sprite_pool", "sprite",
"random_sprite_on_event"}:
return get_choice(option_key, category_dict)
raise Exception(f"Error generating meta option {option_key} for {game}.")
raise Options.OptionError(f"Error generating meta option {option_key} for {game}.")
def roll_linked_options(weights: dict) -> dict:
@@ -373,12 +393,12 @@ def roll_linked_options(weights: dict) -> dict:
else:
logging.debug(f"linked option {option_set['name']} skipped.")
except Exception as e:
raise ValueError(f"Linked option {option_set['name']} is destroyed. "
raise ValueError(f"Linked option {option_set['name']} is invalid. "
f"Please fix your linked option.") from e
return weights
def roll_triggers(weights: dict, triggers: list) -> dict:
def roll_triggers(weights: dict, triggers: list, valid_keys: set) -> dict:
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
weights["_Generator_Version"] = Utils.__version__
for i, option_set in enumerate(triggers):
@@ -401,35 +421,47 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
if category_name:
currently_targeted_weights = currently_targeted_weights[category_name]
update_weights(currently_targeted_weights, category_options, "Triggered", option_set["option_name"])
valid_keys.add(key)
except Exception as e:
raise ValueError(f"Your trigger number {i + 1} is destroyed. "
raise ValueError(f"Your trigger number {i + 1} is invalid. "
f"Please fix your triggers.") from e
return weights
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions):
if option_key in game_weights:
try:
try:
if option_key in game_weights:
if not option.supports_weighting:
player_option = option.from_any(game_weights[option_key])
else:
player_option = option.from_any(get_choice(option_key, game_weights))
setattr(ret, option_key, player_option)
except Exception as e:
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
else:
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
player_option = option.from_any(option.default) # call the from_any here to support default "random"
setattr(ret, option_key, player_option)
except Exception as e:
raise Options.OptionError(f"Error generating option {option_key} in {ret.game}") from e
else:
setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random"
from worlds import AutoWorldRegister
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
"""
Roll options from specified weights, usually originating from a .yaml options file.
Important note:
The same weights dict is shared between all slots using the same yaml (e.g. generic weights file for filler slots).
This means it should never be modified without making a deepcopy first.
"""
from worlds import AutoWorldRegister
if "linked_options" in weights:
weights = roll_linked_options(weights)
valid_keys = set()
if "triggers" in weights:
weights = roll_triggers(weights, weights["triggers"])
weights = roll_triggers(weights, weights["triggers"], valid_keys)
requirements = weights.get("requires", {})
if requirements:
@@ -444,175 +476,57 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
f"which is not enabled.")
ret = argparse.Namespace()
for option_key in Options.per_game_common_options:
if option_key in weights and option_key not in Options.common_options:
for option_key in Options.PerGameCommonOptions.type_hints:
if option_key in weights and option_key not in Options.CommonOptions.type_hints:
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
ret.game = get_choice("game", weights)
if ret.game not in AutoWorldRegister.world_types:
from worlds import failed_world_loads
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]
if picks[0] in failed_world_loads:
raise Exception(f"No functional world found to handle game {ret.game}. "
f"Did you mean '{picks[0]}' ({picks[1]}% sure)? "
f"If so, it appears the world failed to initialize correctly.")
raise Exception(f"No world found to handle game {ret.game}. Did you mean '{picks[0]}' ({picks[1]}% sure)? "
f"Check your spelling or installation of that world.")
if ret.game not in weights:
raise Exception(f"No game options for selected game \"{ret.game}\" found.")
world_type = AutoWorldRegister.world_types[ret.game]
game_weights = weights[ret.game]
for weight in chain(game_weights, weights):
if weight.startswith("+"):
raise Exception(f"Merge tag cannot be used outside of trigger contexts. Found {weight}")
if weight.startswith("-"):
raise Exception(f"Remove tag cannot be used outside of trigger contexts. Found {weight}")
if "triggers" in game_weights:
weights = roll_triggers(weights, game_weights["triggers"])
weights = roll_triggers(weights, game_weights["triggers"], valid_keys)
game_weights = weights[ret.game]
ret.name = get_choice('name', weights)
for option_key, option in Options.common_options.items():
for option_key, option in Options.CommonOptions.type_hints.items():
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
if ret.game in AutoWorldRegister.world_types:
for option_key, option in world_type.option_definitions.items():
handle_option(ret, game_weights, option_key, option, plando_options)
for option_key, option in Options.per_game_common_options.items():
# skip setting this option if already set from common_options, defaulting to root option
if option_key not in world_type.option_definitions and \
(option_key not in Options.common_options or option_key in game_weights):
handle_option(ret, game_weights, option_key, option, plando_options)
if PlandoOptions.items in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
# bad hardcoded behavior to make this work for now
ret.plando_connections = []
if PlandoOptions.connections in plando_options:
options = game_weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
get_choice("direction", placement)
))
elif ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights, plando_options)
else:
raise Exception(f"Unsupported game {ret.game}")
for option_key, option in world_type.options_dataclass.type_hints.items():
handle_option(ret, game_weights, option_key, option, plando_options)
valid_keys.add(option_key)
for option_key in game_weights:
if option_key in {"triggers", *valid_keys}:
continue
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
if PlandoOptions.items in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights)
return ret
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if "dungeon_items" in weights and get_choice_legacy('dungeon_items', weights, "none") != "none":
raise Exception(f"dungeon_items key in A Link to the Past was removed, but is present in these weights as {get_choice_legacy('dungeon_items', weights, False)}.")
glitches_required = get_choice_legacy('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
logging.warning("Only NMG, OWG, HMG and No Logic supported")
glitches_required = 'none'
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches',
'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
glitches_required]
ret.dark_room_logic = get_choice_legacy("dark_room_logic", weights, "lamp")
if not ret.dark_room_logic: # None/False
ret.dark_room_logic = "none"
if ret.dark_room_logic == "sconces":
ret.dark_room_logic = "torches"
if ret.dark_room_logic not in {"lamp", "torches", "none"}:
raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"")
entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla')
if entrance_shuffle.startswith('none-'):
ret.shuffle = 'vanilla'
else:
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
goal = get_choice_legacy('goals', weights, 'ganon')
ret.goal = goals[goal]
extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice_legacy('triforce_pieces_required', weights, 20))
# sum a percentage to required
if extra_pieces == 'percentage':
percentage = max(100, float(get_choice_legacy('triforce_pieces_percentage', weights, 150))) / 100
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
# vanilla mode (specify how many pieces are)
elif extra_pieces == 'available':
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
get_choice_legacy('triforce_pieces_available', weights, 30))
# required pieces + fixed extra
elif extra_pieces == 'extra':
extra_pieces = max(0, int(get_choice_legacy('triforce_pieces_extra', weights, 10)))
ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces
# change minimum to required pieces to avoid problems
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
ret.shop_shuffle = get_choice_legacy('shop_shuffle', weights, '')
if not ret.shop_shuffle:
ret.shop_shuffle = ''
ret.mode = get_choice_legacy("mode", weights)
ret.difficulty = get_choice_legacy('item_pool', weights)
ret.item_functionality = get_choice_legacy('item_functionality', weights)
ret.enemy_damage = {None: 'default',
'default': 'default',
'shuffled': 'shuffled',
'random': 'chaos', # to be removed
'chaos': 'chaos',
}[get_choice_legacy('enemy_damage', weights)]
ret.enemy_health = get_choice_legacy('enemy_health', weights)
ret.timer = {'none': False,
None: False,
False: False,
'timed': 'timed',
'timed_ohko': 'timed-ohko',
'ohko': 'ohko',
'timed_countdown': 'timed-countdown',
'display': 'display'}[get_choice_legacy('timer', weights, False)]
ret.countdown_start_time = int(get_choice_legacy('countdown_start_time', weights, 10))
ret.red_clock_time = int(get_choice_legacy('red_clock_time', weights, -2))
ret.blue_clock_time = int(get_choice_legacy('blue_clock_time', weights, 2))
ret.green_clock_time = int(get_choice_legacy('green_clock_time', weights, 4))
ret.dungeon_counters = get_choice_legacy('dungeon_counters', weights, 'default')
ret.shuffle_prizes = get_choice_legacy('shuffle_prizes', weights, "g")
ret.required_medallions = [get_choice_legacy("misery_mire_medallion", weights, "random"),
get_choice_legacy("turtle_rock_medallion", weights, "random")]
for index, medallion in enumerate(ret.required_medallions):
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \
.get(medallion.lower(), None)
if not ret.required_medallions[index]:
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
ret.plando_texts = {}
if PlandoOptions.texts in plando_options:
tt = TextTable()
tt.removeUnwantedText()
options = weights.get("plando_texts", [])
for placement in options:
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
at = str(get_choice_legacy("at", placement))
if at not in tt:
raise Exception(f"No text target \"{at}\" found.")
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
ret.plando_connections = []
if PlandoOptions.connections in plando_options:
options = weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice_legacy("entrance", placement),
get_choice_legacy("exit", placement),
get_choice_legacy("direction", placement, "both")
))
def roll_alttp_settings(ret: argparse.Namespace, weights):
ret.sprite_pool = weights.get('sprite_pool', [])
ret.sprite = get_choice_legacy('sprite', weights, "Link")
if 'random_sprite_on_event' in weights:
@@ -640,6 +554,17 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if __name__ == '__main__':
import atexit
confirmation = atexit.register(input, "Press enter to close.")
main()
erargs, seed = main()
from Main import main as ERmain
multiworld = ERmain(erargs, seed)
if __debug__:
import gc
import sys
import weakref
weak = weakref.ref(multiworld)
del multiworld
gc.collect() # need to collect to deref all hard references
assert not weak(), f"MultiWorld object was not de-allocated, it's referenced {sys.getrefcount(weak())} times." \
" This would be a memory leak."
# in case of error-free exit should not need confirmation
atexit.unregister(confirmation)

View File

@@ -1,906 +1,8 @@
import os
import asyncio
import ModuleUpdate
import json
import Utils
from pymem import pymem
from worlds.kh2.Items import exclusionItem_table, CheckDupingItems
from worlds.kh2 import all_locations, item_dictionary_table, exclusion_table
from worlds.kh2.WorldLocations import *
from worlds import network_data_package
if __name__ == "__main__":
Utils.init_logging("KH2Client", exception_logger="Client")
from NetUtils import ClientStatus
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
CommonContext, server_loop
from worlds.kh2.Client import launch
ModuleUpdate.update()
kh2_loc_name_to_id = network_data_package["games"]["Kingdom Hearts 2"]["location_name_to_id"]
# class KH2CommandProcessor(ClientCommandProcessor):
class KH2Context(CommonContext):
# command_processor: int = KH2CommandProcessor
game = "Kingdom Hearts 2"
items_handling = 0b101 # Indicates you get items sent from other worlds.
def __init__(self, server_address, password):
super(KH2Context, self).__init__(server_address, password)
self.kh2LocalItems = None
self.ability = None
self.growthlevel = None
self.KH2_sync_task = None
self.syncing = False
self.kh2connected = False
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.lookup_id_to_item: typing.Dict[int, str] = {data.code: item_name for item_name, data in
item_dictionary_table.items() if data.code}
self.lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in
all_locations.items() if data.code}
self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()}
self.location_table = {}
self.collectible_table = {}
self.collectible_override_flags_address = 0
self.collectible_offsets = {}
self.sending = []
# flag for if the player has gotten their starting inventory from the server
self.hasStartingInvo = False
# list used to keep track of locations+items player has. Used for disoneccting
self.kh2seedsave = {"checked_locations": {"0": []},
"starting_inventory": self.hasStartingInvo,
# Character: [back of invo, front of invo]
"SoraInvo": [0x25CC, 0x2546],
"DonaldInvo": [0x2678, 0x2658],
"GoofyInvo": [0x278E, 0x276C],
"AmountInvo": {
"ServerItems": {
"Ability": {},
"Amount": {},
"Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, "Aerial Dodge": 0,
"Glide": 0},
"Bitmask": [],
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
"Equipment": [],
"Magic": {},
"StatIncrease": {},
"Boost": {},
},
"LocalItems": {
"Ability": {},
"Amount": {},
"Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0,
"Aerial Dodge": 0, "Glide": 0},
"Bitmask": [],
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
"Equipment": [],
"Magic": {},
"StatIncrease": {},
"Boost": {},
}},
# 1,3,255 are in this list in case the player gets locations in those "worlds" and I need to still have them checked
"worldIdChecks": {
"1": [], # world of darkness (story cutscenes)
"2": [],
"3": [], # destiny island doesn't have checks to ima put tt checks here
"4": [],
"5": [],
"6": [],
"7": [],
"8": [],
"9": [],
"10": [],
"11": [],
# atlantica isn't a supported world. if you go in atlantica it will check dc
"12": [],
"13": [],
"14": [],
"15": [],
# world map, but you only go to the world map while on the way to goa so checking hb
"16": [],
"17": [],
"18": [],
"255": [], # starting screen
},
"Levels": {
"SoraLevel": 0,
"ValorLevel": 0,
"WisdomLevel": 0,
"LimitLevel": 0,
"MasterLevel": 0,
"FinalLevel": 0,
},
"SoldEquipment": [],
"SoldBoosts": {"Power Boost": 0,
"Magic Boost": 0,
"Defense Boost": 0,
"AP Boost": 0}
}
self.slotDataProgressionNames = {}
self.kh2seedname = None
self.kh2slotdata = None
self.itemamount = {}
# sora equipped, valor equipped, master equipped, final equipped
self.keybladeAnchorList = (0x24F0, 0x32F4, 0x339C, 0x33D4)
if "localappdata" in os.environ:
self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP")
self.amountOfPieces = 0
# hooked object
self.kh2 = None
self.ItemIsSafe = False
self.game_connected = False
self.finalxemnas = False
self.worldid = {
# 1: {}, # world of darkness (story cutscenes)
2: TT_Checks,
# 3: {}, # destiny island doesn't have checks to ima put tt checks here
4: HB_Checks,
5: BC_Checks,
6: Oc_Checks,
7: AG_Checks,
8: LoD_Checks,
9: HundredAcreChecks,
10: PL_Checks,
11: DC_Checks, # atlantica isn't a supported world. if you go in atlantica it will check dc
12: DC_Checks,
13: TR_Checks,
14: HT_Checks,
15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb
16: PR_Checks,
17: SP_Checks,
18: TWTNW_Checks,
# 255: {}, # starting screen
}
# 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
self.Save = 0x09A70B0
self.Sys3 = 0x2A59DF0
self.Bt10 = 0x2A74880
self.BtlEnd = 0x2A0D3E0
self.Slot1 = 0x2A20C98
self.chest_set = set(exclusion_table["Chests"])
self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"])
self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"])
self.shield_set = set(CheckDupingItems["Weapons"]["Shields"])
self.all_weapons = self.keyblade_set.union(self.staff_set).union(self.shield_set)
self.equipment_categories = CheckDupingItems["Equipment"]
self.armor_set = set(self.equipment_categories["Armor"])
self.accessories_set = set(self.equipment_categories["Accessories"])
self.all_equipment = self.armor_set.union(self.accessories_set)
self.Equipment_Anchor_Dict = {
"Armor": [0x2504, 0x2506, 0x2508, 0x250A],
"Accessories": [0x2514, 0x2516, 0x2518, 0x251A]}
self.AbilityQuantityDict = {}
self.ability_categories = CheckDupingItems["Abilities"]
self.sora_ability_set = set(self.ability_categories["Sora"])
self.donald_ability_set = set(self.ability_categories["Donald"])
self.goofy_ability_set = set(self.ability_categories["Goofy"])
self.all_abilities = self.sora_ability_set.union(self.donald_ability_set).union(self.goofy_ability_set)
self.boost_set = set(CheckDupingItems["Boosts"])
self.stat_increase_set = set(CheckDupingItems["Stat Increases"])
self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities}
# Growth:[level 1,level 4,slot]
self.growth_values_dict = {"High Jump": [0x05E, 0x061, 0x25CE],
"Quick Run": [0x62, 0x65, 0x25D0],
"Dodge Roll": [0x234, 0x237, 0x25D2],
"Aerial Dodge": [0x066, 0x069, 0x25D4],
"Glide": [0x6A, 0x6D, 0x25D6]}
self.boost_to_anchor_dict = {
"Power Boost": 0x24F9,
"Magic Boost": 0x24FA,
"Defense Boost": 0x24FB,
"AP Boost": 0x24F8}
self.AbilityCodeList = [self.item_name_to_data[item].code for item in exclusionItem_table["Ability"]]
self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}
self.bitmask_item_code = [
0x130000, 0x130001, 0x130002, 0x130003, 0x130004, 0x130005, 0x130006, 0x130007
, 0x130008, 0x130009, 0x13000A, 0x13000B, 0x13000C
, 0x13001F, 0x130020, 0x130021, 0x130022, 0x130023
, 0x13002A, 0x13002B, 0x13002C, 0x13002D]
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(KH2Context, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
async def connection_closed(self):
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"kh2save{self.kh2seedname}{self.auth}.json"),
'w') as f:
f.write(json.dumps(self.kh2seedsave, indent=4))
await super(KH2Context, self).connection_closed()
async def disconnect(self, allow_autoreconnect: bool = False):
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"kh2save{self.kh2seedname}{self.auth}.json"),
'w') as f:
f.write(json.dumps(self.kh2seedsave, indent=4))
await super(KH2Context, self).disconnect()
@property
def endpoints(self):
if self.server:
return [self.server]
else:
return []
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"kh2save{self.kh2seedname}{self.auth}.json"),
'w') as f:
f.write(json.dumps(self.kh2seedsave, indent=4))
await super(KH2Context, self).shutdown()
def on_package(self, cmd: str, args: dict):
if cmd in {"RoomInfo"}:
self.kh2seedname = args['seed_name']
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"\kh2save{self.kh2seedname}{self.auth}.json"):
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
'wt') as f:
pass
elif os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"):
with open(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json", 'r') as f:
self.kh2seedsave = json.load(f)
if cmd in {"Connected"}:
for player in args['players']:
if str(player.slot) not in self.kh2seedsave["checked_locations"]:
self.kh2seedsave["checked_locations"].update({str(player.slot): []})
self.kh2slotdata = args['slot_data']
self.serverconneced = True
self.kh2LocalItems = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()}
try:
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
logger.info("You are now auto-tracking")
self.kh2connected = True
except Exception as e:
logger.info("Line 247")
if self.kh2connected:
logger.info("Connection Lost")
self.kh2connected = False
logger.info(e)
if cmd in {"ReceivedItems"}:
start_index = args["index"]
if start_index != len(self.items_received):
for item in args['items']:
# starting invo from server
if item.location in {-2}:
if not self.kh2seedsave["starting_inventory"]:
asyncio.create_task(self.give_item(item.item))
# if location is not already given or is !getitem
elif item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \
or item.location in {-1}:
asyncio.create_task(self.give_item(item.item))
if item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \
and item.location not in {-1, -2}:
self.kh2seedsave["checked_locations"][str(item.player)].append(item.location)
if not self.kh2seedsave["starting_inventory"]:
self.kh2seedsave["starting_inventory"] = True
if cmd in {"RoomUpdate"}:
if "checked_locations" in args:
new_locations = set(args["checked_locations"])
# TODO: make this take locations from other players on the same slot so proper coop happens
# items_to_give = [self.kh2slotdata["LocalItems"][str(location_id)] for location_id in new_locations if
# location_id in self.kh2LocalItems.keys()]
self.checked_locations |= new_locations
async def checkWorldLocations(self):
try:
currentworldint = int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big")
if currentworldint in self.worldid:
curworldid = self.worldid[currentworldint]
for location, data in curworldid.items():
if location not in self.locations_checked \
and (int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
"big") & 0x1 << data.bitIndex) > 0:
self.locations_checked.add(location)
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
except Exception as e:
logger.info("Line 285")
if self.kh2connected:
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
async def checkLevels(self):
try:
for location, data in SoraLevels.items():
currentLevel = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), "big")
if location not in self.locations_checked \
and currentLevel >= data.bitIndex:
if self.kh2seedsave["Levels"]["SoraLevel"] < currentLevel:
self.kh2seedsave["Levels"]["SoraLevel"] = currentLevel
self.locations_checked.add(location)
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
formDict = {
0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels],
3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels]}
for i in range(5):
for location, data in formDict[i][1].items():
formlevel = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big")
if location not in self.locations_checked \
and formlevel >= data.bitIndex:
if formlevel > self.kh2seedsave["Levels"][formDict[i][0]]:
self.kh2seedsave["Levels"][formDict[i][0]] = formlevel
self.locations_checked.add(location)
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
except Exception as e:
logger.info("Line 312")
if self.kh2connected:
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
async def checkSlots(self):
try:
for location, data in weaponSlots.items():
if location not in self.locations_checked:
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
"big") > 0:
self.locations_checked.add(location)
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
for location, data in formSlots.items():
if location not in self.locations_checked:
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
"big") & 0x1 << data.bitIndex > 0:
self.locations_checked.add(location)
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
except Exception as e:
if self.kh2connected:
logger.info("Line 333")
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
async def verifyChests(self):
try:
currentworld = str(int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big"))
for location in self.kh2seedsave["worldIdChecks"][currentworld]:
locationName = self.lookup_id_to_Location[location]
if locationName in self.chest_set:
if locationName in self.location_name_to_worlddata.keys():
locationData = self.location_name_to_worlddata[locationName]
if int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1),
"big") & 0x1 << locationData.bitIndex == 0:
roomData = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained,
1), "big")
self.kh2.write_bytes(self.kh2.base_address + self.Save + locationData.addrObtained,
(roomData | 0x01 << locationData.bitIndex).to_bytes(1, 'big'), 1)
except Exception as e:
if self.kh2connected:
logger.info("Line 350")
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
async def verifyLevel(self):
for leveltype, anchor in {"SoraLevel": 0x24FF,
"ValorLevel": 0x32F6,
"WisdomLevel": 0x332E,
"LimitLevel": 0x3366,
"MasterLevel": 0x339E,
"FinalLevel": 0x33D6}.items():
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + anchor, 1), "big") < \
self.kh2seedsave["Levels"][leveltype]:
self.kh2.write_bytes(self.kh2.base_address + self.Save + anchor,
(self.kh2seedsave["Levels"][leveltype]).to_bytes(1, 'big'), 1)
def verifyLocation(self, location):
locationData = self.location_name_to_worlddata[location]
locationName = self.lookup_id_to_Location[location]
isChecked = True
if locationName not in levels_locations:
if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1),
"big") & 0x1 << locationData.bitIndex) == 0:
isChecked = False
elif locationName in SoraLevels:
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1),
"big") < locationData.bitIndex:
isChecked = False
elif int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1),
"big") < locationData.bitIndex:
isChecked = False
return isChecked
async def give_item(self, item, ItemType="ServerItems"):
try:
itemname = self.lookup_id_to_item[item]
itemcode = self.item_name_to_data[itemname]
if itemcode.ability:
abilityInvoType = 0
TwilightZone = 2
if ItemType == "LocalItems":
abilityInvoType = 1
TwilightZone = -2
if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}:
self.kh2seedsave["AmountInvo"][ItemType]["Growth"][itemname] += 1
return
if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Ability"]:
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname] = []
# appending the slot that the ability should be in
if len(self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname]) < \
self.AbilityQuantityDict[itemname]:
if itemname in self.sora_ability_set:
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
self.kh2seedsave["SoraInvo"][abilityInvoType])
self.kh2seedsave["SoraInvo"][abilityInvoType] -= TwilightZone
elif itemname in self.donald_ability_set:
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
self.kh2seedsave["DonaldInvo"][abilityInvoType])
self.kh2seedsave["DonaldInvo"][abilityInvoType] -= TwilightZone
else:
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
self.kh2seedsave["GoofyInvo"][abilityInvoType])
self.kh2seedsave["GoofyInvo"][abilityInvoType] -= TwilightZone
elif itemcode.code in self.bitmask_item_code:
if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"]:
self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"].append(itemname)
elif itemcode.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}:
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Magic"]:
self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] += 1
else:
self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] = 1
elif itemname in self.all_equipment:
self.kh2seedsave["AmountInvo"][ItemType]["Equipment"].append(itemname)
elif itemname in self.all_weapons:
if itemname in self.keyblade_set:
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Sora"].append(itemname)
elif itemname in self.staff_set:
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Donald"].append(itemname)
else:
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Goofy"].append(itemname)
elif itemname in self.boost_set:
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Boost"]:
self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] += 1
else:
self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] = 1
elif itemname in self.stat_increase_set:
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"]:
self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] += 1
else:
self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] = 1
else:
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Amount"]:
self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] += 1
else:
self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] = 1
except Exception as e:
if self.kh2connected:
logger.info("Line 398")
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
def run_gui(self):
"""Import kivy UI system and start running it as self.ui_task."""
from kvui import GameManager
class KH2Manager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago KH2 Client"
self.ui = KH2Manager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def IsInShop(self, sellable, master_boost):
# journal = 0x741230 shop = 0x741320
# if journal=-1 and shop = 5 then in shop
# if journam !=-1 and shop = 10 then journal
journal = self.kh2.read_short(self.kh2.base_address + 0x741230)
shop = self.kh2.read_short(self.kh2.base_address + 0x741320)
if (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
# print("your in the shop")
sellable_dict = {}
for itemName in sellable:
itemdata = self.item_name_to_data[itemName]
amount = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big")
sellable_dict[itemName] = amount
while (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
journal = self.kh2.read_short(self.kh2.base_address + 0x741230)
shop = self.kh2.read_short(self.kh2.base_address + 0x741320)
await asyncio.sleep(0.5)
for item, amount in sellable_dict.items():
itemdata = self.item_name_to_data[item]
afterShop = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big")
if afterShop < amount:
if item in master_boost:
self.kh2seedsave["SoldBoosts"][item] += (amount - afterShop)
else:
self.kh2seedsave["SoldEquipment"].append(item)
async def verifyItems(self):
try:
local_amount = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"].keys())
server_amount = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"].keys())
master_amount = local_amount | server_amount
local_ability = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"].keys())
server_ability = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"].keys())
master_ability = local_ability | server_ability
local_bitmask = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Bitmask"])
server_bitmask = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Bitmask"])
master_bitmask = local_bitmask | server_bitmask
local_keyblade = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Sora"])
local_staff = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Donald"])
local_shield = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Goofy"])
server_keyblade = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Sora"])
server_staff = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Donald"])
server_shield = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Goofy"])
master_keyblade = local_keyblade | server_keyblade
master_staff = local_staff | server_staff
master_shield = local_shield | server_shield
local_equipment = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Equipment"])
server_equipment = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Equipment"])
master_equipment = local_equipment | server_equipment
local_magic = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"].keys())
server_magic = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"].keys())
master_magic = local_magic | server_magic
local_stat = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"].keys())
server_stat = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"].keys())
master_stat = local_stat | server_stat
local_boost = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"].keys())
server_boost = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"].keys())
master_boost = local_boost | server_boost
master_sell = master_equipment | master_staff | master_shield | master_boost
await asyncio.create_task(self.IsInShop(master_sell, master_boost))
for itemName in master_amount:
itemData = self.item_name_to_data[itemName]
amountOfItems = 0
if itemName in local_amount:
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"][itemName]
if itemName in server_amount:
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"][itemName]
if itemName == "Torn Page":
# Torn Pages are handled differently because they can be consumed.
# Will check the progression in 100 acre and - the amount of visits
# amountofitems-amount of visits done
for location, data in tornPageLocks.items():
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
"big") & 0x1 << data.bitIndex > 0:
amountOfItems -= 1
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != amountOfItems and amountOfItems >= 0:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
amountOfItems.to_bytes(1, 'big'), 1)
for itemName in master_keyblade:
itemData = self.item_name_to_data[itemName]
# if the inventory slot for that keyblade is less than the amount they should have
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 1 and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x1CFF, 1),
"big") != 13:
# Checking form anchors for the keyblade
if self.kh2.read_short(self.kh2.base_address + self.Save + 0x24F0) == itemData.kh2id \
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x32F4) == itemData.kh2id \
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x339C) == itemData.kh2id \
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x33D4) == itemData.kh2id:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(0).to_bytes(1, 'big'), 1)
else:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(1).to_bytes(1, 'big'), 1)
for itemName in master_staff:
itemData = self.item_name_to_data[itemName]
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 1 \
and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2604) != itemData.kh2id \
and itemName not in self.kh2seedsave["SoldEquipment"]:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(1).to_bytes(1, 'big'), 1)
for itemName in master_shield:
itemData = self.item_name_to_data[itemName]
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 1 \
and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2718) != itemData.kh2id \
and itemName not in self.kh2seedsave["SoldEquipment"]:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(1).to_bytes(1, 'big'), 1)
for itemName in master_ability:
itemData = self.item_name_to_data[itemName]
ability_slot = []
if itemName in local_ability:
ability_slot += self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"][itemName]
if itemName in server_ability:
ability_slot += self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"][itemName]
for slot in ability_slot:
current = self.kh2.read_short(self.kh2.base_address + self.Save + slot)
ability = current & 0x0FFF
if ability | 0x8000 != (0x8000 + itemData.memaddr):
self.kh2.write_short(self.kh2.base_address + self.Save + slot, itemData.memaddr)
for itemName in self.master_growth:
growthLevel = self.kh2seedsave["AmountInvo"]["ServerItems"]["Growth"][itemName] \
+ self.kh2seedsave["AmountInvo"]["LocalItems"]["Growth"][itemName]
if growthLevel > 0:
slot = self.growth_values_dict[itemName][2]
min_growth = self.growth_values_dict[itemName][0]
max_growth = self.growth_values_dict[itemName][1]
if growthLevel > 4:
growthLevel = 4
current_growth_level = self.kh2.read_short(self.kh2.base_address + self.Save + slot)
ability = current_growth_level & 0x0FFF
# if the player should be getting a growth ability
if ability | 0x8000 != 0x8000 + min_growth - 1 + growthLevel:
# if it should be level one of that growth
if 0x8000 + min_growth - 1 + growthLevel <= 0x8000 + min_growth or ability < min_growth:
self.kh2.write_short(self.kh2.base_address + self.Save + slot, min_growth)
# if it is already in the inventory
elif ability | 0x8000 < (0x8000 + max_growth):
self.kh2.write_short(self.kh2.base_address + self.Save + slot, current_growth_level + 1)
for itemName in master_bitmask:
itemData = self.item_name_to_data[itemName]
itemMemory = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big")
if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") & 0x1 << itemData.bitmask) == 0:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(itemMemory | 0x01 << itemData.bitmask).to_bytes(1, 'big'), 1)
for itemName in master_equipment:
itemData = self.item_name_to_data[itemName]
isThere = False
if itemName in self.accessories_set:
Equipment_Anchor_List = self.Equipment_Anchor_Dict["Accessories"]
else:
Equipment_Anchor_List = self.Equipment_Anchor_Dict["Armor"]
# Checking form anchors for the equipment
for slot in Equipment_Anchor_List:
if self.kh2.read_short(self.kh2.base_address + self.Save + slot) == itemData.kh2id:
isThere = True
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 0:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(0).to_bytes(1, 'big'), 1)
break
if not isThere and itemName not in self.kh2seedsave["SoldEquipment"]:
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 1:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(1).to_bytes(1, 'big'), 1)
for itemName in master_magic:
itemData = self.item_name_to_data[itemName]
amountOfItems = 0
if itemName in local_magic:
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"][itemName]
if itemName in server_magic:
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"][itemName]
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != amountOfItems \
and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x741320, 1), "big") in {10, 8}:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
amountOfItems.to_bytes(1, 'big'), 1)
for itemName in master_stat:
itemData = self.item_name_to_data[itemName]
amountOfItems = 0
if itemName in local_stat:
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"][itemName]
if itemName in server_stat:
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"][itemName]
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != amountOfItems \
and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Slot1 + 0x1B2, 1),
"big") >= 5:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
amountOfItems.to_bytes(1, 'big'), 1)
for itemName in master_boost:
itemData = self.item_name_to_data[itemName]
amountOfItems = 0
if itemName in local_boost:
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"][itemName]
if itemName in server_boost:
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"][itemName]
amountOfBoostsInInvo = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big")
amountOfUsedBoosts = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + self.boost_to_anchor_dict[itemName], 1),
"big")
# Ap Boots start at +50 for some reason
if itemName == "AP Boost":
amountOfUsedBoosts -= 50
totalBoosts = (amountOfBoostsInInvo + amountOfUsedBoosts)
if totalBoosts <= amountOfItems - self.kh2seedsave["SoldBoosts"][itemName] and amountOfBoostsInInvo < 255:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(amountOfBoostsInInvo + 1).to_bytes(1, 'big'), 1)
except Exception as e:
logger.info("Line 573")
if self.kh2connected:
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
def finishedGame(ctx: KH2Context, message):
if ctx.kh2slotdata['FinalXemnas'] == 1:
if 0x1301ED in message[0]["locations"]:
ctx.finalxemnas = True
# three proofs
if ctx.kh2slotdata['Goal'] == 0:
if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, 1), "big") > 0 \
and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, 1), "big") > 0 \
and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, 1), "big") > 0:
if ctx.kh2slotdata['FinalXemnas'] == 1:
if ctx.finalxemnas:
return True
else:
return False
else:
return True
else:
return False
elif ctx.kh2slotdata['Goal'] == 1:
if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x3641, 1), "big") >= \
ctx.kh2slotdata['LuckyEmblemsRequired']:
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1)
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1)
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1)
if ctx.kh2slotdata['FinalXemnas'] == 1:
if ctx.finalxemnas:
return True
else:
return False
else:
return True
else:
return False
elif ctx.kh2slotdata['Goal'] == 2:
for boss in ctx.kh2slotdata["hitlist"]:
if boss in message[0]["locations"]:
ctx.amountOfPieces += 1
if ctx.amountOfPieces >= ctx.kh2slotdata["BountyRequired"]:
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1)
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1)
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1)
if ctx.kh2slotdata['FinalXemnas'] == 1:
if ctx.finalxemnas:
return True
else:
return False
else:
return True
else:
return False
async def kh2_watcher(ctx: KH2Context):
while not ctx.exit_event.is_set():
try:
if ctx.kh2connected and ctx.serverconneced:
ctx.sending = []
await asyncio.create_task(ctx.checkWorldLocations())
await asyncio.create_task(ctx.checkLevels())
await asyncio.create_task(ctx.checkSlots())
await asyncio.create_task(ctx.verifyChests())
await asyncio.create_task(ctx.verifyItems())
await asyncio.create_task(ctx.verifyLevel())
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
if finishedGame(ctx, message):
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
location_ids = []
location_ids = [location for location in message[0]["locations"] if location not in location_ids]
for location in location_ids:
currentWorld = int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + 0x0714DB8, 1), "big")
if location not in ctx.kh2seedsave["worldIdChecks"][str(currentWorld)]:
ctx.kh2seedsave["worldIdChecks"][str(currentWorld)].append(location)
if location in ctx.kh2LocalItems:
item = ctx.kh2slotdata["LocalItems"][str(location)]
await asyncio.create_task(ctx.give_item(item, "LocalItems"))
await ctx.send_msgs(message)
elif not ctx.kh2connected and ctx.serverconneced:
logger.info("Game is not open. Disconnecting from Server.")
await ctx.disconnect()
except Exception as e:
logger.info("Line 661")
if ctx.kh2connected:
logger.info("Connection Lost.")
ctx.kh2connected = False
logger.info(e)
await asyncio.sleep(0.5)
if __name__ == '__main__':
async def main(args):
ctx = KH2Context(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
progression_watcher = asyncio.create_task(
kh2_watcher(ctx), name="KH2ProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await ctx.shutdown()
import colorama
parser = get_base_parser(description="KH2 Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
asyncio.run(main(args))
colorama.deinit()
Utils.init_logging("KH2Client", exception_logger="Client")
launch()

View File

@@ -11,14 +11,19 @@ Scroll down to components= to add components to the launcher as well as setup.py
import argparse
import itertools
import logging
import multiprocessing
import shlex
import subprocess
import sys
import webbrowser
from os.path import isfile
from shutil import which
from typing import Sequence, Union, Optional
from typing import Callable, Sequence, Union, Optional
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier
import Utils
import settings
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths
if __name__ == "__main__":
import ModuleUpdate
@@ -29,7 +34,8 @@ from Utils import is_frozen, user_path, local_path, init_logging, open_filename,
def open_host_yaml():
file = user_path('host.yaml')
file = settings.get_settings().filename
assert file, "host.yaml missing"
if is_linux:
exe = which('sensible-editor') or which('gedit') or \
which('xdg-open') or which('gnome-open') or which('kde-open')
@@ -38,79 +44,103 @@ def open_host_yaml():
exe = which("open")
subprocess.Popen([exe, file])
else:
import webbrowser
webbrowser.open(file)
def open_patch():
suffixes = []
for c in components:
if isfile(get_exe(c)[-1]):
suffixes += c.file_identifier.suffixes if c.type == Type.CLIENT and \
isinstance(c.file_identifier, SuffixIdentifier) else []
if c.type == Type.CLIENT and \
isinstance(c.file_identifier, SuffixIdentifier) and \
(c.script_name is None or isfile(get_exe(c)[-1])):
suffixes += c.file_identifier.suffixes
try:
filename = open_filename('Select patch', (('Patches', suffixes),))
filename = open_filename("Select patch", (("Patches", suffixes),))
except Exception as e:
messagebox('Error', str(e), error=True)
messagebox("Error", str(e), error=True)
else:
file, _, component = identify(filename)
file, component = identify(filename)
if file and component:
launch([*get_exe(component), file], component.cli)
exe = get_exe(component)
if exe is None or not isfile(exe[-1]):
exe = get_exe("Launcher")
launch([*exe, file], component.cli)
def generate_yamls():
from Options import generate_yaml_templates
target = Utils.user_path("Players", "Templates")
generate_yaml_templates(target, False)
open_folder(target)
def browse_files():
file = user_path()
open_folder(user_path())
def open_folder(folder_path):
if is_linux:
exe = which('xdg-open') or which('gnome-open') or which('kde-open')
subprocess.Popen([exe, file])
subprocess.Popen([exe, folder_path])
elif is_macos:
exe = which("open")
subprocess.Popen([exe, file])
subprocess.Popen([exe, folder_path])
else:
import webbrowser
webbrowser.open(file)
webbrowser.open(folder_path)
def update_settings():
from settings import get_settings
get_settings().save()
components.extend([
# Functions
Component('Open host.yaml', func=open_host_yaml),
Component('Open Patch', func=open_patch),
Component('Browse Files', func=browse_files),
Component("Open host.yaml", func=open_host_yaml),
Component("Open Patch", func=open_patch),
Component("Generate Template Options", func=generate_yamls),
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
Component("Unrated/18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
Component("Browse Files", func=browse_files),
])
def identify(path: Union[None, str]):
if path is None:
return None, None, None
return None, None
for component in components:
if component.handles_file(path):
return path, component.script_name, component
return (None, None, None) if '/' in path or '\\' in path else (None, path, None)
return path, component
elif path == component.display_name or path == component.script_name:
return None, component
return None, None
def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]:
if isinstance(component, str):
name = component
component = None
if name.startswith('Archipelago'):
if name.startswith("Archipelago"):
name = name[11:]
if name.endswith('.exe'):
if name.endswith(".exe"):
name = name[:-4]
if name.endswith('.py'):
if name.endswith(".py"):
name = name[:-3]
if not name:
return None
for c in components:
if c.script_name == name or c.frozen_name == f'Archipelago{name}':
if c.script_name == name or c.frozen_name == f"Archipelago{name}":
component = c
break
if not component:
return None
if is_frozen():
suffix = '.exe' if is_windows else ''
return [local_path(f'{component.frozen_name}{suffix}')]
suffix = ".exe" if is_windows else ""
return [local_path(f"{component.frozen_name}{suffix}")] if component.frozen_name else None
else:
return [sys.executable, local_path(f'{component.script_name}.py')]
return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None
def launch(exe, in_terminal=False):
@@ -130,18 +160,21 @@ def launch(exe, in_terminal=False):
subprocess.Popen(exe)
refresh_components: Optional[Callable[[], None]] = None
def run_gui():
from kvui import App, ContainerLayout, GridLayout, Button, Label
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget
from kivy.core.window import Window
from kivy.uix.image import AsyncImage
from kivy.uix.relativelayout import RelativeLayout
class Launcher(App):
base_title: str = "Archipelago Launcher"
container: ContainerLayout
grid: GridLayout
_tools = {c.display_name: c for c in components if c.type == Type.TOOL and isfile(get_exe(c)[-1])}
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT and isfile(get_exe(c)[-1])}
_adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER and isfile(get_exe(c)[-1])}
_funcs = {c.display_name: c for c in components if c.type == Type.FUNC}
_tool_layout: Optional[ScrollBox] = None
_client_layout: Optional[ScrollBox] = None
def __init__(self, ctx=None):
self.title = self.base_title
@@ -149,42 +182,116 @@ def run_gui():
self.icon = r"data/icon.png"
super().__init__()
def _refresh_components(self) -> None:
def build_button(component: Component) -> Widget:
"""
Builds a button widget for a given component.
Args:
component (Component): The component associated with the button.
Returns:
None. The button is added to the parent grid layout.
"""
button = Button(text=component.display_name, size_hint_y=None, height=40)
button.component = component
button.bind(on_release=self.component_action)
if component.icon != "icon":
image = AsyncImage(source=icon_paths[component.icon],
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
box_layout = RelativeLayout(size_hint_y=None, height=40)
box_layout.add_widget(button)
box_layout.add_widget(image)
return box_layout
return button
# clear before repopulating
assert self._tool_layout and self._client_layout, "must call `build` first"
tool_children = reversed(self._tool_layout.layout.children)
for child in tool_children:
self._tool_layout.layout.remove_widget(child)
client_children = reversed(self._client_layout.layout.children)
for child in client_children:
self._client_layout.layout.remove_widget(child)
_tools = {c.display_name: c for c in components if c.type == Type.TOOL}
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT}
_adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER}
_miscs = {c.display_name: c for c in components if c.type == Type.MISC}
for (tool, client) in itertools.zip_longest(itertools.chain(
_tools.items(), _miscs.items(), _adjusters.items()
), _clients.items()):
# column 1
if tool:
self._tool_layout.layout.add_widget(build_button(tool[1]))
# column 2
if client:
self._client_layout.layout.add_widget(build_button(client[1]))
def build(self):
self.container = ContainerLayout()
self.grid = GridLayout(cols=2)
self.container.add_widget(self.grid)
self.grid.add_widget(Label(text="General", size_hint_y=None, height=40))
self.grid.add_widget(Label(text="Clients", size_hint_y=None, height=40))
self._tool_layout = ScrollBox()
self._tool_layout.layout.orientation = "vertical"
self.grid.add_widget(self._tool_layout)
self._client_layout = ScrollBox()
self._client_layout.layout.orientation = "vertical"
self.grid.add_widget(self._client_layout)
button_layout = self.grid # make buttons fill the window
for (tool, client) in itertools.zip_longest(itertools.chain(
self._tools.items(), self._funcs.items(), self._adjusters.items()), self._clients.items()):
# column 1
if tool:
button = Button(text=tool[0])
button.component = tool[1]
button.bind(on_release=self.component_action)
button_layout.add_widget(button)
else:
button_layout.add_widget(Label())
# column 2
if client:
button = Button(text=client[0])
button.component = client[1]
button.bind(on_press=self.component_action)
button_layout.add_widget(button)
else:
button_layout.add_widget(Label())
self._refresh_components()
global refresh_components
refresh_components = self._refresh_components
Window.bind(on_drop_file=self._on_drop_file)
return self.container
@staticmethod
def component_action(button):
if button.component.type == Type.FUNC:
if button.component.func:
button.component.func()
else:
launch(get_exe(button.component), button.component.cli)
def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None:
""" When a patch file is dropped into the window, run the associated component. """
file, component = identify(filename.decode())
if file and component:
run_component(component, file)
else:
logging.warning(f"unable to identify component for {filename}")
def _stop(self, *largs):
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
# Closing the window explicitly cleans it up.
self.root_window.close()
super()._stop(*largs)
Launcher().run()
# avoiding Launcher reference leak
# and don't try to do something with widgets after window closed
global refresh_components
refresh_components = None
def run_component(component: Component, *args):
if component.func:
component.func(*args)
if refresh_components:
refresh_components()
elif component.script_name:
subprocess.run([*get_exe(component.script_name), *args])
else:
logging.warning(f"Component {component} does not appear to be executable.")
def main(args: Optional[Union[argparse.Namespace, dict]] = None):
if isinstance(args, argparse.Namespace):
@@ -192,25 +299,41 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
elif not args:
args = {}
if "Patch|Game|Component" in args:
file, component, _ = identify(args["Patch|Game|Component"])
if args.get("Patch|Game|Component", None) is not None:
file, component = identify(args["Patch|Game|Component"])
if file:
args['file'] = file
if component:
args['component'] = component
if not component:
logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
if args["update_settings"]:
update_settings()
if 'file' in args:
subprocess.run([*get_exe(args['component']), args['file'], *args['args']])
run_component(args["component"], args["file"], *args["args"])
elif 'component' in args:
subprocess.run([*get_exe(args['component']), *args['args']])
else:
run_component(args["component"], *args["args"])
elif not args["update_settings"]:
run_gui()
if __name__ == '__main__':
init_logging('Launcher')
Utils.freeze_support()
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
parser = argparse.ArgumentParser(description='Archipelago Launcher')
parser.add_argument('Patch|Game|Component', type=str, nargs='?',
help="Pass either a patch file, a generated game or the name of a component to run.")
parser.add_argument('args', nargs="*", help="Arguments to pass to component.")
run_group = parser.add_argument_group("Run")
run_group.add_argument("--update_settings", action="store_true",
help="Update host.yaml and exit.")
run_group.add_argument("Patch|Game|Component", type=str, nargs="?",
help="Pass either a patch file, a generated game or the name of a component to run.")
run_group.add_argument("args", nargs="*",
help="Arguments to pass to component.")
main(parser.parse_args())
from worlds.LauncherComponents import processes
for process in processes:
# we await all child processes to close before we tear down the process host
# this makes it feel like each one is its own program, as the Launcher is closed now
process.join()

View File

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

View File

@@ -25,7 +25,7 @@ ModuleUpdate.update()
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
from Utils import output_path, local_path, user_path, open_file, get_cert_none_ssl_context, persistent_store, \
get_adjuster_settings, tkinter_center_window, init_logging
get_adjuster_settings, get_adjuster_settings_no_defaults, tkinter_center_window, init_logging
GAME_ALTTP = "A Link to the Past"
@@ -43,8 +43,49 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def _get_help_string(self, action):
return textwrap.dedent(action.help)
# See argparse.BooleanOptionalAction
class BooleanOptionalActionWithDisable(argparse.Action):
def __init__(self,
option_strings,
dest,
default=None,
type=None,
choices=None,
required=False,
help=None,
metavar=None):
def main():
_option_strings = []
for option_string in option_strings:
_option_strings.append(option_string)
if option_string.startswith('--'):
option_string = '--disable' + option_string[2:]
_option_strings.append(option_string)
if help is not None and default is not None:
help += " (default: %(default)s)"
super().__init__(
option_strings=_option_strings,
dest=dest,
nargs=0,
default=default,
type=type,
choices=choices,
required=required,
help=help,
metavar=metavar)
def __call__(self, parser, namespace, values, option_string=None):
if option_string in self.option_strings:
setattr(namespace, self.dest, not option_string.startswith('--disable'))
def format_usage(self):
return ' | '.join(self.option_strings)
def get_argparser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('rom', nargs="?", default='AP_LttP.sfc', help='Path to an ALttP rom to adjust.')
@@ -52,6 +93,8 @@ def main():
help='Path to an ALttP Japan(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default='info', const='info', nargs='?',
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--auto_apply', default='ask',
choices=['ask', 'always', 'never'], help='Whether or not to apply settings automatically in the future.')
parser.add_argument('--menuspeed', default='normal', const='normal', nargs='?',
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\
@@ -61,7 +104,7 @@ def main():
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--deathlink', help='Enable DeathLink system.', action='store_true')
parser.add_argument('--allowcollect', help='Allow collection of other player items', action='store_true')
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
parser.add_argument('--music', default=True, help='Enables/Disables game music.', action=BooleanOptionalActionWithDisable)
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?',
choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
help='''\
@@ -85,9 +128,6 @@ def main():
parser.add_argument('--ow_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
# parser.add_argument('--link_palettes', default='default',
# choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
# 'sick'])
parser.add_argument('--shield_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
@@ -107,10 +147,23 @@ def main():
Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted.
''')
parser.add_argument('--names', default='', type=str)
parser.add_argument('--sprite_pool', nargs='+', default=[], help='''
A list of sprites to pull from.
''')
parser.add_argument('--oof', help='''\
Path to a sound effect to replace Link's "oof" sound.
Needs to be in a .brr format and have a length of no
more than 2673 bytes, created from a 16-bit signed PCM
.wav at 12khz. https://github.com/boldowa/snesbrr
''')
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
args = parser.parse_args()
args.music = not args.disablemusic
return parser
def main():
parser = get_argparser()
args = parser.parse_args(namespace=get_adjuster_settings_no_defaults(GAME_ALTTP))
# set up logger
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
args.loglevel]
@@ -126,6 +179,13 @@ def main():
if args.sprite is not None and not os.path.isfile(args.sprite) and not Sprite.get_sprite_from_name(args.sprite):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1)
if args.oof is not None and not os.path.isfile(args.oof):
input('Could not find oof sound effect at given location. \nPress Enter to exit.')
sys.exit(1)
if args.oof is not None and os.path.getsize(args.oof) > 2673:
input('"oof" sound effect cannot exceed 2673 bytes. \nPress Enter to exit.')
sys.exit(1)
args, path = adjust(args=args)
if isinstance(args.sprite, Sprite):
@@ -165,7 +225,7 @@ def adjust(args):
world = getattr(args, "world")
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music,
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
args.sprite, args.oof, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
deathlink=args.deathlink, allowcollect=args.allowcollect)
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
rom.write_to_file(path)
@@ -180,7 +240,7 @@ def adjustGUI():
from tkinter import Tk, LEFT, BOTTOM, TOP, \
StringVar, Frame, Label, X, Entry, Button, filedialog, messagebox, ttk
from argparse import Namespace
from Main import __version__ as MWVersion
from Utils import __version__ as MWVersion
adjustWindow = Tk()
adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion)
set_icon(adjustWindow)
@@ -227,6 +287,7 @@ def adjustGUI():
guiargs.sprite = rom_vars.sprite
if rom_vars.sprite_pool:
guiargs.world = AdjusterWorld(rom_vars.sprite_pool)
guiargs.oof = rom_vars.oof
try:
guiargs, path = adjust(args=guiargs)
@@ -265,6 +326,7 @@ def adjustGUI():
else:
guiargs.sprite = rom_vars.sprite
guiargs.sprite_pool = rom_vars.sprite_pool
guiargs.oof = rom_vars.oof
persistent_store("adjuster", GAME_ALTTP, guiargs)
messagebox.showinfo(title="Success", message="Settings saved to persistent storage")
@@ -481,11 +543,38 @@ class BackgroundTaskProgressNullWindow(BackgroundTask):
self.stop()
class AttachTooltip(object):
def __init__(self, parent, text):
self._parent = parent
self._text = text
self._window = None
parent.bind('<Enter>', lambda event : self.show())
parent.bind('<Leave>', lambda event : self.hide())
def show(self):
if self._window or not self._text:
return
self._window = Toplevel(self._parent)
#remove window bar controls
self._window.wm_overrideredirect(1)
#adjust positioning
x, y, *_ = self._parent.bbox("insert")
x = x + self._parent.winfo_rootx() + 20
y = y + self._parent.winfo_rooty() + 20
self._window.wm_geometry("+{0}+{1}".format(x,y))
#show text
label = Label(self._window, text=self._text, justify=LEFT)
label.pack(ipadx=1)
def hide(self):
if self._window:
self._window.destroy()
self._window = None
def get_rom_frame(parent=None):
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
if not adjuster_settings:
adjuster_settings = Namespace()
adjuster_settings.baserom = "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
romFrame = Frame(parent)
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
@@ -513,32 +602,8 @@ def get_rom_frame(parent=None):
return romFrame, romVar
def get_rom_options_frame(parent=None):
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
defaults = {
"auto_apply": 'ask',
"music": True,
"reduceflashing": True,
"deathlink": False,
"sprite": None,
"quickswap": True,
"menuspeed": 'normal',
"heartcolor": 'red',
"heartbeep": 'normal',
"ow_palettes": 'default',
"uw_palettes": 'default',
"hud_palettes": 'default',
"sword_palettes": 'default',
"shield_palettes": 'default',
"sprite_pool": [],
"allowcollect": False,
}
if not adjuster_settings:
adjuster_settings = Namespace()
for key, defaultvalue in defaults.items():
if not hasattr(adjuster_settings, key):
setattr(adjuster_settings, key, defaultvalue)
romOptionsFrame = LabelFrame(parent, text="Rom options")
romOptionsFrame.columnconfigure(0, weight=1)
@@ -598,12 +663,50 @@ def get_rom_options_frame(parent=None):
spriteEntry.pack(side=LEFT)
spriteSelectButton.pack(side=LEFT)
oofDialogFrame = Frame(romOptionsFrame)
oofDialogFrame.grid(row=1, column=1)
baseOofLabel = Label(oofDialogFrame, text='"OOF" Sound:')
vars.oofNameVar = StringVar()
vars.oof = adjuster_settings.oof
def set_oof(oof_param):
nonlocal vars
if isinstance(oof_param, str) and os.path.isfile(oof_param) and os.path.getsize(oof_param) <= 2673:
vars.oof = oof_param
vars.oofNameVar.set(oof_param.rsplit('/',1)[-1])
else:
vars.oof = None
vars.oofNameVar.set('(unchanged)')
set_oof(adjuster_settings.oof)
oofEntry = Label(oofDialogFrame, textvariable=vars.oofNameVar)
def OofSelect():
nonlocal vars
oof_file = filedialog.askopenfilename(
filetypes=[("BRR files", ".brr"),
("All Files", "*")])
try:
set_oof(oof_file)
except Exception:
set_oof(None)
oofSelectButton = Button(oofDialogFrame, text='...', command=OofSelect)
AttachTooltip(oofSelectButton,
text="Select a .brr file no more than 2673 bytes.\n" + \
"This can be created from a <=0.394s 16-bit signed PCM .wav file at 12khz using snesbrr.")
baseOofLabel.pack(side=LEFT)
oofEntry.pack(side=LEFT)
oofSelectButton.pack(side=LEFT)
vars.quickSwapVar = IntVar(value=adjuster_settings.quickswap)
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar)
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
menuspeedFrame = Frame(romOptionsFrame)
menuspeedFrame.grid(row=1, column=1, sticky=E)
menuspeedFrame.grid(row=6, column=1, sticky=E)
menuspeedLabel = Label(menuspeedFrame, text='Menu speed')
menuspeedLabel.pack(side=LEFT)
vars.menuspeedVar = StringVar()
@@ -901,6 +1004,7 @@ class SpriteSelector():
self.add_to_sprite_pool(sprite)
def icon_section(self, frame_label, path, no_results_label):
os.makedirs(path, exist_ok=True)
frame = LabelFrame(self.window, labelwidget=frame_label, padx=5, pady=5)
frame.pack(side=TOP, fill=X)
@@ -1056,7 +1160,6 @@ class SpriteSelector():
def custom_sprite_dir(self):
return user_path("data", "sprites", "custom")
def get_image_for_sprite(sprite, gif_only: bool = False):
if not sprite.valid:
return None

376
MMBN3Client.py Normal file
View File

@@ -0,0 +1,376 @@
import asyncio
import hashlib
import json
import os
import multiprocessing
import subprocess
import zipfile
from asyncio import StreamReader, StreamWriter
import bsdiff4
from CommonClient import CommonContext, server_loop, gui_enabled, \
ClientCommandProcessor, logger, get_base_parser
import Utils
from NetUtils import ClientStatus
from worlds.mmbn3.Items import items_by_id
from worlds.mmbn3.Rom import get_base_rom_path
from worlds.mmbn3.Locations import all_locations, scoutable_locations
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_mmbn3.lua"
CONNECTION_REFUSED_STATUS = \
"Connection refused. Please start your emulator and make sure connector_mmbn3.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_mmbn3.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
CONNECTION_INCORRECT_ROM = "Supplied Base Rom does not match US GBA Blue Version. Please provide the correct ROM version"
script_version: int = 2
debugEnabled = False
locations_checked = []
items_sent = []
itemIndex = 1
CHECKSUM_BLUE = "6fe31df0144759b34ad666badaacc442"
class MMBN3CommandProcessor(ClientCommandProcessor):
def __init__(self, ctx):
super().__init__(ctx)
def _cmd_gba(self):
"""Check GBA Connection State"""
if isinstance(self.ctx, MMBN3Context):
logger.info(f"GBA Status: {self.ctx.gba_status}")
def _cmd_debug(self):
"""Toggle the Debug Text overlay in ROM"""
global debugEnabled
debugEnabled = not debugEnabled
logger.info("Debug Overlay Enabled" if debugEnabled else "Debug Overlay Disabled")
class MMBN3Context(CommonContext):
command_processor = MMBN3CommandProcessor
game = "MegaMan Battle Network 3"
items_handling = 0b101 # full local except starting items
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.gba_streams: (StreamReader, StreamWriter) = None
self.gba_sync_task = None
self.gba_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.location_table = {}
self.version_warning = False
self.auth_name = None
self.slot_data = dict()
self.patching_error = False
self.sent_hints = []
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(MMBN3Context, self).server_auth(password_requested)
if self.auth_name is None:
self.awaiting_rom = True
logger.info("No ROM detected, awaiting conection to Bizhawk to authenticate to the multiworld server")
return
logger.info("Attempting to decode from ROM... ")
self.awaiting_rom = False
self.auth = self.auth_name.decode("utf8").replace('\x00', '')
logger.info("Connecting as "+self.auth)
await self.send_connect(name=self.auth)
def run_gui(self):
from kvui import GameManager
class MMBN3Manager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago MegaMan Battle Network 3 Client"
self.ui = MMBN3Manager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
self.slot_data = args.get("slot_data", {})
print(self.slot_data)
class ItemInfo:
id = 0x00
sender = ""
type = ""
count = 1
itemName = "Unknown"
itemID = 0x00 # Item ID, Chip ID, etc.
subItemID = 0x00 # Code for chips, color for programs
itemIndex = 1
def __init__(self, id, sender, type):
self.id = id
self.sender = sender
self.type = type
def get_json(self):
json_data = {
"id": self.id,
"sender": self.sender,
"type": self.type,
"itemName": self.itemName,
"itemID": self.itemID,
"subItemID": self.subItemID,
"count": self.count,
"itemIndex": self.itemIndex
}
return json_data
def get_payload(ctx: MMBN3Context):
global debugEnabled
items_sent = []
for i, item in enumerate(ctx.items_received):
item_data = items_by_id[item.item]
new_item = ItemInfo(i, ctx.player_names[item.player], item_data.type)
new_item.itemIndex = i+1
new_item.itemName = item_data.itemName
new_item.type = item_data.type
new_item.itemID = item_data.itemID
new_item.subItemID = item_data.subItemID
new_item.count = item_data.count
items_sent.append(new_item)
return json.dumps({
"items": [item.get_json() for item in items_sent],
"debug": debugEnabled
})
async def parse_payload(payload: dict, ctx: MMBN3Context, force: bool):
# Game completion handling
if payload["gameComplete"] and not ctx.finished_game:
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL
}])
ctx.finished_game = True
# Locations handling
if ctx.location_table != payload["locations"]:
ctx.location_table = payload["locations"]
locs = [loc.id for loc in all_locations
if check_location_packet(loc, ctx.location_table)]
await ctx.send_msgs([{
"cmd": "LocationChecks",
"locations": locs
}])
# If trade hinting is enabled, send scout checks
if ctx.slot_data.get("trade_quest_hinting", 0) == 2:
trade_bits = [loc.id for loc in scoutable_locations
if check_location_scouted(loc, payload["locations"])]
scouted_locs = [loc for loc in trade_bits if loc not in ctx.sent_hints]
if len(scouted_locs) > 0:
ctx.sent_hints.extend(scouted_locs)
await ctx.send_msgs([{
"cmd": "LocationScouts",
"locations": scouted_locs,
"create_as_hint": 2
}])
def check_location_packet(location, memory):
if len(memory) == 0:
return False
# Our keys have to be strings to come through the JSON lua plugin so we have to turn our memory address into a string as well
location_key = hex(location.flag_byte)[2:]
byte = memory.get(location_key)
if byte is not None:
return byte & location.flag_mask
def check_location_scouted(location, memory):
if len(memory) == 0:
return False
location_key = hex(location.hint_flag)[2:]
byte = memory.get(location_key)
if byte is not None:
return byte & location.hint_flag_mask
async def gba_sync_task(ctx: MMBN3Context):
logger.info("Starting GBA connector. Use /gba for status information.")
if ctx.patching_error:
logger.error('Unable to Patch ROM. No ROM provided or ROM does not match US GBA Blue Version.')
while not ctx.exit_event.is_set():
error_status = None
if ctx.gba_streams:
(reader, writer) = ctx.gba_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with up to four fields
# 1. str: player name (always)
# 2. int: script version (always)
# 3. dict[str, byte]: value of location's memory byte
# 4. bool: whether the game currently registers as complete
data = await asyncio.wait_for(reader.readline(), timeout=10)
data_decoded = json.loads(data.decode())
reported_version = data_decoded.get("scriptVersion", 0)
if reported_version >= script_version:
if ctx.game is not None and "locations" in data_decoded:
# Not just a keep alive ping, parse
asyncio.create_task((parse_payload(data_decoded, ctx, False)))
if not ctx.auth:
ctx.auth_name = bytes(data_decoded["playerName"])
if ctx.awaiting_rom:
logger.info("Awaiting data from ROM...")
await ctx.server_auth(False)
else:
if not ctx.version_warning:
logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}."
"Please update to the latest version."
"Your connection to the Archipelago server will not be accepted.")
ctx.version_warning = True
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gba_streams = None
except ConnectionResetError:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gba_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gba_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gba_streams = None
if ctx.gba_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to GBA")
ctx.gba_status = CONNECTION_CONNECTED_STATUS
else:
ctx.gba_status = f"Was tentatively connected but error occurred: {error_status}"
elif error_status:
ctx.gba_status = error_status
logger.info("Lost connection to GBA and attempting to reconnect. Use /gba for status updates")
else:
try:
logger.debug("Attempting to connect to GBA")
ctx.gba_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 28922), timeout=10)
ctx.gba_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.gba_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.gba_status = CONNECTION_REFUSED_STATUS
continue
async def run_game(romfile):
options = Utils.get_options().get("mmbn3_options", None)
if options is None:
auto_start = True
else:
auto_start = options.get("rom_start", True)
if auto_start:
import webbrowser
webbrowser.open(romfile)
elif os.path.isfile(auto_start):
subprocess.Popen([auto_start, romfile],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def patch_and_run_game(apmmbn3_file):
base_name = os.path.splitext(apmmbn3_file)[0]
with zipfile.ZipFile(apmmbn3_file, 'r') as patch_archive:
try:
with patch_archive.open("delta.bsdiff4", 'r') as stream:
patch_data = stream.read()
except KeyError:
raise FileNotFoundError("Patch file missing from archive.")
rom_file = get_base_rom_path()
with open(rom_file, 'rb') as rom:
rom_bytes = rom.read()
patched_bytes = bsdiff4.patch(rom_bytes, patch_data)
patched_rom_file = base_name+".gba"
with open(patched_rom_file, 'wb') as patched_rom:
patched_rom.write(patched_bytes)
asyncio.create_task(run_game(patched_rom_file))
def confirm_checksum():
rom_file = get_base_rom_path()
if not os.path.exists(rom_file):
return False
with open(rom_file, 'rb') as rom:
rom_bytes = rom.read()
basemd5 = hashlib.md5()
basemd5.update(rom_bytes)
return CHECKSUM_BLUE == basemd5.hexdigest()
if __name__ == "__main__":
Utils.init_logging("MMBN3Client")
async def main():
multiprocessing.freeze_support()
parser = get_base_parser()
parser.add_argument("patch_file", default="", type=str, nargs="?",
help="Path to an APMMBN3 file")
args = parser.parse_args()
checksum_matches = confirm_checksum()
if checksum_matches:
if args.patch_file:
asyncio.create_task(patch_and_run_game(args.patch_file))
ctx = MMBN3Context(args.connect, args.password)
if not checksum_matches:
ctx.patching_error = True
ctx.server_task = asyncio.create_task(server_loop(ctx), name="Server Loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.gba_sync_task = asyncio.create_task(gba_sync_task(ctx), name="GBA Sync")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.gba_sync_task:
await ctx.gba_sync_task
import colorama
colorama.init()
asyncio.run(main())
colorama.deinit()

393
Main.py
View File

@@ -1,82 +1,53 @@
import collections
import concurrent.futures
import logging
import os
import time
import zlib
import concurrent.futures
import pickle
import tempfile
import time
import zipfile
from typing import Dict, List, Tuple, Optional, Set
import zlib
from typing import Dict, List, Optional, Set, Tuple, Union
from BaseClasses import Item, MultiWorld, CollectionState, Region, LocationProgressType, Location
import worlds
from worlds.alttp.SubClasses import LTTPRegionType
from worlds.alttp.Regions import is_main_entrance
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
from worlds.alttp.Shops import FillDisabledShopSlots
from Utils import output_path, get_options, __version__, version_tuple
from worlds.generic.Rules import locality_rules, exclusion_rules
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
from Options import StartInventoryPool
from Utils import __version__, output_path, version_tuple, get_settings
from settings import get_settings
from worlds import AutoWorld
from worlds.generic.Rules import exclusion_rules, locality_rules
ordered_areas = (
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total"
)
__all__ = ["main"]
def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None):
if not baked_server_options:
baked_server_options = get_options()["server_options"]
baked_server_options = get_settings().server_options.as_dict()
assert isinstance(baked_server_options, dict)
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
output_path.cached_path = args.outputpath
start = time.perf_counter()
# initialize the world
world = MultiWorld(args.multi)
# initialize the multiworld
multiworld = MultiWorld(args.multi)
logger = logging.getLogger()
world.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
world.plando_options = args.plando_options
multiworld.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
multiworld.plando_options = args.plando_options
multiworld.plando_items = args.plando_items.copy()
multiworld.plando_texts = args.plando_texts.copy()
multiworld.plando_connections = args.plando_connections.copy()
multiworld.game = args.game.copy()
multiworld.player_name = args.name.copy()
multiworld.sprite = args.sprite.copy()
multiworld.sprite_pool = args.sprite_pool.copy()
world.shuffle = args.shuffle.copy()
world.logic = args.logic.copy()
world.mode = args.mode.copy()
world.difficulty = args.difficulty.copy()
world.item_functionality = args.item_functionality.copy()
world.timer = args.timer.copy()
world.goal = args.goal.copy()
world.boss_shuffle = args.shufflebosses.copy()
world.enemy_health = args.enemy_health.copy()
world.enemy_damage = args.enemy_damage.copy()
world.beemizer_total_chance = args.beemizer_total_chance.copy()
world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
world.countdown_start_time = args.countdown_start_time.copy()
world.red_clock_time = args.red_clock_time.copy()
world.blue_clock_time = args.blue_clock_time.copy()
world.green_clock_time = args.green_clock_time.copy()
world.dungeon_counters = args.dungeon_counters.copy()
world.triforce_pieces_available = args.triforce_pieces_available.copy()
world.triforce_pieces_required = args.triforce_pieces_required.copy()
world.shop_shuffle = args.shop_shuffle.copy()
world.shuffle_prizes = args.shuffle_prizes.copy()
world.sprite_pool = args.sprite_pool.copy()
world.dark_room_logic = args.dark_room_logic.copy()
world.plando_items = args.plando_items.copy()
world.plando_texts = args.plando_texts.copy()
world.plando_connections = args.plando_connections.copy()
world.required_medallions = args.required_medallions.copy()
world.game = args.game.copy()
world.player_name = args.name.copy()
world.sprite = args.sprite.copy()
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
world.set_options(args)
world.set_item_links()
world.state = CollectionState(world)
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
multiworld.set_options(args)
multiworld.set_item_links()
multiworld.state = CollectionState(multiworld)
logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed)
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
@@ -105,58 +76,117 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
del item_digits, location_digits, item_count, location_count
AutoWorld.call_stage(world, "assert_generate")
# This assertion method should not be necessary to run if we are not outputting any multidata.
if not args.skip_output:
AutoWorld.call_stage(multiworld, "assert_generate")
AutoWorld.call_all(world, "generate_early")
AutoWorld.call_all(multiworld, "generate_early")
logger.info('')
for player in world.player_ids:
for item_name, count in world.start_inventory[player].value.items():
for player in multiworld.player_ids:
for item_name, count in multiworld.worlds[player].options.start_inventory.value.items():
for _ in range(count):
world.push_precollected(world.create_item(item_name, player))
multiworld.push_precollected(multiworld.create_item(item_name, player))
logger.info('Creating World.')
AutoWorld.call_all(world, "create_regions")
for item_name, count in getattr(multiworld.worlds[player].options,
"start_inventory_from_pool",
StartInventoryPool({})).value.items():
for _ in range(count):
multiworld.push_precollected(multiworld.create_item(item_name, player))
# remove from_pool items also from early items handling, as starting is plenty early.
early = multiworld.early_items[player].get(item_name, 0)
if early:
multiworld.early_items[player][item_name] = max(0, early-count)
remaining_count = count-early
if remaining_count > 0:
local_early = multiworld.early_local_items[player].get(item_name, 0)
if local_early:
multiworld.early_items[player][item_name] = max(0, local_early - remaining_count)
del local_early
del early
logger.info('Creating MultiWorld.')
AutoWorld.call_all(multiworld, "create_regions")
logger.info('Creating Items.')
AutoWorld.call_all(world, "create_items")
# All worlds should have finished creating all regions, locations, and entrances.
# Recache to ensure that they are all visible for locality rules.
world._recache()
AutoWorld.call_all(multiworld, "create_items")
logger.info('Calculating Access Rules.')
for player in world.player_ids:
for player in multiworld.player_ids:
# items can't be both local and non-local, prefer local
world.non_local_items[player].value -= world.local_items[player].value
world.non_local_items[player].value -= set(world.local_early_items[player])
multiworld.worlds[player].options.non_local_items.value -= multiworld.worlds[player].options.local_items.value
multiworld.worlds[player].options.non_local_items.value -= set(multiworld.local_early_items[player])
if world.players > 1:
locality_rules(world)
AutoWorld.call_all(multiworld, "set_rules")
for player in multiworld.player_ids:
exclusion_rules(multiworld, player, multiworld.worlds[player].options.exclude_locations.value)
multiworld.worlds[player].options.priority_locations.value -= multiworld.worlds[player].options.exclude_locations.value
for location_name in multiworld.worlds[player].options.priority_locations.value:
try:
location = multiworld.get_location(location_name, player)
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
if location_name not in multiworld.worlds[player].location_name_to_id:
raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e
else:
location.progress_type = LocationProgressType.PRIORITY
# Set local and non-local item rules.
if multiworld.players > 1:
locality_rules(multiworld)
else:
world.non_local_items[1].value = set()
world.local_items[1].value = set()
multiworld.worlds[1].options.non_local_items.value = set()
multiworld.worlds[1].options.local_items.value = set()
AutoWorld.call_all(multiworld, "generate_basic")
AutoWorld.call_all(world, "set_rules")
# remove starting inventory from pool items.
# Because some worlds don't actually create items during create_items this has to be as late as possible.
if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids):
new_items: List[Item] = []
depletion_pool: Dict[int, Dict[str, int]] = {
player: getattr(multiworld.worlds[player].options,
"start_inventory_from_pool",
StartInventoryPool({})).value.copy()
for player in multiworld.player_ids
}
for player, items in depletion_pool.items():
player_world: AutoWorld.World = multiworld.worlds[player]
for count in items.values():
for _ in range(count):
new_items.append(player_world.create_filler())
target: int = sum(sum(items.values()) for items in depletion_pool.values())
for i, item in enumerate(multiworld.itempool):
if depletion_pool[item.player].get(item.name, 0):
target -= 1
depletion_pool[item.player][item.name] -= 1
# quick abort if we have found all items
if not target:
new_items.extend(multiworld.itempool[i+1:])
break
else:
new_items.append(item)
for player in world.player_ids:
exclusion_rules(world, player, world.exclude_locations[player].value)
world.priority_locations[player].value -= world.exclude_locations[player].value
for location_name in world.priority_locations[player].value:
world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY
AutoWorld.call_all(world, "generate_basic")
# leftovers?
if target:
for player, remaining_items in depletion_pool.items():
remaining_items = {name: count for name, count in remaining_items.items() if count}
if remaining_items:
raise Exception(f"{multiworld.get_player_name(player)}"
f" is trying to remove items from their pool that don't exist: {remaining_items}")
assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change."
multiworld.itempool[:] = new_items
# temporary home for item links, should be moved out of Main
for group_id, group in world.groups.items():
for group_id, group in multiworld.groups.items():
def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]]
]:
classifications: Dict[str, int] = collections.defaultdict(int)
counters = {player: {name: 0 for name in shared_pool} for player in players}
for item in world.itempool:
for item in multiworld.itempool:
if item.player in counters and item.name in shared_pool:
counters[item.player][item.name] += 1
classifications[item.name] |= item.classification
@@ -191,13 +221,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
new_item.classification |= classifications[item_name]
new_itempool.append(new_item)
region = Region("Menu", group_id, world, "ItemLink")
world.regions.append(region)
locations = region.locations = []
for item in world.itempool:
region = Region("Menu", group_id, multiworld, "ItemLink")
multiworld.regions.append(region)
locations = region.locations
for item in multiworld.itempool:
count = common_item_count.get(item.player, {}).get(item.name, 0)
if count:
loc = Location(group_id, f"Item Link: {item.name} -> {world.player_name[item.player]} {count}",
loc = Location(group_id, f"Item Link: {item.name} -> {multiworld.player_name[item.player]} {count}",
None, region)
loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \
state.has(item_name, group_id_, count_)
@@ -208,10 +238,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
else:
new_itempool.append(item)
itemcount = len(world.itempool)
world.itempool = new_itempool
itemcount = len(multiworld.itempool)
multiworld.itempool = new_itempool
while itemcount > len(world.itempool):
while itemcount > len(multiworld.itempool):
items_to_add = []
for player in group["players"]:
if group["link_replacement"]:
@@ -219,88 +249,64 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
else:
item_player = player
if group["replacement_items"][player]:
items_to_add.append(AutoWorld.call_single(world, "create_item", item_player,
items_to_add.append(AutoWorld.call_single(multiworld, "create_item", item_player,
group["replacement_items"][player]))
else:
items_to_add.append(AutoWorld.call_single(world, "create_filler", item_player))
world.random.shuffle(items_to_add)
world.itempool.extend(items_to_add[:itemcount - len(world.itempool)])
items_to_add.append(AutoWorld.call_single(multiworld, "create_filler", item_player))
multiworld.random.shuffle(items_to_add)
multiworld.itempool.extend(items_to_add[:itemcount - len(multiworld.itempool)])
if any(world.item_links.values()):
world._recache()
world._all_state = None
if any(multiworld.item_links.values()):
multiworld._all_state = None
logger.info("Running Item Plando")
logger.info("Running Item Plando.")
distribute_planned(world)
distribute_planned(multiworld)
logger.info('Running Pre Main Fill.')
AutoWorld.call_all(world, "pre_fill")
AutoWorld.call_all(multiworld, "pre_fill")
logger.info(f'Filling the world with {len(world.itempool)} items.')
logger.info(f'Filling the multiworld with {len(multiworld.itempool)} items.')
if world.algorithm == 'flood':
flood_items(world) # different algo, biased towards early game progress items
elif world.algorithm == 'balanced':
distribute_items_restrictive(world)
if multiworld.algorithm == 'flood':
flood_items(multiworld) # different algo, biased towards early game progress items
elif multiworld.algorithm == 'balanced':
distribute_items_restrictive(multiworld, get_settings().generator.panic_method)
AutoWorld.call_all(world, 'post_fill')
AutoWorld.call_all(multiworld, 'post_fill')
if world.players > 1:
balance_multiworld_progression(world)
logger.info(f'Beginning output...')
if multiworld.players > 1 and not args.skip_prog_balancing:
balance_multiworld_progression(multiworld)
else:
logger.info("Progression balancing skipped.")
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
world.random.passthrough = False
multiworld.random.passthrough = False
outfilebase = 'AP_' + world.seed_name
if args.skip_output:
logger.info('Done. Skipped output/spoiler generation. Total Time: %s', time.perf_counter() - start)
return multiworld
logger.info(f'Beginning output...')
outfilebase = 'AP_' + multiworld.seed_name
output = tempfile.TemporaryDirectory()
with output as temp_dir:
with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool:
check_accessibility_task = pool.submit(world.fulfills_accessibility)
output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__
is not multiworld.worlds[player].generate_output.__code__]
with concurrent.futures.ThreadPoolExecutor(len(output_players) + 2) as pool:
check_accessibility_task = pool.submit(multiworld.fulfills_accessibility)
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
for player in world.player_ids:
output_file_futures = [pool.submit(AutoWorld.call_stage, multiworld, "generate_output", temp_dir)]
for player in output_players:
# skip starting a thread for methods that say "pass".
if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
output_file_futures.append(
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
output_file_futures.append(
pool.submit(AutoWorld.call_single, multiworld, "generate_output", player, temp_dir))
# collect ER hint info
er_hint_data: Dict[int, Dict[int, str]] = {}
AutoWorld.call_all(world, 'extend_hint_information', er_hint_data)
checks_in_area = {player: {area: list() for area in ordered_areas}
for player in range(1, world.players + 1)}
for player in range(1, world.players + 1):
checks_in_area[player]["Total"] = 0
for location in world.get_filled_locations():
if type(location.address) is int:
if location.game != "A Link to the Past":
checks_in_area[location.player]["Light World"].append(location.address)
else:
main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
if location.parent_region.dungeon:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif location.parent_region.type == LTTPRegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.type == LTTPRegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
elif main_entrance.parent_region.type == LTTPRegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
checks_in_area[location.player]["Total"] += 1
FillDisabledShopSlots(world)
AutoWorld.call_all(multiworld, 'extend_hint_information', er_hint_data)
def write_multidata():
import NetUtils
@@ -309,63 +315,78 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
games = {}
minimum_versions = {"server": AutoWorld.World.required_server_version, "clients": client_versions}
slot_info = {}
names = [[name for player, name in sorted(world.player_name.items())]]
for slot in world.player_ids:
player_world: AutoWorld.World = world.worlds[slot]
names = [[name for player, name in sorted(multiworld.player_name.items())]]
for slot in multiworld.player_ids:
player_world: AutoWorld.World = multiworld.worlds[slot]
minimum_versions["server"] = max(minimum_versions["server"], player_world.required_server_version)
client_versions[slot] = player_world.required_client_version
games[slot] = world.game[slot]
slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot],
world.player_types[slot])
for slot, group in world.groups.items():
games[slot] = world.game[slot]
slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot],
games[slot] = multiworld.game[slot]
slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], multiworld.game[slot],
multiworld.player_types[slot])
for slot, group in multiworld.groups.items():
games[slot] = multiworld.game[slot]
slot_info[slot] = NetUtils.NetworkSlot(group["name"], multiworld.game[slot], multiworld.player_types[slot],
group_members=sorted(group["players"]))
precollected_items = {player: [item.code for item in world_precollected if type(item.code) == int]
for player, world_precollected in world.precollected_items.items()}
precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))}
for player, world_precollected in multiworld.precollected_items.items()}
precollected_hints = {player: set() for player in range(1, multiworld.players + 1 + len(multiworld.groups))}
for slot in world.player_ids:
slot_data[slot] = world.worlds[slot].fill_slot_data()
for slot in multiworld.player_ids:
slot_data[slot] = multiworld.worlds[slot].fill_slot_data()
def precollect_hint(location):
entrance = er_hint_data.get(location.player, {}).get(location.address, "")
hint = NetUtils.Hint(location.item.player, location.player, location.address,
location.item.code, False, entrance, location.item.flags)
precollected_hints[location.player].add(hint)
if location.item.player not in world.groups:
if location.item.player not in multiworld.groups:
precollected_hints[location.item.player].add(hint)
else:
for player in world.groups[location.item.player]["players"]:
for player in multiworld.groups[location.item.player]["players"]:
precollected_hints[player].add(hint)
locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in world.player_ids}
for location in world.get_filled_locations():
locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in multiworld.player_ids}
for location in multiworld.get_filled_locations():
if type(location.address) == int:
assert location.item.code is not None, "item code None should be event, " \
"location.address should then also be None. Location: " \
f" {location}"
assert location.address not in locations_data[location.player], (
f"Locations with duplicate address. {location} and "
f"{locations_data[location.player][location.address]}")
locations_data[location.player][location.address] = \
location.item.code, location.item.player, location.item.flags
if location.name in world.start_location_hints[location.player]:
if location.name in multiworld.worlds[location.player].options.start_location_hints:
precollect_hint(location)
elif location.item.name in world.start_hints[location.item.player]:
elif location.item.name in multiworld.worlds[location.item.player].options.start_hints:
precollect_hint(location)
elif any([location.item.name in world.start_hints[player]
for player in world.groups.get(location.item.player, {}).get("players", [])]):
elif any([location.item.name in multiworld.worlds[player].options.start_hints
for player in multiworld.groups.get(location.item.player, {}).get("players", [])]):
precollect_hint(location)
# embedded data package
data_package = {
game_world.game: worlds.network_data_package["games"][game_world.game]
for game_world in world.worlds.values()
for game_world in multiworld.worlds.values()
}
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}
# get spheres -> filter address==None -> skip empty
spheres: List[Dict[int, Set[int]]] = []
for sphere in multiworld.get_spheres():
current_sphere: Dict[int, Set[int]] = collections.defaultdict(set)
for sphere_location in sphere:
if type(sphere_location.address) is int:
current_sphere[sphere_location.player].add(sphere_location.address)
if current_sphere:
spheres.append(dict(current_sphere))
multidata = {
"slot_data": slot_data,
"slot_info": slot_info,
"names": names, # TODO: remove after 0.3.9
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
"connect_names": {name: (0, player) for player, name in multiworld.player_name.items()},
"locations": locations_data,
"checks_in_area": checks_in_area,
"server_options": baked_server_options,
@@ -375,10 +396,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
"version": tuple(version_tuple),
"tags": ["AP"],
"minimum_versions": minimum_versions,
"seed_name": world.seed_name,
"seed_name": multiworld.seed_name,
"spheres": spheres,
"datapackage": data_package,
}
AutoWorld.call_all(world, "modify_multidata", multidata)
AutoWorld.call_all(multiworld, "modify_multidata", multidata)
multidata = zlib.compress(pickle.dumps(multidata), 9)
@@ -386,15 +408,14 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
f.write(bytes([3])) # version of format
f.write(multidata)
multidata_task = pool.submit(write_multidata)
output_file_futures.append(pool.submit(write_multidata))
if not check_accessibility_task.result():
if not world.can_beat_game():
if not multiworld.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.")
else:
logger.warning("Location Accessibility requirements not fulfilled.")
# retrieve exceptions via .result() if they occurred.
multidata_task.result()
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
if i % 10 == 0 or i == len(output_file_futures):
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
@@ -402,12 +423,12 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
if args.spoiler > 1:
logger.info('Calculating playthrough.')
world.spoiler.create_playthrough(create_paths=args.spoiler > 2)
multiworld.spoiler.create_playthrough(create_paths=args.spoiler > 2)
if args.spoiler:
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
multiworld.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
zipfilename = output_path(f"AP_{world.seed_name}.zip")
zipfilename = output_path(f"AP_{multiworld.seed_name}.zip")
logger.info(f"Creating final archive at {zipfilename}")
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
compresslevel=9) as zf:
@@ -415,4 +436,4 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
zf.write(file.path, arcname=file.name)
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
return world
return multiworld

View File

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

View File

@@ -1,15 +1,32 @@
import os
import sys
import subprocess
import multiprocessing
import warnings
local_dir = os.path.dirname(__file__)
requirements_files = {os.path.join(local_dir, 'requirements.txt')}
if sys.version_info < (3, 8, 6):
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
update_ran = getattr(sys, "frozen", False) # don't run update if environment is frozen/compiled
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process())
update_ran = _skip_update
class RequirementsSet(set):
def add(self, e):
global update_ran
update_ran &= _skip_update
super().add(e)
def update(self, *s):
global update_ran
update_ran &= _skip_update
super().update(*s)
local_dir = os.path.dirname(__file__)
requirements_files = RequirementsSet((os.path.join(local_dir, 'requirements.txt'),))
if not update_ran:
for entry in os.scandir(os.path.join(local_dir, "worlds")):
@@ -53,7 +70,7 @@ def install_pkg_resources(yes=False):
subprocess.call([sys.executable, "-m", "pip", "install", "--upgrade", "setuptools"])
def update(yes=False, force=False):
def update(yes: bool = False, force: bool = False) -> None:
global update_ran
if not update_ran:
update_ran = True
@@ -65,14 +82,23 @@ def update(yes=False, force=False):
install_pkg_resources(yes=yes)
import pkg_resources
prev = "" # if a line ends in \ we store here and merge later
for req_file in requirements_files:
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
if not os.path.exists(path):
path = os.path.join(os.path.dirname(__file__), req_file)
with open(path) as requirementsfile:
for line in requirementsfile:
if not line or line[0] == "#":
continue # ignore comments
if not line or line.lstrip(" \t")[0] == "#":
if not prev:
continue # ignore comments
line = ""
elif line.rstrip("\r\n").endswith("\\"):
prev = prev + line.rstrip("\r\n")[:-1] + " " # continue on next line
continue
line = prev + line
line = line.split("--hash=")[0] # remove hashes from requirement for version checking
prev = ""
if line.startswith(("https://", "git+https://")):
# extract name and version for url
rest = line.split('/')[-1]

File diff suppressed because it is too large Load Diff

View File

@@ -2,11 +2,12 @@ from __future__ import annotations
import typing
import enum
import warnings
from json import JSONEncoder, JSONDecoder
import websockets
from Utils import Version
from Utils import ByValue, Version
class JSONMessagePart(typing.TypedDict, total=False):
@@ -20,7 +21,7 @@ class JSONMessagePart(typing.TypedDict, total=False):
flags: int
class ClientStatus(enum.IntEnum):
class ClientStatus(ByValue, enum.IntEnum):
CLIENT_UNKNOWN = 0
CLIENT_CONNECTED = 5
CLIENT_READY = 10
@@ -28,7 +29,7 @@ class ClientStatus(enum.IntEnum):
CLIENT_GOAL = 30
class SlotType(enum.IntFlag):
class SlotType(ByValue, enum.IntFlag):
spectator = 0b00
player = 0b01
group = 0b10
@@ -39,7 +40,7 @@ class SlotType(enum.IntFlag):
return self.value != 0b01
class Permission(enum.IntFlag):
class Permission(ByValue, enum.IntFlag):
disabled = 0b000 # 0, completely disables access
enabled = 0b001 # 1, allows manual use
goal = 0b010 # 2, allows manual use after goal completion
@@ -197,7 +198,8 @@ class JSONtoTextParser(metaclass=HandlerMeta):
"slateblue": "6D8BE8",
"plum": "AF99EF",
"salmon": "FA8072",
"white": "FFFFFF"
"white": "FFFFFF",
"orange": "FF7700",
}
def __init__(self, ctx):
@@ -246,7 +248,7 @@ class JSONtoTextParser(metaclass=HandlerMeta):
def _handle_item_id(self, node: JSONMessagePart):
item_id = int(node["text"])
node["text"] = self.ctx.item_names[item_id]
node["text"] = self.ctx.item_names.lookup_in_slot(item_id, node["player"])
return self._handle_item_name(node)
def _handle_location_name(self, node: JSONMessagePart):
@@ -254,8 +256,8 @@ class JSONtoTextParser(metaclass=HandlerMeta):
return self._handle_color(node)
def _handle_location_id(self, node: JSONMessagePart):
item_id = int(node["text"])
node["text"] = self.ctx.location_names[item_id]
location_id = int(node["text"])
node["text"] = self.ctx.location_names.lookup_in_slot(location_id, node["player"])
return self._handle_location_name(node)
def _handle_entrance_name(self, node: JSONMessagePart):
@@ -289,8 +291,8 @@ def add_json_item(parts: list, item_id: int, player: int = 0, item_flags: int =
parts.append({"text": str(item_id), "player": player, "flags": item_flags, "type": JSONTypes.item_id, **kwargs})
def add_json_location(parts: list, item_id: int, player: int = 0, **kwargs) -> None:
parts.append({"text": str(item_id), "player": player, "type": JSONTypes.location_id, **kwargs})
def add_json_location(parts: list, location_id: int, player: int = 0, **kwargs) -> None:
parts.append({"text": str(location_id), "player": player, "type": JSONTypes.location_id, **kwargs})
class Hint(typing.NamedTuple):
@@ -343,3 +345,85 @@ class Hint(typing.NamedTuple):
@property
def local(self):
return self.receiving_player == self.finding_player
class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tuple[int, int, int]]]):
def __init__(self, values: typing.MutableMapping[int, typing.Dict[int, typing.Tuple[int, int, int]]]):
super().__init__(values)
if not self:
raise ValueError(f"Rejecting game with 0 players")
if len(self) != max(self):
raise ValueError("Player IDs not continuous")
if len(self.get(0, {})):
raise ValueError("Invalid player id 0 for location")
def find_item(self, slots: typing.Set[int], seeked_item_id: int
) -> typing.Generator[typing.Tuple[int, int, int, int, int], None, None]:
for finding_player, check_data in self.items():
for location_id, (item_id, receiving_player, item_flags) in check_data.items():
if receiving_player in slots and item_id == seeked_item_id:
yield finding_player, location_id, item_id, receiving_player, item_flags
def get_for_player(self, slot: int) -> typing.Dict[int, typing.Set[int]]:
import collections
all_locations: typing.Dict[int, typing.Set[int]] = collections.defaultdict(set)
for source_slot, location_data in self.items():
for location_id, values in location_data.items():
if values[1] == slot:
all_locations[source_slot].add(location_id)
return all_locations
def get_checked(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int
) -> typing.List[int]:
checked = state[team, slot]
if not checked:
# This optimizes the case where everyone connects to a fresh game at the same time.
return []
return [location_id for
location_id in self[slot] if
location_id in checked]
def get_missing(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int
) -> typing.List[int]:
checked = state[team, slot]
if not checked:
# This optimizes the case where everyone connects to a fresh game at the same time.
return list(self[slot])
return [location_id for
location_id in self[slot] if
location_id not in checked]
def get_remaining(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int
) -> typing.List[int]:
checked = state[team, slot]
player_locations = self[slot]
return sorted([player_locations[location_id][0] for
location_id in player_locations if
location_id not in checked])
if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub
LocationStore = _LocationStore
else:
try:
from _speedups import LocationStore
import _speedups
import os.path
if os.path.isfile("_speedups.pyx") and os.path.getctime(_speedups.__file__) < os.path.getctime("_speedups.pyx"):
warnings.warn(f"{_speedups.__file__} outdated! "
f"Please rebuild with `cythonize -b -i _speedups.pyx` or delete it!")
except ImportError:
try:
import pyximport
pyximport.install()
except ImportError:
pyximport = None
try:
from _speedups import LocationStore
except ImportError:
warnings.warn("_speedups not available. Falling back to pure python LocationStore. "
"Install a matching C++ compiler for your platform to compile _speedups.")
LocationStore = _LocationStore

View File

@@ -44,7 +44,7 @@ def adjustGUI():
StringVar, IntVar, Checkbutton, Frame, Label, X, Entry, Button, \
OptionMenu, filedialog, messagebox, ttk
from argparse import Namespace
from Main import __version__ as MWVersion
from Utils import __version__ as MWVersion
window = tk.Tk()
window.wm_title(f"Archipelago {MWVersion} OoT Adjuster")
@@ -195,10 +195,10 @@ def set_icon(window):
window.tk.call('wm', 'iconphoto', window._w, logo)
def adjust(args):
# Create a fake world and OOTWorld to use as a base
world = MultiWorld(1)
world.per_slot_randoms = {1: random}
ootworld = OOTWorld(world, 1)
# Create a fake multiworld and OOTWorld to use as a base
multiworld = MultiWorld(1)
multiworld.per_slot_randoms = {1: random}
ootworld = OOTWorld(multiworld, 1)
# Set options in the fake OOTWorld
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
result = getattr(args, name, None)

View File

@@ -17,9 +17,9 @@ from worlds.oot.N64Patch import apply_patch_file
from worlds.oot.Utils import data_path
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart oot_connector.lua"
CONNECTION_REFUSED_STATUS = "Connection refused. Please start your emulator and make sure oot_connector.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart oot_connector.lua"
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_oot.lua"
CONNECTION_REFUSED_STATUS = "Connection refused. Please start your emulator and make sure connector_oot.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_oot.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
@@ -100,7 +100,7 @@ class OoTContext(CommonContext):
await super(OoTContext, self).server_auth(password_requested)
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to Bizhawk to get player information')
logger.info('Awaiting connection to EmuHawk to get player information')
return
await self.send_connect()
@@ -179,6 +179,12 @@ async def parse_payload(payload: dict, ctx: OoTContext, force: bool):
locations = payload['locations']
collectibles = payload['collectibles']
# The Lua JSON library serializes an empty table into a list instead of a dict. Verify types for safety:
if isinstance(locations, list):
locations = {}
if isinstance(collectibles, list):
collectibles = {}
if ctx.location_table != locations or ctx.collectible_table != collectibles:
ctx.location_table = locations
ctx.collectible_table = collectibles
@@ -290,13 +296,16 @@ async def patch_and_run_game(apz5_file):
comp_path = base_name + '.z64'
# Load vanilla ROM, patch file, compress ROM
rom_file_name = Utils.get_options()["oot_options"]["rom_file"]
if not os.path.exists(rom_file_name):
rom_file_name = Utils.user_path(rom_file_name)
rom = Rom(rom_file_name)
apply_patch_file(rom, apz5_file,
sub_file=(os.path.basename(base_name) + '.zpf'
if zipfile.is_zipfile(apz5_file)
else None))
sub_file = None
if zipfile.is_zipfile(apz5_file):
for name in zipfile.ZipFile(apz5_file).namelist():
if name.endswith('.zpf'):
sub_file = name
break
apply_patch_file(rom, apz5_file, sub_file=sub_file)
rom.write_to_file(decomp_path)
os.chdir(data_path("Compress"))
compress_rom_file(decomp_path, comp_path)

View File

@@ -1,18 +1,38 @@
from __future__ import annotations
import abc
import functools
import logging
from copy import deepcopy
import math
import numbers
import typing
import random
import typing
import enum
from copy import deepcopy
from dataclasses import dataclass
from schema import Schema, And, Or, Optional
from Utils import get_fuzzy_results
from schema import And, Optional, Or, Schema
from typing_extensions import Self
from Utils import get_fuzzy_results, is_iterable_except_str
if typing.TYPE_CHECKING:
from BaseClasses import PlandoOptions
from worlds.AutoWorld import World
import pathlib
class OptionError(ValueError):
pass
class Visibility(enum.IntFlag):
none = 0b0000
template = 0b0001
simple_ui = 0b0010 # show option in simple menus, such as player-options
complex_ui = 0b0100 # show option in complex menus, such as weighted-options
spoiler = 0b1000
all = 0b1111
class AssembleOptions(abc.ABCMeta):
@@ -33,9 +53,14 @@ class AssembleOptions(abc.ABCMeta):
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
options.update(new_options)
# apply aliases, without name_lookup
aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if
name.startswith("alias_")}
aliases = attrs["aliases"] = {name[6:].lower(): option_id for name, option_id in attrs.items() if
name.startswith("alias_")}
assert (
name in {"Option", "VerifyKeys"} or # base abstract classes don't need default
"default" in attrs or
any(hasattr(base, "default") for base in bases)
), f"Option class {name} needs default value"
assert "random" not in aliases, "Choice option 'random' cannot be manually assigned."
# auto-alias Off and On being parsed as True and False
@@ -53,6 +78,7 @@ class AssembleOptions(abc.ABCMeta):
def verify(self, *args, **kwargs) -> None:
for f in verifiers:
f(self, *args, **kwargs)
attrs["verify"] = verify
else:
assert verifiers, "class Option is supposed to implement def verify"
@@ -90,7 +116,8 @@ T = typing.TypeVar('T')
class Option(typing.Generic[T], metaclass=AssembleOptions):
value: T
default = 0
default: typing.ClassVar[typing.Any] # something that __init__ will be able to convert to the correct type
visibility = Visibility.all
# convert option_name_long into Name Long as display_name, otherwise name_long is the result.
# Handled in get_option_name()
@@ -99,9 +126,28 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
# can be weighted between selections
supports_weighting = True
rich_text_doc: typing.Optional[bool] = None
"""Whether the WebHost should render the Option's docstring as rich text.
If this is True, the Option's docstring is interpreted as reStructuredText_,
the standard Python markup format. In the WebHost, it's rendered to HTML so
that lists, emphasis, and other rich text features are displayed properly.
If this is False, the docstring is instead interpreted as plain text, and
displayed as-is on the WebHost with whitespace preserved.
If this is None, it inherits the value of `World.rich_text_options_doc`. For
backwards compatibility, this defaults to False, but worlds are encouraged to
set it to True and use reStructuredText for their Option documentation.
.. _reStructuredText: https://docutils.sourceforge.io/rst.html
"""
# filled by AssembleOptions:
name_lookup: typing.Dict[T, str]
options: typing.Dict[str, int]
name_lookup: typing.ClassVar[typing.Dict[T, str]] # type: ignore
# https://github.com/python/typing/discussions/1460 the reason for this type: ignore
options: typing.ClassVar[typing.Dict[str, int]]
aliases: typing.ClassVar[typing.Dict[str, int]]
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.current_option_name})"
@@ -113,12 +159,6 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
def current_key(self) -> str:
return self.name_lookup[self.value]
def get_current_option_name(self) -> str:
"""Deprecated. use current_option_name instead. TODO remove around 0.4"""
logging.warning(DeprecationWarning(f"get_current_option_name for {self.__class__.__name__} is deprecated."
f" use current_option_name instead. Worlds should use {self}.current_key"))
return self.current_option_name
@property
def current_option_name(self) -> str:
"""For display purposes. Worlds should be using current_key."""
@@ -154,6 +194,8 @@ class FreeText(Option[str]):
"""Text option that allows users to enter strings.
Needs to be validated by the world or option definition."""
default = ""
def __init__(self, value: str):
assert isinstance(value, str), "value of FreeText must be a string"
self.value = value
@@ -174,9 +216,18 @@ class FreeText(Option[str]):
def get_option_name(cls, value: str) -> str:
return value
def __eq__(self, other):
if isinstance(other, self.__class__):
return other.value == self.value
elif isinstance(other, str):
return other == self.value
else:
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
class NumericOption(Option[int], numbers.Integral, abc.ABC):
default = 0
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards
# `int` is not a `numbers.Integral` according to the official typestubs
# (even though isinstance(5, numbers.Integral) == True)
@@ -208,6 +259,12 @@ class NumericOption(Option[int], numbers.Integral, abc.ABC):
else:
return self.value > other
def __ge__(self, other: typing.Union[int, NumericOption]) -> bool:
if isinstance(other, NumericOption):
return self.value >= other.value
else:
return self.value >= other
def __bool__(self) -> bool:
return bool(self.value)
@@ -344,7 +401,8 @@ class Toggle(NumericOption):
default = 0
def __init__(self, value: int):
assert value == 0 or value == 1, "value of Toggle can only be 0 or 1"
# if user puts in an invalid value, make it valid
value = int(bool(value))
self.value = value
@classmethod
@@ -586,7 +644,7 @@ class PlandoBosses(TextChoice, metaclass=BossMeta):
if isinstance(self.value, int):
return
from BaseClasses import PlandoOptions
if not(PlandoOptions.bosses & plando_options):
if not (PlandoOptions.bosses & plando_options):
# plando is disabled but plando options were given so pull the option and change it to an int
option = self.value.split(";")[-1]
self.value = self.options[option]
@@ -684,11 +742,25 @@ class Range(NumericOption):
return int(round(random.triangular(lower, end, tri), 0))
class SpecialRange(Range):
special_range_cutoff = 0
class NamedRange(Range):
special_range_names: typing.Dict[str, int] = {}
"""Special Range names have to be all lowercase as matching is done with text.lower()"""
def __init__(self, value: int) -> None:
if value < self.range_start and value not in self.special_range_names.values():
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__} " +
f"and is also not one of the supported named special values: {self.special_range_names}")
elif value > self.range_end and value not in self.special_range_names.values():
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " +
f"and is also not one of the supported named special values: {self.special_range_names}")
# See docstring
for key in self.special_range_names:
if key != key.lower():
raise Exception(f"{self.__class__.__name__} has an invalid special_range_names key: {key}. "
f"NamedRange keys must use only lowercase letters, and ideally should be snake_case.")
self.value = value
@classmethod
def from_text(cls, text: str) -> Range:
text = text.lower()
@@ -696,27 +768,18 @@ class SpecialRange(Range):
return cls(cls.special_range_names[text])
return super().from_text(text)
@classmethod
def weighted_range(cls, text) -> Range:
if text == "random-low":
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end, cls.special_range_cutoff))
elif text == "random-high":
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end, cls.range_end))
elif text == "random-middle":
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end))
elif text.startswith("random-range-"):
return cls.custom_range(text)
elif text == "random":
return cls(random.randint(cls.special_range_cutoff, cls.range_end))
else:
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
f"Acceptable values are: random, random-high, random-middle, random-low, "
f"random-range-low-<min>-<max>, random-range-middle-<min>-<max>, "
f"random-range-high-<min>-<max>, or random-range-<min>-<max>.")
class FreezeValidKeys(AssembleOptions):
def __new__(mcs, name, bases, attrs):
assert not "_valid_keys" in attrs, "'_valid_keys' gets set by FreezeValidKeys, define 'valid_keys' instead."
if "valid_keys" in attrs:
attrs["_valid_keys"] = frozenset(attrs["valid_keys"])
return super(FreezeValidKeys, mcs).__new__(mcs, name, bases, attrs)
class VerifyKeys:
valid_keys = frozenset()
class VerifyKeys(metaclass=FreezeValidKeys):
valid_keys: typing.Iterable = []
_valid_keys: frozenset # gets created by AssembleOptions from valid_keys
valid_keys_casefold: bool = False
convert_name_groups: bool = False
verify_item_name: bool = False
@@ -724,14 +787,14 @@ class VerifyKeys:
value: typing.Any
@classmethod
def verify_keys(cls, data: typing.List[str]):
def verify_keys(cls, data: typing.Iterable[str]) -> None:
if cls.valid_keys:
data = set(data)
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
extra = dataset - cls.valid_keys
extra = dataset - cls._valid_keys
if extra:
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
f"Allowed keys: {cls.valid_keys}.")
f"Allowed keys: {cls._valid_keys}.")
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
if self.convert_name_groups and self.verify_item_name:
@@ -760,8 +823,8 @@ class VerifyKeys:
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)")
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys):
default: typing.Dict[str, typing.Any] = {}
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
default = {}
supports_weighting = False
def __init__(self, value: typing.Dict[str, typing.Any]):
@@ -778,8 +841,14 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys):
def get_option_name(self, value):
return ", ".join(f"{key}: {v}" for key, v in value.items())
def __contains__(self, item):
return item in self.value
def __getitem__(self, item: str) -> typing.Any:
return self.value.__getitem__(item)
def __iter__(self) -> typing.Iterator[str]:
return self.value.__iter__()
def __len__(self) -> int:
return self.value.__len__()
class ItemDict(OptionDict):
@@ -792,11 +861,15 @@ class ItemDict(OptionDict):
class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
default: typing.List[typing.Any] = []
# Supports duplicate entries and ordering.
# If only unique entries are needed and input order of elements does not matter, OptionSet should be used instead.
# Not a docstring so it doesn't get grabbed by the options system.
default = ()
supports_weighting = False
def __init__(self, value: typing.List[typing.Any]):
self.value = deepcopy(value)
def __init__(self, value: typing.Iterable[typing.Any]):
self.value = list(deepcopy(value))
super(OptionList, self).__init__()
@classmethod
@@ -805,7 +878,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
@classmethod
def from_any(cls, data: typing.Any):
if type(data) == list:
if is_iterable_except_str(data):
cls.verify_keys(data)
return cls(data)
return cls.from_text(str(data))
@@ -818,7 +891,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
class OptionSet(Option[typing.Set[str]], VerifyKeys):
default: typing.Union[typing.Set[str], typing.FrozenSet[str]] = frozenset()
default = frozenset()
supports_weighting = False
def __init__(self, value: typing.Iterable[str]):
@@ -831,7 +904,7 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
@classmethod
def from_any(cls, data: typing.Any):
if isinstance(data, (list, set, frozenset)):
if is_iterable_except_str(data):
cls.verify_keys(data)
return cls(data)
return cls.from_text(str(data))
@@ -848,12 +921,237 @@ class ItemSet(OptionSet):
convert_name_groups = True
class PlandoText(typing.NamedTuple):
at: str
text: typing.List[str]
percentage: int = 100
PlandoTextsFromAnyType = typing.Union[
typing.Iterable[typing.Union[typing.Mapping[str, typing.Any], PlandoText, typing.Any]], typing.Any
]
class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
default = ()
supports_weighting = False
display_name = "Plando Texts"
def __init__(self, value: typing.Iterable[PlandoText]) -> None:
self.value = list(deepcopy(value))
super().__init__()
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
from BaseClasses import PlandoOptions
if self.value and not (PlandoOptions.texts & plando_options):
# plando is disabled but plando options were given so overwrite the options
self.value = []
logging.warning(f"The plando texts module is turned off, "
f"so text for {player_name} will be ignored.")
@classmethod
def from_any(cls, data: PlandoTextsFromAnyType) -> Self:
texts: typing.List[PlandoText] = []
if isinstance(data, typing.Iterable):
for text in data:
if isinstance(text, typing.Mapping):
if random.random() < float(text.get("percentage", 100)/100):
at = text.get("at", None)
if at is not None:
given_text = text.get("text", [])
if isinstance(given_text, str):
given_text = [given_text]
texts.append(PlandoText(
at,
given_text,
text.get("percentage", 100)
))
elif isinstance(text, PlandoText):
if random.random() < float(text.percentage/100):
texts.append(text)
else:
raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}")
cls.verify_keys([text.at for text in texts])
return cls(texts)
else:
raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}")
@classmethod
def get_option_name(cls, value: typing.List[PlandoText]) -> str:
return str({text.at: " ".join(text.text) for text in value})
def __iter__(self) -> typing.Iterator[PlandoText]:
yield from self.value
def __getitem__(self, index: typing.SupportsIndex) -> PlandoText:
return self.value.__getitem__(index)
def __len__(self) -> int:
return self.value.__len__()
class ConnectionsMeta(AssembleOptions):
def __new__(mcs, name: str, bases: tuple[type, ...], attrs: dict[str, typing.Any]):
if name != "PlandoConnections":
assert "entrances" in attrs, f"Please define valid entrances for {name}"
attrs["entrances"] = frozenset((connection.lower() for connection in attrs["entrances"]))
assert "exits" in attrs, f"Please define valid exits for {name}"
attrs["exits"] = frozenset((connection.lower() for connection in attrs["exits"]))
if "__doc__" not in attrs:
attrs["__doc__"] = PlandoConnections.__doc__
cls = super().__new__(mcs, name, bases, attrs)
return cls
class PlandoConnection(typing.NamedTuple):
class Direction:
entrance = "entrance"
exit = "exit"
both = "both"
entrance: str
exit: str
direction: typing.Literal["entrance", "exit", "both"] # TODO: convert Direction to StrEnum once 3.8 is dropped
percentage: int = 100
PlandoConFromAnyType = typing.Union[
typing.Iterable[typing.Union[typing.Mapping[str, typing.Any], PlandoConnection, typing.Any]], typing.Any
]
class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=ConnectionsMeta):
"""Generic connections plando. Format is:
- entrance: "Entrance Name"
exit: "Exit Name"
direction: "Direction"
percentage: 100
Direction must be one of 'entrance', 'exit', or 'both', and defaults to 'both' if omitted.
Percentage is an integer from 1 to 100, and defaults to 100 when omitted."""
display_name = "Plando Connections"
default = ()
supports_weighting = False
entrances: typing.ClassVar[typing.AbstractSet[str]]
exits: typing.ClassVar[typing.AbstractSet[str]]
duplicate_exits: bool = False
"""Whether or not exits should be allowed to be duplicate."""
def __init__(self, value: typing.Iterable[PlandoConnection]):
self.value = list(deepcopy(value))
super(PlandoConnections, self).__init__()
@classmethod
def validate_entrance_name(cls, entrance: str) -> bool:
return entrance.lower() in cls.entrances
@classmethod
def validate_exit_name(cls, exit: str) -> bool:
return exit.lower() in cls.exits
@classmethod
def can_connect(cls, entrance: str, exit: str) -> bool:
"""Checks that a given entrance can connect to a given exit.
By default, this will always return true unless overridden."""
return True
@classmethod
def validate_plando_connections(cls, connections: typing.Iterable[PlandoConnection]) -> None:
used_entrances: typing.List[str] = []
used_exits: typing.List[str] = []
for connection in connections:
entrance = connection.entrance
exit = connection.exit
direction = connection.direction
if direction not in (PlandoConnection.Direction.entrance,
PlandoConnection.Direction.exit,
PlandoConnection.Direction.both):
raise ValueError(f"Unknown direction: {direction}")
if entrance in used_entrances:
raise ValueError(f"Duplicate Entrance {entrance} not allowed.")
if not cls.duplicate_exits and exit in used_exits:
raise ValueError(f"Duplicate Exit {exit} not allowed.")
used_entrances.append(entrance)
used_exits.append(exit)
if not cls.validate_entrance_name(entrance):
raise ValueError(f"{entrance.title()} is not a valid entrance.")
if not cls.validate_exit_name(exit):
raise ValueError(f"{exit.title()} is not a valid exit.")
if not cls.can_connect(entrance, exit):
raise ValueError(f"Connection between {entrance.title()} and {exit.title()} is invalid.")
@classmethod
def from_any(cls, data: PlandoConFromAnyType) -> Self:
if not isinstance(data, typing.Iterable):
raise Exception(f"Cannot create plando connections from non-List value, got {type(data)}.")
value: typing.List[PlandoConnection] = []
for connection in data:
if isinstance(connection, typing.Mapping):
percentage = connection.get("percentage", 100)
if random.random() < float(percentage / 100):
entrance = connection.get("entrance", None)
if is_iterable_except_str(entrance):
entrance = random.choice(sorted(entrance))
exit = connection.get("exit", None)
if is_iterable_except_str(exit):
exit = random.choice(sorted(exit))
direction = connection.get("direction", "both")
if not entrance or not exit:
raise Exception("Plando connection must have an entrance and an exit.")
value.append(PlandoConnection(
entrance,
exit,
direction,
percentage
))
elif isinstance(connection, PlandoConnection):
if random.random() < float(connection.percentage / 100):
value.append(connection)
else:
raise Exception(f"Cannot create connection from non-Dict type, got {type(connection)}.")
cls.validate_plando_connections(value)
return cls(value)
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
from BaseClasses import PlandoOptions
if self.value and not (PlandoOptions.connections & plando_options):
# plando is disabled but plando options were given so overwrite the options
self.value = []
logging.warning(f"The plando connections module is turned off, "
f"so connections for {player_name} will be ignored.")
@classmethod
def get_option_name(cls, value: typing.List[PlandoConnection]) -> str:
return ", ".join(["%s %s %s" % (connection.entrance,
"<=>" if connection.direction == PlandoConnection.Direction.both else
"<=" if connection.direction == PlandoConnection.Direction.exit else
"=>",
connection.exit) for connection in value])
def __getitem__(self, index: typing.SupportsIndex) -> PlandoConnection:
return self.value.__getitem__(index)
def __iter__(self) -> typing.Iterator[PlandoConnection]:
yield from self.value
def __len__(self) -> int:
return len(self.value)
class Accessibility(Choice):
"""Set rules for reachability of your items/locations.
Locations: ensure everything can be reached and acquired.
Items: ensure all logically relevant items can be acquired.
Minimal: ensure what is needed to reach your goal can be acquired."""
- **Locations:** ensure everything can be reached and acquired.
- **Items:** ensure all logically relevant items can be acquired.
- **Minimal:** ensure what is needed to reach your goal can be acquired.
"""
display_name = "Accessibility"
rich_text_doc = True
option_locations = 0
option_items = 1
option_minimal = 2
@@ -861,13 +1159,16 @@ class Accessibility(Choice):
default = 1
class ProgressionBalancing(SpecialRange):
class ProgressionBalancing(NamedRange):
"""A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
A lower setting means more getting stuck. A higher setting means less getting stuck."""
A lower setting means more getting stuck. A higher setting means less getting stuck.
"""
default = 50
range_start = 0
range_end = 99
display_name = "Progression Balancing"
rich_text_doc = True
special_range_names = {
"disabled": 0,
"normal": 50,
@@ -875,59 +1176,128 @@ class ProgressionBalancing(SpecialRange):
}
common_options = {
"progression_balancing": ProgressionBalancing,
"accessibility": Accessibility
}
class OptionsMetaProperty(type):
def __new__(mcs,
name: str,
bases: typing.Tuple[type, ...],
attrs: typing.Dict[str, typing.Any]) -> "OptionsMetaProperty":
for attr_type in attrs.values():
assert not isinstance(attr_type, AssembleOptions), \
f"Options for {name} should be type hinted on the class, not assigned"
return super().__new__(mcs, name, bases, attrs)
@property
@functools.lru_cache(maxsize=None)
def type_hints(cls) -> typing.Dict[str, typing.Type[Option[typing.Any]]]:
"""Returns type hints of the class as a dictionary."""
return typing.get_type_hints(cls)
@dataclass
class CommonOptions(metaclass=OptionsMetaProperty):
progression_balancing: ProgressionBalancing
accessibility: Accessibility
def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]:
"""
Returns a dictionary of [str, Option.value]
:param option_names: names of the options to return
:param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`
"""
option_results = {}
for option_name in option_names:
if option_name in type(self).type_hints:
if casing == "snake":
display_name = option_name
elif casing == "camel":
split_name = [name.title() for name in option_name.split("_")]
split_name[0] = split_name[0].lower()
display_name = "".join(split_name)
elif casing == "pascal":
display_name = "".join([name.title() for name in option_name.split("_")])
elif casing == "kebab":
display_name = option_name.replace("_", "-")
else:
raise ValueError(f"{casing} is invalid casing for as_dict. "
"Valid names are 'snake', 'camel', 'pascal', 'kebab'.")
value = getattr(self, option_name).value
if isinstance(value, set):
value = sorted(value)
option_results[display_name] = value
else:
raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}")
return option_results
class LocalItems(ItemSet):
"""Forces these items to be in their native world."""
display_name = "Local Items"
rich_text_doc = True
class NonLocalItems(ItemSet):
"""Forces these items to be outside their native world."""
display_name = "Not Local Items"
display_name = "Non-local Items"
rich_text_doc = True
class StartInventory(ItemDict):
"""Start with these items."""
verify_item_name = True
display_name = "Start Inventory"
rich_text_doc = True
class StartInventoryPool(StartInventory):
"""Start with these items and don't place them in the world.
The game decides what the replacement items will be.
"""
verify_item_name = True
display_name = "Start Inventory from Pool"
rich_text_doc = True
class StartHints(ItemSet):
"""Start with these item's locations prefilled into the !hint command."""
"""Start with these item's locations prefilled into the ``!hint`` command."""
display_name = "Start Hints"
rich_text_doc = True
class LocationSet(OptionSet):
verify_location_name = True
convert_name_groups = True
class StartLocationHints(LocationSet):
"""Start with these locations and their item prefilled into the !hint command"""
"""Start with these locations and their item prefilled into the ``!hint`` command."""
display_name = "Start Location Hints"
rich_text_doc = True
class ExcludeLocations(LocationSet):
"""Prevent these locations from having an important item"""
"""Prevent these locations from having an important item."""
display_name = "Excluded Locations"
rich_text_doc = True
class PriorityLocations(LocationSet):
"""Prevent these locations from having an unimportant item"""
"""Prevent these locations from having an unimportant item."""
display_name = "Priority Locations"
rich_text_doc = True
class DeathLink(Toggle):
"""When you die, everyone dies. Of course the reverse is true too."""
display_name = "Death Link"
rich_text_doc = True
class ItemLinks(OptionList):
"""Share part of your item pool with other players."""
display_name = "Item Links"
rich_text_doc = True
default = []
schema = Schema([
{
@@ -942,7 +1312,8 @@ class ItemLinks(OptionList):
])
@staticmethod
def verify_items(items: typing.List[str], item_link: str, pool_name: str, world, allow_item_groups: bool = True) -> typing.Set:
def verify_items(items: typing.List[str], item_link: str, pool_name: str, world,
allow_item_groups: bool = True) -> typing.Set:
pool = set()
for item_name in items:
if item_name not in world.item_names and (not allow_item_groups or item_name not in world.item_name_groups):
@@ -988,19 +1359,134 @@ class ItemLinks(OptionList):
raise Exception(f"item_link {link['name']} has {intersection} "
f"items in both its local_items and non_local_items pool.")
link.setdefault("link_replacement", None)
link["item_pool"] = list(pool)
per_game_common_options = {
**common_options, # can be overwritten per-game
"local_items": LocalItems,
"non_local_items": NonLocalItems,
"start_inventory": StartInventory,
"start_hints": StartHints,
"start_location_hints": StartLocationHints,
"exclude_locations": ExcludeLocations,
"priority_locations": PriorityLocations,
"item_links": ItemLinks
}
class Removed(FreeText):
"""This Option has been Removed."""
rich_text_doc = True
default = ""
visibility = Visibility.none
def __init__(self, value: str):
if value:
raise Exception("Option removed, please update your options file.")
super().__init__(value)
@dataclass
class PerGameCommonOptions(CommonOptions):
local_items: LocalItems
non_local_items: NonLocalItems
start_inventory: StartInventory
start_hints: StartHints
start_location_hints: StartLocationHints
exclude_locations: ExcludeLocations
priority_locations: PriorityLocations
item_links: ItemLinks
@dataclass
class DeathLinkMixin:
death_link: DeathLink
class OptionGroup(typing.NamedTuple):
"""Define a grouping of options."""
name: str
"""Name of the group to categorize these options in for display on the WebHost and in generated YAMLS."""
options: typing.List[typing.Type[Option[typing.Any]]]
"""Options to be in the defined group."""
start_collapsed: bool = False
"""Whether the group will start collapsed on the WebHost options pages."""
item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints,
StartLocationHints, ExcludeLocations, PriorityLocations, ItemLinks]
"""
Options that are always populated in "Item & Location Options" Option Group. Cannot be moved to another group.
If desired, a custom "Item & Location Options" Option Group can be defined, but only for adding additional options to
it.
"""
def get_option_groups(world: typing.Type[World], visibility_level: Visibility = Visibility.template) -> typing.Dict[
str, typing.Dict[str, typing.Type[Option[typing.Any]]]]:
"""Generates and returns a dictionary for the option groups of a specified world."""
option_groups = {option: option_group.name
for option_group in world.web.option_groups
for option in option_group.options}
# add a default option group for uncategorized options to get thrown into
ordered_groups = ["Game Options"]
[ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups]
grouped_options = {group: {} for group in ordered_groups}
for option_name, option in world.options_dataclass.type_hints.items():
if visibility_level & option.visibility:
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
# if the world doesn't have any ungrouped options, this group will be empty so just remove it
if not grouped_options["Game Options"]:
del grouped_options["Game Options"]
return grouped_options
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None:
import os
import yaml
from jinja2 import Template
from worlds import AutoWorldRegister
from Utils import local_path, __version__
full_path: str
os.makedirs(target_folder, exist_ok=True)
# clean out old
for file in os.listdir(target_folder):
full_path = os.path.join(target_folder, file)
if os.path.isfile(full_path) and full_path.endswith(".yaml"):
os.unlink(full_path)
def dictify_range(option: Range):
data = {option.default: 50}
for sub_option in ["random", "random-low", "random-high"]:
if sub_option != option.default:
data[sub_option] = 0
notes = {}
for name, number in getattr(option, "special_range_names", {}).items():
notes[name] = f"equivalent to {number}"
if number in data:
data[name] = data[number]
del data[number]
else:
data[name] = 0
return data, notes
def yaml_dump_scalar(scalar) -> str:
# yaml dump may add end of document marker and newlines.
return yaml.dump(scalar).replace("...\n", "").strip()
for game_name, world in AutoWorldRegister.world_types.items():
if not world.hidden or generate_hidden:
option_groups = get_option_groups(world)
with open(local_path("data", "options.yaml")) as f:
file_data = f.read()
res = Template(file_data).render(
option_groups=option_groups,
__version__=__version__, game=game_name, yaml_dump=yaml_dump_scalar,
dictify_range=dictify_range,
)
del file_data
with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f:
f.write(res)
if __name__ == "__main__":

View File

@@ -8,7 +8,7 @@ if __name__ == "__main__":
import ModuleUpdate
ModuleUpdate.update()
from worlds.Files import AutoPatchRegister, APDeltaPatch
from worlds.Files import AutoPatchRegister, APAutoPatchInterface
class RomMeta(TypedDict):
@@ -20,7 +20,7 @@ class RomMeta(TypedDict):
def create_rom_file(patch_file: str) -> Tuple[RomMeta, str]:
auto_handler = AutoPatchRegister.get_handler(patch_file)
if auto_handler:
handler: APDeltaPatch = auto_handler(patch_file)
handler: APAutoPatchInterface = auto_handler(patch_file)
target = os.path.splitext(patch_file)[0]+handler.result_file_ending
handler.patch(target)
return {"server": handler.server,

View File

@@ -1,351 +0,0 @@
import asyncio
import json
import time
import os
import bsdiff4
import subprocess
import zipfile
from asyncio import StreamReader, StreamWriter
from typing import List
import Utils
from Utils import async_start
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
get_base_parser
from worlds.pokemon_rb.locations import location_data
from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}}
location_bytes_bits = {}
for location in location_data:
if location.ram_address is not None:
if type(location.ram_address) == list:
location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address
location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit},
{'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}]
else:
location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address
location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit}
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart pkmn_rb.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure pkmn_rb.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart pkmn_rb.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
DISPLAY_MSGS = True
SCRIPT_VERSION = 3
class GBCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: CommonContext):
super().__init__(ctx)
def _cmd_gb(self):
"""Check Gameboy Connection State"""
if isinstance(self.ctx, GBContext):
logger.info(f"Gameboy Status: {self.ctx.gb_status}")
class GBContext(CommonContext):
command_processor = GBCommandProcessor
game = 'Pokemon Red and Blue'
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.gb_streams: (StreamReader, StreamWriter) = None
self.gb_sync_task = None
self.messages = {}
self.locations_array = None
self.gb_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.display_msgs = True
self.deathlink_pending = False
self.set_deathlink = False
self.client_compatibility_mode = 0
self.items_handling = 0b001
self.sent_release = False
self.sent_collect = False
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(GBContext, self).server_auth(password_requested)
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to Bizhawk to get Player information')
return
await self.send_connect()
def _set_message(self, msg: str, msg_id: int):
if DISPLAY_MSGS:
self.messages[(time.time(), msg_id)] = msg
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
self.locations_array = None
if 'death_link' in args['slot_data'] and args['slot_data']['death_link']:
self.set_deathlink = True
elif cmd == "RoomInfo":
self.seed_name = args['seed_name']
elif cmd == 'Print':
msg = args['text']
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "ReceivedItems":
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
def on_deathlink(self, data: dict):
self.deathlink_pending = True
super().on_deathlink(data)
def run_gui(self):
from kvui import GameManager
class GBManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Pokémon Client"
self.ui = GBManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def get_payload(ctx: GBContext):
current_time = time.time()
ret = json.dumps(
{
"items": [item.item for item in ctx.items_received],
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
if key[0] > current_time - 10},
"deathlink": ctx.deathlink_pending,
"options": ((ctx.permissions['release'] in ('goal', 'enabled')) * 2) + (ctx.permissions['collect'] in ('goal', 'enabled'))
}
)
ctx.deathlink_pending = False
return ret
async def parse_locations(data: List, ctx: GBContext):
locations = []
flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20],
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E],
"Rod": data[0x140 + 0x20 + 0x0E:0x140 + 0x20 + 0x0E + 0x01]}
if len(data) > 0x140 + 0x20 + 0x0E + 0x01:
flags["DexSanityFlag"] = data[0x140 + 0x20 + 0x0E + 0x01:]
else:
flags["DexSanityFlag"] = [0] * 19
for flag_type, loc_map in location_map.items():
for flag, loc_id in loc_map.items():
if flag_type == "list":
if (flags["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 << location_bytes_bits[loc_id][0]['bit']
and flags["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 << location_bytes_bits[loc_id][1]['bit']):
locations.append(loc_id)
elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']:
locations.append(loc_id)
if flags["EventFlag"][280] & 1 and not ctx.finished_game:
await ctx.send_msgs([
{"cmd": "StatusUpdate",
"status": 30}
])
ctx.finished_game = True
if locations == ctx.locations_array:
return
ctx.locations_array = locations
if locations is not None:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}])
async def gb_sync_task(ctx: GBContext):
logger.info("Starting GB connector. Use /gb for status information")
while not ctx.exit_event.is_set():
error_status = None
if ctx.gb_streams:
(reader, writer) = ctx.gb_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with up to two fields:
# 1. A keepalive response of the Players Name (always)
# 2. An array representing the memory values of the locations area (if in game)
data = await asyncio.wait_for(reader.readline(), timeout=5)
data_decoded = json.loads(data.decode())
if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION:
msg = "You are connecting with an incompatible Lua script version. Ensure your connector Lua " \
"and PokemonClient are from the same Archipelago installation."
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
ctx.client_compatibility_mode = data_decoded['clientCompatibilityVersion']
if ctx.client_compatibility_mode == 0:
ctx.items_handling = 0b101 # old patches will not have local start inventory, must be requested
if ctx.seed_name and ctx.seed_name != ''.join([chr(i) for i in data_decoded['seedName'] if i != 0]):
msg = "The server is running a different multiworld than your client is. (invalid seed_name)"
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
ctx.seed_name = ''.join([chr(i) for i in data_decoded['seedName'] if i != 0])
if not ctx.auth:
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
if ctx.auth == '':
msg = "Invalid ROM detected. No player name built into the ROM."
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
if ctx.awaiting_rom:
await ctx.server_auth(False)
if 'locations' in data_decoded and ctx.game and ctx.gb_status == CONNECTION_CONNECTED_STATUS \
and not error_status and ctx.auth:
# Not just a keep alive ping, parse
async_start(parse_locations(data_decoded['locations'], ctx))
if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags:
await ctx.send_death(ctx.auth + " is out of usable Pokémon! " + ctx.auth + " blacked out!")
if 'options' in data_decoded:
msgs = []
if data_decoded['options'] & 4 and not ctx.sent_release:
ctx.sent_release = True
msgs.append({"cmd": "Say", "text": "!release"})
if data_decoded['options'] & 8 and not ctx.sent_collect:
ctx.sent_collect = True
msgs.append({"cmd": "Say", "text": "!collect"})
if msgs:
await ctx.send_msgs(msgs)
if ctx.set_deathlink:
await ctx.update_death_link(True)
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gb_streams = None
except ConnectionResetError as e:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gb_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gb_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gb_streams = None
if ctx.gb_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to Gameboy")
ctx.gb_status = CONNECTION_CONNECTED_STATUS
else:
ctx.gb_status = f"Was tentatively connected but error occured: {error_status}"
elif error_status:
ctx.gb_status = error_status
logger.info("Lost connection to Gameboy and attempting to reconnect. Use /gb for status updates")
else:
try:
logger.debug("Attempting to connect to Gameboy")
ctx.gb_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 17242), timeout=10)
ctx.gb_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.gb_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.gb_status = CONNECTION_REFUSED_STATUS
continue
async def run_game(romfile):
auto_start = Utils.get_options()["pokemon_rb_options"].get("rom_start", True)
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
elif os.path.isfile(auto_start):
subprocess.Popen([auto_start, romfile],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def patch_and_run_game(game_version, patch_file, ctx):
base_name = os.path.splitext(patch_file)[0]
comp_path = base_name + '.gb'
if game_version == "blue":
delta_patch = BlueDeltaPatch
else:
delta_patch = RedDeltaPatch
try:
base_rom = delta_patch.get_source_data()
except Exception as msg:
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
with patch_archive.open('delta.bsdiff4', 'r') as stream:
patch = stream.read()
patched_rom_data = bsdiff4.patch(base_rom, patch)
with open(comp_path, "wb") as patched_rom_file:
patched_rom_file.write(patched_rom_data)
async_start(run_game(comp_path))
if __name__ == '__main__':
Utils.init_logging("PokemonClient")
options = Utils.get_options()
async def main():
parser = get_base_parser()
parser.add_argument('patch_file', default="", type=str, nargs="?",
help='Path to an APRED or APBLUE patch file')
args = parser.parse_args()
ctx = GBContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.gb_sync_task = asyncio.create_task(gb_sync_task(ctx), name="GB Sync")
if args.patch_file:
ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower()
if ext == "apred":
logger.info("APRED file supplied, beginning patching process...")
async_start(patch_and_run_game("red", args.patch_file, ctx))
elif ext == "apblue":
logger.info("APBLUE file supplied, beginning patching process...")
async_start(patch_and_run_game("blue", args.patch_file, ctx))
else:
logger.warning(f"Unknown patch file extension {ext}")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.gb_sync_task:
await ctx.gb_sync_task
import colorama
colorama.init()
asyncio.run(main())
colorama.deinit()

View File

@@ -1,8 +1,10 @@
# [Archipelago](https://archipelago.gg) ![Discord Shield](https://discordapp.com/api/guilds/731205301247803413/widget.png?style=shield) | [Install](https://github.com/ArchipelagoMW/Archipelago/releases)
Archipelago provides a generic framework for developing multiworld capability for game randomizers. In all cases, presently, Archipelago is also the randomizer itself.
Archipelago provides a generic framework for developing multiworld capability for game randomizers. In all cases,
presently, Archipelago is also the randomizer itself.
Currently, the following games are supported:
* The Legend of Zelda: A Link to the Past
* Factorio
* Minecraft
@@ -25,7 +27,7 @@ Currently, the following games are supported:
* Hollow Knight
* The Witness
* Sonic Adventure 2: Battle
* Starcraft 2: Wings of Liberty
* Starcraft 2
* Donkey Kong Country 3
* Dark Souls 3
* Super Mario World
@@ -43,6 +45,33 @@ Currently, the following games are supported:
* The Legend of Zelda: Link's Awakening DX
* Clique
* Adventure
* DLC Quest
* Noita
* Undertale
* Bumper Stickers
* Mega Man Battle Network 3: Blue Version
* Muse Dash
* DOOM 1993
* Terraria
* Lingo
* Pokémon Emerald
* DOOM II
* Shivers
* Heretic
* Landstalker: The Treasures of King Nole
* Final Fantasy Mystic Quest
* TUNIC
* Kirby's Dream Land 3
* Celeste 64
* Zork Grand Inquisitor
* Castlevania 64
* A Short Hike
* Yoshi's Island
* Mario & Luigi: Superstar Saga
* Bomb Rush Cyberfunk
* Aquaria
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
* A Hat in Time
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
@@ -50,36 +79,57 @@ windows binaries.
## History
Archipelago is built upon a strong legacy of brilliant hobbyists. We want to honor that legacy by showing it here. The repositories which Archipelago is built upon, inspired by, or otherwise owes its gratitude to are:
Archipelago is built upon a strong legacy of brilliant hobbyists. We want to honor that legacy by showing it here.
The repositories which Archipelago is built upon, inspired by, or otherwise owes its gratitude to are:
* [bonta0's MultiWorld](https://github.com/Bonta0/ALttPEntranceRandomizer/tree/multiworld_31)
* [AmazingAmpharos' Entrance Randomizer](https://github.com/AmazingAmpharos/ALttPEntranceRandomizer)
* [VT Web Randomizer](https://github.com/sporchia/alttp_vt_randomizer)
* [Dessyreqt's alttprandomizer](https://github.com/Dessyreqt/alttprandomizer)
* [Zarby89's](https://github.com/Ijwu/Enemizer/commits?author=Zarby89) and [sosuke3's](https://github.com/Ijwu/Enemizer/commits?author=sosuke3) contributions to Enemizer, which make the vast majority of Enemizer contributions.
* [Zarby89's](https://github.com/Ijwu/Enemizer/commits?author=Zarby89)
and [sosuke3's](https://github.com/Ijwu/Enemizer/commits?author=sosuke3) contributions to Enemizer, which make up the
vast majority of Enemizer contributions.
We recognize that there is a strong community of incredibly smart people that have come before us and helped pave the path. Just because one person's name may be in a repository title does not mean that only one person made that project happen. We can't hope to perfectly cover every single contribution that lead up to Archipelago but we hope to honor them fairly.
We recognize that there is a strong community of incredibly smart people that have come before us and helped pave the
path. Just because one person's name may be in a repository title does not mean that only one person made that project
happen. We can't hope to perfectly cover every single contribution that lead up to Archipelago, but we hope to honor
them fairly.
### Path to the Archipelago
Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to _MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as "Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository (as opposed to a 'forked repo') and change the name (which came later) to better reflect our project.
Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a
long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to
_MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as
"Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository
(as opposed to a 'forked repo') and change the name (which came later) to better reflect our project.
## Running Archipelago
For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only.
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our doc on [running Archipelago from source](docs/running%20from%20source.md).
For most people, all you need to do is head over to
the [releases page](https://github.com/ArchipelagoMW/Archipelago/releases), then download and run the appropriate
installer, or AppImage for Linux-based systems.
If you are a developer or are running on a platform with no compiled releases available, please see our doc on
[running Archipelago from source](docs/running%20from%20source.md).
## Related Repositories
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the
contributions of their developers, past and present.
* [z3randomizer](https://github.com/ArchipelagoMW/z3randomizer)
* [Enemizer](https://github.com/Ijwu/Enemizer)
* [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer)
## Contributing
For contribution guidelines, please see our [Contributing doc.](/docs/contributing.md)
To contribute to Archipelago, including the WebHost, core program, or by adding a new game, see our
[Contributing guidelines](/docs/contributing.md).
## FAQ
For Frequently asked questions, please see the website's [FAQ Page.](https://archipelago.gg/faq/en/)
For Frequently asked questions, please see the website's [FAQ Page](https://archipelago.gg/faq/en/).
## Code of Conduct
Please refer to our [code of conduct.](/docs/code_of_conduct.md)
Please refer to our [code of conduct](/docs/code_of_conduct.md).

View File

@@ -68,12 +68,11 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
options = snes_options.split()
num_options = len(options)
if num_options > 0:
snes_device_number = int(options[0])
if num_options > 1:
snes_address = options[0]
snes_device_number = int(options[1])
elif num_options > 0:
snes_device_number = int(options[0])
self.ctx.snes_reconnect_address = None
if self.ctx.snes_connect_task:
@@ -86,6 +85,7 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
"""Close connection to a currently connected snes"""
self.ctx.snes_reconnect_address = None
self.ctx.cancel_snes_autoreconnect()
self.ctx.snes_state = SNESState.SNES_DISCONNECTED
if self.ctx.snes_socket and not self.ctx.snes_socket.closed:
async_start(self.ctx.snes_socket.close())
return True
@@ -208,12 +208,12 @@ class SNIContext(CommonContext):
self.killing_player_task = asyncio.create_task(deathlink_kill_player(self))
super(SNIContext, self).on_deathlink(data)
async def handle_deathlink_state(self, currently_dead: bool) -> None:
async def handle_deathlink_state(self, currently_dead: bool, death_text: str = "") -> None:
# in this state we only care about triggering a death send
if self.death_state == DeathState.alive:
if currently_dead:
self.death_state = DeathState.dead
await self.send_death()
await self.send_death(death_text)
# in this state we care about confirming a kill, to move state to dead
elif self.death_state == DeathState.killing_player:
# this is being handled in deathlink_kill_player(ctx) already
@@ -282,7 +282,7 @@ class SNESState(enum.IntEnum):
def launch_sni() -> None:
sni_path = Utils.get_options()["sni_options"]["sni_path"]
sni_path = Utils.get_settings()["sni_options"]["sni_path"]
if not os.path.isdir(sni_path):
sni_path = Utils.local_path(sni_path)
@@ -315,7 +315,7 @@ def launch_sni() -> None:
f"please start it yourself if it is not running")
async def _snes_connect(ctx: SNIContext, address: str) -> WebSocketClientProtocol:
async def _snes_connect(ctx: SNIContext, address: str, retry: bool = True) -> WebSocketClientProtocol:
address = f"ws://{address}" if "://" not in address else address
snes_logger.info("Connecting to SNI at %s ..." % address)
seen_problems: typing.Set[str] = set()
@@ -336,6 +336,8 @@ async def _snes_connect(ctx: SNIContext, address: str) -> WebSocketClientProtoco
await asyncio.sleep(1)
else:
return snes_socket
if not retry:
break
class SNESRequest(typing.TypedDict):
@@ -564,8 +566,6 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int,
try:
for address, data in write_list:
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
# REVIEW: above: `if snes_socket is None: return False`
# Does it need to be checked again?
if ctx.snes_socket is not None:
await ctx.snes_socket.send(dumps(PutAddress_Request))
await ctx.snes_socket.send(data)
@@ -654,7 +654,7 @@ async def game_watcher(ctx: SNIContext) -> None:
async def run_game(romfile: str) -> None:
auto_start = typing.cast(typing.Union[bool, str],
Utils.get_options()["sni_options"].get("snes_rom_start", True))
Utils.get_settings()["sni_options"].get("snes_rom_start", True))
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
@@ -684,6 +684,8 @@ async def main() -> None:
logging.info(f"Wrote rom file to {romfile}")
if args.diff_file.endswith(".apsoe"):
import webbrowser
async_start(run_game(romfile))
await _snes_connect(SNIContext(args.snes, args.connect, args.password), args.snes, False)
webbrowser.open(f"http://www.evermizer.com/apclient/#server={meta['server']}")
logging.info("Starting Evermizer Client in your Browser...")
import time

File diff suppressed because it is too large Load Diff

512
UndertaleClient.py Normal file
View File

@@ -0,0 +1,512 @@
from __future__ import annotations
import os
import sys
import asyncio
import typing
import bsdiff4
import shutil
import Utils
from NetUtils import NetworkItem, ClientStatus
from worlds import undertale
from MultiServer import mark_raw
from CommonClient import CommonContext, server_loop, \
gui_enabled, ClientCommandProcessor, logger, get_base_parser
from Utils import async_start
class UndertaleCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx):
super().__init__(ctx)
def _cmd_resync(self):
"""Manually trigger a resync."""
if isinstance(self.ctx, UndertaleContext):
self.output(f"Syncing items.")
self.ctx.syncing = True
def _cmd_patch(self):
"""Patch the game. Only use this command if /auto_patch fails."""
if isinstance(self.ctx, UndertaleContext):
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True)
self.ctx.patch_game()
self.output("Patched.")
def _cmd_savepath(self, directory: str):
"""Redirect to proper save data folder. This is necessary for Linux users to use before connecting."""
if isinstance(self.ctx, UndertaleContext):
self.ctx.save_game_folder = directory
self.output("Changed to the following directory: " + self.ctx.save_game_folder)
@mark_raw
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
"""Patch the game automatically."""
if isinstance(self.ctx, UndertaleContext):
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True)
tempInstall = steaminstall
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
tempInstall = None
if tempInstall is None:
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
if not os.path.exists(tempInstall):
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
elif not os.path.exists(tempInstall):
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
if not os.path.exists(tempInstall):
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
if not os.path.exists(tempInstall) or not os.path.exists(tempInstall) or not os.path.isfile(os.path.join(tempInstall, "data.win")):
self.output("ERROR: Cannot find Undertale. Please rerun the command with the correct folder."
" command. \"/auto_patch (Steam directory)\".")
else:
for file_name in os.listdir(tempInstall):
if file_name != "steam_api.dll":
shutil.copy(os.path.join(tempInstall, file_name),
os.path.join(os.getcwd(), "Undertale", file_name))
self.ctx.patch_game()
self.output("Patching successful!")
def _cmd_online(self):
"""Toggles seeing other Undertale players."""
if isinstance(self.ctx, UndertaleContext):
self.ctx.update_online_mode(not ("Online" in self.ctx.tags))
if "Online" in self.ctx.tags:
self.output(f"Now online.")
else:
self.output(f"Now offline.")
def _cmd_deathlink(self):
"""Toggles deathlink"""
if isinstance(self.ctx, UndertaleContext):
self.ctx.deathlink_status = not self.ctx.deathlink_status
if self.ctx.deathlink_status:
self.output(f"Deathlink enabled.")
else:
self.output(f"Deathlink disabled.")
class UndertaleContext(CommonContext):
tags = {"AP", "Online"}
game = "Undertale"
command_processor = UndertaleCommandProcessor
items_handling = 0b111
route = None
pieces_needed = None
completed_routes = None
completed_count = 0
save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.pieces_needed = 0
self.finished_game = False
self.game = "Undertale"
self.got_deathlink = False
self.syncing = False
self.deathlink_status = False
self.tem_armor = False
self.completed_count = 0
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
# self.save_game_folder: files go in this path to pass data between us and the actual game
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
def patch_game(self):
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "rb") as f:
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "wb") as f:
f.write(patchedFile)
os.makedirs(name=os.path.join(os.getcwd(), "Undertale", "Custom Sprites"), exist_ok=True)
with open(os.path.expandvars(os.path.join(os.getcwd(), "Undertale", "Custom Sprites",
"Which Character.txt")), "w") as f:
f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only "
"line other than this one.\n", "frisk"])
f.close()
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super().server_auth(password_requested)
await self.get_username()
await self.send_connect()
def clear_undertale_files(self):
path = self.save_game_folder
self.finished_game = False
for root, dirs, files in os.walk(path):
for file in files:
if "check.spot" == file or "scout" == file:
os.remove(os.path.join(root, file))
elif file.endswith((".item", ".victory", ".route", ".playerspot", ".mad",
".youDied", ".LV", ".mine", ".flag", ".hint")):
os.remove(os.path.join(root, file))
async def connect(self, address: typing.Optional[str] = None):
self.clear_undertale_files()
await super().connect(address)
async def disconnect(self, allow_autoreconnect: bool = False):
self.clear_undertale_files()
await super().disconnect(allow_autoreconnect)
async def connection_closed(self):
self.clear_undertale_files()
await super().connection_closed()
async def shutdown(self):
self.clear_undertale_files()
await super().shutdown()
def update_online_mode(self, online):
old_tags = self.tags.copy()
if online:
self.tags.add("Online")
else:
self.tags -= {"Online"}
if old_tags != self.tags and self.server and not self.server.socket.closed:
async_start(self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}]))
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.game = self.slot_info[self.slot].game
async_start(process_undertale_cmd(self, cmd, args))
def run_gui(self):
from kvui import GameManager
class UTManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Undertale Client"
self.ui = UTManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def on_deathlink(self, data: typing.Dict[str, typing.Any]):
self.got_deathlink = True
super().on_deathlink(data)
def to_room_name(place_name: str):
if place_name == "Old Home Exit":
return "room_ruinsexit"
elif place_name == "Snowdin Forest":
return "room_tundra1"
elif place_name == "Snowdin Town Exit":
return "room_fogroom"
elif place_name == "Waterfall":
return "room_water1"
elif place_name == "Waterfall Exit":
return "room_fire2"
elif place_name == "Hotland":
return "room_fire_prelab"
elif place_name == "Hotland Exit":
return "room_fire_precore"
elif place_name == "Core":
return "room_fire_core1"
async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
if cmd == "Connected":
if not os.path.exists(ctx.save_game_folder):
os.mkdir(ctx.save_game_folder)
ctx.route = args["slot_data"]["route"]
ctx.pieces_needed = args["slot_data"]["key_pieces"]
ctx.tem_armor = args["slot_data"]["temy_armor_include"]
await ctx.send_msgs([{"cmd": "Get", "keys": [str(ctx.slot)+" RoutesDone neutral",
str(ctx.slot)+" RoutesDone pacifist",
str(ctx.slot)+" RoutesDone genocide"]}])
await ctx.send_msgs([{"cmd": "SetNotify", "keys": [str(ctx.slot)+" RoutesDone neutral",
str(ctx.slot)+" RoutesDone pacifist",
str(ctx.slot)+" RoutesDone genocide"]}])
if args["slot_data"]["only_flakes"]:
with open(os.path.join(ctx.save_game_folder, "GenoNoChest.flag"), "w") as f:
f.close()
if not args["slot_data"]["key_hunt"]:
ctx.pieces_needed = 0
if args["slot_data"]["rando_love"]:
filename = f"LOVErando.LV"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
f.close()
if args["slot_data"]["rando_stats"]:
filename = f"STATrando.LV"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
f.close()
filename = f"{ctx.route}.route"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
f.close()
filename = f"check.spot"
with open(os.path.join(ctx.save_game_folder, filename), "a") as f:
for ss in set(args["checked_locations"]):
f.write(str(ss-12000)+"\n")
f.close()
elif cmd == "LocationInfo":
for l in args["locations"]:
locationid = l.location
filename = f"{str(locationid-12000)}.hint"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
toDraw = ""
for i in range(20):
if i < len(str(ctx.item_names.lookup_in_game(l.item))):
toDraw += str(ctx.item_names.lookup_in_game(l.item))[i]
else:
break
f.write(toDraw)
f.close()
elif cmd == "Retrieved":
if str(ctx.slot)+" RoutesDone neutral" in args["keys"]:
if args["keys"][str(ctx.slot)+" RoutesDone neutral"] is not None:
ctx.completed_routes["neutral"] = args["keys"][str(ctx.slot)+" RoutesDone neutral"]
if str(ctx.slot)+" RoutesDone genocide" in args["keys"]:
if args["keys"][str(ctx.slot)+" RoutesDone genocide"] is not None:
ctx.completed_routes["genocide"] = args["keys"][str(ctx.slot)+" RoutesDone genocide"]
if str(ctx.slot)+" RoutesDone pacifist" in args["keys"]:
if args["keys"][str(ctx.slot) + " RoutesDone pacifist"] is not None:
ctx.completed_routes["pacifist"] = args["keys"][str(ctx.slot)+" RoutesDone pacifist"]
elif cmd == "SetReply":
if args["value"] is not None:
if str(ctx.slot)+" RoutesDone pacifist" == args["key"]:
ctx.completed_routes["pacifist"] = args["value"]
elif str(ctx.slot)+" RoutesDone genocide" == args["key"]:
ctx.completed_routes["genocide"] = args["value"]
elif str(ctx.slot)+" RoutesDone neutral" == args["key"]:
ctx.completed_routes["neutral"] = args["value"]
elif cmd == "ReceivedItems":
start_index = args["index"]
if start_index == 0:
ctx.items_received = []
elif start_index != len(ctx.items_received):
sync_msg = [{"cmd": "Sync"}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks",
"locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
if start_index == len(ctx.items_received):
counter = -1
placedWeapon = 0
placedArmor = 0
for item in args["items"]:
id = NetworkItem(*item).location
while NetworkItem(*item).location < 0 and \
counter <= id:
id -= 1
if NetworkItem(*item).location < 0:
counter -= 1
filename = f"{str(id)}PLR{str(NetworkItem(*item).player)}.item"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
if NetworkItem(*item).item == 77701:
if placedWeapon == 0:
f.write(str(77013-11000))
elif placedWeapon == 1:
f.write(str(77014-11000))
elif placedWeapon == 2:
f.write(str(77025-11000))
elif placedWeapon == 3:
f.write(str(77045-11000))
elif placedWeapon == 4:
f.write(str(77049-11000))
elif placedWeapon == 5:
f.write(str(77047-11000))
elif placedWeapon == 6:
if str(ctx.route) == "genocide" or str(ctx.route) == "all_routes":
f.write(str(77052-11000))
else:
f.write(str(77051-11000))
else:
f.write(str(77003-11000))
placedWeapon += 1
elif NetworkItem(*item).item == 77702:
if placedArmor == 0:
f.write(str(77012-11000))
elif placedArmor == 1:
f.write(str(77015-11000))
elif placedArmor == 2:
f.write(str(77024-11000))
elif placedArmor == 3:
f.write(str(77044-11000))
elif placedArmor == 4:
f.write(str(77048-11000))
elif placedArmor == 5:
if str(ctx.route) == "genocide":
f.write(str(77053-11000))
else:
f.write(str(77046-11000))
elif placedArmor == 6 and ((not str(ctx.route) == "genocide") or ctx.tem_armor):
if str(ctx.route) == "all_routes":
f.write(str(77053-11000))
elif str(ctx.route) == "genocide":
f.write(str(77064-11000))
else:
f.write(str(77050-11000))
elif placedArmor == 7 and ctx.tem_armor and not str(ctx.route) == "genocide":
f.write(str(77064-11000))
else:
f.write(str(77004-11000))
placedArmor += 1
else:
f.write(str(NetworkItem(*item).item-11000))
f.close()
ctx.items_received.append(NetworkItem(*item))
if [item.item for item in ctx.items_received].count(77000) >= ctx.pieces_needed > 0:
filename = f"{str(-99999)}PLR{str(0)}.item"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
f.write(str(77787 - 11000))
f.close()
filename = f"{str(-99998)}PLR{str(0)}.item"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
f.write(str(77789 - 11000))
f.close()
ctx.watcher_event.set()
elif cmd == "RoomUpdate":
if "checked_locations" in args:
filename = f"check.spot"
with open(os.path.join(ctx.save_game_folder, filename), "a") as f:
for ss in set(args["checked_locations"]):
f.write(str(ss-12000)+"\n")
f.close()
elif cmd == "Bounced":
tags = args.get("tags", [])
if "Online" in tags:
data = args.get("data", {})
if data["player"] != ctx.slot and data["player"] is not None:
filename = f"FRISK" + str(data["player"]) + ".playerspot"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
f.write(str(data["x"]) + str(data["y"]) + str(data["room"]) + str(
data["spr"]) + str(data["frm"]))
f.close()
async def multi_watcher(ctx: UndertaleContext):
while not ctx.exit_event.is_set():
path = ctx.save_game_folder
for root, dirs, files in os.walk(path):
for file in files:
if "spots.mine" in file and "Online" in ctx.tags:
with open(os.path.join(root, file), "r") as mine:
this_x = mine.readline()
this_y = mine.readline()
this_room = mine.readline()
this_sprite = mine.readline()
this_frame = mine.readline()
mine.close()
message = [{"cmd": "Bounce", "tags": ["Online"],
"data": {"player": ctx.slot, "x": this_x, "y": this_y, "room": this_room,
"spr": this_sprite, "frm": this_frame}}]
await ctx.send_msgs(message)
await asyncio.sleep(0.1)
async def game_watcher(ctx: UndertaleContext):
while not ctx.exit_event.is_set():
await ctx.update_death_link(ctx.deathlink_status)
path = ctx.save_game_folder
if ctx.syncing:
for root, dirs, files in os.walk(path):
for file in files:
if ".item" in file:
os.remove(os.path.join(root, file))
sync_msg = [{"cmd": "Sync"}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
if ctx.got_deathlink:
ctx.got_deathlink = False
with open(os.path.join(ctx.save_game_folder, "WelcomeToTheDead.youDied"), "w") as f:
f.close()
sending = []
victory = False
found_routes = 0
for root, dirs, files in os.walk(path):
for file in files:
if "DontBeMad.mad" in file:
os.remove(os.path.join(root, file))
if "DeathLink" in ctx.tags:
await ctx.send_death()
if "scout" == file:
sending = []
try:
with open(os.path.join(root, file), "r") as f:
lines = f.readlines()
for l in lines:
if ctx.server_locations.__contains__(int(l)+12000):
sending = sending + [int(l.rstrip('\n'))+12000]
finally:
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending,
"create_as_hint": int(2)}])
os.remove(os.path.join(root, file))
if "check.spot" in file:
sending = []
try:
with open(os.path.join(root, file), "r") as f:
lines = f.readlines()
for l in lines:
sending = sending+[(int(l.rstrip('\n')))+12000]
finally:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": sending}])
if "victory" in file and str(ctx.route) in file:
victory = True
if ".playerspot" in file and "Online" not in ctx.tags:
os.remove(os.path.join(root, file))
if "victory" in file:
if str(ctx.route) == "all_routes":
if "neutral" in file and ctx.completed_routes["neutral"] != 1:
await ctx.send_msgs([{"cmd": "Set", "key": str(ctx.slot)+" RoutesDone neutral",
"default": 0, "want_reply": True, "operations": [{"operation": "max",
"value": 1}]}])
elif "pacifist" in file and ctx.completed_routes["pacifist"] != 1:
await ctx.send_msgs([{"cmd": "Set", "key": str(ctx.slot)+" RoutesDone pacifist",
"default": 0, "want_reply": True, "operations": [{"operation": "max",
"value": 1}]}])
elif "genocide" in file and ctx.completed_routes["genocide"] != 1:
await ctx.send_msgs([{"cmd": "Set", "key": str(ctx.slot)+" RoutesDone genocide",
"default": 0, "want_reply": True, "operations": [{"operation": "max",
"value": 1}]}])
if str(ctx.route) == "all_routes":
found_routes += ctx.completed_routes["neutral"]
found_routes += ctx.completed_routes["pacifist"]
found_routes += ctx.completed_routes["genocide"]
if str(ctx.route) == "all_routes" and found_routes >= 3:
victory = True
ctx.locations_checked = sending
if (not ctx.finished_game) and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
await asyncio.sleep(0.1)
def main():
Utils.init_logging("UndertaleClient", exception_logger="Client")
async def _main():
ctx = UndertaleContext(None, None)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
asyncio.create_task(
game_watcher(ctx), name="UndertaleProgressionWatcher")
asyncio.create_task(
multi_watcher(ctx), name="UndertaleMultiplayerWatcher")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
await ctx.exit_event.wait()
await ctx.shutdown()
import colorama
colorama.init()
asyncio.run(_main())
colorama.deinit()
if __name__ == "__main__":
parser = get_base_parser(description="Undertale Client, for text interfacing.")
args = parser.parse_args()
main()

644
Utils.py
View File

@@ -5,6 +5,7 @@ import json
import typing
import builtins
import os
import itertools
import subprocess
import sys
import pickle
@@ -13,20 +14,23 @@ import io
import collections
import importlib
import logging
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
import warnings
from yaml import load, load_all, dump, SafeLoader
from argparse import Namespace
from settings import Settings, get_settings
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
from typing_extensions import TypeGuard
from yaml import load, load_all, dump
try:
from yaml import CLoader as UnsafeLoader
from yaml import CDumper as Dumper
from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper
except ImportError:
from yaml import Loader as UnsafeLoader
from yaml import Dumper
from yaml import Loader as UnsafeLoader, SafeLoader, Dumper
if typing.TYPE_CHECKING:
import tkinter
import pathlib
from BaseClasses import Region
def tuplize_version(version: str) -> Version:
@@ -38,8 +42,11 @@ class Version(typing.NamedTuple):
minor: int
build: int
def as_simple_string(self) -> str:
return ".".join(str(item) for item in self)
__version__ = "0.4.0"
__version__ = "0.5.0"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
@@ -66,6 +73,8 @@ def snes_to_pc(value: int) -> int:
RetType = typing.TypeVar("RetType")
S = typing.TypeVar("S")
T = typing.TypeVar("T")
def cache_argsless(function: typing.Callable[[], RetType]) -> typing.Callable[[], RetType]:
@@ -83,6 +92,30 @@ def cache_argsless(function: typing.Callable[[], RetType]) -> typing.Callable[[]
return _wrap
def cache_self1(function: typing.Callable[[S, T], RetType]) -> typing.Callable[[S, T], RetType]:
"""Specialized cache for self + 1 arg. Does not keep global ref to self and skips building a dict key tuple."""
assert function.__code__.co_argcount == 2, "Can only cache 2 argument functions with this cache."
cache_name = f"__cache_{function.__name__}__"
@functools.wraps(function)
def wrap(self: S, arg: T) -> RetType:
cache: Optional[Dict[T, RetType]] = getattr(self, cache_name, None)
if cache is None:
res = function(self, arg)
setattr(self, cache_name, {arg: res})
return res
try:
return cache[arg]
except KeyError:
res = function(self, arg)
cache[arg] = res
return res
return wrap
def is_frozen() -> bool:
return typing.cast(bool, getattr(sys, 'frozen', False))
@@ -135,13 +168,20 @@ def user_path(*path: str) -> str:
user_path.cached_path = local_path()
else:
user_path.cached_path = home_path()
# populate home from local - TODO: upgrade feature
if user_path.cached_path != local_path() and not os.path.exists(user_path("host.yaml")):
import shutil
for dn in ("Players", "data/sprites"):
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
for fn in ("manifest.json", "host.yaml"):
shutil.copy2(local_path(fn), user_path(fn))
# populate home from local
if user_path.cached_path != local_path():
import filecmp
if not os.path.exists(user_path("manifest.json")) or \
not os.path.exists(local_path("manifest.json")) or \
not filecmp.cmp(local_path("manifest.json"), user_path("manifest.json"), shallow=True):
import shutil
for dn in ("Players", "data/sprites", "data/lua"):
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
if not os.path.exists(local_path("manifest.json")):
warnings.warn(f"Upgrading {user_path()} from something that is not a proper install")
else:
shutil.copy2(local_path("manifest.json"), user_path("manifest.json"))
os.makedirs(user_path("worlds"), exist_ok=True)
return os.path.join(user_path.cached_path, *path)
@@ -160,7 +200,7 @@ def cache_path(*path: str) -> str:
def output_path(*path: str) -> str:
if hasattr(output_path, 'cached_path'):
return os.path.join(output_path.cached_path, *path)
output_path.cached_path = user_path(get_options()["general_options"]["output_path"])
output_path.cached_path = user_path(get_settings()["general_options"]["output_path"])
path = os.path.join(output_path.cached_path, *path)
os.makedirs(os.path.dirname(path), exist_ok=True)
return path
@@ -168,10 +208,11 @@ def output_path(*path: str) -> str:
def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
if is_windows:
os.startfile(filename)
os.startfile(filename) # type: ignore
else:
from shutil import which
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
assert open_command, "Didn't find program for open_file! Please report this together with system details."
subprocess.call([open_command, filename])
@@ -184,6 +225,9 @@ class UniqueKeyLoader(SafeLoader):
if key in mapping:
logging.error(f"YAML duplicates sanity check failed{key_node.start_mark}")
raise KeyError(f"Duplicate key {key} found in YAML. Already found keys: {mapping}.")
if (str(key).startswith("+") and (str(key)[1:] in mapping)) or (f"+{key}" in mapping):
logging.error(f"YAML merge duplicates sanity check failed{key_node.start_mark}")
raise KeyError(f"Equivalent key {key} found in YAML. Already found keys: {mapping}.")
mapping.add(key)
return super().construct_mapping(node, deep)
@@ -207,7 +251,13 @@ def get_cert_none_ssl_context():
def get_public_ipv4() -> str:
import socket
import urllib.request
ip = socket.gethostbyname(socket.gethostname())
try:
ip = socket.gethostbyname(socket.gethostname())
except socket.gaierror:
# if hostname or resolvconf is not set up properly, this may fail
warnings.warn("Could not resolve own hostname, falling back to 127.0.0.1")
ip = "127.0.0.1"
ctx = get_cert_none_ssl_context()
try:
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip()
@@ -225,7 +275,13 @@ def get_public_ipv4() -> str:
def get_public_ipv6() -> str:
import socket
import urllib.request
ip = socket.gethostbyname(socket.gethostname())
try:
ip = socket.gethostbyname(socket.gethostname())
except socket.gaierror:
# if hostname or resolvconf is not set up properly, this may fail
warnings.warn("Could not resolve own hostname, falling back to ::1")
ip = "::1"
ctx = get_cert_none_ssl_context()
try:
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
@@ -235,168 +291,30 @@ def get_public_ipv6() -> str:
return ip
OptionsType = typing.Dict[str, typing.Dict[str, typing.Any]]
OptionsType = Settings # TODO: remove when removing get_options
@cache_argsless
def get_default_options() -> OptionsType:
# Refer to host.yaml for comments as to what all these options mean.
options = {
"general_options": {
"output_path": "output",
},
"factorio_options": {
"executable": os.path.join("factorio", "bin", "x64", "factorio"),
"filter_item_sends": False,
"bridge_chat_out": True,
},
"sni_options": {
"sni_path": "SNI",
"snes_rom_start": True,
},
"sm_options": {
"rom_file": "Super Metroid (JU).sfc",
},
"soe_options": {
"rom_file": "Secret of Evermore (USA).sfc",
},
"lttp_options": {
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
},
"ladx_options": {
"rom_file": "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc",
},
"server_options": {
"host": None,
"port": 38281,
"password": None,
"multidata": None,
"savefile": None,
"disable_save": False,
"loglevel": "info",
"server_password": None,
"disable_item_cheat": False,
"location_check_points": 1,
"hint_cost": 10,
"release_mode": "goal",
"collect_mode": "disabled",
"remaining_mode": "goal",
"auto_shutdown": 0,
"compatibility": 2,
"log_network": 0
},
"generator": {
"enemizer_path": os.path.join("EnemizerCLI", "EnemizerCLI.Core"),
"player_files_path": "Players",
"players": 0,
"weights_file_path": "weights.yaml",
"meta_file_path": "meta.yaml",
"spoiler": 3,
"glitch_triforce_room": 1,
"race": 0,
"plando_options": "bosses",
},
"minecraft_options": {
"forge_directory": "Minecraft Forge server",
"max_heap_size": "2G",
"release_channel": "release"
},
"oot_options": {
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
"rom_start": True
},
"dkc3_options": {
"rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc",
},
"smw_options": {
"rom_file": "Super Mario World (USA).sfc",
},
"zillion_options": {
"rom_file": "Zillion (UE) [!].sms",
# RetroArch doesn't make it easy to launch a game from the command line.
# You have to know the path to the emulator core library on the user's computer.
"rom_start": "retroarch",
},
"pokemon_rb_options": {
"red_rom_file": "Pokemon Red (UE) [S][!].gb",
"blue_rom_file": "Pokemon Blue (UE) [S][!].gb",
"rom_start": True
},
"ffr_options": {
"display_msgs": True,
},
"lufia2ac_options": {
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
},
"tloz_options": {
"rom_file": "Legend of Zelda, The (U) (PRG0) [!].nes",
"rom_start": True,
"display_msgs": True,
},
"wargroove_options": {
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
},
"adventure_options": {
"rom_file": "ADVNTURE.BIN",
"display_msgs": True,
"rom_start": True,
"rom_args": ""
},
}
return options
def get_options() -> Settings:
# TODO: switch to Utils.deprecate after 0.4.4
warnings.warn("Utils.get_options() is deprecated. Use the settings API instead.", DeprecationWarning)
return get_settings()
def update_options(src: dict, dest: dict, filename: str, keys: list) -> OptionsType:
for key, value in src.items():
new_keys = keys.copy()
new_keys.append(key)
option_name = '.'.join(new_keys)
if key not in dest:
dest[key] = value
if filename.endswith("options.yaml"):
logging.info(f"Warning: {filename} is missing {option_name}")
elif isinstance(value, dict):
if not isinstance(dest.get(key, None), dict):
if filename.endswith("options.yaml"):
logging.info(f"Warning: {filename} has {option_name}, but it is not a dictionary. overwriting.")
dest[key] = value
else:
dest[key] = update_options(value, dest[key], filename, new_keys)
return dest
@cache_argsless
def get_options() -> OptionsType:
filenames = ("options.yaml", "host.yaml")
locations: typing.List[str] = []
if os.path.join(os.getcwd()) != local_path():
locations += filenames # use files from cwd only if it's not the local_path
locations += [user_path(filename) for filename in filenames]
for location in locations:
if os.path.exists(location):
with open(location) as f:
options = parse_yaml(f.read())
return update_options(get_default_options(), options, location, list())
raise FileNotFoundError(f"Could not find {filenames[1]} to load options.")
def persistent_store(category: str, key: typing.Any, value: typing.Any):
def persistent_store(category: str, key: str, value: typing.Any):
path = user_path("_persistent_storage.yaml")
storage: dict = persistent_load()
category = storage.setdefault(category, {})
category[key] = value
storage = persistent_load()
category_dict = storage.setdefault(category, {})
category_dict[key] = value
with open(path, "wt") as f:
f.write(dump(storage, Dumper=Dumper))
def persistent_load() -> typing.Dict[str, dict]:
storage = getattr(persistent_load, "storage", None)
def persistent_load() -> Dict[str, Dict[str, Any]]:
storage: Union[Dict[str, Dict[str, Any]], None] = getattr(persistent_load, "storage", None)
if storage:
return storage
path = user_path("_persistent_storage.yaml")
storage: dict = {}
storage = {}
if os.path.exists(path):
try:
with open(path, "r") as f:
@@ -405,7 +323,7 @@ def persistent_load() -> typing.Dict[str, dict]:
logging.debug(f"Could not read store: {e}")
if storage is None:
storage = {}
persistent_load.storage = storage
setattr(persistent_load, "storage", storage)
return storage
@@ -448,11 +366,29 @@ def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> N
logging.debug(f"Could not store data package: {e}")
def get_adjuster_settings(game_name: str) -> typing.Dict[str, typing.Any]:
adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {})
def get_default_adjuster_settings(game_name: str) -> Namespace:
import LttPAdjuster
adjuster_settings = Namespace()
if game_name == LttPAdjuster.GAME_ALTTP:
return LttPAdjuster.get_argparser().parse_known_args(args=[])[0]
return adjuster_settings
def get_adjuster_settings_no_defaults(game_name: str) -> Namespace:
return persistent_load().get("adjuster", {}).get(game_name, Namespace())
def get_adjuster_settings(game_name: str) -> Namespace:
adjuster_settings = get_adjuster_settings_no_defaults(game_name)
default_settings = get_default_adjuster_settings(game_name)
# Fill in any arguments from the argparser that we haven't seen before
return Namespace(**vars(adjuster_settings), **{
k: v for k, v in vars(default_settings).items() if k not in vars(adjuster_settings)
})
@cache_argsless
def get_unique_identifier():
uuid = persistent_load().get("client", {}).get("uuid", None)
@@ -472,13 +408,15 @@ safe_builtins = frozenset((
class RestrictedUnpickler(pickle.Unpickler):
def __init__(self, *args, **kwargs):
generic_properties_module: Optional[object]
def __init__(self, *args: Any, **kwargs: Any) -> None:
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
self.options_module = importlib.import_module("Options")
self.net_utils_module = importlib.import_module("NetUtils")
self.generic_properties_module = importlib.import_module("worlds.generic")
self.generic_properties_module = None
def find_class(self, module, name):
def find_class(self, module: str, name: str) -> type:
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
# used by MultiServer -> savegame/multidata
@@ -486,6 +424,8 @@ class RestrictedUnpickler(pickle.Unpickler):
return getattr(self.net_utils_module, name)
# Options and Plando are unpickled by WebHost -> Generate
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
if not self.generic_properties_module:
self.generic_properties_module = importlib.import_module("worlds.generic")
return getattr(self.generic_properties_module, name)
# pep 8 specifies that modules should have "all-lowercase names" (options, not Options)
if module.lower().endswith("options"):
@@ -500,15 +440,33 @@ class RestrictedUnpickler(pickle.Unpickler):
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
def restricted_loads(s):
def restricted_loads(s: bytes) -> Any:
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
class ByValue:
"""
Mixin for enums to pickle value instead of name (restores pre-3.11 behavior). Use as left-most parent.
See https://github.com/python/cpython/pull/26658 for why this exists.
"""
def __reduce_ex__(self, prot):
return self.__class__, (self._value_, )
class KeyedDefaultDict(collections.defaultdict):
"""defaultdict variant that uses the missing key as argument to default_factory"""
default_factory: typing.Callable[[typing.Any], typing.Any]
def __init__(self,
default_factory: typing.Callable[[Any], Any] = None,
seq: typing.Union[typing.Mapping, typing.Iterable, None] = None,
**kwargs):
if seq is not None:
super().__init__(default_factory, seq, **kwargs)
else:
super().__init__(default_factory, **kwargs)
def __missing__(self, key):
self[key] = value = self.default_factory(key)
return value
@@ -537,6 +495,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
root_logger.removeHandler(handler)
handler.close()
root_logger.setLevel(loglevel)
logging.getLogger("websockets").setLevel(loglevel) # make sure level is applied for websockets
if "a" not in write_mode:
name += f"_{datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}"
file_handler = logging.FileHandler(
@@ -544,11 +503,21 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
write_mode,
encoding="utf-8-sig")
file_handler.setFormatter(logging.Formatter(log_format))
class Filter(logging.Filter):
def __init__(self, filter_name: str, condition: typing.Callable[[logging.LogRecord], bool]) -> None:
super().__init__(filter_name)
self.condition = condition
def filter(self, record: logging.LogRecord) -> bool:
return self.condition(record)
file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False)))
root_logger.addHandler(file_handler)
if sys.stdout:
root_logger.addHandler(
logging.StreamHandler(sys.stdout)
)
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.addFilter(Filter("NoFile", lambda record: not getattr(record, "NoStream", False)))
root_logger.addHandler(stream_handler)
# Relay unhandled exceptions to logger.
if not getattr(sys.excepthook, "_wrapped", False): # skip if already modified
@@ -584,10 +553,11 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
f"Archipelago ({__version__}) logging initialized"
f" on {platform.platform()}"
f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
f"{' (frozen)' if is_frozen() else ''}"
)
def stream_input(stream, queue):
def stream_input(stream: typing.TextIO, queue: "asyncio.Queue[str]"):
def queuer():
while 1:
try:
@@ -615,7 +585,7 @@ class VersionException(Exception):
pass
def chaining_prefix(index: int, labels: typing.Tuple[str]) -> str:
def chaining_prefix(index: int, labels: typing.Sequence[str]) -> str:
text = ""
max_label = len(labels) - 1
while index > max_label:
@@ -638,7 +608,7 @@ def format_SI_prefix(value, power=1000, power_labels=("", "k", "M", "G", "T", "P
return f"{value.quantize(decimal.Decimal('1.00'))} {chaining_prefix(n, power_labels)}"
def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: typing.Optional[int] = None) \
def get_fuzzy_results(input_word: str, word_list: typing.Collection[str], limit: typing.Optional[int] = None) \
-> typing.List[typing.Tuple[str, int]]:
import jellyfish
@@ -646,22 +616,58 @@ def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: ty
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
/ max(len(word1), len(word2)))
limit: int = limit if limit else len(wordlist)
limit = limit if limit else len(word_list)
return list(
map(
lambda container: (container[0], int(container[1]*100)), # convert up to limit to int %
sorted(
map(lambda candidate:
(candidate, get_fuzzy_ratio(input_word, candidate)),
wordlist),
map(lambda candidate: (candidate, get_fuzzy_ratio(input_word, candidate)), word_list),
key=lambda element: element[1],
reverse=True)[0:limit]
reverse=True
)[0:limit]
)
)
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]) \
def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bool, str]:
picks = get_fuzzy_results(input_text, possible_answers, limit=2)
if len(picks) > 1:
dif = picks[0][1] - picks[1][1]
if picks[0][1] == 100:
return picks[0][0], True, "Perfect Match"
elif picks[0][1] < 75:
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
elif dif > 5:
return picks[0][0], True, "Close Match"
else:
return picks[0][0], False, f"Too many close matches for '{input_text}', " \
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
else:
if picks[0][1] > 90:
return picks[0][0], True, "Only Option Match"
else:
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
def get_input_text_from_response(text: str, command: str) -> typing.Optional[str]:
if "did you mean " in text:
for question in ("Didn't find something that closely matches",
"Too many close matches"):
if text.startswith(question):
name = get_text_between(text, "did you mean '",
"'? (")
return f"!{command} {name}"
elif text.startswith("Missing: "):
return text.replace("Missing: ", "!hint_location ")
return None
def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \
-> typing.Optional[str]:
logging.info(f"Opening file input dialog for {title}.")
def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
@@ -671,11 +677,12 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
kdialog = which("kdialog")
if kdialog:
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
return run(kdialog, f"--title={title}", "--getopenfilename", ".", k_filters)
return run(kdialog, f"--title={title}", "--getopenfilename", suggest or ".", k_filters)
zenity = which("zenity")
if zenity:
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
return run(zenity, f"--title={title}", "--file-selection", *z_filters)
selection = (f"--filename={suggest}",) if suggest else ()
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
# fall back to tk
try:
@@ -686,9 +693,47 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
f'This attempt was made because open_filename was used for "{title}".')
raise e
else:
root = tkinter.Tk()
try:
root = tkinter.Tk()
except tkinter.TclError:
return None # GUI not available. None is the same as a user clicking "cancel"
root.withdraw()
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes))
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
initialfile=suggest or None)
def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
if is_linux:
# prefer native dialog
from shutil import which
kdialog = which("kdialog")
if kdialog:
return run(kdialog, f"--title={title}", "--getexistingdirectory",
os.path.abspath(suggest) if suggest else ".")
zenity = which("zenity")
if zenity:
z_filters = ("--directory",)
selection = (f"--filename={os.path.abspath(suggest)}/",) if suggest else ()
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
# fall back to tk
try:
import tkinter
import tkinter.filedialog
except Exception as e:
logging.error('Could not load tkinter, which is likely not installed. '
f'This attempt was made because open_filename was used for "{title}".')
raise e
else:
try:
root = tkinter.Tk()
except tkinter.TclError:
return None # GUI not available. None is the same as a user clicking "cancel"
root.withdraw()
return tkinter.filedialog.askdirectory(title=title, mustexist=True, initialdir=suggest or None)
def messagebox(title: str, text: str, error: bool = False) -> None:
@@ -716,6 +761,11 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
if zenity:
return run(zenity, f"--title={title}", f"--text={text}", "--error" if error else "--info")
elif is_windows:
import ctypes
style = 0x10 if error else 0x0
return ctypes.windll.user32.MessageBoxW(0, text, title, style)
# fall back to tk
try:
import tkinter
@@ -731,7 +781,7 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
root.update()
def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))):
def title_sorted(data: typing.Iterable, key=None, ignore: typing.AbstractSet[str] = frozenset(("a", "the"))):
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
def sorter(element: Union[str, Dict[str, Any]]) -> str:
if (not isinstance(element, str)):
@@ -753,10 +803,10 @@ def read_snes_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray:
return buffer
_faf_tasks: "Set[asyncio.Task[None]]" = set()
_faf_tasks: "Set[asyncio.Task[typing.Any]]" = set()
def async_start(co: Coroutine[typing.Any, typing.Any, bool], name: Optional[str] = None) -> None:
def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = None) -> None:
"""
Use this to start a task when you don't keep a reference to it or immediately await it,
to prevent early garbage collection. "fire-and-forget"
@@ -769,6 +819,210 @@ def async_start(co: Coroutine[typing.Any, typing.Any, bool], name: Optional[str]
# ```
# This implementation follows the pattern given in that documentation.
task = asyncio.create_task(co, name=name)
task: asyncio.Task[typing.Any] = asyncio.create_task(co, name=name)
_faf_tasks.add(task)
task.add_done_callback(_faf_tasks.discard)
def deprecate(message: str):
if __debug__:
raise Exception(message)
import warnings
warnings.warn(message)
class DeprecateDict(dict):
log_message: str
should_error: bool
def __init__(self, message: str, error: bool = False) -> None:
self.log_message = message
self.should_error = error
super().__init__()
def __getitem__(self, item: Any) -> Any:
if self.should_error:
deprecate(self.log_message)
elif __debug__:
import warnings
warnings.warn(self.log_message)
return super().__getitem__(item)
def _extend_freeze_support() -> None:
"""Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn."""
# upstream issue: https://github.com/python/cpython/issues/76327
# code based on https://github.com/pyinstaller/pyinstaller/blob/develop/PyInstaller/hooks/rthooks/pyi_rth_multiprocessing.py#L26
import multiprocessing
import multiprocessing.spawn
def _freeze_support() -> None:
"""Minimal freeze_support. Only apply this if frozen."""
from subprocess import _args_from_interpreter_flags
# Prevent `spawn` from trying to read `__main__` in from the main script
multiprocessing.process.ORIGINAL_DIR = None
# Handle the first process that MP will create
if (
len(sys.argv) >= 2 and sys.argv[-2] == '-c' and sys.argv[-1].startswith((
'from multiprocessing.semaphore_tracker import main', # Py<3.8
'from multiprocessing.resource_tracker import main', # Py>=3.8
'from multiprocessing.forkserver import main'
)) and set(sys.argv[1:-2]) == set(_args_from_interpreter_flags())
):
exec(sys.argv[-1])
sys.exit()
# Handle the second process that MP will create
if multiprocessing.spawn.is_forking(sys.argv):
kwargs = {}
for arg in sys.argv[2:]:
name, value = arg.split('=')
if value == 'None':
kwargs[name] = None
else:
kwargs[name] = int(value)
multiprocessing.spawn.spawn_main(**kwargs)
sys.exit()
if not is_windows and is_frozen():
multiprocessing.freeze_support = multiprocessing.spawn.freeze_support = _freeze_support
def freeze_support() -> None:
"""This behaves like multiprocessing.freeze_support but also works on Non-Windows."""
import multiprocessing
_extend_freeze_support()
multiprocessing.freeze_support()
def visualize_regions(root_region: Region, file_name: str, *,
show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True,
linetype_ortho: bool = True) -> None:
"""Visualize the layout of a world as a PlantUML diagram.
:param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.)
:param file_name: The name of the destination .puml file.
:param show_entrance_names: (default False) If enabled, the name of the entrance will be shown near each connection.
:param show_locations: (default True) If enabled, the locations will be listed inside each region.
Priority locations will be shown in bold.
Excluded locations will be stricken out.
Locations without ID will be shown in italics.
Locked locations will be shown with a padlock icon.
For filled locations, the item name will be shown after the location name.
Progression items will be shown in bold.
Items without ID will be shown in italics.
:param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown.
:param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines.
Example usage in World code:
from Utils import visualize_regions
visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
Example usage in Main code:
from Utils import visualize_regions
for player in multiworld.player_ids:
visualize_regions(multiworld.get_region("Menu", player), f"{multiworld.get_out_file_name_base(player)}.puml")
"""
assert root_region.multiworld, "The multiworld attribute of root_region has to be filled"
from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region
from collections import deque
import re
uml: typing.List[str] = list()
seen: typing.Set[Region] = set()
regions: typing.Deque[Region] = deque((root_region,))
multiworld: MultiWorld = root_region.multiworld
def fmt(obj: Union[Entrance, Item, Location, Region]) -> str:
name = obj.name
if isinstance(obj, Item):
name = multiworld.get_name_string_for_object(obj)
if obj.advancement:
name = f"**{name}**"
if obj.code is None:
name = f"//{name}//"
if isinstance(obj, Location):
if obj.progress_type == LocationProgressType.PRIORITY:
name = f"**{name}**"
elif obj.progress_type == LocationProgressType.EXCLUDED:
name = f"--{name}--"
if obj.address is None:
name = f"//{name}//"
return re.sub("[\".:]", "", name)
def visualize_exits(region: Region) -> None:
for exit_ in region.exits:
if exit_.connected_region:
if show_entrance_names:
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\" : \"{fmt(exit_)}\"")
else:
try:
uml.remove(f"\"{fmt(exit_.connected_region)}\" --> \"{fmt(region)}\"")
uml.append(f"\"{fmt(exit_.connected_region)}\" <--> \"{fmt(region)}\"")
except ValueError:
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\"")
else:
uml.append(f"circle \"unconnected exit:\\n{fmt(exit_)}\"")
uml.append(f"\"{fmt(region)}\" --> \"unconnected exit:\\n{fmt(exit_)}\"")
def visualize_locations(region: Region) -> None:
any_lock = any(location.locked for location in region.locations)
for location in region.locations:
lock = "<&lock-locked> " if location.locked else "<&lock-unlocked,color=transparent> " if any_lock else ""
if location.item:
uml.append(f"\"{fmt(region)}\" : {{method}} {lock}{fmt(location)}: {fmt(location.item)}")
else:
uml.append(f"\"{fmt(region)}\" : {{field}} {lock}{fmt(location)}")
def visualize_region(region: Region) -> None:
uml.append(f"class \"{fmt(region)}\"")
if show_locations:
visualize_locations(region)
visualize_exits(region)
def visualize_other_regions() -> None:
if other_regions := [region for region in multiworld.get_regions(root_region.player) if region not in seen]:
uml.append("package \"other regions\" <<Cloud>> {")
for region in other_regions:
uml.append(f"class \"{fmt(region)}\"")
uml.append("}")
uml.append("@startuml")
uml.append("hide circle")
uml.append("hide empty members")
if linetype_ortho:
uml.append("skinparam linetype ortho")
while regions:
if (current_region := regions.popleft()) not in seen:
seen.add(current_region)
visualize_region(current_region)
regions.extend(exit_.connected_region for exit_ in current_region.exits if exit_.connected_region)
if show_other_regions:
visualize_other_regions()
uml.append("@enduml")
with open(file_name, "wt", encoding="utf-8") as f:
f.write("\n".join(uml))
class RepeatableChain:
def __init__(self, iterable: typing.Iterable):
self.iterable = iterable
def __iter__(self):
return itertools.chain.from_iterable(self.iterable)
def __bool__(self):
return any(sub_iterable for sub_iterable in self.iterable)
def __len__(self):
return sum(len(iterable) for iterable in self.iterable)
def is_iterable_except_str(obj: object) -> TypeGuard[typing.Iterable[typing.Any]]:
""" `str` is `Iterable`, but that's not what we want """
if isinstance(obj, str):
return False
return isinstance(obj, typing.Iterable)

View File

@@ -113,6 +113,9 @@ class WargrooveContext(CommonContext):
async def connection_closed(self):
await super(WargrooveContext, self).connection_closed()
self.remove_communication_files()
self.checked_locations.clear()
self.server_locations.clear()
self.finished_game = False
@property
def endpoints(self):
@@ -124,6 +127,9 @@ class WargrooveContext(CommonContext):
async def shutdown(self):
await super(WargrooveContext, self).shutdown()
self.remove_communication_files()
self.checked_locations.clear()
self.server_locations.clear()
self.finished_game = False
def remove_communication_files(self):
for root, dirs, files in os.walk(self.game_communication_path):
@@ -170,7 +176,7 @@ class WargrooveContext(CommonContext):
if not os.path.isfile(path):
open(path, 'w').close()
# Announcing commander unlocks
item_name = self.item_names[network_item.item]
item_name = self.item_names.lookup_in_game(network_item.item)
if item_name in faction_table.keys():
for commander in faction_table[item_name]:
logger.info(f"{commander.name} has been unlocked!")
@@ -191,7 +197,7 @@ class WargrooveContext(CommonContext):
open(print_path, 'w').close()
with open(print_path, 'w') as f:
f.write("Received " +
self.item_names[network_item.item] +
self.item_names.lookup_in_game(network_item.item) +
" from " +
self.player_names[network_item.player])
f.close()
@@ -336,7 +342,7 @@ class WargrooveContext(CommonContext):
faction_items = 0
faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()]
for network_item in self.items_received:
if self.item_names[network_item.item] in faction_item_names:
if self.item_names.lookup_in_game(network_item.item) in faction_item_names:
faction_items += 1
starting_groove = (faction_items - 1) * self.starting_groove_multiplier
# Must be an integer larger than 0
@@ -402,8 +408,10 @@ async def game_watcher(ctx: WargrooveContext):
if file.find("send") > -1:
st = file.split("send", -1)[1]
sending = sending+[(int(st))]
os.remove(os.path.join(ctx.game_communication_path, file))
if file.find("victory") > -1:
victory = True
os.remove(os.path.join(ctx.game_communication_path, file))
ctx.locations_checked = sending
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)

View File

@@ -10,24 +10,22 @@ ModuleUpdate.update()
# in case app gets imported by something like gunicorn
import Utils
import settings
if typing.TYPE_CHECKING:
from flask import Flask
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
from WebHostLib import register, app as raw_app
from waitress import serve
from WebHostLib.models import db
from WebHostLib.autolauncher import autohost, autogen
from WebHostLib.lttpsprites import update_sprites_lttp
from WebHostLib.options import create as create_options_files
settings.no_gui = True
configpath = os.path.abspath("config.yaml")
if not os.path.exists(configpath): # fall back to config.yaml in home
configpath = os.path.abspath(Utils.user_path('config.yaml'))
def get_app():
register()
def get_app() -> "Flask":
from WebHostLib import register, cache, app as raw_app
from WebHostLib.models import db
app = raw_app
if os.path.exists(configpath) and not app.config["TESTING"]:
import yaml
@@ -38,6 +36,8 @@ def get_app():
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
register()
cache.init_app(app)
db.bind(**app.config["PONY"])
db.generate_mapping(create_tables=True)
return app
@@ -58,6 +58,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
worlds[game] = world
base_target_path = Utils.local_path("WebHostLib", "static", "generated", "docs")
shutil.rmtree(base_target_path, ignore_errors=True)
for game, world in worlds.items():
# copy files from world's docs folder to the generated folder
target_path = os.path.join(base_target_path, game)
@@ -72,6 +73,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
with zipfile.ZipFile(zipfile_path) as zf:
for zfile in zf.infolist():
if not zfile.is_dir() and "/docs/" in zfile.filename:
zfile.filename = os.path.basename(zfile.filename)
zf.extract(zfile, target_path)
else:
source_path = Utils.local_path(os.path.dirname(world.__file__), "docs")
@@ -117,6 +119,11 @@ if __name__ == "__main__":
multiprocessing.freeze_support()
multiprocessing.set_start_method('spawn')
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
from WebHostLib.lttpsprites import update_sprites_lttp
from WebHostLib.autolauncher import autohost, autogen, stop
from WebHostLib.options import create as create_options_files
try:
update_sprites_lttp()
except Exception as e:
@@ -133,4 +140,13 @@ if __name__ == "__main__":
if app.config["DEBUG"]:
app.run(debug=True, port=app.config["PORT"])
else:
from waitress import serve
serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"])
else:
from time import sleep
try:
while True:
sleep(1) # wait for process to be killed
except (SystemExit, KeyboardInterrupt):
pass
stop() # stop worker threads

View File

@@ -23,6 +23,7 @@ app.jinja_env.filters['all'] = all
app.config["SELFHOST"] = True # application process is in charge of running the websites
app.config["GENERATORS"] = 8 # maximum concurrent world gens
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
@@ -34,7 +35,7 @@ app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit
# if you want to deploy, make sure you have a non-guessable secret key
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
app.config["JOB_THRESHOLD"] = 2
app.config["JOB_THRESHOLD"] = 1
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
app.config["JOB_TIME"] = 600
app.config['SESSION_PERMANENT'] = True
@@ -49,11 +50,11 @@ app.config["PONY"] = {
'create_db': True
}
app.config["MAX_ROLL"] = 20
app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache"
app.config["JSON_AS_ASCII"] = False
app.config["CACHE_TYPE"] = "SimpleCache"
app.config["HOST_ADDRESS"] = ""
app.config["ASSET_RIGHTS"] = False
cache = Cache(app)
cache = Cache()
Compress(app)
@@ -83,6 +84,6 @@ def register():
from WebHostLib.customserver import run_server_process
# to trigger app routing picking up on it
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options
app.register_blueprint(api.api_endpoints)

View File

@@ -2,9 +2,9 @@
from typing import List, Tuple
from uuid import UUID
from flask import Blueprint, abort
from flask import Blueprint, abort, url_for
from .. import cache
import worlds.Files
from ..models import Room, Seed
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
@@ -21,39 +21,31 @@ def room_info(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
def supports_apdeltapatch(game: str):
return game in worlds.Files.AutoPatchRegister.patch_types
downloads = []
for slot in sorted(room.seed.slots):
if slot.data and not supports_apdeltapatch(slot.game):
slot_download = {
"slot": slot.player_id,
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
}
downloads.append(slot_download)
elif slot.data:
slot_download = {
"slot": slot.player_id,
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
}
downloads.append(slot_download)
return {
"tracker": room.tracker,
"players": get_players(room.seed),
"last_port": room.last_port,
"last_activity": room.last_activity,
"timeout": room.timeout
"timeout": room.timeout,
"downloads": downloads,
}
@api_endpoints.route('/datapackage')
@cache.cached()
def get_datapackage():
from worlds import network_data_package
return network_data_package
@api_endpoints.route('/datapackage_version')
@cache.cached()
def get_datapackage_versions():
from worlds import AutoWorldRegister
version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()}
return version_package
@api_endpoints.route('/datapackage_checksum')
@cache.cached()
def get_datapackage_checksums():
from worlds import network_data_package
version_package = {
game: game_data["checksum"] for game, game_data in network_data_package["games"].items()
}
return version_package
from . import generate, user # trigger registration
from . import generate, user, datapackage # trigger registration

View File

@@ -0,0 +1,32 @@
from flask import abort
from Utils import restricted_loads
from WebHostLib import cache
from WebHostLib.models import GameDataPackage
from . import api_endpoints
@api_endpoints.route('/datapackage')
@cache.cached()
def get_datapackage():
from worlds import network_data_package
return network_data_package
@api_endpoints.route('/datapackage/<string:checksum>')
@cache.memoize(timeout=3600)
def get_datapackage_by_checksum(checksum: str):
package = GameDataPackage.get(checksum=checksum)
if package:
return restricted_loads(package.data)
return abort(404)
@api_endpoints.route('/datapackage_checksum')
@cache.cached()
def get_datapackage_checksums():
from worlds import network_data_package
version_package = {
game: game_data["checksum"] for game, game_data in network_data_package["games"].items()
}
return version_package

View File

@@ -2,7 +2,8 @@ import json
import pickle
from uuid import UUID
from flask import request, session, url_for, Markup
from flask import request, session, url_for
from markupsafe import Markup
from pony.orm import commit
from WebHostLib import app
@@ -19,8 +20,8 @@ def generate_api():
race = False
meta_options_source = {}
if 'file' in request.files:
file = request.files['file']
options = get_yaml_data(file)
files = request.files.getlist('file')
options = get_yaml_data(files)
if isinstance(options, Markup):
return {"text": options.striptags()}, 400
if isinstance(options, str):
@@ -48,9 +49,8 @@ def generate_api():
if len(options) > app.config["MAX_ROLL"]:
return {"text": "Max size of multiworld exceeded",
"detail": app.config["MAX_ROLL"]}, 409
meta = get_meta(meta_options_source)
meta["race"] = race
results, gen_options = roll_options(options, meta["plando_options"])
meta = get_meta(meta_options_source, race)
results, gen_options = roll_options(options, set(meta["plando_options"]))
if any(type(result) == str for result in results.values()):
return {"text": str(results),
"detail": results}, 400

View File

@@ -3,75 +3,25 @@ from __future__ import annotations
import json
import logging
import multiprocessing
import os
import sys
import threading
import time
import typing
from datetime import timedelta, datetime
from threading import Event, Thread
from uuid import UUID
from pony.orm import db_session, select, commit
from Utils import restricted_loads
from .locker import Locker, AlreadyRunningException
_stop_event = Event()
class CommonLocker():
"""Uses a file lock to signal that something is already running"""
lock_folder = "file_locks"
def __init__(self, lockname: str, folder=None):
if folder:
self.lock_folder = folder
os.makedirs(self.lock_folder, exist_ok=True)
self.lockname = lockname
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
class AlreadyRunningException(Exception):
pass
if sys.platform == 'win32':
class Locker(CommonLocker):
def __enter__(self):
try:
if os.path.exists(self.lockfile):
os.unlink(self.lockfile)
self.fp = os.open(
self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
except OSError as e:
raise AlreadyRunningException() from e
def __exit__(self, _type, value, tb):
fp = getattr(self, "fp", None)
if fp:
os.close(self.fp)
os.unlink(self.lockfile)
else: # unix
import fcntl
class Locker(CommonLocker):
def __enter__(self):
try:
self.fp = open(self.lockfile, "wb")
fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError as e:
raise AlreadyRunningException() from e
def __exit__(self, _type, value, tb):
fcntl.flock(self.fp.fileno(), fcntl.LOCK_UN)
self.fp.close()
def launch_room(room: Room, config: dict):
# requires db_session!
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout):
multiworld = multiworlds.get(room.id, None)
if not multiworld:
multiworld = MultiworldInstance(room, config)
multiworld.start()
def stop():
"""Stops previously launched threads"""
global _stop_event
stop_event = _stop_event
_stop_event = Event() # new event for new threads
stop_event.set()
def handle_generation_success(seed_id):
@@ -108,34 +58,55 @@ def init_db(pony_config: dict):
db.generate_mapping()
def cleanup():
"""delete unowned user-content"""
with db_session:
# >>> bool(uuid.UUID(int=0))
# True
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
if rooms or seeds or slots:
logging.info(f"{rooms} Rooms, {seeds} Seeds and {slots} Slots have been deleted.")
def autohost(config: dict):
def keep_running():
stop_event = _stop_event
try:
with Locker("autohost"):
run_guardian()
while 1:
time.sleep(0.1)
cleanup()
hosters = []
for x in range(config["HOSTERS"]):
hoster = MultiworldInstance(config, x)
hosters.append(hoster)
hoster.start()
while not stop_event.wait(0.1):
with db_session:
rooms = select(
room for room in Room if
room.last_activity >= datetime.utcnow() - timedelta(days=3))
for room in rooms:
launch_room(room, config)
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout + 5):
hosters[room.id.int % len(hosters)].start_room(room.id)
except AlreadyRunningException:
logging.info("Autohost reports as already running, not starting another.")
import threading
threading.Thread(target=keep_running, name="AP_Autohost").start()
Thread(target=keep_running, name="AP_Autohost").start()
def autogen(config: dict):
def keep_running():
stop_event = _stop_event
try:
with Locker("autogen"):
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
initargs=(config["PONY"],)) as generator_pool:
initargs=(config["PONY"],), maxtasksperchild=10) as generator_pool:
with db_session:
to_start = select(generation for generation in Generation if generation.state == STATE_STARTED)
@@ -151,8 +122,7 @@ def autogen(config: dict):
commit()
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
while 1:
time.sleep(0.1)
while not stop_event.wait(0.1):
with db_session:
# for update locks the database row(s) during transaction, preventing writes from elsewhere
to_start = select(
@@ -163,37 +133,45 @@ def autogen(config: dict):
except AlreadyRunningException:
logging.info("Autogen reports as already running, not starting another.")
import threading
threading.Thread(target=keep_running, name="AP_Autogen").start()
Thread(target=keep_running, name="AP_Autogen").start()
multiworlds: typing.Dict[type(Room.id), MultiworldInstance] = {}
class MultiworldInstance():
def __init__(self, room: Room, config: dict):
self.room_id = room.id
def __init__(self, config: dict, id: int):
self.room_ids = set()
self.process: typing.Optional[multiprocessing.Process] = None
with guardian_lock:
multiworlds[self.room_id] = self
self.ponyconfig = config["PONY"]
self.cert = config["SELFLAUNCHCERT"]
self.key = config["SELFLAUNCHKEY"]
self.host = config["HOST_ADDRESS"]
self.rooms_to_start = multiprocessing.Queue()
self.rooms_shutting_down = multiprocessing.Queue()
self.name = f"MultiHoster{id}"
def start(self):
if self.process and self.process.is_alive():
return False
logging.info(f"Spinning up {self.room_id}")
process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.room_id, self.ponyconfig, get_static_server_data(),
self.cert, self.key, self.host),
name="MultiHost")
args=(self.name, self.ponyconfig, get_static_server_data(),
self.cert, self.key, self.host,
self.rooms_to_start, self.rooms_shutting_down),
name=self.name)
process.start()
# bind after start to prevent thread sync issues with guardian.
self.process = process
def start_room(self, room_id):
while not self.rooms_shutting_down.empty():
self.room_ids.remove(self.rooms_shutting_down.get(block=True, timeout=None))
if room_id in self.room_ids:
pass # should already be hosted currently.
else:
self.room_ids.add(room_id)
self.rooms_to_start.put(room_id)
def stop(self):
if self.process:
self.process.terminate()
@@ -207,40 +185,6 @@ class MultiworldInstance():
self.process = None
guardian = None
guardian_lock = threading.Lock()
def run_guardian():
global guardian
global multiworlds
with guardian_lock:
if not guardian:
try:
import resource
except ModuleNotFoundError:
pass # unix only module
else:
# Each Server is another file handle, so request as many as we can from the system
file_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
# set soft limit to hard limit
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
def guard():
while 1:
time.sleep(1)
done = []
with guardian_lock:
for key, instance in multiworlds.items():
if instance.done():
instance.collect()
done.append(key)
for key in done:
del (multiworlds[key])
guardian = threading.Thread(name="Guardian", target=guard)
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed, Slot
from .customserver import run_server_process, get_static_server_data
from .generate import gen_game

View File

@@ -1,16 +1,13 @@
import os
import zipfile
from typing import *
import base64
from typing import Union, Dict, Set, Tuple
from flask import request, flash, redirect, url_for, render_template, Markup
from flask import request, flash, redirect, url_for, render_template
from markupsafe import Markup
from WebHostLib import app
banned_zip_contents = (".sfc",)
def allowed_file(filename):
return filename.endswith(('.txt', ".yaml", ".zip"))
from WebHostLib.upload import allowed_options, allowed_options_extensions, banned_file
from Generate import roll_settings, PlandoOptions
from Utils import parse_yamls
@@ -23,13 +20,21 @@ def check():
if 'file' not in request.files:
flash('No file part')
else:
file = request.files['file']
options = get_yaml_data(file)
files = request.files.getlist('file')
options = get_yaml_data(files)
if isinstance(options, str):
flash(options)
else:
results, _ = roll_options(options)
return render_template("checkResult.html", results=results)
if len(options) > 1:
# offer combined file back
combined_yaml = "\n---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}"
for file_name, file_content in options.items())
combined_yaml = base64.b64encode(combined_yaml.encode("utf-8-sig")).decode()
else:
combined_yaml = ""
return render_template("checkResult.html",
results=results, combined_yaml=combined_yaml)
return render_template("check.html")
@@ -38,31 +43,44 @@ def mysterycheck():
return redirect(url_for("check"), 301)
def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]:
def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]:
options = {}
# if user does not select file, browser also
# submit an empty part without filename
if file.filename == '':
return 'No selected file'
elif file and allowed_file(file.filename):
if file.filename.endswith(".zip"):
for uploaded_file in files:
if banned_file(uploaded_file.filename):
return ("Uploaded data contained a rom file, which is likely to contain copyrighted material. "
"Your file was deleted.")
# If the user does not select file, the browser will still submit an empty string without a file name.
elif uploaded_file.filename == "":
return "No selected file."
elif uploaded_file.filename in options:
return f"Conflicting files named {uploaded_file.filename} submitted."
elif uploaded_file and allowed_options(uploaded_file.filename):
if uploaded_file.filename.endswith(".zip"):
if not zipfile.is_zipfile(uploaded_file):
return f"Uploaded file {uploaded_file.filename} is not a valid .zip file and cannot be opened."
with zipfile.ZipFile(file, 'r') as zfile:
infolist = zfile.infolist()
uploaded_file.seek(0) # offset from is_zipfile check
with zipfile.ZipFile(uploaded_file, "r") as zfile:
for file in zfile.infolist():
# Remove folder pathing from str (e.g. "__MACOSX/" folder paths from archives created by macOS).
base_filename = os.path.basename(file.filename)
if any(file.filename.endswith(".archipelago") for file in infolist):
return Markup("Error: Your .zip file contains an .archipelago file. "
'Did you mean to <a href="/uploads">host a game</a>?')
if base_filename.endswith(".archipelago"):
return Markup("Error: Your .zip file contains an .archipelago file. "
'Did you mean to <a href="/uploads">host a game</a>?')
elif base_filename.endswith(".zip"):
return "Nested .zip files inside a .zip are not supported."
elif banned_file(base_filename):
return ("Uploaded data contained a rom file, which is likely to contain copyrighted "
"material. Your file was deleted.")
# Ignore dot-files.
elif not base_filename.startswith(".") and allowed_options(base_filename):
options[file.filename] = zfile.open(file, "r").read()
else:
options[uploaded_file.filename] = uploaded_file.read()
for file in infolist:
if file.filename.endswith(banned_zip_contents):
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
options[file.filename] = zfile.open(file, "r").read()
else:
options = {file.filename: file.read()}
if not options:
return "Did not find a .yaml file to process."
return f"Did not find any valid files to process. Accepted formats: {allowed_options_extensions}"
return options
@@ -90,7 +108,10 @@ def roll_options(options: Dict[str, Union[dict, str]],
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
plando_options=plando_options)
except Exception as e:
results[filename] = f"Failed to generate mystery in {filename}: {e}"
if e.__cause__:
results[filename] = f"Failed to generate options in {filename}: {e} - {e.__cause__}"
else:
results[filename] = f"Failed to generate options in {filename}: {e}"
else:
results[filename] = True
return results, rolled_results

View File

@@ -5,12 +5,14 @@ import collections
import datetime
import functools
import logging
import multiprocessing
import pickle
import random
import socket
import threading
import time
import typing
import sys
import websockets
from pony.orm import commit, db_session, select
@@ -18,15 +20,18 @@ from pony.orm import commit, db_session, select
import Utils
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless
from Utils import restricted_loads, cache_argsless
from .locker import Locker
from .models import Command, GameDataPackage, Room, db
class CustomClientMessageProcessor(ClientMessageProcessor):
ctx: WebHostContext
def _cmd_video(self, platform, user):
"""Set a link for your name in the WebHostLib tracker pointing to a video stream"""
def _cmd_video(self, platform: str, user: str):
"""Set a link for your name in the WebHostLib tracker pointing to a video stream.
Currently, only YouTube and Twitch platforms are supported.
"""
if platform.lower().startswith("t"): # twitch
self.ctx.video[self.client.team, self.client.slot] = "Twitch", user
self.ctx.save()
@@ -49,17 +54,19 @@ del MultiServer
class DBCommandProcessor(ServerCommandProcessor):
def output(self, text: str):
logging.info(text)
self.ctx.logger.info(text)
class WebHostContext(Context):
room_id: int
def __init__(self, static_server_data: dict):
def __init__(self, static_server_data: dict, logger: logging.Logger):
# static server data is used during _load_game_data to load required data,
# without needing to import worlds system, which takes quite a bit of memory
self.static_server_data = static_server_data
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2)
super(WebHostContext, self).__init__("", 0, "", "", 1,
40, True, "enabled", "enabled",
"enabled", 0, 2, logger=logger)
del self.static_server_data
self.main_loop = asyncio.get_running_loop()
self.video = {}
@@ -67,6 +74,7 @@ class WebHostContext(Context):
def _load_game_data(self):
for key, value in self.static_server_data.items():
# NOTE: attributes are mutable and shared, so they will have to be copied before being modified
setattr(self, key, value)
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
@@ -94,17 +102,37 @@ class WebHostContext(Context):
multidata = self.decompress(room.seed.multidata)
game_data_packages = {}
for game in list(multidata["datapackage"]):
static_gamespackage = self.gamespackage # this is shared across all rooms
static_item_name_groups = self.item_name_groups
static_location_name_groups = self.location_name_groups
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", {})}
for game in list(multidata.get("datapackage", {})):
game_data = multidata["datapackage"][game]
if "checksum" in game_data:
if self.gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
# non-custom. remove from multidata
if static_gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
# non-custom. remove from multidata and use static data
# games package could be dropped from static data once all rooms embed data package
del multidata["datapackage"][game]
else:
data = Utils.restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data)
game_data_packages[game] = data
row = GameDataPackage.get(checksum=game_data["checksum"])
if row: # None if rolled on >= 0.3.9 but uploaded to <= 0.3.8. multidata should be complete
game_data_packages[game] = Utils.restricted_loads(row.data)
continue
else:
self.logger.warning(f"Did not find game_data_package for {game}: {game_data['checksum']}")
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:
# all static -> use the static dicts directly
self.gamespackage = static_gamespackage
self.item_name_groups = static_item_name_groups
self.location_name_groups = static_location_name_groups
return self._load(multidata, game_data_packages, True)
@db_session
@@ -114,7 +142,7 @@ class WebHostContext(Context):
savegame_data = Room.get(id=self.room_id).multisave
if savegame_data:
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
self._start_async_saving()
self._start_async_saving(atexit_save=False)
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
@db_session
@@ -140,78 +168,176 @@ def get_random_port():
def get_static_server_data() -> dict:
import worlds
data = {
"non_hintable_names": {},
"gamespackage": worlds.network_data_package["games"],
"item_name_groups": {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()},
"location_name_groups": {world_name: world.location_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()},
"non_hintable_names": {
world_name: world.hint_blacklist
for world_name, world in worlds.AutoWorldRegister.world_types.items()
},
"gamespackage": {
world_name: {
key: value
for key, value in game_package.items()
if key not in ("item_name_groups", "location_name_groups")
}
for world_name, game_package in worlds.network_data_package["games"].items()
},
"item_name_groups": {
world_name: world.item_name_groups
for world_name, world in worlds.AutoWorldRegister.world_types.items()
},
"location_name_groups": {
world_name: world.location_name_groups
for world_name, world in worlds.AutoWorldRegister.world_types.items()
},
}
for world_name, world in worlds.AutoWorldRegister.world_types.items():
data["non_hintable_names"][world_name] = world.hint_blacklist
return data
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
def set_up_logging(room_id) -> logging.Logger:
import os
# logger setup
logger = logging.getLogger(f"RoomLogger {room_id}")
# this *should* be empty, but just in case.
for handler in logger.handlers[:]:
logger.removeHandler(handler)
handler.close()
file_handler = logging.FileHandler(
os.path.join(Utils.user_path("logs"), f"{room_id}.txt"),
"a",
encoding="utf-8-sig")
file_handler.setFormatter(logging.Formatter("[%(asctime)s]: %(message)s"))
logger.setLevel(logging.INFO)
logger.addHandler(file_handler)
return logger
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
host: str):
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
Utils.init_logging(name)
try:
import resource
except ModuleNotFoundError:
pass # unix only module
else:
# Each Server is another file handle, so request as many as we can from the system
file_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
# set soft limit to hard limit
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
del resource, file_limit
# establish DB connection for multidata and multisave
db.bind(**ponyconfig)
db.generate_mapping(check_tables=False)
async def main():
Utils.init_logging(str(room_id), write_mode="a")
ctx = WebHostContext(static_server_data)
ctx.load(room_id)
ctx.init_save()
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
try:
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None,
ping_interval=None, ssl=ssl_context)
if "worlds" in sys.modules:
raise Exception("Worlds system should not be loaded in the custom server.")
await ctx.server
except Exception: # likely port in use - in windows this is OSError, but I didn't check the others
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ping_timeout=None,
ping_interval=None, ssl=ssl_context)
import gc
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
del cert_file, cert_key_file, ponyconfig
gc.collect() # free intermediate objects used during setup
await ctx.server
port = 0
for wssocket in ctx.server.ws_server.sockets:
socketname = wssocket.getsockname()
if wssocket.family == socket.AF_INET6:
# Prefer IPv4, as most users seem to not have working ipv6 support
if not port:
port = socketname[1]
elif wssocket.family == socket.AF_INET:
port = socketname[1]
if port:
logging.info(f'Hosting game at {host}:{port}')
with db_session:
room = Room.get(id=ctx.room_id)
room.last_port = port
else:
logging.exception("Could not determine port. Likely hosting failure.")
with db_session:
ctx.auto_shutdown = Room.get(id=room_id).timeout
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
await ctx.shutdown_task
logging.info("Shutting down")
loop = asyncio.get_event_loop()
from .autolauncher import Locker
with Locker(room_id):
try:
asyncio.run(main())
except KeyboardInterrupt:
with db_session:
room = Room.get(id=room_id)
# ensure the Room does not spin up again on its own, minute of safety buffer
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
except:
with db_session:
room = Room.get(id=room_id)
room.last_port = -1
# ensure the Room does not spin up again on its own, minute of safety buffer
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
raise
async def start_room(room_id):
with Locker(f"RoomLocker {room_id}"):
try:
logger = set_up_logging(room_id)
ctx = WebHostContext(static_server_data, logger)
ctx.load(room_id)
ctx.init_save()
try:
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
await ctx.server
except OSError: # likely port in use
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
await ctx.server
port = 0
for wssocket in ctx.server.ws_server.sockets:
socketname = wssocket.getsockname()
if wssocket.family == socket.AF_INET6:
# Prefer IPv4, as most users seem to not have working ipv6 support
if not port:
port = socketname[1]
elif wssocket.family == socket.AF_INET:
port = socketname[1]
if port:
ctx.logger.info(f'Hosting game at {host}:{port}')
with db_session:
room = Room.get(id=ctx.room_id)
room.last_port = port
else:
ctx.logger.exception("Could not determine port. Likely hosting failure.")
with db_session:
ctx.auto_shutdown = Room.get(id=room_id).timeout
if ctx.saving:
setattr(asyncio.current_task(), "save", lambda: ctx._save(True))
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
await ctx.shutdown_task
except (KeyboardInterrupt, SystemExit):
if ctx.saving:
ctx._save()
setattr(asyncio.current_task(), "save", None)
except Exception as e:
with db_session:
room = Room.get(id=room_id)
room.last_port = -1
logger.exception(e)
raise
else:
if ctx.saving:
ctx._save()
setattr(asyncio.current_task(), "save", None)
finally:
try:
ctx.save_dirty = False # make sure the saving thread does not write to DB after final wakeup
ctx.exit_event.set() # make sure the saving thread stops at some point
# NOTE: async saving should probably be an async task and could be merged with shutdown_task
with (db_session):
# ensure the Room does not spin up again on its own, minute of safety buffer
room = Room.get(id=room_id)
room.last_activity = datetime.datetime.utcnow() - \
datetime.timedelta(minutes=1, seconds=room.timeout)
logging.info(f"Shutting down room {room_id} on {name}.")
finally:
await asyncio.sleep(5)
rooms_shutting_down.put(room_id)
class Starter(threading.Thread):
_tasks: typing.List[asyncio.Future]
def __init__(self):
super().__init__()
self._tasks = []
def _done(self, task: asyncio.Future):
self._tasks.remove(task)
task.result()
def run(self):
while 1:
next_room = rooms_to_run.get(block=True, timeout=None)
task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
self._tasks.append(task)
task.add_done_callback(self._done)
logging.info(f"Starting room {next_room} on {name}.")
starter = Starter()
starter.daemon = True
starter.start()
try:
loop.run_forever()
finally:
# save all tasks that want to be saved during shutdown
for task in asyncio.all_tasks(loop):
save: typing.Optional[typing.Callable[[], typing.Any]] = getattr(task, "save", None)
if save:
save()

View File

@@ -90,6 +90,8 @@ def download_slot_file(room_id, player_id: int):
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json"
elif slot_data.game == "Kingdom Hearts 2":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.zip"
elif slot_data.game == "Final Fantasy Mystic Quest":
fname = f"AP+{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmq"
else:
return "Game download not supported."
return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname)

View File

@@ -1,45 +1,57 @@
import concurrent.futures
import json
import os
import pickle
import random
import tempfile
import zipfile
import concurrent.futures
from collections import Counter
from typing import Dict, Optional, Any
from typing import Any, Dict, List, Optional, Union, Set
from flask import request, flash, redirect, url_for, session, render_template
from flask import flash, redirect, render_template, request, session, url_for
from pony.orm import commit, db_session
from BaseClasses import seeddigits, get_seed
from Generate import handle_name, PlandoOptions
from BaseClasses import get_seed, seeddigits
from Generate import PlandoOptions, handle_name
from Main import main as ERmain
from Utils import __version__
from WebHostLib import app
from settings import ServerOptions, GeneratorOptions
from worlds.alttp.EntranceRandomizer import parse_arguments
from .check import get_yaml_data, roll_options
from .models import Generation, STATE_ERROR, STATE_QUEUED, Seed, UUID
from .upload import upload_zip_to_db
def get_meta(options_source: dict) -> dict:
plando_options = {
options_source.get("plando_bosses", ""),
options_source.get("plando_items", ""),
options_source.get("plando_connections", ""),
options_source.get("plando_texts", "")
}
plando_options -= {""}
def get_meta(options_source: dict, race: bool = False) -> Dict[str, Union[List[str], Dict[str, Any]]]:
plando_options: Set[str] = set()
for substr in ("bosses", "items", "connections", "texts"):
if options_source.get(f"plando_{substr}", substr in GeneratorOptions.plando_options):
plando_options.add(substr)
server_options = {
"hint_cost": int(options_source.get("hint_cost", 10)),
"release_mode": options_source.get("release_mode", "goal"),
"remaining_mode": options_source.get("remaining_mode", "disabled"),
"collect_mode": options_source.get("collect_mode", "disabled"),
"item_cheat": bool(int(options_source.get("item_cheat", 1))),
"hint_cost": int(options_source.get("hint_cost", ServerOptions.hint_cost)),
"release_mode": options_source.get("release_mode", ServerOptions.release_mode),
"remaining_mode": options_source.get("remaining_mode", ServerOptions.remaining_mode),
"collect_mode": options_source.get("collect_mode", ServerOptions.collect_mode),
"item_cheat": bool(int(options_source.get("item_cheat", not ServerOptions.disable_item_cheat))),
"server_password": options_source.get("server_password", None),
}
return {"server_options": server_options, "plando_options": list(plando_options)}
generator_options = {
"spoiler": int(options_source.get("spoiler", GeneratorOptions.spoiler)),
"race": race,
}
if race:
server_options["item_cheat"] = False
server_options["remaining_mode"] = "disabled"
generator_options["spoiler"] = 0
return {
"server_options": server_options,
"plando_options": list(plando_options),
"generator_options": generator_options,
}
@app.route('/generate', methods=['GET', 'POST'])
@@ -50,54 +62,53 @@ def generate(race=False):
if 'file' not in request.files:
flash('No file part')
else:
file = request.files['file']
options = get_yaml_data(file)
files = request.files.getlist('file')
options = get_yaml_data(files)
if isinstance(options, str):
flash(options)
else:
meta = get_meta(request.form)
meta["race"] = race
results, gen_options = roll_options(options, meta["plando_options"])
if race:
meta["server_options"]["item_cheat"] = False
meta["server_options"]["remaining_mode"] = "disabled"
if any(type(result) == str for result in results.values()):
return render_template("checkResult.html", results=results)
elif len(gen_options) > app.config["MAX_ROLL"]:
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
f"If you have a larger group, please generate it yourself and upload it.")
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
gen = Generation(
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
# convert to json compatible
meta=json.dumps(meta),
state=STATE_QUEUED,
owner=session["_id"])
commit()
return redirect(url_for("wait_seed", seed=gen.id))
else:
try:
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
meta=meta, owner=session["_id"].int)
except BaseException as e:
from .autolauncher import handle_generation_failure
handle_generation_failure(e)
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
return redirect(url_for("view_seed", seed=seed_id))
meta = get_meta(request.form, race)
return start_generation(options, meta)
return render_template("generate.html", race=race, version=__version__)
def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any]):
results, gen_options = roll_options(options, set(meta["plando_options"]))
if any(type(result) == str for result in results.values()):
return render_template("checkResult.html", results=results)
elif len(gen_options) > app.config["MAX_ROLL"]:
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
f"If you have a larger group, please generate it yourself and upload it.")
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
gen = Generation(
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
# convert to json compatible
meta=json.dumps(meta),
state=STATE_QUEUED,
owner=session["_id"])
commit()
return redirect(url_for("wait_seed", seed=gen.id))
else:
try:
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
meta=meta, owner=session["_id"].int)
except BaseException as e:
from .autolauncher import handle_generation_failure
handle_generation_failure(e)
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
return redirect(url_for("view_seed", seed=seed_id))
def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
if not meta:
meta: Dict[str, Any] = {}
meta.setdefault("server_options", {}).setdefault("hint_cost", 10)
race = meta.setdefault("race", False)
race = meta.setdefault("generator_options", {}).setdefault("race", False)
def task():
target = tempfile.TemporaryDirectory()
@@ -114,13 +125,15 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
erargs = parse_arguments(['--multi', str(playercount)])
erargs.seed = seed
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten in mystery
erargs.spoiler = 0 if race else 3
erargs.spoiler = meta["generator_options"].get("spoiler", 0)
erargs.race = race
erargs.outputname = seedname
erargs.outputpath = target.name
erargs.teams = 1
erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options",
{"bosses", "items", "connections", "texts"}))
{"bosses", "items", "connections", "texts"}))
erargs.skip_prog_balancing = False
erargs.skip_output = False
name_counter = Counter()
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):

51
WebHostLib/locker.py Normal file
View File

@@ -0,0 +1,51 @@
import os
import sys
class CommonLocker:
"""Uses a file lock to signal that something is already running"""
lock_folder = "file_locks"
def __init__(self, lockname: str, folder=None):
if folder:
self.lock_folder = folder
os.makedirs(self.lock_folder, exist_ok=True)
self.lockname = lockname
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
class AlreadyRunningException(Exception):
pass
if sys.platform == 'win32':
class Locker(CommonLocker):
def __enter__(self):
try:
if os.path.exists(self.lockfile):
os.unlink(self.lockfile)
self.fp = os.open(
self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
except OSError as e:
raise AlreadyRunningException() from e
def __exit__(self, _type, value, tb):
fp = getattr(self, "fp", None)
if fp:
os.close(self.fp)
os.unlink(self.lockfile)
else: # unix
import fcntl
class Locker(CommonLocker):
def __enter__(self):
try:
self.fp = open(self.lockfile, "wb")
fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError as e:
raise AlreadyRunningException() from e
def __exit__(self, _type, value, tb):
fcntl.flock(self.fp.fileno(), fcntl.LOCK_UN)
self.fp.close()

View File

@@ -32,29 +32,21 @@ def page_not_found(err):
# Start Playing Page
@app.route('/start-playing')
@cache.cached()
def start_playing():
return render_template(f"startPlaying.html")
@app.route('/weighted-settings')
def weighted_settings():
return render_template(f"weighted-settings.html")
# Player settings pages
@app.route('/games/<string:game>/player-settings')
def player_settings(game):
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
# Game Info Pages
@app.route('/games/<string:game>/info/<string:lang>')
@cache.cached()
def game_info(game, lang):
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
# List of supported games
@app.route('/games')
@cache.cached()
def games():
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
@@ -64,21 +56,25 @@ def games():
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
@cache.cached()
def tutorial(game, file, lang):
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
@app.route('/tutorial/')
@cache.cached()
def tutorial_landing():
return render_template("tutorialLanding.html")
@app.route('/faq/<string:lang>/')
@cache.cached()
def faq(lang):
return render_template("faq.html", lang=lang)
@app.route('/glossary/<string:lang>/')
@cache.cached()
def terms(lang):
return render_template("glossary.html", lang=lang)
@@ -116,7 +112,11 @@ def display_log(room: UUID):
if room is None:
return abort(404)
if room.owner == session["_id"]:
return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8")
file_path = os.path.join("logs", str(room.id) + ".txt")
if os.path.exists(file_path):
return Response(_read_log(file_path), mimetype="text/plain;charset=UTF-8")
return "Log File does not exist."
return "Access Denied", 403
@@ -131,6 +131,7 @@ def host_room(room: UUID):
if cmd:
Command(room=room, commandtext=cmd)
commit()
return redirect(url_for("host_room", room=room.id))
now = datetime.datetime.utcnow()
# indicate that the page should reload to get the assigned port
@@ -143,7 +144,7 @@ def host_room(room: UUID):
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static/static'),
return send_from_directory(os.path.join(app.root_path, "static", "static"),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@@ -163,10 +164,11 @@ def get_datapackage():
@app.route('/index')
@app.route('/sitemap')
@cache.cached()
def get_sitemap():
available_games: List[Dict[str, Union[str, bool]]] = []
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
has_settings: bool = isinstance(world.web.settings_page, bool) and world.web.settings_page
has_settings: bool = isinstance(world.web.options_page, bool) and world.web.options_page
available_games.append({ 'title': game, 'has_settings': has_settings })
return render_template("siteMap.html", games=available_games)

View File

@@ -21,7 +21,7 @@ class Slot(db.Entity):
class Room(db.Entity):
id = PrimaryKey(UUID, default=uuid4)
last_activity = Required(datetime, default=lambda: datetime.utcnow(), index=True)
creation_time = Required(datetime, default=lambda: datetime.utcnow())
creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
owner = Required(UUID, index=True)
commands = Set('Command')
seed = Required('Seed', index=True)
@@ -38,7 +38,7 @@ class Seed(db.Entity):
rooms = Set(Room)
multidata = Required(bytes, lazy=True)
owner = Required(UUID, index=True)
creation_time = Required(datetime, default=lambda: datetime.utcnow())
creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
slots = Set(Slot)
spoiler = Optional(LongStr, lazy=True)
meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags

View File

@@ -1,178 +1,274 @@
import collections.abc
import json
import logging
import os
import typing
from textwrap import dedent
from typing import Dict, Union
from docutils.core import publish_parts
import yaml
from jinja2 import Template
from flask import redirect, render_template, request, Response
import Options
from Utils import __version__, local_path
from Utils import local_path
from worlds.AutoWorld import AutoWorldRegister
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
"exclude_locations", "priority_locations"}
from . import app, cache
from .generate import get_meta
def create():
def create() -> None:
target_folder = local_path("WebHostLib", "static", "generated")
yaml_folder = os.path.join(target_folder, "configs")
os.makedirs(yaml_folder, exist_ok=True)
for file in os.listdir(yaml_folder):
full_path: str = os.path.join(yaml_folder, file)
if os.path.isfile(full_path):
os.unlink(full_path)
Options.generate_yaml_templates(yaml_folder)
def dictify_range(option: typing.Union[Options.Range, Options.SpecialRange]):
data = {option.default: 50}
for sub_option in ["random", "random-low", "random-high"]:
if sub_option != option.default:
data[sub_option] = 0
notes = {}
for name, number in getattr(option, "special_range_names", {}).items():
notes[name] = f"equivalent to {number}"
if number in data:
data[name] = data[number]
del data[number]
def get_world_theme(game_name: str) -> str:
if game_name in AutoWorldRegister.world_types:
return AutoWorldRegister.world_types[game_name].web.theme
return 'grass'
def render_options_page(template: str, world_name: str, is_complex: bool = False) -> Union[Response, str]:
world = AutoWorldRegister.world_types[world_name]
if world.hidden or world.web.options_page is False:
return redirect("games")
visibility_flag = Options.Visibility.complex_ui if is_complex else Options.Visibility.simple_ui
start_collapsed = {"Game Options": False}
for group in world.web.option_groups:
start_collapsed[group.name] = group.start_collapsed
return render_template(
template,
world_name=world_name,
world=world,
option_groups=Options.get_option_groups(world, visibility_level=visibility_flag),
start_collapsed=start_collapsed,
issubclass=issubclass,
Options=Options,
theme=get_world_theme(world_name),
)
def generate_game(options: Dict[str, Union[dict, str]]) -> Union[Response, str]:
from .generate import start_generation
return start_generation(options, get_meta({}))
def send_yaml(player_name: str, formatted_options: dict) -> Response:
response = Response(yaml.dump(formatted_options, sort_keys=False))
response.headers["Content-Type"] = "text/yaml"
response.headers["Content-Disposition"] = f"attachment; filename={player_name}.yaml"
return response
@app.template_filter("dedent")
def filter_dedent(text: str) -> str:
return dedent(text).strip("\n ")
@app.template_filter("rst_to_html")
def filter_rst_to_html(text: str) -> str:
"""Converts reStructuredText (such as a Python docstring) to HTML."""
if text.startswith(" ") or text.startswith("\t"):
text = dedent(text)
elif "\n" in text:
lines = text.splitlines()
text = lines[0] + "\n" + dedent("\n".join(lines[1:]))
return publish_parts(text, writer_name='html', settings=None, settings_overrides={
'raw_enable': False,
'file_insertion_enabled': False,
'output_encoding': 'unicode'
})['body']
@app.template_test("ordered")
def test_ordered(obj):
return isinstance(obj, collections.abc.Sequence)
@app.route("/games/<string:game>/option-presets", methods=["GET"])
@cache.cached()
def option_presets(game: str) -> Response:
world = AutoWorldRegister.world_types[game]
presets = {}
for preset_name, preset in world.web.options_presets.items():
presets[preset_name] = {}
for preset_option_name, preset_option in preset.items():
if preset_option == "random":
presets[preset_name][preset_option_name] = preset_option
continue
option = world.options_dataclass.type_hints[preset_option_name].from_any(preset_option)
if isinstance(option, Options.NamedRange) and isinstance(preset_option, str):
assert preset_option in option.special_range_names, \
f"Invalid preset value '{preset_option}' for '{preset_option_name}' in '{preset_name}'. " \
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
presets[preset_name][preset_option_name] = option.value
elif isinstance(option, (Options.Range, Options.OptionSet, Options.OptionList, Options.ItemDict)):
presets[preset_name][preset_option_name] = option.value
elif isinstance(preset_option, str):
# Ensure the option value is valid for Choice and Toggle options
assert option.name_lookup[option.value] == preset_option, \
f"Invalid option value '{preset_option}' for '{preset_option_name}' in preset '{preset_name}'. " \
f"Values must not be resolved to a different option via option.from_text (or an alias)."
# Use the name of the option
presets[preset_name][preset_option_name] = option.current_key
else:
data[name] = 0
# Use the name of the option
presets[preset_name][preset_option_name] = option.current_key
return data, notes
class SetEncoder(json.JSONEncoder):
def default(self, obj):
from collections.abc import Set
if isinstance(obj, Set):
return list(obj)
return json.JSONEncoder.default(self, obj)
def get_html_doc(option_type: type(Options.Option)) -> str:
if not option_type.__doc__:
return "Please document me!"
return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip()
json_data = json.dumps(presets, cls=SetEncoder)
response = Response(json_data)
response.headers["Content-Type"] = "application/json"
return response
weighted_settings = {
"baseOptions": {
"description": "Generated by https://archipelago.gg/",
"name": "Player",
"game": {},
},
"games": {},
}
for game_name, world in AutoWorldRegister.world_types.items():
@app.route("/weighted-options")
def weighted_options_old():
return redirect("games", 301)
all_options: typing.Dict[str, Options.AssembleOptions] = {
**Options.per_game_common_options,
**world.option_definitions
}
with open(local_path("WebHostLib", "templates", "options.yaml")) as f:
file_data = f.read()
res = Template(file_data).render(
options=all_options,
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
dictify_range=dictify_range,
)
del file_data
@app.route("/games/<string:game>/weighted-options")
@cache.cached()
def weighted_options(game: str):
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
with open(os.path.join(target_folder, "configs", game_name + ".yaml"), "w", encoding="utf-8") as f:
f.write(res)
# Generate JSON files for player-settings pages
player_settings = {
"baseOptions": {
"description": "Generated by https://archipelago.gg/",
"game": game_name,
"name": "Player",
},
@app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"])
def generate_weighted_yaml(game: str):
if request.method == "POST":
intent_generate = False
options = {}
for key, val in request.form.items():
if "||" not in key:
if len(str(val)) == 0:
continue
options[key] = val
else:
if int(val) == 0:
continue
[option, setting] = key.split("||")
options.setdefault(option, {})[setting] = int(val)
# Error checking
if "name" not in options:
return "Player name is required."
# Remove POST data irrelevant to YAML
if "intent-generate" in options:
intent_generate = True
del options["intent-generate"]
if "intent-export" in options:
del options["intent-export"]
# Properly format YAML output
player_name = options["name"]
del options["name"]
formatted_options = {
"name": player_name,
"game": game,
"description": f"Generated by https://archipelago.gg/ for {game}",
game: options,
}
game_options = {}
for option_name, option in all_options.items():
if option_name in handled_in_js:
pass
if intent_generate:
return generate_game({player_name: formatted_options})
elif issubclass(option, Options.Choice) or issubclass(option, Options.Toggle):
game_options[option_name] = this_option = {
"type": "select",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
"defaultValue": None,
"options": []
}
else:
return send_yaml(player_name, formatted_options)
for sub_option_id, sub_option_name in option.name_lookup.items():
if sub_option_name != "random":
this_option["options"].append({
"name": option.get_option_name(sub_option_id),
"value": sub_option_name,
})
if sub_option_id == option.default:
this_option["defaultValue"] = sub_option_name
if not this_option["defaultValue"]:
this_option["defaultValue"] = "random"
# Player options pages
@app.route("/games/<string:game>/player-options")
@cache.cached()
def player_options(game: str):
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
elif issubclass(option, Options.Range):
game_options[option_name] = {
"type": "range",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
"defaultValue": option.default if hasattr(
option, "default") and option.default != "random" else option.range_start,
"min": option.range_start,
"max": option.range_end,
}
if issubclass(option, Options.SpecialRange):
game_options[option_name]["type"] = 'special_range'
game_options[option_name]["value_names"] = {}
for key, val in option.special_range_names.items():
game_options[option_name]["value_names"][key] = val
elif issubclass(option, Options.ItemSet):
game_options[option_name] = {
"type": "items-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
}
elif issubclass(option, Options.LocationSet):
game_options[option_name] = {
"type": "locations-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
}
elif issubclass(option, Options.VerifyKeys):
if option.valid_keys:
game_options[option_name] = {
"type": "custom-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
"options": list(option.valid_keys),
}
# YAML generator for player-options
@app.route("/games/<string:game>/generate-yaml", methods=["POST"])
def generate_yaml(game: str):
if request.method == "POST":
options = {}
intent_generate = False
for key, val in request.form.items(multi=True):
if key in options:
if not isinstance(options[key], list):
options[key] = [options[key]]
options[key].append(val)
else:
logging.debug(f"{option} not exported to Web Settings.")
options[key] = val
player_settings["gameOptions"] = game_options
for key, val in options.copy().items():
key_parts = key.rsplit("||", 2)
# Detect and build ItemDict options from their name pattern
if key_parts[-1] == "qty":
if key_parts[0] not in options:
options[key_parts[0]] = {}
if val != "0":
options[key_parts[0]][key_parts[1]] = int(val)
del options[key]
os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True)
# Detect keys which end with -custom, indicating a TextChoice with a possible custom value
elif key_parts[-1].endswith("-custom"):
if val:
options[key_parts[-1][:-7]] = val
with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f:
json.dump(player_settings, f, indent=2, separators=(',', ': '))
del options[key]
if not world.hidden and world.web.settings_page is True:
# Add the random option to Choice, TextChoice, and Toggle settings
for option in game_options.values():
if option["type"] == "select":
option["options"].append({"name": "Random", "value": "random"})
# Detect random-* keys and set their options accordingly
for key, val in options.copy().items():
if key.startswith("random-"):
options[key.removeprefix("random-")] = "random"
del options[key]
if not option["defaultValue"]:
option["defaultValue"] = "random"
# Error checking
if not options["name"]:
return "Player name is required."
weighted_settings["baseOptions"]["game"][game_name] = 0
weighted_settings["games"][game_name] = {}
weighted_settings["games"][game_name]["gameSettings"] = game_options
weighted_settings["games"][game_name]["gameItems"] = tuple(world.item_names)
weighted_settings["games"][game_name]["gameLocations"] = tuple(world.location_names)
# Remove POST data irrelevant to YAML
preset_name = 'default'
if "intent-generate" in options:
intent_generate = True
del options["intent-generate"]
if "intent-export" in options:
del options["intent-export"]
if "game-options-preset" in options:
preset_name = options["game-options-preset"]
del options["game-options-preset"]
with open(os.path.join(target_folder, 'weighted-settings.json'), "w") as f:
json.dump(weighted_settings, f, indent=2, separators=(',', ': '))
# Properly format YAML output
player_name = options["name"]
del options["name"]
description = f"Generated by https://archipelago.gg/ for {game}"
if preset_name != 'default' and preset_name != 'custom':
description += f" using {preset_name} preset"
formatted_options = {
"name": player_name,
"game": game,
"description": description,
game: options,
}
if intent_generate:
return generate_game({player_name: formatted_options})
else:
return send_yaml(player_name, formatted_options)

View File

@@ -1,7 +1,10 @@
flask>=2.2.3
pony>=0.7.16
waitress>=2.1.2
Flask-Caching>=2.0.2
Flask-Compress>=1.13
Flask-Limiter>=3.3.0
bokeh>=3.1.0
flask>=3.0.3
werkzeug>=3.0.3
pony>=0.7.17
waitress>=3.0.0
Flask-Caching>=2.3.0
Flask-Compress>=1.15
Flask-Limiter>=3.7.0
bokeh>=3.1.1; python_version <= '3.8'
bokeh>=3.4.1; python_version >= '3.9'
markupsafe>=2.1.5

14
WebHostLib/robots.py Normal file
View File

@@ -0,0 +1,14 @@
from WebHostLib import app
from flask import abort
from . import cache
@cache.cached()
@app.route('/robots.txt')
def robots():
# If this host is not official, do not allow search engine crawling
if not app.config["ASSET_RIGHTS"]:
return app.send_static_file('robots.txt')
# Send 404 if the host has affirmed this to be the official WebHost
abort(404)

View File

@@ -2,13 +2,62 @@
## What is a randomizer?
A randomizer is a modification of a video game which reorganizes the items required to progress through the game. A
normal play-through of a game might require you to use item A to unlock item B, then C, and so forth. In a randomized
A randomizer is a modification of a game which reorganizes the items required to progress through that game. A
normal play-through might require you to use item A to unlock item B, then C, and so forth. In a randomized
game, you might first find item C, then A, then B.
This transforms games from a linear experience into a puzzle, presenting players with a new challenge each time they
play a randomized game. Putting items in non-standard locations can require the player to think about the game world and
the items they encounter in new and interesting ways.
This transforms the game from a linear experience into a puzzle, presenting players with a new challenge each time they
play. Putting items in non-standard locations can require the player to think about the game world and the items they
encounter in new and interesting ways.
## What is a multiworld?
While a randomizer shuffles a game, a multiworld randomizer shuffles that game for multiple players. For example, in a
two player multiworld, players A and B each get their own randomized version of a game, called a world. In each
player's game, they may find items which belong to the other player. If player A finds an item which belongs to
player B, the item will be sent to player B's world over the internet. This creates a cooperative experience, requiring
players to rely upon each other to complete their game.
## What does multi-game mean?
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.
Here is a list of our [Supported Games](https://archipelago.gg/games).
## Can I generate a single-player game with Archipelago?
Yes. All of our supported games can be generated as single-player experiences both on the website and by installing
the Archipelago generator software. The fastest way to do this is on the website. Find the Supported Game you wish to
play, open the Settings Page, pick your settings, and click Generate Game.
## How do I get started?
We have a [Getting Started](https://archipelago.gg/tutorial/Archipelago/setup/en) guide that will help you get the
software set up. You can use that guide to learn how to generate multiworlds. There are also basic instructions for
including multiple games, and hosting multiworlds on the website for ease and convenience.
If you are ready to start randomizing games, or want to start playing your favorite randomizer with others, please join
our discord server at the [Archipelago Discord](https://discord.gg/8Z65BR2). There are always people ready to answer
any questions you might have.
## What are some common terms I should know?
As randomizers and multiworld randomizers have been around for a while now, there are quite a few common terms used
by the communities surrounding them. A list of Archipelago jargon and terms commonly used by the community can be
found in the [Glossary](/glossary/en).
## Does everyone need to be connected at the same time?
There are two different play-styles that are common for Archipelago multiworld sessions. These sessions can either
be considered synchronous (or "sync"), where everyone connects and plays at the same time, or asynchronous (or "async"),
where players connect and play at their own pace. The setup for both is identical. The difference in play-style is how
you and your friends choose to organize and play your multiworld. Most groups decide on the format before creating
their multiworld.
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all items
in that game belonging to other players are sent out automatically. This allows other players to continue to play
uninterrupted. Here is a list of all of our [Server Commands](https://archipelago.gg/tutorial/Archipelago/commands/en).
## What happens if an item is placed somewhere it is impossible to get?
@@ -17,53 +66,15 @@ is to ensure items necessary to complete the game will be accessible to the play
rules allowing certain items to be placed in normally unreachable locations, provided the player has indicated they are
comfortable exploiting certain glitches in the game.
## What is a multi-world?
While a randomizer shuffles a game, a multi-world randomizer shuffles that game for multiple players. For example, in a
two player multi-world, players A and B each get their own randomized version of a game, called a world. In each player's
game, they may find items which belong to the other player. If player A finds an item which belongs to player B, the
item will be sent to player B's world over the internet.
This creates a cooperative experience during multi-world games, requiring players to rely upon each other to complete
their game.
## What happens if a person has to leave early?
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all the
items in that game which belong to other players are sent out automatically, so other players can continue to play.
## What does multi-game mean?
While a multi-world game traditionally requires all players to be playing the same game, a multi-game multi-world allows
players to randomize any of a number of supported games, and send items between them. This allows players of different
games to interact with one another in a single multiplayer environment.
## Can I generate a single-player game with Archipelago?
Yes. All our supported games can be generated as single-player experiences, and so long as you download the software,
the website is not required to generate them.
## How do I get started?
If you are ready to start randomizing games, or want to start playing your favorite randomizer with others, please join
our discord server at the [Archipelago Discord](https://discord.gg/8Z65BR2). There are always people ready to answer
any questions you might have.
## What are some common terms I should know?
As randomizers and multiworld randomizers have been around for a while now there are quite a lot of common terms
and jargon that is used in conjunction by the communities surrounding them. For a lot of the terms that are more common
to Archipelago and its specific systems please see the [Glossary](/glossary/en).
## I want to add a game to the Archipelago randomizer. How do I do that?
The best way to get started is to take a look at our code on GitHub
at [Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago).
The best way to get started is to take a look at our code on GitHub:
[Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago).
There you will find examples of games in the worlds folder
at [/worlds Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds).
There, you will find examples of games in the `worlds` folder:
[/worlds Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds).
You may also find developer documentation in the docs folder
at [/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
You may also find developer documentation in the `docs` folder:
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.

View File

@@ -1,20 +0,0 @@
window.addEventListener('load', () => {
const url = window.location;
setInterval(() => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
// Create a fake DOM using the returned HTML
const domParser = new DOMParser();
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
// Update item and location trackers
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
document.getElementById('location-table').innerHTML = fakeDOM.getElementById('location-table').innerHTML;
};
ajax.open('GET', url);
ajax.send();
}, 15000)
});

View File

@@ -1,395 +0,0 @@
let gameName = null;
window.addEventListener('load', () => {
gameName = document.getElementById('player-settings').getAttribute('data-game');
// Update game name on page
document.getElementById('game-name').innerText = gameName;
fetchSettingData().then((results) => {
let settingHash = localStorage.getItem(`${gameName}-hash`);
if (!settingHash) {
// If no hash data has been set before, set it now
settingHash = md5(JSON.stringify(results));
localStorage.setItem(`${gameName}-hash`, settingHash);
localStorage.removeItem(gameName);
}
if (settingHash !== md5(JSON.stringify(results))) {
showUserMessage("Your settings are out of date! Click here to update them! Be aware this will reset " +
"them all to default.");
document.getElementById('user-message').addEventListener('click', resetSettings);
}
// Page setup
createDefaultSettings(results);
buildUI(results);
adjustHeaderWidth();
// Event listeners
document.getElementById('export-settings').addEventListener('click', () => exportSettings());
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
document.getElementById('generate-game').addEventListener('click', () => generateGame());
// Name input field
const playerSettings = JSON.parse(localStorage.getItem(gameName));
const nameInput = document.getElementById('player-name');
nameInput.addEventListener('keyup', (event) => updateBaseSetting(event));
nameInput.value = playerSettings.name;
}).catch((e) => {
console.error(e);
const url = new URL(window.location.href);
window.location.replace(`${url.protocol}//${url.hostname}/page-not-found`);
})
});
const resetSettings = () => {
localStorage.removeItem(gameName);
localStorage.removeItem(`${gameName}-hash`)
window.location.reload();
};
const fetchSettingData = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status !== 200) {
reject(ajax.responseText);
return;
}
try{ resolve(JSON.parse(ajax.responseText)); }
catch(error){ reject(error); }
};
ajax.open('GET', `${window.location.origin}/static/generated/player-settings/${gameName}.json`, true);
ajax.send();
});
const createDefaultSettings = (settingData) => {
if (!localStorage.getItem(gameName)) {
const newSettings = {
[gameName]: {},
};
for (let baseOption of Object.keys(settingData.baseOptions)){
newSettings[baseOption] = settingData.baseOptions[baseOption];
}
for (let gameOption of Object.keys(settingData.gameOptions)){
newSettings[gameName][gameOption] = settingData.gameOptions[gameOption].defaultValue;
}
localStorage.setItem(gameName, JSON.stringify(newSettings));
}
};
const buildUI = (settingData) => {
// Game Options
const leftGameOpts = {};
const rightGameOpts = {};
Object.keys(settingData.gameOptions).forEach((key, index) => {
if (index < Object.keys(settingData.gameOptions).length / 2) { leftGameOpts[key] = settingData.gameOptions[key]; }
else { rightGameOpts[key] = settingData.gameOptions[key]; }
});
document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts));
document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts));
};
const buildOptionsTable = (settings, romOpts = false) => {
const currentSettings = JSON.parse(localStorage.getItem(gameName));
const table = document.createElement('table');
const tbody = document.createElement('tbody');
Object.keys(settings).forEach((setting) => {
const tr = document.createElement('tr');
// td Left
const tdl = document.createElement('td');
const label = document.createElement('label');
label.textContent = `${settings[setting].displayName}: `;
label.setAttribute('for', setting);
const questionSpan = document.createElement('span');
questionSpan.classList.add('interactive');
questionSpan.setAttribute('data-tooltip', settings[setting].description);
questionSpan.innerText = '(?)';
label.appendChild(questionSpan);
tdl.appendChild(label);
tr.appendChild(tdl);
// td Right
const tdr = document.createElement('td');
let element = null;
const randomButton = document.createElement('button');
switch(settings[setting].type){
case 'select':
element = document.createElement('div');
element.classList.add('select-container');
let select = document.createElement('select');
select.setAttribute('id', setting);
select.setAttribute('data-key', setting);
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
settings[setting].options.forEach((opt) => {
const option = document.createElement('option');
option.setAttribute('value', opt.value);
option.innerText = opt.name;
if ((isNaN(currentSettings[gameName][setting]) &&
(parseInt(opt.value, 10) === parseInt(currentSettings[gameName][setting]))) ||
(opt.value === currentSettings[gameName][setting]))
{
option.selected = true;
}
select.appendChild(option);
});
select.addEventListener('change', (event) => updateGameSetting(event.target));
element.appendChild(select);
// Randomize button
randomButton.innerText = '🎲';
randomButton.classList.add('randomize-button');
randomButton.setAttribute('data-key', setting);
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
randomButton.addEventListener('click', (event) => toggleRandomize(event, [select]));
if (currentSettings[gameName][setting] === 'random') {
randomButton.classList.add('active');
select.disabled = true;
}
element.appendChild(randomButton);
break;
case 'range':
element = document.createElement('div');
element.classList.add('range-container');
let range = document.createElement('input');
range.setAttribute('type', 'range');
range.setAttribute('data-key', setting);
range.setAttribute('min', settings[setting].min);
range.setAttribute('max', settings[setting].max);
range.value = currentSettings[gameName][setting];
range.addEventListener('change', (event) => {
document.getElementById(`${setting}-value`).innerText = event.target.value;
updateGameSetting(event.target);
});
element.appendChild(range);
let rangeVal = document.createElement('span');
rangeVal.classList.add('range-value');
rangeVal.setAttribute('id', `${setting}-value`);
rangeVal.innerText = currentSettings[gameName][setting] !== 'random' ?
currentSettings[gameName][setting] : settings[setting].defaultValue;
element.appendChild(rangeVal);
// Randomize button
randomButton.innerText = '🎲';
randomButton.classList.add('randomize-button');
randomButton.setAttribute('data-key', setting);
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
randomButton.addEventListener('click', (event) => toggleRandomize(event, [range]));
if (currentSettings[gameName][setting] === 'random') {
randomButton.classList.add('active');
range.disabled = true;
}
element.appendChild(randomButton);
break;
case 'special_range':
element = document.createElement('div');
element.classList.add('special-range-container');
// Build the select element
let specialRangeSelect = document.createElement('select');
specialRangeSelect.setAttribute('data-key', setting);
Object.keys(settings[setting].value_names).forEach((presetName) => {
let presetOption = document.createElement('option');
presetOption.innerText = presetName;
presetOption.value = settings[setting].value_names[presetName];
const words = presetOption.innerText.split("_");
for (let i = 0; i < words.length; i++) {
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
}
presetOption.innerText = words.join(" ");
specialRangeSelect.appendChild(presetOption);
});
let customOption = document.createElement('option');
customOption.innerText = 'Custom';
customOption.value = 'custom';
customOption.selected = true;
specialRangeSelect.appendChild(customOption);
if (Object.values(settings[setting].value_names).includes(Number(currentSettings[gameName][setting]))) {
specialRangeSelect.value = Number(currentSettings[gameName][setting]);
}
// Build range element
let specialRangeWrapper = document.createElement('div');
specialRangeWrapper.classList.add('special-range-wrapper');
let specialRange = document.createElement('input');
specialRange.setAttribute('type', 'range');
specialRange.setAttribute('data-key', setting);
specialRange.setAttribute('min', settings[setting].min);
specialRange.setAttribute('max', settings[setting].max);
specialRange.value = currentSettings[gameName][setting];
// Build rage value element
let specialRangeVal = document.createElement('span');
specialRangeVal.classList.add('range-value');
specialRangeVal.setAttribute('id', `${setting}-value`);
specialRangeVal.innerText = currentSettings[gameName][setting] !== 'random' ?
currentSettings[gameName][setting] : settings[setting].defaultValue;
// Configure select event listener
specialRangeSelect.addEventListener('change', (event) => {
if (event.target.value === 'custom') { return; }
// Update range slider
specialRange.value = event.target.value;
document.getElementById(`${setting}-value`).innerText = event.target.value;
updateGameSetting(event.target);
});
// Configure range event handler
specialRange.addEventListener('change', (event) => {
// Update select element
specialRangeSelect.value =
(Object.values(settings[setting].value_names).includes(parseInt(event.target.value))) ?
parseInt(event.target.value) : 'custom';
document.getElementById(`${setting}-value`).innerText = event.target.value;
updateGameSetting(event.target);
});
element.appendChild(specialRangeSelect);
specialRangeWrapper.appendChild(specialRange);
specialRangeWrapper.appendChild(specialRangeVal);
element.appendChild(specialRangeWrapper);
// Randomize button
randomButton.innerText = '🎲';
randomButton.classList.add('randomize-button');
randomButton.setAttribute('data-key', setting);
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
randomButton.addEventListener('click', (event) => toggleRandomize(
event, [specialRange, specialRangeSelect])
);
if (currentSettings[gameName][setting] === 'random') {
randomButton.classList.add('active');
specialRange.disabled = true;
specialRangeSelect.disabled = true;
}
specialRangeWrapper.appendChild(randomButton);
break;
default:
console.error(`Ignoring unknown setting type: ${settings[setting].type} with name ${setting}`);
return;
}
tdr.appendChild(element);
tr.appendChild(tdr);
tbody.appendChild(tr);
});
table.appendChild(tbody);
return table;
};
const toggleRandomize = (event, inputElements) => {
const active = event.target.classList.contains('active');
const randomButton = event.target;
if (active) {
randomButton.classList.remove('active');
for (const element of inputElements) {
element.disabled = undefined;
updateGameSetting(element);
}
} else {
randomButton.classList.add('active');
for (const element of inputElements) {
element.disabled = true;
updateGameSetting(randomButton);
}
}
};
const updateBaseSetting = (event) => {
const options = JSON.parse(localStorage.getItem(gameName));
options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value);
localStorage.setItem(gameName, JSON.stringify(options));
};
const updateGameSetting = (settingElement) => {
const options = JSON.parse(localStorage.getItem(gameName));
if (settingElement.classList.contains('randomize-button')) {
// If the event passed in is the randomize button, then we know what we must do.
options[gameName][settingElement.getAttribute('data-key')] = 'random';
} else {
options[gameName][settingElement.getAttribute('data-key')] = isNaN(settingElement.value) ?
settingElement.value : parseInt(settingElement.value, 10);
}
localStorage.setItem(gameName, JSON.stringify(options));
};
const exportSettings = () => {
const settings = JSON.parse(localStorage.getItem(gameName));
if (!settings.name || settings.name.toLowerCase() === 'player' || settings.name.trim().length === 0) {
return showUserMessage('You must enter a player name!');
}
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
};
/** Create an anchor and trigger a download of a text file. */
const download = (filename, text) => {
const downloadLink = document.createElement('a');
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
downloadLink.setAttribute('download', filename);
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
};
const generateGame = (raceMode = false) => {
const settings = JSON.parse(localStorage.getItem(gameName));
if (!settings.name || settings.name.toLowerCase() === 'player' || settings.name.trim().length === 0) {
return showUserMessage('You must enter a player name!');
}
axios.post('/api/generate', {
weights: { player: settings },
presetData: { player: settings },
playerCount: 1,
race: raceMode ? '1' : '0',
}).then((response) => {
window.location.href = response.data.url;
}).catch((error) => {
let userMessage = 'Something went wrong and your game could not be generated.';
if (error.response.data.text) {
userMessage += ' ' + error.response.data.text;
}
showUserMessage(userMessage);
console.error(error);
});
};
const showUserMessage = (message) => {
const userMessage = document.getElementById('user-message');
userMessage.innerText = message;
userMessage.classList.add('visible');
window.scrollTo(0, 0);
userMessage.addEventListener('click', () => {
userMessage.classList.remove('visible');
userMessage.addEventListener('click', hideUserMessage);
});
};
const hideUserMessage = () => {
const userMessage = document.getElementById('user-message');
userMessage.classList.remove('visible');
userMessage.removeEventListener('click', hideUserMessage);
};

View File

@@ -0,0 +1,335 @@
let presets = {};
window.addEventListener('load', async () => {
// Load settings from localStorage, if available
loadSettings();
// Fetch presets if available
await fetchPresets();
// Handle changes to range inputs
document.querySelectorAll('input[type=range]').forEach((range) => {
const optionName = range.getAttribute('id');
range.addEventListener('change', () => {
document.getElementById(`${optionName}-value`).innerText = range.value;
// Handle updating named range selects to "custom" if appropriate
const select = document.querySelector(`select[data-option-name=${optionName}]`);
if (select) {
let updated = false;
select?.childNodes.forEach((option) => {
if (option.value === range.value) {
select.value = range.value;
updated = true;
}
});
if (!updated) {
select.value = 'custom';
}
}
});
});
// Handle changes to named range selects
document.querySelectorAll('.named-range-container select').forEach((select) => {
const optionName = select.getAttribute('data-option-name');
select.addEventListener('change', (evt) => {
document.getElementById(optionName).value = evt.target.value;
document.getElementById(`${optionName}-value`).innerText = evt.target.value;
});
});
// Handle changes to randomize checkboxes
document.querySelectorAll('.randomize-checkbox').forEach((checkbox) => {
const optionName = checkbox.getAttribute('data-option-name');
checkbox.addEventListener('change', () => {
const optionInput = document.getElementById(optionName);
const namedRangeSelect = document.querySelector(`select[data-option-name=${optionName}]`);
const customInput = document.getElementById(`${optionName}-custom`);
if (checkbox.checked) {
optionInput.setAttribute('disabled', '1');
namedRangeSelect?.setAttribute('disabled', '1');
if (customInput) {
customInput.setAttribute('disabled', '1');
}
} else {
optionInput.removeAttribute('disabled');
namedRangeSelect?.removeAttribute('disabled');
if (customInput) {
customInput.removeAttribute('disabled');
}
}
});
});
// Handle changes to TextChoice input[type=text]
document.querySelectorAll('.text-choice-container input[type=text]').forEach((input) => {
const optionName = input.getAttribute('data-option-name');
input.addEventListener('input', () => {
const select = document.getElementById(optionName);
const optionValues = [];
select.childNodes.forEach((option) => optionValues.push(option.value));
select.value = (optionValues.includes(input.value)) ? input.value : 'custom';
});
});
// Handle changes to TextChoice select
document.querySelectorAll('.text-choice-container select').forEach((select) => {
const optionName = select.getAttribute('id');
select.addEventListener('change', () => {
document.getElementById(`${optionName}-custom`).value = '';
});
});
// Update the "Option Preset" select to read "custom" when changes are made to relevant inputs
const presetSelect = document.getElementById('game-options-preset');
document.querySelectorAll('input, select').forEach((input) => {
if ( // Ignore inputs which have no effect on yaml generation
(input.id === 'player-name') ||
(input.id === 'game-options-preset') ||
(input.classList.contains('group-toggle')) ||
(input.type === 'submit')
) {
return;
}
input.addEventListener('change', () => {
presetSelect.value = 'custom';
});
});
// Handle changes to presets select
document.getElementById('game-options-preset').addEventListener('change', choosePreset);
// Save settings to localStorage when form is submitted
document.getElementById('options-form').addEventListener('submit', (evt) => {
const playerName = document.getElementById('player-name');
if (!playerName.value.trim()) {
evt.preventDefault();
window.scrollTo(0, 0);
showUserMessage('You must enter a player name!');
}
saveSettings();
});
});
// Save all settings to localStorage
const saveSettings = () => {
const options = {
inputs: {},
checkboxes: {},
};
document.querySelectorAll('input, select').forEach((input) => {
if (input.type === 'submit') {
// Ignore submit inputs
}
else if (input.type === 'checkbox') {
options.checkboxes[input.id] = input.checked;
}
else {
options.inputs[input.id] = input.value
}
});
const game = document.getElementById('player-options').getAttribute('data-game');
localStorage.setItem(game, JSON.stringify(options));
};
// Load all options from localStorage
const loadSettings = () => {
const game = document.getElementById('player-options').getAttribute('data-game');
const options = JSON.parse(localStorage.getItem(game));
if (options) {
if (!options.inputs || !options.checkboxes) {
localStorage.removeItem(game);
return;
}
// Restore value-based inputs and selects
Object.keys(options.inputs).forEach((key) => {
try{
document.getElementById(key).value = options.inputs[key];
const rangeValue = document.getElementById(`${key}-value`);
if (rangeValue) {
rangeValue.innerText = options.inputs[key];
}
} catch (err) {
console.error(`Unable to restore value to input with id ${key}`);
}
});
// Restore checkboxes
Object.keys(options.checkboxes).forEach((key) => {
try{
if (options.checkboxes[key]) {
document.getElementById(key).setAttribute('checked', '1');
}
} catch (err) {
console.error(`Unable to restore value to input with id ${key}`);
}
});
}
// Ensure any input for which the randomize checkbox is checked by default, the relevant inputs are disabled
document.querySelectorAll('.randomize-checkbox').forEach((checkbox) => {
const optionName = checkbox.getAttribute('data-option-name');
if (checkbox.checked) {
const input = document.getElementById(optionName);
if (input) {
input.setAttribute('disabled', '1');
}
const customInput = document.getElementById(`${optionName}-custom`);
if (customInput) {
customInput.setAttribute('disabled', '1');
}
}
});
};
/**
* Fetch the preset data for this game and apply the presets if localStorage indicates one was previously chosen
* @returns {Promise<void>}
*/
const fetchPresets = async () => {
const response = await fetch('option-presets');
presets = await response.json();
const presetSelect = document.getElementById('game-options-preset');
presetSelect.removeAttribute('disabled');
const game = document.getElementById('player-options').getAttribute('data-game');
const presetToApply = localStorage.getItem(`${game}-preset`);
const playerName = localStorage.getItem(`${game}-player`);
if (presetToApply) {
localStorage.removeItem(`${game}-preset`);
presetSelect.value = presetToApply;
applyPresets(presetToApply);
}
if (playerName) {
document.getElementById('player-name').value = playerName;
localStorage.removeItem(`${game}-player`);
}
};
/**
* Clear the localStorage for this game and set a preset to be loaded upon page reload
* @param evt
*/
const choosePreset = (evt) => {
if (evt.target.value === 'custom') { return; }
const game = document.getElementById('player-options').getAttribute('data-game');
localStorage.removeItem(game);
localStorage.setItem(`${game}-player`, document.getElementById('player-name').value);
if (evt.target.value !== 'default') {
localStorage.setItem(`${game}-preset`, evt.target.value);
}
document.querySelectorAll('#options-form input, #options-form select').forEach((input) => {
if (input.id === 'player-name') { return; }
input.removeAttribute('value');
});
window.location.replace(window.location.href);
};
const applyPresets = (presetName) => {
// Ignore the "default" preset, because it gets set automatically by Jinja
if (presetName === 'default') {
saveSettings();
return;
}
if (!presets[presetName]) {
console.error(`Unknown preset ${presetName} chosen`);
return;
}
const preset = presets[presetName];
Object.keys(preset).forEach((optionName) => {
const optionValue = preset[optionName];
// Handle List and Set options
if (Array.isArray(optionValue)) {
document.querySelectorAll(`input[type=checkbox][name=${optionName}]`).forEach((checkbox) => {
if (optionValue.includes(checkbox.value)) {
checkbox.setAttribute('checked', '1');
} else {
checkbox.removeAttribute('checked');
}
});
return;
}
// Handle Dict options
if (typeof(optionValue) === 'object' && optionValue !== null) {
const itemNames = Object.keys(optionValue);
document.querySelectorAll(`input[type=number][data-option-name=${optionName}]`).forEach((input) => {
const itemName = input.getAttribute('data-item-name');
input.value = (itemNames.includes(itemName)) ? optionValue[itemName] : 0
});
return;
}
// Identify all possible elements
const normalInput = document.getElementById(optionName);
const customInput = document.getElementById(`${optionName}-custom`);
const rangeValue = document.getElementById(`${optionName}-value`);
const randomizeInput = document.getElementById(`random-${optionName}`);
const namedRangeSelect = document.getElementById(`${optionName}-select`);
// It is possible for named ranges to use name of a value rather than the value itself. This is accounted for here
let trueValue = optionValue;
if (namedRangeSelect) {
namedRangeSelect.querySelectorAll('option').forEach((opt) => {
if (opt.innerText.startsWith(optionValue)) {
trueValue = opt.value;
}
});
namedRangeSelect.value = trueValue;
}
// Handle options whose presets are "random"
if (optionValue === 'random') {
normalInput.setAttribute('disabled', '1');
randomizeInput.setAttribute('checked', '1');
if (customInput) {
customInput.setAttribute('disabled', '1');
}
if (rangeValue) {
rangeValue.innerText = normalInput.value;
}
if (namedRangeSelect) {
namedRangeSelect.setAttribute('disabled', '1');
}
return;
}
// Handle normal (text, number, select, etc.) and custom inputs (custom inputs exist with TextChoice only)
normalInput.value = trueValue;
normalInput.removeAttribute('disabled');
randomizeInput.removeAttribute('checked');
if (customInput) {
document.getElementById(`${optionName}-custom`).removeAttribute('disabled');
}
if (rangeValue) {
rangeValue.innerText = trueValue;
}
});
saveSettings();
};
const showUserMessage = (text) => {
const userMessage = document.getElementById('user-message');
userMessage.innerText = text;
userMessage.addEventListener('click', hideUserMessage);
userMessage.style.display = 'block';
};
const hideUserMessage = () => {
const userMessage = document.getElementById('user-message');
userMessage.removeEventListener('click', hideUserMessage);
userMessage.style.display = 'none';
};

View File

@@ -25,16 +25,16 @@ window.addEventListener('load', () => {
// Collapsible advancement sections
const categories = document.getElementsByClassName("location-category");
for (let i = 0; i < categories.length; i++) {
let hide_id = categories[i].id.split('-')[0];
if (hide_id == 'Total') {
for (let category of categories) {
let hide_id = category.id.split('_')[0];
if (hide_id === 'Total') {
continue;
}
categories[i].addEventListener('click', function() {
category.addEventListener('click', function() {
// Toggle the advancement list
document.getElementById(hide_id).classList.toggle("hide");
// Change text of the header
const tab_header = document.getElementById(hide_id+'-header').children[0];
const tab_header = document.getElementById(hide_id+'_header').children[0];
const orig_text = tab_header.innerHTML;
let new_text;
if (orig_text.includes("▼")) {

View File

@@ -0,0 +1,44 @@
window.addEventListener('load', () => {
// Add toggle listener to all elements with .collapse-toggle
const toggleButtons = document.querySelectorAll('details');
// Handle game filter input
const gameSearch = document.getElementById('game-search');
gameSearch.value = '';
gameSearch.addEventListener('input', (evt) => {
if (!evt.target.value.trim()) {
// If input is empty, display all games as collapsed
return toggleButtons.forEach((header) => {
header.style.display = null;
header.removeAttribute('open');
});
}
// Loop over all the games
toggleButtons.forEach((header) => {
// If the game name includes the search string, display the game. If not, hide it
if (header.getAttribute('data-game').toLowerCase().includes(evt.target.value.toLowerCase())) {
header.style.display = null;
header.setAttribute('open', '1');
} else {
header.style.display = 'none';
header.removeAttribute('open');
}
});
});
document.getElementById('expand-all').addEventListener('click', expandAll);
document.getElementById('collapse-all').addEventListener('click', collapseAll);
});
const expandAll = () => {
document.querySelectorAll('details').forEach((detail) => {
detail.setAttribute('open', '1');
});
};
const collapseAll = () => {
document.querySelectorAll('details').forEach((detail) => {
detail.removeAttribute('open');
});
};

View File

@@ -4,47 +4,72 @@ const adjustTableHeight = () => {
return;
const upperDistance = tablesContainer.getBoundingClientRect().top;
const containerHeight = window.innerHeight - upperDistance;
tablesContainer.style.maxHeight = `calc(${containerHeight}px - 1rem)`;
const tableWrappers = document.getElementsByClassName('table-wrapper');
for(let i=0; i < tableWrappers.length; i++){
const maxHeight = (window.innerHeight - upperDistance) / 2;
tableWrappers[i].style.maxHeight = `calc(${maxHeight}px - 1rem)`;
for (let i = 0; i < tableWrappers.length; i++) {
// Ensure we are starting from maximum size prior to calculation.
tableWrappers[i].style.height = null;
tableWrappers[i].style.maxHeight = null;
// Set as a reasonable height, but still allows the user to resize element if they desire.
const currentHeight = tableWrappers[i].offsetHeight;
const maxHeight = (window.innerHeight - upperDistance) / Math.min(tableWrappers.length, 4);
if (currentHeight > maxHeight) {
tableWrappers[i].style.height = `calc(${maxHeight}px - 1rem)`;
}
tableWrappers[i].style.maxHeight = `${currentHeight}px`;
}
};
/**
* Convert an integer number of seconds into a human readable HH:MM format
* @param {Number} seconds
* @returns {string}
*/
const secondsToHours = (seconds) => {
let hours = Math.floor(seconds / 3600);
let minutes = Math.floor((seconds - (hours * 3600)) / 60).toString().padStart(2, '0');
return `${hours}:${minutes}`;
};
window.addEventListener('load', () => {
const tables = $(".table").DataTable({
paging: false,
info: false,
dom: "t",
stateSave: true,
stateSaveCallback: function(settings, data) {
stateSaveCallback: function (settings, data) {
delete data.search;
localStorage.setItem(`DataTables_${settings.sInstance}_/tracker`, JSON.stringify(data));
},
stateLoadCallback: function(settings) {
stateLoadCallback: function (settings) {
return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`));
},
footerCallback: function (tfoot, data, start, end, display) {
if (tfoot) {
const activityData = this.api().column('lastActivity:name').data().toArray().filter(x => !isNaN(x));
Array.from(tfoot?.children).find(td => td.classList.contains('last-activity')).innerText =
(activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None';
}
},
columnDefs: [
{
targets: 'last-activity',
name: 'lastActivity'
},
{
targets: 'hours',
render: function (data, type, row) {
if (type === "sort" || type === 'type') {
if (data === "None")
return -1;
return Number.MAX_VALUE;
return parseInt(data);
}
if (data === "None")
return data;
let hours = Math.floor(data / 3600);
let minutes = Math.floor((data - (hours * 3600)) / 60);
if (minutes < 10) {minutes = "0"+minutes;}
return hours+':'+minutes;
return secondsToHours(data);
}
},
{
@@ -98,44 +123,64 @@ window.addEventListener('load', () => {
event.preventDefault();
}
});
const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3;
const target_second = parseInt(document.getElementById('tracker-wrapper').getAttribute('data-second')) + 3;
console.log("Target second of refresh: " + target_second);
function getSleepTimeSeconds(){
function getSleepTimeSeconds() {
// -40 % 60 is -40, which is absolutely wrong and should burn
var sleepSeconds = (((target_second - new Date().getSeconds()) % 60) + 60) % 60;
return sleepSeconds || 60;
}
let update_on_view = false;
const update = () => {
const target = $("<div></div>");
console.log("Updating Tracker...");
target.load(location.href, function (response, status) {
if (status === "success") {
target.find(".table").each(function (i, new_table) {
const new_trs = $(new_table).find("tbody>tr");
const old_table = tables.eq(i);
const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
old_table.clear();
old_table.rows.add(new_trs).draw();
$(old_table.settings()[0].nScrollBody).scrollTop(topscroll);
$(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
});
$("#multi-stream-link").replaceWith(target.find("#multi-stream-link"));
} else {
console.log("Failed to connect to Server, in order to update Table Data.");
console.log(response);
}
})
setTimeout(update, getSleepTimeSeconds()*1000);
if (document.hidden) {
console.log("Document reporting as not visible, not updating Tracker...");
update_on_view = true;
} else {
update_on_view = false;
const target = $("<div></div>");
console.log("Updating Tracker...");
target.load(location.href, function (response, status) {
if (status === "success") {
target.find(".table").each(function (i, new_table) {
const new_trs = $(new_table).find("tbody>tr");
const footer_tr = $(new_table).find("tfoot>tr");
const old_table = tables.eq(i);
const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
old_table.clear();
if (footer_tr.length) {
$(old_table.table).find("tfoot").html(footer_tr);
}
old_table.rows.add(new_trs);
old_table.draw();
$(old_table.settings()[0].nScrollBody).scrollTop(topscroll);
$(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
});
$("#multi-stream-link").replaceWith(target.find("#multi-stream-link"));
} else {
console.log("Failed to connect to Server, in order to update Table Data.");
console.log(response);
}
})
}
updater = setTimeout(update, getSleepTimeSeconds() * 1000);
}
setTimeout(update, getSleepTimeSeconds()*1000);
let updater = setTimeout(update, getSleepTimeSeconds() * 1000);
window.addEventListener('resize', () => {
adjustTableHeight();
tables.draw();
});
window.addEventListener('visibilitychange', () => {
if (!document.hidden && update_on_view) {
console.log("Page became visible, tracker should be refreshed.");
clearTimeout(updater);
update();
}
});
adjustTableHeight();
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,223 @@
let deletedOptions = {};
window.addEventListener('load', () => {
const worldName = document.querySelector('#weighted-options').getAttribute('data-game');
// Generic change listener. Detecting unique qualities and acting on them here reduces initial JS initialisation time
// and handles dynamically created elements
document.addEventListener('change', (evt) => {
// Handle updates to range inputs
if (evt.target.type === 'range') {
// Update span containing range value. All ranges have a corresponding `{rangeId}-value` span
document.getElementById(`${evt.target.id}-value`).innerText = evt.target.value;
// If the changed option was the name of a game, determine whether to show or hide that game's div
if (evt.target.id.startsWith('game||')) {
const gameName = evt.target.id.split('||')[1];
const gameDiv = document.getElementById(`${gameName}-container`);
if (evt.target.value > 0) {
gameDiv.classList.remove('hidden');
} else {
gameDiv.classList.add('hidden');
}
}
}
});
// Generic click listener
document.addEventListener('click', (evt) => {
// Handle creating new rows for Range options
if (evt.target.classList.contains('add-range-option-button')) {
const optionName = evt.target.getAttribute('data-option');
addRangeRow(optionName);
}
// Handle deleting range rows
if (evt.target.classList.contains('range-option-delete')) {
const targetRow = document.querySelector(`tr[data-row="${evt.target.getAttribute('data-target')}"]`);
setDeletedOption(
targetRow.getAttribute('data-option-name'),
targetRow.getAttribute('data-value'),
);
targetRow.parentElement.removeChild(targetRow);
}
});
// Listen for enter presses on inputs intended to add range rows
document.addEventListener('keydown', (evt) => {
if (evt.key === 'Enter') {
evt.preventDefault();
}
if (evt.key === 'Enter' && evt.target.classList.contains('range-option-value')) {
const optionName = evt.target.getAttribute('data-option');
addRangeRow(optionName);
}
});
// Detect form submission
document.getElementById('weighted-options-form').addEventListener('submit', (evt) => {
// Save data to localStorage
const weightedOptions = {};
document.querySelectorAll('input[name]').forEach((input) => {
const keys = input.getAttribute('name').split('||');
// Determine keys
const optionName = keys[0] ?? null;
const subOption = keys[1] ?? null;
// Ensure keys exist
if (!weightedOptions[optionName]) { weightedOptions[optionName] = {}; }
if (subOption && !weightedOptions[optionName][subOption]) {
weightedOptions[optionName][subOption] = null;
}
if (subOption) { return weightedOptions[optionName][subOption] = determineValue(input); }
if (optionName) { return weightedOptions[optionName] = determineValue(input); }
});
localStorage.setItem(`${worldName}-weights`, JSON.stringify(weightedOptions));
localStorage.setItem(`${worldName}-deletedOptions`, JSON.stringify(deletedOptions));
});
// Remove all deleted values as specified by localStorage
deletedOptions = JSON.parse(localStorage.getItem(`${worldName}-deletedOptions`) || '{}');
Object.keys(deletedOptions).forEach((optionName) => {
deletedOptions[optionName].forEach((value) => {
const targetRow = document.querySelector(`tr[data-row="${value}-row"]`);
targetRow.parentElement.removeChild(targetRow);
});
});
// Populate all settings from localStorage on page initialisation
const previousSettingsJson = localStorage.getItem(`${worldName}-weights`);
if (previousSettingsJson) {
const previousSettings = JSON.parse(previousSettingsJson);
Object.keys(previousSettings).forEach((option) => {
if (typeof previousSettings[option] === 'string') {
return document.querySelector(`input[name="${option}"]`).value = previousSettings[option];
}
Object.keys(previousSettings[option]).forEach((value) => {
const input = document.querySelector(`input[name="${option}||${value}"]`);
if (!input?.type) {
return console.error(`Unable to populate option with name ${option}||${value}.`);
}
switch (input.type) {
case 'checkbox':
input.checked = (parseInt(previousSettings[option][value], 10) === 1);
break;
case 'range':
input.value = parseInt(previousSettings[option][value], 10);
break;
case 'number':
input.value = previousSettings[option][value].toString();
break;
default:
console.error(`Found unsupported input type: ${input.type}`);
}
});
});
}
});
const addRangeRow = (optionName) => {
const inputQuery = `input[type=number][data-option="${optionName}"].range-option-value`;
const inputTarget = document.querySelector(inputQuery);
const newValue = inputTarget.value;
if (!/^-?\d+$/.test(newValue)) {
alert('Range values must be a positive or negative integer!');
return;
}
inputTarget.value = '';
const tBody = document.querySelector(`table[data-option="${optionName}"].range-rows tbody`);
const tr = document.createElement('tr');
tr.setAttribute('data-row', `${optionName}-${newValue}-row`);
tr.setAttribute('data-option-name', optionName);
tr.setAttribute('data-value', newValue);
const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left');
const label = document.createElement('label');
label.setAttribute('for', `${optionName}||${newValue}`);
label.innerText = newValue.toString();
tdLeft.appendChild(label);
tr.appendChild(tdLeft);
const tdMiddle = document.createElement('td');
tdMiddle.classList.add('td-middle');
const range = document.createElement('input');
range.setAttribute('type', 'range');
range.setAttribute('min', '0');
range.setAttribute('max', '50');
range.setAttribute('value', '0');
range.setAttribute('id', `${optionName}||${newValue}`);
range.setAttribute('name', `${optionName}||${newValue}`);
tdMiddle.appendChild(range);
tr.appendChild(tdMiddle);
const tdRight = document.createElement('td');
tdRight.classList.add('td-right');
const valueSpan = document.createElement('span');
valueSpan.setAttribute('id', `${optionName}||${newValue}-value`);
valueSpan.innerText = '0';
tdRight.appendChild(valueSpan);
tr.appendChild(tdRight);
const tdDelete = document.createElement('td');
const deleteSpan = document.createElement('span');
deleteSpan.classList.add('range-option-delete');
deleteSpan.classList.add('js-required');
deleteSpan.setAttribute('data-target', `${optionName}-${newValue}-row`);
deleteSpan.innerText = '❌';
tdDelete.appendChild(deleteSpan);
tr.appendChild(tdDelete);
tBody.appendChild(tr);
// Remove this option from the set of deleted options if it exists
unsetDeletedOption(optionName, newValue);
};
/**
* Determines the value of an input element, or returns a 1 or 0 if the element is a checkbox
*
* @param {object} input - The input element.
* @returns {number} The value of the input element.
*/
const determineValue = (input) => {
switch (input.type) {
case 'checkbox':
return (input.checked ? 1 : 0);
case 'range':
return parseInt(input.value, 10);
default:
return input.value;
}
};
/**
* Sets the deleted option value for a given world and option name.
* If the world or option does not exist, it creates the necessary entries.
*
* @param {string} optionName - The name of the option.
* @param {*} value - The value to be set for the deleted option.
* @returns {void}
*/
const setDeletedOption = (optionName, value) => {
deletedOptions[optionName] = deletedOptions[optionName] || [];
deletedOptions[optionName].push(`${optionName}-${value}`);
};
/**
* Removes a specific value from the deletedOptions object.
*
* @param {string} optionName - The name of the option.
* @param {*} value - The value to be removed
* @returns {void}
*/
const unsetDeletedOption = (optionName, value) => {
if (!deletedOptions.hasOwnProperty(optionName)) { return; }
if (deletedOptions[optionName].includes(`${optionName}-${value}`)) {
deletedOptions[optionName].splice(deletedOptions[optionName].indexOf(`${optionName}-${value}`), 1);
}
if (deletedOptions[optionName].length === 0) {
delete deletedOptions[optionName];
}
};

View File

@@ -0,0 +1,20 @@
User-agent: Googlebot
Disallow: /
User-agent: APIs-Google
Disallow: /
User-agent: AdsBot-Google-Mobile
Disallow: /
User-agent: AdsBot-Google-Mobile
Disallow: /
User-agent: Mediapartners-Google
Disallow: /
User-agent: Google-Safety
Disallow: /
User-agent: *
Disallow: /

View File

@@ -44,7 +44,7 @@ a{
font-family: LexendDeca-Regular, sans-serif;
}
button{
button, input[type=submit]{
font-weight: 500;
font-size: 0.9rem;
padding: 10px 17px 11px 16px; /* top right bottom left */
@@ -57,7 +57,7 @@ button{
cursor: pointer;
}
button:active{
button:active, input[type=submit]:active{
border-right: 1px solid rgba(0, 0, 0, 0.5);
border-bottom: 1px solid rgba(0, 0, 0, 0.5);
padding-right: 16px;
@@ -66,11 +66,11 @@ button:active{
margin-bottom: 2px;
}
button.button-grass{
button.button-grass, input[type=submit].button-grass{
border: 1px solid black;
}
button.button-dirt{
button.button-dirt, input[type=submit].button-dirt{
border: 1px solid black;
}
@@ -111,4 +111,4 @@ h5, h6{
.interactive{
color: #ffef00;
}
}

View File

@@ -235,9 +235,6 @@ html{
line-height: 30px;
}
#landing .variable{
color: #ffff00;
}
.landing-deco{
position: absolute;

View File

@@ -1,75 +0,0 @@
#player-tracker-wrapper{
margin: 0;
font-family: LexendDeca-Light, sans-serif;
color: white;
font-size: 14px;
}
#inventory-table{
border-top: 2px solid #000000;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 3px 3px 10px;
width: 284px;
background-color: #42b149;
}
#inventory-table td{
width: 40px;
height: 40px;
text-align: center;
vertical-align: middle;
}
#inventory-table img{
height: 100%;
max-width: 40px;
max-height: 40px;
filter: grayscale(100%) contrast(75%) brightness(75%);
}
#inventory-table img.acquired{
filter: none;
}
#inventory-table img.powder-fix{
width: 35px;
height: 35px;
}
#location-table{
width: 284px;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-bottom: 2px solid #000000;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
background-color: #42b149;
padding: 0 3px 3px;
}
#location-table th{
vertical-align: middle;
text-align: center;
padding-right: 10px;
}
#location-table td{
padding-top: 2px;
padding-bottom: 2px;
padding-right: 5px;
line-height: 20px;
}
#location-table td.counter{
padding-right: 8px;
text-align: right;
}
#location-table img{
height: 100%;
max-width: 30px;
max-height: 30px;
}

View File

@@ -23,7 +23,7 @@
.markdown a{}
.markdown h1{
.markdown h1, .markdown details summary.h1{
font-size: 52px;
font-weight: normal;
font-family: LondrinaSolid-Regular, sans-serif;
@@ -33,7 +33,7 @@
text-shadow: 1px 1px 4px #000000;
}
.markdown h2{
.markdown h2, .markdown details summary.h2{
font-size: 38px;
font-weight: normal;
font-family: LondrinaSolid-Light, sans-serif;
@@ -45,7 +45,7 @@
text-shadow: 1px 1px 2px #000000;
}
.markdown h3{
.markdown h3, .markdown details summary.h3{
font-size: 26px;
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
@@ -55,7 +55,7 @@
margin-bottom: 0.5rem;
}
.markdown h4{
.markdown h4, .markdown details summary.h4{
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
font-size: 24px;
@@ -63,21 +63,21 @@
margin-bottom: 24px;
}
.markdown h5{
.markdown h5, .markdown details summary.h5{
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
font-size: 22px;
cursor: pointer;
}
.markdown h6{
.markdown h6, .markdown details summary.h6{
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
font-size: 20px;
cursor: pointer;;
}
.markdown h4, .markdown h5,.markdown h6{
.markdown h4, .markdown h5, .markdown h6{
margin-bottom: 0.5rem;
}

View File

@@ -1,194 +0,0 @@
html{
background-image: url('../static/backgrounds/grass.png');
background-repeat: repeat;
background-size: 650px 650px;
}
#player-settings{
max-width: 1000px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
}
#player-settings #player-settings-button-row{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#player-settings code{
background-color: #d9cd8e;
border-radius: 4px;
padding-left: 0.25rem;
padding-right: 0.25rem;
color: #000000;
}
#player-settings #user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
}
#player-settings #user-message.visible{
display: block;
cursor: pointer;
}
#player-settings h1{
font-size: 2.5rem;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
text-shadow: 1px 1px 4px #000000;
}
#player-settings h2{
font-size: 40px;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
#player-settings input:not([type]){
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
}
#player-settings input:not([type]):focus{
border: 1px solid #ffffff;
}
#player-settings select{
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
background-color: #ffffff;
}
#player-settings #game-options, #player-settings #rom-options{
display: flex;
flex-direction: row;
}
#player-settings .left, #player-settings .right{
flex-grow: 1;
}
#player-settings .left{
margin-right: 10px;
}
#player-settings .right{
margin-left: 10px;
}
#player-settings table{
margin-bottom: 30px;
width: 100%;
}
#player-settings table .select-container{
display: flex;
flex-direction: row;
}
#player-settings table .select-container select{
min-width: 200px;
flex-grow: 1;
}
#player-settings table select:disabled{
background-color: lightgray;
}
#player-settings table .range-container{
display: flex;
flex-direction: row;
}
#player-settings table .range-container input[type=range]{
flex-grow: 1;
}
#player-settings table .range-value{
min-width: 20px;
margin-left: 0.25rem;
}
#player-settings table .special-range-container{
display: flex;
flex-direction: column;
}
#player-settings table .special-range-wrapper{
display: flex;
flex-direction: row;
margin-top: 0.25rem;
}
#player-settings table .special-range-wrapper input[type=range]{
flex-grow: 1;
}
#player-settings table .randomize-button {
max-height: 24px;
line-height: 16px;
padding: 2px 8px;
margin: 0 0 0 0.25rem;
font-size: 12px;
border: 1px solid black;
border-radius: 3px;
}
#player-settings table .randomize-button.active {
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
}
#player-settings table label{
display: block;
min-width: 200px;
margin-right: 4px;
cursor: default;
}
#player-settings th, #player-settings td{
border: none;
padding: 3px;
font-size: 17px;
vertical-align: top;
}
@media all and (max-width: 1000px), all and (orientation: portrait){
#player-settings #game-options{
justify-content: flex-start;
flex-wrap: wrap;
}
#player-settings .left, #player-settings .right{
flex-grow: unset;
}
#game-options table label{
display: block;
min-width: 200px;
}
}

View File

@@ -0,0 +1,310 @@
@import "../markdown.css";
html {
background-image: url("../../static/backgrounds/grass.png");
background-repeat: repeat;
background-size: 650px 650px;
overflow-x: hidden;
}
#player-options {
box-sizing: border-box;
max-width: 1024px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
word-break: break-word;
}
#player-options #player-options-header h1 {
margin-bottom: 0;
padding-bottom: 0;
}
#player-options #player-options-header h1:nth-child(2) {
font-size: 1.4rem;
margin-top: -8px;
margin-bottom: 0.5rem;
}
#player-options .js-warning-banner {
width: calc(100% - 1rem);
padding: 0.5rem;
border-radius: 4px;
background-color: #f3f309;
color: #000000;
margin-bottom: 0.5rem;
text-align: center;
}
#player-options .group-container {
padding: 0;
margin: 0;
}
#player-options .group-container h2 {
user-select: none;
cursor: unset;
}
#player-options .group-container h2 label {
cursor: pointer;
}
#player-options #player-options-button-row {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#player-options #user-message {
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
cursor: pointer;
}
#player-options h1 {
font-size: 2.5rem;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
text-shadow: 1px 1px 4px #000000;
}
#player-options h2 {
font-size: 40px;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
#player-options h3, #player-options h4, #player-options h5, #player-options h6 {
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
#player-options input:not([type]) {
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
}
#player-options input:not([type]):focus {
border: 1px solid #ffffff;
}
#player-options select {
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
background-color: #ffffff;
text-overflow: ellipsis;
}
#player-options .game-options {
display: flex;
flex-direction: row;
}
#player-options .game-options .left, #player-options .game-options .right {
display: grid;
grid-template-columns: 12rem auto;
grid-row-gap: 0.5rem;
grid-auto-rows: min-content;
align-items: start;
min-width: 480px;
width: 50%;
}
#player-options #meta-options {
display: flex;
justify-content: space-between;
gap: 20px;
padding: 3px;
}
#player-options #meta-options input, #player-options #meta-options select {
box-sizing: border-box;
width: 200px;
}
#player-options .left, #player-options .right {
flex-grow: 1;
margin-bottom: 0.5rem;
}
#player-options .left {
margin-right: 20px;
}
#player-options .select-container {
display: flex;
flex-direction: row;
max-width: 270px;
}
#player-options .select-container select {
min-width: 200px;
flex-grow: 1;
}
#player-options .select-container select:disabled {
background-color: lightgray;
}
#player-options .range-container {
display: flex;
flex-direction: row;
max-width: 270px;
}
#player-options .range-container input[type=range] {
flex-grow: 1;
}
#player-options .range-container .range-value {
min-width: 20px;
margin-left: 0.25rem;
}
#player-options .named-range-container {
display: flex;
flex-direction: column;
max-width: 270px;
}
#player-options .named-range-container .named-range-wrapper {
display: flex;
flex-direction: row;
margin-top: 0.25rem;
}
#player-options .named-range-container .named-range-wrapper input[type=range] {
flex-grow: 1;
}
#player-options .free-text-container {
display: flex;
flex-direction: column;
max-width: 270px;
}
#player-options .free-text-container input[type=text] {
flex-grow: 1;
}
#player-options .text-choice-container {
display: flex;
flex-direction: column;
max-width: 270px;
}
#player-options .text-choice-container .text-choice-wrapper {
display: flex;
flex-direction: row;
margin-bottom: 0.25rem;
}
#player-options .text-choice-container .text-choice-wrapper select {
flex-grow: 1;
}
#player-options .option-container {
display: flex;
flex-direction: column;
background-color: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(20, 20, 20, 0.25);
border-radius: 3px;
color: #ffffff;
max-height: 10rem;
min-width: 14.5rem;
overflow-y: auto;
padding-right: 0.25rem;
padding-left: 0.25rem;
}
#player-options .option-container .option-divider {
width: 100%;
height: 2px;
background-color: rgba(20, 20, 20, 0.25);
margin-top: 0.125rem;
margin-bottom: 0.125rem;
}
#player-options .option-container .option-entry {
display: flex;
flex-direction: row;
align-items: flex-start;
margin-bottom: 0.125rem;
margin-top: 0.125rem;
user-select: none;
}
#player-options .option-container .option-entry:hover {
background-color: rgba(20, 20, 20, 0.25);
}
#player-options .option-container .option-entry input[type=checkbox] {
margin-right: 0.25rem;
}
#player-options .option-container .option-entry input[type=number] {
max-width: 1.5rem;
max-height: 1rem;
margin-left: 0.125rem;
text-align: center;
/* Hide arrows on input[type=number] fields */
-moz-appearance: textfield;
}
#player-options .option-container .option-entry input[type=number]::-webkit-outer-spin-button, #player-options .option-container .option-entry input[type=number]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
#player-options .option-container .option-entry label {
flex-grow: 1;
margin-right: 0;
min-width: unset;
display: unset;
}
#player-options .randomize-button {
display: flex;
flex-direction: column;
justify-content: center;
height: 22px;
max-width: 30px;
margin: 0 0 0 0.25rem;
font-size: 14px;
border: 1px solid black;
border-radius: 3px;
background-color: #d3d3d3;
user-select: none;
}
#player-options .randomize-button:hover {
background-color: #c0c0c0;
cursor: pointer;
}
#player-options .randomize-button label {
line-height: 22px;
padding-left: 5px;
padding-right: 2px;
margin-right: 4px;
width: 100%;
height: 100%;
min-width: unset;
}
#player-options .randomize-button label:hover {
cursor: pointer;
}
#player-options .randomize-button input[type=checkbox] {
display: none;
}
#player-options .randomize-button:has(input[type=checkbox]:checked) {
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
}
#player-options .randomize-button:has(input[type=checkbox]:checked):hover {
background-color: #eedd27;
}
#player-options .randomize-button[data-tooltip]::after {
left: unset;
right: 0;
}
#player-options label {
display: block;
margin-right: 4px;
cursor: default;
word-break: break-word;
}
#player-options th, #player-options td {
border: none;
padding: 3px;
font-size: 17px;
vertical-align: top;
}
@media all and (max-width: 1024px) {
#player-options {
border-radius: 0;
}
#player-options #meta-options {
flex-direction: column;
justify-content: flex-start;
gap: 6px;
}
#player-options .game-options {
justify-content: flex-start;
flex-wrap: wrap;
}
}
/*# sourceMappingURL=playerOptions.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["playerOptions.scss"],"names":[],"mappings":"AAAQ;AAER;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAEA;EACI;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;AAEA;EACA;;AACA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACI;;AAIR;EACI;;AAGJ;EACI;;AAEA;EACI;;AAIR;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;;AAIR;EACI;IACI;;EAEA;IACI;IACA;IACA;;EAGJ;IACI;IACA","file":"playerOptions.css"}

View File

@@ -0,0 +1,364 @@
@import "../markdown.css";
html{
background-image: url('../../static/backgrounds/grass.png');
background-repeat: repeat;
background-size: 650px 650px;
overflow-x: hidden;
}
#player-options{
box-sizing: border-box;
max-width: 1024px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
word-break: break-word;
#player-options-header{
h1{
margin-bottom: 0;
padding-bottom: 0;
}
h1:nth-child(2){
font-size: 1.4rem;
margin-top: -8px;
margin-bottom: 0.5rem;
}
}
.js-warning-banner{
width: calc(100% - 1rem);
padding: 0.5rem;
border-radius: 4px;
background-color: #f3f309;
color: #000000;
margin-bottom: 0.5rem;
text-align: center;
}
.group-container{
padding: 0;
margin: 0;
h2{
user-select: none;
cursor: unset;
label{
cursor: pointer;
}
}
}
#player-options-button-row{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
cursor: pointer;
}
h1{
font-size: 2.5rem;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
text-shadow: 1px 1px 4px #000000;
}
h2{
font-size: 40px;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
h3, h4, h5, h6{
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
input:not([type]){
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
&:focus{
border: 1px solid #ffffff;
}
}
select{
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
background-color: #ffffff;
text-overflow: ellipsis;
}
.game-options{
display: flex;
flex-direction: row;
.left, .right{
display: grid;
grid-template-columns: 12rem auto;
grid-row-gap: 0.5rem;
grid-auto-rows: min-content;
align-items: start;
min-width: 480px;
width: 50%;
}
}
#meta-options{
display: flex;
justify-content: space-between;
gap: 20px;
padding: 3px;
input, select{
box-sizing: border-box;
width: 200px;
}
}
.left, .right{
flex-grow: 1;
margin-bottom: 0.5rem;
}
.left{
margin-right: 20px;
}
.select-container{
display: flex;
flex-direction: row;
max-width: 270px;
select{
min-width: 200px;
flex-grow: 1;
&:disabled{
background-color: lightgray;
}
}
}
.range-container{
display: flex;
flex-direction: row;
max-width: 270px;
input[type=range]{
flex-grow: 1;
}
.range-value{
min-width: 20px;
margin-left: 0.25rem;
}
}
.named-range-container{
display: flex;
flex-direction: column;
max-width: 270px;
.named-range-wrapper{
display: flex;
flex-direction: row;
margin-top: 0.25rem;
input[type=range]{
flex-grow: 1;
}
}
}
.free-text-container{
display: flex;
flex-direction: column;
max-width: 270px;
input[type=text]{
flex-grow: 1;
}
}
.text-choice-container{
display: flex;
flex-direction: column;
max-width: 270px;
.text-choice-wrapper{
display: flex;
flex-direction: row;
margin-bottom: 0.25rem;
select{
flex-grow: 1;
}
}
}
.option-container{
display: flex;
flex-direction: column;
background-color: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(20, 20, 20, 0.25);
border-radius: 3px;
color: #ffffff;
max-height: 10rem;
min-width: 14.5rem;
overflow-y: auto;
padding-right: 0.25rem;
padding-left: 0.25rem;
.option-divider{
width: 100%;
height: 2px;
background-color: rgba(20, 20, 20, 0.25);
margin-top: 0.125rem;
margin-bottom: 0.125rem;
}
.option-entry{
display: flex;
flex-direction: row;
align-items: flex-start;
margin-bottom: 0.125rem;
margin-top: 0.125rem;
user-select: none;
&:hover{
background-color: rgba(20, 20, 20, 0.25);
}
input[type=checkbox]{
margin-right: 0.25rem;
}
input[type=number]{
max-width: 1.5rem;
max-height: 1rem;
margin-left: 0.125rem;
text-align: center;
/* Hide arrows on input[type=number] fields */
-moz-appearance: textfield;
&::-webkit-outer-spin-button, &::-webkit-inner-spin-button{
-webkit-appearance: none;
margin: 0;
}
}
label{
flex-grow: 1;
margin-right: 0;
min-width: unset;
display: unset;
}
}
}
.randomize-button{
display: flex;
flex-direction: column;
justify-content: center;
height: 22px;
max-width: 30px;
margin: 0 0 0 0.25rem;
font-size: 14px;
border: 1px solid black;
border-radius: 3px;
background-color: #d3d3d3;
user-select: none;
&:hover{
background-color: #c0c0c0;
cursor: pointer;
}
label{
line-height: 22px;
padding-left: 5px;
padding-right: 2px;
margin-right: 4px;
width: 100%;
height: 100%;
min-width: unset;
&:hover{
cursor: pointer;
}
}
input[type=checkbox]{
display: none;
}
&:has(input[type=checkbox]:checked){
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
&:hover{
background-color: #eedd27;
}
}
&[data-tooltip]::after{
left: unset;
right: 0;
}
}
label{
display: block;
margin-right: 4px;
cursor: default;
word-break: break-word;
}
th, td{
border: none;
padding: 3px;
font-size: 17px;
vertical-align: top;
}
}
@media all and (max-width: 1024px) {
#player-options {
border-radius: 0;
#meta-options {
flex-direction: column;
justify-content: flex-start;
gap: 6px;
}
.game-options{
justify-content: flex-start;
flex-wrap: wrap;
}
}
}

View File

@@ -0,0 +1,160 @@
#player-tracker-wrapper{
margin: 0;
}
#tracker-table td {
vertical-align: top;
}
.inventory-table-area{
border: 2px solid #000000;
border-radius: 4px;
padding: 3px 10px 3px 10px;
}
.inventory-table-area:has(.inventory-table-terran) {
width: 690px;
background-color: #525494;
}
.inventory-table-area:has(.inventory-table-zerg) {
width: 360px;
background-color: #9d60d2;
}
.inventory-table-area:has(.inventory-table-protoss) {
width: 400px;
background-color: #d2b260;
}
#tracker-table .inventory-table td{
width: 40px;
height: 40px;
text-align: center;
vertical-align: middle;
}
.inventory-table td.title{
padding-top: 10px;
height: 20px;
font-family: "JuraBook", monospace;
font-size: 16px;
font-weight: bold;
}
.inventory-table img{
height: 100%;
max-width: 40px;
max-height: 40px;
border: 1px solid #000000;
filter: grayscale(100%) contrast(75%) brightness(20%);
background-color: black;
}
.inventory-table img.acquired{
filter: none;
background-color: black;
}
.inventory-table .tint-terran img.acquired {
filter: sepia(100%) saturate(300%) brightness(130%) hue-rotate(120deg)
}
.inventory-table .tint-protoss img.acquired {
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(180deg)
}
.inventory-table .tint-level-1 img.acquired {
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg)
}
.inventory-table .tint-level-2 img.acquired {
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg) hue-rotate(120deg)
}
.inventory-table .tint-level-3 img.acquired {
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg) hue-rotate(240deg)
}
.inventory-table div.counted-item {
position: relative;
}
.inventory-table div.item-count {
width: 160px;
text-align: left;
color: black;
font-family: "JuraBook", monospace;
font-weight: bold;
}
#location-table{
border: 2px solid #000000;
border-radius: 4px;
background-color: #87b678;
padding: 10px 3px 3px;
font-family: "JuraBook", monospace;
font-size: 16px;
font-weight: bold;
cursor: default;
}
#location-table table{
width: 100%;
}
#location-table th{
vertical-align: middle;
text-align: left;
padding-right: 10px;
}
#location-table td{
padding-top: 2px;
padding-bottom: 2px;
line-height: 20px;
}
#location-table td.counter {
text-align: right;
font-size: 14px;
}
#location-table td.toggle-arrow {
text-align: right;
}
#location-table tr#Total-header {
font-weight: bold;
}
#location-table img{
height: 100%;
max-width: 30px;
max-height: 30px;
}
#location-table tbody.locations {
font-size: 16px;
}
#location-table td.location-name {
padding-left: 16px;
}
#location-table td:has(.location-column) {
vertical-align: top;
}
#location-table .location-column {
width: 100%;
height: 100%;
}
#location-table .location-column .spacer {
min-height: 24px;
}
.hide {
display: none;
}

View File

@@ -1,110 +0,0 @@
#player-tracker-wrapper{
margin: 0;
}
#inventory-table{
border-top: 2px solid #000000;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 3px 3px 10px;
width: 500px;
background-color: #525494;
}
#inventory-table td{
width: 40px;
height: 40px;
text-align: center;
vertical-align: middle;
}
#inventory-table td.title{
padding-top: 10px;
height: 20px;
font-family: "JuraBook", monospace;
font-size: 16px;
font-weight: bold;
}
#inventory-table img{
height: 100%;
max-width: 40px;
max-height: 40px;
border: 1px solid #000000;
filter: grayscale(100%) contrast(75%) brightness(20%);
}
#inventory-table img.acquired{
filter: none;
}
#inventory-table div.counted-item {
position: relative;
}
#inventory-table div.item-count {
text-align: left;
color: black;
font-family: "JuraBook", monospace;
font-weight: bold;
}
#location-table{
width: 500px;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-bottom: 2px solid #000000;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
background-color: #525494;
padding: 10px 3px 3px;
font-family: "JuraBook", monospace;
font-size: 16px;
font-weight: bold;
cursor: default;
}
#location-table th{
vertical-align: middle;
text-align: left;
padding-right: 10px;
}
#location-table td{
padding-top: 2px;
padding-bottom: 2px;
line-height: 20px;
}
#location-table td.counter {
text-align: right;
font-size: 14px;
}
#location-table td.toggle-arrow {
text-align: right;
}
#location-table tr#Total-header {
font-weight: bold;
}
#location-table img{
height: 100%;
max-width: 30px;
max-height: 30px;
}
#location-table tbody.locations {
font-size: 16px;
}
#location-table td.location-name {
padding-left: 16px;
}
.hide {
display: none;
}

View File

@@ -8,14 +8,15 @@
cursor: unset;
}
#games h1{
#games h1, #games details summary.h1{
font-size: 60px;
cursor: unset;
}
#games h2{
#games h2, #games details summary.h2{
color: #93dcff;
margin-bottom: 2px;
text-transform: none;
}
#games a{
@@ -31,3 +32,13 @@
line-height: 25px;
margin-bottom: 7px;
}
#games .page-controls{
display: flex;
flex-direction: row;
margin-top: 0.25rem;
}
#games .page-controls button{
margin-left: 0.5rem;
}

View File

@@ -12,12 +12,12 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
*/
/* Base styles for the element that has a tooltip */
[data-tooltip], .tooltip {
[data-tooltip], .tooltip-container {
position: relative;
}
/* Base styles for the entire tooltip */
[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after {
[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip {
position: absolute;
visibility: hidden;
opacity: 0;
@@ -39,13 +39,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
pointer-events: none;
}
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after{
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before,
.tooltip-container:hover .tooltip {
visibility: visible;
opacity: 1;
word-break: break-word;
}
/** Directional arrow styles */
.tooltip:before, [data-tooltip]:before {
[data-tooltip]:before, .tooltip-container:before {
z-index: 10000;
border: 6px solid transparent;
background: transparent;
@@ -53,7 +55,7 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
}
/** Content styles */
.tooltip:after, [data-tooltip]:after {
[data-tooltip]:after, .tooltip {
width: 260px;
z-index: 10000;
padding: 8px;
@@ -62,24 +64,26 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
background-color: hsla(0, 0%, 20%, 0.9);
color: #fff;
content: attr(data-tooltip);
white-space: pre-wrap;
font-size: 14px;
line-height: 1.2;
}
[data-tooltip]:before, [data-tooltip]:after{
[data-tooltip]:after {
white-space: pre-wrap;
}
[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip {
visibility: hidden;
opacity: 0;
pointer-events: none;
}
[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after,
.tooltip-top:before, .tooltip-top:after {
[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip {
bottom: 100%;
left: 50%;
}
[data-tooltip]:before, .tooltip:before, .tooltip-top:before {
[data-tooltip]:before, .tooltip-container:before {
margin-left: -6px;
margin-bottom: -12px;
border-top-color: #000;
@@ -87,19 +91,19 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
}
/** Horizontally align tooltips on the top and bottom */
[data-tooltip]:after, .tooltip:after, .tooltip-top:after {
[data-tooltip]:after, .tooltip {
margin-left: -80px;
}
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after,
.tooltip-top:hover:before, .tooltip-top:hover:after {
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before,
.tooltip-container:hover .tooltip {
-webkit-transform: translateY(-12px);
-moz-transform: translateY(-12px);
transform: translateY(-12px);
}
/** Tooltips on the left */
.tooltip-left:before, .tooltip-left:after {
.tooltip-left:before, [data-tooltip].tooltip-left:after, .tooltip-left .tooltip {
right: 100%;
bottom: 50%;
left: auto;
@@ -114,14 +118,14 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
border-left-color: hsla(0, 0%, 20%, 0.9);
}
.tooltip-left:hover:before, .tooltip-left:hover:after {
.tooltip-left:hover:before, [data-tooltip].tooltip-left:hover:after, .tooltip-left:hover .tooltip {
-webkit-transform: translateX(-12px);
-moz-transform: translateX(-12px);
transform: translateX(-12px);
}
/** Tooltips on the bottom */
.tooltip-bottom:before, .tooltip-bottom:after {
.tooltip-bottom:before, [data-tooltip].tooltip-bottom:after, .tooltip-bottom .tooltip {
top: 100%;
bottom: auto;
left: 50%;
@@ -135,14 +139,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
border-bottom-color: hsla(0, 0%, 20%, 0.9);
}
.tooltip-bottom:hover:before, .tooltip-bottom:hover:after {
.tooltip-bottom:hover:before, [data-tooltip].tooltip-bottom:hover:after,
.tooltip-bottom:hover .tooltip {
-webkit-transform: translateY(12px);
-moz-transform: translateY(12px);
transform: translateY(12px);
}
/** Tooltips on the right */
.tooltip-right:before, .tooltip-right:after {
.tooltip-right:before, [data-tooltip].tooltip-right:after, .tooltip-right .tooltip {
bottom: 50%;
left: 100%;
}
@@ -155,7 +160,8 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
border-right-color: hsla(0, 0%, 20%, 0.9);
}
.tooltip-right:hover:before, .tooltip-right:hover:after {
.tooltip-right:hover:before, [data-tooltip].tooltip-right:hover:after,
.tooltip-right:hover .tooltip {
-webkit-transform: translateX(12px);
-moz-transform: translateX(12px);
transform: translateX(12px);
@@ -167,7 +173,16 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
}
/** Center content vertically for tooltips ont he left and right */
.tooltip-left:after, .tooltip-right:after {
[data-tooltip].tooltip-left:after, [data-tooltip].tooltip-right:after,
.tooltip-left .tooltip, .tooltip-right .tooltip {
margin-left: 0;
margin-bottom: -16px;
}
.tooltip ul, .tooltip ol {
padding-left: 1rem;
}
.tooltip :last-child {
margin-bottom: 0;
}

View File

@@ -7,134 +7,55 @@
width: calc(100% - 1rem);
}
#tracker-wrapper a{
#tracker-wrapper a {
color: #234ae4;
text-decoration: none;
cursor: pointer;
}
.table-wrapper{
overflow-y: auto;
overflow-x: auto;
margin-bottom: 1rem;
}
#tracker-header-bar{
#tracker-header-bar {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-content: center;
line-height: 20px;
gap: 0.5rem;
margin-bottom: 1rem;
}
#tracker-header-bar .info{
#tracker-header-bar .info {
color: #ffffff;
}
#search{
border: 1px solid #000000;
border-radius: 3px;
padding: 3px;
width: 200px;
margin-bottom: 0.5rem;
margin-right: 1rem;
}
#multi-stream-link{
margin-right: 1rem;
}
div.dataTables_wrapper.no-footer .dataTables_scrollBody{
border: none;
}
table.dataTable{
color: #000000;
}
table.dataTable thead{
font-family: LexendDeca-Regular, sans-serif;
}
table.dataTable tbody{
background-color: #dce2bd;
font-family: LexendDeca-Light, sans-serif;
}
table.dataTable tbody tr:hover{
background-color: #e2eabb;
}
table.dataTable tbody td{
padding: 4px 6px;
}
table.dataTable, table.dataTable.no-footer{
border-left: 1px solid #bba967;
width: calc(100% - 2px) !important;
font-size: 1rem;
}
table.dataTable thead th{
position: -webkit-sticky;
position: sticky;
background-color: #b0a77d;
top: 0;
}
table.dataTable thead th.upper-row{
position: -webkit-sticky;
position: sticky;
background-color: #b0a77d;
height: 36px;
top: 0;
}
table.dataTable thead th.lower-row{
position: -webkit-sticky;
position: sticky;
background-color: #b0a77d;
height: 22px;
top: 46px;
}
table.dataTable tbody td{
border: 1px solid #bba967;
}
div.dataTables_scrollBody{
background-color: inherit !important;
}
table.dataTable .center-column{
text-align: center;
}
img.alttp-sprite {
height: auto;
max-height: 32px;
min-height: 14px;
}
.item-acquired{
background-color: #d3c97d;
padding: 2px;
flex-grow: 1;
align-self: center;
text-align: justify;
}
#tracker-navigation {
display: inline-flex;
display: flex;
flex-wrap: wrap;
margin: 0 0.5rem 0.5rem 0.5rem;
user-select: none;
height: 2rem;
}
.tracker-navigation-bar {
display: flex;
background-color: #b0a77d;
margin: 0.5rem;
border-radius: 4px;
}
.tracker-navigation-button {
display: block;
display: flex;
justify-content: center;
align-items: center;
margin: 4px;
padding-left: 12px;
padding-right: 12px;
border-radius: 4px;
text-align: center;
font-size: 14px;
color: #000;
color: black !important;
font-weight: lighter;
}
@@ -146,6 +67,100 @@ img.alttp-sprite {
background-color: rgb(220, 226, 189);
}
.table-wrapper {
overflow-y: auto;
overflow-x: auto;
margin-bottom: 1rem;
resize: vertical;
}
#search {
border: 1px solid #000000;
border-radius: 3px;
padding: 3px;
width: 200px;
}
div.dataTables_wrapper.no-footer .dataTables_scrollBody {
border: none;
}
table.dataTable {
color: #000000;
}
table.dataTable thead {
font-family: LexendDeca-Regular, sans-serif;
}
table.dataTable tbody, table.dataTable tfoot {
background-color: #dce2bd;
font-family: LexendDeca-Light, sans-serif;
}
table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover {
background-color: #e2eabb;
}
table.dataTable tbody td, table.dataTable tfoot td {
padding: 4px 6px;
}
table.dataTable, table.dataTable.no-footer {
border-left: 1px solid #bba967;
width: calc(100% - 2px) !important;
font-size: 1rem;
}
table.dataTable thead th {
position: -webkit-sticky;
position: sticky;
background-color: #b0a77d;
top: 0;
}
table.dataTable thead th.upper-row {
position: -webkit-sticky;
position: sticky;
background-color: #b0a77d;
height: 36px;
top: 0;
}
table.dataTable thead th.lower-row {
position: -webkit-sticky;
position: sticky;
background-color: #b0a77d;
height: 22px;
top: 46px;
}
table.dataTable tbody td, table.dataTable tfoot td {
border: 1px solid #bba967;
}
table.dataTable tfoot td {
font-weight: bold;
}
div.dataTables_scrollBody {
background-color: inherit !important;
}
table.dataTable .center-column {
text-align: center;
}
img.icon-sprite {
height: auto;
max-height: 32px;
min-height: 14px;
}
.item-acquired {
background-color: #d3c97d;
}
@media all and (max-width: 1700px) {
table.dataTable thead th.upper-row{
position: -webkit-sticky;
@@ -155,7 +170,7 @@ img.alttp-sprite {
top: 0;
}
table.dataTable thead th.lower-row{
table.dataTable thead th.lower-row {
position: -webkit-sticky;
position: sticky;
background-color: #b0a77d;
@@ -163,11 +178,11 @@ img.alttp-sprite {
top: 37px;
}
table.dataTable, table.dataTable.no-footer{
table.dataTable, table.dataTable.no-footer {
font-size: 0.8rem;
}
img.alttp-sprite {
img.icon-sprite {
height: auto;
max-height: 24px;
min-height: 10px;
@@ -183,7 +198,7 @@ img.alttp-sprite {
top: 0;
}
table.dataTable thead th.lower-row{
table.dataTable thead th.lower-row {
position: -webkit-sticky;
position: sticky;
background-color: #b0a77d;
@@ -191,11 +206,11 @@ img.alttp-sprite {
top: 32px;
}
table.dataTable, table.dataTable.no-footer{
table.dataTable, table.dataTable.no-footer {
font-size: 0.6rem;
}
img.alttp-sprite {
img.icon-sprite {
height: auto;
max-height: 20px;
min-height: 10px;

View File

@@ -0,0 +1,142 @@
@import url('https://fonts.googleapis.com/css2?family=Lexend+Deca:wght@100..900&display=swap');
.tracker-container {
width: 440px;
box-sizing: border-box;
font-family: "Lexend Deca", Arial, Helvetica, sans-serif;
border: 2px solid black;
border-radius: 4px;
resize: both;
background-color: #42b149;
color: white;
}
.hidden {
visibility: hidden;
}
/** Inventory Grid ****************************************************************************************************/
.inventory-grid {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
padding: 1rem;
gap: 1rem;
}
.inventory-grid .item {
position: relative;
display: flex;
justify-content: center;
height: 48px;
}
.inventory-grid .dual-item {
display: flex;
justify-content: center;
}
.inventory-grid .missing {
/* Missing items will be in full grayscale to signify "uncollected". */
filter: grayscale(100%) contrast(75%) brightness(75%);
}
.inventory-grid .item img,
.inventory-grid .dual-item img {
display: flex;
align-items: center;
text-align: center;
font-size: 0.8rem;
text-shadow: 0 1px 2px black;
font-weight: bold;
image-rendering: crisp-edges;
background-size: contain;
background-repeat: no-repeat;
}
.inventory-grid .dual-item img {
height: 48px;
margin: 0 -4px;
}
.inventory-grid .dual-item img:first-child {
align-self: flex-end;
}
.inventory-grid .item .quantity {
position: absolute;
bottom: 0;
right: 0;
text-align: right;
font-weight: 600;
font-size: 1.75rem;
line-height: 1.75rem;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;
user-select: none;
}
/** Regions List ******************************************************************************************************/
.regions-list {
padding: 1rem;
}
.regions-list summary {
list-style: none;
display: flex;
gap: 0.5rem;
cursor: pointer;
}
.regions-list summary::before {
content: "⯈";
width: 1em;
flex-shrink: 0;
}
.regions-list details {
font-weight: 300;
}
.regions-list details[open] > summary::before {
content: "⯆";
}
.regions-list .region {
width: 100%;
display: grid;
grid-template-columns: 20fr 8fr 2fr 2fr;
align-items: center;
gap: 4px;
text-align: center;
font-weight: 300;
box-sizing: border-box;
}
.regions-list .region :first-child {
text-align: left;
font-weight: 500;
}
.regions-list .region.region-header {
margin-left: 24px;
width: calc(100% - 24px);
padding: 2px;
}
.regions-list .location-rows {
border-top: 1px solid white;
display: grid;
grid-template-columns: auto 32px;
font-weight: 300;
padding: 2px 8px;
margin-top: 4px;
font-size: 0.8rem;
}
.regions-list .location-rows :nth-child(even) {
text-align: right;
}

View File

@@ -1,309 +0,0 @@
html{
background-image: url('../static/backgrounds/grass.png');
background-repeat: repeat;
background-size: 650px 650px;
scroll-padding-top: 90px;
}
#weighted-settings{
max-width: 1000px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
}
#weighted-settings #games-wrapper{
width: 100%;
}
#weighted-settings .setting-wrapper{
width: 100%;
margin-bottom: 2rem;
}
#weighted-settings .setting-wrapper .add-option-div{
display: flex;
flex-direction: row;
justify-content: flex-start;
margin-bottom: 1rem;
}
#weighted-settings .setting-wrapper .add-option-div button{
width: auto;
height: auto;
margin: 0 0 0 0.15rem;
padding: 0 0.25rem;
border-radius: 4px;
cursor: default;
}
#weighted-settings .setting-wrapper .add-option-div button:active{
margin-bottom: 1px;
}
#weighted-settings p.setting-description{
margin: 0 0 1rem;
}
#weighted-settings p.hint-text{
margin: 0 0 1rem;
font-style: italic;
}
#weighted-settings .jump-link{
color: #ffef00;
cursor: pointer;
text-decoration: underline;
}
#weighted-settings table{
width: 100%;
}
#weighted-settings table th, #weighted-settings table td{
border: none;
}
#weighted-settings table td{
padding: 5px;
}
#weighted-settings table .td-left{
font-family: LexendDeca-Regular, sans-serif;
padding-right: 1rem;
width: 200px;
}
#weighted-settings table .td-middle{
display: flex;
flex-direction: column;
justify-content: space-evenly;
padding-right: 1rem;
}
#weighted-settings table .td-right{
width: 4rem;
text-align: right;
}
#weighted-settings table .td-delete{
width: 50px;
text-align: right;
}
#weighted-settings table .range-option-delete{
cursor: pointer;
}
#weighted-settings .items-wrapper{
display: flex;
flex-direction: row;
justify-content: space-between;
}
#weighted-settings .items-div h3{
margin-bottom: 0.5rem;
}
#weighted-settings .items-wrapper .item-set-wrapper{
width: 24%;
font-weight: bold;
}
#weighted-settings .item-container{
border: 1px solid #ffffff;
border-radius: 2px;
width: 100%;
height: 300px;
overflow-y: auto;
overflow-x: hidden;
margin-top: 0.125rem;
font-weight: normal;
}
#weighted-settings .item-container .item-div{
padding: 0.125rem 0.5rem;
cursor: pointer;
}
#weighted-settings .item-container .item-div:hover{
background-color: rgba(0, 0, 0, 0.1);
}
#weighted-settings .item-container .item-qty-div{
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0.125rem 0.5rem;
cursor: pointer;
}
#weighted-settings .item-container .item-qty-div .item-qty-input-wrapper{
display: flex;
flex-direction: column;
justify-content: space-around;
}
#weighted-settings .item-container .item-qty-div input{
min-width: unset;
width: 1.5rem;
text-align: center;
}
#weighted-settings .item-container .item-qty-div:hover{
background-color: rgba(0, 0, 0, 0.1);
}
#weighted-settings .hints-div, #weighted-settings .locations-div{
margin-top: 2rem;
}
#weighted-settings .hints-div h3, #weighted-settings .locations-div h3{
margin-bottom: 0.5rem;
}
#weighted-settings .hints-container, #weighted-settings .locations-container{
display: flex;
flex-direction: row;
justify-content: space-between;
}
#weighted-settings .hints-wrapper, #weighted-settings .locations-wrapper{
width: calc(50% - 0.5rem);
font-weight: bold;
}
#weighted-settings .hints-wrapper .simple-list, #weighted-settings .locations-wrapper .simple-list{
margin-top: 0.25rem;
height: 300px;
font-weight: normal;
}
#weighted-settings #weighted-settings-button-row{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#weighted-settings code{
background-color: #d9cd8e;
border-radius: 4px;
padding-left: 0.25rem;
padding-right: 0.25rem;
color: #000000;
}
#weighted-settings #user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
}
#weighted-settings #user-message.visible{
display: block;
cursor: pointer;
}
#weighted-settings h1{
font-size: 2.5rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
#weighted-settings h2{
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: none;
text-shadow: 1px 1px 2px #000000;
}
#weighted-settings h3, #weighted-settings h4, #weighted-settings h5, #weighted-settings h6{
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
text-transform: none;
}
#weighted-settings a{
color: #ffef00;
cursor: pointer;
}
#weighted-settings input:not([type]){
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
}
#weighted-settings input:not([type]):focus{
border: 1px solid #ffffff;
}
#weighted-settings select{
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
background-color: #ffffff;
}
#weighted-settings .game-options, #weighted-settings .rom-options{
display: flex;
flex-direction: column;
}
#weighted-settings .simple-list{
display: flex;
flex-direction: column;
max-height: 300px;
overflow-y: auto;
border: 1px solid #ffffff;
border-radius: 4px;
}
#weighted-settings .simple-list .list-row label{
display: block;
width: calc(100% - 0.5rem);
padding: 0.0625rem 0.25rem;
}
#weighted-settings .simple-list .list-row label:hover{
background-color: rgba(0, 0, 0, 0.1);
}
#weighted-settings .simple-list .list-row label input[type=checkbox]{
margin-right: 0.5rem;
}
#weighted-settings .invisible{
display: none;
}
@media all and (max-width: 1000px), all and (orientation: portrait){
#weighted-settings .game-options{
justify-content: flex-start;
flex-wrap: wrap;
}
#game-options table label{
display: block;
min-width: 200px;
}
}

View File

@@ -0,0 +1,232 @@
html {
background-image: url("../../static/backgrounds/grass.png");
background-repeat: repeat;
background-size: 650px 650px;
scroll-padding-top: 90px;
}
#weighted-options {
max-width: 1000px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
}
#weighted-options #weighted-options-header h1 {
margin-bottom: 0;
padding-bottom: 0;
}
#weighted-options #weighted-options-header h1:nth-child(2) {
font-size: 1.4rem;
margin-top: -8px;
margin-bottom: 0.5rem;
}
#weighted-options .js-warning-banner {
width: calc(100% - 1rem);
padding: 0.5rem;
border-radius: 4px;
background-color: #f3f309;
color: #000000;
margin-bottom: 0.5rem;
text-align: center;
}
#weighted-options .option-wrapper {
width: 100%;
margin-bottom: 2rem;
}
#weighted-options .option-wrapper .add-option-div {
display: flex;
flex-direction: row;
justify-content: flex-start;
margin-bottom: 1rem;
}
#weighted-options .option-wrapper .add-option-div button {
width: auto;
height: auto;
margin: 0 0 0 0.15rem;
padding: 0 0.25rem;
border-radius: 4px;
cursor: default;
}
#weighted-options .option-wrapper .add-option-div button:active {
margin-bottom: 1px;
}
#weighted-options p.option-description {
margin: 0 0 1rem;
}
#weighted-options p.hint-text {
margin: 0 0 1rem;
font-style: italic;
}
#weighted-options table {
width: 100%;
margin-top: 0.5rem;
margin-bottom: 1.5rem;
}
#weighted-options table th, #weighted-options table td {
border: none;
}
#weighted-options table td {
padding: 5px;
}
#weighted-options table .td-left {
font-family: LexendDeca-Regular, sans-serif;
padding-right: 1rem;
width: 200px;
}
#weighted-options table .td-middle {
display: flex;
flex-direction: column;
justify-content: space-evenly;
padding-right: 1rem;
}
#weighted-options table .td-right {
width: 4rem;
text-align: right;
}
#weighted-options table .td-delete {
width: 50px;
text-align: right;
}
#weighted-options table .range-option-delete {
cursor: pointer;
}
#weighted-options #weighted-options-button-row {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#weighted-options #user-message {
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
}
#weighted-options #user-message.visible {
display: block;
cursor: pointer;
}
#weighted-options h1 {
font-size: 2.5rem;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
#weighted-options h2, #weighted-options details summary.h2 {
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: none;
text-shadow: 1px 1px 2px #000000;
}
#weighted-options h3, #weighted-options h4, #weighted-options h5, #weighted-options h6 {
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
text-transform: none;
cursor: unset;
}
#weighted-options h3.option-group-header {
margin-top: 0.75rem;
font-weight: bold;
}
#weighted-options a {
color: #ffef00;
cursor: pointer;
}
#weighted-options input:not([type]) {
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
}
#weighted-options input:not([type]):focus {
border: 1px solid #ffffff;
}
#weighted-options .invisible {
display: none;
}
#weighted-options .unsupported-option {
margin-top: 0.5rem;
}
#weighted-options .set-container, #weighted-options .dict-container, #weighted-options .list-container {
display: flex;
flex-direction: column;
background-color: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(20, 20, 20, 0.25);
border-radius: 3px;
color: #ffffff;
max-height: 15rem;
min-width: 14.5rem;
overflow-y: auto;
padding-right: 0.25rem;
padding-left: 0.25rem;
margin-top: 0.5rem;
}
#weighted-options .set-container .divider, #weighted-options .dict-container .divider, #weighted-options .list-container .divider {
width: 100%;
height: 2px;
background-color: rgba(20, 20, 20, 0.25);
margin-top: 0.125rem;
margin-bottom: 0.125rem;
}
#weighted-options .set-container .set-entry, #weighted-options .set-container .dict-entry, #weighted-options .set-container .list-entry, #weighted-options .dict-container .set-entry, #weighted-options .dict-container .dict-entry, #weighted-options .dict-container .list-entry, #weighted-options .list-container .set-entry, #weighted-options .list-container .dict-entry, #weighted-options .list-container .list-entry {
display: flex;
flex-direction: row;
align-items: flex-start;
padding-bottom: 0.25rem;
padding-top: 0.25rem;
user-select: none;
line-height: 1rem;
}
#weighted-options .set-container .set-entry:hover, #weighted-options .set-container .dict-entry:hover, #weighted-options .set-container .list-entry:hover, #weighted-options .dict-container .set-entry:hover, #weighted-options .dict-container .dict-entry:hover, #weighted-options .dict-container .list-entry:hover, #weighted-options .list-container .set-entry:hover, #weighted-options .list-container .dict-entry:hover, #weighted-options .list-container .list-entry:hover {
background-color: rgba(20, 20, 20, 0.25);
}
#weighted-options .set-container .set-entry input[type=checkbox], #weighted-options .set-container .dict-entry input[type=checkbox], #weighted-options .set-container .list-entry input[type=checkbox], #weighted-options .dict-container .set-entry input[type=checkbox], #weighted-options .dict-container .dict-entry input[type=checkbox], #weighted-options .dict-container .list-entry input[type=checkbox], #weighted-options .list-container .set-entry input[type=checkbox], #weighted-options .list-container .dict-entry input[type=checkbox], #weighted-options .list-container .list-entry input[type=checkbox] {
margin-right: 0.25rem;
}
#weighted-options .set-container .set-entry input[type=number], #weighted-options .set-container .dict-entry input[type=number], #weighted-options .set-container .list-entry input[type=number], #weighted-options .dict-container .set-entry input[type=number], #weighted-options .dict-container .dict-entry input[type=number], #weighted-options .dict-container .list-entry input[type=number], #weighted-options .list-container .set-entry input[type=number], #weighted-options .list-container .dict-entry input[type=number], #weighted-options .list-container .list-entry input[type=number] {
max-width: 1.5rem;
max-height: 1rem;
margin-left: 0.125rem;
text-align: center;
/* Hide arrows on input[type=number] fields */
-moz-appearance: textfield;
}
#weighted-options .set-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .set-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .set-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .list-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .list-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .list-entry input[type=number]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
#weighted-options .set-container .set-entry label, #weighted-options .set-container .dict-entry label, #weighted-options .set-container .list-entry label, #weighted-options .dict-container .set-entry label, #weighted-options .dict-container .dict-entry label, #weighted-options .dict-container .list-entry label, #weighted-options .list-container .set-entry label, #weighted-options .list-container .dict-entry label, #weighted-options .list-container .list-entry label {
flex-grow: 1;
margin-right: 0;
min-width: unset;
display: unset;
}
.hidden {
display: none;
}
@media all and (max-width: 1000px), all and (orientation: portrait) {
#weighted-options .game-options {
justify-content: flex-start;
flex-wrap: wrap;
}
#game-options table label {
display: block;
min-width: 200px;
}
}
/*# sourceMappingURL=weightedOptions.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["weightedOptions.scss"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAOZ;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAIR;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;AAEA;EACA;;AACA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;;AAMhB;EACI;;;AAGJ;EACI;IACI;IACA;;EAGJ;IACI;IACA","file":"weightedOptions.css"}

View File

@@ -0,0 +1,274 @@
html{
background-image: url('../../static/backgrounds/grass.png');
background-repeat: repeat;
background-size: 650px 650px;
scroll-padding-top: 90px;
}
#weighted-options{
max-width: 1000px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
#weighted-options-header{
h1{
margin-bottom: 0;
padding-bottom: 0;
}
h1:nth-child(2){
font-size: 1.4rem;
margin-top: -8px;
margin-bottom: 0.5rem;
}
}
.js-warning-banner{
width: calc(100% - 1rem);
padding: 0.5rem;
border-radius: 4px;
background-color: #f3f309;
color: #000000;
margin-bottom: 0.5rem;
text-align: center;
}
.option-wrapper{
width: 100%;
margin-bottom: 2rem;
.add-option-div{
display: flex;
flex-direction: row;
justify-content: flex-start;
margin-bottom: 1rem;
button{
width: auto;
height: auto;
margin: 0 0 0 0.15rem;
padding: 0 0.25rem;
border-radius: 4px;
cursor: default;
&:active{
margin-bottom: 1px;
}
}
}
}
p{
&.option-description{
margin: 0 0 1rem;
}
&.hint-text{
margin: 0 0 1rem;
font-style: italic;
};
}
table{
width: 100%;
margin-top: 0.5rem;
margin-bottom: 1.5rem;
th, td{
border: none;
}
td{
padding: 5px;
}
.td-left{
font-family: LexendDeca-Regular, sans-serif;
padding-right: 1rem;
width: 200px;
}
.td-middle{
display: flex;
flex-direction: column;
justify-content: space-evenly;
padding-right: 1rem;
}
.td-right{
width: 4rem;
text-align: right;
}
.td-delete{
width: 50px;
text-align: right;
}
.range-option-delete{
cursor: pointer;
}
}
#weighted-options-button-row{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
&.visible{
display: block;
cursor: pointer;
}
}
h1{
font-size: 2.5rem;
font-weight: normal;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
h2, details summary.h2{
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: none;
text-shadow: 1px 1px 2px #000000;
}
h3, h4, h5, h6{
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
text-transform: none;
cursor: unset;
}
h3{
&.option-group-header{
margin-top: 0.75rem;
font-weight: bold;
}
}
a{
color: #ffef00;
cursor: pointer;
}
input:not([type]){
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
&:focus{
border: 1px solid #ffffff;
}
}
.invisible{
display: none;
}
.unsupported-option{
margin-top: 0.5rem;
}
.set-container, .dict-container, .list-container{
display: flex;
flex-direction: column;
background-color: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(20, 20, 20, 0.25);
border-radius: 3px;
color: #ffffff;
max-height: 15rem;
min-width: 14.5rem;
overflow-y: auto;
padding-right: 0.25rem;
padding-left: 0.25rem;
margin-top: 0.5rem;
.divider{
width: 100%;
height: 2px;
background-color: rgba(20, 20, 20, 0.25);
margin-top: 0.125rem;
margin-bottom: 0.125rem;
}
.set-entry, .dict-entry, .list-entry{
display: flex;
flex-direction: row;
align-items: flex-start;
padding-bottom: 0.25rem;
padding-top: 0.25rem;
user-select: none;
line-height: 1rem;
&:hover{
background-color: rgba(20, 20, 20, 0.25);
}
input[type=checkbox]{
margin-right: 0.25rem;
}
input[type=number]{
max-width: 1.5rem;
max-height: 1rem;
margin-left: 0.125rem;
text-align: center;
/* Hide arrows on input[type=number] fields */
-moz-appearance: textfield;
&::-webkit-outer-spin-button, &::-webkit-inner-spin-button{
-webkit-appearance: none;
margin: 0;
}
}
label{
flex-grow: 1;
margin-right: 0;
min-width: unset;
display: unset;
}
}
}
}
.hidden{
display: none;
}
@media all and (max-width: 1000px), all and (orientation: portrait){
#weighted-options .game-options{
justify-content: flex-start;
flex-wrap: wrap;
}
#game-options table label{
display: block;
min-width: 200px;
}
}

View File

@@ -17,9 +17,9 @@
</p>
<div id="check-form-wrapper">
<form id="check-form" method="post" enctype="multipart/form-data">
<input id="file-input" type="file" name="file">
<input id="file-input" type="file" name="file" multiple>
</form>
<button id="check-button">Upload</button>
<button id="check-button">Upload File(s)</button>
</div>
</div>
</div>

View File

@@ -28,6 +28,10 @@
{% endfor %}
</tbody>
</table>
{% if combined_yaml %}
<h1>Combined File Download</h1>
<p><a href="data:text/yaml;base64,{{ combined_yaml }}" download="combined.yaml">Download</a></p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -69,8 +69,8 @@
</td>
<td>
<select name="collect_mode" id="collect_mode">
<option value="goal">Allow !collect after goal completion</option>
<option value="auto">Automatic on goal completion</option>
<option value="goal">Allow !collect after goal completion</option>
<option value="auto-enabled">
Automatic on goal completion and manual !collect
</option>
@@ -93,9 +93,9 @@
{% if race -%}
<option value="disabled">Disabled in Race mode</option>
{%- else -%}
<option value="disabled">Disabled</option>
<option value="goal">Allow !remaining after goal completion</option>
<option value="enabled">Manual !remaining</option>
<option value="disabled">Disabled</option>
{%- endif -%}
</select>
</td>
@@ -119,6 +119,28 @@
</select>
</td>
</tr>
<tr>
<td>
<label for="spoiler">Spoiler Log:
<span class="interactive" data-tooltip="Generates a text listing all randomized elements.
Warning: playthrough can take a significant amount of time for larger multiworlds.">
(?)
</span>
</label>
</td>
<td>
<select name="spoiler" id="spoiler">
{% if race -%}
<option value="0">Disabled in Race mode</option>
{%- else -%}
<option value="3">Enabled with playthrough and traversal</option>
<option value="2">Enabled with playthrough</option>
<option value="1">Enabled</option>
<option value="0">Disabled</option>
{%- endif -%}
</select>
</td>
</tr>
</tbody>
</table>
</div>
@@ -163,12 +185,12 @@
</span>
</td>
<td>
<input type="checkbox" id="plando_items" name="plando_items" value="items">
<label for="plando_items">Items</label><br>
<input type="checkbox" id="plando_bosses" name="plando_bosses" value="bosses" checked>
<label for="plando_bosses">Bosses</label><br>
<input type="checkbox" id="plando_items" name="plando_items" value="items" checked>
<label for="plando_items">Items</label><br>
<input type="checkbox" id="plando_connections" name="plando_connections" value="connections" checked>
<label for="plando_connections">Connections</label><br>
@@ -181,10 +203,10 @@
</div>
</div>
<div id="generate-form-button-row">
<input id="file-input" type="file" name="file">
<input id="file-input" type="file" name="file" multiple>
</div>
</form>
<button id="generate-game-button">Upload File</button>
<button id="generate-game-button">Upload File(s)</button>
</div>
</div>
</div>

View File

@@ -1,36 +1,57 @@
{% extends 'tablepage.html' %}
{% extends "tablepage.html" %}
{% block head %}
{{ super() }}
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="styles/tracker.css") }}"/>
<script type="application/ecmascript" src="{{ url_for("static", filename="assets/jquery.scrollsync.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for("static", filename="assets/trackerCommon.js") }}"></script>
{% endblock %}
{% block body %}
{% include 'header/dirtHeader.html' %}
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}" data-second="{{ saving_second }}">
<div id="tracker-header-bar">
<input placeholder="Search" id="search"/>
<span class="info">This tracker will automatically update itself periodically.</span>
{% include "header/dirtHeader.html" %}
<div id="tracker-navigation">
<div class="tracker-navigation-bar">
<a
class="tracker-navigation-button"
href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}"
>
🡸 Return to Multiworld Tracker
</a>
{% if game_specific_tracker %}
<a
class="tracker-navigation-button"
href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}"
>
Game-Specific Tracker
</a>
{% endif %}
</div>
</div>
<div id="tracker-wrapper" data-tracker="{{ room.tracker | suuid }}/{{ team }}/{{ player }}" data-second="{{ saving_second }}">
<div id="tracker-header-bar">
<input placeholder="Search" id="search" />
<div class="info">This tracker will automatically update itself periodically.</div>
</div>
<div id="tables-container">
<div class="table-wrapper">
<table id="received-table" class="table non-unique-item-table">
<thead>
<tr>
<th>Item</th>
<th>Amount</th>
<th>Order Received</th>
<th>Last Order Received</th>
</tr>
</thead>
<tbody>
{% for id, count in inventory.items() %}
<tr>
<td>{{ id | item_name }}</td>
<td>{{ count }}</td>
<td>{{received_items[id]}}</td>
</tr>
{% for id, count in inventory.items() if count > 0 %}
<tr>
<td>{{ item_id_to_name[game][id] }}</td>
<td>{{ count }}</td>
<td>{{ received_items[id] }}</td>
</tr>
{%- endfor -%}
</tbody>
@@ -39,24 +60,62 @@
<div class="table-wrapper">
<table id="locations-table" class="table non-unique-item-table">
<thead>
<tr>
<th>Location</th>
<th>Checked</th>
</tr>
<tr>
<th>Location</th>
<th class="center-column">Checked</th>
</tr>
</thead>
<tbody>
{% for name in checked_locations %}
{%- for location in locations -%}
<tr>
<td>{{ name | location_name}}</td>
<td></td>
<td>{{ location_id_to_name[game][location] }}</td>
<td class="center-column">
{% if location in checked_locations %}✔{% endif %}
</td>
</tr>
{%- endfor -%}
{% for name in not_checked_locations %}
{%- endfor -%}
</tbody>
</table>
</div>
<div class="table-wrapper">
<table id="hints-table" class="table non-unique-item-table">
<thead>
<tr>
<td>{{ name | location_name}}</td>
<td></td>
<th>Finder</th>
<th>Receiver</th>
<th>Item</th>
<th>Location</th>
<th>Game</th>
<th>Entrance</th>
<th class="center-column">Found</th>
</tr>
{%- endfor -%}
</thead>
<tbody>
{%- for hint in hints -%}
<tr>
<td>
{% if hint.finding_player == player %}
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
{% else %}
{{ player_names_with_alias[(team, hint.finding_player)] }}
{% endif %}
</td>
<td>
{% if hint.receiving_player == player %}
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
{% else %}
{{ player_names_with_alias[(team, hint.receiving_player)] }}
{% endif %}
</td>
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>
<td>{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}</td>
<td>{{ games[(team, hint.finding_player)] }}</td>
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
<td class="center-column">{% if hint.found %}✔{% endif %}</td>
</tr>
{%- endfor -%}
</tbody>
</table>
</div>

View File

@@ -30,14 +30,14 @@
<img src="/static/static/button-images/hamburger-menu-icon.png" alt="Menu" />
</a>
</div>
<div id="base-header-mobile-menu">
<a href="/games">supported games</a>
<a href="/tutorial">setup guides</a>
<a href="/faq/en">f.a.q.</a>
<a href="/generate">generate game</a>
<a href="/uploads">host game</a>
<a href="/user-content">user content</a>
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
</div>
</header>
<div id="base-header-mobile-menu">
<a href="/games">supported games</a>
<a href="/tutorial">setup guides</a>
<a href="/faq/en">f.a.q.</a>
<a href="/generate">generate game</a>
<a href="/uploads">host game</a>
<a href="/user-content">user content</a>
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
</div>
{% endblock %}

View File

@@ -3,6 +3,16 @@
{% block head %}
<title>Multiworld {{ room.id|suuid }}</title>
{% if should_refresh %}<meta http-equiv="refresh" content="2">{% endif %}
<meta name="og:site_name" content="Archipelago">
<meta property="og:title" content="Multiworld {{ room.id|suuid }}">
<meta property="og:type" content="website" />
{% if room.seed.slots|length < 2 %}
<meta property="og:description" content="{{ room.seed.slots|length }} Player World
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
{% else %}
<meta property="og:description" content="{{ room.seed.slots|length }} Players Multiworld
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
{% endif %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostRoom.css") }}"/>
{% endblock %}
@@ -14,7 +24,8 @@
<br />
{% endif %}
{% if room.tracker %}
This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled.
This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a>
and a <a href="{{ url_for("get_multiworld_sphere_tracker", tracker=room.tracker) }}">Sphere Tracker</a> enabled.
<br />
{% endif %}
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
@@ -32,13 +43,18 @@
{% endif %}
{{ macros.list_patches_room(room) }}
{% if room.owner == session["_id"] %}
<form method=post>
<div class="form-group">
<label for="cmd"></label>
<input class="form-control" type="text" id="cmd" name="cmd"
placeholder="Server Command. /help to list them, list gets appended to log.">
</div>
</form>
<div style="display: flex; align-items: center;">
<form method=post style="flex-grow: 1; margin-right: 1em;">
<div class="form-group">
<label for="cmd"></label>
<input class="form-control" type="text" id="cmd" name="cmd"
placeholder="Server Command. /help to list them, list gets appended to log.">
</div>
</form>
<a href="{{ url_for("display_log", room=room.id) }}">
Open Log File...
</a>
</div>
<div id="logger"></div>
<script type="application/ecmascript">
let xmlhttp = new XMLHttpRequest();

View File

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

View File

@@ -49,9 +49,9 @@
our crazy idea into a reality.
</p>
<p>
<span class="variable">{{ seeds }}</span>
<a href="{{ url_for("stats") }}">{{ seeds }}</a>
games were generated and
<span class="variable">{{ rooms }}</span>
<a href="{{ url_for("stats") }}">{{ rooms }}</a>
were hosted in the last 7 days.
</p>
</div>

View File

@@ -1,188 +0,0 @@
{% extends 'tablepage.html' %}
{% block head %}
{{ super() }}
<title>ALttP Multiworld Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/lttpMultiTracker.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
{% endblock %}
{% block body %}
{% include 'header/dirtHeader.html' %}
{% include 'multiTrackerNavigation.html' %}
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<div id="tracker-header-bar">
<input placeholder="Search" id="search"/>
<span{% if not video %} hidden{% endif %} id="multi-stream-link">
<a target="_blank" href="https://multistream.me/
{%- for platform, link in video.values()|unique(False, 1)-%}
{%- if platform == "Twitch" -%}t{%- else -%}yt{%- endif -%}:{{- link -}}/
{%- endfor -%}">
Multistream
</a>
</span>
<span class="info">Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically.</span>
</div>
<div id="tables-container">
{% for team, players in inventory.items() %}
<div class="table-wrapper">
<table id="inventory-table" class="table unique-item-table">
<thead>
<tr>
<th>#</th>
<th>Name</th>
{%- for name in tracking_names -%}
{%- if name in icons -%}
<th class="center-column">
<img class="alttp-sprite" src="{{ icons[name] }}" alt="{{ name|e }}">
</th>
{%- else -%}
<th class="center-column">{{ name|e }}</th>
{%- endif -%}
{%- endfor -%}
</tr>
</thead>
<tbody>
{%- for player, items in players.items() -%}
<tr>
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
{%- if (team, loop.index) in video -%}
{%- if video[(team, loop.index)][0] == "Twitch" -%}
<td>
<a target="_blank" href="https://www.twitch.tv/{{ video[(team, loop.index)][1] }}">
{{ player_names[(team, loop.index)] }}
▶️</a></td>
{%- elif video[(team, loop.index)][0] == "Youtube" -%}
<td>
<a target="_blank" href="youtube.com/c/{{ video[(team, loop.index)][1] }}/live">
{{ player_names[(team, loop.index)] }}
▶️</a></td>
{%- endif -%}
{%- else -%}
<td>{{ player_names[(team, loop.index)] }}</td>
{%- endif -%}
{%- for id in tracking_ids -%}
{%- if items[id] -%}
<td class="center-column item-acquired">
{% if id in multi_items %}{{ items[id] }}{% else %}✔️{% endif %}</td>
{%- else -%}
<td></td>
{%- endif -%}
{% endfor %}
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
{% endfor %}
{% for team, players in checks_done.items() %}
<div class="table-wrapper">
<table id="checks-table" class="table non-unique-item-table">
<thead>
<tr>
<th rowspan="2">#</th>
<th rowspan="2">Name</th>
{% for area in ordered_areas %}
{% set colspan = 1 %}
{% if area in key_locations %}
{% set colspan = colspan + 1 %}
{% endif %}
{% if area in big_key_locations %}
{% set colspan = colspan + 1 %}
{% endif %}
{% if area in icons %}
<th colspan="{{ colspan }}" class="center-column upper-row">
<img class="alttp-sprite" src="{{ icons[area] }}" alt="{{ area }}"></th>
{%- else -%}
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
{%- endif -%}
{%- endfor -%}
<th rowspan="2" class="center-column">&percnt;</th>
<th rowspan="2" class="center-column hours">Last<br>Activity</th>
</tr>
<tr>
{% for area in ordered_areas %}
<th class="center-column lower-row fraction">
<img class="alttp-sprite" src="{{ icons["Chest"] }}" alt="Checks">
</th>
{% if area in key_locations %}
<th class="center-column lower-row number">
<img class="alttp-sprite" src="{{ icons["Small Key"] }}" alt="Small Key">
</th>
{% endif %}
{% if area in big_key_locations %}
<th class="center-column lower-row number">
<img class="alttp-sprite" src="{{ icons["Big Key"] }}" alt="Big Key">
</th>
{%- endif -%}
{%- endfor -%}
</tr>
</thead>
<tbody>
{%- for player, checks in players.items() -%}
<tr>
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
<td>{{ player_names[(team, loop.index)]|e }}</td>
{%- for area in ordered_areas -%}
{%- set checks_done = checks[area] -%}
{%- set checks_total = checks_in_area[player][area] -%}
{%- if checks_done == checks_total -%}
<td class="item-acquired center-column">
{{ checks_done }}/{{ checks_total }}</td>
{%- else -%}
<td class="center-column">{{ checks_done }}/{{ checks_total }}</td>
{%- endif -%}
{%- if area in key_locations -%}
<td class="center-column">{{ inventory[team][player][small_key_ids[area]] }}</td>
{%- endif -%}
{%- if area in big_key_locations -%}
<td class="center-column">{% if inventory[team][player][big_key_ids[area]] %}✔️{% endif %}</td>
{%- endif -%}
{%- endfor -%}
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
{%- if activity_timers[(team, player)] -%}
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
{%- else -%}
<td class="center-column">None</td>
{%- endif -%}
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
{% endfor %}
{% for team, hints in hints.items() %}
<div class="table-wrapper">
<table id="hints-table" class="table non-unique-item-table" data-order='[[5, "asc"], [0, "asc"]]'>
<thead>
<tr>
<th>Finder</th>
<th>Receiver</th>
<th>Item</th>
<th>Location</th>
<th>Entrance</th>
<th>Found</th>
</tr>
</thead>
<tbody>
{%- for hint in hints -%}
<tr>
<td>{{ long_player_names[team, hint.finding_player] }}</td>
<td>{{ long_player_names[team, hint.receiving_player] }}</td>
<td>{{ hint.item|item_name }}</td>
<td>{{ hint.location|location_name }}</td>
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
<td>{% if hint.found %}✔{% endif %}</td>
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@@ -1,86 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/globalStyles.css") }}"/>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/lttp-tracker.css") }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/lttp-tracker.js") }}"></script>
</head>
<body>
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<table id="inventory-table">
<tr>
<td><img src="{{ bow_url }}" class="{{ 'acquired' if bow_acquired }}" /></td>
<td><img src="{{ icons["Blue Boomerang"] }}" class="{{ 'acquired' if 'Blue Boomerang' in acquired_items }}" /></td>
<td><img src="{{ icons["Red Boomerang"] }}" class="{{ 'acquired' if 'Red Boomerang' in acquired_items }}" /></td>
<td><img src="{{ icons["Hookshot"] }}" class="{{ 'acquired' if 'Hookshot' in acquired_items }}" /></td>
<td><img src="{{ icons["Magic Powder"] }}" class="powder-fix {{ 'acquired' if 'Magic Powder' in acquired_items }}" /></td>
</tr>
<tr>
<td><img src="{{ icons["Fire Rod"] }}" class="{{ 'acquired' if "Fire Rod" in acquired_items }}" /></td>
<td><img src="{{ icons["Ice Rod"] }}" class="{{ 'acquired' if "Ice Rod" in acquired_items }}" /></td>
<td><img src="{{ icons["Bombos"] }}" class="{{ 'acquired' if "Bombos" in acquired_items }}" /></td>
<td><img src="{{ icons["Ether"] }}" class="{{ 'acquired' if "Ether" in acquired_items }}" /></td>
<td><img src="{{ icons["Quake"] }}" class="{{ 'acquired' if "Quake" in acquired_items }}" /></td>
</tr>
<tr>
<td><img src="{{ icons["Lamp"] }}" class="{{ 'acquired' if "Lamp" in acquired_items }}" /></td>
<td><img src="{{ icons["Hammer"] }}" class="{{ 'acquired' if "Hammer" in acquired_items }}" /></td>
<td><img src="{{ icons["Flute"] }}" class="{{ 'acquired' if "Flute" in acquired_items }}" /></td>
<td><img src="{{ icons["Bug Catching Net"] }}" class="{{ 'acquired' if "Bug Catching Net" in acquired_items }}" /></td>
<td><img src="{{ icons["Book of Mudora"] }}" class="{{ 'acquired' if "Book of Mudora" in acquired_items }}" /></td>
</tr>
<tr>
<td><img src="{{ icons["Bottle"] }}" class="{{ 'acquired' if "Bottle" in acquired_items }}" /></td>
<td><img src="{{ icons["Cane of Somaria"] }}" class="{{ 'acquired' if "Cane of Somaria" in acquired_items }}" /></td>
<td><img src="{{ icons["Cane of Byrna"] }}" class="{{ 'acquired' if "Cane of Byrna" in acquired_items }}" /></td>
<td><img src="{{ icons["Cape"] }}" class="{{ 'acquired' if "Cape" in acquired_items }}" /></td>
<td><img src="{{ icons["Magic Mirror"] }}" class="{{ 'acquired' if "Magic Mirror" in acquired_items }}" /></td>
</tr>
<tr>
<td><img src="{{ icons["Pegasus Boots"] }}" class="{{ 'acquired' if "Pegasus Boots" in acquired_items }}" /></td>
<td><img src="{{ glove_url }}" class="{{ 'acquired' if glove_acquired }}" /></td>
<td><img src="{{ icons["Flippers"] }}" class="{{ 'acquired' if "Flippers" in acquired_items }}" /></td>
<td><img src="{{ icons["Moon Pearl"] }}" class="{{ 'acquired' if "Moon Pearl" in acquired_items }}" /></td>
<td><img src="{{ icons["Mushroom"] }}" class="{{ 'acquired' if "Mushroom" in acquired_items }}" /></td>
</tr>
<tr>
<td><img src="{{ sword_url }}" class="{{ 'acquired' if sword_acquired }}" /></td>
<td><img src="{{ shield_url }}" class="{{ 'acquired' if shield_acquired }}" /></td>
<td><img src="{{ mail_url }}" class="acquired" /></td>
<td><img src="{{ icons["Shovel"] }}" class="{{ 'acquired' if "Shovel" in acquired_items }}" /></td>
<td><img src="{{ icons["Triforce"] }}" class="{{ 'acquired' if "Triforce" in acquired_items }}" /></td>
</tr>
</table>
<table id="location-table">
<tr>
<th></th>
<th class="counter"><img src="{{ icons["Chest"] }}" /></th>
{% if key_locations and "Universal" not in key_locations %}
<th class="counter"><img src="{{ icons["Small Key"] }}" /></th>
{% endif %}
{% if big_key_locations %}
<th><img src="{{ icons["Big Key"] }}" /></th>
{% endif %}
</tr>
{% for area in sp_areas %}
<tr>
<td>{{ area }}</td>
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
{% if key_locations and "Universal" not in key_locations %}
<td class="counter">
{{ inventory[small_key_ids[area]] if area in key_locations else '—' }}
</td>
{% endif %}
{% if big_key_locations %}
<td>
{{ '✔' if area in big_key_locations and inventory[big_key_ids[area]] else ('—' if area not in big_key_locations else '') }}
</td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
</body>
</html>

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