Compare commits

...

377 Commits

Author SHA1 Message Date
Fabian Dill
0d4ffeca0a WebHost: clean internal checksum as well 2024-04-24 06:23:48 +02:00
Zach Parks
c61649cef3 Add webhost config sample entry 2024-04-23 23:16:54 -05:00
Fabian Dill
7ae102741a WebHost: add startup option to clean datapackages 2024-04-24 05:10:36 +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
1366 changed files with 110975 additions and 30626 deletions

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

@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: "Determine modified files (pull_request)"
if: github.event_name == 'pull_request'
@@ -50,7 +50,7 @@ jobs:
run: |
echo "diff=." >> $GITHUB_ENV
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
if: env.diff != ''
with:
python-version: 3.8

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,9 +27,9 @@ 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
@@ -46,25 +48,42 @@ jobs:
cd build
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: Store 7z
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ env.ZIP_NAME }}
path: dist/${{ env.ZIP_NAME }}
retention-days: 7 # keep for 7 days, should be enough
- 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: Store Setup
uses: actions/upload-artifact@v4
with:
name: ${{ env.SETUP_NAME }}
path: setups/${{ env.SETUP_NAME }}
retention-days: 7 # keep for 7 days, should be enough
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.11'
- name: Install build-time dependencies
@@ -100,13 +119,13 @@ jobs:
source venv/bin/activate
python setup.py build_exe --yes
- name: Store AppImage
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ env.APPIMAGE_NAME }}
path: dist/${{ env.APPIMAGE_NAME }}
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 }}

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

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

@@ -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
@@ -35,14 +35,14 @@ jobs:
- 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.11'
- name: Install build-time dependencies
@@ -74,7 +74,7 @@ jobs:
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

@@ -46,9 +46,9 @@ jobs:
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

View File

@@ -18,11 +18,14 @@ import NetUtils
import Options
import Utils
if typing.TYPE_CHECKING:
from worlds import AutoWorld
class Group(TypedDict, total=False):
name: str
game: str
world: auto_world
world: "AutoWorld.World"
players: Set[int]
item_pool: Set[str]
replacement_items: Dict[int, Optional[str]]
@@ -48,14 +51,10 @@ class ThreadBarrierProxy:
class MultiWorld():
debug_types = False
player_name: Dict[int, str]
difficulty_requirements: dict
required_medallions: dict
dark_room_logic: Dict[int, str]
restrict_dungeon_item_on_boss: Dict[int, bool]
plando_texts: List[Dict[str, str]]
plando_items: List[List[Dict[str, Any]]]
plando_connections: List
worlds: Dict[int, auto_world]
worlds: Dict[int, "AutoWorld.World"]
groups: Dict[int, Group]
regions: RegionManager
itempool: List[Item]
@@ -82,7 +81,7 @@ class MultiWorld():
game: Dict[int, str]
random: random.Random
per_slot_randoms: Dict[int, random.Random]
per_slot_randoms: Utils.DeprecateDict[int, random.Random]
"""Deprecated. Please use `self.random` instead."""
class AttributeProxy():
@@ -107,10 +106,14 @@ class MultiWorld():
return self
def append(self, region: Region):
assert region.name not in self.region_cache[region.player], \
f"{region.name} already exists in region cache."
self.region_cache[region.player][region.name] = region
def extend(self, regions: Iterable[Region]):
for region in regions:
assert region.name not in self.region_cache[region.player], \
f"{region.name} already exists in region cache."
self.region_cache[region.player][region.name] = region
def add_group(self, new_id: int):
@@ -130,7 +133,6 @@ class MultiWorld():
self.random = ThreadBarrierProxy(random.Random())
self.players = players
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
self.glitch_triforce = False
self.algorithm = 'balanced'
self.groups = {}
self.regions = self.RegionManager(players)
@@ -153,64 +155,18 @@ class MultiWorld():
self.local_early_items = {player: {} for player in self.player_ids}
self.indirect_connections = {}
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
self.fix_trock_doors = self.AttributeProxy(
lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
self.fix_skullwoods_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
self.fix_palaceofdarkness_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
self.fix_trock_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
for player in range(1, players + 1):
def set_player_attr(attr, val):
self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('shuffle', "vanilla")
set_player_attr('logic', "noglitches")
set_player_attr('mode', 'open')
set_player_attr('difficulty', 'normal')
set_player_attr('item_functionality', 'normal')
set_player_attr('timer', False)
set_player_attr('goal', 'ganon')
set_player_attr('required_medallions', ['Ether', 'Quake'])
set_player_attr('swamp_patch_required', False)
set_player_attr('powder_patch_required', False)
set_player_attr('ganon_at_pyramid', True)
set_player_attr('ganonstower_vanilla', True)
set_player_attr('can_access_trock_eyebridge', None)
set_player_attr('can_access_trock_front', None)
set_player_attr('can_access_trock_big_chest', None)
set_player_attr('can_access_trock_middle', None)
set_player_attr('fix_fake_world', True)
set_player_attr('difficulty_requirements', None)
set_player_attr('boss_shuffle', 'none')
set_player_attr('enemy_health', 'default')
set_player_attr('enemy_damage', 'default')
set_player_attr('beemizer_total_chance', 0)
set_player_attr('beemizer_trap_chance', 0)
set_player_attr('escape_assist', [])
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
set_player_attr('treasure_hunt_count', 0)
set_player_attr('clock_mode', False)
set_player_attr('countdown_start_time', 10)
set_player_attr('red_clock_time', -2)
set_player_attr('blue_clock_time', 2)
set_player_attr('green_clock_time', 4)
set_player_attr('can_take_damage', True)
set_player_attr('triforce_pieces_available', 30)
set_player_attr('triforce_pieces_required', 20)
set_player_attr('shop_shuffle', 'off')
set_player_attr('shuffle_prizes', "g")
set_player_attr('sprite_pool', [])
set_player_attr('dark_room_logic', "lamp")
set_player_attr('plando_items', [])
set_player_attr('plando_texts', {})
set_player_attr('plando_connections', [])
set_player_attr('game', "A Link to the Past")
set_player_attr('game', "Archipelago")
set_player_attr('completion_condition', lambda state: True)
self.worlds = {}
self.per_slot_randoms = {}
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
"world's random object instead (usually self.random)")
self.plando_options = PlandoOptions.none
def get_all_ids(self) -> Tuple[int, ...]:
@@ -219,6 +175,8 @@ class MultiWorld():
def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
"""Create a group with name and return the assigned player ID and group.
If a group of this name already exists, the set of players is extended instead of creating a new one."""
from worlds import AutoWorld
for group_id, group in self.groups.items():
if group["name"] == name:
group["players"] |= players
@@ -242,17 +200,18 @@ class MultiWorld():
return {group_id for group_id, group in self.groups.items() if player in group["players"]}
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
assert not self.worlds, "seed needs to be initialized before Worlds"
self.seed = get_seed(seed)
if secure:
self.secure()
else:
self.random.seed(self.seed)
self.seed_name = name if name else str(self.seed)
self.per_slot_randoms = {player: random.Random(self.random.getrandbits(64)) for player in
range(1, self.players + 1)}
def set_options(self, args: Namespace) -> None:
# TODO - remove this section once all worlds use options dataclasses
from worlds import AutoWorld
all_keys: Set[str] = {key for player in self.player_ids for key in
AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
for option_key in all_keys:
@@ -264,12 +223,13 @@ class MultiWorld():
for player in self.player_ids:
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
self.worlds[player] = world_type(self, player)
self.worlds[player].random = self.per_slot_randoms[player]
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
for option_key in options_dataclass.type_hints})
def set_item_links(self):
from worlds import AutoWorld
item_links = {}
replacement_prio = [False, True, None]
for player in self.player_ids:
@@ -433,7 +393,7 @@ class MultiWorld():
location.item = item
item.location = location
if collect:
self.state.collect(item, location.event, location)
self.state.collect(item, location.advancement, location)
logging.debug('Placed %s at %s', item, location)
@@ -572,15 +532,15 @@ class MultiWorld():
def location_condition(location: Location):
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
if location.player in players["minimal"]:
return False
return True
if location.player in players["locations"] or (location.item and location.item.player not in
players["minimal"]):
return True
return False
def location_relevant(location: Location):
"""Determine if this location is relevant to sweep."""
if location.progress_type != LocationProgressType.EXCLUDED \
and (location.player in players["locations"] or location.event
or (location.item and location.item.advancement)):
and (location.player in players["locations"] or location.advancement):
return True
return False
@@ -703,20 +663,29 @@ class CollectionState():
assert isinstance(player, int), "can_reach: player is required if spot is str"
# try to resolve a name
if resolution_hint == 'Location':
spot = self.multiworld.get_location(spot, player)
return self.can_reach_location(spot, player)
elif resolution_hint == 'Entrance':
spot = self.multiworld.get_entrance(spot, player)
return self.can_reach_entrance(spot, player)
else:
# default to Region
spot = self.multiworld.get_region(spot, player)
return self.can_reach_region(spot, player)
return spot.can_reach(self)
def can_reach_location(self, spot: str, player: int) -> bool:
return self.multiworld.get_location(spot, player).can_reach(self)
def can_reach_entrance(self, spot: str, player: int) -> bool:
return self.multiworld.get_entrance(spot, player).can_reach(self)
def can_reach_region(self, spot: str, player: int) -> bool:
return self.multiworld.get_region(spot, player).can_reach(self)
def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None:
if locations is None:
locations = self.multiworld.get_filled_locations()
reachable_events = True
# since the loop has a good chance to run more than once, only filter the events once
locations = {location for location in locations if location.event and location not in self.events and
locations = {location for location in locations if location.advancement and location not in self.events and
not key_only or getattr(location.item, "locked_dungeon_item", False)}
while reachable_events:
reachable_events = {location for location in locations if location.can_reach(self)}
@@ -823,8 +792,8 @@ class Entrance:
return self.__str__()
def __str__(self):
world = self.parent_region.multiworld if self.parent_region else None
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
multiworld = self.parent_region.multiworld if self.parent_region else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
class Region:
@@ -867,6 +836,8 @@ class Region:
del(self.region_manager.location_cache[location.player][location.name])
def insert(self, index: int, value: Location) -> None:
assert value.name not in self.region_manager.location_cache[value.player], \
f"{value.name} already exists in the location cache."
self._list.insert(index, value)
self.region_manager.location_cache[value.player][value.name] = value
@@ -877,6 +848,8 @@ class Region:
del(self.region_manager.entrance_cache[entrance.player][entrance.name])
def insert(self, index: int, value: Entrance) -> None:
assert value.name not in self.region_manager.entrance_cache[value.player], \
f"{value.name} already exists in the entrance cache."
self._list.insert(index, value)
self.region_manager.entrance_cache[value.player][value.name] = value
@@ -1002,11 +975,10 @@ class Location:
name: str
address: Optional[int]
parent_region: Optional[Region]
event: bool = False
locked: bool = False
show_in_spoiler: bool = True
progress_type: LocationProgressType = LocationProgressType.DEFAULT
always_allow = staticmethod(lambda item, state: False)
always_allow = staticmethod(lambda state, item: False)
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
item_rule = staticmethod(lambda item: True)
item: Optional[Item] = None
@@ -1033,15 +1005,14 @@ class Location:
raise Exception(f"Location {self} already filled.")
self.item = item
item.location = self
self.event = item.advancement
self.locked = True
def __repr__(self):
return self.__str__()
def __str__(self):
world = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
def __hash__(self):
return hash((self.name, self.player))
@@ -1049,6 +1020,15 @@ class Location:
def __lt__(self, other: Location):
return (self.player, self.name) < (other.player, other.name)
@property
def advancement(self) -> bool:
return self.item is not None and self.item.advancement
@property
def is_event(self) -> bool:
"""Returns True if the address of this location is None, denoting it is an Event Location."""
return self.address is None
@property
def native_item(self) -> bool:
"""Returns True if the item in this location matches game."""
@@ -1175,7 +1155,7 @@ class Spoiler:
{"player": player, "entrance": entrance, "exit": exit_, "direction": direction}
def create_playthrough(self, create_paths: bool = True) -> None:
"""Destructive to the world while it is run, damage gets repaired afterwards."""
"""Destructive to the multiworld while it is run, damage gets repaired afterwards."""
from itertools import chain
# get locations containing progress items
multiworld = self.multiworld
@@ -1262,12 +1242,12 @@ class Spoiler:
for location in sphere:
state.collect(location.item, True, location)
required_locations -= sphere
collection_spheres.append(sphere)
logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres),
len(sphere), len(required_locations))
required_locations -= sphere
if not sphere:
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
@@ -1326,10 +1306,15 @@ class Spoiler:
get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player))
def to_file(self, filename: str) -> None:
from itertools import chain
from worlds import AutoWorld
from Options import Visibility
def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
res = getattr(self.multiworld.worlds[player].options, option_key)
display_name = getattr(option_obj, "display_name", option_key)
outfile.write(f"{display_name + ':':33}{res.current_option_name}\n")
if res.visibility & Visibility.spoiler:
display_name = getattr(option_obj, "display_name", option_key)
outfile.write(f"{display_name + ':':33}{res.current_option_name}\n")
with open(filename, 'w', encoding="utf-8-sig") as outfile:
outfile.write(
@@ -1360,6 +1345,14 @@ class Spoiler:
AutoWorld.call_all(self.multiworld, "write_spoiler", outfile)
precollected_items = [f"{item.name} ({self.multiworld.get_player_name(item.player)})"
if self.multiworld.players > 1
else item.name
for item in chain.from_iterable(self.multiworld.precollected_items.values())]
if precollected_items:
outfile.write("\n\nStarting Items:\n\n")
outfile.write("\n".join([item for item in precollected_items]))
locations = [(str(location), str(location.item) if location.item is not None else "Nothing")
for location in self.multiworld.get_locations() if location.show_in_spoiler]
outfile.write('\n\nLocations:\n\n')
@@ -1449,8 +1442,3 @@ def get_seed(seed: Optional[int] = None) -> int:
random.seed(None)
return random.randint(0, pow(10, seeddigits) - 1)
return seed
from worlds import AutoWorld
auto_world = AutoWorld.World

View File

@@ -20,8 +20,8 @@ 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
@@ -72,9 +72,16 @@ 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, filter_text = "") -> bool:
@@ -115,6 +122,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:
@@ -124,6 +140,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
@@ -168,6 +193,7 @@ class CommonContext:
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
@@ -618,15 +644,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:
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)
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)
@@ -733,8 +760,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
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. '
@@ -941,4 +970,5 @@ def run_as_textclient():
if __name__ == '__main__':
logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING
run_as_textclient()

237
Fill.py
View File

@@ -27,12 +27,12 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
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, 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
@@ -68,7 +68,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
maximum_exploration_state = sweep_from_pool(
base_state, item_pool + unplaced_items)
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
@@ -80,8 +80,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.worlds[item_to_place.player].options.accessibility == Accessibility.option_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:
@@ -122,11 +122,11 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
# 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
@@ -156,10 +156,9 @@ 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)
@@ -173,7 +172,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
# validate all placements and remove invalid ones
state = sweep_from_pool(base_state, [])
for placement in placements:
if world.accessibility[placement.item.player] != "minimal" and not placement.can_reach(state):
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
@@ -188,7 +187,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:
@@ -196,19 +195,26 @@ 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") -> None:
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
@@ -261,36 +267,41 @@ 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("Remaining", placed, total)
_log_fill_progress(name, placed, total)
if total > 1000:
_log_fill_progress("Remaining", placed, total)
_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)}')
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.worlds[player].options.accessibility == "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
@@ -298,42 +309,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, name="Accessibility Corrections")
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.worlds[item.player].options.accessibility != '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:
@@ -345,8 +355,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:
@@ -370,28 +380,28 @@ 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, 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, 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:
@@ -400,18 +410,18 @@ 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) -> 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] = []
@@ -425,7 +435,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}
@@ -446,75 +456,78 @@ 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,
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
name="Priority")
accessibility_corrections(world, world.state, prioritylocations, progitempool)
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations
if progitempool:
# "advancement/progression fill"
fill_restrictive(world, world.state, defaultlocations, progitempool, name="Progression")
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, name="Progression")
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(world, excludedlocations, filleritempool)
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded")
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 = filleritempool + usefulitempool
remaining_fill(world, defaultlocations, restitempool)
remaining_fill(multiworld, defaultlocations, restitempool)
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
@@ -524,7 +537,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
@@ -537,15 +550,15 @@ 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
@@ -643,7 +656,7 @@ def balance_multiworld_progression(multiworld: 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
@@ -700,7 +713,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
# 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)
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()
multiworld.random.shuffle(items_to_replace)
@@ -731,7 +744,7 @@ def balance_multiworld_progression(multiworld: 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
@@ -752,10 +765,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}')
@@ -768,24 +780,24 @@ 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)
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'
@@ -799,12 +811,12 @@ def distribute_planned(world: MultiWorld) -> None:
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:
@@ -814,9 +826,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}
@@ -844,7 +856,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):
@@ -894,17 +906,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']
@@ -915,19 +927,19 @@ 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.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(world.state, item, False):
if location.can_fill(multiworld.state, item, False):
successful_pairs.append((item, location))
candidates.remove(location)
count = count + 1
@@ -945,21 +957,20 @@ def distribute_planned(world: MultiWorld) -> None:
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

@@ -21,11 +21,11 @@ from BaseClasses import seeddigits, get_seed, PlandoOptions
from Main import main as ERmain
from settings import get_settings
from Utils import parse_yamls, version_tuple, __version__, tuplize_version
from worlds.alttp import Options as LttPOptions
from worlds.alttp.EntranceRandomizer import parse_arguments
from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister
from worlds.generic import PlandoConnection
from worlds import failed_world_loads
def mystery_argparse():
@@ -34,8 +34,8 @@ def mystery_argparse():
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',
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=defaults.player_files_path,
help="Input directory for player files.")
@@ -103,8 +103,8 @@ def main(args=None, callback=ERmain):
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
@@ -147,7 +147,6 @@ def main(args=None, callback=ERmain):
erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed
erargs.plando_options = args.plando
erargs.glitch_triforce = options.generator.glitch_triforce_room
erargs.spoiler = args.spoiler
erargs.race = args.race
erargs.outputname = seed_name
@@ -156,7 +155,7 @@ def main(args=None, callback=ERmain):
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:
@@ -302,46 +301,43 @@ 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, dict)):
cleaned_value.update(new_value)
elif isinstance(new_value, list):
cleaned_value.extend(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
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
@@ -357,15 +353,6 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
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}.")
@@ -463,7 +450,11 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
ret.game = get_choice("game", weights)
if ret.game not in AutoWorldRegister.world_types:
picks = Utils.get_fuzzy_results(ret.game, AutoWorldRegister.world_types, limit=1)[0]
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.")
@@ -473,6 +464,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
world_type = AutoWorldRegister.world_types[ret.game]
game_weights = weights[ret.game]
if any(weight.startswith("+") for weight in game_weights) or \
any(weight.startswith("+") for weight in weights):
raise Exception(f"Merge tag cannot be used outside of trigger contexts.")
if "triggers" in game_weights:
weights = roll_triggers(weights, game_weights["triggers"])
game_weights = weights[ret.game]
@@ -485,120 +480,23 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
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":
if ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights, plando_options)
if PlandoOptions.connections in plando_options:
ret.plando_connections = []
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, "both")
))
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:
@@ -612,17 +510,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
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")
))
ret.sprite_pool = weights.get('sprite_pool', [])
ret.sprite = get_choice_legacy('sprite', weights, "Link")
if 'random_sprite_on_event' in weights:

View File

@@ -100,9 +100,9 @@ components.extend([
# Functions
Component("Open host.yaml", func=open_host_yaml),
Component("Open Patch", func=open_patch),
Component("Generate Template Settings", func=generate_yamls),
Component("Generate Template Options", func=generate_yamls),
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
Component("18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
Component("Unrated/18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
Component("Browse Files", func=browse_files),
])
@@ -161,7 +161,7 @@ def launch(exe, in_terminal=False):
def run_gui():
from kvui import App, ContainerLayout, GridLayout, Button, Label
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget
from kivy.uix.image import AsyncImage
from kivy.uix.relativelayout import RelativeLayout
@@ -185,11 +185,16 @@ def run_gui():
self.container = ContainerLayout()
self.grid = GridLayout(cols=2)
self.container.add_widget(self.grid)
self.grid.add_widget(Label(text="General"))
self.grid.add_widget(Label(text="Clients"))
button_layout = self.grid # make buttons fill the window
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))
tool_layout = ScrollBox()
tool_layout.layout.orientation = "vertical"
self.grid.add_widget(tool_layout)
client_layout = ScrollBox()
client_layout.layout.orientation = "vertical"
self.grid.add_widget(client_layout)
def build_button(component: Component):
def build_button(component: Component) -> Widget:
"""
Builds a button widget for a given component.
@@ -200,31 +205,26 @@ def run_gui():
None. The button is added to the parent grid layout.
"""
button = Button(text=component.display_name)
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()
box_layout = RelativeLayout(size_hint_y=None, height=40)
box_layout.add_widget(button)
box_layout.add_widget(image)
button_layout.add_widget(box_layout)
else:
button_layout.add_widget(button)
return box_layout
return button
for (tool, client) in itertools.zip_longest(itertools.chain(
self._tools.items(), self._miscs.items(), self._adjusters.items()), self._clients.items()):
# column 1
if tool:
build_button(tool[1])
else:
button_layout.add_widget(Label())
tool_layout.layout.add_widget(build_button(tool[1]))
# column 2
if client:
build_button(client[1])
else:
button_layout.add_widget(Label())
client_layout.layout.add_widget(build_button(client[1]))
return self.container

View File

@@ -348,7 +348,8 @@ class LinksAwakeningClient():
await asyncio.sleep(1.0)
continue
self.stop_bizhawk_spam = False
logger.info(f"Connected to Retroarch {version.decode('ascii')} running {rom_name.decode('ascii')}")
logger.info(f"Connected to Retroarch {version.decode('ascii', errors='replace')} "
f"running {rom_name.decode('ascii', errors='replace')}")
return
except (BlockingIOError, TimeoutError, ConnectionResetError):
await asyncio.sleep(1.0)

259
Main.py
View File

@@ -30,49 +30,24 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
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)
@@ -103,93 +78,93 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
# This assertion method should not be necessary to run if we are not outputting any multidata.
if not args.skip_output:
AutoWorld.call_stage(world, "assert_generate")
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.worlds[player].options.start_inventory.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))
for item_name, count in getattr(world.worlds[player].options,
for item_name, count in getattr(multiworld.worlds[player].options,
"start_inventory_from_pool",
StartInventoryPool({})).value.items():
for _ in range(count):
world.push_precollected(world.create_item(item_name, player))
multiworld.push_precollected(multiworld.create_item(item_name, player))
# remove from_pool items also from early items handling, as starting is plenty early.
early = world.early_items[player].get(item_name, 0)
early = multiworld.early_items[player].get(item_name, 0)
if early:
world.early_items[player][item_name] = max(0, early-count)
multiworld.early_items[player][item_name] = max(0, early-count)
remaining_count = count-early
if remaining_count > 0:
local_early = world.early_local_items[player].get(item_name, 0)
local_early = multiworld.early_local_items[player].get(item_name, 0)
if local_early:
world.early_items[player][item_name] = max(0, local_early - remaining_count)
multiworld.early_items[player][item_name] = max(0, local_early - remaining_count)
del local_early
del early
logger.info('Creating World.')
AutoWorld.call_all(world, "create_regions")
logger.info('Creating MultiWorld.')
AutoWorld.call_all(multiworld, "create_regions")
logger.info('Creating Items.')
AutoWorld.call_all(world, "create_items")
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.worlds[player].options.non_local_items.value -= world.worlds[player].options.local_items.value
world.worlds[player].options.non_local_items.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])
AutoWorld.call_all(world, "set_rules")
AutoWorld.call_all(multiworld, "set_rules")
for player in world.player_ids:
exclusion_rules(world, player, world.worlds[player].options.exclude_locations.value)
world.worlds[player].options.priority_locations.value -= world.worlds[player].options.exclude_locations.value
for location_name in world.worlds[player].options.priority_locations.value:
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 = world.get_location(location_name, player)
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 world.worlds[player].location_name_to_id:
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 world.players > 1:
locality_rules(world)
if multiworld.players > 1:
locality_rules(multiworld)
else:
world.worlds[1].options.non_local_items.value = set()
world.worlds[1].options.local_items.value = set()
multiworld.worlds[1].options.non_local_items.value = set()
multiworld.worlds[1].options.local_items.value = set()
AutoWorld.call_all(world, "generate_basic")
AutoWorld.call_all(multiworld, "generate_basic")
# 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(world.worlds[player].options, "start_inventory_from_pool", None) for player in world.player_ids):
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(world.worlds[player].options,
player: getattr(multiworld.worlds[player].options,
"start_inventory_from_pool",
StartInventoryPool({})).value.copy()
for player in world.player_ids
for player in multiworld.player_ids
}
for player, items in depletion_pool.items():
player_world: AutoWorld.World = world.worlds[player]
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(world.itempool):
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(world.itempool[i+1:])
new_items.extend(multiworld.itempool[i+1:])
break
else:
new_items.append(item)
@@ -199,19 +174,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
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"{world.get_player_name(player)}"
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(world.itempool) == len(new_items), "Item Pool amounts should not change."
world.itempool[:] = new_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
@@ -246,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)
region = Region("Menu", group_id, multiworld, "ItemLink")
multiworld.regions.append(region)
locations = region.locations
for item in world.itempool:
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_)
@@ -263,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"]:
@@ -274,64 +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._all_state = None
if any(multiworld.item_links.values()):
multiworld._all_state = None
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)
AutoWorld.call_all(world, 'post_fill')
AutoWorld.call_all(multiworld, 'post_fill')
if world.players > 1 and not args.skip_prog_balancing:
balance_multiworld_progression(world)
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
if args.skip_output:
logger.info('Done. Skipped output/spoiler generation. Total Time: %s', time.perf_counter() - start)
return world
return multiworld
logger.info(f'Beginning output...')
outfilebase = 'AP_' + world.seed_name
outfilebase = 'AP_' + multiworld.seed_name
output = tempfile.TemporaryDirectory()
with output as temp_dir:
output_players = [player for player in world.player_ids if AutoWorld.World.generate_output.__code__
is not world.worlds[player].generate_output.__code__]
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(world.fulfills_accessibility)
check_accessibility_task = pool.submit(multiworld.fulfills_accessibility)
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
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".
output_file_futures.append(
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
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)
AutoWorld.call_all(multiworld, 'extend_hint_information', er_hint_data)
def write_multidata():
import NetUtils
@@ -340,38 +315,38 @@ 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: " \
@@ -381,18 +356,18 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
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.worlds[location.player].options.start_location_hints:
if location.name in multiworld.worlds[location.player].options.start_location_hints:
precollect_hint(location)
elif location.item.name in world.worlds[location.item.player].options.start_hints:
elif location.item.name in multiworld.worlds[location.item.player].options.start_hints:
precollect_hint(location)
elif any([location.item.name in world.worlds[player].options.start_hints
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]]]] = {}
@@ -400,7 +375,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
multidata = {
"slot_data": slot_data,
"slot_info": slot_info,
"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,
@@ -410,10 +385,10 @@ 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,
"datapackage": data_package,
}
AutoWorld.call_all(world, "modify_multidata", multidata)
AutoWorld.call_all(multiworld, "modify_multidata", multidata)
multidata = zlib.compress(pickle.dumps(multidata), 9)
@@ -423,7 +398,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
output_file_futures.append(pool.submit(write_multidata))
if not check_accessibility_task.result():
if not 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.")
@@ -436,12 +411,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:
@@ -449,4 +424,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

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

View File

@@ -586,7 +586,7 @@ class Context:
self.location_check_points = savedata["game_options"]["location_check_points"]
self.server_password = savedata["game_options"]["server_password"]
self.password = savedata["game_options"]["password"]
self.release_mode = savedata["game_options"].get("release_mode", savedata["game_options"].get("forfeit_mode", "goal"))
self.release_mode = savedata["game_options"]["release_mode"]
self.remaining_mode = savedata["game_options"]["remaining_mode"]
self.collect_mode = savedata["game_options"]["collect_mode"]
self.item_cheat = savedata["game_options"]["item_cheat"]
@@ -631,8 +631,6 @@ class Context:
def _set_options(self, server_options: dict):
for key, value in server_options.items():
if key == "forfeit_mode":
key = "release_mode"
data_type = self.simple_options.get(key, None)
if data_type is not None:
if value not in {False, True, None}: # some can be boolean OR text, such as password
@@ -656,7 +654,8 @@ class Context:
else:
return self.player_names[team, slot]
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False,
recipients: typing.Sequence[int] = None):
"""Send and remember hints."""
if only_new:
hints = [hint for hint in hints if hint not in self.hints[team, hint.finding_player]]
@@ -685,12 +684,13 @@ class Context:
for slot in new_hint_events:
self.on_new_hint(team, slot)
for slot, hint_data in concerns.items():
clients = self.clients[team].get(slot)
if not clients:
continue
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)]
for client in clients:
async_start(self.send_msgs(client, client_hints))
if recipients is None or slot in recipients:
clients = self.clients[team].get(slot)
if not clients:
continue
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)]
for client in clients:
async_start(self.send_msgs(client, client_hints))
# "events"
@@ -705,15 +705,18 @@ class Context:
self.save() # save goal completion flag
def on_new_hint(self, team: int, slot: int):
key: str = f"_read_hints_{team}_{slot}"
targets: typing.Set[Client] = set(self.stored_data_notification_clients[key])
if targets:
self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.hints[team, slot]}])
self.on_changed_hints(team, slot)
self.broadcast(self.clients[team][slot], [{
"cmd": "RoomUpdate",
"hint_points": get_slot_points(self, team, slot)
}])
def on_changed_hints(self, team: int, slot: int):
key: str = f"_read_hints_{team}_{slot}"
targets: typing.Set[Client] = set(self.stored_data_notification_clients[key])
if targets:
self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.hints[team, slot]}])
def on_client_status_change(self, team: int, slot: int):
key: str = f"_read_client_status_{team}_{slot}"
targets: typing.Set[Client] = set(self.stored_data_notification_clients[key])
@@ -973,7 +976,10 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
"hint_points": get_slot_points(ctx, team, slot),
"checked_locations": new_locations, # send back new checks only
}])
old_hints = ctx.hints[team, slot].copy()
ctx.recheck_hints(team, slot)
if old_hints != ctx.hints[team, slot]:
ctx.on_changed_hints(team, slot)
ctx.save()
@@ -1050,17 +1056,19 @@ def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bo
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, " \
f"did you mean {picks[0][0]}? ({picks[0][1]}% sure)"
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, did you mean {picks[0][0]}? ({picks[0][1]}% sure)"
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"Did you mean {picks[0][0]}? ({picks[0][1]}% sure)"
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)"
class CommandMeta(type):
@@ -1337,6 +1345,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
"Sorry, !remaining requires you to have beaten the game on this server")
return False
@mark_raw
def _cmd_missing(self, filter_text="") -> bool:
"""List all missing location checks from the server's perspective.
Can be given text, which will be used as filter."""
@@ -1346,7 +1355,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
if locations:
names = [self.ctx.location_names[location] for location in locations]
if filter_text:
names = [name for name in names if filter_text in name]
location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]]
if filter_text in location_groups: # location group name
names = [name for name in names if name in location_groups[filter_text]]
else:
names = [name for name in names if filter_text in name]
texts = [f'Missing: {name}' for name in names]
if filter_text:
texts.append(f"Found {len(locations)} missing location checks, displaying {len(names)} of them.")
@@ -1357,6 +1370,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output("No missing location checks found.")
return True
@mark_raw
def _cmd_checked(self, filter_text="") -> bool:
"""List all done location checks from the server's perspective.
Can be given text, which will be used as filter."""
@@ -1366,7 +1380,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
if locations:
names = [self.ctx.location_names[location] for location in locations]
if filter_text:
names = [name for name in names if filter_text in name]
location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]]
if filter_text in location_groups: # location group name
names = [name for name in names if name in location_groups[filter_text]]
else:
names = [name for name in names if filter_text in name]
texts = [f'Checked: {name}' for name in names]
if filter_text:
texts.append(f"Found {len(locations)} done location checks, displaying {len(names)} of them.")
@@ -1429,9 +1447,13 @@ class ClientMessageProcessor(CommonCommandProcessor):
hints = {hint.re_check(self.ctx, self.client.team) for hint in
self.ctx.hints[self.client.team, self.client.slot]}
self.ctx.hints[self.client.team, self.client.slot] = hints
self.ctx.notify_hints(self.client.team, list(hints))
self.ctx.notify_hints(self.client.team, list(hints), recipients=(self.client.slot,))
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
f"You have {points_available} points.")
if hints and Utils.version_tuple < (0, 5, 0):
self.output("It was recently changed, so that the above hints are only shown to you. "
"If you meant to alert another player of an above hint, "
"please let them know of the content or to run !hint themselves.")
return True
elif input_text.isnumeric():
@@ -1825,6 +1847,11 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus)
if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion
if new_status == ClientStatus.CLIENT_GOAL:
ctx.on_goal_achieved(client)
# if player has yet to ever connect to the server, they will not be in client_game_state
if all(player in ctx.client_game_state and ctx.client_game_state[player] == ClientStatus.CLIENT_GOAL
for player in ctx.player_names
if player[0] == client.team and player[1] != client.slot):
ctx.broadcast_text_all(f"Team #{client.team + 1} has completed all of their games! Congratulations!")
ctx.client_game_state[client.team, client.slot] = new_status
ctx.on_client_status_change(client.team, client.slot)
@@ -1878,7 +1905,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
@mark_raw
def _cmd_alias(self, player_name_then_alias_name):
"""Set a player's alias, by listing their base name and then their intended alias."""
player_name, alias_name = player_name_then_alias_name.split(" ", 1)
player_name, _, alias_name = player_name_then_alias_name.partition(" ")
player_name, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable:
for (team, slot), name in self.ctx.player_names.items():
@@ -1958,7 +1985,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
@mark_raw
def _cmd_forbid_release(self, player_name: str) -> bool:
""""Disallow the specified player from using the !release command."""
"""Disallow the specified player from using the !release command."""
player = self.resolve_player(player_name)
if player:
team, slot, name = player
@@ -2078,8 +2105,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
if full_name.isnumeric():
location, usable, response = int(full_name), True, None
elif self.ctx.location_names_for_game(game) is not None:
location, usable, response = get_intended_text(full_name, self.ctx.location_names_for_game(game))
elif game in self.ctx.all_location_and_group_names:
location, usable, response = get_intended_text(full_name, self.ctx.all_location_and_group_names[game])
else:
self.output("Can't look up location for unknown game. Hint for ID instead.")
return False
@@ -2087,6 +2114,11 @@ class ServerCommandProcessor(CommonCommandProcessor):
if usable:
if isinstance(location, int):
hints = collect_hint_location_id(self.ctx, team, slot, location)
elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]:
hints = []
for loc_name_from_group in self.ctx.location_name_groups[game][location]:
if loc_name_from_group in self.ctx.location_names_for_game(game):
hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group))
else:
hints = collect_hint_location_name(self.ctx, team, slot, location)
if hints:
@@ -2102,32 +2134,47 @@ class ServerCommandProcessor(CommonCommandProcessor):
self.output(response)
return False
def _cmd_option(self, option_name: str, option: str):
"""Set options for the server."""
attrtype = self.ctx.simple_options.get(option_name, None)
if attrtype:
if attrtype == bool:
def attrtype(input_text: str):
return input_text.lower() not in {"off", "0", "false", "none", "null", "no"}
elif attrtype == str and option_name.endswith("password"):
def attrtype(input_text: str):
if input_text.lower() in {"null", "none", '""', "''"}:
return None
return input_text
setattr(self.ctx, option_name, attrtype(option))
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
if option_name in {"release_mode", "remaining_mode", "collect_mode"}:
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
elif option_name in {"hint_cost", "location_check_points"}:
self.ctx.broadcast_all([{"cmd": "RoomUpdate", option_name: getattr(self.ctx, option_name)}])
return True
else:
known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items())
self.output(f"Unrecognized Option {option_name}, known: "
f"{', '.join(known)}")
def _cmd_option(self, option_name: str, option_value: str):
"""Set an option for the server."""
value_type = self.ctx.simple_options.get(option_name, None)
if not value_type:
known_options = (f"{option}: {option_type}" for option, option_type in self.ctx.simple_options.items())
self.output(f"Unrecognized option '{option_name}', known: {', '.join(known_options)}")
return False
if value_type == bool:
def value_type(input_text: str):
return input_text.lower() not in {"off", "0", "false", "none", "null", "no"}
elif value_type == str and option_name.endswith("password"):
def value_type(input_text: str):
return None if input_text.lower() in {"null", "none", '""', "''"} else input_text
elif value_type == str and option_name.endswith("mode"):
valid_values = {"goal", "enabled", "disabled"}
valid_values.update(("auto", "auto_enabled") if option_name != "remaining_mode" else [])
if option_value.lower() not in valid_values:
self.output(f"Unrecognized {option_name} value '{option_value}', known: {', '.join(valid_values)}")
return False
setattr(self.ctx, option_name, value_type(option_value))
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
if option_name in {"release_mode", "remaining_mode", "collect_mode"}:
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
elif option_name in {"hint_cost", "location_check_points"}:
self.ctx.broadcast_all([{"cmd": "RoomUpdate", option_name: getattr(self.ctx, option_name)}])
return True
def _cmd_datastore(self):
"""Debug Tool: list writable datastorage keys and approximate the size of their values with pickle."""
total: int = 0
texts = []
for key, value in self.ctx.stored_data.items():
size = len(pickle.dumps(value))
total += size
texts.append(f"Key: {key} | Size: {size}B")
texts.insert(0, f"Found {len(self.ctx.stored_data)} keys, "
f"approximately totaling {Utils.format_SI_prefix(total, power=1024)}B")
self.output("\n".join(texts))
async def console(ctx: Context):
import sys

View File

@@ -290,8 +290,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):

View File

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

@@ -1,19 +1,19 @@
from __future__ import annotations
import abc
import logging
from copy import deepcopy
from dataclasses import dataclass
import functools
import logging
import math
import numbers
import random
import typing
import enum
from copy import deepcopy
from dataclasses import dataclass
from schema import And, Optional, Or, Schema
from Utils import get_fuzzy_results
from Utils import get_fuzzy_results, is_iterable_except_str
if typing.TYPE_CHECKING:
from BaseClasses import PlandoOptions
@@ -21,6 +21,15 @@ if typing.TYPE_CHECKING:
import pathlib
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):
def __new__(mcs, name, bases, attrs):
options = attrs["options"] = {}
@@ -42,6 +51,11 @@ class AssembleOptions(abc.ABCMeta):
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
@@ -59,6 +73,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"
@@ -96,7 +111,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()
@@ -106,8 +122,9 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
supports_weighting = True
# 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]]
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.current_option_name})"
@@ -160,6 +177,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
@@ -180,9 +199,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)
@@ -356,7 +384,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
@@ -598,7 +627,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]
@@ -727,7 +756,7 @@ class SpecialRange(NamedRange):
"Consider switching to NamedRange, which supports all use-cases of SpecialRange, and more. In "
"NamedRange, range_start specifies the lower end of the regular range, while special values can be "
"placed anywhere (below, inside, or above the regular range).")
return super().__new__(cls, value)
return super().__new__(cls)
@classmethod
def weighted_range(cls, text) -> Range:
@@ -765,7 +794,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
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)
@@ -802,7 +831,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
default: typing.Dict[str, typing.Any] = {}
default = {}
supports_weighting = False
def __init__(self, value: typing.Dict[str, typing.Any]):
@@ -843,11 +872,11 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
# 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: typing.List[typing.Any] = []
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
@@ -856,7 +885,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))
@@ -869,7 +898,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]):
@@ -882,7 +911,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))
@@ -932,7 +961,7 @@ class OptionsMetaProperty(type):
bases: typing.Tuple[type, ...],
attrs: typing.Dict[str, typing.Any]) -> "OptionsMetaProperty":
for attr_type in attrs.values():
assert not isinstance(attr_type, AssembleOptions),\
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)
@@ -1096,6 +1125,18 @@ 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)
class Removed(FreeText):
"""This Option has been Removed."""
default = ""
visibility = Visibility.none
def __init__(self, value: str):
if value:
raise Exception("Option removed, please update your options file.")
super().__init__(value)
@dataclass
@@ -1110,6 +1151,11 @@ class PerGameCommonOptions(CommonOptions):
item_links: ItemLinks
@dataclass
class DeathLinkMixin:
death_link: DeathLink
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True):
import os
@@ -1148,7 +1194,10 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
for game_name, world in AutoWorldRegister.world_types.items():
if not world.hidden or generate_hidden:
all_options: typing.Dict[str, AssembleOptions] = world.options_dataclass.type_hints
all_options: typing.Dict[str, AssembleOptions] = {
option_name: option for option_name, option in world.options_dataclass.type_hints.items()
if option.visibility & Visibility.template
}
with open(local_path("data", "options.yaml")) as f:
file_data = f.read()

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

@@ -25,7 +25,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
@@ -59,6 +59,12 @@ Currently, the following games are supported:
* 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
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
@@ -80,9 +86,9 @@ We recognize that there is a strong community of incredibly smart people that ha
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.
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, or AppImage for Linux-based systems.
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).
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.

View File

@@ -85,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
@@ -564,16 +565,12 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int,
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
try:
for address, data in write_list:
while data:
# Divide the write into packets of 256 bytes.
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
if ctx.snes_socket is not None:
await ctx.snes_socket.send(dumps(PutAddress_Request))
await ctx.snes_socket.send(data[:256])
address += 256
data = data[256:]
else:
snes_logger.warning(f"Could not send data to SNES: {data}")
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
if ctx.snes_socket is not None:
await ctx.snes_socket.send(dumps(PutAddress_Request))
await ctx.snes_socket.send(data)
else:
snes_logger.warning(f"Could not send data to SNES: {data}")
except ConnectionClosed:
return False

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import ModuleUpdate
ModuleUpdate.update()
from worlds.sc2wol.Client import launch
from worlds.sc2.Client import launch
import Utils
if __name__ == "__main__":

View File

@@ -19,14 +19,13 @@ import warnings
from argparse import Namespace
from settings import Settings, get_settings
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
from yaml import load, load_all, dump, SafeLoader
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
@@ -47,7 +46,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self)
__version__ = "0.4.4"
__version__ = "0.4.6"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
@@ -226,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)
@@ -617,6 +619,8 @@ def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: ty
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]], 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
@@ -714,7 +718,7 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
import ctypes
style = 0x10 if error else 0x0
return ctypes.windll.user32.MessageBoxW(0, text, title, style)
# fall back to tk
try:
import tkinter
@@ -871,8 +875,8 @@ def visualize_regions(root_region: Region, file_name: str, *,
Example usage in Main code:
from Utils import visualize_regions
for player in world.player_ids:
visualize_regions(world.get_region("Menu", player), f"{world.get_out_file_name_base(player)}.puml")
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
@@ -968,3 +972,10 @@ class RepeatableChain:
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

@@ -23,7 +23,6 @@ def get_app():
from WebHostLib import register, cache, app as raw_app
from WebHostLib.models import db
register()
app = raw_app
if os.path.exists(configpath) and not app.config["TESTING"]:
import yaml
@@ -34,6 +33,7 @@ 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)

View File

@@ -38,6 +38,7 @@ 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
app.config["CHECK_DATA_PACKAGE_CHECKSUM_ON_STARTUP"] = False
# waitress uses one thread for I/O, these are for processing of views that then get sent
# archipelago.gg uses gunicorn + nginx; ignoring this option
@@ -51,6 +52,7 @@ app.config["PONY"] = {
app.config["MAX_ROLL"] = 20
app.config["CACHE_TYPE"] = "SimpleCache"
app.config["HOST_ADDRESS"] = ""
app.config["ASSET_RIGHTS"] = False
cache = Cache()
Compress(app)
@@ -82,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
app.register_blueprint(api.api_endpoints)

View File

@@ -2,8 +2,9 @@
from typing import List, Tuple
from uuid import UUID
from flask import Blueprint, abort
from flask import Blueprint, abort, url_for
import worlds.Files
from .. import cache
from ..models import Room, Seed
@@ -21,12 +22,30 @@ 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,
}

View File

@@ -6,6 +6,7 @@ import multiprocessing
import threading
import time
import typing
from uuid import UUID
from datetime import timedelta, datetime
from pony.orm import db_session, select, commit
@@ -62,7 +63,32 @@ def autohost(config: dict):
def keep_running():
try:
with Locker("autohost"):
# 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.")
if config["CHECK_DATA_PACKAGE_CHECKSUM_ON_STARTUP"]:
logging.info("Checking for Data Package Checksum integrity")
from worlds.AutoWorld import data_package_checksum
with db_session:
for data_package in GameDataPackage.select():
data = restricted_loads(data_package.data)
if data["checksum"] != data_package.checksum:
logging.warning(f"Deleting mismatching checksum datapackage {data_package.checksum}.")
data_package.delete()
continue
del data["checksum"]
if data_package.checksum != data_package_checksum(data):
logging.warning(f"Deleting mismatching checksum datapackage {data_package.checksum}.")
data_package.delete()
run_guardian()
logging.info("Starting Autohost.")
while 1:
time.sleep(0.1)
with db_session:
@@ -191,6 +217,6 @@ def run_guardian():
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, GameDataPackage
from .customserver import run_server_process, get_static_server_data
from .generate import gen_game

View File

@@ -28,7 +28,7 @@ def check():
results, _ = roll_options(options)
if len(options) > 1:
# offer combined file back
combined_yaml = "---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}"
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:
@@ -108,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 options 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

@@ -49,12 +49,6 @@ def weighted_options():
return render_template("weighted-options.html")
# TODO for back compat. remove around 0.4.5
@app.route("/games/<string:game>/player-settings")
def player_settings(game: str):
return redirect(url_for("player_options", game=game), 301)
# Player options pages
@app.route("/games/<string:game>/player-options")
@cache.cached()

View File

@@ -45,7 +45,15 @@ def create():
}
game_options = {}
visible: typing.Set[str] = set()
visible_weighted: typing.Set[str] = set()
for option_name, option in all_options.items():
if option.visibility & Options.Visibility.simple_ui:
visible.add(option_name)
if option.visibility & Options.Visibility.complex_ui:
visible_weighted.add(option_name)
if option_name in handled_in_js:
pass
@@ -116,8 +124,6 @@ def create():
else:
logging.debug(f"{option} not exported to Web Options.")
player_options["gameOptions"] = game_options
player_options["presetOptions"] = {}
for preset_name, preset in world.web.options_presets.items():
player_options["presetOptions"][preset_name] = {}
@@ -156,12 +162,23 @@ def create():
os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True)
filtered_player_options = player_options
filtered_player_options["gameOptions"] = {
option_name: option_data for option_name, option_data in game_options.items()
if option_name in visible
}
with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f:
json.dump(player_options, f, indent=2, separators=(',', ': '))
json.dump(filtered_player_options, f, indent=2, separators=(',', ': '))
filtered_player_options["gameOptions"] = {
option_name: option_data for option_name, option_data in game_options.items()
if option_name in visible_weighted
}
if not world.hidden and world.web.options_page is True:
# Add the random option to Choice, TextChoice, and Toggle options
for option in game_options.values():
for option in filtered_player_options["gameOptions"].values():
if option["type"] == "select":
option["options"].append({"name": "Random", "value": "random"})
@@ -170,7 +187,7 @@ def create():
weighted_options["baseOptions"]["game"][game_name] = 0
weighted_options["games"][game_name] = {
"gameSettings": game_options,
"gameSettings": filtered_player_options["gameOptions"],
"gameItems": tuple(world.item_names),
"gameItemGroups": [
group for group in world.item_name_groups.keys() if group != "Everything"

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

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

@@ -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,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: /

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

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

@@ -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,112 +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: 710px;
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%);
background-color: black;
}
#inventory-table img.acquired{
filter: none;
background-color: black;
}
#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: 710px;
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

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

View File

@@ -22,7 +22,7 @@
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
<tr>
<td>{{ patch.player_id }}</td>
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
<td>{{ patch.game }}</td>
<td>
{% if patch.data %}
@@ -47,9 +47,6 @@
{% elif patch.game | supports_apdeltapatch %}
<a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}" download>
Download Patch File...</a>
{% elif patch.game == "Dark Souls III" %}
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
Download JSON File...</a>
{% elif patch.game == "Final Fantasy Mystic Quest" %}
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
Download APMQ File...</a>

View File

@@ -6,52 +6,42 @@
{% endblock %}
{# List all tracker-relevant icons. Format: (Name, Image URL) #}
{%- set icons = {
"Blue Shield": "https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
"Red Shield": "https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
"Mirror Shield": "https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png",
"Fighter Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920",
"Master Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920",
"Tempered Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920",
"Golden Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920",
"Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c",
"Silver Bow": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920",
"Green Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920",
"Blue Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920",
"Red Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920",
"Power Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920",
"Titan Mitts": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
"Progressive Sword": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725",
"Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9",
"Progressive Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
"Flippers": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920",
"Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e",
"Progressive Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed",
"Blue Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e",
"Red Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400",
"Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b",
"Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59",
"Magic Powder": "https://www.zeldadungeon.net/wiki/images/thumb/6/62/MagicPowder-ALttP-Sprite.png/86px-MagicPowder-ALttP-Sprite.png",
"Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0",
"Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc",
"Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26",
"Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5",
"Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879",
"Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce",
"Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500",
"Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05",
"Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390",
"Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6",
"Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744",
"Bottle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b",
"Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943",
"Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54",
"Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832",
"Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc",
"Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48",
{% set icons = {
"Blue Shield": "https://www.zeldadungeon.net/wiki/images/thumb/c/c3/FightersShield-ALttP-Sprite.png/100px-FightersShield-ALttP-Sprite.png",
"Red Shield": "https://www.zeldadungeon.net/wiki/images/thumb/9/9e/FireShield-ALttP-Sprite.png/111px-FireShield-ALttP-Sprite.png",
"Mirror Shield": "https://www.zeldadungeon.net/wiki/images/thumb/e/e3/MirrorShield-ALttP-Sprite.png/105px-MirrorShield-ALttP-Sprite.png",
"Progressive Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/c/cc/ALttP_Master_Sword_Sprite.png",
"Progressive Bow": "https://www.zeldadungeon.net/wiki/images/thumb/8/8c/BowArrows-ALttP-Sprite.png/120px-BowArrows-ALttP-Sprite.png",
"Progressive Glove": "https://www.zeldadungeon.net/wiki/images/thumb/4/41/PowerGlove-ALttP-Sprite.png/105px-PowerGlove-ALttP-Sprite.png",
"Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png",
"Flippers": "https://www.zeldadungeon.net/wiki/images/thumb/b/bc/ZoraFlippers-ALttP-Sprite.png/112px-ZoraFlippers-ALttP-Sprite.png",
"Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png",
"Blue Boomerang": "https://www.zeldadungeon.net/wiki/images/thumb/f/f0/Boomerang-ALttP-Sprite.png/86px-Boomerang-ALttP-Sprite.png",
"Red Boomerang": "https://www.zeldadungeon.net/wiki/images/thumb/3/3c/MagicalBoomerang-ALttP-Sprite.png/86px-MagicalBoomerang-ALttP-Sprite.png",
"Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png",
"Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png",
"Magic Powder": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png",
"Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png",
"Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png",
"Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png",
"Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png",
"Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png",
"Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png",
"Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png",
"Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png",
"Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png",
"Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png",
"Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png",
"Bottles": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png",
"Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png",
"Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png",
"Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png",
"Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png",
"Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png",
"Triforce Piece": "https://www.zeldadungeon.net/wiki/images/thumb/5/54/Triforce_Fragment_-_BS_Zelda.png/62px-Triforce_Fragment_-_BS_Zelda.png",
"Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e",
"Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d",
"Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/38/ALttP_Bomb_Sprite.png",
"Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png",
"Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png",
"Chest": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda",
"Light World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6",
"Dark World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc",
@@ -68,33 +58,93 @@
"Misery Mire": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8",
"Turtle Rock": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be",
"Ganons Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74",
} -%}
} %}
{% set inventory_order = [
"Progressive Sword",
"Progressive Bow",
"Blue Boomerang",
"Red Boomerang",
"Hookshot",
"Bombs",
"Mushroom",
"Magic Powder",
"Fire Rod",
"Ice Rod",
"Bombos",
"Ether",
"Quake",
"Lamp",
"Hammer",
"Flute",
"Bug Catching Net",
"Book of Mudora",
"Cane of Somaria",
"Cane of Byrna",
"Cape",
"Magic Mirror",
"Shovel",
"Pegasus Boots",
"Flippers",
"Progressive Glove",
"Moon Pearl",
"Bottles",
"Triforce Piece",
"Triforce",
] %}
{% set dungeon_keys = {
"Hyrule Castle": ("Small Key (Hyrule Castle)", "Big Key (Hyrule Castle)"),
"Agahnims Tower": ("Small Key (Agahnims Tower)", "Big Key (Agahnims Tower)"),
"Eastern Palace": ("Small Key (Eastern Palace)", "Big Key (Eastern Palace)"),
"Desert Palace": ("Small Key (Desert Palace)", "Big Key (Desert Palace)"),
"Tower of Hera": ("Small Key (Tower of Hera)", "Big Key (Tower of Hera)"),
"Palace of Darkness": ("Small Key (Palace of Darkness)", "Big Key (Palace of Darkness)"),
"Thieves Town": ("Small Key (Thieves Town)", "Big Key (Thieves Town)"),
"Skull Woods": ("Small Key (Skull Woods)", "Big Key (Skull Woods)"),
"Swamp Palace": ("Small Key (Swamp Palace)", "Big Key (Swamp Palace)"),
"Ice Palace": ("Small Key (Ice Palace)", "Big Key (Ice Palace)"),
"Misery Mire": ("Small Key (Misery Mire)", "Big Key (Misery Mire)"),
"Turtle Rock": ("Small Key (Turtle Rock)", "Big Key (Turtle Rock)"),
"Ganons Tower": ("Small Key (Ganons Tower)", "Big Key (Ganons Tower)"),
} %}
{% set multi_items = [
"Progressive Sword",
"Progressive Glove",
"Progressive Bow",
"Bottles",
"Triforce Piece",
] %}
{%- block custom_table_headers %}
{#- macro that creates a table header with display name and image -#}
{%- macro make_header(name, img_src) %}
<th class="center-column">
<img height="24" src="{{ img_src }}" title="{{ name }}" alt="{{ name }}" />
</th>
{% endmacro -%}
{#- call the macro to build the table header -#}
{%- for name in tracking_names %}
{%- if name in icons -%}
{#- macro that creates a table header with display name and image -#}
{%- macro make_header(name, img_src) %}
<th class="center-column">
<img class="icon-sprite" src="{{ icons[name] }}" alt="{{ name | e }}" title="{{ name | e }}" />
<img height="24" src="{{ img_src }}" title="{{ name }}" alt="{{ name }}">
</th>
{%- endif %}
{% endfor -%}
{% endmacro -%}
{#- call the macro to build the table header -#}
{%- for item in inventory_order %}
{%- if item in icons -%}
<th class="center-column">
<img class="icon-sprite" src="{{ icons[item] }}" alt="{{ item | e }}" title="{{ item | e }}">
</th>
{%- endif %}
{% endfor -%}
{% endblock %}
{# build each row of custom entries #}
{% block custom_table_row scoped %}
{%- for id in tracking_ids -%}
{# {{ checks }}#}
{%- if inventories[(team, player)][id] -%}
{%- for item in inventory_order -%}
{%- if inventories[(team, player)][item] -%}
<td class="center-column item-acquired">
{% if id in multi_items %}{{ inventories[(team, player)][id] }}{% else %}✔️{% endif %}
{% if item in multi_items %}
{{ inventories[(team, player)][item] }}
{% else %}
✔️
{% endif %}
</td>
{%- else -%}
<td></td>
@@ -104,102 +154,95 @@
{% block custom_tables %}
{% for team, _ in total_team_locations.items() %}
<div class="table-wrapper">
<table id="area-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="icon-sprite" src="{{ icons[area] }}" alt="{{ area }}" title="{{ 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 %}
{% for team in total_team_locations %}
<div class="table-wrapper">
<table class="table non-unique-item-table">
<thead>
<tr>
<th rowspan="2">#</th>
<th rowspan="2">Name</th>
{% for region in known_regions %}
{% set colspan = 1 %}
{% if region == "Agahnims Tower" %}
{% set colspan = 2 %}
{% elif region in dungeon_keys %}
{% set colspan = 3 %}
{% endif %}
{% if region in icons %}
<th colspan="{{ colspan }}" class="center-column upper-row">
<img class="icon-sprite" src="{{ icons[region] }}" alt="{{ region }}" title="{{ region }}">
</th>
{% else %}
<th colspan="{{ colspan }}" class="center-column">{{ region }}</th>
{% endif %}
{% endfor %}
<th class="center-column">Total</th>
</tr>
<tr>
{% for region in known_regions %}
<th class="center-column lower-row fraction">
<img class="icon-sprite" src="{{ icons["Chest"] }}" alt="Checks" title="Checks Complete">
</th>
{% if region in dungeon_keys %}
<th class="center-column lower-row number">
<img class="icon-sprite" src="{{ icons["Small Key"] }}" alt="Small Key" title="Small Keys">
</th>
{# Special check just for Agahnims Tower, which has no big keys. #}
{% if region != "Agahnims Tower" %}
<th class="center-column lower-row number">
<img class="icon-sprite" src="{{ icons["Big Key"] }}" alt="Big Key" title="Big Keys">
</th>
{% endif %}
{% endif %}
{% endfor %}
{# For "total" checks #}
<th class="center-column lower-row fraction">
<img class="icon-sprite" src="{{ icons["Chest"] }}" alt="Checks" title="Checks Complete">
<img class="icon-sprite" src="{{ icons["Chest"] }}" alt="Checks" title="Total Checks Complete">
</th>
{% if area in key_locations %}
<th class="center-column lower-row number">
<img class="icon-sprite" src="{{ icons["Small Key"] }}" alt="Small Key" title="Small Keys">
</th>
{% endif %}
{% if area in big_key_locations %}
<th class="center-column lower-row number">
<img class="icon-sprite" src="{{ icons["Big Key"] }}" alt="Big Key" title="Big Keys">
</th>
{%- endif -%}
{%- endfor -%}
</tr>
</thead>
<tbody>
{%- for (checks_team, player), area_checks in checks_done.items() if games[(team, player)] == current_tracker and team == checks_team -%}
<tr>
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
tracked_team=team, tracked_player=player)}}">{{ player }}</a></td>
<td>{{ player_names_with_alias[(team, player)] | e }}</td>
{%- for area in ordered_areas -%}
{% if (team, player) in checks_in_area and area in checks_in_area[(team, player)] %}
{%- set checks_done = area_checks[area] -%}
{%- set checks_total = checks_in_area[(team, player)][area] -%}
{%- if checks_done == checks_total -%}
</tr>
</thead>
<tbody>
{% for (player_team, player), player_regions in regions.items() if team == player_team %}
<tr>
<td>
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">
{{ player }}
</a>
</td>
<td>{{ player_names_with_alias[(team, player)] | e }}</td>
{% for region, counts in player_regions.items() %}
<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">{{ inventories[(team, player)][small_key_ids[area]] }}</td>
{%- endif -%}
{%- if area in big_key_locations -%}
<td class="center-column">{% if inventories[(team, player)][big_key_ids[area]] %}✔️{% endif %}</td>
{%- endif -%}
{% else %}
<td class="center-column"></td>
{%- if area in key_locations -%}
<td class="center-column"></td>
{%- endif -%}
{%- if area in big_key_locations -%}
<td class="center-column"></td>
{%- endif -%}
{% endif %}
{%- endfor -%}
{{ counts.checked }}/{{ counts.total }}
</td>
<td class="center-column">
{% set location_count = locations[(team, player)] | length %}
{%- if locations[(team, player)] | length > 0 -%}
{% set percentage_of_completion = locations_complete[(team, player)] / location_count * 100 %}
{{ "{0:.2f}".format(percentage_of_completion) }}
{%- else -%}
100.00
{%- endif -%}
</td>
{% if region in dungeon_keys %}
<td class="center-column">
{{ inventories[(team, player)][dungeon_keys[region][0]] }}
</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>
{# Special check just for Agahnims Tower, which has no big keys. #}
{% if region != "Agahnims Tower" %}
<td class="center-column">
{% if inventories[(team, player)][dungeon_keys[region][1]] %}
✔️
{% endif %}
</td>
{% endif %}
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
{% endblock %}

View File

@@ -18,7 +18,7 @@
<br /><br />
To start playing a game, you'll first need to <a href="/generate">generate a randomized game</a>.
You'll need to upload either a config file or a zip file containing one more config files.
You'll need to upload one or more config files (YAMLs) or a zip file containing one or more config files.
<br /><br />
If you have already generated a game and just need to host it, this site can<br />

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