Compare commits

...

105 Commits

Author SHA1 Message Date
Chris Wilson
9f5fceba2d Merge branch 'main' into player-tracker 2022-09-24 18:35:59 -04:00
CaitSith2
813ee5ee3b Factorio: Add explicit support for factory-levels mod. (#1050)
* Factorio: Add explicit support for factory-levels mod.

* Fix inconsistent space/tabs
2022-09-24 02:43:00 -07:00
Fabian Dill
be1158ad78 Windows: update VC Redistributable to 14.32.31332 from 14.29.30037 2022-09-22 08:46:48 +02:00
black-sliver
6d5ddf3cad MultiServer: allow using IDs for hints 2022-09-20 18:38:31 +02:00
black-sliver
809bda02d1 Test: item/location name must not be numeric 2022-09-20 18:38:16 +02:00
black-sliver
2d5ec6ce22 Doc: item/location name must not be numeric 2022-09-20 18:38:16 +02:00
black-sliver
a95d0ce9ef Doc: clarify requirements.txt in world api.md 2022-09-20 09:48:30 +02:00
alwaysintreble
267d9234e5 core: fix options with "random" as default value not generating (#1033)
* core: fix options with "random" as default value not generating

when option is missing from the player yaml,

Using this in #893 and tested there.

* remove if

* OptionSets default to frozenset so handle that

* range had some specific instances of assuming default as a valid value so change this here to call the from_any

* isinstance instead of type

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

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-19 22:40:15 +02:00
black-sliver
4686881566 WebHost: CustomServer: use defaultdicts
also change non_hintable to defaultdict in MultiServer and add some typing
2022-09-19 01:20:36 +02:00
SoldierofOrder
101dab0ea4 SC2: Add helpful feedback when failing to locate SC2 (#1032)
* SC2: The client now throws a descriptive error when ExecuteInfo.txt exists but is empty, and offers more helpful suggestions when the file doesn't exist.

* SC2: Replaced the new RuntimeError with a warning in the logger to keep things consistent.

* Removed communism

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

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-18 16:48:36 +02:00
Fabian Dill
c2d69cb05e Core: add generic interface to add ER data to hints (#1014) 2022-09-18 14:30:43 +02:00
black-sliver
58f66e0f42 autoworld: don't load files/folders starting with '.' (#1030)
* autoworld: don't load files/folders starting with '.'

The imports fail if the folder has a '.' in the name, with a somewhat obscure error, and adding a '.' in front of it is what a linux user might expect to use when disabling a world temporarily.

* autoworld: use tuple to filter .* and _*
2022-09-18 13:02:05 +02:00
Fabian Dill
0215e1fa28 SC2: always show uncollected locations (#1007) 2022-09-18 12:40:35 +02:00
black-sliver
1c0a93acad doc: update use of relative/absolute imports
it matters for apworlds to function
2022-09-18 10:22:17 +02:00
NewSoupVi
4fcde135e5 The Witness: Renaming, Options, Logic Fixes (#1000)
Fixes to postgame detection for "shuffle_postgame"
Renamed many locations to be symbol-independent ("Outside Tutorial Dots Introduction" becomes "Outside Tutorial Shed Row"). This is to set up future alternate modes, like Sigma Expert, which use completely different symbols.
Renamed most door items to be shorter, more consistent, and less... stupid. ("Bunker Bunker Entry Door" -> "Bunker Entry")
Removed "shuffle_uncommon"
Many logic fixes
2022-09-18 04:20:59 +02:00
alwaysintreble
332dde154f core: new freetext and textchoice options (#728)
* add freetext and freetextchoice options

* fix textchoice. create plando_bosses bool so worlds can check if boss plando is enabled

* remove strange unneccessary \ escapes

* lttp: rip boss plando out of core

* fix broken text methods so they read the data correctly

* revert `None` key in boss_shuffle_options. fix failing tests

* lttp: rewrite boss plando

* lttp: rewrite boss shuffle

* add generic verification step and allow options to set a plando module

* add default typing to plando_options set

* use PlandoSettings intflag for lttp boss plando

* fix plandosettings boss flag check

* minor lttp init cleanup

* make suggested changes. account for "random" existing within plando boss options

* override eq operator

* Please document me!

* Forgot to mention it supports plando

* remove auto_display_name

* Throw warning alerting user to which shuffle is being used if plando is off. Set the remaining boss shuffle in init and boss placement cleanup

* move the convoluted string matching to `from_text`

* remove unneccessary text lowering and actually turn off plando option when it's disabled

* typing

* strong typing for verify method and reorder

* typing is your friend

* log warning correctly

* 3.8 support :(

* also list apparently

* rip out old boss shuffle spoiler code

* verification step for plando bosses and locations

* update plando guide to reference new supported behavior

* empty string is not `None`. remove unneccessary error throw

* Fix bad ordering

* validate boss_shuffle only contains a normal boss option at the end

* get random choice from a list dummy

* >:(

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

* minor textchoice cleanup

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-17 02:55:33 +02:00
black-sliver
8d51205e8f Alttp: only check item.type for own items with retro_cave 2022-09-17 02:25:09 +02:00
black-sliver
ff05e9d7d5 MultiServer: produce nicer output ...
... for headless and when cancelling the file open dialog
2022-09-17 02:24:51 +02:00
N00byKing
516a52c041 sm64ex: Fix WDW 1Up Block Logic 2022-09-17 02:13:32 +02:00
Alchav
9daa64741b New, smarter fast_fill function (#646)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2022-09-17 02:06:25 +02:00
Fabian Dill
af11fa5150 Core: auto alias (#1022)
* Test: check that default templates can be parsed into Option objects
2022-09-16 00:32:30 +02:00
TheBigSalarius
156e9e0e43 FF1: Throw exception for Noverworld 2022-09-12 03:48:07 +02:00
Fabian Dill
ef46979bd8 SC2: fix client freezing on exit while SC2 is running (#1011) 2022-09-12 02:08:33 +02:00
Fabian Dill
b2aa251c47 SC2: fix bitflag overflow when multiple instances of an Item are acquired (#1008) 2022-09-12 01:51:25 +02:00
Fabian Dill
e204a0fce6 Subnautica: fix missed item and correct other item pool counts to fit it 2022-09-12 01:44:10 +02:00
TheBigSalarius
bb386d3bd7 FF1: fix FF1Client messaging and scoped lua messaging with printjson
Corrects the issue causing the client and lua messaging not displaying properly after the printjson changes
2022-09-12 01:19:51 +02:00
Fabian Dill
88a225764a FF1: fix printjson 2022-09-12 01:19:51 +02:00
espeon65536
99d2caa57d ALttP: remove link_palettes option (#1004)
* ALttP: remove link_palettes option
It doesn't work anyway so better to have it not visible.
2022-09-07 20:16:32 +02:00
lordlou
ade82e3d60 SM: varia tracker fix (#1006) 2022-09-06 19:56:23 +02:00
Fabian Dill
7c04e7e06f MultiServer: save goal completion flag 2022-09-05 22:11:26 +02:00
Fabian Dill
baf51e5959 SC2: fix Launching Mission: text pulling the unshuffled ID. (#1001)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-05 21:09:03 +02:00
toasterparty
8aad75ed23 Tests: Check for Holes in the Item Pool (#992)
* test for holes in the item pool

* Update test/general/TestItems.py

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

* Update test/general/TestItems.py

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

* Update test/general/TestItems.py

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

* Update test/general/TestItems.py

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

Co-authored-by: alwaysintreble <mmmcheese158@gmail.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-05 10:02:40 +02:00
black-sliver
1792b66b3a CI: fix automated builds, update SNI and Enemizer
* Launcher.py always running ModuleUpdate breaks setup.py build --yes
* Use env variables in github workflows
* Update SNI and Enemizer versions in github workflows
* Minor cleanup in workflows
* Silence pycharm warning in Launcher.py
2022-09-05 09:23:08 +02:00
wildham0
5e8ac74b2a FFR: fix NoOverworld mode (#999)
* Add Sigil/Mark to item list
2022-09-05 09:21:00 +02:00
PoryGone
2acc129381 SA2B: Fix typo in doc string (#997) 2022-09-04 14:45:45 +02:00
lordlou
0cbb3c2839 SMZ3: data package fix (#996) 2022-09-03 23:52:09 +02:00
espeon65536
539d2e80f1 OoT: prevent glitched + mq dungeons
this combo is not allowed on main ootr, so we won't have it here either
2022-09-03 21:26:31 +02:00
lordlou
f9e28004a0 SMZ3: item link gt fill fix (#995) 2022-09-03 21:25:55 +02:00
Sunny Bat
b7cfcc9272 New features and fixes for Raft (#984)
* Add DeathLink, small logic changes

* Fix generation, rules, use bool for slotData

* Add more island options

* Update Shovel-related logic

* Update docs
2022-09-03 21:25:04 +02:00
Fabian Dill
4b6d46fd74 Core: update modules 2022-09-03 09:55:47 +02:00
Alchav
b45d8bf221 Patch: Save patch file extension in archipelago.json (#968) 2022-09-02 23:37:37 +02:00
Fabian Dill
f7d107fc0c Subnautica: add some more missed aggressive creatures 2022-09-02 09:06:33 +02:00
alwaysintreble
b14d694e1e templates: fix bug report label 2022-09-01 22:33:22 +02:00
skrawpie
8d2333006a Minecraft: Added shuffled recipe list to en_Minecraft.md (#980)
Co-authored-by: KonoTyran <Kono.Tyran@gmail.com>
2022-09-01 21:26:04 +02:00
Fabian Dill
e413619c26 Tests: verify that a world doesn't use the same ID multiple times (#985) 2022-09-01 21:25:06 +02:00
Yussur Mustafa Oraji
03f66a922d sm64ex: Fix a Location (#979) 2022-09-01 21:21:53 +02:00
black-sliver
b115bdafe7 CI/Doc: Use pytest subtests (#986)
* CI/Doc: use pytest-subtests

* CI: clean up pip installs a bit

* make lint and unittests install the same stuff
* make sure to install wheel, which is a recommended (not required) dependency for everything pip
2022-09-01 09:30:28 +02:00
lordlou
0444fdc379 SM: wasteland ap (#983) 2022-09-01 02:20:30 +02:00
Fabian Dill
c617bba959 SC2: client revamp (#967)
SC2 client now relies almost entirely on the map file and server for the locations and just facilitates them, should make it significantly more resilient to objectives being added or removed


* SC2: fix client crash on printjson messages with more [ than ]

* SC2: move text to queue, that actually clears memory

* SC2: Announce which mission is being loaded


Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-08-31 20:55:15 +02:00
lordlou
8da1cfeeb7 SM: remove events from data package (#973) 2022-08-31 06:14:17 +02:00
black-sliver
fcfc2c2e10 WebHost: fix local_path on python 3.8 (#981)
* WebHost: fix local_path on python 3.8

`__file__` is relative in 3.8, so `os.path.dirname(__file__)` ends up being an empty string breaking calls to `local_path()` (without arguments)

* WebHost: add comment to local_path override
2022-08-31 00:10:18 +02:00
espeon65536
a753905ee4 OoT bug fixes (#955)
* OoT: fix shop patching crash due to Item changes

* OoT: more informative failure in triforce piece replacement

* OoT: in triforce hunt, remove ganon BK from pool and lock the door

* OoT: no longer store trap information on the item
2022-08-30 20:54:40 +02:00
strotlog
2a7babce68 SM+SMZ3: don't abandon checks that happen while disconnected from AP (#946) 2022-08-30 17:16:21 +02:00
Fabian Dill
60d1a27079 Subnautica: revamp aggressive creature scans (#966)
* add forgotten aggressive creatures
* fix logic requirements
* added option to opt out of aggressive creature scans
2022-08-30 17:14:34 +02:00
Fabian Dill
4a2a184db1 Core: remove game-specific arguments from Generate (#971)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-08-30 17:12:33 +02:00
alwaysintreble
45fb735320 Clients: allow games without datapackage (#978) 2022-08-30 00:16:13 +02:00
PoryGone
3eb9e7050f DKC3: Fix Wrinkly Softlock (#963) 2022-08-29 20:04:02 +02:00
CaitSith2
26aed9351e Factorio: Fix a bug with single craft free samples. (#974) 2022-08-29 05:58:26 +02:00
Fabian Dill
b1ffbc49c9 LttPAdjuster: fix GUI for invalid sprite files (#885)
* LttPAdjuster: ignore invalid sprite files

* LttPAdjuster: ignore .gitignore in sprites

* LttPAdjuster: log and show message for invalid sprites

* Alttp: set sprite.valid to False for bad zspr and apsprite ...

... when throwing exceptions

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-08-28 18:30:19 +02:00
Fabian Dill
6d6111de2a Launcher: add ModuleUpdate 2022-08-27 11:13:33 +02:00
Fabian Dill
cc8ce32c61 Options: fix corner case where Toggle.value and Toggle.__int__ would be bool
Which lead to a connect failure in Raft
2022-08-27 11:12:28 +02:00
strotlog
4c94bb0ad5 WebHost: sort game list case-insensitively again 2022-08-26 18:20:37 +02:00
strotlog
af19180ff0 SM: Fix rolling saves, add SRAM features
- fix receiving items in an old save (issue #855) by moving receive queue's read pointer to a per-saveslot value
- clear SRAM over $70:2000, and invalidate save data, when booting a new seed number for the first time
- copy important ROM data to SRAM so future clients don't have to read ROM
2022-08-26 10:32:22 +02:00
CaitSith2
a175aa93e7 Factorio: Detect if more than one AP factorio mod is loaded. (#964) 2022-08-26 10:31:30 +02:00
Zach Parks
a78863fde1 Docs: Update community supported libraries in api doc (#788)
* Docs: Update client supported libraries in api doc

* left align table column

* Update table of languages to include Haxe lib and remarks

* Reformat table

* Changed verbiage on SNI remark
2022-08-26 02:12:37 -05:00
Fabian Dill
0d6cbd9093 Core: convert item name groups to frozenset
Some worlds define them in lists, this speeds up lookup via state.has_group() or similar
2022-08-24 00:19:27 +02:00
Magnemania
1aaf89ff2c SC2: Switched mission item group to a list comprehension to fix missile shuffle errors (#959) 2022-08-23 23:20:39 +02:00
Fabian Dill
295ea97544 Subnautica: increment client version 2022-08-23 23:19:46 +02:00
Fabian Dill
33103b209d WebHost: fix error on save 2022-08-23 23:19:19 +02:00
Fabian Dill
fab12dca0b SC2: add anti air to Devil's Playground Victory
People seem to be on the mission long enough to get attacked by Mutalisks, so Victory should require anti air.
Optional Objectives are doable quite comfortably before Mutalisks show up, allowing the anti-air to be on them for later in the mission.
2022-08-23 23:06:58 +02:00
Fabian Dill
c390801c4c Test: verify file webhost file creations work to some degree (#953)
WebHost: fix some file creation paths
2022-08-23 01:07:17 +02:00
Fabian Dill
e548abd332 Subnautica: use correct option parent class (#954)
* Subnautica: use correct option parent class

* Update Options.py
2022-08-22 19:02:29 -04:00
Jarno
0a5b24be2b [Core] Phase out Print packets and added Countdown type to print json (#812)
* [Core] Added Countdown type to print json to distinct the count down message from other types

* Added backward compatibility check

* Fixed review comments

* Updated header category

* Apply suggestions from code review

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

* Completely phased out Print in favor of PrintJson

* Updated docs to warn about phasing out of Print

* Removed faulty import

Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-08-23 01:02:10 +02:00
Chris Wilson
7f41cafffc Explaining the "Style Lockdown" (#940)
* First pass at a contribution guide for the website. Suggestions are welcome.

* Attempt to make the WebHost change guide describe the intent of the style restrictions more accurately.

* Try to improve the explanation of the intention behind the style restrictions.
2022-08-22 19:01:21 -04:00
alwaysintreble
d66f981be6 Github: templates and new user interface (#870)
* move some docs out of readme and link with the headers

* PR template

* bug report template

* task and feature request templates

* md cleanup

* forgot the template

* make expected results separate section

* move pr template to .github. remove assignment field on tasks

* add headers to pr template

* Requested changes

* suggested changes from @black-sliver and @SoldierofOrder

* Update docs/code_of_conduct.md

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* Update docs/contributing.md

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* Update docs/contributing.md

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

Co-authored-by: Hussein Farran <hmfarran@gmail.com>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
2022-08-23 00:39:55 +02:00
alwaysintreble
b66a265726 Docs: Make webworld attribute descriptions docstrings instead of comments for nice IDE things (#929) 2022-08-22 23:50:16 +02:00
Fabian Dill
c695f91198 Subnautica: add Options to Creature Scans (#950) 2022-08-22 23:35:41 +02:00
CaitSith2
11cbc0b40b Factorio: Make the energy bridge a different color. (#952) 2022-08-22 23:30:42 +02:00
N00byKing
87d91aeef3 sm64ex: Option for 1Up Block Rando 2022-08-22 17:52:56 +02:00
Fabian Dill
6a6dfcbaff Core: add some types to generic.Rules 2022-08-22 17:51:06 +02:00
NewSoupVi
9553627136 Witness: More bug fixes (#937)
* Fixed disable_non_randomized and other bugs

* Slight performance & code sensibility increase

* Added River Shortcut to Garden as a disabled check in disable_non_randomized

* Changed no progression items exception to a warning

* Added a list of disabled panels to slot_data for disable_non_randomized, so the client can automatically disable the right panels in the future

* Made no progression exception conditional on playercount
2022-08-22 05:50:01 +02:00
PoryGone
a4a8894d22 Add /SNI to .gitignore (#949) 2022-08-22 01:20:35 +02:00
wordfcuk
bf217dcf85 RoR2: Fixed the link to the game settings page (#945) 2022-08-21 17:30:30 +02:00
CaitSith2
484ee9f065 OoT: More item.type bugs. (#930) 2022-08-21 01:55:41 +02:00
Zach Parks
bba82ccd6c WebHost: Remove "Wiki" link from footer (#943) 2022-08-20 19:17:23 -04:00
alwaysintreble
fb122df5f5 RoR2: code cleanup and styling consistency (#833)
* build locations dict dynamically from the TotalLocations option. Minor styling cleanup

* Minor items styling cleanup. remove unused event items

* minor options cleanup. clarify preset toggle slightly better

* make items.py more readable. add chaos weights dict to use as reference point for generation

* small rules styling and consistency cleanup

* create less regions and other init cleanup

* move region creation to less function calls and move revivals calculation

* typing

* use enum instead of hardcoded ints. fix bug i introduced

* better typing
2022-08-20 19:09:35 -04:00
KonoTyran
be8c3131d8 fix allay advancements requiring note block on the wrong one. (#896) 2022-08-20 19:02:50 -04:00
Fabian Dill
9341332379 WebHost: allow newlines in data-tooltip (#921)
* WebHost: allow newlines in data-tooltip

* WebHost: Tooltips: strip surrounding whitespace

* WebHost: unify tooltips behaviour

* WebHost: unify labels around tooltips

* WebHost: changing tooltips width to max-width to allow small tooltips to not have empty space.

* Minor modifications to tooltips

- Reduce tooltip target to (?) spans
- Set fixed width of 260px on tooltips
- Add space between : and (?) on player-settings
- Removed cursor:pointer on tooltips
- Fix labels for checkboxes on generate.html

Co-authored-by: Chris Wilson <chris@legendserver.info>
2022-08-20 18:58:46 -04:00
Fabian Dill
83bcb441bf Factorio: typo 2022-08-21 00:34:36 +02:00
PoryGone
a074d16297 DKC3 v1.1.0 (#938)
Features:

* KONGsanity option (Collect all KONG letters in each level for a check)
* Autosave option
* Difficulty option
* MERRY option
* Handle collected/co-op locations


Bugfixes:

 * Fixed Mekanos softlock
 * Prevent Brothers Bear giving extra Banana Birds
 * Fixed Banana Bird Mother check sending prematurely
 * Fix Logic bug with Krematoa level costs
2022-08-20 16:46:44 +02:00
TheCondor07
89ab4aff9c SC2: Logic changes and fixes, 6 new locations, 2 removed locations (#933) 2022-08-19 22:50:44 +02:00
lordlou
0ac67bfe76 Smz3 early sword fix (#939) 2022-08-19 15:02:39 +02:00
Chris Wilson
e9e5511583 Merge branch 'main' into player-tracker 2022-08-17 21:48:40 -04:00
Chris Wilson
c546dcd5ff Fix merge conflicts into player-tracker 2022-08-15 21:37:44 -04:00
alwaysintreble
053fb14495 rename variables to fix invalid int loading (#858) 2022-08-03 21:36:26 -04:00
Chris Wilson
ed77d14618 PEP8 Fix 2022-08-03 21:34:59 -04:00
Chris Wilson
3fb287e82b Fix a bug causing the stylized tracker link to point to the wrong player 2022-08-03 19:54:25 -04:00
Chris Wilson
32431cfe04 Merge branch 'main' into player-tracker 2022-08-03 19:15:10 -04:00
Chris Wilson
ca8f4c38ec Merge branch 'main' into player-tracker 2022-07-31 11:17:59 -04:00
Chris Wilson
eb52454ccc Merge branch 'main' into player-tracker 2022-07-31 11:13:14 -04:00
Chris Wilson
14e5f54f59 Merge branch 'main' into player-tracker 2022-07-26 17:19:00 -04:00
Chris Wilson
2052cc55af Merge branch 'main' into player-tracker 2022-07-18 20:06:04 -04:00
Chris Wilson
63a8436240 Merge branch 'main' into player-tracker 2022-07-12 20:03:32 -04:00
Chris Wilson
e60719a20a Merge branch 'main' into player-tracker 2022-06-27 19:19:27 -04:00
alwaysintreble
8742aadc72 Player tracker (#710)
* Player tracker: implement a stylized tracker (#447)

* Move generic tracker to a WebWorld method

* render both a generic tracker at generic_tracker and the specific tracker at /tracker

* create a base template for generic specific tracker and instantiate some information before callng it

* some baseline for the playerTracker.html. update information fed from tracker.py

* playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon.

* player tracker: starting work on regions table

* player tracker: change method calls

* Move generic tracker to a WebWorld method

* render both a generic tracker at generic_tracker and the specific tracker at /tracker

* create a base template for generic specific tracker and instantiate some information before callng it

* some baseline for the playerTracker.html. update information fed from tracker.py

* playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon.

* player tracker: starting work on regions table

* player tracker: change method calls

* Move generic tracker to a WebWorld method

* create a base template for generic specific tracker and instantiate some information before callng it

* some baseline for the playerTracker.html. update information fed from tracker.py

* playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon.

* player tracker: starting work on regions table

* player tracker: switch item, icon and location tables to flex views. Some styling based on theme

* Player Tracker: Finish building html template for all blocks. Set groundwork for theme styling

* Player Tracker: Implement tracker class. Document tracker usage.

* Player Tracker: Add button to switch between trackers. Some styling for styled tracker.

* Player Tracker: reword some text. Attempt to fix page refreshing.

* Player Tracker: reremove the TODOs that got merged back in accidentally.

* player tracker: move render_template import to webworld so it isn't required outside of webhost

* Player Tracker: code cleanup, typing. Add inventory with names to PlayerTracker class in case custom trackers want to use it to change their prog_items attribute.

* Player Tracker: delete a line I forgot about. Add typing to theme.

* Player Tracker: Generate checks_done automatically so worlds don't have to do it

* Player Tracker: Add typing to PlayerTracker class in webworld method. Update documentation

* Player Tracker: code cleanup

* Player Tracker: Sort of implement fetch (works but could be better). Make playerTracker.html more readable.

* specific trackers: significant html cleanup. DOM Endpoint auto updating page every 30 seconds

* Changes by Kono

* specific trackers: cache and only load the data once every minute

* specific tracker: allow for one icon placement to be used for multiple items.

* Player tracker fixes/updates (#635)

* Move generic tracker to a WebWorld method

* render both a generic tracker at generic_tracker and the specific tracker at /tracker

* create a base template for generic specific tracker and instantiate some information before callng it

* some baseline for the playerTracker.html. update information fed from tracker.py

* playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon.

* player tracker: starting work on regions table

* player tracker: change method calls

* Move generic tracker to a WebWorld method

* render both a generic tracker at generic_tracker and the specific tracker at /tracker

* create a base template for generic specific tracker and instantiate some information before callng it

* some baseline for the playerTracker.html. update information fed from tracker.py

* playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon.

* player tracker: starting work on regions table

* player tracker: change method calls

* Move generic tracker to a WebWorld method

* create a base template for generic specific tracker and instantiate some information before callng it

* some baseline for the playerTracker.html. update information fed from tracker.py

* playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon.

* player tracker: starting work on regions table

* player tracker: switch item, icon and location tables to flex views. Some styling based on theme

* Player Tracker: Finish building html template for all blocks. Set groundwork for theme styling

* Player Tracker: Implement tracker class. Document tracker usage.

* Player Tracker: Add button to switch between trackers. Some styling for styled tracker.

* Player Tracker: reword some text. Attempt to fix page refreshing.

* Player Tracker: reremove the TODOs that got merged back in accidentally.

* player tracker: move render_template import to webworld so it isn't required outside of webhost

* Player Tracker: code cleanup, typing. Add inventory with names to PlayerTracker class in case custom trackers want to use it to change their prog_items attribute.

* Player Tracker: delete a line I forgot about. Add typing to theme.

* Player Tracker: Generate checks_done automatically so worlds don't have to do it

* Player Tracker: Add typing to PlayerTracker class in webworld method. Update documentation

* Player Tracker: code cleanup

* Player Tracker: Sort of implement fetch (works but could be better). Make playerTracker.html more readable.

* specific trackers: significant html cleanup. DOM Endpoint auto updating page every 30 seconds

* Changes by Kono

* specific trackers: cache and only load the data once every minute

* specific tracker: allow for one icon placement to be used for multiple items.

* lttp: move tracker to new format. will need more modification to generic solution to handle region keys tracking. likely a new html template that inherits the current

* lttp: fix broken icons rendering, add in progressive mail that i forgor. reorder some icons

* tracker: fix non edited trackers being broken from changes.

* tracker: move theme application before modify method so trackers can use a different theme than the world if desired.

* tracker: starting work on key tracking.

* tracker: styling and cleanup by Farrak

* tracker: styling and cleanup by Farrak

* tracker: styling and cleanup of playerTracker.html

* Revert playerTracker.html

* trackers: rename some files for clarity. move trackers into their own subdirectory

* small tracker.py cleanup

* move minecraft tracker to new system

* add item link attributing from upstream

* change getPlayerTracker to get_player_tracker. refactor broken linkings

* refactor styling files to trackers folders

* fix broken image in minecraft tracker. move oot tracker to new system

* clean up my oot nightmare

* rename lttpKeysTracker to zeldaKeysTracker. Move oot to keys tracker

* implement zeldaKeysTracker.js. fix table locations hiding/showing
2022-06-25 17:01:42 -04:00
172 changed files with 6028 additions and 3778 deletions

35
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Bug Report
description: File a bug report.
title: "Bug: "
labels:
- bug / fix
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! If this bug occurred during local generation check your
Archipelago install for a log (probably `C:\ProgramData\Archipelago\logs`)
and upload it with this report, as well as all yaml files used.
- type: textarea
id: what-happened
attributes:
label: What happened?
validations:
required: true
- type: textarea
id: expected-results
attributes:
label: What were the expected results?
validations:
required: true
- type: dropdown
id: version
attributes:
label: Software
description: Where did this bug occur?
options:
- Website
- Local generation
- While playing
validations:
required: true

View File

@@ -0,0 +1,17 @@
name: Feature Request
description: Request a feature!
title: "Category: "
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
Please replace `Category` in the title with what this feature will be targeting, such as Core generation,
website, documentation, or a game.
Note: this is not for requesting new games to be added. If you would like to request a game, the best place to
ask is about it is in the [discord](https://archipelago.gg/discord).
- type: textarea
id: feature
attributes:
label: What feature would you like to see?

10
.github/ISSUE_TEMPLATE/task.yaml vendored Normal file
View File

@@ -0,0 +1,10 @@
name: Task
description: Submit a task to be done. If this is not targeting core, it should likely be elsewhere.
title: "Core: "
labels:
- core
- enhancement
body:
- type: textarea
attributes:
label: What task needs to be completed?

12
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,12 @@
Please format your title with what portion of the project this pull request is
targeting and what it's changing.
ex. "MyGame4: implement new game" or "Docs: add new guide for customizing MyGame3"
## What is this fixing or adding?
## How was this tested?
## If this makes graphical changes, please attach screenshots.

View File

@@ -4,6 +4,11 @@ name: Build
on: workflow_dispatch
env:
SNI_VERSION: v0.0.84
ENEMIZER_VERSION: 7.1
APPIMAGETOOL_VERSION: 13
jobs:
# build-release-macos: # LF volunteer
@@ -17,9 +22,9 @@ jobs:
python-version: '3.8'
- name: Download run-time dependencies
run: |
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-windows-amd64.zip -OutFile sni.zip
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/${Env:SNI_VERSION}/sni-${Env:SNI_VERSION}-windows-amd64.zip -OutFile sni.zip
Expand-Archive -Path sni.zip -DestinationPath SNI -Force
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/7.0.1/win-x64.zip -OutFile enemizer.zip
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
- name: Build
run: |
@@ -43,6 +48,7 @@ jobs:
build-ubuntu1804:
runs-on: ubuntu-18.04
steps:
# - copy code below to release.yml -
- uses: actions/checkout@v2
- name: Install base dependencies
run: |
@@ -56,18 +62,18 @@ jobs:
- name: Install build-time dependencies
run: |
echo "PYTHON=python3.9" >> $GITHUB_ENV
wget -nv https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract
echo -e '#/bin/sh\n./squashfs-root/AppRun "$@"' > appimagetool
chmod a+rx appimagetool
- name: Download run-time dependencies
run: |
wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz
wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz
tar xf sni-*.tar.xz
rm sni-*.tar.xz
mv sni-* SNI
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
- name: Build
run: |
@@ -84,6 +90,7 @@ jobs:
(cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME")
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
# - copy code above to release.yml -
- name: Store AppImage
uses: actions/upload-artifact@v2
with:

View File

@@ -18,8 +18,8 @@ jobs:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
python -m pip install --upgrade pip wheel
pip install flake8 pytest pytest-subtests
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |

View File

@@ -7,6 +7,11 @@ on:
tags:
- '*.*.*'
env:
SNI_VERSION: v0.0.84
ENEMIZER_VERSION: 7.1
APPIMAGETOOL_VERSION: 13
jobs:
create-release:
runs-on: ubuntu-latest
@@ -44,22 +49,23 @@ jobs:
- name: Install build-time dependencies
run: |
echo "PYTHON=python3.9" >> $GITHUB_ENV
wget -nv https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract
echo -e '#/bin/sh\n./squashfs-root/AppRun "$@"' > appimagetool
chmod a+rx appimagetool
- name: Download run-time dependencies
run: |
wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz
wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz
tar xf sni-*.tar.xz
rm sni-*.tar.xz
mv sni-* SNI
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
- name: Build
run: |
"${{ env.PYTHON }}" -m pip install --upgrade pip setuptools virtualenv PyGObject # pygobject should probably move to requirements
# pygobject is an optional dependency for kivy that's not in requirements
"${{ env.PYTHON }}" -m pip install --upgrade pip virtualenv PyGObject setuptools
"${{ env.PYTHON }}" -m venv venv
source venv/bin/activate
pip install -r requirements.txt

View File

@@ -32,8 +32,8 @@ jobs:
python-version: ${{ matrix.python.version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
python -m pip install --upgrade pip wheel
pip install flake8 pytest pytest-subtests
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
- name: Unittests
run: |

1
.gitignore vendored
View File

@@ -28,6 +28,7 @@ README.html
.vs/
EnemizerCLI/
/Players/
/SNI/
/options.yaml
/config.yaml
/logs/

View File

@@ -955,6 +955,13 @@ class Region:
return True
return False
def get_connecting_entrance(self, is_main_entrance: typing.Callable[[Entrance], bool]) -> Entrance:
for entrance in self.entrances:
if is_main_entrance(entrance):
return entrance
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
def __repr__(self):
return self.__str__()
@@ -1422,7 +1429,6 @@ class Spoiler():
"f" in self.world.shop_shuffle[player]))
outfile.write('Custom Potion Shop: %s\n' %
bool_to_text("w" in self.world.shop_shuffle[player]))
outfile.write('Boss shuffle: %s\n' % self.world.boss_shuffle[player])
outfile.write('Enemy health: %s\n' % self.world.enemy_health[player])
outfile.write('Enemy damage: %s\n' % self.world.enemy_damage[player])
outfile.write('Prize shuffle %s\n' %

View File

@@ -5,6 +5,7 @@ import urllib.parse
import sys
import typing
import time
import functools
import ModuleUpdate
ModuleUpdate.update()
@@ -17,7 +18,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
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \
ClientStatus, Permission, NetworkSlot, RawJSONtoTextParser
from Utils import Version, stream_input
from worlds import network_data_package, AutoWorldRegister
import os
@@ -152,8 +154,9 @@ class CommonContext:
# locations
locations_checked: typing.Set[int] # local state
locations_scouted: typing.Set[int]
missing_locations: typing.Set[int]
missing_locations: typing.Set[int] # server state
checked_locations: typing.Set[int] # server state
server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations
locations_info: typing.Dict[int, NetworkItem]
# internals
@@ -184,8 +187,9 @@ class CommonContext:
self.locations_checked = set() # local state
self.locations_scouted = set()
self.items_received = []
self.missing_locations = set()
self.missing_locations = set() # server state
self.checked_locations = set() # server state
self.server_locations = set() # all locations the server knows of, missing_location | checked_locations
self.locations_info = {}
self.input_queue = asyncio.Queue()
@@ -202,6 +206,10 @@ class CommonContext:
# execution
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
@functools.cached_property
def raw_text_parser(self) -> RawJSONtoTextParser:
return RawJSONtoTextParser(self)
@property
def total_locations(self) -> typing.Optional[int]:
"""Will return None until connected."""
@@ -345,6 +353,8 @@ class CommonContext:
cache_package = Utils.persistent_load().get("datapackage", {}).get("games", {})
needed_updates: typing.Set[str] = set()
for game in relevant_games:
if game not in remote_datepackage_versions:
continue
remote_version: int = remote_datepackage_versions[game]
if remote_version == 0: # custom datapackage for this game
@@ -632,6 +642,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
# when /missing is used for the client side view of what is missing.
ctx.missing_locations = set(args["missing_locations"])
ctx.checked_locations = set(args["checked_locations"])
ctx.server_locations = ctx.missing_locations | ctx. checked_locations
elif cmd == 'ReceivedItems':
start_index = args["index"]

View File

@@ -1,4 +1,5 @@
import asyncio
import copy
import json
import time
from asyncio import StreamReader, StreamWriter
@@ -6,7 +7,7 @@ from typing import List
import Utils
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
get_base_parser
SYSTEM_MESSAGE_ID = 0
@@ -64,7 +65,7 @@ class FF1Context(CommonContext):
def _set_message(self, msg: str, msg_id: int):
if DISPLAY_MSGS:
self.messages[(time.time(), msg_id)] = msg
self.messages[time.time(), msg_id] = msg
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
@@ -73,32 +74,28 @@ class FF1Context(CommonContext):
msg = args['text']
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "ReceivedItems":
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == 'PrintJSON':
print_type = args['type']
item = args['item']
receiving_player_id = args['receiving']
receiving_player_name = self.player_names[receiving_player_id]
sending_player_id = item.player
sending_player_name = self.player_names[item.player]
if print_type == 'Hint':
msg = f"Hint: Your {self.item_names[item.item]} is at" \
f" {self.player_names[item.player]}'s {self.location_names[item.location]}"
self._set_message(msg, item.item)
elif print_type == 'ItemSend' and receiving_player_id != self.slot:
if sending_player_id == self.slot:
if receiving_player_id == self.slot:
msg = f"You found your own {self.item_names[item.item]}"
else:
msg = f"You sent {self.item_names[item.item]} to {receiving_player_name}"
else:
if receiving_player_id == sending_player_id:
msg = f"{sending_player_name} found their {self.item_names[item.item]}"
else:
msg = f"{sending_player_name} sent {self.item_names[item.item]} to " \
f"{receiving_player_name}"
def on_print_json(self, args: dict):
if self.ui:
self.ui.print_json(copy.deepcopy(args["data"]))
else:
text = self.jsontotextparser(copy.deepcopy(args["data"]))
logger.info(text)
relevant = args.get("type", None) in {"Hint", "ItemSend"}
if relevant:
item = args["item"]
# goes to this world
if self.slot_concerns_self(args["receiving"]):
relevant = True
# found in this world
elif self.slot_concerns_self(item.player):
relevant = True
# not related
else:
relevant = False
if relevant:
item = args["item"]
msg = self.raw_text_parser(copy.deepcopy(args["data"]))
self._set_message(msg, item.item)
def run_gui(self):

150
Fill.py
View File

@@ -136,33 +136,98 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
itempool.extend(unplaced_items)
def remaining_fill(world: MultiWorld,
locations: typing.List[Location],
itempool: typing.List[Item]) -> None:
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
while locations and itempool:
item_to_place = itempool.pop()
spot_to_fill: typing.Optional[Location] = None
for i, location in enumerate(locations):
if location.item_rule(item_to_place):
# popping by index is faster than removing by content,
spot_to_fill = locations.pop(i)
# skipping a scan for the element
break
else:
# we filled all reachable spots.
# try swapping this item with previously placed items
for (i, location) in enumerate(placements):
placed_item = location.item
# Unplaceable items can sometimes be swapped infinitely. Limit the
# number of times we will swap an individual item to prevent this
if swapped_items[placed_item.player,
placed_item.name] > 1:
continue
location.item = None
placed_item.location = None
if location.item_rule(item_to_place):
# Add this item to the existing placement, and
# add the old item to the back of the queue
spot_to_fill = placements.pop(i)
swapped_items[placed_item.player,
placed_item.name] += 1
itempool.append(placed_item)
break
# Item can't be placed here, restore original item
location.item = placed_item
placed_item.location = location
if spot_to_fill is None:
# Can't place this item, move on to the next
unplaced_items.append(item_to_place)
continue
world.push_item(spot_to_fill, item_to_place, False)
placements.append(spot_to_fill)
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)}')
itempool.extend(unplaced_items)
def fast_fill(world: 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)
return item_pool[placing:], fill_locations[placing:]
def distribute_items_restrictive(world: MultiWorld) -> None:
fill_locations = sorted(world.get_unfilled_locations())
world.random.shuffle(fill_locations)
# get items to distribute
itempool = sorted(world.itempool)
world.random.shuffle(itempool)
progitempool: typing.List[Item] = []
nonexcludeditempool: typing.List[Item] = []
localrestitempool: typing.Dict[int, typing.List[Item]] = {player: [] for player in range(1, world.players + 1)}
nonlocalrestitempool: typing.List[Item] = []
restitempool: typing.List[Item] = []
usefulitempool: typing.List[Item] = []
filleritempool: typing.List[Item] = []
for item in itempool:
if item.advancement:
progitempool.append(item)
elif item.useful: # this only gets nonprogression items which should not appear in excluded locations
nonexcludeditempool.append(item)
elif item.name in world.local_items[item.player].value:
localrestitempool[item.player].append(item)
elif item.name in world.non_local_items[item.player].value:
nonlocalrestitempool.append(item)
elif item.useful:
usefulitempool.append(item)
else:
restitempool.append(item)
filleritempool.append(item)
call_all(world, "fill_hook", progitempool, nonexcludeditempool,
localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
call_all(world, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
loc_type: [] for loc_type in LocationProgressType}
@@ -184,50 +249,16 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
raise FillError(
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
if nonexcludeditempool:
world.random.shuffle(defaultlocations)
# needs logical fill to not conflict with local items
fill_restrictive(
world, world.state, defaultlocations, nonexcludeditempool)
if nonexcludeditempool:
raise FillError(
f'Not enough locations for non-excluded items. There are {len(nonexcludeditempool)} more items than locations')
remaining_fill(world, excludedlocations, filleritempool)
if excludedlocations:
raise FillError(
f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
defaultlocations = defaultlocations + excludedlocations
world.random.shuffle(defaultlocations)
restitempool = usefulitempool + filleritempool
if any(localrestitempool.values()): # we need to make sure some fills are limited to certain worlds
local_locations: typing.Dict[int, typing.List[Location]] = {player: [] for player in world.player_ids}
for location in defaultlocations:
local_locations[location.player].append(location)
for player_locations in local_locations.values():
world.random.shuffle(player_locations)
remaining_fill(world, defaultlocations, restitempool)
for player, items in localrestitempool.items(): # items already shuffled
player_local_locations = local_locations[player]
for item_to_place in items:
if not player_local_locations:
logging.warning(f"Ran out of local locations for player {player}, "
f"cannot place {item_to_place}.")
break
spot_to_fill = player_local_locations.pop()
world.push_item(spot_to_fill, item_to_place, False)
defaultlocations.remove(spot_to_fill)
for item_to_place in nonlocalrestitempool:
for i, location in enumerate(defaultlocations):
if location.player != item_to_place.player:
world.push_item(defaultlocations.pop(i), item_to_place, False)
break
else:
raise Exception(f"Could not place non_local_item {item_to_place} among {defaultlocations}. "
f"Too many non-local items for too few remaining locations.")
world.random.shuffle(defaultlocations)
restitempool, defaultlocations = fast_fill(
world, restitempool, defaultlocations)
unplaced = progitempool + restitempool
unplaced = restitempool
unfilled = defaultlocations
if unplaced or unfilled:
@@ -241,15 +272,6 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
logging.info(f'Per-Player counts: {print_data})')
def fast_fill(world: 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)
return item_pool[placing:], fill_locations[placing:]
def flood_items(world: MultiWorld) -> None:
# get items to distribute
world.random.shuffle(world.itempool)

View File

@@ -23,7 +23,6 @@ from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from BaseClasses import seeddigits, get_seed
import Options
from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister
import copy
@@ -63,7 +62,7 @@ class PlandoSettings(enum.IntFlag):
def __str__(self) -> str:
if self.value:
return ", ".join((flag.name for flag in PlandoSettings if self.value & flag.value))
return ", ".join(flag.name for flag in PlandoSettings if self.value & flag.value)
return "Off"
@@ -84,11 +83,6 @@ def mystery_argparse():
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
parser.add_argument('--lttp_rom', default=options["lttp_options"]["rom_file"],
help="Path to the 1.0 JP LttP Baserom.") # absolute, relative to cwd or relative to app path
parser.add_argument('--sm_rom', default=options["sm_options"]["rom_file"],
help="Path to the 1.0 JP SM Baserom.")
parser.add_argument('--enemizercli', default=resolve_path(defaults["enemizer_path"], local_path))
parser.add_argument('--outputpath', default=resolve_path(options["general_options"]["output_path"], user_path),
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
parser.add_argument('--race', action='store_true', default=defaults["race"])
@@ -183,10 +177,6 @@ def main(args=None, callback=ERmain):
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
erargs.lttp_rom = args.lttp_rom
erargs.sm_rom = args.sm_rom
erargs.enemizercli = args.enemizercli
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
for fname, yamls in weights_cache.items()}
@@ -346,19 +336,6 @@ def prefer_int(input_data: str) -> Union[str, int]:
return input_data
available_boss_names: Set[str] = {boss.lower() for boss in Bosses.boss_table if boss not in
{'Agahnim', 'Agahnim2', 'Ganon'}}
available_boss_locations: Set[str] = {f"{loc.lower()}{f' {level}' if level else ''}" for loc, level in
Bosses.boss_location_table}
boss_shuffle_options = {None: 'none',
'none': 'none',
'basic': 'basic',
'full': 'full',
'chaos': 'chaos',
'singularity': 'singularity'
}
goals = {
'ganon': 'ganon',
'crystals': 'crystals',
@@ -465,42 +442,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
return weights
def get_plando_bosses(boss_shuffle: str, plando_options: Set[str]) -> str:
if boss_shuffle in boss_shuffle_options:
return boss_shuffle_options[boss_shuffle]
elif PlandoSettings.bosses in plando_options:
options = boss_shuffle.lower().split(";")
remainder_shuffle = "none" # vanilla
bosses = []
for boss in options:
if boss in boss_shuffle_options:
remainder_shuffle = boss_shuffle_options[boss]
elif "-" in boss:
loc, boss_name = boss.split("-")
if boss_name not in available_boss_names:
raise ValueError(f"Unknown Boss name {boss_name}")
if loc not in available_boss_locations:
raise ValueError(f"Unknown Boss Location {loc}")
level = ''
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
# split off level
loc = loc.split(" ")
level = f" {loc[-1]}"
loc = " ".join(loc[:-1])
loc = loc.title().replace("Of", "of")
if not Bosses.can_place_boss(boss_name.title(), loc, level):
raise ValueError(f"Cannot place {boss_name} at {loc}{level}")
bosses.append(boss)
elif boss not in available_boss_names:
raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.")
else:
bosses.append(boss)
return ";".join(bosses + [remainder_shuffle])
else:
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option)):
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoSettings):
if option_key in game_weights:
try:
if not option.supports_weighting:
@@ -511,10 +453,9 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
except Exception as e:
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
else:
if hasattr(player_option, "verify"):
player_option.verify(AutoWorldRegister.world_types[ret.game])
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
else:
setattr(ret, option_key, option(option.default))
setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random"
def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings.bosses):
@@ -558,11 +499,11 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
if ret.game in AutoWorldRegister.world_types:
for option_key, option in world_type.option_definitions.items():
handle_option(ret, game_weights, option_key, option)
handle_option(ret, game_weights, option_key, option, plando_options)
for option_key, option in Options.per_game_common_options.items():
# skip setting this option if already set from common_options, defaulting to root option
if not (option_key in Options.common_options and option_key not in game_weights):
handle_option(ret, game_weights, option_key, option)
handle_option(ret, game_weights, option_key, option, plando_options)
if PlandoSettings.items in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
@@ -645,8 +586,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.item_functionality = get_choice_legacy('item_functionality', weights)
boss_shuffle = get_choice_legacy('boss_shuffle', weights)
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
ret.enemy_damage = {None: 'default',
'default': 'default',

View File

@@ -10,16 +10,21 @@ Scroll down to components= to add components to the launcher as well as setup.py
import argparse
from os.path import isfile
import sys
from typing import Iterable, Sequence, Callable, Union, Optional
import subprocess
import itertools
from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox,\
is_windows, is_macos, is_linux
from shutil import which
import shlex
import subprocess
import sys
from enum import Enum, auto
from os.path import isfile
from shutil import which
from typing import Iterable, Sequence, Callable, Union, Optional
if __name__ == "__main__":
import ModuleUpdate
ModuleUpdate.update()
from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \
is_windows, is_macos, is_linux
def open_host_yaml():
@@ -65,6 +70,7 @@ def browse_files():
webbrowser.open(file)
# noinspection PyArgumentList
class Type(Enum):
TOOL = auto()
FUNC = auto() # not a real component

View File

@@ -83,9 +83,9 @@ def main():
parser.add_argument('--ow_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
parser.add_argument('--link_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
# parser.add_argument('--link_palettes', default='default',
# choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
# 'sick'])
parser.add_argument('--shield_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
@@ -752,6 +752,7 @@ class SpriteSelector():
self.window['pady'] = 5
self.spritesPerRow = 32
self.all_sprites = []
self.invalid_sprites = []
self.sprite_pool = spritePool
def open_custom_sprite_dir(_evt):
@@ -833,6 +834,13 @@ class SpriteSelector():
self.window.focus()
tkinter_center_window(self.window)
if self.invalid_sprites:
invalid = sorted(self.invalid_sprites)
logging.warning(f"The following sprites are invalid: {', '.join(invalid)}")
msg = f"{invalid[0]} "
msg += f"and {len(invalid)-1} more are invalid" if len(invalid) > 1 else "is invalid"
messagebox.showerror("Invalid sprites detected", msg, parent=self.window)
def remove_from_sprite_pool(self, button, spritename):
self.callback(("remove", spritename))
self.spritePoolButtons.buttons.remove(button)
@@ -897,7 +905,13 @@ class SpriteSelector():
sprites = []
for file in os.listdir(path):
sprites.append((file, Sprite(os.path.join(path, file))))
if file == '.gitignore':
continue
sprite = Sprite(os.path.join(path, file))
if sprite.valid:
sprites.append((file, sprite))
else:
self.invalid_sprites.append(file)
sprites.sort(key=lambda s: str.lower(s[1].name or "").strip())

54
Main.py
View File

@@ -12,7 +12,7 @@ from typing import Dict, Tuple, Optional, Set
from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location
from worlds.alttp.Items import item_name_groups
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
from worlds.alttp.Regions import is_main_entrance
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from Utils import output_path, get_options, __version__, version_tuple
@@ -70,7 +70,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.required_medallions = args.required_medallions.copy()
world.game = args.game.copy()
world.player_name = args.name.copy()
world.enemizer = args.enemizercli
world.sprite = args.sprite.copy()
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
@@ -250,24 +249,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
output_file_futures.append(
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
def get_entrance_to_region(region: Region):
for entrance in region.entrances:
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic):
return entrance
for entrance in region.entrances: # BFS might be better here, trying DFS for now.
return get_entrance_to_region(entrance.parent_region)
# collect ER hint info
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
world.shuffle[player] != "vanilla" or world.retro_caves[player]}
for region in world.regions:
if region.player in er_hint_data and region.locations:
main_entrance = get_entrance_to_region(region)
for location in region.locations:
if type(location.address) == int: # skips events and crystals
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
er_hint_data[region.player][location.address] = main_entrance.name
er_hint_data: Dict[int, Dict[int, str]] = {}
AutoWorld.call_all(world, 'extend_hint_information', er_hint_data)
checks_in_area = {player: {area: list() for area in ordered_areas}
for player in range(1, world.players + 1)}
@@ -277,22 +261,23 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for location in world.get_filled_locations():
if type(location.address) is int:
main_entrance = get_entrance_to_region(location.parent_region)
if location.game != "A Link to the Past":
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.dungeon:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif location.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
else:
main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
if location.parent_region.dungeon:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif location.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
checks_in_area[location.player]["Total"] += 1
oldmancaves = []
@@ -306,7 +291,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
player = region.player
location_id = SHOP_ID_START + total_shop_slots + index
main_entrance = get_entrance_to_region(region)
main_entrance = region.get_connecting_entrance(is_main_entrance)
if main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[player]["Light World"].append(location_id)
else:
@@ -341,7 +326,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
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 slot in world.player_ids:
slot_data[slot] = world.worlds[slot].fill_slot_data()

View File

@@ -36,6 +36,7 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ
SlotType
min_client_version = Version(0, 1, 6)
print_command_compatability_threshold = Version(0, 3, 5) # Remove backwards compatibility around 0.3.7
colorama.init()
# functions callable on storable data on the server by clients
@@ -125,6 +126,7 @@ class Context:
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
forced_auto_forfeits: typing.Dict[str, bool]
non_hintable_names: typing.Dict[str, typing.Set[str]]
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
@@ -195,7 +197,7 @@ class Context:
self.item_name_groups = {}
self.all_item_and_group_names = {}
self.forced_auto_forfeits = collections.defaultdict(lambda: False)
self.non_hintable_names = {}
self.non_hintable_names = collections.defaultdict(frozenset)
self._load_game_data()
self._init_game_data()
@@ -220,11 +222,11 @@ class Context:
self.all_item_and_group_names[game_name] = \
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
def item_names_for_game(self, game: str) -> typing.Dict[str, int]:
return self.gamespackage[game]["item_name_to_id"]
def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None
def location_names_for_game(self, game: str) -> typing.Dict[str, int]:
return self.gamespackage[game]["location_name_to_id"]
def location_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
return self.gamespackage[game]["location_name_to_id"] if game in self.gamespackage else None
# General networking
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
@@ -291,20 +293,27 @@ class Context:
# text
def notify_all(self, text):
def notify_all(self, text: str):
logging.info("Notice (all): %s" % text)
self.broadcast_all([{"cmd": "Print", "text": text}])
broadcast_text_all(self, text)
def notify_client(self, client: Client, text: str):
if not client.auth:
return
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
if client.version >= print_command_compatability_threshold:
asyncio.create_task(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }]}]))
else:
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
def notify_client_multiple(self, client: Client, texts: typing.List[str]):
if not client.auth:
return
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
if client.version >= print_command_compatability_threshold:
asyncio.create_task(self.send_msgs(client,
[{"cmd": "PrintJSON", "data": [{ "text": text }]} for text in texts]))
else:
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
# loading
@@ -585,6 +594,7 @@ class Context:
forfeit_player(self, client.team, client.slot)
elif self.forced_auto_forfeits[self.games[client.slot]]:
forfeit_player(self, client.team, client.slot)
self.save() # save goal completion flag
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
@@ -721,20 +731,37 @@ async def on_client_left(ctx: Context, client: Client):
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
async def countdown(ctx: Context, timer):
ctx.notify_all(f'[Server]: Starting countdown of {timer}s')
async def countdown(ctx: Context, timer: int):
broadcast_countdown(ctx, timer, f"[Server]: Starting countdown of {timer}s")
if ctx.countdown_timer:
ctx.countdown_timer = timer # timer is already running, set it to a different time
else:
ctx.countdown_timer = timer
while ctx.countdown_timer > 0:
ctx.notify_all(f'[Server]: {ctx.countdown_timer}')
broadcast_countdown(ctx, ctx.countdown_timer, f"[Server]: {ctx.countdown_timer}")
ctx.countdown_timer -= 1
await asyncio.sleep(1)
ctx.notify_all(f'[Server]: GO')
broadcast_countdown(ctx, 0, f"[Server]: GO")
ctx.countdown_timer = 0
def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {}):
old_clients, new_clients = [], []
for teams in ctx.clients.values():
for clients in teams.values():
for client in clients:
new_clients.append(client) if client.version >= print_command_compatability_threshold \
else old_clients.append(client)
ctx.broadcast(old_clients, [{"cmd": "Print", "text": text }])
ctx.broadcast(new_clients, [{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
def broadcast_countdown(ctx: Context, timer: int, message: str):
broadcast_text_all(ctx, message, {"type": "Countdown", "countdown": timer})
def get_players_string(ctx: Context):
auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth}
@@ -874,14 +901,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
ctx.save()
def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.List[NetUtils.Hint]:
def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str]) -> typing.List[NetUtils.Hint]:
hints = []
slots: typing.Set[int] = {slot}
for group_id, group in ctx.groups.items():
if slot in group:
slots.add(group_id)
seeked_item_id = ctx.item_names_for_game(ctx.games[slot])[item_name]
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
for finding_player, check_data in ctx.locations.items():
for location_id, (item_id, receiving_player, item_flags) in check_data.items():
if receiving_player in slots and item_id == seeked_item_id:
@@ -1309,13 +1336,33 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
f"You have {points_available} points.")
return True
elif input_text.isnumeric():
game = self.ctx.games[self.client.slot]
hint_id = int(input_text)
hint_name = self.ctx.item_names[hint_id] \
if not for_location and hint_id in self.ctx.item_names \
else self.ctx.location_names[hint_id] \
if for_location and hint_id in self.ctx.location_names \
else None
if hint_name in self.ctx.non_hintable_names[game]:
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
hints = []
elif not for_location:
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id)
else:
hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id)
else:
game = self.ctx.games[self.client.slot]
if game not in self.ctx.all_item_and_group_names:
self.output("Can't look up item/location for unknown game. Hint for ID instead.")
return False
names = self.ctx.location_names_for_game(game) \
if for_location else \
self.ctx.all_item_and_group_names[game]
hint_name, usable, response = get_intended_text(input_text,
names)
hint_name, usable, response = get_intended_text(input_text, names)
if usable:
if hint_name in self.ctx.non_hintable_names[game]:
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
@@ -1329,63 +1376,65 @@ class ClientMessageProcessor(CommonCommandProcessor):
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
else: # location name
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
cost = self.ctx.get_hint_cost(self.client.slot)
if hints:
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
old_hints = set(hints) - new_hints
if old_hints:
notify_hints(self.ctx, self.client.team, list(old_hints))
if not new_hints:
self.output("Hint was previously used, no points deducted.")
if new_hints:
found_hints = [hint for hint in new_hints if hint.found]
not_found_hints = [hint for hint in new_hints if not hint.found]
if not not_found_hints: # everything's been found, no need to pay
can_pay = 1000
elif cost:
can_pay = int((points_available // cost) > 0) # limit to 1 new hint per call
else:
can_pay = 1000
self.ctx.random.shuffle(not_found_hints)
# By popular vote, make hints prefer non-local placements
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
hints = found_hints
while can_pay > 0:
if not not_found_hints:
break
hint = not_found_hints.pop()
hints.append(hint)
can_pay -= 1
self.ctx.hints_used[self.client.team, self.client.slot] += 1
points_available = get_client_points(self.ctx, self.client)
if not_found_hints:
if hints and cost and int((points_available // cost) == 0):
self.output(
f"There may be more hintables, however, you cannot afford to pay for any more. "
f" You have {points_available} and need at least "
f"{self.ctx.get_hint_cost(self.client.slot)}.")
elif hints:
self.output(
"There may be more hintables, you can rerun the command to find more.")
else:
self.output(f"You can't afford the hint. "
f"You have {points_available} points and need at least "
f"{self.ctx.get_hint_cost(self.client.slot)}.")
notify_hints(self.ctx, self.client.team, hints)
self.ctx.save()
return True
else:
self.output("Nothing found. Item/Location may not exist.")
return False
else:
self.output(response)
return False
if hints:
cost = self.ctx.get_hint_cost(self.client.slot)
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
old_hints = set(hints) - new_hints
if old_hints:
notify_hints(self.ctx, self.client.team, list(old_hints))
if not new_hints:
self.output("Hint was previously used, no points deducted.")
if new_hints:
found_hints = [hint for hint in new_hints if hint.found]
not_found_hints = [hint for hint in new_hints if not hint.found]
if not not_found_hints: # everything's been found, no need to pay
can_pay = 1000
elif cost:
can_pay = int((points_available // cost) > 0) # limit to 1 new hint per call
else:
can_pay = 1000
self.ctx.random.shuffle(not_found_hints)
# By popular vote, make hints prefer non-local placements
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
hints = found_hints
while can_pay > 0:
if not not_found_hints:
break
hint = not_found_hints.pop()
hints.append(hint)
can_pay -= 1
self.ctx.hints_used[self.client.team, self.client.slot] += 1
points_available = get_client_points(self.ctx, self.client)
if not_found_hints:
if hints and cost and int((points_available // cost) == 0):
self.output(
f"There may be more hintables, however, you cannot afford to pay for any more. "
f" You have {points_available} and need at least "
f"{self.ctx.get_hint_cost(self.client.slot)}.")
elif hints:
self.output(
"There may be more hintables, you can rerun the command to find more.")
else:
self.output(f"You can't afford the hint. "
f"You have {points_available} points and need at least "
f"{self.ctx.get_hint_cost(self.client.slot)}.")
notify_hints(self.ctx, self.client.team, hints)
self.ctx.save()
return True
else:
self.output("Nothing found. Item/Location may not exist.")
return False
@mark_raw
def _cmd_hint(self, item_name: str = "") -> bool:
"""Use !hint {item_name},
@@ -1833,17 +1882,25 @@ class ServerCommandProcessor(CommonCommandProcessor):
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable:
team, slot = self.ctx.player_name_lookup[seeked_player]
item_name = " ".join(item_name)
game = self.ctx.games[slot]
item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game])
full_name = " ".join(item_name)
if full_name.isnumeric():
item, usable, response = int(full_name), True, None
elif game in self.ctx.all_item_and_group_names:
item, usable, response = get_intended_text(full_name, self.ctx.all_item_and_group_names[game])
else:
self.output("Can't look up item for unknown game. Hint for ID instead.")
return False
if usable:
if item_name in self.ctx.item_name_groups[game]:
if game in self.ctx.item_name_groups and item in self.ctx.item_name_groups[game]:
hints = []
for item_name_from_group in self.ctx.item_name_groups[game][item_name]:
for item_name_from_group in self.ctx.item_name_groups[game][item]:
if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
else: # item name
hints = collect_hints(self.ctx, team, slot, item_name)
else: # item name or id
hints = collect_hints(self.ctx, team, slot, item)
if hints:
notify_hints(self.ctx, team, hints)
@@ -1864,11 +1921,22 @@ class ServerCommandProcessor(CommonCommandProcessor):
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable:
team, slot = self.ctx.player_name_lookup[seeked_player]
location_name = " ".join(location_name)
location_name, usable, response = get_intended_text(location_name,
self.ctx.location_names_for_game(self.ctx.games[slot]))
game = self.ctx.games[slot]
full_name = " ".join(location_name)
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))
else:
self.output("Can't look up location for unknown game. Hint for ID instead.")
return False
if usable:
hints = collect_hint_location_name(self.ctx, team, slot, location_name)
if isinstance(location, int):
hints = collect_hint_location_id(self.ctx, team, slot, location)
else:
hints = collect_hint_location_name(self.ctx, team, slot, location)
if hints:
notify_hints(self.ctx, team, hints)
else:
@@ -2018,15 +2086,28 @@ async def main(args: argparse.Namespace):
args.auto_shutdown, args.compatibility, args.log_network)
data_filename = args.multidata
try:
if not data_filename:
if not data_filename:
try:
filetypes = (("Multiworld data", (".archipelago", ".zip")),)
data_filename = Utils.open_filename("Select multiworld data", filetypes)
except Exception as e:
if isinstance(e, ImportError) or (e.__class__.__name__ == "TclError" and "no display" in str(e)):
if not isinstance(e, ImportError):
logging.error(f"Failed to load tkinter ({e})")
logging.info("Pass a multidata filename on command line to run headless.")
exit(1)
raise
if not data_filename:
logging.info("No file selected. Exiting.")
exit(1)
try:
ctx.load(data_filename, args.use_embedded_options)
except Exception as e:
logging.exception('Failed to read multiworld data (%s)' % e)
logging.exception(f"Failed to read multiworld data ({e})")
raise
ctx.init_save(not args.disable_save)

View File

@@ -26,15 +26,31 @@ class AssembleOptions(abc.ABCMeta):
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
options.update(new_options)
# apply aliases, without name_lookup
aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if
name.startswith("alias_")}
assert "random" not in aliases, "Choice option 'random' cannot be manually assigned."
# auto-alias Off and On being parsed as True and False
if "off" in options:
options["false"] = options["off"]
if "on" in options:
options["true"] = options["on"]
options.update(aliases)
if "verify" not in attrs:
# not overridden by class -> look up bases
verifiers = [f for f in (getattr(base, "verify", None) for base in bases) if f]
if len(verifiers) > 1: # verify multiple bases/mixins
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"
# auto-validate schema on __init__
if "schema" in attrs.keys():
@@ -112,6 +128,41 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
def from_any(cls, data: typing.Any) -> Option[T]:
raise NotImplementedError
if typing.TYPE_CHECKING:
from Generate import PlandoSettings
from worlds.AutoWorld import World
def verify(self, world: World, player_name: str, plando_options: PlandoSettings) -> None:
pass
else:
def verify(self, *args, **kwargs) -> None:
pass
class FreeText(Option):
"""Text option that allows users to enter strings.
Needs to be validated by the world or option definition."""
def __init__(self, value: str):
assert isinstance(value, str), "value of FreeText must be a string"
self.value = value
@property
def current_key(self) -> str:
return self.value
@classmethod
def from_text(cls, text: str) -> FreeText:
return cls(text)
@classmethod
def from_any(cls, data: typing.Any) -> FreeText:
return cls.from_text(str(data))
@classmethod
def get_option_name(cls, value: T) -> str:
return value
class NumericOption(Option[int], numbers.Integral):
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards
@@ -298,7 +349,7 @@ class Toggle(NumericOption):
if type(data) == str:
return cls.from_text(data)
else:
return cls(data)
return cls(int(data))
@classmethod
def get_option_name(cls, value):
@@ -368,6 +419,53 @@ class Choice(NumericOption):
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
class TextChoice(Choice):
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
def __init__(self, value: typing.Union[str, int]):
assert isinstance(value, str) or isinstance(value, int), \
f"{value} is not a valid option for {self.__class__.__name__}"
self.value = value
super(TextChoice, self).__init__()
@property
def current_key(self) -> str:
if isinstance(self.value, str):
return self.value
else:
return self.name_lookup[self.value]
@classmethod
def from_text(cls, text: str) -> TextChoice:
if text.lower() == "random": # chooses a random defined option but won't use any free text options
return cls(random.choice(list(cls.name_lookup)))
for option_name, value in cls.options.items():
if option_name.lower() == text.lower():
return cls(value)
return cls(text)
@classmethod
def get_option_name(cls, value: T) -> str:
if isinstance(value, str):
return value
return cls.name_lookup[value]
def __eq__(self, other: typing.Any):
if isinstance(other, self.__class__):
return other.value == self.value
elif isinstance(other, str):
if other in self.options:
return other == self.current_key
return other == self.value
elif isinstance(other, int):
assert other in self.name_lookup, f"compared against an int that could never be equal. {self} == {other}"
return other == self.value
elif isinstance(other, bool):
return other == bool(self.value)
else:
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
class Range(NumericOption):
range_start = 0
range_end = 1
@@ -385,7 +483,7 @@ class Range(NumericOption):
if text.startswith("random"):
return cls.weighted_range(text)
elif text == "default" and hasattr(cls, "default"):
return cls(cls.default)
return cls.from_any(cls.default)
elif text == "high":
return cls(cls.range_end)
elif text == "low":
@@ -396,7 +494,7 @@ class Range(NumericOption):
and text in ("true", "false"):
# these are the conditions where "true" and "false" make sense
if text == "true":
return cls(cls.default)
return cls.from_any(cls.default)
else: # "false"
return cls(0)
return cls(int(text))
@@ -507,7 +605,7 @@ class VerifyKeys:
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
f"Allowed keys: {cls.valid_keys}.")
def verify(self, world):
def verify(self, world, player_name: str, plando_options) -> None:
if self.convert_name_groups and self.verify_item_name:
new_value = type(self.value)() # empty container of whatever value is
for item_name in self.value:
@@ -600,10 +698,7 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
@classmethod
def from_any(cls, data: typing.Any):
if type(data) == list:
cls.verify_keys(data)
return cls(data)
elif type(data) == set:
if isinstance(data, (list, set, frozenset)):
cls.verify_keys(data)
return cls(data)
return cls.from_text(str(data))
@@ -732,8 +827,8 @@ class ItemLinks(OptionList):
pool |= {item_name}
return pool
def verify(self, world):
super(ItemLinks, self).verify(world)
def verify(self, world, player_name: str, plando_options) -> None:
super(ItemLinks, self).verify(world, player_name, plando_options)
existing_links = set()
for link in self.value:
if link["name"] in existing_links:

View File

@@ -17,7 +17,7 @@ ModuleUpdate.update()
import Utils
current_patch_version = 4
current_patch_version = 5
class AutoPatchRegister(type):
@@ -128,6 +128,7 @@ class APDeltaPatch(APContainer, metaclass=AutoPatchRegister):
manifest = super(APDeltaPatch, self).get_manifest()
manifest["base_checksum"] = self.hash
manifest["result_file_ending"] = self.result_file_ending
manifest["patch_file_ending"] = self.patch_file_ending
return manifest
@classmethod

View File

@@ -61,26 +61,10 @@ This project makes use of multiple other projects. We wouldn't be here without t
* [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer)
## Contributing
Contributions are welcome. We have a few asks of any new contributors.
* Ensure that all changes which affect logic are covered by unit tests.
* Do not introduce any unit test failures/regressions.
Otherwise, we tend to judge code on a case to case basis. It is a generally good idea to stick to PEP-8 guidelines to ensure consistency with existing code. (And to make the linter happy.)
For adding a new game to Archipelago and other documentation on how Archipelago functions, please see [the docs folder](docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev channel in our discord.
For contribution guidelines, please see our [Contributing doc.](/docs/contributing.md)
## FAQ
For frequently asked questions see the website's [FAQ Page](https://archipelago.gg/faq/en/)
For Frequently asked questions, please see the website's [FAQ Page.](https://archipelago.gg/faq/en/)
## Code of Conduct
We conduct ourselves openly and inclusively here. Please do not contribute to an environment which makes other people uncomfortable. This means that we expect all contributors or participants here to:
* Be welcoming and inclusive in tone and language.
* Be respectful of others and their abilities.
* Show empathy when speaking with others.
* Be gracious and accept feedback and constructive criticism.
These guidelines apply to all channels of communication within this GitHub repository. Please be respectful in both public channels, such as issues, and private, such as private messaging or emails.
Any incidents of abuse may be reported directly to Ijwu at hmfarran@gmail.com.
Please refer to our [code of conduct.](/docs/code_of_conduct.md)

View File

@@ -15,9 +15,6 @@ import typing
from json import loads, dumps
import ModuleUpdate
ModuleUpdate.update()
from Utils import init_logging, messagebox
if __name__ == "__main__":
@@ -149,8 +146,8 @@ class Context(CommonContext):
def event_invalid_slot(self):
if self.snes_socket is not None and not self.snes_socket.closed:
asyncio.create_task(self.snes_socket.close())
raise Exception('Invalid ROM detected, '
'please verify that you have loaded the correct rom and reconnect your snes (/snes)')
raise Exception("Invalid ROM detected, "
"please verify that you have loaded the correct rom and reconnect your snes (/snes)")
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
@@ -158,7 +155,7 @@ class Context(CommonContext):
if self.rom is None:
self.awaiting_rom = True
snes_logger.info(
'No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)')
"No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)")
return
self.awaiting_rom = False
self.auth = self.rom
@@ -262,7 +259,7 @@ async def deathlink_kill_player(ctx: Context):
SNES_RECONNECT_DELAY = 5
# LttP
# FXPAK Pro protocol memory mapping used by SNI
ROM_START = 0x000000
WRAM_START = 0xF50000
WRAM_SIZE = 0x20000
@@ -293,21 +290,24 @@ SHOP_LEN = (len(Shops.shop_table) * 3) + 5
DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte
# SM
SM_ROMNAME_START = 0x007FC0
SM_ROMNAME_START = ROM_START + 0x007FC0
SM_INGAME_MODES = {0x07, 0x09, 0x0b}
SM_ENDGAME_MODES = {0x26, 0x27}
SM_DEATH_MODES = {0x15, 0x17, 0x18, 0x19, 0x1A}
SM_RECV_PROGRESS_ADDR = SRAM_START + 0x2000 # 2 bytes
SM_RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
SM_RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
# RECV and SEND are from the gameplay's perspective: SNIClient writes to RECV queue and reads from SEND queue
SM_RECV_QUEUE_START = SRAM_START + 0x2000
SM_RECV_QUEUE_WCOUNT = SRAM_START + 0x2602
SM_SEND_QUEUE_START = SRAM_START + 0x2700
SM_SEND_QUEUE_RCOUNT = SRAM_START + 0x2680
SM_SEND_QUEUE_WCOUNT = SRAM_START + 0x2682
SM_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x277f04 # 1 byte
SM_REMOTE_ITEM_FLAG_ADDR = ROM_START + 0x277f06 # 1 byte
# SMZ3
SMZ3_ROMNAME_START = 0x00FFC0
SMZ3_ROMNAME_START = ROM_START + 0x00FFC0
SMZ3_INGAME_MODES = {0x07, 0x09, 0x0b}
SMZ3_ENDGAME_MODES = {0x26, 0x27}
@@ -1083,6 +1083,9 @@ async def game_watcher(ctx: Context):
if ctx.awaiting_rom:
await ctx.server_auth(False)
elif ctx.server is None:
snes_logger.warning("ROM detected but no active multiworld server connection. " +
"Connect using command: /connect server:port")
if ctx.auth and ctx.auth != ctx.rom:
snes_logger.warning("ROM change detected, please reconnect to the multiworld server")
@@ -1159,6 +1162,9 @@ async def game_watcher(ctx: Context):
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
await track_locations(ctx, roomid, roomdata)
elif ctx.game == GAME_SM:
if ctx.server is None or ctx.slot is None:
# not successfully connected to a multiworld server, cannot process the game sending items
continue
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
currently_dead = gamemode[0] in SM_DEATH_MODES
@@ -1169,25 +1175,25 @@ async def game_watcher(ctx: Context):
ctx.finished_game = True
continue
data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x680, 4)
data = await snes_read(ctx, SM_SEND_QUEUE_RCOUNT, 4)
if data is None:
continue
recv_index = data[0] | (data[1] << 8)
recv_item = data[2] | (data[3] << 8)
recv_item = data[2] | (data[3] << 8) # this is actually SM_SEND_QUEUE_WCOUNT
while (recv_index < recv_item):
itemAdress = recv_index * 8
message = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x700 + itemAdress, 8)
message = await snes_read(ctx, SM_SEND_QUEUE_START + itemAdress, 8)
# worldId = message[0] | (message[1] << 8) # unused
# itemId = message[2] | (message[3] << 8) # unused
itemIndex = (message[4] | (message[5] << 8)) >> 3
recv_index += 1
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x680,
snes_buffered_write(ctx, SM_SEND_QUEUE_RCOUNT,
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
from worlds.sm.Locations import locations_start_id
from worlds.sm import locations_start_id
location_id = locations_start_id + itemIndex
ctx.locations_checked.add(location_id)
@@ -1196,15 +1202,14 @@ async def game_watcher(ctx: Context):
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x600, 4)
data = await snes_read(ctx, SM_RECV_QUEUE_WCOUNT, 2)
if data is None:
continue
# recv_itemOutPtr = data[0] | (data[1] << 8) # unused
itemOutPtr = data[2] | (data[3] << 8)
itemOutPtr = data[0] | (data[1] << 8)
from worlds.sm.Items import items_start_id
from worlds.sm.Locations import locations_start_id
from worlds.sm import items_start_id
from worlds.sm import locations_start_id
if itemOutPtr < len(ctx.items_received):
item = ctx.items_received[itemOutPtr]
itemId = item.item - items_start_id
@@ -1214,10 +1219,10 @@ async def game_watcher(ctx: Context):
locationId = 0x00 #backward compat
playerID = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes(
snes_buffered_write(ctx, SM_RECV_QUEUE_START + itemOutPtr * 4, bytes(
[playerID & 0xFF, (playerID >> 8) & 0xFF, itemId & 0xFF, locationId & 0xFF]))
itemOutPtr += 1
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x602,
snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT,
bytes([itemOutPtr & 0xFF, (itemOutPtr >> 8) & 0xFF]))
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_names[item.item], 'red', 'bold'),
@@ -1225,6 +1230,9 @@ async def game_watcher(ctx: Context):
ctx.location_names[item.location], itemOutPtr, len(ctx.items_received)))
await snes_flush_writes(ctx)
elif ctx.game == GAME_SMZ3:
if ctx.server is None or ctx.slot is None:
# not successfully connected to a multiworld server, cannot process the game sending items
continue
currentGame = await snes_read(ctx, SRAM_START + 0x33FE, 2)
if (currentGame is not None):
if (currentGame[0] != 0):
@@ -1260,7 +1268,8 @@ async def game_watcher(ctx: Context):
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
from worlds.smz3.TotalSMZ3.Location import locations_start_id
location_id = locations_start_id + itemIndex
from worlds.smz3 import convertLocSMZ3IDToAPID
location_id = locations_start_id + convertLocSMZ3IDToAPID(itemIndex)
ctx.locations_checked.add(location_id)
location = ctx.location_names[location_id]

View File

@@ -1,31 +1,32 @@
from __future__ import annotations
import multiprocessing
import logging
import asyncio
import copy
import ctypes
import logging
import multiprocessing
import os.path
import re
import sys
import typing
import queue
from pathlib import Path
import nest_asyncio
import sc2
from sc2.main import run_game
from sc2.data import Race
from sc2.bot_ai import BotAI
from sc2.data import Race
from sc2.main import run_game
from sc2.player import Bot
from worlds.sc2wol.Regions import MissionInfo
from worlds.sc2wol.MissionTables import lookup_id_to_mission
from worlds.sc2wol.Items import lookup_id_to_name, item_table
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
from worlds.sc2wol import SC2WoLWorld
from pathlib import Path
import re
import NetUtils
from MultiServer import mark_raw
import ctypes
import sys
from Utils import init_logging, is_windows
from worlds.sc2wol import SC2WoLWorld
from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
from worlds.sc2wol.MissionTables import lookup_id_to_mission
from worlds.sc2wol.Regions import MissionInfo
if __name__ == "__main__":
init_logging("SC2Client", exception_logger="Client")
@@ -35,10 +36,12 @@ sc2_logger = logging.getLogger("Starcraft2")
import colorama
from NetUtils import *
from NetUtils import ClientStatus, RawJSONtoTextParser
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
nest_asyncio.apply()
max_bonus: int = 8
victory_modulo: int = 100
class StarcraftClientProcessor(ClientCommandProcessor):
@@ -98,13 +101,13 @@ class StarcraftClientProcessor(ClientCommandProcessor):
def _cmd_available(self) -> bool:
"""Get what missions are currently available to play"""
request_available_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui)
request_available_missions(self.ctx)
return True
def _cmd_unfinished(self) -> bool:
"""Get what missions are currently available to play and have not had all locations checked"""
request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx)
request_unfinished_missions(self.ctx)
return True
@mark_raw
@@ -125,18 +128,19 @@ class SC2Context(CommonContext):
items_handling = 0b111
difficulty = -1
all_in_choice = 0
mission_req_table = None
items_rec_to_announce = []
rec_announce_pos = 0
items_sent_to_announce = []
sent_announce_pos = 0
announcements = []
announcement_pos = 0
mission_req_table: typing.Dict[str, MissionInfo] = {}
announcements = queue.Queue()
sc2_run_task: typing.Optional[asyncio.Task] = None
missions_unlocked = False
missions_unlocked: bool = False # allow launching missions ignoring requirements
current_tooltip = None
last_loc_list = None
difficulty_override = -1
mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {}
last_bot: typing.Optional[ArchipelagoBot] = None
def __init__(self, *args, **kwargs):
super(SC2Context, self).__init__(*args, **kwargs)
self.raw_text_parser = RawJSONtoTextParser(self)
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
@@ -149,30 +153,35 @@ class SC2Context(CommonContext):
self.difficulty = args["slot_data"]["game_difficulty"]
self.all_in_choice = args["slot_data"]["all_in_map"]
slot_req_table = args["slot_data"]["mission_req"]
self.mission_req_table = {}
# Compatibility for 0.3.2 server data.
if "category" not in next(iter(slot_req_table)):
for i, mission_data in enumerate(slot_req_table.values()):
mission_data["category"] = wol_default_categories[i]
for mission in slot_req_table:
self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission])
self.mission_req_table = {
mission: MissionInfo(**slot_req_table[mission]) for mission in slot_req_table
}
self.build_location_to_mission_mapping()
# Look for and set SC2PATH.
# check_game_install_path() returns True if and only if it finds + sets SC2PATH.
if "SC2PATH" not in os.environ and check_game_install_path():
check_mod_install()
if cmd in {"PrintJSON"}:
if "receiving" in args:
if self.slot_concerns_self(args["receiving"]):
self.announcements.append(args["data"])
return
if "item" in args:
if self.slot_concerns_self(args["item"].player):
self.announcements.append(args["data"])
def on_print_json(self, args: dict):
# goes to this world
if "receiving" in args and self.slot_concerns_self(args["receiving"]):
relevant = True
# found in this world
elif "item" in args and self.slot_concerns_self(args["item"].player):
relevant = True
# not related
else:
relevant = False
if relevant:
self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"])))
super(SC2Context, self).on_print_json(args)
def run_gui(self):
from kvui import GameManager, HoverBehavior, ServerToolTip, fade_in_animation
from kvui import GameManager, HoverBehavior, ServerToolTip
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.tabbedpanel import TabbedPanelItem
@@ -190,6 +199,7 @@ class SC2Context(CommonContext):
class MissionButton(HoverableButton):
tooltip_text = StringProperty("Test")
ctx: SC2Context
def __init__(self, *args, **kwargs):
super(HoverableButton, self).__init__(*args, **kwargs)
@@ -210,10 +220,7 @@ class SC2Context(CommonContext):
self.ctx.current_tooltip = self.layout
def on_leave(self):
if self.ctx.current_tooltip:
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
self.ctx.current_tooltip = None
self.ctx.ui.clear_tooltip()
@property
def ctx(self) -> CommonContext:
@@ -235,13 +242,20 @@ class SC2Context(CommonContext):
mission_panel = None
last_checked_locations = {}
mission_id_to_button = {}
launching = False
launching: typing.Union[bool, int] = False # if int -> mission ID
refresh_from_launching = True
first_check = True
ctx: SC2Context
def __init__(self, ctx):
super().__init__(ctx)
def clear_tooltip(self):
if self.ctx.current_tooltip:
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
self.ctx.current_tooltip = None
def build(self):
container = super().build()
@@ -256,7 +270,7 @@ class SC2Context(CommonContext):
def build_mission_table(self, dt):
if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or
not self.refresh_from_launching)) or self.first_check:
not self.refresh_from_launching)) or self.first_check:
self.refresh_from_launching = True
self.mission_panel.clear_widgets()
@@ -267,12 +281,7 @@ class SC2Context(CommonContext):
self.mission_id_to_button = {}
categories = {}
available_missions = []
unfinished_locations = initialize_blank_mission_dict(self.ctx.mission_req_table)
unfinished_missions = calc_unfinished_missions(self.ctx.checked_locations,
self.ctx.mission_req_table,
self.ctx, available_missions=available_missions,
unfinished_locations=unfinished_locations)
available_missions, unfinished_missions = calc_unfinished_missions(self.ctx)
# separate missions into categories
for mission in self.ctx.mission_req_table:
@@ -283,34 +292,40 @@ class SC2Context(CommonContext):
for category in categories:
category_panel = MissionCategory()
category_panel.add_widget(Label(text=category, size_hint_y=None, height=50, outline_width=1))
category_panel.add_widget(
Label(text=category, size_hint_y=None, height=50, outline_width=1))
# Map is completed
for mission in categories[category]:
text = mission
tooltip = ""
text: str = mission
tooltip: str = ""
# Map has uncollected locations
if mission in unfinished_missions:
text = f"[color=6495ED]{text}[/color]"
tooltip = f"Uncollected locations:\n"
tooltip += "\n".join(location for location in unfinished_locations[mission])
elif mission in available_missions:
text = f"[color=FFFFFF]{text}[/color]"
# Map requirements not met
else:
text = f"[color=a9a9a9]{text}[/color]"
tooltip = f"Requires: "
if len(self.ctx.mission_req_table[mission].required_world) > 0:
tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission-1] for
if self.ctx.mission_req_table[mission].required_world:
tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for
req_mission in
self.ctx.mission_req_table[mission].required_world)
if self.ctx.mission_req_table[mission].number > 0:
if self.ctx.mission_req_table[mission].number:
tooltip += " and "
if self.ctx.mission_req_table[mission].number > 0:
if self.ctx.mission_req_table[mission].number:
tooltip += f"{self.ctx.mission_req_table[mission].number} missions completed"
remaining_location_names: typing.List[str] = [
self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission)
if loc in self.ctx.missing_locations]
if remaining_location_names:
if tooltip:
tooltip += "\n"
tooltip += f"Uncollected locations:\n"
tooltip += "\n".join(remaining_location_names)
mission_button = MissionButton(text=text, size_hint_y=None, height=50)
mission_button.tooltip_text = tooltip
@@ -325,13 +340,16 @@ class SC2Context(CommonContext):
self.refresh_from_launching = False
self.mission_panel.clear_widgets()
self.mission_panel.add_widget(Label(text="Launching Mission"))
self.mission_panel.add_widget(Label(text="Launching Mission: " +
lookup_id_to_mission[self.launching]))
if self.ctx.ui:
self.ctx.ui.clear_tooltip()
def mission_callback(self, button):
if not self.launching:
self.ctx.play_mission(list(self.mission_id_to_button.keys())
[list(self.mission_id_to_button.values()).index(button)])
self.launching = True
mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button)
self.ctx.play_mission(mission_id)
self.launching = mission_id
Clock.schedule_once(self.finish_launching, 10)
def finish_launching(self, dt):
@@ -344,12 +362,14 @@ class SC2Context(CommonContext):
async def shutdown(self):
await super(SC2Context, self).shutdown()
if self.last_bot:
self.last_bot.want_close = True
if self.sc2_run_task:
self.sc2_run_task.cancel()
def play_mission(self, mission_id):
def play_mission(self, mission_id: int):
if self.missions_unlocked or \
is_mission_available(mission_id, self.checked_locations, self.mission_req_table):
is_mission_available(self, mission_id):
if self.sc2_run_task:
if not self.sc2_run_task.done():
sc2_logger.warning("Starcraft 2 Client is still running!")
@@ -358,12 +378,29 @@ class SC2Context(CommonContext):
sc2_logger.warning("Launching Mission without Archipelago authentication, "
"checks will not be registered to server.")
self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id),
name="Starcraft 2 Launch")
name="Starcraft 2 Launch")
else:
sc2_logger.info(
f"{lookup_id_to_mission[mission_id]} is not currently unlocked. "
f"Use /unfinished or /available to see what is available.")
def build_location_to_mission_mapping(self):
mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = {
mission_info.id: set() for mission_info in self.mission_req_table.values()
}
for loc in self.server_locations:
mission_id, objective = divmod(loc - SC2WOL_LOC_ID_OFFSET, victory_modulo)
mission_id_to_location_ids[mission_id].add(objective)
self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in
mission_id_to_location_ids.items()}
def locations_for_mission(self, mission: str):
mission_id: int = self.mission_req_table[mission].id
objectives = self.mission_id_to_location_ids[self.mission_req_table[mission].id]
for objective in objectives:
yield SC2WOL_LOC_ID_OFFSET + mission_id * 100 + objective
async def main():
multiprocessing.freeze_support()
@@ -403,47 +440,27 @@ wol_default_categories = [
]
def calculate_items(items):
unit_unlocks = 0
armory1_unlocks = 0
armory2_unlocks = 0
upgrade_unlocks = 0
building_unlocks = 0
merc_unlocks = 0
lab_unlocks = 0
protoss_unlock = 0
minerals = 0
vespene = 0
supply = 0
def calculate_items(items: typing.List[NetUtils.NetworkItem]) -> typing.List[int]:
network_item: NetUtils.NetworkItem
accumulators: typing.List[int] = [0 for _ in type_flaggroups]
for item in items:
data = lookup_id_to_name[item.item]
for network_item in items:
name: str = lookup_id_to_name[network_item.item]
item_data: ItemData = item_table[name]
if item_table[data].type == "Unit":
unit_unlocks += (1 << item_table[data].number)
elif item_table[data].type == "Upgrade":
upgrade_unlocks += (1 << item_table[data].number)
elif item_table[data].type == "Armory 1":
armory1_unlocks += (1 << item_table[data].number)
elif item_table[data].type == "Armory 2":
armory2_unlocks += (1 << item_table[data].number)
elif item_table[data].type == "Building":
building_unlocks += (1 << item_table[data].number)
elif item_table[data].type == "Mercenary":
merc_unlocks += (1 << item_table[data].number)
elif item_table[data].type == "Laboratory":
lab_unlocks += (1 << item_table[data].number)
elif item_table[data].type == "Protoss":
protoss_unlock += (1 << item_table[data].number)
elif item_table[data].type == "Minerals":
minerals += item_table[data].number
elif item_table[data].type == "Vespene":
vespene += item_table[data].number
elif item_table[data].type == "Supply":
supply += item_table[data].number
# exists exactly once
if item_data.quantity == 1:
accumulators[type_flaggroups[item_data.type]] |= 1 << item_data.number
return [unit_unlocks, upgrade_unlocks, armory1_unlocks, armory2_unlocks, building_unlocks, merc_unlocks,
lab_unlocks, protoss_unlock, minerals, vespene, supply]
# exists multiple times
elif item_data.type == "Upgrade":
accumulators[type_flaggroups[item_data.type]] += 1 << item_data.number
# sum
else:
accumulators[type_flaggroups[item_data.type]] += item_data.number
return accumulators
def calc_difficulty(difficulty):
@@ -459,11 +476,7 @@ def calc_difficulty(difficulty):
return 'X'
async def starcraft_launch(ctx: SC2Context, mission_id):
ctx.rec_announce_pos = len(ctx.items_rec_to_announce)
ctx.sent_announce_pos = len(ctx.items_sent_to_announce)
ctx.announcements_pos = len(ctx.announcements)
async def starcraft_launch(ctx: SC2Context, mission_id: int):
sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
with DllDirectory(None):
@@ -472,32 +485,34 @@ async def starcraft_launch(ctx: SC2Context, mission_id):
class ArchipelagoBot(sc2.bot_ai.BotAI):
game_running = False
mission_completed = False
first_bonus = False
second_bonus = False
third_bonus = False
fourth_bonus = False
fifth_bonus = False
sixth_bonus = False
seventh_bonus = False
eight_bonus = False
ctx: SC2Context = None
mission_id = 0
game_running: bool = False
mission_completed: bool = False
boni: typing.List[bool]
setup_done: bool
ctx: SC2Context
mission_id: int
want_close: bool = False
can_read_game = False
last_received_update = 0
last_received_update: int = 0
def __init__(self, ctx: SC2Context, mission_id):
self.setup_done = False
self.ctx = ctx
self.ctx.last_bot = self
self.mission_id = mission_id
self.boni = [False for _ in range(max_bonus)]
super(ArchipelagoBot, self).__init__()
async def on_step(self, iteration: int):
if self.want_close:
self.want_close = False
await self._client.leave()
return
game_state = 0
if iteration == 0:
if not self.setup_done:
self.setup_done = True
start_items = calculate_items(self.ctx.items_received)
if self.ctx.difficulty_override >= 0:
difficulty = calc_difficulty(self.ctx.difficulty_override)
@@ -511,36 +526,10 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
self.last_received_update = len(self.ctx.items_received)
else:
if self.ctx.announcement_pos < len(self.ctx.announcements):
index = 0
message = ""
while index < len(self.ctx.announcements[self.ctx.announcement_pos]):
message += self.ctx.announcements[self.ctx.announcement_pos][index]["text"]
index += 1
index = 0
start_rem_pos = -1
# Remove unneeded [Color] tags
while index < len(message):
if message[index] == '[':
start_rem_pos = index
index += 1
elif message[index] == ']' and start_rem_pos > -1:
temp_msg = ""
if start_rem_pos > 0:
temp_msg = message[:start_rem_pos]
if index < len(message) - 1:
temp_msg += message[index + 1:]
message = temp_msg
index += start_rem_pos - index
start_rem_pos = -1
else:
index += 1
if not self.ctx.announcements.empty():
message = self.ctx.announcements.get(timeout=1)
await self.chat_send("SendMessage " + message)
self.ctx.announcement_pos += 1
self.ctx.announcements.task_done()
# Archipelago reads the health
for unit in self.all_own_units():
@@ -568,169 +557,97 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
if game_state & (1 << 1) and not self.mission_completed:
if self.mission_id != 29:
print("Mission Completed")
await self.ctx.send_msgs([
{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id]}])
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id]}])
self.mission_completed = True
else:
print("Game Complete")
await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}])
self.mission_completed = True
if game_state & (1 << 2) and not self.first_bonus:
print("1st Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 1]}])
self.first_bonus = True
if not self.second_bonus and game_state & (1 << 3):
print("2nd Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 2]}])
self.second_bonus = True
if not self.third_bonus and game_state & (1 << 4):
print("3rd Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 3]}])
self.third_bonus = True
if not self.fourth_bonus and game_state & (1 << 5):
print("4th Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 4]}])
self.fourth_bonus = True
if not self.fifth_bonus and game_state & (1 << 6):
print("5th Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 5]}])
self.fifth_bonus = True
if not self.sixth_bonus and game_state & (1 << 7):
print("6th Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 6]}])
self.sixth_bonus = True
if not self.seventh_bonus and game_state & (1 << 8):
print("6th Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 7]}])
self.seventh_bonus = True
if not self.eight_bonus and game_state & (1 << 9):
print("6th Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 8]}])
self.eight_bonus = True
for x, completed in enumerate(self.boni):
if not completed and game_state & (1 << (x + 2)):
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id + x + 1]}])
self.boni[x] = True
else:
await self.chat_send("LostConnection - Lost connection to game.")
def calc_objectives_completed(mission, missions_info, locations_done, unfinished_locations, ctx):
objectives_complete = 0
if missions_info[mission].extra_locations > 0:
for i in range(missions_info[mission].extra_locations):
if (missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i) in locations_done:
objectives_complete += 1
else:
unfinished_locations[mission].append(ctx.location_names[
missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i])
return objectives_complete
else:
return -1
def request_unfinished_missions(locations_done, location_table, ui, ctx):
if location_table:
def request_unfinished_missions(ctx: SC2Context):
if ctx.mission_req_table:
message = "Unfinished Missions: "
unlocks = initialize_blank_mission_dict(location_table)
unfinished_locations = initialize_blank_mission_dict(location_table)
unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
unfinished_locations = initialize_blank_mission_dict(ctx.mission_req_table)
unfinished_missions = calc_unfinished_missions(locations_done, location_table, ctx, unlocks=unlocks,
unfinished_locations=unfinished_locations)
_, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks)
message += ", ".join(f"{mark_up_mission_name(mission, location_table, ui,unlocks)}[{location_table[mission].id}] " +
message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " +
mark_up_objectives(
f"[{unfinished_missions[mission]}/{location_table[mission].extra_locations}]",
f"[{len(unfinished_missions[mission])}/"
f"{sum(1 for _ in ctx.locations_for_mission(mission))}]",
ctx, unfinished_locations, mission)
for mission in unfinished_missions)
if ui:
ui.log_panels['All'].on_message_markup(message)
ui.log_panels['Starcraft2'].on_message_markup(message)
if ctx.ui:
ctx.ui.log_panels['All'].on_message_markup(message)
ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
else:
sc2_logger.info(message)
else:
sc2_logger.warning("No mission table found, you are likely not connected to a server.")
def calc_unfinished_missions(locations_done, locations, ctx, unlocks=None, unfinished_locations=None,
available_missions=[]):
def calc_unfinished_missions(ctx: SC2Context, unlocks=None):
unfinished_missions = []
locations_completed = []
if not unlocks:
unlocks = initialize_blank_mission_dict(locations)
unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
if not unfinished_locations:
unfinished_locations = initialize_blank_mission_dict(locations)
if len(available_missions) > 0:
available_missions = []
available_missions.extend(calc_available_missions(locations_done, locations, unlocks))
available_missions = calc_available_missions(ctx, unlocks)
for name in available_missions:
if not locations[name].extra_locations == -1:
objectives_completed = calc_objectives_completed(name, locations, locations_done, unfinished_locations, ctx)
if objectives_completed < locations[name].extra_locations:
objectives = set(ctx.locations_for_mission(name))
if objectives:
objectives_completed = ctx.checked_locations & objectives
if len(objectives_completed) < len(objectives):
unfinished_missions.append(name)
locations_completed.append(objectives_completed)
else:
else: # infer that this is the final mission as it has no objectives
unfinished_missions.append(name)
locations_completed.append(-1)
return {unfinished_missions[i]: locations_completed[i] for i in range(len(unfinished_missions))}
return available_missions, dict(zip(unfinished_missions, locations_completed))
def is_mission_available(mission_id_to_check, locations_done, locations):
unfinished_missions = calc_available_missions(locations_done, locations)
def is_mission_available(ctx: SC2Context, mission_id_to_check):
unfinished_missions = calc_available_missions(ctx)
return any(mission_id_to_check == locations[mission].id for mission in unfinished_missions)
return any(mission_id_to_check == ctx.mission_req_table[mission].id for mission in unfinished_missions)
def mark_up_mission_name(mission, location_table, ui, unlock_table):
def mark_up_mission_name(ctx: SC2Context, mission, unlock_table):
"""Checks if the mission is required for game completion and adds '*' to the name to mark that."""
if location_table[mission].completion_critical:
if ui:
if ctx.mission_req_table[mission].completion_critical:
if ctx.ui:
message = "[color=AF99EF]" + mission + "[/color]"
else:
message = "*" + mission + "*"
else:
message = mission
if ui:
if ctx.ui:
unlocks = unlock_table[mission]
if len(unlocks) > 0:
pre_message = f"[ref={list(location_table).index(mission)}|Unlocks: "
pre_message += ", ".join(f"{unlock}({location_table[unlock].id})" for unlock in unlocks)
pre_message = f"[ref={list(ctx.mission_req_table).index(mission)}|Unlocks: "
pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[unlock].id})" for unlock in unlocks)
pre_message += f"]"
message = pre_message + message + "[/ref]"
@@ -743,7 +660,7 @@ def mark_up_objectives(message, ctx, unfinished_locations, mission):
if ctx.ui:
locations = unfinished_locations[mission]
pre_message = f"[ref={list(ctx.mission_req_table).index(mission)+30}|"
pre_message = f"[ref={list(ctx.mission_req_table).index(mission) + 30}|"
pre_message += "<br>".join(location for location in locations)
pre_message += f"]"
formatted_message = pre_message + message + "[/ref]"
@@ -751,90 +668,91 @@ def mark_up_objectives(message, ctx, unfinished_locations, mission):
return formatted_message
def request_available_missions(locations_done, location_table, ui):
if location_table:
def request_available_missions(ctx: SC2Context):
if ctx.mission_req_table:
message = "Available Missions: "
# Initialize mission unlock table
unlocks = initialize_blank_mission_dict(location_table)
unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
missions = calc_available_missions(locations_done, location_table, unlocks)
missions = calc_available_missions(ctx, unlocks)
message += \
", ".join(f"{mark_up_mission_name(mission, location_table, ui, unlocks)}[{location_table[mission].id}]"
", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}"
f"[{ctx.mission_req_table[mission].id}]"
for mission in missions)
if ui:
ui.log_panels['All'].on_message_markup(message)
ui.log_panels['Starcraft2'].on_message_markup(message)
if ctx.ui:
ctx.ui.log_panels['All'].on_message_markup(message)
ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
else:
sc2_logger.info(message)
else:
sc2_logger.warning("No mission table found, you are likely not connected to a server.")
def calc_available_missions(locations_done, locations, unlocks=None):
def calc_available_missions(ctx: SC2Context, unlocks=None):
available_missions = []
missions_complete = 0
# Get number of missions completed
for loc in locations_done:
if loc % 100 == 0:
for loc in ctx.checked_locations:
if loc % victory_modulo == 0:
missions_complete += 1
for name in locations:
for name in ctx.mission_req_table:
# Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips
if unlocks:
for unlock in locations[name].required_world:
unlocks[list(locations)[unlock-1]].append(name)
for unlock in ctx.mission_req_table[name].required_world:
unlocks[list(ctx.mission_req_table)[unlock - 1]].append(name)
if mission_reqs_completed(name, missions_complete, locations_done, locations):
if mission_reqs_completed(ctx, name, missions_complete):
available_missions.append(name)
return available_missions
def mission_reqs_completed(location_to_check, missions_complete, locations_done, locations):
def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete):
"""Returns a bool signifying if the mission has all requirements complete and can be done
Keyword arguments:
Arguments:
ctx -- instance of SC2Context
locations_to_check -- the mission string name to check
missions_complete -- an int of how many missions have been completed
locations_done -- a list of the location ids that have been complete
locations -- a dict of MissionInfo for mission requirements for this world"""
if len(locations[location_to_check].required_world) >= 1:
"""
if len(ctx.mission_req_table[mission_name].required_world) >= 1:
# A check for when the requirements are being or'd
or_success = False
# Loop through required missions
for req_mission in locations[location_to_check].required_world:
for req_mission in ctx.mission_req_table[mission_name].required_world:
req_success = True
# Check if required mission has been completed
if not (locations[list(locations)[req_mission-1]].id * 100 + SC2WOL_LOC_ID_OFFSET) in locations_done:
if not locations[location_to_check].or_requirements:
if not (ctx.mission_req_table[list(ctx.mission_req_table)[req_mission - 1]].id *
victory_modulo + SC2WOL_LOC_ID_OFFSET) in ctx.checked_locations:
if not ctx.mission_req_table[mission_name].or_requirements:
return False
else:
req_success = False
# Recursively check required mission to see if it's requirements are met, in case !collect has been done
if not mission_reqs_completed(list(locations)[req_mission-1], missions_complete, locations_done,
locations):
if not locations[location_to_check].or_requirements:
if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete):
if not ctx.mission_req_table[mission_name].or_requirements:
return False
else:
req_success = False
# If requirement check succeeded mark or as satisfied
if locations[location_to_check].or_requirements and req_success:
if ctx.mission_req_table[mission_name].or_requirements and req_success:
or_success = True
if locations[location_to_check].or_requirements:
if ctx.mission_req_table[mission_name].or_requirements:
# Return false if or requirements not met
if not or_success:
return False
# Check number of missions
if missions_complete >= locations[location_to_check].number:
if missions_complete >= ctx.mission_req_table[mission_name].number:
return True
else:
return False
@@ -875,7 +793,12 @@ def check_game_install_path() -> bool:
with open(einfo) as f:
content = f.read()
if content:
base = re.search(r" = (.*)Versions", content).group(1)
try:
base = re.search(r" = (.*)Versions", content).group(1)
except AttributeError:
sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, then "
f"try again.")
return False
if os.path.exists(base):
executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions")
@@ -892,7 +815,8 @@ def check_game_install_path() -> bool:
else:
sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.")
else:
sc2_logger.warning(f"Couldn't find {einfo}. Please run /set_path with your SC2 install directory.")
sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. "
f"If that fails, please run /set_path with your SC2 install directory.")
return False
@@ -929,7 +853,7 @@ class DllDirectory:
self.set(self._old)
@staticmethod
def get() -> str:
def get() -> typing.Optional[str]:
if sys.platform == "win32":
n = ctypes.windll.kernel32.GetDllDirectoryW(0, None)
buf = ctypes.create_unicode_buffer(n)

View File

@@ -35,7 +35,7 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.3.4"
__version__ = "0.3.5"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
@@ -619,7 +619,7 @@ def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset
def sorter(element: str) -> str:
parts = element.split(maxsplit=1)
if parts[0].lower() in ignore:
return parts[1]
return parts[1].lower()
else:
return element
return element.lower()
return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i))

View File

@@ -12,7 +12,7 @@ ModuleUpdate.update()
# in case app gets imported by something like gunicorn
import Utils
Utils.local_path.cached_path = os.path.dirname(__file__)
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
from WebHostLib import register, app as raw_app
from waitress import serve
@@ -104,7 +104,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
for games in data:
if 'Archipelago' in games['gameTitle']:
generic_data = data.pop(data.index(games))
sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"].lower())
sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"])
json.dump(sorted_data, json_target, indent=2, ensure_ascii=False)
return sorted_data

46
WebHostLib/README.md Normal file
View File

@@ -0,0 +1,46 @@
# WebHost
## Contribution Guidelines
**Thank you for your interest in contributing to the Archipelago website!**
Much of the content on the website is generated automatically, but there are some things
that need a personal touch. For those things, we rely on contributions from both the core
team and the community. The current primary maintainer of the website is Farrak Kilhn.
He may be found on Discord as `Farrak Kilhn#0418`, or on GitHub as `LegendaryLinux`.
### Small Changes
Little changes like adding a button or a couple new select elements are perfectly fine.
Tweaks to style specific to a PR's content are also probably not a problem. For example, if
you build a new page which needs two side by side tables, and you need to write a CSS file
specific to your page, that is perfectly reasonable.
### Content Additions
Once you develop a new feature or add new content the website, make a pull request. It will
be reviewed by the community and there will probably be some discussion around it. Depending
on the size of the feature, and if new styles are required, there may be an additional step
before the PR is accepted wherein Farrak works with the designer to implement styles.
### Restrictions on Style Changes
A professional designer is paid to develop the styles and assets for the Archipelago website.
In an effort to maintain a consistent look and feel, pull requests which *exclusively*
change site styles are rejected. Please note this applies to code which changes the overall
look and feel of the site, not to small tweaks to CSS for your custom page. The intention
behind these restrictions is to maintain a curated feel for the design of the site. If
any PR affects the overall feel of the site but includes additive changes, there will
likely be a conversation about how to implement those changes without compromising the
curated site style. It is therefore worth noting there are a couple files which, if
changed in your pull request, will cause it to draw additional scrutiny.
These closely guarded files are:
- `globalStyles.css`
- `islandFooter.css`
- `landing.css`
- `markdown.css`
- `tooltip.css`
### Site Themes
There are several themes available for game pages. It is possible to request a new theme in
the `#art-and-design` channel on Discord. Because themes are created by the designer, they
are not free, and take some time to create. Farrak works closely with the designer to implement
these themes, and pays for the assets out of pocket. Therefore, only a couple themes per year
are added. If a proposed theme seems like a cool idea and the community likes it, there is a
good chance it will become a reality.

View File

@@ -46,4 +46,4 @@ def get_datapackage_versions():
return version_package
from . import generate, user # trigger registration
from . import generate, user, tracker # trigger registration

50
WebHostLib/api/tracker.py Normal file
View File

@@ -0,0 +1,50 @@
import collections
from flask import jsonify
from typing import Optional, Dict, Any, Tuple, List
from Utils import restricted_loads
from uuid import UUID
from ..models import Room
from . import api_endpoints
from ..tracker import fill_tracker_data, get_static_room_data
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
from WebHostLib import cache
@api_endpoints.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
@cache.memoize(timeout=60)
def update_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
room: Optional[Room] = Room.get(tracker=tracker)
locations = get_static_room_data(room)[0]
items_counter: Dict[int, collections.Counter] = get_item_names_counter(locations)
player_tracker, multisave, inventory, seed_checks_in_area, lttp_checks_done, \
slot_data, games, player_name, display_icons = fill_tracker_data(room, tracked_team, tracked_player)
# convert numbers to string
for item in player_tracker.items_received:
if items_counter[tracked_player][item] == 1:
player_tracker.items_received[item] = ''
else:
player_tracker.items_received[item] = str(player_tracker.items_received[item])
return jsonify({
"items_received": player_tracker.items_received,
"checked_locations": list(sorted(player_tracker.checked_locations)),
"icons": display_icons,
"progressive_names": player_tracker.progressive_names
})
@cache.cached()
def get_item_names_counter(locations: Dict[int, Dict[int, Tuple[int, int, int]]]):
# create and fill dictionary of all progression items for players
items_counters: Dict[int, collections.Counter] = {}
for player in locations:
for location in locations[player]:
item, recipient, flags = locations[player][location]
item_name = lookup_any_item_id_to_name[item]
items_counters.setdefault(recipient, collections.Counter())[item_name] += 1
return items_counters

View File

@@ -1,15 +1,16 @@
from __future__ import annotations
import functools
import websockets
import asyncio
import collections
import datetime
import functools
import logging
import pickle
import random
import socket
import threading
import time
import random
import pickle
import logging
import datetime
import websockets
import Utils
from .models import db_session, Room, select, commit, Command, db
@@ -49,6 +50,8 @@ class DBCommandProcessor(ServerCommandProcessor):
class WebHostContext(Context):
room_id: int
def __init__(self, static_server_data: dict):
# static server data is used during _load_game_data to load required data,
# without needing to import worlds system, which takes quite a bit of memory
@@ -62,6 +65,8 @@ class WebHostContext(Context):
def _load_game_data(self):
for key, value in self.static_server_data.items():
setattr(self, key, value)
self.forced_auto_forfeits = collections.defaultdict(lambda: False, self.forced_auto_forfeits)
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
def listen_to_db_commands(self):
cmdprocessor = DBCommandProcessor(self)
@@ -103,7 +108,7 @@ class WebHostContext(Context):
room.multisave = pickle.dumps(self.get_save())
# saving only occurs on activity, so we can "abuse" this information to mark this as last_activity
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
room.last_activity = datetime.utcnow()
room.last_activity = datetime.datetime.utcnow()
return True
def get_save(self) -> dict:

View File

@@ -32,9 +32,12 @@ def download_patch(room_id, patch_id):
new_zip.writestr("archipelago.json", json.dumps(manifest))
else:
new_zip.writestr(file.filename, zf.read(file), file.compress_type, 9)
if "patch_file_ending" in manifest:
patch_file_ending = manifest["patch_file_ending"]
else:
patch_file_ending = AutoPatchRegister.patch_types[patch.game].patch_file_ending
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}" \
f"{AutoPatchRegister.patch_types[patch.game].patch_file_ending}"
f"{patch_file_ending}"
new_file.seek(0)
return send_file(new_file, as_attachment=True, download_name=fname)
else:

View File

@@ -1,6 +1,6 @@
import logging
import os
from Utils import __version__
from Utils import __version__, local_path
from jinja2 import Template
import yaml
import json
@@ -9,14 +9,13 @@ import typing
from worlds.AutoWorld import AutoWorldRegister
import Options
target_folder = os.path.join("WebHostLib", "static", "generated")
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
"exclude_locations"}
def create():
os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True)
target_folder = local_path("WebHostLib", "static", "generated")
os.makedirs(os.path.join(target_folder, "configs"), exist_ok=True)
def dictify_range(option: typing.Union[Options.Range, Options.SpecialRange]):
data = {}
@@ -49,6 +48,11 @@ def create():
return list(default_value)
return default_value
def get_html_doc(option_type: type(Options.Option)) -> str:
if not option_type.__doc__:
return "Please document me!"
return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip()
weighted_settings = {
"baseOptions": {
"description": "Generated by https://archipelago.gg/",
@@ -61,12 +65,16 @@ def create():
for game_name, world in AutoWorldRegister.world_types.items():
all_options = {**Options.per_game_common_options, **world.option_definitions}
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
with open(local_path("WebHostLib", "templates", "options.yaml")) as f:
file_data = f.read()
res = Template(file_data).render(
options=all_options,
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
dictify_range=dictify_range, default_converter=default_converter,
)
del file_data
with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
f.write(res)
@@ -88,7 +96,7 @@ def create():
game_options[option_name] = this_option = {
"type": "select",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!",
"description": get_html_doc(option),
"defaultValue": None,
"options": []
}
@@ -114,7 +122,7 @@ def create():
game_options[option_name] = {
"type": "range",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!",
"description": get_html_doc(option),
"defaultValue": option.default if hasattr(
option, "default") and option.default != "random" else option.range_start,
"min": option.range_start,
@@ -131,14 +139,14 @@ def create():
game_options[option_name] = {
"type": "items-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!",
"description": get_html_doc(option),
}
elif getattr(option, "verify_location_name", False):
game_options[option_name] = {
"type": "locations-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!",
"description": get_html_doc(option),
}
elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet):
@@ -146,7 +154,7 @@ def create():
game_options[option_name] = {
"type": "custom-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!",
"description": get_html_doc(option),
"options": list(option.valid_keys),
}

View File

@@ -1,7 +1,7 @@
flask>=2.1.3
flask>=2.2.2
pony>=0.7.16
waitress>=2.1.1
waitress>=2.1.2
Flask-Caching>=2.0.1
Flask-Compress>=1.12
Flask-Limiter>=2.5.0
Flask-Limiter>=2.6.2
bokeh>=2.4.3

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

@@ -102,9 +102,15 @@ const buildOptionsTable = (settings, romOpts = false) => {
// td Left
const tdl = document.createElement('td');
const label = document.createElement('label');
label.textContent = `${settings[setting].displayName}: `;
label.setAttribute('for', setting);
label.setAttribute('data-tooltip', settings[setting].description);
label.innerText = `${settings[setting].displayName}:`;
const questionSpan = document.createElement('span');
questionSpan.classList.add('interactive');
questionSpan.setAttribute('data-tooltip', settings[setting].description);
questionSpan.innerText = '(?)';
label.appendChild(questionSpan);
tdl.appendChild(label);
tr.appendChild(tdl);

View File

@@ -0,0 +1,82 @@
window.addEventListener('load', () => {
// Reload tracker
const update = () => {
const room = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
const request = new Request('/api/tracker/' + room);
fetch(request)
.then(response => response.json())
.then(data => {
// update locations blocks
for (const location of data.checked_locations) {
document.getElementById(location).classList.add('acquired');
}
// update totals checks done
let total_checks_ele = document.getElementById('total-checks');
const total_checks = document.getElementsByClassName('location').length;
let checks_done = data.checked_locations.length;
total_checks_ele.innerText = 'Total Checks Done: ' + checks_done + '/' + total_checks;
// update item and icons blocks
// update icons block
if (data.icons.length > 0) {
for (let item in data.icons) {
if (data.progressive_names.length > 0) {
for (let item_category in data.progressive_names) {
let i = 0;
for (let current_item in current_name) {
if (current_item === item) {
let doc_item = document.getElementById(item_category)
doc_item.children[0].src = data.icons[item];
if (item in data.items_received) {
doc_item.children[0].classList.add('acquired');
doc_item.children[1].innerText = item_category;
}
}
}
}
} else {
if (item in data.items_received) {
let current_item = document.getElementById(item);
current_item.children[0].classList.add('acquired');
current_item.children[0].src = data.icons[item];
current_item.children[1].innerText = item;
}
}
}
} else {
for (const item in data.items_received) {
if (document.getElementById(item)) {
let current_item = document.getElementById(item);
current_item.innerText = item + data.items_received[item];
}
}
}
});
}
update()
setInterval(update, 30000);
// Collapsible regions section
const regions = document.getElementsByClassName('regions-column');
for (let i = 0; i < regions.length; i++) {
let region_name = regions[i].id;
const tab_header = document.getElementById(region_name+'-header');
const locations = document.getElementById(region_name+'-locations');
// toggle locations display
regions[i].addEventListener('click', function(event) {
if (tab_header.innerHTML.includes("▼")) {
locations.classList.remove('hidden');
// change header text
tab_header.innerHTML = tab_header.innerHTML.replace('▼', '▲');
} else {
locations.classList.add('hidden');
// change header text
tab_header.innerHTML = tab_header.innerHTML.replace('▲', '▼');
}
});
}
});

View File

@@ -0,0 +1,82 @@
window.addEventListener('load', () => {
// Reload tracker
const update = () => {
const room = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
const request = new Request('/api/tracker/' + room);
fetch(request)
.then(response => response.json())
.then(data => {
// update locations blocks
for (const location of data.checked_locations) {
document.getElementById(location).classList.add('acquired');
}
// update totals checks done
let total_checks_ele = document.getElementById('total-checks');
const total_checks = document.getElementsByClassName('location').length;
let checks_done = data.checked_locations.length;
total_checks_ele.innerText = 'Total Checks Done: ' + checks_done + '/' + total_checks;
// update item and icons blocks
// update icons block
if (data.icons.length > 0) {
for (let item in data.icons) {
if (data.progressive_names.length > 0) {
for (let item_category in data.progressive_names) {
let i = 0;
for (let current_item in current_name) {
if (current_item === item) {
let doc_item = document.getElementById(item_category)
doc_item.children[0].src = data.icons[item];
if (item in data.items_received) {
doc_item.children[0].classList.add('acquired');
doc_item.children[1].innerText = item_category;
}
}
}
}
} else {
if (item in data.items_received) {
let current_item = document.getElementById(item);
current_item.children[0].classList.add('acquired');
current_item.children[0].src = data.icons[item];
current_item.children[1].innerText = item;
}
}
}
} else {
for (const item in data.items_received) {
if (document.getElementById(item)) {
let current_item = document.getElementById(item);
current_item.innerText = item + data.items_received[item];
}
}
}
});
}
update()
setInterval(update, 30000);
// Collapsible regions section
const regions = document.getElementsByClassName('regions-column');
for (let i = 0; i < regions.length; i++) {
let region_name = regions[i].id;
const tab_header = document.getElementById(region_name+'-header');
const locations = document.getElementById(region_name+'-locations');
// toggle locations display
regions[i].addEventListener('click', function(event) {
if (tab_header.innerHTML.includes("▼")) {
locations.classList.remove('hidden');
// change header text
tab_header.innerHTML = tab_header.innerHTML.replace('▼', '▲');
} else {
locations.classList.add('hidden');
// change header text
tab_header.innerHTML = tab_header.innerHTML.replace('▲', '▼');
}
});
}
});

View File

@@ -56,7 +56,3 @@
#file-input{
display: none;
}
.interactive{
color: #ffef00;
}

View File

@@ -105,3 +105,7 @@ h5, h6{
margin-bottom: 20px;
background-color: #ffff00;
}
.interactive{
color: #ffef00;
}

View File

@@ -14,7 +14,6 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
/* Base styles for the element that has a tooltip */
[data-tooltip], .tooltip {
position: relative;
cursor: pointer;
}
/* Base styles for the entire tooltip */
@@ -55,14 +54,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
/** Content styles */
.tooltip:after, [data-tooltip]:after {
width: 260px;
z-index: 10000;
padding: 8px;
width: 160px;
border-radius: 4px;
background-color: #000;
background-color: hsla(0, 0%, 20%, 0.9);
color: #fff;
content: attr(data-tooltip);
white-space: pre-wrap;
font-size: 14px;
line-height: 1.2;
}

View File

@@ -0,0 +1,150 @@
/* CSS Overrides */
.dirt-wrapper{
background-color: #897249;
}
.dirt-wrapper h1{}
.grass-wrapper{
background-color: #3fb24a;
}
.grass-wrapper h1{}
.grassFlowers-wrapper{
background-color: #3fb24a;
}
.grassFlowers-wrapper h1{}
.ice-wrapper{
background-color: #afe0ef;
}
.ice-wrapper h1{}
.jungle-wrapper{
background-color: #2a7808;
}
.jungle-wrapper h1{}
.ocean-wrapper{
background-color: #3667b1;
}
.ocean-wrapper h1{}
.partyTime-wrapper{
background-color: #3a0f69;
color: #ffffff;
}
.partyTime-wrapper h1{}
/* Actual Styles */
h1 {
font-size: 20px;
color: #ffffff;
padding: 5px;
text-align: center;
text-shadow: 1px 1px black;
}
h2 {
padding: 8px;
}
#player-keys-tracker{
width: 600px;
}
#items-container{
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
padding: 5px;
}
#items-container div{
margin: 0;
padding: 0;
}
.image-container{
display: absolute;
height: 75px;
width: 75px;
}
.bottom-text{
position: relative;
align-items: bottom;
text-align: center;
}
.icon{
height: 100%;
position: relative;
left: 15px;
max-width: 45px;
max-height: 45px;
filter: grayscale(100%) contrast(75%) brightness(40%);
}
.icon.acquired{
filter: none;
}
.total-checks{
text-align: center;
padding: 5px;
font-size: 18px;
}
.locations-container{
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 5px;
margin-left: 50px;
margin-right: 50px;
}
.location.acquired{
text-decoration: line-through;
filter: none;
}
.regions-container{
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: space-evenly;
padding: 5px;
text-align: center;
}
.regions-header{
font-size: 18px;
padding: 15px;
cursor: pointer;
text-align: center;
}
.hidden{
display: none;
}
.button-link{
display: block;
width: 100%;
height: 30px;
text-align: center;
text-decoration: none;
line-height: 30px;
background-color: lightgrey;
cursor: pointer;
color: inherit;
}

View File

@@ -51,6 +51,17 @@ table.dataTable{
color: #000000;
}
table.dataTable img.icon{
height: 100%;
max-width: 60px;
max-height: 60px;
filter: grayscale(100%) contrast(75%) brightness(50%);
}
table.dataTable img.acquired{
filter: none;
}
table.dataTable thead{
font-family: LexendDeca-Regular, sans-serif;
}

View File

@@ -41,12 +41,11 @@
<tbody>
<tr>
<td>
<label for="forfeit_mode">Forfeit Permission:</label>
<span
class="interactive"
data-tooltip="A forfeit releases all remaining items from the locations
in your world.">(?)
</span>
<label for="forfeit_mode">Forfeit Permission:
<span class="interactive" data-tooltip="A forfeit releases all remaining items from the locations in your world.">
(?)
</span>
</label>
</td>
<td>
<select name="forfeit_mode" id="forfeit_mode">
@@ -63,12 +62,11 @@
<tr>
<td>
<label for="collect_mode">Collect Permission:</label>
<span
class="interactive"
data-tooltip="A collect releases all of your remaining items to you
from across the multiworld.">(?)
</span>
<label for="collect_mode">Collect Permission:
<span class="interactive" data-tooltip="A collect releases all of your remaining items to you from across the multiworld.">
(?)
</span>
</label>
</td>
<td>
<select name="collect_mode" id="collect_mode">
@@ -85,12 +83,11 @@
<tr>
<td>
<label for="remaining_mode">Remaining Permission:</label>
<span
class="interactive"
data-tooltip="Remaining lists all items still in your world by name only."
>(?)
</span>
<label for="remaining_mode">Remaining Permission:
<span class="interactive" data-tooltip="Remaining lists all items still in your world by name only.">
(?)
</span>
</label>
</td>
<td>
<select name="remaining_mode" id="remaining_mode">
@@ -106,11 +103,11 @@
</tr>
<tr>
<td>
<label for="item_cheat">Item Cheat:</label>
<span
class="interactive"
data-tooltip="Allows players to use the !getitem command.">(?)
</span>
<label for="item_cheat">Item Cheat:
<span class="interactive" data-tooltip="Allows players to use the !getitem command.">
(?)
</span>
</label>
</td>
<td>
<select name="item_cheat" id="item_cheat">
@@ -131,12 +128,11 @@
<tbody>
<tr>
<td>
<label for="hint_cost"> Hint Cost:</label>
<span
class="interactive"
data-tooltip="After gathering this many checks, players can !hint <itemname>
to get the location of that hint item.">(?)
</span>
<label for="hint_cost"> Hint Cost:
<span class="interactive" data-tooltip="After gathering this many checks, players can !hint <itemname> to get the location of that hint item.">
(?)
</span>
</label>
</td>
<td>
<select name="hint_cost" id="hint_cost">
@@ -150,11 +146,11 @@
</tr>
<tr>
<td>
<label for="server_password">Server Password:</label>
<span
class="interactive"
data-tooltip="Allows for issuing of server console commands from any text client or in-game client using the !admin command.">(?)
</span>
<label for="server_password">Server Password:
<span class="interactive" data-tooltip="Allows for issuing of server console commands from any text client or in-game client using the !admin command.">
(?)
</span>
</label>
</td>
<td>
<input id="server_password" name="server_password">
@@ -162,23 +158,22 @@
</tr>
<tr>
<td>
<label for="plando_options">Plando Options:</label>
<span
class="interactive"
data-tooltip="Allows players to plan some of the randomization. See the 'Archipelago Plando Guide' in 'Setup Guides' for more information.">(?)
Plando Options:
<span class="interactive" data-tooltip="Allows players to plan some of the randomization. See the 'Archipelago Plando Guide' in 'Setup Guides' for more information.">
(?)
</span>
</td>
<td>
<input type="checkbox" name="plando_bosses" value="bosses" checked>
<input type="checkbox" id="plando_bosses" name="plando_bosses" value="bosses" checked>
<label for="plando_bosses">Bosses</label><br>
<input type="checkbox" name="plando_items" value="items" checked>
<input type="checkbox" id="plando_items" name="plando_items" value="items" checked>
<label for="plando_items">Items</label><br>
<input type="checkbox" name="plando_connections" value="connections" checked>
<input type="checkbox" id="plando_connections" name="plando_connections" value="connections" checked>
<label for="plando_connections">Connections</label><br>
<input type="checkbox" name="plando_texts" value="texts" checked>
<input type="checkbox" id="plando_texts" name="plando_texts" value="texts" checked>
<label for="plando_texts">Text</label>
</td>
</tr>

View File

@@ -6,8 +6,6 @@
-
<a href="https://github.com/ArchipelagoMW/Archipelago">Source Code</a>
-
<a href="https://github.com/ArchipelagoMW/Archipelago/wiki">Wiki</a>
-
<a href="https://github.com/ArchipelagoMW/Archipelago/graphs/contributors">Contributors</a>
-
<a href="https://github.com/ArchipelagoMW/Archipelago/issues">Bug Report</a>

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

@@ -50,7 +50,7 @@
No file to download for this game.
{% endif %}
</td>
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker, tracked_team=0, tracked_player=patch.player_id) }}">Tracker</a></td>
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=0, tracked_player=patch.player_id) }}">Tracker</a></td>
</tr>
{% endfor %}
</tbody>

View File

@@ -1,7 +1,7 @@
{% extends 'pageWrapper.html' %}
{% block head %}
<title>Player Settings</title>
<title>Supported Games</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/supportedGames.css") }}" />
{% endblock %}

View File

@@ -2,9 +2,9 @@
{% block head %}
{{ super() }}
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/trackers/tracker.css") }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackers/tracker.js") }}"></script>
{% endblock %}
{% block body %}
@@ -13,6 +13,9 @@
<div id="tracker-header-bar">
<input placeholder="Search" id="search"/>
<span class="info">This tracker will automatically update itself periodically.</span>
<a href="/tracker/{{ room.tracker|suuid }}/{{ team }}/{{ player }}" class="button-link">
Go to Styled Tracker
</a>
</div>
<div class="table-wrapper">
<table class="table non-unique-item-table">

View File

@@ -2,8 +2,8 @@
<html lang="en">
<head>
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/minecraftTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/minecraftTracker.js') }}"></script>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/minecraftTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/minecraftTracker.js') }}"></script>
<link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/minecraftia" type="text/css"/>
</head>

View File

@@ -2,9 +2,9 @@
{% block head %}
{{ super() }}
<title>Multiworld Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/trackers/tracker.css") }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackers/tracker.js") }}"></script>
{% endblock %}
{% block body %}
@@ -44,7 +44,7 @@
<tbody>
{%- for player, items in players.items() -%}
<tr>
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker,
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
{%- if (team, loop.index) in video -%}
{%- if video[(team, loop.index)][0] == "Twitch" -%}
@@ -121,7 +121,7 @@
<tbody>
{%- for player, checks in players.items() -%}
<tr>
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker,
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
<td>{{ player_names[(team, loop.index)]|e }}</td>
{%- for area in ordered_areas -%}

View File

@@ -0,0 +1,99 @@
{% block head %}
<!--suppress XmlDuplicatedId -->
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/playerTracker.css') }}"/>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/tooltip.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/playerTracker.js') }}"></script>
{% endblock %}
{% block body %}
<div id="tracker-wrapper" class="{{ theme }}-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}">
<a href="/generic_tracker/{{ room.tracker|suuid }}/{{ team }}/{{ player }}" class="button-link">
Go to Generic Tracker
</a>
{% if icons %}
{% block icons_render %}
<h1>Items</h1>
<div id="items-container">
{%- for item in icons %}
<div class="image-container tooltip" id="{{ item }}" data-tooltip="{{ item }}">
<img
src="{{ icons[item] }}"
class="icon tooltip {{ 'acquired' if item in received_items }}"
/>
</div>
{%- endfor %}
</div>
{% endblock %}
{% else %}
{% block item_names_render %}
<h1 class="items-header">Items</h1>
<div class="items-container">
{%- for item in received_items|sort -%}
<div class="item" id="{{ item }}">
{{ item }}
{% if all_progression_items[item] > 1 %}
{{ received_items[item] }}
{% else %}
{% endif %}
</div>
{%- endfor -%}
</div>
{% endblock %}
{% endif %}
{# div for total checks done as percentage. Probably needs to be put somewhere else but I liked how it looked here #}
<div class="total-checks" id="total-checks">
Total Checks Done: {{ checked_locations|length }}/{{ locations|length }}
</div>
{% if regions %}
{% block regions_render %}
<div class="regions-container">
{% for region in regions %}
<div class="regions-column" id="{{ region }}">
<h1 class="regions-header" id="{{ region }}-header">{{ region }} ▼ {{ checks_done[region]|length }} / {{ regions[region]|length }}</h1>
<div class="location-column hidden" id="{{ region }}-locations">
{%- for location in regions[region] %}
<div class="location {{ 'acquired' if location in checked_locations }}" id="{{ location }}">{{ location }}</div>
{%- endfor %}
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% else %}
{% block locations_render %}
<h1>Locations</h1>
<div class="locations-container" id="locations-container">
{% for location in locations %}
<div class="location {{ 'acquired' if name in checked_locations }}" id="{{ location }}">
{{ location }}
</div>
{% endfor %}
</div>
{% endblock %}
{% endif %}
</div>
{% endblock %}

View File

@@ -2,8 +2,8 @@
<html lang="en">
<head>
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/supermetroidTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/supermetroidTracker.js') }}"></script>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/supermetroidTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/supermetroidTracker.js') }}"></script>
</head>
<body>

View File

@@ -2,8 +2,8 @@
<html lang="en">
<head>
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/timespinnerTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/timespinnerTracker.js') }}"></script>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/timespinnerTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/timespinnerTracker.js') }}"></script>
</head>
<body>

View File

@@ -0,0 +1,77 @@
{% block head %}
<!--suppress XmlDuplicatedId -->
<title>{{ player_name }}&apos;s Keys Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/playerTracker.css') }}"/>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/tooltip.css') }}" />
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/zeldaKeysTracker.js') }}"/></script>
{% endblock %}
{# this tracker is mostly similar to the generic player tracker but
also adds a table with the key and checks counts for each region in the middle #}
{% block body %}
<div id="tracker-wrapper" class="{{ theme }}-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}">
<a href="/generic_tracker/{{ room.tracker|suuid }}/{{ team }}/{{ player }}" class="button-link">
Go to Generic Tracker
</a>
<h1>Items</h1>
<div id="items-container">
{% for item in icons %}
{% if item not in ['Small Key', 'Big Key'] %}
<div class="image-container tooltip" id="{{ item }}" data-tooltip="{{ item }}">
<img
src="{{ icons[item] }}"
class="icon tooltip {{ 'acquired' if item in received_items }}"
/>
</div>
{% endif %}
{% endfor %}
</div>
<div class="total-checks" id="total-checks">
Total Checks Done: {{ checked_locations|length }}/{{ locations|length }}
</div>
<table id="regions-column">
<tr class="keys-icons">
<td><img src="{{icons['Small Key']}}" class="icon tooltip acquired" id="small-key-icon"/></td>
<td><img src="{{icons['Big Key']}}" class="icon tooltip acquired" id="big-key-icon"/></td>
<td class="right-align">Total</td>
</tr>
{% for region in regions %}
<tr class="regions-column" id="{{ region }}">
<td id="{{ region }}-header">{{ region }} ▼</td>
{% if region in region_keys %}
{%- if region_keys[region]|length > 1 %}
<td class="smallkeys">{{ received_items[region_keys[region][0]] if region_keys[region][0] in received_items else '-' }}</td>
<td class="bigkeys">{{ received_items[region_keys[region][1]] if region_keys[region][1] in received_items else '-' }}</td>
{%- else %}
{% if 'Small Key' in region_keys[region][0] %}
<td class="smallkeys">{{ received_items[region_keys[region][0]] if region_keys[region][0] in received_items else '-' }}</td>
<td class="bigkeys">-</td>
{% else %}
<td class="smallkeys">-</td>
<td class="bigkeys">{{ received_items[region_keys[region][0]] if region_keys[region][0] in received_items else '-' }}</td>
{% endif %}
{%- endif%}
{% else %}
<td class="smallkeys">-</td>
<td class="bigkeys">-</td>
{% endif %}
<td class="counter">{{ checks_done[region]|length }} / {{ regions[region]|length }}</td>
</tr>
<tbody class="locations hidden" id="{{ region }}-locations">
{% for location in regions[region] %}
<tr>
<td class="location {{ 'acquired' if location in checked_locations }}" id="{{ location }}">
{{ location }}
</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,6 @@
import collections
import typing
from typing import Counter, Optional, Dict, Any, Tuple
from typing import Counter, Optional, Dict, Any, Tuple, Set, List, TYPE_CHECKING
from flask import render_template
from werkzeug.exceptions import abort
@@ -11,9 +11,53 @@ from worlds.alttp import Items
from WebHostLib import app, cache, Room
from Utils import restricted_loads
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
from MultiServer import Context
from worlds.AutoWorld import AutoWorldRegister
from MultiServer import get_item_name_from_id, Context
from NetUtils import SlotType
class PlayerTracker:
"""This class will create a basic 'prettier' tracker for each world using their themes automatically. This
can be overridden to customize how it will appear. Can provide icons and custom regions. The html used is also
a jinja template that can be overridden if you want your tracker to look different in certain aspects. To render
icons and regions add dictionaries to the relevant attributes of the tracker_info. To customize the layout of
your icons you can create a new html in your world and extend playerTracker.html and overwrite the icons_render
block then change the tracker_info template attribute to your template."""
template: str = 'playerTracker.html'
icons: Dict[str, str] = {}
progressive_items: List[str] = []
progressive_names: Dict[str, List[str]] = {}
regions: Dict[str, List[str]] = {}
checks_done: Dict[str, Set[str]] = {}
room: Any
team: int
player: int
name: str
all_locations: Set[str]
checked_locations: Set[str]
all_prog_items: Counter[str]
items_received: Counter[str]
received_prog_items: Counter[str]
slot_data: Dict[any, any]
theme: str
region_keys: Dict[str, str] = {}
def __init__(self, room: Any, team: int, player: int, name: str, all_locations: Set[str],
checked_locations: set, all_progression_items: Counter[str], items_received: Counter[str],
slot_data: Dict[any, any]):
self.room = room
self.team = team
self.player = player
self.name = name
self.all_locations = all_locations
self.checked_locations = checked_locations
self.all_prog_items = all_progression_items
self.items_received = items_received
self.slot_data = slot_data
alttp_icons = {
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
"Red Shield": r"https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
@@ -288,7 +332,7 @@ def get_static_room_data(room: Room):
@app.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
@cache.memoize(timeout=60) # multisave is currently created at most every minute
def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False):
def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False):
# Team and player must be positive and greater than zero
if tracked_team < 0 or tracked_player < 1:
abort(404)
@@ -297,13 +341,78 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
if not room:
abort(404)
# Collect seed information and pare it down to a single player
player_tracker, multisave, inventory, seed_checks_in_area, lttp_checks_done, \
slot_data, games, player_name, display_icons = fill_tracker_data(room, tracked_team, tracked_player)
game_name = games[tracked_player]
# TODO move all games in game_specific_trackers to new system
if game_name in game_specific_trackers and not want_generic:
specific_tracker = game_specific_trackers.get(game_name, None)
return specific_tracker(multisave, room, player_tracker.all_locations, inventory, tracked_team, tracked_player,
player_name, seed_checks_in_area, lttp_checks_done, slot_data[tracked_player])
elif game_name in AutoWorldRegister.world_types and not want_generic:
return render_template(
"trackers/" + player_tracker.template,
all_progression_items=player_tracker.all_prog_items,
player=tracked_player,
team=tracked_team,
room=player_tracker.room,
player_name=player_tracker.name,
checked_locations=sorted(player_tracker.checked_locations),
locations=sorted(player_tracker.all_locations),
theme=player_tracker.theme,
icons=display_icons,
regions=player_tracker.regions,
checks_done=player_tracker.checks_done,
region_keys=player_tracker.region_keys
)
else:
return __renderGenericTracker(multisave, room, player_tracker.all_locations, inventory, tracked_team, tracked_player, player_name, seed_checks_in_area, lttp_checks_done)
@app.route('/generic_tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
@cache.memoize(timeout=60)
def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
return get_player_tracker(tracker, tracked_team, tracked_player, True)
def get_tracker_icons_and_regions(player_tracker: PlayerTracker) -> Dict[str, str]:
"""this function allows multiple icons to be used for the same item but it does require the world to submit both
a progressive_items list and the icons dict together"""
display_icons: Dict[str, str] = {}
if player_tracker.progressive_names and player_tracker.icons:
for item in player_tracker.progressive_items:
if item in player_tracker.progressive_names:
level = min(player_tracker.items_received[item], len(player_tracker.progressive_names[item]) - 1)
display_name = player_tracker.progressive_names[item][level]
if display_name in player_tracker.icons:
display_icons[item] = player_tracker.icons[display_name]
else:
display_icons[item] = player_tracker.icons[item]
else:
display_icons[item] = player_tracker.icons[item]
else:
if player_tracker.progressive_items and player_tracker.icons:
for item in player_tracker.progressive_items:
display_icons[item] = player_tracker.icons[item]
if player_tracker.regions:
for region in player_tracker.regions:
for location in region:
if location in player_tracker.checked_locations:
player_tracker.checks_done.setdefault(region, set()).add(location)
return display_icons
def fill_tracker_data(room: Room, tracked_team: int, tracked_player: int) -> Tuple:
"""Collect seed information and pare it down to a single player"""
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
precollected_items, games, slot_data, groups = get_static_room_data(room)
player_name = names[tracked_team][tracked_player - 1]
location_to_area = player_location_to_area[tracked_player]
inventory = collections.Counter()
checks_done = {loc_name: 0 for loc_name in default_locations}
lttp_checks_done = {loc_name: 0 for loc_name in default_locations}
# Add starting items to inventory
starting_items = precollected_items[tracked_player]
@@ -321,6 +430,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
if tracked_player in group_members:
slots_aimed_at_player.add(group_id)
checked_locations = set()
# Add items to player inventory
for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}).items():
# Skip teams and players not matching the request
@@ -332,383 +442,52 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
item, recipient, flags = player_locations[location]
if recipient in slots_aimed_at_player: # a check done for the tracked player
attribute_item_solo(inventory, item)
if ms_player == tracked_player: # a check done by the tracked player
checks_done[location_to_area[location]] += 1
checks_done["Total"] += 1
specific_tracker = game_specific_trackers.get(games[tracked_player], None)
if specific_tracker and not want_generic:
return specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
seed_checks_in_area, checks_done, slot_data[tracked_player])
else:
return __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
seed_checks_in_area, checks_done)
lttp_checks_done[location_to_area[location]] += 1
lttp_checks_done["Total"] += 1
checked_locations.add(lookup_any_location_id_to_name[location])
prog_items = collections.Counter
all_location_names = set()
all_location_names = {lookup_any_location_id_to_name[id] for id in locations[tracked_player]}
prog_items = collections.Counter()
for player in locations:
for location in locations[player]:
item, recipient, flags = locations[player][location]
if recipient == player:
if flags & 1:
item_name = lookup_any_item_id_to_name[item]
prog_items[item_name] += 1
items_received = collections.Counter()
for id in inventory:
items_received[lookup_any_item_id_to_name[id]] = inventory[id]
player_tracker = PlayerTracker(
room,
tracked_team,
tracked_player,
player_name,
all_location_names,
checked_locations,
prog_items,
items_received,
slot_data[tracked_player]
)
# grab webworld and apply its theme to the tracker
webworld = AutoWorldRegister.world_types[games[tracked_player]].web
player_tracker.theme = webworld.theme
# allow the world to add information to the tracker class
webworld.modify_tracker(player_tracker)
display_icons = get_tracker_icons_and_regions(player_tracker)
return player_tracker, multisave, inventory, seed_checks_in_area, lttp_checks_done, slot_data, games, player_name, display_icons
@app.route('/generic_tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
return getPlayerTracker(tracker, tracked_team, tracked_player, True)
def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, player_name: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
# Note the presence of the triforce item
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
if game_state == 30:
inventory[106] = 1 # Triforce
# Progressive items need special handling for icons and class
progressive_items = {
"Progressive Sword": 94,
"Progressive Glove": 97,
"Progressive Bow": 100,
"Progressive Mail": 96,
"Progressive Shield": 95,
}
progressive_names = {
"Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
"Progressive Glove": [None, 'Power Glove', 'Titan Mitts'],
"Progressive Bow": [None, "Bow", "Silver Bow"],
"Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
"Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"]
}
# Determine which icon to use
display_data = {}
for item_name, item_id in progressive_items.items():
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
display_name = progressive_names[item_name][level]
acquired = True
if not display_name:
acquired = False
display_name = progressive_names[item_name][level + 1]
base_name = item_name.split(maxsplit=1)[1].lower()
display_data[base_name + "_acquired"] = acquired
display_data[base_name + "_url"] = alttp_icons[display_name]
# The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
sp_areas = ordered_areas[2:15]
player_big_key_locations = set()
player_small_key_locations = set()
for loc_data in locations.values():
for values in loc_data.values():
item_id, item_player, flags = values
if item_player == player:
if item_id in ids_big_key:
player_big_key_locations.add(ids_big_key[item_id])
elif item_id in ids_small_key:
player_small_key_locations.add(ids_small_key[item_id])
return render_template("lttpTracker.html", inventory=inventory,
player_name=player_name, room=room, icons=alttp_icons, checks_done=checks_done,
checks_in_area=seed_checks_in_area[player],
acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
key_locations=player_small_key_locations,
big_key_locations=player_big_key_locations,
**display_data)
def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
icons = {
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
"Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png",
"Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png",
"Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png",
"Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png",
"Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png",
"Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png",
"Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png",
"Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png",
"Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png",
"Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png",
"Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png",
"Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png",
"Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png",
"Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png",
"Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
"Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
"Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
"Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png",
"Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
"Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
"Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
"Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png",
"Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
}
minecraft_location_ids = {
"Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070,
42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077],
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42014],
"The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046],
"Adventure": [42047, 42050, 42096, 42097, 42098, 42059, 42055, 42072, 42003, 42035, 42016, 42020,
42048, 42054, 42068, 42043, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42099, 42100],
"Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028, 42036,
42057, 42063, 42053, 42102, 42101, 42092, 42093, 42094, 42095],
"Archipelago": [42080, 42081, 42082, 42083, 42084, 42085, 42086, 42087, 42088, 42089, 42090, 42091],
}
display_data = {}
# Determine display for progressive items
progressive_items = {
"Progressive Tools": 45013,
"Progressive Weapons": 45012,
"Progressive Armor": 45014,
"Progressive Resource Crafting": 45001
}
progressive_names = {
"Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"],
"Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"],
"Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"],
"Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"]
}
for item_name, item_id in progressive_items.items():
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
display_name = progressive_names[item_name][level]
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
display_data[base_name + "_url"] = icons[display_name]
# Multi-items
multi_items = {
"3 Ender Pearls": 45029,
"8 Netherite Scrap": 45015
}
for item_name, item_id in multi_items.items():
base_name = item_name.split()[-1].lower()
count = inventory[item_id]
if count >= 0:
display_data[base_name + "_count"] = count
# Victory condition
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
display_data['game_finished'] = game_state == 30
# Turn location IDs into advancement tab counts
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
lookup_name = lambda id: lookup_any_location_id_to_name[id]
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
for tab_name, tab_locations in minecraft_location_ids.items()}
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
for tab_name, tab_locations in minecraft_location_ids.items()}
checks_done['Total'] = len(checked_locations)
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()}
checks_in_area['Total'] = sum(checks_in_area.values())
return render_template("minecraftTracker.html",
inventory=inventory, icons=icons,
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
id in lookup_any_item_id_to_name},
player=player, team=team, room=room, player_name=playerName,
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
**display_data)
def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
icons = {
"Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
"Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png",
"Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png",
"Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png",
"Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png",
"Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png",
"Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png",
"Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png",
"Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png",
"Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png",
"Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png",
"Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png",
"Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png",
"Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png",
"Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png",
"Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png",
"Dins Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png",
"Farores Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png",
"Nayrus Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png",
"Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png",
"Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png",
"Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png",
"Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png",
"Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png",
"Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png",
"Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png",
"Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png",
"Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png",
"Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png",
"Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png",
"Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png",
"Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png",
"Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png",
"Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png",
"Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png",
"Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png",
"Gold Skulltula Token": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png",
"Triforce Piece": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png",
"Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png",
"Zeldas Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Eponas Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Sarias Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Suns Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png",
"Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png",
"Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png",
"Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png",
"Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png",
"Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png",
"Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png",
"Boss Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png",
}
display_data = {}
# Determine display for progressive items
progressive_items = {
"Progressive Hookshot": 66128,
"Progressive Strength Upgrade": 66129,
"Progressive Wallet": 66133,
"Progressive Scale": 66134,
"Magic Meter": 66138,
"Ocarina": 66139,
}
progressive_names = {
"Progressive Hookshot": ["Hookshot", "Hookshot", "Longshot"],
"Progressive Strength Upgrade": ["Goron Bracelet", "Goron Bracelet", "Silver Gauntlets", "Golden Gauntlets"],
"Progressive Wallet": ["Adults Wallet", "Adults Wallet", "Giants Wallet", "Giants Wallet"],
"Progressive Scale": ["Silver Scale", "Silver Scale", "Gold Scale"],
"Magic Meter": ["Small Magic", "Small Magic", "Large Magic"],
"Ocarina": ["Fairy Ocarina", "Fairy Ocarina", "Ocarina of Time"]
}
for item_name, item_id in progressive_items.items():
level = min(inventory[item_id], len(progressive_names[item_name])-1)
display_name = progressive_names[item_name][level]
if item_name.startswith("Progressive"):
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
else:
base_name = item_name.lower().replace(' ', '_')
display_data[base_name+"_url"] = icons[display_name]
if base_name == "hookshot":
display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level)
if base_name == "wallet":
display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level)
# Determine display for bottles. Show letter if it's obtained, determine bottle count
bottle_ids = [66015, 66020, 66021, 66140, 66141, 66142, 66143, 66144, 66145, 66146, 66147, 66148]
display_data['bottle_count'] = min(sum(map(lambda item_id: inventory[item_id], bottle_ids)), 4)
display_data['bottle_url'] = icons['Rutos Letter'] if inventory[66021] > 0 else icons['Bottle']
# Determine bombchu display
display_data['has_bombchus'] = any(map(lambda item_id: inventory[item_id] > 0, [66003, 66106, 66107, 66137]))
# Multi-items
multi_items = {
"Gold Skulltula Token": 66091,
"Triforce Piece": 66202,
}
for item_name, item_id in multi_items.items():
base_name = item_name.split()[-1].lower()
count = inventory[item_id]
display_data[base_name+"_count"] = inventory[item_id]
# Gather dungeon locations
area_id_ranges = {
"Overworld": (67000, 67280),
"Deku Tree": (67281, 67303),
"Dodongo's Cavern": (67304, 67334),
"Jabu Jabu's Belly": (67335, 67359),
"Bottom of the Well": (67360, 67384),
"Forest Temple": (67385, 67420),
"Fire Temple": (67421, 67457),
"Water Temple": (67458, 67484),
"Shadow Temple": (67485, 67532),
"Spirit Temple": (67533, 67582),
"Ice Cavern": (67583, 67596),
"Gerudo Training Ground": (67597, 67635),
"Thieves' Hideout": (67259, 67263),
"Ganon's Castle": (67636, 67673),
}
def lookup_and_trim(id, area):
full_name = lookup_any_location_id_to_name[id]
if id == 67673:
return full_name[13:] # Ganons Tower Boss Key Chest
if area not in ["Overworld", "Thieves' Hideout"]:
# trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC
return full_name[len(area):]
return full_name
checked_locations = multisave.get("location_checks", {}).get((team, player), set()).intersection(set(locations[player]))
location_info = {area: {lookup_and_trim(id, area): id in checked_locations for id in range(min_id, max_id+1) if id in locations[player]}
for area, (min_id, max_id) in area_id_ranges.items()}
checks_done = {area: len(list(filter(lambda x: x, location_info[area].values()))) for area in area_id_ranges}
checks_in_area = {area: len([id for id in range(min_id, max_id+1) if id in locations[player]])
for area, (min_id, max_id) in area_id_ranges.items()}
# Remove Thieves' Hideout checks from Overworld, since it's in the middle of the range
checks_in_area["Overworld"] -= checks_in_area["Thieves' Hideout"]
checks_done["Overworld"] -= checks_done["Thieves' Hideout"]
for loc in location_info["Thieves' Hideout"]:
del location_info["Overworld"][loc]
checks_done['Total'] = sum(checks_done.values())
checks_in_area['Total'] = sum(checks_in_area.values())
# Give skulltulas on non-tracked locations
non_tracked_locations = multisave.get("location_checks", {}).get((team, player), set()).difference(set(locations[player]))
for id in non_tracked_locations:
if "GS" in lookup_and_trim(id, ''):
display_data["token_count"] += 1
# Gather small and boss key info
small_key_counts = {
"Forest Temple": inventory[66175],
"Fire Temple": inventory[66176],
"Water Temple": inventory[66177],
"Spirit Temple": inventory[66178],
"Shadow Temple": inventory[66179],
"Bottom of the Well": inventory[66180],
"Gerudo Training Ground": inventory[66181],
"Thieves' Hideout": inventory[66182],
"Ganon's Castle": inventory[66183],
}
boss_key_counts = {
"Forest Temple": '' if inventory[66149] else '',
"Fire Temple": '' if inventory[66150] else '',
"Water Temple": '' if inventory[66151] else '',
"Spirit Temple": '' if inventory[66152] else '',
"Shadow Temple": '' if inventory[66153] else '',
"Ganon's Castle": '' if inventory[66154] else '',
}
# Victory condition
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
display_data['game_finished'] = game_state == 30
return render_template("ootTracker.html",
inventory=inventory, player=player, team=team, room=room, player_name=playerName,
icons=icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
small_key_counts=small_key_counts, boss_key_counts=boss_key_counts,
**display_data)
def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: set,
inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict[str, Any]) -> str:
@@ -745,7 +524,7 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
}
timespinner_location_ids = {
"Present": [
"Present": [
1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009,
1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019,
1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029,
@@ -766,20 +545,20 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
1337150, 1337151, 1337152, 1337153, 1337154, 1337155,
1337171, 1337172, 1337173, 1337174, 1337175],
"Ancient Pyramid": [
1337236,
1337236,
1337246, 1337247, 1337248, 1337249]
}
if(slot_data["DownloadableItems"]):
timespinner_location_ids["Present"] += [
1337156, 1337157, 1337159,
1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
1337170]
if(slot_data["Cantoran"]):
timespinner_location_ids["Past"].append(1337176)
if(slot_data["LoreChecks"]):
timespinner_location_ids["Present"] += [
1337177, 1337178, 1337179,
1337177, 1337178, 1337179,
1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187]
timespinner_location_ids["Past"] += [
1337188, 1337189,
@@ -808,13 +587,13 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
acquired_items = {lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name}
options = {k for k, v in slot_data.items() if v}
return render_template("timespinnerTracker.html",
return render_template("trackers/" + "timespinnerTracker.html",
inventory=inventory, icons=icons, acquired_items=acquired_items,
player=player, team=team, room=room, player_name=playerName,
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
options=options, **display_data)
def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: set,
inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
@@ -889,6 +668,7 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations
for item_name, item_id in multi_items.items():
base_name = item_name.split()[0].lower()
count = inventory[item_id]
display_data[base_name+"_count"] = inventory[item_id]
# Victory condition
@@ -906,7 +686,7 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in supermetroid_location_ids.items()}
checks_in_area['Total'] = sum(checks_in_area.values())
return render_template("supermetroidTracker.html",
return render_template("trackers/" + "supermetroidTracker.html",
inventory=inventory, icons=icons,
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
id in lookup_any_item_id_to_name},
@@ -914,7 +694,8 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
**display_data)
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: set,
inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
@@ -929,11 +710,11 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic
for order_index, networkItem in enumerate(ordered_items, start=1):
player_received_items[networkItem.item] = order_index
return render_template("genericTracker.html",
return render_template("trackers/" + "genericTracker.html",
inventory=inventory,
player=player, team=team, room=room, player_name=playerName,
checked_locations=checked_locations,
not_checked_locations=set(locations[player]) - checked_locations,
not_checked_locations=locations - checked_locations,
received_items=player_received_items)
@@ -975,9 +756,9 @@ def getTracker(tracker: UUID):
continue
item, recipient, flags = player_locations[location]
if recipient in names:
attribute_item(inventory, team, recipient, item)
checks_done[team][player][player_location_to_area[player][location]] += 1
checks_done[team][player]["Total"] += 1
@@ -1021,7 +802,7 @@ def getTracker(tracker: UUID):
for (team, player), data in multisave.get("video", []):
video[(team, player)] = data
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
return render_template("trackers/" + "multiworldTracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
@@ -1032,9 +813,6 @@ def getTracker(tracker: UUID):
game_specific_trackers: typing.Dict[str, typing.Callable] = {
"Minecraft": __renderMinecraftTracker,
"Ocarina of Time": __renderOoTTracker,
"Timespinner": __renderTimespinnerTracker,
"A Link to the Past": __renderAlttpTracker,
"Super Metroid": __renderSuperMetroidTracker
}
}

View File

@@ -97,6 +97,11 @@ local extensionConsumableLookup = {
[443] = 0x3F
}
local noOverworldItemsLookup = {
[499] = 0x2B,
[500] = 0x12,
}
local itemMessages = {}
local consumableStacks = nil
local prevstate = ""
@@ -341,7 +346,7 @@ function processBlock(block)
-- This is a key item
memoryLocation = memoryLocation - 0x0E0
wU8(memoryLocation, 0x01)
elseif v >= 0x1E0 then
elseif v >= 0x1E0 and v <= 0x1F2 then
-- This is a movement item
-- Minus Offset (0x100) - movement offset (0xE0)
memoryLocation = memoryLocation - 0x1E0
@@ -351,7 +356,10 @@ function processBlock(block)
else
wU8(memoryLocation, 0x01)
end
elseif v >= 0x1F3 and v <= 0x1F4 then
-- NoOverworld special items
memoryLocation = noOverworldItemsLookup[v]
wU8(memoryLocation, 0x01)
elseif v >= 0x16C and v <= 0x1AF then
-- This is a gold item
amountToAdd = goldLookup[v]

View File

@@ -23,3 +23,10 @@ No metadata is specified yet.
## Extra Data
The zip can contain arbitrary files in addition what was specified above.
## Caveats
Imports from other files inside the apworld have to use relative imports.
Imports from AP base have to use absolute imports, e.g. Options.py and worlds/AutoWorld.py.

11
docs/code_of_conduct.md Normal file
View File

@@ -0,0 +1,11 @@
# Code of Conduct
We conduct ourselves openly and inclusively here. Please do not contribute to an environment which makes other people uncomfortable. This means that we expect all contributors or participants here to:
* Be welcoming and inclusive in tone and language.
* Be respectful of others and their abilities.
* Show empathy when speaking with others.
* Be gracious and accept feedback and constructive criticism.
These guidelines apply to all channels of communication within this GitHub repository. Please be respectful in both public channels, such as issues, and private ones, such as private messaging or emails.
Any incidents of abuse may be reported directly to ijwu at hmfarran@gmail.com.

12
docs/contributing.md Normal file
View File

@@ -0,0 +1,12 @@
# Contributing
Contributions are welcome. We have a few requests of any new contributors.
* Ensure that all changes which affect logic are covered by unit tests.
* Do not introduce any unit test failures/regressions.
* Follow styling as designated in our [styling documentation](/docs/style.md).
Otherwise, we tend to judge code on a case to case basis.
For adding a new game to Archipelago and other documentation on how Archipelago functions, please see
[the docs folder](docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev
channel in our [Discord](https://archipelago.gg/discord).

View File

@@ -13,9 +13,18 @@ These steps should be followed in order to establish a gameplay connection with
In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet.
There are libraries available that implement this network protocol in [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py), [Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java), [.Net](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net) and [C++](https://github.com/black-sliver/apclientpp)
There are also a number of community-supported libraries available that implement this network protocol to make integrating with Archipelago easier.
For Super Nintendo games there are clients available in either [Node](https://github.com/ArchipelagoMW/SuperNintendoClient) or [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py), There are also game specific clients available for [The Legend of Zelda: Ocarina of Time](https://github.com/ArchipelagoMW/Z5Client) or [Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/FF1Client.py)
| Language/Runtime | Project | Remarks |
|-------------------------------|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| Python | [Archipelago CommonClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py) | |
| | [Archipelago SNIClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py) | For Super Nintendo Game Support; Utilizes [SNI](https://github.com/alttpo/sni). |
| JVM (Java / Kotlin) | [Archipelago.MultiClient.Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java) | |
| .NET (C# / C++ / F# / VB.NET) | [Archipelago.MultiClient.Net](https://www.nuget.org/packages/Archipelago.MultiClient.Net) | |
| C++ | [apclientpp](https://github.com/black-sliver/apclientpp) | almost-header-only |
| | [APCpp](https://github.com/N00byKing/APCpp) | CMake |
| JavaScript / TypeScript | [archipelago.js](https://www.npmjs.com/package/archipelago.js) | Browser and Node.js Supported |
| Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | |
## Synchronizing Items
When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet.
@@ -152,7 +161,8 @@ The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring:
All arguments for this packet are optional, only changes are sent.
### Print
Sent to clients purely to display a message to the player.
Sent to clients purely to display a message to the player.
* *Deprecation warning: clients that connect with version 0.3.5 or higher will nolonger recieve Print packets, instead all messsages are send as [PrintJSON](#PrintJSON)*
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
@@ -164,10 +174,21 @@ Sent to clients purely to display a message to the player. This packet differs f
| Name | Type | Notes |
| ---- | ---- | ----- |
| data | list\[[JSONMessagePart](#JSONMessagePart)\] | Type of this part of the message. |
| type | str | May be present to indicate the nature of this message. Known types are Hint and ItemSend. |
| type | str | May be present to indicate the [PrintJsonType](#PrintJsonType) of this message. |
| receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. |
| item | [NetworkItem](#NetworkItem) | Is present if type is Hint or ItemSend and marks the source player id, location id, item id and item flags. |
| found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. |
| countdown | int | Is present if type is `Countdown`, denotes the amount of seconds remaining on the countdown. |
##### PrintJsonType
PrintJsonType indicates the type of [PrintJson](#PrintJson) packet, different types can be handled differently by the client and can also contain additional arguments. When receiving an unknown type the data's list\[[JSONMessagePart](#JSONMessagePart)\] should still be printed as normal.
Currently defined types are:
| Type | Notes |
| ---- | ----- |
| ItemSend | The message is in response to a player receiving an item. |
| Hint | The message is in response to a player hinting. |
| Countdown | The message contains information about the current server Countdown. |
### DataPackage
Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most easily communicate with the Archipelago server. Contents include things like location id to name mappings, among others; see [Data Package Contents](#Data-Package-Contents) for more info.

View File

@@ -56,3 +56,8 @@ SNI is required to use SNIClient. If not integrated into the project, it has to
You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases).
It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in
host.yaml at your SNI folder.
## Running tests
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.

View File

@@ -103,8 +103,9 @@ or boss drops for RPG-like games but could also be progress in a research tree.
Each location has a `name` and an `id` (a.k.a. "code" or "address"), is placed
in a Region and has access rules.
The name needs to be unique in each game, the ID needs to be unique across all
games and is best in the same range as the item IDs.
The name needs to be unique in each game and must not be numeric (has to
contain least 1 letter or symbol). The ID needs to be unique across all games
and is best in the same range as the item IDs.
World-specific IDs are 1 to 2<sup>53</sup>-1, IDs ≤ 0 are global and reserved.
Special locations with ID `None` can hold events.
@@ -121,6 +122,9 @@ their world. Progression items will be assigned to locations with higher
priority and moved around to meet defined rules and accomplish progression
balancing.
The name needs to be unique in each game, meaning a duplicate item has the
same ID. Name must not be numeric (has to contain at least 1 letter or symbol).
Special items with ID `None` can mark events (read below).
Other classifications include
@@ -188,15 +192,17 @@ the `/worlds` directory. The starting point for the package is `__init.py__`.
Conventionally, your world class is placed in that file.
World classes must inherit from the `World` class in `/worlds/AutoWorld.py`,
which can be imported as `..AutoWorld.World` from your package.
which can be imported as `worlds.AutoWorld.World` from your package.
AP will pick up your world automatically due to the `AutoWorld` implementation.
### Requirements
If your world needs specific python packages, they can be listed in
`world/[world_name]/requirements.txt`.
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format)
`world/[world_name]/requirements.txt`. ModuleUpdate.py will automatically
pick up and install them.
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format).
### Relative Imports
@@ -209,6 +215,10 @@ e.g. `from .Options import mygame_options` from your `__init__.py` will load
When imported names pile up it may be easier to use `from . import Options`
and access the variable as `Options.mygame_options`.
Imports from directories outside your world should use absolute imports.
Correct use of relative / absolute imports is required for zipped worlds to
function, see [apworld specification.md](apworld%20specification.md).
### Your Item Type
Each world uses its own subclass of `BaseClasses.Item`. The constuctor can be
@@ -274,14 +284,12 @@ Define a property `option_<name> = <number>` per selectable value and
`default = <number>` to set the default selection. Aliases can be set by
defining a property `alias_<name> = <same number>`.
One special case where aliases are required is when option name is `yes`, `no`,
`on` or `off` because they parse to `True` or `False`:
```python
option_off = 0
option_on = 1
option_some = 2
alias_false = 0
alias_true = 1
alias_disabled = 0
alias_enabled = 1
default = 0
```
@@ -323,7 +331,7 @@ mygame_options: typing.Dict[str, type(Option)] = {
```python
# __init__.py
from ..AutoWorld import World
from worlds.AutoWorld import World
from .Options import mygame_options # import the options dict
class MyGameWorld(World):
@@ -352,7 +360,7 @@ more natural. These games typically have been edited to 'bake in' the items.
from .Options import mygame_options # the options we defined earlier
from .Items import mygame_items # data used below to add items to the World
from .Locations import mygame_locations # same as above
from ..AutoWorld import World
from worlds.AutoWorld import World
from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification
from Utils import get_options, output_path
@@ -553,7 +561,7 @@ def generate_basic(self) -> None:
### Setting Rules
```python
from ..generic.Rules import add_rule, set_rule, forbid_item
from worlds.generic.Rules import add_rule, set_rule, forbid_item
from Items import get_item_type
def set_rules(self) -> None:
@@ -603,7 +611,7 @@ implement more complex logic in logic mixins, even if there is no need to add
properties to the `BaseClasses.CollectionState` state object.
When importing a file that defines a class that inherits from
`..AutoWorld.LogicMixin` the state object's class is automatically extended by
`worlds.AutoWorld.LogicMixin` the state object's class is automatically extended by
the mixin's members. These members should be prefixed with underscore following
the name of the implementing world. This is due to sharing a namespace with all
other logic mixins.
@@ -622,7 +630,7 @@ Please do this with caution and only when neccessary.
```python
# Logic.py
from ..AutoWorld import LogicMixin
from worlds.AutoWorld import LogicMixin
class MyGameLogic(LogicMixin):
def _mygame_has_key(self, world: MultiWorld, player: int):
@@ -633,7 +641,7 @@ class MyGameLogic(LogicMixin):
```python
# __init__.py
from ..generic.Rules import set_rule
from worlds.generic.Rules import set_rule
import .Logic # apply the mixin by importing its file
class MyGameWorld(World):

View File

@@ -196,7 +196,7 @@ begin
begin
// Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion);
Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
Result := (CompareStr(strVersion, 'v14.32.31332') < 0);
end
else
begin

View File

@@ -3,6 +3,6 @@ websockets>=10.3
PyYAML>=6.0
jellyfish>=0.9.0
jinja2>=3.1.2
schema>=0.7.4
schema>=0.7.5
kivy>=2.1.0
bsdiff4>=1.2.2

View File

@@ -371,13 +371,13 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
distribute_items_restrictive(multi_world)
self.assertEqual(locations[0].item, basic_items[0])
self.assertEqual(locations[0].item, basic_items[1])
self.assertFalse(locations[0].event)
self.assertEqual(locations[1].item, prog_items[0])
self.assertTrue(locations[1].event)
self.assertEqual(locations[2].item, prog_items[1])
self.assertTrue(locations[2].event)
self.assertEqual(locations[3].item, basic_items[1])
self.assertEqual(locations[3].item, basic_items[0])
self.assertFalse(locations[3].event)
def test_excluded_distribute(self):
@@ -500,8 +500,8 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
removed_item: list[Item] = []
removed_location: list[Location] = []
def fill_hook(progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations):
removed_item.append(restitempool.pop(0))
def fill_hook(progitempool, usefulitempool, filleritempool, fill_locations):
removed_item.append(filleritempool.pop(0))
removed_location.append(fill_locations.pop(0))
multi_world.worlds[player1.id].fill_hook = fill_hook

View File

@@ -52,3 +52,13 @@ class TestIDs(unittest.TestCase):
else:
for location_id in world_type.location_id_to_name:
self.assertGreater(location_id, 0)
def testDuplicateItemIDs(self):
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id))
def testDuplicateLocationIDs(self):
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))

View File

@@ -1,5 +1,6 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister
from . import setup_default_world
class TestBase(unittest.TestCase):
@@ -29,3 +30,17 @@ class TestBase(unittest.TestCase):
with self.subTest(group_name, group_name=group_name):
for item in items:
self.assertIn(item, world_type.item_name_to_id)
def testItemCountGreaterEqualLocations(self):
for game_name, world_type in AutoWorldRegister.world_types.items():
if game_name in {"Final Fantasy"}:
continue
with self.subTest("Game", game=game_name):
world = setup_default_world(world_type)
location_count = sum(0 if location.event or location.item else 1 for location in world.get_locations())
self.assertGreaterEqual(
len(world.itempool),
location_count,
f"{game_name} Item count MUST meet or exceede the number of locations",
)

20
test/general/TestNames.py Normal file
View File

@@ -0,0 +1,20 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister
class TestNames(unittest.TestCase):
def testItemNamesFormat(self):
"""Item names must not be all numeric in order to differentiate between ID and name in !hint"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
for item_name in world_type.item_name_to_id:
self.assertFalse(item_name.isnumeric(),
f"Item name \"{item_name}\" is invalid. It must not be numeric.")
def testLocationNameFormat(self):
"""Location names must not be all numeric in order to differentiate between ID and name in !hint_location"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
for location_name in world_type.location_name_to_id:
self.assertFalse(location_name.isnumeric(),
f"Location name \"{location_name}\" is invalid. It must not be numeric.")

View File

@@ -0,0 +1,34 @@
"""Tests for successful generation of WebHost cached files. Can catch some other deeper errors."""
import os
import unittest
import WebHost
class TestFileGeneration(unittest.TestCase):
def setUp(self) -> None:
self.correct_path = os.path.join(os.path.dirname(WebHost.__file__), "WebHostLib")
# should not create the folder *here*
self.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
def testOptions(self):
WebHost.create_options_files()
target = os.path.join(self.correct_path, "static", "generated", "configs")
self.assertTrue(os.path.exists(target))
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs")))
# folder seems fine, so now we try to generate Options based on the default file
from WebHostLib.check import roll_options
file: os.DirEntry
for file in os.scandir(target):
if file.is_file() and file.name.endswith(".yaml"):
with self.subTest(file=file.name):
with open(file) as f:
for value in roll_options({file.name: f.read()})[0].values():
self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.")
def testTutorial(self):
WebHost.create_ordered_tutorials_file()
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json")))
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "tutorials.json")))

View File

@@ -27,7 +27,8 @@ class AutoWorldRegister(type):
# build rest
dct["item_names"] = frozenset(dct["item_name_to_id"])
dct["item_name_groups"] = dct.get("item_name_groups", {})
dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
in dct.get("item_name_groups", {}).items()}
dct["item_name_groups"]["Everything"] = dct["item_names"]
dct["location_names"] = frozenset(dct["location_name_to_id"])
dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {})))
@@ -97,22 +98,32 @@ def call_stage(world: "MultiWorld", method_name: str, *args: Any) -> None:
class WebWorld:
"""Webhost integration"""
# display a settings page. Can be a link to an out-of-ap settings tool too.
settings_page: Union[bool, str] = True
"""display a settings page. Can be a link to a specific page or external tool."""
# docs folder will be scanned for game info pages using this list in the format '{language}_{game_name}.md'
game_info_languages: List[str] = ['en']
"""docs folder will be scanned for game info pages using this list in the format '{language}_{game_name}.md'"""
# docs folder will also be scanned for tutorial guides given the relevant information in this list. Each Tutorial
# class is to be used for one guide.
tutorials: List["Tutorial"]
"""docs folder will also be scanned for tutorial guides. Each Tutorial class is to be used for one guide."""
# Choose a theme for your /game/* pages
# Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone
theme = "grass"
"""Choose a theme for you /game/* pages.
Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone"""
# display a link to a bug report page, most likely a link to a GitHub issue page.
bug_report_page: Optional[str]
"""display a link to a bug report page, most likely a link to a GitHub issue page."""
if TYPE_CHECKING:
from WebHostLib.tracker import PlayerTracker
else:
PlayerTracker = object
def modify_tracker(self, tracker: PlayerTracker):
"""Can use this to modify tracker data and add icons and regions dictionaries to
allow them to render on the game's tracker page."""
pass
class World(metaclass=AutoWorldRegister):
@@ -220,10 +231,8 @@ class World(metaclass=AutoWorldRegister):
@classmethod
def fill_hook(cls,
progitempool: List["Item"],
nonexcludeditempool: List["Item"],
localrestitempool: Dict[int, List["Item"]],
nonlocalrestitempool: Dict[int, List["Item"]],
restitempool: List["Item"],
usefulitempool: List["Item"],
filleritempool: List["Item"],
fill_locations: List["Location"]) -> None:
"""Special method that gets called as part of distribute_items_restrictive (main fill).
This gets called once per present world type."""
@@ -241,6 +250,11 @@ class World(metaclass=AutoWorldRegister):
"""Fill in the slot_data field in the Connected network package."""
return {}
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
"""Fill in additional entrance information text into locations, which is displayed when hinted.
structure is {player_id: {location_id: text}} You will need to insert your own player_id."""
pass
def modify_multidata(self, multidata: Dict[str, Any]) -> None: # TODO: TypedDict for multidata?
"""For deeper modification of server multidata."""
pass

View File

@@ -27,7 +27,8 @@ class WorldSource(typing.NamedTuple):
world_sources: typing.List[WorldSource] = []
file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly
for file in os.scandir(folder):
if not file.name.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders
# prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "."
if not file.name.startswith(("_", ".")):
if file.is_dir():
world_sources.append(WorldSource(file.name))
elif file.is_file() and file.name.endswith(".apworld"):

View File

@@ -1,8 +1,9 @@
import logging
from typing import Optional
from typing import Optional, Union, List, Tuple, Callable, Dict
from BaseClasses import Boss
from Fill import FillError
from .Options import Bosses
def BossFactory(boss: str, player: int) -> Optional[Boss]:
@@ -12,7 +13,7 @@ def BossFactory(boss: str, player: int) -> Optional[Boss]:
raise Exception('Unknown Boss: %s', boss)
def ArmosKnightsDefeatRule(state, player: int):
def ArmosKnightsDefeatRule(state, player: int) -> bool:
# Magic amounts are probably a bit overkill
return (
state.has_melee_weapon(player) or
@@ -25,7 +26,7 @@ def ArmosKnightsDefeatRule(state, player: int):
state.has('Red Boomerang', player))
def LanmolasDefeatRule(state, player: int):
def LanmolasDefeatRule(state, player: int) -> bool:
return (
state.has_melee_weapon(player) or
state.has('Fire Rod', player) or
@@ -35,16 +36,16 @@ def LanmolasDefeatRule(state, player: int):
state.can_shoot_arrows(player))
def MoldormDefeatRule(state, player: int):
def MoldormDefeatRule(state, player: int) -> bool:
return state.has_melee_weapon(player)
def HelmasaurKingDefeatRule(state, player: int):
def HelmasaurKingDefeatRule(state, player: int) -> bool:
# TODO: technically possible with the hammer
return state.has_sword(player) or state.can_shoot_arrows(player)
def ArrghusDefeatRule(state, player: int):
def ArrghusDefeatRule(state, player: int) -> bool:
if not state.has('Hookshot', player):
return False
# TODO: ideally we would have a check for bow and silvers, which combined with the
@@ -58,7 +59,7 @@ def ArrghusDefeatRule(state, player: int):
(state.has('Ice Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16))))
def MothulaDefeatRule(state, player: int):
def MothulaDefeatRule(state, player: int) -> bool:
return (
state.has_melee_weapon(player) or
(state.has('Fire Rod', player) and state.can_extend_magic(player, 10)) or
@@ -70,11 +71,11 @@ def MothulaDefeatRule(state, player: int):
)
def BlindDefeatRule(state, player: int):
def BlindDefeatRule(state, player: int) -> bool:
return state.has_melee_weapon(player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player)
def KholdstareDefeatRule(state, player: int):
def KholdstareDefeatRule(state, player: int) -> bool:
return (
(
state.has('Fire Rod', player) or
@@ -96,11 +97,11 @@ def KholdstareDefeatRule(state, player: int):
)
def VitreousDefeatRule(state, player: int):
def VitreousDefeatRule(state, player: int) -> bool:
return state.can_shoot_arrows(player) or state.has_melee_weapon(player)
def TrinexxDefeatRule(state, player: int):
def TrinexxDefeatRule(state, player: int) -> bool:
if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)):
return False
return state.has('Hammer', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', player) or \
@@ -108,11 +109,11 @@ def TrinexxDefeatRule(state, player: int):
(state.has_sword(player) and state.can_extend_magic(player, 32))
def AgahnimDefeatRule(state, player: int):
def AgahnimDefeatRule(state, player: int) -> bool:
return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player)
def GanonDefeatRule(state, player: int):
def GanonDefeatRule(state, player: int) -> bool:
if state.world.swordless[player]:
return state.has('Hammer', player) and \
state.has_fire_source(player) and \
@@ -132,7 +133,7 @@ def GanonDefeatRule(state, player: int):
return common and state.has('Silver Bow', player) and state.can_shoot_arrows(player)
boss_table = {
boss_table: Dict[str, Tuple[str, Optional[Callable]]] = {
'Armos Knights': ('Armos', ArmosKnightsDefeatRule),
'Lanmolas': ('Lanmola', LanmolasDefeatRule),
'Moldorm': ('Moldorm', MoldormDefeatRule),
@@ -147,7 +148,7 @@ boss_table = {
'Agahnim2': ('Agahnim2', AgahnimDefeatRule)
}
boss_location_table = [
boss_location_table: List[Tuple[str, str]] = [
('Ganons Tower', 'top'),
('Tower of Hera', None),
('Skull Woods', None),
@@ -164,6 +165,34 @@ boss_location_table = [
]
def place_plando_bosses(bosses: List[str], world, player: int) -> Tuple[List[str], List[Tuple[str, str]]]:
# Most to least restrictive order
boss_locations = boss_location_table.copy()
world.random.shuffle(boss_locations)
boss_locations.sort(key=lambda location: -int(restrictive_boss_locations[location]))
already_placed_bosses: List[str] = []
for boss in bosses:
if "-" in boss: # handle plando locations
loc, boss = boss.split("-")
boss = boss.title()
level: str = None
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
# split off level
loc = loc.split(" ")
level = loc[-1]
loc = " ".join(loc[:-1])
loc = loc.title().replace("Of", "of")
place_boss(world, player, boss, loc, level)
already_placed_bosses.append(boss)
boss_locations.remove((loc, level))
else: # boss chosen with no specified locations
boss = boss.title()
boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations)
return already_placed_bosses, boss_locations
def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> bool:
# blacklist approach
if boss in {"Agahnim", "Agahnim2", "Ganon"}:
@@ -187,62 +216,50 @@ def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) ->
return True
restrictive_boss_locations = {}
restrictive_boss_locations: Dict[Tuple[str, str], bool] = {}
for location in boss_location_table:
restrictive_boss_locations[location] = not all(can_place_boss(boss, *location)
for boss in boss_table if not boss.startswith("Agahnim"))
def place_boss(world, player: int, boss: str, location: str, level: Optional[str]):
def place_boss(world, player: int, boss: str, location: str, level: Optional[str]) -> None:
if location == 'Ganons Tower' and world.mode[player] == 'inverted':
location = 'Inverted Ganons Tower'
logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else ''))
world.get_dungeon(location, player).bosses[level] = BossFactory(boss, player)
def format_boss_location(location, level):
def format_boss_location(location: str, level: str) -> str:
return location + (' (' + level + ')' if level else '')
def place_bosses(world, player: int):
if world.boss_shuffle[player] == 'none':
def place_bosses(world, player: int) -> None:
# will either be an int or a lower case string with ';' between options
boss_shuffle: Union[str, int] = world.boss_shuffle[player].value
already_placed_bosses: List[str] = []
remaining_locations: List[Tuple[str, str]] = []
# handle plando
if isinstance(boss_shuffle, str):
# figure out our remaining mode, convert it to an int and remove it from plando_args
options = boss_shuffle.split(";")
boss_shuffle = Bosses.options[options.pop()]
# place our plando bosses
already_placed_bosses, remaining_locations = place_plando_bosses(options, world, player)
if boss_shuffle == Bosses.option_none: # vanilla boss locations
return
# Most to least restrictive order
boss_locations = boss_location_table.copy()
world.random.shuffle(boss_locations)
boss_locations.sort(key= lambda location: -int(restrictive_boss_locations[location]))
if not remaining_locations and not already_placed_bosses:
remaining_locations = boss_location_table.copy()
world.random.shuffle(remaining_locations)
remaining_locations.sort(key=lambda location: -int(restrictive_boss_locations[location]))
all_bosses = sorted(boss_table.keys()) # sorted to be deterministic on older pythons
placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
shuffle_mode = world.boss_shuffle[player]
already_placed_bosses = []
if ";" in shuffle_mode:
bosses = shuffle_mode.split(";")
shuffle_mode = bosses.pop()
for boss in bosses:
if "-" in boss:
loc, boss = boss.split("-")
boss = boss.title()
level = None
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
# split off level
loc = loc.split(" ")
level = loc[-1]
loc = " ".join(loc[:-1])
loc = loc.title().replace("Of", "of")
if can_place_boss(boss, loc, level) and (loc, level) in boss_locations:
place_boss(world, player, boss, loc, level)
already_placed_bosses.append(boss)
boss_locations.remove((loc, level))
else:
raise Exception(f"Cannot place {boss} at {format_boss_location(loc, level)} for player {player}.")
else:
boss = boss.title()
boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations)
if shuffle_mode == "none":
return # vanilla bosses come pre-placed
if shuffle_mode in ["basic", "full"]:
if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled
if boss_shuffle == Bosses.option_basic or boss_shuffle == Bosses.option_full:
if boss_shuffle == Bosses.option_basic: # vanilla bosses shuffled
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
else: # all bosses present, the three duplicates chosen at random
bosses = placeable_bosses + world.random.sample(placeable_bosses, 3)
@@ -258,7 +275,7 @@ def place_bosses(world, player: int):
logging.debug('Bosses chosen %s', bosses)
world.random.shuffle(bosses)
for loc, level in boss_locations:
for loc, level in remaining_locations:
for _ in range(len(bosses)):
boss = bosses.pop()
if can_place_boss(boss, loc, level):
@@ -272,8 +289,8 @@ def place_bosses(world, player: int):
place_boss(world, player, boss, loc, level)
elif shuffle_mode == "chaos": # all bosses chosen at random
for loc, level in boss_locations:
elif boss_shuffle == Bosses.option_chaos: # all bosses chosen at random
for loc, level in remaining_locations:
try:
boss = world.random.choice(
[b for b in placeable_bosses if can_place_boss(b, loc, level)])
@@ -282,9 +299,9 @@ def place_bosses(world, player: int):
else:
place_boss(world, player, boss, loc, level)
elif shuffle_mode == "singularity":
elif boss_shuffle == Bosses.option_singularity:
primary_boss = world.random.choice(placeable_bosses)
remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, boss_locations)
remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, remaining_locations)
if remaining_boss_locations:
# pick a boss to go into the remaining locations
remaining_boss = world.random.choice([boss for boss in placeable_bosses if all(
@@ -293,12 +310,12 @@ def place_bosses(world, player: int):
if remaining_boss_locations:
raise Exception("Unfilled boss locations!")
else:
raise FillError(f"Could not find boss shuffle mode {shuffle_mode}")
raise FillError(f"Could not find boss shuffle mode {boss_shuffle}")
def place_where_possible(world, player: int, boss: str, boss_locations):
remainder = []
placed_bosses = []
def place_where_possible(world, player: int, boss: str, boss_locations) -> Tuple[List[Tuple[str, str]], List[str]]:
remainder: List[Tuple[str, str]] = []
placed_bosses: List[str] = []
for loc, level in boss_locations:
# place that boss where it can go
if can_place_boss(boss, loc, level):

View File

@@ -212,9 +212,7 @@ def parse_arguments(argv, no_defaults=False):
Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted.
''')
parser.add_argument('--gui', help='Launch the GUI', action='store_true')
parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core'))
parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos',
"singularity"])

View File

@@ -480,7 +480,7 @@ def set_up_take_anys(world, player):
old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots)
world.shops.append(old_man_take_any.shop)
swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player]
swords = [item for item in world.itempool if item.player == player and item.type == 'Sword']
if swords:
sword = world.random.choice(swords)
world.itempool.remove(sword)

View File

@@ -1,7 +1,7 @@
import typing
from BaseClasses import MultiWorld
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice
class Logic(Choice):
@@ -39,8 +39,6 @@ class OpenPyramid(Choice):
option_auto = 3
default = option_goal
alias_true = option_open
alias_false = option_closed
alias_yes = option_open
alias_no = option_closed
@@ -140,13 +138,143 @@ class WorldState(Choice):
option_inverted = 2
class Bosses(Choice):
option_vanilla = 0
option_simple = 1
class Bosses(TextChoice):
"""Shuffles bosses around to different locations.
Basic will shuffle all bosses except Ganon and Agahnim anywhere they can be placed.
Full chooses 3 bosses at random to be placed twice instead of Lanmolas, Moldorm, and Helmasaur.
Chaos allows any boss to appear any number of times.
Singularity places a single boss in as many places as possible, and a second boss in any remaining locations.
Supports plando placement. Formatting here: https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/plando/en"""
display_name = "Boss Shuffle"
option_none = 0
option_basic = 1
option_full = 2
option_chaos = 3
option_singularity = 4
bosses: set = {
"Armos Knights",
"Lanmolas",
"Moldorm",
"Helmasaur King",
"Arrghus",
"Mothula",
"Blind",
"Kholdstare",
"Vitreous",
"Trinexx",
}
locations: set = {
"Ganons Tower Top",
"Tower of Hera",
"Skull Woods",
"Ganons Tower Middle",
"Eastern Palace",
"Desert Palace",
"Palace of Darkness",
"Swamp Palace",
"Thieves Town",
"Ice Palace",
"Misery Mire",
"Turtle Rock",
"Ganons Tower Bottom"
}
def __init__(self, value: typing.Union[str, int]):
assert isinstance(value, str) or isinstance(value, int), \
f"{value} is not a valid option for {self.__class__.__name__}"
self.value = value
@classmethod
def from_text(cls, text: str):
import random
# set all of our text to lower case for name checking
text = text.lower()
cls.bosses = {boss_name.lower() for boss_name in cls.bosses}
cls.locations = {boss_location.lower() for boss_location in cls.locations}
if text == "random":
return cls(random.choice(list(cls.options.values())))
for option_name, value in cls.options.items():
if option_name == text:
return cls(value)
options = text.split(";")
# since plando exists in the option verify the plando values given are valid
cls.validate_plando_bosses(options)
# find out what type of boss shuffle we should use for placing bosses after plando
# and add as a string to look nice in the spoiler
if "random" in options:
shuffle = random.choice(list(cls.options))
options.remove("random")
options = ";".join(options) + ";" + shuffle
boss_class = cls(options)
else:
for option in options:
if option in cls.options:
boss_class = cls(";".join(options))
break
else:
if len(options) == 1:
if cls.valid_boss_name(options[0]):
options = options[0] + ";singularity"
boss_class = cls(options)
else:
options = options[0] + ";none"
boss_class = cls(options)
else:
options = ";".join(options) + ";none"
boss_class = cls(options)
return boss_class
@classmethod
def validate_plando_bosses(cls, options: typing.List[str]) -> None:
from .Bosses import can_place_boss, format_boss_location
for option in options:
if option == "random" or option in cls.options:
if option != options[-1]:
raise ValueError(f"{option} option must be at the end of the boss_shuffle options!")
continue
if "-" in option:
location, boss = option.split("-")
level = ''
if not cls.valid_boss_name(boss):
raise ValueError(f"{boss} is not a valid boss name for location {location}.")
if not cls.valid_location_name(location):
raise ValueError(f"{location} is not a valid boss location name.")
if location.split(" ")[-1] in ("top", "middle", "bottom"):
location = location.split(" ")
level = location[-1]
location = " ".join(location[:-1])
location = location.title().replace("Of", "of")
if not can_place_boss(boss.title(), location, level):
raise ValueError(f"{format_boss_location(location, level)} "
f"is not a valid location for {boss.title()}.")
else:
if not cls.valid_boss_name(option):
raise ValueError(f"{option} is not a valid boss name.")
@classmethod
def valid_boss_name(cls, value: str) -> bool:
return value.lower() in cls.bosses
@classmethod
def valid_location_name(cls, value: str) -> bool:
return value in cls.locations
def verify(self, world, player_name: str, plando_options) -> None:
if isinstance(self.value, int):
return
from Generate import PlandoSettings
if not(PlandoSettings.bosses & plando_options):
import logging
# 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]
logging.warning(f"The plando bosses module is turned off, so {self.name_lookup[self.value].title()} "
f"boss shuffle will be used for player {player_name}.")
class Enemies(Choice):
option_vanilla = 0
@@ -159,8 +287,6 @@ class Progressive(Choice):
option_off = 0
option_grouped_random = 1
option_on = 2
alias_false = 0
alias_true = 2
default = 2
def want_progressives(self, random):
@@ -168,8 +294,8 @@ class Progressive(Choice):
class Swordless(Toggle):
"""No swords. Curtains in Skull Woods and Agahnim\'s
Tower are removed, Agahnim\'s Tower barrier can be
"""No swords. Curtains in Skull Woods and Agahnim's
Tower are removed, Agahnim's Tower barrier can be
destroyed with hammer. Misery Mire and Turtle Rock
can be opened without a sword. Hammer damages Ganon.
Ether and Bombos Tablet can be activated with Hammer
@@ -202,8 +328,6 @@ class Hints(Choice):
option_on = 2
option_full = 3
default = 2
alias_false = 0
alias_true = 2
class Scams(Choice):
@@ -213,7 +337,6 @@ class Scams(Choice):
option_king_zora = 1
option_bottle_merchant = 2
option_all = 3
alias_false = 0
@property
def gives_king_zora_hint(self):
@@ -282,8 +405,8 @@ class ShieldPalette(Palette):
display_name = "Shield Palette"
class LinkPalette(Palette):
display_name = "Link Palette"
# class LinkPalette(Palette):
# display_name = "Link Palette"
class HeartBeep(Choice):
@@ -293,7 +416,6 @@ class HeartBeep(Choice):
option_half = 2
option_quarter = 3
option_off = 4
alias_false = 4
class HeartColor(Choice):
@@ -375,6 +497,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
"hints": Hints,
"scams": Scams,
"restrict_dungeon_item_on_boss": RestrictBossItem,
"boss_shuffle": Bosses,
"pot_shuffle": PotShuffle,
"enemy_shuffle": EnemyShuffle,
"killable_thieves": KillableThieves,
@@ -387,7 +510,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
"hud_palettes": HUDPalette,
"sword_palettes": SwordPalette,
"shield_palettes": ShieldPalette,
"link_palettes": LinkPalette,
# "link_palettes": LinkPalette,
"heartbeep": HeartBeep,
"heartcolor": HeartColor,
"quickswap": QuickSwap,

View File

@@ -4,6 +4,10 @@ import typing
from BaseClasses import Region, Entrance, RegionType
def is_main_entrance(entrance: Entrance) -> bool:
return entrance.parent_region.type in {RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic}
def create_regions(world, player):
world.regions += [

View File

@@ -34,7 +34,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts
DeathMountain_texts, \
LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \
SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml
from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items
from worlds.alttp.EntranceShuffle import door_addresses
from worlds.alttp.Options import smallkey_shuffle
@@ -551,18 +551,22 @@ class Sprite():
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
def from_ap_sprite(self, filedata):
filedata = filedata.decode("utf-8-sig")
import yaml
obj = yaml.safe_load(filedata)
if obj["min_format_version"] > 1:
raise Exception("Sprite file requires an updated reader.")
self.author_name = obj["author"]
self.name = obj["name"]
if obj["data"]: # skip patching for vanilla content
data = bsdiff4.patch(Sprite.base_data, obj["data"])
self.sprite = data[:self.sprite_size]
self.palette = data[self.sprite_size:self.palette_size]
self.glove_palette = data[self.sprite_size + self.palette_size:]
# noinspection PyBroadException
try:
obj = parse_yaml(filedata.decode("utf-8-sig"))
if obj["min_format_version"] > 1:
raise Exception("Sprite file requires an updated reader.")
self.author_name = obj["author"]
self.name = obj["name"]
if obj["data"]: # skip patching for vanilla content
data = bsdiff4.patch(Sprite.base_data, obj["data"])
self.sprite = data[:self.sprite_size]
self.palette = data[self.sprite_size:self.palette_size]
self.glove_palette = data[self.sprite_size + self.palette_size:]
except Exception:
logger = logging.getLogger("apsprite")
logger.exception("Error parsing apsprite file")
self.valid = False
@property
def author_game_display(self) -> str:
@@ -659,7 +663,7 @@ class Sprite():
@staticmethod
def parse_zspr(filedata, expected_kind):
logger = logging.getLogger('ZSPR')
logger = logging.getLogger("ZSPR")
headerstr = "<4xBHHIHIHH6x"
headersize = struct.calcsize(headerstr)
if len(filedata) < headersize:
@@ -667,7 +671,7 @@ class Sprite():
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
headerstr, filedata)
if version not in [1]:
logger.error('Error parsing ZSPR file: Version %g not supported', version)
logger.error("Error parsing ZSPR file: Version %g not supported", version)
return None
if kind != expected_kind:
return None
@@ -676,36 +680,42 @@ class Sprite():
stream.seek(headersize)
def read_utf16le(stream):
"Decodes a null-terminated UTF-16_LE string of unknown size from a stream"
"""Decodes a null-terminated UTF-16_LE string of unknown size from a stream"""
raw = bytearray()
while True:
char = stream.read(2)
if char in [b'', b'\x00\x00']:
if char in [b"", b"\x00\x00"]:
break
raw += char
return raw.decode('utf-16_le')
return raw.decode("utf-16_le")
sprite_name = read_utf16le(stream)
author_name = read_utf16le(stream)
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
# noinspection PyBroadException
try:
sprite_name = read_utf16le(stream)
author_name = read_utf16le(stream)
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
# Ignoring the Author Rom name for the time being.
# Ignoring the Author Rom name for the time being.
real_csum = sum(filedata) % 0x10000
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
logger.warning('ZSPR file has incorrect checksum. It may be corrupted.')
real_csum = sum(filedata) % 0x10000
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
logger.warning("ZSPR file has incorrect checksum. It may be corrupted.")
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
palette = filedata[palette_offset:palette_offset + palette_size]
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
palette = filedata[palette_offset:palette_offset + palette_size]
if len(sprite) != sprite_size or len(palette) != palette_size:
logger.error('Error parsing ZSPR file: Unexpected end of file')
if len(sprite) != sprite_size or len(palette) != palette_size:
logger.error("Error parsing ZSPR file: Unexpected end of file")
return None
return sprite, palette, sprite_name, author_name, author_credits_name
except Exception:
logger.exception("Error parsing ZSPR file")
return None
return (sprite, palette, sprite_name, author_name, author_credits_name)
def decode_palette(self):
"Returns the palettes as an array of arrays of 15 colors"
"""Returns the palettes as an array of arrays of 15 colors"""
def array_chunk(arr, size):
return list(zip(*[iter(arr)] * size))

View File

@@ -4,6 +4,7 @@ import random
import threading
import typing
import Utils
from BaseClasses import Item, CollectionState, Tutorial
from .Dungeons import create_dungeons
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
@@ -11,7 +12,8 @@ from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
from .ItemPool import generate_itempool, difficulties
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
from .Options import alttp_options, smallkey_shuffle
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \
is_main_entrance
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
get_hash_string, get_base_rom_path, LttPDeltaPatch
from .Rules import set_rules
@@ -23,6 +25,7 @@ lttp_logger = logging.getLogger("A Link to the Past")
extras_list = sum(difficulties['normal'].extras[0:5], [])
class ALTTPWeb(WebWorld):
setup_en = Tutorial(
"Multiworld Setup Tutorial",
@@ -98,6 +101,232 @@ class ALTTPWeb(WebWorld):
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando]
if typing.TYPE_CHECKING:
from WebHostLib.tracker import PlayerTracker
else:
PlayerTracker = object
def modify_tracker(self, tracker: PlayerTracker):
tracker.template = 'zeldaKeysTracker.html'
tracker.icons = {
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
"Red Shield": r"https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
"Mirror Shield": r"https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png",
"Fighter Sword": r"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": r"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": r"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": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920",
"Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c",
"Silver Bow": r"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": r"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": r"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": r"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": r"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": r"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": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725",
"Pegasus Boots": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9",
"Progressive Glove": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
"Flippers": r"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": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e",
"Progressive Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed",
"Blue Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e",
"Red Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400",
"Hookshot": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b",
"Mushroom": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59",
"Magic Powder": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=c24e38effbd4f80496d35830ce8ff4ec",
"Fire Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0",
"Ice Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc",
"Bombos": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26",
"Ether": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5",
"Quake": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879",
"Lamp": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce",
"Hammer": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500",
"Shovel": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05",
"Flute": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390",
"Bug Catching Net": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6",
"Book of Mudora": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744",
"Bottle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b",
"Cane of Somaria": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943",
"Cane of Byrna": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54",
"Cape": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832",
"Magic Mirror": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc",
"Triforce": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48",
"Small Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e",
"Big Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d",
"Chest": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda",
"Light World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6",
"Dark World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc",
"Hyrule Castle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be",
"Agahnims Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5",
"Desert Palace": r"https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png",
"Eastern Palace": r"https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png",
"Tower of Hera": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7",
"Palace of Darkness": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022",
"Swamp Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5",
"Skull Woods": r"https://alttp-wiki.net/images/6/6a/Mothula.png",
"Thieves Town": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222",
"Ice Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0",
"Misery Mire": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8",
"Turtle Rock": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be",
"Ganons Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74"
}
tracker.regions = {
'Light World': [
'Lost Woods Hideout', 'Lumberjack Tree', 'Mushroom', 'Master Sword Pedestal', 'Bottle Merchant', 'Flute Spot',
'Blind\'s Hideout - Top', 'Blind\'s Hideout - Left', 'Blind\'s Hideout - Right', 'Blind\'s Hideout - Far Left', 'Blind\'s Hideout - Far Right',
'Link\'s House', 'Link\'s Uncle', 'Secret Passage',
'King Zora', 'Zora\'s Ledge', 'Waterfall Fairy - Left', 'Waterfall Fairy - Right',
'King\'s Tomb', 'Graveyard Cave', 'Bonk Rock Cave',
'Sunken Treasure', 'Floodgate Chest', 'Hobo', 'Ice Rod Cave', 'Lake Hylia Island',
'Kakariko Tavern', 'Chicken House', 'Sick Kid',
'Blacksmith', 'Purple Chest', 'Magic Bat',
'Aginah\'s Cave', 'Cave 45', 'Checkerboard Cave',
'Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla',
'Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', 'Kakariko Well - Bottom',
'Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy',
'Library', 'Maze Race', 'Potion Shop', 'Desert Ledge',
'Old Man', 'Spectacle Rock',
'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle',
'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right',
'Spiral Cave', 'Ether Tablet'
],
'Dark World': [
'Pyramid', 'Catfish', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right',
'Stumpy', 'Digging Game',
'Bombos Tablet',
'Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy',
'Peg Cave', 'Brewery', 'C-Shaped House', 'Chest Game',
'Bumper Cave Ledge',
'Mire Shed - Left', 'Mire Shed - Right',
'Superbunny Cave - Top', 'Superbunny Cave - Bottom',
'Spike Cave', 'Floating Island', 'Mimic Cave',
'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left',
],
'Desert Palace': [
'Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest',
'Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest',
'Desert Palace - Boss'
],
'Eastern Palace': [
'Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest',
'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss'
],
'Hyrule Castle': [
'Hyrule Castle - Map Chest', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Zelda\'s Chest',
'Sewers - Dark Cross', 'Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', 'Sewers - Secret Room - Right',
'Sanctuary'
],
'Agahnims Tower': [
'Castle Tower - Room 03', 'Castle Tower - Dark Maze'
],
'Tower of Hera': [
'Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest', 'Tower of Hera - Big Key Chest',
'Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss'
],
'Swamp Palace': [
'Swamp Palace - Entrance', 'Swamp Palace - Map Chest',
'Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest',
'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest',
'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss'
],
'Thieves Town': [
'Thieves\' Town - Big Key Chest', 'Thieves\' Town - Map Chest', 'Thieves\' Town - Compass Chest', 'Thieves\' Town - Ambush Chest',
'Thieves\' Town - Attic', 'Thieves\' Town - Big Chest', 'Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss'
],
'Skull Woods': [
'Skull Woods - Map Chest', 'Skull Woods - Pinball Room',
'Skull Woods - Compass Chest', 'Skull Woods - Pot Prison',
'Skull Woods - Big Chest',
'Skull Woods - Big Key Chest',
'Skull Woods - Bridge Room', 'Skull Woods - Boss'
],
'Ice Palace': [
'Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest', 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room',
'Ice Palace - Spike Room', 'Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Boss'
],
'Misery Mire': [
'Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
'Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest', 'Misery Mire - Boss'
],
'Turtle Rock': [
'Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right',
'Turtle Rock - Chain Chomps', 'Turtle Rock - Big Key Chest', 'Turtle Rock - Big Chest',
'Turtle Rock - Crystaroller Room',
'Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right',
'Turtle Rock - Boss'
],
'Palace of Darkness': [
'Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement',
'Palace of Darkness - Big Key Chest',
'Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest',
'Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right',
'Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest',
'Palace of Darkness - Harmless Hellway', 'Palace of Darkness - Boss'
],
'Ganons Tower': [
'Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left', 'Ganons Tower - Hope Room - Right',
'Ganons Tower - Tile Room',
'Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right',
'Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
'Ganons Tower - Map Chest', 'Ganons Tower - Firesnake Room',
'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right',
'Ganons Tower - Bob\'s Chest',
'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest',
]
}
tracker.progressive_items = [
'Progressive Sword',
'Progressive Shield',
'Progressive Mail',
'Progressive Bow',
'Progressive Boomerang',
'Hookshot',
'Magic Powder',
'Mushroom',
'Bottle',
'Lamp',
'Progressive Glove',
'Flippers',
'Moon Pearl',
'Bombos',
'Ether',
'Quake',
'Fire Rod',
'Ice Rod',
'Hammer',
'Book of Mudora',
'Shovel',
'Flute',
'Bug Catching Net',
'Cane of Somaria',
'Cane of Byrna',
'Cape',
'Magic Mirror',
'Small Key',
'Big Key'
]
tracker.progressive_names = {
'Progressive Bow': ['Bow', 'Silver Arrows', 'Silver Bow', 'Progressive Bow (Alt)'],
'Bottle': ['Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)'],
'Progressive Sword': ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
'Progressive Glove': ['Power Glove', 'Titans Mitts'],
'Progressive Shield': ['Blue Shield', 'Red Shield', 'Mirror Shield'],
'Progressive Boomerang': ['Red Boomerang', 'Blue Boomerang'],
'Progressive Mail': ['Green Mail', 'Blue Mail', 'Red Mail'],
'Small Key': [f'Small Key ({region})' for region in tracker.regions.keys() if region not in {'Light World', 'Dark World'}],
'Big Key': [f'Big Key ({region})' for region in tracker.regions.keys() if region not in {'Light World', 'Dark World'}],
}
tracker.region_keys = {
region: [f'Small Key ({region})', f'Big Key ({region})'] for region in tracker.regions.keys() if region not in {'Light World', 'Dark World'}
}
class ALTTPWorld(World):
"""
@@ -136,6 +365,10 @@ class ALTTPWorld(World):
create_items = generate_itempool
enemizer_path: str = Utils.get_options()["generator"]["enemizer_path"] \
if os.path.isabs(Utils.get_options()["generator"]["enemizer_path"]) \
else Utils.local_path(Utils.get_options()["generator"]["enemizer_path"])
def __init__(self, *args, **kwargs):
self.dungeon_local_item_names = set()
self.dungeon_specific_item_names = set()
@@ -150,12 +383,12 @@ class ALTTPWorld(World):
raise FileNotFoundError(rom_file)
def generate_early(self):
if self.use_enemizer():
check_enemizer(self.enemizer_path)
player = self.player
world = self.world
if self.use_enemizer():
check_enemizer(world.enemizer)
# system for sharing ER layouts
self.er_seed = str(world.random.randint(0, 2 ** 64))
@@ -344,7 +577,7 @@ class ALTTPWorld(World):
def use_enemizer(self):
world = self.world
player = self.player
return (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
return (world.boss_shuffle[player] or world.enemy_shuffle[player]
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.pot_shuffle[player] or world.bush_shuffle[player]
or world.killable_thieves[player])
@@ -360,7 +593,7 @@ class ALTTPWorld(World):
patch_rom(world, rom, player, use_enemizer)
if use_enemizer:
patch_enemizer(world, player, rom, world.enemizer, output_directory)
patch_enemizer(world, player, rom, self.enemizer_path, output_directory)
if world.is_race:
patch_race_rom(rom, world, player)
@@ -373,7 +606,7 @@ class ALTTPWorld(World):
'hud': world.hud_palettes[player],
'sword': world.sword_palettes[player],
'shield': world.shield_palettes[player],
'link': world.link_palettes[player]
# 'link': world.link_palettes[player]
}
palettes_options = {key: option.current_key for key, option in palettes_options.items()}
@@ -405,6 +638,20 @@ class ALTTPWorld(World):
finally:
self.rom_name_available_event.set() # make sure threading continues and errors are collected
@classmethod
def stage_extend_hint_information(cls, world, hint_data: typing.Dict[int, typing.Dict[int, str]]):
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
world.shuffle[player] != "vanilla" or world.retro_caves[player]}
for region in world.regions:
if region.player in er_hint_data and region.locations:
main_entrance = region.get_connecting_entrance(is_main_entrance)
for location in region.locations:
if type(location.address) == int: # skips events and crystals
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
er_hint_data[region.player][location.address] = main_entrance.name
hint_data.update(er_hint_data)
def modify_multidata(self, multidata: dict):
import base64
# wait for self.rom_name to be available.
@@ -419,8 +666,7 @@ class ALTTPWorld(World):
return ALttPItem(name, self.player, **item_init_table[name])
@classmethod
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
restitempool, fill_locations):
def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
trash_counts = {}
standard_keyshuffle_players = set()
for player in world.get_game_players("A Link to the Past"):
@@ -467,26 +713,15 @@ class ALTTPWorld(World):
for player, trash_count in trash_counts.items():
gtower_locations = locations_mapping[player]
world.random.shuffle(gtower_locations)
localrest = localrestitempool[player]
if localrest:
gt_item_pool = restitempool + localrest
world.random.shuffle(gt_item_pool)
else:
gt_item_pool = restitempool.copy()
while gtower_locations and gt_item_pool and trash_count > 0:
while gtower_locations and filleritempool and trash_count > 0:
spot_to_fill = gtower_locations.pop()
item_to_place = gt_item_pool.pop()
item_to_place = filleritempool.pop()
if spot_to_fill.item_rule(item_to_place):
if item_to_place in localrest:
localrest.remove(item_to_place)
else:
restitempool.remove(item_to_place)
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill) # very slow, unfortunately
trash_count -= 1
def get_filler_item_name(self) -> str:
if self.world.goal[self.player] == "icerodhunt":
item = "Nothing"

View File

@@ -26,10 +26,14 @@
- Example: `Trinexx`
- Takes a particular boss and places that boss in any remaining slots in which this boss can function.
- In this example, it would fill Desert Palace, but not Tower of Hera.
- If no other options are provided this will follow normal singularity rules with that boss.
- Boss Shuffle:
- Example: `simple`
- Example: `basic`
- Runs a particular boss shuffle mode to finish construction instead of vanilla placement, typically used as
a last instruction.
- Supports `random` which will choose a random option from the normal choices.
- If one is not supplied any remaining locations will be unshuffled unless a single specific boss is
supplied in which case it will use singularity as noted above.
- [Available Bosses](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L135)
- [Available Arenas](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L150)

View File

@@ -66,7 +66,7 @@ async def dkc3_game_watcher(ctx: Context):
return
new_checks = []
from worlds.dkc3.Rom import location_rom_data, item_rom_data
from worlds.dkc3.Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map
for loc_id, loc_data in location_rom_data.items():
if loc_id not in ctx.locations_checked:
data = await snes_read(ctx, WRAM_START + loc_data[0], 1)
@@ -186,22 +186,40 @@ async def dkc3_game_watcher(ctx: Context):
# DKC3_TODO: This method of collect should work, however it does not unlock the next level correctly when previous is flagged
# Handle Collected Locations
#for loc_id in ctx.checked_locations:
# if loc_id not in ctx.locations_checked:
# loc_data = location_rom_data[loc_id]
# data = await snes_read(ctx, WRAM_START + loc_data[0], 1)
# invert_bit = ((len(loc_data) >= 3) and loc_data[2])
# if not invert_bit:
# masked_data = data[0] | (1 << loc_data[1])
# print("Collected Location: ", hex(loc_data[0]), " | ", loc_data[1])
# snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
# await snes_flush_writes(ctx)
# else:
# masked_data = data[0] & ~(1 << loc_data[1])
# print("Collected Inverted Location: ", hex(loc_data[0]), " | ", loc_data[1])
# snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
# await snes_flush_writes(ctx)
# ctx.locations_checked.add(loc_id)
for loc_id in ctx.checked_locations:
if loc_id not in ctx.locations_checked and loc_id not in boss_location_ids:
loc_data = location_rom_data[loc_id]
data = await snes_read(ctx, WRAM_START + loc_data[0], 1)
invert_bit = ((len(loc_data) >= 3) and loc_data[2])
if not invert_bit:
masked_data = data[0] | (1 << loc_data[1])
#print("Collected Location: ", hex(loc_data[0]), " | ", loc_data[1])
snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
if (loc_data[1] == 1):
# Make the next levels accessible
level_id = loc_data[0] - 0x632
levels_to_tiles = await snes_read(ctx, ROM_START + 0x3FF800, 0x60)
tiles_to_levels = await snes_read(ctx, ROM_START + 0x3FF860, 0x60)
tile_id = levels_to_tiles[level_id] if levels_to_tiles[level_id] != 0xFF else level_id
tile_id = tile_id + 0x632
#print("Tile ID: ", hex(tile_id))
if tile_id in level_unlock_map:
for next_level_address in level_unlock_map[tile_id]:
next_level_id = next_level_address - 0x632
next_tile_id = tiles_to_levels[next_level_id] if tiles_to_levels[next_level_id] != 0xFF else next_level_id
next_tile_id = next_tile_id + 0x632
#print("Next Level ID: ", hex(next_tile_id))
next_data = await snes_read(ctx, WRAM_START + next_tile_id, 1)
snes_buffered_write(ctx, WRAM_START + next_tile_id, bytes([next_data[0] | 0x01]))
await snes_flush_writes(ctx)
else:
masked_data = data[0] & ~(1 << loc_data[1])
print("Collected Inverted Location: ", hex(loc_data[0]), " | ", loc_data[1])
snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
await snes_flush_writes(ctx)
ctx.locations_checked.add(loc_id)
# Calculate Boomer Cost Text
boomer_cost_text = await snes_read(ctx, WRAM_START + 0xAAFD, 2)

View File

@@ -221,6 +221,55 @@ level_location_table = {
LocationName.rocket_rush_dk: 0xDC30A0,
}
kong_location_table = {
LocationName.lakeside_limbo_kong: 0xDC3100,
LocationName.doorstop_dash_kong: 0xDC3104,
LocationName.tidal_trouble_kong: 0xDC3108,
LocationName.skiddas_row_kong: 0xDC310C,
LocationName.murky_mill_kong: 0xDC3110,
LocationName.barrel_shield_bust_up_kong: 0xDC3114,
LocationName.riverside_race_kong: 0xDC3118,
LocationName.squeals_on_wheels_kong: 0xDC311C,
LocationName.springin_spiders_kong: 0xDC3120,
LocationName.bobbing_barrel_brawl_kong: 0xDC3124,
LocationName.bazzas_blockade_kong: 0xDC3128,
LocationName.rocket_barrel_ride_kong: 0xDC312C,
LocationName.kreeping_klasps_kong: 0xDC3130,
LocationName.tracker_barrel_trek_kong: 0xDC3134,
LocationName.fish_food_frenzy_kong: 0xDC3138,
LocationName.fire_ball_frenzy_kong: 0xDC313C,
LocationName.demolition_drain_pipe_kong: 0xDC3140,
LocationName.ripsaw_rage_kong: 0xDC3144,
LocationName.blazing_bazookas_kong: 0xDC3148,
LocationName.low_g_labyrinth_kong: 0xDC314C,
LocationName.krevice_kreepers_kong: 0xDC3150,
LocationName.tearaway_toboggan_kong: 0xDC3154,
LocationName.barrel_drop_bounce_kong: 0xDC3158,
LocationName.krack_shot_kroc_kong: 0xDC315C,
LocationName.lemguin_lunge_kong: 0xDC3160,
LocationName.buzzer_barrage_kong: 0xDC3164,
LocationName.kong_fused_cliffs_kong: 0xDC3168,
LocationName.floodlit_fish_kong: 0xDC316C,
LocationName.pothole_panic_kong: 0xDC3170,
LocationName.ropey_rumpus_kong: 0xDC3174,
LocationName.konveyor_rope_clash_kong: 0xDC3178,
LocationName.creepy_caverns_kong: 0xDC317C,
LocationName.lightning_lookout_kong: 0xDC3180,
LocationName.koindozer_klamber_kong: 0xDC3184,
LocationName.poisonous_pipeline_kong: 0xDC3188,
LocationName.stampede_sprint_kong: 0xDC318C,
LocationName.criss_cross_cliffs_kong: 0xDC3191,
LocationName.tyrant_twin_tussle_kong: 0xDC3195,
LocationName.swoopy_salvo_kong: 0xDC319A,
}
boss_location_table = {
LocationName.belchas_barn: 0xDC30A1,
@@ -266,6 +315,7 @@ all_locations = {
**boss_location_table,
**secret_cave_location_table,
**brothers_bear_location_table,
**kong_location_table,
}
location_table = {}
@@ -277,6 +327,9 @@ def setup_locations(world, player: int):
if False:#world.include_trade_sequence[player].value:
location_table.update({**brothers_bear_location_table})
if world.kongsanity[player].value:
location_table.update({**kong_location_table})
return location_table

View File

@@ -1,197 +1,236 @@
# Level Definitions
lakeside_limbo_flag = "Lakeside Limbo - Flag"
lakeside_limbo_kong = "Lakeside Limbo - KONG"
lakeside_limbo_bonus_1 = "Lakeside Limbo - Bonus 1"
lakeside_limbo_bonus_2 = "Lakeside Limbo - Bonus 2"
lakeside_limbo_dk = "Lakeside Limbo - DK Coin"
doorstop_dash_flag = "Doorstop Dash - Flag"
doorstop_dash_kong = "Doorstop Dash - KONG"
doorstop_dash_bonus_1 = "Doorstop Dash - Bonus 1"
doorstop_dash_bonus_2 = "Doorstop Dash - Bonus 2"
doorstop_dash_dk = "Doorstop Dash - DK Coin"
tidal_trouble_flag = "Tidal Trouble - Flag"
tidal_trouble_kong = "Tidal Trouble - KONG"
tidal_trouble_bonus_1 = "Tidal Trouble - Bonus 1"
tidal_trouble_bonus_2 = "Tidal Trouble - Bonus 2"
tidal_trouble_dk = "Tidal Trouble - DK Coin"
skiddas_row_flag = "Skidda's Row - Flag"
skiddas_row_kong = "Skidda's Row - KONG"
skiddas_row_bonus_1 = "Skidda's Row - Bonus 1"
skiddas_row_bonus_2 = "Skidda's Row - Bonus 2"
skiddas_row_dk = "Skidda's Row - DK Coin"
murky_mill_flag = "Murky Mill - Flag"
murky_mill_kong = "Murky Mill - KONG"
murky_mill_bonus_1 = "Murky Mill - Bonus 1"
murky_mill_bonus_2 = "Murky Mill - Bonus 2"
murky_mill_dk = "Murky Mill - DK Coin"
barrel_shield_bust_up_flag = "Barrel Shield Bust-Up - Flag"
barrel_shield_bust_up_kong = "Barrel Shield Bust-Up - KONG"
barrel_shield_bust_up_bonus_1 = "Barrel Shield Bust-Up - Bonus 1"
barrel_shield_bust_up_bonus_2 = "Barrel Shield Bust-Up - Bonus 2"
barrel_shield_bust_up_dk = "Barrel Shield Bust-Up - DK Coin"
riverside_race_flag = "Riverside Race - Flag"
riverside_race_kong = "Riverside Race - KONG"
riverside_race_bonus_1 = "Riverside Race - Bonus 1"
riverside_race_bonus_2 = "Riverside Race - Bonus 2"
riverside_race_dk = "Riverside Race - DK Coin"
squeals_on_wheels_flag = "Squeals On Wheels - Flag"
squeals_on_wheels_kong = "Squeals On Wheels - KONG"
squeals_on_wheels_bonus_1 = "Squeals On Wheels - Bonus 1"
squeals_on_wheels_bonus_2 = "Squeals On Wheels - Bonus 2"
squeals_on_wheels_dk = "Squeals On Wheels - DK Coin"
springin_spiders_flag = "Springin' Spiders - Flag"
springin_spiders_kong = "Springin' Spiders - KONG"
springin_spiders_bonus_1 = "Springin' Spiders - Bonus 1"
springin_spiders_bonus_2 = "Springin' Spiders - Bonus 2"
springin_spiders_dk = "Springin' Spiders - DK Coin"
bobbing_barrel_brawl_flag = "Bobbing Barrel Brawl - Flag"
bobbing_barrel_brawl_kong = "Bobbing Barrel Brawl - KONG"
bobbing_barrel_brawl_bonus_1 = "Bobbing Barrel Brawl - Bonus 1"
bobbing_barrel_brawl_bonus_2 = "Bobbing Barrel Brawl - Bonus 2"
bobbing_barrel_brawl_dk = "Bobbing Barrel Brawl - DK Coin"
bazzas_blockade_flag = "Bazza's Blockade - Flag"
bazzas_blockade_kong = "Bazza's Blockade - KONG"
bazzas_blockade_bonus_1 = "Bazza's Blockade - Bonus 1"
bazzas_blockade_bonus_2 = "Bazza's Blockade - Bonus 2"
bazzas_blockade_dk = "Bazza's Blockade - DK Coin"
rocket_barrel_ride_flag = "Rocket Barrel Ride - Flag"
rocket_barrel_ride_kong = "Rocket Barrel Ride - KONG"
rocket_barrel_ride_bonus_1 = "Rocket Barrel Ride - Bonus 1"
rocket_barrel_ride_bonus_2 = "Rocket Barrel Ride - Bonus 2"
rocket_barrel_ride_dk = "Rocket Barrel Ride - DK Coin"
kreeping_klasps_flag = "Kreeping Klasps - Flag"
kreeping_klasps_kong = "Kreeping Klasps - KONG"
kreeping_klasps_bonus_1 = "Kreeping Klasps - Bonus 1"
kreeping_klasps_bonus_2 = "Kreeping Klasps - Bonus 2"
kreeping_klasps_dk = "Kreeping Klasps - DK Coin"
tracker_barrel_trek_flag = "Tracker Barrel Trek - Flag"
tracker_barrel_trek_kong = "Tracker Barrel Trek - KONG"
tracker_barrel_trek_bonus_1 = "Tracker Barrel Trek - Bonus 1"
tracker_barrel_trek_bonus_2 = "Tracker Barrel Trek - Bonus 2"
tracker_barrel_trek_dk = "Tracker Barrel Trek - DK Coin"
fish_food_frenzy_flag = "Fish Food Frenzy - Flag"
fish_food_frenzy_kong = "Fish Food Frenzy - KONG"
fish_food_frenzy_bonus_1 = "Fish Food Frenzy - Bonus 1"
fish_food_frenzy_bonus_2 = "Fish Food Frenzy - Bonus 2"
fish_food_frenzy_dk = "Fish Food Frenzy - DK Coin"
fire_ball_frenzy_flag = "Fire-Ball Frenzy - Flag"
fire_ball_frenzy_kong = "Fire-Ball Frenzy - KONG"
fire_ball_frenzy_bonus_1 = "Fire-Ball Frenzy - Bonus 1"
fire_ball_frenzy_bonus_2 = "Fire-Ball Frenzy - Bonus 2"
fire_ball_frenzy_dk = "Fire-Ball Frenzy - DK Coin"
demolition_drain_pipe_flag = "Demolition Drain-Pipe - Flag"
demolition_drain_pipe_kong = "Demolition Drain-Pipe - KONG"
demolition_drain_pipe_bonus_1 = "Demolition Drain-Pipe - Bonus 1"
demolition_drain_pipe_bonus_2 = "Demolition Drain-Pipe - Bonus 2"
demolition_drain_pipe_dk = "Demolition Drain-Pipe - DK Coin"
ripsaw_rage_flag = "Ripsaw Rage - Flag"
ripsaw_rage_kong = "Ripsaw Rage - KONG"
ripsaw_rage_bonus_1 = "Ripsaw Rage - Bonus 1"
ripsaw_rage_bonus_2 = "Ripsaw Rage - Bonus 2"
ripsaw_rage_dk = "Ripsaw Rage - DK Coin"
blazing_bazookas_flag = "Blazing Bazookas - Flag"
blazing_bazookas_bonus_1 = "Blazing Bazookas - Bonus 1"
blazing_bazookas_bonus_2 = "Blazing Bazookas - Bonus 2"
blazing_bazookas_dk = "Blazing Bazookas - DK Coin"
blazing_bazookas_flag = "Blazing Bazukas - Flag"
blazing_bazookas_kong = "Blazing Bazukas - KONG"
blazing_bazookas_bonus_1 = "Blazing Bazukas - Bonus 1"
blazing_bazookas_bonus_2 = "Blazing Bazukas - Bonus 2"
blazing_bazookas_dk = "Blazing Bazukas - DK Coin"
low_g_labyrinth_flag = "Low-G Labyrinth - Flag"
low_g_labyrinth_kong = "Low-G Labyrinth - KONG"
low_g_labyrinth_bonus_1 = "Low-G Labyrinth - Bonus 1"
low_g_labyrinth_bonus_2 = "Low-G Labyrinth - Bonus 2"
low_g_labyrinth_dk = "Low-G Labyrinth - DK Coin"
krevice_kreepers_flag = "Krevice Kreepers - Flag"
krevice_kreepers_kong = "Krevice Kreepers - KONG"
krevice_kreepers_bonus_1 = "Krevice Kreepers - Bonus 1"
krevice_kreepers_bonus_2 = "Krevice Kreepers - Bonus 2"
krevice_kreepers_dk = "Krevice Kreepers - DK Coin"
tearaway_toboggan_flag = "Tearaway Toboggan - Flag"
tearaway_toboggan_kong = "Tearaway Toboggan - KONG"
tearaway_toboggan_bonus_1 = "Tearaway Toboggan - Bonus 1"
tearaway_toboggan_bonus_2 = "Tearaway Toboggan - Bonus 2"
tearaway_toboggan_dk = "Tearaway Toboggan - DK Coin"
barrel_drop_bounce_flag = "Barrel Drop Bounce - Flag"
barrel_drop_bounce_kong = "Barrel Drop Bounce - KONG"
barrel_drop_bounce_bonus_1 = "Barrel Drop Bounce - Bonus 1"
barrel_drop_bounce_bonus_2 = "Barrel Drop Bounce - Bonus 2"
barrel_drop_bounce_dk = "Barrel Drop Bounce - DK Coin"
krack_shot_kroc_flag = "Krack-Shot Kroc - Flag"
krack_shot_kroc_kong = "Krack-Shot Kroc - KONG"
krack_shot_kroc_bonus_1 = "Krack-Shot Kroc - Bonus 1"
krack_shot_kroc_bonus_2 = "Krack-Shot Kroc - Bonus 2"
krack_shot_kroc_dk = "Krack-Shot Kroc - DK Coin"
lemguin_lunge_flag = "Lemguin Lunge - Flag"
lemguin_lunge_kong = "Lemguin Lunge - KONG"
lemguin_lunge_bonus_1 = "Lemguin Lunge - Bonus 1"
lemguin_lunge_bonus_2 = "Lemguin Lunge - Bonus 2"
lemguin_lunge_dk = "Lemguin Lunge - DK Coin"
buzzer_barrage_flag = "Buzzer Barrage - Flag"
buzzer_barrage_kong = "Buzzer Barrage - KONG"
buzzer_barrage_bonus_1 = "Buzzer Barrage - Bonus 1"
buzzer_barrage_bonus_2 = "Buzzer Barrage - Bonus 2"
buzzer_barrage_dk = "Buzzer Barrage - DK Coin"
kong_fused_cliffs_flag = "Kong-Fused Cliffs - Flag"
kong_fused_cliffs_kong = "Kong-Fused Cliffs - KONG"
kong_fused_cliffs_bonus_1 = "Kong-Fused Cliffs - Bonus 1"
kong_fused_cliffs_bonus_2 = "Kong-Fused Cliffs - Bonus 2"
kong_fused_cliffs_dk = "Kong-Fused Cliffs - DK Coin"
floodlit_fish_flag = "Floodlit Fish - Flag"
floodlit_fish_kong = "Floodlit Fish - KONG"
floodlit_fish_bonus_1 = "Floodlit Fish - Bonus 1"
floodlit_fish_bonus_2 = "Floodlit Fish - Bonus 2"
floodlit_fish_dk = "Floodlit Fish - DK Coin"
pothole_panic_flag = "Pothole Panic - Flag"
pothole_panic_kong = "Pothole Panic - KONG"
pothole_panic_bonus_1 = "Pothole Panic - Bonus 1"
pothole_panic_bonus_2 = "Pothole Panic - Bonus 2"
pothole_panic_dk = "Pothole Panic - DK Coin"
ropey_rumpus_flag = "Ropey Rumpus - Flag"
ropey_rumpus_kong = "Ropey Rumpus - KONG"
ropey_rumpus_bonus_1 = "Ropey Rumpus - Bonus 1"
ropey_rumpus_bonus_2 = "Ropey Rumpus - Bonus 2"
ropey_rumpus_dk = "Ropey Rumpus - DK Coin"
konveyor_rope_clash_flag = "Konveyor Rope Klash - Flag"
konveyor_rope_clash_kong = "Konveyor Rope Klash - KONG"
konveyor_rope_clash_bonus_1 = "Konveyor Rope Klash - Bonus 1"
konveyor_rope_clash_bonus_2 = "Konveyor Rope Klash - Bonus 2"
konveyor_rope_clash_dk = "Konveyor Rope Klash - DK Coin"
creepy_caverns_flag = "Creepy Caverns - Flag"
creepy_caverns_kong = "Creepy Caverns - KONG"
creepy_caverns_bonus_1 = "Creepy Caverns - Bonus 1"
creepy_caverns_bonus_2 = "Creepy Caverns - Bonus 2"
creepy_caverns_dk = "Creepy Caverns - DK Coin"
lightning_lookout_flag = "Lightning Lookout - Flag"
lightning_lookout_kong = "Lightning Lookout - KONG"
lightning_lookout_bonus_1 = "Lightning Lookout - Bonus 1"
lightning_lookout_bonus_2 = "Lightning Lookout - Bonus 2"
lightning_lookout_dk = "Lightning Lookout - DK Coin"
koindozer_klamber_flag = "Koindozer Klamber - Flag"
koindozer_klamber_kong = "Koindozer Klamber - KONG"
koindozer_klamber_bonus_1 = "Koindozer Klamber - Bonus 1"
koindozer_klamber_bonus_2 = "Koindozer Klamber - Bonus 2"
koindozer_klamber_dk = "Koindozer Klamber - DK Coin"
poisonous_pipeline_flag = "Poisonous Pipeline - Flag"
poisonous_pipeline_kong = "Poisonous Pipeline - KONG"
poisonous_pipeline_bonus_1 = "Poisonous Pipeline - Bonus 1"
poisonous_pipeline_bonus_2 = "Poisonous Pipeline - Bonus 2"
poisonous_pipeline_dk = "Poisonous Pipeline - DK Coin"
stampede_sprint_flag = "Stampede Sprint - Flag"
stampede_sprint_kong = "Stampede Sprint - KONG"
stampede_sprint_bonus_1 = "Stampede Sprint - Bonus 1"
stampede_sprint_bonus_2 = "Stampede Sprint - Bonus 2"
stampede_sprint_bonus_3 = "Stampede Sprint - Bonus 3"
stampede_sprint_dk = "Stampede Sprint - DK Coin"
criss_cross_cliffs_flag = "Criss Kross Cliffs - Flag"
criss_cross_cliffs_kong = "Criss Kross Cliffs - KONG"
criss_cross_cliffs_bonus_1 = "Criss Kross Cliffs - Bonus 1"
criss_cross_cliffs_bonus_2 = "Criss Kross Cliffs - Bonus 2"
criss_cross_cliffs_dk = "Criss Kross Cliffs - DK Coin"
tyrant_twin_tussle_flag = "Tyrant Twin Tussle - Flag"
tyrant_twin_tussle_kong = "Tyrant Twin Tussle - KONG"
tyrant_twin_tussle_bonus_1 = "Tyrant Twin Tussle - Bonus 1"
tyrant_twin_tussle_bonus_2 = "Tyrant Twin Tussle - Bonus 2"
tyrant_twin_tussle_bonus_3 = "Tyrant Twin Tussle - Bonus 3"
tyrant_twin_tussle_dk = "Tyrant Twin Tussle - DK Coin"
swoopy_salvo_flag = "Swoopy Salvo - Flag"
swoopy_salvo_kong = "Swoopy Salvo - KONG"
swoopy_salvo_bonus_1 = "Swoopy Salvo - Bonus 1"
swoopy_salvo_bonus_2 = "Swoopy Salvo - Bonus 2"
swoopy_salvo_bonus_3 = "Swoopy Salvo - Bonus 3"

View File

@@ -6,7 +6,7 @@ from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, O
class Goal(Choice):
"""
Determines the goal of the seed
Knautilus: Reach the Knautilus and defeat Baron K. Roolenstein
Knautilus: Scuttle the Knautilus in Krematoa and defeat Baron K. Roolenstein
Banana Bird Hunt: Find a certain number of Banana Birds and rescue their mother
"""
display_name = "Goal"
@@ -75,6 +75,13 @@ class PercentageOfBananaBirds(Range):
default = 100
class KONGsanity(Toggle):
"""
Whether collecting all four KONG letters in each level grants a check
"""
display_name = "KONGsanity"
class LevelShuffle(Toggle):
"""
Whether levels are shuffled
@@ -82,6 +89,41 @@ class LevelShuffle(Toggle):
display_name = "Level Shuffle"
class Difficulty(Choice):
"""
Which Difficulty Level to use
NORML: The Normal Difficulty
HARDR: Many DK Barrels are removed
TUFST: Most DK Barrels and all Midway Barrels are removed
"""
display_name = "Difficulty"
option_norml = 0
option_hardr = 1
option_tufst = 2
default = 0
@classmethod
def get_option_name(cls, value) -> str:
if cls.auto_display_name:
return cls.name_lookup[value].upper()
else:
return cls.name_lookup[value]
class Autosave(DefaultOnToggle):
"""
Whether the game should autosave after each level
"""
display_name = "Autosave"
class MERRY(Toggle):
"""
Whether the Bonus Barrels will be Christmas-themed
"""
display_name = "MERRY"
class MusicShuffle(Toggle):
"""
Whether music is shuffled
@@ -125,7 +167,11 @@ dkc3_options: typing.Dict[str, type(Option)] = {
"percentage_of_extra_bonus_coins": PercentageOfExtraBonusCoins,
"number_of_banana_birds": NumberOfBananaBirds,
"percentage_of_banana_birds": PercentageOfBananaBirds,
"kongsanity": KONGsanity,
"level_shuffle": LevelShuffle,
"difficulty": Difficulty,
"autosave": Autosave,
"merry": MERRY,
"music_shuffle": MusicShuffle,
"kong_palette_swap": KongPaletteSwap,
"starting_life_count": StartingLifeCount,

View File

@@ -44,6 +44,8 @@ def create_regions(world, player: int, active_locations):
LocationName.lakeside_limbo_bonus_2 : [0x657, 3],
LocationName.lakeside_limbo_dk : [0x657, 5],
}
if world.kongsanity[player]:
lakeside_limbo_region_locations[LocationName.lakeside_limbo_kong] = []
lakeside_limbo_region = create_region(world, player, active_locations, LocationName.lakeside_limbo_region,
lakeside_limbo_region_locations, None)
@@ -53,6 +55,8 @@ def create_regions(world, player: int, active_locations):
LocationName.doorstop_dash_bonus_2 : [0x65A, 3],
LocationName.doorstop_dash_dk : [0x65A, 5],
}
if world.kongsanity[player]:
doorstop_dash_region_locations[LocationName.doorstop_dash_kong] = []
doorstop_dash_region = create_region(world, player, active_locations, LocationName.doorstop_dash_region,
doorstop_dash_region_locations, None)
@@ -62,6 +66,8 @@ def create_regions(world, player: int, active_locations):
LocationName.tidal_trouble_bonus_2 : [0x659, 3],
LocationName.tidal_trouble_dk : [0x659, 5],
}
if world.kongsanity[player]:
tidal_trouble_region_locations[LocationName.tidal_trouble_kong] = []
tidal_trouble_region = create_region(world, player, active_locations, LocationName.tidal_trouble_region,
tidal_trouble_region_locations, None)
@@ -71,6 +77,8 @@ def create_regions(world, player: int, active_locations):
LocationName.skiddas_row_bonus_2 : [0x65D, 3],
LocationName.skiddas_row_dk : [0x65D, 5],
}
if world.kongsanity[player]:
skiddas_row_region_locations[LocationName.skiddas_row_kong] = []
skiddas_row_region = create_region(world, player, active_locations, LocationName.skiddas_row_region,
skiddas_row_region_locations, None)
@@ -80,6 +88,8 @@ def create_regions(world, player: int, active_locations):
LocationName.murky_mill_bonus_2 : [0x65C, 3],
LocationName.murky_mill_dk : [0x65C, 5],
}
if world.kongsanity[player]:
murky_mill_region_locations[LocationName.murky_mill_kong] = []
murky_mill_region = create_region(world, player, active_locations, LocationName.murky_mill_region,
murky_mill_region_locations, None)
@@ -89,6 +99,8 @@ def create_regions(world, player: int, active_locations):
LocationName.barrel_shield_bust_up_bonus_2 : [0x662, 3],
LocationName.barrel_shield_bust_up_dk : [0x662, 5],
}
if world.kongsanity[player]:
barrel_shield_bust_up_region_locations[LocationName.barrel_shield_bust_up_kong] = []
barrel_shield_bust_up_region = create_region(world, player, active_locations, LocationName.barrel_shield_bust_up_region,
barrel_shield_bust_up_region_locations, None)
@@ -98,6 +110,8 @@ def create_regions(world, player: int, active_locations):
LocationName.riverside_race_bonus_2 : [0x664, 3],
LocationName.riverside_race_dk : [0x664, 5],
}
if world.kongsanity[player]:
riverside_race_region_locations[LocationName.riverside_race_kong] = []
riverside_race_region = create_region(world, player, active_locations, LocationName.riverside_race_region,
riverside_race_region_locations, None)
@@ -107,6 +121,8 @@ def create_regions(world, player: int, active_locations):
LocationName.squeals_on_wheels_bonus_2 : [0x65B, 3],
LocationName.squeals_on_wheels_dk : [0x65B, 5],
}
if world.kongsanity[player]:
squeals_on_wheels_region_locations[LocationName.squeals_on_wheels_kong] = []
squeals_on_wheels_region = create_region(world, player, active_locations, LocationName.squeals_on_wheels_region,
squeals_on_wheels_region_locations, None)
@@ -116,6 +132,8 @@ def create_regions(world, player: int, active_locations):
LocationName.springin_spiders_bonus_2 : [0x661, 3],
LocationName.springin_spiders_dk : [0x661, 5],
}
if world.kongsanity[player]:
springin_spiders_region_locations[LocationName.springin_spiders_kong] = []
springin_spiders_region = create_region(world, player, active_locations, LocationName.springin_spiders_region,
springin_spiders_region_locations, None)
@@ -125,6 +143,8 @@ def create_regions(world, player: int, active_locations):
LocationName.bobbing_barrel_brawl_bonus_2 : [0x666, 3],
LocationName.bobbing_barrel_brawl_dk : [0x666, 5],
}
if world.kongsanity[player]:
bobbing_barrel_brawl_region_locations[LocationName.bobbing_barrel_brawl_kong] = []
bobbing_barrel_brawl_region = create_region(world, player, active_locations, LocationName.bobbing_barrel_brawl_region,
bobbing_barrel_brawl_region_locations, None)
@@ -134,6 +154,8 @@ def create_regions(world, player: int, active_locations):
LocationName.bazzas_blockade_bonus_2 : [0x667, 3],
LocationName.bazzas_blockade_dk : [0x667, 5],
}
if world.kongsanity[player]:
bazzas_blockade_region_locations[LocationName.bazzas_blockade_kong] = []
bazzas_blockade_region = create_region(world, player, active_locations, LocationName.bazzas_blockade_region,
bazzas_blockade_region_locations, None)
@@ -143,6 +165,8 @@ def create_regions(world, player: int, active_locations):
LocationName.rocket_barrel_ride_bonus_2 : [0x66A, 3],
LocationName.rocket_barrel_ride_dk : [0x66A, 5],
}
if world.kongsanity[player]:
rocket_barrel_ride_region_locations[LocationName.rocket_barrel_ride_kong] = []
rocket_barrel_ride_region = create_region(world, player, active_locations, LocationName.rocket_barrel_ride_region,
rocket_barrel_ride_region_locations, None)
@@ -152,6 +176,8 @@ def create_regions(world, player: int, active_locations):
LocationName.kreeping_klasps_bonus_2 : [0x658, 3],
LocationName.kreeping_klasps_dk : [0x658, 5],
}
if world.kongsanity[player]:
kreeping_klasps_region_locations[LocationName.kreeping_klasps_kong] = []
kreeping_klasps_region = create_region(world, player, active_locations, LocationName.kreeping_klasps_region,
kreeping_klasps_region_locations, None)
@@ -161,6 +187,8 @@ def create_regions(world, player: int, active_locations):
LocationName.tracker_barrel_trek_bonus_2 : [0x66B, 3],
LocationName.tracker_barrel_trek_dk : [0x66B, 5],
}
if world.kongsanity[player]:
tracker_barrel_trek_region_locations[LocationName.tracker_barrel_trek_kong] = []
tracker_barrel_trek_region = create_region(world, player, active_locations, LocationName.tracker_barrel_trek_region,
tracker_barrel_trek_region_locations, None)
@@ -170,6 +198,8 @@ def create_regions(world, player: int, active_locations):
LocationName.fish_food_frenzy_bonus_2 : [0x668, 3],
LocationName.fish_food_frenzy_dk : [0x668, 5],
}
if world.kongsanity[player]:
fish_food_frenzy_region_locations[LocationName.fish_food_frenzy_kong] = []
fish_food_frenzy_region = create_region(world, player, active_locations, LocationName.fish_food_frenzy_region,
fish_food_frenzy_region_locations, None)
@@ -179,6 +209,8 @@ def create_regions(world, player: int, active_locations):
LocationName.fire_ball_frenzy_bonus_2 : [0x66D, 3],
LocationName.fire_ball_frenzy_dk : [0x66D, 5],
}
if world.kongsanity[player]:
fire_ball_frenzy_region_locations[LocationName.fire_ball_frenzy_kong] = []
fire_ball_frenzy_region = create_region(world, player, active_locations, LocationName.fire_ball_frenzy_region,
fire_ball_frenzy_region_locations, None)
@@ -188,6 +220,8 @@ def create_regions(world, player: int, active_locations):
LocationName.demolition_drain_pipe_bonus_2 : [0x672, 3],
LocationName.demolition_drain_pipe_dk : [0x672, 5],
}
if world.kongsanity[player]:
demolition_drain_pipe_region_locations[LocationName.demolition_drain_pipe_kong] = []
demolition_drain_pipe_region = create_region(world, player, active_locations, LocationName.demolition_drain_pipe_region,
demolition_drain_pipe_region_locations, None)
@@ -197,6 +231,8 @@ def create_regions(world, player: int, active_locations):
LocationName.ripsaw_rage_bonus_2 : [0x660, 3],
LocationName.ripsaw_rage_dk : [0x660, 5],
}
if world.kongsanity[player]:
ripsaw_rage_region_locations[LocationName.ripsaw_rage_kong] = []
ripsaw_rage_region = create_region(world, player, active_locations, LocationName.ripsaw_rage_region,
ripsaw_rage_region_locations, None)
@@ -206,6 +242,8 @@ def create_regions(world, player: int, active_locations):
LocationName.blazing_bazookas_bonus_2 : [0x66E, 3],
LocationName.blazing_bazookas_dk : [0x66E, 5],
}
if world.kongsanity[player]:
blazing_bazookas_region_locations[LocationName.blazing_bazookas_kong] = []
blazing_bazookas_region = create_region(world, player, active_locations, LocationName.blazing_bazookas_region,
blazing_bazookas_region_locations, None)
@@ -215,6 +253,8 @@ def create_regions(world, player: int, active_locations):
LocationName.low_g_labyrinth_bonus_2 : [0x670, 3],
LocationName.low_g_labyrinth_dk : [0x670, 5],
}
if world.kongsanity[player]:
low_g_labyrinth_region_locations[LocationName.low_g_labyrinth_kong] = []
low_g_labyrinth_region = create_region(world, player, active_locations, LocationName.low_g_labyrinth_region,
low_g_labyrinth_region_locations, None)
@@ -224,6 +264,8 @@ def create_regions(world, player: int, active_locations):
LocationName.krevice_kreepers_bonus_2 : [0x673, 3],
LocationName.krevice_kreepers_dk : [0x673, 5],
}
if world.kongsanity[player]:
krevice_kreepers_region_locations[LocationName.krevice_kreepers_kong] = []
krevice_kreepers_region = create_region(world, player, active_locations, LocationName.krevice_kreepers_region,
krevice_kreepers_region_locations, None)
@@ -233,6 +275,8 @@ def create_regions(world, player: int, active_locations):
LocationName.tearaway_toboggan_bonus_2 : [0x65F, 3],
LocationName.tearaway_toboggan_dk : [0x65F, 5],
}
if world.kongsanity[player]:
tearaway_toboggan_region_locations[LocationName.tearaway_toboggan_kong] = []
tearaway_toboggan_region = create_region(world, player, active_locations, LocationName.tearaway_toboggan_region,
tearaway_toboggan_region_locations, None)
@@ -242,6 +286,8 @@ def create_regions(world, player: int, active_locations):
LocationName.barrel_drop_bounce_bonus_2 : [0x66C, 3],
LocationName.barrel_drop_bounce_dk : [0x66C, 5],
}
if world.kongsanity[player]:
barrel_drop_bounce_region_locations[LocationName.barrel_drop_bounce_kong] = []
barrel_drop_bounce_region = create_region(world, player, active_locations, LocationName.barrel_drop_bounce_region,
barrel_drop_bounce_region_locations, None)
@@ -251,6 +297,8 @@ def create_regions(world, player: int, active_locations):
LocationName.krack_shot_kroc_bonus_2 : [0x66F, 3],
LocationName.krack_shot_kroc_dk : [0x66F, 5],
}
if world.kongsanity[player]:
krack_shot_kroc_region_locations[LocationName.krack_shot_kroc_kong] = []
krack_shot_kroc_region = create_region(world, player, active_locations, LocationName.krack_shot_kroc_region,
krack_shot_kroc_region_locations, None)
@@ -260,6 +308,8 @@ def create_regions(world, player: int, active_locations):
LocationName.lemguin_lunge_bonus_2 : [0x65E, 3],
LocationName.lemguin_lunge_dk : [0x65E, 5],
}
if world.kongsanity[player]:
lemguin_lunge_region_locations[LocationName.lemguin_lunge_kong] = []
lemguin_lunge_region = create_region(world, player, active_locations, LocationName.lemguin_lunge_region,
lemguin_lunge_region_locations, None)
@@ -269,6 +319,8 @@ def create_regions(world, player: int, active_locations):
LocationName.buzzer_barrage_bonus_2 : [0x676, 3],
LocationName.buzzer_barrage_dk : [0x676, 5],
}
if world.kongsanity[player]:
buzzer_barrage_region_locations[LocationName.buzzer_barrage_kong] = []
buzzer_barrage_region = create_region(world, player, active_locations, LocationName.buzzer_barrage_region,
buzzer_barrage_region_locations, None)
@@ -278,6 +330,8 @@ def create_regions(world, player: int, active_locations):
LocationName.kong_fused_cliffs_bonus_2 : [0x674, 3],
LocationName.kong_fused_cliffs_dk : [0x674, 5],
}
if world.kongsanity[player]:
kong_fused_cliffs_region_locations[LocationName.kong_fused_cliffs_kong] = []
kong_fused_cliffs_region = create_region(world, player, active_locations, LocationName.kong_fused_cliffs_region,
kong_fused_cliffs_region_locations, None)
@@ -287,6 +341,8 @@ def create_regions(world, player: int, active_locations):
LocationName.floodlit_fish_bonus_2 : [0x669, 3],
LocationName.floodlit_fish_dk : [0x669, 5],
}
if world.kongsanity[player]:
floodlit_fish_region_locations[LocationName.floodlit_fish_kong] = []
floodlit_fish_region = create_region(world, player, active_locations, LocationName.floodlit_fish_region,
floodlit_fish_region_locations, None)
@@ -296,6 +352,8 @@ def create_regions(world, player: int, active_locations):
LocationName.pothole_panic_bonus_2 : [0x677, 3],
LocationName.pothole_panic_dk : [0x677, 5],
}
if world.kongsanity[player]:
pothole_panic_region_locations[LocationName.pothole_panic_kong] = []
pothole_panic_region = create_region(world, player, active_locations, LocationName.pothole_panic_region,
pothole_panic_region_locations, None)
@@ -305,6 +363,8 @@ def create_regions(world, player: int, active_locations):
LocationName.ropey_rumpus_bonus_2 : [0x675, 3],
LocationName.ropey_rumpus_dk : [0x675, 5],
}
if world.kongsanity[player]:
ropey_rumpus_region_locations[LocationName.ropey_rumpus_kong] = []
ropey_rumpus_region = create_region(world, player, active_locations, LocationName.ropey_rumpus_region,
ropey_rumpus_region_locations, None)
@@ -314,6 +374,8 @@ def create_regions(world, player: int, active_locations):
LocationName.konveyor_rope_clash_bonus_2 : [0x657, 3],
LocationName.konveyor_rope_clash_dk : [0x657, 5],
}
if world.kongsanity[player]:
konveyor_rope_clash_region_locations[LocationName.konveyor_rope_clash_kong] = []
konveyor_rope_clash_region = create_region(world, player, active_locations, LocationName.konveyor_rope_clash_region,
konveyor_rope_clash_region_locations, None)
@@ -323,6 +385,8 @@ def create_regions(world, player: int, active_locations):
LocationName.creepy_caverns_bonus_2 : [0x678, 3],
LocationName.creepy_caverns_dk : [0x678, 5],
}
if world.kongsanity[player]:
creepy_caverns_region_locations[LocationName.creepy_caverns_kong] = []
creepy_caverns_region = create_region(world, player, active_locations, LocationName.creepy_caverns_region,
creepy_caverns_region_locations, None)
@@ -332,6 +396,8 @@ def create_regions(world, player: int, active_locations):
LocationName.lightning_lookout_bonus_2 : [0x665, 3],
LocationName.lightning_lookout_dk : [0x665, 5],
}
if world.kongsanity[player]:
lightning_lookout_region_locations[LocationName.lightning_lookout_kong] = []
lightning_lookout_region = create_region(world, player, active_locations, LocationName.lightning_lookout_region,
lightning_lookout_region_locations, None)
@@ -341,6 +407,8 @@ def create_regions(world, player: int, active_locations):
LocationName.koindozer_klamber_bonus_2 : [0x679, 3],
LocationName.koindozer_klamber_dk : [0x679, 5],
}
if world.kongsanity[player]:
koindozer_klamber_region_locations[LocationName.koindozer_klamber_kong] = []
koindozer_klamber_region = create_region(world, player, active_locations, LocationName.koindozer_klamber_region,
koindozer_klamber_region_locations, None)
@@ -350,6 +418,8 @@ def create_regions(world, player: int, active_locations):
LocationName.poisonous_pipeline_bonus_2 : [0x671, 3],
LocationName.poisonous_pipeline_dk : [0x671, 5],
}
if world.kongsanity[player]:
poisonous_pipeline_region_locations[LocationName.poisonous_pipeline_kong] = []
poisonous_pipeline_region = create_region(world, player, active_locations, LocationName.poisonous_pipeline_region,
poisonous_pipeline_region_locations, None)
@@ -360,6 +430,8 @@ def create_regions(world, player: int, active_locations):
LocationName.stampede_sprint_bonus_3 : [0x67B, 4],
LocationName.stampede_sprint_dk : [0x67B, 5],
}
if world.kongsanity[player]:
stampede_sprint_region_locations[LocationName.stampede_sprint_kong] = []
stampede_sprint_region = create_region(world, player, active_locations, LocationName.stampede_sprint_region,
stampede_sprint_region_locations, None)
@@ -369,6 +441,8 @@ def create_regions(world, player: int, active_locations):
LocationName.criss_cross_cliffs_bonus_2 : [0x67C, 3],
LocationName.criss_cross_cliffs_dk : [0x67C, 5],
}
if world.kongsanity[player]:
criss_cross_cliffs_region_locations[LocationName.criss_cross_cliffs_kong] = []
criss_cross_cliffs_region = create_region(world, player, active_locations, LocationName.criss_cross_cliffs_region,
criss_cross_cliffs_region_locations, None)
@@ -379,6 +453,8 @@ def create_regions(world, player: int, active_locations):
LocationName.tyrant_twin_tussle_bonus_3 : [0x67D, 4],
LocationName.tyrant_twin_tussle_dk : [0x67D, 5],
}
if world.kongsanity[player]:
tyrant_twin_tussle_region_locations[LocationName.tyrant_twin_tussle_kong] = []
tyrant_twin_tussle_region = create_region(world, player, active_locations, LocationName.tyrant_twin_tussle_region,
tyrant_twin_tussle_region_locations, None)
@@ -389,6 +465,8 @@ def create_regions(world, player: int, active_locations):
LocationName.swoopy_salvo_bonus_3 : [0x663, 4],
LocationName.swoopy_salvo_dk : [0x663, 5],
}
if world.kongsanity[player]:
swoopy_salvo_region_locations[LocationName.swoopy_salvo_kong] = []
swoopy_salvo_region = create_region(world, player, active_locations, LocationName.swoopy_salvo_region,
swoopy_salvo_region_locations, None)
@@ -503,9 +581,7 @@ def create_regions(world, player: int, active_locations):
sky_high_secret_region_locations = {}
if False:#world.include_trade_sequence[player]:
sky_high_secret_region_locations.update({
LocationName.sky_high_secret: [0x64B, 1],
})
sky_high_secret_region_locations[LocationName.sky_high_secret] = [0x64B, 1]
sky_high_secret_region = create_region(world, player, active_locations, LocationName.sky_high_secret_region,
sky_high_secret_region_locations, None)
@@ -517,9 +593,7 @@ def create_regions(world, player: int, active_locations):
cifftop_cache_region_locations = {}
if False:#world.include_trade_sequence[player]:
cifftop_cache_region_locations.update({
LocationName.cifftop_cache: [0x64D, 1],
})
cifftop_cache_region_locations[LocationName.cifftop_cache] = [0x64D, 1]
cifftop_cache_region = create_region(world, player, active_locations, LocationName.cifftop_cache_region,
cifftop_cache_region_locations, None)
@@ -622,29 +696,19 @@ def create_regions(world, player: int, active_locations):
LocationName.bazaars_general_store_2: [0x615, 3, True],
})
bramble_region_locations.update({
LocationName.brambles_bungalow: [0x619, 2],
})
bramble_region_locations[LocationName.brambles_bungalow] = [0x619, 2]
#flower_spot_region_locations.update({
# LocationName.flower_spot: [0x615, 3, True],
#})
barter_region_locations.update({
LocationName.barters_swap_shop: [0x61B, 3],
})
barter_region_locations[LocationName.barters_swap_shop] = [0x61B, 3]
barnacle_region_locations.update({
LocationName.barnacles_island: [0x61D, 2],
})
barnacle_region_locations[LocationName.barnacles_island] = [0x61D, 2]
blue_region_locations.update({
LocationName.blues_beach_hut: [0x621, 4],
})
blue_region_locations[LocationName.blues_beach_hut] = [0x621, 4]
blizzard_region_locations.update({
LocationName.blizzards_basecamp: [0x625, 4, True],
})
blizzard_region_locations[LocationName.blizzards_basecamp] = [0x625, 4, True]
bazaar_region = create_region(world, player, active_locations, LocationName.bazaar_region,
bazaar_region_locations, None)
@@ -817,7 +881,6 @@ def connect_regions(world, player, level_list):
level_list[32],
level_list[33],
level_list[34],
LocationName.kastle_kaos_region,
LocationName.sewer_stockpile_region,
]
@@ -835,10 +898,16 @@ def connect_regions(world, player, level_list):
for i in range(0, len(krematoa_levels)):
connect(world, player, names, LocationName.krematoa_region, krematoa_levels[i],
lambda state: (state.has(ItemName.bonus_coin, player, world.krematoa_bonus_coin_cost[player].value * (i+1))))
connect(world, player, names, LocationName.krematoa_region, LocationName.knautilus_region,
lambda state: (state.has(ItemName.krematoa_cog, player, 5)))
lambda state, i=i: (state.has(ItemName.bonus_coin, player, world.krematoa_bonus_coin_cost[player].value * (i+1))))
if world.goal[player] == "knautilus":
connect(world, player, names, LocationName.kaos_kore_region, LocationName.knautilus_region)
connect(world, player, names, LocationName.krematoa_region, LocationName.kastle_kaos_region,
lambda state: (state.has(ItemName.krematoa_cog, player, 5)))
else:
connect(world, player, names, LocationName.kaos_kore_region, LocationName.kastle_kaos_region)
connect(world, player, names, LocationName.krematoa_region, LocationName.knautilus_region,
lambda state: (state.has(ItemName.krematoa_cog, player, 5)))
def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None, exits=None):

View File

@@ -11,187 +11,270 @@ import os
import math
level_unlock_map = {
0x657: [0x65A],
0x65A: [0x680, 0x639, 0x659],
0x659: [0x65D],
0x65D: [0x65C],
0x65C: [0x688, 0x64F],
0x662: [0x681, 0x664],
0x664: [0x65B],
0x65B: [0x689, 0x661],
0x661: [0x63A, 0x666],
0x666: [0x650, 0x649],
0x667: [0x66A],
0x66A: [0x682, 0x658],
0x658: [0x68A, 0x66B],
0x66B: [0x668],
0x668: [0x651],
0x66D: [0x63C, 0x672],
0x672: [0x68B, 0x660],
0x660: [0x683, 0x66E],
0x66E: [0x670],
0x670: [0x652],
0x673: [0x684, 0x65F],
0x65F: [0x66C],
0x66C: [0x66F],
0x66F: [0x65E],
0x65E: [0x63D, 0x653, 0x68C, 0x64C],
0x676: [0x63E, 0x674, 0x685],
0x674: [0x63F, 0x669],
0x669: [0x677],
0x677: [0x68D, 0x675],
0x675: [0x654],
0x67A: [0x640, 0x678],
0x678: [0x665],
0x665: [0x686, 0x679],
0x679: [0x68E, 0x671],
0x67B: [0x67C],
0x67C: [0x67D],
0x67D: [0x663],
0x663: [0x67E],
}
location_rom_data = {
0xDC3000: [0x657, 1], # Lakeside Limbo
0xDC3001: [0x657, 2],
0xDC3002: [0x657, 3],
0xDC3003: [0x657, 5],
0xDC3100: [0x657, 7],
0xDC3004: [0x65A, 1], # Doorstop Dash
0xDC3005: [0x65A, 2],
0xDC3006: [0x65A, 3],
0xDC3007: [0x65A, 5],
0xDC3104: [0x65A, 7],
0xDC3008: [0x659, 1], # Tidal Trouble
0xDC3009: [0x659, 2],
0xDC300A: [0x659, 3],
0xDC300B: [0x659, 5],
0xDC3108: [0x659, 7],
0xDC300C: [0x65D, 1], # Skidda's Row
0xDC300D: [0x65D, 2],
0xDC300E: [0x65D, 3],
0xDC300F: [0x65D, 5],
0xDC310C: [0x65D, 7],
0xDC3010: [0x65C, 1], # Murky Mill
0xDC3011: [0x65C, 2],
0xDC3012: [0x65C, 3],
0xDC3013: [0x65C, 5],
0xDC3110: [0x65C, 7],
0xDC3014: [0x662, 1], # Barrel Shield Bust-Up
0xDC3015: [0x662, 2],
0xDC3016: [0x662, 3],
0xDC3017: [0x662, 5],
0xDC3114: [0x662, 7],
0xDC3018: [0x664, 1], # Riverside Race
0xDC3019: [0x664, 2],
0xDC301A: [0x664, 3],
0xDC301B: [0x664, 5],
0xDC3118: [0x664, 7],
0xDC301C: [0x65B, 1], # Squeals on Wheels
0xDC301D: [0x65B, 2],
0xDC301E: [0x65B, 3],
0xDC301F: [0x65B, 5],
0xDC311C: [0x65B, 7],
0xDC3020: [0x661, 1], # Springin' Spiders
0xDC3021: [0x661, 2],
0xDC3022: [0x661, 3],
0xDC3023: [0x661, 5],
0xDC3120: [0x661, 7],
0xDC3024: [0x666, 1], # Bobbing Barrel Brawl
0xDC3025: [0x666, 2],
0xDC3026: [0x666, 3],
0xDC3027: [0x666, 5],
0xDC3124: [0x666, 7],
0xDC3028: [0x667, 1], # Bazza's Blockade
0xDC3029: [0x667, 2],
0xDC302A: [0x667, 3],
0xDC302B: [0x667, 5],
0xDC3128: [0x667, 7],
0xDC302C: [0x66A, 1], # Rocket Barrel Ride
0xDC302D: [0x66A, 2],
0xDC302E: [0x66A, 3],
0xDC302F: [0x66A, 5],
0xDC312C: [0x66A, 7],
0xDC3030: [0x658, 1], # Kreeping Klasps
0xDC3031: [0x658, 2],
0xDC3032: [0x658, 3],
0xDC3033: [0x658, 5],
0xDC3130: [0x658, 7],
0xDC3034: [0x66B, 1], # Tracker Barrel Trek
0xDC3035: [0x66B, 2],
0xDC3036: [0x66B, 3],
0xDC3037: [0x66B, 5],
0xDC3134: [0x66B, 7],
0xDC3038: [0x668, 1], # Fish Food Frenzy
0xDC3039: [0x668, 2],
0xDC303A: [0x668, 3],
0xDC303B: [0x668, 5],
0xDC3138: [0x668, 7],
0xDC303C: [0x66D, 1], # Fire-ball Frenzy
0xDC303D: [0x66D, 2],
0xDC303E: [0x66D, 3],
0xDC303F: [0x66D, 5],
0xDC313C: [0x66D, 7],
0xDC3040: [0x672, 1], # Demolition Drainpipe
0xDC3041: [0x672, 2],
0xDC3042: [0x672, 3],
0xDC3043: [0x672, 5],
0xDC3140: [0x672, 7],
0xDC3044: [0x660, 1], # Ripsaw Rage
0xDC3045: [0x660, 2],
0xDC3046: [0x660, 3],
0xDC3047: [0x660, 5],
0xDC3144: [0x660, 7],
0xDC3048: [0x66E, 1], # Blazing Bazukas
0xDC3049: [0x66E, 2],
0xDC304A: [0x66E, 3],
0xDC304B: [0x66E, 5],
0xDC3148: [0x66E, 7],
0xDC304C: [0x670, 1], # Low-G Labyrinth
0xDC304D: [0x670, 2],
0xDC304E: [0x670, 3],
0xDC304F: [0x670, 5],
0xDC314C: [0x670, 7],
0xDC3050: [0x673, 1], # Krevice Kreepers
0xDC3051: [0x673, 2],
0xDC3052: [0x673, 3],
0xDC3053: [0x673, 5],
0xDC3150: [0x673, 7],
0xDC3054: [0x65F, 1], # Tearaway Toboggan
0xDC3055: [0x65F, 2],
0xDC3056: [0x65F, 3],
0xDC3057: [0x65F, 5],
0xDC3154: [0x65F, 7],
0xDC3058: [0x66C, 1], # Barrel Drop Bounce
0xDC3059: [0x66C, 2],
0xDC305A: [0x66C, 3],
0xDC305B: [0x66C, 5],
0xDC3158: [0x66C, 7],
0xDC305C: [0x66F, 1], # Krack-Shot Kroc
0xDC305D: [0x66F, 2],
0xDC305E: [0x66F, 3],
0xDC305F: [0x66F, 5],
0xDC315C: [0x66F, 7],
0xDC3060: [0x65E, 1], # Lemguin Lunge
0xDC3061: [0x65E, 2],
0xDC3062: [0x65E, 3],
0xDC3063: [0x65E, 5],
0xDC3160: [0x65E, 7],
0xDC3064: [0x676, 1], # Buzzer Barrage
0xDC3065: [0x676, 2],
0xDC3066: [0x676, 3],
0xDC3067: [0x676, 5],
0xDC3164: [0x676, 7],
0xDC3068: [0x674, 1], # Kong-Fused Cliffs
0xDC3069: [0x674, 2],
0xDC306A: [0x674, 3],
0xDC306B: [0x674, 5],
0xDC3168: [0x674, 7],
0xDC306C: [0x669, 1], # Floodlit Fish
0xDC306D: [0x669, 2],
0xDC306E: [0x669, 3],
0xDC306F: [0x669, 5],
0xDC316C: [0x669, 7],
0xDC3070: [0x677, 1], # Pothole Panic
0xDC3071: [0x677, 2],
0xDC3072: [0x677, 3],
0xDC3073: [0x677, 5],
0xDC3170: [0x677, 7],
0xDC3074: [0x675, 1], # Ropey Rumpus
0xDC3075: [0x675, 2],
0xDC3076: [0x675, 3],
0xDC3077: [0x675, 5],
0xDC3174: [0x675, 7],
0xDC3078: [0x67A, 1], # Konveyor Rope Klash
0xDC3079: [0x67A, 2],
0xDC307A: [0x67A, 3],
0xDC307B: [0x67A, 5],
0xDC3178: [0x67A, 7],
0xDC307C: [0x678, 1], # Creepy Caverns
0xDC307D: [0x678, 2],
0xDC307E: [0x678, 3],
0xDC307F: [0x678, 5],
0xDC317C: [0x678, 7],
0xDC3080: [0x665, 1], # Lightning Lookout
0xDC3081: [0x665, 2],
0xDC3082: [0x665, 3],
0xDC3083: [0x665, 5],
0xDC3180: [0x665, 7],
0xDC3084: [0x679, 1], # Koindozer Klamber
0xDC3085: [0x679, 2],
0xDC3086: [0x679, 3],
0xDC3087: [0x679, 5],
0xDC3184: [0x679, 7],
0xDC3088: [0x671, 1], # Poisonous Pipeline
0xDC3089: [0x671, 2],
0xDC308A: [0x671, 3],
0xDC308B: [0x671, 5],
0xDC3188: [0x671, 7],
0xDC308C: [0x67B, 1], # Stampede Sprint
@@ -199,23 +282,27 @@ location_rom_data = {
0xDC308E: [0x67B, 3],
0xDC308F: [0x67B, 4],
0xDC3090: [0x67B, 5],
0xDC318C: [0x67B, 7],
0xDC3091: [0x67C, 1], # Criss Kross Cliffs
0xDC3092: [0x67C, 2],
0xDC3093: [0x67C, 3],
0xDC3094: [0x67C, 5],
0xDC3191: [0x67C, 7],
0xDC3095: [0x67D, 1], # Tyrant Twin Tussle
0xDC3096: [0x67D, 2],
0xDC3097: [0x67D, 3],
0xDC3098: [0x67D, 4],
0xDC3099: [0x67D, 5],
0xDC3195: [0x67D, 7],
0xDC309A: [0x663, 1], # Swoopy Salvo
0xDC309B: [0x663, 2],
0xDC309C: [0x663, 3],
0xDC309D: [0x663, 4],
0xDC309E: [0x663, 5],
0xDC319A: [0x663, 7],
0xDC309F: [0x67E, 1], # Rocket Rush
0xDC30A0: [0x67E, 5],
@@ -243,7 +330,7 @@ location_rom_data = {
#0xDC30B4: [0x64D, 1], # Disabled until Trade Sequence
0xDC30B5: [0x64E, 1],
0xDC30B6: [0x5FD, 4], # Banana Bird Mother
0xDC30B6: [0x5FE, 4], # Banana Bird Mother
# DKC3_TODO: Disabled until Trade Sequence
#0xDC30B7: [0x615, 2, True],
@@ -256,6 +343,18 @@ location_rom_data = {
#0xDC30BE: [0x625, 4, True],
}
boss_location_ids = [
0xDC30A1,
0xDC30A2,
0xDC30A3,
0xDC30A4,
0xDC30A5,
0xDC30A6,
0xDC30A7,
0xDC30A8,
0xDC30B6,
]
item_rom_data = {
0xDC3001: [0x5D5], # 1-Up Balloon
@@ -400,10 +499,13 @@ def patch_rom(world, rom, player, active_level_list):
rom.write_byte(0x3484DE, 0xEA)
rom.write_byte(0x348528, 0x80) # Prevent Single-Ski Lock
# Make Swanky free
rom.write_byte(0x348C48, 0x00)
rom.write_bytes(0x34AB70, bytearray([0xEA, 0xEA]))
rom.write_bytes(0x34ABF7, bytearray([0xEA, 0xEA]))
rom.write_bytes(0x34ACD0, bytearray([0xEA, 0xEA]))
# Banana Bird Costs
if world.goal[player] == "banana_bird_hunt":
banana_bird_cost = math.floor(world.number_of_banana_birds[player] * world.percentage_of_banana_birds[player] / 100.0)
@@ -462,6 +564,25 @@ def patch_rom(world, rom, player, active_level_list):
rom.write_byte(0x9130, world.starting_life_count[player].value)
rom.write_byte(0x913B, world.starting_life_count[player].value)
# Cheat options
cheat_bytes = [0x00, 0x00]
if world.merry[player]:
cheat_bytes[0] |= 0x01
if world.autosave[player]:
cheat_bytes[0] |= 0x02
if world.difficulty[player] == "tufst":
cheat_bytes[0] |= 0x80
cheat_bytes[1] |= 0x80
elif world.difficulty[player] == "hardr":
cheat_bytes[0] |= 0x00
cheat_bytes[1] |= 0x00
elif world.difficulty[player] == "norml":
cheat_bytes[1] |= 0x40
rom.write_bytes(0x8303, bytearray(cheat_bytes))
# Handle Level Shuffle Here
if world.level_shuffle[player]:
@@ -469,6 +590,9 @@ def patch_rom(world, rom, player, active_level_list):
rom.write_byte(level_dict[level_list[i]].nameIDAddress, level_dict[active_level_list[i]].nameID)
rom.write_byte(level_dict[level_list[i]].levelIDAddress, level_dict[active_level_list[i]].levelID)
rom.write_byte(0x3FF800 + level_dict[active_level_list[i]].levelID, level_dict[level_list[i]].levelID)
rom.write_byte(0x3FF860 + level_dict[level_list[i]].levelID, level_dict[active_level_list[i]].levelID)
# First levels of each world
rom.write_byte(0x34BC3E, (0x32 + level_dict[active_level_list[0]].levelID))
rom.write_byte(0x34BC47, (0x32 + level_dict[active_level_list[5]].levelID))
@@ -495,6 +619,52 @@ def patch_rom(world, rom, player, active_level_list):
rom.write_byte(0x32F339, 0x55)
# Handle KONGsanity Here
if world.kongsanity[player]:
# Arich's Hoard KONGsanity fix
rom.write_bytes(0x34BA8C, bytearray([0xEA, 0xEA]))
# Don't hide the level flag if the 0x80 bit is set
rom.write_bytes(0x34CE92, bytearray([0x80]))
# Use the `!` next to level name for indicating KONG letters
rom.write_bytes(0x34B8F0, bytearray([0x80]))
rom.write_bytes(0x34B8F3, bytearray([0x80]))
# Hijack to code to set the 0x80 flag for the level when you complete KONG
rom.write_bytes(0x3BCD4B, bytearray([0x22, 0x80, 0xFA, 0XB8])) # JSL $B8FA80
rom.write_bytes(0x38FA80, bytearray([0xDA])) # PHX
rom.write_bytes(0x38FA81, bytearray([0x48])) # PHA
rom.write_bytes(0x38FA82, bytearray([0x08])) # PHP
rom.write_bytes(0x38FA83, bytearray([0xE2, 0x20])) # SEP #20
rom.write_bytes(0x38FA85, bytearray([0x48])) # PHA
rom.write_bytes(0x38FA86, bytearray([0x18])) # CLC
rom.write_bytes(0x38FA87, bytearray([0x6D, 0xD3, 0x18])) # ADC $18D3
rom.write_bytes(0x38FA8A, bytearray([0x8D, 0xD3, 0x18])) # STA $18D3
rom.write_bytes(0x38FA8D, bytearray([0x68])) # PLA
rom.write_bytes(0x38FA8E, bytearray([0xC2, 0x20])) # REP 20
rom.write_bytes(0x38FA90, bytearray([0X18])) # CLC
rom.write_bytes(0x38FA91, bytearray([0x6D, 0xD5, 0x05])) # ADC $05D5
rom.write_bytes(0x38FA94, bytearray([0x8D, 0xD5, 0x05])) # STA $05D5
rom.write_bytes(0x38FA97, bytearray([0xAE, 0xB9, 0x05])) # LDX $05B9
rom.write_bytes(0x38FA9A, bytearray([0xBD, 0x32, 0x06])) # LDA $0632, X
rom.write_bytes(0x38FA9D, bytearray([0x09, 0x80, 0x00])) # ORA #8000
rom.write_bytes(0x38FAA0, bytearray([0x9D, 0x32, 0x06])) # STA $0632, X
rom.write_bytes(0x38FAA3, bytearray([0xAD, 0xD5, 0x18])) # LDA $18D5
rom.write_bytes(0x38FAA6, bytearray([0xD0, 0x03])) # BNE $80EA
rom.write_bytes(0x38FAA8, bytearray([0x9C, 0xD9, 0x18])) # STZ $18D9
rom.write_bytes(0x38FAAB, bytearray([0xA9, 0x78, 0x00])) # LDA #0078
rom.write_bytes(0x38FAAE, bytearray([0x8D, 0xD5, 0x18])) # STA $18D5
rom.write_bytes(0x38FAB1, bytearray([0x28])) # PLP
rom.write_bytes(0x38FAB2, bytearray([0x68])) # PLA
rom.write_bytes(0x38FAB3, bytearray([0xFA])) # PLX
rom.write_bytes(0x38FAB4, bytearray([0x6B])) # RTL
# End Handle KONGsanity
# Handle Credits
rom.write_bytes(0x32A5DF, bytearray([0x41, 0x52, 0x43, 0x48, 0x49, 0x50, 0x45, 0x4C, 0x41, 0x47, 0x4F, 0x20, 0x4D, 0x4F, 0xC4])) # "ARCHIPELAGO MOD"
rom.write_bytes(0x32A5EE, bytearray([0x00, 0x03, 0x50, 0x4F, 0x52, 0x59, 0x47, 0x4F, 0x4E, 0xC5])) # "PORYGONE"
from Main import __version__
rom.name = bytearray(f'D3{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
@@ -516,6 +686,17 @@ def patch_rom(world, rom, player, active_level_list):
rom.write_byte(0x32DD63, 0xEA)
rom.write_byte(0x32DD64, 0xEA)
# Don't grant Banana Birds at Bears
rom.write_byte(0x3492DB, 0xEA)
rom.write_byte(0x3492DC, 0xEA)
rom.write_byte(0x3492DD, 0xEA)
rom.write_byte(0x3493F4, 0xEA)
rom.write_byte(0x3493F5, 0xEA)
rom.write_byte(0x3493F6, 0xEA)
# Don't grant present at Blizzard
rom.write_byte(0x8454, 0x00)
# Don't grant Patch and Skis from their bosses
rom.write_byte(0x3F3762, 0x00)
rom.write_byte(0x3F377B, 0x00)

View File

@@ -4,7 +4,7 @@ import math
import threading
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
from .Items import DKC3Item, ItemData, item_table, inventory_table
from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table
from .Locations import DKC3Location, all_locations, setup_locations
from .Options import dkc3_options
from .Regions import create_regions, connect_regions
@@ -40,7 +40,7 @@ class DKC3World(World):
game: str = "Donkey Kong Country 3"
option_definitions = dkc3_options
topology_present = False
data_version = 1
data_version = 2
#hint_blacklist = {LocationName.rocket_rush_flag}
item_name_to_id = {name: data.code for name, data in item_table.items()}
@@ -99,10 +99,13 @@ class DKC3World(World):
# Bosses
total_required_locations += number_of_bosses
# Secret Caves
total_required_locations += 13
if self.world.kongsanity[self.player]:
total_required_locations += 39
## Brothers Bear
if False:#self.world.include_trade_sequence[self.player]:
total_required_locations += 10
@@ -118,7 +121,11 @@ class DKC3World(World):
total_junk_count = total_required_locations - len(itempool)
itempool += [self.create_item(ItemName.bear_coin)] * total_junk_count
junk_pool = []
for item_name in self.world.random.choices(list(junk_table.keys()), k=total_junk_count):
junk_pool += [self.create_item(item_name)]
itempool += junk_pool
self.active_level_list = level_list.copy()

View File

@@ -34,7 +34,8 @@ base_info = {
"factorio_version": "1.1",
"dependencies": [
"base >= 1.1.0",
"? science-not-invited"
"? science-not-invited",
"? factory-levels"
]
}
@@ -107,7 +108,7 @@ def generate_mod(world, output_directory: str):
random = multiworld.slot_seeds[player]
def flop_random(low, high, base=None):
"""Guarentees 50% below base and 50% above base, uniform distribution in each direction."""
"""Guarantees 50% below base and 50% above base, uniform distribution in each direction."""
if base:
distance = random.random()
if random.randint(0, 1):

View File

@@ -137,8 +137,6 @@ class Progressive(Choice):
option_off = 0
option_grouped_random = 1
option_on = 2
alias_false = 0
alias_true = 2
default = 2
def want_progressives(self, random):

View File

@@ -7,7 +7,8 @@
"description": "Integration client for the Archipelago Randomizer",
"factorio_version": "1.1",
"dependencies": [
"base >= 1.1.0",
"? science-not-invited"
]
"base >= 1.1.0",
"? science-not-invited",
"? factory-levels"
]
}

View File

@@ -249,6 +249,10 @@ script.on_event(defines.events.on_player_main_inventory_changed, update_player_e
function add_samples(force, name, count)
local function add_to_table(t)
if count <= 0 then
-- Fixes a bug with single craft, if a recipe gives 0 of a given item.
return
end
t[name] = (t[name] or 0) + count
end
-- Add to global table of earned samples for future new players

View File

@@ -183,6 +183,18 @@ end
data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
data.raw["assembling-machine"]["assembling-machine-2"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes)
if mods["factory-levels"] then
-- Factory-Levels allows the assembling machines to get faster (and depending on settings), more productive at crafting products, the more the
-- assembling machine crafts the product. If the machine crafts enough, it may auto-upgrade to the next tier.
for i = 1, 25, 1 do
data.raw["assembling-machine"]["assembling-machine-1-level-" .. i].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
data.raw["assembling-machine"]["assembling-machine-1-level-" .. i].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes)
end
for i = 1, 50, 1 do
data.raw["assembling-machine"]["assembling-machine-2-level-" .. i].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
end
end
data.raw["ammo"]["artillery-shell"].stack_size = 10
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}

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